@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.
- package/README.md +41 -0
- package/dist/config/cloud-proxy.d.ts +15 -0
- package/dist/config/cloud-proxy.js +63 -0
- package/dist/config/cloudwatch-store.d.ts +13 -0
- package/dist/config/cloudwatch-store.js +66 -0
- package/dist/config/license.d.ts +29 -0
- package/dist/config/license.js +165 -0
- package/dist/config/ssm-store.d.ts +2 -0
- package/dist/config/ssm-store.js +38 -0
- package/dist/config/telemetry.d.ts +17 -0
- package/dist/config/telemetry.js +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +460 -0
- package/dist/knowledge/dynamo-store.d.ts +17 -0
- package/dist/knowledge/dynamo-store.js +85 -0
- package/dist/knowledge/embeddings.d.ts +2 -0
- package/dist/knowledge/embeddings.js +36 -0
- package/dist/knowledge/loader.d.ts +8 -0
- package/dist/knowledge/loader.js +57 -0
- package/dist/lambda-package.zip +0 -0
- package/dist/lambda.d.ts +22 -0
- package/dist/lambda.js +496 -0
- package/dist/package.json +1 -0
- package/dist/tools/advance-process.d.ts +7 -0
- package/dist/tools/advance-process.js +128 -0
- package/dist/tools/analyze-code.d.ts +7 -0
- package/dist/tools/analyze-code.js +131 -0
- package/dist/tools/analyze-docs.d.ts +8 -0
- package/dist/tools/analyze-docs.js +147 -0
- package/dist/tools/config-registry.d.ts +3 -0
- package/dist/tools/config-registry.js +20 -0
- package/dist/tools/create-process.d.ts +6 -0
- package/dist/tools/create-process.js +257 -0
- package/dist/tools/decompose-epic.d.ts +7 -0
- package/dist/tools/decompose-epic.js +603 -0
- package/dist/tools/diagrams.d.ts +51 -0
- package/dist/tools/diagrams.js +304 -0
- package/dist/tools/generate-report.d.ts +9 -0
- package/dist/tools/generate-report.js +891 -0
- package/dist/tools/generate-wiki.d.ts +10 -0
- package/dist/tools/generate-wiki.js +700 -0
- package/dist/tools/get-architecture.d.ts +6 -0
- package/dist/tools/get-architecture.js +78 -0
- package/dist/tools/get-code-standards.d.ts +7 -0
- package/dist/tools/get-code-standards.js +52 -0
- package/dist/tools/init-process.d.ts +7 -0
- package/dist/tools/init-process.js +82 -0
- package/dist/tools/knowledge-crud.d.ts +26 -0
- package/dist/tools/knowledge-crud.js +142 -0
- package/dist/tools/logo-base64.d.ts +1 -0
- package/dist/tools/logo-base64.js +1 -0
- package/dist/tools/logs-query.d.ts +15 -0
- package/dist/tools/logs-query.js +46 -0
- package/dist/tools/reverse-engineer.d.ts +13 -0
- package/dist/tools/reverse-engineer.js +956 -0
- package/dist/tools/semantic-search.d.ts +7 -0
- package/dist/tools/semantic-search.js +68 -0
- package/dist/tools/update-process.d.ts +17 -0
- package/dist/tools/update-process.js +195 -0
- package/dist/tools/validate-idea.d.ts +7 -0
- package/dist/tools/validate-idea.js +339 -0
- package/dist/tools/validate-process.d.ts +6 -0
- package/dist/tools/validate-process.js +102 -0
- 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
|
+
}
|