diff --git a/nix/opencode.nix b/nix/opencode.nix index 4deac157e2e0..ce449ad333b1 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -75,7 +75,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { # trick yargs into also generating zsh completions installShellCompletion --cmd opencode \ --bash <($out/bin/opencode completion) \ - --zsh <(SHELL=/bin/zsh $out/bin/opencode completion) + --zsh <(SHELL=/bin/zsh $out/bin/opencode completion) \ + --fish <($out/bin/opencode completion --shell fish) ''; nativeInstallCheckInputs = [ diff --git a/packages/opencode/src/cli/cmd/completion.ts b/packages/opencode/src/cli/cmd/completion.ts new file mode 100644 index 000000000000..5d394bf6b9b0 --- /dev/null +++ b/packages/opencode/src/cli/cmd/completion.ts @@ -0,0 +1,594 @@ +export function generateFishCompletions(): string { + const lines: string[] = [ + "# Fish completion script for opencode", + "# Generated by: opencode completion --shell fish", + "", + "# Erase any existing completions", + "complete -c opencode -e", + "", + ] + + function line(s: string) { + lines.push(s) + } + function blank() { + lines.push("") + } + + // Top-level subcommand list (visible commands only) + line( + "set -l commands acp mcp attach run debug providers agent upgrade uninstall serve web models stats export import github pr session plugin db completion", + ) + blank() + + // Subcommand lists for nested commands + line("set -l mcp_cmds add list auth logout debug") + line("set -l mcp_auth_cmds list") + line("set -l debug_cmds config lsp rg file scrap skill snapshot agent paths wait") + line("set -l debug_lsp_cmds diagnostics symbols document-symbols") + line("set -l debug_rg_cmds tree files search") + line("set -l debug_file_cmds read status list search tree") + line("set -l debug_snapshot_cmds track patch diff") + line("set -l providers_cmds list login logout") + line("set -l agent_cmds create list") + line("set -l github_cmds install run") + line("set -l session_cmds list delete") + line("set -l db_cmds path migrate") + blank() + + // --- Global options --- + line("# Global options") + line("complete -f -c opencode -s h -l help -d 'Show help'") + line("complete -f -c opencode -s v -l version -d 'Show version'") + line( + "complete -f -c opencode -l log-level -x -a 'DEBUG INFO WARN ERROR' -d 'Set log level'", + ) + line("complete -f -c opencode -l print-logs -d 'Print logs to stderr'") + line( + "complete -f -c opencode -l pure -d 'Run without external plugins'", + ) + blank() + + // Helper: top-level condition + const noSub = '"not __fish_seen_subcommand_from $commands"' + + // --- Top-level commands --- + line("# Top-level commands") + const topCmds: [string, string][] = [ + ["acp", "Start ACP server"], + ["mcp", "Manage MCP servers"], + ["attach", "Attach to a running opencode server"], + ["run", "Run opencode with a message"], + ["debug", "Debugging and troubleshooting tools"], + ["providers", "Manage AI providers and credentials"], + ["agent", "Manage agents"], + ["upgrade", "Upgrade opencode"], + ["uninstall", "Uninstall opencode"], + ["serve", "Start a headless opencode server"], + ["web", "Start server and open web interface"], + ["models", "List all available models"], + ["stats", "Show token usage and cost statistics"], + ["export", "Export session data as JSON"], + ["import", "Import session data from JSON file or URL"], + ["github", "Manage GitHub agent"], + ["pr", "Checkout a GitHub PR branch"], + ["session", "Manage sessions"], + ["plugin", "Install plugin and update config"], + ["db", "Database tools"], + ["completion", "Generate shell completion script"], + ] + for (const [cmd, desc] of topCmds) { + line(`complete -f -c opencode -n ${noSub} -a ${cmd} -d '${desc}'`) + } + blank() + + // --- Default command (TUI) options --- + line("# Default command (TUI) options") + const tuiCond = noSub + line( + `complete -f -c opencode -n ${tuiCond} -s m -l model -r -d 'Model to use (provider/model)'`, + ) + line( + `complete -f -c opencode -n ${tuiCond} -s c -l continue -d 'Continue the last session'`, + ) + line( + `complete -f -c opencode -n ${tuiCond} -s s -l session -r -d 'Session ID to continue'`, + ) + line( + `complete -f -c opencode -n ${tuiCond} -l fork -d 'Fork the session when continuing'`, + ) + line( + `complete -f -c opencode -n ${tuiCond} -l prompt -r -d 'Prompt to use'`, + ) + line( + `complete -f -c opencode -n ${tuiCond} -l agent -r -d 'Agent to use'`, + ) + addNetworkOptions(line, tuiCond) + blank() + + // --- acp --- + line("# acp options") + const acpCond = "'__fish_seen_subcommand_from acp'" + line( + `complete -f -c opencode -n ${acpCond} -l cwd -r -d 'Working directory'`, + ) + addNetworkOptions(line, acpCond) + blank() + + // --- attach --- + line("# attach options") + const attachCond = "'__fish_seen_subcommand_from attach'" + line( + `complete -f -c opencode -n ${attachCond} -l dir -r -d 'Directory to run in'`, + ) + line( + `complete -f -c opencode -n ${attachCond} -s c -l continue -d 'Continue the last session'`, + ) + line( + `complete -f -c opencode -n ${attachCond} -s s -l session -r -d 'Session ID to continue'`, + ) + line( + `complete -f -c opencode -n ${attachCond} -l fork -d 'Fork the session when continuing'`, + ) + line( + `complete -f -c opencode -n ${attachCond} -s p -l password -r -d 'Basic auth password'`, + ) + blank() + + // --- run --- + line("# run options") + const runCond = "'__fish_seen_subcommand_from run'" + line( + `complete -f -c opencode -n ${runCond} -l command -r -d 'Command to run'`, + ) + line( + `complete -f -c opencode -n ${runCond} -s c -l continue -d 'Continue the last session'`, + ) + line( + `complete -f -c opencode -n ${runCond} -s s -l session -r -d 'Session ID to continue'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l fork -d 'Fork the session before continuing'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l share -d 'Share the session'`, + ) + line( + `complete -f -c opencode -n ${runCond} -s m -l model -r -d 'Model to use (provider/model)'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l agent -r -d 'Agent to use'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l format -x -a 'default json' -d 'Output format'`, + ) + line( + `complete -c opencode -n ${runCond} -s f -l file -r -d 'File(s) to attach'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l title -r -d 'Title for the session'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l attach -r -d 'Attach to running server'`, + ) + line( + `complete -f -c opencode -n ${runCond} -s p -l password -r -d 'Basic auth password'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l dir -r -d 'Directory to run in'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l port -r -d 'Port for local server'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l variant -r -d 'Model variant (reasoning effort)'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l thinking -d 'Show thinking blocks'`, + ) + line( + `complete -f -c opencode -n ${runCond} -l dangerously-skip-permissions -d 'Auto-approve permissions'`, + ) + blank() + + // --- serve --- + line("# serve options") + const serveCond = "'__fish_seen_subcommand_from serve'" + addNetworkOptions(line, serveCond) + blank() + + // --- web --- + line("# web options") + const webCond = "'__fish_seen_subcommand_from web'" + addNetworkOptions(line, webCond) + blank() + + // --- mcp subcommands --- + line("# mcp subcommands") + const mcpNoSub = + "'__fish_seen_subcommand_from mcp; and not __fish_seen_subcommand_from $mcp_cmds'" + line( + `complete -f -c opencode -n ${mcpNoSub} -a add -d 'Add an MCP server'`, + ) + line( + `complete -f -c opencode -n ${mcpNoSub} -a list -d 'List MCP servers'`, + ) + line( + `complete -f -c opencode -n ${mcpNoSub} -a auth -d 'Authenticate with an MCP server'`, + ) + line( + `complete -f -c opencode -n ${mcpNoSub} -a logout -d 'Remove OAuth credentials'`, + ) + line( + `complete -f -c opencode -n ${mcpNoSub} -a debug -d 'Debug OAuth connection'`, + ) + blank() + + // mcp auth subcommands + line("# mcp auth subcommands") + const mcpAuthNoSub = + "'__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from auth; and not __fish_seen_subcommand_from $mcp_auth_cmds'" + line( + `complete -f -c opencode -n ${mcpAuthNoSub} -a list -d 'List OAuth-capable MCP servers'`, + ) + blank() + + // --- providers subcommands --- + line("# providers subcommands") + const provNoSub = + "'__fish_seen_subcommand_from providers; and not __fish_seen_subcommand_from $providers_cmds'" + line( + `complete -f -c opencode -n ${provNoSub} -a list -d 'List providers and credentials'`, + ) + line( + `complete -f -c opencode -n ${provNoSub} -a login -d 'Log in to a provider'`, + ) + line( + `complete -f -c opencode -n ${provNoSub} -a logout -d 'Log out from a provider'`, + ) + blank() + + // providers login options + line("# providers login options") + const provLoginCond = + "'__fish_seen_subcommand_from providers; and __fish_seen_subcommand_from login'" + line( + `complete -f -c opencode -n ${provLoginCond} -s p -l provider -r -d 'Provider ID or name'`, + ) + line( + `complete -f -c opencode -n ${provLoginCond} -s m -l method -r -d 'Login method'`, + ) + blank() + + // --- agent subcommands --- + line("# agent subcommands") + const agentNoSub = + "'__fish_seen_subcommand_from agent; and not __fish_seen_subcommand_from $agent_cmds'" + line( + `complete -f -c opencode -n ${agentNoSub} -a create -d 'Create a new agent'`, + ) + line( + `complete -f -c opencode -n ${agentNoSub} -a list -d 'List all available agents'`, + ) + blank() + + // agent create options + line("# agent create options") + const agentCreateCond = + "'__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from create'" + line( + `complete -f -c opencode -n ${agentCreateCond} -l path -r -d 'Directory for agent file'`, + ) + line( + `complete -f -c opencode -n ${agentCreateCond} -l description -r -d 'What the agent should do'`, + ) + line( + `complete -f -c opencode -n ${agentCreateCond} -l mode -x -a 'all primary subagent' -d 'Agent mode'`, + ) + line( + `complete -f -c opencode -n ${agentCreateCond} -l tools -r -d 'Comma-separated list of tools'`, + ) + line( + `complete -f -c opencode -n ${agentCreateCond} -s m -l model -r -d 'Model to use (provider/model)'`, + ) + blank() + + // --- models options --- + line("# models options") + const modelsCond = "'__fish_seen_subcommand_from models'" + line( + `complete -f -c opencode -n ${modelsCond} -l verbose -d 'Verbose output with metadata'`, + ) + line( + `complete -f -c opencode -n ${modelsCond} -l refresh -d 'Refresh models cache'`, + ) + blank() + + // --- session subcommands --- + line("# session subcommands") + const sessionNoSub = + "'__fish_seen_subcommand_from session; and not __fish_seen_subcommand_from $session_cmds'" + line( + `complete -f -c opencode -n ${sessionNoSub} -a list -d 'List sessions'`, + ) + line( + `complete -f -c opencode -n ${sessionNoSub} -a delete -d 'Delete a session'`, + ) + blank() + + // session list options + line("# session list options") + const sessionListCond = + "'__fish_seen_subcommand_from session; and __fish_seen_subcommand_from list'" + line( + `complete -f -c opencode -n ${sessionListCond} -s n -l max-count -r -d 'Limit to N most recent'`, + ) + line( + `complete -f -c opencode -n ${sessionListCond} -l format -x -a 'table json' -d 'Output format'`, + ) + blank() + + // --- stats options --- + line("# stats options") + const statsCond = "'__fish_seen_subcommand_from stats'" + line( + `complete -f -c opencode -n ${statsCond} -l days -r -d 'Show stats for last N days'`, + ) + line( + `complete -f -c opencode -n ${statsCond} -l tools -r -d 'Number of tools to show'`, + ) + line( + `complete -f -c opencode -n ${statsCond} -l models -d 'Show model statistics'`, + ) + line( + `complete -f -c opencode -n ${statsCond} -l project -r -d 'Filter by project'`, + ) + blank() + + // --- export options --- + line("# export options") + const exportCond = "'__fish_seen_subcommand_from export'" + line( + `complete -f -c opencode -n ${exportCond} -l sanitize -d 'Redact sensitive data'`, + ) + blank() + + // --- upgrade options --- + line("# upgrade options") + const upgradeCond = "'__fish_seen_subcommand_from upgrade'" + line( + `complete -f -c opencode -n ${upgradeCond} -s m -l method -x -a 'curl npm pnpm bun brew choco scoop' -d 'Installation method'`, + ) + blank() + + // --- uninstall options --- + line("# uninstall options") + const uninstallCond = "'__fish_seen_subcommand_from uninstall'" + line( + `complete -f -c opencode -n ${uninstallCond} -s c -l keep-config -d 'Keep configuration files'`, + ) + line( + `complete -f -c opencode -n ${uninstallCond} -s d -l keep-data -d 'Keep session data'`, + ) + line( + `complete -f -c opencode -n ${uninstallCond} -l dry-run -d 'Show what would be removed'`, + ) + line( + `complete -f -c opencode -n ${uninstallCond} -s f -l force -d 'Skip confirmation prompts'`, + ) + blank() + + // --- github subcommands --- + line("# github subcommands") + const ghNoSub = + "'__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from $github_cmds'" + line( + `complete -f -c opencode -n ${ghNoSub} -a install -d 'Install the GitHub agent'`, + ) + line( + `complete -f -c opencode -n ${ghNoSub} -a run -d 'Run the GitHub agent'`, + ) + blank() + + // github run options + line("# github run options") + const ghRunCond = + "'__fish_seen_subcommand_from github; and __fish_seen_subcommand_from run'" + line( + `complete -f -c opencode -n ${ghRunCond} -l event -r -d 'GitHub mock event'`, + ) + line( + `complete -f -c opencode -n ${ghRunCond} -l token -r -d 'GitHub personal access token'`, + ) + blank() + + // --- plugin options --- + line("# plugin options") + const plugCond = "'__fish_seen_subcommand_from plugin'" + line( + `complete -f -c opencode -n ${plugCond} -s g -l global -d 'Install in global config'`, + ) + line( + `complete -f -c opencode -n ${plugCond} -s f -l force -d 'Replace existing plugin'`, + ) + blank() + + // --- db subcommands --- + line("# db subcommands") + const dbNoSub = + "'__fish_seen_subcommand_from db; and not __fish_seen_subcommand_from $db_cmds'" + line( + `complete -f -c opencode -n ${dbNoSub} -a path -d 'Print the database path'`, + ) + line( + `complete -f -c opencode -n ${dbNoSub} -a migrate -d 'Migrate JSON data to SQLite'`, + ) + blank() + + // db default (query) options + line("# db options") + const dbCond = "'__fish_seen_subcommand_from db'" + line( + `complete -f -c opencode -n ${dbCond} -l format -x -a 'json tsv' -d 'Output format'`, + ) + blank() + + // --- debug subcommands --- + line("# debug subcommands") + const debugNoSub = + "'__fish_seen_subcommand_from debug; and not __fish_seen_subcommand_from $debug_cmds'" + line( + `complete -f -c opencode -n ${debugNoSub} -a config -d 'Show resolved configuration'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a lsp -d 'LSP debugging utilities'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a rg -d 'Ripgrep debugging utilities'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a file -d 'File system debugging utilities'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a scrap -d 'List all known projects'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a skill -d 'List all available skills'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a snapshot -d 'Snapshot debugging utilities'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a agent -d 'Show agent configuration'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a paths -d 'Show global paths'`, + ) + line( + `complete -f -c opencode -n ${debugNoSub} -a wait -d 'Wait indefinitely'`, + ) + blank() + + // debug agent options + line("# debug agent options") + const debugAgentCond = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from agent'" + line( + `complete -f -c opencode -n ${debugAgentCond} -l tool -r -d 'Tool ID to execute'`, + ) + line( + `complete -f -c opencode -n ${debugAgentCond} -l params -r -d 'Tool params as JSON'`, + ) + blank() + + // debug lsp subcommands + line("# debug lsp subcommands") + const debugLspNoSub = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from lsp; and not __fish_seen_subcommand_from $debug_lsp_cmds'" + line( + `complete -f -c opencode -n ${debugLspNoSub} -a diagnostics -d 'Get diagnostics for a file'`, + ) + line( + `complete -f -c opencode -n ${debugLspNoSub} -a symbols -d 'Search workspace symbols'`, + ) + line( + `complete -f -c opencode -n ${debugLspNoSub} -a document-symbols -d 'Get symbols from a document'`, + ) + blank() + + // debug rg subcommands + line("# debug rg subcommands") + const debugRgNoSub = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from rg; and not __fish_seen_subcommand_from $debug_rg_cmds'" + line( + `complete -f -c opencode -n ${debugRgNoSub} -a tree -d 'Show file tree'`, + ) + line( + `complete -f -c opencode -n ${debugRgNoSub} -a files -d 'List files'`, + ) + line( + `complete -f -c opencode -n ${debugRgNoSub} -a search -d 'Search file contents'`, + ) + blank() + + // debug rg options + line("# debug rg options") + const debugRgCond = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from rg'" + line( + `complete -f -c opencode -n ${debugRgCond} -l limit -r -d 'Limit results'`, + ) + line( + `complete -f -c opencode -n ${debugRgCond} -l query -r -d 'Search query'`, + ) + line( + `complete -f -c opencode -n ${debugRgCond} -l glob -r -d 'Glob pattern'`, + ) + blank() + + // debug file subcommands + line("# debug file subcommands") + const debugFileNoSub = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from file; and not __fish_seen_subcommand_from $debug_file_cmds'" + line( + `complete -f -c opencode -n ${debugFileNoSub} -a read -d 'Read file contents as JSON'`, + ) + line( + `complete -f -c opencode -n ${debugFileNoSub} -a status -d 'Show file status'`, + ) + line( + `complete -f -c opencode -n ${debugFileNoSub} -a list -d 'List files in directory'`, + ) + line( + `complete -f -c opencode -n ${debugFileNoSub} -a search -d 'Search files by query'`, + ) + line( + `complete -f -c opencode -n ${debugFileNoSub} -a tree -d 'Show directory tree'`, + ) + blank() + + // debug snapshot subcommands + line("# debug snapshot subcommands") + const debugSnapNoSub = + "'__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from snapshot; and not __fish_seen_subcommand_from $debug_snapshot_cmds'" + line( + `complete -f -c opencode -n ${debugSnapNoSub} -a track -d 'Track current snapshot state'`, + ) + line( + `complete -f -c opencode -n ${debugSnapNoSub} -a patch -d 'Show patch for a snapshot hash'`, + ) + line( + `complete -f -c opencode -n ${debugSnapNoSub} -a diff -d 'Show diff for a snapshot hash'`, + ) + blank() + + // --- completion options --- + line("# completion options") + const completionCond = "'__fish_seen_subcommand_from completion'" + line( + `complete -f -c opencode -n ${completionCond} -l shell -x -a 'bash zsh fish' -d 'Shell type'`, + ) + + return lines.join("\n") + "\n" +} + +function addNetworkOptions( + line: (s: string) => void, + cond: string, +) { + line( + `complete -f -c opencode -n ${cond} -l port -r -d 'Port to listen on'`, + ) + line( + `complete -f -c opencode -n ${cond} -l hostname -r -d 'Hostname to listen on'`, + ) + line( + `complete -f -c opencode -n ${cond} -l mdns -d 'Enable mDNS service discovery'`, + ) + line( + `complete -f -c opencode -n ${cond} -l mdns-domain -r -d 'Custom mDNS domain'`, + ) + line( + `complete -f -c opencode -n ${cond} -l cors -r -d 'Additional CORS domains'`, + ) +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 67de87c2aadd..a08d9fc2286e 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -38,6 +38,7 @@ import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" import { drizzle } from "drizzle-orm/bun-sqlite" +import { generateFishCompletions } from "./cli/cmd/completion" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -186,6 +187,15 @@ const cli = yargs(args) }) .strict() +// Handle fish shell completions before yargs processing +if (args[0] === "completion" && args.includes("--shell")) { + const shellArg = args[args.indexOf("--shell") + 1] + if (shellArg === "fish") { + process.stdout.write(generateFishCompletions()) + process.exit(0) + } +} + try { if (args.includes("-h") || args.includes("--help")) { await cli.parse(args, (err: Error | undefined, _argv: unknown, out: string) => {