@ideascol/cli-maker
Version:
A simple library to help create CLIs
1,027 lines (1,026 loc) • 65.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLI = void 0;
const readline_1 = __importDefault(require("readline"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const colors_1 = require("../colors");
const interfaces_1 = require("../interfaces");
const session_1 = require("../session/session");
const validator_1 = require("./validator");
const common_1 = require("../common");
const setup_1 = require("../setup");
const rotate_passphrase_1 = require("../rotate-passphrase");
const ai_guide_1 = require("../ai-guide");
function generatePixelAsciiArt(text) {
const font = {
'A': ['███', '█ █', '███', '█ █', '█ █'],
'B': ['██ ', '█ █', '██ ', '█ █', '██ '],
'C': ['███', '█ ', '█ ', '█ ', '███'],
'D': ['██ ', '█ █', '█ █', '█ █', '██ '],
'E': ['███', '█ ', '██ ', '█ ', '███'],
'F': ['███', '█ ', '██ ', '█ ', '█ '],
'G': ['███', '█ ', '█ █', '█ █', '███'],
'H': ['█ █', '█ █', '███', '█ █', '█ █'],
'I': ['███', ' █ ', ' █ ', ' █ ', '███'],
'J': ['███', ' █', ' █', '█ █', '███'],
'K': ['█ █', '██ ', '█ ', '██ ', '█ █'],
'L': ['█ ', '█ ', '█ ', '█ ', '███'],
'M': ['█ █', '███', '███', '█ █', '█ █'],
'N': ['█ █', '███', '███', '███', '█ █'],
'O': ['███', '█ █', '█ █', '█ █', '███'],
'P': ['██ ', '█ █', '██ ', '█ ', '█ '],
'Q': ['███', '█ █', '█ █', '███', ' █'],
'R': ['██ ', '█ █', '██ ', '█ █', '█ █'],
'S': ['███', '█ ', '███', ' █', '███'],
'T': ['███', ' █ ', ' █ ', ' █ ', ' █ '],
'U': ['█ █', '█ █', '█ █', '█ █', '███'],
'V': ['█ █', '█ █', '█ █', '█ █', ' █ '],
'W': ['█ █', '█ █', '███', '███', '█ █'],
'X': ['█ █', '█ █', ' █ ', '█ █', '█ █'],
'Y': ['█ █', '█ █', ' █ ', ' █ ', ' █ '],
'Z': ['███', ' █', ' █ ', '█ ', '███'],
'-': [' ', ' ', '███', ' ', ' '],
' ': [' ', ' ', ' ', ' ', ' '],
'0': ['███', '█ █', '█ █', '█ █', '███'],
'1': [' █ ', '██ ', ' █ ', ' █ ', '███'],
'2': ['██ ', ' █', '███', '█ ', '███'],
'3': ['██ ', ' █', '██ ', ' █', '██ '],
'4': ['█ █', '█ █', '███', ' █', ' █'],
'5': ['███', '█ ', '██ ', ' █', '██ '],
'6': ['███', '█ ', '███', '█ █', '███'],
'7': ['███', ' █', ' █ ', ' █ ', ' █ '],
'8': ['███', '█ █', '███', '█ █', '███'],
'9': ['███', '█ █', '███', ' █', '███'],
};
const upperText = text.toUpperCase();
const lines = ['', '', '', '', ''];
for (let i = 0; i < upperText.length; i++) {
const char = upperText[i];
const charLines = font[char] || font[' '];
for (let row = 0; row < 5; row++) {
lines[row] += charLines[row] + ' ';
}
}
return lines.map(line => line.trimEnd());
}
const INTRO_PRESETS = {
hacker: {
frames: ['▌', ' ', '▌', ' '],
padding: 1,
speedMs: 120,
loops: 2,
lines: ['Initializing secure channel...'],
},
vaporwave: {
frames: ['✦', '✺', '✹', '✸'],
padding: 2,
speedMs: 85,
loops: 2,
lines: ['Ride the gradient.'],
},
radar: {
frames: ['◜', '◠', '◝', '◡'],
padding: 2,
speedMs: 75,
loops: 3,
lines: ['Scanning environment...'],
},
pixel: {
frames: ['░', '▒', '▓', '█'],
padding: 1,
speedMs: 70,
loops: 2,
lines: ['Booting 8-bit systems...'],
},
steampunk: {
frames: ['⚙︎', '⚙', '⚙︎', '⚙'],
padding: 2,
speedMs: 90,
loops: 2,
lines: ['Warming valves...'],
asciiArt: [
' ___ ',
' __/ o \\__',
'| ⚙ ⚙ |',
'‾‾‾‾‾‾‾‾‾‾‾'
],
},
sonar: {
frames: ['◉', '◎', '◉', '◎'],
padding: 2,
speedMs: 95,
loops: 3,
lines: ['Ping...'],
},
'retro-space': {
frames: ['=≡>', '⇝◎', '⇢✸', '⇝◎'],
padding: 2,
speedMs: 90,
loops: 3,
lines: ['Scanning for lifeforms...'],
asciiArt: [
' /\\',
' /::\\',
' /::::\\',
' |/::\\|',
' ‾‾‾‾‾‾'
],
},
rainbow: {
frames: [
`${colors_1.Colors.FgRed}◐${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgYellow}◐${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgGreen}◐${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgCyan}◐${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgBlue}◐${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgMagenta}◐${colors_1.Colors.Reset}`,
],
padding: 2,
speedMs: 70,
loops: 2,
lines: ['Painting the sky...'],
asciiArt: [
`${colors_1.Colors.FgRed}╭────────────────────╮${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgRed}│${colors_1.Colors.Reset}${colors_1.Colors.FgYellow}████${colors_1.Colors.Reset}${colors_1.Colors.FgGreen}████${colors_1.Colors.Reset}${colors_1.Colors.FgCyan}████${colors_1.Colors.Reset}${colors_1.Colors.FgBlue}████${colors_1.Colors.Reset}${colors_1.Colors.FgMagenta}████${colors_1.Colors.Reset}${colors_1.Colors.FgRed}│${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgRed}│${colors_1.Colors.Reset}${colors_1.Colors.FgMagenta}████${colors_1.Colors.Reset}${colors_1.Colors.FgBlue}████${colors_1.Colors.Reset}${colors_1.Colors.FgCyan}████${colors_1.Colors.Reset}${colors_1.Colors.FgGreen}████${colors_1.Colors.Reset}${colors_1.Colors.FgYellow}████${colors_1.Colors.Reset}${colors_1.Colors.FgRed}│${colors_1.Colors.Reset}`,
`${colors_1.Colors.FgRed}╰────────────────────╯${colors_1.Colors.Reset}`,
],
},
};
class CLI {
constructor(name, description, options) {
this.name = name;
this.description = description;
this.options = options;
this.commands = [];
if (this.options == null) {
this.options = { interactive: true, version: '1.0.0', branding: true, introAnimation: { enabled: false, showOnce: true } };
}
else {
if (this.options.branding === undefined) {
this.options.branding = true;
}
if (this.options.introAnimation == null) {
this.options.introAnimation = { enabled: false, showOnce: true };
}
else {
if (this.options.introAnimation.enabled === undefined) {
this.options.introAnimation.enabled = true;
}
if (this.options.introAnimation.showOnce === undefined) {
this.options.introAnimation.showOnce = true;
}
if (this.options.introAnimation.introMode === undefined) {
this.options.introAnimation.introMode = undefined;
}
}
}
this.validator = new validator_1.Validator();
this.resolvedIntro = this.resolveIntroOptions(this.options?.introAnimation);
// Auto-detect name from package.json bin if available
const binName = this.getBinName();
if (binName) {
this.executableName = binName;
}
else {
this.executableName = this.name;
}
// Add default commands (each can be hidden via options.defaultCommands)
const defaults = this.options?.defaultCommands ?? {};
if (defaults.rotatePassphrase !== false) {
this.commands.push((0, rotate_passphrase_1.createRotatePassphraseCommand)(this.name));
}
if (defaults.aiGuide !== false) {
this.commands.push((0, ai_guide_1.createAiGuideCommand)({
name: this.name,
description: this.description,
executableName: this.executableName,
version: this.options?.version,
getCommands: () => this.commands,
}));
}
}
getBinName() {
try {
const scriptDir = path_1.default.dirname(process.argv[1]);
const packageJsonPath = path_1.default.join(scriptDir, 'package.json');
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
const bin = packageJson.bin;
if (typeof bin === 'string') {
return bin;
}
else if (typeof bin === 'object' && bin !== null) {
// Return the first bin key
const keys = Object.keys(bin);
return keys.length > 0 ? keys[0] : null;
}
}
catch (error) {
// Ignore errors (file not found, invalid JSON, etc.)
}
return null;
}
getCommands() {
return this.commands;
}
getName() {
return this.name;
}
getDescription() {
return this.description;
}
getOptions() {
return this.options;
}
command(command) {
this.commands.push(command);
}
setupCommand(options) {
this.setupSteps = options.steps;
this.setupConfigFileName = options.configFileName;
const setupCmd = (0, setup_1.createSetupCommand)(this.name, options);
this.commands.push(setupCmd);
return this;
}
/**
* Get a specific config value from the setup config.
* For non-Password fields, returns the raw value.
* For Password fields, prompts for passphrase if not provided.
* Passphrase can be provided via CLI_PASSPHRASE environment variable to skip prompting.
*/
async getConfigValue(key, passphrase) {
if (!this.setupSteps) {
// If no setup steps defined, return raw value
const config = (0, setup_1.getRawConfig)(this.name, { configFileName: this.setupConfigFileName });
return config[key];
}
// Check if the requested key is a Password field
const step = this.setupSteps.find(s => s.name === key);
const isPasswordField = step?.type === interfaces_1.ParamType.Password;
if (isPasswordField) {
// Password field requires passphrase
let actualPassphrase = passphrase;
if (!actualPassphrase) {
// Check for environment variable first
actualPassphrase = process.env.CLI_PASSPHRASE;
if (!actualPassphrase) {
actualPassphrase = await (0, setup_1.hiddenPrompt)('Passphrase to decrypt: ');
}
}
const config = (0, setup_1.loadSetupConfig)(this.name, this.setupSteps, {
passphrase: actualPassphrase,
configFileName: this.setupConfigFileName,
});
return config[key];
}
else {
// Non-password field, return raw value
const config = (0, setup_1.getRawConfig)(this.name, { configFileName: this.setupConfigFileName });
return config[key];
}
}
/**
* Load all config values from the setup config.
* For Password fields, prompts for passphrase if not provided.
* Passphrase can be provided via CLI_PASSPHRASE environment variable to skip prompting.
*/
async loadConfig(passphrase) {
if (!this.setupSteps) {
// If no setup steps defined, return raw config
return (0, setup_1.getRawConfig)(this.name, { configFileName: this.setupConfigFileName });
}
// Check if there are any Password fields
const hasPasswordFields = this.setupSteps.some(s => s.type === interfaces_1.ParamType.Password);
if (hasPasswordFields) {
// Password fields require passphrase
let actualPassphrase = passphrase;
if (!actualPassphrase) {
// Check for environment variable first
actualPassphrase = process.env.CLI_PASSPHRASE;
if (!actualPassphrase) {
actualPassphrase = await (0, setup_1.hiddenPrompt)('Passphrase to decrypt: ');
}
}
return (0, setup_1.loadSetupConfig)(this.name, this.setupSteps, {
passphrase: actualPassphrase,
configFileName: this.setupConfigFileName,
});
}
else {
// No password fields, return raw config
return (0, setup_1.getRawConfig)(this.name, { configFileName: this.setupConfigFileName });
}
}
/**
* Start an interactive REPL session with slash commands, tool calls,
* streaming output, and conversation history.
* The returned promise resolves when the user exits the session.
*/
async startSession(options) {
const session = new session_1.InteractiveSession(options);
return session.start();
}
setOptions(options) {
const merged = { ...this.options, ...options };
if (merged.branding === undefined) {
merged.branding = true;
}
if (merged.introAnimation == null) {
merged.introAnimation = { enabled: false, showOnce: true };
}
else {
if (merged.introAnimation.enabled === undefined) {
merged.introAnimation.enabled = true;
}
if (merged.introAnimation.showOnce === undefined) {
merged.introAnimation.showOnce = true;
}
if (merged.introAnimation.introMode === undefined) {
merged.introAnimation.introMode = undefined;
}
}
this.options = merged;
this.resolvedIntro = this.resolveIntroOptions(this.options.introAnimation);
}
parse(argv) {
const [nodePath, scriptPath, ...rawArgs] = argv;
const { introMode, filteredArgs } = this.extractIntroFlags(rawArgs);
const args = filteredArgs;
this.showIntroIfNeeded(introMode);
if (args.length === 0 || args[0] === '--version') {
if (args[0] === '--version') {
console.log(`\n${colors_1.Colors.FgGreen}${this.executableName} version: ${this.options?.version}${colors_1.Colors.Reset}\n`);
}
else {
this.help();
}
return;
}
// Handle global help (when --help is the only argument)
if (args.length === 1 && args[0] === '--help') {
this.help();
return;
}
// Check if we're dealing with a command that has subcommands
const commandPath = [];
let currentArgs = [...args];
let i = 0;
// Build the command path (e.g., "create", "create project", etc.)
while (i < args.length) {
const potentialCommand = args[i];
if (potentialCommand.startsWith('--'))
break;
commandPath.push(potentialCommand);
currentArgs = args.slice(i + 1);
i++;
// Check if we've found a valid command path
if (commandPath.length === 1) {
const rootCommand = this.findCommand(commandPath[0]);
if (!rootCommand) {
this.showUnknownCommandError(commandPath[0]);
return;
}
// If no subcommands or we've reached the end of args, stop here
if (!rootCommand.subcommands || i >= args.length || args[i].startsWith('--'))
break;
}
else {
// We're looking for a subcommand
const result = this.findSubcommand(commandPath);
if (!result) {
// If not found, back up one level and treat the rest as arguments
commandPath.pop();
currentArgs = args.slice(i);
break;
}
// If no further subcommands or we've reached the end of args, stop here
const { command } = result;
if (!command.subcommands || i >= args.length || args[i].startsWith('--'))
break;
}
}
// Handle help flag for any command level
if (currentArgs.includes('--help')) {
if (commandPath.length === 1) {
const command = this.findCommand(commandPath[0]);
if (command)
this.commandHelp(command, commandPath);
}
else if (commandPath.length > 1) {
const result = this.findSubcommand(commandPath);
if (result)
this.commandHelp(result.command, commandPath);
}
return;
}
// Execute the command or subcommand
let commandToExecute;
if (commandPath.length === 1) {
commandToExecute = this.findCommand(commandPath[0]);
}
else if (commandPath.length > 1) {
const result = this.findSubcommand(commandPath);
if (result)
commandToExecute = result.command;
}
if (!commandToExecute) {
this.showUnknownCommandError(commandPath.join(' '));
return;
}
const params = this.parseArgs(currentArgs, commandToExecute);
if (params.error) {
console.log(`\n${params.error}`);
process.exit(1);
}
if (Object.keys(params.result).length > 0 && this.options.interactive) {
this.options.interactive = false;
}
const missingParams = this.getMissingParams(commandToExecute, params.result);
if (missingParams.length > 0 && this.options?.interactive) {
this.handleMissingParams(commandToExecute.params, params.result, commandToExecute);
}
else {
if (missingParams.length > 0) {
console.log(`\n${colors_1.Colors.BgRed}${colors_1.Colors.FgWhite} ERROR ${colors_1.Colors.Reset} ${colors_1.Colors.FgRed}Missing required parameters${colors_1.Colors.Reset}\n`);
missingParams.forEach((param) => {
console.log(`${colors_1.Colors.FgRed}❌ ${colors_1.Colors.Bright}${param.name}${colors_1.Colors.Reset}${colors_1.Colors.FgRed} (${param.type})${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgGray}${param.description}${colors_1.Colors.Reset}`);
if (param.options) {
console.log(` ${colors_1.Colors.FgGray}Options: ${param.options.join(', ')}${colors_1.Colors.Reset}`);
}
console.log('');
});
const optionalParams = this.getOptionalParams(commandToExecute, params.result);
if (optionalParams.length > 0) {
console.log(`${colors_1.Colors.FgYellow}⚠️ Optional parameters you can provide:${colors_1.Colors.Reset}\n`);
optionalParams.forEach((param) => {
console.log(`${colors_1.Colors.FgYellow}○ ${param.name}${colors_1.Colors.Reset}${colors_1.Colors.FgYellow} (${param.type})${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgGray}${param.description}${colors_1.Colors.Reset}`);
if (param.options) {
console.log(` ${colors_1.Colors.FgGray}Options: ${param.options.join(', ')}${colors_1.Colors.Reset}`);
}
console.log('');
});
}
console.log(`${colors_1.Colors.FgGray}Try '${colors_1.Colors.Bright}${this.executableName} ${commandToExecute.name} --help${colors_1.Colors.Reset}${colors_1.Colors.FgGray}' for more information.${colors_1.Colors.Reset}\n`);
process.exit(1);
}
const actionResult = commandToExecute.action(params.result);
Promise.resolve(actionResult)
.then(() => this.showBranding())
.catch(error => {
console.error(`${colors_1.Colors.Error}❌ Command failed:${colors_1.Colors.Reset}`, error);
});
}
}
help() {
console.log(`\n${colors_1.Colors.BgGreen}${colors_1.Colors.FgBlack} ${this.name} ${colors_1.Colors.Reset}`);
console.log(`${colors_1.Colors.FgGreen}${this.description}${colors_1.Colors.Reset}\n`);
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}USAGE${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgGray}$ ${this.executableName} <command> [options]${colors_1.Colors.Reset}\n`);
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}COMMANDS${colors_1.Colors.Reset}`);
this.commands.forEach(cmd => {
console.log(` ${colors_1.Colors.FgGreen}${cmd.name.padEnd(15)}${colors_1.Colors.Reset}${cmd.description}`);
if (cmd.subcommands && cmd.subcommands.length > 0) {
cmd.subcommands.forEach(subcmd => {
console.log(` ${colors_1.Colors.FgGreen}${cmd.name} ${subcmd.name}${colors_1.Colors.Reset}: ${subcmd.description}`);
});
}
});
console.log('');
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}OPTIONS${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgYellow}--help${colors_1.Colors.Reset}${colors_1.Colors.FgGray.padEnd(12)}Show help for a command${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgYellow}--version${colors_1.Colors.Reset}${colors_1.Colors.FgGray.padEnd(10)}Show version information${colors_1.Colors.Reset}\n`);
console.log(`${colors_1.Colors.FgGray}Run '${colors_1.Colors.Bright}${this.executableName} <command> --help${colors_1.Colors.Reset}${colors_1.Colors.FgGray}' for detailed help on a specific command.${colors_1.Colors.Reset}\n`);
}
showBranding() {
if (this.options?.branding) {
console.log(`\n${colors_1.Colors.FgGray}─────────────────────────────────────────────────────────────────${colors_1.Colors.Reset}`);
console.log(`${colors_1.Colors.FgYellow}⭐ ${colors_1.Colors.Bright}Like this CLI? Star us on GitHub!${colors_1.Colors.Reset}`);
console.log(`${colors_1.Colors.FgGray} https://github.com/ideascoldigital/cli-maker${colors_1.Colors.Reset}`);
console.log(`${colors_1.Colors.FgGray}─────────────────────────────────────────────────────────────────${colors_1.Colors.Reset}\n`);
}
}
commandHelp(command, fullCommandPath) {
const fullCommand = fullCommandPath.join(' ');
console.log(`\n${colors_1.Colors.BgGreen}${colors_1.Colors.FgBlack} ${this.name} ${fullCommand} ${colors_1.Colors.Reset}`);
console.log(`${colors_1.Colors.FgGreen}${command.description}${colors_1.Colors.Reset}\n`);
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}USAGE${colors_1.Colors.Reset}`);
const paramList = command.params.map(p => `${p.required ? '<' : '['}${p.name}${p.required ? '>' : ']'} `).join('');
console.log(` ${colors_1.Colors.FgGray}$ ${this.executableName} ${fullCommand} ${paramList}${colors_1.Colors.Reset}\n`);
if (command.params.length > 0) {
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}PARAMETERS${colors_1.Colors.Reset}\n`);
console.log((0, common_1.formatParameterTable)(command.params));
console.log('');
}
if (command.subcommands && command.subcommands.length > 0) {
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}SUBCOMMANDS${colors_1.Colors.Reset}`);
command.subcommands.forEach(subcmd => {
console.log(` ${colors_1.Colors.FgGreen}${subcmd.name}${colors_1.Colors.Reset}: ${subcmd.description}`);
});
console.log('');
}
console.log(`${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}EXAMPLES${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgGray}$ ${this.executableName} ${fullCommand} --help${colors_1.Colors.Reset}`);
if (command.params.some(p => p.required)) {
const exampleParams = command.params.filter(p => p.required).slice(0, 2).map(p => {
if (p.type === interfaces_1.ParamType.List && p.options) {
return `--${p.name} ${p.options[0]}`;
}
else if (p.type === interfaces_1.ParamType.Boolean) {
return `--${p.name}`;
}
else {
return `--${p.name} value`;
}
}).join(' ');
console.log(` ${colors_1.Colors.FgGray}$ ${this.executableName} ${fullCommand} ${exampleParams}${colors_1.Colors.Reset}`);
}
console.log('');
}
findCommand(commandName, commands = this.commands) {
return commands.find(cmd => cmd.name === commandName);
}
findSubcommand(commandPath) {
if (commandPath.length < 2)
return undefined;
const rootCommandName = commandPath[0];
const rootCommand = this.findCommand(rootCommandName);
if (!rootCommand || !rootCommand.subcommands)
return undefined;
let currentCommand = rootCommand;
let currentCommands = rootCommand.subcommands;
let i = 1;
while (i < commandPath.length - 1) {
const subCmd = this.findCommand(commandPath[i], currentCommands);
if (!subCmd || !subCmd.subcommands)
return undefined;
currentCommand = subCmd;
currentCommands = subCmd.subcommands;
i++;
}
const finalSubcommand = this.findCommand(commandPath[commandPath.length - 1], currentCommands);
if (!finalSubcommand)
return undefined;
return { parentCommand: currentCommand, command: finalSubcommand };
}
showUnknownCommandError(commandName) {
console.log(`\n${colors_1.Colors.BgRed}${colors_1.Colors.FgWhite} ERROR ${colors_1.Colors.Reset} ${colors_1.Colors.FgRed}Unknown command: '${colors_1.Colors.Bright}${commandName}${colors_1.Colors.Reset}${colors_1.Colors.FgRed}'${colors_1.Colors.Reset}\n`);
console.log(`${colors_1.Colors.FgYellow}Available commands:${colors_1.Colors.Reset}\n`);
this.commands.forEach(cmd => {
console.log(` ${colors_1.Colors.FgGreen}${cmd.name}${colors_1.Colors.Reset}: ${cmd.description}`);
if (cmd.subcommands && cmd.subcommands.length > 0) {
cmd.subcommands.forEach(subcmd => {
console.log(` ${colors_1.Colors.FgGreen}${cmd.name} ${subcmd.name}${colors_1.Colors.Reset}: ${subcmd.description}`);
});
}
});
console.log(`\n${colors_1.Colors.FgGray}Try '${colors_1.Colors.Bright}${this.executableName} --help${colors_1.Colors.Reset}${colors_1.Colors.FgGray}' for more information.${colors_1.Colors.Reset}\n`);
}
getMissingParams(command, result) {
const requiredParams = command.params.filter(p => {
if (p.required !== true)
return false;
// Honor when() so conditionally-hidden params aren't flagged missing
if (typeof p.when === 'function') {
try {
return p.when(result);
}
catch {
return true;
}
}
return true;
});
return requiredParams.filter(p => result[p.name] === undefined);
}
getOptionalParams(command, result) {
const optionalParams = command.params.filter(p => p.required === false || p.required === undefined);
return optionalParams.filter(p => result[p.name] === undefined);
}
handleMissingParams(missingParams, result, command) {
if (this.options.interactive) {
this.promptForMissingParams(missingParams, result).then(fullParams => {
command.action(fullParams);
});
}
else {
console.log(`${colors_1.Colors.FgRed}Missing required parameters:${colors_1.Colors.Reset} ${missingParams.map(p => p.name).join(', ')}`);
process.exit(1);
}
}
parseArgs(args, command) {
const result = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
let key;
let value;
if (arg.includes('=')) {
// Format: --param=value
[key, value] = arg.split('=');
}
else {
// Format: --param value
key = arg;
value = args[i + 1];
if (value && !value.startsWith('--')) {
i++; // Skip the next argument as it's the value
}
else {
value = undefined;
}
}
const paramName = key.slice(2);
const commandParam = command.params.find(p => p.name === paramName);
if (commandParam) {
const validation = this.validator.validateParam(value, commandParam.type, commandParam.required, commandParam.options, paramName, !!commandParam.optionsLoader);
if (validation.error) {
return { error: validation.error };
}
result[paramName] = validation.value;
}
else {
return {
error: `\n${colors_1.Colors.BgRed}${colors_1.Colors.FgWhite} ERROR ${colors_1.Colors.Reset} ${colors_1.Colors.FgRed}Unknown parameter: '${colors_1.Colors.Bright}${paramName}${colors_1.Colors.Reset}${colors_1.Colors.FgRed}'${colors_1.Colors.Reset}\n${colors_1.Colors.FgGray} Available parameters: ${command.params.map(p => p.name).join(', ')}${colors_1.Colors.Reset}\n`
};
}
}
}
return { result };
}
validateParam(value, type, isRequired, options, paramName) {
return this.validator.validateParam(value, type, isRequired, options, paramName);
}
findParamType(paramName) {
return this.commands.flatMap(command => command.params).find(param => param.name === paramName);
}
showIntroIfNeeded(overrideMode) {
const introOptions = this.resolvedIntro || this.resolveIntroOptions(this.options?.introAnimation);
const mode = (overrideMode || introOptions?.introMode);
if (!introOptions || introOptions.enabled === false) {
if (mode === 'always') {
this.renderIntroAnimation(introOptions || {});
}
return;
}
if (mode === 'never')
return;
const markerPath = this.getIntroMarkerPath(introOptions);
if (mode !== 'always' && introOptions.showOnce !== false && this.hasSeenIntro(markerPath)) {
return;
}
this.renderIntroAnimation(introOptions);
if (introOptions.showOnce !== false && mode !== 'always') {
this.markIntroSeen(markerPath);
}
}
resolveIntroOptions(intro) {
if (!intro)
return { enabled: false, showOnce: true };
const preset = intro.preset ? INTRO_PRESETS[intro.preset] : undefined;
const replacePlaceholders = (text) => {
if (!text)
return text;
return text
.replace(/\{\{cliName\}\}/g, this.name)
.replace(/\{\{cliDescription\}\}/g, this.description);
};
const extractCliNameWithoutCompany = (fullName) => {
const parts = fullName.split('/');
return parts.length > 1 ? parts[1] : fullName;
};
const extractCompany = (fullName) => {
const parts = fullName.split('/');
return parts.length > 1 ? parts[0] : null;
};
let resolvedTitle = replacePlaceholders(intro.title ?? preset?.title);
let resolvedSubtitle = replacePlaceholders(intro.subtitle ?? preset?.subtitle);
let resolvedLines = intro.lines ?? preset?.lines;
let resolvedAsciiArt = intro.asciiArt ?? preset?.asciiArt;
if (intro.preset === 'ascii-art') {
const cliNameOnly = extractCliNameWithoutCompany(this.name);
const company = extractCompany(this.name);
resolvedAsciiArt = generatePixelAsciiArt(cliNameOnly).map(line => `${colors_1.Colors.FgYellow}${line}${colors_1.Colors.Reset}`);
resolvedTitle = undefined;
resolvedSubtitle = undefined;
if (company) {
resolvedLines = [`${colors_1.Colors.FgGray}by ${company}${colors_1.Colors.Reset}`];
}
}
return {
enabled: intro.enabled ?? preset?.enabled ?? false,
showOnce: intro.showOnce ?? preset?.showOnce ?? true,
introMode: intro.introMode ?? preset?.introMode,
animateText: intro.animateText ?? preset?.animateText ?? true,
preset: intro.preset,
frames: intro.frames ?? preset?.frames,
padding: intro.padding ?? preset?.padding,
speedMs: intro.speedMs ?? preset?.speedMs,
loops: intro.loops ?? preset?.loops,
lines: resolvedLines,
asciiArt: resolvedAsciiArt,
title: resolvedTitle,
subtitle: resolvedSubtitle,
storageKey: intro.storageKey ?? preset?.storageKey,
};
}
renderIntroAnimation(introOptions) {
const frames = introOptions.frames && introOptions.frames.length > 0 ? introOptions.frames : ['◢', '◣', '◤', '◥'];
const loops = Math.max(1, introOptions.loops ?? 2);
const speed = Math.max(40, introOptions.speedMs ?? 90);
const totalFrames = frames.length * loops;
const fallbackFrame = frames[0] || '★';
let renderedLines = 0;
if (!process.stdout.isTTY) {
const lines = this.buildIntroLines(fallbackFrame, introOptions, 1);
console.log(lines.join('\n'));
console.log('');
return;
}
for (let i = 0; i < totalFrames; i++) {
const frameSymbol = frames[i % frames.length] || fallbackFrame;
const revealProgress = introOptions.animateText === false ? 1 : Math.min(1, (i + 1) / totalFrames);
const lines = this.buildIntroLines(frameSymbol, introOptions, revealProgress);
if (renderedLines > 0) {
process.stdout.write(`\x1b[${renderedLines}A\r`);
process.stdout.write('\x1b[0J');
}
process.stdout.write(lines.join('\n') + '\n');
renderedLines = lines.length;
this.blockingSleep(speed);
}
console.log('');
}
buildIntroLines(frameSymbol, introOptions, revealProgress) {
const paddingSize = Math.max(0, introOptions.padding ?? 2);
const padding = ' '.repeat(paddingSize);
const title = introOptions.title || this.name;
const subtitle = introOptions.subtitle ?? this.description;
const extraLines = introOptions.lines ?? [];
const asciiArt = introOptions.asciiArt ?? [];
const rawTitle = `${frameSymbol} ${title}`;
const rawLines = [rawTitle];
if (subtitle)
rawLines.push(subtitle);
rawLines.push(...extraLines);
rawLines.push(...asciiArt);
const contentWidth = Math.max(...rawLines.map(line => (0, common_1.stripAnsiCodes)(line).length));
const terminalWidth = process.stdout.columns || 80;
const minWidth = Math.floor(terminalWidth / 2) - paddingSize * 2 - 4;
const maxContentWidth = Math.max(contentWidth, minWidth);
const horizontal = '─'.repeat(maxContentWidth + 2);
const topBorder = `${padding}${colors_1.Colors.FgGray}┌${horizontal}┐${colors_1.Colors.Reset}`;
const bottomBorder = `${padding}${colors_1.Colors.FgGray}└${horizontal}┘${colors_1.Colors.Reset}`;
const formatLine = (text, rawText) => {
const displayLength = (0, common_1.stripAnsiCodes)(text).length;
const spaces = Math.max(0, maxContentWidth - displayLength);
const lineContent = ` ${text}${' '.repeat(spaces)} `;
return `${padding}${colors_1.Colors.FgGray}│${colors_1.Colors.Reset}${lineContent}${colors_1.Colors.FgGray}│${colors_1.Colors.Reset}`;
};
const rainbowMode = introOptions.preset === 'rainbow';
const visibleTitle = this.revealText(title, revealProgress, introOptions.animateText);
const coloredTitle = rainbowMode ? this.colorizeRainbow(visibleTitle) : `${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}${visibleTitle}${colors_1.Colors.Reset}`;
const formattedTitle = `${colors_1.Colors.FgYellow}${frameSymbol}${colors_1.Colors.Reset} ${coloredTitle}`;
const formattedLines = [formatLine(formattedTitle, rawTitle)];
if (subtitle) {
const visibleSubtitle = this.revealText(subtitle, revealProgress, introOptions.animateText);
const coloredSubtitle = rainbowMode ? this.colorizeRainbow(visibleSubtitle) : `${colors_1.Colors.FgGray}${visibleSubtitle}${colors_1.Colors.Reset}`;
formattedLines.push(formatLine(coloredSubtitle, subtitle));
}
extraLines.forEach(line => {
const visibleLine = this.revealText(line, revealProgress, introOptions.animateText);
const coloredLine = rainbowMode ? this.colorizeRainbow(visibleLine) : `${colors_1.Colors.FgGray}${visibleLine}${colors_1.Colors.Reset}`;
formattedLines.push(formatLine(coloredLine, line));
});
asciiArt.forEach(line => {
const shouldAnimateArt = introOptions.animateText !== false && !line.includes('\x1b');
const visibleArt = this.revealText(line, shouldAnimateArt ? revealProgress : 1, shouldAnimateArt);
formattedLines.push(formatLine(visibleArt, line));
});
return [topBorder, ...formattedLines, bottomBorder];
}
getIntroMarkerPath(introOptions) {
if (introOptions.showOnce === false)
return null;
const homeDir = os_1.default.homedir();
const safeName = (introOptions.storageKey || `${this.executableName || this.name}-intro`)
.replace(/[^a-z0-9-_]/gi, '-')
.toLowerCase();
const markerDir = path_1.default.join(homeDir, '.cli-maker');
return path_1.default.join(markerDir, `${safeName}.json`);
}
hasSeenIntro(markerPath) {
if (!markerPath)
return false;
try {
return fs_1.default.existsSync(markerPath);
}
catch {
return false;
}
}
markIntroSeen(markerPath) {
if (!markerPath)
return;
try {
fs_1.default.mkdirSync(path_1.default.dirname(markerPath), { recursive: true });
fs_1.default.writeFileSync(markerPath, JSON.stringify({ seenAt: new Date().toISOString() }), 'utf-8');
}
catch {
// If we cannot persist the marker, just continue without blocking the CLI
}
}
blockingSleep(ms) {
if (ms <= 0)
return;
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
Atomics.wait(sharedArray, 0, 0, ms);
}
revealText(text, progress, animate) {
if (!text)
return '';
if (animate === false)
return text;
const clean = (0, common_1.stripAnsiCodes)(text);
const len = clean.length;
if (len === 0)
return '';
const visible = Math.min(len, Math.max(0, Math.round(len * progress)));
return text.slice(0, visible);
}
colorizeRainbow(text) {
const palette = [colors_1.Colors.FgRed, colors_1.Colors.FgYellow, colors_1.Colors.FgGreen, colors_1.Colors.FgCyan, colors_1.Colors.FgBlue, colors_1.Colors.FgMagenta];
let colored = '';
let colorIndex = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char.trim().length === 0) {
colored += char;
continue;
}
colored += `${palette[colorIndex % palette.length]}${char}${colors_1.Colors.Reset}`;
colorIndex++;
}
return colored;
}
extractIntroFlags(rawArgs) {
let introMode;
const filteredArgs = rawArgs.filter(arg => {
if (arg === '--intro-always') {
introMode = 'always';
return false;
}
if (arg === '--no-intro') {
introMode = 'never';
return false;
}
return true;
});
return { introMode, filteredArgs };
}
async promptForMissingParams(missingParams, existingParams) {
let rl = readline_1.default.createInterface({
input: process.stdin,
output: process.stdout
});
const askQuestion = (question) => {
return new Promise(resolve => rl.question(question, resolve));
};
console.log(`\n${colors_1.Colors.BgBlue}${colors_1.Colors.FgWhite} INTERACTIVE MODE ${colors_1.Colors.Reset} ${colors_1.Colors.FgBlue}Please provide the following information:${colors_1.Colors.Reset}\n`);
const prompts = missingParams.reduce((promise, param) => {
return promise.then(async (answers) => {
let answer;
let validation;
const p = param;
// Conditional visibility: skip param when when() returns false
if (typeof p.when === 'function') {
try {
if (!p.when(answers)) {
return { ...answers, [param.name]: undefined };
}
}
catch (e) {
console.log(`${colors_1.Colors.Error}when() failed for ${param.name}: ${e?.message ?? e}${colors_1.Colors.Reset}`);
}
}
// Resolve default value (static or context-aware function)
let resolvedDefault = undefined;
if (p.defaultValue !== undefined) {
try {
resolvedDefault = typeof p.defaultValue === 'function'
? await p.defaultValue(answers)
: p.defaultValue;
}
catch (e) {
console.log(`${colors_1.Colors.Error}defaultValue() failed for ${param.name}: ${e?.message ?? e}${colors_1.Colors.Reset}`);
}
}
// Lazy-load options if a loader was provided and no options were set
if (param.type === interfaces_1.ParamType.List && !param.options && p.optionsLoader) {
try {
const loaded = await p.optionsLoader(answers);
p.options = loaded;
}
catch (e) {
console.log(`${colors_1.Colors.Error}Failed to load options for ${param.name}: ${e?.message ?? e}${colors_1.Colors.Reset}`);
}
}
// Show parameter header
const requiredIndicator = param.required ? `${colors_1.Colors.FgRed}*${colors_1.Colors.Reset}` : `${colors_1.Colors.FgGray}○${colors_1.Colors.Reset}`;
console.log(`${requiredIndicator} ${colors_1.Colors.Bright}${param.name}${colors_1.Colors.Reset} ${colors_1.Colors.FgGray}(${param.type})${colors_1.Colors.Reset}`);
console.log(` ${colors_1.Colors.FgGray}${param.description}${colors_1.Colors.Reset}`);
const optsForHeader = param.options;
if (optsForHeader && optsForHeader.length > 0 && optsForHeader.length <= 12) {
console.log(` ${colors_1.Colors.FgGray}Options: ${optsForHeader.join(', ')}${colors_1.Colors.Reset}`);
}
else if (optsForHeader && optsForHeader.length > 12) {
console.log(` ${colors_1.Colors.FgGray}${optsForHeader.length} options available — type to search${colors_1.Colors.Reset}`);
}
if (resolvedDefault !== undefined && resolvedDefault !== '') {
console.log(` ${colors_1.Colors.FgGray}Default: ${colors_1.Colors.FgCyan}${resolvedDefault}${colors_1.Colors.Reset}`);
}
console.log('');
if (param.type === interfaces_1.ParamType.Array) {
rl.close();
const arr = await this.promptArray(p, answers);
rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
validation = { value: arr };
console.log(`${colors_1.Colors.Success}✓ Collected ${arr.length} item(s)${colors_1.Colors.Reset}\n`);
}
else if (param.type === interfaces_1.ParamType.List && param.options) {
const opts = param.options;
const pageSize = p.pageSize ?? 10;
const isSearchable = p.searchable === true || opts.length > pageSize * 2;
if (isSearchable) {
const selected = await this.promptSearchableList(p);
validation = { value: selected };
const label = (p.optionLabel ?? String)(selected);
console.log(`${colors_1.Colors.Success}✓ Selected: ${label}${colors_1.Colors.Reset}\n`);
}
else {
console.log(`${colors_1.Colors.FgCyan}Use ↑/↓ arrow keys to navigate, Enter to select:${colors_1.Colors.Reset}`);
answer = await this.promptWithArrows(param);
validation = { value: opts[parseInt(answer, 10)] };
console.log(`${colors_1.Colors.Success}✓ Selected: ${validation.value}${colors_1.Colors.Reset}\n`);
}
}
else {
let attemptCount = 0;
do {
if (attemptCount > 0) {
console.log(`${colors_1.Colors.FgYellow}Please try again:${colors_1.Colors.Reset}`);
}
const hasDefault = resolvedDefault !== undefined && resolvedDefault !== '';
const promptText = hasDefault
? `${colors_1.Colors.FgGreen}Enter value${colors_1.Colors.Reset} ${colors_1.Colors.FgGray}[${resolvedDefault}]${colors_1.Colors.Reset}: `
: (param.required
? `${colors_1.Colors.FgGreen}Enter value${colors_1.Colors.Reset} ${colors_1.Colors.FgGray}(required)${colors_1.Colors.Reset}: `
: `${colors_1.Colors.FgGreen}Enter value${colors_1.Colors.Reset} ${colors_1.Colors.FgGray}(or press Enter to skip)${colors_1.Colors.Reset}: `);
// Use hidden input for Password type
if (param.type === interfaces_1.ParamType.Password) {
rl.close(); // Close readline to avoid interference with raw mode
answer = await CLI.askHiddenQuestion(promptText);
// Recreate readline for next questions
rl = readline_1.default.createInterface({
input: process.stdin,
output: process.stdout
});
}
else {
answer = await askQuestion(promptText);
}
// Apply default when user submitted nothing
if ((answer === undefined || answer === '') && hasDefault) {
answer = String(resolvedDefault);
}
validation = this.validateParam(answer, param.type, param.required, param.options);
if (validation.error) {
console.log(validation.error);
attemptCount++;
}
else if (validation.value !== undefined) {
console.log(`${colors_1.Colors.Success}✓ Accepted${colors_1.Colors.Reset}\n`);
}
else {
console.log(`${colors_1.Colors.FgGray}○ Skipped${colors_1.Colors.Reset}\n`);
}
} while (validation.error);
}
return { ...answers, [param.name]: validation.value };
});
}, Promise.resolve(existingParams));
return prompts.finally(() => {
rl.close();
console.log(`${colors_1.Colors.Success}✅ All parameters collected successfully!${colors_1.Colors.Reset}\n`);
});
}
async promptWithArrows(param) {
return new Promise(resolve => {
let index = 0;