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