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

@thomas.ca/orpc-cli

v0.0.3

Published

Generate command-line interfaces from oRPC routers and clients using Standard Schema

Readme

@thomas.ca/orpc-cli

npm version License: MIT

Generate a type-safe CLI HTTP client from your oRPC router. Perfect for AI agents and developers who love command-line tools.

What it does: @thomas.ca/orpc-cli introspects your oRPC router at build time and generates @drizzle-team/brocli command definitions. You bring your own oRPC client—we generate the command structure, not the full CLI.

TL;DR

Turn your oRPC router into a CLI HTTP client. Define routes, generate brocli commands, and start making HTTP requests from the terminal. Your server code stays on the server—only the command structure is generated.

npx @thomas.ca/orpc-cli generate
npx tsx cli.ts users list --limit 10

Features

  • Build-time code generation — Router is introspected at build time, never bundled into the CLI
  • Nested subcommands — Supports arbitrary depth (e.g., inbox.message.list)
  • Platform import stubbing — Works with cloudflare:workers, deno, and other platform-specific imports via esbuild plugin
  • tsconfig paths — Automatically resolved via esbuild
  • SSE streaming — Detects async iterators and streams output with timestamps
  • Standard Schema — Works with Zod, Valibot, ArkType, and any Standard Schema library

Installation

# Install as a dev dependency (requires runtime deps: esbuild, bundle-require)
npm install -D @thomas.ca/orpc-cli

# Required peer dependencies
npm install @drizzle-team/brocli @orpc/client

Note: This package requires runtime dependencies (esbuild, bundle-require). It must be installed before running and cannot be used directly with npx without first installing.

Quick Start

@thomas.ca/orpc-cli generates brocli command definitions—not a complete CLI. You create the entry point, we generate the commands.

1. Define your router

// router.ts
import { os } from "@orpc/server";
import { z } from "zod";

export const router = {
  users: {
    list: os
      .input(z.object({ 
        limit: z.number().optional().describe("Maximum number of users")
      }))
      .handler(async ({ input }) => ({ users: [] })),
    
    create: os
      .input(z.object({ 
        name: z.string().describe("User name"),
        email: z.string().email().describe("User email")
      }))
      .handler(async ({ input }) => ({ id: 1, ...input })),
  },
};

export type AppRouter = typeof router;

2. Create config

// orpc-cli.config.ts
import { defineConfig } from "@thomas.ca/orpc-cli/config";
import { router } from "./router.ts";

export default defineConfig({
  router,
  output: ".orpc-cli",
});

3. Generate CLI

npx @thomas.ca/orpc-cli generate

4. Create CLI entry point

// cli.ts
import { run } from "@drizzle-team/brocli";
import { createORPCClient } from "@orpc/client";
import { buildCommands } from "./.orpc-cli/index.ts";
import type { AppRouter } from "./router.ts";

const client = createORPCClient<AppRouter>({
  baseURL: "http://localhost:3000/api",
});

run(buildCommands(client), {
  name: "mycli",
  description: "My generated CLI",
  version: "1.0.0",
});

5. Run CLI

npx tsx cli.ts users list --limit 10
npx tsx cli.ts users create --name "Alice" --email "[email protected]"

How It Works

┌────────────────────────────────────────────────────────────────┐
│                        Build Time                               │
├────────────────────────────────────────────────────────────────┤
│  orpc-cli.config.ts                                            │
│  └── Router imported and introspected                          │
│                                                                │
│  npx @thomas.ca/orpc-cli generate                                         │
│  ├── bundle-require loads config (esbuild + virtual modules)   │
│  ├── Platform stubs: cloudflare:workers → { env: Proxy }       │
│  ├── tsconfig paths resolved automatically                     │
│  ├── Router structure analyzed (no handlers executed)          │
│  └── .orpc-cli/index.ts generated                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│                       Runtime (Your CLI)                        │
├────────────────────────────────────────────────────────────────┤
│  cli.ts                                                        │
│  ├── import { buildCommands } from "./.orpc-cli/index.ts"      │
│  ├── @orpc/client (no server code)                             │
│  └── buildCommands(client) → brocli commands                   │
└────────────────────────────────────────────────────────────────┘

Key points:

  • Router is never bundled into your CLI
  • Platform imports are stubbed via esbuild plugin (no temp files)
  • Handlers are not executed during introspection
  • tsconfig paths are auto-resolved via esbuild

Authentication

Use environment variables to authenticate your CLI HTTP client:

// cli.ts
import { RPCLink } from "@orpc/client/fetch";

const link = new RPCLink({
  url: 'http://localhost:3000/api',
  headers: {
    authorization: process.env.API_TOKEN ? `Bearer ${process.env.API_TOKEN}` : undefined,
  },
});

