aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
430 lines (387 loc) • 12.9 kB
JavaScript
/**
* Validate Command Module
*
* CLI command for validating AIOS-Core installation integrity.
* Compares installed files against the install manifest.
*
* @module cli/commands/validate
* @version 1.0.0
* @story 6.19 - Post-Installation Validation & Integrity Verification
*
* Usage:
* aios validate # Validate current installation
* aios validate --repair # Repair missing/corrupted files
* aios validate --repair --dry-run # Preview repairs without applying
* aios validate --detailed # Show detailed file list
* aios validate --json # Output as JSON
*/
;
const { Command } = require('commander');
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const ora = require('ora');
/**
* Exit codes for CLI consistency
* @enum {number}
*/
const ExitCode = {
SUCCESS: 0,
VALIDATION_FAILED: 1,
ERROR: 2,
};
// Resolve validator module path
const validatorPath = path.resolve(__dirname, '../../../../packages/installer/src/installer/post-install-validator');
let PostInstallValidator, formatReport;
let validatorLoadError = null;
try {
const validator = require(validatorPath);
PostInstallValidator = validator.PostInstallValidator;
formatReport = validator.formatReport;
} catch (error) {
// Store error for later - will be reported during command execution
// This allows proper JSON output when --json flag is used
validatorLoadError = error;
}
/**
* Create the validate command
* @returns {Command} Commander command instance
*/
function createValidateCommand() {
const validate = new Command('validate');
validate
.description('Validate AIOS-Core installation integrity')
.option('-r, --repair', 'Repair missing or corrupted files')
.option('-d, --dry-run', 'Preview repairs without applying (use with --repair)')
.option('--detailed', 'Show detailed file list')
.option('--no-hash', 'Skip hash verification (faster)')
.option('--extras', 'Detect extra files not in manifest')
.option('-v, --verbose', 'Enable verbose output')
.option('--json', 'Output results as JSON')
.option('--source <dir>', 'Source directory for repairs')
.addHelpText(
'after',
`
Examples:
${chalk.dim('# Validate current installation')}
$ aios validate
${chalk.dim('# Validate with detailed file list')}
$ aios validate --detailed
${chalk.dim('# Repair missing/corrupted files')}
$ aios validate --repair
${chalk.dim('# Preview what would be repaired')}
$ aios validate --repair --dry-run
${chalk.dim('# Quick validation (skip hash check)')}
$ aios validate --no-hash
${chalk.dim('# Output as JSON for CI/CD')}
$ aios validate --json
Exit Codes:
0 - Validation passed
1 - Validation failed (missing/corrupted files)
2 - Validation error (could not complete)
`
)
.action(async (options) => {
await runValidation(options);
});
return validate;
}
/**
* Run the validation process
* @param {Object} options - Command options
*/
async function runValidation(options) {
const projectRoot = process.cwd();
const aiosCoreDir = path.join(projectRoot, '.aios-core');
// Check if AIOS-Core is installed
if (!fs.existsSync(aiosCoreDir)) {
if (options.json) {
console.log(
JSON.stringify(
{
status: 'failed',
error: 'AIOS-Core not found in current directory',
// SECURITY: Sanitize path - only show relative indicator
location: '.aios-core',
},
null,
2
)
);
} else {
console.error(chalk.red('\nError: AIOS-Core not found in current directory'));
console.error(chalk.dim(`Expected at: ${aiosCoreDir}`));
console.error(chalk.dim('\nRun `npx aios-core install` to install AIOS-Core'));
}
process.exit(ExitCode.ERROR);
}
// Check if validator module is available
if (!PostInstallValidator) {
const errorMsg = validatorLoadError
? `Validator module failed to load: ${validatorLoadError.message}`
: 'Validator module not available';
if (options.json) {
console.log(
JSON.stringify(
{
status: 'error',
error: errorMsg,
hint: 'This may indicate a corrupted installation',
},
null,
2
)
);
} else {
console.error(chalk.red(`\nError: ${errorMsg}`));
console.error(chalk.dim('This may indicate a corrupted installation'));
}
process.exit(ExitCode.ERROR);
}
// Determine source directory for repairs
let sourceDir = options.source;
// SECURITY: Validate --source directory if provided
if (sourceDir) {
const sourceManifest = path.join(sourceDir, '.aios-core', 'install-manifest.yaml');
if (!fs.existsSync(sourceManifest)) {
if (options.json) {
console.log(
JSON.stringify(
{
status: 'error',
error: 'Invalid source directory: manifest not found',
path: sourceDir,
},
null,
2
)
);
} else {
console.error(chalk.red('\nError: Invalid source directory'));
console.error(chalk.dim(`Expected manifest at: ${sourceManifest}`));
}
process.exit(ExitCode.ERROR);
}
}
if (!sourceDir && options.repair) {
// Try to find source in common locations
const possibleSources = [
path.join(__dirname, '../../../../..'), // npm package root
path.join(projectRoot, 'node_modules/aios-core'),
path.join(projectRoot, 'node_modules/aios-core'),
];
for (const src of possibleSources) {
if (fs.existsSync(path.join(src, '.aios-core', 'install-manifest.yaml'))) {
sourceDir = src;
break;
}
}
}
// Show spinner for non-JSON output (must be defined before validator for closure)
let spinner = null;
if (!options.json) {
console.log('');
spinner = ora({
text: 'Loading installation manifest...',
color: 'cyan',
}).start();
}
// Create validator instance
const validator = new PostInstallValidator(projectRoot, sourceDir, {
verifyHashes: options.hash !== false,
detectExtras: options.extras === true,
verbose: options.verbose === true,
onProgress: options.json
? () => {}
: (current, total, file) => {
if (spinner) {
spinner.text = `Validating ${current}/${total}: ${truncatePath(file, 40)}`;
}
},
});
try {
// Run validation
const report = await validator.validate();
if (spinner) {
spinner.succeed('Validation complete');
}
// Handle repair mode
let repairResult = null;
let repairAttempted = false;
if (options.repair && (report.stats.missingFiles > 0 || report.stats.corruptedFiles > 0)) {
repairAttempted = true;
if (!sourceDir) {
if (!options.json) {
console.error(chalk.yellow('\nWarning: Cannot repair - source directory not found'));
console.error(
chalk.dim('Specify source with --source <dir> or ensure package is installed')
);
}
repairResult = {
success: false,
error: 'Source directory not found',
repaired: [],
failed: [],
};
} else {
repairResult = await runRepair(validator, options, spinner);
}
}
// Output results
if (options.json) {
// Include repair results in JSON output for CI/CD pipelines
// SECURITY: Use explicit property copy to prevent prototype pollution
const output = {
status: report.status,
integrityScore: report.integrityScore,
manifestVerified: report.manifestVerified,
timestamp: report.timestamp,
duration: report.duration,
manifest: report.manifest,
stats: report.stats,
summary: report.summary,
recommendations: report.recommendations,
// Only include issues count in JSON to avoid leaking internal paths
issueCount: report.issues?.length ?? 0,
repair: repairAttempted
? {
attempted: true,
success: repairResult?.success ?? false,
error: repairResult?.error ?? null,
dryRun: options.dryRun === true,
repairedCount: repairResult?.repaired?.length ?? 0,
failedCount: repairResult?.failed?.length ?? 0,
repaired: repairResult?.repaired ?? [],
failed: repairResult?.failed ?? [],
}
: { attempted: false },
};
console.log(JSON.stringify(output, null, 2));
} else {
console.log(formatReport(report, { colors: true, detailed: options.detailed }));
// Surface repair refusal errors in human-readable output
if (repairAttempted && !repairResult?.success && repairResult?.error) {
console.log('');
console.log(chalk.red(`Repair failed: ${repairResult.error}`));
}
}
// Exit with appropriate code
// If repair was attempted and successful (not dry-run), exit 0
// If repair failed or was not attempted and there are issues, exit 1
if (repairAttempted && !options.dryRun && repairResult?.success) {
// Repair succeeded - exit 0
process.exit(ExitCode.SUCCESS);
} else if (report.status === 'failed') {
process.exit(ExitCode.VALIDATION_FAILED);
} else if (
report.status === 'warning' &&
(report.stats.missingFiles > 0 || report.stats.corruptedFiles > 0)
) {
process.exit(ExitCode.VALIDATION_FAILED);
} else {
process.exit(ExitCode.SUCCESS);
}
} catch (error) {
if (spinner) {
spinner.fail('Validation failed');
}
if (options.json) {
console.log(
JSON.stringify(
{
status: 'error',
error: error.message,
// SECURITY: Only include stack in verbose mode for debugging
stack: options.verbose ? error.stack : undefined,
},
null,
2
)
);
} else {
console.error(chalk.red(`\nError: ${error.message}`));
if (options.verbose) {
console.error(chalk.dim(error.stack));
}
}
process.exit(ExitCode.ERROR);
}
}
/**
* Run repair process
* @param {PostInstallValidator} validator - Validator instance
* @param {Object} options - Command options
* @param {Object} spinner - Ora spinner instance
* @returns {Object} Repair result with success status, repaired files, and failed files
*/
async function runRepair(validator, options, spinner) {
const dryRun = options.dryRun === true;
if (!options.json) {
console.log('');
if (dryRun) {
spinner = ora({
text: 'Analyzing files to repair (dry run)...',
color: 'yellow',
}).start();
} else {
spinner = ora({
text: 'Repairing files...',
color: 'green',
}).start();
}
}
const repairResult = await validator.repair({
dryRun,
onProgress: options.json
? () => {}
: (current, total, file) => {
if (spinner) {
const action = dryRun ? 'Checking' : 'Repairing';
spinner.text = `${action} ${current}/${total}: ${truncatePath(file, 40)}`;
}
},
});
if (spinner) {
if (repairResult.success) {
const action = dryRun ? 'would be repaired' : 'repaired';
spinner.succeed(`${repairResult.repaired.length} file(s) ${action}`);
} else {
spinner.warn('Repair completed with some failures');
}
}
if (!options.json) {
// Show repair summary
if (repairResult.repaired.length > 0) {
console.log('');
console.log(chalk.bold(dryRun ? 'Files that would be repaired:' : 'Repaired files:'));
for (const file of repairResult.repaired.slice(0, 20)) {
const icon = dryRun ? chalk.yellow('○') : chalk.green('✓');
console.log(` ${icon} ${file.path}`);
}
if (repairResult.repaired.length > 20) {
console.log(chalk.dim(` ... and ${repairResult.repaired.length - 20} more`));
}
}
if (repairResult.failed.length > 0) {
console.log('');
console.log(chalk.bold(chalk.red('Failed to repair:')));
for (const file of repairResult.failed) {
console.log(` ${chalk.red('✗')} ${file.path}: ${file.reason}`);
}
}
}
return repairResult;
}
/**
* Truncate path for display
* @param {string} filePath - File path
* @param {number} maxLen - Maximum length
* @returns {string} - Truncated path
*/
function truncatePath(filePath, maxLen) {
if (!filePath || filePath.length <= maxLen) return filePath;
return '...' + filePath.slice(-(maxLen - 3));
}
module.exports = {
createValidateCommand,
};