Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
const args = parseArgs(process.argv.slice(2));
Expand Down Expand Up @@ -88,6 +89,12 @@ async function main(): Promise<number> {
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) {
Expand Down
84 changes: 84 additions & 0 deletions apps/cli/src/completion.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
120 changes: 120 additions & 0 deletions apps/cli/src/completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Shell completion scripts for `deepcode completion <bash|zsh|fish>` (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 <shell>` 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 <bash|zsh|fish>\n` +
(shell ? `Unknown shell "${shell}".\n` : 'Missing shell argument.\n'),
);
return 2;
}
io.output.write(completionScript(shell));
return 0;
}
1 change: 1 addition & 0 deletions apps/cli/src/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ USAGE
deepcode plugins install <spec> Install a plugin (gh:owner/repo | name@npm | ./path)
deepcode plugins uninstall <name> Remove an installed plugin
deepcode skills list [--json] List available skills
deepcode completion <shell> Print a bash/zsh/fish shell-completion script

MODE
--mode <name> default / acceptEdits / plan / auto / dontAsk / bypassPermissions
Expand Down
Loading