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

@politty/valibot

v0.1.1

Published

A lightweight CLI framework inspired by citty with valibot integration for type-safe metadata management

Readme

politty

politty is a lightweight, type-safe CLI framework for Node.js built on Zod v4.

From simple scripts to complex CLI tools with subcommands, validation, and auto-generated help, you can build them all with a developer-friendly API.

Features

  • Zod Native: Use Zod schemas directly for argument definition and validation
  • Type Safety: Full TypeScript support with automatic type inference for parsed arguments
  • Flexible Argument Definition: Support for positional arguments, flags, aliases, arrays, and environment variable fallbacks
  • Subcommands: Build Git-style nested subcommands (with lazy loading and alias support)
  • Lifecycle Management: Guaranteed setupruncleanup execution order
  • Signal Handling: Proper SIGINT/SIGTERM handling with guaranteed cleanup execution
  • Auto Help Generation: Automatically generate help text from definitions
  • Interactive Prompts: Prompt for missing arguments with pluggable adapters (clack, inquirer)
  • Discriminated Union: Support for mutually exclusive argument sets
  • Skill Management: Manage agent skills (SKILL.md) with file-based install/uninstall
  • Fast Startup: Automatic Node.js compile cache (V8 code cache) integration, with a generate-shim command to cache the whole CLI graph

Requirements

  • Node.js >= 20.12.0 (excluding 21.0.0 - 21.6.x)
  • Zod >= 4.2.1

Installation

npm install politty zod
# or
pnpm add politty zod
# or
yarn add politty zod

Using valibot instead of zod

politty is also available as @politty/valibot, built on valibot schemas — same API (defineCommand, arg, runMain, and all subpath modules), and its published build never loads zod:

npm install @politty/valibot valibot
import * as v from "valibot";
import { defineCommand, runMain, arg } from "@politty/valibot";

const command = defineCommand({
  name: "greet",
  args: v.object({
    name: arg(v.string(), { description: "Name to greet", positional: true }),
    loud: arg(v.optional(v.boolean(), false), { alias: "l" }),
  }),
  run: (args) => {
    const greeting = `Hello, ${args.name}!`;
    console.log(args.loud ? greeting.toUpperCase() : greeting);
  },
});

runMain(command);

Field descriptions can also come from valibot's own metadata actions. They are actions, not schemas, so they belong inside v.pipe(...): v.pipe(v.string(), v.description("...")) or v.pipe(v.string(), v.metadata({ description: "..." })). The one zod-only feature without a valibot counterpart is the politty/augment module (zod GlobalMeta interface augmentation) — use arg() or v.metadata() instead. The politty package itself remains the zod flavor (an alias of @politty/zod).

Quick Start

import { z } from "zod";
import { defineCommand, runMain, arg } from "politty";

const command = defineCommand({
  name: "greet",
  description: "A CLI tool that displays greetings",
  args: z.object({
    name: arg(z.string(), {
      positional: true,
      description: "Name of the person to greet",
    }),
    greeting: arg(z.string().default("Hello"), {
      alias: "g",
      description: "Greeting phrase",
    }),
    loud: arg(z.boolean().default(false), {
      alias: "l",
      description: "Output in uppercase",
    }),
  }),
  run: (args) => {
    let message = `${args.greeting}, ${args.name}!`;
    if (args.loud) {
      message = message.toUpperCase();
    }
    console.log(message);
  },
});

runMain(command);

Example usage:

$ my-cli World
Hello, World!

$ my-cli World -g "Hi" -l
HI, WORLD!

$ my-cli --help
Usage: greet <name> [options]

A CLI tool that displays greetings

Arguments:
  name    Name of the person to greet

Options:
  -g, --greeting <value>  Greeting phrase (default: "Hello")
  -l, --loud              Output in uppercase
  -h, --help              Show help

Basic Usage

Defining Arguments

Use the arg() function to define argument metadata:

import { z } from "zod";
import { arg, defineCommand } from "politty";

const command = defineCommand({
  name: "example",
  args: z.object({
    // Positional argument (required)
    input: arg(z.string(), {
      positional: true,
      description: "Input file",
    }),

    // Optional positional argument
    output: arg(z.string().optional(), {
      positional: true,
      description: "Output file",
    }),

    // Flag (with alias)
    verbose: arg(z.boolean().default(false), {
      alias: "v",
      description: "Verbose output",
    }),

    // Environment variable fallback
    apiKey: arg(z.string().optional(), {
      env: "API_KEY",
      description: "API key",
    }),

    // Array argument (--file a.txt --file b.txt)
    files: arg(z.array(z.string()).default([]), {
      alias: "f",
      description: "Files to process",
    }),
  }),
  run: (args) => {
    console.log(args);
  },
});

Use args.$source(name) to check whether a value came from an explicit CLI token, an env fallback, or neither (e.g. a schema default). It's typed as optional since directly-constructed args objects (e.g. in unit tests) won't have it:

