UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

245 lines 8.03 kB
import { z } from 'zod'; import * as fs from 'fs'; import * as path from 'path'; export class ValidationError extends Error { constructor(message, field, code) { super(message); this.field = field; this.code = code; this.name = 'ValidationError'; } } export const schemas = { filePath: z.string().refine((path) => { try { return fs.existsSync(path); } catch { return false; } }, { message: 'File does not exist' }), directoryPath: z.string().refine((path) => { try { return fs.existsSync(path) && fs.lstatSync(path).isDirectory(); } catch { return false; } }, { message: 'Directory does not exist' }), outputFormat: z.enum(['text', 'json', 'yaml', 'csv']), nonEmptyString: z.string().min(1, 'Value cannot be empty'), port: z.number().int().min(1).max(65535), url: z.string().url(), jsonString: z.string().refine((str) => { try { JSON.parse(str); return true; } catch { return false; } }, { message: 'Invalid JSON format' }), variables: z.record(z.any()), hostSelector: z.string().regex(/^[a-zA-Z0-9._-]+$/, 'Invalid host selector format'), moduleName: z.string().regex(/^[a-zA-Z0-9._-]+$/, 'Invalid module name format'), taskName: z.string().regex(/^[a-zA-Z0-9._:-]+$/, 'Invalid task name format'), recipeName: z.string().regex(/^[a-zA-Z0-9._-]+$/, 'Invalid recipe name format'), semver: z.string().regex(/^\d+\.\d+\.\d+/, 'Invalid semantic version format'), environmentName: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid environment name format'), }; export function validateFileExtension(filePath, allowedExtensions) { const ext = path.extname(filePath).toLowerCase(); if (!allowedExtensions.includes(ext)) { throw new ValidationError(`File must have one of these extensions: ${allowedExtensions.join(', ')}`, 'filePath'); } } export function validateFileReadable(filePath) { try { fs.accessSync(filePath, fs.constants.R_OK); } catch { throw new ValidationError('File is not readable', 'filePath'); } } export function validateFileWritable(filePath) { try { const dir = path.dirname(filePath); fs.accessSync(dir, fs.constants.W_OK); } catch { throw new ValidationError('File is not writable', 'filePath'); } } export function validateDirectoryWritable(dirPath) { try { fs.accessSync(dirPath, fs.constants.W_OK); } catch { throw new ValidationError('Directory is not writable', 'directoryPath'); } } export function validateAndParseJson(jsonString) { try { return JSON.parse(jsonString); } catch (error) { throw new ValidationError('Invalid JSON format', 'json'); } } export function validateVariables(vars) { if (!vars) return {}; try { const parsed = JSON.parse(vars); if (typeof parsed === 'object' && parsed !== null) { return parsed; } throw new Error('Variables must be an object'); } catch { const result = {}; const pairs = vars.split(',').map(pair => pair.trim()); for (const pair of pairs) { const [key, ...valueParts] = pair.split('='); if (!key || valueParts.length === 0) { throw new ValidationError('Invalid variable format. Use JSON or key=value format', 'variables'); } const value = valueParts.join('='); try { result[key.trim()] = JSON.parse(value); } catch { result[key.trim()] = value; } } return result; } } export function validateTimeout(timeout) { let timeoutMs; if (typeof timeout === 'string') { const match = timeout.match(/^(\d+)([smh]?)$/); if (!match || !match[1]) { throw new ValidationError('Invalid timeout format. Use number or format like "30s", "5m", "1h"', 'timeout'); } const value = parseInt(match[1]); const unit = match[2] || 's'; switch (unit) { case 's': timeoutMs = value * 1000; break; case 'm': timeoutMs = value * 60 * 1000; break; case 'h': timeoutMs = value * 60 * 60 * 1000; break; default: timeoutMs = value; } } else { timeoutMs = timeout; } if (timeoutMs < 0) { throw new ValidationError('Timeout must be positive', 'timeout'); } if (timeoutMs > 24 * 60 * 60 * 1000) { throw new ValidationError('Timeout cannot exceed 24 hours', 'timeout'); } return timeoutMs; } export function validateHostPattern(pattern) { const validPattern = /^[a-zA-Z0-9.*_-]+$/; if (!validPattern.test(pattern)) { throw new ValidationError('Invalid host pattern', 'hostPattern'); } } export function validateTagPattern(pattern) { const validPattern = /^[a-zA-Z0-9._-]+$/; if (!validPattern.test(pattern)) { throw new ValidationError('Invalid tag pattern', 'tagPattern'); } } export function validateOptions(options, schema) { try { schema.parse(options); } catch (error) { if (error instanceof z.ZodError) { const messages = error.errors.map(err => { const path = err.path.join('.'); return `${path}: ${err.message}`; }).join(', '); throw new ValidationError(`Validation failed: ${messages}`); } throw error; } } export function validateProjectStructure(projectPath) { const requiredFiles = [ 'package.json', 'tsconfig.json', ]; for (const file of requiredFiles) { const filePath = path.join(projectPath, file); if (!fs.existsSync(filePath)) { throw new ValidationError(`Missing required file: ${file}`, 'projectStructure'); } } } export function validateXecConfig(config) { const configSchema = z.object({ version: z.string(), name: z.string().optional(), description: z.string().optional(), author: z.string().optional(), modules: z.array(z.string()).optional(), environments: z.record(z.any()).optional(), defaults: z.record(z.any()).optional(), }); try { configSchema.parse(config); } catch (error) { if (error instanceof z.ZodError) { const messages = error.errors.map(err => { const path = err.path.join('.'); return `${path}: ${err.message}`; }).join(', '); throw new ValidationError(`Invalid xec configuration: ${messages}`); } throw error; } } export function validateRecipeStructure(recipe) { const recipeSchema = z.object({ name: z.string(), description: z.string().optional(), version: z.string().optional(), author: z.string().optional(), tasks: z.array(z.object({ name: z.string(), type: z.string().optional(), handler: z.any().optional(), command: z.string().optional(), })).optional(), phases: z.record(z.any()).optional(), vars: z.record(z.any()).optional(), metadata: z.record(z.any()).optional(), }); try { recipeSchema.parse(recipe); } catch (error) { if (error instanceof z.ZodError) { const messages = error.errors.map(err => { const path = err.path.join('.'); return `${path}: ${err.message}`; }).join(', '); throw new ValidationError(`Invalid recipe structure: ${messages}`); } throw error; } } //# sourceMappingURL=validation.js.map