alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,129 lines (1,128 loc) • 39.5 kB
JavaScript
import { $atom, $env, $hook, $inject, $module, $state, Alepha, AlephaError, KIND, Primitive, TypeBoxError, coerceScalar, createPrimitive, z } from "alepha";
import { stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import { $logger, ConsoleColorProvider } from "alepha/logger";
import { FileSystemProvider, ShellProvider } from "alepha/system";
import * as fs from "node:fs/promises";
import { cp, glob, rm } from "node:fs/promises";
import { DateTimeProvider } from "alepha/datetime";
//#region ../../src/command/helpers/Asker.ts
/**
* Reads interactive input from the terminal using plain readline prompts.
*
* One straightforward code path: questions are printed through the logger
* and answers are read with Node's `readline`. No raw-mode cursor control,
* no ANSI framing — output stays greppable and works the same in a TTY,
* under CI, or when piped.
*/
var Asker = class {
log = $logger();
ask;
alepha = $inject(Alepha);
constructor() {
this.ask = this.createAskMethod();
}
createAskMethod() {
const askFn = async (question, options = {}) => {
return await this.prompt(question, options);
};
askFn.permission = async (question) => {
return (await this.prompt(`${question} [Y/n]`, { schema: z.enum([
"Y",
"y",
"n",
"no",
"yes"
]).default("Y") })).charAt(0).toLowerCase() === "y";
};
askFn.intro = (title) => {
this.log.info(title);
};
askFn.outro = (message) => {
if (message) this.log.info(message);
};
return askFn;
}
async prompt(question, options) {
const rl = this.createPromptInterface();
let value;
try {
do
try {
this.log.info(question);
const answer = await rl.question("> ");
if (options.schema) {
const raw = answer ? answer.trim() : void 0;
value = this.alepha.codec.decode(options.schema, raw === void 0 ? void 0 : coerceScalar(options.schema, raw));
} else value = String(answer.trim());
if (options.validate) options.validate(value);
} catch (error) {
if (error instanceof AlephaError) {
this.log.error(`${error.message}\n`);
value = void 0;
} else throw error;
}
while (value === void 0);
} finally {
rl.close();
}
return value;
}
createPromptInterface() {
return createInterface({
input: stdin,
output: stdout
});
}
};
//#endregion
//#region ../../src/command/helpers/EnvUtils.ts
var EnvUtils = class {
log = $logger();
fs = $inject(FileSystemProvider);
/**
* Load environment variables from .env files into process.env.
*
* Variables that already exist in process.env are NOT overwritten,
* matching the standard dotenv convention where the shell environment
* takes precedence over .env file values.
*
* By default, it loads from ".env" and ".env.local".
* You can specify additional files to load, e.g. [".env", ".env.production"].
*/
async loadEnv(root, files = [".env"]) {
const vars = await this.parseEnv(root, files);
for (const [key, value] of Object.entries(vars)) if (process.env[key] === void 0) process.env[key] = value;
}
/**
* Parse environment variables from .env files without mutating process.env.
*
* Returns a merged record from all files (later files override earlier ones).
* For each file, also tries the `.local` variant (e.g. `.env.production.local`).
*/
async parseEnv(root, files = [".env"]) {
const result = {};
for (const it of files) for (const file of [it, `${it}.local`]) {
const envPath = this.fs.join(root, file);
try {
const envContent = (await this.fs.readFile(envPath)).toString("utf8");
for (const line of envContent.split("\n")) {
const [key, ...rest] = line.split("=");
if (key) {
const trimmedKey = key.trim();
if (trimmedKey && !trimmedKey.startsWith("#")) {
let value = rest.join("=").trim();
const last = value.length - 1;
if (value.length >= 2 && value[0] === "\"" && value[last] === "\"") try {
value = JSON.parse(value);
} catch {
value = value.slice(1, -1);
}
else if (value.length >= 2 && value[0] === "'" && value[last] === "'") value = value.slice(1, -1);
result[trimmedKey] = value;
}
}
}
this.log.debug(`Parsed environment variables from ${envPath}`);
} catch {
this.log.debug(`No ${file} file found at ${envPath}, skipping.`);
}
}
return result;
}
};
//#endregion
//#region ../../src/command/errors/CommandError.ts
var CommandError = class extends AlephaError {
name = "CommandError";
};
//#endregion
//#region ../../src/command/helpers/Runner.ts
/**
* Runs CLI tasks (shell commands or functions) and logs their lifecycle.
*
* Output is intentionally plain and verbose: every task logs a
* `Starting …` / `Finished … after Ns` line through the standard logger,
* and shelled commands **stream** their stdout/stderr straight to the
* terminal (`capture: false`) so tool output — `vite build` warnings,
* Biome diagnostics, nested `alepha` subcommands — is visible live.
*/
var Runner = class {
log = $logger();
dateTime = $inject(DateTimeProvider);
timers = [];
startTime = this.dateTime.nowMillis();
alepha = $inject(Alepha);
shell = $inject(ShellProvider);
run;
constructor() {
this.run = this.createRunMethod();
}
/**
* Start a new command session.
*
* Retained for API compatibility (the CLI calls it before each command);
* task lifecycle is now logged statelessly, so there is nothing to reset.
*/
startCommand(_cliName, _commandName) {}
createRunMethod() {
const runFn = async (cmd, options) => {
const root = typeof options === "object" && options.root ? options.root : void 0;
if (Array.isArray(cmd)) return await this.execute(cmd.map((it) => typeof it === "string" ? {
name: it,
handler: () => this.exec(it, { root })
} : it));
const name = (typeof options === "object" ? options.alias : void 0) ?? (typeof cmd === "string" ? cmd : cmd.name);
const handler = typeof options === "function" ? options : typeof cmd === "string" ? () => this.exec(cmd, { root }) : cmd.handler;
return await this.execute({
name,
handler
});
};
runFn.rm = async (files, options = {}) => {
if (Array.isArray(files) || files.includes("*")) return runFn({
name: options.alias ?? `rm -rf ${Array.isArray(files) ? files.join(" ") : files}`,
handler: async () => {
for await (const file of glob(files)) {
this.log.trace(`Removing ${file}`);
await rm(file, {
recursive: true,
force: true
});
}
}
});
this.log.trace(`Removing ${files}`);
return runFn({
name: options.alias ?? `rm -rf ${files}`,
handler: () => rm(files, {
recursive: true,
force: true
})
});
};
runFn.cp = async (source, dist, options = {}) => {
this.log.trace(`Copying ${source} to ${dist}`);
return runFn({
name: options.alias ?? `cp -r ${source} ${dist}`,
handler: () => cp(source, dist, { recursive: true })
}, options);
};
runFn.end = () => this.end();
return runFn;
}
async exec(cmd, opts = {}) {
const stream = this.log.isLevelEnabled("DEBUG");
return this.shell.run(cmd, {
root: opts.root,
capture: !stream
});
}
/**
* Executes one or more tasks.
*
* @param task - A single task or an array of tasks to run in parallel.
*/
async execute(task) {
if (Array.isArray(task)) {
await Promise.all(task.map((t) => this.executeTask(t)));
return "";
} else return await this.executeTask(task);
}
/**
* Prints a summary of all executed tasks and their durations.
*/
end() {
if (this.timers.length === 0) return;
const totalTime = ((this.dateTime.nowMillis() - this.startTime) / 1e3).toFixed(1);
this.log.info(`Total time: ${totalTime}s`);
this.timers = [];
}
async executeTask(task) {
const now = this.dateTime.nowMillis();
this.log.info(`Starting '${task.name}' ...`);
let stdout = "";
try {
stdout = String(await task.handler() ?? "");
} catch (error) {
const err = error;
const captured = [err?.stdout, err?.stderr].filter(Boolean).join("\n").trim();
if (captured) this.log.info(`\n\n${captured}`);
throw new CommandError(`Task '${task.name}' failed`, { cause: error });
}
if (stdout) this.log.trace(stdout);
const duration = ((this.dateTime.nowMillis() - now) / 1e3).toFixed(1);
const message = stdout && !stdout.includes("\n") ? stdout.trim() : void 0;
const suffix = message ? ` - ${message}` : "";
this.log.info(`Finished '${task.name}' after ${duration}s${suffix}`);
this.timers.push({
name: task.name,
duration: `${duration}s`
});
return stdout;
}
};
//#endregion
//#region ../../src/command/primitives/$command.ts
/**
* Declares a CLI command.
*
* This primitive allows you to define a command, its flags, and its handler
* within your Alepha application structure.
*/
const $command = (options) => createPrimitive(CommandPrimitive, options);
var CommandPrimitive = class extends Primitive {
flags = this.options.flags ?? z.object({});
env = this.options.env ?? z.object({});
aliases = this.options.aliases ?? [];
onInit() {
if (this.options.pre || this.options.post) this.options.hide ??= true;
}
get name() {
if (this.options.root) return "";
if (this.options.pre) return `pre${this.options.pre}`;
if (this.options.post) return `post${this.options.post}`;
return this.options.name ?? `${this.config.propertyKey}`;
}
/**
* Get the child commands (subcommands) for this command.
*/
get children() {
return this.options.children ?? [];
}
/**
* Check if this command has child commands (is a parent command).
*/
get hasChildren() {
return this.children.length > 0;
}
/**
* Find a child command by name or alias.
*/
findChild(name) {
return this.children.find((child) => child.name === name || child.aliases.includes(name));
}
};
$command[KIND] = CommandPrimitive;
//#endregion
//#region ../../src/command/providers/CliProvider.ts
const envSchema = z.object({
CLI_NAME: z.text({
default: "cli",
description: "Name of the CLI application."
}),
CLI_DESCRIPTION: z.text({
default: "",
description: "Description of the CLI application."
})
});
/**
* CLI provider configuration atom
*/
const cliOptions = $atom({
name: "alepha.command.cli.options",
schema: z.object({
name: z.string().describe("Name of the CLI application.").optional(),
description: z.string().describe("Description of the CLI application.").optional(),
argv: z.array(z.string()).describe("Command line arguments to parse.").optional()
}),
default: {}
});
/**
* CLI provider for parsing and executing commands.
*
* Handles:
* - Command resolution (simple, nested, colon-notation)
* - Flag and argument parsing
* - Environment variable validation
* - Help generation
* - Pre/post command hooks
*
* @example
* ```typescript
* // Define a command
* class MyCommands {
* build = $command({
* name: "build",
* description: "Build the project",
* flags: z.object({ watch: z.boolean().optional() }),
* handler: async ({ flags }) => { ... }
* });
* }
*
* // CLI automatically discovers and executes commands
* const alepha = Alepha.create().with(MyCommands);
* ```
*/
var CliProvider = class {
env = $env(envSchema);
alepha = $inject(Alepha);
log = $logger();
color = $inject(ConsoleColorProvider);
runner = $inject(Runner);
asker = $inject(Asker);
envUtils = $inject(EnvUtils);
options = $state(cliOptions);
get name() {
return this.options.name || this.env.CLI_NAME;
}
get description() {
return this.options.description || this.env.CLI_DESCRIPTION;
}
get argv() {
return this.options.argv || (typeof process !== "undefined" ? process.argv.slice(2) : []);
}
/**
* Global flags available to all commands.
*/
globalFlags = {
help: {
aliases: ["h", "help"],
description: "Show this help message",
schema: z.boolean()
},
verbose: {
aliases: ["verbose"],
description: "Verbose output: trace-level logs (framework internals) in pretty format. Task output already streams by default.",
schema: z.boolean()
}
};
/**
* Main entry point - resolves and executes the command from process.argv.
* This is the production execution path with full lifecycle support.
*/
onReady = $hook({
on: "ready",
handler: async () => {
const argv = [...this.argv];
const positionalArgs = argv.filter((arg) => !arg.startsWith("-"));
const { command, consumedArgs } = this.resolveCommand(positionalArgs);
const globalFlags = this.parseFlags(argv, Object.entries(this.getAllGlobalFlags()).map(([key, value]) => ({
key,
...value
})), { strict: false });
if (globalFlags.verbose || !!this.alepha.env.CLAUDECODE) {
this.alepha.store.set("alepha.logger.level", "trace");
this.alepha.store.set("alepha.logger.format", "pretty");
}
if (globalFlags.help) {
this.printHelp(command);
return;
}
if (!command) {
const rootCommand = this.findCommand("");
const commandName = positionalArgs[0] ?? "";
if (commandName !== "" && !rootCommand?.options.args) {
this.log.error(`Unknown command: '${commandName}'`);
this.printHelp();
return;
}
if (rootCommand) {
await this.executeCommand(rootCommand, argv, true);
return;
}
return;
}
const remainingArgv = this.removeConsumedArgs(argv, consumedArgs);
await this.executeCommand(command, remainingArgv, true);
}
});
/**
* Execute a command with full lifecycle support.
*
* This is the production execution path that includes:
* - Mode-based .env file loading
* - Pre/post command hooks
* - Runner session for pretty CLI output
* - Alepha context wrapper for proper scoping
*
* @see run() for a lightweight test-only alternative
*/
async executeCommand(command, argv, isRootCommand) {
const root = process.cwd();
let modeValue;
if (command.options.mode) {
modeValue = this.parseModeFlag(argv);
if (modeValue === void 0 && typeof command.options.mode === "string") modeValue = command.options.mode;
await this.loadModeEnv(root, modeValue);
}
const commandFlags = this.parseCommandFlags(argv, command.flags, { modeEnabled: !!command.options.mode });
const commandArgs = this.parseCommandArgs(argv, command.options.args, isRootCommand, command.flags);
const commandEnv = this.parseCommandEnv(command.env, command.name);
await this.alepha.context.run(async () => {
this.log.debug(`Executing command '${command.name}'...`, {
flags: commandFlags,
args: commandArgs,
mode: modeValue
});
const runner = this.runner;
runner.startCommand(this.name, command.name);
const args = {
flags: commandFlags,
args: commandArgs,
env: commandEnv,
run: runner.run,
ask: this.asker.ask,
fs,
glob,
root,
help: () => this.printHelp(command),
mode: modeValue
};
const preHooks = this.findPreHooks(command.name);
for (const hook of preHooks) {
this.log.debug(`Executing pre-hook for '${command.name}'...`);
await hook.options.handler(args);
}
await command.options.handler(args);
const postHooks = this.findPostHooks(command.name);
for (const hook of postHooks) {
this.log.debug(`Executing post-hook for '${command.name}'...`);
await hook.options.handler(args);
}
runner.end();
this.log.debug(`Command '${command.name}' executed successfully.`);
});
}
/**
* Remove consumed command path arguments from argv (keeps flags and remaining args).
*/
removeConsumedArgs(argv, consumedArgs) {
const result = [];
let consumedIndex = 0;
for (const arg of argv) if (arg.startsWith("-")) result.push(arg);
else if (consumedIndex < consumedArgs.length && arg === consumedArgs[consumedIndex]) consumedIndex++;
else result.push(arg);
return result;
}
/**
* Resolve a command from positional arguments.
*
* Supports:
* 1. Space-separated subcommands: `deploy vercel` -> finds deploy command, then vercel child
* 2. Colon notation (backwards compat): `deploy:vercel` -> finds command with name "deploy:vercel"
* 3. Simple commands: `build` -> finds command with name "build"
*/
resolveCommand(positionalArgs) {
if (positionalArgs.length === 0) return {
command: void 0,
consumedArgs: []
};
const firstArg = positionalArgs[0];
if (firstArg.includes(":")) {
const command = this.findCommand(firstArg);
if (command) return {
command,
consumedArgs: [firstArg]
};
}
let currentCommand = this.findTopLevelCommand(firstArg);
const consumedArgs = [];
if (!currentCommand) return {
command: void 0,
consumedArgs: []
};
consumedArgs.push(firstArg);
for (let i = 1; i < positionalArgs.length; i++) {
const arg = positionalArgs[i];
if (!currentCommand.hasChildren) break;
const childCommand = currentCommand.findChild(arg);
if (childCommand) {
currentCommand = childCommand;
consumedArgs.push(arg);
} else break;
}
return {
command: currentCommand,
consumedArgs
};
}
/**
* Get all registered commands in the application.
*/
get commands() {
return this.alepha.primitives($command);
}
/**
* Execute a command handler with given arguments.
*
* This is a **lightweight test helper** that directly invokes the command handler
* without the full production lifecycle. It intentionally skips:
* - Pre/post command hooks
* - Runner session (pretty CLI output)
* - Alepha context wrapper
* - .env.{mode} file loading
*
* For production execution, the `onReady` hook uses `executeCommand()` which
* provides the full lifecycle. Merging them would either make this method too
* heavy for simple testing or require many optional parameters to toggle behaviors.
*
* @example
* ```typescript
* // In tests
* const cli = alepha.inject(CliProvider);
* const cmd = alepha.inject(InitCommand);
*
* await cli.run(cmd.init, "--agent --pm=yarn");
* await cli.run(cmd.init, { argv: "--agent", root: "/project" });
* ```
*/
async run(command, options = {}) {
const opts = typeof options === "string" || Array.isArray(options) ? { argv: options } : options;
const args = typeof opts.argv === "string" ? opts.argv.split(" ").filter(Boolean) : opts.argv ?? [];
const root = opts.root ?? process.cwd();
const commandFlags = this.parseCommandFlags(args, command.flags, { modeEnabled: !!command.options.mode });
const commandArgs = this.parseCommandArgs(args, command.options.args, true, command.flags);
const commandEnv = this.parseCommandEnv(command.env, command.name);
let modeValue;
if (command.options.mode) {
modeValue = this.parseModeFlag(args);
if (modeValue === void 0 && typeof command.options.mode === "string") modeValue = command.options.mode;
}
await command.options.handler({
flags: commandFlags,
args: commandArgs,
env: commandEnv,
run: this.runner.run,
ask: this.asker.ask,
fs,
glob,
root,
help: () => this.printHelp(command),
mode: modeValue
});
}
/**
* Find a command by name or alias
*/
findCommand(name) {
return this.commands.findLast((command) => command.name === name || command.aliases.includes(name));
}
/**
* Find a top-level command by name or alias (excludes child commands)
*/
findTopLevelCommand(name) {
return this.getTopLevelCommands().findLast((command) => command.name === name || command.aliases.includes(name));
}
/**
* Find all pre-hooks for a command (commands named `pre{commandName}`)
*/
findPreHooks(commandName) {
return this.commands.filter((cmd) => cmd.name === `pre${commandName}`);
}
/**
* Find all post-hooks for a command (commands named `post{commandName}`)
*/
findPostHooks(commandName) {
return this.commands.filter((cmd) => cmd.name === `post${commandName}`);
}
/**
* Get global flags (help only, root command flags are NOT global)
*/
getAllGlobalFlags() {
return { ...this.globalFlags };
}
/**
* Read a schema's metadata (`title`, `description`, `aliases`, `alias`, …).
*
* Under zod these options live on the schema's `.meta()` registry rather than
* as direct properties (typebox), and they sit on the INNER schema — so any
* optional / nullable / default wrappers are peeled first.
*/
schemaMeta(schema) {
if (!schema) return {};
const base = z.schema.unwrap(schema);
return (typeof base?.meta === "function" ? base.meta() : void 0) ?? {};
}
/**
* Build flag definitions (key, aliases, description, schema) from a flags
* object schema. Centralises the metadata reading so every call-site (parsing,
* arg-splitting, help) extracts aliases/descriptions the same way.
*/
extractFlagDefs(schema) {
return Object.entries(schema.properties).map(([key, value]) => {
const meta = this.schemaMeta(value);
return {
key,
aliases: [key, ...meta.aliases ?? (meta.alias ? [meta.alias] : [])],
description: meta.description,
schema: value
};
});
}
/**
* Parse command flags from argv using the command's flag schema
*/
parseCommandFlags(argv, schema, options = {}) {
const { modeEnabled = false } = options;
const flagDefs = this.extractFlagDefs(schema);
if (modeEnabled) flagDefs.push({
key: "__mode__",
aliases: ["mode", "m"],
description: void 0,
schema: z.string()
});
const claimed = new Set(flagDefs.flatMap((d) => d.aliases));
for (const [key, value] of Object.entries(this.getAllGlobalFlags())) {
if (value.aliases.some((a) => claimed.has(a))) continue;
flagDefs.push({
key: `__global_${key}__`,
aliases: value.aliases,
description: void 0,
schema: value.schema
});
}
const parsed = this.parseFlags(argv, flagDefs);
parsed.__mode__ = void 0;
for (const key of Object.keys(parsed)) if (key.startsWith("__global_")) delete parsed[key];
for (const [key, value] of Object.entries(schema.properties)) if (!(key in parsed)) {
const def = z.schema.getDefault(value);
if (def !== void 0) parsed[key] = def;
}
try {
return this.alepha.codec.decode(schema, parsed);
} catch (error) {
if (error instanceof TypeBoxError) throw new CommandError(`Invalid flag: ${error.cause.instancePath || "command"} ${error.cause.message}`);
throw error;
}
}
/**
* Parse and validate environment variables using the command's env schema
*/
parseCommandEnv(schema, commandName) {
const result = {};
const missing = [];
for (const [key, propSchema] of Object.entries(schema.properties)) {
const value = process.env[key];
if (value !== void 0) result[key] = value;
else {
const def = z.schema.getDefault(propSchema);
if (def !== void 0) result[key] = def;
else if (z.schema.isOptional(propSchema)) {} else missing.push(key);
}
}
if (missing.length > 0) {
const vars = missing.join(", ");
throw new CommandError(`Missing required environment variable${missing.length > 1 ? "s" : ""}: ${vars}`);
}
try {
return this.alepha.codec.decode(schema, result);
} catch (error) {
if (error instanceof TypeBoxError) throw new CommandError(`Invalid environment variable: ${error.cause.instancePath || "env"} ${error.cause.message}`);
throw error;
}
}
/**
* Parse --mode or -m flag from argv for environment file loading
*/
parseModeFlag(argv) {
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith("--mode=") || arg.startsWith("-m=")) return arg.split("=")[1];
if (arg === "--mode" || arg === "-m") {
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) return nextArg;
throw new CommandError("Flag --mode requires a value.");
}
}
}
/**
* Load .env and .env.{mode} files into process.env
*/
async loadModeEnv(root, mode) {
const envFiles = [".env"];
if (mode) envFiles.push(`.env.${mode}`);
this.log.debug(`Loading env files: ${envFiles.join(", ")}`);
await this.envUtils.loadEnv(root, envFiles);
}
/**
* Low-level flag parser - extracts flag values from argv based on definitions
*/
parseFlags(argv, flagDefs, options = {}) {
const { strict = true } = options;
const result = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("-")) continue;
const [rawKey, ...valueParts] = arg.replace(/^-{1,2}/, "").split("=");
const value = valueParts.join("=");
const def = flagDefs.find((d) => d.aliases.includes(rawKey));
if (!def) {
if (strict) throw new CommandError(`Unknown flag: --${rawKey}`);
continue;
}
const base = z.schema.unwrap(def.schema);
const isUnionWithBoolean = z.schema.isUnion(base) && base.anyOf.some((s) => z.schema.isBoolean(s));
if (z.schema.isBoolean(base)) result[def.key] = true;
else if (isUnionWithBoolean && !value) {
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) {
result[def.key] = nextArg;
i++;
} else result[def.key] = true;
} else if (value) result[def.key] = this.castFlagValue(value, base, rawKey);
else {
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) result[def.key] = this.castFlagValue(nextArg, base, rawKey);
else throw new CommandError(`Flag --${rawKey} requires a value.`);
}
}
return result;
}
/**
* Convert a raw flag value string into the value its schema expects.
*
* zod no longer coerces, so scalar values (number / integer / boolean) are
* cast + validated via {@link parseArgumentValue} (same path as positional
* args); object / array / record values are JSON-parsed. `schema` is expected
* to already be unwrapped of optional/nullable/default.
*/
castFlagValue(value, schema, rawKey) {
if (z.schema.isObject(schema) || z.schema.isArray(schema) || z.schema.isRecord(schema)) try {
return JSON.parse(value);
} catch {
throw new CommandError(`Invalid JSON value for flag --${rawKey}`);
}
return this.parseArgumentValue(value, schema);
}
/**
* Get indices of argv elements consumed by flags (for separating args from flags)
*/
getFlagConsumedIndices(argv, flagDefs) {
const consumed = /* @__PURE__ */ new Set();
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("-")) continue;
consumed.add(i);
const [rawKey, ...valueParts] = arg.replace(/^-{1,2}/, "").split("=");
const hasEqualValue = valueParts.length > 0;
const def = flagDefs.find((d) => d.aliases.includes(rawKey));
if (!def) continue;
const base = z.schema.unwrap(def.schema);
const isUnionWithBoolean = z.schema.isUnion(base) && base.anyOf.some((s) => z.schema.isBoolean(s));
if (!z.schema.isBoolean(base) && !isUnionWithBoolean && !hasEqualValue) {
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) consumed.add(i + 1);
} else if (isUnionWithBoolean && !hasEqualValue) {
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith("-")) consumed.add(i + 1);
}
}
return consumed;
}
parseCommandArgs(argv, schema, isRootCommand = false, flagSchema) {
if (!schema) return;
const flagDefs = flagSchema ? this.extractFlagDefs(flagSchema) : [];
const consumedIndices = this.getFlagConsumedIndices(argv, flagDefs);
const positionalArgs = argv.filter((arg, idx) => !arg.startsWith("-") && !consumedIndices.has(idx));
const argsOnly = isRootCommand ? positionalArgs : positionalArgs.slice(1);
try {
if (z.schema.isOptional(schema)) {
if (argsOnly.length === 0) return;
return this.parseArgumentValue(argsOnly[0], schema);
} else if (z.schema.isTuple(schema) && schema.items) {
const result = [];
const items = schema.items;
for (let i = 0; i < items.length; i++) {
const itemSchema = items[i];
if (i < argsOnly.length) result.push(this.parseArgumentValue(argsOnly[i], itemSchema));
else if (z.schema.isOptional(itemSchema)) result.push(void 0);
else throw new CommandError(`Missing required argument at position ${i + 1}`);
}
return result;
} else {
if (argsOnly.length === 0) throw new CommandError("Missing required argument");
return this.parseArgumentValue(argsOnly[0], schema);
}
} catch (error) {
if (error instanceof TypeBoxError) throw new CommandError(`Invalid argument: ${error.value.message}`);
throw error;
}
}
/**
* Convert a string argument value to the appropriate type based on schema
*/
parseArgumentValue(value, schema) {
if (z.schema.isString(schema)) return value;
if (z.schema.isNumber(schema) || z.schema.isInteger(schema)) {
const num = Number(value);
if (Number.isNaN(num)) throw new CommandError(`Expected number, got "${value}"`);
if (z.schema.isInteger(schema) && !Number.isInteger(num)) throw new CommandError(`Expected integer, got "${value}"`);
return num;
}
if (z.schema.isBoolean(schema)) {
const lower = value.toLowerCase();
if (lower === "true" || lower === "1") return true;
if (lower === "false" || lower === "0") return false;
throw new CommandError(`Expected boolean, got "${value}"`);
}
return value;
}
/**
* Generate usage string for command arguments (e.g., "<path>" or "[path]")
*/
generateArgsUsage(schema) {
if (!schema) return "";
if (z.schema.isOptional(schema)) {
const typeName = this.getTypeName(schema);
return ` [${this.schemaMeta(schema).title ?? "arg1"}${typeName}]`;
}
if (z.schema.isTuple(schema) && schema.items) return ` ${schema.items.map((item, index) => {
const argName = `arg${index + 1}`;
const typeName = this.getTypeName(item);
if (z.schema.isOptional(item)) return `[${argName}${typeName}]`;
return `<${argName}${typeName}>`;
}).join(" ")}`;
const typeName = this.getTypeName(schema);
return ` <${this.schemaMeta(schema).title ?? "arg1"}${typeName}>`;
}
/**
* Get display type name for a schema (e.g., ": number", ": boolean")
*/
getTypeName(schema) {
if (!schema) return "";
const base = z.schema.unwrap(schema);
if (z.schema.isString(base)) return "";
if (z.schema.isInteger(base)) return ": integer";
if (z.schema.isNumber(base)) return ": number";
if (z.schema.isBoolean(base)) return ": boolean";
return "";
}
/**
* Print help for a specific command or general CLI help.
*
* @param command - If provided, shows help for this specific command.
* If omitted, shows general CLI help with all commands.
*/
printHelp(command) {
this.alepha.store.set("alepha.logger.format", "raw");
const cliName = this.name || "cli";
const c = this.color;
this.log.info("");
if (command?.name) {
const hasChildren = command.hasChildren;
const argsUsage = hasChildren ? ` ${c.set("CYAN", "<command>")}` : this.generateColoredArgsUsage(command.options.args);
const commandPath = this.getCommandPath(command);
const usage = `${c.set("GREY_LIGHT", cliName)} ${c.set("CYAN", commandPath)}${argsUsage}`.trim();
this.log.info(`${c.set("WHITE_BOLD", "Usage:")} ${usage}`);
if (command.options.description) {
this.log.info(``);
this.log.info(`\t${command.options.description}`);
}
if (hasChildren) {
this.log.info("");
this.log.info(c.set("WHITE_BOLD", "Commands:"));
const maxSubCmdLength = this.getMaxChildCmdLength(command.children);
for (const child of command.children) {
if (child.options.hide) continue;
const childArgsUsage = this.generateArgsUsage(child.options.args);
const fullCmdStr = `${[child.name, ...child.aliases].join(", ")}${childArgsUsage}`;
const coloredCmd = `${c.set("GREY_LIGHT", cliName)} ${c.set("CYAN", commandPath)} ${c.set("CYAN", fullCmdStr)}`;
const padding = " ".repeat(Math.max(0, maxSubCmdLength - fullCmdStr.length));
this.log.info(` ${coloredCmd}${padding} ${child.options.description ?? ""}`);
}
}
this.log.info("");
this.log.info(c.set("WHITE_BOLD", "Flags:"));
const flags = [
...this.extractFlagDefs(command.flags),
...command.options.mode ? [{
key: "mode",
aliases: ["m", "mode"],
description: typeof command.options.mode === "string" ? `Environment mode - loads .env.{mode} (default: ${command.options.mode})` : "Environment mode (e.g., production, staging) - loads .env.{mode}",
schema: z.string()
}] : [],
...Object.entries(this.getAllGlobalFlags()).map(([key, value]) => ({
key,
...value
}))
];
const maxFlagLength = this.getMaxFlagLength(flags);
for (const flag of flags) {
const { aliases, description } = flag;
const schema = "schema" in flag ? flag.schema : void 0;
const flagStr = (Array.isArray(aliases) ? aliases : [aliases]).slice().sort((a, b) => a.length - b.length).map((a) => a.length === 1 ? `-${a}` : `--${a}`).join(", ");
const coloredFlag = c.set("GREY_LIGHT", flagStr);
const padding = " ".repeat(Math.max(0, maxFlagLength - flagStr.length));
const formattedDesc = this.formatFlagDescription(description, schema);
this.log.info(` ${coloredFlag}${padding} ${formattedDesc}`);
}
const envVars = Object.entries(command.env.properties);
if (envVars.length > 0) {
this.log.info("");
this.log.info(c.set("WHITE_BOLD", "Env:"));
const maxEnvLength = Math.max(...envVars.map(([key]) => key.length));
for (const [key, schema] of envVars) {
const isOptional = z.schema.isOptional(schema);
const description = schema.description ?? "";
const optionalStr = isOptional ? c.set("GREY_DARK", " (optional)") : c.set("RED", " (required)");
const coloredKey = c.set("CYAN", key);
const padding = " ".repeat(Math.max(0, maxEnvLength - key.length));
this.log.info(` ${coloredKey}${padding} ${description}${optionalStr}`);
}
}
} else {
this.log.info(this.description || "Available commands:");
this.log.info("");
this.log.info(c.set("WHITE_BOLD", "Commands:"));
const topLevelCommands = this.getTopLevelCommands();
const maxCmdLength = this.getMaxCmdLength(topLevelCommands);
for (const cmd of topLevelCommands) {
if (cmd.name === "" || cmd.options.hide) continue;
const fullCmdStr = `${[cmd.name, ...cmd.aliases].join(", ")}${cmd.hasChildren ? " <command>" : this.generateArgsUsage(cmd.options.args)}`;
const coloredCmd = `${c.set("GREY_LIGHT", cliName)} ${c.set("CYAN", fullCmdStr)}`;
const padding = " ".repeat(Math.max(0, maxCmdLength - fullCmdStr.length));
this.log.info(` ${coloredCmd}${padding} ${cmd.options.description ?? ""}`);
}
this.log.info("");
this.log.info(c.set("WHITE_BOLD", "Flags:"));
const rootCommand = this.commands.find((cmd) => cmd.name === "");
const globalFlags = [...rootCommand ? this.extractFlagDefs(rootCommand.flags) : [], ...Object.values(this.getAllGlobalFlags())];
const maxFlagLength = this.getMaxFlagLength(globalFlags);
for (const { aliases, description, schema } of globalFlags) {
const flagStr = aliases.map((a) => a.length === 1 ? `-${a}` : `--${a}`).join(", ");
const coloredFlag = c.set("GREY_LIGHT", flagStr);
const padding = " ".repeat(Math.max(0, maxFlagLength - flagStr.length));
const formattedDesc = this.formatFlagDescription(description, schema);
this.log.info(` ${coloredFlag}${padding} ${formattedDesc}`);
}
}
this.log.info("");
}
/**
* Generate colored usage string for command arguments (for help display)
*/
generateColoredArgsUsage(schema) {
if (!schema) return "";
const c = this.color;
if (z.schema.isOptional(schema)) {
const typeName = this.getTypeName(schema);
const key = this.schemaMeta(schema).title ?? "arg1";
return ` ${c.set("GREY_DARK", `[${key}${typeName}]`)}`;
}
if (z.schema.isTuple(schema) && schema.items) return ` ${schema.items.map((item, index) => {
const argName = `arg${index + 1}`;
const typeName = this.getTypeName(item);
if (z.schema.isOptional(item)) return c.set("GREY_DARK", `[${argName}${typeName}]`);
return c.set("CYAN", `<${argName}${typeName}>`);
}).join(" ")}`;
const typeName = this.getTypeName(schema);
const key = this.schemaMeta(schema).title ?? "arg1";
return ` ${c.set("CYAN", `<${key}${typeName}>`)}`;
}
/**
* Get the full command path (e.g., "deploy vercel" for a nested command)
*/
getCommandPath(command) {
const path = [command.name];
let current = command;
while (true) {
const parent = this.findParentCommand(current);
if (!parent) break;
path.unshift(parent.name);
current = parent;
}
return path.join(" ");
}
/**
* Find the parent command of a nested command
*/
findParentCommand(command) {
for (const cmd of this.commands) if (cmd.children.includes(command)) return cmd;
}
/**
* Get top-level commands (commands that are not children of other commands)
*/
getTopLevelCommands() {
const allChildren = /* @__PURE__ */ new Set();
for (const command of this.commands) for (const child of command.children) allChildren.add(child);
return this.commands.filter((cmd) => !allChildren.has(cmd));
}
/**
* Calculate max display length for child commands (for help alignment)
*/
getMaxChildCmdLength(children) {
return Math.max(...children.filter((c) => !c.options.hide).map((c) => {
return `${[c.name, ...c.aliases].join(", ")}${this.generateArgsUsage(c.options.args)}`.length;
}), 0);
}
/**
* Calculate max display length for commands (for help alignment)
*/
getMaxCmdLength(commands) {
return Math.max(...commands.filter((c) => !c.options.hide && c.name !== "").map((c) => {
return `${[c.name, ...c.aliases].join(", ")}${c.hasChildren ? " <command>" : this.generateArgsUsage(c.options.args)}`.length;
}));
}
/**
* Calculate max display length for flags (for help alignment)
*/
getMaxFlagLength(flags) {
return Math.max(...flags.map((f) => {
return (Array.isArray(f.aliases) ? f.aliases : [f.aliases]).map((a) => a.length === 1 ? `-${a}` : `--${a}`).join(", ").length;
}));
}
/**
* Extract enum values from a schema if it represents an enum.
* Returns undefined if the schema is not an enum.
*/
getEnumValues(schema) {
if (!schema) return void 0;
const base = z.schema.unwrap(schema);
if (z.schema.isEnum(base)) {
const values = z.schema.enumValues(base);
return values.length > 0 && values.every((v) => typeof v === "string") ? values : void 0;
}
if (z.schema.isUnion(base)) {
const variants = base.anyOf ?? [];
const values = [];
for (const variant of variants) {
const value = variant.value;
if (z.schema.isLiteral(variant) && typeof value === "string") values.push(value);
else return;
}
return values.length > 0 ? values : void 0;
}
}
/**
* Format flag description with enum values if applicable.
*/
formatFlagDescription(description, schema) {
const baseDesc = description ?? "";
if (!schema) return baseDesc;
const enumValues = this.getEnumValues(schema);
if (enumValues && enumValues.length > 0) {
const valuesStr = enumValues.join(", ");
const enumHint = this.color.set("GREY_DARK", `[${valuesStr}]`);
return baseDesc ? `${baseDesc} ${enumHint}` : enumHint;
}
return baseDesc;
}
};
//#endregion
//#region ../../src/command/index.ts
/**
* Declarative CLI command framework.
*
* **Features:**
* - CLI command definitions
* - Interactive CLI prompts (plain readline)
* - Command execution with streamed, verbose output
* - Environment variable utilities
* - Schema validation for CLI arguments
*
* @module alepha.command
*/
const AlephaCommand = $module({
name: "alepha.command",
primitives: [$command],
services: [
CliProvider,
Runner,
Asker,
EnvUtils
]
});
//#endregion
export { $command, AlephaCommand, Asker, CliProvider, CommandError, CommandPrimitive, EnvUtils, Runner, cliOptions };
//# sourceMappingURL=index.js.map