@lenne.tech/cli
Version:
lenne.Tech CLI: lt
162 lines (161 loc) • 7.05 kB
JavaScript
;
/**
* Generic `--help` / `-h` support for **every** lt command.
*
* Problem this solves: gluegun's built-in `.help()` only handles the top-level
* `lt --help` (the command list). For a subcommand, `lt fullstack convert-mode
* --help` simply *runs* the command — so a user who only wanted to read about a
* command accidentally triggers it. {@link installHelpInterceptor} wraps every
* loaded command so that, when help is requested, the command prints rich help
* and returns **without executing**.
*
* Two levels of detail:
* 1. Generic (always available) — usage, aliases and description from the
* command metadata gluegun already has.
* 2. Rich (opt-in) — a command module may `export const help: CommandHelp`
* describing options, features, examples and configuration. The interceptor
* loads it from `command.file` without running the command.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.installHelpInterceptor = installHelpInterceptor;
exports.isHelpRequested = isHelpRequested;
exports.loadCommandHelp = loadCommandHelp;
exports.renderCommandHelp = renderCommandHelp;
/**
* Wrap every command's `run` so that `--help` / `-h` prints help and returns
* without executing. Call once after `build().create()`, before `cli.run()`.
*
* Idempotent per command (guarded by `__helpWrapped`), so a command is never
* double-wrapped if this runs more than once in the same process (e.g. tests).
*/
function installHelpInterceptor(commands, defaultCommand) {
for (const command of commands || []) {
if (typeof command.run !== 'function' || command.__helpWrapped) {
continue;
}
// Leave the top-level/default command and gluegun's preloaded builtins
// (`help`, `version` — they have `file === null`) to gluegun's own
// `.help()`, so that `lt --help` keeps printing the brand banner + full
// command list. Real file-backed subcommands (incl. `lt config help`) are
// still wrapped.
if (command === defaultCommand || !command.file || !(command.commandPath && command.commandPath.length)) {
continue;
}
const originalRun = command.run;
command.run = (toolbox) => {
if ((toolbox === null || toolbox === void 0 ? void 0 : toolbox.print) && isHelpRequested(toolbox.parameters)) {
renderCommandHelp(toolbox.print, command, loadCommandHelp(command.file));
return undefined;
}
return originalRun(toolbox);
};
command.__helpWrapped = true;
}
}
/**
* True when the invocation asks for help (`--help` or `-h`) — but NOT for
* `--help-json` (handled separately by `tools.helpJson`).
*/
function isHelpRequested(parameters) {
const options = (parameters === null || parameters === void 0 ? void 0 : parameters.options) || {};
if (options['help-json'] === true || options.helpJson === true) {
return false;
}
return options.help === true || options.h === true;
}
/** Best-effort load of a command's rich `help` export from its file. */
function loadCommandHelp(file) {
if (!file) {
return undefined;
}
try {
const mod = require(file);
const help = (mod && (mod.help || (mod.default && mod.default.help)));
return help && typeof help === 'object' ? help : undefined;
}
catch (_a) {
return undefined;
}
}
/**
* Render human-readable help for a command to `print`. Uses the rich `help`
* definition when available, otherwise a useful generic fallback. Never runs
* the command.
*/
function renderCommandHelp(print, command, help) {
var _a, _b, _c;
const { bold, cyan, dim, yellow } = print.colors;
const usage = usagePath(command);
const description = (help === null || help === void 0 ? void 0 : help.description) || command.description || '(no description)';
print.info('');
print.info(`${bold(usage)} — ${description}`);
const aliases = aliasList(command, help);
if (aliases.length) {
print.info(dim(`Aliases: ${aliases.join(', ')}`));
}
if ((_a = help === null || help === void 0 ? void 0 : help.features) === null || _a === void 0 ? void 0 : _a.length) {
print.info('');
print.info(bold('What it does:'));
for (const feature of help.features) {
print.info(` • ${feature}`);
}
}
print.info('');
print.info(bold('Usage:'));
print.info(` ${usage} [options]`);
if ((_b = help === null || help === void 0 ? void 0 : help.examples) === null || _b === void 0 ? void 0 : _b.length) {
print.info('');
print.info(bold('Examples:'));
for (const example of help.examples) {
print.info(` ${cyan(example.startsWith('lt ') ? example : `lt ${example}`)}`);
}
}
print.info('');
print.info(bold('Options:'));
const options = [...((help === null || help === void 0 ? void 0 : help.options) || [])];
for (const option of options) {
const meta = [];
if (option.required) {
meta.push('required');
}
if ((_c = option.values) === null || _c === void 0 ? void 0 : _c.length) {
meta.push(option.values.join('|'));
}
else if (option.type) {
meta.push(option.type);
}
if (option.default !== undefined) {
meta.push(`default: ${String(option.default)}`);
}
const metaText = meta.length ? dim(` (${meta.join(', ')})`) : '';
print.info(` ${option.flag.padEnd(28)} ${option.description}${metaText}`);
}
// Always-present global flags
print.info(` ${'--help, -h'.padEnd(28)} Show this help and exit (does not run the command)`);
print.info(` ${'--help-json'.padEnd(28)} Machine-readable help as JSON (where provided)`);
if (!options.some((o) => o.flag === '--noConfirm')) {
print.info(` ${'--noConfirm'.padEnd(28)} Skip confirmation prompts (where supported)`);
}
if (help === null || help === void 0 ? void 0 : help.configuration) {
print.info('');
print.info(bold('Configuration (lt.config.json / lt.config.yaml):'));
for (const line of help.configuration.split('\n')) {
print.info(` ${line}`);
}
}
if (!help) {
print.info('');
print.info(dim('Tip: see docs/commands.md and docs/lt.config.md for full reference.'));
}
print.info('');
print.info(yellow('This is help output — the command was NOT executed.'));
print.info('');
}
function aliasList(command, help) {
return (help === null || help === void 0 ? void 0 : help.aliases) || command.aliases || command.alias || [];
}
/** Resolve the user-facing invocation, e.g. `lt fullstack convert-mode`. */
function usagePath(command) {
const path = command.commandPath && command.commandPath.length ? command.commandPath : [command.name || ''];
return `lt ${path.filter(Boolean).join(' ')}`.trim();
}