From 42e6b9148078950d99a5a9df723adc77acf6220f Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Fri, 5 Jun 2026 00:24:28 -0500 Subject: [PATCH] feat(mcp): add shell completion command for bash, zsh, and fish Add `gittensory-mcp completion ` to print a tab-completion script for the user's shell. It completes the top-level commands and the subcommands of `profile`, `cache`, and `agent`, driven by a single command spec so it stays in sync with the CLI. `--json` returns `{ shell, script }` for tooling; a missing or unsupported shell errors with the supported list. Tests cover all three shells, the JSON form, and the missing/unsupported shell errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/gittensory-mcp/README.md | 18 ++++ packages/gittensory-mcp/bin/gittensory-mcp.js | 100 ++++++++++++++++++ test/unit/mcp-cli.test.ts | 29 +++++ 3 files changed, 147 insertions(+) diff --git a/packages/gittensory-mcp/README.md b/packages/gittensory-mcp/README.md index 2328674aeb..6a9cc284f6 100644 --- a/packages/gittensory-mcp/README.md +++ b/packages/gittensory-mcp/README.md @@ -43,6 +43,9 @@ gittensory-mcp cache clear gittensory-mcp init-client --print codex gittensory-mcp init-client --print claude gittensory-mcp init-client --print cursor +gittensory-mcp completion bash +gittensory-mcp completion zsh +gittensory-mcp completion fish gittensory-mcp decision-pack --login jsonbored --json gittensory-mcp repo-decision --login jsonbored --repo we-promise/sure --json gittensory-mcp analyze-branch --login jsonbored --json @@ -71,6 +74,21 @@ Add `--json` for machine-readable output: } ``` +### Shell completion + +`gittensory-mcp completion ` prints a tab-completion script for your shell. It completes top-level commands and the subcommands of `profile`, `cache`, and `agent`. Add `--json` to get `{ "shell": "...", "script": "..." }` for tooling. + +```sh +# bash (add to ~/.bashrc) +source <(gittensory-mcp completion bash) + +# zsh (add to a file on your fpath, or to ~/.zshrc) +source <(gittensory-mcp completion zsh) + +# fish +gittensory-mcp completion fish > ~/.config/fish/completions/gittensory-mcp.fish +``` + For near-term what-if scoreability, pass the situational assumptions explicitly: ```sh diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index bb781e0315..4d97e898f6 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -23,6 +23,25 @@ const decisionPackCacheMaxBytes = 512 * 1024; const changelogPath = new URL("../CHANGELOG.md", import.meta.url); const cliArgs = process.argv.slice(2); const defaultProfileName = "default"; +// Single source of truth for shell-completion: top-level command -> its subcommands (if any). +const CLI_COMMAND_SPEC = { + login: [], + logout: [], + whoami: [], + status: [], + changelog: [], + version: [], + doctor: [], + "init-client": [], + "decision-pack": [], + "repo-decision": [], + "analyze-branch": [], + preflight: [], + profile: ["list", "create", "switch", "remove"], + cache: ["status", "clear"], + agent: ["plan", "status", "explain", "packet"], +}; +const COMPLETION_SHELLS = ["bash", "zsh", "fish"]; const configPath = process.env.GITTENSORY_CONFIG_PATH ?? (process.env.GITTENSORY_CONFIG_DIR @@ -518,6 +537,7 @@ async function runCli(args) { const command = args[0]; if (command === "--help" || command === "help") return printHelp(); if (command === "--version" || command === "-v" || command === "version") return printVersion(parseOptions(args.slice(1))); + if (command === "completion") return completionCommand(args.slice(1)); if (command === "agent") return runAgentCli(args.slice(1)); if (command === "cache") return runCacheCli(args.slice(1)); const options = parseOptions(args.slice(1)); @@ -761,10 +781,90 @@ function printVersion(options) { process.stdout.write(`${packageName}/${packageVersion} (api ${currentApiVersion}, node ${process.version})\n`); } +function completionCommand(args) { + const shell = args[0] && !args[0].startsWith("--") ? args[0] : undefined; + const options = parseOptions(args.filter((arg) => arg.startsWith("--"))); + if (!shell) throw new Error(`Usage: gittensory-mcp completion <${COMPLETION_SHELLS.join("|")}> [--json]`); + if (!COMPLETION_SHELLS.includes(shell)) throw new Error(`Unsupported shell: ${shell}. Supported shells: ${COMPLETION_SHELLS.join(", ")}.`); + const script = buildCompletionScript(shell); + if (options.json) { + process.stdout.write(`${JSON.stringify({ shell, script }, null, 2)}\n`); + return; + } + process.stdout.write(`${script}\n`); +} + +function buildCompletionScript(shell) { + const topLevel = [...Object.keys(CLI_COMMAND_SPEC), "help"]; + const withSubcommands = Object.entries(CLI_COMMAND_SPEC).filter(([, subcommands]) => subcommands.length > 0); + if (shell === "bash") return buildBashCompletion(topLevel, withSubcommands); + if (shell === "zsh") return buildZshCompletion(topLevel, withSubcommands); + return buildFishCompletion(topLevel, withSubcommands); +} + +function buildBashCompletion(topLevel, withSubcommands) { + const subcommandCases = withSubcommands + .map(([command, subcommands]) => ` ${command}) COMPREPLY=( $(compgen -W "${subcommands.join(" ")}" -- "$cur") ); return 0;;`) + .join("\n"); + return `# gittensory-mcp bash completion. Add to ~/.bashrc: +# source <(gittensory-mcp completion bash) +_gittensory_mcp() { + local cur prev cword + cur="\${COMP_WORDS[COMP_CWORD]}" + prev="\${COMP_WORDS[COMP_CWORD-1]}" + cword=\$COMP_CWORD + local commands="${topLevel.join(" ")}" + if [ "\$cword" -eq 1 ]; then + COMPREPLY=( $(compgen -W "\$commands --help --version" -- "$cur") ) + return 0 + fi + case "\${COMP_WORDS[1]}" in +${subcommandCases} + *) COMPREPLY=( $(compgen -W "--json --login --repo --profile --base --cwd" -- "$cur") ); return 0;; + esac +} +complete -F _gittensory_mcp gittensory-mcp`; +} + +function buildZshCompletion(topLevel, withSubcommands) { + const subcommandCases = withSubcommands + .map(([command, subcommands]) => ` ${command}) _values 'subcommand' ${subcommands.join(" ")} ;;`) + .join("\n"); + return `#compdef gittensory-mcp +# gittensory-mcp zsh completion. Add to your fpath, or: +# source <(gittensory-mcp completion zsh) +_gittensory_mcp() { + local -a commands + commands=(${topLevel.join(" ")}) + if (( CURRENT == 2 )); then + _describe 'command' commands + return + fi + case $words[2] in +${subcommandCases} + esac +} +_gittensory_mcp "$@"`; +} + +function buildFishCompletion(topLevel, withSubcommands) { + const topLevelLines = topLevel + .map((command) => `complete -c gittensory-mcp -n __fish_use_subcommand -a ${command} -d 'gittensory-mcp command'`) + .join("\n"); + const subcommandLines = withSubcommands + .map(([command, subcommands]) => `complete -c gittensory-mcp -n '__fish_seen_subcommand_from ${command}' -a '${subcommands.join(" ")}'`) + .join("\n"); + return `# gittensory-mcp fish completion. Save to: +# ~/.config/fish/completions/gittensory-mcp.fish +${topLevelLines} +${subcommandLines}`; +} + function printHelp() { process.stdout.write(`Usage: gittensory-mcp --stdio gittensory-mcp version [--json] + gittensory-mcp completion bash|zsh|fish [--json] gittensory-mcp login [--profile name] [--github-token ] [--json] gittensory-mcp logout [--profile name] [--all] [--json] gittensory-mcp whoami [--profile name] [--json] diff --git a/test/unit/mcp-cli.test.ts b/test/unit/mcp-cli.test.ts index be8c89b13d..081c26045c 100644 --- a/test/unit/mcp-cli.test.ts +++ b/test/unit/mcp-cli.test.ts @@ -924,6 +924,35 @@ describe("gittensory-mcp CLI", () => { expect(() => run(["bogus-command"])).toThrow(/Unknown command: bogus-command/); expect(() => run(["bogus-command"])).toThrow(/gittensory-mcp --help/); }); + + it("prints shell completion scripts for bash, zsh, and fish", () => { + const bash = run(["completion", "bash"]); + expect(bash).toContain("_gittensory_mcp()"); + expect(bash).toContain("complete -F _gittensory_mcp gittensory-mcp"); + expect(bash).toContain("analyze-branch"); + expect(bash).toContain("version"); + expect(bash).toContain("plan status explain packet"); + + const zsh = run(["completion", "zsh"]); + expect(zsh).toContain("#compdef gittensory-mcp"); + expect(zsh).toContain("_describe 'command' commands"); + expect(zsh).toContain("list create switch remove"); + + const fish = run(["completion", "fish"]); + expect(fish).toContain("complete -c gittensory-mcp"); + expect(fish).toContain("__fish_seen_subcommand_from agent"); + }); + + it("emits completion as machine-readable json", () => { + const payload = JSON.parse(run(["completion", "zsh", "--json"])) as { shell: string; script: string }; + expect(payload.shell).toBe("zsh"); + expect(payload.script).toContain("#compdef gittensory-mcp"); + }); + + it("rejects missing or unsupported completion shells", () => { + expect(() => run(["completion"])).toThrow(/Usage: gittensory-mcp completion /); + expect(() => run(["completion", "powershell"])).toThrow(/Unsupported shell: powershell/); + }); }); function run(args: string[], env: Record = {}) {