@adonisjs/ace
Version:
A CLI framework for Node.js
1,652 lines (1,631 loc) • 66.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
// src/parser.ts
import yargsParser from "yargs-parser";
// src/yars_config.ts
var 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
};
// src/parser.ts
var Parser = class {
/**
* Parser options
*/
#options;
constructor(options) {
this.#options = options;
}
/**
* Parsers flags using yargs
*/
#parseFlags(argv) {
return yargsParser(argv, { ...this.#options.flagsParserOptions, configuration: yarsConfig });
}
/**
* Scans for unknown flags in yargs output.
*/
#scanUnknownFlags(parsed) {
const unknownFlags = [];
for (let key of Object.keys(parsed)) {
if (!this.#options.flagsParserOptions.all.includes(key)) {
unknownFlags.push(key);
}
}
return unknownFlags;
}
/**
* Parsers arguments by mimicking the yargs behavior
*/
#parseArguments(parsedOutput) {
let lastParsedIndex = -1;
const output = this.#options.argumentsParserOptions.map((option, index) => {
if (option.type === "spread") {
let value2 = parsedOutput._.slice(index);
lastParsedIndex = parsedOutput._.length;
if (!value2.length) {
value2 = Array.isArray(option.default) ? option.default : option.default === void 0 ? void 0 : [option.default];
}
if (value2 !== void 0 && option.parse) {
value2 = option.parse(value2);
}
return value2;
}
let value = parsedOutput._[index];
lastParsedIndex = index + 1;
if (value === void 0) {
value = option.default;
}
if (value !== void 0 && option.parse) {
value = option.parse(value);
}
return value;
});
const { "_": args2, "--": o, ...rest } = parsedOutput;
return {
args: output,
nodeArgs: [],
_: args2.slice(lastParsedIndex === -1 ? 0 : lastParsedIndex),
unknownFlags: this.#scanUnknownFlags(rest),
flags: rest
};
}
/**
* Parse commandline arguments
*/
parse(argv) {
return this.#parseArguments(this.#parseFlags(argv));
}
};
// src/kernel.ts
import Hooks from "@poppinss/hooks";
import { cliui } from "@poppinss/cliui";
import { Prompt } from "@poppinss/prompts";
import { distance } from "fastest-levenshtein";
import { RuntimeException as RuntimeException2 } from "@poppinss/utils";
// src/debug.ts
import { debuglog } from "util";
var debug_default = debuglog("adonisjs:ace");
// src/errors.ts
var errors_exports = {};
__export(errors_exports, {
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
});
import { errors } from "@poppinss/prompts";
import { createError, Exception } from "@poppinss/utils";
var E_PROMPT_CANCELLED = errors.E_PROMPT_CANCELLED;
var E_MISSING_COMMAND_NAME = createError(
'Cannot serialize command "%s". Missing static property "commandName"',
"E_MISSING_COMMAND_NAME"
);
var E_COMMAND_NOT_FOUND = class CommandNotFound extends Exception {
static status = 404;
commandName;
constructor(args2) {
super(`Command "${args2[0]}" is not defined`, { code: "E_COMMAND_NOT_FOUND" });
this.commandName = args2[0];
}
};
var E_MISSING_FLAG = createError(
'Missing required option "%s"',
"E_MISSING_FLAG"
);
var E_MISSING_FLAG_VALUE = createError(
'Missing value for option "%s"',
"E_MISSING_FLAG_VALUE"
);
var E_MISSING_ARG = createError(
'Missing required argument "%s"',
"E_MISSING_ARG"
);
var E_MISSING_ARG_VALUE = createError(
'Missing value for argument "%s"',
"E_MISSING_ARG_VALUE"
);
var E_UNKNOWN_FLAG = createError(
'Unknown flag "%s". The mentioned flag is not accepted by the command',
"E_UNKNOWN_FLAG"
);
var E_INVALID_FLAG = createError(
'Invalid value. The "%s" flag accepts a "%s" value',
"E_INVALID_FLAG"
);
// src/commands/base.ts
import { inspect } from "util";
import string from "@poppinss/utils/string";
import Macroable from "@poppinss/macroable";
import lodash from "@poppinss/utils/lodash";
import { AssertionError } from "assert";
import { defineStaticProperty, InvalidArgumentsException } from "@poppinss/utils";
var BaseCommand = class extends Macroable {
constructor(kernel, parsed, ui, prompt) {
super();
this.kernel = kernel;
this.parsed = parsed;
this.ui = ui;
this.prompt = prompt;
}
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.
*/
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.
*
* ```ts
* Command.defineArgument('entity', { type: 'string' })
* ```
*/
static defineArgument(name, options) {
this.boot();
const arg = { name, argumentName: string.dashCase(name), required: true, ...options };
const lastArg = this.args[this.args.length - 1];
if (!arg.type) {
throw new InvalidArgumentsException(
`Cannot define argument "${this.name}.${name}". Specify the argument type`
);
}
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`
);
}
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.
*
* ```ts
* Command.defineFlag('connection', { type: 'string', required: true })
* ```
*/
static defineFlag(name, options) {
this.boot();
const flag = { name, flagName: string.dashCase(name), required: false, ...options };
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
*/
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}
*/
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 againts the command.
*/
static validate(parsedOutput) {
this.boot();
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]);
}
});
if (!this.options.allowUnknownFlags && parsedOutput.unknownFlags.length) {
const unknowFlag = parsedOutput.unknownFlags[0];
const unknowFlagName = unknowFlag.length === 1 ? `-${unknowFlag}` : `--${unknowFlag}`;
throw new E_UNKNOWN_FLAG([unknowFlagName]);
}
this.flags.forEach((flag) => {
const hasMentionedFlag = Object.hasOwn(parsedOutput.flags, flag.flagName);
const value = parsedOutput.flags[flag.flagName];
switch (flag.type) {
case "boolean":
if (flag.required && !hasMentionedFlag) {
throw new E_MISSING_FLAG([flag.flagName]);
}
break;
case "number":
if (flag.required && !hasMentionedFlag) {
throw new E_MISSING_FLAG([flag.flagName]);
}
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.required && !hasMentionedFlag) {
throw new E_MISSING_FLAG([flag.flagName]);
}
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 hypdrated
*/
hydrated = false;
/**
* The exit code for the command
*/
exitCode;
/**
* The error raised at the time of the 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 commands sets it explicitly)
*/
result;
/**
* Logger to log messages
*/
get logger() {
return this.ui.logger;
}
/**
* Add colors to console messages
*/
get colors() {
return this.ui.colors;
}
/**
* Is the current command the main command executed from the
* CLI
*/
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;
}
/**
* Hydrate command by setting class properties from
* the parsed output
*/
hydrate() {
if (this.hydrated) {
return;
}
const CommandConstructor = this.constructor;
CommandConstructor.args.forEach((arg, index) => {
Object.defineProperty(this, arg.name, {
value: this.parsed.args[index],
enumerable: true,
writable: true,
configurable: true
});
});
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.
*/
async run(..._) {
}
/**
* Executes the commands by running the command's run method.
*/
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
*/
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 exists with a given exit code
*/
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 exists with a given exit code
*/
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 exists with zero exit code
*/
assertSucceeded() {
return this.assertExitCode(0);
}
/**
* Assert the command exists with non-zero exit code
*/
assertFailed() {
return this.assertNotExitCode(0);
}
/**
* Assert command to log the expected message
*/
assertLog(message, stream) {
const logs = this.logger.getLogs();
const logMessages = logs.map((log) => log.message);
const matchingLog = logs.find((log) => log.message === message);
if (!matchingLog) {
throw new AssertionError({
message: `Expected log messages to include ${inspect(message)}`,
actual: logMessages,
expected: [message],
operator: "strictEqual",
stackStartFn: this.assertLog
});
}
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 to log the expected message
*/
assertLogMatches(matchingRegex, stream) {
const logs = this.logger.getLogs();
const matchingLog = logs.find((log) => matchingRegex.test(log.message));
if (!matchingLog) {
throw new AssertionError({
message: `Expected log messages to match ${inspect(matchingRegex)}`,
stackStartFn: this.assertLogMatches
});
}
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 to stdout
*/
assertTableRows(rows) {
const logs = this.logger.getLogs();
const hasAllMatchingRows = rows.every((row) => {
const columnsContent = row.join("|");
return !!logs.find((log) => log.message === columnsContent);
});
if (!hasAllMatchingRows) {
throw new AssertionError({
message: `Expected log messages to include a table with the expected rows`,
operator: "strictEqual",
stackStartFn: this.assertTableRows
});
}
}
};
// src/decorators/args.ts
var args = {
/**
* Define argument that accepts a string value
*/
string(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineArgument(propertyName, { ...options, type: "string" });
};
},
/**
* Define argument that accepts a spread value
*/
spread(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineArgument(propertyName, { ...options, type: "spread" });
};
}
};
// src/decorators/flags.ts
var flags = {
/**
* Define option that accepts a string value
*/
string(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineFlag(propertyName, { type: "string", ...options });
};
},
/**
* Define option that accepts a boolean value
*/
boolean(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineFlag(propertyName, { type: "boolean", ...options });
};
},
/**
* Define option that accepts a number value
*/
number(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineFlag(propertyName, { type: "number", ...options });
};
},
/**
* Define option that accepts an array of values
*/
array(options) {
return function addArg(target, propertyName) {
const Command = target.constructor;
Command.defineFlag(propertyName, { type: "array", ...options });
};
}
};
// src/formatters/flag.ts
var FlagFormatter = class {
#flag;
#colors;
constructor(flag, colors) {
this.#flag = flag;
this.#colors = colors;
}
/**
* Formats the value flag
*/
#formatValueFlag(flag, valuePlaceholder) {
return flag.required ? `=${valuePlaceholder}` : `[=${valuePlaceholder}]`;
}
/**
* Formats the aliases for the flag
*/
#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 to indicate if it is required or not
*/
#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 it is required or not
*/
#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 it is required or not
*/
#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 needs no wrapping
*/
#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
*/
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
*/
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);
}
}
};
// src/formatters/list.ts
import stringWidth from "string-width";
import { justify, TERMINAL_SIZE, wrap } from "@poppinss/cliui/helpers";
var ListFormatter = class {
#tables;
#largestOptionColumnWidth;
constructor(tables) {
this.#tables = tables;
this.#largestOptionColumnWidth = Math.max(
...this.#tables.map((table) => table.columns.map((column) => stringWidth(column.option))).flat()
);
}
/**
* Formats the table to an array of plain text rows.
*/
#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 tables list into an array of rows
*/
format(terminalWidth = TERMINAL_SIZE) {
return this.#tables.map((table) => {
return {
heading: table.heading,
rows: this.#formatTable(table, terminalWidth)
};
});
}
};
// src/helpers.ts
import { Validator } from "jsonschema";
import { RuntimeException } from "@poppinss/utils";
// schemas/main.ts
var 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"
}
}
};
// src/helpers.ts
function sortAlphabetically(prev, curr) {
if (curr > prev) {
return -1;
}
if (curr < prev) {
return 1;
}
return 0;
}
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());
}
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) {
throw new RuntimeException(`Invalid command exported from ${exportPath}. ${error.message}`);
}
}
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);
}
// src/formatters/command.ts
import string2 from "@poppinss/utils/string";
import { TERMINAL_SIZE as TERMINAL_SIZE2, wrap as wrap2 } from "@poppinss/cliui/helpers";
// src/formatters/argument.ts
var ArgumentFormatter = class {
#argument;
#colors;
constructor(argument, colors) {
this.#argument = argument;
this.#colors = colors;
}
/**
* Wraps the optional placeholder on option arguments
*/
#formatArgument(argument, valuePlaceholder) {
return argument.required ? `${valuePlaceholder}` : `[${valuePlaceholder}]`;
}
/**
* Returns formatted description for the argument
*/
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
*/
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
*/
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}>`)}`
);
}
}
};
// src/formatters/command.ts
var CommandFormatter = class {
#command;
#colors;
constructor(command, colors) {
this.#command = command;
this.#colors = colors;
}
/**
* Returns the formatted command name to be displayed in the list
* of commands
*/
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
*/
formatDescription() {
return this.#command.description || "";
}
/**
* Returns multiline command help
*/
formatHelp(binaryName, terminalWidth = TERMINAL_SIZE2) {
const binary = binaryName ? `${binaryName}` : "";
if (!this.#command.help) {
return "";
}
const help = Array.isArray(this.#command.help) ? this.#command.help : [this.#command.help];
return wrap2(
help.map((line) => string2.interpolate(line, { binaryName: binary })),
{
startColumn: 2,
trimStart: false,
endColumn: terminalWidth
}
).join("\n");
}
/**
* Returns the formatted description to be displayed in the list
* of commands
*/
formatListDescription() {
if (!this.#command.description) {
return "";
}
return this.#colors.dim(this.#command.description);
}
/**
* Returns an array of strings, each line contains an individual usage
*/
formatUsage(aliases, binaryName) {
const binary = binaryName ? `${binaryName} ` : "";
const flags2 = this.#command.flags.length ? this.#colors.dim("[options]") : "";
const args2 = this.#command.args.map((arg) => new ArgumentFormatter(arg, this.#colors).formatOption()).join(" ");
const separator = flags2 && args2 ? ` ${this.#colors.dim("[--]")} ` : "";
const mainUsage = [` ${binary}${this.#command.commandName} ${flags2}${separator}${args2}`];
return mainUsage.concat(
aliases.map((alias) => ` ${binary}${alias} ${flags2}${separator}${args2}`)
);
}
};
// src/commands/list.ts
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.
*/
#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
*/
#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 the commands or for mentioned
* namespaces only
*/
#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"
* flag
*/
#validateNamespace() {
if (!this.namespaces) {
return true;
}
const namespaces = this.kernel.getNamespaces();
const unknownNamespace = this.namespaces.find((namespace) => !namespaces.includes(namespace));
if (unknownNamespace) {
renderErrorWithSuggestions(
this.ui,
`Namespace "${unknownNamespace}" is not defined`,
this.kernel.getNamespaceSuggestions(unknownNamespace)
);
return false;
}
return true;
}
/**
* The method is used to render a list of options and commands
*/
renderList() {
const tables = this.#getOptionsTable().concat(this.#getCommandsTables(this.namespaces));
new ListFormatter(tables).format().forEach((table) => {
this.logger.log("");
this.logger.log(table.heading);
this.logger.log(table.rows.join("\n"));
});
}
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)
);
}
/**
* Executed by ace directly
*/
async run() {
const hasValidNamespaces = this.#validateNamespace();
if (!hasValidNamespaces) {
this.exitCode = 1;
return;
}
if (this.json) {
this.logger.log(JSON.stringify(this.renderToJSON(), null, 2));
return;
}
this.renderList();
}
};
__decorateClass([
args.spread({
description: "Filter list by namespace",
required: false
})
], ListCommand.prototype, "namespaces", 2);
__decorateClass([
flags.boolean({ description: "Get list of commands as JSON" })
], ListCommand.prototype, "json", 2);
// src/loaders/list_loader.ts
var ListLoader = class {
#commands;
constructor(commands) {
this.#commands = commands;
}
/**
* Returns an array of command's metadata
*/
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
*/
async getCommand(metaData) {
return this.#commands.find((command) => command.commandName === metaData.commandName) || null;
}
};
// src/exception_handler.ts
import { errors as promptsErrors } from "@poppinss/prompts";
var ExceptionHandler = class {
debug = true;
/**
* Known error codes. For these error, 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
*/
logError(error, kernel) {
kernel.ui.logger.logError(`${kernel.ui.colors.bgRed().white(" ERROR ")} ${error.message}`);
}
/**
* Pretty prints uncaught error in debug mode
*/
async prettyPrintError(error) {
const { default: youchTerminal } = await import("youch-terminal");
const { default: Youch } = await import("youch");
const youch = new Youch(error, {});
console.log(youchTerminal(await youch.toJSON(), { displayShortPath: true }));
}
/**
* Renders an exception for the console
*/
async render(error, kernel) {
if (typeof error !== "object" || error === null || !("message" in error)) {
this.logError({ message: String(error) }, kernel);
return;
}
if (error instanceof errors_exports.E_COMMAND_NOT_FOUND) {
renderErrorWithSuggestions(
kernel.ui,
error.message,
kernel.getCommandSuggestions(error.commandName)
);
return;
}
if (error instanceof promptsErrors.E_PROMPT_CANCELLED) {
this.logError({ message: "Prompt cancelled" }, kernel);
return;
}
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;
}
if ("render" in error && typeof error.render === "function") {
return error.render(error, kernel);
}
if (!this.debug) {
this.logError({ message: error.message }, kernel);
return;
}
return this.prettyPrintError(error);
}
};
// src/kernel.ts
var Kernel2 = class _Kernel {
errorHandler = new ExceptionHandler();
/**
* The default executor for creating command's instance
* 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
*/
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 code when not set explicitly.
*/
exitCode;
/**
* The UI primitives to use within commands
*/
ui = cliui();
/**
* 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
*/
info = /* @__PURE__ */ new Map();
/**
* List of global flags
*/
get flags() {
return this.#globalCommand.flags;
}
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) {
const parsed = new Parser(Command.getParserOptions()).parse(argv);
Command.validate(parsed);
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) {
const Command = await this.find(commandName);
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);
await this.#hooks.runner("executing").run(commandInstance, false);
await this.#executor.run(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);
const aliasExpansions = this.#aliasExpansions.get(commandName);
if (aliasExpansions) {
argv = aliasExpansions.concat(argv);
debug_default("expanding alias %O, cli args %O", commandName, argv);
}
const parsed = new Parser(
Command.getParserOptions(this.#globalCommand.getParserOptions().flagsParserOptions)
).parse(argv);
parsed.nodeArgs = nodeArgv;
this.#globalCommand.validate(parsed);
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;
}
}
}
Command.validate(parsed);
if (shortcircuit) {
debug_default("short circuiting from flag listener");
this.exitCode = this.exitCode ?? 0;
this.#state = "completed";
return;
}
this.#mainCommand = await this.#executor.create(Command, parsed, this);
this.#mainCommand.hydrate();
await this.#hooks.runner("executing").run(this.#mainCommand, true);
await this.#executor.run(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 the
* commands.
*/
defineFlag(name, options) {
if (this.#state !== "idle") {
throw new RuntimeException2(`Cannot register global flag in "${this.#state}" state`);
}
this.#g