newspaper_wedding/.aidlc/tools/aidlc-completions.ts
Andrew Ridgway bec1eaac87
Some checks failed
Test / test (push) Has been cancelled
first pass at the newspaper builder
2026-09-14 11:57:22 +10:00

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));
}