UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

326 lines 12.9 kB
export class ConfigValidator { constructor() { this.errors = []; } async validate(config) { this.errors = []; this.validateVersion(config.version); if (config.vars) { this.validateVars(config.vars, 'vars'); } if (config.targets) { this.validateTargets(config.targets); } if (config.profiles) { this.validateProfiles(config.profiles); } if (config.tasks) { this.validateTasks(config.tasks); } if (config.scripts) { this.validateScripts(config.scripts); } if (config.commands) { this.validateCommands(config.commands); } if (config.secrets) { this.validateSecrets(config.secrets); } if (config.extensions) { this.validateExtensions(config.extensions); } return this.errors; } validateVersion(version) { if (!version) { this.addError('version', 'Version is required'); return; } if (!/^\d+\.\d+$/.test(version)) { this.addError('version', `Invalid version format: ${version}. Expected: major.minor (e.g., 1.0)`); } const versionParts = version.split('.').map(Number); const major = versionParts[0]; if (!major || major < 1) { this.addError('version', `Version ${version} is not supported. Minimum version: 1.0`); } } validateVars(vars, path) { for (const [key, value] of Object.entries(vars)) { const varPath = `${path}.${key}`; if (['env', 'params', 'cmd', 'secret'].includes(key)) { this.addError(varPath, `Variable name '${key}' is reserved`); } if (typeof value === 'string' && value.includes(`\${vars.${key}}`)) { this.addError(varPath, 'Circular reference detected'); } if (value && typeof value === 'object' && !Array.isArray(value)) { this.validateVars(value, varPath); } } } validateTargets(targets) { if (targets.hosts) { for (const [name, config] of Object.entries(targets.hosts)) { this.validateHost(config, `targets.hosts.${name}`); } } if (targets.containers) { for (const [name, config] of Object.entries(targets.containers)) { this.validateContainer(config, `targets.containers.${name}`); } } if (targets.pods) { for (const [name, config] of Object.entries(targets.pods)) { this.validatePod(config, `targets.pods.${name}`); } } } validateHost(config, path) { if (!config.host) { this.addError(path, 'Host is required for SSH target'); } if (config.port && (typeof config.port !== 'number' || config.port < 1 || config.port > 65535)) { this.addError(`${path}.port`, 'Port must be a number between 1 and 65535'); } if (config.privateKey && config.password) { this.addWarning(path, 'Both privateKey and password specified. privateKey will take precedence'); } } validateContainer(config, path) { if (!config.image && !config.container) { this.addError(path, 'Either image or container must be specified'); } if (config.volumes && !Array.isArray(config.volumes)) { this.addError(`${path}.volumes`, 'Volumes must be an array'); } if (config.ports && !Array.isArray(config.ports)) { this.addError(`${path}.ports`, 'Ports must be an array'); } } validatePod(config, path) { if (!config.pod && !config.selector) { this.addError(path, 'Either pod or selector must be specified'); } if (config.pod && config.selector) { this.addWarning(path, 'Both pod and selector specified. pod will take precedence'); } } validateProfiles(profiles) { for (const [name, profile] of Object.entries(profiles)) { const path = `profiles.${name}`; if (profile.extends) { if (!profiles[profile.extends]) { this.addError(`${path}.extends`, `Extended profile '${profile.extends}' not found`); } if (this.hasCircularInheritance(name, profiles)) { this.addError(path, 'Circular profile inheritance detected'); } } if (profile.vars) { this.validateVars(profile.vars, `${path}.vars`); } if (profile.targets) { this.validateTargets(profile.targets); } } } validateTasks(tasks) { const taskNames = new Set(Object.keys(tasks)); for (const [name, task] of Object.entries(tasks)) { const path = `tasks.${name}`; if (typeof task === 'string') { continue; } const taskDef = task; if (!taskDef.command && !taskDef.steps && !taskDef.script) { this.addError(path, 'Task must have either command, steps, or script'); } if (taskDef.command && taskDef.steps) { this.addError(path, 'Task cannot have both command and steps'); } if (taskDef.params) { this.validateTaskParameters(taskDef.params, `${path}.params`); } if (taskDef.steps) { this.validateTaskSteps(taskDef.steps, `${path}.steps`, taskNames); } if (taskDef.target) { this.validateTargetReference(taskDef.target, `${path}.target`); } if (taskDef.targets) { for (let i = 0; i < taskDef.targets.length; i++) { const target = taskDef.targets[i]; if (target !== undefined) { this.validateTargetReference(target, `${path}.targets[${i}]`); } } } if (taskDef.timeout) { this.validateTimeout(taskDef.timeout, `${path}.timeout`); } if (taskDef.dependsOn) { for (const dep of taskDef.dependsOn) { if (!taskNames.has(dep)) { this.addError(`${path}.dependsOn`, `Dependency task '${dep}' not found`); } } } if (taskDef.template && !tasks[taskDef.template]) { this.addError(`${path}.template`, `Template task '${taskDef.template}' not found`); } } } validateTaskParameters(params, path) { const names = new Set(); for (let i = 0; i < params.length; i++) { const param = params[i]; if (!param) continue; const paramPath = `${path}[${i}]`; if (!param.name) { this.addError(`${paramPath}.name`, 'Parameter name is required'); continue; } if (names.has(param.name)) { this.addError(paramPath, `Duplicate parameter name: ${param.name}`); } names.add(param.name); if (param.type && !['string', 'number', 'boolean', 'array', 'enum'].includes(param.type)) { this.addError(`${paramPath}.type`, `Invalid parameter type: ${param.type}`); } if (param.type === 'enum' && !param.values) { this.addError(paramPath, 'Enum parameter must have values'); } if (param.pattern) { try { new RegExp(param.pattern); } catch { this.addError(`${paramPath}.pattern`, 'Invalid regular expression'); } } if (param.min !== undefined && param.max !== undefined && param.min > param.max) { this.addError(paramPath, 'min cannot be greater than max'); } } } validateTaskSteps(steps, path, taskNames) { for (let i = 0; i < steps.length; i++) { const step = steps[i]; const stepPath = `${path}[${i}]`; if (!step.command && !step.task && !step.script) { this.addError(stepPath, 'Step must have either command, task, or script'); } if (step.task && !taskNames.has(step.task)) { this.addError(`${stepPath}.task`, `Task '${step.task}' not found`); } if (step.target) { this.validateTargetReference(step.target, `${stepPath}.target`); } if (step.targets) { for (let j = 0; j < step.targets.length; j++) { const targetRef = step.targets[j]; if (targetRef) { this.validateTargetReference(targetRef, `${stepPath}.targets[${j}]`); } } } } } validateTargetReference(ref, path) { const validPrefixes = ['hosts.', 'containers.', 'pods.', 'local']; const hasValidPrefix = validPrefixes.some(prefix => ref === 'local' || ref.startsWith(prefix)); if (!hasValidPrefix) { this.addWarning(path, `Target reference '${ref}' may not be valid. Expected format: hosts.name, containers.name, pods.name, or local`); } } validateTimeout(timeout, path) { if (typeof timeout === 'number') { if (timeout < 0) { this.addError(path, 'Timeout must be positive'); } } else if (typeof timeout === 'string') { if (!/^\d+[smh]$/.test(timeout)) { this.addError(path, 'Invalid timeout format. Use number (ms) or duration string (e.g., 30s, 5m, 1h)'); } } } validateScripts(scripts) { if (scripts.sandbox) { const sandbox = scripts.sandbox; if (sandbox.restrictions && !Array.isArray(sandbox.restrictions)) { this.addError('scripts.sandbox.restrictions', 'Restrictions must be an array'); } if (sandbox.memoryLimit && !/^\d+[KMG]B?$/i.test(sandbox.memoryLimit)) { this.addError('scripts.sandbox.memoryLimit', 'Invalid memory limit format'); } if (sandbox.timeout) { this.validateTimeout(sandbox.timeout, 'scripts.sandbox.timeout'); } } } validateCommands(commands) { for (const [cmd, config] of Object.entries(commands)) { const path = `commands.${cmd}`; if (config.defaultTimeout) { this.validateTimeout(config.defaultTimeout, `${path}.defaultTimeout`); } if (config.interval && (typeof config.interval !== 'number' || config.interval < 0)) { this.addError(`${path}.interval`, 'Interval must be a positive number'); } } } validateSecrets(secrets) { if (!secrets.provider) { this.addError('secrets.provider', 'Provider is required'); return; } const validProviders = ['local', 'vault', '1password', 'aws-secrets', 'env', 'dotenv']; if (!validProviders.includes(secrets.provider)) { this.addError('secrets.provider', `Invalid provider: ${secrets.provider}. Valid options: ${validProviders.join(', ')}`); } } validateExtensions(extensions) { for (let i = 0; i < extensions.length; i++) { const ext = extensions[i]; const path = `extensions[${i}]`; if (!ext.source) { this.addError(`${path}.source`, 'Extension source is required'); } if (ext.tasks && !Array.isArray(ext.tasks)) { this.addError(`${path}.tasks`, 'Tasks must be an array'); } } } hasCircularInheritance(profile, profiles) { const visited = new Set(); let current = profile; while (current) { if (visited.has(current)) { return true; } visited.add(current); current = profiles[current]?.extends; } return false; } addError(path, message, value) { this.errors.push({ path, message, value, rule: 'error' }); } addWarning(path, message, value) { this.errors.push({ path, message, value, rule: 'warning' }); } } //# sourceMappingURL=config-validator.js.map