UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

346 lines 12.5 kB
import chalk from 'chalk'; import * as path from 'path'; import { $ } from '@xec-sh/core'; import { Command } from 'commander'; import * as clack from '@clack/prompts'; import { handleError } from './error-handler.js'; import { OutputFormatter } from './output-formatter.js'; import { TaskManager, TargetResolver, ConfigurationManager } from '../config/index.js'; export class BaseCommand { constructor(config) { this.config = config; this.options = { verbose: false, quiet: false, output: 'text', dryRun: false }; this.xecConfig = null; this.targetResolver = null; this.taskManager = null; this.formatter = new OutputFormatter(); } create() { const command = new Command(this.config.name); command .description(this.config.description) .option('-o, --output <format>', 'Output format (text|json|yaml|csv)', 'text') .option('-c, --config <path>', 'Path to configuration file') .option('--dry-run', 'Perform a dry run without making changes'); if (this.config.arguments) { command.arguments(this.config.arguments); } if (this.config.aliases) { this.config.aliases.forEach(alias => command.alias(alias)); } if (this.config.options) { this.config.options.forEach(opt => { command.option(opt.flags, opt.description, opt.defaultValue); }); } if (this.config.examples) { const exampleText = this.config.examples .map(ex => ` ${chalk.cyan(ex.command)}\n ${ex.description}`) .join('\n\n'); command.addHelpText('after', `\nExamples:\n\n${exampleText}`); } command.action(async (...args) => { try { const options = args[args.length - 1]; const parentOptions = options.parent?.opts() || {}; const commandOptions = {}; for (const key in options) { if (!key.startsWith('_') && key !== 'parent' && key !== 'args' && key !== 'commands' && key !== 'options' && typeof options[key] !== 'function') { commandOptions[key] = options[key]; } } this.options = { ...commandOptions, verbose: parentOptions.verbose || options.verbose || false, quiet: parentOptions.quiet || options.quiet || false, output: options.output || 'text', config: options.config, dryRun: options.dryRun || false, }; if (this.config.validateOptions) { this.config.validateOptions(options); } this.formatter.setFormat(this.options.output || 'text'); this.formatter.setQuiet(this.options.quiet || false); this.formatter.setVerbose(this.options.verbose || false); await this.execute(args); } catch (error) { handleError(error, this.options); } }); return command; } getCommandConfigKey() { return this.config.name; } async initializeConfig(options) { this.configManager = new ConfigurationManager({ projectRoot: options.configPath ? path.dirname(path.dirname(options.configPath)) : process.cwd(), profile: options.profile, }); this.xecConfig = await this.configManager.load(); this.targetResolver = new TargetResolver(this.xecConfig); this.taskManager = new TaskManager({ configManager: this.configManager, debug: options.verbose, dryRun: options.dryRun }); await this.taskManager.load(); } getCommandDefaults() { if (!this.xecConfig) { return {}; } const commandKey = this.getCommandConfigKey(); const defaults = this.xecConfig.commands?.[commandKey] || {}; return defaults; } async resolveTarget(targetSpec) { if (!this.targetResolver) { throw new Error('Configuration not initialized'); } return this.targetResolver.resolve(targetSpec); } async findTargets(pattern) { if (!this.targetResolver) { throw new Error('Configuration not initialized'); } return this.targetResolver.find(pattern); } async createTargetEngine(target) { const config = target.config; switch (target.type) { case 'local': return $; case 'ssh': { if (this.options?.verbose) { console.log('SSH target config:', JSON.stringify(config, null, 2)); } const sshEngine = $.ssh({ host: config.host, username: config.user || config.username, port: config.port, privateKey: config.privateKey, password: config.password, passphrase: config.passphrase }); if (config.env && Object.keys(config.env).length > 0) { return sshEngine.env(config.env); } return sshEngine; } case 'docker': { const dockerOptions = { container: config.container, image: config.image, user: config.user, workingDir: config.workdir, tty: config.tty, ...config }; Object.keys(dockerOptions).forEach(key => { if (dockerOptions[key] === undefined) { delete dockerOptions[key]; } }); const dockerEngine = $.docker(dockerOptions); if (config.env && Object.keys(config.env).length > 0) { return dockerEngine.env(config.env); } return dockerEngine; } case 'k8s': { const k8sOptions = { pod: config.pod, namespace: config.namespace || 'default', container: config.container, context: config.context, kubeconfig: config.kubeconfig, ...config }; Object.keys(k8sOptions).forEach(key => { if (k8sOptions[key] === undefined) { delete k8sOptions[key]; } }); return $.k8s(k8sOptions); } default: throw new Error(`Unsupported target type: ${target.type}`); } } formatTargetDisplay(target) { const name = chalk.cyan(target.name || target.id); const type = chalk.gray(`[${target.type}]`); let details = ''; switch (target.type) { case 'ssh': { const sshConfig = target.config; const username = sshConfig.user || sshConfig.username || 'unknown'; details = ` ${chalk.gray(`${username}@${sshConfig.host}`)}`; break; } case 'docker': { const dockerConfig = target.config; if (dockerConfig.image) { details = ` ${chalk.gray(`(${dockerConfig.image})`)}`; } break; } case 'k8s': { const k8sConfig = target.config; if (k8sConfig.namespace && k8sConfig.namespace !== 'default') { details = ` ${chalk.gray(`(ns: ${k8sConfig.namespace})`)}`; } if (k8sConfig.container) { details += ` ${chalk.gray(`[${k8sConfig.container}]`)}`; } break; } } return `${name}${details} ${type}`; } applyDefaults(options, defaults) { const merged = { ...options }; Object.keys(defaults).forEach(key => { if (defaults[key] !== undefined) { if (!this.wasOptionExplicitlySet(key, options)) { merged[key] = defaults[key]; } } }); return merged; } wasOptionExplicitlySet(key, options) { return options[key] !== undefined; } startSpinner(message) { if (!this.options.quiet) { this.spinner = clack.spinner(); this.spinner.start(message); } } stopSpinner(message, code) { if (this.spinner) { this.spinner.stop(message, code); this.spinner = null; } } log(message, level = 'info') { if (this.options.quiet) return; switch (level) { case 'success': clack.log.success(message); break; case 'warn': clack.log.warn(message); break; case 'error': clack.log.error(message); break; default: clack.log.info(message); } } output(data, title) { this.formatter.output(data, title); } table(rows, headers) { const tableData = { columns: headers ? headers.map(h => ({ header: h })) : Object.keys(rows[0] || {}).map(k => ({ header: k })), rows: rows.map(row => { if (headers) { return headers.map(h => row[h] || ''); } else { return Object.values(row); } }) }; this.formatter.table(tableData); } async confirm(message, initial = false) { if (this.options.quiet) return Promise.resolve(initial); const result = await clack.confirm({ message, initialValue: initial }); if (typeof result === 'symbol') { return initial; } return result; } async prompt(message, initial) { if (this.options.quiet) return Promise.resolve(initial || ''); const result = await clack.text({ message, initialValue: initial }); if (typeof result === 'symbol') { return initial || ''; } return result; } async select(message, options) { if (this.options.quiet) return Promise.resolve(options[0]?.value || ''); const result = await clack.select({ message, options }); if (typeof result === 'symbol') { return options[0]?.value || ''; } return result; } async multiselect(message, options) { if (this.options.quiet) return Promise.resolve([]); const result = await clack.multiselect({ message, options }); if (typeof result === 'symbol') { return []; } return result; } intro(message) { if (!this.options.quiet) { clack.intro(message); } } outro(message) { if (!this.options.quiet) { clack.outro(message); } } isDryRun() { return this.options.dryRun || false; } isVerbose() { return this.options.verbose || false; } isQuiet() { return this.options.quiet || false; } } export class SubcommandBase extends BaseCommand { create() { const command = super.create(); this.setupSubcommands(command); return command; } async execute(args) { const command = args[args.length - 1]; if (!command.args.length) { command.help(); } } } export const ConfigAwareCommand = BaseCommand; //# sourceMappingURL=command-base.js.map