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

@rune-cli/rune

v0.3.3

Published

Rune is an agent-native CLI framework designed for the agentic era.

Downloads

1,108

Readme

Rune

Rune is an agent-native CLI framework designed for the agentic era. It is built around two principles:

  • Rune must be a CLI framework that is easy to understand not only for humans, but also for agents
  • CLIs built with Rune must likewise be easy to work with for both humans and agents

Key features:

  • File-based command routing — directory structure maps directly to the CLI command tree
  • Type-safe command definitions with full inference from defineCommand()
  • Global options via defineConfig({ options })
  • Global lifecycle hooks via defineConfig({ hooks })
  • Project locals via defineConfig({ locals })
  • Standard Schema support for options and args (Zod, Valibot, ArkType, ...)
  • Built-in --json mode that turns the same command into a machine-readable API, auto-enabled under AI agents
  • In-process test utility with no child-process overhead
  • Automatic --help generation, with per-command and project-wide customization hooks
  • Structured errors via CommandError, rendered for humans or emitted as JSON
  • Official Agent Skill for AI agents working on Rune projects

[!IMPORTANT] This package is experimental and unstable. Proceed with caution when using it.

Getting Started

Rune requires Node.js v22.12.0 or higher.

Scaffold a new project:

npm create rune-app@latest my-cli
cd my-cli
npm install

This generates the following structure:

my-cli/
  src/
    commands/
      hello.ts
      hello.test.ts
      text/
        _group.ts
        count.ts
        count.test.ts
  package.json
  tsconfig.json

Run your CLI directly from source:

npm run start -- hello

Build for production:

npm run build

Defining Commands

Commands are TypeScript files under src/commands/. The directory structure maps directly to the command structure:

src/commands/
  index.ts     -> my-cli
  hello.ts     -> my-cli hello
  project/
    index.ts   -> my-cli project
    create.ts  -> my-cli project create
    list.ts    -> my-cli project list

Simple leaf commands can be bare files (hello.ts), while commands that need subcommands use a directory with index.ts. Only the matched leaf command module is loaded at runtime.

Files and directories whose command name starts with _ are ignored by routing, so command-specific helpers can live next to the command that uses them. Colocated test files ending in .test.ts or .spec.ts are also ignored:

src/commands/
  deploy.ts          -> my-cli deploy
  deploy.test.ts     -> ignored
  _deploy-logic.ts   -> ignored
  project/
    _group.ts        -> group metadata
    _schema.ts       -> ignored
    create.ts        -> my-cli project create

_group.ts is a reserved metadata file, not a private helper. The _ prefix keeps Rune-owned metadata and private implementation files out of the public command namespace.

Each command file exports a default defineCommand() call:

import { defineCommand } from "@rune-cli/rune";

export default defineCommand({
  description: "Greet someone",
  options: [{ name: "loud", type: "boolean", short: "l" }],
  args: [{ name: "name", type: "string", required: true }],
  run({ options, args, output }) {
    const greeting = `Hello, ${args.name}!`;
    output.log(options.loud ? greeting.toUpperCase() : greeting);
  },
});

Command Groups

Place a _group.ts file in a command directory to attach metadata (description, aliases, examples) to the group itself:

import { defineGroup } from "@rune-cli/rune";

export default defineGroup({
  description: "Manage projects",
});

Options and Arguments

options are --name flags; args are positional. Required args must come before optional ones.

Global options can be defined once in rune.config.ts and are available to every executable command:

import { defineConfig } from "@rune-cli/rune";

export default defineConfig({
  options: [{ name: "profile", type: "string", env: "APP_PROFILE", default: "prod" }],
});

Use them after the resolved command path:

my-cli deploy --profile dev

Global Hooks

Project-wide hooks can be registered in rune.config.ts and run around each matched command's run() lifecycle:

import { defineConfig } from "@rune-cli/rune";

export default defineConfig({
  hooks: {
    beforeRun(ctx) {
      ctx.output.error(`running ${ctx.command.path.join(" ")}`);
    },
    afterRun(ctx) {
      ctx.output.error(`completed ${ctx.result.kind}`);
    },
    onRunError(ctx) {
      ctx.output.error(`${ctx.stage} failed: ${ctx.error.message}`);
    },
  },
});

Hooks run only after routing and argument parsing succeed for an executable command. They do not run for help, version, unknown-command, group-help, JSON-help, or parse-failure paths. Hook context exposes parsed args, parsed options, outputMode, command metadata, output, and stdin. Global options declared in the same defineConfig() call are inferred on hook ctx.options.

Project Locals

Project-defined runtime values can be created once per command invocation in rune.config.ts and read from ctx.locals:

import { defineConfig } from "@rune-cli/rune";

