@nikelyh/metamorph 2.0.11 → 2.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/dist/{chunk-GNWRSZXT.js → chunk-GKRBTA6E.js} +13 -0
- package/dist/chunk-O2QLEES3.js +61 -0
- package/dist/chunk-OXXEISZT.js +969 -0
- package/dist/chunk-VEAYUIOZ.js +1256 -0
- package/dist/index.js +2244 -1546
- package/dist/public/assets/index-DDJRaEaa.css +1 -0
- package/dist/public/assets/index-PNJ1umlc.js +63 -0
- package/dist/public/github.svg +1 -0
- package/dist/public/index.html +5 -4
- package/dist/public/logo_metamorph.png +0 -0
- package/dist/registry-7WZ5RTGE.js +14 -0
- package/dist/{runtime-5OVGH2Y7.js → runtime-L7VOQROE.js} +1 -1
- package/dist/src-2UXWWNPF.js +37 -0
- package/dist/src-UC3SN46X.js +40 -0
- package/package.json +1 -1
- package/dist/public/assets/index-BsEDNMSB.css +0 -1
- package/dist/public/assets/index-CYjhs-Jc.js +0 -66
- package/dist/public/favicon.svg +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MigrationIntegrator,
|
|
4
|
+
SQLiteStateStore,
|
|
5
|
+
ShadowWorkspace,
|
|
6
|
+
createApiServer,
|
|
7
|
+
createCheckProjectDiagnosticsTool,
|
|
8
|
+
createCreateFileTool,
|
|
9
|
+
createDeleteFileTool,
|
|
10
|
+
createListDirectoryTool,
|
|
11
|
+
createReadFileTool,
|
|
12
|
+
createRenameFileTool,
|
|
13
|
+
createRunBuildTool,
|
|
14
|
+
createWriteFileTool
|
|
15
|
+
} from "./chunk-OXXEISZT.js";
|
|
16
|
+
import {
|
|
17
|
+
findMigrationCatalogEntry,
|
|
18
|
+
formatCatalogRules
|
|
19
|
+
} from "./chunk-VEAYUIOZ.js";
|
|
2
20
|
import {
|
|
3
21
|
detectTechnologies
|
|
4
22
|
} from "./chunk-3QN2Q75J.js";
|
|
@@ -10,1234 +28,573 @@ import {
|
|
|
10
28
|
resolveRuntime,
|
|
11
29
|
runLoop,
|
|
12
30
|
sendEvent
|
|
13
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-GKRBTA6E.js";
|
|
32
|
+
import {
|
|
33
|
+
collectFileHints,
|
|
34
|
+
registerMigrationPlugin
|
|
35
|
+
} from "./chunk-O2QLEES3.js";
|
|
14
36
|
|
|
15
37
|
// src/index.ts
|
|
16
38
|
import { Command } from "commander";
|
|
17
39
|
import chalk from "chalk";
|
|
18
40
|
import ora from "ora";
|
|
19
41
|
import "dotenv/config";
|
|
42
|
+
import * as path11 from "path";
|
|
20
43
|
|
|
21
|
-
// ../
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
const dbPath = join2(storageDir, "history.db");
|
|
33
|
-
this.db = new DatabaseSync(dbPath);
|
|
34
|
-
this.initTables();
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Creates the necessary tables if they do not exist.
|
|
38
|
-
*/
|
|
39
|
-
initTables() {
|
|
40
|
-
this.db.exec(`
|
|
41
|
-
CREATE TABLE IF NOT EXISTS plans (
|
|
42
|
-
id TEXT PRIMARY KEY,
|
|
43
|
-
run_id TEXT,
|
|
44
|
-
target_path TEXT,
|
|
45
|
-
source_framework TEXT,
|
|
46
|
-
target_framework TEXT,
|
|
47
|
-
rules_json TEXT,
|
|
48
|
-
created_at TEXT
|
|
49
|
-
);
|
|
50
|
-
`);
|
|
51
|
-
try {
|
|
52
|
-
this.db.exec("ALTER TABLE plans ADD COLUMN run_id TEXT");
|
|
53
|
-
} catch (e) {
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
this.db.exec("ALTER TABLE plans ADD COLUMN target_path TEXT");
|
|
57
|
-
} catch (e) {
|
|
58
|
-
}
|
|
59
|
-
this.db.exec(`
|
|
60
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
61
|
-
plan_id TEXT,
|
|
62
|
-
file_path TEXT,
|
|
63
|
-
status TEXT,
|
|
64
|
-
dependencies_json TEXT,
|
|
65
|
-
error TEXT,
|
|
66
|
-
PRIMARY KEY (plan_id, file_path),
|
|
67
|
-
FOREIGN KEY (plan_id) REFERENCES plans(id)
|
|
68
|
-
);
|
|
69
|
-
`);
|
|
70
|
-
this.db.exec(`
|
|
71
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
72
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
-
event_name TEXT,
|
|
74
|
-
payload_json TEXT,
|
|
75
|
-
timestamp TEXT
|
|
76
|
-
);
|
|
77
|
-
`);
|
|
78
|
-
}
|
|
79
|
-
async savePlan(plan) {
|
|
80
|
-
const stmtPlan = this.db.prepare(`
|
|
81
|
-
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at)
|
|
82
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
83
|
-
`);
|
|
84
|
-
stmtPlan.run(
|
|
85
|
-
plan.id,
|
|
86
|
-
plan.runId,
|
|
87
|
-
plan.targetPath || "",
|
|
88
|
-
plan.profile.source,
|
|
89
|
-
plan.profile.target,
|
|
90
|
-
JSON.stringify(plan.profile.rules || []),
|
|
91
|
-
plan.createdAt.toISOString()
|
|
92
|
-
);
|
|
93
|
-
const stmtTask = this.db.prepare(`
|
|
94
|
-
INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
|
|
95
|
-
VALUES (?, ?, ?, ?, ?)
|
|
96
|
-
`);
|
|
97
|
-
for (const task of plan.tasks) {
|
|
98
|
-
stmtTask.run(
|
|
99
|
-
plan.id,
|
|
100
|
-
task.filePath,
|
|
101
|
-
task.status,
|
|
102
|
-
JSON.stringify(task.dependencies || []),
|
|
103
|
-
task.error || null
|
|
104
|
-
);
|
|
105
|
-
}
|
|
44
|
+
// ../application/src/agents/MapperAgent.ts
|
|
45
|
+
import * as fs from "fs";
|
|
46
|
+
import * as path from "path";
|
|
47
|
+
import {
|
|
48
|
+
createAgent,
|
|
49
|
+
SituationSpecification
|
|
50
|
+
} from "@mozaik-ai/core";
|
|
51
|
+
var WhenMigrationStarts = class extends SituationSpecification {
|
|
52
|
+
isSatisfiedBy({ event }) {
|
|
53
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
106
54
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
55
|
+
};
|
|
56
|
+
var mapFilesProcessor = {
|
|
57
|
+
async apply({ event, participant }) {
|
|
58
|
+
const payload = event.payload;
|
|
59
|
+
console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
|
|
60
|
+
const shadowDir = payload.shadowWorkspacePath || "";
|
|
61
|
+
if (!shadowDir) {
|
|
62
|
+
console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
|
|
63
|
+
return;
|
|
112
64
|
}
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
status: row.status,
|
|
128
|
-
dependencies: JSON.parse(row.dependencies_json),
|
|
129
|
-
error: row.error || void 0
|
|
130
|
-
}))
|
|
65
|
+
const filesToMigrate = [];
|
|
66
|
+
const scanDir = (dir) => {
|
|
67
|
+
if (!fs.existsSync(dir)) return;
|
|
68
|
+
const files = fs.readdirSync(dir);
|
|
69
|
+
const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
|
|
72
|
+
const fullPath = path.join(dir, file);
|
|
73
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
74
|
+
scanDir(fullPath);
|
|
75
|
+
} else if (/\.(ts|js|tsx|jsx|vue|svelte)$/.test(file) || file.endsWith(".html") && file !== "index.html") {
|
|
76
|
+
filesToMigrate.push(fullPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
131
79
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
stmt.run(eventName, JSON.stringify(payload), (/* @__PURE__ */ new Date()).toISOString());
|
|
148
|
-
}
|
|
149
|
-
async getAllPlans() {
|
|
150
|
-
const stmtPlans = this.db.prepare(`SELECT * FROM plans ORDER BY created_at DESC`);
|
|
151
|
-
const planRows = stmtPlans.all();
|
|
152
|
-
const stmtTasks = this.db.prepare(`SELECT * FROM tasks`);
|
|
153
|
-
const taskRows = stmtTasks.all();
|
|
154
|
-
return planRows.map((planRow) => {
|
|
155
|
-
const planTasks = taskRows.filter((t) => t.plan_id === planRow.id);
|
|
156
|
-
return {
|
|
157
|
-
id: planRow.id,
|
|
158
|
-
runId: planRow.run_id,
|
|
159
|
-
targetPath: planRow.target_path,
|
|
160
|
-
profile: {
|
|
161
|
-
source: planRow.source_framework,
|
|
162
|
-
target: planRow.target_framework,
|
|
163
|
-
rules: JSON.parse(planRow.rules_json)
|
|
80
|
+
scanDir(shadowDir);
|
|
81
|
+
const runtime = resolveRuntime();
|
|
82
|
+
const repository = runtime.state.repository;
|
|
83
|
+
for (const discoveredFile of filesToMigrate) {
|
|
84
|
+
console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
|
|
85
|
+
await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
|
|
86
|
+
sendEvent(
|
|
87
|
+
{
|
|
88
|
+
type: "file.discovered" /* FILE_DISCOVERED */,
|
|
89
|
+
producerId: participant.getId(),
|
|
90
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
91
|
+
payload: {
|
|
92
|
+
planId: payload.planId,
|
|
93
|
+
filePath: discoveredFile
|
|
94
|
+
}
|
|
164
95
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
status: row.status,
|
|
169
|
-
dependencies: JSON.parse(row.dependencies_json),
|
|
170
|
-
error: row.error || void 0
|
|
171
|
-
}))
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
async getEvents() {
|
|
176
|
-
const stmt = this.db.prepare(`SELECT * FROM events ORDER BY id DESC LIMIT 100`);
|
|
177
|
-
const rows = stmt.all();
|
|
178
|
-
return rows.map((row) => ({
|
|
179
|
-
id: row.id,
|
|
180
|
-
eventName: row.event_name,
|
|
181
|
-
payload: JSON.parse(row.payload_json),
|
|
182
|
-
timestamp: new Date(row.timestamp)
|
|
183
|
-
}));
|
|
184
|
-
}
|
|
185
|
-
async reset() {
|
|
186
|
-
this.db.exec(`DELETE FROM tasks`);
|
|
187
|
-
this.db.exec(`DELETE FROM plans`);
|
|
188
|
-
this.db.exec(`DELETE FROM events`);
|
|
96
|
+
participant.getId()
|
|
97
|
+
);
|
|
98
|
+
}
|
|
189
99
|
}
|
|
190
100
|
};
|
|
101
|
+
var mapFilesHandler = {
|
|
102
|
+
specification: new WhenMigrationStarts(),
|
|
103
|
+
processor: mapFilesProcessor
|
|
104
|
+
};
|
|
105
|
+
function createMapperAgent() {
|
|
106
|
+
return createAgent({
|
|
107
|
+
name: "Mapper",
|
|
108
|
+
capabilities: ["code_analysis"],
|
|
109
|
+
instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
|
|
110
|
+
tools: [],
|
|
111
|
+
// AST scanning tools will go here later
|
|
112
|
+
handlers: [mapFilesHandler]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
191
115
|
|
|
192
|
-
// ../
|
|
193
|
-
import {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
116
|
+
// ../application/src/agents/WorkerAgent.ts
|
|
117
|
+
import {
|
|
118
|
+
createAgent as createAgent2,
|
|
119
|
+
SituationSpecification as SituationSpecification2
|
|
120
|
+
} from "@mozaik-ai/core";
|
|
121
|
+
import * as fs4 from "fs";
|
|
122
|
+
|
|
123
|
+
// ../application/src/utils/FileTreeBuilder.ts
|
|
124
|
+
import * as path2 from "path";
|
|
125
|
+
function getCommonBaseDir(filePaths) {
|
|
126
|
+
if (!filePaths || filePaths.length === 0) return "";
|
|
127
|
+
if (filePaths.length === 1) return path2.dirname(filePaths[0]);
|
|
128
|
+
const splitPaths = filePaths.map((p) => path2.normalize(p).split(path2.sep));
|
|
129
|
+
let commonPath = "";
|
|
130
|
+
for (let i = 0; i < splitPaths[0].length; i++) {
|
|
131
|
+
const part = splitPaths[0][i];
|
|
132
|
+
const isCommon = splitPaths.every((parts) => parts[i] === part);
|
|
133
|
+
if (isCommon) {
|
|
134
|
+
commonPath = commonPath === "" ? part : commonPath + path2.sep + part;
|
|
135
|
+
} else {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return commonPath;
|
|
140
|
+
}
|
|
141
|
+
function buildFileTree(filePaths) {
|
|
142
|
+
if (!filePaths || filePaths.length === 0) return "No files found.";
|
|
143
|
+
const baseDir = getCommonBaseDir(filePaths);
|
|
144
|
+
const relativePaths = filePaths.map((p) => path2.relative(baseDir, p)).filter((p) => p.length > 0).sort();
|
|
145
|
+
if (relativePaths.length === 0) return "No files found.";
|
|
146
|
+
const tree = {};
|
|
147
|
+
for (const relPath of relativePaths) {
|
|
148
|
+
const parts = relPath.split(path2.sep);
|
|
149
|
+
let current = tree;
|
|
150
|
+
for (let i = 0; i < parts.length; i++) {
|
|
151
|
+
const part = parts[i];
|
|
152
|
+
if (i === parts.length - 1) {
|
|
153
|
+
current[part] = null;
|
|
218
154
|
} else {
|
|
219
|
-
|
|
155
|
+
current[part] = current[part] || {};
|
|
156
|
+
current = current[part];
|
|
220
157
|
}
|
|
221
|
-
return {
|
|
222
|
-
filePath,
|
|
223
|
-
content: sourceFile.getFullText()
|
|
224
|
-
};
|
|
225
158
|
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
strict: true,
|
|
243
|
-
invoke: async ({ filePath, newContent }) => {
|
|
244
|
-
let sourceFile = project.getSourceFile(filePath);
|
|
245
|
-
if (!sourceFile) {
|
|
246
|
-
if (existsSync(filePath)) {
|
|
247
|
-
project.addSourceFileAtPath(filePath);
|
|
248
|
-
sourceFile = project.getSourceFileOrThrow(filePath);
|
|
249
|
-
} else {
|
|
250
|
-
sourceFile = project.createSourceFile(filePath, "");
|
|
251
|
-
}
|
|
159
|
+
}
|
|
160
|
+
let output = `/
|
|
161
|
+
`;
|
|
162
|
+
function renderTree(node, prefix = "") {
|
|
163
|
+
if (node === null) return;
|
|
164
|
+
const keys = Object.keys(node);
|
|
165
|
+
for (let i = 0; i < keys.length; i++) {
|
|
166
|
+
const key = keys[i];
|
|
167
|
+
const isLast = i === keys.length - 1;
|
|
168
|
+
const isFile = node[key] === null;
|
|
169
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
170
|
+
output += `${prefix}${connector}${key}
|
|
171
|
+
`;
|
|
172
|
+
if (!isFile) {
|
|
173
|
+
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
174
|
+
renderTree(node[key], nextPrefix);
|
|
252
175
|
}
|
|
253
|
-
sourceFile.replaceWithText(newContent);
|
|
254
|
-
await sourceFile.save();
|
|
255
|
-
return {
|
|
256
|
-
success: true,
|
|
257
|
-
message: `File ${filePath} successfully updated.`
|
|
258
|
-
};
|
|
259
176
|
}
|
|
260
|
-
}
|
|
177
|
+
}
|
|
178
|
+
renderTree(tree);
|
|
179
|
+
return output.trim();
|
|
261
180
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
181
|
+
|
|
182
|
+
// ../application/src/utils/NeighborContext.ts
|
|
183
|
+
import * as fs3 from "fs";
|
|
184
|
+
import * as path4 from "path";
|
|
185
|
+
|
|
186
|
+
// ../application/src/utils/NextMigrationHints.ts
|
|
187
|
+
import * as fs2 from "fs";
|
|
188
|
+
import * as path3 from "path";
|
|
189
|
+
function findShadowRoot(filePath) {
|
|
190
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
191
|
+
const marker = "/.metamorph/shadow/";
|
|
192
|
+
const index = normalized.indexOf(marker);
|
|
193
|
+
if (index === -1) return null;
|
|
194
|
+
const after = normalized.slice(index + marker.length);
|
|
195
|
+
const runId = after.split("/")[0];
|
|
196
|
+
if (!runId) return null;
|
|
197
|
+
return filePath.slice(0, index + marker.length + runId.length);
|
|
198
|
+
}
|
|
199
|
+
function isFsdPageComponent(filePath) {
|
|
200
|
+
return /[/\\]src[/\\]pages[/\\][^/\\]+[/\\]ui[/\\]/i.test(filePath);
|
|
201
|
+
}
|
|
202
|
+
function isSpaRootComponent(filePath) {
|
|
203
|
+
return /[/\\](App|main|index)\.(t|j)sx?$/i.test(filePath) && !/[/\\]src[/\\]app[/\\]/i.test(filePath) && !isFsdPageComponent(filePath);
|
|
204
|
+
}
|
|
205
|
+
function hasNextAppRoutes(shadowRoot) {
|
|
206
|
+
const appDir = path3.join(shadowRoot, "src", "app");
|
|
207
|
+
if (!fs2.existsSync(appDir)) return false;
|
|
208
|
+
const stack = [appDir];
|
|
209
|
+
while (stack.length > 0) {
|
|
210
|
+
const dir = stack.pop();
|
|
211
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
stack.push(path3.join(dir, entry.name));
|
|
214
|
+
} else if (entry.name === "page.tsx" || entry.name === "page.jsx" || entry.name === "page.js") {
|
|
215
|
+
return true;
|
|
289
216
|
}
|
|
290
|
-
return { success: true, message: `File renamed from ${oldPath} to ${newPath}.` };
|
|
291
217
|
}
|
|
292
|
-
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
293
220
|
}
|
|
294
|
-
function
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
221
|
+
function nextMigrationFileHint(filePath, target) {
|
|
222
|
+
if (target !== "next") return "";
|
|
223
|
+
if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
|
|
224
|
+
return `
|
|
225
|
+
This file sits under src/pages. In Next.js that directory is the Pages Router.
|
|
226
|
+
Move this file out of src/pages (keep the component and its business logic) into a folder Next does not treat as routes.
|
|
227
|
+
Then add or update src/app/<route>/page.tsx so the App Router imports that screen for the former URL.
|
|
228
|
+
The home route ("/") is src/app/page.tsx and must render the former "/" screen \u2014 not an empty shell, null, a TODO, or a deleted App.tsx.
|
|
229
|
+
Delete the old src/pages copy after the screen exists elsewhere.
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
if (isSpaRootComponent(filePath)) {
|
|
233
|
+
return `
|
|
234
|
+
This is a SPA root/entry file (App.tsx / main.tsx / index.tsx).
|
|
235
|
+
Read it and map each route to src/app/<path>/page.tsx that imports the actual screen component (after moving anything that lived under src/pages).
|
|
236
|
+
src/app/page.tsx must render the home screen. Never import App from the new page (App is being removed).
|
|
237
|
+
Never write a stub that the root component "could not be resolved". After App Router files exist with real UI, delete this legacy root file.
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
if (/[/\\]src[/\\]app[/\\].*page\.(t|j)sx$/i.test(filePath)) {
|
|
241
|
+
return `
|
|
242
|
+
This is a Next App Router page. Import the existing screen component and return it.
|
|
243
|
+
Do not paste that screen's JSX here. Do not rename its public props at the wrapper.
|
|
244
|
+
Do not hollow out the module you imported from.
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
return "";
|
|
248
|
+
}
|
|
249
|
+
var NEXT_PAGES_RESERVED = /(?:^|[/\\])(_app|_document|_error|404|500)\.(t|j)sx?$/i;
|
|
250
|
+
function isSpaPagesTreeFile(filePath) {
|
|
251
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
252
|
+
if (!/\/src\/pages\//i.test(normalized)) return false;
|
|
253
|
+
if (/\/src\/pages\/api\//i.test(normalized)) return false;
|
|
254
|
+
if (NEXT_PAGES_RESERVED.test(normalized)) return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
function listFilesRecursive(dir) {
|
|
258
|
+
if (!fs2.existsSync(dir)) return [];
|
|
259
|
+
const out = [];
|
|
260
|
+
const stack = [dir];
|
|
261
|
+
while (stack.length > 0) {
|
|
262
|
+
const current = stack.pop();
|
|
263
|
+
for (const entry of fs2.readdirSync(current, { withFileTypes: true })) {
|
|
264
|
+
const full = path3.join(current, entry.name);
|
|
265
|
+
if (entry.isDirectory()) stack.push(full);
|
|
266
|
+
else if (/\.(t|j)sx?$/.test(entry.name)) out.push(full);
|
|
316
267
|
}
|
|
317
|
-
}
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
318
270
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
271
|
+
function findAppRouterHome(shadowRoot) {
|
|
272
|
+
const candidates = [
|
|
273
|
+
path3.join(shadowRoot, "src", "app", "page.tsx"),
|
|
274
|
+
path3.join(shadowRoot, "src", "app", "page.jsx"),
|
|
275
|
+
path3.join(shadowRoot, "app", "page.tsx"),
|
|
276
|
+
path3.join(shadowRoot, "app", "page.jsx")
|
|
277
|
+
];
|
|
278
|
+
return candidates.find((file) => fs2.existsSync(file)) || null;
|
|
279
|
+
}
|
|
280
|
+
function listAppRouterFiles(shadowRoot) {
|
|
281
|
+
const roots = [path3.join(shadowRoot, "src", "app"), path3.join(shadowRoot, "app")];
|
|
282
|
+
const names = /^(page|layout)\.(t|j)sx$/i;
|
|
283
|
+
return roots.flatMap((dir) => listFilesRecursive(dir).filter((file) => names.test(path3.basename(file))));
|
|
284
|
+
}
|
|
285
|
+
function isHollowView(content) {
|
|
286
|
+
const withoutComments = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
287
|
+
const hasFeatureImports = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(withoutComments);
|
|
288
|
+
const tinyTree = /<h1[^>]*>[^<]{0,40}<\/h1>/.test(withoutComments) && !hasFeatureImports;
|
|
289
|
+
const emptyMain = /<main>\s*<h1>/.test(withoutComments) && !hasFeatureImports;
|
|
290
|
+
return tinyTree || emptyMain;
|
|
291
|
+
}
|
|
292
|
+
function findViewPages(shadowRoot) {
|
|
293
|
+
const viewsDir = path3.join(shadowRoot, "src", "views");
|
|
294
|
+
return listFilesRecursive(viewsDir).filter((file) => /(?:^|[/\\])(?:.+Page|page)\.(t|j)sx$/i.test(file));
|
|
295
|
+
}
|
|
296
|
+
function localTsxImports(fromFile, content) {
|
|
297
|
+
const dir = path3.dirname(fromFile);
|
|
298
|
+
const out = [];
|
|
299
|
+
const re = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"](\.\.?\/[^'"]+|@\/[^'"]+)['"]/g;
|
|
300
|
+
let match;
|
|
301
|
+
while ((match = re.exec(content)) !== null) {
|
|
302
|
+
const resolved = match[3].startsWith("@/") ? path3.join(findSrcRoot(fromFile), match[3].slice(2)) : path3.resolve(dir, match[3]);
|
|
303
|
+
const candidates = [
|
|
304
|
+
resolved,
|
|
305
|
+
`${resolved}.tsx`,
|
|
306
|
+
`${resolved}.jsx`,
|
|
307
|
+
`${resolved}.ts`,
|
|
308
|
+
path3.join(resolved, "index.tsx"),
|
|
309
|
+
path3.join(resolved, "index.ts")
|
|
310
|
+
];
|
|
311
|
+
const absPath = candidates.find((file) => fs2.existsSync(file));
|
|
312
|
+
if (!absPath) continue;
|
|
313
|
+
if (match[2]) out.push({ tag: match[2], absPath });
|
|
314
|
+
if (match[1]) {
|
|
315
|
+
for (const part of match[1].split(",")) {
|
|
316
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
317
|
+
if (name) out.push({ tag: name, absPath });
|
|
343
318
|
}
|
|
344
|
-
return { success: true, message: `File marked as obsolete at ${obsoletePath} instead of deleting.` };
|
|
345
319
|
}
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ../infrastructure/src/workspace/ShadowWorkspace.ts
|
|
350
|
-
import fs from "fs-extra";
|
|
351
|
-
import { join as join3, resolve } from "path";
|
|
352
|
-
var ShadowWorkspace = class {
|
|
353
|
-
baseDir;
|
|
354
|
-
constructor(basePath = ".metamorph/shadow") {
|
|
355
|
-
this.baseDir = resolve(process.cwd(), basePath);
|
|
356
320
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function findSrcRoot(filePath) {
|
|
324
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
325
|
+
const idx = normalized.lastIndexOf("/src/");
|
|
326
|
+
if (idx === -1) return path3.dirname(filePath);
|
|
327
|
+
return filePath.slice(0, idx + 4);
|
|
328
|
+
}
|
|
329
|
+
function declaredCallbackProps(componentSource) {
|
|
330
|
+
const names = /* @__PURE__ */ new Set();
|
|
331
|
+
const iface = /interface\s+\w+\s*\{([^}]+)\}/g;
|
|
332
|
+
let block;
|
|
333
|
+
while ((block = iface.exec(componentSource)) !== null) {
|
|
334
|
+
const field = /(\b(?:on[A-Z]\w+|action[A-Z]\w+)\??)\s*:/g;
|
|
335
|
+
let f;
|
|
336
|
+
while ((f = field.exec(block[1])) !== null) names.add(f[1].replace("?", ""));
|
|
337
|
+
}
|
|
338
|
+
const dest = /export\s+(?:default\s+)?function\s+\w+\s*\(\s*\{\s*([^}]+)\}/;
|
|
339
|
+
const d = dest.exec(componentSource);
|
|
340
|
+
if (d) {
|
|
341
|
+
for (const part of d[1].split(",")) {
|
|
342
|
+
const name = part.trim().split(":")[0].trim();
|
|
343
|
+
if (/^(on[A-Z]|action[A-Z])/.test(name)) names.add(name);
|
|
371
344
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
}
|
|
346
|
+
return [...names];
|
|
347
|
+
}
|
|
348
|
+
function jsxCallbackProps(callerSource, tag) {
|
|
349
|
+
const names = [];
|
|
350
|
+
const tagRe = new RegExp(`<${tag}\\b([^>]*)\\/?>`, "g");
|
|
351
|
+
let match;
|
|
352
|
+
while ((match = tagRe.exec(callerSource)) !== null) {
|
|
353
|
+
const propRe = /\b((?:on|action)[A-Z]\w+)\s*=/g;
|
|
354
|
+
let p;
|
|
355
|
+
while ((p = propRe.exec(match[1])) !== null) names.push(p[1]);
|
|
356
|
+
}
|
|
357
|
+
return names;
|
|
358
|
+
}
|
|
359
|
+
function analyzeCallbackPropMismatches(shadowRoot) {
|
|
360
|
+
const issues = [];
|
|
361
|
+
const files = [
|
|
362
|
+
...listFilesRecursive(path3.join(shadowRoot, "src", "app")),
|
|
363
|
+
...listFilesRecursive(path3.join(shadowRoot, "app")),
|
|
364
|
+
...listFilesRecursive(path3.join(shadowRoot, "src", "views"))
|
|
365
|
+
].filter((file) => /\.(t|j)sx$/.test(file));
|
|
366
|
+
for (const file of files) {
|
|
367
|
+
const content = fs2.readFileSync(file, "utf-8");
|
|
368
|
+
for (const imp of localTsxImports(file, content)) {
|
|
369
|
+
let callee;
|
|
370
|
+
try {
|
|
371
|
+
callee = fs2.readFileSync(imp.absPath, "utf-8");
|
|
372
|
+
} catch {
|
|
376
373
|
continue;
|
|
377
374
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
375
|
+
const expected = declaredCallbackProps(callee);
|
|
376
|
+
if (expected.length === 0) continue;
|
|
377
|
+
const passed = jsxCallbackProps(content, imp.tag);
|
|
378
|
+
const missing = expected.filter((name) => !passed.includes(name));
|
|
379
|
+
const extra = passed.filter((name) => !expected.includes(name));
|
|
380
|
+
if (missing.length === 0 && extra.length === 0) continue;
|
|
381
|
+
issues.push({
|
|
382
|
+
filePath: file,
|
|
383
|
+
errors: [
|
|
384
|
+
`<${imp.tag}> public callback props do not match ${path3.relative(shadowRoot, imp.absPath)}. Expected: ${expected.join(", ") || "(none)"}. Passed: ${passed.join(", ") || "(none)"}. Do not rename props when wrapping a screen in a router file.`
|
|
385
|
+
]
|
|
382
386
|
});
|
|
383
387
|
}
|
|
384
|
-
return shadowDir;
|
|
385
388
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
return issues;
|
|
390
|
+
}
|
|
391
|
+
function analyzeReactToNextComposition(shadowRoot) {
|
|
392
|
+
const issues = [];
|
|
393
|
+
const home = findAppRouterHome(shadowRoot);
|
|
394
|
+
if (!home) return issues;
|
|
395
|
+
const homeContent = fs2.readFileSync(home, "utf-8");
|
|
396
|
+
const views = findViewPages(shadowRoot);
|
|
397
|
+
const hollowViews = views.filter((file) => isHollowView(fs2.readFileSync(file, "utf-8")));
|
|
398
|
+
const homeImportsView = views.some((file) => {
|
|
399
|
+
const stem = path3.basename(file).replace(/\.(t|j)sx$/, "");
|
|
400
|
+
return homeContent.includes(stem);
|
|
401
|
+
});
|
|
402
|
+
const homeAssemblesFeatures = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(homeContent);
|
|
403
|
+
if (views.length > 0 && !homeImportsView && homeAssemblesFeatures) {
|
|
404
|
+
issues.push({
|
|
405
|
+
filePath: home,
|
|
406
|
+
errors: [
|
|
407
|
+
"The App Router home reassembles the UI from lower-level modules instead of importing the existing route-level screen. Import that screen and return it. Renaming props at the wrapper causes runtime TypeError while next build still succeeds."
|
|
408
|
+
]
|
|
409
|
+
});
|
|
395
410
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
for (const file of hollowViews) {
|
|
412
|
+
issues.push({
|
|
413
|
+
filePath: file,
|
|
414
|
+
errors: [
|
|
415
|
+
"This screen was hollowed out. Restore the original UI here. The App Router page should import this file, not replace it with a stub heading."
|
|
416
|
+
]
|
|
417
|
+
});
|
|
401
418
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
419
|
+
return issues;
|
|
420
|
+
}
|
|
421
|
+
function isStubAppRouterPage(content) {
|
|
422
|
+
const hasLocalImport = /from\s+['"](\.\.?\/|@\/)/.test(content);
|
|
423
|
+
const emptyish = /return\s*\(\s*<main\s*\/>\s*\)|return\s+null\s*;?|return\s*\(\s*<>\s*<\/>\s*\)|TODO:|could not be resolved/i.test(content);
|
|
424
|
+
const onlyLayoutShell = /<main\s*\/>/.test(content) && !hasLocalImport;
|
|
425
|
+
return !hasLocalImport || emptyish || onlyLayoutShell;
|
|
426
|
+
}
|
|
427
|
+
function analyzeReactToNextStructure(shadowRoot) {
|
|
428
|
+
const issues = [];
|
|
429
|
+
const pagesDir = path3.join(shadowRoot, "src", "pages");
|
|
430
|
+
if (fs2.existsSync(pagesDir)) {
|
|
431
|
+
for (const file of listFilesRecursive(pagesDir)) {
|
|
432
|
+
if (!isSpaPagesTreeFile(file)) continue;
|
|
433
|
+
issues.push({
|
|
434
|
+
filePath: file,
|
|
435
|
+
errors: [
|
|
436
|
+
"This UI still lives under src/pages. Next.js will treat it as a Pages Router route. Move it to a folder Next does not reserve, then import it from src/app/**/page.tsx for the old URL. Remove the src/pages copy."
|
|
437
|
+
]
|
|
438
|
+
});
|
|
415
439
|
}
|
|
416
440
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
const home = findAppRouterHome(shadowRoot);
|
|
442
|
+
if (!home) {
|
|
443
|
+
issues.push({
|
|
444
|
+
filePath: path3.join(shadowRoot, "src", "app", "page.tsx"),
|
|
445
|
+
errors: [
|
|
446
|
+
'Missing App Router home (src/app/page.tsx). Create it so it imports and renders the screen the SPA showed at "/".'
|
|
447
|
+
]
|
|
448
|
+
});
|
|
449
|
+
return issues;
|
|
450
|
+
}
|
|
451
|
+
const homeContent = fs2.readFileSync(home, "utf-8");
|
|
452
|
+
if (isStubAppRouterPage(homeContent)) {
|
|
453
|
+
issues.push({
|
|
454
|
+
filePath: home,
|
|
455
|
+
errors: [
|
|
456
|
+
"src/app/page.tsx does not render the application UI. Import the former home screen component (after moving any SPA UI out of src/pages if needed). Do not return an empty shell, null, a TODO, or a deleted App.tsx."
|
|
457
|
+
]
|
|
458
|
+
});
|
|
428
459
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
return this.applyWithZipFallback(runId, shadowDir, resolvedTarget);
|
|
460
|
+
issues.push(...analyzeReactToNextComposition(shadowRoot));
|
|
461
|
+
issues.push(...analyzeCallbackPropMismatches(shadowRoot));
|
|
462
|
+
const pkgPath = path3.join(shadowRoot, "package.json");
|
|
463
|
+
if (fs2.existsSync(pkgPath)) {
|
|
464
|
+
try {
|
|
465
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
466
|
+
const viteScripts = Object.entries(pkg.scripts || {}).filter(([, cmd]) => /\bvite\b/.test(cmd));
|
|
467
|
+
if (viteScripts.length > 0) {
|
|
468
|
+
issues.push({
|
|
469
|
+
filePath: pkgPath,
|
|
470
|
+
errors: [
|
|
471
|
+
`package.json still runs Vite in scripts (${viteScripts.map(([name]) => name).join(", ")}). After moving to Next, drop leftover preview/vite scripts.`
|
|
472
|
+
]
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
446
476
|
}
|
|
447
477
|
}
|
|
448
|
-
|
|
449
|
-
|
|
478
|
+
return issues;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ../application/src/utils/NeighborContext.ts
|
|
482
|
+
var MAX_FILES = 8;
|
|
483
|
+
var MAX_FILE_CHARS = 4e3;
|
|
484
|
+
var MAX_TOTAL_CHARS = 24e3;
|
|
485
|
+
function readTruncated(filePath) {
|
|
486
|
+
try {
|
|
487
|
+
if (!fs3.existsSync(filePath) || !fs3.statSync(filePath).isFile()) return null;
|
|
488
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
489
|
+
if (raw.length <= MAX_FILE_CHARS) return raw;
|
|
490
|
+
return `${raw.slice(0, MAX_FILE_CHARS)}
|
|
491
|
+
|
|
492
|
+
/* \u2026 truncated (${raw.length} chars) */`;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
450
495
|
}
|
|
451
|
-
|
|
452
|
-
|
|
496
|
+
}
|
|
497
|
+
function findSrcRoot2(filePath) {
|
|
498
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
499
|
+
const idx = normalized.lastIndexOf("/src/");
|
|
500
|
+
if (idx === -1) return null;
|
|
501
|
+
return filePath.slice(0, idx + 4);
|
|
502
|
+
}
|
|
503
|
+
function resolveImport(fromFile, spec) {
|
|
504
|
+
const dir = path4.dirname(fromFile);
|
|
505
|
+
const base = spec.startsWith("@/") ? (() => {
|
|
506
|
+
const src = findSrcRoot2(fromFile);
|
|
507
|
+
return src ? path4.join(src, spec.slice(2)) : path4.resolve(dir, spec);
|
|
508
|
+
})() : path4.resolve(dir, spec);
|
|
509
|
+
const candidates = [
|
|
510
|
+
base,
|
|
511
|
+
`${base}.tsx`,
|
|
512
|
+
`${base}.ts`,
|
|
513
|
+
`${base}.jsx`,
|
|
514
|
+
`${base}.js`,
|
|
515
|
+
`${base}.vue`,
|
|
516
|
+
`${base}.svelte`,
|
|
517
|
+
path4.join(base, "index.tsx"),
|
|
518
|
+
path4.join(base, "index.ts"),
|
|
519
|
+
path4.join(base, "index.vue"),
|
|
520
|
+
path4.join(base, "index.svelte")
|
|
521
|
+
];
|
|
522
|
+
return candidates.find((file) => fs3.existsSync(file) && fs3.statSync(file).isFile()) || null;
|
|
523
|
+
}
|
|
524
|
+
function localImportSpecs(content) {
|
|
525
|
+
const specs = [];
|
|
526
|
+
const re = /from\s+['"](\.\.?\/[^'"]+|@\/[^'"]+)['"]/g;
|
|
527
|
+
let match;
|
|
528
|
+
while ((match = re.exec(content)) !== null) specs.push(match[1]);
|
|
529
|
+
return specs;
|
|
530
|
+
}
|
|
531
|
+
function listNearbyModules(fromFile, limit) {
|
|
532
|
+
const shadow = findShadowRoot(fromFile);
|
|
533
|
+
const src = findSrcRoot2(fromFile) || (shadow ? path4.join(shadow, "src") : path4.dirname(fromFile));
|
|
534
|
+
if (!fs3.existsSync(src)) return [];
|
|
535
|
+
const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
|
|
536
|
+
const out = [];
|
|
537
|
+
const stack = [src];
|
|
538
|
+
while (stack.length > 0 && out.length < limit) {
|
|
539
|
+
const dir = stack.pop();
|
|
540
|
+
let entries;
|
|
453
541
|
try {
|
|
454
|
-
|
|
455
|
-
} catch
|
|
456
|
-
|
|
457
|
-
throw new Error(`Failed to create/reset git branch ${branchName}: ${e.message}`);
|
|
458
|
-
}
|
|
459
|
-
throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
|
|
542
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
543
|
+
} catch {
|
|
544
|
+
continue;
|
|
460
545
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
throw new Error(`Failed to commit changes to branch ${branchName}: ${e.message}`);
|
|
468
|
-
}
|
|
469
|
-
throw new Error(`Failed to commit changes to branch ${branchName}: ${String(e)}`);
|
|
546
|
+
for (const entry of entries) {
|
|
547
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
548
|
+
const full = path4.join(dir, entry.name);
|
|
549
|
+
if (entry.isDirectory()) stack.push(full);
|
|
550
|
+
else if (interesting.test(entry.name)) out.push(full);
|
|
551
|
+
if (out.length >= limit) break;
|
|
470
552
|
}
|
|
471
|
-
return `Successfully applied migration via Git to branch: ${branchName}`;
|
|
472
553
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const itemDest = path.join(targetPath, item);
|
|
496
|
-
fs2.copySync(itemSrc, itemDest, { overwrite: true });
|
|
497
|
-
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
function buildNeighborContext(filePath) {
|
|
557
|
+
const sections = [];
|
|
558
|
+
let budget = MAX_TOTAL_CHARS;
|
|
559
|
+
const push = (title, body) => {
|
|
560
|
+
const block = `### ${title}
|
|
561
|
+
\`\`\`
|
|
562
|
+
${body}
|
|
563
|
+
\`\`\`
|
|
564
|
+
`;
|
|
565
|
+
if (block.length > budget) return;
|
|
566
|
+
budget -= block.length;
|
|
567
|
+
sections.push(block);
|
|
568
|
+
};
|
|
569
|
+
const self = readTruncated(filePath);
|
|
570
|
+
if (self) {
|
|
571
|
+
push(filePath, self);
|
|
572
|
+
} else {
|
|
573
|
+
sections.push(`### ${filePath}
|
|
574
|
+
(file does not exist yet \u2014 you may need to create it)
|
|
575
|
+
`);
|
|
498
576
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
architecturalRules: [
|
|
513
|
-
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
514
|
-
'Remove all NestJS core imports (e.g., from "@nestjs/common" or "@nestjs/core").',
|
|
515
|
-
"Controllers must be converted into Express Router factory functions or classes that return an Express Router.",
|
|
516
|
-
"Modules must be converted to plain dependency composition functions or startup scripts that wire Routers together.",
|
|
517
|
-
"Services/Providers must be converted to plain TypeScript classes or functions, with dependencies passed explicitly via constructors (manual dependency injection) since NestJS DI is gone.",
|
|
518
|
-
"Make sure to export the router or the configured Express application, not a Nest module."
|
|
519
|
-
],
|
|
520
|
-
examples: [
|
|
521
|
-
{
|
|
522
|
-
description: "Converting a NestJS Controller to an Express Router",
|
|
523
|
-
before: `import { Controller, Get } from '@nestjs/common';
|
|
524
|
-
import { AppService } from './app.service';
|
|
525
|
-
|
|
526
|
-
@Controller()
|
|
527
|
-
export class AppController {
|
|
528
|
-
constructor(private readonly appService: AppService) {}
|
|
529
|
-
|
|
530
|
-
@Get()
|
|
531
|
-
getHello(): string {
|
|
532
|
-
return this.appService.getHello();
|
|
577
|
+
const importSpecs = self ? localImportSpecs(self) : [];
|
|
578
|
+
const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path4.resolve(filePath)));
|
|
579
|
+
const unique = [...new Set(resolved)].slice(0, MAX_FILES);
|
|
580
|
+
for (const dep of unique) {
|
|
581
|
+
const body = readTruncated(dep);
|
|
582
|
+
if (body) push(`imported: ${dep}`, body);
|
|
583
|
+
}
|
|
584
|
+
const nearby = listNearbyModules(filePath, 24).filter((file) => path4.resolve(file) !== path4.resolve(filePath));
|
|
585
|
+
if (nearby.length > 0) {
|
|
586
|
+
const listing = nearby.map((file) => path4.relative(findShadowRoot(filePath) || path4.dirname(filePath), file)).join("\n");
|
|
587
|
+
sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
|
|
588
|
+
${listing}
|
|
589
|
+
`);
|
|
533
590
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const router = Router();
|
|
540
|
-
|
|
541
|
-
router.get('/', (req, res) => {
|
|
542
|
-
const result = appService.getHello();
|
|
543
|
-
res.send(result);
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
return router;
|
|
547
|
-
}`
|
|
548
|
-
}
|
|
549
|
-
]
|
|
550
|
-
},
|
|
551
|
-
{
|
|
552
|
-
source: "express",
|
|
553
|
-
target: "nestjs",
|
|
554
|
-
description: "Migration from a plain Express application to the NestJS framework.",
|
|
555
|
-
dependenciesToRemove: ["express"],
|
|
556
|
-
dependenciesToAdd: {
|
|
557
|
-
"@nestjs/common": "latest",
|
|
558
|
-
"@nestjs/core": "latest",
|
|
559
|
-
"@nestjs/platform-express": "latest",
|
|
560
|
-
"reflect-metadata": "^0.1.13",
|
|
561
|
-
"rxjs": "^7.8.1"
|
|
562
|
-
},
|
|
563
|
-
devDependenciesToRemove: ["@types/express"],
|
|
564
|
-
devDependenciesToAdd: {
|
|
565
|
-
"@nestjs/cli": "latest",
|
|
566
|
-
"@nestjs/schematics": "latest",
|
|
567
|
-
"@nestjs/testing": "latest",
|
|
568
|
-
"@types/supertest": "^2.0.12",
|
|
569
|
-
"source-map-support": "^0.5.21",
|
|
570
|
-
"supertest": "^6.3.3"
|
|
571
|
-
},
|
|
572
|
-
architecturalRules: [
|
|
573
|
-
"Express routing logic must be encapsulated in classes decorated with @Controller().",
|
|
574
|
-
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
575
|
-
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
576
|
-
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of Express route definitions.",
|
|
577
|
-
"Rely on NestJS dependency injection instead of manual class instantiation or function passing.",
|
|
578
|
-
"The entry point must use NestFactory.create() to bootstrap the application, replacing app.listen().",
|
|
579
|
-
"You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
|
|
580
|
-
],
|
|
581
|
-
examples: [
|
|
582
|
-
{
|
|
583
|
-
description: "Converting an Express Router to a NestJS Controller and Service",
|
|
584
|
-
before: `import { Router } from 'express';
|
|
585
|
-
import { getHelloLogic } from './logic';
|
|
586
|
-
|
|
587
|
-
export function createAppRouter(): Router {
|
|
588
|
-
const router = Router();
|
|
589
|
-
router.get('/', (req, res) => {
|
|
590
|
-
res.send(getHelloLogic());
|
|
591
|
-
});
|
|
592
|
-
return router;
|
|
593
|
-
}`,
|
|
594
|
-
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
595
|
-
|
|
596
|
-
@Injectable()
|
|
597
|
-
export class AppService {
|
|
598
|
-
getHello(): string {
|
|
599
|
-
return 'Hello from logic';
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
@Controller()
|
|
604
|
-
export class AppController {
|
|
605
|
-
constructor(private readonly appService: AppService) {}
|
|
606
|
-
|
|
607
|
-
@Get()
|
|
608
|
-
getHello(): string {
|
|
609
|
-
return this.appService.getHello();
|
|
610
|
-
}
|
|
611
|
-
}`
|
|
612
|
-
}
|
|
613
|
-
]
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
source: "nestjs",
|
|
617
|
-
target: "fastify",
|
|
618
|
-
description: "Migration from NestJS framework to a Fastify application structure.",
|
|
619
|
-
filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
|
|
620
|
-
dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
|
|
621
|
-
dependenciesToAdd: { "fastify": "latest" },
|
|
622
|
-
devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
|
|
623
|
-
devDependenciesToAdd: { "fastify-plugin": "latest" },
|
|
624
|
-
architecturalRules: [
|
|
625
|
-
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
626
|
-
"Remove all NestJS core imports.",
|
|
627
|
-
"The entry point (main.ts) must initialize a FastifyInstance (fastify()) instead of NestFactory.",
|
|
628
|
-
"Controllers and routing logic must be converted to Fastify route declarations (fastify.get, fastify.post).",
|
|
629
|
-
"Ideally, encapsulate routes and modules as Fastify plugins.",
|
|
630
|
-
"Remove NestJS dependency injection; use standard JavaScript/TypeScript classes or functions passing dependencies directly.",
|
|
631
|
-
"Ensure standard Fastify schema validation (JSON Schema) is added if types or DTOs are available.",
|
|
632
|
-
"You are authorized to rename, create, or delete files to adhere to a standard fastify plugin-based directory structure."
|
|
633
|
-
],
|
|
634
|
-
examples: [
|
|
635
|
-
{
|
|
636
|
-
description: "Converting a NestJS Controller to a Fastify Plugin",
|
|
637
|
-
before: `import { Controller, Get } from '@nestjs/common';
|
|
638
|
-
import { AppService } from './app.service';
|
|
639
|
-
|
|
640
|
-
@Controller('users')
|
|
641
|
-
export class UsersController {
|
|
642
|
-
constructor(private readonly appService: AppService) {}
|
|
643
|
-
|
|
644
|
-
@Get()
|
|
645
|
-
getUsers() {
|
|
646
|
-
return this.appService.getUsers();
|
|
647
|
-
}
|
|
648
|
-
}`,
|
|
649
|
-
after: `import { FastifyInstance } from 'fastify';
|
|
650
|
-
import { AppService } from './app.service';
|
|
651
|
-
|
|
652
|
-
export default async function usersPlugin(fastify: FastifyInstance, options: any) {
|
|
653
|
-
const appService = new AppService(); // Or inject via options
|
|
654
|
-
|
|
655
|
-
fastify.get('/users', async (request, reply) => {
|
|
656
|
-
return appService.getUsers();
|
|
657
|
-
});
|
|
658
|
-
}`
|
|
659
|
-
}
|
|
660
|
-
]
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
source: "express",
|
|
664
|
-
target: "fastify",
|
|
665
|
-
description: "Migration from a plain Express application to Fastify.",
|
|
666
|
-
dependenciesToRemove: ["express"],
|
|
667
|
-
dependenciesToAdd: { "fastify": "latest", "fastify-plugin": "latest" },
|
|
668
|
-
devDependenciesToRemove: ["@types/express"],
|
|
669
|
-
devDependenciesToAdd: {},
|
|
670
|
-
architecturalRules: [
|
|
671
|
-
"Replace all Express app instantiations with fastify().",
|
|
672
|
-
"Replace Express middleware signatures (req, res, next) with Fastify hooks (onRequest, preHandler, etc.) or Fastify plugins.",
|
|
673
|
-
"Replace Express routing (app.use(), app.get()) with Fastify routing (fastify.get(), fastify.register()).",
|
|
674
|
-
"Replace Express response methods (res.send(), res.json()) with Fastify reply methods (reply.send()) or simply return the payload from the async handler.",
|
|
675
|
-
"Migrate standard Express middlewares (like cors, helmet) to their Fastify native equivalents (@fastify/cors, @fastify/helmet) if they are present.",
|
|
676
|
-
"You are authorized to rename, create, or delete files to restructure the Express app into modular Fastify plugins."
|
|
677
|
-
],
|
|
678
|
-
examples: [
|
|
679
|
-
{
|
|
680
|
-
description: "Converting an Express Route to a Fastify Route",
|
|
681
|
-
before: `const express = require('express');
|
|
682
|
-
const app = express();
|
|
683
|
-
|
|
684
|
-
app.get('/api/data', (req, res) => {
|
|
685
|
-
res.json({ message: 'Hello Express' });
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
app.listen(3000);`,
|
|
689
|
-
after: `import Fastify from 'fastify';
|
|
690
|
-
|
|
691
|
-
const fastify = Fastify({ logger: true });
|
|
692
|
-
|
|
693
|
-
fastify.get('/api/data', async (request, reply) => {
|
|
694
|
-
return { message: 'Hello Fastify' };
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
fastify.listen({ port: 3000 }, (err) => {
|
|
698
|
-
if (err) {
|
|
699
|
-
fastify.log.error(err);
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
});`
|
|
703
|
-
}
|
|
704
|
-
]
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
source: "fastify",
|
|
708
|
-
target: "express",
|
|
709
|
-
description: "Migration from a Fastify application to Express.",
|
|
710
|
-
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
711
|
-
dependenciesToAdd: { "express": "latest" },
|
|
712
|
-
devDependenciesToRemove: [],
|
|
713
|
-
devDependenciesToAdd: { "@types/express": "latest" },
|
|
714
|
-
architecturalRules: [
|
|
715
|
-
"Replace all fastify() app instantiations with express().",
|
|
716
|
-
"Replace Fastify hooks (onRequest, preHandler, etc.) with standard Express middleware signatures (req, res, next).",
|
|
717
|
-
"Replace Fastify routing (fastify.get, fastify.register) with Express routing (app.get, express.Router().use).",
|
|
718
|
-
"Replace Fastify reply methods (reply.send) or implicit returns with Express response methods (res.send, res.json).",
|
|
719
|
-
"CRITICAL: Fastify catches async errors automatically. When migrating to Express, ensure all asynchronous route handlers use try/catch blocks passing the error to next(err), or wrap them in an async wrapper.",
|
|
720
|
-
"You are authorized to rename, create, or delete files to restructure the Fastify plugins into standard Express routers."
|
|
721
|
-
],
|
|
722
|
-
examples: [
|
|
723
|
-
{
|
|
724
|
-
description: "Converting a Fastify Route to an Express Route",
|
|
725
|
-
before: `import Fastify from 'fastify';
|
|
726
|
-
|
|
727
|
-
const fastify = Fastify();
|
|
728
|
-
|
|
729
|
-
fastify.get('/api/data', async (request, reply) => {
|
|
730
|
-
return { message: 'Hello Fastify' };
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
fastify.listen({ port: 3000 });`,
|
|
734
|
-
after: `const express = require('express');
|
|
735
|
-
const app = express();
|
|
736
|
-
|
|
737
|
-
app.get('/api/data', async (req, res, next) => {
|
|
738
|
-
try {
|
|
739
|
-
res.json({ message: 'Hello Fastify' });
|
|
740
|
-
} catch (err) {
|
|
741
|
-
next(err);
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
app.listen(3000);`
|
|
746
|
-
}
|
|
747
|
-
]
|
|
748
|
-
},
|
|
749
|
-
{
|
|
750
|
-
source: "fastify",
|
|
751
|
-
target: "nestjs",
|
|
752
|
-
description: "Migration from Fastify framework to NestJS (using platform-express by default).",
|
|
753
|
-
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
754
|
-
dependenciesToAdd: {
|
|
755
|
-
"@nestjs/common": "latest",
|
|
756
|
-
"@nestjs/core": "latest",
|
|
757
|
-
"@nestjs/platform-express": "latest",
|
|
758
|
-
"reflect-metadata": "^0.1.13",
|
|
759
|
-
"rxjs": "^7.8.1"
|
|
760
|
-
},
|
|
761
|
-
devDependenciesToRemove: [],
|
|
762
|
-
devDependenciesToAdd: {
|
|
763
|
-
"@nestjs/cli": "latest",
|
|
764
|
-
"@nestjs/schematics": "latest",
|
|
765
|
-
"@nestjs/testing": "latest",
|
|
766
|
-
"@types/supertest": "^2.0.12",
|
|
767
|
-
"source-map-support": "^0.5.21",
|
|
768
|
-
"supertest": "^6.3.3"
|
|
769
|
-
},
|
|
770
|
-
architecturalRules: [
|
|
771
|
-
"Fastify routing logic must be encapsulated in classes decorated with @Controller().",
|
|
772
|
-
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
773
|
-
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
774
|
-
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of fastify route definitions.",
|
|
775
|
-
"Rely on NestJS dependency injection instead of Fastify decorators or options passing.",
|
|
776
|
-
"The entry point must use NestFactory.create() to bootstrap the application, replacing fastify().",
|
|
777
|
-
"You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
|
|
778
|
-
],
|
|
779
|
-
examples: [
|
|
780
|
-
{
|
|
781
|
-
description: "Converting a Fastify Plugin to a NestJS Controller and Service",
|
|
782
|
-
before: `export default async function usersPlugin(fastify, options) {
|
|
783
|
-
fastify.get('/users', async (request, reply) => {
|
|
784
|
-
return { users: [] };
|
|
785
|
-
});
|
|
786
|
-
}`,
|
|
787
|
-
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
788
|
-
|
|
789
|
-
@Injectable()
|
|
790
|
-
export class UsersService {
|
|
791
|
-
getUsers() {
|
|
792
|
-
return { users: [] };
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
@Controller('users')
|
|
797
|
-
export class UsersController {
|
|
798
|
-
constructor(private readonly usersService: UsersService) {}
|
|
799
|
-
|
|
800
|
-
@Get()
|
|
801
|
-
getUsers() {
|
|
802
|
-
return this.usersService.getUsers();
|
|
803
|
-
}
|
|
804
|
-
}`
|
|
805
|
-
}
|
|
806
|
-
]
|
|
807
|
-
}
|
|
808
|
-
];
|
|
809
|
-
|
|
810
|
-
// ../domain/src/entities/catalogs/frontendCatalog.ts
|
|
811
|
-
var frontendCatalog = [
|
|
812
|
-
{
|
|
813
|
-
source: "react",
|
|
814
|
-
target: "next",
|
|
815
|
-
description: "Migration from a plain React SPA (like Vite or Create React App) to Next.js using the App Router.",
|
|
816
|
-
filesToDelete: ["vite.config.ts", "vite.config.js", "index.html"],
|
|
817
|
-
dependenciesToRemove: ["react-router-dom", "vite", "@vitejs/plugin-react"],
|
|
818
|
-
dependenciesToAdd: { "next": "latest", "react": "latest", "react-dom": "latest" },
|
|
819
|
-
devDependenciesToRemove: [],
|
|
820
|
-
devDependenciesToAdd: {},
|
|
821
|
-
architecturalRules: [
|
|
822
|
-
'Migrate standard React routing (e.g. react-router-dom) to Next.js file-system routing in the "app" directory.',
|
|
823
|
-
"Convert the main entry point (usually main.tsx or index.tsx) into Next.js app/layout.tsx and app/page.tsx.",
|
|
824
|
-
'For components that use browser-only APIs (like window, useEffect, useState), add the "use client" directive at the very top of the file.',
|
|
825
|
-
"Remove any react-router-dom <BrowserRouter>, <Routes>, and <Route> components.",
|
|
826
|
-
"Replace react-router-dom <Link> components with Next.js next/link <Link> components.",
|
|
827
|
-
"You are authorized to rename, create, or delete files to restructure the source into the Next.js App Router structure (e.g. app/page.tsx, app/layout.tsx)."
|
|
828
|
-
],
|
|
829
|
-
examples: [
|
|
830
|
-
{
|
|
831
|
-
description: "Converting a React Router setup to Next.js App Router",
|
|
832
|
-
before: `import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
833
|
-
import Home from './Home';
|
|
834
|
-
import About from './About';
|
|
835
|
-
|
|
836
|
-
export default function App() {
|
|
837
|
-
return (
|
|
838
|
-
<BrowserRouter>
|
|
839
|
-
<Routes>
|
|
840
|
-
<Route path="/" element={<Home />} />
|
|
841
|
-
<Route path="/about" element={<About />} />
|
|
842
|
-
</Routes>
|
|
843
|
-
</BrowserRouter>
|
|
844
|
-
);
|
|
845
|
-
}`,
|
|
846
|
-
after: `// This routing logic is deleted.
|
|
847
|
-
// Instead, create app/page.tsx for Home and app/about/page.tsx for About.`
|
|
848
|
-
}
|
|
849
|
-
]
|
|
850
|
-
},
|
|
851
|
-
{
|
|
852
|
-
source: "next",
|
|
853
|
-
target: "react",
|
|
854
|
-
description: "Migration from Next.js to a plain React SPA using Vite.",
|
|
855
|
-
filesToDelete: ["next.config.js", "next.config.mjs", "next.config.ts"],
|
|
856
|
-
dependenciesToRemove: ["next"],
|
|
857
|
-
dependenciesToAdd: { "react-router-dom": "latest" },
|
|
858
|
-
devDependenciesToRemove: [],
|
|
859
|
-
devDependenciesToAdd: { "vite": "latest", "@vitejs/plugin-react": "latest" },
|
|
860
|
-
architecturalRules: [
|
|
861
|
-
"Replace Next.js file-system routing (app/ or pages/) with declarative React Router (react-router-dom) components.",
|
|
862
|
-
'Remove all "use client" and "use server" directives, as they are Next.js specific.',
|
|
863
|
-
"Convert Next.js Server Components into standard React Client Components (they will need to fetch data via useEffect or a data-fetching library).",
|
|
864
|
-
'Replace Next.js <Link href="..."> with React Router <Link to="...">.',
|
|
865
|
-
"Replace Next.js next/image <Image> with standard HTML <img>.",
|
|
866
|
-
"Create a standard Vite entry point (index.html at the root, and src/main.tsx) to bootstrap the React application."
|
|
867
|
-
]
|
|
868
|
-
},
|
|
869
|
-
{
|
|
870
|
-
source: "vue",
|
|
871
|
-
target: "react",
|
|
872
|
-
description: "Migration from Vue.js (Vue 3 Composition API) to React.",
|
|
873
|
-
filesToDelete: ["vite.config.ts", "vue.config.js"],
|
|
874
|
-
dependenciesToRemove: ["vue", "vue-router", "pinia"],
|
|
875
|
-
dependenciesToAdd: { "react": "latest", "react-dom": "latest", "react-router-dom": "latest" },
|
|
876
|
-
devDependenciesToRemove: ["@vitejs/plugin-vue"],
|
|
877
|
-
devDependenciesToAdd: { "@vitejs/plugin-react": "latest" },
|
|
878
|
-
architecturalRules: [
|
|
879
|
-
"Convert all .vue Single File Components (SFC) into .tsx or .jsx React function components.",
|
|
880
|
-
"Replace Vue <template> with React JSX/TSX return statements.",
|
|
881
|
-
"Replace Vue ref() and reactive() with React useState().",
|
|
882
|
-
"Replace Vue computed() with React useMemo().",
|
|
883
|
-
"Replace Vue watch() and lifecycle hooks (onMounted, etc.) with React useEffect().",
|
|
884
|
-
"Replace vue-router with react-router-dom.",
|
|
885
|
-
"Convert Vue props definition (defineProps) into React function component parameters.",
|
|
886
|
-
"Convert Vue emit (defineEmits) into React callback props (e.g. onAction={...})."
|
|
887
|
-
]
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
source: "react",
|
|
891
|
-
target: "vue",
|
|
892
|
-
description: "Migration from React to Vue.js (Vue 3 Composition API).",
|
|
893
|
-
filesToDelete: ["vite.config.ts"],
|
|
894
|
-
dependenciesToRemove: ["react", "react-dom", "react-router-dom"],
|
|
895
|
-
dependenciesToAdd: { "vue": "latest", "vue-router": "latest" },
|
|
896
|
-
devDependenciesToRemove: ["@vitejs/plugin-react"],
|
|
897
|
-
devDependenciesToAdd: { "@vitejs/plugin-vue": "latest" },
|
|
898
|
-
architecturalRules: [
|
|
899
|
-
"Convert React .tsx/.jsx function components into Vue 3 .vue Single File Components (using <script setup>).",
|
|
900
|
-
"Move the JSX return statement into the Vue <template> block.",
|
|
901
|
-
"Replace React useState() with Vue ref() or reactive().",
|
|
902
|
-
"Replace React useMemo() with Vue computed().",
|
|
903
|
-
"Replace React useEffect() with Vue watch() or lifecycle hooks (onMounted).",
|
|
904
|
-
"Replace react-router-dom with vue-router.",
|
|
905
|
-
"Extract component props into defineProps().",
|
|
906
|
-
"Extract callback props (e.g. onClick) into defineEmits() and emit events."
|
|
907
|
-
]
|
|
908
|
-
}
|
|
909
|
-
];
|
|
910
|
-
|
|
911
|
-
// ../domain/src/entities/catalogs/index.ts
|
|
912
|
-
var MigrationCatalog = [
|
|
913
|
-
...backendCatalog,
|
|
914
|
-
...frontendCatalog
|
|
915
|
-
];
|
|
916
|
-
function findMigrationCatalogEntry(source, target) {
|
|
917
|
-
return MigrationCatalog.find(
|
|
918
|
-
(entry) => entry.source.toLowerCase() === source.toLowerCase() && entry.target.toLowerCase() === target.toLowerCase()
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// ../infrastructure/src/server/api.ts
|
|
923
|
-
async function createApiServer(store, migrationRunner) {
|
|
924
|
-
const express = (await import("express")).default;
|
|
925
|
-
const cors = (await import("cors")).default;
|
|
926
|
-
const app = express();
|
|
927
|
-
app.use(cors());
|
|
928
|
-
app.use(express.json());
|
|
929
|
-
app.get("/api/plans", async (_req, res) => {
|
|
930
|
-
try {
|
|
931
|
-
const plans = await store.getAllPlans();
|
|
932
|
-
res.json(plans);
|
|
933
|
-
} catch (error) {
|
|
934
|
-
if (error instanceof Error) {
|
|
935
|
-
res.status(500).json({ error: error.message });
|
|
936
|
-
} else {
|
|
937
|
-
res.status(500).json({ error: String(error) });
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
app.get("/api/events", async (_req, res) => {
|
|
942
|
-
try {
|
|
943
|
-
const events = await store.getEvents();
|
|
944
|
-
res.json(events);
|
|
945
|
-
} catch (error) {
|
|
946
|
-
if (error instanceof Error) {
|
|
947
|
-
res.status(500).json({ error: error.message });
|
|
948
|
-
} else {
|
|
949
|
-
res.status(500).json({ error: String(error) });
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
});
|
|
953
|
-
app.get("/api/browse", async (req, res) => {
|
|
954
|
-
try {
|
|
955
|
-
const { readdirSync: readdirSync2, statSync: statSync2 } = await import("fs");
|
|
956
|
-
const { resolve: resolve3, join: join8 } = await import("path");
|
|
957
|
-
const targetPath = req.query.path ? resolve3(String(req.query.path)) : resolve3(".");
|
|
958
|
-
const entries = readdirSync2(targetPath).filter(
|
|
959
|
-
(name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist" && name !== "build"
|
|
960
|
-
).map((name) => {
|
|
961
|
-
const full = join8(targetPath, name);
|
|
962
|
-
try {
|
|
963
|
-
const stat = statSync2(full);
|
|
964
|
-
return {
|
|
965
|
-
name,
|
|
966
|
-
path: full,
|
|
967
|
-
isDirectory: stat.isDirectory()
|
|
968
|
-
};
|
|
969
|
-
} catch {
|
|
970
|
-
return null;
|
|
971
|
-
}
|
|
972
|
-
}).filter(Boolean);
|
|
973
|
-
res.json({
|
|
974
|
-
current: targetPath,
|
|
975
|
-
parent: resolve3(targetPath, ".."),
|
|
976
|
-
entries
|
|
977
|
-
});
|
|
978
|
-
} catch (error) {
|
|
979
|
-
if (error instanceof Error) {
|
|
980
|
-
res.status(500).json({ error: error.message });
|
|
981
|
-
} else {
|
|
982
|
-
res.status(500).json({ error: String(error) });
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
app.get("/api/detect", async (req, res) => {
|
|
987
|
-
try {
|
|
988
|
-
const { detectTechnologies: detectTechnologies2 } = await import("./TechDetector-5YP44RVR.js");
|
|
989
|
-
const targetPath = req.query.path ? String(req.query.path) : ".";
|
|
990
|
-
const detected = detectTechnologies2(targetPath);
|
|
991
|
-
res.json({
|
|
992
|
-
detected,
|
|
993
|
-
primary: detected.length > 0 ? detected[0].framework : null
|
|
994
|
-
});
|
|
995
|
-
} catch (error) {
|
|
996
|
-
if (error instanceof Error) {
|
|
997
|
-
res.status(500).json({ error: error.message });
|
|
998
|
-
} else {
|
|
999
|
-
res.status(500).json({ error: String(error) });
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
if (migrationRunner) {
|
|
1004
|
-
app.post("/api/migrations/start", async (req, res) => {
|
|
1005
|
-
try {
|
|
1006
|
-
const { targetPath, from, to } = req.body;
|
|
1007
|
-
if (!targetPath || !from || !to) {
|
|
1008
|
-
res.status(400).json({
|
|
1009
|
-
error: "Missing required fields: targetPath, from, to"
|
|
1010
|
-
});
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
const catalogEntry = findMigrationCatalogEntry(from, to);
|
|
1014
|
-
if (!catalogEntry) {
|
|
1015
|
-
res.status(400).json({
|
|
1016
|
-
error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
|
|
1017
|
-
});
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const result = await migrationRunner.startMigration({
|
|
1021
|
-
targetPath,
|
|
1022
|
-
from,
|
|
1023
|
-
to
|
|
1024
|
-
});
|
|
1025
|
-
res.json({ ok: true, ...result });
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
if (error instanceof Error) {
|
|
1028
|
-
res.status(500).json({ error: error.message });
|
|
1029
|
-
} else {
|
|
1030
|
-
res.status(500).json({ error: String(error) });
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
app.post("/api/migrations/apply", async (req, res) => {
|
|
1035
|
-
try {
|
|
1036
|
-
const { runId, targetPath } = req.body;
|
|
1037
|
-
if (!runId || !targetPath) {
|
|
1038
|
-
res.status(400).json({
|
|
1039
|
-
error: "Missing required fields: runId, targetPath"
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
1044
|
-
const message = await integrator.applyMigration(runId, targetPath);
|
|
1045
|
-
res.json({ ok: true, message });
|
|
1046
|
-
} catch (error) {
|
|
1047
|
-
if (error instanceof Error) {
|
|
1048
|
-
res.status(500).json({ error: error.message });
|
|
1049
|
-
} else {
|
|
1050
|
-
res.status(500).json({ error: String(error) });
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
app.post("/api/migrations/rollback", async (req, res) => {
|
|
1055
|
-
try {
|
|
1056
|
-
const { runId } = req.body;
|
|
1057
|
-
if (!runId) {
|
|
1058
|
-
res.status(400).json({
|
|
1059
|
-
error: "Missing required field: runId"
|
|
1060
|
-
});
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
await migrationRunner.rollbackMigration(runId);
|
|
1064
|
-
res.json({ ok: true, message: `Rolled back run: ${runId}` });
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
if (error instanceof Error) {
|
|
1067
|
-
res.status(500).json({ error: error.message });
|
|
1068
|
-
} else {
|
|
1069
|
-
res.status(500).json({ error: String(error) });
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1073
|
-
app.post("/api/migrations/reset", async (_req, res) => {
|
|
1074
|
-
try {
|
|
1075
|
-
await store.reset();
|
|
1076
|
-
res.json({ ok: true, message: "Migration state reset successfully" });
|
|
1077
|
-
} catch (error) {
|
|
1078
|
-
if (error instanceof Error) {
|
|
1079
|
-
res.status(500).json({ error: error.message });
|
|
1080
|
-
} else {
|
|
1081
|
-
res.status(500).json({ error: String(error) });
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
const path6 = await import("path");
|
|
1087
|
-
const fs7 = await import("fs");
|
|
1088
|
-
let publicDir = path6.join(import.meta.dirname, "public");
|
|
1089
|
-
if (!fs7.existsSync(publicDir)) {
|
|
1090
|
-
publicDir = path6.join(import.meta.dirname, "../public");
|
|
1091
|
-
}
|
|
1092
|
-
if (fs7.existsSync(publicDir)) {
|
|
1093
|
-
app.use(express.static(publicDir));
|
|
1094
|
-
app.get(/^(.*)$/, (_req, res) => {
|
|
1095
|
-
res.sendFile(path6.join(publicDir, "index.html"));
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
return app;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// ../application/src/agents/MapperAgent.ts
|
|
1102
|
-
import * as fs3 from "fs";
|
|
1103
|
-
import * as path2 from "path";
|
|
1104
|
-
import {
|
|
1105
|
-
createAgent,
|
|
1106
|
-
SituationSpecification
|
|
1107
|
-
} from "@mozaik-ai/core";
|
|
1108
|
-
var WhenMigrationStarts = class extends SituationSpecification {
|
|
1109
|
-
isSatisfiedBy({ event }) {
|
|
1110
|
-
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
var mapFilesProcessor = {
|
|
1114
|
-
async apply({ event, participant }) {
|
|
1115
|
-
const payload = event.payload;
|
|
1116
|
-
console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
|
|
1117
|
-
const shadowDir = payload.shadowWorkspacePath || "";
|
|
1118
|
-
if (!shadowDir) {
|
|
1119
|
-
console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
const filesToMigrate = [];
|
|
1123
|
-
const scanDir = (dir) => {
|
|
1124
|
-
if (!fs3.existsSync(dir)) return;
|
|
1125
|
-
const files = fs3.readdirSync(dir);
|
|
1126
|
-
const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
|
|
1127
|
-
for (const file of files) {
|
|
1128
|
-
if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
|
|
1129
|
-
const fullPath = path2.join(dir, file);
|
|
1130
|
-
if (fs3.statSync(fullPath).isDirectory()) {
|
|
1131
|
-
scanDir(fullPath);
|
|
1132
|
-
} else if (file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".tsx") || file.endsWith(".jsx")) {
|
|
1133
|
-
filesToMigrate.push(fullPath);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
};
|
|
1137
|
-
scanDir(shadowDir);
|
|
1138
|
-
const runtime = resolveRuntime();
|
|
1139
|
-
const repository = runtime.state.repository;
|
|
1140
|
-
for (const discoveredFile of filesToMigrate) {
|
|
1141
|
-
console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
|
|
1142
|
-
await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
|
|
1143
|
-
sendEvent(
|
|
1144
|
-
{
|
|
1145
|
-
type: "file.discovered" /* FILE_DISCOVERED */,
|
|
1146
|
-
producerId: participant.getId(),
|
|
1147
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1148
|
-
payload: {
|
|
1149
|
-
planId: payload.planId,
|
|
1150
|
-
filePath: discoveredFile
|
|
1151
|
-
}
|
|
1152
|
-
},
|
|
1153
|
-
participant.getId()
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
};
|
|
1158
|
-
var mapFilesHandler = {
|
|
1159
|
-
specification: new WhenMigrationStarts(),
|
|
1160
|
-
processor: mapFilesProcessor
|
|
1161
|
-
};
|
|
1162
|
-
function createMapperAgent() {
|
|
1163
|
-
return createAgent({
|
|
1164
|
-
name: "Mapper",
|
|
1165
|
-
capabilities: ["code_analysis"],
|
|
1166
|
-
instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
|
|
1167
|
-
tools: [],
|
|
1168
|
-
// AST scanning tools will go here later
|
|
1169
|
-
handlers: [mapFilesHandler]
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// ../application/src/agents/WorkerAgent.ts
|
|
1174
|
-
import * as fs4 from "fs";
|
|
1175
|
-
import {
|
|
1176
|
-
createAgent as createAgent2,
|
|
1177
|
-
SituationSpecification as SituationSpecification2
|
|
1178
|
-
} from "@mozaik-ai/core";
|
|
1179
|
-
|
|
1180
|
-
// ../application/src/utils/FileTreeBuilder.ts
|
|
1181
|
-
import * as path3 from "path";
|
|
1182
|
-
function getCommonBaseDir(filePaths) {
|
|
1183
|
-
if (!filePaths || filePaths.length === 0) return "";
|
|
1184
|
-
if (filePaths.length === 1) return path3.dirname(filePaths[0]);
|
|
1185
|
-
const splitPaths = filePaths.map((p) => path3.normalize(p).split(path3.sep));
|
|
1186
|
-
let commonPath = "";
|
|
1187
|
-
for (let i = 0; i < splitPaths[0].length; i++) {
|
|
1188
|
-
const part = splitPaths[0][i];
|
|
1189
|
-
const isCommon = splitPaths.every((parts) => parts[i] === part);
|
|
1190
|
-
if (isCommon) {
|
|
1191
|
-
commonPath = commonPath === "" ? part : commonPath + path3.sep + part;
|
|
1192
|
-
} else {
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return commonPath;
|
|
1197
|
-
}
|
|
1198
|
-
function buildFileTree(filePaths) {
|
|
1199
|
-
if (!filePaths || filePaths.length === 0) return "No files found.";
|
|
1200
|
-
const baseDir = getCommonBaseDir(filePaths);
|
|
1201
|
-
const relativePaths = filePaths.map((p) => path3.relative(baseDir, p)).filter((p) => p.length > 0).sort();
|
|
1202
|
-
if (relativePaths.length === 0) return "No files found.";
|
|
1203
|
-
const tree = {};
|
|
1204
|
-
for (const relPath of relativePaths) {
|
|
1205
|
-
const parts = relPath.split(path3.sep);
|
|
1206
|
-
let current = tree;
|
|
1207
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1208
|
-
const part = parts[i];
|
|
1209
|
-
if (i === parts.length - 1) {
|
|
1210
|
-
current[part] = null;
|
|
1211
|
-
} else {
|
|
1212
|
-
current[part] = current[part] || {};
|
|
1213
|
-
current = current[part];
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
let output = `/
|
|
1218
|
-
`;
|
|
1219
|
-
function renderTree(node, prefix = "") {
|
|
1220
|
-
if (node === null) return;
|
|
1221
|
-
const keys = Object.keys(node);
|
|
1222
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1223
|
-
const key = keys[i];
|
|
1224
|
-
const isLast = i === keys.length - 1;
|
|
1225
|
-
const isFile = node[key] === null;
|
|
1226
|
-
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1227
|
-
output += `${prefix}${connector}${key}
|
|
1228
|
-
`;
|
|
1229
|
-
if (!isFile) {
|
|
1230
|
-
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1231
|
-
renderTree(node[key], nextPrefix);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
renderTree(tree);
|
|
1236
|
-
return output.trim();
|
|
1237
|
-
}
|
|
591
|
+
if (sections.length === 0) return "";
|
|
592
|
+
return `
|
|
593
|
+
Evidence from disk (read this before editing; match existing exports and callback prop names):
|
|
594
|
+
${sections.join("\n")}`;
|
|
595
|
+
}
|
|
1238
596
|
|
|
1239
597
|
// ../application/src/agents/WorkerAgent.ts
|
|
1240
|
-
var retryCountMap = /* @__PURE__ */ new Map();
|
|
1241
598
|
var MAX_RETRIES = 2;
|
|
1242
599
|
var ConcurrencyQueue = class {
|
|
1243
600
|
constructor(concurrencyLimit) {
|
|
@@ -1267,7 +624,7 @@ var ConcurrencyQueue = class {
|
|
|
1267
624
|
}
|
|
1268
625
|
}
|
|
1269
626
|
};
|
|
1270
|
-
var workerQueue = new ConcurrencyQueue(
|
|
627
|
+
var workerQueue = new ConcurrencyQueue(3);
|
|
1271
628
|
var WhenFileDiscovered = class extends SituationSpecification2 {
|
|
1272
629
|
isSatisfiedBy({ event }) {
|
|
1273
630
|
return event.type === "file.discovered" /* FILE_DISCOVERED */;
|
|
@@ -1291,7 +648,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
|
|
|
1291
648
|
async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
1292
649
|
return new Promise(async (originalResolve) => {
|
|
1293
650
|
let isDone = false;
|
|
1294
|
-
const
|
|
651
|
+
const resolve5 = () => {
|
|
1295
652
|
if (!isDone) {
|
|
1296
653
|
isDone = true;
|
|
1297
654
|
originalResolve();
|
|
@@ -1299,13 +656,12 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1299
656
|
};
|
|
1300
657
|
const runtime = resolveRuntime();
|
|
1301
658
|
const repository = runtime.state.repository;
|
|
1302
|
-
console.log(`[WorkerAgent] Marking file as 'in_progress' in database...`);
|
|
1303
659
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
1304
660
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
1305
661
|
const tempAgent = createMozaikAgent({
|
|
1306
662
|
name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
|
|
1307
663
|
capabilities: ["code_refactoring", "inference"],
|
|
1308
|
-
instruction: "You are
|
|
664
|
+
instruction: "You are a staff engineer migrating a real codebase. Deduce from files on disk: read the target and every local module it imports before writing. Never invent callback prop names or re-implement a screen you could import. Prefer evidence over catalog examples.",
|
|
1309
665
|
tools: participant.getTools(),
|
|
1310
666
|
handlers: [
|
|
1311
667
|
{
|
|
@@ -1324,7 +680,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1324
680
|
} catch (e) {
|
|
1325
681
|
console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
|
|
1326
682
|
}
|
|
1327
|
-
await repository.updateTaskStatus(planId, filePath, "
|
|
683
|
+
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
1328
684
|
sendEvent(
|
|
1329
685
|
{
|
|
1330
686
|
type: "file.migrated" /* FILE_MIGRATED */,
|
|
@@ -1339,7 +695,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1339
695
|
tempParticipant.getId()
|
|
1340
696
|
);
|
|
1341
697
|
leave(tempParticipant);
|
|
1342
|
-
|
|
698
|
+
resolve5();
|
|
1343
699
|
}
|
|
1344
700
|
}
|
|
1345
701
|
}
|
|
@@ -1358,8 +714,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1358
714
|
if (!isDone) {
|
|
1359
715
|
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
|
|
1360
716
|
await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
|
|
717
|
+
sendEvent({
|
|
718
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
719
|
+
producerId: tempAgent.getId(),
|
|
720
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
721
|
+
payload: { planId, filePath, reason: "Inference timed out" }
|
|
722
|
+
}, tempAgent.getId());
|
|
1361
723
|
leave(tempAgent);
|
|
1362
|
-
|
|
724
|
+
resolve5();
|
|
1363
725
|
}
|
|
1364
726
|
}, 45e3);
|
|
1365
727
|
} catch (error) {
|
|
@@ -1367,8 +729,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1367
729
|
if (!isDone) {
|
|
1368
730
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1369
731
|
await repository.updateTaskStatus(planId, filePath, "failed", errorMessage || "Sync error");
|
|
732
|
+
sendEvent({
|
|
733
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
734
|
+
producerId: tempAgent.getId(),
|
|
735
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
736
|
+
payload: { planId, filePath, reason: errorMessage || "Sync error" }
|
|
737
|
+
}, tempAgent.getId());
|
|
1370
738
|
leave(tempAgent);
|
|
1371
|
-
|
|
739
|
+
resolve5();
|
|
1372
740
|
}
|
|
1373
741
|
}
|
|
1374
742
|
});
|
|
@@ -1387,10 +755,10 @@ var workOnFileProcessor = {
|
|
|
1387
755
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1388
756
|
if (catalogEntry) {
|
|
1389
757
|
prompt += `
|
|
1390
|
-
|
|
758
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
759
|
+
`;
|
|
760
|
+
prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
|
|
1391
761
|
`;
|
|
1392
|
-
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1393
|
-
`);
|
|
1394
762
|
if (catalogEntry.examples && catalogEntry.examples.length > 0) {
|
|
1395
763
|
prompt += `
|
|
1396
764
|
Reference Examples:
|
|
@@ -1427,8 +795,20 @@ ${projectTree}
|
|
|
1427
795
|
prompt += `Use this context to ensure your changes align with the overall project structure and other files.
|
|
1428
796
|
`;
|
|
1429
797
|
}
|
|
798
|
+
prompt += collectFileHints(
|
|
799
|
+
payload.filePath,
|
|
800
|
+
{ source: plan.profile.source, target: plan.profile.target },
|
|
801
|
+
catalogEntry?.layer
|
|
802
|
+
);
|
|
1430
803
|
}
|
|
1431
|
-
prompt +=
|
|
804
|
+
prompt += buildNeighborContext(payload.filePath);
|
|
805
|
+
prompt += `
|
|
806
|
+
You are not a mechanical find-replace bot. Deduce the correct change from the evidence above and from tools (read_file, list_directory).
|
|
807
|
+
Before writing:
|
|
808
|
+
1. Read every local import you will call.
|
|
809
|
+
2. Keep that module's public prop/export names exactly.
|
|
810
|
+
3. If this is a router/bootstrap file, import an existing screen from the nearby paths \u2014 do not paste a new copy of its JSX.
|
|
811
|
+
Then write the file(s). If structure must change, create/rename/delete accordingly.`;
|
|
1432
812
|
workerQueue.enqueue(async () => {
|
|
1433
813
|
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1434
814
|
});
|
|
@@ -1438,22 +818,39 @@ var fixRejectedFileProcessor = {
|
|
|
1438
818
|
async apply({ event, participant }) {
|
|
1439
819
|
const payload = event.payload;
|
|
1440
820
|
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1441
|
-
const
|
|
821
|
+
const retries = resolveRuntime().state.retryCounts;
|
|
822
|
+
if (payload.source === "integration") {
|
|
823
|
+
retries.delete(retryKey);
|
|
824
|
+
}
|
|
825
|
+
const currentRetries = retries.get(retryKey) || 0;
|
|
1442
826
|
if (currentRetries >= MAX_RETRIES) {
|
|
1443
827
|
console.warn(`[WorkerAgent] File ${payload.filePath} exceeded max retries (${MAX_RETRIES}). Marking as failed.`);
|
|
1444
828
|
const runtime2 = resolveRuntime();
|
|
1445
829
|
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", `Exceeded max retries (${MAX_RETRIES})`);
|
|
1446
|
-
|
|
830
|
+
retries.delete(retryKey);
|
|
831
|
+
sendEvent({
|
|
832
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
833
|
+
producerId: participant.getId(),
|
|
834
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
835
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: `Exceeded max retries (${MAX_RETRIES})` }
|
|
836
|
+
}, participant.getId());
|
|
1447
837
|
return;
|
|
1448
838
|
}
|
|
1449
|
-
|
|
839
|
+
retries.set(retryKey, currentRetries + 1);
|
|
1450
840
|
console.log(`[WorkerAgent] File REJECTED (attempt ${currentRetries + 1}/${MAX_RETRIES}), initiating repair loop: ${payload.filePath}`);
|
|
1451
841
|
const runtime = resolveRuntime();
|
|
1452
842
|
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1453
843
|
let prompt = `You need to FIX the file at path: ${payload.filePath}
|
|
1454
844
|
`;
|
|
1455
|
-
|
|
845
|
+
if (payload.source === "integration") {
|
|
846
|
+
prompt += `The Integration Agent rejected this file because the shadow workspace failed npm install / npm run build, or a catalog verifier flagged it.
|
|
847
|
+
`;
|
|
848
|
+
prompt += `If the file does not exist yet, create it. If it is a router/bootstrap file, import existing screens \u2014 do not re-implement them or rename their callback props.
|
|
849
|
+
`;
|
|
850
|
+
} else {
|
|
851
|
+
prompt += `Your previous migration was REJECTED by the Quality Assurance Reviewer.
|
|
1456
852
|
`;
|
|
853
|
+
}
|
|
1457
854
|
prompt += `Feedback/Errors:
|
|
1458
855
|
${payload.errors.join("\n")}
|
|
1459
856
|
|
|
@@ -1464,10 +861,10 @@ ${payload.errors.join("\n")}
|
|
|
1464
861
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1465
862
|
if (catalogEntry) {
|
|
1466
863
|
prompt += `
|
|
1467
|
-
|
|
864
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
865
|
+
`;
|
|
866
|
+
prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
|
|
1468
867
|
`;
|
|
1469
|
-
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1470
|
-
`);
|
|
1471
868
|
}
|
|
1472
869
|
if (plan.tasks && plan.tasks.length > 0) {
|
|
1473
870
|
const allFiles = plan.tasks.map((t) => t.filePath);
|
|
@@ -1482,19 +879,26 @@ ${projectTree}
|
|
|
1482
879
|
prompt += `Ensure you respect this overall project structure while fixing the issues.
|
|
1483
880
|
`;
|
|
1484
881
|
}
|
|
882
|
+
prompt += collectFileHints(
|
|
883
|
+
payload.filePath,
|
|
884
|
+
{ source: plan.profile.source, target: plan.profile.target },
|
|
885
|
+
catalogEntry?.layer
|
|
886
|
+
);
|
|
1485
887
|
}
|
|
1486
|
-
prompt +=
|
|
888
|
+
prompt += buildNeighborContext(payload.filePath);
|
|
889
|
+
prompt += `
|
|
890
|
+
Deduce the fix from the errors and the disk evidence. Read imported modules (and list_directory if you need another screen). Do not guess APIs. Then write the corrected file(s). If the path does not exist, create it. Do not delete existing screens just to make a router file compile.`;
|
|
1487
891
|
workerQueue.enqueue(async () => {
|
|
1488
892
|
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1489
893
|
});
|
|
1490
894
|
}
|
|
1491
895
|
};
|
|
1492
896
|
var fatalMismatchProcessor = {
|
|
1493
|
-
async apply({ event }) {
|
|
897
|
+
async apply({ event, participant }) {
|
|
1494
898
|
const payload = event.payload;
|
|
1495
899
|
console.error(`[WorkerAgent] \u{1F6D1} FATAL MISMATCH received for ${payload.filePath}. Reason: ${payload.reason}`);
|
|
1496
900
|
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1497
|
-
|
|
901
|
+
resolveRuntime().state.retryCounts.delete(retryKey);
|
|
1498
902
|
const runtime = resolveRuntime();
|
|
1499
903
|
await runtime.state.repository.updateTaskStatus(
|
|
1500
904
|
payload.planId,
|
|
@@ -1502,6 +906,12 @@ var fatalMismatchProcessor = {
|
|
|
1502
906
|
"failed",
|
|
1503
907
|
`Fatal Architectural Mismatch: ${payload.reason}`
|
|
1504
908
|
);
|
|
909
|
+
sendEvent({
|
|
910
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
911
|
+
producerId: participant.getId(),
|
|
912
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
913
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: payload.reason }
|
|
914
|
+
}, participant.getId());
|
|
1505
915
|
}
|
|
1506
916
|
};
|
|
1507
917
|
function createWorkerAgent(tools) {
|
|
@@ -1523,6 +933,7 @@ import {
|
|
|
1523
933
|
createAgent as createAgent3,
|
|
1524
934
|
SituationSpecification as SituationSpecification3
|
|
1525
935
|
} from "@mozaik-ai/core";
|
|
936
|
+
import * as path5 from "path";
|
|
1526
937
|
var ConcurrencyQueue2 = class {
|
|
1527
938
|
constructor(concurrencyLimit) {
|
|
1528
939
|
this.concurrencyLimit = concurrencyLimit;
|
|
@@ -1551,7 +962,7 @@ var ConcurrencyQueue2 = class {
|
|
|
1551
962
|
}
|
|
1552
963
|
}
|
|
1553
964
|
};
|
|
1554
|
-
var reviewerQueue = new ConcurrencyQueue2(
|
|
965
|
+
var reviewerQueue = new ConcurrencyQueue2(3);
|
|
1555
966
|
var WhenFileMigrated = class extends SituationSpecification3 {
|
|
1556
967
|
isSatisfiedBy({ event }) {
|
|
1557
968
|
return event.type === "file.migrated" /* FILE_MIGRATED */;
|
|
@@ -1575,7 +986,7 @@ var reviewFileProcessor = {
|
|
|
1575
986
|
reviewerQueue.enqueue(async () => {
|
|
1576
987
|
return new Promise(async (originalResolve) => {
|
|
1577
988
|
let isDone = false;
|
|
1578
|
-
const
|
|
989
|
+
const resolve5 = () => {
|
|
1579
990
|
if (!isDone) {
|
|
1580
991
|
isDone = true;
|
|
1581
992
|
console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
|
|
@@ -1583,12 +994,12 @@ var reviewFileProcessor = {
|
|
|
1583
994
|
}
|
|
1584
995
|
};
|
|
1585
996
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
1586
|
-
const { sendEvent: sendEvent3 } = await import("./runtime-
|
|
997
|
+
const { sendEvent: sendEvent3 } = await import("./runtime-L7VOQROE.js");
|
|
1587
998
|
const reviewerId = `Reviewer-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
1588
999
|
const tempAgent = createMozaikAgent({
|
|
1589
1000
|
name: reviewerId,
|
|
1590
1001
|
capabilities: ["code_review", "inference"],
|
|
1591
|
-
instruction: "You are
|
|
1002
|
+
instruction: "You are a staff engineer reviewing a migrated file against its neighbors. Reject if public props/emits do not match imported modules, if a router file re-implements a screen instead of importing it, or if a screen was hollowed out. UI that lived under src/pages in a SPA is not a Next Pages Router file. Do not reject a file just because it lacks every catalog feature. FATAL_MISMATCH only for an unfixable paradigm break.",
|
|
1592
1003
|
tools: [],
|
|
1593
1004
|
// No tools to avoid Gemini thought_signature crash
|
|
1594
1005
|
handlers: [
|
|
@@ -1625,6 +1036,8 @@ var reviewFileProcessor = {
|
|
|
1625
1036
|
try {
|
|
1626
1037
|
if (reviewResult.status === "APPROVED") {
|
|
1627
1038
|
console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} APPROVED!`);
|
|
1039
|
+
const runtime2 = await resolveRuntime();
|
|
1040
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
|
|
1628
1041
|
sendEvent3({
|
|
1629
1042
|
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
1630
1043
|
producerId: participantId,
|
|
@@ -1651,27 +1064,9 @@ var reviewFileProcessor = {
|
|
|
1651
1064
|
} catch (e) {
|
|
1652
1065
|
console.error(`[ReviewerAgent:${reviewerId}] Failed to send review event:`, e);
|
|
1653
1066
|
}
|
|
1654
|
-
try {
|
|
1655
|
-
const runtime2 = await resolveRuntime();
|
|
1656
|
-
const plan2 = await runtime2.state.repository.getPlan(payload.planId);
|
|
1657
|
-
if (plan2) {
|
|
1658
|
-
const allTerminal = plan2.tasks.every((t) => t.status === "completed" || t.status === "failed");
|
|
1659
|
-
if (allTerminal) {
|
|
1660
|
-
console.log(`[App] All tasks reached terminal state. Emitting MIGRATION_COMPLETED.`);
|
|
1661
|
-
sendEvent3({
|
|
1662
|
-
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1663
|
-
producerId: participantId,
|
|
1664
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1665
|
-
payload: { planId: payload.planId }
|
|
1666
|
-
}, participantId);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
} catch (e) {
|
|
1670
|
-
console.error(`[ReviewerAgent:${reviewerId}] Failed to check migration completion:`, e);
|
|
1671
|
-
}
|
|
1672
1067
|
}
|
|
1673
1068
|
leave(tempParticipant);
|
|
1674
|
-
|
|
1069
|
+
resolve5();
|
|
1675
1070
|
}
|
|
1676
1071
|
}
|
|
1677
1072
|
}
|
|
@@ -1681,16 +1076,58 @@ var reviewFileProcessor = {
|
|
|
1681
1076
|
join(tempAgent);
|
|
1682
1077
|
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
|
|
1683
1078
|
const runtime = resolveRuntime();
|
|
1079
|
+
const { existsSync: existsSync8 } = await import("fs");
|
|
1080
|
+
if (!existsSync8(payload.filePath)) {
|
|
1081
|
+
const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
|
|
1082
|
+
const planForMissing = await runtime.state.repository.getPlan(payload.planId);
|
|
1083
|
+
const target = planForMissing?.profile.target;
|
|
1084
|
+
const shadowRoot = findShadowRoot(payload.filePath);
|
|
1085
|
+
const deletedFsdPage = target === "next" && isFsdPageComponent(payload.filePath);
|
|
1086
|
+
const viewsReplacement = payload.filePath.replace(/([/\\])src\1pages\1/, "$1src$1views$1");
|
|
1087
|
+
const fsdMovedToViews = deletedFsdPage && existsSync8(viewsReplacement);
|
|
1088
|
+
const deletedRootWithoutRoutes = target === "next" && isSpaRootComponent(payload.filePath) && (!shadowRoot || !hasNextAppRoutes(shadowRoot));
|
|
1089
|
+
if (deletedFsdPage && !fsdMovedToViews || deletedRootWithoutRoutes) {
|
|
1090
|
+
console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} is not a valid Next.js move. Rejecting.`);
|
|
1091
|
+
sendEvent4({
|
|
1092
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
1093
|
+
producerId: tempAgent.getId(),
|
|
1094
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1095
|
+
payload: {
|
|
1096
|
+
planId: payload.planId,
|
|
1097
|
+
filePath: payload.filePath,
|
|
1098
|
+
errors: deletedFsdPage ? [
|
|
1099
|
+
"This file lived under src/pages as SPA UI. Next.js reserves src/pages for the Pages Router \u2014 move the tree to a non-reserved folder and import those screens from src/app. Do not delete the UI without a replacement."
|
|
1100
|
+
] : [
|
|
1101
|
+
"The SPA root was deleted before src/app/**/page.tsx existed with real UI. Recreate routing as Next App Router pages that import the original screen components. Do not import App from those pages."
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
}, tempAgent.getId());
|
|
1105
|
+
leave(tempAgent);
|
|
1106
|
+
resolve5();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} does not exist on disk (likely intentionally deleted/moved). Auto-approving.`);
|
|
1110
|
+
await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
|
|
1111
|
+
sendEvent4({
|
|
1112
|
+
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
1113
|
+
producerId: tempAgent.getId(),
|
|
1114
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1115
|
+
payload: { planId: payload.planId, filePath: payload.filePath }
|
|
1116
|
+
}, tempAgent.getId());
|
|
1117
|
+
leave(tempAgent);
|
|
1118
|
+
resolve5();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1684
1121
|
if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
|
|
1685
1122
|
try {
|
|
1686
|
-
const { Project
|
|
1687
|
-
const tsProject = new
|
|
1123
|
+
const { Project } = await import("ts-morph");
|
|
1124
|
+
const tsProject = new Project();
|
|
1688
1125
|
const sf = tsProject.addSourceFileAtPath(payload.filePath);
|
|
1689
1126
|
const diagnostics = sf.getPreEmitDiagnostics();
|
|
1690
1127
|
const syntaxErrors = diagnostics.filter((d) => d.getCode() >= 1e3 && d.getCode() < 2e3);
|
|
1691
1128
|
if (syntaxErrors.length > 0) {
|
|
1692
1129
|
console.error(`[ReviewerAgent:${reviewerId}] Syntax Error in ${payload.filePath}. Rejecting immediately.`);
|
|
1693
|
-
const { sendEvent: sendEvent4 } = await import("./runtime-
|
|
1130
|
+
const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
|
|
1694
1131
|
sendEvent4({
|
|
1695
1132
|
type: "file.rejected" /* FILE_REJECTED */,
|
|
1696
1133
|
producerId: tempAgent.getId(),
|
|
@@ -1702,7 +1139,7 @@ var reviewFileProcessor = {
|
|
|
1702
1139
|
}
|
|
1703
1140
|
}, tempAgent.getId());
|
|
1704
1141
|
leave(tempAgent);
|
|
1705
|
-
|
|
1142
|
+
resolve5();
|
|
1706
1143
|
return;
|
|
1707
1144
|
}
|
|
1708
1145
|
} catch (e) {
|
|
@@ -1710,6 +1147,28 @@ var reviewFileProcessor = {
|
|
|
1710
1147
|
}
|
|
1711
1148
|
}
|
|
1712
1149
|
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1150
|
+
if (plan && plan.profile.source.toLowerCase() === "react" && plan.profile.target.toLowerCase() === "next") {
|
|
1151
|
+
const shadowRoot = findShadowRoot(payload.filePath);
|
|
1152
|
+
if (shadowRoot) {
|
|
1153
|
+
const current = path5.resolve(payload.filePath);
|
|
1154
|
+
const structureIssues = analyzeReactToNextStructure(shadowRoot).filter(
|
|
1155
|
+
(issue) => path5.resolve(issue.filePath) === current
|
|
1156
|
+
);
|
|
1157
|
+
if (structureIssues.length > 0) {
|
|
1158
|
+
const errors = structureIssues.flatMap((issue) => issue.errors);
|
|
1159
|
+
console.error(`[ReviewerAgent:${reviewerId}] Structure verifier rejected ${payload.filePath}.`, errors);
|
|
1160
|
+
sendEvent3({
|
|
1161
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
1162
|
+
producerId: tempAgent.getId(),
|
|
1163
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1164
|
+
payload: { planId: payload.planId, filePath: payload.filePath, errors }
|
|
1165
|
+
}, tempAgent.getId());
|
|
1166
|
+
leave(tempAgent);
|
|
1167
|
+
resolve5();
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1713
1172
|
let prompt = `Review the following file migration: ${payload.filePath}
|
|
1714
1173
|
`;
|
|
1715
1174
|
if (plan) {
|
|
@@ -1718,360 +1177,1579 @@ var reviewFileProcessor = {
|
|
|
1718
1177
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1719
1178
|
if (catalogEntry) {
|
|
1720
1179
|
prompt += `
|
|
1721
|
-
|
|
1180
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
|
|
1722
1181
|
`;
|
|
1723
|
-
|
|
1724
|
-
|
|
1182
|
+
prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
|
|
1183
|
+
`;
|
|
1184
|
+
}
|
|
1185
|
+
prompt += collectFileHints(
|
|
1186
|
+
payload.filePath,
|
|
1187
|
+
{ source: plan.profile.source, target: plan.profile.target },
|
|
1188
|
+
catalogEntry?.layer
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
prompt += `
|
|
1192
|
+
${buildNeighborContext(payload.filePath)}
|
|
1193
|
+
Diff or new content of the file under review:
|
|
1194
|
+
${payload.diff}
|
|
1195
|
+
Compare the diff to imported modules in the evidence. If props/exports do not match, REJECT with a specific error. You MUST respond in JSON.`;
|
|
1196
|
+
const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
1197
|
+
console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
|
|
1198
|
+
try {
|
|
1199
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
|
|
1200
|
+
runLoop(tempAgent.getId(), prompt, {
|
|
1201
|
+
model: modelToUse,
|
|
1202
|
+
context: tempAgent.getMemory().getContext(),
|
|
1203
|
+
structuredOutput: {
|
|
1204
|
+
name: "review_result",
|
|
1205
|
+
schema: {
|
|
1206
|
+
type: "object",
|
|
1207
|
+
properties: {
|
|
1208
|
+
status: { type: "string", enum: ["APPROVED", "REJECTED", "FATAL_MISMATCH"] },
|
|
1209
|
+
errors: { type: "array", items: { type: "string" } }
|
|
1210
|
+
},
|
|
1211
|
+
required: ["status", "errors"],
|
|
1212
|
+
additionalProperties: false
|
|
1213
|
+
},
|
|
1214
|
+
strict: true
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
|
|
1218
|
+
setTimeout(async () => {
|
|
1219
|
+
if (!isDone) {
|
|
1220
|
+
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
|
|
1221
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
1222
|
+
const runtime2 = resolveRuntime2();
|
|
1223
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
|
|
1224
|
+
sendEvent3({
|
|
1225
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
1226
|
+
producerId: tempAgent.getId(),
|
|
1227
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1228
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 45s" }
|
|
1229
|
+
}, tempAgent.getId());
|
|
1230
|
+
leave(tempAgent);
|
|
1231
|
+
resolve5();
|
|
1232
|
+
}
|
|
1233
|
+
}, 45e3);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
1236
|
+
if (!isDone) {
|
|
1237
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
1238
|
+
const runtime2 = resolveRuntime2();
|
|
1239
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1240
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
|
|
1241
|
+
sendEvent3({
|
|
1242
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
1243
|
+
producerId: tempAgent.getId(),
|
|
1244
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1245
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
|
|
1246
|
+
}, tempAgent.getId());
|
|
1247
|
+
leave(tempAgent);
|
|
1248
|
+
resolve5();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
var reviewFileHandler = {
|
|
1256
|
+
specification: new WhenFileMigrated(),
|
|
1257
|
+
processor: reviewFileProcessor
|
|
1258
|
+
};
|
|
1259
|
+
function createReviewerAgent(tools) {
|
|
1260
|
+
return createAgent3({
|
|
1261
|
+
name: "Reviewer",
|
|
1262
|
+
capabilities: ["code_review", "inference"],
|
|
1263
|
+
instruction: "You are the Reviewer Worker.",
|
|
1264
|
+
tools: [],
|
|
1265
|
+
// No tools
|
|
1266
|
+
handlers: [reviewFileHandler]
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ../application/src/agents/PackageManagerAgent.ts
|
|
1271
|
+
import * as fs5 from "fs";
|
|
1272
|
+
import * as path6 from "path";
|
|
1273
|
+
import {
|
|
1274
|
+
createAgent as createAgent4,
|
|
1275
|
+
SituationSpecification as SituationSpecification4
|
|
1276
|
+
} from "@mozaik-ai/core";
|
|
1277
|
+
var WhenMigrationStarts2 = class extends SituationSpecification4 {
|
|
1278
|
+
isSatisfiedBy({ event }) {
|
|
1279
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
var managePackagesProcessor = {
|
|
1283
|
+
async apply({ event, participant }) {
|
|
1284
|
+
const payload = event.payload;
|
|
1285
|
+
console.log(`[PackageManagerAgent] Managing dependencies and cleanup for Plan: ${payload.planId}`);
|
|
1286
|
+
const runtime = resolveRuntime();
|
|
1287
|
+
const emitPackagesReady = async (status = "completed") => {
|
|
1288
|
+
const currentPlan = await runtime.state.repository.getPlan(payload.planId);
|
|
1289
|
+
if (currentPlan) {
|
|
1290
|
+
const pmTaskToComplete = currentPlan.tasks.find((t) => t.filePath === "system:package_manager");
|
|
1291
|
+
if (pmTaskToComplete) {
|
|
1292
|
+
pmTaskToComplete.status = status;
|
|
1293
|
+
await runtime.state.repository.savePlan(currentPlan);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
sendEvent({
|
|
1297
|
+
type: "phase.packages_ready" /* PHASE_PACKAGES_READY */,
|
|
1298
|
+
producerId: participant.getId(),
|
|
1299
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1300
|
+
payload: { planId: payload.planId }
|
|
1301
|
+
}, participant.getId());
|
|
1302
|
+
};
|
|
1303
|
+
const shadowDir = payload.shadowWorkspacePath || "";
|
|
1304
|
+
if (!shadowDir || !fs5.existsSync(shadowDir)) {
|
|
1305
|
+
console.warn("[PackageManagerAgent] No valid shadow workspace provided. Skipping.");
|
|
1306
|
+
await emitPackagesReady("failed");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1310
|
+
if (!plan) {
|
|
1311
|
+
console.warn("[PackageManagerAgent] Migration plan not found.");
|
|
1312
|
+
await emitPackagesReady("failed");
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1316
|
+
if (!catalogEntry) {
|
|
1317
|
+
console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
|
|
1318
|
+
await emitPackagesReady("failed");
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const pmTask = plan.tasks.find((t) => t.filePath === "system:package_manager");
|
|
1322
|
+
if (pmTask) {
|
|
1323
|
+
pmTask.status = "in_progress";
|
|
1324
|
+
await runtime.state.repository.savePlan(plan);
|
|
1325
|
+
}
|
|
1326
|
+
if (catalogEntry.filesToDelete && catalogEntry.filesToDelete.length > 0) {
|
|
1327
|
+
for (const fileToDelete of catalogEntry.filesToDelete) {
|
|
1328
|
+
const fullPath = path6.join(shadowDir, fileToDelete);
|
|
1329
|
+
if (fs5.existsSync(fullPath)) {
|
|
1330
|
+
fs5.unlinkSync(fullPath);
|
|
1331
|
+
console.log(`[PackageManagerAgent] Deleted deprecated file: ${fileToDelete}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (catalogEntry.filesToScaffold) {
|
|
1336
|
+
for (const [filename, content] of Object.entries(catalogEntry.filesToScaffold)) {
|
|
1337
|
+
const fullPath = path6.join(shadowDir, filename);
|
|
1338
|
+
if (!fs5.existsSync(fullPath)) {
|
|
1339
|
+
fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
|
|
1340
|
+
fs5.writeFileSync(fullPath, content, "utf-8");
|
|
1341
|
+
console.log(`[PackageManagerAgent] Scaffolded missing structural file: ${filename}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const packageJsonPath = path6.join(shadowDir, "package.json");
|
|
1346
|
+
if (fs5.existsSync(packageJsonPath)) {
|
|
1347
|
+
try {
|
|
1348
|
+
const pkgRaw = fs5.readFileSync(packageJsonPath, "utf-8");
|
|
1349
|
+
const pkg = JSON.parse(pkgRaw);
|
|
1350
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
1351
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
1352
|
+
if (catalogEntry.dependenciesToRemove) {
|
|
1353
|
+
for (const dep of catalogEntry.dependenciesToRemove) {
|
|
1354
|
+
delete pkg.dependencies[dep];
|
|
1355
|
+
delete pkg.devDependencies[dep];
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (catalogEntry.devDependenciesToRemove) {
|
|
1359
|
+
for (const dep of catalogEntry.devDependenciesToRemove) {
|
|
1360
|
+
delete pkg.dependencies[dep];
|
|
1361
|
+
delete pkg.devDependencies[dep];
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (catalogEntry.dependenciesToAdd) {
|
|
1365
|
+
for (const [dep, ver] of Object.entries(catalogEntry.dependenciesToAdd)) {
|
|
1366
|
+
pkg.dependencies[dep] = ver;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (catalogEntry.devDependenciesToAdd) {
|
|
1370
|
+
for (const [dep, ver] of Object.entries(catalogEntry.devDependenciesToAdd)) {
|
|
1371
|
+
pkg.devDependencies[dep] = ver;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (Object.keys(pkg.devDependencies).length === 0) delete pkg.devDependencies;
|
|
1375
|
+
if (catalogEntry.scriptsToUpdate) {
|
|
1376
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
1377
|
+
for (const [scriptName, scriptCmd] of Object.entries(catalogEntry.scriptsToUpdate)) {
|
|
1378
|
+
pkg.scripts[scriptName] = scriptCmd;
|
|
1725
1379
|
}
|
|
1726
1380
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1381
|
+
if (catalogEntry.scriptsToRemove && pkg.scripts) {
|
|
1382
|
+
for (const scriptName of catalogEntry.scriptsToRemove) {
|
|
1383
|
+
delete pkg.scripts[scriptName];
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
fs5.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1387
|
+
console.log(`[PackageManagerAgent] Updated package.json dependencies directly (no npm subprocess).`);
|
|
1388
|
+
sendEvent({
|
|
1389
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1390
|
+
producerId: participant.getId(),
|
|
1391
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1392
|
+
payload: { planId: payload.planId, message: "PackageManager updated package.json. IntegrationAgent will run npm install in the shadow workspace \u2014 the original project is untouched until you Apply.", level: "info" }
|
|
1393
|
+
}, participant.getId());
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
console.error(`[PackageManagerAgent] Error editing package.json:`, err);
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
console.log(`[PackageManagerAgent] No package.json found at ${packageJsonPath}`);
|
|
1399
|
+
}
|
|
1400
|
+
await emitPackagesReady("completed");
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
var managePackagesHandler = {
|
|
1404
|
+
specification: new WhenMigrationStarts2(),
|
|
1405
|
+
processor: managePackagesProcessor
|
|
1406
|
+
};
|
|
1407
|
+
function createPackageManagerAgent() {
|
|
1408
|
+
return createAgent4({
|
|
1409
|
+
name: "PackageManager",
|
|
1410
|
+
capabilities: ["package_management"],
|
|
1411
|
+
instruction: "You are the Package Manager Agent. Your job is to resolve dependencies and clean up unused framework files.",
|
|
1412
|
+
tools: [],
|
|
1413
|
+
handlers: [managePackagesHandler]
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// ../application/src/agents/CoordinatorAgent.ts
|
|
1418
|
+
import {
|
|
1419
|
+
createAgent as createAgent5,
|
|
1420
|
+
SituationSpecification as SituationSpecification5
|
|
1421
|
+
} from "@mozaik-ai/core";
|
|
1422
|
+
|
|
1423
|
+
// ../application/src/agents/planReady.ts
|
|
1424
|
+
function planReadyForIntegration(plan) {
|
|
1425
|
+
if (plan.phase === "integration" || plan.phase === "completed" || plan.phase === "failed") {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
const files = plan.tasks.filter((task) => !task.filePath.startsWith("system:"));
|
|
1429
|
+
if (files.length === 0) return false;
|
|
1430
|
+
const packages = plan.tasks.find((task) => task.filePath === "system:package_manager");
|
|
1431
|
+
if (packages && packages.status !== "completed" && packages.status !== "failed") {
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
const live = plan.tasks.filter((task) => !task.filePath.startsWith("system:"));
|
|
1435
|
+
if (live.some((task) => task.status === "pending" || task.status === "in_progress")) {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
return live.every((task) => task.status === "completed" || task.status === "failed");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// ../application/src/agents/CoordinatorAgent.ts
|
|
1442
|
+
var WATCHDOG_MS = 8e3;
|
|
1443
|
+
var WATCHDOG_MAX_MS = 30 * 60 * 1e3;
|
|
1444
|
+
var WhenSwarmMayBeIdle = class extends SituationSpecification5 {
|
|
1445
|
+
isSatisfiedBy({ event }) {
|
|
1446
|
+
return event.type === "file.reviewed" /* FILE_REVIEWED */ || event.type === "file.failed" /* FILE_FAILED */ || event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */ || event.type === "phase.packages_ready" /* PHASE_PACKAGES_READY */;
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
var WhenMigrationStarted = class extends SituationSpecification5 {
|
|
1450
|
+
isSatisfiedBy({ event }) {
|
|
1451
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
async function emitIntegrationIfReady(producerId, planId) {
|
|
1455
|
+
const runtime = resolveRuntime();
|
|
1456
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1457
|
+
if (!plan || !planReadyForIntegration(plan)) return;
|
|
1458
|
+
console.log(`[Coordinator] File work and packages are settled. Emitting phase.integration_started.`);
|
|
1459
|
+
sendEvent({
|
|
1460
|
+
type: "phase.integration_started" /* PHASE_INTEGRATION_STARTED */,
|
|
1461
|
+
producerId,
|
|
1462
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1463
|
+
payload: { planId }
|
|
1464
|
+
}, producerId);
|
|
1465
|
+
}
|
|
1466
|
+
function stopWatchdog(planId) {
|
|
1467
|
+
const runtime = resolveRuntime();
|
|
1468
|
+
const timer = runtime.state.integrationWatchdogs.get(planId);
|
|
1469
|
+
if (!timer) return;
|
|
1470
|
+
clearInterval(timer);
|
|
1471
|
+
runtime.state.integrationWatchdogs.delete(planId);
|
|
1472
|
+
}
|
|
1473
|
+
var startIntegrationIfIdle = {
|
|
1474
|
+
async apply({ event, participant }) {
|
|
1475
|
+
const planId = event.payload.planId;
|
|
1476
|
+
if (!planId) return;
|
|
1477
|
+
await emitIntegrationIfReady(participant.getId(), planId);
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
var startWatchdogProcessor = {
|
|
1481
|
+
async apply({ event, participant }) {
|
|
1482
|
+
const p = event.payload;
|
|
1483
|
+
const runtime = resolveRuntime();
|
|
1484
|
+
stopWatchdog(p.planId);
|
|
1485
|
+
const startedAt = Date.now();
|
|
1486
|
+
const producerId = participant.getId();
|
|
1487
|
+
sendEvent({
|
|
1488
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1489
|
+
producerId,
|
|
1490
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1491
|
+
payload: {
|
|
1492
|
+
planId: p.planId,
|
|
1493
|
+
message: "Coordinator watchdog is on: if file work settles and Integration never starts, it will kick within 8s. No extra model calls.",
|
|
1494
|
+
level: "info"
|
|
1495
|
+
}
|
|
1496
|
+
}, producerId);
|
|
1497
|
+
const timer = setInterval(async () => {
|
|
1498
|
+
try {
|
|
1499
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1500
|
+
if (!plan || plan.phase === "completed" || plan.phase === "failed") {
|
|
1501
|
+
stopWatchdog(p.planId);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (Date.now() - startedAt >= WATCHDOG_MAX_MS) {
|
|
1505
|
+
stopWatchdog(p.planId);
|
|
1506
|
+
sendEvent({
|
|
1507
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1508
|
+
producerId,
|
|
1509
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1510
|
+
payload: {
|
|
1511
|
+
planId: p.planId,
|
|
1512
|
+
message: "Coordinator watchdog stopped after 30 minutes. Integration never became ready (files still in progress, or the run stalled).",
|
|
1513
|
+
level: "warning"
|
|
1761
1514
|
}
|
|
1762
|
-
},
|
|
1763
|
-
|
|
1764
|
-
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
1765
|
-
if (!isDone) {
|
|
1766
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
|
|
1767
|
-
const runtime2 = resolveRuntime2();
|
|
1768
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1769
|
-
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
|
|
1770
|
-
leave(tempAgent);
|
|
1771
|
-
resolve3();
|
|
1772
|
-
}
|
|
1515
|
+
}, producerId);
|
|
1516
|
+
return;
|
|
1773
1517
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1518
|
+
await emitIntegrationIfReady(producerId, p.planId);
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
console.error("[Coordinator] Watchdog tick failed:", error);
|
|
1521
|
+
}
|
|
1522
|
+
}, WATCHDOG_MS);
|
|
1523
|
+
runtime.state.integrationWatchdogs.set(p.planId, timer);
|
|
1776
1524
|
}
|
|
1777
1525
|
};
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
return createAgent3({
|
|
1784
|
-
name: "Reviewer",
|
|
1785
|
-
capabilities: ["code_review", "inference"],
|
|
1786
|
-
instruction: "You are the Reviewer Worker.",
|
|
1526
|
+
function createCoordinatorAgent() {
|
|
1527
|
+
return createAgent5({
|
|
1528
|
+
name: "Coordinator",
|
|
1529
|
+
capabilities: [],
|
|
1530
|
+
instruction: "You watch the bus and start Integration when file tasks and packages are settled. You do not call the model.",
|
|
1787
1531
|
tools: [],
|
|
1788
|
-
|
|
1789
|
-
|
|
1532
|
+
handlers: [
|
|
1533
|
+
{ specification: new WhenMigrationStarted(), processor: startWatchdogProcessor },
|
|
1534
|
+
{ specification: new WhenSwarmMayBeIdle(), processor: startIntegrationIfIdle }
|
|
1535
|
+
]
|
|
1790
1536
|
});
|
|
1791
1537
|
}
|
|
1792
1538
|
|
|
1793
|
-
// ../application/src/agents/
|
|
1794
|
-
import * as fs5 from "fs";
|
|
1795
|
-
import * as path4 from "path";
|
|
1539
|
+
// ../application/src/agents/ReporterAgent.ts
|
|
1796
1540
|
import {
|
|
1797
|
-
|
|
1798
|
-
|
|
1541
|
+
Agent as Agent6,
|
|
1542
|
+
createAgent as createAgent6,
|
|
1543
|
+
SituationSpecification as SituationSpecification6
|
|
1799
1544
|
} from "@mozaik-ai/core";
|
|
1800
|
-
|
|
1545
|
+
function log(participantId, planId, message, level = "info") {
|
|
1546
|
+
sendEvent({
|
|
1547
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1548
|
+
producerId: participantId,
|
|
1549
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1550
|
+
payload: { planId, message, level }
|
|
1551
|
+
}, participantId);
|
|
1552
|
+
}
|
|
1553
|
+
async function shadowPathFor(plan, journal) {
|
|
1554
|
+
if (journal?.shadowPath) return journal.shadowPath;
|
|
1555
|
+
const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-2UXWWNPF.js");
|
|
1556
|
+
return new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
1557
|
+
}
|
|
1558
|
+
function fallbackReport(params) {
|
|
1559
|
+
const completed = params.tasks.filter((t) => t.status === "completed");
|
|
1560
|
+
const failed = params.tasks.filter((t) => t.status === "failed");
|
|
1561
|
+
const rejected = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal");
|
|
1562
|
+
const fileLines = params.tasks.filter((t) => !t.filePath.startsWith("system:")).slice(0, 40).map((t) => `- \`${t.filePath}\` \u2014 ${t.status}${t.error ? ` (${t.error})` : ""}`).join("\n");
|
|
1563
|
+
const riskLines = rejected.slice(0, 12).map((n) => `- \`${n.filePath}\`: ${n.detail || n.outcome}`).join("\n");
|
|
1564
|
+
return `# What changed
|
|
1565
|
+
|
|
1566
|
+
Metamorph migrated this project from **${params.source}** to **${params.target}**.
|
|
1567
|
+
${params.phase === "failed" ? "\n**Status:** shadow `npm run build` (or catalog checks) did not pass. Treat this as an incomplete migration.\n" : ""}
|
|
1568
|
+
Your original files were not overwritten until you click **Apply Migration**. After apply, the result lives on a git branch named like \`metamorph/${params.runId}\`.
|
|
1569
|
+
|
|
1570
|
+
## How to run it
|
|
1571
|
+
|
|
1572
|
+
1. Apply the migration in the dashboard (or keep working in the shadow workspace).
|
|
1573
|
+
2. From the project folder:
|
|
1574
|
+
\`\`\`bash
|
|
1575
|
+
npm install
|
|
1576
|
+
npm run dev
|
|
1577
|
+
\`\`\`
|
|
1578
|
+
3. Open the URL the framework prints (\`next dev\` \u2192 port 3000, Vite \u2192 5173).
|
|
1579
|
+
|
|
1580
|
+
## What we touched
|
|
1581
|
+
|
|
1582
|
+
- Files completed: ${completed.length}
|
|
1583
|
+
- Files failed: ${failed.length}
|
|
1584
|
+
- Started: ${params.startedAt}
|
|
1585
|
+
- Finished: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
1586
|
+
|
|
1587
|
+
${fileLines || "_No file tasks recorded._"}
|
|
1588
|
+
|
|
1589
|
+
## Watch out for
|
|
1590
|
+
|
|
1591
|
+
${riskLines || "- Review `package.json` scripts and the new entry files (layout/page or `index.html` + `src/main.tsx`).\n- Confirm global CSS still loads in the browser."}
|
|
1592
|
+
|
|
1593
|
+
## Need a retry?
|
|
1594
|
+
|
|
1595
|
+
Discard in the dashboard and start a new run. Do not mix two shadow workspaces in the same tree.
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
var STATUS_PASSED = "**Status:** Shadow `npm run build` passed.";
|
|
1599
|
+
var STATUS_FAILED = "**Status:** Shadow `npm run build` (or catalog checks) did not pass. Treat this as an incomplete migration.";
|
|
1600
|
+
function statusLine(phase) {
|
|
1601
|
+
return phase === "failed" ? STATUS_FAILED : STATUS_PASSED;
|
|
1602
|
+
}
|
|
1603
|
+
function patchReportStatus(markdown, phase) {
|
|
1604
|
+
const line = statusLine(phase);
|
|
1605
|
+
const stripped = markdown.replace(/\n?\*\*Status:\*\*[^\n]*\n?/g, "\n");
|
|
1606
|
+
if (stripped.startsWith("# ")) {
|
|
1607
|
+
const nl = stripped.indexOf("\n");
|
|
1608
|
+
if (nl === -1) return `${stripped}
|
|
1609
|
+
|
|
1610
|
+
${line}
|
|
1611
|
+
`;
|
|
1612
|
+
return `${stripped.slice(0, nl)}
|
|
1613
|
+
|
|
1614
|
+
${line}${stripped.slice(nl)}`;
|
|
1615
|
+
}
|
|
1616
|
+
return `${line}
|
|
1617
|
+
|
|
1618
|
+
${stripped.trimStart()}`;
|
|
1619
|
+
}
|
|
1620
|
+
async function writeOrPatchMigrationMd(dest, phase, fallback) {
|
|
1621
|
+
const fs8 = await import("fs");
|
|
1622
|
+
if (!fs8.existsSync(dest) || fs8.statSync(dest).size < 80) {
|
|
1623
|
+
fs8.writeFileSync(dest, patchReportStatus(fallback, phase), "utf-8");
|
|
1624
|
+
return "wrote";
|
|
1625
|
+
}
|
|
1626
|
+
const current = fs8.readFileSync(dest, "utf-8");
|
|
1627
|
+
fs8.writeFileSync(dest, patchReportStatus(current, phase), "utf-8");
|
|
1628
|
+
return "patched";
|
|
1629
|
+
}
|
|
1630
|
+
var WhenMigrationStarted2 = class extends SituationSpecification6 {
|
|
1801
1631
|
isSatisfiedBy({ event }) {
|
|
1802
1632
|
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1803
1633
|
}
|
|
1804
1634
|
};
|
|
1805
|
-
var
|
|
1635
|
+
var WhenFileMigrated2 = class extends SituationSpecification6 {
|
|
1636
|
+
isSatisfiedBy({ event }) {
|
|
1637
|
+
return event.type === "file.migrated" /* FILE_MIGRATED */;
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
var WhenFileReviewed = class extends SituationSpecification6 {
|
|
1641
|
+
isSatisfiedBy({ event }) {
|
|
1642
|
+
return event.type === "file.reviewed" /* FILE_REVIEWED */;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
var WhenFileRejected2 = class extends SituationSpecification6 {
|
|
1646
|
+
isSatisfiedBy({ event }) {
|
|
1647
|
+
return event.type === "file.rejected" /* FILE_REJECTED */;
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
var WhenFileFatalMismatch2 = class extends SituationSpecification6 {
|
|
1651
|
+
isSatisfiedBy({ event }) {
|
|
1652
|
+
return event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */;
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
var WhenIntegrationStarts = class extends SituationSpecification6 {
|
|
1656
|
+
isSatisfiedBy({ event }) {
|
|
1657
|
+
return event.type === "phase.integration_started" /* PHASE_INTEGRATION_STARTED */;
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
var WhenMigrationCompleted = class extends SituationSpecification6 {
|
|
1661
|
+
isSatisfiedBy({ event }) {
|
|
1662
|
+
return event.type === "migration.completed" /* MIGRATION_COMPLETED */;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
var WhenOwnAnswer = class extends SituationSpecification6 {
|
|
1666
|
+
isSatisfiedBy({ event, participant }) {
|
|
1667
|
+
return event.type === "model.answer" && event.producerId === participant.getId();
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
var startedProcessor = {
|
|
1806
1671
|
async apply({ event, participant }) {
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1672
|
+
const p = event.payload;
|
|
1673
|
+
const entry = resolveRuntime().state.journal(p.planId);
|
|
1674
|
+
if (p.shadowWorkspacePath) entry.shadowPath = p.shadowWorkspacePath;
|
|
1675
|
+
log(participant.getId(), p.planId, "Reporter is on the bus. It will draft MIGRATION.md during install/build (one model call, in parallel with Integration).");
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var migratedProcessor = {
|
|
1679
|
+
async apply({ event }) {
|
|
1680
|
+
const p = event.payload;
|
|
1681
|
+
resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "migrated" });
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
var reviewedProcessor = {
|
|
1685
|
+
async apply({ event }) {
|
|
1686
|
+
const p = event.payload;
|
|
1687
|
+
resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "approved" });
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
var rejectedProcessor = {
|
|
1691
|
+
async apply({ event }) {
|
|
1692
|
+
const p = event.payload;
|
|
1693
|
+
resolveRuntime().state.journal(p.planId).notes.push({
|
|
1694
|
+
filePath: p.filePath,
|
|
1695
|
+
outcome: "rejected",
|
|
1696
|
+
detail: (p.errors || []).slice(0, 3).join("; ")
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
var fatalProcessor = {
|
|
1701
|
+
async apply({ event }) {
|
|
1702
|
+
const p = event.payload;
|
|
1703
|
+
resolveRuntime().state.journal(p.planId).notes.push({ filePath: p.filePath, outcome: "fatal", detail: p.reason });
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
function draftPrompt(params) {
|
|
1707
|
+
const notable = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal").slice(-15).map((n) => `${n.filePath}: ${n.detail || n.outcome}`).join("\n");
|
|
1708
|
+
return `Write a user-facing migration briefing to this exact path:
|
|
1709
|
+
${params.reportAbs}
|
|
1710
|
+
|
|
1711
|
+
Audience: the engineer who clicked Start Migration. They are not reading agent telemetry.
|
|
1712
|
+
File migration is done; Integration is installing/building in parallel \u2014 you may say the shadow build is still running.
|
|
1713
|
+
|
|
1714
|
+
Migration: ${params.plan.profile.source} \u2192 ${params.plan.profile.target}
|
|
1715
|
+
Project path: ${params.plan.targetPath || "(unknown)"}
|
|
1716
|
+
Run id: ${params.plan.runId}
|
|
1717
|
+
|
|
1718
|
+
Include these sections, in this order, with markdown headings:
|
|
1719
|
+
1. What changed \u2014 1-2 short paragraphs in plain language
|
|
1720
|
+
2. How to run it \u2014 concrete commands (npm install, npm run dev, which port if you can infer from package.json)
|
|
1721
|
+
3. What we touched \u2014 group by area (routes, styles, config, dependencies). Do not dump every file as a checklist unless there are under 12 files.
|
|
1722
|
+
4. Watch out for \u2014 honest risks from the notes below and from reading package.json / the new entry files
|
|
1723
|
+
5. Next steps after Apply \u2014 git branch checkout if they applied, npm install on the real project
|
|
1724
|
+
|
|
1725
|
+
You may read package.json, README, src/app/layout.tsx, src/main.tsx, index.html, or MIGRATION.md if they exist.
|
|
1726
|
+
|
|
1727
|
+
Rejected / structure notes:
|
|
1728
|
+
${notable || "(none recorded)"}
|
|
1729
|
+
|
|
1730
|
+
If the model cannot infer something, say so briefly. Do not invent URLs, secrets, or features.
|
|
1731
|
+
|
|
1732
|
+
A factual draft you may rewrite (keep facts, improve voice):
|
|
1733
|
+
---
|
|
1734
|
+
${params.draft}
|
|
1735
|
+
---
|
|
1736
|
+
When the file is written, stop.`;
|
|
1737
|
+
}
|
|
1738
|
+
var draftDuringInstallProcessor = {
|
|
1739
|
+
async apply({ event, participant }) {
|
|
1740
|
+
if (!(participant instanceof Agent6)) return;
|
|
1741
|
+
const p = event.payload;
|
|
1742
|
+
const runtime = resolveRuntime();
|
|
1743
|
+
const board = runtime.state.journal(p.planId);
|
|
1744
|
+
if (board.reportLoopActive || board.reportStarted) return;
|
|
1745
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1746
|
+
if (!plan) return;
|
|
1747
|
+
const shadowPath = await shadowPathFor(plan, board);
|
|
1748
|
+
const draft = fallbackReport({
|
|
1749
|
+
source: plan.profile.source,
|
|
1750
|
+
target: plan.profile.target,
|
|
1751
|
+
targetPath: plan.targetPath,
|
|
1752
|
+
runId: plan.runId,
|
|
1753
|
+
phase: plan.phase,
|
|
1754
|
+
tasks: plan.tasks || [],
|
|
1755
|
+
notes: board.notes,
|
|
1756
|
+
startedAt: board.startedAt
|
|
1757
|
+
});
|
|
1758
|
+
const tools = participant.getTools();
|
|
1759
|
+
if (tools.length === 0) {
|
|
1760
|
+
board.reportStarted = true;
|
|
1761
|
+
const fs8 = await import("fs");
|
|
1762
|
+
const path12 = await import("path");
|
|
1763
|
+
fs8.writeFileSync(path12.join(shadowPath, "MIGRATION.md"), draft, "utf-8");
|
|
1764
|
+
log(participant.getId(), p.planId, "Reporter wrote a fallback MIGRATION.md (no file tools on the agent).");
|
|
1812
1765
|
return;
|
|
1813
1766
|
}
|
|
1767
|
+
board.reportStarted = true;
|
|
1768
|
+
board.reportLoopActive = true;
|
|
1769
|
+
log(participant.getId(), p.planId, "Reporter is drafting MIGRATION.md while Integration runs npm install / build (one model call).");
|
|
1770
|
+
const reportAbs = `${shadowPath.replace(/\\/g, "/")}/MIGRATION.md`;
|
|
1771
|
+
const modelToUse = process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
1772
|
+
runLoop(participant.getId(), draftPrompt({ reportAbs, plan, notes: board.notes, draft }), {
|
|
1773
|
+
model: modelToUse,
|
|
1774
|
+
context: participant.getMemory().getContext(),
|
|
1775
|
+
tools
|
|
1776
|
+
});
|
|
1777
|
+
setTimeout(async () => {
|
|
1778
|
+
const still = runtime.state.journals.get(p.planId);
|
|
1779
|
+
if (!still?.reportLoopActive) return;
|
|
1780
|
+
still.reportLoopActive = false;
|
|
1781
|
+
try {
|
|
1782
|
+
const path12 = await import("path");
|
|
1783
|
+
const dest = path12.join(shadowPath, "MIGRATION.md");
|
|
1784
|
+
const phase = still.pendingStatusPhase;
|
|
1785
|
+
await writeOrPatchMigrationMd(dest, phase, draft);
|
|
1786
|
+
still.pendingStatusPhase = void 0;
|
|
1787
|
+
log(participant.getId(), p.planId, "Reporter draft timed out; kept fallback MIGRATION.md.", "warning");
|
|
1788
|
+
} catch {
|
|
1789
|
+
}
|
|
1790
|
+
}, 9e4);
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
var ownAnswerProcessor = {
|
|
1794
|
+
async apply() {
|
|
1814
1795
|
const runtime = resolveRuntime();
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1796
|
+
for (const [planId, journal] of runtime.state.journals) {
|
|
1797
|
+
journal.reportLoopActive = false;
|
|
1798
|
+
if (!journal.pendingStatusPhase) continue;
|
|
1799
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1800
|
+
if (!plan) continue;
|
|
1801
|
+
const shadowPath = await shadowPathFor(plan, journal);
|
|
1802
|
+
const path12 = await import("path");
|
|
1803
|
+
const dest = path12.join(shadowPath, "MIGRATION.md");
|
|
1804
|
+
const draft = fallbackReport({
|
|
1805
|
+
source: plan.profile.source,
|
|
1806
|
+
target: plan.profile.target,
|
|
1807
|
+
targetPath: plan.targetPath,
|
|
1808
|
+
runId: plan.runId,
|
|
1809
|
+
phase: journal.pendingStatusPhase,
|
|
1810
|
+
tasks: plan.tasks || [],
|
|
1811
|
+
notes: journal.notes,
|
|
1812
|
+
startedAt: journal.startedAt
|
|
1813
|
+
});
|
|
1814
|
+
await writeOrPatchMigrationMd(dest, journal.pendingStatusPhase, draft);
|
|
1815
|
+
journal.pendingStatusPhase = void 0;
|
|
1819
1816
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
var completedProcessor = {
|
|
1820
|
+
async apply({ event, participant }) {
|
|
1821
|
+
const p = event.payload;
|
|
1822
|
+
const runtime = resolveRuntime();
|
|
1823
|
+
const board = runtime.state.journal(p.planId);
|
|
1824
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1825
|
+
if (!plan) return;
|
|
1826
|
+
const shadowPath = await shadowPathFor(plan, board);
|
|
1827
|
+
const draft = fallbackReport({
|
|
1828
|
+
source: plan.profile.source,
|
|
1829
|
+
target: plan.profile.target,
|
|
1830
|
+
targetPath: plan.targetPath,
|
|
1831
|
+
runId: plan.runId,
|
|
1832
|
+
phase: plan.phase,
|
|
1833
|
+
tasks: plan.tasks || [],
|
|
1834
|
+
notes: board.notes,
|
|
1835
|
+
startedAt: board.startedAt
|
|
1836
|
+
});
|
|
1837
|
+
board.pendingStatusPhase = plan.phase === "failed" ? "failed" : "completed";
|
|
1838
|
+
try {
|
|
1839
|
+
const path12 = await import("path");
|
|
1840
|
+
const dest = path12.join(shadowPath, "MIGRATION.md");
|
|
1841
|
+
if (board.reportLoopActive) {
|
|
1842
|
+
log(participant.getId(), p.planId, "Reporter draft is still running; Status will be patched when it finishes (no second model call).");
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const result = await writeOrPatchMigrationMd(dest, plan.phase, draft);
|
|
1846
|
+
board.pendingStatusPhase = void 0;
|
|
1847
|
+
log(
|
|
1848
|
+
participant.getId(),
|
|
1849
|
+
p.planId,
|
|
1850
|
+
result === "patched" ? "Reporter patched MIGRATION.md with the final build Status (no extra model call)." : "Reporter finished. Open MIGRATION.md in the shadow workspace (it is copied to your project when you Apply)."
|
|
1851
|
+
);
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.error("[ReporterAgent] Could not ensure MIGRATION.md:", error);
|
|
1854
|
+
log(participant.getId(), p.planId, "Reporter could not write MIGRATION.md.", "warning");
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
function createReporterAgent(tools = []) {
|
|
1859
|
+
return createAgent6({
|
|
1860
|
+
name: "Reporter",
|
|
1861
|
+
capabilities: ["reporting", "inference"],
|
|
1862
|
+
instruction: "You are the Reporter on the Metamorph swarm. Watch the bus. When Integration starts, write MIGRATION.md for the human \u2014 not an internal log. Use write_file. Do not modify application source. Do not start extra model calls; one draft per run is enough.",
|
|
1863
|
+
tools,
|
|
1864
|
+
handlers: [
|
|
1865
|
+
{ specification: new WhenMigrationStarted2(), processor: startedProcessor },
|
|
1866
|
+
{ specification: new WhenFileMigrated2(), processor: migratedProcessor },
|
|
1867
|
+
{ specification: new WhenFileReviewed(), processor: reviewedProcessor },
|
|
1868
|
+
{ specification: new WhenFileRejected2(), processor: rejectedProcessor },
|
|
1869
|
+
{ specification: new WhenFileFatalMismatch2(), processor: fatalProcessor },
|
|
1870
|
+
{ specification: new WhenIntegrationStarts(), processor: draftDuringInstallProcessor },
|
|
1871
|
+
{ specification: new WhenOwnAnswer(), processor: ownAnswerProcessor },
|
|
1872
|
+
{ specification: new WhenMigrationCompleted(), processor: completedProcessor }
|
|
1873
|
+
]
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// ../application/src/agents/IntegrationAgent.ts
|
|
1878
|
+
import {
|
|
1879
|
+
createAgent as createAgent7,
|
|
1880
|
+
SituationSpecification as SituationSpecification7
|
|
1881
|
+
} from "@mozaik-ai/core";
|
|
1882
|
+
var MAX_INTEGRATION_ROUNDS = 4;
|
|
1883
|
+
var REPAIR_TIMEOUT_MS = 12e4;
|
|
1884
|
+
var WhenIntegrationStarts2 = class extends SituationSpecification7 {
|
|
1885
|
+
isSatisfiedBy({ event }) {
|
|
1886
|
+
return event.type === "phase.integration_started" /* PHASE_INTEGRATION_STARTED */;
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
async function completeMigration(participantId, planId, message, level) {
|
|
1890
|
+
const runtime = resolveRuntime();
|
|
1891
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1892
|
+
if (plan) {
|
|
1893
|
+
plan.phase = "completed";
|
|
1894
|
+
await runtime.state.repository.savePlan(plan);
|
|
1895
|
+
}
|
|
1896
|
+
sendEvent({
|
|
1897
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1898
|
+
producerId: participantId,
|
|
1899
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1900
|
+
payload: { planId, message, level }
|
|
1901
|
+
}, participantId);
|
|
1902
|
+
sendEvent({
|
|
1903
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1904
|
+
producerId: participantId,
|
|
1905
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1906
|
+
payload: { planId }
|
|
1907
|
+
}, participantId);
|
|
1908
|
+
}
|
|
1909
|
+
function log2(participantId, planId, message, level = "info") {
|
|
1910
|
+
sendEvent({
|
|
1911
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1912
|
+
producerId: participantId,
|
|
1913
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1914
|
+
payload: { planId, message, level }
|
|
1915
|
+
}, participantId);
|
|
1916
|
+
}
|
|
1917
|
+
async function failMigration(participantId, planId, message) {
|
|
1918
|
+
const runtime = resolveRuntime();
|
|
1919
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1920
|
+
if (plan) {
|
|
1921
|
+
plan.phase = "failed";
|
|
1922
|
+
await runtime.state.repository.savePlan(plan);
|
|
1923
|
+
}
|
|
1924
|
+
log2(participantId, planId, message, "error");
|
|
1925
|
+
sendEvent({
|
|
1926
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1927
|
+
producerId: participantId,
|
|
1928
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1929
|
+
payload: { planId }
|
|
1930
|
+
}, participantId);
|
|
1931
|
+
}
|
|
1932
|
+
async function requeueBrokenFiles(producerId, planId, files, errors) {
|
|
1933
|
+
const runtime = resolveRuntime();
|
|
1934
|
+
const unique = [...new Set(files.filter(Boolean))];
|
|
1935
|
+
log2(
|
|
1936
|
+
producerId,
|
|
1937
|
+
planId,
|
|
1938
|
+
`Shadow install/build did not pass. Reopening ${unique.length} file task(s) for Worker repair.`,
|
|
1939
|
+
"warning"
|
|
1940
|
+
);
|
|
1941
|
+
for (const filePath of unique) {
|
|
1942
|
+
await runtime.state.repository.updateTaskStatus(
|
|
1943
|
+
planId,
|
|
1944
|
+
filePath,
|
|
1945
|
+
"pending",
|
|
1946
|
+
errors.slice(0, 3).join("\n").slice(0, 1500)
|
|
1947
|
+
);
|
|
1948
|
+
sendEvent({
|
|
1949
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
1950
|
+
producerId,
|
|
1951
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1952
|
+
payload: {
|
|
1953
|
+
planId,
|
|
1954
|
+
filePath,
|
|
1955
|
+
errors,
|
|
1956
|
+
source: "integration"
|
|
1957
|
+
}
|
|
1958
|
+
}, producerId);
|
|
1959
|
+
}
|
|
1960
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1961
|
+
if (plan) {
|
|
1962
|
+
plan.phase = "files";
|
|
1963
|
+
await runtime.state.repository.savePlan(plan);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
var integrationProcessor = {
|
|
1967
|
+
async apply({ event, participant }) {
|
|
1968
|
+
const payload = event.payload;
|
|
1969
|
+
const planId = payload.planId;
|
|
1970
|
+
const producerId = participant.getId();
|
|
1971
|
+
const locks = resolveRuntime().state.integrationLocks;
|
|
1972
|
+
if (locks.has(planId)) {
|
|
1973
|
+
console.log(`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`);
|
|
1823
1974
|
return;
|
|
1824
1975
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1976
|
+
locks.add(planId);
|
|
1977
|
+
try {
|
|
1978
|
+
const runtime = resolveRuntime();
|
|
1979
|
+
const plan = await runtime.state.repository.getPlan(planId);
|
|
1980
|
+
if (!plan) {
|
|
1981
|
+
console.warn(`[IntegrationAgent] Plan ${planId} not found.`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (plan.phase === "completed" || plan.phase === "failed") {
|
|
1985
|
+
console.log(`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
plan.phase = "integration";
|
|
1989
|
+
const currentRound = plan.integrationRounds ?? 0;
|
|
1990
|
+
if (currentRound >= MAX_INTEGRATION_ROUNDS) {
|
|
1991
|
+
await failMigration(
|
|
1992
|
+
producerId,
|
|
1993
|
+
planId,
|
|
1994
|
+
`Integration budget exhausted after ${MAX_INTEGRATION_ROUNDS} shadow build rounds. npm run build still does not pass \u2014 the run is not successful.`
|
|
1995
|
+
);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
plan.integrationRounds = currentRound + 1;
|
|
1828
1999
|
await runtime.state.repository.savePlan(plan);
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2000
|
+
const lastRound = plan.integrationRounds >= MAX_INTEGRATION_ROUNDS;
|
|
2001
|
+
const { ShadowWorkspace: ShadowWorkspace2, createReadFileTool: createReadFileTool2, createWriteFileTool: createWriteFileTool2, createListDirectoryTool: createListDirectoryTool2, createRunBuildTool: createRunBuildTool2, runInShadowWorkspace, parseImplicatedFiles } = await import("./src-2UXWWNPF.js");
|
|
2002
|
+
const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
|
|
2003
|
+
const { findMigrationCatalogEntry: findMigrationCatalogEntry2 } = await import("./src-UC3SN46X.js");
|
|
2004
|
+
const shadowPath = payload.shadowWorkspacePath || new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
2005
|
+
console.log(`[IntegrationAgent] Shadow build round ${plan.integrationRounds}/${MAX_INTEGRATION_ROUNDS} in ${shadowPath}`);
|
|
2006
|
+
log2(producerId, planId, `File migration is done. Next: install dependencies in the shadow workspace (npm install). This can take several minutes \u2014 counters staying at 0 is expected.`);
|
|
2007
|
+
console.log(`[IntegrationAgent] npm install starting in ${shadowPath}`);
|
|
2008
|
+
let lastInstallBeat = Date.now();
|
|
2009
|
+
const install = await runInShadowWorkspace(shadowPath, "npm install --no-fund --no-audit", (chunk) => {
|
|
2010
|
+
process.stdout.write(chunk);
|
|
2011
|
+
if (Date.now() - lastInstallBeat >= 15e3) {
|
|
2012
|
+
lastInstallBeat = Date.now();
|
|
2013
|
+
log2(producerId, planId, "npm install is still running in the shadow workspace. Wait \u2014 this is not finished.");
|
|
1836
2014
|
}
|
|
2015
|
+
});
|
|
2016
|
+
if (!install.ok) {
|
|
2017
|
+
console.warn("[IntegrationAgent] npm install failed in shadow:", install.output.slice(0, 2e3));
|
|
2018
|
+
log2(producerId, planId, "npm install failed in the shadow workspace. Continuing to build anyway.", "warning");
|
|
2019
|
+
} else {
|
|
2020
|
+
log2(producerId, planId, "npm install finished. Next: npm run build in the shadow workspace.");
|
|
1837
2021
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2022
|
+
let lastBuildBeat = Date.now();
|
|
2023
|
+
let build = await runInShadowWorkspace(shadowPath, "npm run build", (chunk) => {
|
|
2024
|
+
process.stdout.write(chunk);
|
|
2025
|
+
if (Date.now() - lastBuildBeat >= 15e3) {
|
|
2026
|
+
lastBuildBeat = Date.now();
|
|
2027
|
+
log2(producerId, planId, "npm run build is still running in the shadow workspace. Wait \u2014 this is not finished.");
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
if (!build.ok) {
|
|
2031
|
+
log2(producerId, planId, "Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files.");
|
|
2032
|
+
const shadowTools = [
|
|
2033
|
+
createReadFileTool2(shadowPath),
|
|
2034
|
+
createWriteFileTool2(shadowPath),
|
|
2035
|
+
createListDirectoryTool2(shadowPath),
|
|
2036
|
+
createRunBuildTool2(shadowPath)
|
|
2037
|
+
];
|
|
2038
|
+
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
2039
|
+
await new Promise((originalResolve) => {
|
|
2040
|
+
let isDone = false;
|
|
2041
|
+
const resolve5 = () => {
|
|
2042
|
+
if (!isDone) {
|
|
2043
|
+
isDone = true;
|
|
2044
|
+
originalResolve();
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
const tempAgent = createMozaikAgent({
|
|
2048
|
+
name: `Integration-${Date.now()}`,
|
|
2049
|
+
capabilities: ["integration_testing", "code_repair"],
|
|
2050
|
+
instruction: "You are a staff engineer repairing a migrated project. Read failing files and their imports. Deduce real APIs from disk. Do not invent prop names. Do not wait for Git.",
|
|
2051
|
+
tools: shadowTools,
|
|
2052
|
+
handlers: []
|
|
2053
|
+
});
|
|
2054
|
+
join(tempAgent);
|
|
2055
|
+
const modelToUse = process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
2056
|
+
const excerpt = build.output.slice(0, 4e3);
|
|
2057
|
+
const prompt = `A real \`npm run build\` already failed in the shadow workspace at ${shadowPath}.
|
|
2058
|
+
You must deduce the fix from source, not guess. Use list_directory / read_file on the implicated files AND the modules they import. Keep existing callback/export names. If a router file was reassembled, import the existing screen instead.
|
|
2059
|
+
|
|
2060
|
+
Do NOT require Git. You may run run_project_build again after edits.
|
|
2061
|
+
|
|
2062
|
+
Build output:
|
|
2063
|
+
${excerpt}
|
|
2064
|
+
|
|
2065
|
+
When you have applied fixes (or cannot fix further), stop. A deterministic rebuild will run after you finish.`;
|
|
2066
|
+
Promise.resolve(runLoop(tempAgent.getId(), prompt, {
|
|
2067
|
+
model: modelToUse,
|
|
2068
|
+
context: tempAgent.getMemory().getContext(),
|
|
2069
|
+
tools: shadowTools
|
|
2070
|
+
})).catch((error) => {
|
|
2071
|
+
console.error("[IntegrationAgent] runLoop error:", error);
|
|
2072
|
+
leave(tempAgent);
|
|
2073
|
+
resolve5();
|
|
1848
2074
|
});
|
|
1849
|
-
|
|
2075
|
+
setTimeout(() => {
|
|
2076
|
+
if (!isDone) {
|
|
2077
|
+
console.warn("[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild.");
|
|
2078
|
+
leave(tempAgent);
|
|
2079
|
+
resolve5();
|
|
2080
|
+
}
|
|
2081
|
+
}, REPAIR_TIMEOUT_MS);
|
|
1850
2082
|
});
|
|
1851
|
-
|
|
1852
|
-
try {
|
|
1853
|
-
let toRemove = [];
|
|
1854
|
-
if (catalogEntry.dependenciesToRemove) toRemove.push(...catalogEntry.dependenciesToRemove);
|
|
1855
|
-
if (catalogEntry.devDependenciesToRemove) toRemove.push(...catalogEntry.devDependenciesToRemove);
|
|
1856
|
-
if (toRemove.length > 0) {
|
|
1857
|
-
console.log(`[PackageManagerAgent] Uninstalling: ${toRemove.join(" ")}`);
|
|
1858
|
-
await runCommandAsync("npm", ["uninstall", ...toRemove]);
|
|
1859
|
-
}
|
|
1860
|
-
if (catalogEntry.dependenciesToAdd && Object.keys(catalogEntry.dependenciesToAdd).length > 0) {
|
|
1861
|
-
const deps = Object.entries(catalogEntry.dependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
1862
|
-
console.log(`[PackageManagerAgent] Installing dependencies (this may take a few minutes)...`);
|
|
1863
|
-
sendEvent({
|
|
1864
|
-
type: "system.log" /* SYSTEM_LOG */,
|
|
1865
|
-
producerId: participant.getId(),
|
|
1866
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1867
|
-
payload: { planId: payload.planId, message: "Downloading and installing dependencies (this may take a few minutes)...", level: "info" }
|
|
1868
|
-
}, participant.getId());
|
|
1869
|
-
await runCommandAsync("npm", ["install", ...deps]);
|
|
1870
|
-
}
|
|
1871
|
-
if (catalogEntry.devDependenciesToAdd && Object.keys(catalogEntry.devDependenciesToAdd).length > 0) {
|
|
1872
|
-
const devDeps = Object.entries(catalogEntry.devDependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
1873
|
-
console.log(`[PackageManagerAgent] Installing devDependencies...`);
|
|
1874
|
-
await runCommandAsync("npm", ["install", "-D", ...devDeps]);
|
|
1875
|
-
}
|
|
1876
|
-
console.log(`[PackageManagerAgent] Successfully updated dependencies via npm.`);
|
|
1877
|
-
sendEvent({
|
|
1878
|
-
type: "system.log" /* SYSTEM_LOG */,
|
|
1879
|
-
producerId: participant.getId(),
|
|
1880
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1881
|
-
payload: { planId: payload.planId, message: "Dependencies installed successfully!", level: "info" }
|
|
1882
|
-
}, participant.getId());
|
|
1883
|
-
} catch (err) {
|
|
1884
|
-
console.error(`[PackageManagerAgent] Error running npm commands:`, err);
|
|
2083
|
+
build = await runInShadowWorkspace(shadowPath, "npm run build");
|
|
1885
2084
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
await runtime.state.repository.savePlan(currentPlan);
|
|
2085
|
+
const pluginCtx = { source: plan.profile.source, target: plan.profile.target };
|
|
2086
|
+
const catalogEntry = findMigrationCatalogEntry2(plan.profile.source, plan.profile.target);
|
|
2087
|
+
const structureIssues = collectShadowIssues(shadowPath, pluginCtx, catalogEntry?.layer);
|
|
2088
|
+
const buildFailed = !build.ok || !install.ok;
|
|
2089
|
+
const verifierFailed = structureIssues.length > 0;
|
|
2090
|
+
if (!buildFailed && !verifierFailed) {
|
|
2091
|
+
await completeMigration(producerId, planId, "Shadow build succeeded. Reporter is writing MIGRATION.md next. Then Apply copies the result into your project.", "info");
|
|
2092
|
+
return;
|
|
1895
2093
|
}
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2094
|
+
const implicated = parseImplicatedFiles(
|
|
2095
|
+
`${install.ok ? "" : install.output + "\n"}${build.output}`,
|
|
2096
|
+
shadowPath
|
|
2097
|
+
);
|
|
2098
|
+
const repairFiles = collectRepairTargets(shadowPath, pluginCtx, {
|
|
2099
|
+
implicated,
|
|
2100
|
+
issues: structureIssues,
|
|
2101
|
+
catalog: catalogEntry,
|
|
2102
|
+
layer: catalogEntry?.layer
|
|
2103
|
+
});
|
|
2104
|
+
const errors = [
|
|
2105
|
+
...structureIssues.flatMap((issue) => issue.errors),
|
|
2106
|
+
...install.ok ? [] : [`npm install failed:
|
|
2107
|
+
${install.output.slice(0, 1500)}`],
|
|
2108
|
+
...build.ok ? [] : [`npm run build failed:
|
|
2109
|
+
${build.output.slice(0, 2500)}`]
|
|
2110
|
+
];
|
|
2111
|
+
if (!lastRound && repairFiles.length > 0) {
|
|
2112
|
+
log2(
|
|
2113
|
+
producerId,
|
|
2114
|
+
planId,
|
|
2115
|
+
buildFailed ? "npm run build (or npm install) failed. Assigning repair tasks to Worker \u2014 the run is not finished." : `Shadow compile may succeed while the app is still wrong: ${structureIssues.length} verifier issue(s). Assigning repair tasks.`,
|
|
2116
|
+
"warning"
|
|
2117
|
+
);
|
|
2118
|
+
await requeueBrokenFiles(producerId, planId, repairFiles, errors.length > 0 ? errors : ["Shadow verification failed."]);
|
|
2119
|
+
return;
|
|
1911
2120
|
}
|
|
2121
|
+
await failMigration(
|
|
2122
|
+
producerId,
|
|
2123
|
+
planId,
|
|
2124
|
+
lastRound ? `npm run build still does not pass after ${MAX_INTEGRATION_ROUNDS} integration rounds. The migration is not successful.` : "Shadow verification failed and no repair files could be assigned."
|
|
2125
|
+
);
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
console.error(`[IntegrationAgent] Error:`, error);
|
|
2128
|
+
await failMigration(producerId, planId, "Integration Phase encountered an error. The migration is not successful.");
|
|
2129
|
+
} finally {
|
|
2130
|
+
resolveRuntime().state.integrationLocks.delete(planId);
|
|
1912
2131
|
}
|
|
1913
2132
|
}
|
|
1914
2133
|
};
|
|
1915
|
-
var
|
|
1916
|
-
specification: new
|
|
1917
|
-
processor:
|
|
2134
|
+
var manageIntegrationHandler = {
|
|
2135
|
+
specification: new WhenIntegrationStarts2(),
|
|
2136
|
+
processor: integrationProcessor
|
|
1918
2137
|
};
|
|
1919
|
-
function
|
|
1920
|
-
return
|
|
1921
|
-
name: "
|
|
1922
|
-
capabilities: ["
|
|
1923
|
-
instruction: "You are the
|
|
1924
|
-
tools
|
|
1925
|
-
handlers: [
|
|
2138
|
+
function createIntegrationAgent(tools = []) {
|
|
2139
|
+
return createAgent7({
|
|
2140
|
+
name: "IntegrationAgent",
|
|
2141
|
+
capabilities: ["integration_testing", "code_repair"],
|
|
2142
|
+
instruction: "You are the Integration Agent. You run npm install and npm run build in the shadow workspace. A passing compile is not enough if catalog verifiers fail. Never mark the migration complete unless the shadow build passes and verifiers are clean. On failure, reopen file tasks for Worker.",
|
|
2143
|
+
tools,
|
|
2144
|
+
handlers: [manageIntegrationHandler]
|
|
1926
2145
|
});
|
|
1927
2146
|
}
|
|
1928
2147
|
|
|
1929
|
-
// ../application/src/
|
|
2148
|
+
// ../application/src/migration/plugins/frontendTargets.ts
|
|
2149
|
+
import * as path8 from "path";
|
|
2150
|
+
|
|
2151
|
+
// ../application/src/utils/FrontendRuntimeHints.ts
|
|
1930
2152
|
import * as fs6 from "fs";
|
|
1931
|
-
import * as
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
isSatisfiedBy({ event }) {
|
|
1938
|
-
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
2153
|
+
import * as path7 from "path";
|
|
2154
|
+
function readIfExists(...candidates) {
|
|
2155
|
+
for (const filePath of candidates) {
|
|
2156
|
+
if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
|
|
2157
|
+
return { filePath, content: fs6.readFileSync(filePath, "utf-8") };
|
|
2158
|
+
}
|
|
1939
2159
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
function leftoverScripts(shadowRoot, forbidden, message) {
|
|
2163
|
+
const pkgPath = path7.join(shadowRoot, "package.json");
|
|
2164
|
+
if (!fs6.existsSync(pkgPath)) return [];
|
|
2165
|
+
try {
|
|
2166
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
2167
|
+
const hits = Object.entries(pkg.scripts || {}).filter(([, cmd]) => forbidden.test(cmd));
|
|
2168
|
+
if (hits.length === 0) return [];
|
|
2169
|
+
return [{
|
|
2170
|
+
filePath: pkgPath,
|
|
2171
|
+
errors: [`${message} (${hits.map(([name]) => name).join(", ")}).`]
|
|
2172
|
+
}];
|
|
2173
|
+
} catch {
|
|
2174
|
+
return [];
|
|
1944
2175
|
}
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2176
|
+
}
|
|
2177
|
+
function viteConfigUsesPlugin(shadowRoot, pluginNeedle) {
|
|
2178
|
+
const file = readIfExists(
|
|
2179
|
+
path7.join(shadowRoot, "vite.config.ts"),
|
|
2180
|
+
path7.join(shadowRoot, "vite.config.js")
|
|
2181
|
+
);
|
|
2182
|
+
if (!file) {
|
|
2183
|
+
return [{
|
|
2184
|
+
filePath: path7.join(shadowRoot, "vite.config.ts"),
|
|
2185
|
+
errors: ["Missing vite.config.ts for this Vite SPA target."]
|
|
2186
|
+
}];
|
|
2187
|
+
}
|
|
2188
|
+
const issues = [];
|
|
2189
|
+
if (!pluginNeedle.test(file.content)) {
|
|
2190
|
+
issues.push({
|
|
2191
|
+
filePath: file.filePath,
|
|
2192
|
+
errors: ["vite.config.ts does not register the plugin for the target framework."]
|
|
2193
|
+
});
|
|
1949
2194
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2195
|
+
if (/__dirname/.test(file.content) && !/import\.meta\.dirname/.test(file.content)) {
|
|
2196
|
+
issues.push({
|
|
2197
|
+
filePath: file.filePath,
|
|
2198
|
+
errors: ["vite.config.ts uses __dirname in ESM. Replace it with import.meta.dirname."]
|
|
2199
|
+
});
|
|
1954
2200
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2201
|
+
return issues;
|
|
2202
|
+
}
|
|
2203
|
+
function indexHtmlIssues(shadowRoot, mountId, scriptNeedle) {
|
|
2204
|
+
const html = readIfExists(path7.join(shadowRoot, "index.html"));
|
|
2205
|
+
if (!html) {
|
|
2206
|
+
return [{
|
|
2207
|
+
filePath: path7.join(shadowRoot, "index.html"),
|
|
2208
|
+
errors: [`Missing Vite index.html. It must contain <div id="${mountId}"> and a module script to the SPA entry.`]
|
|
2209
|
+
}];
|
|
2210
|
+
}
|
|
2211
|
+
const issues = [];
|
|
2212
|
+
if (!new RegExp(`id=["']${mountId}["']`).test(html.content)) {
|
|
2213
|
+
issues.push({
|
|
2214
|
+
filePath: html.filePath,
|
|
2215
|
+
errors: [`index.html must mount the app on #${mountId} (the entry's mount() / createApp / createRoot target).`]
|
|
2216
|
+
});
|
|
1959
2217
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2218
|
+
if (!scriptNeedle.test(html.content)) {
|
|
2219
|
+
issues.push({
|
|
2220
|
+
filePath: html.filePath,
|
|
2221
|
+
errors: ['index.html must load the SPA entry as <script type="module" src="/src/main.ts"> (or main.tsx).']
|
|
2222
|
+
});
|
|
1964
2223
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2224
|
+
return issues;
|
|
2225
|
+
}
|
|
2226
|
+
function leftoverNextApp(shadowRoot) {
|
|
2227
|
+
const files = [
|
|
2228
|
+
path7.join(shadowRoot, "src", "app", "page.tsx"),
|
|
2229
|
+
path7.join(shadowRoot, "src", "app", "layout.tsx"),
|
|
2230
|
+
path7.join(shadowRoot, "app", "page.tsx"),
|
|
2231
|
+
path7.join(shadowRoot, "app", "layout.tsx")
|
|
2232
|
+
].filter((file) => fs6.existsSync(file));
|
|
2233
|
+
return files.map((filePath) => ({
|
|
2234
|
+
filePath,
|
|
2235
|
+
errors: ["Leftover Next App Router file. Delete it after the target SPA/CLI boots so it cannot be mistaken for the new tree."]
|
|
2236
|
+
}));
|
|
2237
|
+
}
|
|
2238
|
+
function leftoverViteWhenAngular(shadowRoot) {
|
|
2239
|
+
const issues = [];
|
|
2240
|
+
for (const rel of ["vite.config.ts", "vite.config.js", "index.html"]) {
|
|
2241
|
+
const filePath = path7.join(shadowRoot, rel);
|
|
2242
|
+
if (fs6.existsSync(filePath)) {
|
|
2243
|
+
issues.push({
|
|
2244
|
+
filePath,
|
|
2245
|
+
errors: [`Leftover Vite file ${rel}. Angular boots from src/main.ts + src/index.html + angular.json \u2014 delete the Vite entry.`]
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
1973
2248
|
}
|
|
2249
|
+
return issues;
|
|
1974
2250
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2251
|
+
function isStubEntry(content) {
|
|
2252
|
+
const hasLocalImport = /from\s+['"](\.\.?\/|@\/)/.test(content);
|
|
2253
|
+
const emptyHeading = /<h1[^>]*>[^<]{0,40}<\/h1>|TODO:|coming soon/i.test(content);
|
|
2254
|
+
return emptyHeading && !hasLocalImport;
|
|
2255
|
+
}
|
|
2256
|
+
function analyzeVueTarget(shadowRoot) {
|
|
2257
|
+
const issues = [
|
|
2258
|
+
...indexHtmlIssues(shadowRoot, "app", /src\/main\.(t|j)sx?/),
|
|
2259
|
+
...viteConfigUsesPlugin(shadowRoot, /plugin-vue|@vitejs\/plugin-vue/),
|
|
2260
|
+
...leftoverScripts(shadowRoot, /\b(next|ng)\b/, "package.json still runs Next/Angular scripts; Vue target must use Vite"),
|
|
2261
|
+
...leftoverNextApp(shadowRoot)
|
|
2262
|
+
];
|
|
2263
|
+
const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"), path7.join(shadowRoot, "src", "main.js"));
|
|
2264
|
+
if (!main) {
|
|
2265
|
+
issues.push({
|
|
2266
|
+
filePath: path7.join(shadowRoot, "src", "main.ts"),
|
|
2267
|
+
errors: ['Missing src/main.ts. createApp(App).use(router).mount("#app") and import global CSS.']
|
|
2268
|
+
});
|
|
2269
|
+
} else {
|
|
2270
|
+
if (!/createApp\(/.test(main.content) || !/mount\(/.test(main.content)) {
|
|
2271
|
+
issues.push({
|
|
2272
|
+
filePath: main.filePath,
|
|
2273
|
+
errors: ['Vue entry must createApp(...).mount("#app"). Do not leave ReactDOM.createRoot here.']
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
if (/createRoot|react-dom/.test(main.content)) {
|
|
2277
|
+
issues.push({
|
|
2278
|
+
filePath: main.filePath,
|
|
2279
|
+
errors: ["Vue entry still mounts React. Replace createRoot with createApp and delete ReactDOM."]
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
if (!/import\s+['"][^'"]+\.(?:css|scss|sass)['"]/.test(main.content)) {
|
|
2283
|
+
const css = path7.join(shadowRoot, "src", "index.css");
|
|
2284
|
+
if (fs6.existsSync(css)) {
|
|
2285
|
+
issues.push({
|
|
2286
|
+
filePath: main.filePath,
|
|
2287
|
+
errors: ["Global CSS exists but is not imported from src/main.ts. Vite will ship an unstyled app."]
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (isStubEntry(main.content)) {
|
|
2292
|
+
issues.push({
|
|
2293
|
+
filePath: main.filePath,
|
|
2294
|
+
errors: ["Vue entry does not import the real App/router. Import existing screens; do not mount an empty shell."]
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return issues;
|
|
2299
|
+
}
|
|
2300
|
+
function analyzeSvelteTarget(shadowRoot) {
|
|
2301
|
+
const issues = [
|
|
2302
|
+
...indexHtmlIssues(shadowRoot, "app", /src\/main\.(t|j)sx?/),
|
|
2303
|
+
...viteConfigUsesPlugin(shadowRoot, /vite-plugin-svelte|svelte\(/),
|
|
2304
|
+
...leftoverScripts(shadowRoot, /\b(next|ng)\b/, "package.json still runs Next/Angular scripts; Svelte target must use Vite"),
|
|
2305
|
+
...leftoverNextApp(shadowRoot)
|
|
2306
|
+
];
|
|
2307
|
+
const kit = path7.join(shadowRoot, "src", "routes", "+page.svelte");
|
|
2308
|
+
if (fs6.existsSync(kit)) {
|
|
2309
|
+
issues.push({
|
|
2310
|
+
filePath: kit,
|
|
2311
|
+
errors: ["SvelteKit +page.svelte appeared but this catalog targets a Vite SPA. Use App.svelte + mount() instead of Kit file routes."]
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"), path7.join(shadowRoot, "src", "main.js"));
|
|
2315
|
+
if (!main) {
|
|
2316
|
+
issues.push({
|
|
2317
|
+
filePath: path7.join(shadowRoot, "src", "main.ts"),
|
|
2318
|
+
errors: ['Missing src/main.ts. Use mount(App, { target: document.getElementById("app") }) and import global CSS.']
|
|
2319
|
+
});
|
|
2320
|
+
} else if (!/from\s+['"]svelte['"]/.test(main.content) && !/new\s+\w+\(\s*\{\s*target/.test(main.content)) {
|
|
2321
|
+
issues.push({
|
|
2322
|
+
filePath: main.filePath,
|
|
2323
|
+
errors: ["Svelte entry must mount the root .svelte component onto #app."]
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return issues;
|
|
2327
|
+
}
|
|
2328
|
+
function analyzeAngularTarget(shadowRoot) {
|
|
2329
|
+
const issues = [
|
|
2330
|
+
...leftoverViteWhenAngular(shadowRoot),
|
|
2331
|
+
...leftoverScripts(shadowRoot, /\b(vite|next)\b/, "package.json still runs Vite/Next; Angular target must use ng serve / ng build")
|
|
2332
|
+
];
|
|
2333
|
+
const angularJson = path7.join(shadowRoot, "angular.json");
|
|
2334
|
+
if (!fs6.existsSync(angularJson)) {
|
|
2335
|
+
issues.push({
|
|
2336
|
+
filePath: angularJson,
|
|
2337
|
+
errors: ["Missing angular.json. ng build will not run. Scaffold it and point browser to src/main.ts and styles to src/styles.css."]
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
const main = readIfExists(path7.join(shadowRoot, "src", "main.ts"));
|
|
2341
|
+
if (!main || !/bootstrapApplication\(/.test(main.content || "")) {
|
|
2342
|
+
issues.push({
|
|
2343
|
+
filePath: path7.join(shadowRoot, "src", "main.ts"),
|
|
2344
|
+
errors: ["Missing bootstrapApplication in src/main.ts. Angular standalone boots here, not from a Vite main.tsx."]
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
const routes = readIfExists(path7.join(shadowRoot, "src", "app", "app.routes.ts"));
|
|
2348
|
+
if (routes && /export const routes: Routes = \[\s*\]/.test(routes.content)) {
|
|
2349
|
+
issues.push({
|
|
2350
|
+
filePath: routes.filePath,
|
|
2351
|
+
errors: ["app.routes.ts has no routes. Map each source URL to the existing feature component. App stays a <router-outlet /> shell."]
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
const html = readIfExists(path7.join(shadowRoot, "src", "index.html"));
|
|
2355
|
+
if (!html) {
|
|
2356
|
+
issues.push({
|
|
2357
|
+
filePath: path7.join(shadowRoot, "src", "index.html"),
|
|
2358
|
+
errors: ["Missing src/index.html with <app-root></app-root>."]
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
return issues;
|
|
2362
|
+
}
|
|
2363
|
+
function vueFileHint(filePath, target) {
|
|
2364
|
+
if (target !== "vue") return "";
|
|
2365
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
2366
|
+
if (/\/main\.(t|j)sx?$/.test(normalized)) {
|
|
2367
|
+
return `
|
|
2368
|
+
This is the Vue entry. It must createApp(App).use(router).mount("#app") and import global CSS.
|
|
2369
|
+
index.html uses #app, not #root. Do not leave createRoot/ReactDOM.
|
|
1989
2370
|
`;
|
|
1990
|
-
fs6.writeFileSync(reportPath, header, "utf-8");
|
|
1991
|
-
console.log(`[ReporterAgent] Initialized report at ${reportPath}`);
|
|
1992
2371
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
|
|
1998
|
-
const runtime = resolveRuntime2();
|
|
1999
|
-
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2000
|
-
if (!plan) return;
|
|
2001
|
-
const shadowDir = plan.targetPath;
|
|
2002
|
-
appendToReport(shadowDir, `- \u23F3 **Refactored**: \`${p.filePath}\` (Pending Review)`);
|
|
2372
|
+
if (/\.(t|j)sx$/.test(normalized) && !/vite\.config/.test(normalized)) {
|
|
2373
|
+
return `
|
|
2374
|
+
Convert this UI file into a .vue SFC (<script setup> + template + style). Keep prop and emit names. Then delete this leftover TSX once the SFC exists.
|
|
2375
|
+
`;
|
|
2003
2376
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2377
|
+
return "";
|
|
2378
|
+
}
|
|
2379
|
+
function svelteFileHint(filePath, target) {
|
|
2380
|
+
if (target !== "svelte") return "";
|
|
2381
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
2382
|
+
if (/\/main\.(t|j)sx?$/.test(normalized)) {
|
|
2383
|
+
return `
|
|
2384
|
+
This is the Svelte entry. mount(App, { target: document.getElementById("app") }). Do not create SvelteKit +page.svelte routes.
|
|
2385
|
+
`;
|
|
2386
|
+
}
|
|
2387
|
+
return "";
|
|
2388
|
+
}
|
|
2389
|
+
function angularFileHint(filePath, target) {
|
|
2390
|
+
if (target !== "angular") return "";
|
|
2391
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
2392
|
+
if (/app\.routes\.ts$/.test(normalized)) {
|
|
2393
|
+
return `
|
|
2394
|
+
Map each source route to an existing feature component. Do not inline that component's template. App is <router-outlet />.
|
|
2395
|
+
Keep @Input / @Output names when the host binds them.
|
|
2396
|
+
`;
|
|
2397
|
+
}
|
|
2398
|
+
if (/\/main\.(t|j)sx$/.test(normalized)) {
|
|
2399
|
+
return `
|
|
2400
|
+
Vite/React main.tsx is not the Angular entry. Angular boots from src/main.ts via bootstrapApplication. Delete this file after src/main.ts exists.
|
|
2401
|
+
`;
|
|
2402
|
+
}
|
|
2403
|
+
return "";
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// ../application/src/migration/plugins/frontendTargets.ts
|
|
2407
|
+
var vueTargetPlugin = {
|
|
2408
|
+
id: "target-vue",
|
|
2409
|
+
target: "vue",
|
|
2410
|
+
layer: "frontend",
|
|
2411
|
+
fileHint(filePath, ctx) {
|
|
2412
|
+
return vueFileHint(filePath, ctx.target);
|
|
2413
|
+
},
|
|
2414
|
+
verifyShadow(shadowRoot) {
|
|
2415
|
+
return analyzeVueTarget(shadowRoot);
|
|
2416
|
+
},
|
|
2417
|
+
repairTargets(shadowRoot) {
|
|
2418
|
+
return [
|
|
2419
|
+
path8.join(shadowRoot, "package.json"),
|
|
2420
|
+
path8.join(shadowRoot, "vite.config.ts"),
|
|
2421
|
+
path8.join(shadowRoot, "index.html"),
|
|
2422
|
+
path8.join(shadowRoot, "src", "main.ts"),
|
|
2423
|
+
path8.join(shadowRoot, "src", "App.vue")
|
|
2424
|
+
];
|
|
2013
2425
|
}
|
|
2014
2426
|
};
|
|
2015
|
-
var
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2427
|
+
var svelteTargetPlugin = {
|
|
2428
|
+
id: "target-svelte",
|
|
2429
|
+
target: "svelte",
|
|
2430
|
+
layer: "frontend",
|
|
2431
|
+
fileHint(filePath, ctx) {
|
|
2432
|
+
return svelteFileHint(filePath, ctx.target);
|
|
2433
|
+
},
|
|
2434
|
+
verifyShadow(shadowRoot) {
|
|
2435
|
+
return analyzeSvelteTarget(shadowRoot);
|
|
2436
|
+
},
|
|
2437
|
+
repairTargets(shadowRoot) {
|
|
2438
|
+
return [
|
|
2439
|
+
path8.join(shadowRoot, "package.json"),
|
|
2440
|
+
path8.join(shadowRoot, "vite.config.ts"),
|
|
2441
|
+
path8.join(shadowRoot, "svelte.config.js"),
|
|
2442
|
+
path8.join(shadowRoot, "index.html"),
|
|
2443
|
+
path8.join(shadowRoot, "src", "main.ts"),
|
|
2444
|
+
path8.join(shadowRoot, "src", "App.svelte")
|
|
2445
|
+
];
|
|
2024
2446
|
}
|
|
2025
2447
|
};
|
|
2026
|
-
var
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2448
|
+
var angularTargetPlugin = {
|
|
2449
|
+
id: "target-angular",
|
|
2450
|
+
target: "angular",
|
|
2451
|
+
layer: "frontend",
|
|
2452
|
+
fileHint(filePath, ctx) {
|
|
2453
|
+
return angularFileHint(filePath, ctx.target);
|
|
2454
|
+
},
|
|
2455
|
+
verifyShadow(shadowRoot) {
|
|
2456
|
+
return analyzeAngularTarget(shadowRoot);
|
|
2457
|
+
},
|
|
2458
|
+
repairTargets(shadowRoot) {
|
|
2459
|
+
return [
|
|
2460
|
+
path8.join(shadowRoot, "package.json"),
|
|
2461
|
+
path8.join(shadowRoot, "angular.json"),
|
|
2462
|
+
path8.join(shadowRoot, "src", "main.ts"),
|
|
2463
|
+
path8.join(shadowRoot, "src", "index.html"),
|
|
2464
|
+
path8.join(shadowRoot, "src", "styles.css"),
|
|
2465
|
+
path8.join(shadowRoot, "src", "app", "app.ts"),
|
|
2466
|
+
path8.join(shadowRoot, "src", "app", "app.routes.ts"),
|
|
2467
|
+
path8.join(shadowRoot, "src", "app", "app.config.ts")
|
|
2468
|
+
];
|
|
2035
2469
|
}
|
|
2036
2470
|
};
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
if (
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2471
|
+
|
|
2472
|
+
// ../application/src/migration/plugins/nextToReact.ts
|
|
2473
|
+
import * as fs7 from "fs";
|
|
2474
|
+
import * as path9 from "path";
|
|
2475
|
+
function readIfExists2(...candidates) {
|
|
2476
|
+
for (const filePath of candidates) {
|
|
2477
|
+
if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
|
|
2478
|
+
return { filePath, content: fs7.readFileSync(filePath, "utf-8") };
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
return null;
|
|
2482
|
+
}
|
|
2483
|
+
function listCssFiles(dir) {
|
|
2484
|
+
if (!fs7.existsSync(dir)) return [];
|
|
2485
|
+
return fs7.readdirSync(dir).filter((name) => /\.(css|scss|sass)$/i.test(name)).map((name) => path9.join(dir, name));
|
|
2486
|
+
}
|
|
2487
|
+
function hasLocalComponentImport(content) {
|
|
2488
|
+
return /import\s+(?:type\s+)?(?:[\w*{}\s,]+)\s+from\s+['"](\.\.?\/|@\/)(?!.*\.(?:css|scss|sass)['"])/i.test(content) || /import\s+['"](\.\.?\/|@\/)[^'"]+\.(t|j)sx['"]/.test(content);
|
|
2489
|
+
}
|
|
2490
|
+
function isStubSpaEntry(content) {
|
|
2491
|
+
const emptyMount = /<div\s+id=["']app["']\s*\/>|<div\s*\/>\s*[;)]|return\s+null\s*;?|return\s*\(\s*<>\s*<\/>\s*\)/i.test(content);
|
|
2492
|
+
const hasRouter = /BrowserRouter|RouterProvider|createBrowserRouter|HashRouter/i.test(content);
|
|
2493
|
+
if (hasRouter && hasLocalComponentImport(content)) return false;
|
|
2494
|
+
if (emptyMount) return true;
|
|
2495
|
+
if (!hasLocalComponentImport(content) && /createRoot\(/.test(content)) return true;
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
function listTsxFiles(dir) {
|
|
2499
|
+
if (!fs7.existsSync(dir)) return [];
|
|
2500
|
+
const out = [];
|
|
2501
|
+
const stack = [dir];
|
|
2502
|
+
while (stack.length > 0) {
|
|
2503
|
+
const current = stack.pop();
|
|
2504
|
+
for (const entry of fs7.readdirSync(current, { withFileTypes: true })) {
|
|
2505
|
+
const full = path9.join(current, entry.name);
|
|
2506
|
+
if (entry.isDirectory()) stack.push(full);
|
|
2507
|
+
else if (/\.(t|j)sx$/.test(entry.name) && !entry.name.endsWith(".obsolete")) out.push(full);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return out;
|
|
2511
|
+
}
|
|
2512
|
+
function leftoverNextAppFiles(shadowRoot) {
|
|
2513
|
+
const dirs = [path9.join(shadowRoot, "src", "app"), path9.join(shadowRoot, "app")];
|
|
2514
|
+
const found = [];
|
|
2515
|
+
for (const dir of dirs) {
|
|
2516
|
+
for (const file of listTsxFiles(dir)) {
|
|
2517
|
+
const base = path9.basename(file);
|
|
2518
|
+
if (/^(page|layout|template|loading|error|not-found)\.(t|j)sx$/.test(base)) {
|
|
2519
|
+
found.push(file);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return found;
|
|
2524
|
+
}
|
|
2525
|
+
function findUnmountedScreens(shadowRoot, entryContent) {
|
|
2526
|
+
const candidates = [
|
|
2527
|
+
path9.join(shadowRoot, "src", "App.tsx"),
|
|
2528
|
+
path9.join(shadowRoot, "src", "App.jsx"),
|
|
2529
|
+
path9.join(shadowRoot, "src", "routes", "AppRoutes.tsx"),
|
|
2530
|
+
path9.join(shadowRoot, "src", "routes", "AppRoutes.jsx")
|
|
2531
|
+
];
|
|
2532
|
+
const unused = [];
|
|
2533
|
+
for (const file of candidates) {
|
|
2534
|
+
if (!fs7.existsSync(file)) continue;
|
|
2535
|
+
const stem = path9.basename(file).replace(/\.(t|j)sx$/, "");
|
|
2536
|
+
if (!entryContent.includes(stem)) unused.push(file);
|
|
2537
|
+
}
|
|
2538
|
+
return unused;
|
|
2539
|
+
}
|
|
2540
|
+
function analyzeNextToReactBootstrap(shadowRoot) {
|
|
2541
|
+
const issues = [];
|
|
2542
|
+
const main = readIfExists2(
|
|
2543
|
+
path9.join(shadowRoot, "src", "main.tsx"),
|
|
2544
|
+
path9.join(shadowRoot, "src", "main.jsx"),
|
|
2545
|
+
path9.join(shadowRoot, "src", "index.tsx")
|
|
2546
|
+
);
|
|
2547
|
+
if (!main) {
|
|
2548
|
+
issues.push({
|
|
2549
|
+
filePath: path9.join(shadowRoot, "src", "main.tsx"),
|
|
2550
|
+
errors: [
|
|
2551
|
+
"Missing SPA entry. Create src/main.tsx that mounts BrowserRouter and the former home screen into #root. Vite can start with a blank page if this file only renders an empty div."
|
|
2552
|
+
]
|
|
2553
|
+
});
|
|
2554
|
+
return issues;
|
|
2555
|
+
}
|
|
2556
|
+
if (isStubSpaEntry(main.content)) {
|
|
2557
|
+
issues.push({
|
|
2558
|
+
filePath: main.filePath,
|
|
2559
|
+
errors: [
|
|
2560
|
+
"src/main.tsx does not mount the application UI. Import the router and the former home screen. Do not render only an empty element, null, or a placeholder \u2014 that is a blank page even when npm run dev succeeds."
|
|
2561
|
+
]
|
|
2562
|
+
});
|
|
2563
|
+
} else if (!/BrowserRouter|RouterProvider|createBrowserRouter|HashRouter/i.test(main.content)) {
|
|
2564
|
+
issues.push({
|
|
2565
|
+
filePath: main.filePath,
|
|
2566
|
+
errors: [
|
|
2567
|
+
"SPA entry has no React Router. Wrap the tree in <BrowserRouter> (or RouterProvider) so former Next routes still have URLs."
|
|
2568
|
+
]
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
const unused = findUnmountedScreens(shadowRoot, main.content);
|
|
2572
|
+
for (const file of unused) {
|
|
2573
|
+
issues.push({
|
|
2574
|
+
filePath: file,
|
|
2575
|
+
errors: [
|
|
2576
|
+
`${path9.relative(shadowRoot, file)} exists but is not imported from the SPA entry. Wire it from src/main.tsx or it will never render.`
|
|
2577
|
+
]
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
for (const file of leftoverNextAppFiles(shadowRoot)) {
|
|
2581
|
+
issues.push({
|
|
2582
|
+
filePath: file,
|
|
2583
|
+
errors: [
|
|
2584
|
+
"Leftover Next App Router file. After the Vite SPA boots, delete src/app/page.tsx and layout.tsx (or they confuse the next run)."
|
|
2585
|
+
]
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
const pkgPath = path9.join(shadowRoot, "package.json");
|
|
2589
|
+
if (fs7.existsSync(pkgPath)) {
|
|
2590
|
+
try {
|
|
2591
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2592
|
+
const nextScripts = Object.entries(pkg.scripts || {}).filter(([, cmd]) => /\bnext\b/.test(cmd));
|
|
2593
|
+
if (nextScripts.length > 0) {
|
|
2594
|
+
issues.push({
|
|
2595
|
+
filePath: pkgPath,
|
|
2596
|
+
errors: [
|
|
2597
|
+
`package.json still runs Next in scripts (${nextScripts.map(([name]) => name).join(", ")}). Replace them with Vite/eslint equivalents (dev/build/preview/start/lint).`
|
|
2598
|
+
]
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
} catch {
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return issues;
|
|
2605
|
+
}
|
|
2606
|
+
function analyzeNextToReactStyles(shadowRoot) {
|
|
2607
|
+
const issues = [];
|
|
2608
|
+
const main = readIfExists2(
|
|
2609
|
+
path9.join(shadowRoot, "src", "main.tsx"),
|
|
2610
|
+
path9.join(shadowRoot, "src", "main.jsx"),
|
|
2611
|
+
path9.join(shadowRoot, "src", "main.ts"),
|
|
2612
|
+
path9.join(shadowRoot, "src", "index.tsx")
|
|
2613
|
+
);
|
|
2614
|
+
const layout = readIfExists2(
|
|
2615
|
+
path9.join(shadowRoot, "src", "app", "layout.tsx"),
|
|
2616
|
+
path9.join(shadowRoot, "src", "app", "layout.jsx"),
|
|
2617
|
+
path9.join(shadowRoot, "app", "layout.tsx"),
|
|
2618
|
+
path9.join(shadowRoot, "app", "layout.jsx")
|
|
2619
|
+
);
|
|
2620
|
+
const cssFromLayout = [];
|
|
2621
|
+
if (layout) {
|
|
2622
|
+
const importRe = /import\s+['"]([^'"]+\.(?:css|scss|sass))['"]/g;
|
|
2623
|
+
let match;
|
|
2624
|
+
while ((match = importRe.exec(layout.content)) !== null) {
|
|
2625
|
+
cssFromLayout.push(match[1]);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
const srcCss = listCssFiles(path9.join(shadowRoot, "src"));
|
|
2629
|
+
const hasGlobalCss = srcCss.length > 0 || cssFromLayout.length > 0;
|
|
2630
|
+
if (!main) {
|
|
2631
|
+
if (hasGlobalCss) {
|
|
2632
|
+
issues.push({
|
|
2633
|
+
filePath: path9.join(shadowRoot, "src", "main.tsx"),
|
|
2634
|
+
errors: [
|
|
2635
|
+
"Missing Vite/React entry (src/main.tsx). Create it so it mounts the app into #root and imports the global CSS that Next layout used to load."
|
|
2636
|
+
]
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
return issues;
|
|
2640
|
+
}
|
|
2641
|
+
const entryImportsCss = /import\s+['"][^'"]+\.(?:css|scss|sass)['"]/.test(main.content);
|
|
2642
|
+
if (hasGlobalCss && !entryImportsCss) {
|
|
2643
|
+
const hint = cssFromLayout[0] || (srcCss[0] ? `./${path9.basename(srcCss[0])}` : "./index.css");
|
|
2644
|
+
issues.push({
|
|
2645
|
+
filePath: main.filePath,
|
|
2646
|
+
errors: [
|
|
2647
|
+
`Global CSS is not imported from the SPA entry. Next.js applied styles via layout; Vite only bundles CSS imported from JS. Add \`import '${hint}'\` (or the path layout used) to ${path9.basename(main.filePath)}.`
|
|
2648
|
+
]
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
const html = readIfExists2(path9.join(shadowRoot, "index.html"));
|
|
2652
|
+
if (html && !/src\/main\.(t|j)sx?/.test(html.content) && !/\/src\/index\.(t|j)sx?/.test(html.content)) {
|
|
2653
|
+
issues.push({
|
|
2654
|
+
filePath: html.filePath,
|
|
2655
|
+
errors: [
|
|
2656
|
+
'index.html must load the SPA entry as a module script (typically <script type="module" src="/src/main.tsx">).'
|
|
2657
|
+
]
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
const viteConfig = readIfExists2(
|
|
2661
|
+
path9.join(shadowRoot, "vite.config.ts"),
|
|
2662
|
+
path9.join(shadowRoot, "vite.config.js")
|
|
2663
|
+
);
|
|
2664
|
+
if (viteConfig && /__dirname/.test(viteConfig.content) && !/import\.meta\.dirname/.test(viteConfig.content)) {
|
|
2665
|
+
issues.push({
|
|
2666
|
+
filePath: viteConfig.filePath,
|
|
2667
|
+
errors: [
|
|
2668
|
+
"vite.config.ts uses __dirname in ESM. Replace it with import.meta.dirname so aliases and CSS resolution work under Vite."
|
|
2669
|
+
]
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
return issues;
|
|
2673
|
+
}
|
|
2674
|
+
var nextToReactPlugin = {
|
|
2675
|
+
id: "next-to-react",
|
|
2676
|
+
source: "next",
|
|
2677
|
+
target: "react",
|
|
2678
|
+
layer: "frontend",
|
|
2679
|
+
fileHint(filePath) {
|
|
2680
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
2681
|
+
if (/\/(main|index)\.(t|j)sx?$/i.test(normalized) && !/\/src\/app\//i.test(normalized)) {
|
|
2682
|
+
return `
|
|
2683
|
+
This is the Vite/React entry. It must:
|
|
2684
|
+
1. import the global CSS Next loaded from layout (often ./index.css)
|
|
2685
|
+
2. wrap the tree in BrowserRouter
|
|
2686
|
+
3. render the former home screen (whatever the source used as "/")
|
|
2687
|
+
Never render only an empty mount node \u2014 Vite will show a blank page and still report ready.
|
|
2688
|
+
`;
|
|
2689
|
+
}
|
|
2690
|
+
if (/layout\.(t|j)sx?$/i.test(normalized) && /\/(src\/)?app\//i.test(normalized)) {
|
|
2691
|
+
return `
|
|
2692
|
+
This Next layout will not run in Vite. Move its global CSS import to src/main.tsx and its HTML shell into index.html. Then delete this layout after the SPA boots.
|
|
2052
2693
|
`;
|
|
2053
|
-
|
|
2694
|
+
}
|
|
2695
|
+
return "";
|
|
2696
|
+
},
|
|
2697
|
+
verifyShadow(shadowRoot) {
|
|
2698
|
+
return [...analyzeNextToReactStyles(shadowRoot), ...analyzeNextToReactBootstrap(shadowRoot)];
|
|
2699
|
+
},
|
|
2700
|
+
repairTargets(shadowRoot) {
|
|
2701
|
+
return [
|
|
2702
|
+
path9.join(shadowRoot, "package.json"),
|
|
2703
|
+
path9.join(shadowRoot, "vite.config.ts"),
|
|
2704
|
+
path9.join(shadowRoot, "vite.config.js"),
|
|
2705
|
+
path9.join(shadowRoot, "index.html"),
|
|
2706
|
+
path9.join(shadowRoot, "src", "main.tsx"),
|
|
2707
|
+
path9.join(shadowRoot, "src", "main.jsx"),
|
|
2708
|
+
path9.join(shadowRoot, "src", "App.tsx"),
|
|
2709
|
+
path9.join(shadowRoot, "src", "App.jsx")
|
|
2710
|
+
];
|
|
2054
2711
|
}
|
|
2055
2712
|
};
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2713
|
+
|
|
2714
|
+
// ../application/src/migration/plugins/reactToNext.ts
|
|
2715
|
+
import * as path10 from "path";
|
|
2716
|
+
var reactToNextPlugin = {
|
|
2717
|
+
id: "react-to-next",
|
|
2718
|
+
source: "react",
|
|
2719
|
+
target: "next",
|
|
2720
|
+
layer: "frontend",
|
|
2721
|
+
fileHint(filePath, ctx) {
|
|
2722
|
+
return nextMigrationFileHint(filePath, ctx.target);
|
|
2723
|
+
},
|
|
2724
|
+
verifyShadow(shadowRoot) {
|
|
2725
|
+
return analyzeReactToNextStructure(shadowRoot);
|
|
2726
|
+
},
|
|
2727
|
+
repairTargets(shadowRoot) {
|
|
2728
|
+
return [
|
|
2729
|
+
path10.join(shadowRoot, "package.json"),
|
|
2730
|
+
path10.join(shadowRoot, "next.config.ts"),
|
|
2731
|
+
path10.join(shadowRoot, "next.config.js"),
|
|
2732
|
+
path10.join(shadowRoot, "next.config.mjs"),
|
|
2733
|
+
path10.join(shadowRoot, "src", "app", "page.tsx"),
|
|
2734
|
+
path10.join(shadowRoot, "src", "app", "layout.tsx"),
|
|
2735
|
+
path10.join(shadowRoot, "app", "page.tsx"),
|
|
2736
|
+
path10.join(shadowRoot, "app", "layout.tsx"),
|
|
2737
|
+
...listAppRouterFiles(shadowRoot)
|
|
2738
|
+
];
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
|
|
2742
|
+
// ../application/src/migration/plugins/index.ts
|
|
2743
|
+
function registerBuiltinMigrationPlugins() {
|
|
2744
|
+
registerMigrationPlugin(reactToNextPlugin);
|
|
2745
|
+
registerMigrationPlugin(nextToReactPlugin);
|
|
2746
|
+
registerMigrationPlugin(vueTargetPlugin);
|
|
2747
|
+
registerMigrationPlugin(svelteTargetPlugin);
|
|
2748
|
+
registerMigrationPlugin(angularTargetPlugin);
|
|
2071
2749
|
}
|
|
2072
2750
|
|
|
2073
2751
|
// ../application/src/index.ts
|
|
2074
|
-
import { createAgent as
|
|
2752
|
+
import { createAgent as createAgent8, createHuman, SituationSpecification as SituationSpecification8 } from "@mozaik-ai/core";
|
|
2075
2753
|
|
|
2076
2754
|
// ../application/src/MigrationRunner.ts
|
|
2077
2755
|
import { randomUUID } from "crypto";
|
|
@@ -2119,13 +2797,14 @@ var MigrationRunner = class {
|
|
|
2119
2797
|
createdAt: /* @__PURE__ */ new Date()
|
|
2120
2798
|
};
|
|
2121
2799
|
await this.store.savePlan(plan);
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2800
|
+
const dispatcherId = resolveRuntime().state.dispatcherId;
|
|
2801
|
+
if (!dispatcherId) {
|
|
2802
|
+
throw new Error("Mozaik System participant is not joined.");
|
|
2803
|
+
}
|
|
2125
2804
|
sendEvent(
|
|
2126
2805
|
{
|
|
2127
2806
|
type: "migration.started" /* MIGRATION_STARTED */,
|
|
2128
|
-
producerId:
|
|
2807
|
+
producerId: dispatcherId,
|
|
2129
2808
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
2130
2809
|
payload: {
|
|
2131
2810
|
planId,
|
|
@@ -2133,7 +2812,7 @@ var MigrationRunner = class {
|
|
|
2133
2812
|
shadowWorkspacePath: shadowPath
|
|
2134
2813
|
}
|
|
2135
2814
|
},
|
|
2136
|
-
|
|
2815
|
+
dispatcherId
|
|
2137
2816
|
);
|
|
2138
2817
|
return { planId, shadowPath, runId };
|
|
2139
2818
|
}
|
|
@@ -2149,15 +2828,19 @@ var MigrationRunner = class {
|
|
|
2149
2828
|
|
|
2150
2829
|
// ../application/src/index.ts
|
|
2151
2830
|
function bootstrapMetamorph(repository, tools = []) {
|
|
2831
|
+
registerBuiltinMigrationPlugins();
|
|
2152
2832
|
initializeRuntime({
|
|
2153
2833
|
state: new MetamorphState(repository)
|
|
2154
2834
|
});
|
|
2155
|
-
|
|
2835
|
+
const dispatcher = createHuman({ name: "System", capabilities: [], handlers: [] });
|
|
2836
|
+
join(dispatcher);
|
|
2837
|
+
resolveRuntime().state.dispatcherId = dispatcher.getId();
|
|
2838
|
+
class AllEventsSpecification extends SituationSpecification8 {
|
|
2156
2839
|
isSatisfiedBy() {
|
|
2157
2840
|
return true;
|
|
2158
2841
|
}
|
|
2159
2842
|
}
|
|
2160
|
-
const loggerAgent =
|
|
2843
|
+
const loggerAgent = createAgent8({
|
|
2161
2844
|
name: "TelemetryLogger",
|
|
2162
2845
|
capabilities: [],
|
|
2163
2846
|
instruction: "You silently log everything.",
|
|
@@ -2167,10 +2850,10 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2167
2850
|
specification: new AllEventsSpecification(),
|
|
2168
2851
|
processor: {
|
|
2169
2852
|
async apply({ event }) {
|
|
2170
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-
|
|
2853
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
2171
2854
|
const runtime = resolveRuntime2();
|
|
2172
2855
|
const repository2 = runtime.state.repository;
|
|
2173
|
-
if (event.type.includes("migration") || event.type.includes("file")) {
|
|
2856
|
+
if (event.type.includes("migration") || event.type.includes("file") || event.type.includes("phase") || event.type.includes("system")) {
|
|
2174
2857
|
await repository2.logEvent(event.type, {
|
|
2175
2858
|
...event.payload,
|
|
2176
2859
|
producerId: event.producerId
|
|
@@ -2186,13 +2869,18 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2186
2869
|
const worker = createWorkerAgent(tools);
|
|
2187
2870
|
const reviewer = createReviewerAgent(tools);
|
|
2188
2871
|
const packageManager = createPackageManagerAgent();
|
|
2189
|
-
const
|
|
2872
|
+
const coordinator = createCoordinatorAgent();
|
|
2873
|
+
const reporterTools = tools.filter((tool) => tool.name === "read_file" || tool.name === "write_file" || tool.name === "list_directory");
|
|
2874
|
+
const reporter = createReporterAgent(reporterTools);
|
|
2875
|
+
const integrationAgent = createIntegrationAgent(tools);
|
|
2190
2876
|
join(mapper);
|
|
2191
2877
|
join(worker);
|
|
2192
2878
|
join(reviewer);
|
|
2193
2879
|
join(packageManager);
|
|
2880
|
+
join(coordinator);
|
|
2194
2881
|
join(reporter);
|
|
2195
|
-
|
|
2882
|
+
join(integrationAgent);
|
|
2883
|
+
console.log(`[App] Mozaik initialized. Agents joined: Mapper, Worker, Reviewer, PackageManager, Coordinator, Reporter, IntegrationAgent`);
|
|
2196
2884
|
return {
|
|
2197
2885
|
mapperId: mapper.getId(),
|
|
2198
2886
|
workerId: worker.getId(),
|
|
@@ -2212,9 +2900,11 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2212
2900
|
express: ["fastify", "nestjs"],
|
|
2213
2901
|
fastify: ["express", "nestjs"],
|
|
2214
2902
|
nestjs: ["express", "fastify"],
|
|
2215
|
-
react: ["next", "vue"],
|
|
2216
|
-
next: ["react"],
|
|
2217
|
-
vue: ["react"]
|
|
2903
|
+
react: ["next", "vue", "angular", "svelte"],
|
|
2904
|
+
next: ["react", "vue", "angular", "svelte"],
|
|
2905
|
+
vue: ["react", "next", "angular", "svelte"],
|
|
2906
|
+
angular: ["react", "next", "vue", "svelte"],
|
|
2907
|
+
svelte: ["react", "next", "vue", "angular"]
|
|
2218
2908
|
};
|
|
2219
2909
|
if (!from) {
|
|
2220
2910
|
console.log(chalk.gray(`
|
|
@@ -2265,12 +2955,16 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2265
2955
|
const spinner = ora("Initializing...").start();
|
|
2266
2956
|
try {
|
|
2267
2957
|
const store = new SQLiteStateStore(".metamorph");
|
|
2958
|
+
const shadowBase = path11.resolve(".metamorph/shadow");
|
|
2268
2959
|
const tools = [
|
|
2269
|
-
createReadFileTool(),
|
|
2270
|
-
createWriteFileTool(),
|
|
2271
|
-
createRenameFileTool(),
|
|
2272
|
-
createCreateFileTool(),
|
|
2273
|
-
createDeleteFileTool()
|
|
2960
|
+
createReadFileTool(shadowBase),
|
|
2961
|
+
createWriteFileTool(shadowBase),
|
|
2962
|
+
createRenameFileTool(shadowBase),
|
|
2963
|
+
createCreateFileTool(shadowBase),
|
|
2964
|
+
createDeleteFileTool(shadowBase),
|
|
2965
|
+
createListDirectoryTool(shadowBase),
|
|
2966
|
+
createCheckProjectDiagnosticsTool(shadowBase),
|
|
2967
|
+
createRunBuildTool(shadowBase)
|
|
2274
2968
|
];
|
|
2275
2969
|
const runner = new MigrationRunner(store, tools);
|
|
2276
2970
|
const result = await runner.startMigration({
|
|
@@ -2282,7 +2976,7 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2282
2976
|
console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
|
|
2283
2977
|
console.log(chalk.blue(`
|
|
2284
2978
|
\u23F3 Agents are now working...`));
|
|
2285
|
-
await new Promise((
|
|
2979
|
+
await new Promise((resolve5) => setTimeout(resolve5, 2e4));
|
|
2286
2980
|
console.log(chalk.green(`
|
|
2287
2981
|
\u2705 Migration simulation finished.`));
|
|
2288
2982
|
process.exit(0);
|
|
@@ -2292,11 +2986,11 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2292
2986
|
process.exit(1);
|
|
2293
2987
|
}
|
|
2294
2988
|
});
|
|
2295
|
-
program.command("apply <runId> <targetPath>").description("Apply a completed migration to the target project").action(async (runId, targetPath) => {
|
|
2989
|
+
program.command("apply <runId> <targetPath>").description("Apply a completed migration to the target project (requires Git at the project or monorepo root)").action(async (runId, targetPath) => {
|
|
2296
2990
|
try {
|
|
2297
2991
|
const integrator = new MigrationIntegrator(new ShadowWorkspace());
|
|
2298
|
-
const
|
|
2299
|
-
console.log(chalk.green(`\u2705 ${message}`));
|
|
2992
|
+
const result = await integrator.applyMigration(runId, targetPath);
|
|
2993
|
+
console.log(chalk.green(`\u2705 ${result.message}`));
|
|
2300
2994
|
} catch (error) {
|
|
2301
2995
|
console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
2302
2996
|
process.exit(1);
|
|
@@ -2380,12 +3074,16 @@ program.command("ui").description("Start the Metamorph Dashboard API server").op
|
|
|
2380
3074
|
process.exit(1);
|
|
2381
3075
|
}
|
|
2382
3076
|
const store = new SQLiteStateStore(".metamorph");
|
|
3077
|
+
const shadowBase = path11.resolve(".metamorph/shadow");
|
|
2383
3078
|
const tools = [
|
|
2384
|
-
createReadFileTool(),
|
|
2385
|
-
createWriteFileTool(),
|
|
2386
|
-
createRenameFileTool(),
|
|
2387
|
-
createCreateFileTool(),
|
|
2388
|
-
createDeleteFileTool()
|
|
3079
|
+
createReadFileTool(shadowBase),
|
|
3080
|
+
createWriteFileTool(shadowBase),
|
|
3081
|
+
createRenameFileTool(shadowBase),
|
|
3082
|
+
createCreateFileTool(shadowBase),
|
|
3083
|
+
createDeleteFileTool(shadowBase),
|
|
3084
|
+
createListDirectoryTool(shadowBase),
|
|
3085
|
+
createCheckProjectDiagnosticsTool(shadowBase),
|
|
3086
|
+
createRunBuildTool(shadowBase)
|
|
2389
3087
|
];
|
|
2390
3088
|
const runner = new MigrationRunner(store, tools);
|
|
2391
3089
|
const app = await createApiServer(store, runner);
|