# Stricli

> Build type-safe TypeScript CLIs with Bloomberg's Stricli framework. Use when the project already depends on `@stricli/core` or the user names Stricli — authoring a new CLI, changing typed flags/positionals/parsers, wiring subcommand routing via `buildRouteMap`, testing via `run(app, argv, context)`, or configuring bash auto-complete via `@stricli/auto-complete`. For the `cleye` argv parser use the cleye skill instead; skip for commander/yargs/oclif/minimist and for generic "which CLI framework should I use?" questions.

- Skill: `johnie/stricli` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add johnie/stricli`
- Raw SKILL.md: https://api.skillmd.com/api/skills/johnie/stricli/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: johnie (https://skillmd.com/u/johnie)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/johnie/stricli

---


# Stricli CLI Framework

Stricli is Bloomberg's type-safe CLI framework for TypeScript. Strongly-typed flags and positional arguments, explicit command routing, automatic help generation, and an isolated `CommandContext` per run.

Reference files live in `${CLAUDE_SKILL_DIR}/references/`.

## When NOT to use

- The user has an existing CLI on a different framework (commander, yargs, oclif, minimist) — this skill doesn't migrate, and the APIs don't translate.
- The project uses `cleye` — different library, non-transferable API. Use the cleye skill.
- Generic "which CLI framework should I use?" — that's a design conversation, not a Stricli question.
- Non-TypeScript CLIs — Stricli's core value is its compile-time type safety.
- Runtime debugging of an installed CLI (not developing it) — use shell/debugging tooling.

## Core API surface

Stricli's public API is intentionally narrow. If something isn't listed here or in the references, assume it doesn't exist — checking the upstream repo is faster than guessing, and invented APIs compile until they don't.

| Entry point | Purpose |
| --- | --- |
| `buildCommand({ func \| loader, parameters, docs })` | Define a single command |
| `buildRouteMap({ routes, docs, aliases?, defaultCommand? })` | Compose subcommands |
| `buildApplication(rootCommandOrRouteMap, config, integrations?)` | Wrap with app-level config (name, scanner, …); third argument registers integrations (1.3.0+) |
| `run(app, inputs, context)` | Execute the app against tokenized input + a runtime context |
| `CommandContext` | The shape that runtime context extends |
| `help(config)` / `version(config)` | Built-in integrations for `--help` / `--version` (1.3.0+) |

## Version awareness

Check the installed `@stricli/core` before using version-gated API. The `integrations` argument, lifecycle hooks, and the exported `help`/`version` factories arrived in **1.3.0**; `withNegated` on boolean flags and defaults on variadic flags arrived in **1.2.5**. The rest of this skill applies across the 1.x line. Details in [`references/integrations.md`](references/integrations.md).

## Installation

Upstream docs assume Node + npm. Stay agnostic to the user's package manager — pnpm and bun work equally well.

```bash
npm install @stricli/core              # required
npm install @stricli/auto-complete     # optional, bash completion
# pnpm add / bun add work the same way
```

## Scaffolding a new app

```bash
npx @stricli/create-app@latest my-app
# pnpm dlx / bunx work the same way
```

The generator produces the reference directory layout. For hand-written apps, follow the quick start below.

## Quick start: single-command CLI

### 1. Define the command

```typescript
import { buildCommand, type CommandContext } from "@stricli/core";

interface GreetFlags {
  readonly shout?: boolean;
}

export const greetCommand = buildCommand({
  docs: { brief: "Print a greeting" },
  parameters: {
    flags: {
      shout: {
        kind: "boolean",
        brief: "Uppercase the greeting",
        optional: true,
      },
    },
    positional: {
      kind: "tuple",
      parameters: [
        { brief: "Name to greet", parse: String, placeholder: "name" },
      ],
    },
  },
  func(this: CommandContext, flags: GreetFlags, name: string) {
    const message = `Hello, ${name}!`;
    this.process.stdout.write(
      `${flags.shout ? message.toUpperCase() : message}\n`
    );
  },
});
```

### 2. Build the application

```typescript
import { buildApplication, help, version } from "@stricli/core";
import { version as currentVersion } from "../package.json";
import { greetCommand } from "./commands/greet";

const formatting = {
  useAliasInUsageLine: false,
  onlyRequiredInUsageLine: false,
  caseStyle: "original",
} as const;

export const app = buildApplication(
  greetCommand,
  { name: "my-cli" },
  {
    help: help({
      brief: "Print help information and exit",
      defaultForRouteMap: true,
      formatting,
    }),
    helpAll: help({
      brief:
        "Print help information (including hidden commands/flags) and exit",
      alias: "H",
      hidden: true,
      includeHidden: true,
      formatting,
    }),
    version: version({
      brief: "Print version information and exit",
      info: { currentVersion },
    }),
  }
);
```

On 1.3.0+ the `version` integration is the primary way to enable `--version`; the config key `versionInfo: { currentVersion }` (with no third argument) is the legacy 1.2.x form and is `@deprecated` on 1.3.0, though it still works. Passing the third argument replaces every default, which is why `help` and `helpAll` are re-registered above.

### 3. Run it

```typescript
import { run } from "@stricli/core";
import { app } from "./app";

