#!/usr/bin/env node
import path from 'path';
import fs from 'fs';
import { execSync } from 'child_process';
import { crawlApi, ApiRoute } from './crawler/api';
import { crawlComponents, ComponentInfo } from './crawler/components';
import { generateHooks, GeneratorOptions } from './generator/hooks';
import { analyzePages, PageAnalysis } from './mcp/pageAnalyzer';
import { generateDocs, DocOptions } from './mcp/generateDocs';
import { generateComponent } from './mcp/componentGenerator';
import { generateComponentDocs } from './mcp/generateComponentDocs';
import { suggestHookImplementation, suggestPageProps, loadMCPConfig } from './ai/mcpAgent';
import { enrichComponents } from './ai/enrichComponents';
import { generateComponentTests } from './generator/generateComponentTests';
import {
checkHooksDiff,
checkComponentsDiff,
checkPagesDiff,
saveSnapshot,
} from './mcp-ai-diff-checker';
import { runEslintOnPaths } from './mcp/lint/runEslint';
// --------------------
// CLI Options
// --------------------
interface CLIOptions {
tests?: boolean;
crawl?: boolean;
hooks?: boolean;
analyze?: boolean;
docs?: boolean;
all?: boolean;
ci?: boolean;
dryRun?: boolean;
useCursorAssist?: boolean;
aiAgent?: 'none' | 'copilot' | 'openai' | 'custom';
autoInferRender?: boolean;
}
function parseCLIArgs(): CLIOptions {
const args = process.argv.slice(2);
return {
crawl: args.includes('--crawl'),
hooks: args.includes('--hooks'),
analyze: args.includes('--analyze'),
docs: args.includes('--docs'),
all: args.includes('--all') || args.length === 0,
ci: args.includes('--ci'),
dryRun: args.includes('--dry-run'),
useCursorAssist: args.includes('--use-cursor-assist'),
aiAgent: args.includes('--ai-agent') ? (args[args.indexOf('--ai-agent') + 1] as any) : 'none',
autoInferRender: !args.includes('--no-auto-infer'),
tests: args.includes('--tests'),
};
}
// --------------------
// Detect Package Manager
// --------------------
function detectPackageManager() {
const lockFiles = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
for (const file of lockFiles) {
if (fs.existsSync(path.join(process.cwd(), file))) {
if (file === 'pnpm-lock.yaml') return 'pnpm';
if (file === 'package-lock.json') return 'npm';
if (file === 'yarn.lock') return 'yarn';
}
}
return 'unknown';
}
// --------------------
// Incremental file detection
// --------------------
function getChangedFiles(rootDir: string): string[] {
try {
const output = execSync('git diff --name-only HEAD~1', {
cwd: rootDir,
}).toString();
return output.split('\n').filter((f) => f.endsWith('.ts') || f.endsWith('.tsx'));
} catch (err) {
console.warn('⚠️ Unable to detect changed files, fallback to full crawl.');
return [];
}
}
// --------------------
// Safe AI wrapper
// --------------------
async function safeAISuggestion(
hookName: string,
route: ApiRoute,
aiAgent: 'none' | 'copilot' | 'openai' | 'custom',
): Promise<string> {
try {
return await suggestHookImplementation(hookName, route, aiAgent);
} catch {
console.warn(`⚠️ AI suggestion failed for ${hookName}, using default.`);
return `// Default hook for ${hookName}`;
}
}
async function safePageSuggestion(
pagePath: string,
page: PageAnalysis,
aiAgent: 'none' | 'copilot' | 'openai' | 'custom',
): Promise<string> {
try {
return await suggestPageProps(pagePath, page, aiAgent);
} catch {
console.warn(`⚠️ AI suggestion failed for ${pagePath}`);
return `// Default props for ${pagePath}`;
}
}
// --------------------
// Format generated code
// --------------------
function formatGeneratedCode(outputDir: string) {
try {
execSync(`npx prettier --write ${outputDir}`, { stdio: 'inherit' });
execSync(`npx eslint --fix ${outputDir}`, { stdio: 'inherit' });
console.log('🎨 Generated files formatted and linted');
} catch {
console.warn('⚠️ Formatting/Linting failed');
}
}
// --------------------
// MCP Runner
// --------------------
async function runMCP() {
const options = parseCLIArgs();
const rootDir = process.cwd();
const hooksOutputDir = path.join(rootDir, 'hooks');
const docsOutputDir = path.join(rootDir, 'mcp-docs');
const componentsOutputDir = path.join(rootDir, 'components');
const config = loadMCPConfig();
console.log(`📦 Detected package manager: ${detectPackageManager()}`);
console.log(`⚡ CI Mode: ${options.ci ? 'ON' : 'OFF'}`);
console.log(`🌙 Dry Run: ${options.dryRun ? 'ON' : 'OFF'}`);
// --------------------
// Crawl API routes
// --------------------
const changedFiles = getChangedFiles(rootDir);
let apiRoutes: ApiRoute[] = [];
if (options.crawl || options.all) {
console.log('🔍 Crawling APIs...');
apiRoutes = crawlApi(rootDir).filter(
(route) => changedFiles.length === 0 || changedFiles.some((f) => route.path.includes(f)),
);
console.log(`✅ Found ${apiRoutes.length} API routes`);
}
// --------------------
// Generate Hooks
// --------------------
if (options.hooks || options.all) {
if (!apiRoutes.length) apiRoutes = crawlApi(rootDir);
console.log('💻 Generating typed hooks...');
const hookOptions: GeneratorOptions = {
outputDir: hooksOutputDir,
useSWRInfinite: true, // optional
};
await generateHooks(apiRoutes, hookOptions);
// AI suggestions for hooks
if (config.enableAI && !options.dryRun) {
for (const route of apiRoutes) {
for (const method of route.methods) {
const suggestion = await safeAISuggestion(
method,
route,
options.aiAgent || config.defaultAgent,
);
console.log(`🤖 AI Suggestion for ${method}:\n${suggestion}`);
}
}
}
}
// --------------------
// Generate Components
// --------------------
let components: ComponentInfo[] = [];
if (options.analyze || options.all || options.docs) {
console.log('🧩 Crawling components...');
components = crawlComponents(rootDir, 'components');
// AI enrichment
console.log('🤖 Enriching component docs with AI...');
components = await enrichComponents(components, options.aiAgent || 'none');
console.log('📚 Generating component documentation...');
generateComponentDocs(path.join(rootDir, 'mcp-docs/components'), components, {
dryRun: options.dryRun,
});
console.log(`✅ Found ${components.length} components`);
components.forEach((c) => console.log(` - ${c.name} (${c.props.length} props)`));
}
if (config.autoSuggest?.components) {
for (const route of apiRoutes) {
await generateComponent(route);
}
}
// --------------------
// Generate Component Docs
// --------------------
if (options.docs || options.all) {
if (!components.length) components = crawlComponents(rootDir, 'components');
console.log('📚 Generating component documentation...');
generateComponentDocs(path.join(rootDir, 'mcp-docs/components'), components, {
dryRun: options.dryRun,
});
}
// --------------------
// Component Tests
// --------------------
if (options.all || options.tests) {
console.log('🧪 Generating component tests...');
const components = crawlComponents(rootDir);
const enriched = await enrichComponents(components, 'copilot');
generateComponentTests(enriched, { outputDir: 'tests/components', overwrite: true });
}
// --------------------
// Analyze Pages
// --------------------
let pageAnalyses: PageAnalysis[] = [];
if (options.analyze || options.all || options.docs) {
if (!apiRoutes.length) apiRoutes = crawlApi(rootDir);
console.log('📄 Analyzing pages...');
pageAnalyses = analyzePages(rootDir, apiRoutes);
console.log(`✅ Analyzed ${pageAnalyses.length} pages`);
}
// --------------------
// Generate Docs
// --------------------
if (options.docs || options.all) {
if (!pageAnalyses.length) pageAnalyses = analyzePages(rootDir, apiRoutes);
console.log('📝 Generating documentation...');
const docOptions: DocOptions = {
autoInferRender: options.autoInferRender,
};
generateDocs(docsOutputDir, pageAnalyses, docOptions);
// AI suggestions for pages
if (config.enableAI && !options.dryRun) {
for (const page of pageAnalyses) {
const suggestion = await safePageSuggestion(
page.pagePath,
page,
options.aiAgent || config.defaultAgent,
);
console.log(`🤖 AI Suggestion for ${page.pagePath}:\n${suggestion}`);
}
}
}
// --------------------
// Format generated files
// --------------------
formatGeneratedCode(hooksOutputDir);
formatGeneratedCode(docsOutputDir);
// --------------------
// Check diffs with AI
// --------------------
if (!options.dryRun && options.aiAgent !== 'none') {
console.log('🛡 Checking for potential breakages in developer changes...');
const pagesDir = path.join(rootDir, 'pages');
await checkHooksDiff(apiRoutes, hooksOutputDir, options.aiAgent);
await checkComponentsDiff(components, componentsOutputDir, options.aiAgent);
await checkPagesDiff(pageAnalyses, pagesDir, options.aiAgent);
}
// Save snapshots after successful generation
const pagesDir = path.join(rootDir, 'pages');
saveSnapshot(path.join(hooksOutputDir, 'hooks.ts'));
saveSnapshot(path.join(componentsOutputDir, 'components.ts'));
saveSnapshot(path.join(pagesDir, 'pages.ts'));
apiRoutes.forEach((route) => saveSnapshot(route.path));
components.forEach((comp) => saveSnapshot(comp.filePath));
pageAnalyses.forEach((page) => saveSnapshot(page.pagePath));
// -------------------- // Run ESLint on generated files
// --------------------
if (!options.dryRun) {
const lintPaths = [
hooksOutputDir,
path.join(rootDir, 'components/**/*.tsx'),
path.join(rootDir, 'pages/**/*.{ts,tsx}'),
];
const lintSummary = await runEslintOnPaths(lintPaths, rootDir);
console.log(`ESLint: ${lintSummary.errorCount} errors, ${lintSummary.warningCount} warnings`);
if (lintSummary.errorCount > 0 && config.ci?.failOnBreakingChange) {
process.exit(1);
}
}
console.log('🎉 MCP pipeline completed!');
}
// --------------------
// Run MCP
// --------------------
runMCP().catch((err) => {
console.error('❌ MCP pipeline failed:', err);
process.exit(1);
});