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

@ontrails/cli

v0.2.3

Published

Framework-agnostic CLI command model for Trails. Import command derivation from `@ontrails/cli`; import the Commander runtime adapter from `@ontrails/commander`.

Readme

@ontrails/cli

Framework-agnostic CLI command model for Trails. Import command derivation from @ontrails/cli; import the Commander runtime adapter from @ontrails/commander.

Usage

import { trail, topo, Result } from '@ontrails/core';
import { surface } from '@ontrails/commander';
import { z } from 'zod';

const greet = trail('greet', {
  input: z.object({ name: z.string().describe('Who to greet') }),
  implementation: (input) => Result.ok(`Hello, ${input.name}!`),
});

const graph = topo('myapp', { greet });
await surface(graph);
$ myapp greet --name World
Hello, World!

$ myapp greet --help
Usage: myapp greet [options]

Options:
  --name <value>  Who to greet
  -h, --help      display help for command

For more control, build the commands yourself:

import { deriveCliCommands } from '@ontrails/cli';
import { toCommander } from '@ontrails/commander';

const commands = deriveCliCommands(graph);
if (commands.isErr()) throw commands.error;

const program = toCommander(commands.value, { name: 'myapp' });
program.parse();

deriveCliCommands returns Result<CliCommand[], Error>. Use toCommander with commands.value on success, or write your own adapter. Invalid command models are rejected before adapter wiring, including duplicate CLI paths and executable parents that also declare positional args beneath child commands.

API

| Export | What it does | | --- | --- | | deriveCliCommands(graph) | Framework-agnostic command builder, returns Result<CliCommand[], Error> | | validateCliCommands(commands) | Validate CliCommand[] shapes before wiring a CLI adapter | | deriveFlags(schema) | Extract honest CLI flags from a Zod schema | | normalizeCliArgv(commands, argv) | Normalize framework-owned CLI syntax before adapter parsing | | output(data, mode) | Format output as JSON, JSONL, or text | | deriveOutputMode(flags, topoName) | Derive output mode from flags and topo-derived env vars (<TOPO>_JSON, <TOPO>_JSONL) |

@ontrails/commander

| Export | What it does | | --- | --- | | surface(graph, options?) | One-liner: build commands, wire Commander, parse argv | | createProgram(graph, options?) | Build a Commander program without parsing argv | | toCommander(commands, options?) | Connect CliCommand[] to a Commander program |

See the API Reference for the full list.

Flag derivation

Flags come from the Zod schema automatically when the field shape can be represented truthfully on the command line. No manual flag definitions.

| Zod type | CLI flag | Notes | | --- | --- | --- | | z.string() | --name <value> | Required | | z.boolean() | --verbose | Switch | | z.enum(["a","b"]) | --format <value> | With choices | | z.array(z.enum(["a","b"])) | --mode a b or --mode a --mode b | Bounded multiselect | | z.array(z.string()) | --tag <values...> | Repeatable | | z.optional(...) | --name [value] | Optional |

camelCase fields become --kebab-case flags. .describe() becomes help text.

Nested objects and arrays of objects are intentionally omitted from automatic flag derivation. The CLI prefers fewer flags over dishonest flags.

CLI adapters should pass user argv through normalizeCliArgv(commands, argv) before parsing. This gives bounded multiselects one framework-owned grammar: contiguous and repeated values are both accepted. The first matching token after a flag is its explicit value; after that first value, additional collection stops before known child routes or values outside the declared choices.

Enum flags can expose standalone boolean aliases when a surface wants pipe-friendly shortcuts without inventing parallel flags. The alias still normalizes to the canonical enum field before the trail input is validated:

deriveFlags(z.object({ format: z.enum(['summary', 'json']) }), {
  format: { aliases: { json: 'json' } },
});

An adapter such as @ontrails/commander will parse --json as if the caller had passed --format json.

Positional arguments

When a trail's input schema has exactly one required string field with no default, the CLI auto-promotes it to a positional argument instead of a flag:

const greet = trail('greet', {
  input: z.object({ name: z.string().describe('Who to greet') }),
  implementation: (input) => Result.ok(`Hello, ${input.name}!`),
});
myapp greet World          # positional
myapp greet --name World   # flag alias is kept for backward compatibility

The heuristic is intentionally conservative: multiple required strings stay as flags. To override, declare args on the trail:

const copy = trail('file.copy', {
  input: z.object({ src: z.string(), dest: z.string() }),
  args: ['src'],
  implementation: (input) => Result.ok({ src: input.src, dest: input.dest }),
});

args accepts string[] for explicit positional order, false to suppress auto-promotion entirely, or undefined (omit) for the heuristic.

myapp file copy ./readme.md --dest /tmp/readme.md

App auto-discovery

When running from the workspace root without an explicit --module flag, the CLI automatically discovers your app entry point:

  1. src/app.ts (single-app layout)
  2. apps/*/src/app.ts (monorepo convention)

If exactly one candidate is found, it is used automatically. If multiple candidates are found, the CLI lists them and asks you to choose with --module.

# Auto-discovers src/app.ts — no --module needed
myapp topo

# Explicit when multiple apps exist
myapp topo --module ./apps/api/src/app.ts

Use findAppModuleCandidates(cwd) and findAppModule(cwd, explicit?) directly for programmatic access.

Structured input

For every non-empty object input schema, the CLI also exposes:

  • --input-json <json>
  • --input <path|->

These channels supply the full input object before positional args and explicit flags are merged on top. Explicit CLI inputs always win on conflict, and the final merged object is still validated once by the trail schema.

myapp gist create \
  --input-json '{"files":[{"filename":"README.md","content":"Hello"}]}'

Subcommands

Dotted trail IDs derive to full ordered command paths:

  • entity.show -> myapp entity show
  • topo.pin -> myapp topo pin
  • topo.pin.remove -> myapp topo pin remove

Command-path nodes may be both executable and parents, so myapp topo and myapp topo pin can coexist naturally.

CliCommand[] validation rejects ambiguous parent/child shapes, so an executable parent cannot also declare positional args if child commands exist beneath that path.

Resource Resolution

Declared resources on each trail are resolved into the context before execution enters the implementation.

Filtering

await surface(graph, { include: ['entity.**'] });
await surface(graph, { exclude: ['dev.**'] });

* matches one dotted segment and ** matches any depth. Trails declared with visibility: 'internal' stay hidden unless you include their exact trail ID intentionally.

Derived behavior

The CLI surface derives previously layer-shaped behavior directly from trail schemas:

  • Trails whose output matches the pagination pattern (items, hasMore, nextCursor) automatically get an --all flag that walks every page.
  • Trails with since/until input fields automatically expand date shortcuts ("today", "yesterday", "7d", "30d", "this-week", "this-month") into ISO 8601 strings before validation.

The legacy autoIterateLayer and dateShortcutsLayer exports were removed in TRL-475; these behaviors are now intrinsic to the CLI surface and require no wiring.

Installation

These installation examples target Trails 0.2.1 on the normal npm release line.

bun add --exact @ontrails/[email protected] @ontrails/[email protected]

@ontrails/cli owns command derivation. @ontrails/commander owns Commander program materialization and parsing.