UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

278 lines 10.5 kB
export class TaskParser { constructor() { this.errors = []; } parseTask(taskName, config) { this.errors = []; if (typeof config === 'string') { return { command: config, description: `Execute: ${config}`, }; } const task = config; this.validateTaskDefinition(taskName, task); if (this.errors.length > 0) { return null; } return task; } parseTasks(tasks) { const parsed = {}; const allErrors = []; for (const [name, config] of Object.entries(tasks)) { const task = this.parseTask(name, config); if (task) { parsed[name] = task; } else { allErrors.push(...this.errors); } } if (allErrors.length > 0) { throw new TaskParseError('Failed to parse tasks', allErrors); } return parsed; } validateTaskDefinition(name, task) { const path = `tasks.${name}`; if (!task.command && !task.steps && !task.script) { this.addError(path, 'Task must have either command, steps, or script'); } if (task.command && task.steps) { this.addError(path, 'Task cannot have both command and steps'); } if (task.params) { this.validateParameters(path, task.params); } if (task.steps) { this.validateSteps(path, task.steps); } if (task.parallel && task.maxConcurrent !== undefined) { if (task.maxConcurrent < 1) { this.addError(`${path}.maxConcurrent`, 'Must be at least 1'); } } if (task.cache) { if (!task.cache.key) { this.addError(`${path}.cache`, 'Cache key is required'); } if (task.cache.ttl !== undefined && task.cache.ttl < 0) { this.addError(`${path}.cache.ttl`, 'TTL must be positive'); } } if (task.timeout !== undefined) { const timeout = this.parseTimeout(task.timeout); if (timeout < 0) { this.addError(`${path}.timeout`, 'Timeout must be positive'); } } } validateParameters(path, params) { const names = new Set(); params.forEach((param, index) => { const paramPath = `${path}.params[${index}]`; if (names.has(param.name)) { this.addError(paramPath, `Duplicate parameter name: ${param.name}`); } names.add(param.name); if (param.type) { const validTypes = ['string', 'number', 'boolean', 'array', 'enum']; if (!validTypes.includes(param.type)) { this.addError(`${paramPath}.type`, `Invalid type: ${param.type}`); } } if (param.type === 'enum' && !param.values) { this.addError(paramPath, 'Enum type requires values array'); } if (param.pattern && param.type && param.type !== 'string') { this.addError(paramPath, 'Pattern can only be used with string type'); } if ((param.min !== undefined || param.max !== undefined) && param.type !== 'number') { this.addError(paramPath, 'Min/max can only be used with number type'); } if ((param.minItems !== undefined || param.maxItems !== undefined) && param.type !== 'array') { this.addError(paramPath, 'minItems/maxItems can only be used with array type'); } if (param.default !== undefined && param.type) { this.validateDefaultValue(paramPath, param); } }); } validateSteps(path, steps) { steps.forEach((step, index) => { const stepPath = `${path}.steps[${index}]`; if (!step.command && !step.task && !step.script) { this.addError(stepPath, 'Step must have command, task, or script'); } const execTypes = [step.command, step.task, step.script].filter(Boolean).length; if (execTypes > 1) { this.addError(stepPath, 'Step can only have one of: command, task, or script'); } if (step.target && step.targets) { this.addError(stepPath, 'Step cannot have both target and targets'); } if (step.onFailure && typeof step.onFailure === 'object') { const handler = step.onFailure; if (handler.retry !== undefined && handler.retry < 0) { this.addError(`${stepPath}.onFailure.retry`, 'Retry count must be positive'); } } if (step.when) { if (step.when.trim() === '') { this.addError(`${stepPath}.when`, 'Condition cannot be empty'); } } }); } validateDefaultValue(path, param) { const { type, default: defaultValue } = param; switch (type) { case 'string': if (typeof defaultValue !== 'string') { this.addError(`${path}.default`, 'Default must be a string'); } break; case 'number': if (typeof defaultValue !== 'number') { this.addError(`${path}.default`, 'Default must be a number'); } break; case 'boolean': if (typeof defaultValue !== 'boolean') { this.addError(`${path}.default`, 'Default must be a boolean'); } break; case 'array': if (!Array.isArray(defaultValue)) { this.addError(`${path}.default`, 'Default must be an array'); } break; case 'enum': if (param.values && !param.values.includes(defaultValue)) { this.addError(`${path}.default`, 'Default must be one of the allowed values'); } break; } } parseTimeout(timeout) { if (typeof timeout === 'number') { return timeout; } const match = timeout.match(/^(\d+)(ms|s|m|h)?$/); if (!match) { return -1; } 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 -1; } } addError(path, message, value) { this.errors.push({ path, message, value }); } getErrors() { return [...this.errors]; } validateParams(task, providedParams) { const errors = []; if (!task.params) { return errors; } for (const param of task.params) { if (param.required && !(param.name in providedParams)) { errors.push(`Required parameter '${param.name}' is missing`); } if (param.name in providedParams) { const value = providedParams[param.name]; if (param.type === 'number' && typeof value !== 'number' && typeof value !== 'string') { errors.push(`Parameter '${param.name}' must be a number`); } if (param.type === 'boolean' && typeof value !== 'boolean' && typeof value !== 'string') { errors.push(`Parameter '${param.name}' must be a boolean`); } if (param.type === 'array' && !Array.isArray(value) && typeof value !== 'string') { errors.push(`Parameter '${param.name}' must be an array`); } if (param.type === 'enum' && param.values) { const strValue = String(value); if (!param.values.includes(strValue)) { errors.push(`Parameter '${param.name}' must be one of: ${param.values.join(', ')}`); } } } } return errors; } parseParams(task, providedParams) { const parsed = {}; if (!task.params) { return providedParams; } for (const param of task.params) { if (!(param.name in providedParams) && param.default !== undefined) { parsed[param.name] = param.default; } } for (const [name, value] of Object.entries(providedParams)) { const param = task.params.find(p => p.name === name); if (!param || !param.type) { parsed[name] = value; continue; } switch (param.type) { case 'number': if (typeof value === 'string') { parsed[name] = parseFloat(value); } else { parsed[name] = value; } break; case 'boolean': if (typeof value === 'string') { parsed[name] = value === 'true' || value === '1' || value === 'yes'; } else { parsed[name] = !!value; } break; case 'array': if (typeof value === 'string') { parsed[name] = value.split(',').map(v => v.trim()); } else if (Array.isArray(value)) { parsed[name] = value; } else { parsed[name] = [value]; } break; default: parsed[name] = value; } } return parsed; } } export class TaskParseError extends Error { constructor(message, errors) { super(message); this.errors = errors; this.name = 'TaskParseError'; } } export function createTaskParser() { return new TaskParser(); } //# sourceMappingURL=task-parser.js.map