mrkeyoor.com_
Thu 06 Aug 02:44 UTC
npmCLI & Toolingupdated 06 Aug 2026

meow

meow turns a Node script into a command-line tool in about ten lines. You hand it a block of help text and a flags object, and it parses process.argv, converts --kebab-case flags to camelCase keys, applies types and defaults, validates required flags and choices, negates flags written as --no-thing, wires up --help and --version from your package.json, and sets the process title. What comes back is a small object with input (the non-flag arguments), flags, command, pkg, and helpers like showHelp() and showVersion(). It deliberately does not build a subcommand framework for you: version 14.1 added a flat commands list that stops parsing at the first non-flag argument, and nesting deeper means calling meow again with the leftover arguments.

Verdict

For a one-verb CLI on modern Node, meow is the least ceremony you can get away with and the zero-dependency install is a genuine advantage in a tool people install globally. The moment you need real subcommands or want help text you do not maintain by hand, move to commander.

API stability4/5The meow() signature and flag schema have barely moved in years, and v13 and v14 were pure Node engine bumps. The two disruptive changes are behind you but worth knowing about: v10 went ESM-only and v12 renamed the alias flag option to shortFlag, so older snippets you find online will not run.
Docs4/5One README that documents every option with an example and calls out real caveats, such as isMultiple not accepting comma-separated values. There is no separate docs site, no recipe section, and no guidance on structuring a CLI larger than a single file.
Maintenance4/514.1.0 landed February 2026, the repo was pushed 21 July 2026, and there are 3 open issues total, which for a twelve-year-old package means it is finished rather than abandoned. It is a single-maintainer sindresorhus package, so feature requests outside his taste generally do not land.
Ecosystem4/5About 41M weekly downloads makes it one of the most installed CLI helpers on npm, largely because it sits inside other tools' dependency trees. There is no plugin ecosystem to speak of, and its 3.7k stars against commander's install base means fewer Stack Overflow answers when something is odd.

Use it if

  • You are writing a small single-purpose CLI on Node 20+ in ESM and want argv parsing, --help, and --version handled in a dozen lines
  • You want your CLI's dependency tree to stay empty: since v12.1 meow bundles its own parser, so installing it adds exactly one node_modules entry
  • You want basic validation without a schema library: type, default, choices, isRequired (which can be a function of the other flags), and isMultiple cover most flag rules
  • You want to hand-write the help output and control its exact wording and layout rather than accept whatever a framework generates
  • You need a couple of top-level verbs like run and list, which the commands option handles by parsing the verb and leaving the rest in input for you to route
Skip it if

Setup reality

npm install meow, add a #!/usr/bin/env node shebang, and point the bin field at your entry file. The two requirements that bite are structural rather than fiddly: the package is ESM-only with "type": "module" and engines node >=20, and importMeta: import.meta is a mandatory option because meow uses it to walk upwards and find your package.json for the version and description. That last part is the classic production failure. If you bundle the CLI with esbuild, ncc, or rollup, import.meta.url resolves inside your build output, meow finds the wrong package.json or none at all, and --version prints something unexpected; the fix is to pass the pkg option explicitly, which also stops the filesystem lookup. TypeScript users need "module" and "moduleResolution" set to node16 or later, otherwise the types will not resolve at all. Everything else is optional: types are bundled and there are no peer dependencies.

Patterns

A minimal CLI entry pointbasic-cli

#!/usr/bin/env node
import meow from 'meow';

const cli = meow(`
	Usage
	  $ resize <file>

	Options
	  --width, -w  Output width in pixels

	Examples
	  $ resize photo.png --width 800
`, {
	importMeta: import.meta,
	flags: {
		width: {type: 'number', shortFlag: 'w', default: 640},
	},
});

console.log(cli.input.at(0), cli.flags.width);

importMeta is required, not optional: meow uses it to locate your package.json for the version and description. The help string is reindented and trimmed, so a tab-indented template literal comes out looking correct.

Types, short flags, defaults, and choicesdefine-flags

flags: {
	format: {
		type: 'string',
		shortFlag: 'f',
		choices: ['json', 'yaml', 'table'],
		default: 'table',
	},
	dryRun: {type: 'boolean', default: false},
	retries: {type: 'number', default: 3},
}

Flag keys are camelCase but match kebab-case on the command line, so dryRun is passed as --dry-run and read as cli.flags.dryRun. shortFlag is the single-letter form; the separate aliases array is for longer synonyms. An invalid choice exits with a parse error rather than falling back to the default.

Require a flag, conditionallyrequired-flags

flags: {
	apiKey: {type: 'string', isRequired: true},
	region: {
		type: 'string',
		isRequired: (flags, input) => flags.deploy === true,
	},
}

isRequired accepts a function of the parsed flags and the non-flag input, which covers most 'this flag only matters with that flag' rules without a validation library. A missing required flag prints an error and exits before your code runs, so there is no hook to customise the message.

