/** * Arcadia Suite - Context Indexer * * Cria um mapa do projeto para dar contexto aos agentes. * Analisa estrutura, rotas, schemas e dependências. */ import * as fs from "fs/promises"; import * as path from "path"; export interface ProjectContext { timestamp: string; structure: DirectoryNode; schemas: SchemaInfo[]; routes: RouteInfo[]; components: ComponentInfo[]; dependencies: DependencyInfo; } export interface DirectoryNode { name: string; type: "file" | "directory"; path: string; children?: DirectoryNode[]; size?: number; } export interface SchemaInfo { name: string; file: string; columns: string[]; } export interface RouteInfo { method: string; path: string; file: string; } export interface ComponentInfo { name: string; file: string; type: "page" | "component" | "hook"; } export interface DependencyInfo { dependencies: Record; devDependencies: Record; } const SCAN_DIRS = ["client/src", "server", "shared"]; const IGNORE_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__", ".next"]; const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]; class ContextIndexer { private cachedContext: ProjectContext | null = null; private cacheTime: number = 0; private cacheDuration = 60000; // 1 minuto async getContext(forceRefresh = false): Promise { const now = Date.now(); if (!forceRefresh && this.cachedContext && (now - this.cacheTime) < this.cacheDuration) { return this.cachedContext; } console.log("[ContextIndexer] Indexando projeto..."); const context: ProjectContext = { timestamp: new Date().toISOString(), structure: await this.scanDirectory(process.cwd(), 0, 3), schemas: await this.extractSchemas(), routes: await this.extractRoutes(), components: await this.extractComponents(), dependencies: await this.extractDependencies(), }; this.cachedContext = context; this.cacheTime = now; console.log(`[ContextIndexer] Indexado: ${context.schemas.length} schemas, ${context.routes.length} rotas, ${context.components.length} componentes`); return context; } async getContextSummary(): Promise { const ctx = await this.getContext(); const lines: string[] = [ "# Contexto do Projeto Arcadia Suite\n", "## Estrutura Principal", "```", this.formatStructure(ctx.structure, 0), "```\n", "## Schemas do Banco de Dados", ...ctx.schemas.slice(0, 20).map(s => `- **${s.name}**: ${s.columns.slice(0, 5).join(", ")}${s.columns.length > 5 ? "..." : ""}`), "\n## Rotas da API", ...ctx.routes.slice(0, 30).map(r => `- \`${r.method} ${r.path}\` (${r.file})`), "\n## Componentes React", ...ctx.components.slice(0, 20).map(c => `- **${c.name}** (${c.type}): ${c.file}`), "\n## Dependências Principais", ...Object.entries(ctx.dependencies.dependencies || {}).slice(0, 15).map(([k, v]) => `- ${k}: ${v}`), ]; return lines.join("\n"); } private async scanDirectory(dirPath: string, depth: number, maxDepth: number): Promise { const name = path.basename(dirPath); const relativePath = path.relative(process.cwd(), dirPath); if (depth >= maxDepth) { return { name, type: "directory", path: relativePath }; } try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const children: DirectoryNode[] = []; for (const entry of entries) { if (IGNORE_DIRS.includes(entry.name)) continue; if (entry.name.startsWith(".") && entry.name !== ".env.example") continue; const fullPath = path.join(dirPath, entry.name); const relPath = path.relative(process.cwd(), fullPath); if (entry.isDirectory()) { if (SCAN_DIRS.some(d => relPath.startsWith(d)) || depth === 0) { children.push(await this.scanDirectory(fullPath, depth + 1, maxDepth)); } } else { const ext = path.extname(entry.name); if (CODE_EXTENSIONS.includes(ext) || [".json", ".md", ".css"].includes(ext)) { try { const stats = await fs.stat(fullPath); children.push({ name: entry.name, type: "file", path: relPath, size: stats.size }); } catch {} } } } return { name, type: "directory", path: relativePath, children }; } catch { return { name, type: "directory", path: relativePath }; } } private async extractSchemas(): Promise { const schemas: SchemaInfo[] = []; try { const schemaPath = path.join(process.cwd(), "shared/schema.ts"); const content = await fs.readFile(schemaPath, "utf-8"); const tableRegex = /export const (\w+) = pgTable\("(\w+)",\s*\{([^}]+)\}/g; let match; while ((match = tableRegex.exec(content)) !== null) { const columns = match[3] .split(",") .map(c => c.trim().split(":")[0]?.trim()) .filter(c => c && !c.startsWith("//")); schemas.push({ name: match[1], file: "shared/schema.ts", columns, }); } } catch {} return schemas; } private async extractRoutes(): Promise { const routes: RouteInfo[] = []; try { const routesPath = path.join(process.cwd(), "server/routes.ts"); const content = await fs.readFile(routesPath, "utf-8"); const routeRegex = /app\.(get|post|put|patch|delete)\s*\(\s*["'`]([^"'`]+)["'`]/gi; let match; while ((match = routeRegex.exec(content)) !== null) { routes.push({ method: match[1].toUpperCase(), path: match[2], file: "server/routes.ts", }); } const useRegex = /app\.use\s*\(\s*["'`]([^"'`]+)["'`]/gi; while ((match = useRegex.exec(content)) !== null) { routes.push({ method: "USE", path: match[1], file: "server/routes.ts", }); } } catch {} return routes; } private async extractComponents(): Promise { const components: ComponentInfo[] = []; const pagesDir = path.join(process.cwd(), "client/src/pages"); try { const files = await fs.readdir(pagesDir); for (const file of files) { if (file.endsWith(".tsx") || file.endsWith(".jsx")) { const name = file.replace(/\.(tsx|jsx)$/, ""); components.push({ name, file: `client/src/pages/${file}`, type: "page", }); } } } catch {} const componentsDir = path.join(process.cwd(), "client/src/components"); try { const files = await fs.readdir(componentsDir); for (const file of files) { if (file.endsWith(".tsx") || file.endsWith(".jsx")) { const name = file.replace(/\.(tsx|jsx)$/, ""); components.push({ name, file: `client/src/components/${file}`, type: "component", }); } } } catch {} return components; } private async extractDependencies(): Promise { try { const pkgPath = path.join(process.cwd(), "package.json"); const content = await fs.readFile(pkgPath, "utf-8"); const pkg = JSON.parse(content); return { dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, }; } catch { return { dependencies: {}, devDependencies: {} }; } } private formatStructure(node: DirectoryNode, indent: number): string { const prefix = " ".repeat(indent); let result = `${prefix}${node.type === "directory" ? "📁" : "📄"} ${node.name}\n`; if (node.children) { for (const child of node.children.slice(0, 15)) { result += this.formatStructure(child, indent + 1); } if (node.children.length > 15) { result += `${prefix} ... e mais ${node.children.length - 15} itens\n`; } } return result; } } export const contextIndexer = new ContextIndexer();