name: citty description: > Reference for the citty CLI framework (UnJS). Auto-loads when importing from 'citty', using defineCommand, runMain, or runCommand, or building CLI tools with argument parsing, subcommands, or lifecycle hooks. Covers defineCommand, runMain, argument definitions, subcommands, and lifecycle hooks.
citty — CLI Framework Reference
citty is a lightweight, TypeScript-first CLI builder from the UnJS ecosystem. It uses native Node.js util.parseArgs under the hood. Zero dependencies.
Install: bun add citty
Import: import { defineCommand, runMain, runCommand, createMain, parseArgs, renderUsage, showUsage } from "citty"
defineCommand
The primary API. Defines a command with metadata, arguments, subcommands, and lifecycle hooks.
import { defineCommand, runMain } from "citty"
const main = defineCommand({
meta: {
name: "mycli",
version: "0.1.0",
description: "Install agent skills from any git host",
},
args: {
verbose: {
type: "boolean",
description: "Enable verbose output",
alias: "v",
},
},
subCommands: {
install: () => import("./commands/install").then(m => m.default),
remove: () => import("./commands/remove").then(m => m.default),
list: () => import("./commands/list").then(m => m.default),
// Nested subcommands via inline define
tap: defineCommand({
meta: { name: "tap", description: "Manage taps" },
subCommands: {
add: () => import("./commands/tap/add").then(m => m.default),
remove: () => import("./commands/tap/remove").then(m => m.default),
},
}),
},
run({ args }) {
// Runs if no subcommand matched
showUsage(this)
},
})
runMain(main)
Argument Definitions
Each key in args defines an argument or option. Properties:
| Property | Type | Description |
|---|---|---|
type |
"positional" | "boolean" |
Positional arg or boolean flag. Omit for string options. |
description |
string |
Help text shown in --help |
alias |
string | string[] |
Short alias(es), e.g. "v" for -v |
default |
string | boolean |
Default value |
required |
boolean |
Whether the argument is required |
valueHint |
string |
Placeholder in help, e.g. "file" shows --output <file> |
Argument types
Positional — matched by order of definition:
args: {
source: {
type: "positional",
description: "Git URL, tap name, or local path",
required: true,
},
}
// Usage: mycli install https://example.com/repo
// args.source === "https://example.com/repo"
Boolean flags — true when present, false when absent:
args: {
project: {
type: "boolean",
description: "Install to project scope",
default: false,
},
yes: {
type: "boolean",
alias: "y",
description: "Auto-accept prompts",
},
}
// Usage: mycli install foo --project -y
// args.project === true, args.yes === true
String options — omit type (default behavior):
args: {
also: {
description: "Also symlink to agent directory",
alias: "a",
valueHint: "agent",
},
ref: {
description: "Branch or tag to install",
valueHint: "ref",
},
}
// Usage: mycli install foo --also claude-code --ref v1.2.0
// args.also === "claude-code", args.ref === "v1.2.0"
Parsing behavior
- Kebab-to-camel:
--dry-run→args.dryRun - Negation:
--no-color→args.color === false(for booleans withdefault: true) - Equals syntax:
--output=result.jsonworks - Rest args:
args._contains ALL positional args (including the first one captured as a named positional). Do NOT use[args.source, ...args._]— that duplicates the first positional. Useargs._ as string[]directly to get all values.
Subcommands
Subcommands can be static objects, lazy functions, or async imports:
subCommands: {
// Static
list: listCommand,
// Lazy (loaded only when invoked)
install: () => installCommand,
// Async import (code-split)
update: () => import("./commands/update").then(m => m.default),
// Nested (subcommands can have their own subcommands)
config: defineCommand({
meta: { name: "config" },
subCommands: {
"agent-mode": agentModeCommand,
},
}),
}
Lifecycle Hooks
Three hooks run in order: setup → run → cleanup.
defineCommand({
args: { /* ... */ },
async setup({ args }) {
// Pre-processing, validation, initialization
// Runs before run() or subcommand dispatch
},
async run({ args }) {
// Main command logic
// Only runs if no subcommand was matched
},
async cleanup({ args }) {
// Runs after run() completes (even on error)
// Close connections, clean temp files, etc.
},
})
The CommandContext passed to each hook:
interface CommandContext<T> {
rawArgs: string[] // Raw argv array
args: ParsedArgs<T> // Typed parsed arguments
cmd: CommandDef<T> // The command definition
subCommand?: CommandDef // Matched subcommand (if any)
}
runMain(command, options?)
Entry point for CLI apps. Handles:
- Parsing
process.argv --help/-hauto-handling (prints usage, exits)--version/-Vauto-handling (prints version, exits)- Subcommand dispatch
- Error handling with formatted output +
process.exit(1)
runMain(main)
// Custom argv:
runMain(main, { rawArgs: ["install", "foo", "--project"] })
runCommand(command, options)
Lower-level — runs a command without process.exit behavior. Good for programmatic use and testing.
await runCommand(installCommand, {
rawArgs: ["https://example.com/repo", "--project"],
})
Other Exports
| Function | Purpose |
|---|---|
createMain(cmd) |
Returns (rawArgs?) => Promise<void> wrapper around runMain |
parseArgs(rawArgs, argsDef) |
Low-level arg parser. Returns typed ParsedArgs |
renderUsage(cmd) |
Returns formatted usage/help string |
showUsage(cmd) |
Prints usage to stdout |
Type Inference
citty infers parsed arg types from definitions:
const cmd = defineCommand({
args: {
name: { type: "positional", required: true },
count: { default: "5" }, // string (has default)
verbose: { type: "boolean" }, // boolean
},
run({ args }) {
args.name // string
args.count // string
args.verbose // boolean
},
})
Built-in Flags (automatic)
| Flag | Alias | Behavior |
|---|---|---|
--help |
-h |
Prints formatted usage, exits 0 |
--version |
-V |
Prints meta.version, exits 0 |
Pattern: mycli Command Structure
This is the pattern used in this project for packages/cli/src/commands/:
// packages/cli/src/commands/install.ts
import { defineCommand } from "citty"
import { installSkill } from "@mycli/core"
export default defineCommand({
meta: {
name: "install",
description: "Install a skill from a URL, tap name, or local path",
},
args: {
source: {
type: "positional",
description: "Git URL, github:owner/repo, tap skill name, or local path",
required: true,
},
project: {
type: "boolean",
description: "Install to .agents/skills/ in current project",
default: false,
},
also: {
description: "Create symlink in agent-specific directory",
valueHint: "agent",
},
ref: {
description: "Branch or tag to install",
valueHint: "ref",
},
"skip-scan": {
type: "boolean",
description: "Skip security scanning",
default: false,
},
yes: {
type: "boolean",
alias: "y",
description: "Auto-accept prompts",
default: false,
},
strict: {
type: "boolean",
description: "Abort on any security warning",
},
semantic: {
type: "boolean",
description: "Force semantic scan",
default: false,
},
},
async run({ args }) {
// Command implementation
},
})