agentleFS
bendruckerbendrucker/claude/.claude/skills/cli/SKILL.md

cli

Skill18 starsChanged 25 days agoMITView on GitHub

Type-safe CLI argument parsing with commander and @commander-js/extra-typings, the standard parser for this repo's Bun scripts. Use when writing or editing any script that takes arguments (flags, positional parameters, subcommands, nested subcommands, --help) instead of reading existing scripts for the pattern.

25 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use desktop, api and 2 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • zjl4616Skill

    desktop

    桌面操控。让 CC 看屏幕、动鼠标、点按钮、输文字。基于 macOS 原生 OCR 触觉反馈,不截全屏传 AI,极致省 token。触发词:"/desktop"、"帮我操作桌面"、"点一下那个按钮"、"看看屏幕上有什么

    Also in 4 projects that use this

  • VercelSkill

    api

    Publish HTTP handlers for external webhooks and callbacks on this agent's subdomain.

    Also in 5 projects that use this

  • VercelSkill

    mcp

    MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.

    Also in 5 projects that use this

  • NVIDIASkill

    testing

    Run Python and Rust tests for physicsnemo-curator using uv + pytest with coverage reporting via pytest-cov, and cargo-nextest for Rust tests. Includes benchmark workflows with pytest-benchmark and criterion.

    Also in 4 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 19 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Import Command, Option, and the error classes from @commander-js/extra-typings.

    line 10
  2. Very specific:

    Use collect for repeated flags instead of a variadic option (--tag <names...>), which also absorbs every positional that follows it.

    line 93
  3. Very specific:

    Call exitOverride() and configureOutput() on every command in the tree.

    line 139

“Common” is measured: kept by at least 6 in 10 of the 19 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

CLI

New scripts parse arguments with commander through @commander-js/extra-typings, which infers each option's and argument's type from its flag string. Scripts still on cleye migrate to commander over time.

Import Command, Option, and the error classes from @commander-js/extra-typings. Importing them from commander drops the inference and types every option as any.

Basic Usage

#!/usr/bin/env bun

import { Command, Option } from "@commander-js/extra-typings";

export const program = new Command("scan")
  .description("Scan repository prose for AI writing tropes.")
  .argument("<path>", "directory to scan")
  .option("-o, --output <file>", "output path")
  .addOption(new Option("--format <format>", "output format").choices(["text", "json"]).default("text"))
  .option("--dry-run", "report without writing")
  .action((path, options) => {
    console.log(path); // string
    console.log(options.output); // string | undefined
    console.log(options.format); // "text" | "json"
    console.log(options.dryRun); // true | undefined
  });

if (import.meta.main) {
  await program.parseAsync();
}

The action receives each declared argument in order, then the options object, then the command. Reading an undeclared key such as options.outptu is a type error.

parseAsync handles --help and -h, and awaits an async action. A missing argument, an unknown option, an excess positional, or a value outside choices prints an error and exits 1.

Options

DeclarationType
--name <value>string | undefined
--flagtrue | undefined
--no-colorboolean, default true
--since [date]string | true | undefined
.requiredOption("--repo <r>")string
.option("--limit <n>", "…", int, 20)number
.argument("<path>")string
.argument("[files...]")string[]

A default or requiredOption drops undefined from the type. A kebab-case flag (--dry-run) arrives as a camelCase key (dryRun). An enum needs addOption with new Option(...).choices([...]), which types the value as the union.

Numbers and Repeated Flags

Commander has no number type and keeps only the last value of a repeated flag. Pass a parser function as the third argument for both:

#!/usr/bin/env bun

import { Command, InvalidArgumentError } from "@commander-js/extra-typings";

function int(value: string): number {
  const n = Number(value);
  if (!Number.isInteger(n)) throw new InvalidArgumentError("Not an integer.");
  return n;
}

function collect(value: string, previous: string[] = []): string[] {
  return [...previous, value];
}

export const program = new Command("search")
  .option("-n, --limit <n>", "maximum results", int, 20)
  .option("--tag <name>", "tag to match, repeatable", collect, [])
  .requiredOption("--label <name>", "label to apply, repeatable", collect)
  .action((options) => {
    console.log(options.limit); // number
    console.log(options.tag); // string[]
    console.log(options.label); // string[]
  });

if (import.meta.main) {
  await program.parseAsync();
}
  • Copy int and collect into each script that uses them. A distributed plugin cannot import them from another plugin or a workspace package, and they are too small to publish.
  • A required repeated flag takes collect with no default. requiredOption counts a [] default as present and never reports the flag missing.
  • An InvalidArgumentError thrown from a parser prints error: option '...' argument '...' is invalid. <message> and exits 1.
  • Use collect for repeated flags instead of a variadic option (--tag <names...>), which also absorbs every positional that follows it.

Subcommands

Build each command with new Command(name) and attach it with addCommand. A group is a command whose children are its actions, so trials trial start nests with no extra wiring:

#!/usr/bin/env bun

import { Command } from "@commander-js/extra-typings";

export const start = new Command("start")
  .description("Start a trial.")
  .option("--name <name>", "trial name", "trial")
  .argument("<command...>", "command to run, with its arguments")
  .passThroughOptions()
  .action((forward, options) => {
    console.log(options.name, forward);
  });

const end = new Command("end").description("End a trial.").action(() => {
  console.log("ended");
});

const trial = new Command("trial")
  .description("Start or end a trial.")
  .enablePositionalOptions()
  .addCommand(start)
  .addCommand(end);

export const program = new Command("trials").enablePositionalOptions().addCommand(trial);

if (import.meta.main) {
  await program.parseAsync();
}
  • A group without an action prints its usage and exits 1 when invoked without a subcommand.
  • Options declared on a group belong to the group, and must precede the subcommand under enablePositionalOptions(). A leaf reads them with command.optsWithGlobals(), which types them only when the group's .command(name) created the leaf. A leaf attached with addCommand gets them at runtime without the types.
  • -- does not reach the action as its own value. [forward...] holds the operands before -- and after it with no boundary between them, so a stray positional before -- gets forwarded.
  • To forward arguments to another program, declare <command...> and call passThroughOptions() on the leaf. Everything from the first positional onward forwards verbatim, including flags, so start --name x git diff --stat and start --name x -- git diff --stat both forward git diff --stat.
  • Bun drops a -- that comes first after the script path, so ./tool.ts -- git --stat reaches commander as git --stat. passThroughOptions() forwards both spellings the same way.
  • passThroughOptions() requires enablePositionalOptions() on every ancestor, and throws at parse time when one lacks it.

Testing

Call exitOverride() and configureOutput() on every command in the tree. addCommand does not copy them to children. With both set, a parse error or --help throws a CommanderError carrying code and exitCode instead of exiting, and output lands in the test.

import { CommanderError, type CommandUnknownOpts } from "@commander-js/extra-typings";
import { expect, test } from "bun:test";
import { program, start } from "./trials";

async function run(command: CommandUnknownOpts, argv: string[]): Promise<string> {
  let output = "";
  const configure = (cmd: CommandUnknownOpts): void => {
    cmd.exitOverride().configureOutput({
      writeOut: (text) => {
        output += text;
      },
      writeErr: (text) => {
        output += text;
      },
    });
    for (const sub of cmd.commands) configure(sub);
  };
  configure(command);
  await command.parseAsync(argv, { from: "user" });
  return output;
}

test("forwards the command after the leaf's options", async () => {
  await run(program, ["trial", "start", "--name", "x", "--", "git", "diff"]);
  expect(start.opts().name).toBe("x");
  expect(start.processedArgs).toEqual([["git", "diff"]]);
});

test.each<{ name: string; argv: string[]; code: string; exitCode: number }>([
  { name: "unknown subcommand", argv: ["trial", "nope"], code: "commander.unknownCommand", exitCode: 1 },
  { name: "help", argv: ["--help"], code: "commander.helpDisplayed", exitCode: 0 },
])("$name exits through CommanderError", async ({ argv, code, exitCode }) => {
  const failure = await run(program, argv).catch((error: unknown) => error);
  expect(failure).toBeInstanceOf(CommanderError);
  expect(failure).toMatchObject({ code, exitCode });
});
  • Pass { from: "user" } so commander reads argv as arguments only, with no runtime and script path in front.
  • parseAsync resolves to the command, not the action's return value. Assert on a leaf's typed opts() and processedArgs, or on the exported core functions the action calls.
  • Reparsing the same program is safe. Commander restores option defaults before each parse.

Conventions

  • Shebang: #!/usr/bin/env bun
  • Entry point: call await program.parseAsync() inside if (import.meta.main) so the module is importable.
  • Export the root program, any leaf a test inspects, and the core functions. Keep argument parsing in the command definitions.
  • A distributed plugin declares both commander and @commander-js/extra-typings in its own package.json at the same ~15.0.0 range, then regenerates its lockfile with bun run plugin-lockfiles generate. extra-typings pins its commander peer to one minor line, so the two upgrade together. Renovate groups them into one PR.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.