run: (args) => {
  args.$source?.("apiKey"); // "cli" | "env" | "default"
},

Subcommands

Define Git-style subcommands:

import { z } from "zod";
import { arg, defineCommand, runMain } from "politty";

const initCommand = defineCommand({
  name: "init",
  description: "Initialize a project",
  aliases: ["i"],
  args: z.object({
    template: arg(z.string().default("default"), {
      alias: "t",
      description: "Template name",
    }),
  }),
  run: (args) => {
    console.log(`Initializing with template: ${args.template}`);
  },
});

const buildCommand = defineCommand({
  name: "build",
  description: "Build the project",
  aliases: ["b"],
  args: z.object({
    output: arg(z.string().default("dist"), {
      alias: "o",
      description: "Output directory",
    }),
    minify: arg(z.boolean().default(false), {
      alias: "m",
      description: "Minify output",
    }),
  }),
  run: (args) => {
    console.log(`Building to: ${args.output}`);
  },
});

const cli = defineCommand({
  name: "my-cli",
  description: "Example CLI with subcommands",
  subCommands: {
    init: initCommand,
    build: buildCommand,
  },
});

runMain(cli, { version: "1.0.0" });

Example usage:

$ my-cli init -t react
$ my-cli i -t react        # alias for init
$ my-cli build -o out -m
$ my-cli b -o out -m        # alias for build
$ my-cli --help

Lifecycle Hooks

Execute hooks in setupruncleanup order. The cleanup hook is always executed, even if an error occurs:

const command = defineCommand({
  name: "db-query",
  description: "Execute database queries",
  args: z.object({
    database: arg(z.string(), {
      alias: "d",
      description: "Database connection string",
    }),
    query: arg(z.string(), {
      alias: "q",
      description: "SQL query",
    }),
  }),
  setup: async ({ args }) => {
    console.log("[setup] Connecting to database...");
    // Establish DB connection
  },
  run: async (args) => {
    console.log("[run] Executing query...");
    // Execute query
    return { rowCount: 42 };
  },
  cleanup: async ({ args, error }) => {
    console.log("[cleanup] Closing connection...");
    if (error) {
      console.error(`Error occurred: ${error.message}`);
    }
    // Close connection
  },
});

API

defineCommand(options)

Define a command.

| Option | Type | Description | | ------------- | ----------------------------- | ------------------- | | name | string | Command name | | description | string? | Command description | | args | ZodSchema | Argument schema | | aliases | string[]? | Command aliases | | subCommands | Record<string, Command>? | Subcommands | | setup | (context) => Promise<void>? | Setup hook | | run | (args) => T? | Run function | | cleanup | (context) => Promise<void>? | Cleanup hook |

runMain(command, options?)

CLI entry point. Handles signals and calls process.exit().

runMain(command, {
  version: "1.0.0", // Displayed with --version flag
});

runCommand(command, argv, options?)

Programmatic/testing entry point. Does not call process.exit() and returns a result object.

const result = await runCommand(command, ["arg1", "--flag"]);
if (result.success) {
  console.log(result.result);
} else {
  console.error(result.error);
}

arg(schema, meta)

Attach metadata to an argument.

| Metadata | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------------ | | positional | boolean? | Treat as positional argument | | alias | string? | Short alias (e.g., -v) | | description | string? | Argument description | | placeholder | string? | Placeholder shown in help | | env | string? | Environment variable name (fallback) | | completion | object? | Shell completion configuration | | prompt | PromptMeta? | Interactive prompt configuration (docs) |

Shell Completion

politty provides automatic shell completion generation for bash, zsh, and fish. The default generated script is a small runtime dispatcher: when the user presses TAB, it resolves the executable currently visible on PATH and uses a bundled static worker from that executable's package when one is available. If no package-relative bundled worker is found, it falls back to a per-binary static worker cache and regenerates that cache from the hidden __refresh-completion command when needed. Sourced workers are memoized for the shell session, so warm completions call the worker function directly. Completion fields that require runtime JavaScript still delegate to the binary's hidden __complete command. When NODE_COMPILE_CACHE is unset, the dispatcher sets it to a program-specific cache directory before invoking __complete, letting Node.js 22+ reuse V8 module compile cache across repeated completion requests.

Quick Setup

Use withCompletionCommand to add completion support to your CLI:

import { defineCommand, runMain, withCompletionCommand } from "politty";

const mainCommand = withCompletionCommand(
  defineCommand({
    name: "mycli",
    subCommands: {
      build: buildCommand,
      test: testCommand,
    },
  }),
);

runMain(mainCommand);

Then users can enable completions:

# Bash
eval "$(mycli completion bash)"

# Zsh
eval "$(mycli completion zsh)"

# Fish
mycli completion fish | source

For project-local CLIs, put the local binary directory in PATH with your environment manager, for example:

# .envrc
PATH_add node_modules/.bin

Completion then follows the same executable the shell would run.

Published CLIs can ship a fast worker artifact:

