Get the FREE Ultimate OpenClaw Setup Guide →

citty

Scanned
npx machina-cli add skill nklisch/skilltap/citty --openclaw
Files (1)
SKILL.md
7.8 KB

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: "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:

PropertyTypeDescription
type"positional" | "boolean"Positional arg or boolean flag. Omit for string options.
descriptionstringHelp text shown in --help
aliasstring | string[]Short alias(es), e.g. "v" for -v
defaultstring | booleanDefault value
requiredbooleanWhether the argument is required
valueHintstringPlaceholder 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 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: 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-runargs.dryRun
  • Negation: --no-colorargs.color === false (for booleans with default: true)
  • Equals syntax: --output=result.json works
  • 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: 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

FunctionPurpose
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)

FlagAliasBehavior
--help-hPrints formatted usage, exits 0
--version-VPrints 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

  1. Step 1: Install citty (e.g., bun add citty).
  2. Step 2: Create a command with defineCommand, including meta, args, and subCommands.
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers