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

@kjanat/dreamcli

v2.0.1

Published

Schema-first, fully typed TypeScript CLI framework

Readme

dreamcli

NPM JSR

Schema-first, fully typed TypeScript CLI framework. Zero runtime dependencies.

One flag declaration configures the entire resolution pipeline:

import {
  cli,
  command,
  flag,
  arg,
  middleware,
  CLIError,
} from '@kjanat/dreamcli';

const deploy = command('deploy')
  .description('Deploy to an environment')
  .arg('target', arg.string().describe('Deploy target'))
  .flag(
    'region',
    flag
      .enum(['us', 'eu', 'ap'])
      .alias('r')
      .env('DEPLOY_REGION')
      .config('deploy.region')
      .prompt({ kind: 'select', message: 'Which region?' })
      .default('us')
      .propagate(),
  )
  .action(({ args, flags, out }) => {
    out.log(`Deploying ${args.target} to ${flags.region}`);
  });

By the time action runs, flags.region is "us" | "eu" | "ap" — not string | undefined.

The value is resolved through a documented chain: CLI → env → config → interactive prompt → default. Every step is opt-in. Every step preserves types.

Install

npm install @kjanat/dreamcli
bun add @kjanat/dreamcli
deno add jsr:@kjanat/dreamcli  # or npm:@kjanat/dreamcli

Quick start

Single command

import { command, flag, arg } from '@kjanat/dreamcli';

const greet = command('greet')
  .description('Greet someone')
  .arg('name', arg.string().describe('Who to greet'))
  .flag(
    'loud',
    flag
      .boolean()
      .alias('l')
      .describe('Shout the greeting'),
  )
  .flag(
    'times',
    flag.number().default(1).describe('Repeat count'),
  )
  .action(({ args, flags, out }) => {
    for (let i = 0; i < flags.times; i++) {
      const msg = `Hello, ${args.name}!`;
      out.log(flags.loud ? msg.toUpperCase() : msg);
    }
  });

greet.run();

Multi-command CLI

import {
  cli,
  command,
  group,
  flag,
  arg,
} from '@kjanat/dreamcli';

const deploy = command('deploy')
  .description('Deploy to an environment')
  .arg('target', arg.string())
  .flag('force', flag.boolean().alias('f'))
  .flag(
    'region',
    flag.enum(['us', 'eu', 'ap']).env('DEPLOY_REGION'),
  )
  .action(({ args, flags, out }) => {
    out.log(
      `Deploying ${args.target} to ${flags.region ?? 'default'}`,
    );
  });

const login = command('login')
  .description('Authenticate with the service')
  .flag('token', flag.string().describe('Auth token'))
  .action(({ flags, out }) => {
    out.log(
      flags.token
        ? 'Authenticated via token'
        : 'Authenticated interactively',
    );
  });

// Nested command groups
const migrate = command('migrate')
  .description('Run migrations')
  .flag('steps', flag.number())
  .action(({ flags, out }) => {
    out.log(`migrating ${flags.steps ?? 'all'} steps`);
  });

const seed = command('seed')
  .description('Seed database')
  .action(({ out }) => {
    out.log('seeding');
  });

const db = group('db')
  .description('Database operations')
  .command(migrate)
  .command(seed);

cli('mycli')
  .version('1.0.0')
  .description('My awesome tool')
  .command(deploy)
  .command(login)
  .command(db)
  .run();

// mycli deploy production --force
// mycli login --token abc123
// mycli db migrate --steps 3
// mycli db seed

Why dreamcli

Most TypeScript CLI frameworks treat the type system like decoration. You define flags in one place, then use parsed values somewhere else as a loosely typed blob. Env vars, config files, and interactive prompts live in separate universes. Testing means hacking process.argv.

dreamcli collapses all of that into a single typed schema:

Approximate comparison of first-party, built-in support as documented by each project. Third-party plugins and custom glue can extend the other libraries.

| Capability | dreamcli | Commander | Yargs | Citty | CAC | Cleye | | ------------------------------------------ | ------------------------------------- | ------------------- | ---------------------- | --------------- | ------------- | ------------- | | Type inference from definition | Full — flags, args, context | Manual .opts<T>() | Good | Good | Basic | Good | | Built-in value sources | CLI, env, config, prompt, default | CLI, defaults, env | CLI, env, config | CLI, defaults | CLI, defaults | CLI, defaults | | Schema-driven prompts | Integrated | No | No | No | No | No | | Middleware / hooks | Yes — typed middleware | Lifecycle hooks | Middleware | Plugins / hooks | Events | No | | Built-in test harness with output capture | runCommand() + capture | No | No | No | No | No | | Shell completions from command definitions | Built-in (bash/zsh/fish/powershell) | No | Built-in (bash/zsh) | No | No | No | | Structured output primitives | Built-in (--json, tables, spinners) | DIY | DIY | DIY | DIY | DIY | | Config file support | Built-in (XDG discovery, JSON) | DIY | Built-in (.config()) | No | No | No |

