Package Exports
- @guanghechen/commander
- @guanghechen/commander/browser
- @guanghechen/commander/node
Readme
@guanghechen/commander
A minimal, type-safe command-line interface builder with fluent API. Supports subcommands, option parsing, shell completion generation (bash, fish, pwsh), and built-in help/version handling.
opts / args are designed for strong type inference from the current command's own declarations.
Install
npm
npm install --save @guanghechen/commander
yarn
yarn add @guanghechen/commander
Usage
Basic Command
import { Command } from '@guanghechen/commander/browser'
const cli = new Command({
name: 'mycli',
version: '1.0.0',
desc: 'My awesome CLI tool',
})
cli
.option({
long: 'verbose',
short: 'v',
type: 'boolean',
args: 'none',
desc: 'Enable verbose output',
})
.option({
long: 'output',
short: 'o',
type: 'string',
args: 'required',
desc: 'Output file path',
default: './output.txt',
})
.argument({
name: 'file',
kind: 'required',
desc: 'Input file to process',
})
.action(({ opts, args, ctx }) => {
const file = String(args.file)
ctx.reporter.info(`Processing ${file}...`)
if (opts.verbose) {
ctx.reporter.debug(`Output: ${opts.output}`)
}
})
cli.run({
argv: process.argv.slice(2),
envs: process.env,
})Subcommands
import { Command } from '@guanghechen/commander/browser'
const root = new Command({
name: 'git',
version: '1.0.0',
desc: 'A simple git-like CLI',
})
const clone = new Command({
desc: 'Clone a repository',
})
.argument({ name: 'url', kind: 'required', desc: 'Repository URL' })
.option({ long: 'depth', type: 'number', args: 'required', desc: 'Shallow clone depth' })
.action(({ args, opts }) => {
console.log(`Cloning ${args.url} with depth ${opts.depth ?? 'full'}`)
})
const commit = new Command({
desc: 'Record changes to the repository',
})
.option({ long: 'message', short: 'm', type: 'string', args: 'required', required: true, desc: 'Commit message' })
.option({ long: 'amend', type: 'boolean', args: 'none', desc: 'Amend previous commit' })
.action(({ opts }) => {
console.log(`Committing: ${opts.message}`)
})
root.subcommand('clone', clone).subcommand('commit', commit).subcommand('ci', commit)
root.run({ argv: process.argv.slice(2), envs: process.env })Shell Completion
import { Command } from '@guanghechen/commander/browser'
import { CompletionCommand } from '@guanghechen/commander/node'
const root = new Command({
name: 'mycli',
version: '1.0.0',
desc: 'My CLI with completion support',
})
// Add completion subcommand
root.subcommand('completion', new CompletionCommand(root))
// Generate completion scripts:
// mycli completion --bash > ~/.local/share/bash-completion/completions/mycli
// mycli completion --fish > ~/.config/fish/completions/mycli.fish
// mycli completion --pwsh >> $PROFILEOption Types
import { Command } from '@guanghechen/commander/browser'
new Command({ name: 'example', desc: 'Option types demo' })
// Boolean (flags)
.option({ long: 'debug', type: 'boolean', args: 'none', desc: 'Enable debug mode' })
// String with choices
.option({
long: 'format',
type: 'string',
args: 'required',
choices: ['json', 'yaml', 'toml'],
default: 'json',
desc: 'Output format'
})
// Number
.option({ long: 'port', type: 'number', args: 'required', default: 3000, desc: 'Server port' })
// Array (generated by variadic args, not a standalone type)
.option({ long: 'include', type: 'string', args: 'variadic', desc: 'Files to include' })
// Required option
.option({ long: 'config', type: 'string', args: 'required', required: true, desc: 'Config file' })
// Custom coercion
.option({
long: 'date',
type: 'string',
args: 'required',
coerce: (value) => new Date(value),
desc: 'Date value',
})Preset Input Files
--preset-opts=<file> and --preset-envs=<file> allow injecting preset argv and
env inputs before normal CLI parsing.
mycli --preset-opts=./options.argv --preset-envs=./preset.env --log-level debug --colorBehavior:
- Route command chain from user argv (name/alias only, no argv rewrite), then store route tokens in
sources.user.cmds. - Run
control-scanon user tail argv before preset merge: detect--help/--versionby token scan (--versiononly whensupportsBuiltinVersion(leaf)), detecthelponly when it is the first tail token, writectx.controls, and strip control tokens from parse input. - In
run(), executerun-controlbefore preset merge: short-circuit byhelp > version. If short-circuit hits, preset files are not loaded. - Scan preset directives before
--, remove them from control-tail argv, and store cleaned tokens insources.user.argv. - Read options preset file(s) and tokenize by whitespace to
sources.preset.argv. - Read env preset file(s) and parse via
@guanghechen/env.parsetosources.preset.envs. - Build
effectiveTailArgv = [...sources.preset.argv, ...sources.user.argv]. - Build
ctx.envs = { ...sources.user.envs, ...sources.preset.envs }. - Expose source snapshots through
ctx.sourcesand reuse existing tokenize/resolve/parse pipeline.
Precedence for same option key:
- User CLI tokens (highest)
- Tokens loaded from
--preset-opts - Option
default/ implicit defaults NO_COLORfallback for color rendering only (applies only when no explicit--color/--no-colortoken appears)
Precedence for same env key:
- Key-values loaded from
--preset-envs(highest) - User envs (e.g.
process.env)
Additional notes:
variadicoptions append in appearance order.NO_COLORis evaluated fromctx.envsand remains a fallback only when no color token is explicitly provided.- The
--preset-optsfile is expected to contain option fragments (-x/--xxxand their values), not command-route tokens. - The
--preset-envsfile must be parseable by@guanghechen/env. - Only preset flags before
--are processed; after--they are treated as normal args. - Repeated preset flags are processed in appearance order.
- Built-in control semantics recognize
--help/help/--versiononly (no short aliases). long: 'help'andlong: 'version'are reserved and must not be user-defined in.option().--help/help/--versionare forbidden in--preset-optsfiles; loading should fail fast.--is forbidden inside--preset-optsfiles; loading should fail fast.parse()never executes control handlers; it only records control hits inctx.controls.
Built-in Coerce Factories
import { Coerce, Command } from '@guanghechen/commander/browser'
new Command({ name: 'example', desc: 'Coerce demo' })
.option({
long: 'offset',
type: 'number',
args: 'required',
coerce: Coerce.integer('--offset'),
desc: 'Signed offset',
})
.option({
long: 'parallel',
type: 'number',
args: 'required',
coerce: Coerce.positiveInteger('--parallel'),
desc: 'Parallel workers',
})
.option({
long: 'duration',
type: 'number',
args: 'required',
coerce: Coerce.positiveNumber('--duration'),
desc: 'Duration in seconds',
})
.option({
long: 'port',
type: 'number',
args: 'required',
coerce: Coerce.port('--port'),
desc: 'Server port',
})
.option({
long: 'domain',
type: 'string',
args: 'required',
coerce: Coerce.domain('--domain'),
desc: 'Domain name',
})
.option({
long: 'ip',
type: 'string',
args: 'required',
coerce: Coerce.ip('--ip'),
desc: 'IP address',
})
.option({
long: 'host',
type: 'string',
args: 'required',
coerce: Coerce.host('--host'),
desc: 'Host (IP or domain)',
})
.option({
long: 'mode',
type: 'string',
args: 'required',
coerce: Coerce.choice('--mode', ['dev', 'test', 'prod'] as const),
desc: 'Deploy mode',
})
.option({
long: 'scale',
type: 'number',
args: 'required',
coerce: Coerce.number('--scale'),
desc: 'Scale factor',
})Default error message format:
{name} is expected as {coerce type}, but got {raw}You can still override the message via Coerce.xxx(name, 'custom error message').
Built-in Is Helpers
import { isDomain, isIp, isIpv4, isIpv6 } from '@guanghechen/commander/browser'
isIpv4('127.0.0.1') // true
isIpv6('::1') // true
isIp('2001:db8::1') // true
isDomain('example.com') // trueHelp Examples
import { Command } from '@guanghechen/commander/browser'
const cli = new Command({ name: 'mycli', desc: 'My CLI tool' })
cli
.example('Initialize Project', 'init my-app', 'Create project scaffold')
.example('Watch Build', 'build --watch', 'Rebuild on file changes')
.action(() => {})
await cli.run({ argv: ['--help'], envs: process.env })usage 是相对当前 command path 的片段,help 中会自动补齐前缀,例如 mycli build --watch。
--color / --no-color 仅控制 help 文本的终端着色; --log-colorful / --no-log-colorful 控制
Reporter 的日志着色。
当环境变量 NO_COLOR 存在时,help 渲染默认视为 --no-color;显式传入 --color
可以覆盖这个默认值。