@nikelyh/metamorph 2.1.1 → 2.2.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 CHANGED
@@ -20,10 +20,15 @@ Metamorph is a CLI tool powered by [Mozaik](https://github.com/jigjoy-ai/mozaik)
20
20
 
21
21
  ## Features
22
22
 
23
+ - **Project Intelligence Engine (PIE)** -- Autonomously detects project architecture, frameworks, dynamic entrypoints, and monorepo workspace topologies (Turborepo, pnpm workspaces, npm/yarn workspaces, Lerna).
24
+ - **Polymorphic Package Manager (PPME)** -- Automatically detects and adapts to your chosen package manager (`npm`, `pnpm`, `yarn`, `bun`) without host process leakage.
25
+ - **Structural Validation Plugins** -- Deterministic shadow workspace assertions, scaffold verifications, and repair heuristics for both frontend (Vue, Svelte, Angular) and backend (Express, Fastify, NestJS) migration targets.
26
+ - **Cascading Configuration System** -- Tiered configuration management via `metamorph config` across CLI flags, env vars, `.metamorphrc.json`, and global user defaults.
27
+ - **Token Accounting & Real-Time Cost Estimation** -- Live token tracking per model and estimated inference costs persisted in SQLite and displayed in the dashboard.
23
28
  - **Multi-Agent Concurrency** -- Utilizes Mapper, Worker, Reviewer, Integration, and Coordinator agents to migrate code in parallel.
24
- - **Zero Risk** -- All migrations happen in a `.metamorph/shadow` workspace. Your original code is completely untouched until you explicitly approve and apply the changes.
29
+ - **Zero Risk (Shadow Workspace)** -- All migrations happen in an isolated `.metamorph/shadow` workspace. Your original code is completely untouched until you explicitly approve and apply the changes.
30
+ - **Shadow Build Verification** -- Every migration is validated with your package manager (`npm`, `pnpm`, `yarn`, `bun`) and verification build inside the shadow workspace before completion.
25
31
  - **Visual Dashboard** -- Watch your agents work in real-time through the built-in local dashboard UI.
26
- - **Shadow Build Verification** -- Every migration is validated with `npm install` and `npm run build` inside the shadow workspace before completion.
27
32
  - **Extensive Framework Support** -- Easily migrate between popular frontend and backend frameworks.
28
33
 
29
34
  ## Installation
@@ -96,6 +101,25 @@ Clear the local database and reset all events:
96
101
  metamorph reset
97
102
  ```
98
103
 
104
+ ### 5. Managing Configuration
105
+
106
+ Metamorph provides a tiered configuration system that resolves settings across CLI flags, environment variables, local `.metamorphrc.json`, and global user config (`~/.metamorphrc.json`):
107
+
108
+ ```bash
109
+ # View all effective configuration values and their sources
110
+ metamorph config list
111
+
112
+ # Set a persistent config value (e.g. LLM model or concurrency)
113
+ metamorph config set model "gpt-4o"
114
+ metamorph config set concurrency 3
115
+
116
+ # Disable specific swarm agents for specialized runs
117
+ metamorph config set disabledAgents "reporter"
118
+
119
+ # Inspect the resolved config path
120
+ metamorph config path
121
+ ```
122
+
99
123
  ### Utility Commands
100
124
 
101
125
  Test the technology detection on your repository without starting a migration:
@@ -120,4 +144,4 @@ See the complete, up-to-date list of all supported frontend and backend framewor
120
144
 
121
145
  ---
122
146
 
123
- *Built with [Mozaik](https://github.com/jigjoy-ai/mozaik).*
147
+ *Built with [Mozaik](https://github.com/jigjoy-ai/mozaik).* 💖
@@ -0,0 +1,16 @@
1
+ import {
2
+ detectMonorepo,
3
+ detectTechnologies,
4
+ inspectManifest,
5
+ inspectProject,
6
+ inspectStructure,
7
+ resolveProjectRoot
8
+ } from "./chunk-4G3X6IOY.js";
9
+ export {
10
+ detectMonorepo,
11
+ detectTechnologies,
12
+ inspectManifest,
13
+ inspectProject,
14
+ inspectStructure,
15
+ resolveProjectRoot
16
+ };
@@ -0,0 +1,533 @@
1
+ // ../infrastructure/src/detector/WorkspaceResolver.ts
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
3
+ import { join, resolve, dirname } from "path";
4
+ function resolveProjectRoot(startPath) {
5
+ let current = resolve(startPath);
6
+ const root = resolve("/");
7
+ while (current && current !== root && current !== dirname(current)) {
8
+ if (existsSync(join(current, "package.json"))) {
9
+ return current;
10
+ }
11
+ if (existsSync(join(current, ".git"))) {
12
+ break;
13
+ }
14
+ current = dirname(current);
15
+ }
16
+ return resolve(startPath);
17
+ }
18
+ function findWorkspacePackages(rootDir, patterns) {
19
+ const packages = [];
20
+ for (const pattern of patterns) {
21
+ const cleanPattern = pattern.replace(/[\\/]+$/, "");
22
+ if (cleanPattern.endsWith("/*")) {
23
+ const parentDir = join(rootDir, cleanPattern.slice(0, -2));
24
+ if (existsSync(parentDir)) {
25
+ try {
26
+ const entries = readdirSync(parentDir);
27
+ for (const entry of entries) {
28
+ const subDir = join(parentDir, entry);
29
+ const pkgJsonPath = join(subDir, "package.json");
30
+ if (existsSync(pkgJsonPath) && statSync(subDir).isDirectory()) {
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
33
+ packages.push({
34
+ name: pkg.name || entry,
35
+ relativePath: join(cleanPattern.slice(0, -2), entry).replace(/\\/g, "/"),
36
+ absolutePath: subDir
37
+ });
38
+ } catch {
39
+ }
40
+ }
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ } else {
46
+ const subDir = join(rootDir, cleanPattern);
47
+ const pkgJsonPath = join(subDir, "package.json");
48
+ if (existsSync(pkgJsonPath) && statSync(subDir).isDirectory()) {
49
+ try {
50
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
51
+ packages.push({
52
+ name: pkg.name || cleanPattern,
53
+ relativePath: cleanPattern.replace(/\\/g, "/"),
54
+ absolutePath: subDir
55
+ });
56
+ } catch {
57
+ }
58
+ }
59
+ }
60
+ }
61
+ return packages;
62
+ }
63
+ function detectMonorepo(projectPath) {
64
+ const rootPath = resolve(projectPath);
65
+ const pkgPath = join(rootPath, "package.json");
66
+ let tool;
67
+ let patterns = [];
68
+ const pnpmWorkspacePath = join(rootPath, "pnpm-workspace.yaml");
69
+ if (existsSync(pnpmWorkspacePath)) {
70
+ tool = "pnpm";
71
+ try {
72
+ const content = readFileSync(pnpmWorkspacePath, "utf-8");
73
+ const lines = content.split("\n");
74
+ let inPackages = false;
75
+ for (const line of lines) {
76
+ const trimmed = line.trim();
77
+ if (trimmed.startsWith("packages:")) {
78
+ inPackages = true;
79
+ continue;
80
+ }
81
+ if (inPackages) {
82
+ if (trimmed.startsWith("-")) {
83
+ const pattern = trimmed.replace(/^-\s*['"]?/, "").replace(/['"]?\s*$/, "");
84
+ if (pattern) patterns.push(pattern);
85
+ } else if (trimmed && !trimmed.startsWith("#")) {
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ }
92
+ }
93
+ if (existsSync(pkgPath)) {
94
+ try {
95
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
96
+ if (pkg.workspaces) {
97
+ if (Array.isArray(pkg.workspaces)) {
98
+ patterns.push(...pkg.workspaces);
99
+ } else if (Array.isArray(pkg.workspaces.packages)) {
100
+ patterns.push(...pkg.workspaces.packages);
101
+ }
102
+ if (!tool) tool = "npm";
103
+ }
104
+ } catch {
105
+ }
106
+ }
107
+ if (existsSync(join(rootPath, "turbo.json"))) {
108
+ tool = "turbo";
109
+ }
110
+ if (existsSync(join(rootPath, "lerna.json"))) {
111
+ if (!tool) tool = "lerna";
112
+ try {
113
+ const lerna = JSON.parse(readFileSync(join(rootPath, "lerna.json"), "utf-8"));
114
+ if (Array.isArray(lerna.packages)) {
115
+ patterns.push(...lerna.packages);
116
+ }
117
+ } catch {
118
+ }
119
+ }
120
+ if (existsSync(join(rootPath, "nx.json"))) {
121
+ if (!tool) tool = "nx";
122
+ }
123
+ if (tool && patterns.length === 0) {
124
+ patterns = ["apps/*", "packages/*", "services/*"];
125
+ }
126
+ const uniquePatterns = Array.from(new Set(patterns));
127
+ const packages = findWorkspacePackages(rootPath, uniquePatterns);
128
+ return {
129
+ isMonorepo: !!tool || packages.length > 0,
130
+ tool,
131
+ packages,
132
+ rootPath
133
+ };
134
+ }
135
+
136
+ // ../infrastructure/src/detector/ManifestInspector.ts
137
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
138
+ import { join as join2 } from "path";
139
+ function inspectManifest(projectPath) {
140
+ const pkgPath = join2(projectPath, "package.json");
141
+ let rawPackageJson;
142
+ let dependencies = {};
143
+ let devDependencies = {};
144
+ let scripts = {};
145
+ if (existsSync2(pkgPath)) {
146
+ try {
147
+ rawPackageJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
148
+ dependencies = rawPackageJson?.dependencies || {};
149
+ devDependencies = rawPackageJson?.devDependencies || {};
150
+ scripts = rawPackageJson?.scripts || {};
151
+ } catch {
152
+ }
153
+ }
154
+ const allDependencies = {
155
+ ...devDependencies,
156
+ ...dependencies
157
+ };
158
+ let packageManager = "npm";
159
+ if (existsSync2(join2(projectPath, "pnpm-lock.yaml"))) {
160
+ packageManager = "pnpm";
161
+ } else if (existsSync2(join2(projectPath, "yarn.lock"))) {
162
+ packageManager = "yarn";
163
+ } else if (existsSync2(join2(projectPath, "bun.lockb")) || existsSync2(join2(projectPath, "bun.lock"))) {
164
+ packageManager = "bun";
165
+ } else if (rawPackageJson?.packageManager && typeof rawPackageJson.packageManager === "string") {
166
+ const pm = rawPackageJson.packageManager.toLowerCase();
167
+ if (pm.startsWith("pnpm")) packageManager = "pnpm";
168
+ else if (pm.startsWith("yarn")) packageManager = "yarn";
169
+ else if (pm.startsWith("bun")) packageManager = "bun";
170
+ }
171
+ let hasTsConfig = false;
172
+ let pathAliases = {};
173
+ const tsConfigPath = join2(projectPath, "tsconfig.json");
174
+ if (existsSync2(tsConfigPath)) {
175
+ hasTsConfig = true;
176
+ try {
177
+ const content = readFileSync2(tsConfigPath, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/g, "");
178
+ const tsconfig = JSON.parse(content);
179
+ if (tsconfig?.compilerOptions?.paths) {
180
+ pathAliases = tsconfig.compilerOptions.paths;
181
+ }
182
+ } catch {
183
+ }
184
+ }
185
+ const hasTsDependency = !!(allDependencies["typescript"] || allDependencies["ts-node"] || allDependencies["tsx"]);
186
+ const language = hasTsConfig || hasTsDependency ? "typescript" : "javascript";
187
+ return {
188
+ rawPackageJson,
189
+ allDependencies,
190
+ dependencies,
191
+ devDependencies,
192
+ scripts,
193
+ packageManager,
194
+ hasTsConfig,
195
+ language,
196
+ pathAliases
197
+ };
198
+ }
199
+
200
+ // ../infrastructure/src/detector/StructureInspector.ts
201
+ import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
202
+ import { join as join3 } from "path";
203
+ function hasAnyFile(projectPath, relativePaths) {
204
+ for (const rel of relativePaths) {
205
+ if (existsSync3(join3(projectPath, rel))) {
206
+ return rel;
207
+ }
208
+ }
209
+ return void 0;
210
+ }
211
+ function scanCharacteristicExtensions(projectPath) {
212
+ const counts = {
213
+ vue: 0,
214
+ svelte: 0,
215
+ reactJsx: 0,
216
+ nestDecorators: 0
217
+ };
218
+ const srcDir = existsSync3(join3(projectPath, "src")) ? join3(projectPath, "src") : projectPath;
219
+ if (!existsSync3(srcDir)) return counts;
220
+ function traverse(dir, depth) {
221
+ if (depth > 3) return;
222
+ try {
223
+ const entries = readdirSync2(dir);
224
+ for (const entry of entries) {
225
+ if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === ".next") {
226
+ continue;
227
+ }
228
+ const fullPath = join3(dir, entry);
229
+ try {
230
+ const stat = statSync2(fullPath);
231
+ if (stat.isDirectory()) {
232
+ traverse(fullPath, depth + 1);
233
+ } else if (stat.isFile()) {
234
+ const lower = entry.toLowerCase();
235
+ if (lower.endsWith(".vue")) counts.vue++;
236
+ else if (lower.endsWith(".svelte")) counts.svelte++;
237
+ else if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) counts.reactJsx++;
238
+ else if (lower.endsWith(".controller.ts") || lower.endsWith(".module.ts")) counts.nestDecorators++;
239
+ }
240
+ } catch {
241
+ }
242
+ }
243
+ } catch {
244
+ }
245
+ }
246
+ traverse(srcDir, 0);
247
+ return counts;
248
+ }
249
+ function inspectStructure(projectPath, deps, scripts) {
250
+ const existingConfigFiles = [];
251
+ const candidateConfigs = [
252
+ "next.config.js",
253
+ "next.config.mjs",
254
+ "next.config.ts",
255
+ "next.config.cjs",
256
+ "vite.config.ts",
257
+ "vite.config.js",
258
+ "vite.config.mjs",
259
+ "vite.config.cjs",
260
+ "svelte.config.js",
261
+ "svelte.config.ts",
262
+ "angular.json",
263
+ "nest-cli.json",
264
+ "webpack.config.js",
265
+ "webpack.config.ts",
266
+ "vue.config.js"
267
+ ];
268
+ for (const config of candidateConfigs) {
269
+ if (existsSync3(join3(projectPath, config))) {
270
+ existingConfigFiles.push(config);
271
+ }
272
+ }
273
+ let bundler = "unknown";
274
+ if (existingConfigFiles.some((c) => c.startsWith("vite.config"))) {
275
+ bundler = "vite";
276
+ } else if (existingConfigFiles.includes("angular.json")) {
277
+ bundler = "angular-cli";
278
+ } else if (deps["next"] || existingConfigFiles.some((c) => c.startsWith("next.config"))) {
279
+ const scriptValues = Object.values(scripts).join(" ");
280
+ bundler = scriptValues.includes("--turbo") ? "turbopack" : "webpack";
281
+ } else if (existingConfigFiles.some((c) => c.startsWith("webpack.config")) || deps["webpack"]) {
282
+ bundler = "webpack";
283
+ } else if (deps["vite"]) {
284
+ bundler = "vite";
285
+ } else if (deps["esbuild"]) {
286
+ bundler = "esbuild";
287
+ } else if (deps["rollup"]) {
288
+ bundler = "rollup";
289
+ }
290
+ let routerVariant = "none";
291
+ const hasAppRouter = hasAnyFile(projectPath, [
292
+ "src/app/layout.tsx",
293
+ "src/app/layout.js",
294
+ "src/app/page.tsx",
295
+ "src/app/page.js",
296
+ "app/layout.tsx",
297
+ "app/layout.js",
298
+ "app/page.tsx",
299
+ "app/page.js"
300
+ ]);
301
+ const hasPagesRouter = hasAnyFile(projectPath, [
302
+ "src/pages/_app.tsx",
303
+ "src/pages/_app.js",
304
+ "src/pages/index.tsx",
305
+ "src/pages/index.js",
306
+ "pages/_app.tsx",
307
+ "pages/_app.js",
308
+ "pages/index.tsx",
309
+ "pages/index.js"
310
+ ]);
311
+ if (hasAppRouter) {
312
+ routerVariant = "next-app-router";
313
+ } else if (hasPagesRouter) {
314
+ routerVariant = "next-pages-router";
315
+ } else if (hasAnyFile(projectPath, ["src/routes/+page.svelte", "src/routes/+layout.svelte", "routes/+page.svelte"]) || deps["@sveltejs/kit"]) {
316
+ routerVariant = "sveltekit";
317
+ } else if (deps["vue-router"] || hasAnyFile(projectPath, ["src/router/index.ts", "src/router/index.js"])) {
318
+ routerVariant = "vue-router";
319
+ } else if (deps["react-router-dom"] || deps["react-router"] || deps["@remix-run/react"]) {
320
+ routerVariant = "react-router";
321
+ } else if (deps["@angular/router"]) {
322
+ routerVariant = "angular-router";
323
+ }
324
+ const extensionCounts = scanCharacteristicExtensions(projectPath);
325
+ return {
326
+ routerVariant,
327
+ bundler,
328
+ existingConfigFiles,
329
+ extensionCounts
330
+ };
331
+ }
332
+
333
+ // ../infrastructure/src/detector/rules/detectionRules.ts
334
+ var FRAMEWORK_DETECTION_RULES = [
335
+ {
336
+ framework: "next",
337
+ category: "frontend-meta",
338
+ packageKeys: ["next"],
339
+ configPrefixes: ["next.config"],
340
+ routerVariants: ["next-app-router", "next-pages-router"],
341
+ subsumes: ["react", "react-dom"],
342
+ suggestedTargets: ["react", "vue", "angular", "svelte"]
343
+ },
344
+ {
345
+ framework: "react",
346
+ category: "frontend-spa",
347
+ packageKeys: ["react"],
348
+ configPrefixes: ["vite.config", "webpack.config"],
349
+ routerVariants: ["react-router"],
350
+ suggestedTargets: ["next", "vue", "angular", "svelte"]
351
+ },
352
+ {
353
+ framework: "vue",
354
+ category: "frontend-spa",
355
+ packageKeys: ["vue"],
356
+ devPackageKeys: ["@vitejs/plugin-vue"],
357
+ configPrefixes: ["vue.config", "vite.config"],
358
+ routerVariants: ["vue-router"],
359
+ suggestedTargets: ["react", "next", "angular", "svelte"]
360
+ },
361
+ {
362
+ framework: "svelte",
363
+ category: "frontend-spa",
364
+ packageKeys: ["svelte", "@sveltejs/kit"],
365
+ devPackageKeys: ["@sveltejs/vite-plugin-svelte"],
366
+ configPrefixes: ["svelte.config", "vite.config"],
367
+ routerVariants: ["sveltekit"],
368
+ suggestedTargets: ["react", "next", "vue", "angular"]
369
+ },
370
+ {
371
+ framework: "angular",
372
+ category: "frontend-spa",
373
+ packageKeys: ["@angular/core"],
374
+ devPackageKeys: ["@angular/cli", "@angular-devkit/build-angular"],
375
+ configPrefixes: ["angular.json"],
376
+ routerVariants: ["angular-router"],
377
+ suggestedTargets: ["react", "next", "vue", "svelte"]
378
+ },
379
+ {
380
+ framework: "nestjs",
381
+ category: "backend-api",
382
+ packageKeys: ["@nestjs/core", "@nestjs/common"],
383
+ configPrefixes: ["nest-cli.json"],
384
+ subsumes: ["express", "fastify", "@nestjs/platform-express", "@nestjs/platform-fastify"],
385
+ suggestedTargets: ["express", "fastify"]
386
+ },
387
+ {
388
+ framework: "express",
389
+ category: "backend-api",
390
+ packageKeys: ["express"],
391
+ devPackageKeys: ["@types/express"],
392
+ configPrefixes: [],
393
+ suggestedTargets: ["fastify", "nestjs"]
394
+ },
395
+ {
396
+ framework: "fastify",
397
+ category: "backend-api",
398
+ packageKeys: ["fastify"],
399
+ devPackageKeys: ["fastify-plugin"],
400
+ configPrefixes: [],
401
+ suggestedTargets: ["express", "nestjs"]
402
+ }
403
+ ];
404
+
405
+ // ../infrastructure/src/detector/SubsumptionEngine.ts
406
+ function evaluateAndDisambiguate(manifest, structure) {
407
+ const candidates = [];
408
+ for (const rule of FRAMEWORK_DETECTION_RULES) {
409
+ const hasPackageMatch = rule.packageKeys.some((k) => manifest.dependencies[k] || manifest.devDependencies[k]) || rule.devPackageKeys && rule.devPackageKeys.some((k) => manifest.devDependencies[k]) || rule.framework === "vue" && structure.extensionCounts.vue > 0 || rule.framework === "svelte" && structure.extensionCounts.svelte > 0 || rule.framework === "nestjs" && structure.extensionCounts.nestDecorators > 0;
410
+ const hasDedicatedConfig = structure.existingConfigFiles.some(
411
+ (c) => rule.configPrefixes.some((p) => p !== "vite.config" && p !== "webpack.config" && c.startsWith(p))
412
+ );
413
+ if (!hasPackageMatch && !hasDedicatedConfig) {
414
+ continue;
415
+ }
416
+ let score = 0;
417
+ const evidence = [];
418
+ const subsumed = [];
419
+ for (const key of rule.packageKeys) {
420
+ if (manifest.dependencies[key]) {
421
+ score += 50;
422
+ evidence.push(`dependencies: "${key}": "${manifest.dependencies[key]}"`);
423
+ } else if (manifest.devDependencies[key]) {
424
+ score += 35;
425
+ evidence.push(`devDependencies: "${key}": "${manifest.devDependencies[key]}"`);
426
+ }
427
+ }
428
+ if (rule.devPackageKeys) {
429
+ for (const devKey of rule.devPackageKeys) {
430
+ if (manifest.devDependencies[devKey]) {
431
+ score += 25;
432
+ evidence.push(`devTooling: "${devKey}": "${manifest.devDependencies[devKey]}"`);
433
+ }
434
+ }
435
+ }
436
+ for (const prefix of rule.configPrefixes) {
437
+ const match = structure.existingConfigFiles.find((c) => c.startsWith(prefix));
438
+ if (match) {
439
+ score += 25;
440
+ evidence.push(`config: ${match}`);
441
+ }
442
+ }
443
+ if (rule.routerVariants && rule.routerVariants.includes(structure.routerVariant)) {
444
+ score += 30;
445
+ evidence.push(`router: ${structure.routerVariant}`);
446
+ }
447
+ if (rule.framework === "react" && structure.extensionCounts.reactJsx > 0) {
448
+ score += 20;
449
+ evidence.push(`source: found ${structure.extensionCounts.reactJsx} .tsx/.jsx file(s)`);
450
+ } else if (rule.framework === "vue" && structure.extensionCounts.vue > 0) {
451
+ score += 20;
452
+ evidence.push(`source: found ${structure.extensionCounts.vue} .vue file(s)`);
453
+ } else if (rule.framework === "svelte" && structure.extensionCounts.svelte > 0) {
454
+ score += 20;
455
+ evidence.push(`source: found ${structure.extensionCounts.svelte} .svelte file(s)`);
456
+ } else if (rule.framework === "nestjs" && structure.extensionCounts.nestDecorators > 0) {
457
+ score += 20;
458
+ evidence.push(`source: found ${structure.extensionCounts.nestDecorators} nest decorator file(s)`);
459
+ }
460
+ if (score > 0) {
461
+ candidates.push({
462
+ rule,
463
+ score,
464
+ evidence,
465
+ subsumed
466
+ });
467
+ }
468
+ }
469
+ for (const candidate of candidates) {
470
+ if (candidate.rule.subsumes && candidate.score >= 50) {
471
+ for (const subsumedTarget of candidate.rule.subsumes) {
472
+ const targetCandidate = candidates.find((c) => c.rule.framework === subsumedTarget);
473
+ if (targetCandidate) {
474
+ candidate.subsumed.push(subsumedTarget);
475
+ candidate.evidence.push(
476
+ `subsumed "${subsumedTarget}" as an internal runtime/driver of ${candidate.rule.framework}`
477
+ );
478
+ targetCandidate.score = 0;
479
+ }
480
+ }
481
+ }
482
+ }
483
+ const activeCandidates = candidates.filter((c) => c.score > 0).sort((a, b) => b.score - a.score);
484
+ return activeCandidates.map((c) => {
485
+ return {
486
+ framework: c.rule.framework,
487
+ category: c.rule.category,
488
+ variant: structure.routerVariant,
489
+ bundler: structure.bundler,
490
+ packageManager: manifest.packageManager,
491
+ language: manifest.language,
492
+ hasTsConfig: manifest.hasTsConfig,
493
+ pathAliases: manifest.pathAliases,
494
+ confidence: Math.min(c.score, 100),
495
+ evidence: c.evidence,
496
+ subsumedDependencies: c.subsumed,
497
+ suggestedTargets: c.rule.suggestedTargets
498
+ };
499
+ });
500
+ }
501
+
502
+ // ../infrastructure/src/detector/ProjectDetector.ts
503
+ function inspectProject(targetPath) {
504
+ const normalizedPath = resolveProjectRoot(targetPath);
505
+ const monorepo = detectMonorepo(normalizedPath);
506
+ const manifest = inspectManifest(normalizedPath);
507
+ const structure = inspectStructure(normalizedPath, manifest.allDependencies, manifest.scripts);
508
+ const profiles = evaluateAndDisambiguate(manifest, structure);
509
+ for (const profile of profiles) {
510
+ if (monorepo.isMonorepo) {
511
+ profile.monorepo = monorepo;
512
+ }
513
+ }
514
+ return profiles;
515
+ }
516
+ function detectTechnologies(targetPath) {
517
+ const profiles = inspectProject(targetPath);
518
+ return profiles.map((p) => ({
519
+ framework: p.framework,
520
+ confidence: p.confidence,
521
+ evidence: p.evidence,
522
+ profile: p
523
+ }));
524
+ }
525
+
526
+ export {
527
+ resolveProjectRoot,
528
+ detectMonorepo,
529
+ inspectManifest,
530
+ inspectStructure,
531
+ inspectProject,
532
+ detectTechnologies
533
+ };