UNPKG

@adonisjs/ace

Version:

A CLI framework for Node.js

1,736 lines 83.6 kB
import { cliui, cliui as cliui$1 } from "@poppinss/cliui"; import yargsParser from "yargs-parser"; import Hooks from "@poppinss/hooks"; import { Prompt, errors } from "@poppinss/prompts"; import { distance } from "fastest-levenshtein"; import { Exception, InvalidArgumentsException, RuntimeException, createError } from "@poppinss/utils/exception"; import { debuglog, inspect } from "node:util"; import string from "@poppinss/utils/string"; import Macroable from "@poppinss/macroable"; import lodash from "@poppinss/utils/lodash"; import { AssertionError } from "node:assert"; import { defineStaticProperty, importDefault } from "@poppinss/utils"; import stringWidth from "string-width"; import * as cliHelpers from "@poppinss/cliui/helpers"; import { TERMINAL_SIZE, justify, wrap } from "@poppinss/cliui/helpers"; import { Validator } from "jsonschema"; import diagnostics_channel from "node:diagnostics_channel"; import { fileURLToPath } from "node:url"; import { fsReadAll } from "@poppinss/utils/fs"; import { basename, extname, join, relative } from "node:path"; import { copyFile, mkdir, writeFile } from "node:fs/promises"; //#region \0rolldown/runtime.js var __defProp = Object.defineProperty; var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; //#endregion //#region src/yars_config.ts /** * The fixed config used to parse command line arguments using yargs. We * do not allow changing these options, since some of the internal * checks and features rely on this specific config */ const yarsConfig = { "camel-case-expansion": false, "combine-arrays": true, "short-option-groups": true, "dot-notation": false, "parse-numbers": true, "parse-positional-numbers": false, "boolean-negation": true, "flatten-duplicate-arrays": true, "greedy-arrays": false, "strip-aliased": true, "nargs-eats-options": false, "unknown-options-as-args": false }; //#endregion //#region src/parser.ts /** * Parses the command line arguments. The flags are parsed using yargs-parser * * @example * ```ts * const parser = new Parser({ flagsParserOptions, argumentsParserOptions }) * const parsed = parser.parse(['--verbose', 'create', 'User']) * console.log(parsed.args) // ['User'] * console.log(parsed.flags) // { verbose: true } * ``` */ var Parser = class { /** * Parser options for flags and arguments */ #options; /** * Create a new parser instance * * @param options - Parser configuration options */ constructor(options) { this.#options = options; } /** * Parse flags using yargs parser * * @param argv - Command line arguments */ #parseFlags(argv) { return yargsParser(argv, { ...this.#options.flagsParserOptions, configuration: yarsConfig }); } /** * Scans for unknown flags in yargs output * * @param parsed - Parsed flags object */ #scanUnknownFlags(parsed) { const unknownFlags = []; for (let key of Object.keys(parsed)) if (!this.#options.flagsParserOptions.all.includes(key)) unknownFlags.push(key); return unknownFlags; } /** * Parse arguments by mimicking the yargs behavior * * @param parsedOutput - Output from yargs parser */ #parseArguments(parsedOutput) { let lastParsedIndex = -1; const output = this.#options.argumentsParserOptions.map((option, index) => { if (option.type === "spread") { let value = parsedOutput._.slice(index); lastParsedIndex = parsedOutput._.length; /** * Step 1 * * Use default value when original value is not defined. */ if (!value.length) value = Array.isArray(option.default) ? option.default : option.default === void 0 ? void 0 : [option.default]; /** * Step 2 * * Call parse method when value is not undefined */ if (value !== void 0 && option.parse) value = option.parse(value); return value; } let value = parsedOutput._[index]; lastParsedIndex = index + 1; /** * Step 1: * * Use default value when original value is undefined * Original value set to empty string will be used * as real value. The behavior is same as yargs * flags parser `--connection=` */ if (value === void 0) value = option.default; /** * Step 2 * * Call parse method when value is not undefined */ if (value !== void 0 && option.parse) value = option.parse(value); return value; }); const { "_": args, "--": o, ...rest } = parsedOutput; return { args: output, nodeArgs: [], _: args.slice(lastParsedIndex === -1 ? 0 : lastParsedIndex), unknownFlags: this.#scanUnknownFlags(rest), flags: rest }; } /** * Parse command-line arguments into structured format * * @param argv - Command line arguments as string or array * * @example * ```ts * const parsed = parser.parse(['--verbose', 'create', 'User']) * // Returns: { args: ['User'], flags: { verbose: true }, ... } * ``` */ parse(argv) { return this.#parseArguments(this.#parseFlags(argv)); } }; //#endregion //#region src/debug.ts var debug_default = debuglog("adonisjs:ace"); //#endregion //#region src/errors.ts var errors_exports = /* @__PURE__ */ __exportAll({ E_COMMAND_NOT_FOUND: () => E_COMMAND_NOT_FOUND, E_INVALID_FLAG: () => E_INVALID_FLAG, E_MISSING_ARG: () => E_MISSING_ARG, E_MISSING_ARG_VALUE: () => E_MISSING_ARG_VALUE, E_MISSING_COMMAND_NAME: () => E_MISSING_COMMAND_NAME, E_MISSING_FLAG: () => E_MISSING_FLAG, E_MISSING_FLAG_VALUE: () => E_MISSING_FLAG_VALUE, E_PROMPT_CANCELLED: () => E_PROMPT_CANCELLED, E_UNKNOWN_FLAG: () => E_UNKNOWN_FLAG }); const E_PROMPT_CANCELLED = errors.E_PROMPT_CANCELLED; /** * Command is missing the static property command name */ const E_MISSING_COMMAND_NAME = createError("Cannot serialize command \"%s\". Missing static property \"commandName\"", "E_MISSING_COMMAND_NAME"); /** * Cannot find a command for the given name */ const E_COMMAND_NOT_FOUND = class CommandNotFound extends Exception { static status = 404; commandName; constructor(args) { super(`Command "${args[0]}" is not defined`, { code: "E_COMMAND_NOT_FOUND" }); this.commandName = args[0]; } }; /** * Missing a required flag when running the command */ const E_MISSING_FLAG = createError("Missing required option \"%s\"", "E_MISSING_FLAG"); /** * Missing value for a flag that accepts values */ const E_MISSING_FLAG_VALUE = createError("Missing value for option \"%s\"", "E_MISSING_FLAG_VALUE"); /** * Missing a required argument when running the command */ const E_MISSING_ARG = createError("Missing required argument \"%s\"", "E_MISSING_ARG"); /** * Missing value for an argument */ const E_MISSING_ARG_VALUE = createError("Missing value for argument \"%s\"", "E_MISSING_ARG_VALUE"); /** * An unknown flag was mentioned */ const E_UNKNOWN_FLAG = createError("Unknown flag \"%s\". The mentioned flag is not accepted by the command", "E_UNKNOWN_FLAG"); /** * Invalid value provided for the flag */ const E_INVALID_FLAG = createError("Invalid value. The \"%s\" flag accepts a \"%s\" value", "E_INVALID_FLAG"); //#endregion //#region src/commands/base.ts /** * The base command sets the foundation for defining ace commands. * Every command should inherit from the base command. * * @example * ```ts * export class MyCommand extends BaseCommand { * static commandName = 'my:command' * static description = 'My custom command' * * async run() { * this.logger.info('Hello from my command!') * } * } * ``` */ var BaseCommand = class extends Macroable { /** * Whether the command class has been booted */ static booted = false; /** * Configuration options accepted by the command */ static options; /** * A collection of aliases for the command */ static aliases; /** * The command name one can type to run the command */ static commandName; /** * The command description */ static description; /** * The help text for the command. Help text can be a multiline * string explaining the usage of command */ static help; /** * Registered arguments */ static args; /** * Registered flags */ static flags; /** * Define static properties on the class. During inheritance, certain * properties must inherit from the parent. * * @example * ```ts * MyCommand.boot() * ``` */ static boot() { if (Object.hasOwn(this, "booted") && this.booted === true) return; this.booted = true; defineStaticProperty(this, "args", { initialValue: [], strategy: "inherit" }); defineStaticProperty(this, "flags", { initialValue: [], strategy: "inherit" }); defineStaticProperty(this, "aliases", { initialValue: [], strategy: "inherit" }); defineStaticProperty(this, "commandName", { initialValue: "", strategy: "inherit" }); defineStaticProperty(this, "description", { initialValue: "", strategy: "inherit" }); defineStaticProperty(this, "help", { initialValue: "", strategy: "inherit" }); defineStaticProperty(this, "options", { initialValue: { staysAlive: false, allowUnknownFlags: false }, strategy: "inherit" }); } /** * Specify the argument the command accepts. The arguments via the CLI * will be accepted in the same order as they are defined. * * Mostly, you will be using the `@args` decorator to define the arguments. * * @param name - The name of the argument * @param options - Configuration options for the argument * * @example * ```ts * Command.defineArgument('entity', { type: 'string' }) * Command.defineArgument('files', { type: 'spread', required: false }) * ``` */ static defineArgument(name, options) { this.boot(); const arg = { name, argumentName: string.dashCase(name), required: true, ...options }; const lastArg = this.args[this.args.length - 1]; /** * Ensure the arg type is specified */ if (!arg.type) throw new InvalidArgumentsException(`Cannot define argument "${this.name}.${name}". Specify the argument type`); /** * Ensure we are not adding arguments after a spread argument */ if (lastArg && lastArg.type === "spread") throw new InvalidArgumentsException(`Cannot define argument "${this.name}.${name}" after spread argument "${this.name}.${lastArg.name}". Spread argument should be the last one`); /** * Ensure we are not adding a required argument after an optional * argument */ if (arg.required && lastArg && lastArg.required === false) throw new InvalidArgumentsException(`Cannot define required argument "${this.name}.${name}" after optional argument "${this.name}.${lastArg.name}"`); if (debug_default.enabled) debug_default("defining arg %O, command: %O", arg, `[class: ${this.name}]`); this.args.push(arg); } /** * Specify a flag the command accepts. * * Mostly, you will be using the `@flags` decorator to define a flag. * * @param name - The name of the flag * @param options - Configuration options for the flag * * @example * ```ts * Command.defineFlag('connection', { type: 'string', required: true }) * Command.defineFlag('force', { type: 'boolean' }) * Command.defineFlag('tags', { type: 'array' }) * ``` */ static defineFlag(name, options) { this.boot(); const flag = { name, flagName: string.dashCase(name), required: false, ...options }; /** * Ensure the arg type is specified */ if (!flag.type) throw new InvalidArgumentsException(`Cannot define flag "${this.name}.${name}". Specify the flag type`); if (debug_default.enabled) debug_default("defining flag %O, command: %O", flag, `[class: ${this.name}]`); this.flags.push(flag); } /** * Returns the options for parsing flags and arguments * * @param options - Optional parser options to merge */ static getParserOptions(options) { this.boot(); const argumentsParserOptions = this.args.map((arg) => { return { type: arg.type, default: arg.default, parse: arg.parse }; }); const flagsParserOptions = lodash.merge({ all: [], string: [], boolean: [], array: [], number: [], alias: {}, count: [], coerce: {}, default: {} }, options); this.flags.forEach((flag) => { flagsParserOptions.all.push(flag.flagName); if (flag.alias) flagsParserOptions.alias[flag.flagName] = flag.alias; if (flag.parse) flagsParserOptions.coerce[flag.flagName] = flag.parse; if (flag.default !== void 0) flagsParserOptions.default[flag.flagName] = flag.default; switch (flag.type) { case "string": flagsParserOptions.string.push(flag.flagName); break; case "boolean": flagsParserOptions.boolean.push(flag.flagName); break; case "number": flagsParserOptions.number.push(flag.flagName); break; case "array": flagsParserOptions.array.push(flag.flagName); break; } }); return { flagsParserOptions, argumentsParserOptions }; } /** * Serializes the command to JSON. The return value satisfies the * {@link CommandMetaData} * * @example * ```ts * const metadata = MyCommand.serialize() * console.log(metadata.commandName) // 'my:command' * ``` */ static serialize() { this.boot(); if (!this.commandName) throw new E_MISSING_COMMAND_NAME([this.name]); const [namespace, name] = this.commandName.split(":"); return { commandName: this.commandName, description: this.description, help: this.help, namespace: name ? namespace : null, aliases: this.aliases, flags: this.flags.map((flag) => { const { parse, ...rest } = flag; return rest; }), args: this.args.map((arg) => { const { parse, ...rest } = arg; return rest; }), options: this.options }; } /** * Validate the yargs parsed output against the command. * * @param parsedOutput - The parsed CLI input to validate * * @example * ```ts * const parsed = { args: ['value'], flags: { force: true }, unknownFlags: [] } * MyCommand.validate(parsed) * ``` */ static validate(parsedOutput) { this.boot(); /** * Validates args and their values */ this.args.forEach((arg, index) => { const value = parsedOutput.args[index]; const hasDefinedArgument = value !== void 0; if (arg.required && !hasDefinedArgument) throw new E_MISSING_ARG([arg.name]); if (hasDefinedArgument && !arg.allowEmptyValue && (value === "" || !value.length)) { if (debug_default.enabled) debug_default("disallowing empty value \"%s\" for arg: \"%s\"", value, arg.name); throw new E_MISSING_ARG_VALUE([arg.name]); } }); /** * Disallow unknown flags */ if (!this.options.allowUnknownFlags && parsedOutput.unknownFlags.length) { const unknowFlag = parsedOutput.unknownFlags[0]; throw new E_UNKNOWN_FLAG([unknowFlag.length === 1 ? `-${unknowFlag}` : `--${unknowFlag}`]); } /** * Validate flags */ this.flags.forEach((flag) => { const hasMentionedFlag = Object.hasOwn(parsedOutput.flags, flag.flagName); const value = parsedOutput.flags[flag.flagName]; /** * Validate the value by flag type */ switch (flag.type) { case "boolean": /** * If flag is required, then it should be mentioned */ if (flag.required && !hasMentionedFlag) throw new E_MISSING_FLAG([flag.flagName]); break; case "number": /** * If flag is required, then it should be mentioned */ if (flag.required && !hasMentionedFlag) throw new E_MISSING_FLAG([flag.flagName]); /** * Regardless of whether flag is required or not. If it is mentioned, * then some value should be provided. * * In case of number input, yargs sends undefined */ if (hasMentionedFlag && value === void 0) throw new E_MISSING_FLAG_VALUE([flag.flagName]); if (Number.isNaN(value)) throw new E_INVALID_FLAG([flag.flagName, "numeric"]); break; case "string": case "array": /** * If flag is required, then it should be mentioned */ if (flag.required && !hasMentionedFlag) throw new E_MISSING_FLAG([flag.flagName]); /** * Regardless of whether flag is required or not. If it is mentioned, * then some value should be provided, unless empty values are * allowed. * * In case of string, flag with no value receives an empty string * In case of array, flag with no value receives an empty array */ if (hasMentionedFlag && !flag.allowEmptyValue && (value === "" || !value.length)) { if (debug_default.enabled) debug_default("disallowing empty value \"%s\" for flag: \"%s\"", value, flag.name); throw new E_MISSING_FLAG_VALUE([flag.flagName]); } } }); } /** * Check if a command has been hydrated */ hydrated = false; /** * The exit code for the command */ exitCode; /** * The error raised at the time of executing the command. * The value is undefined if no error is raised. */ error; /** * The result property stores the return value of the "run" * method (unless command sets it explicitly) */ result; /** * Logger to log messages * * @example * ```ts * this.logger.info('Command executed successfully') * this.logger.error('Something went wrong') * ``` */ get logger() { return this.ui.logger; } /** * Add colors to console messages * * @example * ```ts * this.logger.info(this.colors.green('Success!')) * this.logger.error(this.colors.red('Error!')) * ``` */ get colors() { return this.ui.colors; } /** * Is the current command the main command executed from the CLI * * @example * ```ts * if (this.isMain) { * this.logger.info('This is the main command') * } * ``` */ get isMain() { return this.kernel.getMainCommand() === this; } /** * Reference to the command name */ get commandName() { return this.constructor.commandName; } /** * Reference to the command options */ get options() { return this.constructor.options; } /** * Reference to the command args */ get args() { return this.constructor.args; } /** * Reference to the command flags */ get flags() { return this.constructor.flags; } /** * Create a new base command instance * * @param kernel - The Ace kernel instance * @param parsed - The parsed CLI input * @param ui - UI primitives for output * @param prompt - Prompt utilities for user interaction */ constructor(kernel, parsed, ui, prompt) { super(); this.kernel = kernel; this.parsed = parsed; this.ui = ui; this.prompt = prompt; } /** * Hydrate command by setting class properties from the parsed output * * @example * ```ts * command.hydrate() * console.log(command.name) // Argument value * console.log(command.force) // Flag value * ``` */ hydrate() { if (this.hydrated) return; const CommandConstructor = this.constructor; /** * Set args as properties on the command instance */ CommandConstructor.args.forEach((arg, index) => { Object.defineProperty(this, arg.name, { value: this.parsed.args[index], enumerable: true, writable: true, configurable: true }); }); /** * Set flags as properties on the command instance */ CommandConstructor.flags.forEach((flag) => { Object.defineProperty(this, flag.name, { value: this.parsed.flags[flag.flagName], enumerable: true, writable: true, configurable: true }); }); this.hydrated = true; } /** * The run method should include the implementation for the command. * * @param _ - Additional arguments (not used in base implementation) * * @example * ```ts * async run() { * this.logger.info('Running my command') * return 'Command completed' * } * ``` */ async run(..._) {} /** * Executes the command by running the command's run method. * * @example * ```ts * const result = await command.exec() * console.log('Exit code:', command.exitCode) * ``` */ async exec() { this.hydrate(); try { this.result = await this.run(); this.exitCode = this.exitCode ?? 0; return this.result; } catch (error) { this.error = error; this.exitCode = this.exitCode ?? 1; throw error; } } /** * JSON representation of the command * * @example * ```ts * const json = command.toJSON() * console.log(json.commandName, json.exitCode) * ``` */ toJSON() { return { commandName: this.constructor.commandName, options: this.constructor.options, args: this.parsed.args, flags: this.parsed.flags, error: this.error, result: this.result, exitCode: this.exitCode }; } /** * Assert the command exits with a given exit code * * @param code - The expected exit code * * @example * ```ts * command.assertExitCode(0) // Assert successful execution * command.assertExitCode(1) // Assert failure * ``` */ assertExitCode(code) { if (this.exitCode !== code) throw new AssertionError({ message: `Expected '${this.commandName}' command to finish with exit code '${code}'`, actual: this.exitCode, expected: code, operator: "strictEqual", stackStartFn: this.assertExitCode }); } /** * Assert the command does not exit with a given exit code * * @param code - The exit code that should not be used * * @example * ```ts * command.assertNotExitCode(1) // Assert no failure * ``` */ assertNotExitCode(code) { if (this.exitCode === code) throw new AssertionError({ message: `Expected '${this.commandName}' command to finish without exit code '${this.exitCode}'`, stackStartFn: this.assertNotExitCode }); } /** * Assert the command exits with zero exit code * * @example * ```ts * command.assertSucceeded() // Assert success * ``` */ assertSucceeded() { return this.assertExitCode(0); } /** * Assert the command exits with non-zero exit code * * @example * ```ts * command.assertFailed() // Assert failure * ``` */ assertFailed() { return this.assertNotExitCode(0); } /** * Assert command logs the expected message * * @param message - The expected log message * @param stream - Optional stream to check ('stdout' or 'stderr') * * @example * ```ts * command.assertLog('Command executed successfully') * command.assertLog('Error occurred', 'stderr') * ``` */ assertLog(message, stream) { const logs = this.logger.getLogs(); const logMessages = logs.map((log) => log.message); const matchingLog = logs.find((log) => log.message === message); /** * No log found */ if (!matchingLog) throw new AssertionError({ message: `Expected log messages to include ${inspect(message)}`, actual: logMessages, expected: [message], operator: "strictEqual", stackStartFn: this.assertLog }); /** * Log is on a different stream */ if (stream && matchingLog.stream !== stream) throw new AssertionError({ message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect(matchingLog.stream)}`, actual: matchingLog.stream, expected: stream, operator: "strictEqual", stackStartFn: this.assertLog }); } /** * Assert command logs a message matching the given regex * * @param matchingRegex - The regex pattern to match against log messages * @param stream - Optional stream to check ('stdout' or 'stderr') * * @example * ```ts * command.assertLogMatches(/^Command.*completed$/) * command.assertLogMatches(/error/i, 'stderr') * ``` */ assertLogMatches(matchingRegex, stream) { const matchingLog = this.logger.getLogs().find((log) => matchingRegex.test(log.message)); /** * No log found */ if (!matchingLog) throw new AssertionError({ message: `Expected log messages to match ${inspect(matchingRegex)}`, stackStartFn: this.assertLogMatches }); /** * Log is on a different stream */ if (stream && matchingLog.stream !== stream) throw new AssertionError({ message: `Expected log message stream to be ${inspect(stream)}, instead received ${inspect(matchingLog.stream)}`, actual: matchingLog.stream, expected: stream, operator: "strictEqual", stackStartFn: this.assertLogMatches }); } /** * Assert the command prints a table with the expected rows to stdout * * @param rows - The expected table rows as arrays of strings * * @example * ```ts * command.assertTableRows([ * ['Name', 'Age'], * ['John', '25'], * ['Jane', '30'] * ]) * ``` */ assertTableRows(rows) { const logs = this.logger.getLogs(); if (!rows.every((row) => { const columnsContent = row.join("|"); return !!logs.find((log) => log.message === columnsContent); })) throw new AssertionError({ message: `Expected log messages to include a table with the expected rows`, operator: "strictEqual", stackStartFn: this.assertTableRows }); } }; //#endregion //#region src/decorators/args.ts /** * Namespace for defining arguments using decorators. * * Arguments are positional parameters that commands accept from the CLI. * They are parsed in the order they are defined and made available as * properties on the command instance. * * @example * ```ts * export class MakeCommand extends BaseCommand { * @args.string({ description: 'Entity name' }) * declare name: string * * @args.spread({ description: 'Additional files', required: false }) * declare files?: string[] * } * ``` */ const args = { string(options) { return function addArg(target, propertyName) { target.constructor.defineArgument(propertyName, { ...options, type: "string" }); }; }, spread(options) { return function addArg(target, propertyName) { target.constructor.defineArgument(propertyName, { ...options, type: "spread" }); }; } }; //#endregion //#region src/decorators/flags.ts /** * Namespace for defining flags using decorators. * * Flags are named options that commands can accept from the CLI. * They can be passed using --flag-name or -alias syntax and are * made available as properties on the command instance. * * @example * ```ts * export class MakeCommand extends BaseCommand { * @flags.boolean({ description: 'Skip confirmation prompts' }) * declare force: boolean * * @flags.string({ description: 'Database connection', alias: 'c' }) * declare connection?: string * * @flags.array({ description: 'Additional tags' }) * declare tags?: string[] * } * ``` */ const flags = { string(options) { return function addArg(target, propertyName) { target.constructor.defineFlag(propertyName, { type: "string", ...options }); }; }, boolean(options) { return function addArg(target, propertyName) { target.constructor.defineFlag(propertyName, { type: "boolean", ...options }); }; }, number(options) { return function addArg(target, propertyName) { target.constructor.defineFlag(propertyName, { type: "number", ...options }); }; }, array(options) { return function addArg(target, propertyName) { target.constructor.defineFlag(propertyName, { type: "array", ...options }); }; } }; //#endregion //#region src/formatters/flag.ts /** * The flag formatter formats a flag as per the http://docopt.org/ specification * * @example * ```ts * const formatter = new FlagFormatter(flag, colors) * const formatted = formatter.formatOption() // '--connection[=CONNECTION]' * const description = formatter.formatDescription() // 'Database connection' * ``` */ var FlagFormatter = class { /** * The flag configuration */ #flag; /** * Color utilities for formatting output */ #colors; /** * Create a new flag formatter * * @param flag - The flag configuration to format * @param colors - Color utilities for output formatting */ constructor(flag, colors) { this.#flag = flag; this.#colors = colors; } /** * Formats the value flag with proper placeholder syntax * * @param flag - The flag configuration * @param valuePlaceholder - The placeholder text for the flag value */ #formatValueFlag(flag, valuePlaceholder) { return flag.required ? `=${valuePlaceholder}` : `[=${valuePlaceholder}]`; } /** * Formats the aliases for the flag * * @param flag - The flag configuration */ #formatAliases(flag) { if (!flag.alias) return []; if (typeof flag.alias === "string") return [`-${flag.alias}`]; return flag.alias.map((alias) => `-${alias}`); } /** * Formats the array flag by appending ellipsis and wrapping the value * * @param flag - The array flag configuration */ #formatArrayFlag(flag) { const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}...`); const aliases = this.#formatAliases(flag); const flagWithValue = `--${flag.flagName}${value}`; if (aliases.length) return ` ${this.#colors.green(`${aliases.join(",")}, ${flagWithValue}`)} `; return ` ${this.#colors.green(flagWithValue)} `; } /** * Formats the string flag by wrapping the value to indicate if required * * @param flag - The string flag configuration */ #formatStringFlag(flag) { const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}`); const aliases = this.#formatAliases(flag); const flagWithValue = `--${flag.flagName}${value}`; if (aliases.length) return ` ${this.#colors.green(`${aliases.join(",")}, ${flagWithValue}`)} `; return ` ${this.#colors.green(flagWithValue)} `; } /** * Formats the numeric flag by wrapping the value to indicate if required * * @param flag - The numeric flag configuration */ #formatNumericFlag(flag) { const value = this.#formatValueFlag(flag, `${flag.flagName.toUpperCase()}`); const aliases = this.#formatAliases(flag); const flagWithValue = `--${flag.flagName}${value}`; if (aliases.length) return ` ${this.#colors.green(`${aliases.join(",")}, ${flagWithValue}`)} `; return ` ${this.#colors.green(flagWithValue)} `; } /** * Formats the boolean flag. Boolean flags need no value wrapping * * @param flag - The boolean flag configuration */ #formatBooleanFlag(flag) { const aliases = this.#formatAliases(flag); const negatedVariant = flag.showNegatedVariantInHelp ? `|--no-${flag.flagName}` : ""; const flagWithVariant = `--${flag.flagName}${negatedVariant}`; if (aliases.length) return ` ${this.#colors.green(`${aliases.join(",")}, ${flagWithVariant}`)} `; return ` ${this.#colors.green(flagWithVariant)} `; } /** * Returns formatted description for the flag * * @example * ```ts * formatter.formatDescription() // 'Database connection [default: mysql]' * ``` */ formatDescription() { const defaultValue = this.#flag.default !== void 0 ? `[default: ${this.#flag.default}]` : ""; const separator = defaultValue && this.#flag.description ? " " : ""; return this.#colors.dim(`${this.#flag.description || ""}${separator}${defaultValue}`); } /** * Returns a formatted version of the flag name and aliases * * @example * ```ts * formatter.formatOption() // '--connection[=CONNECTION]' or '--force, -f' * ``` */ formatOption() { switch (this.#flag.type) { case "array": return this.#formatArrayFlag(this.#flag); case "string": return this.#formatStringFlag(this.#flag); case "number": return this.#formatNumericFlag(this.#flag); case "boolean": return this.#formatBooleanFlag(this.#flag); } } }; //#endregion //#region src/formatters/list.ts /** * The list formatter formats the list of commands and flags. The option column * is justified to have same width across all the rows. * * @example * ```ts * const formatter = new ListFormatter(tables) * const formatted = formatter.format() // Array of formatted tables * ``` */ var ListFormatter = class { /** * Array of tables to format */ #tables; /** * The width of the largest option column across all tables */ #largestOptionColumnWidth; /** * Create a new list formatter * * @param tables - Array of tables to format */ constructor(tables) { this.#tables = tables; this.#largestOptionColumnWidth = Math.max(...this.#tables.map((table) => table.columns.map((column) => stringWidth(column.option))).flat()); } /** * Formats a single table to an array of plain text rows * * @param table - The table to format * @param terminalWidth - Width of the terminal for text wrapping */ #formatTable(table, terminalWidth) { const options = justify(table.columns.map(({ option }) => option), { maxWidth: this.#largestOptionColumnWidth }); const descriptions = wrap(table.columns.map(({ description }) => description), { startColumn: this.#largestOptionColumnWidth, endColumn: terminalWidth, trimStart: true }); return table.columns.map((_, index) => `${options[index]}${descriptions[index]}`); } /** * Format all tables into an array of formatted table objects * * @param terminalWidth - Width of the terminal for text wrapping * * @example * ```ts * formatter.format() // [{ heading: 'Commands:', rows: [...] }] * ``` */ format(terminalWidth = TERMINAL_SIZE) { return this.#tables.map((table) => { return { heading: table.heading, rows: this.#formatTable(table, terminalWidth) }; }); } }; //#endregion //#region schemas/main.ts const schema = { $ref: "#/definitions/CommandMetaData", $schema: "http://json-schema.org/draft-07/schema#", definitions: { CommandMetaData: { description: "Command metdata required to display command help.", properties: { aliases: { description: "Command aliases. The same command can be run using these aliases as well.", items: { type: "string" }, type: "array" }, args: { description: "Args accepted by the command", items: { additionalProperties: false, properties: { allowEmptyValue: { description: "Whether or not to allow empty values. When set to false, the validation will fail if the argument is provided an empty string\n\nDefaults to false", type: "boolean" }, argumentName: { type: "string" }, default: {}, description: { type: "string" }, name: { type: "string" }, required: { type: "boolean" }, type: { enum: ["string", "spread"], type: "string" } }, required: [ "name", "argumentName", "type" ], type: "object" }, type: "array" }, commandName: { description: "The name of the command", type: "string" }, description: { description: "The command description to show on the help screen", type: "string" }, flags: { description: "Flags accepted by the command", items: { additionalProperties: false, properties: { alias: { anyOf: [{ type: "string" }, { items: { type: "string" }, type: "array" }] }, allowEmptyValue: { description: "Whether or not to allow empty values. When set to false, the validation will fail if the flag is mentioned but no value is provided\n\nDefaults to false", type: "boolean" }, default: {}, description: { type: "string" }, flagName: { type: "string" }, name: { type: "string" }, required: { type: "boolean" }, showNegatedVariantInHelp: { description: "Whether or not to display the negated variant in the help output.\n\nApplicable for boolean flags only\n\nDefaults to false", type: "boolean" }, type: { enum: [ "string", "boolean", "number", "array" ], type: "string" } }, required: [ "name", "flagName", "type" ], type: "object" }, type: "array" }, help: { anyOf: [{ type: "string" }, { items: { type: "string" }, type: "array" }], description: "Help text for the command" }, namespace: { description: "Command namespace. The namespace is extracted from the command name", type: ["string", "null"] }, options: { $ref: "#/definitions/CommandOptions", description: "Command configuration options" } }, required: [ "aliases", "args", "commandName", "description", "flags", "namespace", "options" ], type: "object" }, CommandOptions: { description: "Static set of command options", properties: { allowUnknownFlags: { description: "Whether or not to allow for unknown flags. If set to false, the command will not run when unknown flags are provided through the CLI\n\nDefaults to false", type: "boolean" }, staysAlive: { description: "When flag set to true, the kernel will not trigger the termination process unless the command explicitly calls the terminate method.\n\nDefaults to false", type: "boolean" } }, type: "object" } } }; //#endregion //#region src/utils.ts /** * Helper to sort array of strings alphabetically. */ function sortAlphabetically(prev, curr) { if (curr > prev) return -1; if (curr < prev) return 1; return 0; } /** * Renders an error message and lists suggestions. */ function renderErrorWithSuggestions(ui, message, suggestions) { const instructions = ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar)); instructions.add(ui.colors.red(message)); if (suggestions.length) { instructions.add(""); instructions.add(`${ui.colors.dim("Did you mean?")} ${suggestions.slice(0, 4).join(", ")}`); } instructions.getRenderer().logError(instructions.prepare()); } /** * Validates the metadata of a command to ensure it has all the neccessary * properties */ function validateCommandMetaData(command, exportPath) { if (!command || typeof command !== "object") throw new RuntimeException(`Invalid command metadata exported from ${exportPath}`); try { new Validator().validate(command, schema, { throwError: true }); } catch (error) { if (error && typeof error === "object" && "message" in error) throw new RuntimeException(`Invalid command exported from ${exportPath}. ${error.message}`); } } /** * Validates the command class. We do not check it against the "BaseCommand" * class, because the ace version mis-match could make the validation * fail. */ function validateCommand(command, exportPath) { if (typeof command !== "function" || !command.toString().startsWith("class ")) throw new RuntimeException(`Invalid command exported from ${exportPath}. Expected command to be a class`); const commandConstructor = command; if (typeof commandConstructor.serialize !== "function") throw new RuntimeException(`Invalid command exported from ${exportPath}. Expected command to extend the "BaseCommand"`); validateCommandMetaData(commandConstructor.serialize(), exportPath); } //#endregion //#region src/formatters/argument.ts /** * The argument formatter formats an argument as per the http://docopt.org/ specification * * @example * ```ts * const formatter = new ArgumentFormatter(argument, colors) * const formatted = formatter.formatOption() // '<entity>' * const listOption = formatter.formatListOption() // ' entity ' * ``` */ var ArgumentFormatter = class { /** * The argument configuration */ #argument; /** * Color utilities for formatting output */ #colors; /** * Create a new argument formatter * * @param argument - The argument configuration to format * @param colors - Color utilities for output formatting */ constructor(argument, colors) { this.#argument = argument; this.#colors = colors; } /** * Wraps the optional placeholder on option arguments * * @param argument - The argument configuration * @param valuePlaceholder - The placeholder text for the argument value */ #formatArgument(argument, valuePlaceholder) { return argument.required ? `${valuePlaceholder}` : `[${valuePlaceholder}]`; } /** * Returns formatted description for the argument * * @example * ```ts * formatter.formatDescription() // 'The entity name [default: user]' * ``` */ formatDescription() { const defaultValue = this.#argument.default ? `[default: ${this.#argument.default}]` : ""; const separator = defaultValue && this.#argument.description ? " " : ""; return this.#colors.dim(`${this.#argument.description || ""}${separator}${defaultValue}`); } /** * Returns a formatted version of the argument name to be displayed inside a list * * @example * ```ts * formatter.formatListOption() // ' entity ' or ' [entity] ' for optional * ``` */ formatListOption() { switch (this.#argument.type) { case "spread": return ` ${this.#colors.green(this.#formatArgument(this.#argument, `${this.#argument.argumentName}...`))} `; case "string": return ` ${this.#colors.green(this.#formatArgument(this.#argument, `${this.#argument.argumentName}`))} `; } } /** * Returns a formatted version of the argument name to be displayed next to usage * * @example * ```ts * formatter.formatOption() // '<entity>' or '[<entity>]' for optional * ``` */ formatOption() { switch (this.#argument.type) { case "spread": return this.#colors.dim(`${this.#formatArgument(this.#argument, `<${this.#argument.argumentName}...>`)}`); case "string": return this.#colors.dim(`${this.#formatArgument(this.#argument, `<${this.#argument.argumentName}>`)}`); } } }; //#endregion //#region src/formatters/command.ts /** * The command formatter exposes API to format command data for the * commands list and the command help * * @example * ```ts * const formatter = new CommandFormatter(command, colors) * const usage = formatter.formatUsage(['alias'], 'node ace') * const description = formatter.formatDescription() * ``` */ var CommandFormatter = class { /** * The command metadata */ #command; /** * Color utilities for formatting output */ #colors; /** * Create a new command formatter * * @param command - The command metadata to format * @param colors - Color utilities for output formatting */ constructor(command, colors) { this.#command = command; this.#colors = colors; } /** * Returns the formatted command name to be displayed in the list of commands * * @param aliases - Array of command aliases * * @example * ```ts * formatter.formatListName(['make']) // ' generate:model (make) ' * ``` */ formatListName(aliases) { const formattedAliases = aliases.length ? ` ${this.#colors.dim(`(${aliases.join(", ")})`)}` : ""; return ` ${this.#colors.green(this.#command.commandName)}${formattedAliases} `; } /** * Returns the formatted description of the command * * @example * ```ts * formatter.formatDescription() // 'Generate a new model' * ``` */ formatDescription() { return this.#command.description || ""; } /** * Returns multiline command help with proper text wrapping * * @param binaryName - The binary name for interpolation * @param terminalWidth - Terminal width for text wrapping * * @example * ```ts * formatter.formatHelp('node ace') // Formatted help text * ``` */ formatHelp(binaryName, terminalWidth = TERMINAL_SIZE) { const binary = binaryName ? `${binaryName}` : ""; if (!this.#command.help) return ""; /** * Wrap text when goes over the terminal size */ return wrap((Array.isArray(this.#command.help) ? this.#command.help : [this.#command.help]).map((line) => string.interpolate(line, { binaryName: binary })), { startColumn: 2, trimStart: false, endColumn: terminalWidth }).join("\n"); } /** * Returns the formatted description to be displayed in the list of commands * * @example * ```ts * formatter.formatListDescription() // Dimmed description text * ``` */ formatListDescription() { if (!this.#command.description) return ""; return this.#colors.dim(this.#command.description); } /** * Returns an array of strings, each line contains an individual usage example * * @param aliases - Array of command aliases * @param binaryName - The binary name for usage examples * * @example * ```ts * formatter.formatUsage(['make'], 'node ace') * // [' node ace generate:model [options] <name>', ' node ace make [options] <name>'] * ``` */ formatUsage(aliases, binaryName) { const binary = binaryName ? `${binaryName} ` : ""; /** * Display options placeholder for flags */ const flags = this.#command.flags.length ? this.#colors.dim("[options]") : ""; /** * Display a list of named args */ const args = this.#command.args.map((arg) => new ArgumentFormatter(arg, this.#colors).formatOption()).join(" "); /** * Separator between options placeholder and args */ const separator = flags && args ? ` ${this.#colors.dim("[--]")} ` : ""; return [` ${binary}${this.#command.commandName} ${flags}${separator}${args}`].concat(aliases.map((alias) => ` ${binary}${alias} ${flags}${separator}${args}`)); } }; //#endregion //#region \0@oxc-project+runtime@0.121.0/helpers/decorate.js function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } //#endregion //#region src/commands/list.ts /** * The list command is used to view a list of commands * * @example * ```ts * // Usage from CLI: node ace list * // Or with namespace filter: node ace list migrate * // Or as JSON: node ace list --json * const listCommand = new ListCommand(kernel, parsed, ui, prompt) * await listCommand.run() * ``` */ var ListCommand = class extends BaseCommand { /** * Command metadata */ static commandName = "list"; static description = "View list of available commands"; static help = [ "The list command displays a list of all the commands:", " {{ binaryName }} list", "", "You can also display the commands for a specific namespace:", " {{ binaryName }} list <namespace...>" ]; /** * Returns a table for an array of commands * * @param heading - The table heading text * @param commands - Array of command metadata */ #makeCommandsTable(heading, commands) { return { heading: this.colors.yellow(heading), columns: commands.map((command) => { const aliases = this.kernel.getCommandAliases(command.commandName); const commandFormatter = new CommandFormatter(command, this.colors); return { option: commandFormatter.formatListName(aliases), description: commandFormatter.formatListDescription() }; }) }; } /** * Returns a table for an array of global options * * @param heading - The table heading text * @param flagsList - Array of global flags */ #makeOptionsTable(heading, flagsList) { return { heading: this.colors.yellow(heading), columns: flagsList.map((flag) => { const flagFormatter = new FlagFormatter(flag, this.colors); return { option: flagFormatter.formatOption(), description: flagFormatter.formatDescription() }; }) }; } /** * Returns an array of tables for all commands or for mentioned namespaces only * * @param namespaces - Optional array of namespaces to filter by */ #getCommandsTables(namespaces) { if (namespaces && namespaces.length) return namespaces.map((namespace) => { return this.#makeCommandsTable(namespace, this.kernel.getNamespaceCommands(namespace)); }); return [this.#makeCommandsTable("Available commands:", this.kernel.getNamespaceCommands()), ...this.kernel.getNamespaces().map((namespace) => this.#makeCommandsTable(namespace, this.kernel.getNamespaceCommands(namespace)))]; } /** * Returns table for the global flags */ #getOptionsTable() { if (!this.kernel.flags.length) return []; return [this.#makeOptionsTable("Options:", this.kernel.flags)]; } /** * Validates the namespaces mentioned via the namespaces argument */ #validateNamespace() { if (!this.namespaces) return true; const namespaces = this.kernel.getNamespaces(); const unknownNamespace = this.namespaces.find((namespace) => !namespaces.includes(namespace)); /** * Show error when the namespace is not known */ if (unknownNamespace) { renderErrorWithSuggestions(this.ui, `Namespace "${unknownNamespace}" is not defined`, this.kernel.getNamespaceSuggestions(unknownNamespace)); return false; } return true; } /** * Renders a formatted list of options and commands to the console */ renderList() { new ListFormatter(this.#getOptionsTable().concat(this.#getCommandsTables(this.namespaces))).format().forEach((table) => { this.logger.log(""); this.logger.log(table.heading); this.logger.log(table.rows.join("\n")); }); } /** * Returns command data as JSON for the --json flag */ renderToJSON() { if (this.namespaces && this.namespaces.length) return this.namespaces.map((namespace) => { return this.kernel.getNamespaceCommands(namespace); }).flat(1); return this.kernel.getNamespaceCommands().concat(this.kernel.getNamespaces().map((namespace) => this.kernel.getNamespaceCommands(namespace)).flat(1)); } /** * Executes the list command to display available commands * * @example * ```ts * await listCommand.run() * ``` */ async run() { if (!this.#validateNamespace()) { this.exitCode = 1; return; } if (this.json) { this.logger.log(JSON.stringify(this.renderToJSON(), null, 2)); return; } this.renderList(); } }; __decorate([args.spread({ description: "Filter list by namespace", required: false })], ListCommand.prototype, "namespaces", void 0); __decorate([flags.boolean({ description: "Get list of commands as JSON" })], ListCommand.prototype, "json", void 0); //#endregion //#region src/loaders/list_loader.ts /** * List loader exposes the API to register commands as classes * * @example * ```ts * const loader = new ListLoader([MyCommand, AnotherCommand]) * const metadata = await loader.getMetaData() * const command = await loader.getCommand(metadata[0]) * ``` */ var ListLoader = class { /** * Array of command classes */ #commands; /** * Create a new list loader * * @param commands - Array of command classes to register */ constructor(commands) { this.#