Skip to main content

citty vs caporal vs vorpal: Modern CLI Frameworks for Node.js (2026)

·PkgPulse Team

TL;DR

citty is the modern CLI framework from the UnJS ecosystem — lightweight, TypeScript-first, composable sub-commands, zero dependencies. caporal is a batteries-included CLI framework — validation, auto-generated help, shell completions, and colored output built-in. vorpal is an interactive CLI framework — builds REPL-style command-line apps where users type commands inside your running program. In 2026: citty for modern TypeScript CLIs, caporal for feature-rich traditional CLIs, vorpal for interactive terminal applications (though vorpal is largely unmaintained).

Key Takeaways

  • citty: ~2M weekly downloads — UnJS ecosystem, zero deps, TypeScript-native, composable
  • caporal: ~100K weekly downloads — batteries-included, validation, completions, colorful help
  • vorpal: ~50K weekly downloads — interactive REPL mode, in-app command execution (largely unmaintained)
  • citty is used by Nuxt CLI, unbuild, and other UnJS tools
  • For most new CLI projects in 2026: citty or commander/yargs — caporal and vorpal are legacy choices
  • Interactive prompts: combine citty with @clack/prompts instead of using vorpal

citty

citty — modern, minimal CLI framework:

Basic command

import { defineCommand, runMain } from "citty"

const main = defineCommand({
  meta: {
    name: "pkgpulse",
    version: "1.0.0",
    description: "Package health analyzer",
  },
  args: {
    package: {
      type: "positional",
      description: "Package name to analyze",
      required: true,
    },
    format: {
      type: "string",
      description: "Output format",
      default: "table",
    },
    verbose: {
      type: "boolean",
      description: "Enable verbose output",
      alias: "v",
    },
  },
  run({ args }) {
    console.log(`Analyzing ${args.package}...`)
    console.log(`Format: ${args.format}`)
    if (args.verbose) console.log("Verbose mode enabled")
  },
})

runMain(main)
# Usage:
pkgpulse react --format json -v
# Analyzing react...
# Format: json
# Verbose mode enabled

Sub-commands

import { defineCommand, runMain } from "citty"

const analyze = defineCommand({
  meta: { name: "analyze", description: "Analyze a package" },
  args: {
    package: { type: "positional", required: true },
    depth: { type: "string", default: "full" },
  },
  run({ args }) {
    console.log(`Analyzing ${args.package} (depth: ${args.depth})`)
  },
})

const compare = defineCommand({
  meta: { name: "compare", description: "Compare two packages" },
  args: {
    packages: { type: "positional", required: true },
  },
  run({ args }) {
    const [a, b] = args.packages.split(",")
    console.log(`Comparing ${a} vs ${b}`)
  },
})

const main = defineCommand({
  meta: { name: "pkgpulse", version: "1.0.0" },
  subCommands: {
    analyze,
    compare,
  },
})

runMain(main)
pkgpulse analyze react --depth shallow
pkgpulse compare "react,vue"
pkgpulse --help  # Auto-generated help with sub-commands
citty design principles:
  ✅ Zero dependencies — tiny install size
  ✅ TypeScript-first — args are fully typed
  ✅ Composable — sub-commands are just defineCommand()
  ✅ Auto-generated help — --help works out of the box
  ✅ Used by UnJS ecosystem (Nuxt CLI, unbuild, nitro)

citty vs commander/yargs:
  - Simpler API — defineCommand + runMain
  - Better TypeScript types — args inferred from definition
  - Smaller — zero deps vs commander's 0 deps (similar) vs yargs' many deps
  - Less mature — fewer community examples and plugins

caporal

caporal — batteries-included CLI:

Setup

npm install caporal

Basic command

const prog = require("caporal")

prog
  .version("1.0.0")
  .description("Package health analyzer")

  // Default command:
  .argument("<package>", "Package name to analyze")
  .option("--format <format>", "Output format", ["json", "table", "csv"], "table")
  .option("-v, --verbose", "Enable verbose output")
  .action(({ args, options }) => {
    console.log(`Analyzing ${args.package}...`)
    console.log(`Format: ${options.format}`)
  })

prog.parse(process.argv)

Sub-commands with validation

const prog = require("caporal")

prog
  .version("1.0.0")

  // analyze sub-command:
  .command("analyze", "Analyze a package")
  .argument("<package>", "Package name", /^[@a-z]/)  // Regex validation
  .option("--depth <depth>", "Analysis depth", ["shallow", "full"], "full")
  .option("--timeout <ms>", "Timeout in milliseconds", prog.INT, 5000)
  .action(({ args, options, logger }) => {
    logger.info(`Analyzing ${args.package} (depth: ${options.depth})`)
  })

  // compare sub-command:
  .command("compare", "Compare two packages")
  .argument("<pkg1>", "First package")
  .argument("<pkg2>", "Second package")
  .option("--metric <metric>", "Comparison metric", ["downloads", "health", "all"], "all")
  .action(({ args, options, logger }) => {
    logger.info(`Comparing ${args.pkg1} vs ${args.pkg2}`)
  })

prog.parse(process.argv)

Built-in features

