UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

455 lines 18.3 kB
import * as path from 'path'; import { $ } from '@xec-sh/core'; import * as fs from 'fs/promises'; import { EventEmitter } from 'events'; import { getUnifiedScriptLoader } from '../utils/script-loader.js'; export class TaskExecutor extends EventEmitter { constructor(options) { super(); this.options = options; } async execute(taskName, task, options = {}) { const startTime = Date.now(); const result = { task: taskName, success: false, duration: 0, steps: [], }; const context = { params: options.params || {}, vars: options.vars || {}, env: Object.fromEntries(Object.entries({ ...process.env, ...options.env }) .filter(([_, v]) => v !== undefined) .map(([k, v]) => [k, v])), }; try { this.emit('task:start', { task: taskName, definition: task }); if (task.command) { const execResult = await this.executeCommand(task, context, options); result.output = execResult.stdout; result.success = true; } else if (task.steps) { const stepResults = await this.executePipeline(task, context, options); result.steps = stepResults; const hasUnhandledFailure = stepResults.some(step => { if (step.success) return false; const taskStep = task.steps?.find(s => s.name === step.name); const handler = taskStep?.onFailure; return !handler || (handler !== 'continue' && handler !== 'ignore'); }); const wasAborted = stepResults.length < task.steps.length; result.success = !wasAborted && !hasUnhandledFailure; } else if (task.script) { const execResult = await this.executeScript(task, context, options); result.output = execResult.stdout; result.success = true; } result.duration = Date.now() - startTime; this.emit('task:complete', { task: taskName, result }); return result; } catch (error) { result.duration = Date.now() - startTime; result.error = error; this.emit('task:error', { task: taskName, error, result }); if (task.onError) { await this.handleTaskError(task.onError, error, context); } return result; } } async executeCommand(task, context, options) { const command = this.options.interpolator.interpolate(task.command, context); if (this.options.dryRun) { console.log(`[DRY RUN] Would execute: ${command}`); return { stdout: '', stderr: '', exitCode: 0, ok: true }; } const targetRef = options.target || task.target; const target = targetRef ? await this.resolveTarget(targetRef, context) : null; const engine = target ? await this.createTargetEngine(target) : $; const cmdOptions = { env: context.env, cwd: options.cwd || task.workdir, timeout: this.getTimeout(task.timeout, options.timeout), }; const result = await engine.raw `${command}`.env(cmdOptions.env || {}).cwd(cmdOptions.cwd || process.cwd()).timeout(cmdOptions.timeout || 60000).nothrow(); if (!options.quiet) { if (result.stdout) console.log(result.stdout); if (result.stderr) console.error(result.stderr); } if (!result.ok) { throw new Error(`Command failed with exit code ${result.exitCode}`); } return result; } async executePipeline(task, context, options) { const steps = task.steps; const results = []; const parallel = task.parallel || false; const maxConcurrent = task.maxConcurrent || steps.length; const failFast = task.failFast !== false; if (parallel) { return this.executeStepsParallel(steps, context, options, maxConcurrent, failFast); } else { return this.executeStepsSequential(steps, context, options, failFast); } } async executeStepsSequential(steps, context, options, failFast) { const results = []; const stepContext = { ...context }; for (const step of steps) { const stepResult = await this.executeStep(step, stepContext, options); results.push(stepResult); if (step.register && stepResult.output) { stepContext.vars = { ...stepContext.vars, [step.register]: stepResult.output.trim(), }; } if (!stepResult.success && !step.alwaysRun) { const shouldContinue = step.onFailure === 'continue' || step.onFailure === 'ignore'; if (failFast && !shouldContinue) { break; } } } return results; } async executeStepsParallel(steps, context, options, maxConcurrent, failFast) { const results = []; const queue = [...steps]; const executing = new Set(); while (queue.length > 0 || executing.size > 0) { while (queue.length > 0 && executing.size < maxConcurrent) { const step = queue.shift(); const promise = this.executeStep(step, context, options); executing.add(promise); promise.then(result => { results.push(result); executing.delete(promise); if (!result.success && failFast) { queue.length = 0; } }).catch(error => { executing.delete(promise); if (failFast) { queue.length = 0; } }); } if (executing.size > 0) { await Promise.race(executing); } } await Promise.all(executing); return results; } async executeStep(step, context, options) { const startTime = Date.now(); const result = { name: step.name, success: false, duration: 0, }; try { if (step.when && !await this.evaluateCondition(step.when, context)) { result.success = true; result.duration = Date.now() - startTime; return result; } if (step.command) { const output = await this.executeStepCommand(step, context, options); result.output = output.stdout; if (!output.ok) { throw new Error(`Step command failed with exit code ${output.exitCode}`); } result.success = true; } else if (step.task) { const nestedResult = await this.executeStepCommand({ ...step, command: `echo "Would execute task: ${step.task}"` }, context, options); result.output = nestedResult.stdout; if (!nestedResult.ok) { throw new Error(`Step task failed with exit code ${nestedResult.exitCode}`); } result.success = true; } else if (step.script) { const output = await this.executeStepScript(step, context, options); result.output = output.stdout; if (!output.ok) { throw new Error(`Step script failed with exit code ${output.exitCode}`); } result.success = true; } result.duration = Date.now() - startTime; return result; } catch (error) { result.error = error; result.duration = Date.now() - startTime; if (step.onFailure) { const retryResult = await this.handleStepError(step, error, context, options, result); if (retryResult) { return retryResult; } } result.success = false; return result; } } async executeStepCommand(step, context, options) { const command = this.options.interpolator.interpolate(step.command, context); if (this.options.dryRun) { console.log(`[DRY RUN] Step: ${step.name || 'unnamed'} - Would execute: ${command}`); return { stdout: '', stderr: '', exitCode: 0, ok: true }; } if (step.targets && step.targets.length > 0) { const results = await Promise.all(step.targets.map(targetRef => this.executeOnTarget(command, targetRef, context, options))); return { stdout: results.map(r => r.stdout).join('\n'), stderr: results.map(r => r.stderr).join('\n'), exitCode: results.every(r => r.ok) ? 0 : 1, ok: results.every(r => r.ok), }; } const targetRef = step.target || options.target; return this.executeOnTarget(command, targetRef, context, options); } async executeOnTarget(command, targetRef, context, options) { const target = targetRef ? await this.resolveTarget(targetRef, context) : null; const engine = target ? await this.createTargetEngine(target) : $; const cmdOptions = { env: context.env, cwd: options.cwd, timeout: options.timeout, }; return engine.raw `${command}`.env(cmdOptions.env || {}).cwd(cmdOptions.cwd || process.cwd()).timeout(cmdOptions.timeout || 60000).nothrow(); } async executeStepScript(step, context, options) { const scriptPath = this.options.interpolator.interpolate(step.script, context); try { await fs.access(scriptPath); } catch (error) { throw new Error(`Script file not found: ${scriptPath}`); } const targetRef = step.target || options.target; const target = targetRef ? await this.resolveTarget(targetRef, context) : null; const targetEngine = target ? await this.createTargetEngine(target) : null; const scriptLoader = getUnifiedScriptLoader({ verbose: this.options.debug, quiet: options.quiet, }); const result = await scriptLoader.executeScript(scriptPath, { target: target || undefined, targetEngine: targetEngine || undefined, context: { args: [], argv: [process.argv[0] || 'node', scriptPath], __filename: scriptPath, __dirname: path.dirname(scriptPath), }, quiet: options.quiet, }); if (!result.success) { const errorMessage = result.error?.message || 'Script execution failed'; throw new Error(errorMessage); } return { stdout: result.output || '', stderr: '', exitCode: 0, ok: true, }; } async executeScript(task, context, options) { const scriptPath = this.options.interpolator.interpolate(task.script, context); try { await fs.access(scriptPath); } catch (error) { throw new Error(`Script file not found: ${scriptPath}`); } const targetRef = options.target || task.target; const target = targetRef ? await this.resolveTarget(targetRef, context) : null; const targetEngine = target ? await this.createTargetEngine(target) : null; const scriptLoader = getUnifiedScriptLoader({ verbose: this.options.debug, quiet: options.quiet, }); const result = await scriptLoader.executeScript(scriptPath, { target: target || undefined, targetEngine: targetEngine || undefined, context: { args: [], argv: [process.argv[0] || 'node', scriptPath], __filename: scriptPath, __dirname: path.dirname(scriptPath), }, quiet: options.quiet, }); if (!result.success) { const errorMessage = result.error?.message || 'Script execution failed'; throw new Error(errorMessage); } return { stdout: result.output || '', stderr: '', exitCode: 0, ok: true, }; } async handleStepError(step, error, context, options, originalResult) { const handler = step.onFailure; if (!handler) { return null; } if (handler === 'continue' || handler === 'ignore') { return null; } if (handler === 'abort') { return null; } if (typeof handler === 'object' && handler.retry) { const retryHandler = handler; const retries = retryHandler.retry || 0; const delay = this.parseDelay(retryHandler.delay || '1s'); for (let i = 0; i < retries; i++) { this.emit('step:retry', { step, attempt: i + 1, maxAttempts: retries }); await new Promise(resolve => setTimeout(resolve, delay)); try { const result = await this.executeStepCommand(step, context, options); if (result.ok) { return { name: step.name, success: true, output: result.stdout, duration: originalResult.duration, }; } } catch (retryError) { } } } return null; } async handleTaskError(handler, error, context) { if (handler.emit) { this.emit('event', { name: handler.emit, data: { error: error.message } }); } if (handler.command) { const command = this.options.interpolator.interpolate(handler.command, context); await $.raw `${command}`.shell(true).nothrow(); } } async evaluateCondition(condition, context) { const interpolated = this.options.interpolator.interpolate(condition, context); return interpolated === 'true' || interpolated === '1'; } async resolveTarget(targetRef, context) { const interpolated = this.options.interpolator.interpolate(targetRef, context); return this.options.targetResolver.resolve(interpolated); } async createTargetEngine(target) { const config = target.config; switch (target.type) { case 'ssh': { const sshEngine = $.ssh({ host: config.host, username: config.user, port: config.port, privateKey: config.privateKey, password: config.password, passphrase: config.passphrase, ...config }); 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, ...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); } case 'local': default: return $.local(); } } getTimeout(taskTimeout, optionTimeout) { if (optionTimeout !== undefined) { return optionTimeout; } if (taskTimeout !== undefined) { return typeof taskTimeout === 'number' ? taskTimeout : this.parseTimeout(taskTimeout); } return this.options.defaultTimeout; } parseTimeout(timeout) { const match = timeout.match(/^(\d+)(ms|s|m|h)?$/); if (!match) { return 0; } const value = parseInt(match[1] || '0', 10); const unit = match[2] || 'ms'; switch (unit) { case 'ms': return value; case 's': return value * 1000; case 'm': return value * 60 * 1000; case 'h': return value * 60 * 60 * 1000; default: return 0; } } parseDelay(delay) { return this.parseTimeout(delay); } } //# sourceMappingURL=task-executor.js.map