Something went wrong. Try again.
A learning, self-improving AI agent pipobscure.tngl.io/golem
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238import { parseArgs, type ParseArgsOptionDescriptor } from 'node:util';
// golem's command line as one tree. Every command says what it is, what it// takes and what it does, in one place, and everything else is read from that:// parsing (node:util's parseArgs), `--help` at any level, `golem help <command…>`,// and Tab completion in every shell (complete.ts). A command added to the tree// has all of them without another line.//// A word is a subcommand while the command so far has subcommands and the// word names one; options can come anywhere, before or after, at any level.// The global options (`--config`, `--help`) are known everywhere.
/** What a value is, for Tab completion: file names, a fixed list, or something looked up. */export type Hint = 'files' | 'dirs' | readonly string[] | ((context: HintContext) => Candidate[] | Promise<Candidate[]>);
export interface HintContext { /** the options given so far, `--config` among them */ values: Record<string, unknown>; /** the arguments given so far */ positionals: string[];}
export interface Candidate { value: string; description?: string | undefined;}
export interface Option { type: 'string' | 'boolean'; short?: string | undefined; multiple?: boolean | undefined; /** what the option is for, in help and completion */ description: string; /** the value's name in help: `--config <file>` */ value?: string | undefined; /** shown in help, not applied: defaults belong to the command */ default?: string | undefined; complete?: Hint | undefined;}
export interface Positional { name: string; description: string; optional?: boolean | undefined; /** takes every remaining argument */ variadic?: boolean | undefined; complete?: Hint | undefined;}
export interface Invocation { /** the command's words, from the root: `['models', 'list']` */ path: string[]; values: Record<string, unknown>; positionals: string[]; io: IO;}
export interface IO { out(text: string): void; err(text: string): void;}
export interface Command { name: string; /** one line, for the list of commands and the top of the help */ summary: string; /** more, under the summary in this command's own help */ description?: string | undefined; options?: Record<string, Option> | undefined; positionals?: Positional[] | undefined; commands?: Command[] | undefined; /** left out of help and completion: `__complete` */ hidden?: boolean | undefined; /** a command with subcommands and no `run` shows its help */ run?: ((invocation: Invocation) => number | Promise<number>) | undefined;}
export const GLOBAL: Record<string, Option> = { config: { type: 'string', value: 'file', description: 'the config file to use', default: '~/.config/golem/golem.json', complete: 'files' }, help: { type: 'boolean', short: 'h', description: 'show help for this command' },};
/** A usage mistake: what went wrong, and which command's help would set it right. */export class UsageError extends Error { readonly path: string[]; constructor(message: string, path: string[]) { super(message); this.path = path; }}
/** Every option a command knows: its own and the global ones. */export function optionsOf(command: Command, root: Command): Record<string, Option> { return { ...(command === root ? root.options : {}), ...command.options, ...GLOBAL };}
/** Find the command a line of words names, and what is left for it. */export function resolve(root: Command, argv: string[]): { command: Command; path: string[]; rest: string[] } { let command = root; const path: string[] = []; const rest: string[] = []; let descending = true; for (let i = 0; i < argv.length; i++) { const word = argv[i]!; if (word === '--') { rest.push(...argv.slice(i)); break; } if (word.startsWith('-') && word !== '-') { rest.push(word); // An option that takes a value, given as a separate word, takes the next one with it. const option = optionByWord(optionsOf(command, root), word); if (option?.type === 'string' && !word.includes('=') && !(word.startsWith('-') && !word.startsWith('--') && word.length > 2) && i + 1 < argv.length) rest.push(argv[++i]!); continue; } const sub = descending ? command.commands?.find((c) => c.name === word) : undefined; if (sub) { command = sub; path.push(word); } else { descending = false; rest.push(word); } } return { command, path, rest };}
export function optionByWord(options: Record<string, Option>, word: string): Option | undefined { if (word.startsWith('--')) return options[word.slice(2).split('=')[0]!]; return Object.values(options).find((o) => o.short === word[1]);}
/** Run a line of words against the tree: parse, check, show help, or run. */export async function execute(root: Command, argv: string[], io: IO): Promise<number> { const { command, path, rest } = resolve(root, argv); try { const options = optionsOf(command, root); const descriptors: Record<string, ParseArgsOptionDescriptor> = {}; for (const [name, o] of Object.entries(options)) { const d: ParseArgsOptionDescriptor = { type: o.type }; if (o.short) d.short = o.short; if (o.multiple) d.multiple = true; descriptors[name] = d; } let parsed: { values: Record<string, unknown>; positionals: string[] }; try { parsed = parseArgs({ args: rest, options: descriptors, allowPositionals: true, strict: true }); } catch (error) { throw new UsageError(plain((error as Error).message), path); } const { values, positionals } = parsed; if (values['help']) { io.out(help(root, path)); return 0; } if (!command.run) { if (positionals.length) throw new UsageError(`unknown command '${positionals[0]}'`, path); io.err(help(root, path)); return 2; } if (command.commands?.length && !command.positionals?.length && positionals.length) throw new UsageError(`unknown command '${positionals[0]}'`, path); checkPositionals(command, positionals, path); return await command.run({ path, values, positionals, io }); } catch (error) { if (!(error instanceof UsageError)) throw error; const name = ['golem', ...error.path].join(' '); io.err(`${name}: ${error.message}\nRun '${name} --help' for what it takes.\n`); return 2; }}
function checkPositionals(command: Command, given: string[], path: string[]): void { const declared = command.positionals ?? []; const required = declared.filter((p) => !p.optional && !p.variadic).length; if (given.length < required) throw new UsageError(`missing <${declared[given.length]!.name}>`, path); if (!declared.some((p) => p.variadic) && given.length > declared.length) throw new UsageError(`unexpected argument '${given[declared.length]}'`, path);}
/** parseArgs's messages name the fix for a script author; this is for a person at a shell. */function plain(message: string): string { const unknown = /Unknown option '([^']+)'/.exec(message); if (unknown) return `unknown option '${unknown[1]}'`; const missing = /Option '([^']+)' argument missing/.exec(message); if (missing) return `${missing[1]!.replace(/^-., /, '').replace(/ <value>$/, '')} needs a value`; return message.replace(/\. To specify .*$/s, '');}
// help
/** The command a path of words names, or the root. */export function find(root: Command, path: string[]): Command { let command = root; for (const word of path) command = command.commands?.find((c) => c.name === word) ?? command; return command;}
export function help(root: Command, path: string[]): string { const command = find(root, path); const name = ['golem', ...path].join(' '); const subcommands = (command.commands ?? []).filter((c) => !c.hidden); const positionals = command.positionals ?? []; const own = command === root ? { ...root.options } : { ...command.options }; const out: string[] = [];
out.push(`${name} — ${command.summary}`, ''); const shape = [name]; if (subcommands.length) shape.push(command.run ? '[<command>]' : '<command>'); if (Object.keys(own).length || subcommands.length || command.run) shape.push('[options]'); for (const p of positionals) shape.push(p.optional ? `[<${p.name}>${p.variadic ? '…' : ''}]` : `<${p.name}>${p.variadic ? '…' : ''}`); out.push(`usage: ${shape.join(' ')}`); if (command.description) out.push('', command.description.trim());
const tables: [string, [string, string][]][] = [ ['commands', subcommands.map((c) => [c.name, c.summary])], ['arguments', positionals.map((p) => [`<${p.name}>`, p.description])], ['options', Object.entries(own).map(([n, o]) => [flag(n, o), describe(o)])], ['global options', Object.entries(GLOBAL).map(([n, o]) => [flag(n, o), describe(o)])], ]; // One width for every table on the page, so they line up with each other. const width = Math.min(28, Math.max(...tables.flatMap(([, rows]) => rows.map(([left]) => left.length))) + 2); const table = (title: string, rows: [string, string][]) => { if (!rows.length) return; out.push('', `${title}:`); for (const [left, right] of rows) out.push(left.length >= width ? ` ${left}\n ${' '.repeat(width)}${right}` : ` ${left.padEnd(width)}${right}`); }; for (const [title, rows] of tables) table(title, rows); if (subcommands.length) out.push('', `Run '${name} <command> --help' for more about a command.`); return `${out.join('\n')}\n`;}
function flag(name: string, o: Option): string { return `${o.short ? `-${o.short}, ` : ''}--${name}${o.type === 'string' ? ` <${o.value ?? 'value'}>` : ''}`;}
function describe(o: Option): string { return `${o.description}${o.default ? ` (default: ${o.default})` : ''}${o.multiple ? ' (can be repeated)' : ''}`;}