Something went wrong. Try again.
A nicer S3 CLI
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115import 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 — on the command line and in the interactive shell alike,// which run the very same specs. Nothing about a command's interface is// written down twice, so help and completion cannot drift from the parser.
/** * What Tab can offer for a value: a remote path (`/bucket/key`), a local file * or directory, a profile, a remembered bucket, a setting, or static choices. */export type Completer = 'remote' | 'remote-dir' | 'local' | 'local-dir' | 'profile' | 'bucket' | 'setting' | 'command' | readonly string[];
export interface OptionSpec { /** Long name, without the dashes: `content-type`. */ name: string; /** One letter: `-r`. */ 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 (`s3cli 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; /** * Where the command can be run: only on the command line (`cli`: setting * up, which asks its own questions), only in the interactive shell * (`shell`: cd, lcd and the like, which change the shell's state), or, by * default, in both. */ scope?: 'cli' | 'shell'; /** * What the command writes, set on every runnable command: `remote` when * it changes what is stored at the service, `local` when it only writes * this machine's files or state, `false` when it only reads. */ writes?: false | 'remote' | '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: 'format', type: 'string', value: 'format', choices: ['text', 'json'], description: 'how to print the result (default: the format setting, else text)' }, { name: 'profile', short: 'P', type: 'string', value: 'name', description: "use this profile's keys for buckets that are not remembered (default: the default profile)", complete: 'profile' }, { 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];}
/** The command tree as one mode sees it: without the commands scoped to the other. */export function forScope(command: CommandSpec, scope: 'cli' | 'shell'): CommandSpec { if (!command.subcommands) return command; return { ...command, subcommands: command.subcommands.filter((sub) => !sub.scope || sub.scope === scope).map((sub) => forScope(sub, scope)) };}
export const YES: OptionSpec = { name: 'yes', short: 'y', type: 'boolean', description: 'do it without asking first' };export const RECURSIVE: OptionSpec = { name: 'recursive', short: 'r', type: 'boolean', description: 'everything beneath a folder, not just the folder itself' };