npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@andrei.fyi/picocli

v0.1.0

Published

Tiny CLI framework for Node and Bun.

Readme

picocli

Tiny CLI framework for Node and Bun.

pnpm add @andrei.fyi/picocli zod
# or
bun add @andrei.fyi/picocli zod

Requires Node.js ≥ 22 or Bun, and zod@^4.

Quickstart

Sample code:

import cli from "@andrei.fyi/picocli";
import { z } from "zod";

// create app
const app = cli.create("gh", {
  version: "1.0.0",
  description: "Tiny GitHub-style CLI",
});

// create and add a command directly
app.command("status", {
  description: "Show repository status",
  run: () => {
    const result = { clean: true, branch: "main" };
    console.log(`clean (branch: ${result.branch})`);
  },
});

// create a command and then add it
// if it has no run() handler it will just print the help for its subcommands
const pr = cli.command("pr", {
  description: "Manage pull requests",
});
// create and add a subcommand
pr.command("list", {
  alias: "ls", // alternate command name
  description: "List pull requests", // help & MCP tool description
  args: z.object({
    repo: z.string().describe("owner/name"), // positional args
  }),
  options: z.object({
    state: z.enum(["open", "closed", "all"]).default("open").describe("Filter by state"), // --state
  }),
  env: z.object({
    GITHUB_TOKEN: z.string().optional().describe("GitHub token"),
  }),
  outputSchema: z.object({
    prs: z.array(z.object({ number: z.number(), title: z.string() })),
    state: z.enum(["open", "closed", "all"]),
    authenticated: z.boolean(),
  }), // describes --json data and MCP output schema
  examples: [{ command: "gh pr list acme/widgets --state closed" }], // shown in --help
  hidden: false, // true hides from help and MCP
  run: ({ args, options, env }) => {
    const prs = [{ number: 1, title: `Fix CI in ${args.repo}` }];
    for (const pull of prs) console.log(`#${pull.number} ${pull.title}`);

    // returned value is used for --json and MCP tool result
    return { prs, state: options.state, authenticated: Boolean(env.GITHUB_TOKEN) };
  },
});
app.command(pr);

app.serve();

Sample output:

$ gh
Usage: gh <command>

Tiny GitHub-style CLI

Commands:
  status  Show repository status
  pr  Manage pull requests

Global options:
  --help  show help
  --version  print version
  --json  emit JSON output
  --format <value>  output format: pretty | json
  --color / --no-color  force/disable color (--color / --no-color)
  --mcp  run as an MCP stdio server
  --llms  print a Markdown command manifest
  --schema  print input/output JSON Schemas for the command

$ gh status
clean (branch: main)

$ gh pr
Usage: gh pr <command>

Manage pull requests

Commands:
  list (ls)  List pull requests

Global options:
  --help  show help
  --version  print version
  --json  emit JSON output
  --format <value>  output format: pretty | json
  --color / --no-color  force/disable color (--color / --no-color)
  --mcp  run as an MCP stdio server
  --llms  print a Markdown command manifest
  --schema  print input/output JSON Schemas for the command

$ gh pr list acme/widgets --state closed
#1 Fix CI in acme/widgets

$ gh pr ls acme/widgets --state closed --json
{"prs":[{"number":1,"title":"Fix CI in acme/widgets"}],"state":"closed","authenticated":false}

Examples

Examples live in examples/ and are grouped by what you are trying to learn:

| Area | Examples | | ---- | -------- | | Basics | smallest app, options/coercion, returned data with outputSchema | | Commands | direct commands, nested command trees, inherited options/env | | Runtime IO | env schemas, stdin, raw tokens after -- | | Integrations | --schema, feature toggles, thrown errors | | Testing | runCli() and runJson() |

bun run examples/basics/hello.ts Ada
bun run examples/commands/nested-tree.ts task list --state closed
bun run examples/io/rest.ts rest -- --raw -x
bun run examples/testing/testkit.ts

Defining commands

Use cli.create() for the root app. Use cli.command() for commands you mount under an app or command.

Common definition fields:

| Field | Meaning | | ---- | ---- | | description | Help text; MCP tool description for runnable commands | | args | Positional arguments, assigned by schema key order | | options | Named --flags; booleans support --flag and --no-flag | | env | Environment variables read by exact key name | | outputSchema | Describes successful structured data and MCP output schema | | validateOutput | Validate returned data against outputSchema; defaults to "development" | | examples | Help examples | | run | Command handler | | version | Value printed by --version |

Root app fields:

| Field | Meaning | | ---- | ---- | | features | Enable or disable optional built-in flags; all enabled by default |

Child command fields:

| Field | Meaning | | ---- | ---- | | alias | Alternate command name | | hidden | Hide from help and MCP |