// caporal includes out of the box:
// ✅ Colored help output
// ✅ Argument type validation (INT, FLOAT, BOOL, LIST, REPEATABLE)
// ✅ Regex argument validation
// ✅ Shell auto-completion (bash, zsh, fish)
// ✅ Built-in logger with levels (debug, info, warn, error)
// ✅ Fuzzy command matching (typo correction)

// Shell completion setup:
// pkgpulse completion >> ~/.bashrc
// pkgpulse completion >> ~/.zshrc

Caporal limitations

In 2026:
  ❌ Last major release was 2020 — limited maintenance
  ❌ CJS-only — no native ESM support
  ❌ TypeScript types are @types/caporal (community)
  ❌ Superseded by modern alternatives (citty, commander v12, cleye)

Still useful for:
  ✅ Existing projects already using caporal
  ✅ Quick prototypes needing validation + help
  ✅ Projects that need built-in shell completions

vorpal

vorpal — interactive REPL CLI:

Interactive mode

const vorpal = require("vorpal")()

vorpal
  .command("analyze <package>", "Analyze a package")
  .option("--format <format>", "Output format")
  .action(function (args) {
    this.log(`Analyzing ${args.package}...`)
    // Simulate async work:
    return new Promise((resolve) => {
      setTimeout(() => {
        this.log(`Health score: 87/100`)
        resolve()
      }, 1000)
    })
  })

vorpal
  .command("compare <pkg1> <pkg2>", "Compare two packages")
  .action(function (args) {
    this.log(`${args.pkg1} vs ${args.pkg2}:`)
    this.log(`  Downloads: 5M vs 2M`)
    this.log(`  Health: 87 vs 72`)
  })

// Start interactive REPL:
vorpal
  .delimiter("pkgpulse$")
  .show()
# Running the CLI opens an interactive prompt:
$ node cli.js
pkgpulse$ analyze react --format json
Analyzing react...
Health score: 87/100
pkgpulse$ compare react vue
react vs vue:
  Downloads: 5M vs 2M
  Health: 87 vs 72
pkgpulse$ exit

Interactive prompts

vorpal
  .command("setup", "Configure pkgpulse")
  .action(function (args) {
    // Built-in prompts (like inquirer):
    return this.prompt([
      {
        type: "input",
        name: "apiKey",
        message: "Enter your API key: ",
      },
      {
        type: "list",
        name: "defaultFormat",
        message: "Default output format:",
        choices: ["table", "json", "csv"],
      },
      {
        type: "confirm",
        name: "analytics",
        message: "Enable analytics?",
        default: true,
      },
    ]).then((answers) => {
      this.log(`Saved config: ${JSON.stringify(answers)}`)
    })
  })

Vorpal status

In 2026:
  ❌ UNMAINTAINED — last commit ~2018
  ❌ No TypeScript types
  ❌ CJS only
  ❌ Security vulnerabilities in dependencies
  ❌ Node.js compatibility issues with newer versions

Modern alternatives for interactive CLIs:
  ✅ @clack/prompts + citty — modern interactive CLI
  ✅ Ink — React-based terminal UI
  ✅ enquirer + commander — prompts + parsing
  ✅ Node.js readline/promises — built-in REPL

Modern Alternative: citty + @clack/prompts

// Instead of vorpal, combine citty + @clack/prompts for interactive CLIs:
import { defineCommand, runMain } from "citty"
import * as p from "@clack/prompts"

const setup = defineCommand({
  meta: { name: "setup", description: "Configure pkgpulse" },
  async run() {
    p.intro("PkgPulse Setup")

    const config = await p.group({
      apiKey: () => p.text({ message: "API key:", validate: (v) => !v ? "Required" : undefined }),
      format: () => p.select({
        message: "Default format:",
        options: [
          { value: "table", label: "Table" },
          { value: "json", label: "JSON" },
        ],
      }),
      analytics: () => p.confirm({ message: "Enable analytics?" }),
    })

    p.outro(`Config saved! Format: ${config.format}`)
  },
})

const main = defineCommand({
  meta: { name: "pkgpulse", version: "1.0.0" },
  subCommands: { setup },
})

runMain(main)

Feature Comparison

Featurecittycaporalvorpal
TypeScript-first❌ (@types)
ESM support
Zero dependencies
Sub-commands
Auto-generated help✅ (colored)
Argument validationType-basedRegex + typesBasic
Shell completions
Interactive REPL
Built-in prompts
Actively maintained⚠️
Weekly downloads~2M~100K~50K

When to Use Each

Use citty if:

  • Building a modern TypeScript CLI tool
  • Want zero dependencies and small install footprint
  • Already in the UnJS ecosystem (Nuxt, unbuild, nitro)
  • Need composable sub-commands with typed arguments

Use caporal if:

  • Existing project already using caporal
  • Need built-in shell completion generation
  • Want argument validation with regex patterns
  • Don't need ESM or modern TypeScript support

Use vorpal if:

  • Building an interactive terminal application (REPL-style)
  • Legacy project — not recommended for new projects
  • Better alternative: citty + @clack/prompts or Ink for modern interactive CLIs

For new projects in 2026:

  • Simple CLI: citty or commander
  • Complex CLI with plugins: oclif
  • Interactive terminal UI: Ink or @clack/prompts + citty
  • Quick prototype: cleye (TypeScript flag inference)

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on citty v0.x, caporal v2.x, and vorpal v1.x.

Compare CLI frameworks and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.