await run(app, process.argv.slice(2), { process });
```

## Parameter model

- **Flag kinds**: `parsed`, `enum`, `boolean`, `counter`. Everything else is expressed via `parse` / `variadic` on a `parsed` flag, not a new kind.
- **Positional modes**: `tuple` (fixed-shape, typed per-position) or `array` (variadic homogeneous).
- **Variadic**: set `variadic: true` for repeated occurrences, or `variadic: ","` (or any separator) for delimited input. It's a property, not a kind.

Full details in [`references/parameters.md`](references/parameters.md). Parser specifics (built-ins, custom, async) in [`references/parsers.md`](references/parsers.md).

## Recommended workflow

### Single-command CLI

`buildCommand` → `buildApplication(command, config)` → `run(app, argv, context)`.

### Multi-command CLI

Define commands independently, compose with `buildRouteMap`, add `aliases` / `defaultCommand` where UX benefits. See [`references/routing.md`](references/routing.md).

### Large CLIs — prefer the `loader` pattern

For commands whose implementation is expensive to import (heavy transitive deps, slow module-level work), use `loader` instead of inline `func`. Stricli resolves the loader only when that command is actually invoked, keeping startup fast.

```typescript
import { buildCommand, numberParser } from "@stricli/core";

export const analyzeCommand = buildCommand({
  docs: { brief: "Analyze a report" },
  parameters: {
    flags: {
      depth: {
        kind: "parsed",
        parse: numberParser,
        brief: "Traversal depth",
        optional: true,
        default: "1",
      },
    },
  },
  loader: async () => import("./impl"),
});
```

Rule of thumb: `func` for a few-line handler you don't mind parsing at app start; `loader` when the implementation (or its imports) would dominate cold start for unrelated commands. See [`references/routing.md`](references/routing.md).

## Context and testing

- Runtime context extends `CommandContext`. Inject logger / clients / clocks there — not module-level singletons — so tests can swap them.
- Command handlers receive context through `this`.
- Test either end-to-end via `run(app, inputs, ctx)` with a fake context, or import the command's implementation directly for pure unit tests.

See [`references/context.md`](references/context.md) and [`references/examples.md`](references/examples.md) (including `Testing Error Paths` for parser/missing-arg/enum-error tests).

## Auto-complete

`@stricli/auto-complete` supports bash only. Integrate via the standalone install/uninstall flow plus `buildInstallCommand()` / `buildUninstallCommand()` added to your app. Details in [`references/auto-complete.md`](references/auto-complete.md).

## Upstream conventions worth keeping

- `strict: true` in `tsconfig.json`. Stricli leans on inference — loose mode loses the whole value proposition.
- `--version` appears only when a `version` integration is registered (1.3.0+) or the legacy `versionInfo` config key is set (1.2.x; deprecated on 1.3.0).
- `--helpAll` is built-in and surfaces hidden commands and flags.
- Reserved short flags: `-h` (help), `-H` (helpAll), `-v` (version, when enabled).
- On 1.3.0+, passing the `integrations` argument to `buildApplication` **replaces** the defaults — re-register `help`, `helpAll`, and `version` or you lose those flags. See [`references/integrations.md`](references/integrations.md).
- Upstream docs are npm-first; show `pnpm` / `bun` equivalents when the user uses them.

## References

- [`routing.md`](references/routing.md) — `buildCommand`, `buildRouteMap`, `buildApplication`, `run`, lazy loaders, aliases, default commands
- [`parameters.md`](references/parameters.md) — flag kinds and positional modes
- [`parsers.md`](references/parsers.md) — built-in / custom / async parsers
- [`context.md`](references/context.md) — `CommandContext`, custom context, testing, exit codes
- [`integrations.md`](references/integrations.md) — integrations, lifecycle hooks, application flags, customizing `help`/`version` (1.3.0+)
- [`auto-complete.md`](references/auto-complete.md) — bash auto-complete integration
- [`examples.md`](references/examples.md) — composite patterns, end-to-end apps, **testing error paths**

## External

- [Stricli GitHub](https://github.com/bloomberg/stricli)
- [Official documentation](https://bloomberg.github.io/stricli/)
- [`@stricli/core` on npm](https://www.npmjs.com/package/@stricli/core)
- [`@stricli/create-app` on npm](https://www.npmjs.com/package/@stricli/create-app)

