UNPKG

alepha

Version:

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

750 lines 25.1 kB
import { Alepha, AlephaError, Async, KIND, Primitive, Static, TObject, TSchema, TString } from "alepha"; import { ConsoleColorProvider } from "alepha/logger"; import { FileSystemProvider, ShellProvider } from "alepha/system"; import * as fs from "node:fs/promises"; import { glob } from "node:fs/promises"; import { DateTimeProvider } from "alepha/datetime"; //#region ../../src/command/errors/CommandError.d.ts declare class CommandError extends AlephaError { readonly name = "CommandError"; } //#endregion //#region ../../src/command/helpers/Asker.d.ts interface AskOptions<T extends TSchema = TString> { /** * Response schema expected. * * Recommended schemas: * - z.text() - for free text input * - z.number() - for numeric input * - z.boolean() - for yes/no input (accepts "true", "false", "1", "0") * - z.enum(["option1", "option2"]) - for predefined options * * You can use schema.default to provide a default value. * * @example * ```ts * ask("What is your name?", { schema: z.text({ default: "John Doe" }) }) * ``` * * @default TString */ schema?: T; /** * Custom validation function. * Throws an AlephaError in case of validation failure. */ validate?: (value: Static<T>) => void; } interface AskMethod { <T extends TSchema = TString>(question: string, options?: AskOptions<T>): Promise<Static<T>>; permission: (question: string) => Promise<boolean>; intro: (title: string) => void; outro: (message: string) => void; } /** * 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. */ declare class Asker { protected readonly log: import("alepha/logger").Logger; readonly ask: AskMethod; protected readonly alepha: Alepha; constructor(); protected createAskMethod(): AskMethod; protected prompt<T extends TSchema = TString>(question: string, options: AskOptions<T>): Promise<Static<T>>; protected createPromptInterface(): import("node:readline/promises").Interface; } //#endregion //#region ../../src/command/helpers/EnvUtils.d.ts declare class EnvUtils { protected readonly log: import("alepha/logger").Logger; protected readonly fs: 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"]. */ loadEnv(root: string, files?: string[]): Promise<void>; /** * 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`). */ parseEnv(root: string, files?: string[]): Promise<Record<string, string>>; } //#endregion //#region ../../src/command/helpers/Runner.d.ts type Task = { name: string; handler: () => any; }; interface Timer { name: string; duration: string; } interface RunOptions { /** * Rename the command for logging purposes. */ alias?: string; /** * Root directory to execute the command in. */ root?: string; } interface RunnerMethod { (cmd: string | Task | Array<string | Task>, options?: RunOptions | (() => any)): Promise<string>; rm: (glob: string | string[], options?: RunOptions) => Promise<string>; cp: (source: string, dest: string, options?: RunOptions) => Promise<string>; /** * Ends the runner and prints a summary of executed tasks. * * > This is automatically called at the end of command execution. * > But can be called manually if needed to print more stuff before the command ends. */ end: () => void; } /** * 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. */ declare class Runner { protected readonly log: import("alepha/logger").Logger; protected readonly dateTime: DateTimeProvider; protected timers: Timer[]; protected readonly startTime: number; protected readonly alepha: Alepha; protected readonly shell: ShellProvider; readonly run: RunnerMethod; constructor(); /** * 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: string, _commandName: string): void; protected createRunMethod(): RunnerMethod; protected exec(cmd: string, opts?: { root?: string; }): Promise<string>; /** * Executes one or more tasks. * * @param task - A single task or an array of tasks to run in parallel. */ protected execute(task: Task | Task[]): Promise<string>; /** * Prints a summary of all executed tasks and their durations. */ end(): void; protected executeTask(task: Task): Promise<string>; } //#endregion //#region ../../src/command/primitives/$command.d.ts /** * Declares a CLI command. * * This primitive allows you to define a command, its flags, and its handler * within your Alepha application structure. */ declare const $command: { <T extends TObject, A extends TSchema, E extends TObject>(options: CommandPrimitiveOptions<T, A, E>): CommandPrimitive<T, A, E>; [KIND]: typeof CommandPrimitive; }; interface CommandPrimitiveOptions<T extends TObject, A extends TSchema, E extends TObject = TObject> { /** * The handler function to execute when the command is matched. * * For parent commands with children, the handler is called when: * - The parent command is invoked without a subcommand * - The parent command is invoked with --help (to show available subcommands) */ handler: (args: CommandHandlerArgs<T, A, E>) => Async<void>; /** * The name of the command. If omitted, the property key is used. * * An empty string "" denotes the root command. */ name?: string; /** * A short description of the command, shown in the help message. */ description?: string; /** * An array of alternative names for the command. */ aliases?: string[]; /** * A TypeBox object schema defining the flags for the command. */ flags?: T; /** * A TypeBox object schema defining required environment variables. * * Environment variables are validated before the handler runs (fail fast). * They are displayed in the help output under "Env:" section. * * @example * ```ts * $command({ * env: z.object({ * VERCEL_TOKEN: z.text({ description: "Vercel API token" }), * VERCEL_ORG_ID: z.text({ description: "Organization ID" }).optional(), * }), * handler: async ({ env }) => { * // env.VERCEL_TOKEN is typed & guaranteed to exist * console.log(env.VERCEL_TOKEN); * } * }) * ``` */ env?: E; /** * An optional TypeBox schema defining the arguments for the command. * * @example * args: z.text() * my-cli command <arg1: string> * * args: z.text().optional() * my-cli command [arg1: string] * * args: z.tuple([z.text(), z.number()]) * my-cli command <arg1: string> <arg2: number> * * args: z.tuple([z.text(), z.number().optional()]) * my-cli command <arg1: string> [arg2: number] */ args?: A; /** * Marks this command as the root command. * Equivalent to setting name to an empty string "". */ root?: boolean; /** * Run this command's handler BEFORE the specified target command. * * Pre-hooks are not listed in help and cannot be called directly. * They receive the same parsed flags and args as the target command. * * @example * ```ts * class BuildCommands { * prebuild = $command({ * pre: "build", * handler: async ({ run }) => { * await run("cleaning dist folder...", () => fs.rm("dist")); * } * }); * * build = $command({ * name: "build", * handler: async () => { ... } * }); * } * ``` */ pre?: string; /** * Run this command's handler AFTER the specified target command. * * Post-hooks are not listed in help and cannot be called directly. * They receive the same parsed flags and args as the target command. * * @example * ```ts * class BuildCommands { * build = $command({ * name: "build", * handler: async () => { ... } * }); * * postbuild = $command({ * post: "build", * handler: async ({ run }) => { * await run("generating checksums...", generateChecksums); * } * }); * } * ``` */ post?: string; /** * If true, this command will be hidden from the help output. */ hide?: boolean; /** * Adds a `--mode, -m` flag to load environment files. * * When enabled: * - Loads `.env` and `.env.local` by default * - With `--mode production`, also loads `.env.production` and `.env.production.local` * - The mode value is exposed in the handler as `mode: string | undefined` * * Set to `true` to enable with no default, or a string to set a default mode. * * This follows Vite's environment loading convention. * @see https://vite.dev/guide/env-and-mode * * @example * ```ts * // No default mode * build = $command({ * mode: true, * handler: async ({ mode }) => { * console.log(`Building for ${mode ?? 'development'}...`); * } * }); * * // Default mode "production" * deploy = $command({ * mode: "production", * handler: async ({ mode }) => { * console.log(`Deploying for ${mode}...`); // always defined * } * }); * ``` * * Usage: * - `cli build` - loads .env (mode = undefined) * - `cli build --mode production` - loads .env and .env.production * - `cli deploy` - loads .env and .env.production (default mode) * - `cli deploy --mode staging` - loads .env and .env.staging */ mode?: boolean | string; /** * Child commands (subcommands) for this command. * * When children are defined, the command becomes a parent command that * can be invoked with space-separated subcommands: * * @example * ```ts * class DeployCommands { * // Subcommands * vercel = $command({ * description: "Deploy to Vercel", * handler: async () => { ... } * }); * * cloudflare = $command({ * description: "Deploy to Cloudflare", * handler: async () => { ... } * }); * * // Parent command with children * deploy = $command({ * description: "Deploy the application", * children: [this.vercel, this.cloudflare], * handler: async () => { * // Called when "deploy" is invoked without subcommand * console.log("Available: deploy vercel, deploy cloudflare"); * } * }); * } * ``` * * This allows CLI usage like: * - `cli deploy vercel` - runs the vercel subcommand * - `cli deploy cloudflare` - runs the cloudflare subcommand * - `cli deploy` - runs the parent handler (shows available subcommands) * - `cli deploy --help` - shows help with all available subcommands */ children?: CommandPrimitive<any, any>[]; } declare class CommandPrimitive<T extends TObject = TObject, A extends TSchema = TSchema, E extends TObject = TObject> extends Primitive<CommandPrimitiveOptions<T, A, E>> { readonly flags: T | import("zod").ZodObject<{}, import("zod/v4/core").$strip>; readonly env: E | import("zod").ZodObject<{}, import("zod/v4/core").$strip>; readonly aliases: string[]; protected onInit(): void; get name(): string; /** * Get the child commands (subcommands) for this command. */ get children(): CommandPrimitive<any, any>[]; /** * Check if this command has child commands (is a parent command). */ get hasChildren(): boolean; /** * Find a child command by name or alias. */ findChild(name: string): CommandPrimitive<any, any> | undefined; } interface CommandHandlerArgs<T extends TObject, A extends TSchema = TSchema, E extends TObject = TObject> { flags: Static<T>; args: A extends TSchema ? Static<A> : Array<string>; env: Static<E>; run: RunnerMethod; ask: AskMethod; glob: typeof glob; fs: typeof fs; /** * The root directory where the command is executed. */ root: string; /** * Display help for the current command. * * Useful for parent commands with children to show available subcommands * when invoked without a specific subcommand. * * @example * ```ts * deploy = $command({ * children: [this.vercel, this.cloudflare], * handler: async ({ help }) => { * help(); // Shows available subcommands * } * }); * ``` */ help: () => void; /** * The current execution mode (e.g., "development", "production", "staging"). * * Use --mode flag to set this value when running the command. */ mode?: string; } //#endregion //#region ../../src/command/providers/CliProvider.d.ts declare const envSchema: import("zod").ZodObject<{ CLI_NAME: import("zod").ZodString; CLI_DESCRIPTION: import("zod").ZodString; }, import("zod/v4/core").$strip>; declare module "alepha" { interface Env extends Partial<Static<typeof envSchema>> {} } /** * CLI provider configuration atom */ declare const cliOptions: import("alepha").Atom<import("zod").ZodObject<{ name: import("zod").ZodOptional<import("zod").ZodString>; description: import("zod").ZodOptional<import("zod").ZodString>; argv: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>; }, import("zod/v4/core").$strip>, "alepha.command.cli.options">; type CliProviderOptions = Static<typeof cliOptions.schema>; declare module "alepha" { interface State { [cliOptions.key]: CliProviderOptions; } } /** * 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); * ``` */ declare class CliProvider { protected readonly env: { CLI_NAME: string; CLI_DESCRIPTION: string; }; protected readonly alepha: Alepha; protected readonly log: import("alepha/logger").Logger; protected readonly color: ConsoleColorProvider; protected readonly runner: Runner; protected readonly asker: Asker; protected readonly envUtils: EnvUtils; protected readonly options: Readonly<{ name?: string | undefined; description?: string | undefined; argv?: string[] | undefined; }>; protected get name(): string; protected get description(): string; protected get argv(): string[]; /** * Global flags available to all commands. */ protected readonly globalFlags: { help: { aliases: string[]; description: string; schema: import("zod").ZodBoolean; }; verbose: { aliases: string[]; description: string; schema: import("zod").ZodBoolean; }; }; /** * Main entry point - resolves and executes the command from process.argv. * This is the production execution path with full lifecycle support. */ protected readonly onReady: import("alepha").HookPrimitive<"ready">; /** * 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 */ protected executeCommand(command: CommandPrimitive<TObject>, argv: string[], isRootCommand: boolean): Promise<void>; /** * Remove consumed command path arguments from argv (keeps flags and remaining args). */ protected removeConsumedArgs(argv: string[], consumedArgs: string[]): string[]; /** * 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" */ protected resolveCommand(positionalArgs: string[]): { command: CommandPrimitive<TObject> | undefined; consumedArgs: string[]; }; /** * Get all registered commands in the application. */ get commands(): CommandPrimitive<any>[]; /** * 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" }); * ``` */ run<T extends TObject, A extends TSchema>(command: CommandPrimitive<T, A>, options?: string | string[] | { argv?: string | string[]; root?: string; }): Promise<void>; /** * Find a command by name or alias */ protected findCommand(name: string): CommandPrimitive<TObject> | undefined; /** * Find a top-level command by name or alias (excludes child commands) */ protected findTopLevelCommand(name: string): CommandPrimitive<TObject> | undefined; /** * Find all pre-hooks for a command (commands named `pre{commandName}`) */ protected findPreHooks(commandName: string): CommandPrimitive<TObject>[]; /** * Find all post-hooks for a command (commands named `post{commandName}`) */ protected findPostHooks(commandName: string): CommandPrimitive<TObject>[]; /** * Get global flags (help only, root command flags are NOT global) */ protected getAllGlobalFlags(): Record<string, { aliases: string[]; description?: string; schema: TSchema; }>; /** * 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. */ protected schemaMeta(schema: TSchema | undefined): Record<string, any>; /** * 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. */ protected extractFlagDefs(schema: TObject): Array<{ key: string; aliases: string[]; description?: string; schema: TSchema; }>; /** * Parse command flags from argv using the command's flag schema */ protected parseCommandFlags(argv: string[], schema: TObject, options?: { modeEnabled?: boolean; }): Record<string, any>; /** * Parse and validate environment variables using the command's env schema */ protected parseCommandEnv(schema: TObject, commandName: string): Record<string, any>; /** * Parse --mode or -m flag from argv for environment file loading */ protected parseModeFlag(argv: string[]): string | undefined; /** * Load .env and .env.{mode} files into process.env */ protected loadModeEnv(root: string, mode: string | undefined): Promise<void>; /** * Low-level flag parser - extracts flag values from argv based on definitions */ protected parseFlags(argv: string[], flagDefs: { key: string; aliases: string[]; schema: TSchema; }[], options?: { strict?: boolean; }): Record<string, any>; /** * 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. */ protected castFlagValue(value: string, schema: TSchema, rawKey: string): any; /** * Get indices of argv elements consumed by flags (for separating args from flags) */ protected getFlagConsumedIndices(argv: string[], flagDefs: { key: string; aliases: string[]; schema: TSchema; }[]): Set<number>; protected parseCommandArgs(argv: string[], schema?: TSchema, isRootCommand?: boolean, flagSchema?: TObject): any; /** * Convert a string argument value to the appropriate type based on schema */ protected parseArgumentValue(value: string, schema: TSchema): any; /** * Generate usage string for command arguments (e.g., "<path>" or "[path]") */ protected generateArgsUsage(schema?: TSchema): string; /** * Get display type name for a schema (e.g., ": number", ": boolean") */ protected getTypeName(schema: TSchema): string; /** * 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?: CommandPrimitive<any>): void; /** * Generate colored usage string for command arguments (for help display) */ protected generateColoredArgsUsage(schema?: TSchema): string; /** * Get the full command path (e.g., "deploy vercel" for a nested command) */ protected getCommandPath(command: CommandPrimitive<any>): string; /** * Find the parent command of a nested command */ protected findParentCommand(command: CommandPrimitive<any>): CommandPrimitive<any> | undefined; /** * Get top-level commands (commands that are not children of other commands) */ protected getTopLevelCommands(): CommandPrimitive<any>[]; /** * Calculate max display length for child commands (for help alignment) */ protected getMaxChildCmdLength(children: CommandPrimitive<any>[]): number; /** * Calculate max display length for commands (for help alignment) */ protected getMaxCmdLength(commands: CommandPrimitive[]): number; /** * Calculate max display length for flags (for help alignment) */ protected getMaxFlagLength(flags: { aliases: string[]; }[]): number; /** * Extract enum values from a schema if it represents an enum. * Returns undefined if the schema is not an enum. */ protected getEnumValues(schema: TSchema): string[] | undefined; /** * Format flag description with enum values if applicable. */ protected formatFlagDescription(description: string | undefined, schema: TSchema | undefined): string; } //#endregion //#region ../../src/command/index.d.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 */ declare const AlephaCommand: import("alepha").Service<import("alepha").Module>; declare module "alepha" { interface StringOptions { /** * Additional aliases for the flags. * * @module alepha.command */ aliases?: string[]; } } //#endregion export { $command, AlephaCommand, AskMethod, AskOptions, Asker, CliProvider, CliProviderOptions, CommandError, CommandHandlerArgs, CommandPrimitive, CommandPrimitiveOptions, EnvUtils, RunOptions, Runner, RunnerMethod, Task, cliOptions }; //# sourceMappingURL=index.d.ts.map