From 2b4ea0df2d3f8abe3c6f21e01015f927a5796d3e Mon Sep 17 00:00:00 2001 From: Agustin Date: Fri, 29 Aug 2025 13:21:03 +0200 Subject: [PATCH] feat: provide terminal tab autocompletion --- CHANGELOG.md | 6 +- bin/cli.js | 7 +- lib/cli/autoCompletions.js | 39 +++++++++++ package-lock.json | 4 +- package.json | 2 +- resources/autoCompletion/autocomplete | 93 +++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 lib/cli/autoCompletions.js create mode 100644 resources/autoCompletion/autocomplete diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a19015e..caa3194c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [1.48.0] (25/09/2025) +In this version we are introducing TAB autocompletion for the CLI commands. It should autocomplete command names, flags, and template handles and names. +To enable it, run `silverfin config --set-autocompletion` and follow the instructions. + ## [1.47.1] (13/11/2025) - Fix: Update authorize command to use user-inputted firm ID when calling `getFirmName` function rather than default firm ID @@ -24,7 +28,7 @@ All notable changes to this project will be documented in this file. ## [1.45.0] (28/08/2025) - A new config file attribute `test_firm_id` was added for account templates and reconciliations texts. -Adding it with a specific firm will make sure that this firm is used for the Github actions. +Adding it with a specific firm will make sure that this firm is used for the Github actions. ## [1.44.0] (11/08/2025) - `create-account-template` command will now create an empty .yml file diff --git a/bin/cli.js b/bin/cli.js index d4e826c9..68aa48e3 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -16,6 +16,7 @@ const path = require("path"); const { consola } = require("consola"); const { runCommandChecks } = require("../lib/cli/utils"); const { CwdValidator } = require("../lib/cli/cwdValidator"); +const { AutoCompletions } = require("../lib/cli/autoCompletions"); const firmIdDefault = cliUtils.loadDefaultFirmId(); cliUtils.handleUncaughtErrors(); @@ -536,8 +537,9 @@ program .addOption(new Option("--refresh-partner-token ", "Get a new partner api key using the stored api key")) .addOption(new Option("--set-host ", "Set a custom host for the Silverfin API (e.g. https://live.getsilverfin.com)")) .addOption(new Option("--get-host", "Get the current host for the Silverfin API")) + .addOption(new Option("--set-autocompletion", "Set TAB autocompletion for the CLI")) .action(async (options) => { - cliUtils.checkUniqueOption(["setFirm", "getFirm", "listAll", "updateName", "refreshToken", "refreshPartnerToken", "setHost", "getHost"], options); + cliUtils.checkUniqueOption(["setFirm", "getFirm", "listAll", "updateName", "refreshToken", "refreshPartnerToken", "setHost", "getHost", "setAutocompletion"], options); if (options.setFirm) { firmCredentials.setDefaultFirmId(options.setFirm); const currentDirectory = path.basename(process.cwd()); @@ -592,6 +594,9 @@ program const host = firmCredentials.getHost(); consola.info(`Current host: ${host}`); } + if (options.setAutocompletion) { + AutoCompletions.set(); + } }); program diff --git a/lib/cli/autoCompletions.js b/lib/cli/autoCompletions.js new file mode 100644 index 00000000..5319c8d9 --- /dev/null +++ b/lib/cli/autoCompletions.js @@ -0,0 +1,39 @@ +const fs = require("fs"); +const path = require("path"); +const { consola } = require("consola"); +const homedir = require("os").homedir(); + +// Create the necesary files to enable tab auto-completion for your CLI tool +class AutoCompletions { + static #SF_FOLDER_PATH = path.resolve(homedir, ".silverfin/"); + static #SCRIPT_ORIGIN_PATH = path.resolve(__dirname, "../../resources/autoCompletion/autocomplete"); + static #SCRIPT_DESTINATION_PATH = path.resolve(this.#SF_FOLDER_PATH, "autocomplete"); + + static set() { + this.#copyFile(); + this.#addToShellConfig(); + } + + static #copyFile() { + try { + if (!fs.existsSync(this.#SF_FOLDER_PATH)) { + fs.mkdirSync(this.#SF_FOLDER_PATH, { recursive: true }); + } + + if (fs.existsSync(this.#SCRIPT_DESTINATION_PATH)) { + fs.unlinkSync(this.#SCRIPT_DESTINATION_PATH); + } + + fs.copyFileSync(this.#SCRIPT_ORIGIN_PATH, this.#SCRIPT_DESTINATION_PATH); + } catch (error) { + consola.error("Error copying the auto-completion script:", error); + } + } + + static #addToShellConfig() { + consola.info(`To enable auto-completions, add the following line to your shell config (e.g. ~/.zshrc or ~/.bashrc):`); + consola.info(`source ${this.#SCRIPT_DESTINATION_PATH}`); + } +} + +module.exports = { AutoCompletions }; diff --git a/package-lock.json b/package-lock.json index 7e09d550..62ff6f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "silverfin-cli", - "version": "1.47.0", + "version": "1.48.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "silverfin-cli", - "version": "1.47.0", + "version": "1.48.0", "license": "MIT", "dependencies": { "axios": "^1.6.2", diff --git a/package.json b/package.json index efa78f76..6c39a3e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silverfin-cli", - "version": "1.47.1", + "version": "1.48.0", "description": "Command line tool for Silverfin template development", "main": "index.js", "license": "MIT", diff --git a/resources/autoCompletion/autocomplete b/resources/autoCompletion/autocomplete new file mode 100644 index 00000000..1a8010f4 --- /dev/null +++ b/resources/autoCompletion/autocomplete @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +_complete_directories() { + local directory="$1" + local cur="$2" + if [ -d "$directory" ]; then + local IFS=$'\n' + local dirs=() + + # Use appropriate find command based on availability + if command -v gfind &>/dev/null; then + # macOS with GNU findutils + dirs=($(gfind "$directory" -mindepth 1 -maxdepth 1 -type d -printf '%f\n')) + elif find --version 2>/dev/null | grep -q GNU; then + # Linux with GNU find + dirs=($(find "$directory" -mindepth 1 -maxdepth 1 -type d -printf '%f\n')) + else + # BSD find (macOS default) - use alternative + dirs=($(find "$directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)) + fi + + COMPREPLY=() + for dir in "${dirs[@]}"; do + if [[ "$dir" == "$cur"* ]]; then + if [[ "$dir" == *" "* ]]; then + COMPREPLY+=("\"$dir\"") + else + COMPREPLY+=("$dir") + fi + fi + done + fi +} +_silverfin() { + local cur=${COMP_WORDS[COMP_CWORD]} + local prev=${COMP_WORDS[COMP_CWORD - 1]} + local command=${COMP_WORDS[1]} + COMPREPLY=() + # Command + if [ $COMP_CWORD -eq 1 ]; then + local sf_commands="" + sf_commands=$(silverfin --help 2>/dev/null | grep -E "^\s+[a-z][a-z-]*\s" | awk '{print $1}' | tr '\n' ' ') + # Fallback + if [ -z "$sf_commands" ]; then + sf_commands="import-reconciliation update-reconciliation create-reconciliation import-export-file update-export-file create-export-file import-account-template update-account-template create-account-template import-shared-part update-shared-part create-shared-part add-shared-part remove-shared-part run-test create-test authorize authorize-partner stats config get-reconciliation-id get-export-file-id get-account-template-id get-shared-part-id development-mode update help" + fi + COMPREPLY=($(compgen -W "$sf_commands" -- "$cur")) + return 0 + fi + # Template name + case "$prev" in + --handle | -h) + if [[ "$command" == *-reconciliation ]]; then + _complete_directories "./reconciliation_texts" "$cur" + fi + return 0 + ;; + --name | -n) + if [[ "$command" == *-export-file ]]; then + _complete_directories "./export_files" "$cur" + elif [[ "$command" == *-account-template ]]; then + _complete_directories "./account_templates" "$cur" + fi + return 0 + ;; + --shared-part | -s) + if [[ "$command" == *-shared-part ]]; then + _complete_directories "./shared_parts" "$cur" + fi + return 0 + ;; + esac + # Flags + if [[ "$cur" == --* ]]; then + local flags="" + flags=$(silverfin "$command" --help 2>/dev/null | grep -oE -- '--[a-zA-Z][a-zA-Z0-9-]*' | sort -u | tr '\n' ' ') + COMPREPLY=($(compgen -W "$flags" -- "$cur")) + return 0 + fi +} +_silverfin_init_completion() { + if [[ -n ${ZSH_VERSION-} ]]; then + if ! command -v bashcompinit >/dev/null 2>&1; then + autoload -U bashcompinit + fi + if ! command -v complete >/dev/null 2>&1; then + bashcompinit + fi + fi + if command -v complete >/dev/null 2>&1; then + complete -F _silverfin silverfin + fi +} +_silverfin_init_completion