flags-builder

Use when building or extending a CLI tool that reads process.argv. Triggers for: defining --flags or -f aliases, parsing boolean/string/number/repeated flags, supporting subcommands with independent flag sets, adding defaults or required validation to CLI inputs, or any mention of 'parse argv', 'command-line flags', or argument parsing in a Node.js/TypeScript context.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "flags-builder" with this command: npx skills add jondotsoy/flags/jondotsoy-flags-flags-builder

@jondotsoy/flags

A zero-dependency, type-safe CLI argument parser with a fluent builder API.

Overview

What this skill is for

Use this skill whenever the task involves parsing command-line arguments in a Node.js or TypeScript program. It covers the full lifecycle: reading process.argv, defining typed flags, handling subcommands, and validating inputs.

When to use it

Trigger this skill when the user's request matches one of these scenarios:

Building a CLI tool from scratch

"I want to create a CLI that accepts --port and --verbose" "Make a script that reads arguments from the terminal"

Adding flags to an existing script

"Add a --dry-run flag to my deploy script" "Support --output=dist in my build tool"

Parsing subcommands

"My CLI needs serve and build commands, each with their own flags" "How do I parse mycli deploy --env production?"

Handling repeated or typed inputs

"I need --tag to be repeatable" "Validate that --port is a number and defaults to 3000"

User phrases that signal this skill

  • "parse argv", "read CLI args", "command-line flags"
  • "how do I get --flag from process.argv"
  • mentions of --flag, -f, subcommands, or argument parsing in a Node.js/TypeScript context

When NOT to use it

  • The project already uses another parser (e.g. yargs, commander, minimist) — prefer extending the existing setup
  • Arguments come from a config file, environment variables, or an HTTP request — this library is specifically for argv

Getting Started

import { flags, flag, command } from "@jondotsoy/flags";

const args = process.argv.slice(2); // remove "node" and script path

const parser = flags({ ... });
const [ok, error, options] = parser.safeParse(args);
if (!ok) { console.error(parser.formatError(error)); process.exit(1); }

Flag Types

All input styles are supported automatically — no extra config needed:

const parser = flags({
  verbose: flag("--verbose", "-v").boolean(), // --verbose
  version: flag("--version").number(),        // --version 1.2
  name:    flag("--name", "-n").string(),     // --name foo  OR  --name=foo
});

const [ok, error, output] = parser.safeParse(args);
if (!ok) { console.error(parser.formatError(error)); process.exit(1); }

const { verbose, version, name } = output;

.boolean()

Presence of the flag sets the value to true. Default: false.

flag("--verbose", "-v").boolean();
// --verbose       → true
// (absent)        → false

.string()

Reads the next token or the value after =. Returns string | null (null if absent).

flag("--name", "-n").string();
// --name foo      → "foo"
// --name=foo      → "foo"
// (absent)        → null

.number()

Like .string() but coerces the value to a number. Returns number | null.

flag("--port", "-p").number();
// --port 3000     → 3000
// --port=3000     → 3000
// (absent)        → null

.strings()

Accumulates repeated flags into an array. Returns string[] (always an array, never null).

flag("--tag").strings();
// --tag a --tag b --tag c   → ["a", "b", "c"]
// (absent)                  → []

.keyValue()

Reads key=value pairs and merges them into a record. Returns Record<string, string> | null.

flag("--env").keyValue();
// --env NODE_ENV=production --env PORT=3000   → { NODE_ENV: "production", PORT: "3000" }
// (absent)                                    → null

Modifiers

Chain modifiers after the type method:

flag("--port", "-p").number().default(3000);   // default value
flag("--output").string().required();           // throw if missing
flag("--tag").strings();                        // accumulate: --tag a --tag b → ["a", "b"]
flag("--count").number().positive();            // must be > 0
flag("--port").number().describe("HTTP port"); // help text

Subcommands — Multi-Parser Pattern

Use command().restArgs() to capture a subcommand's raw arguments, then run a second .safeParse() on them:

import { flags, flag, command } from "@jondotsoy/flags";

// Level 1: identify which subcommand was used
const mainParser = flags({
  serve: command("serve").restArgs(),
});

const [ok, error, output] = mainParser.safeParse(process.argv.slice(2));
if (!ok) { console.error(mainParser.formatError(error)); process.exit(1); }

// Level 2: parse the subcommand's own flags
if (output.serve) {
  const serveParser = flags({
    port: flag("--port", "-p").number().default(3000),
  });

  const [ok2, error2, serveOutput] = serveParser.safeParse(output.serve);
  if (!ok2) { console.error(serveParser.formatError(error2)); process.exit(1); }

  startServer(serveOutput.port);
}

This pattern scales to any number of subcommands:

const mainParser = flags({
  build:  command("build").restArgs(),
  deploy: command("deploy").restArgs(),
});

const [ok, error, output] = mainParser.safeParse(process.argv.slice(2));
if (!ok) { console.error(mainParser.formatError(error)); process.exit(1); }

if (output.build) {
  const buildParser = flags({ outDir: flag("--out").string().default("dist") });
  const [ok2, error2, buildOutput] = buildParser.safeParse(output.build);
  if (!ok2) { console.error(buildParser.formatError(error2)); process.exit(1); }
}

if (output.deploy) {
  const deployParser = flags({ env: flag("--env").string().required() });
  const [ok2, error2, deployOutput] = deployParser.safeParse(output.deploy);
  if (!ok2) { console.error(deployParser.formatError(error2)); process.exit(1); }
}

Recommended File Structure (Large Projects)

This is a recommendation, not a requirement. Always ask the user before applying this structure — it may not fit every project.

As a program grows, putting all parsers in a single file becomes hard to maintain. The recommended approach is to split each parser into its own file under src/args/, mirroring the command hierarchy.

