@creative-ia/cortex 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +41 -0
  2. package/dist/config/cloud-proxy.d.ts +15 -0
  3. package/dist/config/cloud-proxy.js +63 -0
  4. package/dist/config/cloudwatch-store.d.ts +13 -0
  5. package/dist/config/cloudwatch-store.js +66 -0
  6. package/dist/config/license.d.ts +29 -0
  7. package/dist/config/license.js +165 -0
  8. package/dist/config/ssm-store.d.ts +2 -0
  9. package/dist/config/ssm-store.js +38 -0
  10. package/dist/config/telemetry.d.ts +17 -0
  11. package/dist/config/telemetry.js +93 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +460 -0
  14. package/dist/knowledge/dynamo-store.d.ts +17 -0
  15. package/dist/knowledge/dynamo-store.js +85 -0
  16. package/dist/knowledge/embeddings.d.ts +2 -0
  17. package/dist/knowledge/embeddings.js +36 -0
  18. package/dist/knowledge/loader.d.ts +8 -0
  19. package/dist/knowledge/loader.js +57 -0
  20. package/dist/lambda-package.zip +0 -0
  21. package/dist/lambda.d.ts +22 -0
  22. package/dist/lambda.js +496 -0
  23. package/dist/package.json +1 -0
  24. package/dist/tools/advance-process.d.ts +7 -0
  25. package/dist/tools/advance-process.js +128 -0
  26. package/dist/tools/analyze-code.d.ts +7 -0
  27. package/dist/tools/analyze-code.js +131 -0
  28. package/dist/tools/analyze-docs.d.ts +8 -0
  29. package/dist/tools/analyze-docs.js +147 -0
  30. package/dist/tools/config-registry.d.ts +3 -0
  31. package/dist/tools/config-registry.js +20 -0
  32. package/dist/tools/create-process.d.ts +6 -0
  33. package/dist/tools/create-process.js +257 -0
  34. package/dist/tools/decompose-epic.d.ts +7 -0
  35. package/dist/tools/decompose-epic.js +603 -0
  36. package/dist/tools/diagrams.d.ts +51 -0
  37. package/dist/tools/diagrams.js +304 -0
  38. package/dist/tools/generate-report.d.ts +9 -0
  39. package/dist/tools/generate-report.js +891 -0
  40. package/dist/tools/generate-wiki.d.ts +10 -0
  41. package/dist/tools/generate-wiki.js +700 -0
  42. package/dist/tools/get-architecture.d.ts +6 -0
  43. package/dist/tools/get-architecture.js +78 -0
  44. package/dist/tools/get-code-standards.d.ts +7 -0
  45. package/dist/tools/get-code-standards.js +52 -0
  46. package/dist/tools/init-process.d.ts +7 -0
  47. package/dist/tools/init-process.js +82 -0
  48. package/dist/tools/knowledge-crud.d.ts +26 -0
  49. package/dist/tools/knowledge-crud.js +142 -0
  50. package/dist/tools/logo-base64.d.ts +1 -0
  51. package/dist/tools/logo-base64.js +1 -0
  52. package/dist/tools/logs-query.d.ts +15 -0
  53. package/dist/tools/logs-query.js +46 -0
  54. package/dist/tools/reverse-engineer.d.ts +13 -0
  55. package/dist/tools/reverse-engineer.js +956 -0
  56. package/dist/tools/semantic-search.d.ts +7 -0
  57. package/dist/tools/semantic-search.js +68 -0
  58. package/dist/tools/update-process.d.ts +17 -0
  59. package/dist/tools/update-process.js +195 -0
  60. package/dist/tools/validate-idea.d.ts +7 -0
  61. package/dist/tools/validate-idea.js +339 -0
  62. package/dist/tools/validate-process.d.ts +6 -0
  63. package/dist/tools/validate-process.js +102 -0
  64. package/package.json +31 -0
