UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

570 lines 24.7 kB
import { z } from 'zod'; import path from 'path'; import chalk from 'chalk'; import { $ } from '@xec-sh/core'; import { parseTimeout } from '../utils/time.js'; import { validateOptions } from '../utils/validation.js'; import { ScriptLoader } from '../utils/script-loader.js'; import { ConfigAwareCommand } from '../utils/command-base.js'; import { InteractiveHelpers } from '../utils/interactive-helpers.js'; export class InCommand extends ConfigAwareCommand { constructor() { super({ name: 'in', description: 'Execute commands in containers or Kubernetes pods', arguments: '<target> [command...]', options: [ { flags: '-p, --profile <profile>', description: 'Configuration profile to use', }, { flags: '--task <task>', description: 'Execute a configured task in the target', }, { flags: '--repl', description: 'Start a REPL session with $target available', }, { flags: '-t, --timeout <duration>', description: 'Command timeout (e.g., 30s, 5m)', }, { flags: '-e, --env <key=value>', description: 'Environment variables (can be used multiple times)', }, { flags: '-d, --cwd <path>', description: 'Working directory in container', }, { flags: '-u, --user <user>', description: 'User to run command as', }, { flags: '-i, --interactive', description: 'Interactive mode (attach to container)', }, { flags: '--parallel', description: 'Execute on multiple targets in parallel', }, ], examples: [ { command: 'xec in containers.app "npm test"', description: 'Execute in configured Docker container', }, { command: 'xec in pods.webapp "date"', description: 'Execute in configured Kubernetes pod', }, { command: 'xec in mycontainer ./scripts/deploy.ts', description: 'Execute script with $target context', }, { command: 'xec in containers.* --task test --parallel', description: 'Run test task on all containers', }, { command: 'xec in app --repl', description: 'Start REPL with $target available', }, ], validateOptions: (options) => { const schema = z.object({ profile: z.string().optional(), task: z.string().optional(), repl: z.boolean().optional(), timeout: z.string().optional(), env: z.array(z.string()).optional(), cwd: z.string().optional(), user: z.string().optional(), interactive: z.boolean().optional(), parallel: z.boolean().optional(), verbose: z.boolean().optional(), quiet: z.boolean().optional(), dryRun: z.boolean().optional(), }); validateOptions(options, schema); }, }); } getCommandConfigKey() { return 'in'; } async execute(args) { const [targetPattern, ...commandParts] = args.slice(0, -1); const options = args[args.length - 1]; if (!targetPattern) { throw new Error('Target specification is required'); } await this.initializeConfig(options); const defaults = this.getCommandDefaults(); const mergedOptions = this.applyDefaults(options, defaults); let targets; if (targetPattern.includes('*') || targetPattern.includes('{')) { targets = await this.findTargets(targetPattern); if (targets.length === 0) { throw new Error(`No targets found matching pattern: ${targetPattern}`); } } else { const target = await this.resolveTarget(targetPattern); targets = [target]; } if (mergedOptions.task) { await this.executeTask(targets, mergedOptions.task, mergedOptions); } else if (mergedOptions.repl) { if (targets.length === 0) { throw new Error('No targets found'); } if (targets.length > 1) { throw new Error('REPL mode is only supported for single targets'); } await this.startRepl(targets[0], mergedOptions); } else if (commandParts.length > 0) { const command = commandParts.join(' '); if (command.endsWith('.ts') || command.endsWith('.js')) { await this.executeScript(targets, command, mergedOptions); } else { await this.executeCommand(targets, command, mergedOptions); } } else { if (targets.length === 0) { throw new Error('No targets found'); } if (targets.length > 1) { throw new Error('Interactive mode is only supported for single targets'); } await this.executeInteractive(targets[0], mergedOptions); } } async executeCommand(targets, command, options) { if (options.dryRun) { for (const target of targets) { this.log(`[DRY RUN] Would execute in ${this.formatTargetDisplay(target)}: ${chalk.yellow(command)}`, 'info'); } return; } if (options.parallel && targets.length > 1) { await this.executeParallel(targets, command, options); } else { for (const target of targets) { await this.executeSingle(target, command, options); } } } async executeSingle(target, command, options) { const targetDisplay = this.formatTargetDisplay(target); if (!options.quiet) { this.startSpinner(`Executing in ${targetDisplay}...`); } try { const engine = await this.createTargetEngine(target); if (options.verbose) { console.log(`[DEBUG] Created engine for target type: ${target.type}`); } let execEngine = engine; if (options.env && options.env.length > 0) { const envVars = {}; for (const envVar of options.env) { const [key, value] = envVar.split('='); if (key && value !== undefined) { envVars[key] = value; } } execEngine = execEngine.env(envVars); } if (options.cwd) { execEngine = execEngine.cd(options.cwd); } if (options.timeout) { const timeoutMs = parseTimeout(options.timeout); execEngine = execEngine.timeout(timeoutMs); } if (options.verbose) { console.log(`[DEBUG] Executing command: "${command}"`); } const result = await execEngine.raw `${command}`; if (!options.quiet) { this.stopSpinner(); this.log(`${chalk.green('✓')} ${targetDisplay}`, 'success'); if (result.stdout) { console.log(result.stdout.trim()); } if (result.stderr && options.verbose) { console.error(chalk.yellow(result.stderr.trim())); } } } catch (error) { if (!options.quiet) { this.stopSpinner(); } const errorMessage = error instanceof Error ? error.message : String(error); this.log(`${chalk.red('✗')} ${targetDisplay}: ${errorMessage}`, 'error'); throw error; } } async executeParallel(targets, command, options) { this.log(`Executing on ${targets.length} targets in parallel...`, 'info'); const promises = targets.map(async (target) => { try { await this.executeSingle(target, command, { ...options, quiet: true }); return { target, success: true, error: null }; } catch (error) { return { target, success: false, error }; } }); const results = await Promise.all(promises); const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); if (successful.length > 0) { this.log(`${chalk.green('✓')} Succeeded on ${successful.length} targets:`, 'success'); for (const result of successful) { this.log(` - ${this.formatTargetDisplay(result.target)}`, 'info'); } } if (failed.length > 0) { this.log(`${chalk.red('✗')} Failed on ${failed.length} targets:`, 'error'); for (const result of failed) { const errorMessage = result.error instanceof Error ? result.error.message : String(result.error); this.log(` - ${this.formatTargetDisplay(result.target)}: ${errorMessage}`, 'error'); } throw new Error(`Command failed on ${failed.length} targets`); } } async executeTask(targets, taskName, options) { if (!this.taskManager) { throw new Error('Task manager not initialized'); } for (const target of targets) { const targetDisplay = this.formatTargetDisplay(target); this.log(`Running task '${taskName}' on ${targetDisplay}...`, 'info'); try { const result = await this.taskManager.run(taskName, {}, { target: target.id }); if (result.success) { this.log(`${chalk.green('✓')} Task completed on ${targetDisplay}`, 'success'); } else { throw new Error(result.error?.message || 'Task failed'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`${chalk.red('✗')} Task failed on ${targetDisplay}: ${errorMessage}`, 'error'); throw error; } } } async executeScript(targets, scriptPath, options) { const scriptLoader = new ScriptLoader({ verbose: options.verbose || process.env['XEC_DEBUG'] === 'true', quiet: options.quiet, cache: true, preferredCDN: 'esm.sh' }); for (const target of targets) { const targetDisplay = this.formatTargetDisplay(target); this.log(`Running script '${scriptPath}' on ${targetDisplay}...`, 'info'); try { const engine = await this.createTargetEngine(target); const execOptions = { target, targetEngine: engine, context: { args: process.argv.slice(3), argv: [process.argv[0] || 'node', scriptPath, ...process.argv.slice(3)], __filename: path.resolve(scriptPath), __dirname: path.dirname(path.resolve(scriptPath)) }, verbose: options.verbose, quiet: options.quiet }; const result = await scriptLoader.executeScript(scriptPath, execOptions); if (result.success) { this.log(`${chalk.green('✓')} Script completed on ${targetDisplay}`, 'success'); } else { throw result.error || new Error('Script execution failed'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`${chalk.red('✗')} Script failed on ${targetDisplay}: ${errorMessage}`, 'error'); throw error; } } } async startRepl(target, options) { const targetDisplay = this.formatTargetDisplay(target); this.log(`Starting REPL with $target configured for ${targetDisplay}...`, 'info'); const scriptLoader = new ScriptLoader({ verbose: options.verbose || process.env['XEC_DEBUG'] === 'true', quiet: options.quiet, cache: true, preferredCDN: 'esm.sh' }); const engine = await this.createTargetEngine(target); const execOptions = { target, targetEngine: engine, verbose: options.verbose, quiet: options.quiet }; await scriptLoader.startRepl(execOptions); } async executeInteractive(target, options) { const targetDisplay = this.formatTargetDisplay(target); if (options.dryRun) { this.log(`[DRY RUN] Would start interactive session in ${targetDisplay}`, 'info'); return; } this.log(`Starting interactive session in ${targetDisplay}...`, 'info'); try { let command; if (target.type === 'docker') { const config = target.config; command = ['docker', 'exec', '-it']; if (options.user || config.user) { command.push('-u', options.user || config.user); } if (options.cwd || config.workdir) { command.push('-w', options.cwd || config.workdir); } if (options.env) { for (const envVar of options.env) { command.push('-e', envVar); } } command.push(config.container || target.name || ''); command.push(config.shell || '/bin/sh'); } else if (target.type === 'k8s') { const config = target.config; command = ['kubectl', 'exec', '-it']; command.push('-n', config.namespace || 'default'); if (config.container) { command.push('-c', config.container); } command.push(config.pod || target.name || ''); command.push('--'); command.push(config.shell || '/bin/sh'); } else { throw new Error(`Interactive mode not supported for target type: ${target.type}`); } const result = await $.local().raw `${command.join(' ')}`.interactive(); if (result.exitCode !== 0 && result.exitCode !== 130) { throw new Error(`Interactive session ended with exit code ${result.exitCode}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.log(`${chalk.red('✗')} Failed to start interactive session: ${errorMessage}`, 'error'); throw error; } } async runInteractiveMode(options) { InteractiveHelpers.startInteractiveMode('Interactive Container/Pod Execution'); try { const execType = await InteractiveHelpers.selectFromList('What do you want to do?', [ { value: 'command', label: 'Execute a command' }, { value: 'script', label: 'Run a script file' }, { value: 'task', label: 'Run a configured task' }, { value: 'repl', label: 'Start REPL session' }, { value: 'shell', label: 'Interactive shell' }, ], (item) => item.label); if (!execType) return null; const allowMultiple = execType.value !== 'repl' && execType.value !== 'shell'; const targets = await InteractiveHelpers.selectTarget({ message: allowMultiple ? 'Select target(s):' : 'Select target:', type: 'all', allowMultiple, allowCustom: true, }); if (!targets) return null; const targetPattern = Array.isArray(targets) ? targets.map(t => t.id).join(' ') : targets.id; const inOptions = {}; let commandParts = []; switch (execType.value) { case 'command': { const command = await InteractiveHelpers.inputText('Enter command to execute:', { placeholder: 'ls -la, npm test, date, etc.', validate: (value) => { if (!value || value.trim().length === 0) { return 'Command cannot be empty'; } return undefined; }, }); if (!command) return null; commandParts = [command]; if (Array.isArray(targets) && targets.length > 1) { inOptions.parallel = await InteractiveHelpers.confirmAction('Execute in parallel?', false); } const configureEnv = await InteractiveHelpers.confirmAction('Set environment variables?', false); if (configureEnv) { const envVars = []; let addMore = true; while (addMore) { const envVar = await InteractiveHelpers.inputText('Enter environment variable:', { placeholder: 'KEY=value', validate: (value) => { if (value && !value.includes('=')) { return 'Format must be KEY=value'; } return undefined; }, }); if (envVar) { envVars.push(envVar); } addMore = envVar ? await InteractiveHelpers.confirmAction('Add another variable?', false) : false; } if (envVars.length > 0) { inOptions.env = envVars; } } const configureCwd = await InteractiveHelpers.confirmAction('Set working directory?', false); if (configureCwd) { const cwd = await InteractiveHelpers.inputText('Enter working directory:', { placeholder: '/app, /home/user, etc.', }); if (cwd) { inOptions.cwd = cwd; } } const configureTimeout = await InteractiveHelpers.confirmAction('Set command timeout?', false); if (configureTimeout) { const timeout = await InteractiveHelpers.inputText('Enter timeout:', { placeholder: '30s, 5m, 1h', validate: (value) => { try { if (value) parseTimeout(value); return undefined; } catch { return 'Invalid timeout format (use 30s, 5m, etc.)'; } }, }); if (timeout) { inOptions.timeout = timeout; } } break; } case 'script': { const scriptPath = await InteractiveHelpers.inputText('Enter script path:', { placeholder: './deploy.js, /scripts/test.ts', validate: (value) => { if (!value || value.trim().length === 0) { return 'Script path cannot be empty'; } if (!value.endsWith('.js') && !value.endsWith('.ts')) { return 'Script must be a .js or .ts file'; } return undefined; }, }); if (!scriptPath) return null; commandParts = [scriptPath]; break; } case 'task': { const taskName = await InteractiveHelpers.inputText('Enter task name:', { placeholder: 'test, build, deploy', validate: (value) => { if (!value || value.trim().length === 0) { return 'Task name cannot be empty'; } return undefined; }, }); if (!taskName) return null; inOptions.task = taskName; break; } case 'repl': { inOptions.repl = true; break; } case 'shell': { inOptions.interactive = true; commandParts = []; break; } } InteractiveHelpers.showInfo('\nExecution Summary:'); console.log(` Target(s): ${chalk.cyan(targetPattern)}`); if (commandParts.length > 0) { console.log(` Command: ${chalk.cyan(commandParts.join(' '))}`); } if (inOptions.task) { console.log(` Task: ${chalk.cyan(inOptions.task)}`); } if (inOptions.repl) { console.log(` Mode: ${chalk.gray('REPL session')}`); } if (inOptions.interactive && execType.value === 'shell') { console.log(` Mode: ${chalk.gray('Interactive shell')}`); } if (inOptions.parallel) { console.log(` Execution: ${chalk.gray('parallel')}`); } if (inOptions.env) { console.log(` Environment: ${chalk.gray(inOptions.env.join(', '))}`); } if (inOptions.cwd) { console.log(` Working directory: ${chalk.gray(inOptions.cwd)}`); } if (inOptions.timeout) { console.log(` Timeout: ${chalk.gray(inOptions.timeout)}`); } const confirm = await InteractiveHelpers.confirmAction('\nProceed with execution?', true); if (!confirm) { InteractiveHelpers.endInteractiveMode('Execution cancelled'); return null; } return { targetPattern, commandParts, options: inOptions, }; } catch (error) { if (error instanceof Error && error.message.includes('cancelled')) { InteractiveHelpers.endInteractiveMode('Execution cancelled'); } else { InteractiveHelpers.showError(error instanceof Error ? error.message : String(error)); } return null; } } } export default function command(program) { const cmd = new InCommand(); program.addCommand(cmd.create()); } //# sourceMappingURL=in.js.map