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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140import { bindArgs, parse } from './args.ts';import { Client } from './api.ts';import { Context, type Format, type Result } from './context.ts';import { setting } from './config.ts';import { toHtml, toMarkdown } from './doc.ts';import { autoDoc } from './docs.ts';import { CliError, EXIT, UsageError } from './errors.ts';import { helpDoc, helpJson, helpText } from './help.ts';import { errRed, render, setColour, stripAnsi } from './output.ts';import { setVerbose } from './xrpc.ts';import { root } from './commands/index.ts';import { respond } from './completion.ts';import { version } from './version.ts';
// The whole program, as a function from argv to an exit code, so tests and// hosts can run commands without a subprocess. main.ts is the only place that// turns the code into an exit.
export interface IO { stdout: (text: string) => void; stderr: (text: string) => void;}
const defaultIO: IO = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text),};
export async function main(argv: string[], io: IO = defaultIO): Promise<number> { // What a shell runs on Tab: never fails loudly — an error is nothing to offer. if (argv[0] === '__complete') { try { const lines = respond(root, argv.slice(1)); if (lines.length) io.stdout(`${lines.join('\n')}\n`); } catch { // Nothing to offer. } return EXIT.ok; } if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) { io.stdout(`atcli ${version()}\n`); return EXIT.ok; }
const format = resolveFormat(argv); const json = format === 'json'; try { const parsed = parse(root, argv); if (parsed.options['color'] !== undefined) setColour(Boolean(parsed.options['color'])); if (format !== 'text') setColour(false); setVerbose(Boolean(parsed.options['verbose']));
if (parsed.help || !parsed.command.run) { const title = ['atcli', ...parsed.path.slice(1).map((command) => command.name)].join(' '); io.stdout(format === 'json' ? `${JSON.stringify(helpJson(parsed.path), null, 2)}\n` : format === 'markdown' ? toMarkdown(helpDoc(parsed.path, title)) : format === 'html' ? toHtml(helpDoc(parsed.path, title)) : helpText(parsed.path)); return EXIT.ok; }
const ctx = new Context({ command: parsed.command, path: parsed.path, options: parsed.options, args: bindArgs(parsed.command, parsed.positionals), client: new Client(parsed.options['account'] as string | undefined), format, out: io.stdout, }); const outcome = await parsed.command.run(ctx) as Result | undefined; if (outcome) print(outcome, format, ctx.name, io); return outcome?.exit ?? EXIT.ok; } catch (error) { return report(error, json, argv.includes('--verbose') || /^-[a-zA-Z]*v/.test(argv.find((word) => /^-[^-]/.test(word)) ?? ''), io); }}
const FORMATS: readonly Format[] = ['text', 'json', 'markdown', 'html'];
/** * How results are printed: the last of --json, --markdown, --html and * --format on the line; else the format setting; else text. Worked out before * parsing, so that a command line that does not parse still fails in the * format asked for. */export function resolveFormat(argv: readonly string[], saved: () => string = () => setting('format')): Format { let chosen: Format | undefined; for (let at = 0; at < argv.length; at++) { const word = argv[at]!; if (word === '--') break; if (word === '--json' || word === '--markdown' || word === '--html') chosen = word.slice(2) as Format; else if (word.startsWith('--format=')) chosen = FORMATS.find((each) => each === word.slice('--format='.length)) ?? chosen; else if (word === '--format') chosen = FORMATS.find((each) => each === argv[at + 1]) ?? chosen; } if (chosen) return chosen; try { const value = saved(); return FORMATS.includes(value as Format) ? value as Format : 'text'; } catch { return 'text'; }}
function print(outcome: Result, format: Format, title: string, io: IO): void { if (format === 'json') { io.stdout(`${JSON.stringify(outcome.data ?? null, null, process.stdout.isTTY ? 2 : 0)}\n`); return; } if (format !== 'text' && !outcome.raw) { const doc = outcome.doc ? outcome.doc() : autoDoc(outcome.data, title); io.stdout(format === 'markdown' ? toMarkdown(doc) : toHtml(doc)); return; } const text = typeof outcome.text === 'function' ? outcome.text() : outcome.text; const shown = text ?? (outcome.data === undefined ? '' : render(outcome.data)); if (shown) io.stdout(shown.endsWith('\n') ? shown : `${shown}\n`);}
function report(error: unknown, json: boolean, verbose: boolean, io: IO): number { // --verbose shows where an unexpected error came from. if (verbose && error instanceof Error && !(error instanceof CliError)) io.stderr(`${error.stack}\n`); const known = error instanceof CliError; const exit = known ? error.exit : EXIT.error; const code = known ? error.code : 'InternalError'; const message = error instanceof Error ? error.message : String(error); if (json) { const status = (error as { status?: number }).status; io.stdout(`${JSON.stringify({ error: { code, message: stripAnsi(message), exit, ...(status ? { status } : {}) } })}\n`); } if (error instanceof UsageError) { io.stderr(`${errRed('error:')} ${message}\n`); const name = ['atcli', ...error.path.slice(1).map((command) => command.name)].join(' '); io.stderr(`Run '${name} --help' for usage.\n`); } else if (!json || process.stderr.isTTY) { io.stderr(`${errRed('error:')} ${message}\n`); } return exit;}