@@ -0,0 +1,956 @@
1
+ /**
2
+ * Tool: reverse_engineer
3
+ * Analisa um repositório local ou remoto (Git URL) e gera artefato .md completo de engenharia reversa:
4
+ * - Stack detection (linguagens, frameworks, package managers)
5
+ * - Estrutura de módulos (pastas, camadas)
6
+ * - Dependências (package.json, pom.xml, build.gradle, requirements.txt, etc.)
7
+ * - APIs e contratos (REST endpoints, GraphQL schemas)
8
+ * - Padrões de arquitetura (MVC, Clean, Hexagonal, etc.)
9
+ * - Métricas (arquivos, linhas, testes)
10
+ *
11
+ * Repos remotos: aceita URLs Git (https:// ou git@). O token de acesso e lido
12
+ * das env vars configuradas no mcp.json do cliente:
13
+ * GITHUB_TOKEN, GITLAB_TOKEN, BITBUCKET_TOKEN, AZURE_DEVOPS_TOKEN
14
+ */
15
+ import { readdir, readFile, writeFile, rm } from "node:fs/promises";
16
+ import { join, extname, basename, relative } from "node:path";
17
+ import { existsSync } from "node:fs";
18
+ import { execSync } from "node:child_process";
19
+ import { randomUUID } from "node:crypto";
20
+ import { tmpdir } from "node:os";
21
+ // ============================================================
22
+ // CONSTANTS
23
+ // ============================================================
24
+ const IGNORE_DIRS = new Set([
25
+ "node_modules", ".git", ".svn", "dist", "build", "out", ".next",
26
+ ".nuxt", "target", "__pycache__", ".gradle", ".idea", ".vscode",
27
+ "coverage", ".nyc_output", "vendor", "bower_components", ".terraform",
28
+ "cdk.out", ".serverless", ".aws-sam", "bin", "obj",
29
+ // Artefatos gerados
30
+ "wiki", "generated-diagrams",
31
+ // Mobile build artifacts (React Native)
32
+ "Pods", "ios", "android",
33
+ ]);
34
+ const CODE_EXTENSIONS = new Set([
35
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
36
+ ".java", ".kt", ".scala", ".groovy",
37
+ ".py", ".pyx",
38
+ ".go", ".rs", ".rb", ".php", ".cs", ".fs",
39
+ ".swift", ".m", ".mm",
40
+ ".c", ".cpp", ".h", ".hpp",
41
+ ".vue", ".svelte", ".astro",
42
+ ".sql", ".graphql", ".gql", ".proto",
43
+ ]);
44
+ const CONFIG_EXTENSIONS = new Set([
45
+ ".json", ".yaml", ".yml", ".toml", ".xml", ".ini", ".env",
46
+ ".config", ".cfg", ".properties",
47
+ ]);
48
+ const TEST_PATTERNS = [
49
+ /\.test\./i, /\.spec\./i, /\.e2e\./i, /__tests__/i,
50
+ /test[/\\]/i, /tests[/\\]/i, /spec[/\\]/i,
51
+ ];
52
+ const LANG_MAP = {
53
+ ".ts": "TypeScript", ".tsx": "TypeScript (React)", ".js": "JavaScript",
54
+ ".jsx": "JavaScript (React)", ".mjs": "JavaScript (ESM)", ".cjs": "JavaScript (CJS)",
55
+ ".java": "Java", ".kt": "Kotlin", ".scala": "Scala", ".groovy": "Groovy",
56
+ ".py": "Python", ".go": "Go", ".rs": "Rust", ".rb": "Ruby",
57
+ ".php": "PHP", ".cs": "C#", ".fs": "F#", ".swift": "Swift",
58
+ ".c": "C", ".cpp": "C++", ".h": "C/C++ Header",
59
+ ".vue": "Vue", ".svelte": "Svelte", ".astro": "Astro",
60
+ ".sql": "SQL", ".graphql": "GraphQL", ".gql": "GraphQL", ".proto": "Protobuf",
61
+ };
62
+ // ============================================================
63
+ // REMOTE REPO SUPPORT
64
+ // ============================================================
65
+ /** Detecta se o input e uma URL Git remota */
66
+ function isRemoteUrl(input) {
67
+ return /^https?:\/\//i.test(input) || /^git@/i.test(input);
68
+ }
69
+ /** Identifica o provider Git a partir da URL */
70
+ function detectGitProvider(url) {
71
+ const lower = url.toLowerCase();
72
+ if (lower.includes("github.com") || lower.includes("github"))
73
+ return "github";
74
+ if (lower.includes("gitlab.com") || lower.includes("gitlab"))
75
+ return "gitlab";
76
+ if (lower.includes("bitbucket.org") || lower.includes("bitbucket"))
77
+ return "bitbucket";
78
+ if (lower.includes("dev.azure.com") || lower.includes("visualstudio.com"))
79
+ return "azure";
80
+ return "unknown";
81
+ }
82
+ /** Busca o token do provider nas env vars */
83
+ function getProviderToken(provider) {
84
+ const envMap = {
85
+ github: ["GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN_COR", "GITHUB_TOKEN_PAR"],
86
+ gitlab: ["GITLAB_TOKEN", "GL_TOKEN"],
87
+ bitbucket: ["BITBUCKET_TOKEN", "BB_TOKEN"],
88
+ azure: ["AZURE_DEVOPS_TOKEN", "AZURE_DEVOPS_PAT", "AZURE_TOKEN"],
89
+ unknown: ["GIT_TOKEN"],
90
+ };
91
+ const keys = envMap[provider] || envMap.unknown;
92
+ for (const key of keys) {
93
+ if (process.env[key])
94
+ return process.env[key];
95
+ }
96
+ return undefined;
97
+ }
98
+ /** Injeta token na URL HTTPS para autenticacao */
99
+ function injectTokenInUrl(url, token, provider) {
100
+ // https://github.com/org/repo.git → https://x-access-token:TOKEN@github.com/org/repo.git
101
+ if (provider === "github") {
102
+ return url.replace(/^https:\/\//, `https://x-access-token:${token}@`);
103
+ }
104
+ if (provider === "gitlab") {
105
+ return url.replace(/^https:\/\//, `https://oauth2:${token}@`);
106
+ }
107
+ if (provider === "bitbucket") {
108
+ return url.replace(/^https:\/\//, `https://x-token-auth:${token}@`);
109
+ }
110
+ if (provider === "azure") {
111
+ return url.replace(/^https:\/\//, `https://pat:${token}@`);
112
+ }
113
+ // fallback generico
114
+ return url.replace(/^https:\/\//, `https://token:${token}@`);
115
+ }
116
+ /** Extrai nome do repo a partir da URL */
117
+ function repoNameFromUrl(url) {
118
+ // Remove .git suffix, pega ultimo segmento
119
+ const clean = url.replace(/\.git\s*$/, "").replace(/\/+$/, "");
120
+ const parts = clean.split("/");
121
+ return parts[parts.length - 1] || "repo";
122
+ }
123
+ /** Clona repo remoto em /tmp e retorna path local + cleanup function */
124
+ async function cloneRemoteRepo(url) {
125
+ const provider = detectGitProvider(url);
126
+ const token = getProviderToken(provider);
127
+ const repoName = repoNameFromUrl(url);
128
+ const cloneDir = join(tmpdir(), `re-${randomUUID().slice(0, 8)}`);
129
+ let cloneUrl = url;
130
+ if (token && url.startsWith("https://")) {
131
+ cloneUrl = injectTokenInUrl(url, token, provider);
132
+ }
133
+ // Clone com depth 1 para performance
134
+ try {
135
+ execSync(`git clone --depth 1 --single-branch "${cloneUrl}" "${cloneDir}"`, {
136
+ timeout: 120_000,
137
+ stdio: "pipe",
138
+ });
139
+ }
140
+ catch (err) {
141
+ const msg = err.stderr?.toString() || err.message || "Erro desconhecido";
142
+ // Limpar token da mensagem de erro
143
+ const safeMsg = token ? msg.replace(new RegExp(token, "g"), "***") : msg;
144
+ throw new Error(`Falha ao clonar ${url}: ${safeMsg}`);
145
+ }
146
+ return {
147
+ localDir: cloneDir,
148
+ repoName,
149
+ cleanup: async () => {
150
+ try {
151
+ await rm(cloneDir, { recursive: true, force: true });
152
+ }
153
+ catch { /* ignore */ }
154
+ },
155
+ };
156
+ }
157
+ // ============================================================
158
+ // FILE WALKER
159
+ // ============================================================
160
+ async function walkDir(dir, maxDepth = 12, depth = 0) {
161
+ if (depth > maxDepth)
162
+ return [];
163
+ const files = [];
164
+ try {
165
+ const entries = await readdir(dir, { withFileTypes: true });
166
+ for (const entry of entries) {
167
+ if (IGNORE_DIRS.has(entry.name))
168
+ continue;
169
+ if (entry.name.startsWith(".") && entry.name !== ".env")
170
+ continue;
171
+ const fullPath = join(dir, entry.name);
172
+ if (entry.isDirectory()) {
173
+ const sub = await walkDir(fullPath, maxDepth, depth + 1);
174
+ files.push(...sub);
175
+ }
176
+ else {
177
+ files.push(fullPath);
178
+ }
179
+ }
180
+ }
181
+ catch { /* permission denied, etc */ }
182
+ return files;
183
+ }
184
+ // ============================================================
185
+ // STACK DETECTION
186
+ // ============================================================
187
+ async function detectStack(repoDir, allFiles) {
188
+ const languages = new Map();
189
+ const frameworks = new Set();
190
+ const packageManagers = new Set();
191
+ const runtimes = new Set();
192
+ // Pastas cujo conteudo nao deve ser contado como codigo-fonte do repo
193
+ const NON_SOURCE_SEGMENTS = new Set(["docs", "documentation", "knowledge", "examples", "samples", "fixtures", "testdata"]);
194
+ // Count languages by extension (skip files inside non-source folders)
195
+ for (const f of allFiles) {
196
+ const rel = relative(repoDir, f);
197
+ const segments = rel.split("/");
198
+ const inNonSource = segments.some(s => NON_SOURCE_SEGMENTS.has(s.toLowerCase()));
199
+ if (inNonSource)
200
+ continue;
201
+ const ext = extname(f).toLowerCase();
202
+ const lang = LANG_MAP[ext];
203
+ if (lang)
204
+ languages.set(lang, (languages.get(lang) || 0) + 1);
205
+ }
206
+ // Detect package managers
207
+ const rootFiles = new Set(allFiles.filter(f => relative(repoDir, f).split("/").length === 1).map(f => basename(f)));
208
+ if (rootFiles.has("package.json"))
209
+ packageManagers.add("npm/yarn");
210
+ if (rootFiles.has("yarn.lock"))
211
+ packageManagers.add("Yarn");
212
+ if (rootFiles.has("pnpm-lock.yaml"))
213
+ packageManagers.add("pnpm");
214
+ if (rootFiles.has("package-lock.json"))
215
+ packageManagers.add("npm");
216
+ if (rootFiles.has("pom.xml"))
217
+ packageManagers.add("Maven");
218
+ if (rootFiles.has("build.gradle") || rootFiles.has("build.gradle.kts"))
219
+ packageManagers.add("Gradle");
220
+ if (rootFiles.has("requirements.txt") || rootFiles.has("Pipfile") || rootFiles.has("pyproject.toml"))
221
+ packageManagers.add("pip/Poetry");
222
+ if (rootFiles.has("go.mod"))
223
+ packageManagers.add("Go Modules");
224
+ if (rootFiles.has("Cargo.toml"))
225
+ packageManagers.add("Cargo");
226
+ if (rootFiles.has("Gemfile"))
227
+ packageManagers.add("Bundler");
228
+ if (rootFiles.has("composer.json"))
229
+ packageManagers.add("Composer");
230
+ // Detect frameworks from package.json
231
+ const pkgPath = join(repoDir, "package.json");
232
+ if (existsSync(pkgPath)) {
233
+ try {
234
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
235
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
236
+ if (allDeps["react"])
237
+ frameworks.add("React");
238
+ if (allDeps["react-native"])
239
+ frameworks.add("React Native");
240
+ if (allDeps["next"])
241
+ frameworks.add("Next.js");
242
+ if (allDeps["nuxt"] || allDeps["nuxt3"])
243
+ frameworks.add("Nuxt");
244
+ if (allDeps["vue"])
245
+ frameworks.add("Vue.js");
246
+ if (allDeps["@angular/core"])
247
+ frameworks.add("Angular");
248
+ if (allDeps["svelte"])
249
+ frameworks.add("Svelte");
250
+ if (allDeps["express"])
251
+ frameworks.add("Express");
252
+ if (allDeps["fastify"])
253
+ frameworks.add("Fastify");
254
+ if (allDeps["nestjs"] || allDeps["@nestjs/core"])
255
+ frameworks.add("NestJS");
256
+ if (allDeps["hapi"] || allDeps["@hapi/hapi"])
257
+ frameworks.add("Hapi");
258
+ if (allDeps["koa"])
259
+ frameworks.add("Koa");
260
+ if (allDeps["apollo-server"] || allDeps["@apollo/server"])
261
+ frameworks.add("Apollo Server");
262
+ if (allDeps["@apollo/client"])
263
+ frameworks.add("Apollo Client");
264
+ if (allDeps["graphql"])
265
+ frameworks.add("GraphQL");
266
+ if (allDeps["prisma"] || allDeps["@prisma/client"])
267
+ frameworks.add("Prisma");
268
+ if (allDeps["typeorm"])
269
+ frameworks.add("TypeORM");
270
+ if (allDeps["sequelize"])
271
+ frameworks.add("Sequelize");
272
+ if (allDeps["mongoose"])
273
+ frameworks.add("Mongoose");
274
+ if (allDeps["redux"] || allDeps["@reduxjs/toolkit"])
275
+ frameworks.add("Redux");
276
+ if (allDeps["zustand"])
277
+ frameworks.add("Zustand");
278
+ if (allDeps["tailwindcss"])
279
+ frameworks.add("Tailwind CSS");
280
+ if (allDeps["jest"])
281
+ frameworks.add("Jest");
282
+ if (allDeps["vitest"])
283
+ frameworks.add("Vitest");
284
+ if (allDeps["mocha"])
285
+ frameworks.add("Mocha");
286
+ if (allDeps["cypress"])
287
+ frameworks.add("Cypress");
288
+ if (allDeps["playwright"] || allDeps["@playwright/test"])
289
+ frameworks.add("Playwright");
290
+ if (allDeps["storybook"] || allDeps["@storybook/react"])
291
+ frameworks.add("Storybook");
292
+ if (allDeps["aws-cdk-lib"])
293
+ frameworks.add("AWS CDK");
294
+ if (allDeps["serverless"])
295
+ frameworks.add("Serverless Framework");
296
+ if (allDeps["@aws-sdk/client-s3"] || allDeps["aws-sdk"])
297
+ frameworks.add("AWS SDK");
298
+ if (allDeps["datadog-lambda-js"] || allDeps["dd-trace"])
299
+ frameworks.add("Datadog");
300
+ if (allDeps["@sentry/node"] || allDeps["@sentry/react"])
301
+ frameworks.add("Sentry");
302
+ // Runtime detection
303
+ if (pkg.engines?.node)
304
+ runtimes.add(`Node.js ${pkg.engines.node}`);
305
+ if (allDeps["typescript"])
306
+ runtimes.add(`TypeScript ${allDeps["typescript"]}`);
307
+ }
308
+ catch { /* ignore */ }
309
+ }
310
+ // Detect from pom.xml
311
+ const pomPath = join(repoDir, "pom.xml");
312
+ if (existsSync(pomPath)) {
313
+ try {
314
+ const pom = await readFile(pomPath, "utf-8");
315
+ if (pom.includes("spring-boot"))
316
+ frameworks.add("Spring Boot");
317
+ if (pom.includes("quarkus"))
318
+ frameworks.add("Quarkus");
319
+ if (pom.includes("micronaut"))
320
+ frameworks.add("Micronaut");
321
+ runtimes.add("JVM");
322
+ }
323
+ catch { /* ignore */ }
324
+ }
325
+ // Detect from requirements.txt / pyproject.toml
326
+ for (const pyFile of ["requirements.txt", "pyproject.toml"]) {
327
+ const pyPath = join(repoDir, pyFile);
328
+ if (existsSync(pyPath)) {
329
+ try {
330
+ const content = await readFile(pyPath, "utf-8");
331
+ if (content.includes("django"))
332
+ frameworks.add("Django");
333
+ if (content.includes("flask"))
334
+ frameworks.add("Flask");
335
+ if (content.includes("fastapi"))
336
+ frameworks.add("FastAPI");
337
+ if (content.includes("boto3"))
338
+ frameworks.add("AWS SDK (Python)");
339
+ runtimes.add("Python");
340
+ }
341
+ catch { /* ignore */ }
342
+ }
343
+ }
344
+ return {
345
+ languages,
346
+ frameworks: [...frameworks],
347
+ packageManagers: [...packageManagers],
348
+ runtimes: [...runtimes],
349
+ };
350
+ }
351
+ // ============================================================
352
+ // DEPENDENCY ANALYSIS
353
+ // ============================================================
354
+ async function analyzeDependencies(repoDir) {
355
+ const deps = [];
356
+ // package.json
357
+ const pkgPath = join(repoDir, "package.json");
358
+ if (existsSync(pkgPath)) {
359
+ try {
360
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
361
+ deps.push({
362
+ file: "package.json",
363
+ type: "npm",
364
+ dependencies: pkg.dependencies || {},
365
+ devDependencies: pkg.devDependencies || {},
366
+ });
367
+ }
368
+ catch { /* ignore */ }
369
+ }
370
+ // Check for monorepo workspaces
371
+ const pkgJsonFiles = await findFiles(repoDir, "package.json", 3);
372
+ for (const pf of pkgJsonFiles) {
373
+ const rel = relative(repoDir, pf);
374
+ if (rel === "package.json")
375
+ continue;
376
+ try {
377
+ const pkg = JSON.parse(await readFile(pf, "utf-8"));
378
+ if (pkg.name) {
379
+ deps.push({
380
+ file: rel,
381
+ type: "npm (workspace)",
382
+ dependencies: pkg.dependencies || {},
383
+ devDependencies: pkg.devDependencies || {},
384
+ });
385
+ }
386
+ }
387
+ catch { /* ignore */ }
388
+ }
389
+ return deps;
390
+ }
391
+ async function findFiles(dir, filename, maxDepth, depth = 0) {
392
+ if (depth > maxDepth)
393
+ return [];
394
+ const results = [];
395
+ try {
396
+ const entries = await readdir(dir, { withFileTypes: true });
397
+ for (const entry of entries) {
398
+ if (IGNORE_DIRS.has(entry.name))
399
+ continue;
400
+ const fullPath = join(dir, entry.name);
401
+ if (entry.isDirectory()) {
402
+ results.push(...await findFiles(fullPath, filename, maxDepth, depth + 1));
403
+ }
404
+ else if (entry.name === filename) {
405
+ results.push(fullPath);
406
+ }
407
+ }
408
+ }
409
+ catch { /* ignore */ }
410
+ return results;
411
+ }
412
+ // ============================================================
413
+ // MODULE STRUCTURE ANALYSIS
414
+ // ============================================================
415
+ async function analyzeModules(repoDir) {
416
+ const modules = [];
417
+ try {
418
+ const entries = await readdir(repoDir, { withFileTypes: true });
419
+ for (const entry of entries) {
420
+ if (!entry.isDirectory())
421
+ continue;
422
+ if (IGNORE_DIRS.has(entry.name))
423
+ continue;
424
+ if (entry.name.startsWith("."))
425
+ continue;
426
+ const dirPath = join(repoDir, entry.name);
427
+ const files = await walkDir(dirPath, 10);
428
+ const codeFiles = files.filter(f => CODE_EXTENSIONS.has(extname(f).toLowerCase()));
429
+ let type = "directory";
430
+ if (entry.name === "src")
431
+ type = "source root";
432
+ else if (entry.name === "lib" || entry.name === "libs")
433
+ type = "library";
434
+ else if (entry.name === "test" || entry.name === "tests" || entry.name === "__tests__")
435
+ type = "tests";
436
+ else if (entry.name === "config" || entry.name === "configs")
437
+ type = "configuration";
438
+ else if (entry.name === "scripts")
439
+ type = "scripts";
440
+ else if (entry.name === "docs" || entry.name === "documentation")
441
+ type = "documentation";
442
+ else if (entry.name === "api")
443
+ type = "API layer";
444
+ else if (entry.name === "components")
445
+ type = "UI components";
446
+ else if (entry.name === "pages" || entry.name === "views" || entry.name === "screens")
447
+ type = "views/pages";
448
+ else if (entry.name === "services")
449
+ type = "services";
450
+ else if (entry.name === "models" || entry.name === "entities")
451
+ type = "data models";
452
+ else if (entry.name === "utils" || entry.name === "helpers")
453
+ type = "utilities";
454
+ else if (entry.name === "hooks")
455
+ type = "React hooks";
456
+ else if (entry.name === "store" || entry.name === "redux")
457
+ type = "state management";
458
+ else if (entry.name === "middleware" || entry.name === "middlewares")
459
+ type = "middleware";
460
+ else if (entry.name === "routes" || entry.name === "router")
461
+ type = "routing";
462
+ else if (entry.name === "modules" || entry.name === "features")
463
+ type = "feature modules";
464
+ else if (entry.name === "infra" || entry.name === "infrastructure")
465
+ type = "infrastructure";
466
+ else if (entry.name === "domain")
467
+ type = "domain layer";
468
+ else if (entry.name === "application")
469
+ type = "application layer";
470
+ else if (entry.name === "presentation")
471
+ type = "presentation layer";
472
+ // Deeper analysis for src/
473
+ if (entry.name === "src") {
474
+ const subModules = await analyzeModules(dirPath);
475
+ modules.push(...subModules.map(m => ({ ...m, path: `src/${m.path}` })));
476
+ }
477
+ modules.push({
478
+ path: entry.name,
479
+ type,
480
+ files: codeFiles.length,
481
+ description: `${codeFiles.length} code files`,
482
+ });
483
+ }
484
+ }
485
+ catch { /* ignore */ }
486
+ return modules;
487
+ }
488
+ // ============================================================
489
+ // API DETECTION
490
+ // ============================================================
491
+ async function detectApis(repoDir, allFiles) {
492
+ const endpoints = [];
493
+ const NON_SOURCE_SEGMENTS = new Set(["docs", "documentation", "knowledge", "examples", "samples", "fixtures", "testdata"]);
494
+ const codeFiles = allFiles.filter(f => {
495
+ const ext = extname(f).toLowerCase();
496
+ if (![".ts", ".js", ".tsx", ".jsx", ".py", ".java", ".go", ".rb", ".php"].includes(ext))
497
+ return false;
498
+ const rel = relative(repoDir, f);
499
+ return !rel.split("/").some(s => NON_SOURCE_SEGMENTS.has(s.toLowerCase()));
500
+ });
501
+ // Limit to first 500 files to avoid timeout
502
+ const filesToScan = codeFiles.slice(0, 500);
503
+ for (const file of filesToScan) {
504
+ try {
505
+ const content = await readFile(file, "utf-8");
506
+ const lines = content.split("\n");
507
+ const rel = relative(repoDir, file);
508
+ // Track Spring @RequestMapping class-level prefix
509
+ let classPrefix = "";
510
+ for (let i = 0; i < lines.length; i++) {
511
+ const line = lines[i];
512
+ // --- Express/Fastify/Koa/Hapi patterns ---
513
+ const routeMatch = line.match(/\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/i);
514
+ if (routeMatch) {
515
+ endpoints.push({ method: routeMatch[1].toUpperCase(), path: routeMatch[2], file: rel, line: i + 1 });
516
+ continue;
517
+ }
518
+ // --- Express router.route().get/post chain ---
519
+ const routeChain = line.match(/\.route\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\s*\.(get|post|put|patch|delete)/i);
520
+ if (routeChain) {
521
+ endpoints.push({ method: routeChain[2].toUpperCase(), path: routeChain[1], file: rel, line: i + 1 });
522
+ continue;
523
+ }
524
+ // --- NestJS decorators ---
525
+ const nestMatch = line.match(/@(Get|Post|Put|Patch|Delete)\s*\(\s*['"`]?([^'"`)\s]*)['"`]?\s*\)/i);
526
+ if (nestMatch) {
527
+ endpoints.push({ method: nestMatch[1].toUpperCase(), path: nestMatch[2] || "/", file: rel, line: i + 1 });
528
+ continue;
529
+ }
530
+ // --- Spring Boot: class-level @RequestMapping ---
531
+ const classMapping = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?['"`]([^'"`]+)['"`]/);
532
+ if (classMapping) {
533
+ classPrefix = classMapping[1];
534
+ continue;
535
+ }
536
+ // --- Spring Boot: method-level mappings (with or without path) ---
537
+ const springAnnotation = line.match(/@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\b/i);
538
+ if (springAnnotation) {
539
+ const method = springAnnotation[1].replace("Mapping", "").toUpperCase();
540
+ let springPath = "/";
541
+ // Try explicit value= or path= attribute
542
+ const valueAttr = line.match(/@\w+Mapping\s*\(.*?(?:value|path)\s*=\s*['"`]([^'"`]*)['"`]/i);
543
+ if (valueAttr) {
544
+ springPath = valueAttr[1] || "/";
545
+ }
546
+ else {
547
+ // Try first positional string arg (must NOT be preceded by a key like consumes=, produces=, etc.)
548
+ const positionalArg = line.match(/@\w+Mapping\s*\(\s*['"`]([^'"`]*)['"`]/i);
549
+ if (positionalArg) {
550
+ springPath = positionalArg[1] || "/";
551
+ }
552
+ }
553
+ const fullPath = classPrefix ? `${classPrefix}${springPath === "/" ? "" : springPath}` : springPath;
554
+ endpoints.push({ method, path: fullPath, file: rel, line: i + 1 });
555
+ continue;
556
+ }
557
+ // --- FastAPI / Flask ---
558
+ const pyMatch = line.match(/@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/i);
559
+ if (pyMatch) {
560
+ endpoints.push({ method: pyMatch[1].toUpperCase(), path: pyMatch[2], file: rel, line: i + 1 });
561
+ continue;
562
+ }
563
+ // --- GraphQL: schema field definitions ---
564
+ // Pattern: fieldName: { type: ..., resolve: ... } (in *Queries.js / *Mutations.js)
565
+ const isQueryFile = /quer/i.test(rel);
566
+ const isMutationFile = /mutat/i.test(rel);
567
+ if (isQueryFile || isMutationFile) {
568
+ const gqlField = line.match(/^\s{2,}(\w+)\s*:\s*\{/);
569
+ if (gqlField) {
570
+ const fieldName = gqlField[1];
571
+ if (!["type", "args", "resolve", "description", "deprecationReason"].includes(fieldName)) {
572
+ endpoints.push({
573
+ method: isMutationFile ? "MUTATION" : "QUERY",
574
+ path: fieldName,
575
+ file: rel,
576
+ line: i + 1,
577
+ });
578
+ continue;
579
+ }
580
+ }
581
+ }
582
+ // --- GraphQL: resolver return object ---
583
+ // Pattern: return { getKeys, createKey, ... } (in *Resolver.js)
584
+ if (/resolver/i.test(rel) && /^\s*return\s*\{/.test(line)) {
585
+ // Collect all fields from the return block
586
+ let block = line;
587
+ for (let j = i; j < Math.min(i + 80, lines.length); j++) {
588
+ block += " " + lines[j];
589
+ if (lines[j].includes("};"))
590
+ break;
591
+ }
592
+ const fields = block.match(/\b(\w+)\s*[,}]/g);
593
+ if (fields) {
594
+ for (const f of fields) {
595
+ const name = f.replace(/[,}\s]/g, "");
596
+ if (name && name.length > 2 && !["module", "exports", "require", "const", "let", "var"].includes(name)) {
597
+ endpoints.push({ method: "RESOLVER", path: name, file: rel, line: i + 1 });
598
+ }
599
+ }
600
+ }
601
+ continue;
602
+ }
603
+ // --- GraphQL typeDefs: type Query { field } / type Mutation { field } ---
604
+ const gqlTypeDef = line.match(/^\s*(type\s+(?:Query|Mutation)\s*\{)/i);
605
+ if (gqlTypeDef) {
606
+ const isMut = /mutation/i.test(gqlTypeDef[1]);
607
+ // Scan next lines for field definitions
608
+ for (let j = i + 1; j < Math.min(i + 50, lines.length); j++) {
609
+ if (lines[j].includes("}"))
610
+ break;
611
+ const fieldMatch = lines[j].match(/^\s*(\w+)\s*[\(:]/);
612
+ if (fieldMatch) {
613
+ endpoints.push({
614
+ method: isMut ? "MUTATION" : "QUERY",
615
+ path: fieldMatch[1],
616
+ file: rel,
617
+ line: j + 1,
618
+ });
619
+ }
620
+ }
621
+ }
622
+ // --- Kafka consumer/producer ---
623
+ const kafkaConsumer = line.match(/(?:consumer|subscribe)\s*\(\s*\{?\s*(?:topic|topics)\s*:\s*['"`]([^'"`]+)['"`]/i);
624
+ if (kafkaConsumer) {
625
+ endpoints.push({ method: "KAFKA_CONSUMER", path: kafkaConsumer[1], file: rel, line: i + 1 });
626
+ continue;
627
+ }
628
+ const kafkaProducer = line.match(/(?:producer|send)\s*\(\s*\{?\s*topic\s*:\s*['"`]([^'"`]+)['"`]/i);
629
+ if (kafkaProducer) {
630
+ endpoints.push({ method: "KAFKA_PRODUCER", path: kafkaProducer[1], file: rel, line: i + 1 });
631
+ continue;
632
+ }
633
+ // --- Lambda handler ---
634
+ const lambdaHandler = line.match(/exports\.handler\s*=|export\s+(?:async\s+)?function\s+handler|module\.exports\s*=\s*\{?\s*handler/);
635
+ if (lambdaHandler) {
636
+ endpoints.push({ method: "LAMBDA", path: "handler", file: rel, line: i + 1 });
637
+ continue;
638
+ }
639
+ }
640
+ }
641
+ catch { /* ignore binary files, etc */ }
642
+ }
643
+ return endpoints;
644
+ }
645
+ // ============================================================
646
+ // ARCHITECTURE PATTERN DETECTION
647
+ // ============================================================
648
+ function detectArchitecturePatterns(modules, allFiles, repoDir) {
649
+ const patterns = [];
650
+ const dirs = new Set(modules.map(m => m.path.toLowerCase()));
651
+ const relFiles = allFiles.map(f => relative(repoDir, f).toLowerCase());
652
+ // Clean Architecture / Hexagonal
653
+ if (dirs.has("domain") && (dirs.has("application") || dirs.has("use-cases") || dirs.has("usecases"))) {
654
+ patterns.push("Clean Architecture / Hexagonal");
655
+ }
656
+ // MVC
657
+ if ((dirs.has("models") || dirs.has("entities")) && (dirs.has("views") || dirs.has("pages")) && dirs.has("controllers")) {
658
+ patterns.push("MVC (Model-View-Controller)");
659
+ }
660
+ // MVVM
661
+ if ((dirs.has("models") || dirs.has("entities")) && dirs.has("viewmodels") && (dirs.has("views") || dirs.has("screens"))) {
662
+ patterns.push("MVVM (Model-View-ViewModel)");
663
+ }
664
+ // Feature-based / Modular
665
+ if (dirs.has("modules") || dirs.has("features")) {
666
+ patterns.push("Feature-based / Modular");
667
+ }
668
+ // Layered
669
+ if (dirs.has("services") && (dirs.has("controllers") || dirs.has("routes")) && (dirs.has("models") || dirs.has("repositories"))) {
670
+ patterns.push("Layered Architecture");
671
+ }
672
+ // BFF (Backend for Frontend)
673
+ if (relFiles.some(f => f.includes("resolver")) && (relFiles.some(f => f.includes("graphql")) || relFiles.some(f => f.endsWith(".graphql") || f.endsWith(".gql")))) {
674
+ patterns.push("BFF (Backend for Frontend) — GraphQL");
675
+ }
676
+ else if (dirs.has("resolvers") || relFiles.some(f => f.includes("resolver"))) {
677
+ patterns.push("GraphQL Resolvers");
678
+ }
679
+ // GraphQL API
680
+ if (relFiles.some(f => f.endsWith(".graphql") || f.endsWith(".gql") || f.includes("schema.graphql") || f.includes("typedef"))) {
681
+ patterns.push("GraphQL API");
682
+ }
683
+ // Monorepo
684
+ if (dirs.has("packages") || dirs.has("apps") || relFiles.some(f => f.includes("lerna.json") || f.includes("turbo.json"))) {
685
+ patterns.push("Monorepo");
686
+ }
687
+ // Serverless
688
+ if (relFiles.some(f => f.includes("serverless.yml") || f.includes("serverless.yaml") || f.includes("template.yaml") || f.includes("cdk.json") || f.includes("sam.json"))) {
689
+ patterns.push("Serverless / IaC");
690
+ }
691
+ // Terraform
692
+ if (relFiles.some(f => f.endsWith(".tf") || f.includes("terraform"))) {
693
+ patterns.push("Terraform (IaC)");
694
+ }
695
+ // Docker
696
+ if (relFiles.some(f => f.includes("docker-compose"))) {
697
+ patterns.push("Containerized (Docker Compose)");
698
+ }
699
+ if (relFiles.some(f => f.includes("dockerfile"))) {
700
+ patterns.push("Docker");
701
+ }
702
+ // Kafka / Event-driven
703
+ if (relFiles.some(f => f.includes("kafka") || f.includes("consumer") || f.includes("producer"))) {
704
+ patterns.push("Event-Driven (Kafka)");
705
+ }
706
+ // REST
707
+ if (relFiles.some(f => f.includes("swagger") || f.includes("openapi"))) {
708
+ patterns.push("REST API (OpenAPI/Swagger)");
709
+ }
710
+ // Spring Boot
711
+ if (relFiles.some(f => f.includes("application.properties") || f.includes("application.yml") || f.includes("application.yaml"))) {
712
+ if (relFiles.some(f => f.endsWith(".java"))) {
713
+ patterns.push("Spring Boot");
714
+ }
715
+ }
716
+ // React Native
717
+ if (relFiles.some(f => f.includes("android/") || f.includes("ios/")) && relFiles.some(f => f.includes("app.json") || f.includes("react-native"))) {
718
+ patterns.push("React Native (Mobile)");
719
+ }
720
+ // Component Library
721
+ if (dirs.has("components") && (relFiles.some(f => f.includes("storybook")) || relFiles.some(f => f.includes("stories")))) {
722
+ patterns.push("Component Library (Storybook)");
723
+ }
724
+ // Interceptors / Middleware pattern
725
+ if (dirs.has("interceptors") || dirs.has("middleware") || dirs.has("middlewares")) {
726
+ patterns.push("Middleware / Interceptor Pattern");
727
+ }
728
+ // Adapters pattern
729
+ if (dirs.has("adapters") || dirs.has("gateways")) {
730
+ patterns.push("Adapter / Gateway Pattern");
731
+ }
732
+ if (patterns.length === 0)
733
+ patterns.push("Convencional (sem padrao arquitetural forte detectado)");
734
+ return patterns;
735
+ }
736
+ // ============================================================
737
+ // METRICS
738
+ // ============================================================
739
+ async function computeMetrics(repoDir, allFiles) {
740
+ const byExtension = new Map();
741
+ let totalLines = 0;
742
+ let codeFiles = 0;
743
+ let testFiles = 0;
744
+ let configFiles = 0;
745
+ const NON_SOURCE_SEGMENTS = new Set(["docs", "documentation", "knowledge", "examples", "samples", "fixtures", "testdata"]);
746
+ for (const f of allFiles) {
747
+ const ext = extname(f).toLowerCase();
748
+ byExtension.set(ext, (byExtension.get(ext) || 0) + 1);
749
+ // Nao contar arquivos em pastas de documentacao como codigo-fonte
750
+ const rel = relative(repoDir, f);
751
+ const segments = rel.split("/");
752
+ const inNonSource = segments.some(s => NON_SOURCE_SEGMENTS.has(s.toLowerCase()));
753
+ if (CODE_EXTENSIONS.has(ext) && !inNonSource) {
754
+ codeFiles++;
755
+ if (TEST_PATTERNS.some(p => p.test(rel)))
756
+ testFiles++;
757
+ try {
758
+ const content = await readFile(f, "utf-8");
759
+ totalLines += content.split("\n").length;
760
+ }
761
+ catch { /* binary */ }
762
+ }
763
+ else if (CONFIG_EXTENSIONS.has(ext)) {
764
+ configFiles++;
765
+ }
766
+ }
767
+ return { totalFiles: allFiles.length, totalLines, codeFiles, testFiles, configFiles, byExtension };
768
+ }
769
+ // ============================================================
770
+ // MARKDOWN REPORT GENERATOR
771
+ // ============================================================
772
+ function generateReport(repoDir, stack, deps, modules, apis, patterns, metrics, repoName) {
773
+ const date = new Date().toISOString().slice(0, 10);
774
+ let md = `# Engenharia Reversa — ${repoName}\n\n`;
775
+ md += `> Data: ${date}\n`;
776
+ md += `> Repositório: ${repoName}\n`;
777
+ md += `> Caminho: ${repoDir}\n\n---\n\n`;
778
+ // 1. Stack
779
+ md += `## 1. Stack Tecnológica\n\n`;
780
+ md += `| Camada | Tecnologia | Arquivos |\n|--------|-----------|----------|\n`;
781
+ const sortedLangs = [...stack.languages.entries()].sort((a, b) => b[1] - a[1]);
782
+ for (const [lang, count] of sortedLangs) {
783
+ md += `| Linguagem | ${lang} | ${count} |\n`;
784
+ }
785
+ if (stack.frameworks.length > 0) {
786
+ for (const fw of stack.frameworks) {
787
+ md += `| Framework | ${fw} | — |\n`;
788
+ }
789
+ }
790
+ if (stack.packageManagers.length > 0) {
791
+ md += `| Package Manager | ${stack.packageManagers.join(", ")} | — |\n`;
792
+ }
793
+ if (stack.runtimes.length > 0) {
794
+ md += `| Runtime | ${stack.runtimes.join(", ")} | — |\n`;
795
+ }
796
+ md += `\n`;
797
+ // 2. Padrões de Arquitetura
798
+ md += `## 2. Padrões de Arquitetura\n\n`;
799
+ for (const p of patterns) {
800
+ md += `- ${p}\n`;
801
+ }
802
+ md += `\n`;
803
+ // 3. Estrutura de Módulos
804
+ md += `## 3. Estrutura de Módulos\n\n`;
805
+ md += `| Pasta | Tipo | Arquivos |\n|-------|------|----------|\n`;
806
+ const topModules = modules.filter(m => !m.path.includes("/")).sort((a, b) => b.files - a.files);
807
+ for (const m of topModules) {
808
+ md += `| \`${m.path}/\` | ${m.type} | ${m.files} |\n`;
809
+ }
810
+ // Sub-modules (src/)
811
+ const srcModules = modules.filter(m => m.path.startsWith("src/") && m.path.split("/").length === 2);
812
+ if (srcModules.length > 0) {
813
+ md += `\n### Detalhamento src/\n\n`;
814
+ md += `| Pasta | Tipo | Arquivos |\n|-------|------|----------|\n`;
815
+ for (const m of srcModules.sort((a, b) => b.files - a.files)) {
816
+ md += `| \`${m.path}/\` | ${m.type} | ${m.files} |\n`;
817
+ }
818
+ }
819
+ md += `\n`;
820
+ // 4. Dependências
821
+ md += `## 4. Dependências\n\n`;
822
+ for (const dep of deps) {
823
+ const depCount = Object.keys(dep.dependencies).length;
824
+ const devCount = Object.keys(dep.devDependencies).length;
825
+ md += `### ${dep.file} (${dep.type})\n\n`;
826
+ md += `- **Dependências de produção**: ${depCount}\n`;
827
+ md += `- **Dependências de desenvolvimento**: ${devCount}\n\n`;
828
+ if (depCount > 0) {
829
+ md += `| Pacote | Versão |\n|--------|--------|\n`;
830
+ const sorted = Object.entries(dep.dependencies).sort((a, b) => a[0].localeCompare(b[0]));
831
+ for (const [name, version] of sorted) {
832
+ md += `| ${name} | ${version} |\n`;
833
+ }
834
+ md += `\n`;
835
+ }
836
+ if (devCount > 0) {
837
+ md += `**Dev Dependencies:**\n\n`;
838
+ md += `| Pacote | Versão |\n|--------|--------|\n`;
839
+ const sorted = Object.entries(dep.devDependencies).sort((a, b) => a[0].localeCompare(b[0]));
840
+ for (const [name, version] of sorted) {
841
+ md += `| ${name} | ${version} |\n`;
842
+ }
843
+ md += `\n`;
844
+ }
845
+ }
846
+ // 5. APIs e Contratos
847
+ md += `## 5. APIs e Contratos\n\n`;
848
+ if (apis.length === 0) {
849
+ md += `Nenhum endpoint REST/GraphQL detectado automaticamente.\n\n`;
850
+ }
851
+ else {
852
+ md += `${apis.length} endpoints detectados:\n\n`;
853
+ md += `| Método | Path | Arquivo | Linha |\n|--------|------|---------|-------|\n`;
854
+ for (const api of apis.slice(0, 100)) {
855
+ md += `| ${api.method} | \`${api.path}\` | ${api.file} | ${api.line} |\n`;
856
+ }
857
+ if (apis.length > 100) {
858
+ md += `\n> ... e mais ${apis.length - 100} endpoints (truncado)\n`;
859
+ }
860
+ md += `\n`;
861
+ }
862
+ // 6. Métricas
863
+ md += `## 6. Métricas\n\n`;
864
+ md += `| Métrica | Valor |\n|---------|-------|\n`;
865
+ md += `| Total de arquivos | ${metrics.totalFiles.toLocaleString()} |\n`;
866
+ md += `| Arquivos de código | ${metrics.codeFiles.toLocaleString()} |\n`;
867
+ md += `| Arquivos de teste | ${metrics.testFiles.toLocaleString()} |\n`;
868
+ md += `| Arquivos de configuração | ${metrics.configFiles.toLocaleString()} |\n`;
869
+ md += `| Total de linhas de código | ${metrics.totalLines.toLocaleString()} |\n`;
870
+ md += `\n### Distribuição por extensão\n\n`;
871
+ md += `| Extensão | Arquivos |\n|----------|----------|\n`;
872
+ const sortedExt = [...metrics.byExtension.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20);
873
+ for (const [ext, count] of sortedExt) {
874
+ md += `| ${ext} | ${count} |\n`;
875
+ }
876
+ md += `\n`;
877
+ return md;
878
+ }
879
+ // ============================================================
880
+ // MAIN EXPORT
881
+ // ============================================================
882
+ export async function reverseEngineer(req) {
883
+ const { repoDir: inputDir, processId, processDir, outputFilename } = req;
884
+ // Detectar se e URL remota
885
+ const isRemote = isRemoteUrl(inputDir);
886
+ let repoDir;
887
+ let repoName;
888
+ let cleanup;
889
+ if (isRemote) {
890
+ try {
891
+ const cloneResult = await cloneRemoteRepo(inputDir);
892
+ repoDir = cloneResult.localDir;
893
+ repoName = cloneResult.repoName;
894
+ cleanup = cloneResult.cleanup;
895
+ }
896
+ catch (err) {
897
+ return { success: false, message: err.message || `Erro ao clonar: ${inputDir}` };
898
+ }
899
+ }
900
+ else {
901
+ if (!existsSync(inputDir)) {
902
+ return { success: false, message: `Diretório não encontrado: ${inputDir}` };
903
+ }
904
+ repoDir = inputDir;
905
+ repoName = basename(repoDir);
906
+ }
907
+ try {
908
+ // 1. Walk all files
909
+ const allFiles = await walkDir(repoDir);
910
+ if (allFiles.length === 0) {
911
+ return { success: false, message: `Nenhum arquivo encontrado em: ${inputDir}` };
912
+ }
913
+ // 2. Run all analyses in parallel
914
+ const [stack, deps, modules, apis, metrics] = await Promise.all([
915
+ detectStack(repoDir, allFiles),
916
+ analyzeDependencies(repoDir),
917
+ analyzeModules(repoDir),
918
+ detectApis(repoDir, allFiles),
919
+ computeMetrics(repoDir, allFiles),
920
+ ]);
921
+ // 3. Detect architecture patterns
922
+ const patterns = detectArchitecturePatterns(modules, allFiles, repoDir);
923
+ // 4. Generate markdown report
924
+ const displayDir = isRemote ? inputDir : repoDir;
925
+ const report = generateReport(displayDir, stack, deps, modules, apis, patterns, metrics, repoName);
926
+ // 5. Write output file
927
+ const filename = outputFilename || `engenharia-reversa-${repoName}.md`;
928
+ let outputPath;
929
+ if (processDir && existsSync(processDir)) {
930
+ outputPath = join(processDir, filename);
931
+ }
932
+ else if (isRemote) {
933
+ // Repo remoto sem processDir: salva no cwd
934
+ outputPath = join(process.cwd(), filename);
935
+ }
936
+ else {
937
+ outputPath = join(repoDir, filename);
938
+ }
939
+ await writeFile(outputPath, report, "utf-8");
940
+ // 6. Summary
941
+ const topLangs = [...stack.languages.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([l]) => l).join(", ");
942
+ const source = isRemote ? `(remoto) ${repoName}` : repoName;
943
+ const summary = `${source}: ${metrics.codeFiles} code files, ${metrics.totalLines.toLocaleString()} lines, ${topLangs}. ${stack.frameworks.length} frameworks, ${apis.length} endpoints, ${patterns.join(", ")}.`;
944
+ return {
945
+ success: true,
946
+ message: `Engenharia reversa concluída. Artefato salvo em: ${outputPath}`,
947
+ outputFile: outputPath,
948
+ summary,
949
+ };
950
+ }
951
+ finally {
952
+ // Limpar clone temporario
953
+ if (cleanup)
954
+ await cleanup();
955
+ }
956
+ }