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