ProvenPin Install

Commander CLI with Zod option validation, tested in-process: working versions (tested 2026-10-15)

A Commander subcommand CLI with variadic arguments, repeatable options, choices and Zod-backed argument parsers, made testable with exitOverride and configureOutput so node:test runs it in-process without spawning.

passed5 passed · 0 failed2026-10-15 14:57 UTC
Status
passed
Last passed
2026-10-15 14:57 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
commander@15.0.0zod@4.6.7tsx@4.23.15@types/node@26.6.5
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38755782594

The code

Exactly the files that passed. Your agent can pull them with get_recipe("commander-zod-cli") (install), or see them on GitHub.

package.json
{
  "name": "commander-zod-cli",
  "private": true,
  "type": "module",
  "bin": { "notes": "./src/bin.ts" },
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "commander": "15.0.0",
    "zod": "4.6.7"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "26.6.5"
  }
}
src/bin.ts
#!/usr/bin/env -S node --import tsx
import { run } from "./cli.ts";

const r = await run(process.argv.slice(2));
process.stdout.write(r.stdout);
process.stderr.write(r.stderr);
process.exitCode = r.code;
src/cli.ts
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
import { z } from "zod";

export type Note = { id: number; text: string; tags: string[]; priority: "low" | "normal" | "high" };

const AddOpts = z.object({
  tag: z.array(z.string().regex(/^[a-z0-9-]+$/, "tags are lowercase kebab-case")).default([]),
  priority: z.enum(["low", "normal", "high"]),
});

/** commander argParser that funnels a Zod schema into commander's own error reporting. */
function zodArg<T>(schema: z.ZodType<T>) {
  return (value: string) => {
    const r = schema.safeParse(value);
    if (!r.success) throw new InvalidArgumentError(r.error.issues[0].message);
    return r.data;
  };
}

/**
 * Builds the CLI around an injected store and output sinks so tests can run it in-process.
 * exitOverride turns process.exit into a thrown CommanderError.
 */
export function buildCli(store: Note[], out: (s: string) => void, err: (s: string) => void) {
  const program = new Command("notes")
    .description("tiny notes CLI")
    .version("1.0.0")
    .exitOverride()
    .configureOutput({ writeOut: out, writeErr: err });

  program.command("add")
    .description("add a note")
    .argument("<text...>", "note text")
    .option("-t, --tag <tag>", "tag (repeatable)", (v: string, prev: string[] = []) => [...prev, v])
    .addOption(new Option("-p, --priority <level>", "priority").choices(["low", "normal", "high"]).default("normal"))
    .action((words: string[], opts: unknown) => {
      const parsed = AddOpts.safeParse(opts);
      if (!parsed.success) program.error(`error: ${parsed.error.issues[0].message}`, { exitCode: 2, code: "notes.invalid" });
      const note: Note = { id: store.length + 1, text: words.join(" "), tags: parsed.data!.tag, priority: parsed.data!.priority };
      store.push(note);
      out(`added #${note.id}\n`);
    });

  program.command("list")
    .description("list notes")
    .option("--json", "print JSON")
    .option("--min <n>", "show at most n notes", zodArg(z.coerce.number().int().positive()))
    .action((opts: { json?: boolean; min?: number }) => {
      const notes = opts.min ? store.slice(0, opts.min) : store;
      if (opts.json) out(JSON.stringify(notes) + "\n");
      else for (const n of notes) out(`#${n.id} [${n.priority}] ${n.text}${n.tags.length ? " #" + n.tags.join(" #") : ""}\n`);
    });

  return program;
}

/** Runs argv (without node and script) and returns an exit code instead of exiting. */
export async function run(argv: string[], store: Note[] = []) {
  let stdout = "", stderr = "";
  const program = buildCli(store, (s) => (stdout += s), (s) => (stderr += s));
  try {
    await program.parseAsync(argv, { from: "user" });
    return { code: 0, stdout, stderr, store };
  } catch (e) {
    if (e instanceof CommanderError) return { code: e.exitCode, stdout, stderr, store, errorCode: e.code };
    throw e;
  }
}
test/cli.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { run, type Note } from "../src/cli.ts";

test("add collects variadic text, repeatable tags and a default priority", async () => {
  const r = await run(["add", "buy", "milk", "-t", "home", "--tag", "errand"]);
  assert.equal(r.code, 0);
  assert.equal(r.stdout, "added #1\n");
  assert.deepEqual(r.store[0], { id: 1, text: "buy milk", tags: ["home", "errand"], priority: "normal" });
});

test("choices() rejects an unknown priority with exit code 1", async () => {
  const r = await run(["add", "x", "-p", "urgent"]);
  assert.equal(r.code, 1);
  assert.equal(r.errorCode, "commander.invalidArgument");
  assert.match(r.stderr, /Allowed choices are low, normal, high/);
});

test("zod refinements surface through program.error with a custom exit code", async () => {
  const r = await run(["add", "x", "-t", "Bad Tag"]);
  assert.equal(r.code, 2);
  assert.equal(r.errorCode, "notes.invalid");
  assert.match(r.stderr, /kebab-case/);
});

test("list prints text or JSON and validates --min through a zod argParser", async () => {
  const store: Note[] = [];
  await run(["add", "one", "-p", "high"], store);
  await run(["add", "two", "-t", "work"], store);
  const text = await run(["list"], store);
  assert.equal(text.stdout, "#1 [high] one\n#2 [normal] two #work\n");
  const json = await run(["list", "--json", "--min", "1"], store);
  assert.deepEqual(JSON.parse(json.stdout).map((n: Note) => n.text), ["one"]);
  const bad = await run(["list", "--min", "zero"], store);
  assert.equal(bad.code, 1);
  assert.match(bad.stderr, /option '--min <n>' argument 'zero' is invalid/);
});

test("--version and missing arguments exit through exitOverride", async () => {
  const v = await run(["--version"]);
  assert.equal(v.code, 0);
  assert.equal(v.stdout.trim(), "1.0.0");
  const missing = await run(["add"]);
  assert.equal(missing.code, 1);
  assert.equal(missing.errorCode, "commander.missingArgument");
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.