222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
#!/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<string, string[]>;
|
|
options: Record<string, string[]>;
|
|
};
|
|
|
|
const GLOBAL_OPTIONS = ["--no-color"];
|
|
|
|
function routeOptions(
|
|
route: ReturnType<typeof listRoutes>[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<string>();
|
|
const nouns: Record<string, string[]> = {};
|
|
const options: Record<string, string[]> = {};
|
|
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<string, string[]>, 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<string, string[]>, 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, string[]>): 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<void> {
|
|
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 <bash|zsh|fish|powershell>");
|
|
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));
|
|
}
|