archbyte 0.1.0
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 +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Static Analysis — Component Detector
|
|
2
|
+
// Detects project components via workspaces, conventional directories, build configs, or single-app fallback
|
|
3
|
+
import { slugify, assignLayer } from "./utils.js";
|
|
4
|
+
// Map well-known deps to architecturally significant tech names
|
|
5
|
+
const TECH_MAP = {
|
|
6
|
+
react: "React", "react-dom": "React", "next": "Next.js", "vue": "Vue", "nuxt": "Nuxt",
|
|
7
|
+
svelte: "Svelte", angular: "Angular", "@angular/core": "Angular",
|
|
8
|
+
express: "Express", fastify: "Fastify", "@nestjs/core": "NestJS", hono: "Hono", koa: "Koa",
|
|
9
|
+
prisma: "Prisma", "@prisma/client": "Prisma", typeorm: "TypeORM", drizzle: "Drizzle",
|
|
10
|
+
sequelize: "Sequelize", mongoose: "Mongoose", pg: "PostgreSQL", mysql2: "MySQL",
|
|
11
|
+
redis: "Redis", ioredis: "Redis",
|
|
12
|
+
graphql: "GraphQL", "@apollo/server": "Apollo GraphQL",
|
|
13
|
+
"socket.io": "Socket.IO", ws: "WebSocket",
|
|
14
|
+
tailwindcss: "Tailwind CSS", "@xyflow/react": "React Flow",
|
|
15
|
+
commander: "Commander.js", yargs: "Yargs",
|
|
16
|
+
vite: "Vite", webpack: "Webpack", esbuild: "esbuild",
|
|
17
|
+
typescript: "TypeScript",
|
|
18
|
+
kafkajs: "Kafka", amqplib: "RabbitMQ", bullmq: "BullMQ", bull: "Bull",
|
|
19
|
+
stripe: "Stripe", "@stripe/stripe-js": "Stripe",
|
|
20
|
+
playwright: "Playwright", "@playwright/test": "Playwright",
|
|
21
|
+
jest: "Jest", vitest: "Vitest", mocha: "Mocha",
|
|
22
|
+
flutter: "Flutter",
|
|
23
|
+
};
|
|
24
|
+
// Conventional directory → type mapping (hint-based, not exclusive)
|
|
25
|
+
const DIR_TYPE_HINTS = {
|
|
26
|
+
ui: { type: "frontend", label: "UI" },
|
|
27
|
+
web: { type: "frontend", label: "Web App" },
|
|
28
|
+
frontend: { type: "frontend", label: "Frontend" },
|
|
29
|
+
client: { type: "frontend", label: "Client" },
|
|
30
|
+
app: { type: "frontend", label: "App" },
|
|
31
|
+
server: { type: "api", label: "Server" },
|
|
32
|
+
api: { type: "api", label: "API" },
|
|
33
|
+
backend: { type: "api", label: "Backend" },
|
|
34
|
+
gateway: { type: "api", label: "Gateway" },
|
|
35
|
+
cli: { type: "service", label: "CLI" },
|
|
36
|
+
agents: { type: "library", label: "Agents" },
|
|
37
|
+
lib: { type: "library", label: "Library" },
|
|
38
|
+
packages: { type: "library", label: "Packages" },
|
|
39
|
+
shared: { type: "library", label: "Shared" },
|
|
40
|
+
common: { type: "library", label: "Common" },
|
|
41
|
+
cloud: { type: "service", label: "Cloud" },
|
|
42
|
+
infra: { type: "service", label: "Infrastructure" },
|
|
43
|
+
deploy: { type: "service", label: "Deployment" },
|
|
44
|
+
deployment: { type: "service", label: "Deployment" },
|
|
45
|
+
workers: { type: "worker", label: "Workers" },
|
|
46
|
+
jobs: { type: "worker", label: "Jobs" },
|
|
47
|
+
scripts: { type: "service", label: "Scripts" },
|
|
48
|
+
e2e: { type: "service", label: "E2E Tests" },
|
|
49
|
+
tests: { type: "service", label: "Tests" },
|
|
50
|
+
tools: { type: "service", label: "Tools" },
|
|
51
|
+
homepage: { type: "frontend", label: "Homepage" },
|
|
52
|
+
docs: { type: "service", label: "Docs" },
|
|
53
|
+
};
|
|
54
|
+
// Build config files that indicate a directory is a standalone component
|
|
55
|
+
const BUILD_CONFIG_FILES = [
|
|
56
|
+
"package.json",
|
|
57
|
+
"Cargo.toml",
|
|
58
|
+
"go.mod",
|
|
59
|
+
"pyproject.toml",
|
|
60
|
+
"requirements.txt",
|
|
61
|
+
"setup.py",
|
|
62
|
+
"pubspec.yaml",
|
|
63
|
+
"build.gradle",
|
|
64
|
+
"pom.xml",
|
|
65
|
+
"Gemfile",
|
|
66
|
+
"Makefile",
|
|
67
|
+
"Dockerfile",
|
|
68
|
+
"wrangler.toml",
|
|
69
|
+
"tsconfig.json",
|
|
70
|
+
];
|
|
71
|
+
// Skip these dirs — not components
|
|
72
|
+
const SKIP_DIRS = new Set([
|
|
73
|
+
"node_modules", "dist", "build", "target", ".git",
|
|
74
|
+
"coverage", "tmp", ".cache", "venv", "__pycache__",
|
|
75
|
+
]);
|
|
76
|
+
export async function detectComponents(tk, structure) {
|
|
77
|
+
// Strategy 1: Monorepo workspaces
|
|
78
|
+
if (structure.isMonorepo) {
|
|
79
|
+
const components = await detectFromWorkspaces(tk, structure);
|
|
80
|
+
if (components.length > 0)
|
|
81
|
+
return { components };
|
|
82
|
+
}
|
|
83
|
+
// Strategy 2: Scan ALL top-level directories for build configs + conventional names
|
|
84
|
+
const components = await detectAllComponents(tk, structure);
|
|
85
|
+
if (components.length > 0)
|
|
86
|
+
return { components };
|
|
87
|
+
// Strategy 3: Single app fallback
|
|
88
|
+
return { components: [buildSingleAppComponent(structure)] };
|
|
89
|
+
}
|
|
90
|
+
async function detectFromWorkspaces(tk, structure) {
|
|
91
|
+
// Collect workspace patterns from package.json OR pnpm-workspace.yaml
|
|
92
|
+
const workspaces = await resolveWorkspacePatterns(tk);
|
|
93
|
+
if (workspaces.length === 0)
|
|
94
|
+
return [];
|
|
95
|
+
const components = [];
|
|
96
|
+
for (const wsPattern of workspaces) {
|
|
97
|
+
// Skip root workspace (`.`)
|
|
98
|
+
if (wsPattern === ".")
|
|
99
|
+
continue;
|
|
100
|
+
// For glob patterns like "extensions/*", find matching package.json files
|
|
101
|
+
const matches = await tk.globFiles(`${wsPattern}/package.json`);
|
|
102
|
+
for (const match of matches) {
|
|
103
|
+
const wsDir = match.replace(/\/package\.json$/, "");
|
|
104
|
+
const wsPkg = await tk.readJSON(match);
|
|
105
|
+
if (!wsPkg)
|
|
106
|
+
continue;
|
|
107
|
+
const name = wsPkg.name ?? wsDir.split("/").pop() ?? wsDir;
|
|
108
|
+
const type = detectTypeFromDeps(wsPkg);
|
|
109
|
+
const techs = extractTechStack(wsPkg);
|
|
110
|
+
components.push({
|
|
111
|
+
id: slugify(name) ?? wsDir,
|
|
112
|
+
name,
|
|
113
|
+
type,
|
|
114
|
+
layer: assignLayer(type),
|
|
115
|
+
path: wsDir,
|
|
116
|
+
description: wsPkg.description ?? "",
|
|
117
|
+
technologies: techs,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Also detect non-workspace components (Cargo.toml, Python, native apps, etc.)
|
|
122
|
+
const wsPaths = new Set(components.map((c) => c.path));
|
|
123
|
+
const allComponents = await detectAllComponents(tk, structure);
|
|
124
|
+
for (const c of allComponents) {
|
|
125
|
+
if (!wsPaths.has(c.path)) {
|
|
126
|
+
components.push(c);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return components;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve workspace patterns from package.json workspaces OR pnpm-workspace.yaml.
|
|
133
|
+
* Deterministic — just reads config files.
|
|
134
|
+
*/
|
|
135
|
+
async function resolveWorkspacePatterns(tk) {
|
|
136
|
+
// 1. Try package.json workspaces
|
|
137
|
+
const pkg = await tk.readJSON("package.json");
|
|
138
|
+
if (pkg?.workspaces) {
|
|
139
|
+
return Array.isArray(pkg.workspaces)
|
|
140
|
+
? pkg.workspaces
|
|
141
|
+
: (pkg.workspaces.packages ?? []);
|
|
142
|
+
}
|
|
143
|
+
// 2. Try pnpm-workspace.yaml
|
|
144
|
+
const pnpmWs = await tk.readYAML("pnpm-workspace.yaml");
|
|
145
|
+
if (pnpmWs?.packages && Array.isArray(pnpmWs.packages)) {
|
|
146
|
+
return pnpmWs.packages;
|
|
147
|
+
}
|
|
148
|
+
// 3. Try lerna.json
|
|
149
|
+
const lerna = await tk.readJSON("lerna.json");
|
|
150
|
+
if (lerna?.packages && Array.isArray(lerna.packages)) {
|
|
151
|
+
return lerna.packages;
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Scan every top-level directory. If it has a build config, README, Dockerfile,
|
|
157
|
+
* or code files, treat it as a component. Use dir name hints + config file analysis
|
|
158
|
+
* to determine type.
|
|
159
|
+
*/
|
|
160
|
+
async function detectAllComponents(tk, structure) {
|
|
161
|
+
const components = [];
|
|
162
|
+
const dirs = Object.keys(structure.directories);
|
|
163
|
+
for (const dir of dirs) {
|
|
164
|
+
if (SKIP_DIRS.has(dir) || dir.startsWith("."))
|
|
165
|
+
continue;
|
|
166
|
+
const entries = await tk.listDir(dir);
|
|
167
|
+
if (entries.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
const fileNames = entries.map((e) => e.name);
|
|
170
|
+
// Check for build config files
|
|
171
|
+
const hasBuildConfig = BUILD_CONFIG_FILES.some((f) => fileNames.includes(f));
|
|
172
|
+
const hasCode = entries.some((e) => e.type === "file" &&
|
|
173
|
+
/\.(ts|js|tsx|jsx|py|go|rs|java|rb|dart|kt|swift|c|cpp|cs)$/.test(e.name));
|
|
174
|
+
const hasSrcDir = entries.some((e) => e.type === "directory" && (e.name === "src" || e.name === "lib" || e.name === "app"));
|
|
175
|
+
const hasReadme = fileNames.includes("README.md") || fileNames.includes("readme.md");
|
|
176
|
+
// Must have at least some signal that this is a real component
|
|
177
|
+
if (!hasBuildConfig && !hasCode && !hasSrcDir)
|
|
178
|
+
continue;
|
|
179
|
+
// Detect language + type + tech stack from config files
|
|
180
|
+
const detected = await detectFromBuildConfigs(tk, dir, fileNames);
|
|
181
|
+
const hint = DIR_TYPE_HINTS[dir.toLowerCase()];
|
|
182
|
+
const type = detected.type ?? hint?.type ?? "service";
|
|
183
|
+
const label = hint?.label ?? capitalize(dir);
|
|
184
|
+
const name = label;
|
|
185
|
+
// Read description from sub-README if available
|
|
186
|
+
let description = detected.description;
|
|
187
|
+
if (!description && hasReadme) {
|
|
188
|
+
const readme = await tk.readFileSafe(`${dir}/README.md`);
|
|
189
|
+
if (readme) {
|
|
190
|
+
description = extractFirstParagraph(readme);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
components.push({
|
|
194
|
+
id: slugify(dir) ?? dir,
|
|
195
|
+
name,
|
|
196
|
+
type,
|
|
197
|
+
layer: assignLayer(type),
|
|
198
|
+
path: dir,
|
|
199
|
+
description: description ?? "",
|
|
200
|
+
technologies: detected.technologies,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return components;
|
|
204
|
+
}
|
|
205
|
+
async function detectFromBuildConfigs(tk, dir, fileNames) {
|
|
206
|
+
const info = {
|
|
207
|
+
type: null,
|
|
208
|
+
description: "",
|
|
209
|
+
technologies: [],
|
|
210
|
+
language: null,
|
|
211
|
+
};
|
|
212
|
+
// package.json — richest source of info
|
|
213
|
+
if (fileNames.includes("package.json")) {
|
|
214
|
+
const pkg = await tk.readJSON(`${dir}/package.json`);
|
|
215
|
+
if (pkg) {
|
|
216
|
+
info.description = pkg.description ?? "";
|
|
217
|
+
info.type = detectTypeFromDeps(pkg);
|
|
218
|
+
info.technologies = extractTechStack(pkg);
|
|
219
|
+
info.language = "TypeScript/JavaScript";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Cargo.toml — Rust
|
|
223
|
+
if (fileNames.includes("Cargo.toml")) {
|
|
224
|
+
const cargo = await tk.readFileSafe(`${dir}/Cargo.toml`);
|
|
225
|
+
if (cargo) {
|
|
226
|
+
info.language = "Rust";
|
|
227
|
+
info.technologies.push("Rust");
|
|
228
|
+
// Detect type from Cargo deps
|
|
229
|
+
if (cargo.includes("actix") || cargo.includes("axum") || cargo.includes("rocket") || cargo.includes("warp")) {
|
|
230
|
+
info.type = info.type ?? "api";
|
|
231
|
+
if (cargo.includes("axum"))
|
|
232
|
+
info.technologies.push("Axum");
|
|
233
|
+
if (cargo.includes("actix"))
|
|
234
|
+
info.technologies.push("Actix");
|
|
235
|
+
}
|
|
236
|
+
if (cargo.includes("wasm") || cargo.includes("wasm-bindgen")) {
|
|
237
|
+
info.type = info.type ?? "frontend";
|
|
238
|
+
info.technologies.push("WebAssembly");
|
|
239
|
+
}
|
|
240
|
+
if (cargo.includes("clap") || cargo.includes("structopt")) {
|
|
241
|
+
info.type = info.type ?? "service";
|
|
242
|
+
}
|
|
243
|
+
// Extract description
|
|
244
|
+
const descMatch = cargo.match(/description\s*=\s*"([^"]+)"/);
|
|
245
|
+
if (descMatch && !info.description)
|
|
246
|
+
info.description = descMatch[1];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// wrangler.toml — Cloudflare Workers
|
|
250
|
+
if (fileNames.includes("wrangler.toml")) {
|
|
251
|
+
info.technologies.push("Cloudflare Workers");
|
|
252
|
+
info.type = info.type ?? "service";
|
|
253
|
+
}
|
|
254
|
+
// requirements.txt or pyproject.toml — Python
|
|
255
|
+
if (fileNames.includes("requirements.txt") || fileNames.includes("pyproject.toml")) {
|
|
256
|
+
info.language = "Python";
|
|
257
|
+
info.technologies.push("Python");
|
|
258
|
+
const reqs = await tk.readFileSafe(`${dir}/requirements.txt`);
|
|
259
|
+
if (reqs) {
|
|
260
|
+
if (reqs.includes("django")) {
|
|
261
|
+
info.type = info.type ?? "api";
|
|
262
|
+
info.technologies.push("Django");
|
|
263
|
+
}
|
|
264
|
+
if (reqs.includes("fastapi")) {
|
|
265
|
+
info.type = info.type ?? "api";
|
|
266
|
+
info.technologies.push("FastAPI");
|
|
267
|
+
}
|
|
268
|
+
if (reqs.includes("flask")) {
|
|
269
|
+
info.type = info.type ?? "api";
|
|
270
|
+
info.technologies.push("Flask");
|
|
271
|
+
}
|
|
272
|
+
if (reqs.includes("celery")) {
|
|
273
|
+
info.type = info.type ?? "worker";
|
|
274
|
+
info.technologies.push("Celery");
|
|
275
|
+
}
|
|
276
|
+
if (reqs.includes("streamlit")) {
|
|
277
|
+
info.type = info.type ?? "frontend";
|
|
278
|
+
info.technologies.push("Streamlit");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// pubspec.yaml — Dart/Flutter
|
|
283
|
+
if (fileNames.includes("pubspec.yaml")) {
|
|
284
|
+
const pubspec = await tk.readYAML(`${dir}/pubspec.yaml`);
|
|
285
|
+
info.language = "Dart";
|
|
286
|
+
info.technologies.push("Flutter");
|
|
287
|
+
info.type = info.type ?? "frontend";
|
|
288
|
+
if (pubspec?.description) {
|
|
289
|
+
info.description = info.description || pubspec.description;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// go.mod — Go
|
|
293
|
+
if (fileNames.includes("go.mod")) {
|
|
294
|
+
info.language = "Go";
|
|
295
|
+
info.technologies.push("Go");
|
|
296
|
+
const goMod = await tk.readFileSafe(`${dir}/go.mod`);
|
|
297
|
+
if (goMod) {
|
|
298
|
+
if (goMod.includes("gin-gonic")) {
|
|
299
|
+
info.type = info.type ?? "api";
|
|
300
|
+
info.technologies.push("Gin");
|
|
301
|
+
}
|
|
302
|
+
if (goMod.includes("labstack/echo")) {
|
|
303
|
+
info.type = info.type ?? "api";
|
|
304
|
+
info.technologies.push("Echo");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Dockerfile — any language, suggests deployable service
|
|
309
|
+
if (fileNames.includes("Dockerfile")) {
|
|
310
|
+
info.technologies.push("Docker");
|
|
311
|
+
}
|
|
312
|
+
// docker-compose.yml — infrastructure component
|
|
313
|
+
if (fileNames.includes("docker-compose.yml") || fileNames.includes("docker-compose.yaml")) {
|
|
314
|
+
info.technologies.push("Docker Compose");
|
|
315
|
+
}
|
|
316
|
+
// K8s manifests
|
|
317
|
+
if (fileNames.some((f) => f.endsWith(".yaml") || f.endsWith(".yml"))) {
|
|
318
|
+
const entries = await tk.listDir(dir);
|
|
319
|
+
const yamlDirs = entries.filter((e) => e.type === "directory").map((e) => e.name);
|
|
320
|
+
if (yamlDirs.some((d) => ["base", "overlays", "local"].includes(d)) ||
|
|
321
|
+
fileNames.includes("kustomization.yaml")) {
|
|
322
|
+
info.type = info.type ?? "service";
|
|
323
|
+
info.technologies.push("Kubernetes");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Playwright / test config
|
|
327
|
+
if (fileNames.includes("playwright.config.ts") || fileNames.includes("playwright.config.js")) {
|
|
328
|
+
info.type = info.type ?? "service";
|
|
329
|
+
info.technologies.push("Playwright");
|
|
330
|
+
}
|
|
331
|
+
// Keep top 5 techs
|
|
332
|
+
info.technologies = [...new Set(info.technologies)].slice(0, 5);
|
|
333
|
+
return info;
|
|
334
|
+
}
|
|
335
|
+
function buildSingleAppComponent(structure) {
|
|
336
|
+
return {
|
|
337
|
+
id: slugify(structure.projectName) ?? "app",
|
|
338
|
+
name: structure.projectName || "Application",
|
|
339
|
+
type: structure.framework
|
|
340
|
+
? (["React", "Vue", "Svelte", "Angular", "Next.js", "Nuxt"].includes(structure.framework)
|
|
341
|
+
? "frontend"
|
|
342
|
+
: "api")
|
|
343
|
+
: "service",
|
|
344
|
+
layer: "application",
|
|
345
|
+
path: ".",
|
|
346
|
+
description: "",
|
|
347
|
+
technologies: [structure.language, structure.framework].filter(Boolean),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function detectTypeFromDeps(pkg) {
|
|
351
|
+
const deps = Object.keys({
|
|
352
|
+
...pkg.dependencies,
|
|
353
|
+
...pkg.devDependencies,
|
|
354
|
+
});
|
|
355
|
+
// Frontend indicators
|
|
356
|
+
if (deps.some((d) => ["react", "react-dom", "vue", "svelte", "@angular/core", "next", "nuxt"].includes(d))) {
|
|
357
|
+
return "frontend";
|
|
358
|
+
}
|
|
359
|
+
// API indicators
|
|
360
|
+
if (deps.some((d) => ["express", "fastify", "@nestjs/core", "hono", "koa"].includes(d))) {
|
|
361
|
+
return "api";
|
|
362
|
+
}
|
|
363
|
+
// Worker indicators
|
|
364
|
+
if (deps.some((d) => ["bullmq", "bull", "bee-queue"].includes(d))) {
|
|
365
|
+
return "worker";
|
|
366
|
+
}
|
|
367
|
+
// Test indicators
|
|
368
|
+
if (deps.some((d) => ["@playwright/test", "playwright", "cypress"].includes(d))) {
|
|
369
|
+
return "service";
|
|
370
|
+
}
|
|
371
|
+
return "service";
|
|
372
|
+
}
|
|
373
|
+
function extractTechStack(pkg) {
|
|
374
|
+
const deps = Object.keys({
|
|
375
|
+
...pkg.dependencies,
|
|
376
|
+
...pkg.devDependencies,
|
|
377
|
+
});
|
|
378
|
+
const techs = [];
|
|
379
|
+
for (const dep of deps) {
|
|
380
|
+
if (TECH_MAP[dep] && !techs.includes(TECH_MAP[dep])) {
|
|
381
|
+
techs.push(TECH_MAP[dep]);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return techs.slice(0, 5);
|
|
385
|
+
}
|
|
386
|
+
function capitalize(s) {
|
|
387
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
388
|
+
}
|
|
389
|
+
function extractFirstParagraph(readme) {
|
|
390
|
+
const lines = readme.split("\n");
|
|
391
|
+
let capturing = false;
|
|
392
|
+
const descLines = [];
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
if (!capturing && /^#\s+/.test(line)) {
|
|
395
|
+
capturing = true;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (capturing && /^##\s+/.test(line))
|
|
399
|
+
break;
|
|
400
|
+
if (capturing)
|
|
401
|
+
descLines.push(line);
|
|
402
|
+
}
|
|
403
|
+
return descLines.join("\n").trim().replace(/\n{3,}/g, "\n\n").slice(0, 300);
|
|
404
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ConnectionResult, StaticComponent, InfraResult, EventResult } from "./types.js";
|
|
2
|
+
import type { StaticToolkit } from "./utils.js";
|
|
3
|
+
export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult): Promise<ConnectionResult>;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Static Analysis — Connection Mapper
|
|
2
|
+
// Maps connections between components via imports, Docker, K8s, env vars, known SDKs
|
|
3
|
+
// Only deterministic matches — ambiguous mappings are left as gaps for the LLM
|
|
4
|
+
import { slugify } from "./utils.js";
|
|
5
|
+
export async function mapConnections(tk, components, infra, events) {
|
|
6
|
+
const connections = [];
|
|
7
|
+
const componentIds = new Set(components.map((c) => c.id));
|
|
8
|
+
// Run all detection methods in parallel
|
|
9
|
+
await Promise.all([
|
|
10
|
+
detectDockerDependencies(infra, components, connections),
|
|
11
|
+
detectK8sIngress(infra, components, connections),
|
|
12
|
+
detectImportConnections(tk, components, connections),
|
|
13
|
+
detectDatabaseConnections(tk, components, connections),
|
|
14
|
+
detectServerServesUI(tk, components, connections),
|
|
15
|
+
detectKnownSDKConnections(components, connections),
|
|
16
|
+
]);
|
|
17
|
+
// Deduplicate and filter
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const filtered = connections.filter((c) => {
|
|
20
|
+
// Filter self-connections
|
|
21
|
+
if (c.from === c.to)
|
|
22
|
+
return false;
|
|
23
|
+
// Filter connections to/from unknown components
|
|
24
|
+
if (!componentIds.has(c.from) && !componentIds.has(c.to))
|
|
25
|
+
return false;
|
|
26
|
+
// Dedup
|
|
27
|
+
const key = `${c.from}::${c.to}::${c.type}`;
|
|
28
|
+
if (seen.has(key))
|
|
29
|
+
return false;
|
|
30
|
+
seen.add(key);
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
return { connections: filtered, flows: [] };
|
|
34
|
+
}
|
|
35
|
+
async function detectDockerDependencies(infra, components, connections) {
|
|
36
|
+
if (!infra.docker.composeFile)
|
|
37
|
+
return;
|
|
38
|
+
// Build a map from docker service names to component IDs
|
|
39
|
+
// Only uses deterministic matching: build context path resolution + exact name match
|
|
40
|
+
const dockerToComponent = new Map();
|
|
41
|
+
const composeDir = infra.docker.composeFilePath ?? "";
|
|
42
|
+
for (const svc of infra.docker.services) {
|
|
43
|
+
let id = null;
|
|
44
|
+
// 1. Try build context resolved relative to compose file directory (deterministic)
|
|
45
|
+
if (svc.buildContext) {
|
|
46
|
+
const resolvedPath = resolveBuildContext(composeDir, svc.buildContext);
|
|
47
|
+
if (resolvedPath) {
|
|
48
|
+
id = findComponentByPath(resolvedPath, components);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 2. Exact match on service name → component id/path/name
|
|
52
|
+
if (!id) {
|
|
53
|
+
id = findComponentExact(svc.name, components);
|
|
54
|
+
}
|
|
55
|
+
if (id)
|
|
56
|
+
dockerToComponent.set(svc.name, id);
|
|
57
|
+
// If no match found → gap collector will flag this as "unresolved_docker_service"
|
|
58
|
+
}
|
|
59
|
+
for (const svc of infra.docker.services) {
|
|
60
|
+
const fromId = dockerToComponent.get(svc.name);
|
|
61
|
+
if (!fromId)
|
|
62
|
+
continue;
|
|
63
|
+
// depends_on connections
|
|
64
|
+
for (const dep of svc.dependsOn ?? []) {
|
|
65
|
+
const toId = dockerToComponent.get(dep);
|
|
66
|
+
if (toId && fromId !== toId) {
|
|
67
|
+
const depSvc = infra.docker.services.find((s) => s.name === dep);
|
|
68
|
+
const type = inferDockerConnectionType(depSvc?.image);
|
|
69
|
+
connections.push({
|
|
70
|
+
from: fromId,
|
|
71
|
+
to: toId,
|
|
72
|
+
type,
|
|
73
|
+
description: `Docker depends_on: ${svc.name} → ${dep}`,
|
|
74
|
+
confidence: 100,
|
|
75
|
+
async: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Environment variable connections (e.g. SERVICE_BASE_URL: http://other-service:8000)
|
|
80
|
+
if (svc.environment) {
|
|
81
|
+
for (const [, value] of Object.entries(svc.environment)) {
|
|
82
|
+
if (!value)
|
|
83
|
+
continue;
|
|
84
|
+
const urlMatch = value.match(/https?:\/\/([a-z0-9_-]+):\d+/i);
|
|
85
|
+
if (urlMatch) {
|
|
86
|
+
const targetName = urlMatch[1];
|
|
87
|
+
const toId = dockerToComponent.get(targetName) ?? findComponentExact(targetName, components);
|
|
88
|
+
if (toId && fromId !== toId) {
|
|
89
|
+
connections.push({
|
|
90
|
+
from: fromId,
|
|
91
|
+
to: toId,
|
|
92
|
+
type: "http",
|
|
93
|
+
description: `Docker env: ${svc.name} → ${targetName} (${value})`,
|
|
94
|
+
confidence: 100,
|
|
95
|
+
async: false,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function detectK8sIngress(infra, components, connections) {
|
|
104
|
+
const ingresses = infra.kubernetes.resources.filter((r) => r.kind === "Ingress");
|
|
105
|
+
const services = infra.kubernetes.resources.filter((r) => r.kind === "Service");
|
|
106
|
+
for (const ingress of ingresses) {
|
|
107
|
+
for (const svc of services) {
|
|
108
|
+
const toId = findComponentExact(svc.name, components);
|
|
109
|
+
if (toId) {
|
|
110
|
+
connections.push({
|
|
111
|
+
from: ingress.name,
|
|
112
|
+
to: toId,
|
|
113
|
+
type: "http",
|
|
114
|
+
description: `K8s ingress → ${svc.name}`,
|
|
115
|
+
confidence: 100,
|
|
116
|
+
async: false,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function detectImportConnections(tk, components, connections) {
|
|
123
|
+
for (const source of components) {
|
|
124
|
+
if (source.path === ".")
|
|
125
|
+
continue;
|
|
126
|
+
const connectedTargets = new Set();
|
|
127
|
+
for (const target of components) {
|
|
128
|
+
if (target.id === source.id)
|
|
129
|
+
continue;
|
|
130
|
+
if (target.path === ".")
|
|
131
|
+
continue;
|
|
132
|
+
const results = await tk.grepFiles(`from .*\\.\\./${target.path}/`, source.path);
|
|
133
|
+
if (results.length > 0 && !connectedTargets.has(target.id)) {
|
|
134
|
+
connectedTargets.add(target.id);
|
|
135
|
+
connections.push({
|
|
136
|
+
from: source.id,
|
|
137
|
+
to: target.id,
|
|
138
|
+
type: "import",
|
|
139
|
+
description: `Import: ${source.name} → ${target.name}`,
|
|
140
|
+
confidence: 90,
|
|
141
|
+
async: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function detectDatabaseConnections(tk, components, connections) {
|
|
148
|
+
const dbComp = components.find((c) => c.type === "database" || c.layer === "data");
|
|
149
|
+
for (const comp of components) {
|
|
150
|
+
if (comp.path === ".")
|
|
151
|
+
continue;
|
|
152
|
+
if (comp.id === dbComp?.id)
|
|
153
|
+
continue;
|
|
154
|
+
const dbResults = await tk.grepFiles("DATABASE_URL|DB_HOST|MONGO_URI|REDIS_URL", comp.path);
|
|
155
|
+
if (dbResults.length > 0 && dbComp) {
|
|
156
|
+
connections.push({
|
|
157
|
+
from: comp.id,
|
|
158
|
+
to: dbComp.id,
|
|
159
|
+
type: "database",
|
|
160
|
+
description: `Database connection from ${comp.name}`,
|
|
161
|
+
confidence: 95,
|
|
162
|
+
async: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function detectServerServesUI(tk, components, connections) {
|
|
168
|
+
const serverComp = components.find((c) => c.type === "api");
|
|
169
|
+
const uiComp = components.find((c) => c.type === "frontend");
|
|
170
|
+
if (!serverComp || !uiComp)
|
|
171
|
+
return;
|
|
172
|
+
const staticResults = await tk.grepFiles("ui/dist|express\\.static|serveStatic|sendFile", serverComp.path);
|
|
173
|
+
if (staticResults.length > 0) {
|
|
174
|
+
connections.push({
|
|
175
|
+
from: serverComp.id,
|
|
176
|
+
to: uiComp.id,
|
|
177
|
+
type: "http",
|
|
178
|
+
description: `${serverComp.name} serves ${uiComp.name} static files`,
|
|
179
|
+
confidence: 85,
|
|
180
|
+
async: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const apiResults = await tk.grepFiles("fetch\\(|axios|useQuery|EventSource", uiComp.path);
|
|
184
|
+
if (apiResults.length > 0) {
|
|
185
|
+
connections.push({
|
|
186
|
+
from: uiComp.id,
|
|
187
|
+
to: serverComp.id,
|
|
188
|
+
type: "http",
|
|
189
|
+
description: `${uiComp.name} calls ${serverComp.name} API`,
|
|
190
|
+
confidence: 80,
|
|
191
|
+
async: false,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function detectKnownSDKConnections(components, connections) {
|
|
196
|
+
const externalTechs = {
|
|
197
|
+
Stripe: "stripe-api",
|
|
198
|
+
Firebase: "firebase",
|
|
199
|
+
Supabase: "supabase",
|
|
200
|
+
Redis: "redis",
|
|
201
|
+
Kafka: "kafka",
|
|
202
|
+
RabbitMQ: "rabbitmq",
|
|
203
|
+
"Socket.IO": "websocket",
|
|
204
|
+
PostgreSQL: "database",
|
|
205
|
+
MySQL: "database",
|
|
206
|
+
MongoDB: "database",
|
|
207
|
+
};
|
|
208
|
+
for (const comp of components) {
|
|
209
|
+
for (const tech of comp.technologies) {
|
|
210
|
+
const externalId = externalTechs[tech];
|
|
211
|
+
if (externalId) {
|
|
212
|
+
const target = components.find((c) => c.id !== comp.id && (c.technologies.includes(tech) ||
|
|
213
|
+
c.type === "database" && (externalId === "database")));
|
|
214
|
+
if (target) {
|
|
215
|
+
connections.push({
|
|
216
|
+
from: comp.id,
|
|
217
|
+
to: target.id,
|
|
218
|
+
type: externalId === "database" ? "database" : "http",
|
|
219
|
+
description: `${comp.name} uses ${tech}`,
|
|
220
|
+
confidence: 95,
|
|
221
|
+
async: false,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Exact match only: id, slug, path, or name must match exactly.
|
|
230
|
+
* No fuzzy matching, synonyms, or partial matches — those are for the LLM.
|
|
231
|
+
*/
|
|
232
|
+
function findComponentExact(name, components) {
|
|
233
|
+
const lower = name.toLowerCase();
|
|
234
|
+
const slug = slugify(name);
|
|
235
|
+
const match = components.find((c) => c.id === slug ||
|
|
236
|
+
c.id === lower ||
|
|
237
|
+
c.path.toLowerCase() === lower ||
|
|
238
|
+
c.name.toLowerCase() === lower);
|
|
239
|
+
return match?.id ?? null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a docker build context path relative to the compose file directory.
|
|
243
|
+
* Pure path arithmetic — deterministic.
|
|
244
|
+
*/
|
|
245
|
+
function resolveBuildContext(composeDir, context) {
|
|
246
|
+
const ctx = context.replace(/\/$/, "");
|
|
247
|
+
if (ctx === "." || ctx === "./") {
|
|
248
|
+
return composeDir || null;
|
|
249
|
+
}
|
|
250
|
+
if (ctx.startsWith("../")) {
|
|
251
|
+
const remaining = ctx.replace(/^\.\.\//, "");
|
|
252
|
+
return remaining || null;
|
|
253
|
+
}
|
|
254
|
+
if (ctx.startsWith("./")) {
|
|
255
|
+
const remaining = ctx.replace(/^\.\//, "");
|
|
256
|
+
return composeDir ? `${composeDir}/${remaining}` : remaining;
|
|
257
|
+
}
|
|
258
|
+
return composeDir ? `${composeDir}/${ctx}` : ctx;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Find a component by its directory path (exact match).
|
|
262
|
+
*/
|
|
263
|
+
function findComponentByPath(dirPath, components) {
|
|
264
|
+
const lower = dirPath.toLowerCase();
|
|
265
|
+
const comp = components.find((c) => c.path.toLowerCase() === lower);
|
|
266
|
+
return comp?.id ?? null;
|
|
267
|
+
}
|
|
268
|
+
function inferDockerConnectionType(image) {
|
|
269
|
+
if (!image)
|
|
270
|
+
return "docker";
|
|
271
|
+
const img = image.toLowerCase();
|
|
272
|
+
if (img.includes("postgres") || img.includes("mysql") || img.includes("mongo") || img.includes("mariadb")) {
|
|
273
|
+
return "database";
|
|
274
|
+
}
|
|
275
|
+
if (img.includes("redis") || img.includes("memcached"))
|
|
276
|
+
return "cache";
|
|
277
|
+
if (img.includes("rabbit") || img.includes("kafka") || img.includes("nats"))
|
|
278
|
+
return "queue";
|
|
279
|
+
return "docker";
|
|
280
|
+
}
|