Build complex CLIs with type safety and no dependencies
npx @tessl/cli install tessl/npm-stricli--core@1.2.0@stricli/core is a comprehensive TypeScript library for building type-safe, complex command-line interface (CLI) applications with zero runtime dependencies. It provides a complete framework including command builders, parameter parsing, routing, automatic help text generation, shell completion support, and extensive configuration options. The library leverages TypeScript's type system to provide full compile-time type safety for CLI parameters and commands.
Package: @stricli/core | Type: npm | Language: TypeScript
npm install @stricli/coreimport {
buildApplication,
buildCommand,
buildRouteMap,
run,
type CommandContext,
type Application
} from "@stricli/core";For CommonJS:
const {
buildApplication,
buildCommand,
buildRouteMap,
run
} = require("@stricli/core");import { buildApplication, buildCommand, run } from "@stricli/core";
const app = buildApplication(
buildCommand({
func: async function(flags, name) {
this.process.stdout.write(`Hello, ${name || flags.default}!\n`);
},
parameters: {
flags: {
default: { kind: "parsed", parse: String, brief: "Default name", default: "World" }
},
positional: {
kind: "tuple",
parameters: [{ brief: "Name", parse: String, optional: true }]
}
},
docs: { brief: "Greet someone" }
}),
{ name: "greet" }
);
await run(app, process.argv.slice(2), { process });Common Patterns Cheat Sheet
buildCommand({
func: async function(flags) { /* ... */ },
parameters: { flags: { /* ... */ } },
docs: { brief: "Description" }
})buildApplication(
buildRouteMap({
routes: { cmd1: command1, cmd2: command2 },
docs: { brief: "CLI description" }
}),
{ name: "myapp" }
)// Boolean
verbose: { kind: "boolean", brief: "Enable verbose", default: false }
// Counter (repeatable -vvv)
verbosity: { kind: "counter", brief: "Verbosity level" }
// Enum (predefined values)
env: { kind: "enum", values: ["dev", "prod"], brief: "Environment", default: "dev" }
// Parsed (custom type)
port: { kind: "parsed", parse: numberParser, brief: "Port", default: "3000" }
// Variadic (multiple values)
files: { kind: "parsed", parse: String, variadic: true, brief: "Files" }
// Optional
timeout: { kind: "parsed", parse: numberParser, brief: "Timeout", optional: true }// Tuple (fixed args)
positional: {
kind: "tuple",
parameters: [
{ brief: "Source", parse: String },
{ brief: "Dest", parse: String, optional: true }
]
}
// Array (variadic args)
positional: {
kind: "array",
parameter: { brief: "Files", parse: String },
minimum: 1
}parameters: {
flags: { verbose: { /* ... */ }, output: { /* ... */ } },
aliases: { v: "verbose", o: "output" } // -v, -o
}@stricli/core is built around several key components:
buildApplication and run functions for creating and executing CLI applicationsbuildCommand and buildRouteMap for defining commands and nested command structuresCore functionality for creating and running CLI applications. Supports both single-command and multi-command (route map) applications with full configuration.
/**
* Builds a CLI application from a command or route map with configuration
*/
function buildApplication<CONTEXT extends CommandContext>(
root: Command<CONTEXT>,
appConfig: PartialApplicationConfiguration
): Application<CONTEXT>;
function buildApplication<CONTEXT extends CommandContext>(
root: RouteMap<CONTEXT>,
config: PartialApplicationConfiguration
): Application<CONTEXT>;
/**
* Runs a CLI application with given inputs and context
*/
async function run<CONTEXT extends CommandContext>(
app: Application<CONTEXT>,
inputs: readonly string[],
context: StricliDynamicCommandContext<CONTEXT>
): Promise<void>;
interface Application<CONTEXT extends CommandContext> {
root: Command<CONTEXT> | RouteMap<CONTEXT>;
config: ApplicationConfiguration;
defaultText: ApplicationText;
}System for defining individual commands and organizing them into nested route maps. Supports both immediate and lazy-loaded command functions with full type inference.
/**
* Builds a command with parameters, action, and documentation
*/
function buildCommand<
const FLAGS extends Readonly<Partial<Record<keyof FLAGS, unknown>>> = NonNullable<unknown>,
const ARGS extends BaseArgs = [],
const CONTEXT extends CommandContext = CommandContext
>(builderArgs: CommandBuilderArguments<FLAGS, ARGS, CONTEXT>): Command<CONTEXT>;
/**
* Builds a route map for organizing multiple commands
*/
function buildRouteMap<
R extends string,
CONTEXT extends CommandContext = CommandContext
>({
routes,
defaultCommand,
docs,
aliases
}: RouteMapBuilderArguments<R, CONTEXT>): RouteMap<CONTEXT>;
interface Command<CONTEXT extends CommandContext> {
kind: typeof CommandSymbol;
loader: CommandFunctionLoader<BaseFlags, BaseArgs, CONTEXT>;
parameters: CommandParameters;
usesFlag: (flagName: string) => boolean;
brief: string;
formatUsageLine: (args: UsageFormattingArguments) => string;
formatHelp: (args: HelpFormattingArguments) => string;
}
interface RouteMap<CONTEXT extends CommandContext> {
kind: typeof RouteMapSymbol;
getRoutingTargetForInput: (input: string) => Command<CONTEXT> | RouteMap<CONTEXT> | undefined;
getDefaultCommand: () => Command<CONTEXT> | undefined;
getOtherAliasesForInput: (input: string, caseStyle: ScannerCaseStyle) => Readonly<Record<DisplayCaseStyle, readonly string[]>>;
getAllEntries: () => readonly RouteMapEntry<CONTEXT>[];
brief: string;
formatUsageLine: (args: UsageFormattingArguments) => string;
formatHelp: (args: HelpFormattingArguments) => string;
}
type CommandFunction<FLAGS extends BaseFlags, ARGS extends BaseArgs, CONTEXT extends CommandContext> =
(this: CONTEXT, flags: FLAGS, ...args: ARGS) => void | Error | Promise<void | Error>;Built-in parsers for common types and utilities for building custom parsers. Supports synchronous and asynchronous parsing.
/**
* Generic input parser type
*/
type InputParser<T, CONTEXT extends CommandContext = CommandContext> =
(this: CONTEXT, input: string) => T | Promise<T>;
/**
* Parse boolean values (strict: "true" or "false")
*/
const booleanParser: (input: string) => boolean;
/**
* Parse boolean values (loose: "yes", "no", "on", "off", "1", "0", etc.)
*/
const looseBooleanParser: (input: string) => boolean;
/**
* Parse numeric values
*/
const numberParser: (input: string) => number;
/**
* Build a parser that validates against a set of choices
*/
function buildChoiceParser<T extends string>(choices: readonly T[]): InputParser<T>;interface PartialApplicationConfiguration {
name: string
versionInfo?: VersionInfo
scanner?: Partial<ScannerConfiguration>
documentation?: Partial<DocumentationConfiguration>
completion?: Partial<CompletionConfiguration>
localization?: Partial<LocalizationConfiguration>
determineExitCode?: (exc: unknown) => number
}Scanner Options:
caseStyle: "original" | "allow-kebab-for-camel" (default: "original")allowArgumentEscapeSequence: boolean (default: false) - Enable -- separatorDocumentation Options:
caseStyle: "original" | "convert-camel-to-kebab"useAliasInUsageLine: boolean (default: false)disableAnsiColor: boolean (default: false)interface CommandContext {
process: { stdout: Writable; stderr: Writable }
}
// Extend for custom context
interface MyContext extends CommandContext {
database: Database
logger: Logger
}
await run(app, inputs, {
process,
async forCommand(info) {
return { process, database: await connectDB(), logger: createLogger() }
}
})const ExitCode = {
Success: 0,
CommandRunError: 1,
InternalError: -1,
CommandLoadError: -2,
ContextLoadError: -3,
InvalidArgument: -4,
UnknownCommand: -5
}Comprehensive error classes for parameter parsing and validation with detailed error messages and "did you mean?" suggestions.
/**
* Base class for all argument scanner errors
*/
abstract class ArgumentScannerError extends Error {}
class FlagNotFoundError extends ArgumentScannerError {
readonly input: string;
readonly corrections: readonly string[];
readonly aliasName?: string;
}
class AliasNotFoundError extends ArgumentScannerError {
readonly input: string;
}
class ArgumentParseError extends ArgumentScannerError {
readonly externalFlagNameOrPlaceholder: string;
readonly input: string;
readonly exception: unknown;
}
class EnumValidationError extends ArgumentScannerError {
readonly externalFlagName: string;
readonly input: string;
readonly values: readonly string[];
}
class UnsatisfiedFlagError extends ArgumentScannerError {
readonly externalFlagName: string;
readonly nextFlagName?: string;
}
class UnsatisfiedPositionalError extends ArgumentScannerError {
readonly placeholder: string;
readonly limit?: [minimum: number, count: number];
}
class UnexpectedPositionalError extends ArgumentScannerError {
readonly expectedCount: number;
readonly input: string;
}
class UnexpectedFlagError extends ArgumentScannerError {
readonly externalFlagName: string;
readonly previousInput: string;
readonly input: string;
}
class InvalidNegatedFlagSyntaxError extends ArgumentScannerError {
readonly externalFlagName: string;
readonly valueText: string;
}
/**
* Utility for formatting scanner error messages
*/
function formatMessageForArgumentScannerError(
error: ArgumentScannerError,
formatter: Partial<ArgumentScannerErrorFormatter>
): string;Automatic help text generation for all commands with customizable formatting and localization support. Includes shell completion proposals.
/**
* Generates help text for all commands in an application
*/
function generateHelpTextForAllCommands(
app: Application<CommandContext>,
locale?: string
): readonly DocumentedCommand[];
type DocumentedCommand = readonly [route: string, helpText: string];
/**
* Proposes completions for partial input (aliased as proposeCompletions)
*/
async function proposeCompletionsForApplication<CONTEXT extends CommandContext>(
app: Application<CONTEXT>,
rawInputs: readonly string[],
context: StricliDynamicCommandContext<CONTEXT>
): Promise<readonly InputCompletion[]>;
type InputCompletion = ArgumentCompletion | RoutingTargetCompletion;
interface RoutingTargetCompletion {
kind: "routing-target:command" | "routing-target:route-map";
completion: string;
brief: string;
}import {
buildApplication,
buildRouteMap,
buildCommand,
run,
numberParser,
type CommandContext
} from "@stricli/core";
// Custom context
interface AppContext extends CommandContext {
config: { apiUrl: string };
}
// Commands
const deployCmd = buildCommand({
func: async function(this: AppContext, flags, service) {
this.process.stdout.write(
`Deploying ${service} to ${flags.env} (${this.config.apiUrl})\n`
);
if (flags.force) this.process.stdout.write("Force mode enabled\n");
},
parameters: {
flags: {
env: { kind: "enum", values: ["dev", "staging", "prod"], brief: "Environment", default: "dev" },
force: { kind: "boolean", brief: "Force deployment", default: false },
timeout: { kind: "parsed", parse: numberParser, brief: "Timeout (seconds)", optional: true }
},
positional: {
kind: "tuple",
parameters: [{ brief: "Service name", parse: String }]
},
aliases: { e: "env", f: "force", t: "timeout" }
},
docs: { brief: "Deploy a service" }
});
const statusCmd = buildCommand({
func: async function() {
this.process.stdout.write("All systems operational\n");
},
parameters: {},
docs: { brief: "Check status" }
});
// Application
const app = buildApplication(
buildRouteMap({
routes: { deploy: deployCmd, status: statusCmd },
docs: { brief: "Deployment CLI" }
}),
{
name: "myapp",
versionInfo: { currentVersion: "1.0.0" },
scanner: { caseStyle: "allow-kebab-for-camel", allowArgumentEscapeSequence: true }
}
);
// Run with custom context
await run(app, process.argv.slice(2), {
process,
locale: "en",
async forCommand() {
return { process, config: { apiUrl: "https://api.example.com" } };
}
});Comprehensive localization system with customizable text for all user-facing strings including help text, error messages, and documentation.
interface ApplicationText extends ApplicationErrorFormatting {
keywords: DocumentationKeywords;
headers: DocumentationHeaders;
briefs: DocumentationBriefs;
currentVersionIsNotLatest: (args: {
currentVersion: string;
latestVersion: string;
upgradeCommand?: string;
ansiColor: boolean;
}) => string;
}
/**
* Default English text implementation
*/
const text_en: ApplicationText;
interface DocumentationKeywords {
default: string;
separator: string;
}
interface DocumentationHeaders {
usage: string;
aliases: string;
commands: string;
flags: string;
arguments: string;
}
interface DocumentationBriefs {
help: string;
helpAll: string;
version: string;
argumentEscapeSequence: string;
}Standard exit codes returned by Stricli applications for different execution outcomes.
/**
* Enumeration of all possible exit codes
*/
const ExitCode: {
readonly UnknownCommand: -5;
readonly InvalidArgument: -4;
readonly ContextLoadError: -3;
readonly CommandLoadError: -2;
readonly InternalError: -1;
readonly Success: 0;
readonly CommandRunError: 1;
};
/**
* Environment variable names used by Stricli
*/
type EnvironmentVariableName =
| "STRICLI_SKIP_VERSION_CHECK"
| "STRICLI_NO_COLOR";interface StricliProcess {
stdout: Writable;
stderr: Writable;
env?: Readonly<Partial<Record<string, string>>>;
exitCode?: number | string | null;
}
interface Writable {
write: (str: string) => void;
getColorDepth?: (env?: Readonly<Partial<Record<string, string>>>) => number;
}
interface CommandInfo {
prefix: readonly string[];
}
type StricliCommandContextBuilder<CONTEXT extends CommandContext> =
(info: CommandInfo) => CONTEXT | Promise<CONTEXT>;
interface CommandModule<
FLAGS extends BaseFlags,
ARGS extends BaseArgs,
CONTEXT extends CommandContext
> {
default: CommandFunction<FLAGS, ARGS, CONTEXT>;
}
type CommandFunctionLoader<
FLAGS extends BaseFlags,
ARGS extends BaseArgs,
CONTEXT extends CommandContext
> = () => Promise<CommandModule<FLAGS, ARGS, CONTEXT> | CommandFunction<FLAGS, ARGS, CONTEXT>>;STRICLI_SKIP_VERSION_CHECK=1 - Skip automatic version checkingSTRICLI_NO_COLOR=1 - Disable ANSI color output