aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
334 lines (297 loc) • 9.32 kB
JavaScript
/**
* Dependency Validator
* Task 1.8.4: Validates dependency installation
*
* @module wizard/validation/validators/dependency-validator
*/
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
/**
* Validate dependencies
*
* @param {Object} depsContext - Dependency installation context
* @param {boolean} depsContext.success - Installation success status
* @param {string} depsContext.packageManager - Package manager used
* @param {boolean} depsContext.offlineMode - Offline mode flag
* @param {string} depsContext.projectType - Project type (GREENFIELD, BROWNFIELD, etc.)
* @returns {Promise<Object>} Validation result
*/
async function validateDependencies(depsContext = {}) {
const results = {
success: true,
checks: [],
errors: [],
warnings: [],
};
try {
// Check if dependencies were installed (skip for greenfield with no deps defined)
if (depsContext.success === false) {
results.success = false;
results.errors.push({
severity: 'critical',
message: 'Dependencies not installed - installation failed',
code: 'DEPS_INSTALL_FAILED',
});
return results;
}
// Check node_modules existence
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
const packageJsonPath = path.join(process.cwd(), 'package.json');
// For greenfield projects, check if package.json has dependencies
let hasDependencies = false;
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
hasDependencies = !!(
(packageJson.dependencies && Object.keys(packageJson.dependencies).length > 0) ||
(packageJson.devDependencies && Object.keys(packageJson.devDependencies).length > 0)
);
} catch {
// If we can't parse package.json, assume no dependencies
hasDependencies = false;
}
}
if (!fs.existsSync(nodeModulesPath)) {
// For greenfield projects with no dependencies, this is OK
if (!hasDependencies) {
results.checks.push({
component: 'Dependencies',
file: 'node_modules',
status: 'skipped',
message: 'No dependencies defined in package.json (greenfield project)',
});
return results;
}
// Otherwise, it's an error
results.success = false;
results.errors.push({
severity: 'critical',
message: 'node_modules directory not found',
file: nodeModulesPath,
code: 'NODE_MODULES_MISSING',
});
return results;
}
results.checks.push({
component: 'Dependencies',
file: 'node_modules',
status: 'success',
message: 'Directory exists',
});
// Validate package.json integrity
await validatePackageJson(results);
// Check critical dependencies
await checkCriticalDependencies(results);
// Run npm audit (non-blocking - warnings only)
await runSecurityAudit(results, depsContext.packageManager);
// Count installed packages
await countInstalledPackages(results, nodeModulesPath);
return results;
} catch (error) {
results.errors.push({
severity: 'high',
message: `Dependency validation failed: ${error.message}`,
code: 'DEPS_VALIDATION_ERROR',
details: error.stack,
});
results.success = false;
return results;
}
}
/**
* Validate package.json
* @private
*/
async function validatePackageJson(results) {
const packageJsonPath = 'package.json';
if (!fs.existsSync(packageJsonPath)) {
results.errors.push({
severity: 'critical',
message: 'package.json not found',
file: packageJsonPath,
code: 'PACKAGE_JSON_MISSING',
});
results.success = false;
return;
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (!packageJson.dependencies && !packageJson.devDependencies) {
results.warnings.push({
severity: 'medium',
message: 'package.json has no dependencies',
file: packageJsonPath,
code: 'NO_DEPENDENCIES',
});
}
results.checks.push({
component: 'Package Manifest',
file: packageJsonPath,
status: 'success',
message: 'Valid JSON',
});
} catch (error) {
results.errors.push({
severity: 'high',
message: `package.json parsing failed: ${error.message}`,
file: packageJsonPath,
code: 'PACKAGE_JSON_PARSE_ERROR',
});
results.success = false;
}
}
/**
* Check critical dependencies
* @private
*/
async function checkCriticalDependencies(results) {
const criticalDeps = [
'inquirer',
'chalk',
'yaml',
'fs-extra',
'@clack/prompts',
];
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
const missingDeps = [];
for (const dep of criticalDeps) {
const depPath = path.join(nodeModulesPath, dep);
if (!fs.existsSync(depPath)) {
missingDeps.push(dep);
}
}
if (missingDeps.length > 0) {
results.warnings.push({
severity: 'high',
message: `Critical dependencies missing: ${missingDeps.join(', ')}`,
code: 'CRITICAL_DEPS_MISSING',
solution: 'Re-run dependency installation',
});
} else {
results.checks.push({
component: 'Critical Dependencies',
status: 'success',
message: `All ${criticalDeps.length} critical dependencies installed`,
});
}
}
/**
* Run security audit
* @private
*/
async function runSecurityAudit(results, packageManager = 'npm') {
try {
const auditCommand = packageManager === 'yarn' ? 'yarn audit --json' : 'npm audit --json';
const { stdout } = await execAsync(auditCommand, {
timeout: 10000,
cwd: process.cwd(),
});
let auditResults;
if (packageManager === 'npm') {
auditResults = JSON.parse(stdout);
const vulnerabilities = auditResults.metadata?.vulnerabilities;
if (vulnerabilities) {
const total =
vulnerabilities.low +
vulnerabilities.moderate +
vulnerabilities.high +
vulnerabilities.critical;
if (total > 0) {
results.warnings.push({
severity: vulnerabilities.critical > 0 ? 'high' : 'medium',
message: `${total} vulnerabilities found (${vulnerabilities.critical} critical, ${vulnerabilities.high} high, ${vulnerabilities.moderate} moderate, ${vulnerabilities.low} low)`,
code: 'VULNERABILITIES_FOUND',
solution: 'Run \'npm audit fix\' to resolve',
});
} else {
results.checks.push({
component: 'Security Audit',
status: 'success',
message: 'No vulnerabilities found',
});
}
}
} else {
// Yarn audit format is different
results.checks.push({
component: 'Security Audit',
status: 'skipped',
message: 'Yarn audit not implemented in validation',
});
}
} catch (error) {
// Audit may fail if there are vulnerabilities (exit code 1)
// This is expected - parse the error output
if (error.stdout) {
try {
const auditResults = JSON.parse(error.stdout);
const vulnerabilities = auditResults.metadata?.vulnerabilities;
if (vulnerabilities) {
const total =
vulnerabilities.low +
vulnerabilities.moderate +
vulnerabilities.high +
vulnerabilities.critical;
results.warnings.push({
severity: vulnerabilities.critical > 0 ? 'high' : 'medium',
message: `${total} vulnerabilities found`,
code: 'VULNERABILITIES_FOUND',
solution: `Run '${packageManager} audit fix' to resolve`,
});
}
} catch {
// Could not parse audit output - skip
results.checks.push({
component: 'Security Audit',
status: 'skipped',
message: 'Audit output could not be parsed',
});
}
} else {
// Audit command failed completely
results.checks.push({
component: 'Security Audit',
status: 'skipped',
message: 'Audit failed to run',
});
}
}
}
/**
* Count installed packages
* @private
*/
async function countInstalledPackages(results, nodeModulesPath) {
try {
const packages = fs.readdirSync(nodeModulesPath);
const packageCount = packages.filter(pkg => {
// Filter out .bin, .cache, etc.
if (pkg.startsWith('.')) return false;
// Check if it's a directory
const pkgPath = path.join(nodeModulesPath, pkg);
try {
return fs.statSync(pkgPath).isDirectory();
} catch {
return false;
}
}).length;
results.checks.push({
component: 'Package Count',
status: 'success',
message: `${packageCount} packages installed`,
});
} catch {
// Not critical if we can't count packages
results.checks.push({
component: 'Package Count',
status: 'skipped',
message: 'Could not count packages',
});
}
}
module.exports = {
validateDependencies,
};