import type { Context } from './context.ts'; import type { SettingKey } from './config.ts'; // The shape every command is written in. // // One declarative tree is the single source of truth for everything a command // is: what it parses, the help it prints (as text or as JSON), and what Tab // completion offers. Nothing about a command's interface is written down twice, // so help and completion cannot drift from what the parser accepts. /** What Tab can offer for a value. Static choices, files, or something looked up. */ export type Completer = 'file' | 'directory' | 'account' | 'shell' | 'command' | readonly string[]; export interface OptionSpec { /** Long name, without the dashes: `display-name`. */ name: string; /** One letter: `-n`. */ short?: string; /** * `boolean` takes no value (and `--no-` turns it off), `string` and * `number` take one, `list` takes one and may be given again. */ type: 'boolean' | 'string' | 'number' | 'list'; description: string; /** What the value is called in help: `--limit `. */ value?: string; /** The only values accepted, which are also what Tab offers. */ choices?: readonly string[]; default?: string | number | boolean; complete?: Completer; /** Left out of help and completion: for options kept for compatibility. */ hidden?: boolean; /** Taken from this setting (`atcli config`) when not given. */ setting?: SettingKey; } export interface ArgSpec { name: string; description: string; required?: boolean; /** Takes every remaining positional. Only the last argument may. */ variadic?: boolean; complete?: Completer; } export interface CommandSpec { name: string; aliases?: readonly string[]; /** One line, shown in lists of commands. */ summary: string; /** Paragraphs, shown in this command's own help. */ description?: string; /** Heading this command is listed under in its parent's help. */ group?: string; options?: readonly OptionSpec[]; args?: readonly ArgSpec[]; examples?: readonly string[]; subcommands?: readonly CommandSpec[]; /** Run this subcommand when a group is given no subcommand. */ default?: string; /** Not listed in help, nor offered by Tab. */ hidden?: boolean; /** * What the command writes, set on every runnable command: `network` when * it changes state on the network (the account, or what other people can * see), `local` when it only writes this machine's state (sessions, * config, the shell's startup file), `false` when it only reads. The * reference generator refuses to run while any runnable command has none, * so the classification cannot silently fall behind the commands. */ writes?: false | 'network' | 'local'; run?: (ctx: Context) => Promise; } /** Options every command takes. */ export const GLOBAL_OPTIONS: readonly OptionSpec[] = [ { name: 'help', short: 'h', type: 'boolean', description: 'show help for the command and stop' }, { name: 'json', type: 'boolean', description: 'print the result as JSON (same as --format json)' }, { name: 'markdown', type: 'boolean', description: 'print the result as Markdown (same as --format markdown)' }, { name: 'html', type: 'boolean', description: 'print the result as an HTML page (same as --format html)' }, { name: 'format', type: 'string', value: 'format', choices: ['text', 'json', 'markdown', 'html'], description: "how to print the result (default: the format setting, else text)" }, { name: 'account', short: 'a', type: 'string', value: 'handle|did', description: 'act as this signed-in account instead of the default', complete: 'account' }, { name: 'color', type: 'boolean', description: 'colour human-readable output (default: when stdout is a terminal)' }, { name: 'verbose', short: 'v', type: 'boolean', description: 'log every request to stderr' }, ]; /** The names a command answers to. */ export function namesOf(command: CommandSpec): string[] { return [command.name, ...(command.aliases ?? [])]; } export function findSubcommand(command: CommandSpec, name: string): CommandSpec | undefined { return command.subcommands?.find((sub) => namesOf(sub).includes(name)); } /** The options a command accepts: its own, then the global ones. */ export function optionsOf(command: CommandSpec): OptionSpec[] { return [...(command.options ?? []), ...GLOBAL_OPTIONS]; } /** For listings of things that happened at a time: only the newer ones. */ export const SINCE: OptionSpec = { name: 'since', type: 'string', value: 'time', description: 'only what is newer than this: an ISO 8601 time (2026-10-15, 2026-10-15T09:00:00Z; UTC unless it says otherwise) or how long ago (30m, 2h, 1d, 1w). Exact for timelines, posts, notifications and messages, which come newest first; an algorithmic feed is only roughly in order, so it may stop early there', }; /** Paging options shared by every command that lists things. */ export const PAGING: readonly OptionSpec[] = [ { name: 'limit', short: 'n', type: 'number', value: 'n', description: 'how many items to fetch in total, across as many pages as that takes (default: 25)' }, { name: 'all', type: 'boolean', description: 'fetch every item, however many pages that takes' }, { name: 'cursor', type: 'string', value: 'cursor', description: 'continue from where a previous listing stopped (its "cursor")' }, ]; /** For listings of dated things: only what has not been shown before. */ export const NEW: OptionSpec = { name: 'new', type: 'boolean', description: 'only what is newer than the newest item shown the last time this was listed with --new (the first time: the newest, as usual). With -n, the oldest n of those, so nothing new is skipped', }; /** Paging for listings of dated things, which can also be cut off by --since or --new. */ export const DATED_PAGING: readonly OptionSpec[] = [...PAGING, SINCE, NEW];