citty

star 5

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.

nklisch By nklisch schedule Updated 5/31/2026

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 flagstrue 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-runargs.dryRun
  • Negation: --no-colorargs.color === false (for booleans with default: true)
  • Equals syntax: --output=result.json works
  • 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. Use args._ 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: setupruncleanup.

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 / -h auto-handling (prints usage, exits)
  • --version / -V auto-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
  },
})
Install via CLI
npx skills add https://github.com/nklisch/skills --skill citty
Repository Details
star Stars 5
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator