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
- BrightGold70/skillstheir own version, first seen Nov 8, 2026
- yuri-xyz/skillstheir own version, first seen Nov 8, 2026
- lobehub/lobehubtheir own version, first seen Nov 5, 2026
- jinhuang712/qq-music-clitheir own version, first seen Oct 25, 2026
- deepgram/skillstheir own version, first seen Oct 25, 2026
- kreuzberg-dev/liter-llmtheir own version, first seen Oct 8, 2026
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.
zjl4616Skilldesktop
桌面操控。让 CC 看屏幕、动鼠标、点按钮、输文字。基于 macOS 原生 OCR 触觉反馈,不截全屏传 AI,极致省 token。触发词:"/desktop"、"帮我操作桌面"、"点一下那个按钮"、"看看屏幕上有什么
Also in 4 projects that use this
VercelSkillapi
Publish HTTP handlers for external webhooks and callbacks on this agent's subdomain.
Also in 5 projects that use this
VercelSkillmcp
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
NVIDIASkilltesting
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
Other versions25 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 25
- lobehub/lobe-chat★ 84ksame name
- lobehub/lobehub★ 84ksame name
- clockworklabs/SpacetimeDB★ 26ksame name
- saifyxpro/HeadlessX★ 2.4ksame name
- Goldziher/html-to-markdown★ 888same name
- kreuzberg-dev/html-to-markdown★ 881same name
- kreuzberg-dev/tree-sitter-language-pack★ 483same name
- kreuzberg-dev/liter-llm★ 264same name
- AterDev/Perigon.CLI★ 54same name
- tiero/claw-cash★ 36same name
15 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository4 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- user/CLAUDE.mdCLAUDE.md
- agent-ideasSkill
- prose-labSkill
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
- Very specific: line 10
Import Command, Option, and the error classes from @commander-js/extra-typings.
- Very specific: line 93
Use collect for repeated flags instead of a variadic option (--tag <names...>), which also absorbs every positional that follows it.
- Very specific: line 139
Call exitOverride() and configureOutput() on every command in the tree.
“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.
Files
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
| Declaration | Type |
--name <value> | string | undefined |
--flag | true | undefined |
--no-color | boolean, 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
intandcollectinto 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
collectwith no default.requiredOptioncounts a[]default as present and never reports the flag missing. - An
InvalidArgumentErrorthrown from a parser printserror: option '...' argument '...' is invalid. <message>and exits 1. - Use
collectfor 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 withcommand.optsWithGlobals(), which types them only when the group's.command(name)created the leaf. A leaf attached withaddCommandgets 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 callpassThroughOptions()on the leaf. Everything from the first positional onward forwards verbatim, including flags, sostart --name x git diff --statandstart --name x -- git diff --statboth forwardgit diff --stat. - Bun drops a
--that comes first after the script path, so./tool.ts -- git --statreaches commander asgit --stat.passThroughOptions()forwards both spellings the same way. passThroughOptions()requiresenablePositionalOptions()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 readsargvas arguments only, with no runtime and script path in front. parseAsyncresolves to the command, not the action's return value. Assert on a leaf's typedopts()andprocessedArgs, 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()insideif (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
commanderand@commander-js/extra-typingsin its ownpackage.jsonat the same~15.0.0range, then regenerates its lockfile withbun run plugin-lockfiles generate.extra-typingspins itscommanderpeer to one minor line, so the two upgrade together. Renovate groups them into one PR.
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
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.

