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