The closest analog is what tRPC did to API routes — individual pieces existed, the insight was wiring them so types flow end-to-end.

Features

Flag types

flag.string(); // string | undefined
flag.number(); // number | undefined
flag.boolean(); // boolean (defaults to false)
flag.enum(['us', 'eu', 'ap']); // "us" | "eu" | "ap" | undefined
flag.array(flag.string()); // string[] | undefined
flag.custom((v) => new URL(v)); // URL | undefined

Every flag supports: .default(), .required(), .alias(), .env(), .config(), .describe(), .prompt(), .deprecated(), .propagate().

Resolution chain

Each flag resolves through an ordered pipeline. Every step is opt-in:

CLI argv  →  environment variable  →  config file  →  interactive prompt  →  default value

Required flags that don't resolve produce a structured error before the action handler runs. In non-interactive contexts (CI, piped stdin), prompts are automatically skipped.

Interactive prompts

Four prompt types, declared per-flag or per-command:

// Per-flag
flag.string().prompt({ kind: 'input', message: 'Name?' });
flag
  .boolean()
  .prompt({ kind: 'confirm', message: 'Sure?' });
flag
  .enum(['a', 'b'])
  .prompt({ kind: 'select', message: 'Pick one' });
flag.array(flag.string()).prompt({
  kind: 'multiselect',
  message: 'Pick many',
  choices: [{ value: 'a' }, { value: 'b' }],
});

// Per-command (conditional — receives partially resolved flags)
command('deploy')
  .flag('region', flag.enum(['us', 'eu', 'ap']))
  .interactive(({ flags }) => ({
    region: !flags.region && {
      kind: 'select',
      message: 'Which region?',
    },
  }));

Derive typed context from resolved input

import { CLIError } from '@kjanat/dreamcli';

command('deploy')
  .flag('token', flag.string().env('AUTH_TOKEN'))
  .derive(({ flags }) => {
    if (!flags.token)
      throw new CLIError('Not authenticated', {
        code: 'AUTH_REQUIRED',
        suggest: 'Run `mycli login`',
      });
    return { token: flags.token };
  })
  .action(({ ctx }) => {
    ctx.token; // string — typed
  });

Use derive() when you need typed, command-scoped access to fully resolved flags and args before the action handler runs.

Middleware with typed context

import { middleware } from '@kjanat/dreamcli';

const timing = middleware<{ startTime: number }>(
  async ({ next }) => {
    const startTime = Date.now();
    await next({ startTime });
  },
);

const trace = middleware<{ traceId: string }>(
  async ({ next }) =>
    next({ traceId: crypto.randomUUID() }),
);

command('deploy')
  .middleware(timing)
  .middleware(trace)
  .action(({ ctx }) => {
    ctx.startTime; // number — typed
    ctx.traceId; // string — typed
  });

Context accumulates through the middleware chain via type intersection. No manual interface merging.

Use middleware when you need wrapper behavior with next().

Output channel

Handlers receive out instead of console. Adapts to context automatically:

cli('mycli')
  // ... omitted for brevity
  .action(({ out }) => {
    out.log('Human-readable message');
    out.json({ status: 'ok', count: 42 });
    out.table(rows, [
      { key: 'name', header: 'Name' },
      { key: 'status', header: 'Status' },
    ]);

    const spinner = out.spinner('Deploying...');
    spinner.succeed('Done');

    const progress = out.progress({
      label: 'Uploading',
      total: 100,
    });
    progress.update(50);
    progress.done('Upload complete');
  });
  • TTY → pretty formatting, spinners animate
  • Piped → minimal stable output, spinners suppressed
  • --json → structured JSON to stdout, everything else to stderr

Shell completions

Generated from the command schema — always in sync:

import { generateCompletion } from '@kjanat/dreamcli';

generateCompletion(myCli.schema, 'bash');
generateCompletion(myCli.schema, 'zsh');

Config file discovery