export default defineConfig({
  options: [{ name: "profile", type: "string", default: "prod" }],
  async locals(ctx) {
    const workspace = await resolveWorkspace(ctx.cwd);

    return {
      workspace,
      api: createApiClient({ profile: ctx.options.profile }),
    };
  },
});

locals runs after routing and argument parsing succeed, before beforeRun. It does not run for help, version, unknown-command, group-help, JSON-help, or parse-failure paths. Global options declared in the same defineConfig() call are inferred on ctx.options. The factory context intentionally does not include stdin; hooks and commands can still use ctx.stdin.

Primitive Fields

| Property | Description | | ------------- | -------------------------------------------------------- | | name | Identifier used as the key in ctx.args / ctx.options | | type | "string", "number", "boolean", or "enum" | | required | Whether the field must be provided | | default | Default value; shown in --help output | | description | Help text | | short | Single-letter alias (options only, e.g. "f"-f) | | env | Environment variable fallback for scalar options | | multiple | Allow an option to be repeated and parsed as an array |

defineCommand({
  options: [
    { name: "retries", type: "number", default: 3, description: "Retry count" },
    { name: "verbose", type: "boolean", short: "v" },
  ],
  args: [{ name: "name", type: "string", required: true }],
  run({ options, args }) {
    // options.retries: number, options.verbose: boolean, args.name: string
  },
});

Environment Variable Fallback

Scalar options can read an environment variable when the CLI flag is omitted. Priority is CLI > env > default, and env values use the same parser and validation path as CLI values.

defineCommand({
  options: [{ name: "port", type: "number", env: "PORT", default: 3000 }],
  run({ options }) {
    // --port 4000 wins over PORT=5000; PORT wins over default
    // options.port: number
  },
});

env does not affect type inference. Repeatable options cannot use it.

Enum Fields

Use type: "enum" with a values list to accept only a fixed set of string or number choices. The allowed-values union is inferred automatically and rendered in --help.

defineCommand({
  options: [{ name: "mode", type: "enum", values: ["dev", "prod"], default: "dev" }],
  args: [{ name: "target", type: "enum", values: ["web", "node"], required: true }],
  run({ options, args }) {
    // options.mode: "dev" | "prod", args.target: "web" | "node"
  },
});

String values must match /^[A-Za-z0-9_.-]+$/. For free-form strings or runtime validation (regex, uniqueness, transformation), use a type: "string" field or a schema field.

Repeatable Options

Set multiple: true on a string, number, enum, or schema value option to allow repeated flags. Values are parsed in declaration order and exposed as an array. If omitted, the option is optional unless you provide an array default such as [] or set required: true.

defineCommand({
  options: [
    { name: "tag", type: "string", multiple: true, default: [] },
    { name: "level", type: "number", multiple: true },
  ],
  run({ options }) {
    // options.tag: string[], options.level?: number[]
  },
});

Primitive boolean options and schema flag: true options cannot be repeatable.

Standard Schema Fields

Use schema instead of type to plug in any Standard Schema-compatible library (Zod, Valibot, ArkType, ...). Rune calls validators through the Standard Schema contract, so there is no lock-in to a specific library.

import { defineCommand } from "@rune-cli/rune";
import { z } from "zod";

export default defineCommand({
  description: "Fetch a resource by id",
  options: [{ name: "retries", schema: z.coerce.number().int().min(0).max(10), defaultLabel: "3" }],
  args: [{ name: "id", schema: z.uuid(), typeLabel: "uuid", description: "Resource id" }],
  run({ options, args }) {
    // options.retries: number, args.id: string
  },
});

typeLabel and defaultLabel are display-only hints rendered in --help. Required/optional semantics are derived from the schema itself.

Negatable Boolean Options

A primitive boolean option with default: true automatically gets a --no-<name> counterpart:

options: [{ name: "color", type: "boolean", default: true }];
// --color     -> true  (default)
// --no-color  -> false

Kebab-case Field Names

Hyphenated field names (e.g. dry-run) are accessible as both ctx.options["dry-run"] and ctx.options.dryRun, with full type safety.

JSON Output

Set json: true to opt into a built-in --json flag. The run() return value becomes the structured output, while output.log() is suppressed so the stdout stream remains machine-parseable. output.error() still writes to stderr, and JSON error payloads are emitted to stderr on failure. Inside run(), options.json is true whenever JSON mode is active.

export default defineCommand({
  json: true,
  run({ options }) {
    if (!options.json) {
      // Render human-readable output here.
    }

    return { items: [1, 2, 3] };
  },
});
my-cli         # human-readable text CLI
my-cli --json  # {"items":[1,2,3]}

