Something went wrong. Try again.
A fully featured CLI used as an alternative to AppView applications (Bsky.app/mu.social)
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125import 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-<name>` 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 <n>`. */ 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<unknown>;}
/** 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];