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

@bioleyl/zargv

v0.2.0

Published

A Zod-first CLI framework where Zod schemas remain the single source of truth.

Readme

zargv

A type-safe CLI framework powered by Zod. Define your command tree once, validate everything through schemas you already trust, and keep handler types inferred end-to-end.

Table of Contents

  1. Installation
  2. First Example: A Command Without Arguments
  3. Adding Options with --flag <value>
  4. Boolean Flags (--verbose)
  5. Default Values
  6. Positional Arguments: A Single Operand
  7. Positional Arguments: Multiple Operands
  8. Composing a Command Tree
  9. External Handlers with HandlerCtx
  10. API Reference

Installation

npm install @bioleyl/zargv zod@^4

First Example: A Command Without Arguments

The simplest form — a command that does something, without any arguments:

import { zargv } from "zargv";

export default zargv.command({
  description: "Print a greeting",

  async handler() {
    console.log("Hello!");
  },
});

Usage :

$ mycli greet
Hello!

Adding Options with --flag <value>

To accept named arguments, use zargv.from() which converts a Zod schema into an options definition:

import { z } from "zod";
import { zargv } from "zargv";

export default zargv.command({
  description: "Greet a user by name",

  args: zargv.from(
    z.object({
      name: z.string().describe("User to greet"),
    }),
  ),

  async handler({ args }) {
    // TypeScript knows: args.name is string
    console.log(`Hello, ${args.name}!`);
  },
});

Usage :

$ mycli greet --name Alice
Hello, Alice!

Short aliases are optional:

zargv.from(
  z.object({ name: z.string() }),
  { aliases: { name: "n" } }, // now both -n and --name work
);

Boolean Flags (--verbose)

Booleans act as flags: the presence of the flag activates the value, its absence deactivates it.

import { z } from "zod";
import { zargv } from "zargv";

export default zargv.command({
  description: "Run a build",

  args: zargv.from(
    z.object({
      verbose: z.boolean().describe("Enable verbose output"),
    }),
  ),

  async handler({ args }) {
    if (args.verbose) {
      console.log("[verbose] Starting build...");
    } else {
      console.log("Building...");
    }
  },
});

Usage :

$ mycli build          # → Building...
$ mycli build --verbose # → [verbose] Starting build...

Default Values

When an option has a default value, it becomes optional — the flag does not appear in the usage signature.

import { z } from "zod";
import { zargv } from "zargv";

export default zargv.command({
  description: "Create a user",

  args: zargv.from(
    z.object({
      name:   z.string().describe("User display name"),
      admin:  z.boolean().default(false).describe("Grant administrator privileges"),
    }),
  ),

  async handler({ args }) {
    console.log(`Creating user ${args.name} (admin=${args.admin})`);
  },
});

Usage :

$ mycli users create --name Bob          # → Creating user Bob (admin=false)
$ mycli users create -n Alice --admin    # → Creating user Alice (admin=true)

Positional Arguments: A Single Operand

For commands that accept exactly one positional argument, use zargv.positionals.single():

import { z } from "zod";
import { zargv } from "zargv";

export default zargv.command({
  description: "Stage a destination",

  positionals: zargv.positionals.single([
    "destination",
    z.string(),
  ]),

  async handler({ positionals }) {
    // TypeScript knows: positionals.destination is string
    console.log(`Staging to ${positionals.destination}`);
  },
});

Usage :

$ mycli stage dist/       # → Staging to dist/
$ mycli stage             # → Error: exactly one operand required
$ mycli stage a b         # → Error: too many operands

Positional Arguments: Multiple Operands

For commands with multiple positional arguments, use zargv.positionals.splitLast() — it splits all operands except the last into a group, and keeps the last one separate. This is the classic pattern of cp SOURCE... DEST or mv SOURCE... DIR:

import { z } from "zod";
import { zargv } from "zargv";

export default zargv.command({
  description: "Move files",

  args: zargv.from(
    z.object({ force: z.boolean().default(false) }),
    { aliases: { force: "f" } },
  ),

  positionals: zargv.positionals.splitLast([
    ["sources",   z.array(z.string()).min(1)],
    ["directory", z.string()],
  ]),

  async handler({ args, positionals }) {
    // args.force: boolean
    // positionals.sources: string[]
    // positionals.directory: string
    console.log(args.force, positionals.sources, positionals.directory);
  },
});

Usage :

$ mycli mv file1.txt dir/
$ mycli mv -f a.txt b.txt c.txt output/

Composing a Command Tree

Commands compose into trees. A parent command has no handler — it contains sub-commands:

// src/commands/users/create.ts
import create from "./create";
import remove from "./remove";

export default zargv.command({
  description: "Manage users",
  commands: { create, remove }, // ← sub-commands
});

// src/cli.ts — entry point
import users from "./commands/users";

zargv({
  name: "mycli",
  description: "My application CLI",
  commands: { users },
}).run(process.argv);

Result :

$ mycli --help
My application CLI

Commands:
  users   Manage users [command]

$ mycli users create --name Bob
Creating user Bob (admin=false)

External Handlers with HandlerCtx

To separate command definitions from their logic, use the helper type HandlerCtx:

import { z } from "zod";
import { zargv, HandlerCtx } from "zargv";

// 1. Define args and positionals separately
const mvArgs = zargv.from(
  z.object({ force: z.boolean().default(false) }),
  { aliases: { force: "f" } },
);

const mvPositionals = zargv.positionals.splitLast([
  ["sources",   z.array(z.string()).min(1)],
  ["directory", z.string()],
]);

// 2. Build the command with an inline handler that delegates
export default zargv.command({
  description: "Move files",
  args: mvArgs,
  positionals: mvPositionals,
  async handler(ctx) { await handleMove(ctx); },
});

// 3. Infer the full context type
type MvCtx = HandlerCtx<typeof import("./mv").default>;

// 4. Write logic separately — automatically typed
async function handleMove({ args, positionals }: MvCtx) {
  console.log(args.force, positionals.sources, positionals.directory);
}

API Reference

zargv.from(schema, options?)

Converts a Zod schema into an options definition for zargv.command().

| Parameter | Description | |-----------|-------------| | schema | A ZodObject, e.g. z.object({ name: z.string() }) | | options.aliases | Optional map: canonical key → short flag character |

zargv.command(options)

Builds a command node (leaf or parent).

| Parameter | Description | |-----------|-------------| | description | Short description, displayed in help | | args | Options definition from zargv.from() (optional) | | positionals | Positional arguments definition (optional) | | commands | Nested sub-commands (optional) | | handler(ctx) | Function receiving { args, positionals } |

zargv.positionals.single(tuple)

Command with exactly one operand.

zargv.positionals.single(["destination", z.string()]);

zargv.positionals.splitLast(tuple)

SOURCE... DEST pattern — all operands except the last form a group, the last one is separate.

zargv.positionals.splitLast([
  ["sources",   z.array(z.string()).min(1)],
  ["directory", z.string()],
]);

zargv(options).run(argv)

Creates and runs a CLI application.

| Parameter | Description | |-----------|-------------| | name | Binary name (displayed in help) | | description | Root-level description | | commands | Command tree |