Skip to main content

cac vs meow vs arg: Lightweight CLI Argument Parsers for Node.js (2026)

·PkgPulse Team

TL;DR

cac is the lightweight CLI framework — sub-commands, options, help generation, used by Vite and Vitest internally. meow is the CLI helper from Sindre Sorhus — parses flags, auto-generates help from a string template, handles common patterns cleanly. arg is the zero-dependency argument parser — type coercion, aliasing, strict mode, nothing more. In 2026: cac for Vite-ecosystem tools needing sub-commands, meow for simple CLIs with pretty help output, arg for the most minimal argument parsing.

Key Takeaways

  • cac: ~10M weekly downloads — used by Vite, Vitest, sub-commands + options, ~3 KB
  • meow: ~8M weekly downloads — Sindre Sorhus, string-based help, flag parsing, ~5 KB
  • arg: ~10M weekly downloads — zero deps, type coercion, strict parsing, ~2 KB
  • All three are lightweight alternatives to commander (~60M) and yargs (~80M)
  • cac is the only one with sub-command support
  • arg is the most minimal — just parsing, no help generation

arg

arg — minimal argument parser:

Basic usage

import arg from "arg"

const args = arg({
  // Type coercion:
  "--port": Number,
  "--host": String,
  "--verbose": Boolean,
  "--config": String,

  // Aliases:
  "-p": "--port",
  "-h": "--host",
  "-v": "--verbose",
  "-c": "--config",
})

// npx my-cli --port 3000 --verbose src/index.ts
console.log(args)
// {
//   "--port": 3000,
//   "--verbose": true,
//   _: ["src/index.ts"]    // Positional arguments
// }

Strict mode

import arg from "arg"

// Strict mode — throws on unknown flags:
const args = arg({
  "--port": Number,
  "--verbose": Boolean,
}, {
  permissive: false,  // Default: throws on unknown args
})

// npx my-cli --unknown-flag
// ❌ Error: Unknown or unexpected option: --unknown-flag

// Permissive mode — collects unknown args:
const args2 = arg({
  "--port": Number,
}, {
  permissive: true,
})
// Unknown flags end up in args._

Array and custom types

import arg from "arg"

// Array (collect multiple values):
const args = arg({
  "--file": [String],       // Collects: --file a.ts --file b.ts
  "--exclude": [String],
  "--port": Number,
})
// args["--file"] → ["a.ts", "b.ts"]

