/** * Shell completion CLI subcommands. * * localnest completion bash -- Output bash completion script * localnest completion zsh -- Output zsh completion script * localnest completion fish -- Output fish completion script * * Users pipe the output to their shell config: * localnest completion bash >> ~/.bashrc * localnest completion zsh >> ~/.zshrc * localnest completion fish > ~/.config/fish/completions/localnest.fish * * @module src/cli/commands/completion */ import { printSubcommandHelp } from '../help.js'; import type { VerbDef } from '../help.js'; import { writeError } from '../output.js'; import type { GlobalOptions } from '../options.js'; const VERBS: VerbDef[] = [ { name: 'bash', desc: 'Output bash completion script' }, { name: 'zsh', desc: 'Output zsh completion script' }, { name: 'fish', desc: 'Output fish completion script' }, ]; /* ------------------------------------------------------------------ */ /* Command tree definition */ /* ------------------------------------------------------------------ */ /** * Complete command tree for localnest CLI. * Each key is a top-level command; value is an array of subcommands * (empty array means no subcommands). */ const COMMAND_TREE: Record = { start: [], setup: [], doctor: [], upgrade: [], version: [], status: [], memory: ['add', 'search', 'list', 'show', 'delete'], kg: ['add', 'query', 'timeline', 'stats'], skill: ['install', 'list', 'remove'], mcp: ['start', 'status', 'config'], ingest: [], completion: ['bash', 'zsh', 'fish'], 'task-context': [], 'capture-outcome': [], }; /** Global flags available on every command. */ const GLOBAL_FLAGS: string[] = [ '--json', '--verbose', '--quiet', '--config', '--version', '--help', ]; /** Short aliases for global flags. */ const GLOBAL_SHORT: string[] = ['-v', '-h']; /* ------------------------------------------------------------------ */ /* Bash completion generator */ /* ------------------------------------------------------------------ */ function generateBash(): string { const topCmds = Object.keys(COMMAND_TREE).join(' '); const flags = [...GLOBAL_FLAGS, ...GLOBAL_SHORT].join(' '); const subcmdCases = Object.entries(COMMAND_TREE) .filter(([, subs]) => subs.length > 0) .map(([cmd, subs]) => ` ${cmd})\n COMPREPLY=($(compgen -W "${subs.join(' ')}" -- "$cur"))\n return 0\n ;;`) .join('\n'); return `# localnest bash completion # Generated by: localnest completion bash # Add to ~/.bashrc or ~/.bash_profile: # eval "$(localnest completion bash)" # Or: # localnest completion bash >> ~/.bashrc _localnest() { local cur prev words cword _init_completion || return local commands="${topCmds}" local global_flags="${flags}" # If completing a flag value, do file completion case "$prev" in --config) _filedir return 0 ;; esac # If current word starts with -, complete flags if [[ "$cur" == -* ]]; then COMPREPLY=($(compgen -W "$global_flags" -- "$cur")) return 0 fi # Find the subcommand position (first non-flag arg after 'localnest') local subcmd="" local i for (( i=1; i < cword; i++ )); do case "\${words[i]}" in -*) continue ;; *) subcmd="\${words[i]}"; break ;; esac done # No subcommand yet -- complete top-level commands if [[ -z "$subcmd" ]]; then COMPREPLY=($(compgen -W "$commands" -- "$cur")) return 0 fi # Subcommand found -- complete its verbs case "$subcmd" in ${subcmdCases} esac return 0 } complete -F _localnest localnest `; } /* ------------------------------------------------------------------ */ /* Zsh completion generator */ /* ------------------------------------------------------------------ */ function generateZsh(): string { const topEntries = Object.entries(COMMAND_TREE) .map(([cmd]) => { const desc = getCommandDesc(cmd); return ` '${cmd}:${desc}'`; }) .join('\n'); const subcmdCases = Object.entries(COMMAND_TREE) .filter(([, subs]) => subs.length > 0) .map(([cmd, subs]) => { const verbLines = subs .map((s) => ` '${s}:${getVerbDesc(cmd, s)}'`) .join('\n'); return ` ${cmd})\n _values 'subcommand' \\\n${verbLines}\n ;;`; }) .join('\n'); return `#compdef localnest # localnest zsh completion # Generated by: localnest completion zsh # Add to ~/.zshrc: # eval "$(localnest completion zsh)" # Or place in your fpath: # localnest completion zsh > ~/.zsh/completions/_localnest _localnest() { local -a commands local curcontext="$curcontext" state line _arguments -C \\ '(--json)--json[Output in JSON format]' \\ '(--verbose)--verbose[Increase output detail]' \\ '(--quiet)--quiet[Suppress non-essential output]' \\ '(--config)--config[Use custom config file]:config file:_files' \\ '(--version -v)'{--version,-v}'[Print version]' \\ '(--help -h)'{--help,-h}'[Show help]' \\ '1:command:->command' \\ '*::arg:->args' case "$state" in command) commands=( ${topEntries} ) _describe -t commands 'localnest command' commands ;; args) case "$line[1]" in ${subcmdCases} esac ;; esac } _localnest "$@" `; } /* ------------------------------------------------------------------ */ /* Fish completion generator */ /* ------------------------------------------------------------------ */ function generateFish(): string { const lines: string[] = [ '# localnest fish completion', '# Generated by: localnest completion fish', '# Save to: ~/.config/fish/completions/localnest.fish', '# localnest completion fish > ~/.config/fish/completions/localnest.fish', '', '# Disable file completions by default', 'complete -c localnest -f', '', '# Global flags', 'complete -c localnest -l json -d "Output in JSON format"', 'complete -c localnest -l verbose -d "Increase output detail"', 'complete -c localnest -l quiet -d "Suppress non-essential output"', 'complete -c localnest -l config -r -d "Use custom config file"', 'complete -c localnest -l version -s v -d "Print version"', 'complete -c localnest -l help -s h -d "Show help"', '', '# Condition helpers', 'function __localnest_no_subcommand', ' set -l cmd (commandline -opc)', ' for c in $cmd[2..]', ' switch $c', ' case "-*"', ' continue', ' case "*"', ' return 1', ' end', ' end', ' return 0', 'end', '', 'function __localnest_using_subcommand', ' set -l cmd (commandline -opc)', ' for c in $cmd[2..]', ' switch $c', ' case "-*"', ' continue', ' case $argv[1]', ' return 0', ' case "*"', ' return 1', ' end', ' end', ' return 1', 'end', '', '# Top-level commands', ]; for (const [cmd] of Object.entries(COMMAND_TREE)) { const desc = getCommandDesc(cmd); lines.push( `complete -c localnest -n __localnest_no_subcommand -a ${cmd} -d "${desc}"` ); } lines.push(''); lines.push('# Subcommands'); for (const [cmd, subs] of Object.entries(COMMAND_TREE)) { if (subs.length === 0) continue; for (const sub of subs) { const desc = getVerbDesc(cmd, sub); lines.push( `complete -c localnest -n "__localnest_using_subcommand ${cmd}" -a ${sub} -d "${desc}"` ); } } lines.push(''); return lines.join('\n'); } /* ------------------------------------------------------------------ */ /* Description helpers */ /* ------------------------------------------------------------------ */ const COMMAND_DESCS: Record = { start: 'Start MCP server (stdio)', setup: 'Run setup wizard', doctor: 'Run diagnostics', upgrade: 'Upgrade package and migrate setup', version: 'Print version', status: 'Show server status', memory: 'Manage memories', kg: 'Knowledge graph operations', skill: 'Manage skills', mcp: 'MCP server lifecycle', ingest: 'Ingest conversation file', completion: 'Generate shell completion script', 'task-context': 'Print runtime and recalled context', 'capture-outcome': 'Persist a task outcome', }; const VERB_DESCS: Record> = { memory: { add: 'Store a memory entry', search: 'Search memories by query', list: 'List stored memories', show: 'Show a single memory by ID', delete: 'Delete a memory by ID', }, kg: { add: 'Create a triple', query: 'Query entity relationships', timeline: 'Show entity fact timeline', stats: 'Show graph statistics', }, skill: { install: 'Install bundled skills', list: 'List installed skills', remove: 'Remove a skill', }, mcp: { start: 'Start MCP server', status: 'Show MCP server status', config: 'Output MCP client config JSON', }, completion: { bash: 'Output bash completion script', zsh: 'Output zsh completion script', fish: 'Output fish completion script', }, }; function getCommandDesc(cmd: string): string { return COMMAND_DESCS[cmd] || cmd; } function getVerbDesc(cmd: string, verb: string): string { return VERB_DESCS[cmd]?.[verb] || verb; } /* ------------------------------------------------------------------ */ /* Generators map */ /* ------------------------------------------------------------------ */ const GENERATORS: Record string> = { bash: generateBash, zsh: generateZsh, fish: generateFish, }; /* ------------------------------------------------------------------ */ /* Router */ /* ------------------------------------------------------------------ */ export async function run(args: string[], opts: GlobalOptions): Promise { const verb = args[0] || ''; if (!verb || verb === 'help' || verb === '--help' || verb === '-h') { printSubcommandHelp('completion', VERBS); return; } const known = VERBS.find((v) => v.name === verb); if (!known) { writeError(`Unknown completion target: ${verb}`); printSubcommandHelp('completion', VERBS); process.exitCode = 1; return; } const generator = GENERATORS[verb]; const script = generator(); if (opts.json) { process.stdout.write( JSON.stringify({ shell: verb, script }) + '\n' ); } else { process.stdout.write(script); } }