Skip to content

ilbertt/parsh

Repository files navigation

parsh

Build type-safe CLIs in TypeScript.

  • File-based commands router - inspired by TanStack Router.
  • Inherited options — parent options flow into every descendant, fully typed.
  • Schema-agnostic - validate options and params with any Standard Schema library (Zod, Valibot, ...).
  • Extensible context - inject shared services once, access them typed in every handler.
  • Schemas are types — params, options, and context infer end-to-end. Mistyped keys are compile errors.
  • Headless core — no Ink, chalk, or terminal deps. Plug in whatever TUI you want.
  • Type-safe aliases — alias one command path to another with a single prop; the compiler enforces the target exists and the param shapes match.

For Agents

Guidelines and instructions on how to build amazing CLI applications are available in the skills/ folder.

Quick start

npm install @parshjs/core
npm install -D @parshjs/codegen
// src/commands/_root.ts
import { defineRootCommand } from '@parshjs/core';
import { z } from 'zod';

export const command = defineRootCommand({
  options: {
    verbose: { schema: z.boolean().optional(), forwardToChildren: true },
  },
});
// src/commands/hello.ts
import { defineCommand } from '@parshjs/core';
import { z } from 'zod';

export const command = defineCommand('hello', {
  description: 'Say hello.',
  options: { name: { schema: z.string().default('world') } },
  handler: ({ options, rootOptions, print }) => {
    if (rootOptions.verbose) {
      print.dim(`greeting ${options.name}…`);
    }
    print.success(`hello, ${options.name}`);
  },
});
parsh-codegen generate --commands src/commands --out src/commandTree.gen.ts
// src/main.ts
import { createCli } from '@parshjs/core';
import { commandTree } from './commandTree.gen.ts';

await createCli({ programName: 'mycli', tree: commandTree }).main();

Why parsh

Options and params are typed from their schemas. No generics, no casts.

defineCommand('deploy [env]', {
  params: { env: { schema: z.enum(['staging', 'prod']) } },
  options: { force: { schema: z.boolean().optional() } },
  handler: ({ params, options }) => {
    params.env;     // 'staging' | 'prod'
    options.force;  // boolean | undefined
    options.foo;    // throws a compile-time TypeScript error
  },
});

Forwarded options flow into descendants, fully typed.

// _root.ts
defineRootCommand({
  options: { region: { schema: z.string().default('eu-west-2'), forwardToChildren: true } },
});

// s3/buckets/list.ts
defineCommand('s3 buckets list', {
  options: {},
  handler: ({ rootOptions }) => {
    rootOptions.region;   // string
    rootOptions.bar;      // throws a compile-time TypeScript error
  },
});

Aliases are checked at compile time. Point one command path at another with a single prop — the target must be a registered path with the same param shape, or it's a TypeScript error.

// commands/s3/ls.ts
defineCommand('s3 ls', { aliasOf: 's3 buckets list' });

// commands/s3/c/[name].ts
defineCommand('s3 c [name]', { aliasOf: 's3 buckets [name] create' });

// compile errors:
defineCommand('s3 ls', { aliasOf: 'totally made up' });        // unknown target
defineCommand('s3 ls', { aliasOf: 's3 buckets [name] create' }); // param shape mismatch

Help is auto-generated, colored, and respects NO_COLOR. No need to wire up usage strings, format flags, or pull in chalk — --help works on the root and on every subcommand out of the box.

$ awslike --help
A fake AWS CLI.

Usage: awslike <command> [options]

Options:
  --identity    AWS account identity (required for every command).
  --region, -r  AWS region. Defaults to eu-west-2.

Commands:
  configure                   Persist access/secret keys to disk for later use.
  s3                          Manage S3 buckets and objects.
  s3 buckets list             List S3 buckets.
  s3 buckets <name> create    Create a new S3 bucket.
  …

Shared context is injected and typed on every handler under ctx.context. Register the Cli once, then ctx.context.db, ctx.context.env, ctx.context.files light up everywhere — separated from framework-provided fields so nothing collides.

const cli = createCli({
  programName: 'mycli',
  tree: commandTree,
  context: {
    db: createDbClient(),
    env: createEnvContext({ vars: { DATABASE_URL: { schema: z.url() } } }),
  },
});

declare module '@parshjs/core' {
  interface Register { cli: typeof cli }
}

// any handler, anywhere:
defineCommand('migrate', {
  options: {},
  handler: async (ctx) => {
    ctx.context.env.DATABASE_URL;   // string
    await ctx.context.db.query(/* ... */);
    ctx.context.foo;                // throws a compile-time TypeScript error
  },
});

Every handler gets a ctx.print helper for colored, leveled output. No need to import chalk or write to process.stderr by hand.

defineCommand('deploy', {
  options: {},
  handler: ({ print }) => {
    print.info('starting deploy…');     // plain
    print.success('deploy complete');   // green
    print.warn('config is stale');      // yellow → stderr
    print.error('failed to push image');// red → stderr
    print.dim('took 12.4s');            // dim
  },
});

Examples

Example What it shows
awslike Deeply nested commands modeled after the AWS CLI (s3 buckets [name] create), forwardToChildren flags inherited down the tree, and @parshjs/files for credentials on disk.
httpie A small HTTPie-style CLI showing type-safe command aliases (httpie https://...GET [url]) and repeatable flags from z.array(z.string()) schemas.
pomo Pomodoro timer with a live Ink TUI rendered from inside a handler. Demonstrates that core stays headless — any TUI library plugs in.
env-vars @parshjs/env with createEnvContext for typed, lazy process.env access (PORT, NODE_ENV, DATABASE_URL).
config-store @parshjs/files for typed JSON config in ~/.config/mycli/, with ensureExists() gating reads via beforeHandler.
scaffold A create-app-style wizard built with @clack/prompts — options can fall back to interactive prompts when absent.

Core Packages

Package Description
@parshjs/core The router. defineCommand, createCli, types.
@parshjs/codegen parsh-codegen CLI that walks commands/ and emits commandTree.gen.ts.

Add-on Packages

Package Description
@parshjs/env Typed, lazy process.env access for the cli's ctx.
@parshjs/files Typed JSON file storage for the cli's ctx.

About

Build type-safe CLIs in TypeScript

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Contributors