import { GLOBAL_OPTIONS, type Command, type OptionSpec } from './lib/command.ts'; import { root } from './commands/root.ts'; // The command reference as Markdown, for the agent skill: generated from the // same tree the parser uses. Every runnable command declares whether it // writes (`writes` on its spec); this generator refuses to run while one // does not, so the classification cannot silently fall behind the commands. /** What to say, beneath the summary, of a command whose writeness depends on how it is used. */ const CAVEATS: Record = { 'tangled api': 'Whether it writes depends on the method: queries (GET) read, procedures (POST) write.', 'tangled issue label': 'Without --add or --remove it only shows labels; with them it writes.', 'tangled pr label': 'Without --add or --remove it only shows labels; with them it writes.', }; function flag(name: string, option: OptionSpec): string { const value = option.type === 'boolean' ? '' : ` <${option.placeholder ?? 'string'}>`; return `${option.short ? `-${option.short}, ` : ''}--${name}${value}`; } function describe(option: OptionSpec): string { return option.multiple ? `${option.description} (repeatable)` : option.description; } function writesLine(command: Command): string | undefined { if (command.writes === 'network') return '**Writes to the network.** It changes a repository, a knot, or publishes something other people can see.'; if (command.writes === 'local') return "**Writes only on this machine.** It changes local state — sessions, config, git config, the shell's startup file — and nothing on the network."; return undefined; } export function reference(): string { const network: string[] = []; const local: string[] = []; const lines: string[] = [ '# tangled command reference', '', '', '', '## Which commands write', '', 'Every command reads unless it is listed here.', '', 'These write to the network: they change a repository, a knot or a spindle, or publish something other people can see. An agent should have its user\'s ok before running one, unless the user asked for that exact action:', '', ]; const visit = (path: Command[]) => { const command = path[path.length - 1]!; if (command.hidden) return; if (path.length > 1) { const name = ['tangled', ...path.slice(1).map((each) => each.name)].join(' '); if (command.run && command.writes === undefined) { throw new Error(`reference: ${name} does not say whether it writes — set writes: false, 'local' or 'network' on its spec`); } if (command.run && command.writes === 'network') network.push(`\`${name}\``); if (command.run && command.writes === 'local') local.push(`\`${name}\``); lines.push(`${'#'.repeat(Math.min(path.length + 1, 4))} \`${name}\``, '', `${command.summary}.`, ''); const writes = writesLine(command); if (writes) lines.push(writes, ''); const caveat = CAVEATS[name]; if (caveat) lines.push(caveat, ''); if (command.aliases?.length) lines.push(`Also: ${command.aliases.map((alias) => `\`${alias}\``).join(', ')}.`, ''); if (command.run) { const flags = Object.keys(command.options ?? {}).length ? ' [flags]' : ''; lines.push('```', `${name}${command.args ? ` ${command.args}` : ''}${flags}`, '```', ''); for (const option of Object.entries(command.options ?? {})) lines.push(`- \`${flag(option[0], option[1])}\`: ${describe(option[1]).replace(/\|/g, '\\|')}`); if (Object.keys(command.options ?? {}).length) lines.push(''); if (command.examples?.length) { lines.push('Examples:', ''); for (const example of command.examples) lines.push(`- \`${example.replace(/`/g, '\\`')}\``); lines.push(''); } } } for (const sub of command.subcommands ?? []) visit([...path, sub]); }; visit([root]); lines.push( network.length ? `- ${network.join(', ')}.` : '- (none).', '', 'These write only on this machine — sessions, config, git config, the shell\'s startup file. Nothing on the network changes:', '', local.length ? `- ${local.join(', ')}.` : '- (none).', '', 'And `tangled api` does either, depending on the method: queries (GET) read, procedures (POST) write.', '', 'Every command also takes the global options:', '', ...Object.entries(GLOBAL_OPTIONS).map(([name, option]) => `- \`${flag(name, option)}\`: ${describe(option).replace(/\|/g, '\\|')}`), '', ); return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`; }