cac vs meow vs arg: Lightweight CLI Argument Parsers for Node.js (2026)
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
| Feature | cac | meow | arg |
|---|---|---|---|
| Sub-commands | ✅ | ❌ | ❌ |
| Auto help | ✅ | ✅ (string) | ❌ (manual) |
| Auto version | ✅ | ✅ | ❌ |
| Flag types | ✅ | ✅ | ✅ |
| Array flags | ✅ | ✅ | ✅ |
| Required flags | ❌ | ✅ | ❌ |
| Custom types | ❌ | ❌ | ✅ |
| Strict mode | ❌ | ✅ | ✅ |
| Dependencies | 0 | Few | 0 |
| Size | ~3 KB | ~5 KB | ~2 KB |
| Used by | Vite, Vitest | Sindre tools | Vercel, 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.