@adonisjs/ace
Version:
A CLI framework for Node.js
3,025 lines • 87.3 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 = {
/**
* Define argument that accepts a string value
*
* @param options - Configuration options for the string argument
*
* @example
* ```ts
* export class MakeCommand extends BaseCommand {
* @args.string({ description: 'The entity name' })
* declare name: string
*
* @args.string({ description: 'Template type', required: false, default: 'default' })
* declare template?: string
* }
* ```
*/
string(options) {
return function addArg(target, propertyName) {
target.constructor.defineArgument(propertyName, {
...options,
type: "string"
});
};
},
/**
* Define argument that accepts a spread of values (variable number of arguments)
*
* Spread arguments collect all remaining positional arguments into an array.
* Only one spread argument is allowed per command and it must be the last argument.
*
* @param options - Configuration options for the spread argument
*
* @example
* ```ts
* export class InstallCommand extends BaseCommand {
* @args.string({ description: 'Package manager' })
* declare manager: string
*
* @args.spread({ description: 'Package names to install', required: false })
* declare packages?: string[]
* }
* // Usage: node ace install npm lodash axios moment
* // manager = 'npm', packages = ['lodash', 'axios', 'moment']
* ```
*/
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 = {
/**
* Define flag that accepts a string value
*
* @param options - Configuration options for the string flag
*
* @example
* ```ts
* export class MakeCommand extends BaseCommand {
* @flags.string({ description: 'Database connection name', alias: 'c' })
* declare connection?: string
*
* @flags.string({ description: 'Template name', required: true, default: 'default' })
* declare template: string
* }
* // Usage: node ace make --connection=mysql --template=api
* // Usage: node ace make -c mysql --template=api
* ```
*/
string(options) {
return function addArg(target, propertyName) {
target.constructor.defineFlag(propertyName, {
type: "string",
...options
});
};
},
/**
* Define flag that accepts a boolean value
*
* Boolean flags don't require a value - their presence indicates true.
* They can optionally support negated variants with --no-flag-name.
*
* @param options - Configuration options for the boolean flag
*
* @example
* ```ts
* export class MigrateCommand extends BaseCommand {
* @flags.boolean({ description: 'Skip confirmation prompts', alias: 'f' })
* declare force: boolean
*
* @flags.boolean({ description: 'Run in dry mode', showNegatedVariantInHelp: true })
* declare dryRun: boolean
* }
* // Usage: node ace migrate --force
* // Usage: node ace migrate -f
* // Usage: node ace migrate --dry-run or --no-dry-run
* ```
*/
boolean(options) {
return function addArg(target, propertyName) {
target.constructor.defineFlag(propertyName, {
type: "boolean",
...options
});
};
},
/**
* Define flag that accepts a numeric value
*
* @param options - Configuration options for the number flag
*
* @example
* ```ts
* export class ServeCommand extends BaseCommand {
* @flags.number({ description: 'Port number', alias: 'p', default: 3000 })
* declare port: number
*
* @flags.number({ description: 'Number of workers', required: false })
* declare workers?: number
* }
* // Usage: node ace serve --port=8080
* // Usage: node ace serve -p 8080 --workers=4
* ```
*/
number(options) {
return function addArg(target, propertyName) {
target.constructor.defineFlag(propertyName, {
type: "number",
...options
});
};
},
/**
* Define flag that accepts an array of values
*
* Array flags can be specified multiple times to build up an array of values.
*
* @param options - Configuration options for the array flag
*
* @example
* ```ts
* export class BuildCommand extends BaseCommand {
* @flags.array({ description: 'Include additional files' })
* declare include?: string[]
*
* @flags.array({ description: 'Environment variables', alias: 'e' })
* declare env?: string[]
* }
* // Usage: node ace build --include=*.js --include=*.css
* // Usage: node ace build -e NODE_ENV=production -e DEBUG=false
* // Results: include = ['*.js', '*.css'], env = ['NODE_ENV=production', 'DEBUG=false']
* ```
*/
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 = {
$id: "adonisjs://ace/command-metadata",
$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.127.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.#commands = commands;
}
/**
* Returns an array of command metadata for all registered commands
*
* @example
* ```ts
* const metadata = await loader.getMetaData()
* metadata.forEach(cmd => console.log(cmd.commandName))
* ```
*/
async getMetaData() {
return this.#commands.map((command) => command.serialize());
}
/**
* Returns the command class constructor for a given command. Null
* is returned when unable to lookup the command
*
* @param metaData - The command metadata to find
*
* @example
* ```ts
* const command = await loader.getCommand(metadata)
* if (command) {
* const instance = new command(kernel, parsed, ui, prompt)
* }
* ```
*/
async getCommand(metaData) {
return this.#commands.find((command) => command.commandName === metaData.commandName) || null;
}
};
//#endregion
//#region src/exception_handler.ts
/**
* The base exception handler that is used by default to handle
* Ace exceptions.
*
* You can extend this class to customize the exception rendering
* behavior.
*
* @example
* ```ts
* export class MyExceptionHandler extends ExceptionHandler {
* async render(error: unknown, kernel: Kernel<any>) {
* // Custom error handling
* await super.render(error, kernel)
* }
* }
* ```
*/
var ExceptionHandler = class {
/**
* Enable debug mode for detailed error reporting
*/
debug = true;
/**
* Known error codes. For these errors, only the error message is
* reported using the logger
*/
knownErrorCodes = [];
/**
* Internal set of known error codes
*/
internalKnownErrorCode = Object.keys(errors_exports);
/**
* Logs error to stderr using logger
*
* @param error - The error object with a message property
* @param kernel - The Ace kernel instance
*/
logError(error, kernel) {
kernel.ui.logger.logError(`${kernel.ui.colors.bgRed().white(" ERROR ")} ${error.message}`);
}
/**
* Pretty prints uncaught error in debug mode using Youch
*
* @param error - The error object to pretty print
*/
async prettyPrintError(error) {
const { Youch } = await import("youch");
const youch = new Youch();
console.log(await youch.toANSI(error));
}
/**
* Renders an exception for the console with appropriate formatting
*
* @param error - The error to render
* @param kernel - The Ace kernel instance
*
* @example
* ```ts
* const handler = new ExceptionHandler()
* await handler.render(new Error('Something went wrong'), kernel)
* ```
*/
async render(error, kernel) {
/**
* Render non object errors or errors without message property
* as a string using the logger
*/
if (typeof error !== "object" || error === null || !("message" in error)) {
this.logError({ message: String(error) }, kernel);
return;
}
/**
* Report command not found error with command suggestions
*/
if (error instanceof E_COMMAND_NOT_FOUND) {
renderErrorWithSuggestions(kernel.ui, error.message, kernel.getCommandSuggestions(error.commandName));
return;
}
/**
* Display prompt cancellation error
*/
if (error instanceof errors.E_PROMPT_CANCELLED) {
this.logError({ message: "Prompt cancelled" }, kernel);
return;
}
/**
* Known errors should always be reported with a message
*/
if ("code" in error && typeof error.code === "string" && (this.internalKnownErrorCode.includes(error.code) || this.knownErrorCodes.includes(error.code))) {
this.logError({ message: error.message }, kernel);
return;
}
/**
* Allow errors to be self handled.
*/
if ("render" in error && typeof error.render === "function") return error.render(error, kernel);
/**
* Log error message and stack only when not in debug mode
*/
if (!this.debug) {
kernel.ui.logger.fatal(error);
return;
}
try {
await this.prettyPrintError(error);
} catch {
kernel.ui.logger.fatal(error);
}
}
};
//#endregion
//#region src/tracing_channels.ts
var tracing_channels_exports = /* @__PURE__ */ __exportAll({ commandExec: () => commandExec });
/**
* Traces every command execution handled by the {@link Kernel} class.
*/
const commandExec = diagnostics_channel.tracingChannel("adonisjs.command.exec");
//#endregion
//#region src/kernel.ts
/**
* The Ace kernel manages the registration and execution of commands.
*
* The kernel is the main entry point of a console application, and
* is tailored for a standard CLI environment.
*
* @example
* ```ts
* const kernel = Kernel.create()
*
* kernel.defineFlag('help', {
* type: 'boolean',
* alias: 'h',
* description: 'Display help for the given command. When no command is given display help for the list command'
* })
* kernel.on('help', async (command, $kernel, options) => {
* options.args.unshift(command.commandName)
* await new HelpCommand($kernel, options, kernel.ui, kernel.prompt).exec()
* return true
* })
*
* kernel.addLoader(new FsLoader('./commands'))
*
* kernel.info.set('binary', 'node ace')
* kernel.info.set('Framework version', '9.1')
* kernel.info.set('App version', '1.1.1')
*
* await kernel.handle(process.argv.slice(2))
* ```
*/
var Kernel = class Kernel {
/**
* The error handler for rendering exceptions
*/
errorHandler = new ExceptionHandler();
/**
* The default executor for creating command instances and running them
*/
static commandExecutor = {
create(command, parsedArgs, kernel) {
return new command(kernel, parsedArgs, kernel.ui, kernel.prompt);
},
run(command) {
return command.exec();
}
};
/**
* The default command to use when creating kernel instance via static create method
*/
static defaultCommand = ListCommand;
/**
* Creates an instance of kernel with the default executor and default command
*
* @example
* ```ts
* const kernel = Kernel.create()
* ```
*/
static create() {
return new Kernel(this.defaultCommand, this.commandExecutor);
}
/**
* Listeners for CLI options. Executed for the main command
* only
*/
#optionListeners = /* @__PURE__ */ new Map();
/**
* The global command is used to register global flags applicable
* on all the commands
*/
#globalCommand = class extends BaseCommand {
static options = { allowUnknownFlags: true };
};
/**
* The default command to run when no command is mentioned. The default
* command will also run when only flags are mentioned.
*/
#defaultCommand;
/**
* Available hooks
*/
#hooks = new Hooks();
/**
* Executors are used to instantiate a command and execute
* the run method.
*/
#executor;
/**
* Keeping track of the main command. There are some action (like termination)
* that only the main command can perform
*/
#mainCommand;
/**
* The current state of kernel. The `running` and `terminated`
* states are only set when kernel takes over the process.
*/
#state = "idle";
/**
* Collection of loaders to use for loading commands
*/
#loaders = [];
/**
* An array of registered namespaces. Sorted alphabetically
*/
#namespaces = [];
/**
* A collection of aliases for the commands. The key is the alias name
* and the value is the command name.
*
* In case of duplicate aliases, the most recent alias will override
* the previous existing alias.
*/
#aliases = /* @__PURE__ */ new Map();
/**
* An collection of expansion arguments and flags set on
* an alias. The key is the alias name and the value is
* everything after it.
*/
#aliasExpansions = /* @__PURE__ */ new Map();
/**
* A collection of commands by the command name. This allows us to keep only
* the unique commands and also keep the loader reference to know which
* loader to ask for loading the command.
*/
#commands = /* @__PURE__ */ new Map();
/**
* The exit code for the kernel. The exit code is inferred
* from the main command when not set explicitly
*/
exitCode;
/**
* The UI primitives to use within commands
*/
ui = cliui$1();
/**
* Instance of prompt to display CLI prompts. We share
* a single instance with all the commands. This
* allows trapping prompts for commands executed internally
*/
prompt = new Prompt();
/**
* CLI info map containing metadata about the application
*/
info = /* @__PURE__ */ new Map();
/**
* List of global flags available across all commands
*
* @example
* ```ts
* kernel.flags // [{ name: 'help', type: 'boolean', ... }]
* ```
*/
get flags() {
return this.#globalCommand.flags;
}
/**
* Create a new Kernel instance
*
* @param defaultCommand - The default command to run when no command is specified
* @param executor - The executor for creating and running command instances
*/
constructor(defaultCommand, executor) {
this.#defaultCommand = defaultCommand;
this.#executor = executor;
}
/**
* Process command line arguments. All flags before the command
* name are considered as Node.js argv and all flags after
* the command name are considered as command argv.
*
* The behavior is same as Node.js CLI, where all flags before the
* script file name are Node.js argv.
*/
#processArgv(argv) {
const commandNameIndex = argv.findIndex((value) => !value.startsWith("-"));
if (commandNameIndex === -1) return {
nodeArgv: [],
commandName: null,
commandArgv: argv
};
return {
nodeArgv: argv.slice(0, commandNameIndex),
commandName: argv[commandNameIndex],
commandArgv: argv.slice(commandNameIndex + 1)
};
}
/**
* Creates an instance of a command by parsing and validating
* the command line arguments.
*/
async #create(Command, argv) {
/**
* Parse CLI argv without global flags. When running commands directly, we
* should not be using global flags anyways
*/
const parsed = new Parser(Command.getParserOptions()).parse(argv);
/**
* Validate the parsed output
*/
Command.validate(parsed);
/**
* Construct command instance using the executor
*/
const commandInstance = await this.#executor.create(Command, parsed, this);
commandInstance.hydrate();
return commandInstance;
}
/**
* Executes a given command. The main commands are executed using the
* "execMain" method.
*/
async #exec(commandName, argv, options) {
const Command = await this.find(commandName);
/**
* Expand aliases
*/
const aliasExpansions = this.#aliasExpansions.get(commandName);
if (aliasExpansions) {
argv = aliasExpansions.concat(argv);
debug_default("expanding alias %O, cli args %O", commandName, argv);
}
const commandInstance = await this.#create(Command, argv);
/**
* Override the UI primitives when a custom instance is provided.
* This allows callers to silence command output by passing a
* UI backed by a memory renderer.
*/
if (options?.ui && "ui" in commandInstance) commandInstance.ui = options.ui;
/**
* Execute the command using the executor
*/
await this.#hooks.runner("executing").run(commandInstance, false);
await commandExec.tracePromise(this.#executor.run, commandExec.hasSubscribers ? {
command: Command,
commandInstance,
argv
} : void 0, this.#executor, commandInstance, this);
await this.#hooks.runner("executed").run(commandInstance, false);
return commandInstance;
}
/**
* Executes the main command and handles the exceptions by
* reporting them
*/
async #execMain(commandName, nodeArgv, argv) {
try {
const Command = await this.find(commandName);
/**
* Expand aliases
*/
const aliasExpansions = this.#aliasExpansions.get(commandName);
if (aliasExpansions) {
argv = aliasExpansions.concat(argv);
debug_default("expanding alias %O, cli args %O", commandName, argv);
}
/**
* Parse CLI argv and also merge global flags parser options.
*/
const parsed = new Parser(Command.getParserOptions(this.#globalCommand.getParserOptions().flagsParserOptions)).parse(argv);
/**
* Defined only for the main command
*/
parsed.nodeArgs = nodeArgv;
/**
* Validate the flags against the global list as well
*/
this.#globalCommand.validate(parsed);
/**
* Run options listeners. Option listeners can terminate
* the process early
*/
let shortcircuit = false;
for (let [option, listener] of this.#optionListeners) if (parsed.flags[option] !== void 0) {
debug_default("running listener for \"%s\" flag", option);
shortcircuit = await listener(Command, this, parsed);
if (shortcircuit) break;
}
/**
* Validate the parsed output
*/
Command.validate(parsed);
/**
* Return early if a flag listener shortcircuits
*/
if (shortcircuit) {
debug_default("short circuiting from flag listener");
this.exitCode = this.exitCode ?? 0;
this.#state = "completed";
return;
}
/**
* Keep a note of the main command
*/
this.#mainCommand = await this.#executor.create(Command, parsed, this);
this.#mainCommand.hydrate();
/**
* Execute the command using the executor
*/
await this.#hooks.runner("executing").run(this.#mainCommand, true);
await commandExec.tracePromise(this.#executor.run, commandExec.hasSubscribers ? {
command: Command,
commandInstance: this.#mainCommand,
argv
} : void 0, this.#executor, this.#mainCommand, this);
await this.#hooks.runner("executed").run(this.#mainCommand, true);
this.exitCode = this.exitCode ?? this.#mainCommand.exitCode;
this.#state = "completed";
} catch (error) {
this.exitCode = 1;
this.#state = "completed";
await this.errorHandler.render(error, this);
}
}
/**
* Listen for CLI options and execute an action. Only one listener
* can be defined per option.
*
* The callbacks are only executed for the main command
*/
on(option, callback) {
debug_default("registering flag listener for \"%s\" flag", option);
this.#optionListeners.set(option, callback);
return this;
}
/**
* Define a global flag that is applicable for all commands
*
* @param name - The flag name
* @param options - Flag configuration options
*
* @example
* ```ts
* kernel.defineFlag('verbose', { type: 'boolean', description: 'Enable verbose output' })
* ```
*/
defineFlag(name, options) {
if (this.#state !== "idle") throw new RuntimeException(`Cannot register global flag in "${this.#state}" state`);
this.#globalCommand.defineFlag(name, options);
}
/**
* Register a commands loader. The commands will be collected by all loaders.
*
* In case multiple loaders return a single command, the command from the
* most recent loader will be used.
*
* @param loader - The loader instance or a function that returns a loader
*
* @example
* ```ts
* kernel.addLoader(new FsLoader('./commands'))
* kernel.addLoader(() => import('./lazy-loader').then(m => new m.LazyLoader()))
* ```
*/
addLoader(loader) {
if (this.#state !== "idle") throw new RuntimeException(`Cannot add loader in "${this.#state}" state`);
this.#loaders.push(loader);
return this;
}
/**
* Register alias for a command name
*
* @param alias - The alias name
* @param command - The command name (can include arguments)
*
* @example
* ```ts
* kernel.addAlias('m', 'make:model')
* kernel.addAlias('migrate:fresh', 'migration:rollback --to=0 && migration:run')
* ```
*/
addAlias(alias, command) {
const [commandName, ...expansions] = command.split(" ");
this.#aliases.set(alias, commandName);
if (expansions.length) {
debug_default("registering alias %O for command %O with options %O", alias, commandName, expansions);
this.#aliasExpansions.set(alias, expansions);
} else debug_default("registering alias %O for command %O", alias, commandName);
return this;
}
/**
* Check if a command or an alias is registered with kernel
*/
hasCommand(commandName) {
commandName = this.#aliases.get(commandName) || commandName;
return this.#commands.has(commandName);
}
/**
* Get the current state of the kernel.
*/
getState() {
return this.#state;
}
/**
* Returns a flat list of commands metadata registered with the kernel.
* The list is sorted alphabetically by the command name.
*/
getCommands() {
return [...this.#commands.keys()].sort(sortAlphabetically).map((name) => this.#commands.get(name).metaData);
}
/**
* Get a list of commands for a specific namespace. All non-namespaces
* commands will be returned if no namespace is defined.
*/
getNamespaceCommands(namespace) {
let commandNames = [...this.#commands.keys()];
/**
* Filter a list of commands by the namespace
*/
if (namespace) commandNames = commandNames.filter((name) => this.#commands.get(name).metaData.namespace === namespace);
else commandNames = commandNames.filter((name) => !this.#commands.get(name).metaData.namespace);
return commandNames.sort(sortAlphabetically).map((name) => this.#commands.get(name).metaData);
}
/**
* Returns the command metadata by its name. Returns null when the
* command is missing.
*/
getCommand(commandName) {
return this.#commands.get(commandName)?.metaData || null;
}
/**
* Returns a reference for the default command. The return value
* is the default command constructor
*/
getDefaultCommand() {
return this.#defaultCommand;
}
/**
* Returns reference to the main command
*/
getMainCommand() {
return this.#mainCommand;
}
/**
* Returns an array of aliases registered.
*
* - Call `getCommandAliases` method to get aliases for a given command
* - Call `getAliasCommand` to get the command or a given alias
*/
getAliases() {
return [...this.#aliases.keys()];
}
/**
* Returns the command metata for a given alias. Returns null
* if alias is not recognized.
*/
getAliasCommand(alias) {
const aliasCommand = this.#aliases.get(alias);
if (!aliasCommand) return null;
return this.#commands.get(aliasCommand)?.metaData || null;
}
/**
* Returns an array of aliases for a given command
*/
getCommandAliases(commandName) {
return [...this.#aliases.entries()].filter(([, command]) => {
return command === commandName;
}).map(([alias]) => alias);
}
/**
* Returns a list of namespaces. The list is sorted alphabetically
* by the namespace name
*/
getNamespaces() {
return this.#namespaces;
}
/**
* Returns an array of command and aliases name suggestions for
* a given keyword.
*/
getCommandSuggestions(keyword) {
/**
* Priortize namespace commands when the keyword matches the
* namespace
*/
if (this.#namespaces.includes(keyword)) return this.getNamespaceCommands(keyword).map((command) => command.commandName);
return [...this.#commands.keys()].concat([...this.#aliases.keys()]).map((value) => {
return {
value,
distance: distance(keyword, value)
};
}).sort((current, next) => next.distance - current.distance).filter((rating) => {
return rating.distance <= 3;
}).map((rating) => rating.value);
}
/**
* Returns an array of namespaces suggestions for a given keyword.
*/
getNamespaceSuggestions(keyword) {
return this.#namespaces.map((value) => {
return {
value,
distance: distance(keyword, value)
};
}).sort((current, next) => next.distance - current.distance).filter((rating) => {
return rating.distance <= 3;
}).map((rating) => rating.value);
}
/**
* Listen for the event before we begin the process of finding
* the command.
*/
finding(callback) {
this.#hooks.add("finding", callback);
return this;
}
/**
* Listen for the event when importing the command
*/
loading(callback) {
this.#hooks.add("loading", callback);
return this;
}
/**
* Listen for the event when the command has been imported
*/
loaded(callback) {
this.#hooks.add("loaded", callback);
return this;
}
/**
* Listen for the event before we start to execute the command.
*/
executing(callback) {
this.#hooks.add("executing", callback);
return this;
}
/**
* Listen for the event after the command has been executed
*/
executed(callback) {
this.#hooks.add("executed", callback);
return this;
}
/**
* Loads commands from all the registered loaders. The "addLoader" method
* must be called before calling the "load" method.
*/
async boot() {
if (this.#state !== "idle") return;
/**
* Boot global command is not already booted
*/
this.#globalCommand.boot();
/**
* Registering the default command
*/
this.addLoader(new ListLoader([this.#defaultCommand]));
/**
* Set state to booted
*/
this.#state = "booted";
/**
* A set of unique namespaces. Later, we will store them on kernel
* directly as an alphabetically sorted array.
*/
const namespaces = /* @__PURE__ */ new Set();
/**
* Load metadata for all commands using the loaders
*/
for (let loader of this.#loaders) {
let loaderInstance;
/**
* A loader can be a function that lazily imports and instantiates
* a loader
*/
if (typeof loader === "function") loaderInstance = await loader();
else loaderInstance = loader;
(await loaderInstance.getMetaData()).forEach((command) => {
this.#commands.set(command.commandName, {
metaData: command,
loader: loaderInstance
});
command.aliases.forEach((alias) => this.addAlias(alias, command.commandName));
command.namespace && namespaces.add(command.namespace);
});
}
this.#namespaces = [...namespaces].sort(sortAlphabetically);
}
/**
* Find a command by its name
*/
async find(commandName) {
/**
* Get command name from the alias (if one exists)
*/
commandName = this.#aliases.get(commandName) || commandName;
await this.#hooks.runner("finding").run(commandName);
/**
* Find if we have a command registered
*/
const command = this.#commands.get(commandName);
if (!command) throw new E_COMMAND_NOT_FOUND([commandName]);
await this.#hooks.runner("loading").run(command.metaData);
/**
* Find if the loader is able to load the command
*/
const commandConstructor = await command.loader.getCommand(command.metaData);
if (!commandConstructor) throw new E_COMMAND_NOT_FOUND([commandName]);
await this.#hooks.runner("loaded").run(commandConstructor);
return commandConstructor;
}
/**
* Execute a command. The second argument is an array of command-line
* arguments (without the command name)
*
* @param commandName - The name of the command to execute
* @param argv - Array of command-line arguments
*
* @example
* ```ts
* await kernel.exec('make:model', ['User', '--migration'])
* ```
*/
async exec(commandName, argv, options) {
/**
* Boot if not already booted
*/
if (this.#state === "idle") await this.boot();
/**
* Cannot execute commands after the main command has exited
*/
if (this.#state === "completed") throw new RuntimeException("The kernel has been terminated. Create a fresh instance to execute commands");
return this.#exec(commandName, argv, options);
}
/**
* Creates a command instance by parsing and validating the command-line arguments
*
* @param command - The command class to instantiate
* @param argv - Command-line arguments as string or array
*
* @example
* ```ts
* const commandInstance = await kernel.create(MyCommand, ['--verbose', 'arg1'])
* ```
*/
async create(command, argv) {
/**
* Boot if not already booted
*/
if (this.#state === "idle") await this.boot();
return this.#create(command, argv);
}
/**
* Handle process argv and execute the command. Calling this method
* makes kernel own the process and register SIGNAL listeners
*
* @param argv - Array of command-line arguments from process.argv
*
* @example
* ```ts
* await kernel.handle(process.argv.slice(2))
* ```
*/
async handle(argv) {
/**
* Cannot run multiple main commands from a single process
*/
if (this.#state === "running") throw new RuntimeException("Cannot run multiple main commands from a single process");
/**
* Cannot run multiple main commands from the same instance
*/
if (this.#state === "completed") throw new RuntimeException("The kernel has been terminated. Create a fresh instance to execute commands");
/**
* Boot kernel
*/
if (this.#state === "idle") await this.boot();
this.#state = "running";
const { commandName, nodeArgv, commandArgv } = this.#processArgv(argv);
/**
* Run the default command
*/
if (!commandName) {
debug_default("running default command \"%s\"", this.#defaultCommand.commandName);
return this.#execMain(this.#defaultCommand.commandName, nodeArgv, commandArgv);
}
/**
* Run the mentioned command as the main command
*/
debug_default("running main command \"%s\"", commandName);
return this.#execMain(commandName, nodeArgv, commandArgv);
}
/**
* A named function that returns true. To be used
* by flag listeners
*/
shortcircuit() {
return true;
}
};
//#endregion
//#region src/commands/help.ts
/**
* The Help command is used to view help for a given command
*
* @example
* ```ts
* // Usage from CLI: node ace help my:command
* // Or programmatically:
* const helpCommand = new HelpCommand(kernel, parsed, ui, prompt)
* await helpCommand.run()
* ```
*/
var HelpCommand = class extends BaseCommand {
/**
* Command metadata
*/
static commandName = "help";
static description = "View help for a given command";
/**
* Returns the command arguments table
*
* @param heading - The table heading text
* @param command - The command metadata
*/
#makeArgumentsTable(heading, command) {
if (!command.args.length) return [];
return [{
heading: this.colors.yellow(heading),
columns: command.args.map((arg) => {
const formatter = new ArgumentFormatter(arg, this.colors);
return {
option: formatter.formatListOption(),
description: formatter.formatDescription()
};
})
}];
}
/**
* Returns the command options table
*
* @param heading - The table heading text
* @param command - The command metadata
*/
#makeOptionsTable(heading, command) {
if (!command.flags.length) return [];
return [{
heading: this.colors.yellow(heading),
columns: command.flags.map((flag) => {
const formatter = new FlagFormatter(flag, this.colors);
return {
option: formatter.formatOption(),
description: formatter.formatDescription()
};
})
}];
}
/**
* Validates the command name to ensure it exists
*/
#validateCommandName() {
if (!this.kernel.getCommand(this.name)) {
renderErrorWithSuggestions(this.ui, `Command "${this.name}" is not defined`, this.kernel.getCommandSuggestions(this.name));
return false;
}
return true;
}
/**
* Logs command description
*
* @param command - The command metadata
*/
renderDescription(command) {
const description = new CommandFormatter(command, this.colors).formatDescription();
if (!description) return;
this.logger.log("");
this.logger.log(this.colors.yellow("Description:"));
this.logger.log(wrap([description], {
startColumn: 2,
trimStart: false,
endColumn: TERMINAL_SIZE
}).join("\n"));
}
/**
* Logs command usage
*
* @param command - The command metadata
*/
renderUsage(command) {
const aliases = this.kernel.getCommandAliases(command.commandName);
const usage = new CommandFormatter(command, this.colors).formatUsage(aliases, this.kernel.info.get("binary")).join("\n");
this.logger.log("");
this.logger.log(this.colors.yellow("Usage:"));
this.logger.log(usage);
}
/**
* Logs command arguments and options tables
*
* @param command - The command metadata
*/
renderList(command) {
new ListFormatter(this.#makeArgumentsTable("Arguments:", command).concat(this.#makeOptionsTable("Options:", command))).format().forEach((table) => {
this.logger.log("");
this.logger.log(table.heading);
this.logger.log(table.rows.join("\n"));
});
}
/**
* Logs command help text
*
* @param command - The command metadata
*/
renderHelp(command) {
const help = new CommandFormatter(command, this.colors).formatHelp(this.kernel.info.get("binary"));
if (!help) return;
this.logger.log("");
this.logger.log(this.colors.yellow("Help:"));
this.logger.log(help);
}
/**
* Executes the help command to display help for the specified command
*
* @example
* ```ts
* await helpCommand.run()
* ```
*/
async run() {
if (!this.#validateCommandName()) {
this.exitCode = 1;
return;
}
const command = this.kernel.getCommand(this.name);
this.renderDescription(command);
this.renderUsage(command);
this.renderList(command);
this.renderHelp(command);
}
};
__decorate([args.string({
description: "Command name",
argumentName: "command"
})], HelpCommand.prototype, "name", void 0);
//#endregion
//#region src/loaders/fs_loader.ts
const JS_MODULES = [
".js",
".cjs",
".mjs"
];
/**
* Fs loader exposes the API to load commands from a directory. All files
* ending with ".js", ".cjs", ".mjs", ".ts" and ".mts" are considered
* as commands
*
* @example
* ```ts
* const loader = new FsLoader('./commands')
* const metadata = await loader.getMetaData()
* const command = await loader.getCommand(metadata[0])
* ```
*/
var FsLoader = class {
/**
* Absolute path to directory from which to load files
*/
#comandsDirectory;
/**
* Filter function to ignore certain files
*/
#filter;
/**
* An array of loaded commands with their file paths
*/
#commands = [];
/**
* Create a new filesystem loader
*
* @param comandsDirectory - Path to the directory containing command files
* @param filter - Optional filter function to exclude certain files
*/
constructor(comandsDirectory, filter) {
this.#comandsDirectory = comandsDirectory;
this.#filter = filter;
}
/**
* Returns a collection of commands. The command values are unknown and must be validated
*/
async #loadCommands() {
const commands = {};
/**
* Scanning all files
*/
const commandFiles = await fsReadAll(this.#comandsDirectory, {
pathType: "url",
ignoreMissingRoot: true,
filter: (filePath) => {
const ext = extname(filePath);
/**
* Ignore files prefixed with _
*/
if (basename(filePath).startsWith("_")) return false;
if (JS_MODULES.includes(ext)) return true;
if (ext === ".ts" && !filePath.endsWith(".d.ts")) return true;
return false;
}
});
/**
* Importing files and validating the exports to have a default
* export
*/
for (let file of commandFiles) {
/**
* Remapping .ts files to .js, otherwise the file cannot imported
*/
if (file.endsWith(".ts")) file = file.replace(/\.ts$/, ".js");
const relativeFileName = string.toUnixSlash(relative(this.#comandsDirectory, fileURLToPath(file)));
/**
* Import file if no filters are defined or the filter
* allows the file
*/
if (!this.#filter || this.#filter(relativeFileName)) commands[relativeFileName] = await importDefault(() => import(file), relativeFileName);
}
return commands;
}
/**
* Returns the metadata of all commands in the directory
*
* @example
* ```ts
* const metadata = await loader.getMetaData()
* metadata.forEach(cmd => console.log(cmd.commandName))
* ```
*/
async getMetaData() {
const commandsCollection = await this.#loadCommands();
Object.keys(commandsCollection).forEach((key) => {
const command = commandsCollection[key];
validateCommand(command, `"${key}" file`);
this.#commands.push({
command,
filePath: key
});
});
return this.#commands.map(({ command, filePath }) => {
return Object.assign({}, command.serialize(), { filePath });
});
}
/**
* Returns the command class constructor for a given command. Null
* is returned when unable to lookup the command
*
* @param metaData - The command metadata to find
*
* @example
* ```ts
* const command = await loader.getCommand(metadata)
* if (command) {
* const instance = new command(kernel, parsed, ui, prompt)
* }
* ```
*/
async getCommand(metaData) {
return this.#commands.find(({ command }) => {
return command.commandName === metaData.commandName;
})?.command || null;
}
};
//#endregion
//#region stubs/main.ts
const stubsRoot = import.meta.dirname;
//#endregion
//#region src/generators/index_generator.ts
/**
* The index generator creates a commands loader that can be lazily imported.
*
* Also, a commands.json index file is created that has metadata for all
* the files. Doing so speeds up commands lookup, as we do not
* have to import all the classes just to find if a command exists or not.
*
* @example
* ```ts
* const generator = new IndexGenerator('./build/commands')
* await generator.generate()
* ```
*/
var IndexGenerator = class {
/**
* The directory where commands are located
*/
#commandsDir;
/**
* Create a new index generator
*
* @param commandsDir - The directory containing command files
*/
constructor(commandsDir) {
this.#commandsDir = commandsDir;
}
/**
* Generate the commands index files (commands.json, main.js, main.d.ts)
*
* @example
* ```ts
* await generator.generate()
* ```
*/
async generate() {
const commandsMetaData = await new FsLoader(this.#commandsDir).getMetaData();
const indexJSON = JSON.stringify({
commands: commandsMetaData,
version: 1
});
const indexFile = join(this.#commandsDir, "commands.json");
const loaderFile = join(this.#commandsDir, "main.js");
const loaderStub = join(stubsRoot, "commands_loader.stub");
const loaderTypes = join(this.#commandsDir, "main.d.ts");
const loaderTypesStub = join(stubsRoot, "commands_loader_types.stub");
await mkdir(this.#commandsDir, { recursive: true });
console.log(`artifacts directory: ${this.#commandsDir}`);
await writeFile(indexFile, indexJSON);
console.log("create commands.json");
await copyFile(loaderStub, loaderFile);
console.log("create main.js");
await copyFile(loaderTypesStub, loaderTypes);
console.log("create main.d.ts");
}
};
//#endregion
export { BaseCommand, ExceptionHandler, FsLoader, HelpCommand, IndexGenerator, Kernel, ListCommand, ListLoader, Parser, args, cliHelpers, cliui, errors_exports as errors, flags, tracing_channels_exports as tracingChannels };