Accept a flag more than oncerepeatable-flags

flags: {
	include: {type: 'string', shortFlag: 'i', isMultiple: true},
}

// $ mytool -i src -i tests
// cli.flags.include === ['src', 'tests']

Values must be supplied by repeating the flag. Comma-separated and space-separated lists are explicitly not supported (upstream issue 164), so --include src,tests gives you the single string 'src,tests' and you split it yourself. With isMultiple the default value is an empty array when the flag is absent.

Support --no-somethingnegated-flags

const cli = meow(`
	Options
	  --no-color  Disable coloured output
`, {
	importMeta: import.meta,
	flags: {
		color: {type: 'boolean', default: true},
	},
});

// $ mytool --no-color  ->  cli.flags.color === false

Declare the positive flag; the --no- prefix is handled for you. Do not also declare a noColor flag, because both would then appear in flags and the two states can disagree.

Route a flat set of commandssubcommands

const cli = meow({
	importMeta: import.meta,
	commands: ['run', 'list'],
	flags: {verbose: {type: 'boolean', shortFlag: 'v'}},
});

if (!cli.command) {
	cli.showHelp(0);
}

if (cli.command === 'run') {
	const runCli = meow({importMeta: import.meta, argv: cli.input});
	// parse the subcommand's own flags here
}

Added in 14.1.0. Parsing stops at the first non-flag argument, so parent flags must appear before the command: mytool -v run works, mytool run -v leaves -v in input for the child parser. An unknown command prints help and exits 2; no command at all leaves cli.command undefined and meow does nothing, so the check above is on you.

Reject unknown flagsstrict-flags

const cli = meow(helpText, {
	importMeta: import.meta,
	allowUnknownFlags: false,
	flags: {
		verbose: {type: 'boolean', shortFlag: 'v'},
	},
});

The default is true, which means a typo such as --verbsoe is quietly accepted and dropped. Turning this on is almost always what you want; camelCase-declared flags still accept both --camelCase and --camel-case in strict mode.

Require and type the positional argumentsrequire-input

const cli = meow(helpText, {
	importMeta: import.meta,
	input: {
		type: 'string-array',
		isRequired: true,
	},
});

for (const file of cli.input) {
	process(file);
}

input covers the non-flag arguments and accepts string, boolean, number, array, and the typed array forms. isRequired only checks that at least one argument is present, so 'exactly two arguments' is still a manual check.

Get numbers instead of stringsnumber-flags

// explicit, and what you should do:
flags: {port: {type: 'number', default: 3000}}

// blanket coercion for everything, including positional input:
const cli = meow(helpText, {importMeta: import.meta, inferType: true});

Without a declared type, --port 3000 arrives as the string '3000' and the positional argument 5 arrives as '5'. inferType: true flips that globally, but it also turns a version-looking argument or a zero-padded id into something you did not intend, so per-flag types are the safer choice.

Tell 'not passed' apart from 'passed as false'tri-state-booleans

const cli = meow(helpText, {
	importMeta: import.meta,
	booleanDefault: undefined,
	flags: {
		cache: {type: 'boolean'},
	},
});

const useCache = cli.flags.cache ?? configFile.cache ?? true;

booleanDefault is false by default, which makes an omitted boolean indistinguishable from --no-cache and quietly overrides config file values. Setting it to undefined excludes unpassed booleans from flags entirely, which is what you want when the CLI layers over a config file. A per-flag default still wins over booleanDefault.

Show help or version yourselfhelp-and-exit

if (cli.input.length === 0) {
	cli.showHelp(); // prints help and exits with code 2
}

if (someCondition) {
	cli.showHelp(0); // treat it as success
}

cli.showVersion();

showHelp defaults to exit code 2, which is correct for a usage error but wrong when the user asked for help on purpose, so pass 0 in that case. Automatic --help and --version only fire when that flag is the only argument, which is why a wrapper CLI passing arguments through can set autoHelp and autoVersion to false.

Parse a fixed argv in teststest-a-cli

import meow from 'meow';
import {readFileSync} from 'node:fs';

function parse(argv) {
	return meow(helpText, {
		importMeta: import.meta,
		argv,
		pkg: JSON.parse(readFileSync('./package.json', 'utf8')),
		flags: {width: {type: 'number', default: 640}},
	});
}

const cli = parse(['image.png', '--width', '800']);

argv replaces process.argv.slice(2) so tests never touch global state. Passing pkg explicitly stops meow searching the filesystem for a package.json, which is also the fix when your CLI is bundled by esbuild or ncc and import.meta.url no longer points at your source tree.

Alternatives

PackageRegistryPick it when
commandernpmNested subcommands, per-command options, and help text generated from your declarations
yargsnpmBigger tools that want command modules, middleware, config file merging, and shell completion built in
cacnpmSimilarly small and fast, but it does generate help from your option definitions and supports subcommands