automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
173 lines (142 loc) • 4.33 kB
JavaScript
#!/usr/bin/env node
/**
* Validate MCP Build - Ensure dist files are in sync with source
*
* Purpose:
* - Prevent accidental deletion of MCP dist files
* - Ensure HTML files are copied from src to dist
* - Validate TypeScript compilation is up-to-date
*
* Triggered by: pre-commit hook
* Exit codes: 0 = valid, 1 = needs rebuild
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
/**
* Get list of staged files from git
*/
function getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
if (!output) return [];
return output.split('\n').filter(Boolean);
} catch (error) {
return [];
}
}
/**
* Check if any MCP-related files are staged
*/
function hasMCPChanges(stagedFiles) {
return stagedFiles.some(file =>
file.startsWith('src/mcp/') ||
file.startsWith('dist/mcp/')
);
}
/**
* Check if HTML files exist in dist
*/
function validateHTMLFiles() {
const errors = [];
// Check if authorize.html exists in dist
const srcHtml = path.join(gitRoot, 'src/mcp/lib/views/authorize.html');
const distHtml = path.join(gitRoot, 'dist/mcp/lib/views/authorize.html');
if (fs.existsSync(srcHtml) && !fs.existsSync(distHtml)) {
errors.push({
file: 'authorize.html',
message: 'HTML file exists in src/ but missing from dist/'
});
}
// Check if files are identical (if both exist)
if (fs.existsSync(srcHtml) && fs.existsSync(distHtml)) {
const srcContent = fs.readFileSync(srcHtml, 'utf8');
const distContent = fs.readFileSync(distHtml, 'utf8');
if (srcContent !== distContent) {
errors.push({
file: 'authorize.html',
message: 'HTML file in dist/ is out of sync with src/'
});
}
}
return errors;
}
/**
* Check if TypeScript files are compiled
*/
function validateTypeScriptBuild(stagedFiles) {
const errors = [];
// Get all staged .ts files in src/
const stagedTsFiles = stagedFiles.filter(file =>
file.startsWith('src/mcp/') && file.endsWith('.ts')
);
for (const tsFile of stagedTsFiles) {
const srcPath = path.join(gitRoot, tsFile);
// Skip deleted files (they won't exist in filesystem)
if (!fs.existsSync(srcPath)) {
continue; // File deleted - no validation needed
}
// Convert src path to expected dist path
const distFile = tsFile
.replace('src/mcp/', 'dist/mcp/')
.replace(/\.ts$/, '.js');
const distPath = path.join(gitRoot, distFile);
// Check if compiled file exists
if (!fs.existsSync(distPath)) {
errors.push({
file: tsFile,
message: `TypeScript file staged but compiled output missing: ${distFile}`
});
} else {
// Check if source is newer than compiled output
const srcMtime = fs.statSync(srcPath).mtime;
const distMtime = fs.statSync(distPath).mtime;
if (srcMtime > distMtime) {
errors.push({
file: tsFile,
message: `Source file is newer than compiled output (${distFile})`
});
}
}
}
return errors;
}
/**
* Main validation logic
*/
function main() {
const stagedFiles = getStagedFiles();
// Skip validation if no MCP files are staged
if (!hasMCPChanges(stagedFiles)) {
return 0; // Success - no validation needed
}
let hasErrors = false;
const allErrors = [];
// Validate HTML files
const htmlErrors = validateHTMLFiles();
if (htmlErrors.length > 0) {
hasErrors = true;
allErrors.push(...htmlErrors);
}
// Validate TypeScript compilation
const tsErrors = validateTypeScriptBuild(stagedFiles);
if (tsErrors.length > 0) {
hasErrors = true;
allErrors.push(...tsErrors);
}
if (hasErrors) {
console.error('❌ MCP build validation failed:\n');
for (const error of allErrors) {
console.error(` ${error.file}`);
console.error(` └─ ${error.message}\n`);
}
console.error('🔧 Fix by running:');
console.error(' pnpm run build:mcp');
console.error(' git add dist/mcp/\n');
return 1; // Failure
}
console.log('✅ MCP build validation passed');
return 0; // Success
}
process.exit(main());