Under AI agents (Claude Code, Cursor, Codex, etc.), json: true commands auto-enable JSON mode even without --json, so a single command serves both humans and agents seamlessly. Detection only triggers on known agent environment variables — CI jobs and shell pipes continue to produce human-readable output unless --json is passed explicitly. Auto-enabled JSON mode also sets options.json to true.

Set RUNE_DISABLE_AUTO_JSON=1 to opt out of auto-activation while keeping --json working as usual. This is mainly intended for AI agents that are themselves developing a Rune-based CLI and need to inspect the human-facing output.

JSON Lines Output

Set jsonl: true when a command's stdout contract is a stream of newline-delimited JSON records. The run() function returns an Iterable or AsyncIterable; Rune serializes each record as one compact JSON line, suppresses output.log(), and keeps output.error() on stderr.

export default defineCommand({
  jsonl: true,
  async *run() {
    yield { id: "a" };
    yield { id: "b" };
  },
});
my-cli  # {"id":"a"}
        # {"id":"b"}

JSON Lines mode is fixed for the command; Rune does not add a --jsonl flag and json: true cannot be combined with jsonl: true. If the downstream pipe closes early, Rune treats that broken pipe as a normal early stop instead of printing an error. If you need both a human view and a JSON Lines stream, prefer separate commands. In tests, runCommand() exposes yielded records as result.output.records.

Structured Errors

CommandError carries kind, message, hint, and details. Rune formats it for humans in normal mode and emits it as structured JSON to stderr under --json.

import { CommandError, defineCommand } from "@rune-cli/rune";

export default defineCommand({
  json: true,
  run() {
    throw new CommandError({
      kind: "not-found",
      message: "Resource not found",
      hint: "Check the id and try again",
    });
  },
});

Help Output

--help output is generated from description, options, args, examples, and the surrounding command tree. Override per command with defineCommand({ help }), or configure project-wide help and CLI metadata via rune.config.ts:

import { defineConfig, renderDefaultHelp } from "@rune-cli/rune";

export default defineConfig({
  name: "my-cli",
  version: "1.0.0",
  help(data) {
    return `${data.cliName}\n\n${renderDefaultHelp(data)}\n\nDocs: https://example.com`;
  },
});

name and version affect help output, --version, and JSON help metadata. When omitted, Rune derives them from package.json.

Pass --json with --help to inspect the same help data as structured JSON. This works even for commands that do not enable runtime JSON output with json: true:

my-cli deploy --help --json

Testing

Import runCommand() from @rune-cli/rune/test to exercise commands in-process — argv parsing, type coercion, schema validation, env fallbacks, and defaults all run exactly as they do at real invocation.

import { runCommand } from "@rune-cli/rune/test";
import { expect, test } from "vitest";

import greeting from "../src/commands/index.ts";

test("greets by name", async () => {
  const result = await runCommand(greeting, ["world"]);

  expect(result.exitCode).toBe(0);
  expect(result.stdout).toBe("Hello, world!\n");
});

Pass { env } as the third argument to test option env fallbacks. The env map replaces process.env for that command test, and omitted env defaults to an empty map so tests stay isolated from the host environment.

If you intentionally want to inherit the current process environment, merge it explicitly:

const result = await runCommand(command, [], {
  env: { ...process.env, TASKS_FILE: file },
});

Pass { stdin } to test commands that read ctx.stdin without mocking process.stdin:

const result = await runCommand(command, [], {
  stdin: "hello\n",
});

The returned CommandExecutionResult exposes exitCode, stdout, stderr, error, and an output union. Use output.document for json: true commands and output.records for jsonl: true commands.

CLI

The rune binary provides two commands:

| Command | Description | | ------------ | ---------------------------------------------------------------------------------- | | rune run | Run the project directly from source. Trailing args are forwarded to the user CLI. | | rune build | Build the project into a distributable CLI. |

Both accept --project <path> to target a project root other than the current directory.

rune run hello --loud
rune build --project ./my-app

Agent Skills

Rune ships an official Agent Skill that gives AI agents on-demand access to Rune-specific conventions — file-based routing, defineCommand() usage, testing patterns, and more — so agents can work on Rune projects more accurately and efficiently.

API

| Export | Description | | --------------------- | ----------------------------------------------------------------------- | | defineCommand(def) | Define a command. The returned value must be the file's default export. | | defineGroup(def) | Define metadata for a command group in _group.ts. | | defineConfig(def) | Define project-wide CLI metadata, help, options, hooks, and locals. | | CommandError | Structured error class for command failures. | | renderDefaultHelp() | Render the default help output as a string; useful from custom help. | | runCommand() | (from @rune-cli/rune/test) Execute a command in-process for testing. |

Documentation

Full documentation is available at the Rune docs site.

License

Published under the MIT License.