citty
Scannednpx machina-cli add skill nklisch/skilltap/citty --openclawcitty — 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: "skilltap",
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: skilltap 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: skilltap 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: skilltap 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: Unmatched args go to
args._array
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: skilltap 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 "@skilltap/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
},
})
Source
git clone https://github.com/nklisch/skilltap/blob/main/.agents/skills/citty/SKILL.mdView on GitHub Overview
citty is a lightweight, TypeScript-first CLI builder from the UnJS ecosystem. It uses native Node.js util.parseArgs with zero dependencies and exposes defineCommand, runMain, argument definitions, subcommands, and lifecycle hooks to model robust CLI tools.
How This Skill Works
Use defineCommand to declare a command with metadata, arguments, and subcommands. Subcommands can be static, lazy-loaded, or dynamically imported, enabling code-splitting. The framework handles parsing (including kebab-to-camel, negation, and rest args), and runMain executes the command tree, with showUsage invoked when no subcommand matches.
When to Use It
- You're building a CLI with a command-and-subcommand structure (e.g., install, remove, list).
- You want lazy-loading of command implementations to reduce startup time and bundle size.
- You prefer zero-dependency tooling and TypeScript-first ergonomics.
- You need strong, typed argument definitions (positional, boolean flags, or string options).
- You want built-in usage rendering and lifecycle hooks to manage command flow.
Quick Start
- Step 1: Install citty (e.g., bun add citty).
- Step 2: Create a command with defineCommand, including meta, args, and subCommands.
- Step 3: Run with runMain(main) and showUsage(this) when no subcommand matches.
Best Practices
- Define comprehensive meta (name, version, description) to power helpful output.
- Use defineCommand for each command, including nested subcommands for modularity.
- Leverage lazy imports (or inline define) for rarely-used subcommands to improve startup time.
- Explicitly define args with correct types, aliases, defaults, and value hints for clear help.
- Test kebab-to-camel parsing, boolean negation, and rest args to ensure predictable CLI behavior.
Example Use Cases
- Main command with meta, args, and subCommands: install, remove, list (as shown in the citty example).
- Nested subcommand via inline define: tap with add and remove subcommands.
- Boolean flag example: verbose with alias, and project flag with a default.
- Positional argument example: source as a required positional for CLI usage.
- Usage path: defineCommand, then runMain(main) and showUsage(this) when no subcommand matches.