Build complex CLIs with type safety and no dependencies
Define commands and organize them into nested route maps.
// Simple command
const cmd = buildCommand({
func: async function(flags, arg1, arg2) { /* ... */ },
parameters: { flags: { /* ... */ }, positional: { /* ... */ } },
docs: { brief: "Description" }
})
// Lazy-loaded command
const cmd = buildCommand({
loader: () => import("./commands/deploy.js"),
parameters: { /* ... */ },
docs: { brief: "Description" }
})
// Route map
const routes = buildRouteMap({
routes: { cmd1, cmd2, nested: nestedRouteMap },
defaultCommand: "cmd1", // optional
docs: { brief: "Description" },
aliases: { c: "cmd1" } // optional
})Builds a command from a function or loader with parameters and documentation.
function buildCommand<FLAGS, ARGS, CONTEXT>(
builderArgs: CommandBuilderArguments<FLAGS, ARGS, CONTEXT>
): Command<CONTEXT>Local Function:
buildCommand({
func: async function(flags, ...args) {
this.process.stdout.write("Hello\n");
},
parameters: {
flags: { /* FlagParametersForType<FLAGS> */ },
positional: { /* TypedPositionalParameters<ARGS> */ },
aliases: { /* single-char to flag name mapping */ }
},
docs: {
brief: "Short description",
fullDescription: "Optional longer description"
}
})Lazy-Loaded Function:
buildCommand({
loader: async () => import("./commands/deploy.js"), // or () => CommandFunction
parameters: { /* ... */ },
docs: { /* ... */ }
})
// In commands/deploy.ts:
export default async function(flags, ...args) {
// Command implementation
}CommandFunction Signature:
type CommandFunction<FLAGS, ARGS, CONTEXT> = (
this: CONTEXT,
flags: FLAGS,
...args: ARGS
) => void | Error | Promise<void | Error>Validation: Checks for reserved flags (--help, --helpAll, -h, -H), negation collisions, and variadic separators.
Organizes commands into nested structures with optional default command and aliases.
function buildRouteMap<R extends string, CONTEXT>(
args: RouteMapBuilderArguments<R, CONTEXT>
): RouteMap<CONTEXT>Example:
const dbRoutes = buildRouteMap({
routes: {
connect: connectCmd,
migrate: migrateCmd,
backup: backupCmd
},
defaultCommand: "connect", // Optional: runs when user types "myapp db"
docs: { brief: "Database commands" },
aliases: { c: "connect", m: "migrate", b: "backup" } // Optional
});
const app = buildApplication(
buildRouteMap({
routes: {
db: dbRoutes,
server: serverCmd
},
docs: { brief: "Application CLI" }
}),
{ name: "myapp" }
);
// Usage:
// myapp db connect --host localhost
// myapp db c --host localhost (using alias)
// myapp db (runs default connect command)
// myapp server --port 8080Validation: Route map must contain at least one route, aliases can't conflict with route names, default command must be a Command (not another RouteMap).
import { buildApplication, buildRouteMap, buildCommand, numberParser } from "@stricli/core";
// Commands
const deployCmd = buildCommand({
func: async function(flags, service) {
this.process.stdout.write(`Deploying ${service} to ${flags.env}\n`);
},
parameters: {
flags: {
env: { kind: "enum", values: ["dev", "staging", "prod"], brief: "Environment", default: "dev" },
force: { kind: "boolean", brief: "Force deployment" }
},
positional: {
kind: "tuple",
parameters: [{ brief: "Service name", parse: String }]
},
aliases: { e: "env", f: "force" }
},
docs: { brief: "Deploy service" }
});
const statusCmd = buildCommand({
func: async function(flags) {
this.process.stdout.write(`Checking status (verbose: ${flags.verbose})\n`);
},
parameters: {
flags: {
verbose: { kind: "boolean", brief: "Verbose output", default: false }
},
aliases: { v: "verbose" }
},
docs: { brief: "Check status" }
});
// Route map
const app = buildApplication(
buildRouteMap({
routes: {
deploy: deployCmd,
status: statusCmd
},
docs: { brief: "Deployment CLI" }
}),
{
name: "myapp",
scanner: { caseStyle: "allow-kebab-for-camel" }
}
);
// Usage:
// myapp deploy api --env prod --force
// myapp deploy api -e prod -f
// myapp status --verbose
// myapp status -vInstall with Tessl CLI
npx tessl i tessl/npm-stricli--core