@adonisjs/ace
Version:
A CLI framework for Node.js
1,736 lines • 83.6 kB
JavaScript
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.#