| name | bun-cli |
|---|---|
| description | Implement or refactor any CLI functionality in Bun and TypeScript projects using `@andreas-timm/cli` as the standard foundation for command registration, options (defaults, required flags, choice enums), multi-word commands, validation, help output, zsh completion, and install commands. Use whenever a Bun project needs a CLI entrypoint, subcommands, flags, parsing fixes, help text, completion, install workflows, or migration away from ad-hoc CLI helpers. |
This skill covers CLI work in Bun projects generally. The default rule is simple: if a Bun project needs CLI functionality, implement it with cac plus @andreas-timm/cli.
Do not copy helper implementations into the project source. Project code should own command intent and business logic; @andreas-timm/cli should own shared CLI mechanics such as command aliases, option normalization, multi-word parsing, help behavior, and completion output.
- Treat the CLI as a first-class application surface, not a pile of one-off scripts.
- Use
cacto define commands and flags. - Use
@andreas-timm/clifor reusable CLI infrastructure. - Keep handlers lazy-loaded and focused on domain logic.
- If a project already has local CLI utilities that duplicate the package, migrate toward the package when touching that area.
- Any new CLI functionality in a Bun project should use
@andreas-timm/cli. - Any modified CLI functionality should be moved closer to
@andreas-timm/clipatterns instead of extending custom local helpers. - Only keep project-local CLI helpers when the package does not support a required behavior yet.
- If the package is missing something important, prefer extending the package over cloning its code into the app.
bun add '@andreas-timm/cli'Use this skill when the task is about any of the following in a Bun project:
- creating or restructuring
cli.ts - adding or changing commands, subcommands, or aliases
- adding flags, array options, required options, choice enums, validation, or parsing fixes
- improving
--help,help, or subcommand help behavior - adding shell completion
- adding an
installcommand that symlinks a CLI into~/.local/bin - migrating away from ad-hoc
cacwrappers or copied helper code
Example requests that should trigger this skill:
- "Add a
deploy runcommand to this Bun app" - "Refactor this Bun CLI to support aliases and nested commands"
- "Fix repeated
--tagflags in our CLI" - "Add zsh completion to the Bun CLI"
- "Move these local CLI helpers to
@andreas-timm/cli"
- Keep the root
cli.tsfocused on assembly only: createcac('<app-name>'), register global options, register command groups, optionally register completion commands, install help behavior, set version, then callawait run(cli). - Put each command group in its own registrar module such as
registerFeatureCommands. - Keep business logic in lazy-loaded handler modules, not in the registrar.
- Reuse the package exports instead of re-implementing command registration, option normalization, help patches, or completion generation.
Use these exports from @andreas-timm/cli:
registerCommandsandregisterCommandNamesfor primary names plus aliases.addCommandOptionsfor declarative option arrays. Each item may includeconfig(see Option defaults, Required options, Enum / choice options).processCommandRawOptionsandensureScalarto normalize repeated scalar flags.preserveEmptyStringOptionwhen""must survive CAC parsing.parsePositiveIntegerfor strict positive integer validation.CliOptionRawScalar(string | number | undefined) for typing CACconfig.typetransforms; CAC itself types parsed options asany.installDefaultCommandHelp,installSubcommandHelp, andrunfor help behavior, multi-word command parsing, and unknown-command handling.installDefaultCommandHelpcollapses the top-level "For more info, run any command with the--helpflag" section into a single note without per-command examples. Pass{ showHelpHint: false }toinstallSubcommandHelpto suppress the "For more info, run any subcommand with the--helpflag" footer section.registerCompletionCommandsandgenerateZshCompletionwhen the CLI should emit zsh completion.generateZshCompletionreadsconfig.choiceson value options and emitscompaddfor those values after the flag (see Enum / choice options).assertOptionValueInChoicesfor manual validation when not usingaddCommandOptions.assertRequiredCliOptionsandgetCliOptionPropertyKeywhen you validate parsed options yourself (see Required options); preferconfig.requiredonCliOptionItemwithaddCommandOptionsso checks run automatically.registerInstallCommandandrunInstallCommandwhen the CLI should ship a built-ininstallcommand that symlinks the app CLI into~/.local/bin. By default,registerInstallCommandusescli.namefor the link name andprocess.argv[1]for the target path; passpackageNameandtargetPathwhen you want explicit control.
See files/cli.ts for a complete cli.ts: global options, command group registration, optional registerCompletionCommands / registerInstallCommand, installDefaultCommandHelp + installSubcommandHelp, cli.version(...), and await run(cli).
CliOptionItem supports config?: { default?: unknown; type?: readonly unknown[]; choices?: readonly string[]; required?: boolean }. Values are passed through to CAC’s command.option(name, description, config) except for choices and required, which @andreas-timm/cli handles around registration. Use readonly so as const option arrays (including tuple type: [(v) => …]) type-check.
- Set
config.defaultwhen a flag should have a value even when the user omits it. CAC merges defaults into parsed options and usually prints(default: …)in--help. - Prefer declaring the default on the option instead of repeating
options.foo ?? defaultValuein the action when the default is unconditional for that command. - Conditional defaults (e.g. “only when
--otheris set”) often still belong in the handler: CAC appliesdefaultfor every run of that command, so the parsed options object will always include that key when omitted.
Example:
const OPTIONS = [
{ rawName: '--page <n>', description: 'Page number', config: { default: 1 } },
] as const;Set config.required: true when the user must pass the flag so the parsed value is not undefined. This is different from CAC’s angle brackets in rawName (--foo <bar>), which only mean “if the flag is used, a value is required,” not “the flag itself must appear.”
Behavior:
addCommandOptionsregisters options, then wrapscommand.actionso that before your handler runs, every item withconfig.requiredis checked:options[getCliOptionPropertyKey(rawName)] !== undefined.config.requiredandconfig.defaultcannot be used together (registration throws): a default always supplies a value, so “required” would be meaningless.- Boolean flags work as expected:
falseis still a defined value; omitting the flag leavesundefinedand fails the check.
For options registered without addCommandOptions, call assertRequiredCliOptions(parsedOptions, items) yourself, or use getCliOptionPropertyKey('--flag <name>') to look up the camelCased key CAC uses.
Example:
import type { CliOptionItem } from '@andreas-timm/cli';
const OPTIONS = [
{
rawName: '--env <env>',
description: 'Target environment',
config: { required: true },
},
] as const satisfies readonly CliOptionItem[];Set config.choices on a value option (<name> or [name] in rawName). addCommandOptions composes a type wrapper so parse-time validation matches CAC: the value must satisfy choices.includes(String(value)) after any custom config.type[0] transform. Omit choices or use an empty array to disable this.
- If you set both
defaultandchoices, the default must appear inchoices(enforced when registering). - With
choices, CAC may still expose values as a one-element array; useensureScalar/processCommandRawOptionslike othertypeoptions. - Prefer
addCommandOptionsforchoices: CAC’s ownOptionConfigtypings do not includechoices, andaddCommandOptionscomposes validation plus keepsoption.config.choicespopulated forgenerateZshCompletion.
Example:
const OPTIONS = [
{
rawName: '--format <format>',
description: 'Output format',
config: { choices: ['json', 'text', 'table'] },
},
] as const satisfies readonly CliOptionItem[];generateZshCompletion uses config.choices for value completion: after typing --format , the script offers only those strings (with the same prefix filtering as flags and subcommands). Options that also have choices do not use file/directory inference for that flag.
Validate in the option definition with CAC’s config.type[0] transform instead of hand-parsing in the action. Use CliOptionRawScalar for the callback parameter type. Prefer a label that matches the flag (e.g. '--limit') in parsePositiveInteger error messages.
See files/positive-integer-options.ts for a full OPTIONS array with default + type: [(v) => parsePositiveInteger(v, '--limit')].
Optional value options (no default) need a transform that allows undefined (CAC may still run transforms when other flags are parsed); required or defaulted options match that pattern. Boolean flags use parseBooleanOption instead; its input type includes boolean.
See files/feature/cli.ts for a registrar that uses registerCommands with an alias, addCommandOptions, preserveEmptyStringOption, processCommandRawOptions with array keys, and a lazy-loaded handler import.
See files/validation-handler.ts for a handler that calls processCommandRawOptions then parsePositiveInteger on the parsed value.
When updating an existing Bun CLI:
- Keep
caccommand definitions. - Replace local helper implementations with imports from
@andreas-timm/cli. - Move heavy command actions into handler modules if they are still inline.
- Replace direct
cli.parse(...)calls withawait run(cli)when multi-word commands or normalized help behavior matter. - Remove duplicate local utilities after the package-based path is working.
- Use
registerCommandswhen command creation and configuration happen together. - Use
registerCommandNameswhen command registration and configuration are intentionally split. - Pass array option keys to
processCommandRawOptions(..., ['tag'])so repeated values remain arrays; unlisted keys collapse to the last scalar value. - Use
preserveEmptyStringOptionwhen a flag must distinguish""from CAC's default coercion. - Use
config: { default: … }on aCliOptionItemwhen the default should live in the option definition and appear in help; keep handler-side fallbacks only when the default depends on other flags. - Use
config: { choices: […] }for fixed string enums on value options; useassertOptionValueInChoicesonly when you are not registering throughaddCommandOptions. - Use
config: { required: true }when the user must pass the flag (parsed value must not beundefined); do not combine withdefault. UseassertRequiredCliOptions/getCliOptionPropertyKeyonly when options are not registered viaaddCommandOptions. - Use
config: { type: [(v: CliOptionRawScalar) => parsePositiveInteger(v, '--flag')] }(and optionaldefault) for positive integers; use handler-sideparsePositiveIntegeronly when validation is conditional on other options. - Call
await run(cli)instead ofcli.parse(...)whenever the CLI supports multi-word commands or should normalizehelp. - Add
registerCompletionCommands(cli)only when the CLI should ship a built-incompletion zshcommand. - Add
registerInstallCommand(cli, options?)only when the CLI should ship a built-ininstallcommand. PasspackageNameandtargetPathexplicitly when you want the link name and target path to be independent fromcli.nameandprocess.argv[1]. - Call
installDefaultCommandHelp(cli)alongsideinstallSubcommandHelp(cli)to ensure proper formatting of usage strings and subcommands.
- Default to
@andreas-timm/clifor any CLI infrastructure in Bun projects. - Prefer aliases for frequently used commands.
- Use
.alias('!')on a named command to treat it as the default command in help layouts without auto-running it on bare invocation. - Use multi-word command names only when grouping improves discoverability.
- Keep descriptions explicit and short.
- Keep side effects out of registrar files.
- Avoid eager imports of heavy handlers at startup.
- Do not duplicate code from
@andreas-timm/cliinto project utils unless the package is missing a required capability; extend the package instead.
- Run
<app> --helpand ensure global options appear once. - Run
<app> helpand<app> <command> --helpand confirmawait run(cli)normalizes both paths correctly. - Run all aliases for a command and confirm identical behavior.
- Run multi-word commands with and without aliases.
- Pass repeated scalar flags and verify the last value wins.
- Pass repeated array flags and verify listed array keys preserve all values.
- Run
<app> completion zshif enabled and confirm the script is emitted; for options withconfig.choices, complete after the flag and confirm only those strings are offered. - For commands that declare
config.required, run without those flags and confirm a clear error; run with them and confirm the handler executes. - Run
<app> installif enabled and confirm~/.local/bin/<link-name>points at the intended CLI entrypoint path. - Confirm removed local CLI helpers are no longer imported anywhere.