#!/usr/bin/env bun import { emitResult, globalOptions, success, usage, } from "./aidlc-command.ts"; import { listRoutes } from "./aidlc.ts"; export type Shell = "bash" | "zsh" | "fish" | "powershell"; type CompletionInventory = { top: string[]; nouns: Record; options: Record; }; const GLOBAL_OPTIONS = ["--no-color"]; function routeOptions( route: ReturnType[number], verb: string, ): string[] { const values = new Set(GLOBAL_OPTIONS); if (route.outputModes.includes("json")) values.add("--json"); if (route.outputModes.includes("quiet")) values.add("--quiet"); if (route.networkPolicy !== "forbidden") values.add("--offline"); for ( const line of (route.all ?? []).filter((value) => value.split(/\s+/, 1)[0] === verb ) ) { for (const option of line.match(/--[a-z][a-z-]*/g) ?? []) values.add(option); } return [...values].sort(); } function inventory(): CompletionInventory { const routes = listRoutes().filter((route) => route.visibility === "public" && route.classification !== "stub" ); const top = new Set(); const nouns: Record = {}; const options: Record = {}; for (const route of routes) { const verbs = route.verbs.filter((verb) => !verb.startsWith("<") && !verb.startsWith("__") ); if (route.group === "top") { for (const verb of verbs) { top.add(verb); const routeOptionValues = routeOptions(route, verb); options[verb] = [...new Set([ ...(options[verb] ?? []), ...routeOptionValues, ])].sort(); } continue; } if (!nouns[route.group]) nouns[route.group] = []; top.add(route.group); for (const verb of verbs) { const first = verb.split(/\s+/, 1)[0]; const routeOptionValues = routeOptions(route, first); nouns[route.group].push(first); const key = `${route.group} ${first}`; options[key] = [...new Set([ ...(options[key] ?? []), ...routeOptionValues, ])].sort(); } } for (const noun of Object.keys(nouns)) { nouns[noun] = [...new Set(nouns[noun])].sort(); } return { top: [...top].sort(), nouns, options }; } function bashCase(rows: Record, indent: string): string { return Object.entries(rows) .map(([key, values]) => `${indent}'${key}') words="${values.join(" ")}" ;;`) .join("\n"); } function bash(data: CompletionInventory): string { return `# generated by the AI-DLC installer _aidlc_completions() { local cur="\${COMP_WORDS[COMP_CWORD]}" words="" key if [ "\${COMP_CWORD}" -eq 1 ]; then words="${data.top.join(" ")}" elif [[ "$cur" == --* ]]; then key="\${COMP_WORDS[1]}" [ "\${COMP_CWORD}" -ge 3 ] && key="$key \${COMP_WORDS[2]}" case "$key" in ${bashCase(data.options, " ")} esac elif [ "\${COMP_CWORD}" -eq 2 ]; then case "\${COMP_WORDS[1]}" in ${bashCase(data.nouns, " ")} esac fi COMPREPLY=( $(compgen -W "$words" -- "$cur") ) } complete -F _aidlc_completions aidlc `; } function zshCase(rows: Record, indent: string): string { return Object.entries(rows) .map(([key, values]) => `${indent}'${key}') values=(${values.join(" ")}) ;;`) .join("\n"); } function zsh(data: CompletionInventory): string { return `#compdef aidlc # generated by the AI-DLC installer _aidlc() { local -a values local key="\${words[2]}" if (( CURRENT == 2 )); then values=(${data.top.join(" ")}) elif [[ "\${words[CURRENT]}" == --* ]]; then (( CURRENT >= 4 )) && key="$key \${words[3]}" case "$key" in ${zshCase(data.options, " ")} esac else case "\${words[2]}" in ${zshCase(data.nouns, " ")} esac fi _describe 'aidlc value' values } compdef _aidlc aidlc `; } function fish(data: CompletionInventory): string { const lines = [ "# generated by the AI-DLC installer", ...data.top.map((command) => `complete -c aidlc -n '__fish_use_subcommand' -a '${command}'` ), ]; for (const [noun, verbs] of Object.entries(data.nouns)) { for (const verb of verbs) { lines.push( `complete -c aidlc -n '__fish_seen_subcommand_from ${noun}' -a '${verb}'`, ); } } const optionNames = [...new Set(Object.values(data.options).flat())].sort(); for (const option of optionNames) { lines.push(`complete -c aidlc -l '${option.slice(2)}'`); } return `${lines.join("\n")}\n`; } function psArray(values: readonly string[]): string { return `@(${values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ")})`; } function powershellSwitch(rows: Record): string { return Object.entries(rows) .map(([key, values]) => ` '${key.replaceAll("'", "''")}' { ${psArray(values)}; break }` ) .join("\n"); } function powershell(data: CompletionInventory): string { return `# generated by the AI-DLC installer Register-ArgumentCompleter -Native -CommandName aidlc -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $tokens = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text }) $values = if ($tokens.Count -le 2) { ${psArray(data.top)} } elseif ($wordToComplete.StartsWith('--')) { $key = $tokens[1] if ($tokens.Count -ge 3) { $key = "$key $($tokens[2])" } switch ($key) { ${powershellSwitch(data.options)} default { @() } } } else { switch ($tokens[1]) { ${powershellSwitch(data.nouns)} default { @() } } } $values | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } `; } export function renderCompletion(shell: Shell): string { const data = inventory(); if (shell === "bash") return bash(data); if (shell === "zsh") return zsh(data); if (shell === "fish") return fish(data); return powershell(data); } export async function main(argv: string[]): Promise { const shell = (argv[0] === "completions" ? argv[1] : argv[0]) as Shell | undefined; const result = shell && ["bash", "zsh", "fish", "powershell"].includes(shell) ? success(renderCompletion(shell), { shell }) : usage("usage: aidlc system completions "); if (result.ok && globalOptions(argv).mode === "human") { process.stdout.write(result.message); process.exitCode = 0; return; } emitResult(result, globalOptions(argv)); } if (import.meta.main) { void main(process.argv.slice(2)); }