command('deploy').flag(
  'region',
  flag.enum(['us', 'eu']).config('deploy.region'),
);

Searches XDG-standard paths automatically. JSON built-in, plugin hook for YAML/TOML:

import { configFormat } from '@kjanat/dreamcli';
import { parse as parseYAML } from 'yaml';

cli('mycli')
  .config('mycli')
  .configLoader(configFormat(['yaml', 'yml'], parseYAML));

Structured errors

throw new CLIError('Deployment failed', {
  code: 'DEPLOY_FAILED',
  exitCode: 1,
  suggest: 'Check your credentials with `mycli login`',
  details: { target, region },
});

Parse and validation errors include "did you mean?" suggestions.
In --json mode, errors serialize to machine-readable JSON.

Testing

dreamcli's test harness runs commands in-process with full control over inputs and outputs. No subprocesses, no process.argv mutation, no mocking.

import { arg, command, flag } from '@kjanat/dreamcli';
import {
  runCommand,
  createTestPrompter,
  PROMPT_CANCEL,
} from '@kjanat/dreamcli/testkit';

const greet = command('greet')
  .arg('name', arg.string())
  .flag('loud', flag.boolean())
  .action(({ args, flags, out }) => {
    const message = `Hello, ${args.name}!`;
    out.log(flags.loud ? message.toUpperCase() : message);
  });

const deploy = command('deploy')
  .arg('target', arg.string())
  .flag(
    'region',
    flag
      .enum(['us', 'eu', 'ap'])
      .env('DEPLOY_REGION')
      .config('deploy.region')
      .required()
      .prompt({ kind: 'select', message: 'Which region?' }),
  )
  .action(({ args, flags, out }) => {
    out.log(`Deploying ${args.target} to ${flags.region}`);
  });

const build = command('build').action(({ out }) => {
  const spinner = out.spinner('Building');
  spinner.succeed('Done');
});

// Basic execution
const basic = await runCommand(greet, ['Alice', '--loud']);

expect(basic.exitCode).toBe(0);
expect(basic.stdout).toEqual(['HELLO, ALICE!\n']);
expect(basic.stderr).toEqual([]);
expect(basic.error).toBeUndefined();

// Resolve from environment
const fromEnv = await runCommand(deploy, ['production'], {
  env: { DEPLOY_REGION: 'eu' },
});
expect(fromEnv.stdout).toEqual([
  'Deploying production to eu\n',
]);

// Resolve from config
const fromConfig = await runCommand(
  deploy,
  ['production'],
  {
    config: { deploy: { region: 'us' } },
  },
);
expect(fromConfig.stdout).toEqual([
  'Deploying production to us\n',
]);

// Resolve from prompt answers
const fromPrompt = await runCommand(
  deploy,
  ['production'],
  {
    answers: ['ap'],
  },
);
expect(fromPrompt.stdout).toEqual([
  'Deploying production to ap\n',
]);

// Simulate prompt cancellation
const cancelled = await runCommand(deploy, ['production'], {
  prompter: createTestPrompter([PROMPT_CANCEL]),
});
expect(cancelled.exitCode).not.toBe(0);

// Activity events (spinners, progress)
const activity = await runCommand(build, []);
expect(activity.activity).toContainEqual(
  expect.objectContaining({ type: 'spinner:start' }),
);

RunOptions accepts: env, config, stdinData, answers, prompter, help, jsonMode, verbosity, and isTTY. Every dimension of command behavior is controllable from tests.

Package structure

Three subpath exports, each with a focused API surface:

| Import | Purpose | | -------------------------- | -------------------------------------------------------------------------------------- | | @kjanat/dreamcli | Schema builders, CLI runner, output, parsing, resolution, errors | | @kjanat/dreamcli/testkit | runCommand(), createCaptureOutput(), createTestPrompter(), createTestAdapter() | | @kjanat/dreamcli/runtime | createAdapter(), RuntimeAdapter, runtime detection, platform adapters |

ESM-only. Source included in package (src/).

Runtime support

| Runtime | Status | | ------------------ | ----------------------------------- | | Node.js >= 22.22.2 | Supported | | Bun >= 1.3.11 | Supported | | Deno >= 2.6.0 | Supported (JSR: @kjanat/dreamcli) |

Runtime detection is automatic. The core framework never imports platform-specific APIs directly — a thin RuntimeAdapter interface handles the divergent edges (argv, env, filesystem, TTY detection, exit behavior).

License

MIT © 2026 Kaj Kowalski