Build complex CLIs with type safety and no dependencies
Built-in parsers for common types and utilities for building custom parsers. Supports synchronous and asynchronous parsing.
// Built-in parsers
booleanParser // "true" | "false" (strict)
looseBooleanParser // "yes|no|on|off|1|0|true|false|y|n|t|f"
numberParser // Any number (int or float)
String // No-op parser (just returns string)
// Build choice parser
buildChoiceParser(["dev", "staging", "prod"])
// Custom parser
const portParser: InputParser<number> = (input) => {
const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error("Port must be 1-65535");
}
return port;
}
// Async parser with context
const userIdParser: InputParser<string, MyContext> = async function(input) {
const exists = await this.database.userExists(input);
if (!exists) throw new Error(`User not found: ${input}`);
return input;
}Generic function that synchronously or asynchronously parses a string to an arbitrary type.
/**
* Generic function for parsing string input to type T
* @param this - Command context (provides access to process, custom context, etc.)
* @param input - Raw string input from command line
* @returns Parsed value of type T (or Promise of T)
*/
type InputParser<T, CONTEXT extends CommandContext = CommandContext> = (
this: CONTEXT,
input: string
) => T | Promise<T>;Usage Example:
import { InputParser, buildCommand } from "@stricli/core";
// Simple synchronous parser
const portParser: InputParser<number> = (input: string): number => {
const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${input}`);
}
return port;
};
// Async parser with context
interface MyContext extends CommandContext {
configService: {
validatePath: (path: string) => Promise<boolean>;
};
}
const pathParser: InputParser<string, MyContext> = async function(input: string) {
const isValid = await this.configService.validatePath(input);
if (!isValid) {
throw new Error(`Invalid path: ${input}`);
}
return input;
};
const command = buildCommand({
func: async function(flags) {
this.process.stdout.write(`Listening on port ${flags.port}\n`);
},
parameters: {
flags: {
port: {
kind: "parsed",
parse: portParser,
brief: "Server port (1-65535)"
}
}
},
docs: {
brief: "Start server"
}
});Parses input strings as booleans. Transforms to lowercase then checks against "true" and "false". Throws for invalid inputs.
/**
* Parses input strings as booleans (strict)
* @param input - Input string to parse
* @returns true for "true", false for "false"
* @throws SyntaxError if input is not "true" or "false" (case-insensitive)
*/
const booleanParser: (input: string) => boolean;Usage Example:
import { booleanParser, buildCommand } from "@stricli/core";
const command = buildCommand({
func: async function(flags) {
this.process.stdout.write(`Enabled: ${flags.enabled}\n`);
},
parameters: {
flags: {
enabled: {
kind: "parsed",
parse: booleanParser,
brief: "Enable feature (true/false)"
}
}
},
docs: {
brief: "Configure feature"
}
});
// Usage:
// myapp --enabled true
// myapp --enabled false
// myapp --enabled TRUE (case-insensitive)
// myapp --enabled yes (throws error - use looseBooleanParser)Parses input strings as booleans loosely, accepting multiple truthy and falsy values.
/**
* Parses input strings as booleans (loose)
* @param input - Input string to parse
* @returns true for truthy values, false for falsy values
* @throws SyntaxError if input doesn't match any recognized value
*
* Truthy values: "true", "t", "yes", "y", "on", "1"
* Falsy values: "false", "f", "no", "n", "off", "0"
* All comparisons are case-insensitive
*/
const looseBooleanParser: (input: string) => boolean;Usage Example:
import { looseBooleanParser, buildCommand } from "@stricli/core";
const command = buildCommand({
func: async function(flags) {
if (flags.confirm) {
this.process.stdout.write("Operation confirmed\n");
// Proceed with operation
} else {
this.process.stdout.write("Operation cancelled\n");
}
},
parameters: {
flags: {
confirm: {
kind: "parsed",
parse: looseBooleanParser,
brief: "Confirm operation (yes/no/on/off/1/0)"
}
}
},
docs: {
brief: "Process operation"
}
});
// Usage:
// myapp --confirm yes
// myapp --confirm on
// myapp --confirm 1
// myapp --confirm noParses numeric values from string input.
/**
* Parse numeric values
* @param input - Input string to parse
* @returns Parsed number
* @throws SyntaxError if input cannot be parsed as a number
*/
const numberParser: (input: string) => number;Builds a parser that validates against a set of choices.
/**
* Build a parser that validates against a set of choices
* @param choices - Array of valid string choices
* @returns Parser function that validates input against choices
*/
function buildChoiceParser<T extends string>(choices: readonly T[]): InputParser<T>;Example:
const logLevelParser = buildChoiceParser(["debug", "info", "warn", "error"]);
flags: {
logLevel: {
kind: "parsed",
parse: logLevelParser,
brief: "Log level",
default: "info"
}
}Note: For string literal unions, prefer enum flags over parsed with buildChoiceParser for better UX. Use buildChoiceParser when you need custom error messages, complex validation, or non-string types.
const portParser: InputParser<number> = (input) => {
const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${input}`);
}
return port;
};const filePathParser: InputParser<string> = async (input) => {
try {
await access(input, constants.R_OK);
return input;
} catch {
throw new Error(`File not found or not readable: ${input}`);
}
};interface MyContext extends CommandContext {
database: { userExists: (id: string) => Promise<boolean> };
}
const userIdParser: InputParser<string, MyContext> = async function(input) {
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
throw new Error(`Invalid user ID format: ${input}`);
}
const exists = await this.database.userExists(input);
if (!exists) {
throw new Error(`User not found: ${input}`);
}
return input;
};interface Range {
min: number;
max: number;
}
const rangeParser: InputParser<Range> = (input) => {
const match = /^(\d+)-(\d+)$/.exec(input);
if (!match) {
throw new Error(`Invalid range format, expected "min-max": ${input}`);
}
const min = parseInt(match[1], 10);
const max = parseInt(match[2], 10);
if (min > max) {
throw new Error(`Invalid range, min must be <= max: ${input}`);
}
return { min, max };
};// URL
const urlParser: InputParser<URL> = (input) => {
try {
return new URL(input);
} catch {
throw new Error(`Invalid URL: ${input}`);
}
};
// Date (ISO format)
const isoDateParser: InputParser<Date> = (input) => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(input)) {
throw new Error(`Date must be YYYY-MM-DD: ${input}`);
}
const date = new Date(input);
if (isNaN(date.getTime())) {
throw new Error(`Invalid date: ${input}`);
}
return date;
};
// JSON
const jsonParser: InputParser<unknown> = (input) => {
try {
return JSON.parse(input);
} catch (err) {
throw new Error(`Invalid JSON: ${err.message}`);
}
};
// Email
const emailParser: InputParser<string> = (input) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
throw new Error(`Invalid email: ${input}`);
}
return input.toLowerCase();
};const command = buildCommand({
func: async function(flags) {
this.process.stdout.write(`Creating task:\n`);
this.process.stdout.write(` Title: ${flags.title}\n`);
this.process.stdout.write(` Assignee: ${flags.assignee}\n`);
if (flags.dueInDays) {
const due = new Date();
due.setDate(due.getDate() + flags.dueInDays);
this.process.stdout.write(` Due: ${due.toDateString()}\n`);
}
},
parameters: {
flags: {
title: { kind: "parsed", parse: String, brief: "Task title" },
assignee: { kind: "parsed", parse: emailParser, brief: "Assignee email" },
dueInDays: { kind: "parsed", parse: numberParser, brief: "Due date (days from now)", optional: true }
},
aliases: { t: "title", a: "assignee", d: "dueInDays" }
},
docs: { brief: "Create task" }
});
// Usage:
// myapp -t "Fix bug" -a john@example.com -d 3
// myapp --title "Review PR" --assignee jane@example.comimport { access, constants } from "fs/promises";
const filePathParser: InputParser<string> = async (input) => {
try {
await access(input, constants.R_OK);
return input;
} catch {
throw new Error(`File not found or not readable: ${input}`);
}
};const urlParser: InputParser<URL> = (input) => {
try {
return new URL(input);
} catch {
throw new Error(`Invalid URL: ${input}`);
}
};const isoDateParser: InputParser<Date> = (input) => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(input)) {
throw new Error(`Date must be YYYY-MM-DD: ${input}`);
}
const date = new Date(input);
if (isNaN(date.getTime())) {
throw new Error(`Invalid date: ${input}`);
}
return date;
};const jsonParser: InputParser<unknown> = (input) => {
try {
return JSON.parse(input);
} catch (err) {
throw new Error(`Invalid JSON: ${err.message}`);
}
};interface Range {
min: number;
max: number;
}
const rangeParser: InputParser<Range> = (input) => {
const match = /^(\d+)-(\d+)$/.exec(input);
if (!match) {
throw new Error(`Invalid range format, expected "min-max": ${input}`);
}
const min = parseInt(match[1], 10);
const max = parseInt(match[2], 10);
if (min > max) {
throw new Error(`Invalid range, min must be <= max: ${input}`);
}
return { min, max };
};Install with Tessl CLI
npx tessl i tessl/npm-stricli--core