@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
455 lines • 18.3 kB
JavaScript
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