skills/flags-builder/SKILL.md
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.
npx skillsauth add jondotsoy/flags flags-builderInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A zero-dependency, type-safe CLI argument parser with a fluent builder API.
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.
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
--portand--verbose" "Make a script that reads arguments from the terminal"
Adding flags to an existing script
"Add a
--dry-runflag to my deploy script" "Support--output=distin my build tool"
Parsing subcommands
"My CLI needs
serveandbuildcommands, each with their own flags" "How do I parsemycli deploy --env production?"
Handling repeated or typed inputs
"I need
--tagto be repeatable" "Validate that--portis a number and defaults to 3000"
User phrases that signal this skill
--flag from process.argv"--flag, -f, subcommands, or argument parsing in a Node.js/TypeScript contextyargs, commander, minimist) — prefer extending the existing setupargvimport { 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); }
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
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
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); }
}
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:
When to keep it simple (single file):
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)
--helpconst [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);
}
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); }
}
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:
| Position | Name | Value when ok | Value when error |
|---|---|---|---|
| [0] | ok | true | false |
| [1] | error | undefined | the caught error (unknown) |
| [2] | output | parsed result (fully typed) | undefined |
.parse() insteadUse .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.
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);
}
development
Create and run tests for the @jondotsoy/flags project. Use when asked to write tests, add test cases, run the test suite, debug failing tests, or verify behavior of flags/argument/command builders.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------