johnieskills

Builds type-safe command-line tools with 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.

$ npx skills add johnie/skills --skill stricli
skills/stricli/references/examples.mdGitHub

Stricli Examples

Patterns that complement the reference files. For parsers, context, routing, and auto-complete, see the dedicated reference files — this page covers composite scenarios and patterns not shown elsewhere. Variadic flags (variadic: true / variadic: ",") are covered in parameters.md — Variadic.

Multi-Command Application with Aliases

Combines buildRouteMap aliases, scanner case style, and version info into a realistic application entry point.

import { buildApplication, buildRouteMap } from "@stricli/core";

import { version } from "../package.json";
import { createCommand } from "./commands/create";
import { listCommand } from "./commands/list";
import { removeCommand } from "./commands/remove";

const projectRoutes = buildRouteMap({
  routes: {
    create: createCommand,
    list: listCommand,
    remove: removeCommand,
  },
  aliases: {
    ls: "list",
    rm: "remove",
  },
  docs: {
    brief: "Manage projects",
  },
});

export const app = buildApplication(projectRoutes, {
  name: "pm",
  versionInfo: {
    currentVersion: version,
  },
  scanner: {
    caseStyle: "allow-kebab-for-camel",
  },
});

versionInfo is the 1.2.x form. On 1.3.0+ it is deprecated: pass help/helpAll/version integrations as the third argument instead — see integrations.md.

Usage:

pm create my-app
pm ls
pm rm my-app
pm --version

End-to-End: Command with Custom Context and Testing

Shows the full lifecycle — command definition, application wiring, context setup, and tests — in one place. Individual pieces are covered in more detail in Context and Commands, Routing, and Applications.

Command

// src/commands/deploy.ts
import { buildCommand, type CommandContext } from "@stricli/core";

interface DeployContext extends CommandContext {
  readonly logger: Logger;
  readonly deployer: Deployer;
}

interface DeployFlags {
  readonly dryRun?: boolean;
}

export const deployCommand = buildCommand<
  DeployFlags,
  [env: string],
  DeployContext
>({
  docs: {
    brief: "Deploy to an environment",
  },
  parameters: {
    flags: {
      dryRun: {
        kind: "boolean",
        brief: "Preview without applying",
        optional: true,
      },
    },
    positional: {
      kind: "tuple",
      parameters: [
        {
          brief: "Target environment",
          parse: String,
          placeholder: "env",
        },
      ],
    },
  },
  async func(this, flags, env) {
    this.logger.info(`Deploying to ${env}`);
    if (!flags.dryRun) {
      await this.deployer.deploy(env);
    }
    this.process.stdout.write(`Done (dry=${!!flags.dryRun})\n`);
  },
});

Application

// src/app.ts
import { buildApplication } from "@stricli/core";

import { version } from "../package.json";
import { deployCommand } from "./commands/deploy";

// 1.2.x form; on 1.3.0+ pass help/helpAll/version integrations as the third argument (integrations.md)
export const app = buildApplication(deployCommand, {
  name: "deploy-cli",
  versionInfo: { currentVersion: version },
});

Entry point

// src/index.ts
import { run } from "@stricli/core";

import { app } from "./app";

await run(app, process.argv.slice(2), {
  process,
  logger: createLogger(),
  deployer: createDeployer(),
});

Test

Uses the buildContextForTest() helper from context.md — Testing with Context, extended with the logger and deployer fields this command needs:

import { run } from "@stricli/core";

import { app } from "../src/app";

it("deploys in dry-run mode", async () => {
  // Object.assign keeps the helper's stdout/stderr getters live; a spread would snapshot them
  const ctx = Object.assign(buildContextForTest(), {
    logger: { info: () => {} },
    deployer: { deploy: async () => {} },
  });
  await run(app, ["--dryRun", "staging"], ctx);
  expect(ctx.stdout).toContain("dry=true");
});

Testing Error Paths

Stricli prints parameter errors to stderr and sets a non-zero exit code via the process context. Real users hit these paths often (forgotten flags, typos in enum values, parsers that throw on bad input) — your tests should cover them too, because the error message is the user's first contact with your CLI.

Missing required positional

it("errors when the target env is missing", async () => {
  const ctx = buildContextForTest();
  await run(app, ["--dryRun"], ctx);
  expect(ctx.stderr).toMatch(/env/i); // Stricli names the missing positional
  // exit code is surfaced via process.exitCode on the context's process
});

Invalid enum value

// flag: region: { kind: "enum", values: ["us", "eu"] as const }
it("rejects an unknown region", async () => {
  const ctx = buildContextForTest();
  await run(app, ["--region", "apac", "staging"], ctx);
  expect(ctx.stderr).toMatch(/region/);
  expect(ctx.stderr).toMatch(/us|eu/);
});

Parser that throws

Custom parsers should throw a descriptive Error; Stricli surfaces the message to the user. Verify both the failure and the message.

const positiveIntParser = (raw: string) => {
  const n = Number(raw);
  if (!Number.isInteger(n) || n <= 0) {
    throw new Error(`expected a positive integer, got "${raw}"`);
  }
  return n;
};

it("reports a helpful error from a custom parser", async () => {
  const ctx = buildContextForTest();
  await run(app, ["--retries", "-1", "staging"], ctx);
  expect(ctx.stderr).toContain("positive integer");
});

Help / version are not errors

-h, --help, --helpAll, and -v/--version (when version info is configured) write to stdout and exit with code 0. If your tests assert error-on-any-stderr-output, carve these out.