// Custom type coercion:
function parseDate(value: string): Date {
  const date = new Date(value)
  if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`)
  return date
}

const args2 = arg({
  "--since": parseDate,
  "--count": Number,
})
// npx my-cli --since 2026-01-01
// args2["--since"] → Date object

Real-world example

import arg from "arg"

function parseArgs() {
  const args = arg({
    "--help": Boolean,
    "--version": Boolean,
    "--format": String,
    "--output": String,
    "--verbose": Boolean,
    "--limit": Number,

    "-h": "--help",
    "-V": "--version",
    "-f": "--format",
    "-o": "--output",
    "-v": "--verbose",
    "-n": "--limit",
  })

  if (args["--help"]) {
    console.log(`
  Usage: pkgpulse [options] <package>

  Options:
    -f, --format   Output format (json, table, csv)
    -o, --output   Output file path
    -n, --limit    Max results (default: 10)
    -v, --verbose  Verbose output
    -h, --help     Show help
    -V, --version  Show version
    `)
    process.exit(0)
  }

  return {
    packages: args._,
    format: args["--format"] ?? "table",
    output: args["--output"],
    verbose: args["--verbose"] ?? false,
    limit: args["--limit"] ?? 10,
  }
}

cac

cac — lightweight CLI framework:

Basic usage

import cac from "cac"

const cli = cac("pkgpulse")

cli
  .option("--format <format>", "Output format", { default: "table" })
  .option("--verbose", "Verbose output")
  .option("--limit <count>", "Max results", { default: 10 })

cli
  .command("<package>", "Analyze a package")
  .action((package_, options) => {
    console.log(`Analyzing ${package_}...`)
    console.log(`Format: ${options.format}`)
    console.log(`Limit: ${options.limit}`)
  })

cli.help()
cli.version("1.0.0")
cli.parse()

Sub-commands

import cac from "cac"

const cli = cac("pkgpulse")

// Analyze command:
cli
  .command("analyze <package>", "Analyze package health")
  .option("--depth <depth>", "Analysis depth", { default: "full" })
  .option("--format <format>", "Output format", { default: "table" })
  .action((package_, options) => {
    console.log(`Analyzing ${package_} (${options.depth})`)
  })

// Compare command:
cli
  .command("compare <pkg1> <pkg2>", "Compare two packages")
  .option("--metric <metric>", "Comparison metric", { default: "all" })
  .action((pkg1, pkg2, options) => {
    console.log(`Comparing ${pkg1} vs ${pkg2} (${options.metric})`)
  })

// Search command:
cli
  .command("search <query>", "Search packages")
  .option("--limit <count>", "Max results", { default: 10 })
  .action((query, options) => {
    console.log(`Searching: ${query} (limit: ${options.limit})`)
  })

// Default command (no sub-command):
cli
  .command("", "Show help")
  .action(() => cli.outputHelp())

cli.help()
cli.version("1.0.0")
cli.parse()
pkgpulse analyze react --format json
pkgpulse compare react vue --metric downloads
pkgpulse search "state management" --limit 5
pkgpulse --help

Why Vite uses cac

Vite CLI:
  vite              → start dev server
  vite build        → production build
  vite preview      → preview build
  vite optimize     → pre-bundle deps

cac provides:
  ✅ Sub-commands (build, preview, optimize)
  ✅ Auto-generated --help
  ✅ Type coercion for options
  ✅ Tiny (~3 KB) — keeps Vite install small
  ✅ No dependencies

vs commander/yargs:
  commander ~7 KB, yargs ~30 KB + deps
  cac has everything Vite needs in 3 KB

meow

meow — CLI helper:

Basic usage

import meow from "meow"

const cli = meow(`
  Usage
    $ pkgpulse <package>

  Options
    --format, -f   Output format (json, table, csv)
    --limit, -n    Max results (default: 10)
    --verbose, -v  Verbose output

  Examples
    $ pkgpulse react
    $ pkgpulse react --format json
    $ pkgpulse react vue --limit 5
`, {
  importMeta: import.meta,
  flags: {
    format: {
      type: "string",
      shortFlag: "f",
      default: "table",
    },
    limit: {
      type: "number",
      shortFlag: "n",
      default: 10,
    },
    verbose: {
      type: "boolean",
      shortFlag: "v",
      default: false,
    },
  },
})

// cli.input → positional args: ["react"] or ["react", "vue"]
// cli.flags → parsed flags: { format: "table", limit: 10, verbose: false }

console.log(`Packages: ${cli.input.join(", ")}`)
console.log(`Format: ${cli.flags.format}`)

Flag types

import meow from "meow"

const cli = meow(`Usage: ...`, {
  importMeta: import.meta,
  flags: {
    name: {
      type: "string",
      isRequired: true,  // Error if missing
    },
    count: {
      type: "number",
      default: 1,
    },
    tags: {
      type: "string",
      isMultiple: true,  // Collects: --tags a --tags b
    },
    verbose: {
      type: "boolean",
      default: false,
    },
  },
})

// cli.flags.tags → ["a", "b"]

Auto features

// meow automatically handles:

// --help → prints the help string you provided
// --version → prints version from package.json
// Unknown flags → ignored (or error with allowUnknownFlags: false)
// camelCase conversion → --my-flag becomes cli.flags.myFlag

const cli = meow(`...`, {
  importMeta: import.meta,
  allowUnknownFlags: false,  // Strict mode
  autoHelp: true,             // --help (default)
  autoVersion: true,          // --version (default)
  flags: { /* ... */ },
})

Feature Comparison

Featurecacmeowarg
Sub-commands
Auto help✅ (string)❌ (manual)
Auto version
Flag types
Array flags
Required flags
Custom types
Strict mode
Dependencies0Few0
Size~3 KB~5 KB~2 KB
Used byVite, VitestSindre toolsVercel, Next.js
Weekly downloads~10M~8M~10M

When to Use Each

Use cac if:

  • Need sub-commands (like Vite: vite build, vite preview)
  • Want auto-generated help with minimal setup
  • Building Vite/Vitest ecosystem tools
  • Need the lightest option that still has sub-commands

Use meow if:

  • Want pretty help text from a string template
  • Building simple CLIs (no sub-commands needed)
  • Need required flag validation
  • In the Sindre Sorhus ecosystem

Use arg if:

  • Want the absolute minimum — just argument parsing
  • Need custom type coercion functions
  • Want strict mode to reject unknown flags
  • Building TypeScript-first tools (great type inference)
  • Don't need help generation (you'll write it yourself)

For larger CLIs:

  • commander — if you need extensive help formatting and options
  • yargs — if you need middleware, completion, and complex parsing
  • oclif — if you need plugins, hooks, and enterprise features
  • citty — if you want modern UnJS-style CLI definitions

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on cac v6.x, meow v13.x, and arg v5.x.

Compare CLI tooling and developer utilities on PkgPulse →

Comments

Stay Updated

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