ProvenPin Install

clipanion typed CLI parser: working versions (tested 2026-10-16)

Define a clipanion Cli with two commands using Option.String/Option.Boolean, then drive it in tests via Cli.run with captured stdout/stderr to assert help text and exit codes without process.exit.

passed9 passed · 0 failed2026-10-16 02:18 UTC
Status
passed
Last passed
2026-10-16 02:18 UTC
Tests
9 / 9
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
clipanion@3.2.1@types/node@26.6.5tsx@4.23.15
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 38839686104

The code

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

package.json
{
  "name": "clipanion-cli-parser",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "clipanion": "3.2.1"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/cli.ts
import { Builtins, Cli, Command, Option } from "clipanion";

export class AddCommand extends Command {
  static paths = [["add"]];

  static usage = Command.Usage({
    category: "Math",
    description: "Add two integers together.",
    examples: [["Add 1 and 2", "$0 add --a 1 --b 2"]],
  });

  a = Option.String("--a", { required: true });
  b = Option.String("--b", { required: true });
  verbose = Option.Boolean("-v,--verbose", false);

  async execute(): Promise<number> {
    const left = Number(this.a);
    const right = Number(this.b);

    if (Number.isNaN(left) || Number.isNaN(right)) {
      this.context.stderr.write("error: --a and --b must be numbers\n");
      return 1;
    }

    if (this.verbose) {
      this.context.stderr.write(`verbose: ${left} + ${right}\n`);
    }

    this.context.stdout.write(`${left + right}\n`);
    return 0;
  }
}

export class GreetCommand extends Command {
  static paths = [["greet"]];

  static usage = Command.Usage({
    category: "Text",
    description: "Greet someone by name.",
    examples: [["Greet Ada", "$0 greet --name ada --shout"]],
  });

  name = Option.String("--name");
  shout = Option.Boolean("--shout", false);

  async execute(): Promise<number> {
    const message = `Hello, ${this.name ?? "world"}`;
    this.context.stdout.write(`${this.shout ? message.toUpperCase() : message}\n`);
    return 0;
  }
}

export function createCli(): Cli {
  const cli = new Cli({
    binaryLabel: "demo",
    binaryName: "demo",
    binaryVersion: "1.0.0",
  });

  cli.register(Builtins.HelpCommand);
  cli.register(Builtins.VersionCommand);
  cli.register(AddCommand);
  cli.register(GreetCommand);

  return cli;
}
test/cli.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { Writable } from "node:stream";
import { Cli } from "clipanion";
import { createCli } from "../src/cli.ts";

function capture() {
  let output = "";
  const stream = new Writable({
    write(chunk, _encoding, callback) {
      output += chunk.toString();
      callback();
    },
  });

  return {
    stream,
    get output() {
      return output;
    },
  };
}

function makeContext() {
  const stdout = capture();
  const stderr = capture();

  return {
    stdout,
    stderr,
    context: {
      ...Cli.defaultContext,
      stdout: stdout.stream,
      stderr: stderr.stream,
    },
  };
}

test("runs the add command and exits 0", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["add", "--a", "1", "--b", "2"], context);

  assert.equal(code, 0);
  assert.equal(stdout.output, "3\n");
});

test("supports boolean flags", async () => {
  const cli = createCli();
  const { context, stdout, stderr } = makeContext();

  const code = await cli.run(["add", "--a", "2", "--b", "3", "--verbose"], context);

  assert.equal(code, 0);
  assert.equal(stdout.output, "5\n");
  assert.equal(stderr.output, "verbose: 2 + 3\n");
});

test("greet falls back to its default name", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["greet"], context);

  assert.equal(code, 0);
  assert.equal(stdout.output, "Hello, world\n");
});

test("greet honours --shout", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["greet", "--name", "ada", "--shout"], context);

  assert.equal(code, 0);
  assert.equal(stdout.output, "HELLO, ADA\n");
});

test("prints command help and exits 0", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["add", "--help"], context);

  assert.equal(code, 0);
  assert.match(stdout.output, /Usage/);
  assert.match(stdout.output, /--a/);
  assert.match(stdout.output, /--b/);
});

test("prints top-level help listing commands", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["--help"], context);

  assert.equal(code, 0);
  assert.match(stdout.output, /add/);
  assert.match(stdout.output, /greet/);
});

test("returns exit code 1 for a usage error", async () => {
  const cli = createCli();
  const { context, stdout, stderr } = makeContext();

  const code = await cli.run(["add", "--a", "1"], context);

  assert.equal(code, 1);
  assert.match(stdout.output + stderr.output, /--b/);
});

test("returns exit code 1 for an unknown command", async () => {
  const cli = createCli();
  const { context } = makeContext();

  const code = await cli.run(["nope"], context);

  assert.equal(code, 1);
});

test("prints the binary version", async () => {
  const cli = createCli();
  const { context, stdout } = makeContext();

  const code = await cli.run(["--version"], context);

  assert.equal(code, 0);
  assert.match(stdout.output, /1\.0\.0/);
});

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.