dna-template-cli
Version:
DNA Template CLI v0.3.4 - Enhanced Commands Added (enhanced-create, enhanced-list, enhanced-validate)
531 lines • 22.7 kB
JavaScript
;
/**
* @fileoverview Enhanced validation system for DNA CLI
* Provides comprehensive validation for pre-generation, templates, environment, and dependencies
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationEngine = exports.projectConfigSchema = exports.templateSchema = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const semver_1 = tslib_1.__importDefault(require("semver"));
const child_process_1 = require("child_process");
const zod_1 = require("zod");
// Validation schemas
exports.templateSchema = zod_1.z.object({
id: zod_1.z.string().min(1),
name: zod_1.z.string().min(1),
description: zod_1.z.string().min(1),
type: zod_1.z.enum(['ai-saas', 'mobile-app', 'web-app', 'desktop-app', 'api', 'full-stack']),
framework: zod_1.z.enum(['nextjs', 'react-native', 'flutter', 'tauri', 'sveltekit', 'express', 'fastapi']),
version: zod_1.z.string().regex(/^\d+\.\d+\.\d+$/),
author: zod_1.z.string().min(1),
tags: zod_1.z.array(zod_1.z.string()),
dnaModules: zod_1.z.array(zod_1.z.string()),
requirements: zod_1.z.object({
node: zod_1.z.string().optional(),
npm: zod_1.z.string().optional(),
frameworks: zod_1.z.array(zod_1.z.string()).optional(),
}).optional(),
features: zod_1.z.array(zod_1.z.string()),
complexity: zod_1.z.enum(['beginner', 'intermediate', 'advanced']),
estimatedSetupTime: zod_1.z.number().positive(),
lastUpdated: zod_1.z.date(),
downloadCount: zod_1.z.number().optional(),
rating: zod_1.z.number().min(0).max(5).optional(),
variables: zod_1.z.array(zod_1.z.object({
name: zod_1.z.string().min(1),
description: zod_1.z.string().min(1),
required: zod_1.z.boolean(),
default: zod_1.z.string().optional(),
sensitive: zod_1.z.boolean().optional(),
type: zod_1.z.enum(['string', 'number', 'boolean', 'select']).optional(),
options: zod_1.z.array(zod_1.z.string()).optional(),
})).optional(),
});
exports.projectConfigSchema = zod_1.z.object({
name: zod_1.z.string().min(3).max(50).regex(/^[a-zA-Z][a-zA-Z0-9-_]*$/),
path: zod_1.z.string().min(1),
template: zod_1.z.string().min(1),
framework: zod_1.z.enum(['nextjs', 'react-native', 'flutter', 'tauri', 'sveltekit', 'express', 'fastapi']),
dnaModules: zod_1.z.array(zod_1.z.string()),
variables: zod_1.z.record(zod_1.z.string()),
packageManager: zod_1.z.enum(['npm', 'yarn', 'pnpm', 'bun']),
skipInstall: zod_1.z.boolean(),
skipGit: zod_1.z.boolean(),
});
class ValidationEngine {
constructor() {
this.validationCache = new Map();
}
static getInstance() {
if (!ValidationEngine.instance) {
ValidationEngine.instance = new ValidationEngine();
}
return ValidationEngine.instance;
}
/**
* Comprehensive pre-generation validation
*/
async validatePreGeneration(config) {
const cacheKey = `pre-gen-${JSON.stringify(config)}`;
if (this.validationCache.has(cacheKey)) {
return this.validationCache.get(cacheKey);
}
const result = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
};
// Validate project configuration
await this.validateProjectConfig(config, result);
// Validate system environment
await this.validateEnvironment(result);
// Validate disk space and permissions
await this.validateSystemResources(config, result);
// Validate project name and path
await this.validateProjectNameAndPath(config, result);
result.valid = result.errors.length === 0;
this.validationCache.set(cacheKey, result);
return result;
}
/**
* Template validation
*/
async validateTemplate(template) {
const cacheKey = `template-${template.id}-${template.version}`;
if (this.validationCache.has(cacheKey)) {
return this.validationCache.get(cacheKey);
}
const result = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
};
try {
// Validate template metadata structure
exports.templateSchema.parse(template);
}
catch (error) {
if (error instanceof zod_1.z.ZodError) {
result.errors.push(`Template metadata validation failed: ${error.errors.map(e => e.message).join(', ')}`);
}
else {
result.errors.push(`Template validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Validate template files exist
await this.validateTemplateFiles(template, result);
// Validate DNA module compatibility
await this.validateDnaModuleCompatibility(template, result);
// Validate template dependencies
await this.validateTemplateDependencies(template, result);
result.valid = result.errors.length === 0;
this.validationCache.set(cacheKey, result);
return result;
}
/**
* Environment validation
*/
async validateEnvironment(result) {
if (!result) {
result = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
};
}
// Check Node.js version
const nodeVersion = process.version;
const minNodeVersion = '18.0.0';
if (!semver_1.default.gte(nodeVersion, minNodeVersion)) {
result.errors.push(`Node.js version ${nodeVersion} is below minimum required version ${minNodeVersion}`);
result.suggestions.push(`Upgrade Node.js to version ${minNodeVersion} or higher`);
}
// Check npm version
try {
const npmVersion = await this.getCommandVersion('npm', '--version');
const minNpmVersion = '8.0.0';
if (npmVersion && !semver_1.default.gte(npmVersion, minNpmVersion)) {
result.warnings.push(`npm version ${npmVersion} is below recommended version ${minNpmVersion}`);
result.suggestions.push(`Consider upgrading npm: npm install -g npm@latest`);
}
}
catch (error) {
result.errors.push('npm is not available in PATH');
result.suggestions.push('Install Node.js which includes npm, or install npm separately');
}
// Check git availability
try {
await this.getCommandVersion('git', '--version');
}
catch (error) {
result.warnings.push('Git is not available in PATH');
result.suggestions.push('Install Git for version control features');
}
// Check available package managers
const packageManagers = ['yarn', 'pnpm', 'bun'];
const availableManagers = [];
for (const manager of packageManagers) {
try {
await this.getCommandVersion(manager, '--version');
availableManagers.push(manager);
}
catch {
// Manager not available, which is fine
}
}
if (availableManagers.length > 0) {
result.suggestions.push(`Alternative package managers available: ${availableManagers.join(', ')}`);
}
result.valid = result.errors.length === 0;
return result;
}
/**
* Dependency validation
*/
async validateDependencies(config, template) {
const result = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
};
// Check package manager availability
try {
await this.getCommandVersion(config.packageManager, '--version');
}
catch (error) {
result.errors.push(`Package manager "${config.packageManager}" is not available`);
result.suggestions.push('Install the selected package manager or choose a different one');
}
// Validate framework-specific requirements
await this.validateFrameworkRequirements(config.framework, result);
// Check template requirements
if (template.requirements) {
if (template.requirements.node) {
const nodeVersion = process.version;
if (!semver_1.default.satisfies(nodeVersion, template.requirements.node)) {
result.errors.push(`Node.js version ${nodeVersion} does not satisfy template requirement: ${template.requirements.node}`);
}
}
if (template.requirements.npm) {
try {
const npmVersion = await this.getCommandVersion('npm', '--version');
if (npmVersion && !semver_1.default.satisfies(npmVersion, template.requirements.npm)) {
result.warnings.push(`npm version ${npmVersion} may not be compatible with template requirement: ${template.requirements.npm}`);
}
}
catch {
// npm not available, already handled in environment validation
}
}
}
result.valid = result.errors.length === 0;
return result;
}
/**
* Health check for generated projects
*/
async validateGeneratedProject(projectPath) {
const result = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
};
// Check if project directory exists
if (!(await fs_extra_1.default.pathExists(projectPath))) {
result.errors.push('Project directory does not exist');
return result;
}
// Check for essential files
const essentialFiles = ['package.json', 'README.md', '.gitignore'];
for (const file of essentialFiles) {
const filePath = path_1.default.join(projectPath, file);
if (!(await fs_extra_1.default.pathExists(filePath))) {
result.warnings.push(`Essential file missing: ${file}`);
}
}
// Validate package.json
try {
const packageJsonPath = path_1.default.join(projectPath, 'package.json');
if (await fs_extra_1.default.pathExists(packageJsonPath)) {
const packageJson = await fs_extra_1.default.readJSON(packageJsonPath);
if (!packageJson.name) {
result.warnings.push('package.json is missing name field');
}
if (!packageJson.scripts) {
result.warnings.push('package.json is missing scripts field');
}
if (!packageJson.dependencies && !packageJson.devDependencies) {
result.warnings.push('package.json has no dependencies defined');
}
}
}
catch (error) {
result.errors.push('Invalid package.json file');
}
// Check for DNA configuration
const dnaConfigPath = path_1.default.join(projectPath, 'dna.config.json');
if (!(await fs_extra_1.default.pathExists(dnaConfigPath))) {
result.warnings.push('DNA configuration file (dna.config.json) is missing');
}
// Validate file permissions
await this.validateProjectPermissions(projectPath, result);
result.valid = result.errors.length === 0;
return result;
}
/**
* DNA module compatibility validation
*/
async validateDnaModuleCompatibility(template, result) {
// Check for conflicting modules
const conflictMap = {
'auth-firebase': ['auth-supabase', 'auth-cognito'],
'auth-supabase': ['auth-firebase', 'auth-cognito'],
'auth-cognito': ['auth-firebase', 'auth-supabase'],
'payment-stripe': ['payment-paypal'],
'payment-paypal': ['payment-stripe'],
'database-postgres': ['database-mongodb', 'database-mysql'],
'database-mongodb': ['database-postgres', 'database-mysql'],
'database-mysql': ['database-postgres', 'database-mongodb'],
};
for (const module of template.dnaModules) {
const conflicts = conflictMap[module];
if (conflicts) {
const conflicting = template.dnaModules.filter(m => conflicts.includes(m));
if (conflicting.length > 0) {
result.warnings.push(`DNA module "${module}" conflicts with: ${conflicting.join(', ')}`);
result.suggestions.push(`Choose only one module from conflicting group: ${[module, ...conflicting].join(', ')}`);
}
}
}
// Check framework compatibility
const frameworkCompatibility = {
'ui-tailwind': ['nextjs', 'react-native'],
'ui-material': ['nextjs', 'react-native', 'flutter'],
'ui-chakra': ['nextjs'],
'mobile-navigation': ['react-native', 'flutter'],
'web-analytics': ['nextjs', 'sveltekit'],
};
for (const module of template.dnaModules) {
const compatibleFrameworks = frameworkCompatibility[module];
if (compatibleFrameworks && !compatibleFrameworks.includes(template.framework)) {
result.warnings.push(`DNA module "${module}" may not be compatible with framework "${template.framework}"`);
result.suggestions.push(`Consider using alternative modules compatible with ${template.framework}`);
}
}
}
// Private helper methods
async validateProjectConfig(config, result) {
try {
exports.projectConfigSchema.parse(config);
}
catch (error) {
if (error instanceof zod_1.z.ZodError) {
for (const issue of error.errors) {
result.errors.push(`Configuration error: ${issue.path.join('.')} - ${issue.message}`);
}
}
}
}
async validateSystemResources(config, result) {
// Check disk space
try {
const parentDir = path_1.default.dirname(config.path);
if (await fs_extra_1.default.pathExists(parentDir)) {
const stats = await fs_extra_1.default.stat(parentDir);
// Note: This is a simplified check; in reality, you'd use a library like 'check-disk-space'
const requiredSpace = 500 * 1024 * 1024; // 500MB
// Since we can't easily get available space in Node.js without additional dependencies,
// we'll add this as a warning to install check-disk-space if needed
result.suggestions.push('Consider checking available disk space before generation');
}
}
catch (error) {
result.warnings.push('Could not verify disk space availability');
}
// Check write permissions
try {
const parentDir = path_1.default.dirname(config.path);
await fs_extra_1.default.access(parentDir, fs_extra_1.default.constants.W_OK);
}
catch (error) {
result.errors.push(`No write permission for directory: ${path_1.default.dirname(config.path)}`);
result.suggestions.push('Check directory permissions or choose a different location');
}
}
async validateProjectNameAndPath(config, result) {
// Validate project name format
const nameRegex = /^[a-zA-Z][a-zA-Z0-9-_]*$/;
if (!nameRegex.test(config.name)) {
result.errors.push('Project name must start with a letter and contain only letters, numbers, hyphens, and underscores');
}
// Check for reserved names
const reservedNames = [
'node_modules', 'npm', 'yarn', 'pnpm', 'bun',
'src', 'lib', 'test', 'tests', 'dist', 'build',
'public', 'static', 'assets', '.git', '.env',
'package', 'packages', 'config', 'scripts'
];
if (reservedNames.includes(config.name.toLowerCase())) {
result.errors.push(`"${config.name}" is a reserved name and cannot be used`);
}
// Validate path
if (config.path.includes('..')) {
result.errors.push('Path cannot contain relative path traversal (..)');
}
if (/[<>:"|?*]/.test(config.path)) {
result.errors.push('Path contains invalid characters');
}
// Check if directory already exists
if (await fs_extra_1.default.pathExists(config.path)) {
const stats = await fs_extra_1.default.stat(config.path);
if (!stats.isDirectory()) {
result.errors.push('Target path exists but is not a directory');
}
else {
const files = await fs_extra_1.default.readdir(config.path);
if (files.length > 0) {
result.warnings.push('Target directory is not empty');
result.suggestions.push('Use --overwrite flag to replace existing files or choose a different location');
}
}
}
}
async validateTemplateFiles(template, result) {
// In a real implementation, this would check if template files exist in the registry
// For now, we'll add basic validation
if (!template.id || !template.name) {
result.errors.push('Template is missing required metadata');
}
if (template.dnaModules.length === 0) {
result.warnings.push('Template has no DNA modules configured');
}
if (!template.features || template.features.length === 0) {
result.warnings.push('Template has no features listed');
}
}
async validateTemplateDependencies(template, result) {
// Validate that required DNA modules are available
// This would check against the actual DNA module registry
for (const moduleId of template.dnaModules) {
if (!this.isDnaModuleAvailable(moduleId)) {
result.warnings.push(`DNA module "${moduleId}" may not be available`);
}
}
}
async validateFrameworkRequirements(framework, result) {
const requirements = {
'nextjs': [
{ commands: ['node', 'npm'] },
],
'react-native': [
{ commands: ['node', 'npm'] },
{ commands: ['watchman'], optional: true },
{ commands: ['adb'], optional: true },
],
'flutter': [
{ commands: ['flutter'] },
{ commands: ['dart'] },
],
'tauri': [
{ commands: ['node', 'npm'] },
{ commands: ['cargo', 'rustc'] },
],
'sveltekit': [
{ commands: ['node', 'npm'] },
],
};
const frameworkReqs = requirements[framework];
if (frameworkReqs) {
for (const req of frameworkReqs) {
for (const command of req.commands) {
try {
await this.getCommandVersion(command, '--version');
}
catch (error) {
if (req.optional) {
result.warnings.push(`Optional tool "${command}" is not available for ${framework} development`);
}
else {
result.errors.push(`Required tool "${command}" is not available for ${framework} development`);
result.suggestions.push(`Install ${command} for ${framework} development`);
}
}
}
}
}
}
async validateProjectPermissions(projectPath, result) {
try {
// Check read/write permissions on the project directory
await fs_extra_1.default.access(projectPath, fs_extra_1.default.constants.R_OK | fs_extra_1.default.constants.W_OK);
}
catch (error) {
result.warnings.push('Project directory may have permission issues');
result.suggestions.push('Check and adjust directory permissions if needed');
}
}
async getCommandVersion(command, versionFlag) {
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(command, [versionFlag], {
stdio: 'pipe',
shell: process.platform === 'win32',
});
let output = '';
child.stdout?.on('data', (data) => {
output += data.toString();
});
child.stderr?.on('data', (data) => {
output += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
// Extract version number from output
const versionMatch = output.match(/(\d+\.\d+\.\d+)/);
resolve(versionMatch ? versionMatch[1] : output.trim());
}
else {
reject(new Error(`Command failed: ${command} ${versionFlag}`));
}
});
child.on('error', (error) => {
reject(error);
});
});
}
isDnaModuleAvailable(moduleId) {
// In a real implementation, this would check against the DNA module registry
// For now, return true for common modules
const commonModules = [
'auth-firebase', 'auth-supabase', 'auth-cognito',
'payment-stripe', 'payment-paypal',
'database-postgres', 'database-mongodb', 'database-mysql',
'ui-tailwind', 'ui-material', 'ui-chakra',
'mobile-navigation', 'web-analytics',
];
return commonModules.includes(moduleId);
}
/**
* Clear validation cache
*/
clearCache() {
this.validationCache.clear();
}
/**
* Get validation cache statistics
*/
getCacheStats() {
return {
size: this.validationCache.size,
keys: Array.from(this.validationCache.keys()),
};
}
}
exports.ValidationEngine = ValidationEngine;
//# sourceMappingURL=validation-engine.js.map