politty generate-worker --bin dist/cli/index.mjs --program mycli --shell zsh --verify

or call generateBundledCompletionWorker() from politty/completion in your own build script. The default output path is dist/completion/<shell>-worker.<ext>.

The dispatcher looks for common package-relative worker paths such as dist/completion/zsh-worker.zsh from the visible binary. Use withCompletionCommand({ bundledWorker: { relativePaths: { zsh: [...] } } }) to customize the package-relative lookup. For package layouts that cannot be expressed with relative paths, bundledWorker.queryCommand: true lets the dispatcher ask the binary's hidden __completion-worker-path command on the miss path.

To generate the older static script with command metadata baked in, use:

eval "$(mycli completion bash --static)"
eval "$(mycli completion zsh --static)"

Value Completion

Define completion hints for arguments:

const command = defineCommand({
  name: "build",
  args: z.object({
    // Auto-detected from z.enum()
    format: arg(z.enum(["json", "yaml", "xml"]), {
      alias: "f",
      description: "Output format",
    }),

    // File completion
    config: arg(z.string(), {
      completion: { type: "file", extensions: ["json", "yaml"] },
    }),

    // Directory completion
    outputDir: arg(z.string(), {
      completion: { type: "directory" },
    }),

    // Custom shell command
    branch: arg(z.string().optional(), {
      completion: {
        custom: { shellCommand: "git branch --format='%(refname:short)'" },
      },
    }),

    // Static choices
    environment: arg(z.string(), {
      completion: {
        custom: { choices: ["development", "staging", "production"] },
      },
    }),
  }),
  run: (args) => {
    /* ... */
  },
});

Faster Startup (Compile Cache)

politty integrates Node.js's on-disk compile cache (V8 code cache, Node >= 22.8.0) so warm starts skip recompilation; older runtimes are a silent no-op. runMain enables it automatically, which covers dynamically imported modules such as lazy() subcommands. To cache the whole CLI graph — politty, zod, and your command definitions — generate a bin shim as part of your build:

// package.json
{
  "bin": { "my-cli": "./dist/bin.js" },
  "scripts": {
    "build": "tsdown", // builds src/cli.ts -> dist/cli.js
    "postbuild": "politty generate-shim --entry ./cli.js",
  },
}

generate-shim writes an executable shim that enables the cache and loads --entry (a specifier relative to the shim) with a dynamic import. The output path and program name come from package.json (the bin path and the bin name), and the shim imports the cache helper from the politty package whose politty bin you ran — the one you installed, so it resolves from your package. Packages with multiple bins pass --entry once per bin, and --entry itself can be omitted when the entry sits next to the shim under a conventional name (./cli.js, ./index.js, ...). See Faster Startup (Compile Cache) for the flags, the opt-out (compileCache: false), and the hand-written shim variant.

Skill Management

politty manages SKILL.md-based agent skills distributed via npm packages.

Quick Setup

Use withSkillCommand to add skill management to your CLI:

import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineCommand, runMain } from "politty";
import { withSkillCommand } from "politty/skill";

// Resolves to ../skills from both src/ and dist/
const sourceDir = resolve(dirname(fileURLToPath(import.meta.url)), "../skills");

const cli = withSkillCommand(
  defineCommand({
    name: "my-agent",
    subCommands: {/* ... */},
  }),
  { sourceDir, package: "@my-agent/skills" },
);

runMain(cli);

package identifies who owns these skills. It is combined with the command name as "{package}:{cliName}" and must match the metadata["politty-cli"] stamp pre-declared in each source SKILL.md — skills add/sync refuse mismatches, and remove/sync refuse to delete skills belonging to another tool. The default install mode is "symlink" (.agents/skills/<name> -> source, .claude/skills/<name> -> canonical), so source updates propagate live; on filesystems without symlink support (e.g. Windows without Developer Mode) install throws with guidance to retry with mode: "copy", which recursively copies instead (source updates then require re-running sync). See Skill Management for details.

Skills are SKILL.md files with YAML frontmatter (spec-compliant: https://agentskills.io/specification). The metadata["politty-cli"] stamp is authored by the skill package:

---
name: commit
description: Git commit message generation
license: MIT
metadata:
  politty-cli: "@my-agent/skills:my-agent"
---

# Instructions for the agent...

Then users can manage skills:

my-agent skills sync              # Remove and reinstall all skills
my-agent skills add commit        # Install a specific skill
my-agent skills remove commit     # Remove a specific skill
my-agent skills list              # List available skills

Documentation

For detailed documentation, see the docs/ directory:

Examples

The playground/ directory contains many examples:

  • 01-hello-world - Minimal command configuration
  • 02-greet - Positional arguments and flags
  • 03-array-args - Array arguments
  • 05-lifecycle-hooks - Lifecycle hooks
  • 10-subcommands - Subcommands
  • 12-discriminated-union - Discriminated Union
  • 21-lazy-subcommands - Lazy loading
  • 26-command-alias - Command aliases

License

MIT