@nikelyh/metamorph 2.0.10 → 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 -1545
- 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,1233 +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
|
-
|
|
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
|
+
});
|
|
427
459
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
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 {
|
|
445
476
|
}
|
|
446
477
|
}
|
|
447
|
-
|
|
448
|
-
|
|
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;
|
|
449
495
|
}
|
|
450
|
-
|
|
451
|
-
|
|
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;
|
|
452
541
|
try {
|
|
453
|
-
|
|
454
|
-
} catch
|
|
455
|
-
|
|
456
|
-
throw new Error(`Failed to create/reset git branch ${branchName}: ${e.message}`);
|
|
457
|
-
}
|
|
458
|
-
throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
|
|
542
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
543
|
+
} catch {
|
|
544
|
+
continue;
|
|
459
545
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
throw new Error(`Failed to commit changes to branch ${branchName}: ${e.message}`);
|
|
467
|
-
}
|
|
468
|
-
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;
|
|
469
552
|
}
|
|
470
|
-
return `Successfully applied migration via Git to branch: ${branchName}`;
|
|
471
553
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const itemDest = path.join(targetPath, item);
|
|
495
|
-
fs2.copySync(itemSrc, itemDest, { overwrite: true });
|
|
496
|
-
}
|
|
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
|
+
`);
|
|
497
576
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
architecturalRules: [
|
|
512
|
-
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
513
|
-
'Remove all NestJS core imports (e.g., from "@nestjs/common" or "@nestjs/core").',
|
|
514
|
-
"Controllers must be converted into Express Router factory functions or classes that return an Express Router.",
|
|
515
|
-
"Modules must be converted to plain dependency composition functions or startup scripts that wire Routers together.",
|
|
516
|
-
"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.",
|
|
517
|
-
"Make sure to export the router or the configured Express application, not a Nest module."
|
|
518
|
-
],
|
|
519
|
-
examples: [
|
|
520
|
-
{
|
|
521
|
-
description: "Converting a NestJS Controller to an Express Router",
|
|
522
|
-
before: `import { Controller, Get } from '@nestjs/common';
|
|
523
|
-
import { AppService } from './app.service';
|
|
524
|
-
|
|
525
|
-
@Controller()
|
|
526
|
-
export class AppController {
|
|
527
|
-
constructor(private readonly appService: AppService) {}
|
|
528
|
-
|
|
529
|
-
@Get()
|
|
530
|
-
getHello(): string {
|
|
531
|
-
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
|
+
`);
|
|
532
590
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const router = Router();
|
|
539
|
-
|
|
540
|
-
router.get('/', (req, res) => {
|
|
541
|
-
const result = appService.getHello();
|
|
542
|
-
res.send(result);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
return router;
|
|
546
|
-
}`
|
|
547
|
-
}
|
|
548
|
-
]
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
source: "express",
|
|
552
|
-
target: "nestjs",
|
|
553
|
-
description: "Migration from a plain Express application to the NestJS framework.",
|
|
554
|
-
dependenciesToRemove: ["express"],
|
|
555
|
-
dependenciesToAdd: {
|
|
556
|
-
"@nestjs/common": "latest",
|
|
557
|
-
"@nestjs/core": "latest",
|
|
558
|
-
"@nestjs/platform-express": "latest",
|
|
559
|
-
"reflect-metadata": "^0.1.13",
|
|
560
|
-
"rxjs": "^7.8.1"
|
|
561
|
-
},
|
|
562
|
-
devDependenciesToRemove: ["@types/express"],
|
|
563
|
-
devDependenciesToAdd: {
|
|
564
|
-
"@nestjs/cli": "latest",
|
|
565
|
-
"@nestjs/schematics": "latest",
|
|
566
|
-
"@nestjs/testing": "latest",
|
|
567
|
-
"@types/supertest": "^2.0.12",
|
|
568
|
-
"source-map-support": "^0.5.21",
|
|
569
|
-
"supertest": "^6.3.3"
|
|
570
|
-
},
|
|
571
|
-
architecturalRules: [
|
|
572
|
-
"Express routing logic must be encapsulated in classes decorated with @Controller().",
|
|
573
|
-
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
574
|
-
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
575
|
-
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of Express route definitions.",
|
|
576
|
-
"Rely on NestJS dependency injection instead of manual class instantiation or function passing.",
|
|
577
|
-
"The entry point must use NestFactory.create() to bootstrap the application, replacing app.listen().",
|
|
578
|
-
"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)."
|
|
579
|
-
],
|
|
580
|
-
examples: [
|
|
581
|
-
{
|
|
582
|
-
description: "Converting an Express Router to a NestJS Controller and Service",
|
|
583
|
-
before: `import { Router } from 'express';
|
|
584
|
-
import { getHelloLogic } from './logic';
|
|
585
|
-
|
|
586
|
-
export function createAppRouter(): Router {
|
|
587
|
-
const router = Router();
|
|
588
|
-
router.get('/', (req, res) => {
|
|
589
|
-
res.send(getHelloLogic());
|
|
590
|
-
});
|
|
591
|
-
return router;
|
|
592
|
-
}`,
|
|
593
|
-
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
594
|
-
|
|
595
|
-
@Injectable()
|
|
596
|
-
export class AppService {
|
|
597
|
-
getHello(): string {
|
|
598
|
-
return 'Hello from logic';
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
@Controller()
|
|
603
|
-
export class AppController {
|
|
604
|
-
constructor(private readonly appService: AppService) {}
|
|
605
|
-
|
|
606
|
-
@Get()
|
|
607
|
-
getHello(): string {
|
|
608
|
-
return this.appService.getHello();
|
|
609
|
-
}
|
|
610
|
-
}`
|
|
611
|
-
}
|
|
612
|
-
]
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
source: "nestjs",
|
|
616
|
-
target: "fastify",
|
|
617
|
-
description: "Migration from NestJS framework to a Fastify application structure.",
|
|
618
|
-
filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
|
|
619
|
-
dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
|
|
620
|
-
dependenciesToAdd: { "fastify": "latest" },
|
|
621
|
-
devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
|
|
622
|
-
devDependenciesToAdd: { "fastify-plugin": "latest" },
|
|
623
|
-
architecturalRules: [
|
|
624
|
-
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
625
|
-
"Remove all NestJS core imports.",
|
|
626
|
-
"The entry point (main.ts) must initialize a FastifyInstance (fastify()) instead of NestFactory.",
|
|
627
|
-
"Controllers and routing logic must be converted to Fastify route declarations (fastify.get, fastify.post).",
|
|
628
|
-
"Ideally, encapsulate routes and modules as Fastify plugins.",
|
|
629
|
-
"Remove NestJS dependency injection; use standard JavaScript/TypeScript classes or functions passing dependencies directly.",
|
|
630
|
-
"Ensure standard Fastify schema validation (JSON Schema) is added if types or DTOs are available.",
|
|
631
|
-
"You are authorized to rename, create, or delete files to adhere to a standard fastify plugin-based directory structure."
|
|
632
|
-
],
|
|
633
|
-
examples: [
|
|
634
|
-
{
|
|
635
|
-
description: "Converting a NestJS Controller to a Fastify Plugin",
|
|
636
|
-
before: `import { Controller, Get } from '@nestjs/common';
|
|
637
|
-
import { AppService } from './app.service';
|
|
638
|
-
|
|
639
|
-
@Controller('users')
|
|
640
|
-
export class UsersController {
|
|
641
|
-
constructor(private readonly appService: AppService) {}
|
|
642
|
-
|
|
643
|
-
@Get()
|
|
644
|
-
getUsers() {
|
|
645
|
-
return this.appService.getUsers();
|
|
646
|
-
}
|
|
647
|
-
}`,
|
|
648
|
-
after: `import { FastifyInstance } from 'fastify';
|
|
649
|
-
import { AppService } from './app.service';
|
|
650
|
-
|
|
651
|
-
export default async function usersPlugin(fastify: FastifyInstance, options: any) {
|
|
652
|
-
const appService = new AppService(); // Or inject via options
|
|
653
|
-
|
|
654
|
-
fastify.get('/users', async (request, reply) => {
|
|
655
|
-
return appService.getUsers();
|
|
656
|
-
});
|
|
657
|
-
}`
|
|
658
|
-
}
|
|
659
|
-
]
|
|
660
|
-
},
|
|
661
|
-
{
|
|
662
|
-
source: "express",
|
|
663
|
-
target: "fastify",
|
|
664
|
-
description: "Migration from a plain Express application to Fastify.",
|
|
665
|
-
dependenciesToRemove: ["express"],
|
|
666
|
-
dependenciesToAdd: { "fastify": "latest", "fastify-plugin": "latest" },
|
|
667
|
-
devDependenciesToRemove: ["@types/express"],
|
|
668
|
-
devDependenciesToAdd: {},
|
|
669
|
-
architecturalRules: [
|
|
670
|
-
"Replace all Express app instantiations with fastify().",
|
|
671
|
-
"Replace Express middleware signatures (req, res, next) with Fastify hooks (onRequest, preHandler, etc.) or Fastify plugins.",
|
|
672
|
-
"Replace Express routing (app.use(), app.get()) with Fastify routing (fastify.get(), fastify.register()).",
|
|
673
|
-
"Replace Express response methods (res.send(), res.json()) with Fastify reply methods (reply.send()) or simply return the payload from the async handler.",
|
|
674
|
-
"Migrate standard Express middlewares (like cors, helmet) to their Fastify native equivalents (@fastify/cors, @fastify/helmet) if they are present.",
|
|
675
|
-
"You are authorized to rename, create, or delete files to restructure the Express app into modular Fastify plugins."
|
|
676
|
-
],
|
|
677
|
-
examples: [
|
|
678
|
-
{
|
|
679
|
-
description: "Converting an Express Route to a Fastify Route",
|
|
680
|
-
before: `const express = require('express');
|
|
681
|
-
const app = express();
|
|
682
|
-
|
|
683
|
-
app.get('/api/data', (req, res) => {
|
|
684
|
-
res.json({ message: 'Hello Express' });
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
app.listen(3000);`,
|
|
688
|
-
after: `import Fastify from 'fastify';
|
|
689
|
-
|
|
690
|
-
const fastify = Fastify({ logger: true });
|
|
691
|
-
|
|
692
|
-
fastify.get('/api/data', async (request, reply) => {
|
|
693
|
-
return { message: 'Hello Fastify' };
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
fastify.listen({ port: 3000 }, (err) => {
|
|
697
|
-
if (err) {
|
|
698
|
-
fastify.log.error(err);
|
|
699
|
-
process.exit(1);
|
|
700
|
-
}
|
|
701
|
-
});`
|
|
702
|
-
}
|
|
703
|
-
]
|
|
704
|
-
},
|
|
705
|
-
{
|
|
706
|
-
source: "fastify",
|
|
707
|
-
target: "express",
|
|
708
|
-
description: "Migration from a Fastify application to Express.",
|
|
709
|
-
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
710
|
-
dependenciesToAdd: { "express": "latest" },
|
|
711
|
-
devDependenciesToRemove: [],
|
|
712
|
-
devDependenciesToAdd: { "@types/express": "latest" },
|
|
713
|
-
architecturalRules: [
|
|
714
|
-
"Replace all fastify() app instantiations with express().",
|
|
715
|
-
"Replace Fastify hooks (onRequest, preHandler, etc.) with standard Express middleware signatures (req, res, next).",
|
|
716
|
-
"Replace Fastify routing (fastify.get, fastify.register) with Express routing (app.get, express.Router().use).",
|
|
717
|
-
"Replace Fastify reply methods (reply.send) or implicit returns with Express response methods (res.send, res.json).",
|
|
718
|
-
"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.",
|
|
719
|
-
"You are authorized to rename, create, or delete files to restructure the Fastify plugins into standard Express routers."
|
|
720
|
-
],
|
|
721
|
-
examples: [
|
|
722
|
-
{
|
|
723
|
-
description: "Converting a Fastify Route to an Express Route",
|
|
724
|
-
before: `import Fastify from 'fastify';
|
|
725
|
-
|
|
726
|
-
const fastify = Fastify();
|
|
727
|
-
|
|
728
|
-
fastify.get('/api/data', async (request, reply) => {
|
|
729
|
-
return { message: 'Hello Fastify' };
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
fastify.listen({ port: 3000 });`,
|
|
733
|
-
after: `const express = require('express');
|
|
734
|
-
const app = express();
|
|
735
|
-
|
|
736
|
-
app.get('/api/data', async (req, res, next) => {
|
|
737
|
-
try {
|
|
738
|
-
res.json({ message: 'Hello Fastify' });
|
|
739
|
-
} catch (err) {
|
|
740
|
-
next(err);
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
app.listen(3000);`
|
|
745
|
-
}
|
|
746
|
-
]
|
|
747
|
-
},
|
|
748
|
-
{
|
|
749
|
-
source: "fastify",
|
|
750
|
-
target: "nestjs",
|
|
751
|
-
description: "Migration from Fastify framework to NestJS (using platform-express by default).",
|
|
752
|
-
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
753
|
-
dependenciesToAdd: {
|
|
754
|
-
"@nestjs/common": "latest",
|
|
755
|
-
"@nestjs/core": "latest",
|
|
756
|
-
"@nestjs/platform-express": "latest",
|
|
757
|
-
"reflect-metadata": "^0.1.13",
|
|
758
|
-
"rxjs": "^7.8.1"
|
|
759
|
-
},
|
|
760
|
-
devDependenciesToRemove: [],
|
|
761
|
-
devDependenciesToAdd: {
|
|
762
|
-
"@nestjs/cli": "latest",
|
|
763
|
-
"@nestjs/schematics": "latest",
|
|
764
|
-
"@nestjs/testing": "latest",
|
|
765
|
-
"@types/supertest": "^2.0.12",
|
|
766
|
-
"source-map-support": "^0.5.21",
|
|
767
|
-
"supertest": "^6.3.3"
|
|
768
|
-
},
|
|
769
|
-
architecturalRules: [
|
|
770
|
-
"Fastify routing logic must be encapsulated in classes decorated with @Controller().",
|
|
771
|
-
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
772
|
-
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
773
|
-
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of fastify route definitions.",
|
|
774
|
-
"Rely on NestJS dependency injection instead of Fastify decorators or options passing.",
|
|
775
|
-
"The entry point must use NestFactory.create() to bootstrap the application, replacing fastify().",
|
|
776
|
-
"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)."
|
|
777
|
-
],
|
|
778
|
-
examples: [
|
|
779
|
-
{
|
|
780
|
-
description: "Converting a Fastify Plugin to a NestJS Controller and Service",
|
|
781
|
-
before: `export default async function usersPlugin(fastify, options) {
|
|
782
|
-
fastify.get('/users', async (request, reply) => {
|
|
783
|
-
return { users: [] };
|
|
784
|
-
});
|
|
785
|
-
}`,
|
|
786
|
-
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
787
|
-
|
|
788
|
-
@Injectable()
|
|
789
|
-
export class UsersService {
|
|
790
|
-
getUsers() {
|
|
791
|
-
return { users: [] };
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
@Controller('users')
|
|
796
|
-
export class UsersController {
|
|
797
|
-
constructor(private readonly usersService: UsersService) {}
|
|
798
|
-
|
|
799
|
-
@Get()
|
|
800
|
-
getUsers() {
|
|
801
|
-
return this.usersService.getUsers();
|
|
802
|
-
}
|
|
803
|
-
}`
|
|
804
|
-
}
|
|
805
|
-
]
|
|
806
|
-
}
|
|
807
|
-
];
|
|
808
|
-
|
|
809
|
-
// ../domain/src/entities/catalogs/frontendCatalog.ts
|
|
810
|
-
var frontendCatalog = [
|
|
811
|
-
{
|
|
812
|
-
source: "react",
|
|
813
|
-
target: "next",
|
|
814
|
-
description: "Migration from a plain React SPA (like Vite or Create React App) to Next.js using the App Router.",
|
|
815
|
-
filesToDelete: ["vite.config.ts", "vite.config.js", "index.html"],
|
|
816
|
-
dependenciesToRemove: ["react-router-dom", "vite", "@vitejs/plugin-react"],
|
|
817
|
-
dependenciesToAdd: { "next": "latest", "react": "latest", "react-dom": "latest" },
|
|
818
|
-
devDependenciesToRemove: [],
|
|
819
|
-
devDependenciesToAdd: {},
|
|
820
|
-
architecturalRules: [
|
|
821
|
-
'Migrate standard React routing (e.g. react-router-dom) to Next.js file-system routing in the "app" directory.',
|
|
822
|
-
"Convert the main entry point (usually main.tsx or index.tsx) into Next.js app/layout.tsx and app/page.tsx.",
|
|
823
|
-
'For components that use browser-only APIs (like window, useEffect, useState), add the "use client" directive at the very top of the file.',
|
|
824
|
-
"Remove any react-router-dom <BrowserRouter>, <Routes>, and <Route> components.",
|
|
825
|
-
"Replace react-router-dom <Link> components with Next.js next/link <Link> components.",
|
|
826
|
-
"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)."
|
|
827
|
-
],
|
|
828
|
-
examples: [
|
|
829
|
-
{
|
|
830
|
-
description: "Converting a React Router setup to Next.js App Router",
|
|
831
|
-
before: `import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
832
|
-
import Home from './Home';
|
|
833
|
-
import About from './About';
|
|
834
|
-
|
|
835
|
-
export default function App() {
|
|
836
|
-
return (
|
|
837
|
-
<BrowserRouter>
|
|
838
|
-
<Routes>
|
|
839
|
-
<Route path="/" element={<Home />} />
|
|
840
|
-
<Route path="/about" element={<About />} />
|
|
841
|
-
</Routes>
|
|
842
|
-
</BrowserRouter>
|
|
843
|
-
);
|
|
844
|
-
}`,
|
|
845
|
-
after: `// This routing logic is deleted.
|
|
846
|
-
// Instead, create app/page.tsx for Home and app/about/page.tsx for About.`
|
|
847
|
-
}
|
|
848
|
-
]
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
source: "next",
|
|
852
|
-
target: "react",
|
|
853
|
-
description: "Migration from Next.js to a plain React SPA using Vite.",
|
|
854
|
-
filesToDelete: ["next.config.js", "next.config.mjs", "next.config.ts"],
|
|
855
|
-
dependenciesToRemove: ["next"],
|
|
856
|
-
dependenciesToAdd: { "react-router-dom": "latest" },
|
|
857
|
-
devDependenciesToRemove: [],
|
|
858
|
-
devDependenciesToAdd: { "vite": "latest", "@vitejs/plugin-react": "latest" },
|
|
859
|
-
architecturalRules: [
|
|
860
|
-
"Replace Next.js file-system routing (app/ or pages/) with declarative React Router (react-router-dom) components.",
|
|
861
|
-
'Remove all "use client" and "use server" directives, as they are Next.js specific.',
|
|
862
|
-
"Convert Next.js Server Components into standard React Client Components (they will need to fetch data via useEffect or a data-fetching library).",
|
|
863
|
-
'Replace Next.js <Link href="..."> with React Router <Link to="...">.',
|
|
864
|
-
"Replace Next.js next/image <Image> with standard HTML <img>.",
|
|
865
|
-
"Create a standard Vite entry point (index.html at the root, and src/main.tsx) to bootstrap the React application."
|
|
866
|
-
]
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
source: "vue",
|
|
870
|
-
target: "react",
|
|
871
|
-
description: "Migration from Vue.js (Vue 3 Composition API) to React.",
|
|
872
|
-
filesToDelete: ["vite.config.ts", "vue.config.js"],
|
|
873
|
-
dependenciesToRemove: ["vue", "vue-router", "pinia"],
|
|
874
|
-
dependenciesToAdd: { "react": "latest", "react-dom": "latest", "react-router-dom": "latest" },
|
|
875
|
-
devDependenciesToRemove: ["@vitejs/plugin-vue"],
|
|
876
|
-
devDependenciesToAdd: { "@vitejs/plugin-react": "latest" },
|
|
877
|
-
architecturalRules: [
|
|
878
|
-
"Convert all .vue Single File Components (SFC) into .tsx or .jsx React function components.",
|
|
879
|
-
"Replace Vue <template> with React JSX/TSX return statements.",
|
|
880
|
-
"Replace Vue ref() and reactive() with React useState().",
|
|
881
|
-
"Replace Vue computed() with React useMemo().",
|
|
882
|
-
"Replace Vue watch() and lifecycle hooks (onMounted, etc.) with React useEffect().",
|
|
883
|
-
"Replace vue-router with react-router-dom.",
|
|
884
|
-
"Convert Vue props definition (defineProps) into React function component parameters.",
|
|
885
|
-
"Convert Vue emit (defineEmits) into React callback props (e.g. onAction={...})."
|
|
886
|
-
]
|
|
887
|
-
},
|
|
888
|
-
{
|
|
889
|
-
source: "react",
|
|
890
|
-
target: "vue",
|
|
891
|
-
description: "Migration from React to Vue.js (Vue 3 Composition API).",
|
|
892
|
-
filesToDelete: ["vite.config.ts"],
|
|
893
|
-
dependenciesToRemove: ["react", "react-dom", "react-router-dom"],
|
|
894
|
-
dependenciesToAdd: { "vue": "latest", "vue-router": "latest" },
|
|
895
|
-
devDependenciesToRemove: ["@vitejs/plugin-react"],
|
|
896
|
-
devDependenciesToAdd: { "@vitejs/plugin-vue": "latest" },
|
|
897
|
-
architecturalRules: [
|
|
898
|
-
"Convert React .tsx/.jsx function components into Vue 3 .vue Single File Components (using <script setup>).",
|
|
899
|
-
"Move the JSX return statement into the Vue <template> block.",
|
|
900
|
-
"Replace React useState() with Vue ref() or reactive().",
|
|
901
|
-
"Replace React useMemo() with Vue computed().",
|
|
902
|
-
"Replace React useEffect() with Vue watch() or lifecycle hooks (onMounted).",
|
|
903
|
-
"Replace react-router-dom with vue-router.",
|
|
904
|
-
"Extract component props into defineProps().",
|
|
905
|
-
"Extract callback props (e.g. onClick) into defineEmits() and emit events."
|
|
906
|
-
]
|
|
907
|
-
}
|
|
908
|
-
];
|
|
909
|
-
|
|
910
|
-
// ../domain/src/entities/catalogs/index.ts
|
|
911
|
-
var MigrationCatalog = [
|
|
912
|
-
...backendCatalog,
|
|
913
|
-
...frontendCatalog
|
|
914
|
-
];
|
|
915
|
-
function findMigrationCatalogEntry(source, target) {
|
|
916
|
-
return MigrationCatalog.find(
|
|
917
|
-
(entry) => entry.source.toLowerCase() === source.toLowerCase() && entry.target.toLowerCase() === target.toLowerCase()
|
|
918
|
-
);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// ../infrastructure/src/server/api.ts
|
|
922
|
-
async function createApiServer(store, migrationRunner) {
|
|
923
|
-
const express = (await import("express")).default;
|
|
924
|
-
const cors = (await import("cors")).default;
|
|
925
|
-
const app = express();
|
|
926
|
-
app.use(cors());
|
|
927
|
-
app.use(express.json());
|
|
928
|
-
app.get("/api/plans", async (_req, res) => {
|
|
929
|
-
try {
|
|
930
|
-
const plans = await store.getAllPlans();
|
|
931
|
-
res.json(plans);
|
|
932
|
-
} catch (error) {
|
|
933
|
-
if (error instanceof Error) {
|
|
934
|
-
res.status(500).json({ error: error.message });
|
|
935
|
-
} else {
|
|
936
|
-
res.status(500).json({ error: String(error) });
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
app.get("/api/events", async (_req, res) => {
|
|
941
|
-
try {
|
|
942
|
-
const events = await store.getEvents();
|
|
943
|
-
res.json(events);
|
|
944
|
-
} catch (error) {
|
|
945
|
-
if (error instanceof Error) {
|
|
946
|
-
res.status(500).json({ error: error.message });
|
|
947
|
-
} else {
|
|
948
|
-
res.status(500).json({ error: String(error) });
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
});
|
|
952
|
-
app.get("/api/browse", async (req, res) => {
|
|
953
|
-
try {
|
|
954
|
-
const { readdirSync: readdirSync2, statSync: statSync2 } = await import("fs");
|
|
955
|
-
const { resolve: resolve3, join: join8 } = await import("path");
|
|
956
|
-
const targetPath = req.query.path ? resolve3(String(req.query.path)) : resolve3(".");
|
|
957
|
-
const entries = readdirSync2(targetPath).filter(
|
|
958
|
-
(name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist" && name !== "build"
|
|
959
|
-
).map((name) => {
|
|
960
|
-
const full = join8(targetPath, name);
|
|
961
|
-
try {
|
|
962
|
-
const stat = statSync2(full);
|
|
963
|
-
return {
|
|
964
|
-
name,
|
|
965
|
-
path: full,
|
|
966
|
-
isDirectory: stat.isDirectory()
|
|
967
|
-
};
|
|
968
|
-
} catch {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
}).filter(Boolean);
|
|
972
|
-
res.json({
|
|
973
|
-
current: targetPath,
|
|
974
|
-
parent: resolve3(targetPath, ".."),
|
|
975
|
-
entries
|
|
976
|
-
});
|
|
977
|
-
} catch (error) {
|
|
978
|
-
if (error instanceof Error) {
|
|
979
|
-
res.status(500).json({ error: error.message });
|
|
980
|
-
} else {
|
|
981
|
-
res.status(500).json({ error: String(error) });
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
app.get("/api/detect", async (req, res) => {
|
|
986
|
-
try {
|
|
987
|
-
const { detectTechnologies: detectTechnologies2 } = await import("./TechDetector-5YP44RVR.js");
|
|
988
|
-
const targetPath = req.query.path ? String(req.query.path) : ".";
|
|
989
|
-
const detected = detectTechnologies2(targetPath);
|
|
990
|
-
res.json({
|
|
991
|
-
detected,
|
|
992
|
-
primary: detected.length > 0 ? detected[0].framework : null
|
|
993
|
-
});
|
|
994
|
-
} catch (error) {
|
|
995
|
-
if (error instanceof Error) {
|
|
996
|
-
res.status(500).json({ error: error.message });
|
|
997
|
-
} else {
|
|
998
|
-
res.status(500).json({ error: String(error) });
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
if (migrationRunner) {
|
|
1003
|
-
app.post("/api/migrations/start", async (req, res) => {
|
|
1004
|
-
try {
|
|
1005
|
-
const { targetPath, from, to } = req.body;
|
|
1006
|
-
if (!targetPath || !from || !to) {
|
|
1007
|
-
res.status(400).json({
|
|
1008
|
-
error: "Missing required fields: targetPath, from, to"
|
|
1009
|
-
});
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
const catalogEntry = findMigrationCatalogEntry(from, to);
|
|
1013
|
-
if (!catalogEntry) {
|
|
1014
|
-
res.status(400).json({
|
|
1015
|
-
error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
|
|
1016
|
-
});
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
const result = await migrationRunner.startMigration({
|
|
1020
|
-
targetPath,
|
|
1021
|
-
from,
|
|
1022
|
-
to
|
|
1023
|
-
});
|
|
1024
|
-
res.json({ ok: true, ...result });
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
if (error instanceof Error) {
|
|
1027
|
-
res.status(500).json({ error: error.message });
|
|
1028
|
-
} else {
|
|
1029
|
-
res.status(500).json({ error: String(error) });
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
app.post("/api/migrations/apply", async (req, res) => {
|
|
1034
|
-
try {
|
|
1035
|
-
const { runId, targetPath } = req.body;
|
|
1036
|
-
if (!runId || !targetPath) {
|
|
1037
|
-
res.status(400).json({
|
|
1038
|
-
error: "Missing required fields: runId, targetPath"
|
|
1039
|
-
});
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
1043
|
-
const message = await integrator.applyMigration(runId, targetPath);
|
|
1044
|
-
res.json({ ok: true, message });
|
|
1045
|
-
} catch (error) {
|
|
1046
|
-
if (error instanceof Error) {
|
|
1047
|
-
res.status(500).json({ error: error.message });
|
|
1048
|
-
} else {
|
|
1049
|
-
res.status(500).json({ error: String(error) });
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
app.post("/api/migrations/rollback", async (req, res) => {
|
|
1054
|
-
try {
|
|
1055
|
-
const { runId } = req.body;
|
|
1056
|
-
if (!runId) {
|
|
1057
|
-
res.status(400).json({
|
|
1058
|
-
error: "Missing required field: runId"
|
|
1059
|
-
});
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
await migrationRunner.rollbackMigration(runId);
|
|
1063
|
-
res.json({ ok: true, message: `Rolled back run: ${runId}` });
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
if (error instanceof Error) {
|
|
1066
|
-
res.status(500).json({ error: error.message });
|
|
1067
|
-
} else {
|
|
1068
|
-
res.status(500).json({ error: String(error) });
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
app.post("/api/migrations/reset", async (_req, res) => {
|
|
1073
|
-
try {
|
|
1074
|
-
await store.reset();
|
|
1075
|
-
res.json({ ok: true, message: "Migration state reset successfully" });
|
|
1076
|
-
} catch (error) {
|
|
1077
|
-
if (error instanceof Error) {
|
|
1078
|
-
res.status(500).json({ error: error.message });
|
|
1079
|
-
} else {
|
|
1080
|
-
res.status(500).json({ error: String(error) });
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
const path6 = await import("path");
|
|
1086
|
-
const fs7 = await import("fs");
|
|
1087
|
-
let publicDir = path6.join(import.meta.dirname, "public");
|
|
1088
|
-
if (!fs7.existsSync(publicDir)) {
|
|
1089
|
-
publicDir = path6.join(import.meta.dirname, "../public");
|
|
1090
|
-
}
|
|
1091
|
-
if (fs7.existsSync(publicDir)) {
|
|
1092
|
-
app.use(express.static(publicDir));
|
|
1093
|
-
app.get(/^(.*)$/, (_req, res) => {
|
|
1094
|
-
res.sendFile(path6.join(publicDir, "index.html"));
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
return app;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// ../application/src/agents/MapperAgent.ts
|
|
1101
|
-
import * as fs3 from "fs";
|
|
1102
|
-
import * as path2 from "path";
|
|
1103
|
-
import {
|
|
1104
|
-
createAgent,
|
|
1105
|
-
SituationSpecification
|
|
1106
|
-
} from "@mozaik-ai/core";
|
|
1107
|
-
var WhenMigrationStarts = class extends SituationSpecification {
|
|
1108
|
-
isSatisfiedBy({ event }) {
|
|
1109
|
-
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
var mapFilesProcessor = {
|
|
1113
|
-
async apply({ event, participant }) {
|
|
1114
|
-
const payload = event.payload;
|
|
1115
|
-
console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
|
|
1116
|
-
const shadowDir = payload.shadowWorkspacePath || "";
|
|
1117
|
-
if (!shadowDir) {
|
|
1118
|
-
console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
const filesToMigrate = [];
|
|
1122
|
-
const scanDir = (dir) => {
|
|
1123
|
-
if (!fs3.existsSync(dir)) return;
|
|
1124
|
-
const files = fs3.readdirSync(dir);
|
|
1125
|
-
const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
|
|
1126
|
-
for (const file of files) {
|
|
1127
|
-
if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
|
|
1128
|
-
const fullPath = path2.join(dir, file);
|
|
1129
|
-
if (fs3.statSync(fullPath).isDirectory()) {
|
|
1130
|
-
scanDir(fullPath);
|
|
1131
|
-
} else if (file.endsWith(".ts") || file.endsWith(".js")) {
|
|
1132
|
-
filesToMigrate.push(fullPath);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
};
|
|
1136
|
-
scanDir(shadowDir);
|
|
1137
|
-
const runtime = resolveRuntime();
|
|
1138
|
-
const repository = runtime.state.repository;
|
|
1139
|
-
for (const discoveredFile of filesToMigrate) {
|
|
1140
|
-
console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
|
|
1141
|
-
await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
|
|
1142
|
-
sendEvent(
|
|
1143
|
-
{
|
|
1144
|
-
type: "file.discovered" /* FILE_DISCOVERED */,
|
|
1145
|
-
producerId: participant.getId(),
|
|
1146
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1147
|
-
payload: {
|
|
1148
|
-
planId: payload.planId,
|
|
1149
|
-
filePath: discoveredFile
|
|
1150
|
-
}
|
|
1151
|
-
},
|
|
1152
|
-
participant.getId()
|
|
1153
|
-
);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
};
|
|
1157
|
-
var mapFilesHandler = {
|
|
1158
|
-
specification: new WhenMigrationStarts(),
|
|
1159
|
-
processor: mapFilesProcessor
|
|
1160
|
-
};
|
|
1161
|
-
function createMapperAgent() {
|
|
1162
|
-
return createAgent({
|
|
1163
|
-
name: "Mapper",
|
|
1164
|
-
capabilities: ["code_analysis"],
|
|
1165
|
-
instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
|
|
1166
|
-
tools: [],
|
|
1167
|
-
// AST scanning tools will go here later
|
|
1168
|
-
handlers: [mapFilesHandler]
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// ../application/src/agents/WorkerAgent.ts
|
|
1173
|
-
import * as fs4 from "fs";
|
|
1174
|
-
import {
|
|
1175
|
-
createAgent as createAgent2,
|
|
1176
|
-
SituationSpecification as SituationSpecification2
|
|
1177
|
-
} from "@mozaik-ai/core";
|
|
1178
|
-
|
|
1179
|
-
// ../application/src/utils/FileTreeBuilder.ts
|
|
1180
|
-
import * as path3 from "path";
|
|
1181
|
-
function getCommonBaseDir(filePaths) {
|
|
1182
|
-
if (!filePaths || filePaths.length === 0) return "";
|
|
1183
|
-
if (filePaths.length === 1) return path3.dirname(filePaths[0]);
|
|
1184
|
-
const splitPaths = filePaths.map((p) => path3.normalize(p).split(path3.sep));
|
|
1185
|
-
let commonPath = "";
|
|
1186
|
-
for (let i = 0; i < splitPaths[0].length; i++) {
|
|
1187
|
-
const part = splitPaths[0][i];
|
|
1188
|
-
const isCommon = splitPaths.every((parts) => parts[i] === part);
|
|
1189
|
-
if (isCommon) {
|
|
1190
|
-
commonPath = commonPath === "" ? part : commonPath + path3.sep + part;
|
|
1191
|
-
} else {
|
|
1192
|
-
break;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return commonPath;
|
|
1196
|
-
}
|
|
1197
|
-
function buildFileTree(filePaths) {
|
|
1198
|
-
if (!filePaths || filePaths.length === 0) return "No files found.";
|
|
1199
|
-
const baseDir = getCommonBaseDir(filePaths);
|
|
1200
|
-
const relativePaths = filePaths.map((p) => path3.relative(baseDir, p)).filter((p) => p.length > 0).sort();
|
|
1201
|
-
if (relativePaths.length === 0) return "No files found.";
|
|
1202
|
-
const tree = {};
|
|
1203
|
-
for (const relPath of relativePaths) {
|
|
1204
|
-
const parts = relPath.split(path3.sep);
|
|
1205
|
-
let current = tree;
|
|
1206
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1207
|
-
const part = parts[i];
|
|
1208
|
-
if (i === parts.length - 1) {
|
|
1209
|
-
current[part] = null;
|
|
1210
|
-
} else {
|
|
1211
|
-
current[part] = current[part] || {};
|
|
1212
|
-
current = current[part];
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
let output = `/
|
|
1217
|
-
`;
|
|
1218
|
-
function renderTree(node, prefix = "") {
|
|
1219
|
-
if (node === null) return;
|
|
1220
|
-
const keys = Object.keys(node);
|
|
1221
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1222
|
-
const key = keys[i];
|
|
1223
|
-
const isLast = i === keys.length - 1;
|
|
1224
|
-
const isFile = node[key] === null;
|
|
1225
|
-
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1226
|
-
output += `${prefix}${connector}${key}
|
|
1227
|
-
`;
|
|
1228
|
-
if (!isFile) {
|
|
1229
|
-
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1230
|
-
renderTree(node[key], nextPrefix);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
renderTree(tree);
|
|
1235
|
-
return output.trim();
|
|
1236
|
-
}
|
|
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
|
+
}
|
|
1237
596
|
|
|
1238
597
|
// ../application/src/agents/WorkerAgent.ts
|
|
1239
|
-
var retryCountMap = /* @__PURE__ */ new Map();
|
|
1240
598
|
var MAX_RETRIES = 2;
|
|
1241
599
|
var ConcurrencyQueue = class {
|
|
1242
600
|
constructor(concurrencyLimit) {
|
|
@@ -1266,7 +624,7 @@ var ConcurrencyQueue = class {
|
|
|
1266
624
|
}
|
|
1267
625
|
}
|
|
1268
626
|
};
|
|
1269
|
-
var workerQueue = new ConcurrencyQueue(
|
|
627
|
+
var workerQueue = new ConcurrencyQueue(3);
|
|
1270
628
|
var WhenFileDiscovered = class extends SituationSpecification2 {
|
|
1271
629
|
isSatisfiedBy({ event }) {
|
|
1272
630
|
return event.type === "file.discovered" /* FILE_DISCOVERED */;
|
|
@@ -1290,7 +648,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
|
|
|
1290
648
|
async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
1291
649
|
return new Promise(async (originalResolve) => {
|
|
1292
650
|
let isDone = false;
|
|
1293
|
-
const
|
|
651
|
+
const resolve5 = () => {
|
|
1294
652
|
if (!isDone) {
|
|
1295
653
|
isDone = true;
|
|
1296
654
|
originalResolve();
|
|
@@ -1298,13 +656,12 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1298
656
|
};
|
|
1299
657
|
const runtime = resolveRuntime();
|
|
1300
658
|
const repository = runtime.state.repository;
|
|
1301
|
-
console.log(`[WorkerAgent] Marking file as 'in_progress' in database...`);
|
|
1302
659
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
1303
660
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
1304
661
|
const tempAgent = createMozaikAgent({
|
|
1305
662
|
name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
|
|
1306
663
|
capabilities: ["code_refactoring", "inference"],
|
|
1307
|
-
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.",
|
|
1308
665
|
tools: participant.getTools(),
|
|
1309
666
|
handlers: [
|
|
1310
667
|
{
|
|
@@ -1323,7 +680,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1323
680
|
} catch (e) {
|
|
1324
681
|
console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
|
|
1325
682
|
}
|
|
1326
|
-
await repository.updateTaskStatus(planId, filePath, "
|
|
683
|
+
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
1327
684
|
sendEvent(
|
|
1328
685
|
{
|
|
1329
686
|
type: "file.migrated" /* FILE_MIGRATED */,
|
|
@@ -1338,7 +695,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1338
695
|
tempParticipant.getId()
|
|
1339
696
|
);
|
|
1340
697
|
leave(tempParticipant);
|
|
1341
|
-
|
|
698
|
+
resolve5();
|
|
1342
699
|
}
|
|
1343
700
|
}
|
|
1344
701
|
}
|
|
@@ -1357,8 +714,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1357
714
|
if (!isDone) {
|
|
1358
715
|
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
|
|
1359
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());
|
|
1360
723
|
leave(tempAgent);
|
|
1361
|
-
|
|
724
|
+
resolve5();
|
|
1362
725
|
}
|
|
1363
726
|
}, 45e3);
|
|
1364
727
|
} catch (error) {
|
|
@@ -1366,8 +729,14 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
1366
729
|
if (!isDone) {
|
|
1367
730
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1368
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());
|
|
1369
738
|
leave(tempAgent);
|
|
1370
|
-
|
|
739
|
+
resolve5();
|
|
1371
740
|
}
|
|
1372
741
|
}
|
|
1373
742
|
});
|
|
@@ -1386,10 +755,10 @@ var workOnFileProcessor = {
|
|
|
1386
755
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1387
756
|
if (catalogEntry) {
|
|
1388
757
|
prompt += `
|
|
1389
|
-
|
|
758
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
759
|
+
`;
|
|
760
|
+
prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
|
|
1390
761
|
`;
|
|
1391
|
-
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1392
|
-
`);
|
|
1393
762
|
if (catalogEntry.examples && catalogEntry.examples.length > 0) {
|
|
1394
763
|
prompt += `
|
|
1395
764
|
Reference Examples:
|
|
@@ -1426,8 +795,20 @@ ${projectTree}
|
|
|
1426
795
|
prompt += `Use this context to ensure your changes align with the overall project structure and other files.
|
|
1427
796
|
`;
|
|
1428
797
|
}
|
|
798
|
+
prompt += collectFileHints(
|
|
799
|
+
payload.filePath,
|
|
800
|
+
{ source: plan.profile.source, target: plan.profile.target },
|
|
801
|
+
catalogEntry?.layer
|
|
802
|
+
);
|
|
1429
803
|
}
|
|
1430
|
-
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.`;
|
|
1431
812
|
workerQueue.enqueue(async () => {
|
|
1432
813
|
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1433
814
|
});
|
|
@@ -1437,22 +818,39 @@ var fixRejectedFileProcessor = {
|
|
|
1437
818
|
async apply({ event, participant }) {
|
|
1438
819
|
const payload = event.payload;
|
|
1439
820
|
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1440
|
-
const
|
|
821
|
+
const retries = resolveRuntime().state.retryCounts;
|
|
822
|
+
if (payload.source === "integration") {
|
|
823
|
+
retries.delete(retryKey);
|
|
824
|
+
}
|
|
825
|
+
const currentRetries = retries.get(retryKey) || 0;
|
|
1441
826
|
if (currentRetries >= MAX_RETRIES) {
|
|
1442
827
|
console.warn(`[WorkerAgent] File ${payload.filePath} exceeded max retries (${MAX_RETRIES}). Marking as failed.`);
|
|
1443
828
|
const runtime2 = resolveRuntime();
|
|
1444
829
|
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", `Exceeded max retries (${MAX_RETRIES})`);
|
|
1445
|
-
|
|
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());
|
|
1446
837
|
return;
|
|
1447
838
|
}
|
|
1448
|
-
|
|
839
|
+
retries.set(retryKey, currentRetries + 1);
|
|
1449
840
|
console.log(`[WorkerAgent] File REJECTED (attempt ${currentRetries + 1}/${MAX_RETRIES}), initiating repair loop: ${payload.filePath}`);
|
|
1450
841
|
const runtime = resolveRuntime();
|
|
1451
842
|
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1452
843
|
let prompt = `You need to FIX the file at path: ${payload.filePath}
|
|
1453
844
|
`;
|
|
1454
|
-
|
|
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.
|
|
1455
852
|
`;
|
|
853
|
+
}
|
|
1456
854
|
prompt += `Feedback/Errors:
|
|
1457
855
|
${payload.errors.join("\n")}
|
|
1458
856
|
|
|
@@ -1463,10 +861,10 @@ ${payload.errors.join("\n")}
|
|
|
1463
861
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1464
862
|
if (catalogEntry) {
|
|
1465
863
|
prompt += `
|
|
1466
|
-
|
|
864
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
865
|
+
`;
|
|
866
|
+
prompt += `${formatCatalogRules(catalogEntry.ruleSections)}
|
|
1467
867
|
`;
|
|
1468
|
-
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1469
|
-
`);
|
|
1470
868
|
}
|
|
1471
869
|
if (plan.tasks && plan.tasks.length > 0) {
|
|
1472
870
|
const allFiles = plan.tasks.map((t) => t.filePath);
|
|
@@ -1481,19 +879,26 @@ ${projectTree}
|
|
|
1481
879
|
prompt += `Ensure you respect this overall project structure while fixing the issues.
|
|
1482
880
|
`;
|
|
1483
881
|
}
|
|
882
|
+
prompt += collectFileHints(
|
|
883
|
+
payload.filePath,
|
|
884
|
+
{ source: plan.profile.source, target: plan.profile.target },
|
|
885
|
+
catalogEntry?.layer
|
|
886
|
+
);
|
|
1484
887
|
}
|
|
1485
|
-
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.`;
|
|
1486
891
|
workerQueue.enqueue(async () => {
|
|
1487
892
|
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1488
893
|
});
|
|
1489
894
|
}
|
|
1490
895
|
};
|
|
1491
896
|
var fatalMismatchProcessor = {
|
|
1492
|
-
async apply({ event }) {
|
|
897
|
+
async apply({ event, participant }) {
|
|
1493
898
|
const payload = event.payload;
|
|
1494
899
|
console.error(`[WorkerAgent] \u{1F6D1} FATAL MISMATCH received for ${payload.filePath}. Reason: ${payload.reason}`);
|
|
1495
900
|
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1496
|
-
|
|
901
|
+
resolveRuntime().state.retryCounts.delete(retryKey);
|
|
1497
902
|
const runtime = resolveRuntime();
|
|
1498
903
|
await runtime.state.repository.updateTaskStatus(
|
|
1499
904
|
payload.planId,
|
|
@@ -1501,6 +906,12 @@ var fatalMismatchProcessor = {
|
|
|
1501
906
|
"failed",
|
|
1502
907
|
`Fatal Architectural Mismatch: ${payload.reason}`
|
|
1503
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());
|
|
1504
915
|
}
|
|
1505
916
|
};
|
|
1506
917
|
function createWorkerAgent(tools) {
|
|
@@ -1522,6 +933,7 @@ import {
|
|
|
1522
933
|
createAgent as createAgent3,
|
|
1523
934
|
SituationSpecification as SituationSpecification3
|
|
1524
935
|
} from "@mozaik-ai/core";
|
|
936
|
+
import * as path5 from "path";
|
|
1525
937
|
var ConcurrencyQueue2 = class {
|
|
1526
938
|
constructor(concurrencyLimit) {
|
|
1527
939
|
this.concurrencyLimit = concurrencyLimit;
|
|
@@ -1550,7 +962,7 @@ var ConcurrencyQueue2 = class {
|
|
|
1550
962
|
}
|
|
1551
963
|
}
|
|
1552
964
|
};
|
|
1553
|
-
var reviewerQueue = new ConcurrencyQueue2(
|
|
965
|
+
var reviewerQueue = new ConcurrencyQueue2(3);
|
|
1554
966
|
var WhenFileMigrated = class extends SituationSpecification3 {
|
|
1555
967
|
isSatisfiedBy({ event }) {
|
|
1556
968
|
return event.type === "file.migrated" /* FILE_MIGRATED */;
|
|
@@ -1574,7 +986,7 @@ var reviewFileProcessor = {
|
|
|
1574
986
|
reviewerQueue.enqueue(async () => {
|
|
1575
987
|
return new Promise(async (originalResolve) => {
|
|
1576
988
|
let isDone = false;
|
|
1577
|
-
const
|
|
989
|
+
const resolve5 = () => {
|
|
1578
990
|
if (!isDone) {
|
|
1579
991
|
isDone = true;
|
|
1580
992
|
console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
|
|
@@ -1582,12 +994,12 @@ var reviewFileProcessor = {
|
|
|
1582
994
|
}
|
|
1583
995
|
};
|
|
1584
996
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
1585
|
-
const { sendEvent: sendEvent3 } = await import("./runtime-
|
|
997
|
+
const { sendEvent: sendEvent3 } = await import("./runtime-L7VOQROE.js");
|
|
1586
998
|
const reviewerId = `Reviewer-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
1587
999
|
const tempAgent = createMozaikAgent({
|
|
1588
1000
|
name: reviewerId,
|
|
1589
1001
|
capabilities: ["code_review", "inference"],
|
|
1590
|
-
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.",
|
|
1591
1003
|
tools: [],
|
|
1592
1004
|
// No tools to avoid Gemini thought_signature crash
|
|
1593
1005
|
handlers: [
|
|
@@ -1624,6 +1036,8 @@ var reviewFileProcessor = {
|
|
|
1624
1036
|
try {
|
|
1625
1037
|
if (reviewResult.status === "APPROVED") {
|
|
1626
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");
|
|
1627
1041
|
sendEvent3({
|
|
1628
1042
|
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
1629
1043
|
producerId: participantId,
|
|
@@ -1650,27 +1064,9 @@ var reviewFileProcessor = {
|
|
|
1650
1064
|
} catch (e) {
|
|
1651
1065
|
console.error(`[ReviewerAgent:${reviewerId}] Failed to send review event:`, e);
|
|
1652
1066
|
}
|
|
1653
|
-
try {
|
|
1654
|
-
const runtime2 = await resolveRuntime();
|
|
1655
|
-
const plan2 = await runtime2.state.repository.getPlan(payload.planId);
|
|
1656
|
-
if (plan2) {
|
|
1657
|
-
const allTerminal = plan2.tasks.every((t) => t.status === "completed" || t.status === "failed");
|
|
1658
|
-
if (allTerminal) {
|
|
1659
|
-
console.log(`[App] All tasks reached terminal state. Emitting MIGRATION_COMPLETED.`);
|
|
1660
|
-
sendEvent3({
|
|
1661
|
-
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1662
|
-
producerId: participantId,
|
|
1663
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1664
|
-
payload: { planId: payload.planId }
|
|
1665
|
-
}, participantId);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
} catch (e) {
|
|
1669
|
-
console.error(`[ReviewerAgent:${reviewerId}] Failed to check migration completion:`, e);
|
|
1670
|
-
}
|
|
1671
1067
|
}
|
|
1672
1068
|
leave(tempParticipant);
|
|
1673
|
-
|
|
1069
|
+
resolve5();
|
|
1674
1070
|
}
|
|
1675
1071
|
}
|
|
1676
1072
|
}
|
|
@@ -1680,16 +1076,58 @@ var reviewFileProcessor = {
|
|
|
1680
1076
|
join(tempAgent);
|
|
1681
1077
|
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
|
|
1682
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
|
+
}
|
|
1683
1121
|
if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
|
|
1684
1122
|
try {
|
|
1685
|
-
const { Project
|
|
1686
|
-
const tsProject = new
|
|
1123
|
+
const { Project } = await import("ts-morph");
|
|
1124
|
+
const tsProject = new Project();
|
|
1687
1125
|
const sf = tsProject.addSourceFileAtPath(payload.filePath);
|
|
1688
1126
|
const diagnostics = sf.getPreEmitDiagnostics();
|
|
1689
1127
|
const syntaxErrors = diagnostics.filter((d) => d.getCode() >= 1e3 && d.getCode() < 2e3);
|
|
1690
1128
|
if (syntaxErrors.length > 0) {
|
|
1691
1129
|
console.error(`[ReviewerAgent:${reviewerId}] Syntax Error in ${payload.filePath}. Rejecting immediately.`);
|
|
1692
|
-
const { sendEvent: sendEvent4 } = await import("./runtime-
|
|
1130
|
+
const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
|
|
1693
1131
|
sendEvent4({
|
|
1694
1132
|
type: "file.rejected" /* FILE_REJECTED */,
|
|
1695
1133
|
producerId: tempAgent.getId(),
|
|
@@ -1701,7 +1139,7 @@ var reviewFileProcessor = {
|
|
|
1701
1139
|
}
|
|
1702
1140
|
}, tempAgent.getId());
|
|
1703
1141
|
leave(tempAgent);
|
|
1704
|
-
|
|
1142
|
+
resolve5();
|
|
1705
1143
|
return;
|
|
1706
1144
|
}
|
|
1707
1145
|
} catch (e) {
|
|
@@ -1709,6 +1147,28 @@ var reviewFileProcessor = {
|
|
|
1709
1147
|
}
|
|
1710
1148
|
}
|
|
1711
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
|
+
}
|
|
1712
1172
|
let prompt = `Review the following file migration: ${payload.filePath}
|
|
1713
1173
|
`;
|
|
1714
1174
|
if (plan) {
|
|
@@ -1717,360 +1177,1579 @@ var reviewFileProcessor = {
|
|
|
1717
1177
|
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1718
1178
|
if (catalogEntry) {
|
|
1719
1179
|
prompt += `
|
|
1720
|
-
|
|
1180
|
+
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
|
|
1721
1181
|
`;
|
|
1722
|
-
|
|
1723
|
-
|
|
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;
|
|
1724
1379
|
}
|
|
1725
1380
|
}
|
|
1726
|
-
|
|
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
|
-
|
|
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"
|
|
1760
1514
|
}
|
|
1761
|
-
},
|
|
1762
|
-
|
|
1763
|
-
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
1764
|
-
if (!isDone) {
|
|
1765
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
|
|
1766
|
-
const runtime2 = resolveRuntime2();
|
|
1767
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1768
|
-
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
|
|
1769
|
-
leave(tempAgent);
|
|
1770
|
-
resolve3();
|
|
1771
|
-
}
|
|
1515
|
+
}, producerId);
|
|
1516
|
+
return;
|
|
1772
1517
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
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);
|
|
1775
1524
|
}
|
|
1776
1525
|
};
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
return createAgent3({
|
|
1783
|
-
name: "Reviewer",
|
|
1784
|
-
capabilities: ["code_review", "inference"],
|
|
1785
|
-
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.",
|
|
1786
1531
|
tools: [],
|
|
1787
|
-
|
|
1788
|
-
|
|
1532
|
+
handlers: [
|
|
1533
|
+
{ specification: new WhenMigrationStarted(), processor: startWatchdogProcessor },
|
|
1534
|
+
{ specification: new WhenSwarmMayBeIdle(), processor: startIntegrationIfIdle }
|
|
1535
|
+
]
|
|
1789
1536
|
});
|
|
1790
1537
|
}
|
|
1791
1538
|
|
|
1792
|
-
// ../application/src/agents/
|
|
1793
|
-
import * as fs5 from "fs";
|
|
1794
|
-
import * as path4 from "path";
|
|
1539
|
+
// ../application/src/agents/ReporterAgent.ts
|
|
1795
1540
|
import {
|
|
1796
|
-
|
|
1797
|
-
|
|
1541
|
+
Agent as Agent6,
|
|
1542
|
+
createAgent as createAgent6,
|
|
1543
|
+
SituationSpecification as SituationSpecification6
|
|
1798
1544
|
} from "@mozaik-ai/core";
|
|
1799
|
-
|
|
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 {
|
|
1800
1631
|
isSatisfiedBy({ event }) {
|
|
1801
1632
|
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1802
1633
|
}
|
|
1803
1634
|
};
|
|
1804
|
-
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 = {
|
|
1805
1671
|
async apply({ event, participant }) {
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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).");
|
|
1811
1765
|
return;
|
|
1812
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() {
|
|
1813
1795
|
const runtime = resolveRuntime();
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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;
|
|
1818
1816
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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.`);
|
|
1822
1974
|
return;
|
|
1823
1975
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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;
|
|
1827
1999
|
await runtime.state.repository.savePlan(plan);
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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.");
|
|
1835
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.");
|
|
1836
2021
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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();
|
|
1847
2074
|
});
|
|
1848
|
-
|
|
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);
|
|
1849
2082
|
});
|
|
1850
|
-
|
|
1851
|
-
try {
|
|
1852
|
-
let toRemove = [];
|
|
1853
|
-
if (catalogEntry.dependenciesToRemove) toRemove.push(...catalogEntry.dependenciesToRemove);
|
|
1854
|
-
if (catalogEntry.devDependenciesToRemove) toRemove.push(...catalogEntry.devDependenciesToRemove);
|
|
1855
|
-
if (toRemove.length > 0) {
|
|
1856
|
-
console.log(`[PackageManagerAgent] Uninstalling: ${toRemove.join(" ")}`);
|
|
1857
|
-
await runCommandAsync("npm", ["uninstall", ...toRemove]);
|
|
1858
|
-
}
|
|
1859
|
-
if (catalogEntry.dependenciesToAdd && Object.keys(catalogEntry.dependenciesToAdd).length > 0) {
|
|
1860
|
-
const deps = Object.entries(catalogEntry.dependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
1861
|
-
console.log(`[PackageManagerAgent] Installing dependencies (this may take a few minutes)...`);
|
|
1862
|
-
sendEvent({
|
|
1863
|
-
type: "system.log" /* SYSTEM_LOG */,
|
|
1864
|
-
producerId: participant.getId(),
|
|
1865
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1866
|
-
payload: { planId: payload.planId, message: "Downloading and installing dependencies (this may take a few minutes)...", level: "info" }
|
|
1867
|
-
}, participant.getId());
|
|
1868
|
-
await runCommandAsync("npm", ["install", ...deps]);
|
|
1869
|
-
}
|
|
1870
|
-
if (catalogEntry.devDependenciesToAdd && Object.keys(catalogEntry.devDependenciesToAdd).length > 0) {
|
|
1871
|
-
const devDeps = Object.entries(catalogEntry.devDependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
1872
|
-
console.log(`[PackageManagerAgent] Installing devDependencies...`);
|
|
1873
|
-
await runCommandAsync("npm", ["install", "-D", ...devDeps]);
|
|
1874
|
-
}
|
|
1875
|
-
console.log(`[PackageManagerAgent] Successfully updated dependencies via npm.`);
|
|
1876
|
-
sendEvent({
|
|
1877
|
-
type: "system.log" /* SYSTEM_LOG */,
|
|
1878
|
-
producerId: participant.getId(),
|
|
1879
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1880
|
-
payload: { planId: payload.planId, message: "Dependencies installed successfully!", level: "info" }
|
|
1881
|
-
}, participant.getId());
|
|
1882
|
-
} catch (err) {
|
|
1883
|
-
console.error(`[PackageManagerAgent] Error running npm commands:`, err);
|
|
2083
|
+
build = await runInShadowWorkspace(shadowPath, "npm run build");
|
|
1884
2084
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
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;
|
|
1894
2093
|
}
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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;
|
|
1910
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);
|
|
1911
2131
|
}
|
|
1912
2132
|
}
|
|
1913
2133
|
};
|
|
1914
|
-
var
|
|
1915
|
-
specification: new
|
|
1916
|
-
processor:
|
|
2134
|
+
var manageIntegrationHandler = {
|
|
2135
|
+
specification: new WhenIntegrationStarts2(),
|
|
2136
|
+
processor: integrationProcessor
|
|
1917
2137
|
};
|
|
1918
|
-
function
|
|
1919
|
-
return
|
|
1920
|
-
name: "
|
|
1921
|
-
capabilities: ["
|
|
1922
|
-
instruction: "You are the
|
|
1923
|
-
tools
|
|
1924
|
-
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]
|
|
1925
2145
|
});
|
|
1926
2146
|
}
|
|
1927
2147
|
|
|
1928
|
-
// ../application/src/
|
|
2148
|
+
// ../application/src/migration/plugins/frontendTargets.ts
|
|
2149
|
+
import * as path8 from "path";
|
|
2150
|
+
|
|
2151
|
+
// ../application/src/utils/FrontendRuntimeHints.ts
|
|
1929
2152
|
import * as fs6 from "fs";
|
|
1930
|
-
import * as
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
isSatisfiedBy({ event }) {
|
|
1937
|
-
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
|
+
}
|
|
1938
2159
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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 [];
|
|
1943
2175
|
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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
|
+
});
|
|
1948
2194
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
+
});
|
|
1953
2200
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
+
});
|
|
1958
2217
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
+
});
|
|
1963
2223
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
+
}
|
|
1972
2248
|
}
|
|
2249
|
+
return issues;
|
|
1973
2250
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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.
|
|
1988
2370
|
`;
|
|
1989
|
-
fs6.writeFileSync(reportPath, header, "utf-8");
|
|
1990
|
-
console.log(`[ReporterAgent] Initialized report at ${reportPath}`);
|
|
1991
2371
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-5OVGH2Y7.js");
|
|
1997
|
-
const runtime = resolveRuntime2();
|
|
1998
|
-
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1999
|
-
if (!plan) return;
|
|
2000
|
-
const shadowDir = plan.targetPath;
|
|
2001
|
-
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
|
+
`;
|
|
2002
2376
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
+
];
|
|
2012
2425
|
}
|
|
2013
2426
|
};
|
|
2014
|
-
var
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
+
];
|
|
2023
2446
|
}
|
|
2024
2447
|
};
|
|
2025
|
-
var
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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
|
+
];
|
|
2034
2469
|
}
|
|
2035
2470
|
};
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
if (
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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.
|
|
2051
2693
|
`;
|
|
2052
|
-
|
|
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
|
+
];
|
|
2053
2711
|
}
|
|
2054
2712
|
};
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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);
|
|
2070
2749
|
}
|
|
2071
2750
|
|
|
2072
2751
|
// ../application/src/index.ts
|
|
2073
|
-
import { createAgent as
|
|
2752
|
+
import { createAgent as createAgent8, createHuman, SituationSpecification as SituationSpecification8 } from "@mozaik-ai/core";
|
|
2074
2753
|
|
|
2075
2754
|
// ../application/src/MigrationRunner.ts
|
|
2076
2755
|
import { randomUUID } from "crypto";
|
|
@@ -2118,13 +2797,14 @@ var MigrationRunner = class {
|
|
|
2118
2797
|
createdAt: /* @__PURE__ */ new Date()
|
|
2119
2798
|
};
|
|
2120
2799
|
await this.store.savePlan(plan);
|
|
2121
|
-
const
|
|
2122
|
-
|
|
2123
|
-
|
|
2800
|
+
const dispatcherId = resolveRuntime().state.dispatcherId;
|
|
2801
|
+
if (!dispatcherId) {
|
|
2802
|
+
throw new Error("Mozaik System participant is not joined.");
|
|
2803
|
+
}
|
|
2124
2804
|
sendEvent(
|
|
2125
2805
|
{
|
|
2126
2806
|
type: "migration.started" /* MIGRATION_STARTED */,
|
|
2127
|
-
producerId:
|
|
2807
|
+
producerId: dispatcherId,
|
|
2128
2808
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
2129
2809
|
payload: {
|
|
2130
2810
|
planId,
|
|
@@ -2132,7 +2812,7 @@ var MigrationRunner = class {
|
|
|
2132
2812
|
shadowWorkspacePath: shadowPath
|
|
2133
2813
|
}
|
|
2134
2814
|
},
|
|
2135
|
-
|
|
2815
|
+
dispatcherId
|
|
2136
2816
|
);
|
|
2137
2817
|
return { planId, shadowPath, runId };
|
|
2138
2818
|
}
|
|
@@ -2148,15 +2828,19 @@ var MigrationRunner = class {
|
|
|
2148
2828
|
|
|
2149
2829
|
// ../application/src/index.ts
|
|
2150
2830
|
function bootstrapMetamorph(repository, tools = []) {
|
|
2831
|
+
registerBuiltinMigrationPlugins();
|
|
2151
2832
|
initializeRuntime({
|
|
2152
2833
|
state: new MetamorphState(repository)
|
|
2153
2834
|
});
|
|
2154
|
-
|
|
2835
|
+
const dispatcher = createHuman({ name: "System", capabilities: [], handlers: [] });
|
|
2836
|
+
join(dispatcher);
|
|
2837
|
+
resolveRuntime().state.dispatcherId = dispatcher.getId();
|
|
2838
|
+
class AllEventsSpecification extends SituationSpecification8 {
|
|
2155
2839
|
isSatisfiedBy() {
|
|
2156
2840
|
return true;
|
|
2157
2841
|
}
|
|
2158
2842
|
}
|
|
2159
|
-
const loggerAgent =
|
|
2843
|
+
const loggerAgent = createAgent8({
|
|
2160
2844
|
name: "TelemetryLogger",
|
|
2161
2845
|
capabilities: [],
|
|
2162
2846
|
instruction: "You silently log everything.",
|
|
@@ -2166,10 +2850,10 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2166
2850
|
specification: new AllEventsSpecification(),
|
|
2167
2851
|
processor: {
|
|
2168
2852
|
async apply({ event }) {
|
|
2169
|
-
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-
|
|
2853
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
2170
2854
|
const runtime = resolveRuntime2();
|
|
2171
2855
|
const repository2 = runtime.state.repository;
|
|
2172
|
-
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")) {
|
|
2173
2857
|
await repository2.logEvent(event.type, {
|
|
2174
2858
|
...event.payload,
|
|
2175
2859
|
producerId: event.producerId
|
|
@@ -2185,13 +2869,18 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2185
2869
|
const worker = createWorkerAgent(tools);
|
|
2186
2870
|
const reviewer = createReviewerAgent(tools);
|
|
2187
2871
|
const packageManager = createPackageManagerAgent();
|
|
2188
|
-
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);
|
|
2189
2876
|
join(mapper);
|
|
2190
2877
|
join(worker);
|
|
2191
2878
|
join(reviewer);
|
|
2192
2879
|
join(packageManager);
|
|
2880
|
+
join(coordinator);
|
|
2193
2881
|
join(reporter);
|
|
2194
|
-
|
|
2882
|
+
join(integrationAgent);
|
|
2883
|
+
console.log(`[App] Mozaik initialized. Agents joined: Mapper, Worker, Reviewer, PackageManager, Coordinator, Reporter, IntegrationAgent`);
|
|
2195
2884
|
return {
|
|
2196
2885
|
mapperId: mapper.getId(),
|
|
2197
2886
|
workerId: worker.getId(),
|
|
@@ -2211,9 +2900,11 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2211
2900
|
express: ["fastify", "nestjs"],
|
|
2212
2901
|
fastify: ["express", "nestjs"],
|
|
2213
2902
|
nestjs: ["express", "fastify"],
|
|
2214
|
-
react: ["next", "vue"],
|
|
2215
|
-
next: ["react"],
|
|
2216
|
-
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"]
|
|
2217
2908
|
};
|
|
2218
2909
|
if (!from) {
|
|
2219
2910
|
console.log(chalk.gray(`
|
|
@@ -2264,12 +2955,16 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2264
2955
|
const spinner = ora("Initializing...").start();
|
|
2265
2956
|
try {
|
|
2266
2957
|
const store = new SQLiteStateStore(".metamorph");
|
|
2958
|
+
const shadowBase = path11.resolve(".metamorph/shadow");
|
|
2267
2959
|
const tools = [
|
|
2268
|
-
createReadFileTool(),
|
|
2269
|
-
createWriteFileTool(),
|
|
2270
|
-
createRenameFileTool(),
|
|
2271
|
-
createCreateFileTool(),
|
|
2272
|
-
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)
|
|
2273
2968
|
];
|
|
2274
2969
|
const runner = new MigrationRunner(store, tools);
|
|
2275
2970
|
const result = await runner.startMigration({
|
|
@@ -2281,7 +2976,7 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2281
2976
|
console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
|
|
2282
2977
|
console.log(chalk.blue(`
|
|
2283
2978
|
\u23F3 Agents are now working...`));
|
|
2284
|
-
await new Promise((
|
|
2979
|
+
await new Promise((resolve5) => setTimeout(resolve5, 2e4));
|
|
2285
2980
|
console.log(chalk.green(`
|
|
2286
2981
|
\u2705 Migration simulation finished.`));
|
|
2287
2982
|
process.exit(0);
|
|
@@ -2291,11 +2986,11 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2291
2986
|
process.exit(1);
|
|
2292
2987
|
}
|
|
2293
2988
|
});
|
|
2294
|
-
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) => {
|
|
2295
2990
|
try {
|
|
2296
2991
|
const integrator = new MigrationIntegrator(new ShadowWorkspace());
|
|
2297
|
-
const
|
|
2298
|
-
console.log(chalk.green(`\u2705 ${message}`));
|
|
2992
|
+
const result = await integrator.applyMigration(runId, targetPath);
|
|
2993
|
+
console.log(chalk.green(`\u2705 ${result.message}`));
|
|
2299
2994
|
} catch (error) {
|
|
2300
2995
|
console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
2301
2996
|
process.exit(1);
|
|
@@ -2379,12 +3074,16 @@ program.command("ui").description("Start the Metamorph Dashboard API server").op
|
|
|
2379
3074
|
process.exit(1);
|
|
2380
3075
|
}
|
|
2381
3076
|
const store = new SQLiteStateStore(".metamorph");
|
|
3077
|
+
const shadowBase = path11.resolve(".metamorph/shadow");
|
|
2382
3078
|
const tools = [
|
|
2383
|
-
createReadFileTool(),
|
|
2384
|
-
createWriteFileTool(),
|
|
2385
|
-
createRenameFileTool(),
|
|
2386
|
-
createCreateFileTool(),
|
|
2387
|
-
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)
|
|
2388
3087
|
];
|
|
2389
3088
|
const runner = new MigrationRunner(store, tools);
|
|
2390
3089
|
const app = await createApiServer(store, runner);
|