@nikelyh/metamorph 2.1.1 → 2.1.2
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 +5 -3
- package/dist/ProjectDetector-WU3OD6WT.js +16 -0
- package/dist/{chunk-L7B6KUCW.js → chunk-APRESSUR.js} +17 -7
- package/dist/{chunk-VEAYUIOZ.js → chunk-WLHSZWET.js} +39 -0
- package/dist/chunk-XLD7NXYU.js +528 -0
- package/dist/index.js +115 -52
- package/dist/public/assets/index-BX5N2qs-.js +63 -0
- package/dist/public/assets/index-C7odDrsP.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/{src-JHOGUCRS.js → src-Q3DGKZEQ.js} +14 -4
- package/dist/{src-UC3SN46X.js → src-YPCIVBLZ.js} +3 -1
- package/package.json +2 -2
- package/dist/TechDetector-5YP44RVR.js +0 -6
- package/dist/chunk-3QN2Q75J.js +0 -101
- package/dist/public/assets/index-C0AxAuiv.css +0 -1
- package/dist/public/assets/index-DtMWFmXl.js +0 -63
package/README.md
CHANGED
|
@@ -20,10 +20,12 @@ 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, 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.
|
|
23
25
|
- **Multi-Agent Concurrency** -- Utilizes Mapper, Worker, Reviewer, Integration, and Coordinator agents to migrate code in parallel.
|
|
24
|
-
- **Zero Risk** -- All migrations happen in
|
|
26
|
+
- **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.
|
|
27
|
+
- **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
28
|
- **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
29
|
- **Extensive Framework Support** -- Easily migrate between popular frontend and backend frameworks.
|
|
28
30
|
|
|
29
31
|
## Installation
|
|
@@ -120,4 +122,4 @@ See the complete, up-to-date list of all supported frontend and backend framewor
|
|
|
120
122
|
|
|
121
123
|
---
|
|
122
124
|
|
|
123
|
-
*Built with [Mozaik](https://github.com/jigjoy-ai/mozaik).*
|
|
125
|
+
*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-XLD7NXYU.js";
|
|
9
|
+
export {
|
|
10
|
+
detectMonorepo,
|
|
11
|
+
detectTechnologies,
|
|
12
|
+
inspectManifest,
|
|
13
|
+
inspectProject,
|
|
14
|
+
inspectStructure,
|
|
15
|
+
resolveProjectRoot
|
|
16
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveMigrationCatalog
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WLHSZWET.js";
|
|
4
4
|
|
|
5
5
|
// ../infrastructure/src/db/SQLiteStateStore.ts
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -62,6 +62,10 @@ var SQLiteStateStore = class {
|
|
|
62
62
|
this.db.exec("ALTER TABLE plans ADD COLUMN applied_branch TEXT");
|
|
63
63
|
} catch (e) {
|
|
64
64
|
}
|
|
65
|
+
try {
|
|
66
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN package_manager TEXT DEFAULT 'npm'");
|
|
67
|
+
} catch (e) {
|
|
68
|
+
}
|
|
65
69
|
this.db.exec(`
|
|
66
70
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
67
71
|
plan_id TEXT,
|
|
@@ -84,8 +88,8 @@ var SQLiteStateStore = class {
|
|
|
84
88
|
}
|
|
85
89
|
async savePlan(plan) {
|
|
86
90
|
const stmtPlan = this.db.prepare(`
|
|
87
|
-
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, outcome, applied_at, applied_branch)
|
|
88
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
91
|
+
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, outcome, applied_at, applied_branch, package_manager)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
89
93
|
`);
|
|
90
94
|
stmtPlan.run(
|
|
91
95
|
plan.id,
|
|
@@ -99,7 +103,8 @@ var SQLiteStateStore = class {
|
|
|
99
103
|
plan.phase || "files",
|
|
100
104
|
plan.outcome || null,
|
|
101
105
|
plan.appliedAt ? plan.appliedAt.toISOString() : null,
|
|
102
|
-
plan.appliedBranch || null
|
|
106
|
+
plan.appliedBranch || null,
|
|
107
|
+
plan.packageManager || "npm"
|
|
103
108
|
);
|
|
104
109
|
const stmtTask = this.db.prepare(`
|
|
105
110
|
INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
|
|
@@ -120,6 +125,7 @@ var SQLiteStateStore = class {
|
|
|
120
125
|
id: planRow.id,
|
|
121
126
|
runId: planRow.run_id,
|
|
122
127
|
targetPath: planRow.target_path,
|
|
128
|
+
packageManager: planRow.package_manager || "npm",
|
|
123
129
|
profile: {
|
|
124
130
|
source: planRow.source_framework,
|
|
125
131
|
target: planRow.target_framework,
|
|
@@ -705,12 +711,16 @@ async function createApiServer(store, migrationRunner) {
|
|
|
705
711
|
});
|
|
706
712
|
app.get("/api/detect", async (req, res) => {
|
|
707
713
|
try {
|
|
708
|
-
const { detectTechnologies } = await import("./
|
|
714
|
+
const { detectTechnologies: detectTechnologies2, inspectProject: inspectProject2, detectMonorepo } = await import("./ProjectDetector-WU3OD6WT.js");
|
|
709
715
|
const targetPath = req.query.path ? String(req.query.path) : ".";
|
|
710
|
-
const
|
|
716
|
+
const monorepo = detectMonorepo(targetPath);
|
|
717
|
+
const profiles = inspectProject2(targetPath);
|
|
718
|
+
const detected = detectTechnologies2(targetPath);
|
|
711
719
|
res.json({
|
|
712
720
|
detected,
|
|
713
|
-
|
|
721
|
+
profiles,
|
|
722
|
+
monorepo: monorepo.isMonorepo ? monorepo : null,
|
|
723
|
+
primary: profiles.length > 0 ? profiles[0].framework : null
|
|
714
724
|
});
|
|
715
725
|
} catch (error) {
|
|
716
726
|
if (error instanceof Error) {
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
// ../domain/src/entities/PackageManagerCommands.ts
|
|
2
|
+
function resolvePackageManagerCommands(pm) {
|
|
3
|
+
switch (pm) {
|
|
4
|
+
case "pnpm":
|
|
5
|
+
return {
|
|
6
|
+
name: "pnpm",
|
|
7
|
+
install: "pnpm install",
|
|
8
|
+
cleanInstall: "pnpm install",
|
|
9
|
+
runBuild: "pnpm run build",
|
|
10
|
+
runDev: "pnpm dev"
|
|
11
|
+
};
|
|
12
|
+
case "yarn":
|
|
13
|
+
return {
|
|
14
|
+
name: "yarn",
|
|
15
|
+
install: "yarn install",
|
|
16
|
+
cleanInstall: "yarn install",
|
|
17
|
+
runBuild: "yarn build",
|
|
18
|
+
runDev: "yarn dev"
|
|
19
|
+
};
|
|
20
|
+
case "bun":
|
|
21
|
+
return {
|
|
22
|
+
name: "bun",
|
|
23
|
+
install: "bun install",
|
|
24
|
+
cleanInstall: "bun install",
|
|
25
|
+
runBuild: "bun run build",
|
|
26
|
+
runDev: "bun dev"
|
|
27
|
+
};
|
|
28
|
+
default:
|
|
29
|
+
return {
|
|
30
|
+
name: "npm",
|
|
31
|
+
install: "npm install",
|
|
32
|
+
cleanInstall: "npm install --no-fund --no-audit",
|
|
33
|
+
runBuild: "npm run build",
|
|
34
|
+
runDev: "npm run dev"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
1
39
|
// ../domain/src/entities/catalogs/backendCatalog.ts
|
|
2
40
|
var backendCatalog = [
|
|
3
41
|
{
|
|
@@ -1235,6 +1273,7 @@ var SemanticEventName = /* @__PURE__ */ ((SemanticEventName2) => {
|
|
|
1235
1273
|
})(SemanticEventName || {});
|
|
1236
1274
|
|
|
1237
1275
|
export {
|
|
1276
|
+
resolvePackageManagerCommands,
|
|
1238
1277
|
FRAMEWORK_PACKS,
|
|
1239
1278
|
rulesForFramework,
|
|
1240
1279
|
packForFramework,
|
|
@@ -0,0 +1,528 @@
|
|
|
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
|
+
configPrefixes: [],
|
|
392
|
+
suggestedTargets: ["fastify", "nestjs"]
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
framework: "fastify",
|
|
396
|
+
category: "backend-api",
|
|
397
|
+
packageKeys: ["fastify"],
|
|
398
|
+
configPrefixes: [],
|
|
399
|
+
suggestedTargets: ["express", "nestjs"]
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
// ../infrastructure/src/detector/SubsumptionEngine.ts
|
|
404
|
+
function evaluateAndDisambiguate(manifest, structure) {
|
|
405
|
+
const candidates = [];
|
|
406
|
+
for (const rule of FRAMEWORK_DETECTION_RULES) {
|
|
407
|
+
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;
|
|
408
|
+
const hasDedicatedConfig = structure.existingConfigFiles.some(
|
|
409
|
+
(c) => rule.configPrefixes.some((p) => p !== "vite.config" && p !== "webpack.config" && c.startsWith(p))
|
|
410
|
+
);
|
|
411
|
+
if (!hasPackageMatch && !hasDedicatedConfig) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
let score = 0;
|
|
415
|
+
const evidence = [];
|
|
416
|
+
const subsumed = [];
|
|
417
|
+
for (const key of rule.packageKeys) {
|
|
418
|
+
if (manifest.dependencies[key]) {
|
|
419
|
+
score += 50;
|
|
420
|
+
evidence.push(`dependencies: "${key}": "${manifest.dependencies[key]}"`);
|
|
421
|
+
} else if (manifest.devDependencies[key]) {
|
|
422
|
+
score += 35;
|
|
423
|
+
evidence.push(`devDependencies: "${key}": "${manifest.devDependencies[key]}"`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (rule.devPackageKeys) {
|
|
427
|
+
for (const devKey of rule.devPackageKeys) {
|
|
428
|
+
if (manifest.devDependencies[devKey]) {
|
|
429
|
+
score += 25;
|
|
430
|
+
evidence.push(`devTooling: "${devKey}": "${manifest.devDependencies[devKey]}"`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const prefix of rule.configPrefixes) {
|
|
435
|
+
const match = structure.existingConfigFiles.find((c) => c.startsWith(prefix));
|
|
436
|
+
if (match) {
|
|
437
|
+
score += 25;
|
|
438
|
+
evidence.push(`config: ${match}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (rule.routerVariants && rule.routerVariants.includes(structure.routerVariant)) {
|
|
442
|
+
score += 30;
|
|
443
|
+
evidence.push(`router: ${structure.routerVariant}`);
|
|
444
|
+
}
|
|
445
|
+
if (rule.framework === "vue" && structure.extensionCounts.vue > 0) {
|
|
446
|
+
score += 20;
|
|
447
|
+
evidence.push(`source: found ${structure.extensionCounts.vue} .vue file(s)`);
|
|
448
|
+
} else if (rule.framework === "svelte" && structure.extensionCounts.svelte > 0) {
|
|
449
|
+
score += 20;
|
|
450
|
+
evidence.push(`source: found ${structure.extensionCounts.svelte} .svelte file(s)`);
|
|
451
|
+
} else if (rule.framework === "nestjs" && structure.extensionCounts.nestDecorators > 0) {
|
|
452
|
+
score += 20;
|
|
453
|
+
evidence.push(`source: found ${structure.extensionCounts.nestDecorators} nest decorator file(s)`);
|
|
454
|
+
}
|
|
455
|
+
if (score > 0) {
|
|
456
|
+
candidates.push({
|
|
457
|
+
rule,
|
|
458
|
+
score,
|
|
459
|
+
evidence,
|
|
460
|
+
subsumed
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const candidate of candidates) {
|
|
465
|
+
if (candidate.rule.subsumes && candidate.score >= 50) {
|
|
466
|
+
for (const subsumedTarget of candidate.rule.subsumes) {
|
|
467
|
+
const targetCandidate = candidates.find((c) => c.rule.framework === subsumedTarget);
|
|
468
|
+
if (targetCandidate) {
|
|
469
|
+
candidate.subsumed.push(subsumedTarget);
|
|
470
|
+
candidate.evidence.push(
|
|
471
|
+
`subsumed "${subsumedTarget}" as an internal runtime/driver of ${candidate.rule.framework}`
|
|
472
|
+
);
|
|
473
|
+
targetCandidate.score = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const activeCandidates = candidates.filter((c) => c.score > 0).sort((a, b) => b.score - a.score);
|
|
479
|
+
return activeCandidates.map((c) => {
|
|
480
|
+
return {
|
|
481
|
+
framework: c.rule.framework,
|
|
482
|
+
category: c.rule.category,
|
|
483
|
+
variant: structure.routerVariant,
|
|
484
|
+
bundler: structure.bundler,
|
|
485
|
+
packageManager: manifest.packageManager,
|
|
486
|
+
language: manifest.language,
|
|
487
|
+
hasTsConfig: manifest.hasTsConfig,
|
|
488
|
+
pathAliases: manifest.pathAliases,
|
|
489
|
+
confidence: Math.min(c.score, 100),
|
|
490
|
+
evidence: c.evidence,
|
|
491
|
+
subsumedDependencies: c.subsumed,
|
|
492
|
+
suggestedTargets: c.rule.suggestedTargets
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ../infrastructure/src/detector/ProjectDetector.ts
|
|
498
|
+
function inspectProject(targetPath) {
|
|
499
|
+
const normalizedPath = resolveProjectRoot(targetPath);
|
|
500
|
+
const monorepo = detectMonorepo(normalizedPath);
|
|
501
|
+
const manifest = inspectManifest(normalizedPath);
|
|
502
|
+
const structure = inspectStructure(normalizedPath, manifest.allDependencies, manifest.scripts);
|
|
503
|
+
const profiles = evaluateAndDisambiguate(manifest, structure);
|
|
504
|
+
for (const profile of profiles) {
|
|
505
|
+
if (monorepo.isMonorepo) {
|
|
506
|
+
profile.monorepo = monorepo;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return profiles;
|
|
510
|
+
}
|
|
511
|
+
function detectTechnologies(targetPath) {
|
|
512
|
+
const profiles = inspectProject(targetPath);
|
|
513
|
+
return profiles.map((p) => ({
|
|
514
|
+
framework: p.framework,
|
|
515
|
+
confidence: p.confidence,
|
|
516
|
+
evidence: p.evidence,
|
|
517
|
+
profile: p
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export {
|
|
522
|
+
resolveProjectRoot,
|
|
523
|
+
detectMonorepo,
|
|
524
|
+
inspectManifest,
|
|
525
|
+
inspectStructure,
|
|
526
|
+
inspectProject,
|
|
527
|
+
detectTechnologies
|
|
528
|
+
};
|