A command module is the default export of a command file: a plain object describing what the command does, what flags/positionals it accepts, and how it runs.
import { command } from 'concise-ti'
export default command({
meta: {
description: 'Apply pending database migrations',
examples: ['app db migrate', 'app db migrate --steps 2'],
},
flags: {
steps: { type: 'number', default: 1, description: 'Migrations to apply' },
},
run(ctx) {
ctx.io.write(`Applying ${ctx.flags.steps} migration(s)`)
},
})command() is the identity function: it returns its argument unchanged. Its
only job is giving TypeScript enough context to infer ctx.flags' shape from
your flags declaration, so wrap every command definition with it rather than
exporting a plain object literal.
export function command<F = Record<string, unknown>, C extends Config = Config>(
module: CommandModule<F, C>,
): CommandModule<F, C> {
return module
}The second generic, C, only matters for CLIs whose entrypoint calls
run<ConfigType>() with an extended Config (see
Config). Pass it explicitly to type
ctx.config without a cast: command<Flags, MyConfig>({ ... }).
It does not validate anything at load time; a malformed FlagSpec or missing
run is caught by the TypeScript compiler (bun run check:types), not at
runtime.
Only run is required:
export default command({
run(ctx) {
ctx.io.write('Hello!')
},
})interface CommandMeta {
description?: string
aliases?: readonly string[]
hidden?: boolean
examples?: readonly string[]
}All optional. description is read by the default help command (see
Default Commands): non-hidden entries are listed with
their description alongside the route. examples is currently just data
carried on the manifest entry, reserved for richer generated help later (see
the Roadmap). aliases is likewise reserved, not yet
read anywhere.
export default command({
flags: {
verbose: { type: 'boolean', short: 'v' },
output: { type: 'string', short: 'o' },
},
args: [{ name: 'target', required: false }],
run(ctx) {
// ctx.flags.verbose, ctx.flags.output: parsed and typed
// ctx.positionals[0]: the raw string; args[] is descriptive only today
},
})See Flag Parsing and Positional Arguments for the full spec of each, including what's enforced today versus documented for later.
Set rawArgs: true to bypass flag parsing entirely and receive the command's
remaining argv verbatim in ctx.positionals, with ctx.flags left {}:
export default command({
rawArgs: true,
run(ctx) {
// ctx.positionals is the untouched argv tail, e.g. ['./main.ts', '--outfile', 'dist/x']
},
})This is for commands that forward arbitrary flags to another CLI rather than
declaring their own — concise-ti compile uses it to pass --outfile and
other bun build flags straight through, since strict flag parsing would
otherwise reject any flag not declared on the command itself.
A file at commands/db/migrate.ts is the db migrate command. Nesting is
purely a filesystem convention; the module shape doesn't change:
// commands/db/migrate.ts
export default command({
meta: { description: 'Run pending migrations' },
run(ctx) {
ctx.io.write('Running migrations...')
},
})See Command Routing.
Receives one argument, the Context. Destructure what you need or take it
whole:
run(ctx) { /* ctx.flags, ctx.positionals, ctx.io, ... */ }
run({ flags, io }) { /* only what you use */ }
run: async (ctx) => { await doWork(ctx) }run(ctx) {
if (somethingWrong) {
ctx.io.writeError('Failed!')
return 1
}
return 0
}Returning nothing is equivalent to 0.