UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,129 lines (1,128 loc) 39.5 kB
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