@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
302 lines • 12.1 kB
JavaScript
import { EventEmitter } from 'events';
import { TaskParser } from './task-parser.js';
import { TargetResolver } from './target-resolver.js';
import { VariableInterpolator } from './variable-interpolator.js';
import { TaskExecutor } from './task-executor.js';
export class TaskManager extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.parsedTasks = new Map();
this.config = null;
this.configManager = options.configManager;
this.parser = new TaskParser();
this.interpolator = new VariableInterpolator();
this.targetResolver = null;
this.executor = new TaskExecutor({
interpolator: this.interpolator,
targetResolver: this.targetResolver,
defaultTimeout: options.defaultTimeout,
debug: options.debug,
dryRun: options.dryRun,
});
this.executor.on('task:start', event => this.emit('task:start', event));
this.executor.on('task:complete', event => this.emit('task:complete', event));
this.executor.on('task:error', event => this.emit('task:error', event));
this.executor.on('step:retry', event => this.emit('step:retry', event));
this.executor.on('event', event => this.emit('event', event));
}
async load() {
this.config = await this.configManager.load();
this.targetResolver = new TargetResolver(this.config);
this.executor = new TaskExecutor({
interpolator: this.interpolator,
targetResolver: this.targetResolver,
defaultTimeout: this.executor.options.defaultTimeout,
debug: this.executor.options.debug || this.options.debug,
dryRun: this.executor.options.dryRun
});
this.executor.on('task:start', event => this.emit('task:start', event));
this.executor.on('task:complete', event => this.emit('task:complete', event));
this.executor.on('task:error', event => this.emit('task:error', event));
this.executor.on('step:retry', event => this.emit('step:retry', event));
this.executor.on('event', event => this.emit('event', event));
if (!this.config.tasks) {
return;
}
const parsed = this.parser.parseTasks(this.config.tasks);
for (const [name, task] of Object.entries(parsed)) {
this.parsedTasks.set(name, task);
}
}
async list() {
await this.ensureLoaded();
const tasks = [];
for (const [name, task] of this.parsedTasks) {
if (task.private && !this.options.debug) {
continue;
}
tasks.push({
name,
description: task.description,
params: task.params,
isPrivate: task.private,
hasSteps: !!task.steps,
hasCommand: !!task.command,
hasScript: !!task.script,
target: task.target,
targets: task.targets,
});
}
return tasks.sort((a, b) => a.name.localeCompare(b.name));
}
async get(taskName) {
await this.ensureLoaded();
return this.parsedTasks.get(taskName) || null;
}
async exists(taskName) {
await this.ensureLoaded();
return this.parsedTasks.has(taskName);
}
async run(taskName, params, options) {
await this.ensureLoaded();
const task = this.parsedTasks.get(taskName);
if (!task) {
throw new Error(`Task '${taskName}' not found`);
}
let finalParams = params || {};
if (task.params) {
this.validateParameters(taskName, task.params, finalParams);
finalParams = this.applyParameterDefaults(task.params, finalParams);
}
return this.executor.execute(taskName, task, {
...options,
params: finalParams,
vars: this.config?.vars || {},
});
}
async runOnTarget(taskName, target, params, options) {
return this.run(taskName, params, {
...options,
target,
});
}
async create(taskName, config) {
const task = this.parser.parseTask(taskName, config);
if (!task) {
const errors = this.parser.getErrors();
throw new Error(`Invalid task configuration: ${errors[0]?.message}`);
}
this.parsedTasks.set(taskName, task);
if (!this.config) {
await this.load();
}
const currentConfig = this.config;
currentConfig.tasks = currentConfig.tasks || {};
currentConfig.tasks[taskName] = config;
await this.configManager.save();
}
async update(taskName, config) {
if (!this.parsedTasks.has(taskName)) {
throw new Error(`Task '${taskName}' not found`);
}
await this.create(taskName, config);
}
async delete(taskName) {
if (!this.parsedTasks.has(taskName)) {
throw new Error(`Task '${taskName}' not found`);
}
this.parsedTasks.delete(taskName);
if (!this.config) {
await this.load();
}
const currentConfig = this.config;
if (currentConfig.tasks) {
delete currentConfig.tasks[taskName];
}
await this.configManager.save();
}
async explain(taskName, params) {
await this.ensureLoaded();
const task = this.parsedTasks.get(taskName);
if (!task) {
throw new Error(`Task '${taskName}' not found`);
}
const explanation = [];
if (task.description) {
explanation.push(`Task: ${task.description}`);
}
else {
explanation.push(`Task: ${taskName}`);
}
if (task.params && task.params.length > 0) {
explanation.push('');
explanation.push('Parameters:');
for (const param of task.params) {
const value = params?.[param.name] ?? param.default;
const required = param.required ? ' (required)' : '';
explanation.push(` ${param.name}: ${value}${required}`);
if (param.description) {
explanation.push(` ${param.description}`);
}
}
}
explanation.push('');
explanation.push('Execution plan:');
if (task.command) {
const interpolated = this.interpolator.interpolate(task.command, {
params: params || {},
vars: await this.configManager.get('vars') || {},
});
explanation.push(` Execute: ${interpolated}`);
}
else if (task.steps) {
const parallel = task.parallel ? ' (in parallel)' : '';
explanation.push(` Execute ${task.steps.length} steps${parallel}:`);
for (let i = 0; i < task.steps.length; i++) {
const step = task.steps[i];
if (!step)
continue;
const prefix = ` ${i + 1}. `;
if (step.command) {
const interpolated = this.interpolator.interpolate(step.command, {
params: params || {},
vars: await this.configManager.get('vars') || {},
});
explanation.push(`${prefix}${step.name || 'Command'}: ${interpolated}`);
}
else if (step.task) {
explanation.push(`${prefix}${step.name || 'Task'}: Run task '${step.task}'`);
}
else if (step.script) {
explanation.push(`${prefix}${step.name || 'Script'}: Execute ${step.script}`);
}
if (step.when) {
explanation.push(` When: ${step.when}`);
}
if (step.target || step.targets) {
const targets = step.targets || [step.target];
explanation.push(` On: ${targets.join(', ')}`);
}
}
}
else if (task.script) {
explanation.push(` Execute script: ${task.script}`);
}
if (task.target || task.targets) {
explanation.push('');
const targets = task.targets || [task.target];
explanation.push(`Target${targets.length > 1 ? 's' : ''}: ${targets.join(', ')}`);
}
if (task.timeout) {
explanation.push('');
explanation.push(`Timeout: ${task.timeout}`);
}
if (task.cache) {
explanation.push('');
explanation.push(`Cached with key: ${task.cache.key}`);
}
return explanation;
}
applyParameterDefaults(params, provided) {
const result = { ...provided };
for (const param of params) {
if (!(param.name in result) && param.default !== undefined) {
result[param.name] = param.default;
}
}
return result;
}
validateParameters(taskName, params, provided) {
const errors = [];
for (const param of params) {
const value = provided[param.name];
if (param.required && value === undefined) {
errors.push(`Missing required parameter: ${param.name}`);
continue;
}
if (value === undefined) {
continue;
}
if (param.type) {
const valid = this.validateParameterType(param, value);
if (!valid) {
errors.push(`Invalid type for parameter '${param.name}': expected ${param.type}`);
}
}
if (param.pattern && typeof value === 'string') {
const regex = new RegExp(param.pattern);
if (!regex.test(value)) {
errors.push(`Parameter '${param.name}' does not match pattern: ${param.pattern}`);
}
}
if (param.values && !param.values.includes(value)) {
errors.push(`Parameter '${param.name}' must be one of: ${param.values.join(', ')}`);
}
if (typeof value === 'number') {
if (param.min !== undefined && value < param.min) {
errors.push(`Parameter '${param.name}' must be at least ${param.min}`);
}
if (param.max !== undefined && value > param.max) {
errors.push(`Parameter '${param.name}' must be at most ${param.max}`);
}
}
if (Array.isArray(value)) {
if (param.minItems !== undefined && value.length < param.minItems) {
errors.push(`Parameter '${param.name}' must have at least ${param.minItems} items`);
}
if (param.maxItems !== undefined && value.length > param.maxItems) {
errors.push(`Parameter '${param.name}' must have at most ${param.maxItems} items`);
}
}
}
if (errors.length > 0) {
throw new Error(`Task '${taskName}' validation failed:\n${errors.join('\n')}`);
}
}
validateParameterType(param, value) {
switch (param.type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'enum':
return param.values?.includes(value) ?? false;
default:
return true;
}
}
async ensureLoaded() {
if (this.parsedTasks.size === 0) {
await this.load();
}
}
clearCache() {
this.parsedTasks.clear();
}
}
//# sourceMappingURL=task-manager.js.map