@nikelyh/metamorph 2.1.0 → 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/LICENSE +674 -0
- package/README.md +58 -31
- package/bin/metamorph.js +0 -0
- package/dist/NextMigrationHints-BGO2W3K4.js +32 -0
- package/dist/ProjectDetector-WU3OD6WT.js +16 -0
- package/dist/{chunk-OXXEISZT.js → chunk-APRESSUR.js} +103 -44
- package/dist/chunk-H2LZEPNI.js +352 -0
- package/dist/{chunk-VEAYUIOZ.js → chunk-WLHSZWET.js} +39 -0
- package/dist/chunk-XLD7NXYU.js +528 -0
- package/dist/index.js +624 -520
- package/dist/public/assets/index-BX5N2qs-.js +63 -0
- package/dist/public/assets/index-C7odDrsP.css +1 -0
- package/dist/public/icons/angular.svg +1 -1
- package/dist/public/icons/expressjs.svg +1 -1
- package/dist/public/icons/fastify.svg +1 -1
- package/dist/public/icons/hapi.svg +1 -14
- package/dist/public/icons/nestjs.svg +1 -1
- package/dist/public/icons/nextjs.svg +1 -1
- package/dist/public/icons/react.svg +1 -1
- package/dist/public/icons/svelte.svg +1 -21
- package/dist/public/icons/vue.svg +1 -1
- package/dist/public/icons.svg +1 -24
- package/dist/public/index.html +2 -2
- package/dist/{src-2UXWWNPF.js → src-Q3DGKZEQ.js} +20 -4
- package/dist/{src-UC3SN46X.js → src-YPCIVBLZ.js} +3 -1
- package/package.json +18 -5
- package/dist/TechDetector-5YP44RVR.js +0 -6
- package/dist/chunk-3QN2Q75J.js +0 -101
- package/dist/public/assets/index-DDJRaEaa.css +0 -1
- package/dist/public/assets/index-PNJ1umlc.js +0 -63
package/README.md
CHANGED
|
@@ -16,16 +16,19 @@
|
|
|
16
16
|
|
|
17
17
|
Metamorph is a CLI tool powered by [Mozaik](https://github.com/jigjoy-ai/mozaik) that automates complex architectural shifts and framework migrations in your codebase. Instead of doing it manually, Metamorph spawns a swarm of specialized AI agents that safely refactor your code in an isolated "Shadow Workspace".
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**[Read the Full Documentation](https://metamorph.nikelyh.tech/docs)** | **[System Architecture](https://github.com/yohanvillarp/metamorph/blob/main/docs/architecture.md)**
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Features
|
|
22
22
|
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
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.
|
|
25
|
+
- **Multi-Agent Concurrency** -- Utilizes Mapper, Worker, Reviewer, Integration, and Coordinator agents to migrate code in parallel.
|
|
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.
|
|
28
|
+
- **Visual Dashboard** -- Watch your agents work in real-time through the built-in local dashboard UI.
|
|
29
|
+
- **Extensive Framework Support** -- Easily migrate between popular frontend and backend frameworks.
|
|
27
30
|
|
|
28
|
-
##
|
|
31
|
+
## Installation
|
|
29
32
|
|
|
30
33
|
Install Metamorph globally via NPM:
|
|
31
34
|
|
|
@@ -33,66 +36,90 @@ Install Metamorph globally via NPM:
|
|
|
33
36
|
npm install -g @nikelyh/metamorph
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
### Prerequisites
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
- Node.js >= 20
|
|
42
|
+
- An LLM provider API key (OpenAI, Anthropic, or compatible). Set it as an environment variable:
|
|
43
|
+
```bash
|
|
44
|
+
export OPENAI_API_KEY=sk-...
|
|
45
|
+
```
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
metamorph --help
|
|
44
|
-
```
|
|
47
|
+
## Typical Workflow
|
|
48
|
+
|
|
49
|
+
### 1. Start a Migration
|
|
45
50
|
|
|
46
|
-
### 2. Start a Migration
|
|
47
51
|
Navigate to the root of the project you want to migrate and run:
|
|
52
|
+
|
|
48
53
|
```bash
|
|
49
54
|
metamorph run
|
|
50
55
|
```
|
|
51
|
-
|
|
56
|
+
|
|
57
|
+
Metamorph automatically detects your current stack (e.g., Express, React) and prompts you to select the target framework. You can bypass the interactive prompt by passing the source explicitly:
|
|
58
|
+
|
|
52
59
|
```bash
|
|
53
60
|
metamorph run --from express
|
|
54
61
|
```
|
|
55
62
|
|
|
56
|
-
###
|
|
57
|
-
|
|
63
|
+
### 2. Monitor the Agents (Dashboard UI)
|
|
64
|
+
|
|
65
|
+
Once a migration is running, the AI swarm begins working in the background. To see their real-time progress, open a new terminal in the same directory and run:
|
|
66
|
+
|
|
58
67
|
```bash
|
|
59
68
|
metamorph ui
|
|
60
69
|
```
|
|
61
|
-
This will start the internal dashboard server and open a Neo-Brutalist interface in your default browser. You can specify a port with `--port 8080`.
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
This starts the internal dashboard server and opens a monitoring interface in your default browser. You can specify a port with `--port 8080`.
|
|
72
|
+
|
|
73
|
+
### 3. Review and Apply
|
|
74
|
+
|
|
75
|
+
After the AI finishes the migration in the isolated Shadow Workspace, you will receive a `runId`. Apply the completed migration to your actual repository (which automatically creates a new Git branch for safety):
|
|
76
|
+
|
|
65
77
|
```bash
|
|
66
78
|
metamorph apply <runId> .
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
###
|
|
70
|
-
|
|
81
|
+
### 4. Managing Migrations
|
|
82
|
+
|
|
83
|
+
Discard a run without applying it:
|
|
84
|
+
|
|
71
85
|
```bash
|
|
72
86
|
metamorph rollback <runId>
|
|
73
87
|
```
|
|
74
|
-
|
|
88
|
+
|
|
89
|
+
View migration history:
|
|
90
|
+
|
|
75
91
|
```bash
|
|
76
92
|
metamorph list
|
|
77
93
|
```
|
|
78
|
-
|
|
94
|
+
|
|
95
|
+
Clear the local database and reset all events:
|
|
96
|
+
|
|
79
97
|
```bash
|
|
80
98
|
metamorph reset
|
|
81
99
|
```
|
|
82
100
|
|
|
83
101
|
### Utility Commands
|
|
84
|
-
|
|
102
|
+
|
|
103
|
+
Test the technology detection on your repository without starting a migration:
|
|
104
|
+
|
|
85
105
|
```bash
|
|
86
106
|
metamorph detect .
|
|
87
107
|
```
|
|
88
108
|
|
|
89
|
-
|
|
109
|
+
View all available commands and options:
|
|
90
110
|
|
|
91
|
-
|
|
111
|
+
```bash
|
|
112
|
+
metamorph --help
|
|
113
|
+
```
|
|
92
114
|
|
|
93
|
-
|
|
115
|
+
## Supported Migrations
|
|
94
116
|
|
|
95
|
-
|
|
117
|
+
The list of supported architectural shifts is updated frequently as new migration catalogs are added.
|
|
118
|
+
|
|
119
|
+
See the complete, up-to-date list of all supported frontend and backend framework migrations in the official documentation:
|
|
120
|
+
|
|
121
|
+
**[View Supported Migrations](https://metamorph.nikelyh.tech/docs/migrations)**
|
|
96
122
|
|
|
97
123
|
---
|
|
98
|
-
|
|
124
|
+
|
|
125
|
+
*Built with [Mozaik](https://github.com/jigjoy-ai/mozaik).* 💖
|
package/bin/metamorph.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeCallbackPropMismatches,
|
|
3
|
+
analyzeReactToNextComposition,
|
|
4
|
+
analyzeReactToNextStructure,
|
|
5
|
+
analyzeUnresolvedLocalImports,
|
|
6
|
+
findShadowRoot,
|
|
7
|
+
hasNextAppRoutes,
|
|
8
|
+
isFsdPageComponent,
|
|
9
|
+
isHollowView,
|
|
10
|
+
isSpaPagesTreeFile,
|
|
11
|
+
isSpaRootComponent,
|
|
12
|
+
isStubAppRouterPage,
|
|
13
|
+
listAppRouterFiles,
|
|
14
|
+
nextMigrationFileHint,
|
|
15
|
+
unresolvedLocalImports
|
|
16
|
+
} from "./chunk-H2LZEPNI.js";
|
|
17
|
+
export {
|
|
18
|
+
analyzeCallbackPropMismatches,
|
|
19
|
+
analyzeReactToNextComposition,
|
|
20
|
+
analyzeReactToNextStructure,
|
|
21
|
+
analyzeUnresolvedLocalImports,
|
|
22
|
+
findShadowRoot,
|
|
23
|
+
hasNextAppRoutes,
|
|
24
|
+
isFsdPageComponent,
|
|
25
|
+
isHollowView,
|
|
26
|
+
isSpaPagesTreeFile,
|
|
27
|
+
isSpaRootComponent,
|
|
28
|
+
isStubAppRouterPage,
|
|
29
|
+
listAppRouterFiles,
|
|
30
|
+
nextMigrationFileHint,
|
|
31
|
+
unresolvedLocalImports
|
|
32
|
+
};
|
|
@@ -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
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
resolveMigrationCatalog
|
|
3
|
+
} from "./chunk-WLHSZWET.js";
|
|
4
4
|
|
|
5
5
|
// ../infrastructure/src/db/SQLiteStateStore.ts
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -50,6 +50,10 @@ var SQLiteStateStore = class {
|
|
|
50
50
|
this.db.exec("ALTER TABLE plans ADD COLUMN phase TEXT");
|
|
51
51
|
} catch (e) {
|
|
52
52
|
}
|
|
53
|
+
try {
|
|
54
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN outcome TEXT");
|
|
55
|
+
} catch (e) {
|
|
56
|
+
}
|
|
53
57
|
try {
|
|
54
58
|
this.db.exec("ALTER TABLE plans ADD COLUMN applied_at TEXT");
|
|
55
59
|
} catch (e) {
|
|
@@ -58,6 +62,10 @@ var SQLiteStateStore = class {
|
|
|
58
62
|
this.db.exec("ALTER TABLE plans ADD COLUMN applied_branch TEXT");
|
|
59
63
|
} catch (e) {
|
|
60
64
|
}
|
|
65
|
+
try {
|
|
66
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN package_manager TEXT DEFAULT 'npm'");
|
|
67
|
+
} catch (e) {
|
|
68
|
+
}
|
|
61
69
|
this.db.exec(`
|
|
62
70
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
63
71
|
plan_id TEXT,
|
|
@@ -80,8 +88,8 @@ var SQLiteStateStore = class {
|
|
|
80
88
|
}
|
|
81
89
|
async savePlan(plan) {
|
|
82
90
|
const stmtPlan = this.db.prepare(`
|
|
83
|
-
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, applied_at, applied_branch)
|
|
84
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
85
93
|
`);
|
|
86
94
|
stmtPlan.run(
|
|
87
95
|
plan.id,
|
|
@@ -93,8 +101,10 @@ var SQLiteStateStore = class {
|
|
|
93
101
|
plan.createdAt.toISOString(),
|
|
94
102
|
plan.integrationRounds ?? 0,
|
|
95
103
|
plan.phase || "files",
|
|
104
|
+
plan.outcome || null,
|
|
96
105
|
plan.appliedAt ? plan.appliedAt.toISOString() : null,
|
|
97
|
-
plan.appliedBranch || null
|
|
106
|
+
plan.appliedBranch || null,
|
|
107
|
+
plan.packageManager || "npm"
|
|
98
108
|
);
|
|
99
109
|
const stmtTask = this.db.prepare(`
|
|
100
110
|
INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
|
|
@@ -115,6 +125,7 @@ var SQLiteStateStore = class {
|
|
|
115
125
|
id: planRow.id,
|
|
116
126
|
runId: planRow.run_id,
|
|
117
127
|
targetPath: planRow.target_path,
|
|
128
|
+
packageManager: planRow.package_manager || "npm",
|
|
118
129
|
profile: {
|
|
119
130
|
source: planRow.source_framework,
|
|
120
131
|
target: planRow.target_framework,
|
|
@@ -123,6 +134,7 @@ var SQLiteStateStore = class {
|
|
|
123
134
|
createdAt: new Date(planRow.created_at),
|
|
124
135
|
integrationRounds: planRow.integration_rounds ?? 0,
|
|
125
136
|
phase: planRow.phase || "files",
|
|
137
|
+
outcome: planRow.outcome || void 0,
|
|
126
138
|
appliedAt: planRow.applied_at ? new Date(planRow.applied_at) : void 0,
|
|
127
139
|
appliedBranch: planRow.applied_branch || void 0,
|
|
128
140
|
tasks: taskRows.map((row) => ({
|
|
@@ -195,7 +207,8 @@ function assertSandbox(filePath, sandboxDir) {
|
|
|
195
207
|
const resolved = path.resolve(filePath);
|
|
196
208
|
if (sandboxDir) {
|
|
197
209
|
const resolvedSandbox = path.resolve(sandboxDir);
|
|
198
|
-
|
|
210
|
+
const isInside = resolved === resolvedSandbox || resolved.startsWith(resolvedSandbox + path.sep);
|
|
211
|
+
if (!isInside) {
|
|
199
212
|
throw new Error(
|
|
200
213
|
`BLOCKED: Path "${resolved}" is outside the sandbox "${resolvedSandbox}". All file operations must stay within the shadow workspace.`
|
|
201
214
|
);
|
|
@@ -396,6 +409,16 @@ function createListDirectoryTool(sandboxDir) {
|
|
|
396
409
|
}
|
|
397
410
|
};
|
|
398
411
|
}
|
|
412
|
+
function createShadowTools(shadowDir) {
|
|
413
|
+
return [
|
|
414
|
+
createReadFileTool(shadowDir),
|
|
415
|
+
createWriteFileTool(shadowDir),
|
|
416
|
+
createRenameFileTool(shadowDir),
|
|
417
|
+
createCreateFileTool(shadowDir),
|
|
418
|
+
createDeleteFileTool(shadowDir),
|
|
419
|
+
createListDirectoryTool(shadowDir)
|
|
420
|
+
];
|
|
421
|
+
}
|
|
399
422
|
|
|
400
423
|
// ../infrastructure/src/workspace/ShadowWorkspace.ts
|
|
401
424
|
import fs from "fs-extra";
|
|
@@ -454,15 +477,16 @@ var ShadowWorkspace = class {
|
|
|
454
477
|
* Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
|
|
455
478
|
* @param keepCount Number of recent runs to keep (default: 3)
|
|
456
479
|
*/
|
|
457
|
-
cleanupOldRuns(keepCount = 3) {
|
|
480
|
+
cleanupOldRuns(keepCount = 3, protectedRunIds = []) {
|
|
458
481
|
if (!fs.existsSync(this.baseDir)) return;
|
|
482
|
+
const protectedSet = new Set(protectedRunIds);
|
|
459
483
|
const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
|
|
460
484
|
runs.sort();
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
485
|
+
const unprotected = runs.filter((run) => !protectedSet.has(run));
|
|
486
|
+
if (unprotected.length <= keepCount) return;
|
|
487
|
+
const toDelete = unprotected.slice(0, unprotected.length - keepCount);
|
|
488
|
+
for (const run of toDelete) {
|
|
489
|
+
this.rollback(run);
|
|
466
490
|
}
|
|
467
491
|
}
|
|
468
492
|
};
|
|
@@ -472,7 +496,11 @@ import fs2 from "fs-extra";
|
|
|
472
496
|
import * as path2 from "path";
|
|
473
497
|
import { execFileSync } from "child_process";
|
|
474
498
|
var COPY_IGNORED = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
|
|
475
|
-
|
|
499
|
+
function isCopyIgnored(itemName) {
|
|
500
|
+
if (COPY_IGNORED.includes(itemName)) return true;
|
|
501
|
+
if (itemName === ".env" || itemName.startsWith(".env.")) return true;
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
476
504
|
var MigrationIntegrator = class {
|
|
477
505
|
shadowWorkspace;
|
|
478
506
|
constructor(shadowWorkspace) {
|
|
@@ -494,6 +522,20 @@ var MigrationIntegrator = class {
|
|
|
494
522
|
"Metamorph apply requires a Git repository. Run this from a project that already has git (including monorepo packages whose .git lives in a parent folder). Nested git init is not supported."
|
|
495
523
|
);
|
|
496
524
|
}
|
|
525
|
+
const rootPkgJson = path2.join(gitRoot, "package.json");
|
|
526
|
+
if (fs2.existsSync(rootPkgJson)) {
|
|
527
|
+
try {
|
|
528
|
+
const pkg = JSON.parse(fs2.readFileSync(rootPkgJson, "utf-8"));
|
|
529
|
+
const normalizedTarget = resolvedTarget.replace(/\\/g, "/");
|
|
530
|
+
if (pkg.name === "metamorph-monorepo" && normalizedTarget.includes("/scratch/")) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`BLOCKED: Cannot apply to "${resolvedTarget}" because it is inside the development scratch/ directory of the Metamorph repository. Use a standalone git repository for user project migrations.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
} catch (e) {
|
|
536
|
+
if (e instanceof Error && e.message.startsWith("BLOCKED:")) throw e;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
497
539
|
if (this.isIgnoredByGit(gitRoot, resolvedTarget)) {
|
|
498
540
|
throw new Error(
|
|
499
541
|
`Cannot apply via Git: "${resolvedTarget}" is ignored by the repository at "${gitRoot}" (.gitignore). Metamorph will not create branches on a parent repo for ignored sandboxes. Use a standalone git project, or a tracked package inside a monorepo.`
|
|
@@ -536,22 +578,17 @@ var MigrationIntegrator = class {
|
|
|
536
578
|
throw new Error(`git ${args.join(" ")} failed: ${detail || "unknown git error"}`);
|
|
537
579
|
}
|
|
538
580
|
}
|
|
539
|
-
ensureApplyGitignore(gitRoot) {
|
|
540
|
-
const gitignorePath = path2.join(gitRoot, ".gitignore");
|
|
541
|
-
const existing = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
|
|
542
|
-
const missing = GITIGNORE_ENTRIES.filter((entry) => {
|
|
543
|
-
const name = entry.replace(/\/$/, "").replace(".", "\\.");
|
|
544
|
-
const pattern = new RegExp(`(^|\\n)${name}(/)?(\\n|$)`);
|
|
545
|
-
return !pattern.test(existing.replace(/\r/g, "\n"));
|
|
546
|
-
});
|
|
547
|
-
if (missing.length === 0) return;
|
|
548
|
-
const block = `
|
|
549
|
-
# Metamorph apply
|
|
550
|
-
${missing.join("\n")}
|
|
551
|
-
`;
|
|
552
|
-
fs2.appendFileSync(gitignorePath, block, "utf-8");
|
|
553
|
-
}
|
|
554
581
|
async applyWithGit(runId, shadowDir, targetPath, gitRoot, branchName) {
|
|
582
|
+
const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
|
|
583
|
+
const addPath = relativeTarget === "" ? "." : relativeTarget;
|
|
584
|
+
const dirtyStatus = this.git(["status", "--porcelain", "--", addPath], gitRoot).trim();
|
|
585
|
+
if (dirtyStatus) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Cannot apply migration: the target directory has uncommitted changes:
|
|
588
|
+
${dirtyStatus}
|
|
589
|
+
Please commit or stash your changes before applying.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
555
592
|
try {
|
|
556
593
|
this.git(["checkout", "-B", branchName], gitRoot);
|
|
557
594
|
} catch (e) {
|
|
@@ -561,9 +598,6 @@ ${missing.join("\n")}
|
|
|
561
598
|
throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
|
|
562
599
|
}
|
|
563
600
|
this.copyShadowToTarget(shadowDir, targetPath);
|
|
564
|
-
this.ensureApplyGitignore(gitRoot);
|
|
565
|
-
const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
|
|
566
|
-
const addPath = relativeTarget === "" ? "." : relativeTarget;
|
|
567
601
|
try {
|
|
568
602
|
try {
|
|
569
603
|
this.git(["-c", "advice.addIgnoredFile=false", "add", "-A", "--", addPath], gitRoot);
|
|
@@ -597,13 +631,13 @@ ${missing.join("\n")}
|
|
|
597
631
|
if (fs2.existsSync(targetPath)) {
|
|
598
632
|
const targetItems = fs2.readdirSync(targetPath);
|
|
599
633
|
for (const item of targetItems) {
|
|
600
|
-
if (
|
|
634
|
+
if (isCopyIgnored(item)) continue;
|
|
601
635
|
fs2.removeSync(path2.join(targetPath, item));
|
|
602
636
|
}
|
|
603
637
|
}
|
|
604
638
|
const shadowItems = fs2.readdirSync(shadowDir);
|
|
605
639
|
for (const item of shadowItems) {
|
|
606
|
-
if (
|
|
640
|
+
if (isCopyIgnored(item)) continue;
|
|
607
641
|
const itemSrc = path2.join(shadowDir, item);
|
|
608
642
|
const itemDest = path2.join(targetPath, item);
|
|
609
643
|
fs2.copySync(itemSrc, itemDest, { overwrite: true });
|
|
@@ -677,12 +711,16 @@ async function createApiServer(store, migrationRunner) {
|
|
|
677
711
|
});
|
|
678
712
|
app.get("/api/detect", async (req, res) => {
|
|
679
713
|
try {
|
|
680
|
-
const { detectTechnologies } = await import("./
|
|
714
|
+
const { detectTechnologies: detectTechnologies2, inspectProject: inspectProject2, detectMonorepo } = await import("./ProjectDetector-WU3OD6WT.js");
|
|
681
715
|
const targetPath = req.query.path ? String(req.query.path) : ".";
|
|
682
|
-
const
|
|
716
|
+
const monorepo = detectMonorepo(targetPath);
|
|
717
|
+
const profiles = inspectProject2(targetPath);
|
|
718
|
+
const detected = detectTechnologies2(targetPath);
|
|
683
719
|
res.json({
|
|
684
720
|
detected,
|
|
685
|
-
|
|
721
|
+
profiles,
|
|
722
|
+
monorepo: monorepo.isMonorepo ? monorepo : null,
|
|
723
|
+
primary: profiles.length > 0 ? profiles[0].framework : null
|
|
686
724
|
});
|
|
687
725
|
} catch (error) {
|
|
688
726
|
if (error instanceof Error) {
|
|
@@ -702,7 +740,7 @@ async function createApiServer(store, migrationRunner) {
|
|
|
702
740
|
});
|
|
703
741
|
return;
|
|
704
742
|
}
|
|
705
|
-
const catalogEntry =
|
|
743
|
+
const catalogEntry = resolveMigrationCatalog(from, to);
|
|
706
744
|
if (!catalogEntry) {
|
|
707
745
|
res.status(400).json({
|
|
708
746
|
error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
|
|
@@ -732,15 +770,29 @@ async function createApiServer(store, migrationRunner) {
|
|
|
732
770
|
});
|
|
733
771
|
return;
|
|
734
772
|
}
|
|
735
|
-
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
736
|
-
const result = await integrator.applyMigration(runId, targetPath);
|
|
737
773
|
const plans = await store.getAllPlans();
|
|
738
774
|
const plan = plans.find((item) => item.runId === runId);
|
|
739
|
-
if (plan) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
775
|
+
if (!plan) {
|
|
776
|
+
res.status(404).json({ error: `Plan for run "${runId}" not found in database.` });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (plan.phase !== "completed" || plan.outcome !== "success") {
|
|
780
|
+
res.status(400).json({
|
|
781
|
+
error: `Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
743
784
|
}
|
|
785
|
+
if (plan.appliedAt) {
|
|
786
|
+
res.status(400).json({
|
|
787
|
+
error: `Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
792
|
+
const result = await integrator.applyMigration(runId, targetPath);
|
|
793
|
+
plan.appliedAt = /* @__PURE__ */ new Date();
|
|
794
|
+
plan.appliedBranch = result.branch;
|
|
795
|
+
await store.savePlan(plan);
|
|
744
796
|
await store.logEvent("migration.applied", { runId, branch: result.branch, targetPath });
|
|
745
797
|
res.json({ ok: true, ...result });
|
|
746
798
|
} catch (error) {
|
|
@@ -918,7 +970,11 @@ function parseImplicatedFiles(output, shadowPath) {
|
|
|
918
970
|
const resolved = path4.resolve(abs);
|
|
919
971
|
const inside = resolved === root || resolved.startsWith(root + path4.sep);
|
|
920
972
|
if (!inside) return;
|
|
921
|
-
if (fs3.existsSync(resolved)
|
|
973
|
+
if (fs3.existsSync(resolved)) {
|
|
974
|
+
if (fs3.statSync(resolved).isFile()) found.add(resolved);
|
|
975
|
+
} else {
|
|
976
|
+
found.add(resolved);
|
|
977
|
+
}
|
|
922
978
|
};
|
|
923
979
|
for (const re of patterns) {
|
|
924
980
|
let match;
|
|
@@ -953,13 +1009,16 @@ ${result.output}`;
|
|
|
953
1009
|
|
|
954
1010
|
export {
|
|
955
1011
|
SQLiteStateStore,
|
|
1012
|
+
assertSandbox,
|
|
956
1013
|
createReadFileTool,
|
|
957
1014
|
createWriteFileTool,
|
|
958
1015
|
createRenameFileTool,
|
|
959
1016
|
createCreateFileTool,
|
|
960
1017
|
createDeleteFileTool,
|
|
961
1018
|
createListDirectoryTool,
|
|
1019
|
+
createShadowTools,
|
|
962
1020
|
ShadowWorkspace,
|
|
1021
|
+
isCopyIgnored,
|
|
963
1022
|
MigrationIntegrator,
|
|
964
1023
|
createApiServer,
|
|
965
1024
|
createCheckProjectDiagnosticsTool,
|