src/args/
  main.ts           ← top-level parser (subcommands)
  serve.ts          ← flags for `serve` subcommand
  containers/
    main.ts         ← flags for `containers` subcommand
    pull.ts         ← flags for `containers pull`
    push.ts         ← flags for `containers push`

Each file exports a named parser constant. The name should be descriptive of the command it handles:

// src/args/main.ts
import { flags, command } from "@jondotsoy/flags";

export const mainParserArgs = flags({
  serve:      command("serve").restArgs(),
  containers: command("containers").restArgs(),
});
// src/args/serve.ts
import { flags, flag } from "@jondotsoy/flags";

export const serveParserArgs = flags({
  port: flag("--port", "-p").number().default(3000),
  host: flag("--host").string().default("localhost"),
});
// src/args/containers/main.ts
import { flags, command } from "@jondotsoy/flags";

export const containersParserArgs = flags({
  pull: command("pull").restArgs(),
  push: command("push").restArgs(),
});

Consuming parsers in the entry point:

import { mainParserArgs } from "./args/main.js";
import { serveParserArgs } from "./args/serve.js";
import { containersParserArgs } from "./args/containers/main.js";

const [ok, error, output] = mainParserArgs.safeParse(process.argv.slice(2));
if (!ok) { console.error(mainParserArgs.formatError(error)); process.exit(1); }

if (output.serve) {
  const [ok2, error2, serveOutput] = serveParserArgs.safeParse(output.serve);
  if (!ok2) { console.error(serveParserArgs.formatError(error2)); process.exit(1); }
}

if (output.containers) {
  const [ok2, error2, containersOutput] = containersParserArgs.safeParse(output.containers);
  if (!ok2) { console.error(containersParserArgs.formatError(error2)); process.exit(1); }
}

When to suggest this structure:

  • The program has 3 or more subcommands
  • Each subcommand has its own set of flags
  • The codebase is expected to grow or be maintained long-term

When to keep it simple (single file):

  • Small scripts with 1–2 flags and no subcommands
  • Throwaway or single-use tools

Help Message

Call .helpMessage() on any parser to get a formatted help string. Use .program(), .describe(), and .version() to populate it, and add .describe() to individual flags for per-flag documentation.

const parser = flags({
  port:    flag("--port", "-p").number().default(3000).describe("Port to listen on"),
  verbose: flag("--verbose", "-v").boolean().describe("Enable verbose output"),
  output:  flag("--output", "-o").string().required().describe("Output directory"),
})
  .program("mycli")
  .describe("A simple CLI tool")
  .version("1.0.0");

console.log(parser.helpMessage());

Typical output:

mycli 1.0.0

A simple CLI tool

Options:
  --port, -p      Port to listen on (default: 3000)
  --verbose, -v   Enable verbose output
  --output, -o    Output directory (required)

Common pattern — print help on --help

const [ok, error, output] = parser.safeParse(process.argv.slice(2));
if (!ok) { console.error(parser.formatError(error)); process.exit(1); }

if (output.help) {
  console.log(parser.helpMessage());
  process.exit(0);
}

With the file structure pattern

Each sub-parser can expose its own help message independently:

// src/args/serve.ts
export const serveParserArgs = flags({
  port: flag("--port", "-p").number().default(3000).describe("Port to listen on"),
  host: flag("--host").string().default("localhost").describe("Host to bind"),
})
  .program("mycli serve")
  .describe("Start the development server");
if (output.serve) {
  const [ok2, error2, serveOutput] = serveParserArgs.safeParse(output.serve);
  if (!ok2) { console.error(serveParserArgs.formatError(error2)); process.exit(1); }
}

Safe Parse

Prefer .safeParse() over .parse() — it makes error handling explicit and avoids unexpected exceptions propagating through the program.

.safeParse() returns a tuple [ok, error, output]. When ok is false, output is undefined and error contains the caught value. When ok is true, output is the fully typed result.

const [ok, error, output] = parser.safeParse(process.argv.slice(2));

if (!ok) {
  console.error(parser.formatError(error));
  process.exit(1);
}

const port = output.port; // fully typed, no undefined check needed

Tuple shape:

PositionNameValue when okValue when error
[0]oktruefalse
[1]errorundefinedthe caught error (unknown)
[2]outputparsed result (fully typed)undefined

When to use .parse() instead

Use .parse() only when you intentionally want to skip error handling — for example in quick scripts or tests where an unhandled exception is acceptable:

// ok for tests or throwaway scripts
const output = parser.parse(["--port", "3000"]);

Never use .parse() in production CLI entry points — prefer .safeParse() there.

Error Handling

Use parser.formatError(error) to get a formatted message that includes the error and a hint to run --help. For advanced cases, the error value can also be narrowed with the exported error classes:

import { UnexpectedArgumentError, RequiredFlagMissingError } from "@jondotsoy/flags";

const [ok, error, output] = parser.safeParse(process.argv.slice(2));

if (!ok) {
  console.error(parser.formatError(error));
  process.exit(1);
}

Narrowing for specific messages:

if (!ok) {
  if (error instanceof UnexpectedArgumentError) console.error("Unknown argument:", error.message);
  else if (error instanceof RequiredFlagMissingError) console.error("Missing required flag:", error.message);
  else console.error(parser.formatError(error));
  process.exit(1);
}

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

flags

No summary provided by upstream source.

Repository SourceNeeds Review
General

flags

No summary provided by upstream source.

Repository SourceNeeds Review
200-vercel
General

flags-sdk

No summary provided by upstream source.

Repository SourceNeeds Review
177-vercel
General

runbook-executor

No summary provided by upstream source.

Repository SourceNeeds Review