UNPKG

@ideascol/cli-maker

Version:
1,027 lines (1,026 loc) 65.8 kB
"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;