run() owns normal output. Print to stdout, start a TUI, call APIs, or return nothing.

Return a value when --json or MCP should receive structured data. In structured mode, console.* is suppressed. If run() returns a value, --json prints that value directly; if it returns nothing, structured output is null.

The object passed to run() is the command context. Destructure only the fields you need:

run: ({ args }) => {
  console.log(`hello ${args.name}`);
  return { greeted: args.name };
}

Context fields:

| Field | Meaning | | ---- | ---- | | name | Resolved command name | | args | Parsed positional args | | options | Parsed options, including inherited parent options | | env | Parsed env values, including inherited parent env | | isTTY | stdout is an interactive terminal | | isJSON | structured output was requested | | rest | tokens after -- | | readStdin() | read piped stdin |

Zod coercion

CLI values arrive as strings. Booleans are inferred from presence. Use z.coerce.* for other non-string values.

options: z.object({
  port: z.coerce.number().default(3000),
  when: z.coerce.date().optional(),
})

Output schemas

outputSchema describes returned data and is included in the MCP output schema. Runtime validation is controlled by validateOutput, which defaults to "development": invalid returned data fails only when NODE_ENV=development. Use true to always validate, or false to always return best-effort data.

app.command("status", {
  outputSchema: z.object({
    clean: z.boolean(),
    branch: z.string(),
  }),
  validateOutput: "development",
  run: () => ({ clean: true, branch: "main" }),
});

Invalid returned data exits with a VALIDATION error only when output validation is enabled.

Subcommands

Add a command directly:

app.command("status", {
  alias: "st",
  run: () => console.log("clean"),
});

Or build it separately and mount it:

const pr = cli.command("pr", {
  description: "Manage pull requests",
  run: () => console.log("use `gh pr list` to list pull requests"),
});
pr.command("list", { alias: "ls", run: () => console.log("no open PRs") });
app.command(pr);

If a command has subcommands but no run, invoking it prints its help.

Inheritance

Direct child definitions inherit parent options and env schemas in the handler type. Child fields win on name conflicts.

const app = cli.create("app", {
  options: z.object({ verbose: z.boolean().default(false) }),
  env: z.object({ TOKEN: z.string().optional() }),
});
app.command("build", {
  options: z.object({ target: z.string() }),
  run: ({ options, env }) => {
    options.verbose;
    options.target;
    env.TOKEN;
  },
});

Separately built cli.command() trees are typed from their own schemas. Repeat shared options or env on that tree when its handlers need typed access:

const pr = cli.command("pr", {
  env: z.object({ TOKEN: z.string().optional() }),
});
pr.command("list", {
  run: ({ env }) => {
    env.TOKEN;
  },
});
app.command(pr);

Inherited options appear in help and --schema. Inherited options and env appear in --llms; MCP input schemas include options, while env is read from the server environment.

Output, color, exit codes

Normal mode is human output. picocli does not print returned values unless structured output is requested.

| Behavior | Rule | | ---- | ---- | | Structured output | --json or --format json | | Color | --color / --no-color, then FORCE_COLOR / NO_COLOR, then TTY | | Success | exit 0 | | Usage or validation error | exit 2 | | Handler failure | exit 1 | | Error format | error (CODE): message or structured JSON |

Built-in flags

| Flag | Effect | | ---- | ------ | | --help | Show help (hidden commands omitted) | | --version | Print the CLI version | | --json / --format <pretty\|json> | json prints the structured data; pretty (default) is human | | --color / --no-color | Force/disable ANSI color | | --mcp | Run as an MCP stdio server | | --llms | Print a Markdown command manifest | | --schema | Print { input, output } JSON Schemas for the resolved command |

If a command declares an option that collides with a built-in name, the command's option wins and that built-in is disabled for it.

Disable optional built-in flags independently:

cli.create("internal", {
  features: { mcp: false, schema: false, llms: false },
});

MCP

--mcp starts a stdio MCP server. Every non-hidden runnable command becomes a tool. Nested command names are joined with _. Input schemas come from args and options. Output schemas are generated from outputSchema.

The MCP SDK is lazy-loaded through @andrei.fyi/picocli/mcp; install @modelcontextprotocol/sdk if you use --mcp or import that subpath.

Testing

@andrei.fyi/picocli/testing runs commands in-process.

import { runCli, runJson } from "@andrei.fyi/picocli/testing";

const r = await runCli(app, ["Ada", "--loud"]);
expect(r.stdout).toBe("HELLO ADA\n");
expect(r.exitCode).toBe(0);

const j = await runJson(app, ["Ada", "--loud"]);
expect(j.json).toEqual({ message: "HELLO ADA" });

await runCli(app, ["Ada"], { isTTY: true, env: { NO_COLOR: "1" }, stdin: "piped" });