const client = createORPCClient(link);
run(buildCommands(client), { name: "mycli", version: "1.0.0" });
API_TOKEN=abc123 npx tsx cli.ts users list --limit 10

Platform-Specific Imports

@thomas.ca/orpc-cli stubs platform modules during config loading via an esbuild plugin. This means you can import Cloudflare Workers bindings in your router without issues:

// router.ts
import { env } from "cloudflare:workers";

export const router = {
  users: {
    get: os
      .input(z.object({ id: z.number() }))
      .handler(async ({ input }) => {
        const db = env.DB; // Safe - only used at runtime
        return db.get(input.id);
      }),
  },
};

Supported stubs:

  • cloudflare:workersenv (Proxy), ExecutionContext, ScheduledController
  • deno — empty object

Configuration

defineConfig(options)

import { defineConfig } from "@thomas.ca/orpc-cli/config";

defineConfig({
  // Required: Your oRPC router
  router: yourRouter,
  
  // Optional: Output directory (default: ".orpc-cli")
  output: ".orpc-cli",
});

Generated Code

@thomas.ca/orpc-cli generates a buildCommands(client) function that returns @drizzle-team/brocli command definitions. You wire these commands into your own CLI entry point.

What gets generated:

  • buildCommands(client) — Function that takes an oRPC client and returns brocli command array
  • Command definitions with options mapped from your Standard Schema inputs
  • processOutput() helper — Handles JSON output and SSE streaming
  • handleError() helper — Consistent error formatting

What you write:

  • CLI entry point (using run() from brocli)
  • oRPC client configuration
  • Any custom middleware or setup

Example input router:

export const router = {
  users: {
    list: os
      .input(z.object({ 
        limit: z.number().default(10),
        active: z.boolean().default(true)
      }))
      .handler(async ({ input }) => ({ users: [] })),
  },
};

Generated output:

// Generated by orpc-cli - DO NOT EDIT
import { command, string, number, boolean } from "@drizzle-team/brocli";

async function processOutput(result: unknown): Promise<void> {
  // Handle async iterator / SSE streaming
  if (result && typeof (result as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function') {
    let index = 0;
    for await (const event of result as AsyncIterable<unknown>) {
      const timestamp = new Date().toISOString();
      console.log(`[${timestamp}] [${++index}] ${JSON.stringify(event)}`);
    }
    return;
  }
  // Regular output
  console.log(JSON.stringify(result, null, 2));
}

function handleError(error: unknown): never {
  const msg = error instanceof Error ? error.message : String(error);
  console.error(msg);
  process.exit(1);
}

export const buildCommands = (client: any) => {
  const users_list = command({
    name: "list",
    desc: "users.list",
    options: {
      limit: number("limit").desc("number").default(10),
      active: boolean("active").desc("boolean").default(true),
    },
    handler: async (opts) => {
      const { ...input } = opts;
      try {
        const result = await client.users.list(input, { context: { options: opts } });
        await processOutput(result);
      } catch (error) {
        handleError(error);
      }
    },
  });

  const users = command({
    name: "users",
    desc: "users commands",
    subcommands: [users_list],
  });

  return [users];
};

Key generated features:

  • processOutput() — Handles both regular JSON and async iterator (SSE) streaming
  • handleError() — Clean error output with process.exit(1)
  • Type-safe defaults — Numbers without quotes, booleans as literals
  • Nested subcommands — Arbitrary depth supported

Output Format

Regular responses are pretty-printed JSON:

$ mycli users list
{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}

Errors show only the message:

$ mycli users get --id 999
User not found

Async iterators (SSE) are streamed with timestamps:

$ mycli events subscribe
[2024-01-15T10:30:00.000Z] [1] {"type":"update","data":"first"}
[2024-01-15T10:30:01.000Z] [2] {"type":"update","data":"second"}

CLI Reference

npx @thomas.ca/orpc-cli generate

Generate CLI code from your configuration.

Aliases: gen

Options:

| Option | Description | Default | |--------|-------------|---------| | --cwd <path> | Current working directory | process.cwd() | | --output <dir> | Output directory (overrides config) | "" |

Config file resolution: Looks for config files in order:

  1. orpc-cli.config.ts
  2. orpc-cli.config.mts
  3. orpc-cli.config.js
  4. orpc-cli.config.mjs

Examples

See the examples/ directory:

  • examples/basic/ — Simple users/posts router
  • examples/cloudflare/ — Router using cloudflare:workers imports
  • examples/nested/ — Deep nesting: inbox.message.list, workspace.project.task.create

License

MIT