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.
Installation
Upstream docs assume Node + npm. Stay agnostic to the user's package manager — pnpm and bun work equally well.
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
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
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
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
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 viaparse/variadicon aparsedflag, not a new kind. - Positional modes:
tuple(fixed-shape, typed per-position) orarray(variadic homogeneous). - Variadic: set
variadic: truefor repeated occurrences, orvariadic: ","(or any separator) for delimited input. It's a property, not a kind.
Full details in references/parameters.md. Parser specifics (built-ins, custom, async) in 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.
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.
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.
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 and 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.
Upstream conventions worth keeping
strict: trueintsconfig.json. Stricli leans on inference — loose mode loses the whole value proposition.--versionappears only when aversionintegration is registered (1.3.0+) or the legacyversionInfoconfig key is set (1.2.x; deprecated on 1.3.0).--helpAllis 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
integrationsargument tobuildApplicationreplaces the defaults — re-registerhelp,helpAll, andversionor you lose those flags. Seereferences/integrations.md. - Upstream docs are npm-first; show
pnpm/bunequivalents when the user uses them.
References
routing.md—buildCommand,buildRouteMap,buildApplication,run, lazy loaders, aliases, default commandsparameters.md— flag kinds and positional modesparsers.md— built-in / custom / async parserscontext.md—CommandContext, custom context, testing, exit codesintegrations.md— integrations, lifecycle hooks, application flags, customizinghelp/version(1.3.0+)auto-complete.md— bash auto-complete integrationexamples.md— composite patterns, end-to-end apps, testing error paths
Scan to join WeChat group