From d407a25d15a7a2849c223d65c4b0f6032b78e4a7 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 2 Jun 2026 22:58:09 +0800 Subject: [PATCH] feat(cli): add `deepcode completion ` (Codex parity) Codex ships `codex completion`; DeepCode had no shell completion. Adds a `completion` subcommand that prints a bash / zsh / fish completion script covering the top-level flags and subcommands. Unknown or missing shell prints a usage message and exits 2. Smoke-tested end-to-end (`node dist/cli.js completion bash|fish`) plus 8 unit tests. Flag/subcommand lists live in completion.ts with a note to keep them in sync with parse-args.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/cli/src/cli.ts | 7 ++ apps/cli/src/completion.test.ts | 84 ++++++++++++++++++++++ apps/cli/src/completion.ts | 120 ++++++++++++++++++++++++++++++++ apps/cli/src/parse-args.ts | 1 + 4 files changed, 212 insertions(+) create mode 100644 apps/cli/src/completion.test.ts create mode 100644 apps/cli/src/completion.ts diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index 653d97c..acfeaaf 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -15,6 +15,7 @@ import { runCronCommand, runSchedulerRun } from './scheduler.js'; import { runTrustCommand } from './trust-cmd.js'; import { runPluginsCommand, runSkillsCommand } from './list-cmd.js'; import { runSetupToken } from './setup-token.js'; +import { runCompletion } from './completion.js'; async function main(): Promise { const args = parseArgs(process.argv.slice(2)); @@ -88,6 +89,12 @@ async function main(): Promise { json: args.json, }); } + if (args.positional[0] === 'completion') { + return runCompletion(args.positional.slice(1), { + output: process.stdout, + errOutput: process.stderr, + }); + } // Headless one-shot (-p / --print) if (args.prompt !== undefined) { diff --git a/apps/cli/src/completion.test.ts b/apps/cli/src/completion.test.ts new file mode 100644 index 0000000..c51116d --- /dev/null +++ b/apps/cli/src/completion.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; +import type { Writable } from 'node:stream'; +import { + completionScript, + isCompletionShell, + runCompletion, + COMPLETION_SHELLS, +} from './completion.js'; + +function collector(): { stream: Writable; text: () => string } { + let buf = ''; + const stream = { + write: (s: string): boolean => { + buf += s; + return true; + }, + } as unknown as Writable; + return { stream, text: () => buf }; +} + +describe('completionScript', () => { + it('bash script registers a completion function with flags + subcommands', () => { + const s = completionScript('bash'); + expect(s).toContain('complete -F _deepcode deepcode'); + expect(s).toContain('--model'); + expect(s).toContain('doctor'); + expect(s).toContain('-C'); + }); + + it('zsh script uses compdef', () => { + const s = completionScript('zsh'); + expect(s).toContain('#compdef deepcode'); + expect(s).toContain('compdef _deepcode deepcode'); + expect(s).toContain("'--effort'"); + }); + + it('fish script registers completions per flag + subcommands', () => { + const s = completionScript('fish'); + expect(s).toContain('complete -c deepcode -l model'); + expect(s).toContain('complete -c deepcode -o C'); // the -C short flag + expect(s).toContain('completion'); // subcommand listed + }); + + it('COMPLETION_SHELLS lists the three supported shells', () => { + expect(COMPLETION_SHELLS).toEqual(['bash', 'zsh', 'fish']); + }); +}); + +describe('isCompletionShell', () => { + it('accepts known shells, rejects others', () => { + expect(isCompletionShell('bash')).toBe(true); + expect(isCompletionShell('zsh')).toBe(true); + expect(isCompletionShell('fish')).toBe(true); + expect(isCompletionShell('powershell')).toBe(false); + expect(isCompletionShell(undefined)).toBe(false); + }); +}); + +describe('runCompletion', () => { + it('writes the script and returns 0 for a known shell', () => { + const out = collector(); + const err = collector(); + const code = runCompletion(['bash'], { output: out.stream, errOutput: err.stream }); + expect(code).toBe(0); + expect(out.text()).toContain('complete -F _deepcode deepcode'); + expect(err.text()).toBe(''); + }); + + it('returns 2 and a usage message for an unknown shell', () => { + const out = collector(); + const err = collector(); + const code = runCompletion(['powershell'], { output: out.stream, errOutput: err.stream }); + expect(code).toBe(2); + expect(err.text()).toMatch(/Usage: deepcode completion/); + expect(out.text()).toBe(''); + }); + + it('returns 2 when no shell argument is given', () => { + const out = collector(); + const err = collector(); + const code = runCompletion([], { output: out.stream, errOutput: err.stream }); + expect(code).toBe(2); + }); +}); diff --git a/apps/cli/src/completion.ts b/apps/cli/src/completion.ts new file mode 100644 index 0000000..f938260 --- /dev/null +++ b/apps/cli/src/completion.ts @@ -0,0 +1,120 @@ +// Shell completion scripts for `deepcode completion ` (Codex +// parity: `codex completion`). Emits a script to stdout that the user installs, +// e.g. `deepcode completion zsh > ~/.zfunc/_deepcode` or +// `eval "$(deepcode completion bash)"`. +// +// The flag/subcommand lists are kept here deliberately (not derived from the +// parser) so the script is a self-contained string; keep them in sync with +// parse-args.ts when the flag set changes. + +import type { Writable } from 'node:stream'; + +export type CompletionShell = 'bash' | 'zsh' | 'fish'; + +export const COMPLETION_SHELLS: CompletionShell[] = ['bash', 'zsh', 'fish']; + +/** Top-level flags `deepcode` accepts (see parse-args.ts). */ +const FLAGS = [ + '--help', + '--version', + '--print', + '--resume', + '--continue', + '--fork-session', + '--mode', + '--permission-mode', + '--model', + '--effort', + '--max-turns', + '--bare', + '-C', + '--cd', + '--system-prompt', + '--append-system-prompt', + '--append-system-prompt-file', + '--allowedTools', + '--disallowedTools', + '--output-format', + '--json-schema', + '--include-partial-messages', + '--verbose', + '--json', + '--settings', + '--agents', + '--mcp-config', + '--plugin-dir', + '--plugin-url', + '--no-plugins', + '--strict', +]; + +/** Positional subcommands (see cli.ts dispatch). */ +const SUBCOMMANDS = [ + 'doctor', + 'upgrade', + 'mcp', + 'trust', + 'plugins', + 'skills', + 'cron', + 'scheduler', + 'setup-token', + 'completion', +]; + +export function isCompletionShell(value: string | undefined): value is CompletionShell { + return value === 'bash' || value === 'zsh' || value === 'fish'; +} + +/** Return the completion script for `shell`. */ +export function completionScript(shell: CompletionShell): string { + const words = [...FLAGS, ...SUBCOMMANDS].join(' '); + if (shell === 'bash') { + return `# deepcode bash completion — eval "$(deepcode completion bash)" +_deepcode() { + local cur="\${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=( $(compgen -W "${words}" -- "\${cur}") ) +} +complete -F _deepcode deepcode +`; + } + if (shell === 'zsh') { + return `#compdef deepcode +# deepcode zsh completion — deepcode completion zsh > "\${fpath[1]}/_deepcode" +_deepcode() { + local -a words + words=(${[...FLAGS, ...SUBCOMMANDS].map((w) => `'${w}'`).join(' ')}) + compadd -- $words +} +compdef _deepcode deepcode +`; + } + // fish + const lines = [ + '# deepcode fish completion — deepcode completion fish > ~/.config/fish/completions/deepcode.fish', + ]; + lines.push('complete -c deepcode -f'); + for (const f of FLAGS) { + if (f.startsWith('--')) lines.push(`complete -c deepcode -l ${f.slice(2)}`); + else if (f.startsWith('-')) lines.push(`complete -c deepcode -o ${f.slice(1)}`); + } + lines.push(`complete -c deepcode -a '${SUBCOMMANDS.join(' ')}'`); + return lines.join('\n') + '\n'; +} + +/** `deepcode completion ` handler. Returns the process exit code. */ +export function runCompletion( + args: string[], + io: { output: Writable; errOutput: Writable }, +): number { + const shell = args[0]; + if (!isCompletionShell(shell)) { + io.errOutput.write( + `Usage: deepcode completion \n` + + (shell ? `Unknown shell "${shell}".\n` : 'Missing shell argument.\n'), + ); + return 2; + } + io.output.write(completionScript(shell)); + return 0; +} diff --git a/apps/cli/src/parse-args.ts b/apps/cli/src/parse-args.ts index ca7b58d..5d80630 100644 --- a/apps/cli/src/parse-args.ts +++ b/apps/cli/src/parse-args.ts @@ -281,6 +281,7 @@ USAGE deepcode plugins install Install a plugin (gh:owner/repo | name@npm | ./path) deepcode plugins uninstall Remove an installed plugin deepcode skills list [--json] List available skills + deepcode completion Print a bash/zsh/fish shell-completion script MODE --mode default / acceptEdits / plan / auto / dontAsk / bypassPermissions