@nikelyh/metamorph 2.1.0 → 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-H2LZEPNI.js +352 -0
- package/dist/{chunk-OXXEISZT.js → chunk-L7B6KUCW.js} +88 -39
- package/dist/index.js +529 -488
- package/dist/public/assets/index-C0AxAuiv.css +1 -0
- package/dist/public/assets/index-DtMWFmXl.js +63 -0
- package/dist/public/icons/angular.svg +1 -1
- package/dist/public/icons/expressjs.svg +1 -1
- package/dist/public/icons/fastify.svg +1 -1
- package/dist/public/icons/hapi.svg +1 -14
- package/dist/public/icons/nestjs.svg +1 -1
- package/dist/public/icons/nextjs.svg +1 -1
- package/dist/public/icons/react.svg +1 -1
- package/dist/public/icons/svelte.svg +1 -21
- package/dist/public/icons/vue.svg +1 -1
- package/dist/public/icons.svg +1 -24
- package/dist/public/index.html +2 -2
- package/dist/{src-2UXWWNPF.js → src-JHOGUCRS.js} +7 -1
- package/package.json +17 -4
- package/dist/public/assets/index-DDJRaEaa.css +0 -1
- package/dist/public/assets/index-PNJ1umlc.js +0 -63
package/dist/index.js
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
createRenameFileTool,
|
|
13
13
|
createRunBuildTool,
|
|
14
14
|
createWriteFileTool
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-L7B6KUCW.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
formatCatalogRules,
|
|
18
|
+
resolveMigrationCatalog
|
|
19
19
|
} from "./chunk-VEAYUIOZ.js";
|
|
20
20
|
import {
|
|
21
21
|
detectTechnologies
|
|
@@ -33,6 +33,16 @@ import {
|
|
|
33
33
|
collectFileHints,
|
|
34
34
|
registerMigrationPlugin
|
|
35
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";
|
|
36
46
|
|
|
37
47
|
// src/index.ts
|
|
38
48
|
import { Command } from "commander";
|
|
@@ -118,7 +128,7 @@ import {
|
|
|
118
128
|
createAgent as createAgent2,
|
|
119
129
|
SituationSpecification as SituationSpecification2
|
|
120
130
|
} from "@mozaik-ai/core";
|
|
121
|
-
import * as
|
|
131
|
+
import * as fs3 from "fs";
|
|
122
132
|
|
|
123
133
|
// ../application/src/utils/FileTreeBuilder.ts
|
|
124
134
|
import * as path2 from "path";
|
|
@@ -180,312 +190,15 @@ function buildFileTree(filePaths) {
|
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
// ../application/src/utils/NeighborContext.ts
|
|
183
|
-
import * as fs3 from "fs";
|
|
184
|
-
import * as path4 from "path";
|
|
185
|
-
|
|
186
|
-
// ../application/src/utils/NextMigrationHints.ts
|
|
187
193
|
import * as fs2 from "fs";
|
|
188
194
|
import * as path3 from "path";
|
|
189
|
-
function findShadowRoot(filePath) {
|
|
190
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
191
|
-
const marker = "/.metamorph/shadow/";
|
|
192
|
-
const index = normalized.indexOf(marker);
|
|
193
|
-
if (index === -1) return null;
|
|
194
|
-
const after = normalized.slice(index + marker.length);
|
|
195
|
-
const runId = after.split("/")[0];
|
|
196
|
-
if (!runId) return null;
|
|
197
|
-
return filePath.slice(0, index + marker.length + runId.length);
|
|
198
|
-
}
|
|
199
|
-
function isFsdPageComponent(filePath) {
|
|
200
|
-
return /[/\\]src[/\\]pages[/\\][^/\\]+[/\\]ui[/\\]/i.test(filePath);
|
|
201
|
-
}
|
|
202
|
-
function isSpaRootComponent(filePath) {
|
|
203
|
-
return /[/\\](App|main|index)\.(t|j)sx?$/i.test(filePath) && !/[/\\]src[/\\]app[/\\]/i.test(filePath) && !isFsdPageComponent(filePath);
|
|
204
|
-
}
|
|
205
|
-
function hasNextAppRoutes(shadowRoot) {
|
|
206
|
-
const appDir = path3.join(shadowRoot, "src", "app");
|
|
207
|
-
if (!fs2.existsSync(appDir)) return false;
|
|
208
|
-
const stack = [appDir];
|
|
209
|
-
while (stack.length > 0) {
|
|
210
|
-
const dir = stack.pop();
|
|
211
|
-
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
212
|
-
if (entry.isDirectory()) {
|
|
213
|
-
stack.push(path3.join(dir, entry.name));
|
|
214
|
-
} else if (entry.name === "page.tsx" || entry.name === "page.jsx" || entry.name === "page.js") {
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
function nextMigrationFileHint(filePath, target) {
|
|
222
|
-
if (target !== "next") return "";
|
|
223
|
-
if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
|
|
224
|
-
return `
|
|
225
|
-
This file sits under src/pages. In Next.js that directory is the Pages Router.
|
|
226
|
-
Move this file out of src/pages (keep the component and its business logic) into a folder Next does not treat as routes.
|
|
227
|
-
Then add or update src/app/<route>/page.tsx so the App Router imports that screen for the former URL.
|
|
228
|
-
The home route ("/") is src/app/page.tsx and must render the former "/" screen \u2014 not an empty shell, null, a TODO, or a deleted App.tsx.
|
|
229
|
-
Delete the old src/pages copy after the screen exists elsewhere.
|
|
230
|
-
`;
|
|
231
|
-
}
|
|
232
|
-
if (isSpaRootComponent(filePath)) {
|
|
233
|
-
return `
|
|
234
|
-
This is a SPA root/entry file (App.tsx / main.tsx / index.tsx).
|
|
235
|
-
Read it and map each route to src/app/<path>/page.tsx that imports the actual screen component (after moving anything that lived under src/pages).
|
|
236
|
-
src/app/page.tsx must render the home screen. Never import App from the new page (App is being removed).
|
|
237
|
-
Never write a stub that the root component "could not be resolved". After App Router files exist with real UI, delete this legacy root file.
|
|
238
|
-
`;
|
|
239
|
-
}
|
|
240
|
-
if (/[/\\]src[/\\]app[/\\].*page\.(t|j)sx$/i.test(filePath)) {
|
|
241
|
-
return `
|
|
242
|
-
This is a Next App Router page. Import the existing screen component and return it.
|
|
243
|
-
Do not paste that screen's JSX here. Do not rename its public props at the wrapper.
|
|
244
|
-
Do not hollow out the module you imported from.
|
|
245
|
-
`;
|
|
246
|
-
}
|
|
247
|
-
return "";
|
|
248
|
-
}
|
|
249
|
-
var NEXT_PAGES_RESERVED = /(?:^|[/\\])(_app|_document|_error|404|500)\.(t|j)sx?$/i;
|
|
250
|
-
function isSpaPagesTreeFile(filePath) {
|
|
251
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
252
|
-
if (!/\/src\/pages\//i.test(normalized)) return false;
|
|
253
|
-
if (/\/src\/pages\/api\//i.test(normalized)) return false;
|
|
254
|
-
if (NEXT_PAGES_RESERVED.test(normalized)) return false;
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
function listFilesRecursive(dir) {
|
|
258
|
-
if (!fs2.existsSync(dir)) return [];
|
|
259
|
-
const out = [];
|
|
260
|
-
const stack = [dir];
|
|
261
|
-
while (stack.length > 0) {
|
|
262
|
-
const current = stack.pop();
|
|
263
|
-
for (const entry of fs2.readdirSync(current, { withFileTypes: true })) {
|
|
264
|
-
const full = path3.join(current, entry.name);
|
|
265
|
-
if (entry.isDirectory()) stack.push(full);
|
|
266
|
-
else if (/\.(t|j)sx?$/.test(entry.name)) out.push(full);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
function findAppRouterHome(shadowRoot) {
|
|
272
|
-
const candidates = [
|
|
273
|
-
path3.join(shadowRoot, "src", "app", "page.tsx"),
|
|
274
|
-
path3.join(shadowRoot, "src", "app", "page.jsx"),
|
|
275
|
-
path3.join(shadowRoot, "app", "page.tsx"),
|
|
276
|
-
path3.join(shadowRoot, "app", "page.jsx")
|
|
277
|
-
];
|
|
278
|
-
return candidates.find((file) => fs2.existsSync(file)) || null;
|
|
279
|
-
}
|
|
280
|
-
function listAppRouterFiles(shadowRoot) {
|
|
281
|
-
const roots = [path3.join(shadowRoot, "src", "app"), path3.join(shadowRoot, "app")];
|
|
282
|
-
const names = /^(page|layout)\.(t|j)sx$/i;
|
|
283
|
-
return roots.flatMap((dir) => listFilesRecursive(dir).filter((file) => names.test(path3.basename(file))));
|
|
284
|
-
}
|
|
285
|
-
function isHollowView(content) {
|
|
286
|
-
const withoutComments = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
287
|
-
const hasFeatureImports = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(withoutComments);
|
|
288
|
-
const tinyTree = /<h1[^>]*>[^<]{0,40}<\/h1>/.test(withoutComments) && !hasFeatureImports;
|
|
289
|
-
const emptyMain = /<main>\s*<h1>/.test(withoutComments) && !hasFeatureImports;
|
|
290
|
-
return tinyTree || emptyMain;
|
|
291
|
-
}
|
|
292
|
-
function findViewPages(shadowRoot) {
|
|
293
|
-
const viewsDir = path3.join(shadowRoot, "src", "views");
|
|
294
|
-
return listFilesRecursive(viewsDir).filter((file) => /(?:^|[/\\])(?:.+Page|page)\.(t|j)sx$/i.test(file));
|
|
295
|
-
}
|
|
296
|
-
function localTsxImports(fromFile, content) {
|
|
297
|
-
const dir = path3.dirname(fromFile);
|
|
298
|
-
const out = [];
|
|
299
|
-
const re = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"](\.\.?\/[^'"]+|@\/[^'"]+)['"]/g;
|
|
300
|
-
let match;
|
|
301
|
-
while ((match = re.exec(content)) !== null) {
|
|
302
|
-
const resolved = match[3].startsWith("@/") ? path3.join(findSrcRoot(fromFile), match[3].slice(2)) : path3.resolve(dir, match[3]);
|
|
303
|
-
const candidates = [
|
|
304
|
-
resolved,
|
|
305
|
-
`${resolved}.tsx`,
|
|
306
|
-
`${resolved}.jsx`,
|
|
307
|
-
`${resolved}.ts`,
|
|
308
|
-
path3.join(resolved, "index.tsx"),
|
|
309
|
-
path3.join(resolved, "index.ts")
|
|
310
|
-
];
|
|
311
|
-
const absPath = candidates.find((file) => fs2.existsSync(file));
|
|
312
|
-
if (!absPath) continue;
|
|
313
|
-
if (match[2]) out.push({ tag: match[2], absPath });
|
|
314
|
-
if (match[1]) {
|
|
315
|
-
for (const part of match[1].split(",")) {
|
|
316
|
-
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
317
|
-
if (name) out.push({ tag: name, absPath });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
function findSrcRoot(filePath) {
|
|
324
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
325
|
-
const idx = normalized.lastIndexOf("/src/");
|
|
326
|
-
if (idx === -1) return path3.dirname(filePath);
|
|
327
|
-
return filePath.slice(0, idx + 4);
|
|
328
|
-
}
|
|
329
|
-
function declaredCallbackProps(componentSource) {
|
|
330
|
-
const names = /* @__PURE__ */ new Set();
|
|
331
|
-
const iface = /interface\s+\w+\s*\{([^}]+)\}/g;
|
|
332
|
-
let block;
|
|
333
|
-
while ((block = iface.exec(componentSource)) !== null) {
|
|
334
|
-
const field = /(\b(?:on[A-Z]\w+|action[A-Z]\w+)\??)\s*:/g;
|
|
335
|
-
let f;
|
|
336
|
-
while ((f = field.exec(block[1])) !== null) names.add(f[1].replace("?", ""));
|
|
337
|
-
}
|
|
338
|
-
const dest = /export\s+(?:default\s+)?function\s+\w+\s*\(\s*\{\s*([^}]+)\}/;
|
|
339
|
-
const d = dest.exec(componentSource);
|
|
340
|
-
if (d) {
|
|
341
|
-
for (const part of d[1].split(",")) {
|
|
342
|
-
const name = part.trim().split(":")[0].trim();
|
|
343
|
-
if (/^(on[A-Z]|action[A-Z])/.test(name)) names.add(name);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return [...names];
|
|
347
|
-
}
|
|
348
|
-
function jsxCallbackProps(callerSource, tag) {
|
|
349
|
-
const names = [];
|
|
350
|
-
const tagRe = new RegExp(`<${tag}\\b([^>]*)\\/?>`, "g");
|
|
351
|
-
let match;
|
|
352
|
-
while ((match = tagRe.exec(callerSource)) !== null) {
|
|
353
|
-
const propRe = /\b((?:on|action)[A-Z]\w+)\s*=/g;
|
|
354
|
-
let p;
|
|
355
|
-
while ((p = propRe.exec(match[1])) !== null) names.push(p[1]);
|
|
356
|
-
}
|
|
357
|
-
return names;
|
|
358
|
-
}
|
|
359
|
-
function analyzeCallbackPropMismatches(shadowRoot) {
|
|
360
|
-
const issues = [];
|
|
361
|
-
const files = [
|
|
362
|
-
...listFilesRecursive(path3.join(shadowRoot, "src", "app")),
|
|
363
|
-
...listFilesRecursive(path3.join(shadowRoot, "app")),
|
|
364
|
-
...listFilesRecursive(path3.join(shadowRoot, "src", "views"))
|
|
365
|
-
].filter((file) => /\.(t|j)sx$/.test(file));
|
|
366
|
-
for (const file of files) {
|
|
367
|
-
const content = fs2.readFileSync(file, "utf-8");
|
|
368
|
-
for (const imp of localTsxImports(file, content)) {
|
|
369
|
-
let callee;
|
|
370
|
-
try {
|
|
371
|
-
callee = fs2.readFileSync(imp.absPath, "utf-8");
|
|
372
|
-
} catch {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const expected = declaredCallbackProps(callee);
|
|
376
|
-
if (expected.length === 0) continue;
|
|
377
|
-
const passed = jsxCallbackProps(content, imp.tag);
|
|
378
|
-
const missing = expected.filter((name) => !passed.includes(name));
|
|
379
|
-
const extra = passed.filter((name) => !expected.includes(name));
|
|
380
|
-
if (missing.length === 0 && extra.length === 0) continue;
|
|
381
|
-
issues.push({
|
|
382
|
-
filePath: file,
|
|
383
|
-
errors: [
|
|
384
|
-
`<${imp.tag}> public callback props do not match ${path3.relative(shadowRoot, imp.absPath)}. Expected: ${expected.join(", ") || "(none)"}. Passed: ${passed.join(", ") || "(none)"}. Do not rename props when wrapping a screen in a router file.`
|
|
385
|
-
]
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return issues;
|
|
390
|
-
}
|
|
391
|
-
function analyzeReactToNextComposition(shadowRoot) {
|
|
392
|
-
const issues = [];
|
|
393
|
-
const home = findAppRouterHome(shadowRoot);
|
|
394
|
-
if (!home) return issues;
|
|
395
|
-
const homeContent = fs2.readFileSync(home, "utf-8");
|
|
396
|
-
const views = findViewPages(shadowRoot);
|
|
397
|
-
const hollowViews = views.filter((file) => isHollowView(fs2.readFileSync(file, "utf-8")));
|
|
398
|
-
const homeImportsView = views.some((file) => {
|
|
399
|
-
const stem = path3.basename(file).replace(/\.(t|j)sx$/, "");
|
|
400
|
-
return homeContent.includes(stem);
|
|
401
|
-
});
|
|
402
|
-
const homeAssemblesFeatures = /from\s+['"][^'"]*\/(features|entities|widgets)\//.test(homeContent);
|
|
403
|
-
if (views.length > 0 && !homeImportsView && homeAssemblesFeatures) {
|
|
404
|
-
issues.push({
|
|
405
|
-
filePath: home,
|
|
406
|
-
errors: [
|
|
407
|
-
"The App Router home reassembles the UI from lower-level modules instead of importing the existing route-level screen. Import that screen and return it. Renaming props at the wrapper causes runtime TypeError while next build still succeeds."
|
|
408
|
-
]
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
for (const file of hollowViews) {
|
|
412
|
-
issues.push({
|
|
413
|
-
filePath: file,
|
|
414
|
-
errors: [
|
|
415
|
-
"This screen was hollowed out. Restore the original UI here. The App Router page should import this file, not replace it with a stub heading."
|
|
416
|
-
]
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
return issues;
|
|
420
|
-
}
|
|
421
|
-
function isStubAppRouterPage(content) {
|
|
422
|
-
const hasLocalImport = /from\s+['"](\.\.?\/|@\/)/.test(content);
|
|
423
|
-
const emptyish = /return\s*\(\s*<main\s*\/>\s*\)|return\s+null\s*;?|return\s*\(\s*<>\s*<\/>\s*\)|TODO:|could not be resolved/i.test(content);
|
|
424
|
-
const onlyLayoutShell = /<main\s*\/>/.test(content) && !hasLocalImport;
|
|
425
|
-
return !hasLocalImport || emptyish || onlyLayoutShell;
|
|
426
|
-
}
|
|
427
|
-
function analyzeReactToNextStructure(shadowRoot) {
|
|
428
|
-
const issues = [];
|
|
429
|
-
const pagesDir = path3.join(shadowRoot, "src", "pages");
|
|
430
|
-
if (fs2.existsSync(pagesDir)) {
|
|
431
|
-
for (const file of listFilesRecursive(pagesDir)) {
|
|
432
|
-
if (!isSpaPagesTreeFile(file)) continue;
|
|
433
|
-
issues.push({
|
|
434
|
-
filePath: file,
|
|
435
|
-
errors: [
|
|
436
|
-
"This UI still lives under src/pages. Next.js will treat it as a Pages Router route. Move it to a folder Next does not reserve, then import it from src/app/**/page.tsx for the old URL. Remove the src/pages copy."
|
|
437
|
-
]
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
const home = findAppRouterHome(shadowRoot);
|
|
442
|
-
if (!home) {
|
|
443
|
-
issues.push({
|
|
444
|
-
filePath: path3.join(shadowRoot, "src", "app", "page.tsx"),
|
|
445
|
-
errors: [
|
|
446
|
-
'Missing App Router home (src/app/page.tsx). Create it so it imports and renders the screen the SPA showed at "/".'
|
|
447
|
-
]
|
|
448
|
-
});
|
|
449
|
-
return issues;
|
|
450
|
-
}
|
|
451
|
-
const homeContent = fs2.readFileSync(home, "utf-8");
|
|
452
|
-
if (isStubAppRouterPage(homeContent)) {
|
|
453
|
-
issues.push({
|
|
454
|
-
filePath: home,
|
|
455
|
-
errors: [
|
|
456
|
-
"src/app/page.tsx does not render the application UI. Import the former home screen component (after moving any SPA UI out of src/pages if needed). Do not return an empty shell, null, a TODO, or a deleted App.tsx."
|
|
457
|
-
]
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
issues.push(...analyzeReactToNextComposition(shadowRoot));
|
|
461
|
-
issues.push(...analyzeCallbackPropMismatches(shadowRoot));
|
|
462
|
-
const pkgPath = path3.join(shadowRoot, "package.json");
|
|
463
|
-
if (fs2.existsSync(pkgPath)) {
|
|
464
|
-
try {
|
|
465
|
-
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
466
|
-
const viteScripts = Object.entries(pkg.scripts || {}).filter(([, cmd]) => /\bvite\b/.test(cmd));
|
|
467
|
-
if (viteScripts.length > 0) {
|
|
468
|
-
issues.push({
|
|
469
|
-
filePath: pkgPath,
|
|
470
|
-
errors: [
|
|
471
|
-
`package.json still runs Vite in scripts (${viteScripts.map(([name]) => name).join(", ")}). After moving to Next, drop leftover preview/vite scripts.`
|
|
472
|
-
]
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
} catch {
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return issues;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ../application/src/utils/NeighborContext.ts
|
|
482
195
|
var MAX_FILES = 8;
|
|
483
196
|
var MAX_FILE_CHARS = 4e3;
|
|
484
197
|
var MAX_TOTAL_CHARS = 24e3;
|
|
485
198
|
function readTruncated(filePath) {
|
|
486
199
|
try {
|
|
487
|
-
if (!
|
|
488
|
-
const raw =
|
|
200
|
+
if (!fs2.existsSync(filePath) || !fs2.statSync(filePath).isFile()) return null;
|
|
201
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
489
202
|
if (raw.length <= MAX_FILE_CHARS) return raw;
|
|
490
203
|
return `${raw.slice(0, MAX_FILE_CHARS)}
|
|
491
204
|
|
|
@@ -494,18 +207,18 @@ function readTruncated(filePath) {
|
|
|
494
207
|
return null;
|
|
495
208
|
}
|
|
496
209
|
}
|
|
497
|
-
function
|
|
210
|
+
function findSrcRoot(filePath) {
|
|
498
211
|
const normalized = filePath.replace(/\\/g, "/");
|
|
499
212
|
const idx = normalized.lastIndexOf("/src/");
|
|
500
213
|
if (idx === -1) return null;
|
|
501
214
|
return filePath.slice(0, idx + 4);
|
|
502
215
|
}
|
|
503
216
|
function resolveImport(fromFile, spec) {
|
|
504
|
-
const dir =
|
|
217
|
+
const dir = path3.dirname(fromFile);
|
|
505
218
|
const base = spec.startsWith("@/") ? (() => {
|
|
506
|
-
const src =
|
|
507
|
-
return src ?
|
|
508
|
-
})() :
|
|
219
|
+
const src = findSrcRoot(fromFile);
|
|
220
|
+
return src ? path3.join(src, spec.slice(2)) : path3.resolve(dir, spec);
|
|
221
|
+
})() : path3.resolve(dir, spec);
|
|
509
222
|
const candidates = [
|
|
510
223
|
base,
|
|
511
224
|
`${base}.tsx`,
|
|
@@ -514,12 +227,12 @@ function resolveImport(fromFile, spec) {
|
|
|
514
227
|
`${base}.js`,
|
|
515
228
|
`${base}.vue`,
|
|
516
229
|
`${base}.svelte`,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
230
|
+
path3.join(base, "index.tsx"),
|
|
231
|
+
path3.join(base, "index.ts"),
|
|
232
|
+
path3.join(base, "index.vue"),
|
|
233
|
+
path3.join(base, "index.svelte")
|
|
521
234
|
];
|
|
522
|
-
return candidates.find((file) =>
|
|
235
|
+
return candidates.find((file) => fs2.existsSync(file) && fs2.statSync(file).isFile()) || null;
|
|
523
236
|
}
|
|
524
237
|
function localImportSpecs(content) {
|
|
525
238
|
const specs = [];
|
|
@@ -530,8 +243,8 @@ function localImportSpecs(content) {
|
|
|
530
243
|
}
|
|
531
244
|
function listNearbyModules(fromFile, limit) {
|
|
532
245
|
const shadow = findShadowRoot(fromFile);
|
|
533
|
-
const src =
|
|
534
|
-
if (!
|
|
246
|
+
const src = findSrcRoot(fromFile) || (shadow ? path3.join(shadow, "src") : path3.dirname(fromFile));
|
|
247
|
+
if (!fs2.existsSync(src)) return [];
|
|
535
248
|
const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
|
|
536
249
|
const out = [];
|
|
537
250
|
const stack = [src];
|
|
@@ -539,13 +252,13 @@ function listNearbyModules(fromFile, limit) {
|
|
|
539
252
|
const dir = stack.pop();
|
|
540
253
|
let entries;
|
|
541
254
|
try {
|
|
542
|
-
entries =
|
|
255
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
543
256
|
} catch {
|
|
544
257
|
continue;
|
|
545
258
|
}
|
|
546
259
|
for (const entry of entries) {
|
|
547
260
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
548
|
-
const full =
|
|
261
|
+
const full = path3.join(dir, entry.name);
|
|
549
262
|
if (entry.isDirectory()) stack.push(full);
|
|
550
263
|
else if (interesting.test(entry.name)) out.push(full);
|
|
551
264
|
if (out.length >= limit) break;
|
|
@@ -575,15 +288,15 @@ ${body}
|
|
|
575
288
|
`);
|
|
576
289
|
}
|
|
577
290
|
const importSpecs = self ? localImportSpecs(self) : [];
|
|
578
|
-
const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !==
|
|
291
|
+
const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path3.resolve(filePath)));
|
|
579
292
|
const unique = [...new Set(resolved)].slice(0, MAX_FILES);
|
|
580
293
|
for (const dep of unique) {
|
|
581
294
|
const body = readTruncated(dep);
|
|
582
295
|
if (body) push(`imported: ${dep}`, body);
|
|
583
296
|
}
|
|
584
|
-
const nearby = listNearbyModules(filePath, 24).filter((file) =>
|
|
297
|
+
const nearby = listNearbyModules(filePath, 24).filter((file) => path3.resolve(file) !== path3.resolve(filePath));
|
|
585
298
|
if (nearby.length > 0) {
|
|
586
|
-
const listing = nearby.map((file) =>
|
|
299
|
+
const listing = nearby.map((file) => path3.relative(findShadowRoot(filePath) || path3.dirname(filePath), file)).join("\n");
|
|
587
300
|
sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
|
|
588
301
|
${listing}
|
|
589
302
|
`);
|
|
@@ -594,6 +307,12 @@ Evidence from disk (read this before editing; match existing exports and callbac
|
|
|
594
307
|
${sections.join("\n")}`;
|
|
595
308
|
}
|
|
596
309
|
|
|
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
|
+
|
|
597
316
|
// ../application/src/agents/WorkerAgent.ts
|
|
598
317
|
var MAX_RETRIES = 2;
|
|
599
318
|
var ConcurrencyQueue = class {
|
|
@@ -648,7 +367,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
|
|
|
648
367
|
async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
649
368
|
return new Promise(async (originalResolve) => {
|
|
650
369
|
let isDone = false;
|
|
651
|
-
const
|
|
370
|
+
const resolve4 = () => {
|
|
652
371
|
if (!isDone) {
|
|
653
372
|
isDone = true;
|
|
654
373
|
originalResolve();
|
|
@@ -658,28 +377,51 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
658
377
|
const repository = runtime.state.repository;
|
|
659
378
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
660
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
|
+
}
|
|
661
387
|
const tempAgent = createMozaikAgent({
|
|
662
388
|
name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
|
|
663
389
|
capabilities: ["code_refactoring", "inference"],
|
|
664
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.",
|
|
665
|
-
tools:
|
|
391
|
+
tools: workerTools,
|
|
666
392
|
handlers: [
|
|
667
393
|
{
|
|
668
394
|
specification: new WhenInferenceCompleted(),
|
|
669
395
|
processor: {
|
|
670
|
-
async apply({
|
|
671
|
-
|
|
672
|
-
|
|
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 = "";
|
|
673
418
|
try {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
|
|
677
|
-
} else {
|
|
678
|
-
console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk: ${filePath}`);
|
|
679
|
-
}
|
|
419
|
+
fileContent = fs3.readFileSync(filePath, "utf-8");
|
|
420
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
|
|
680
421
|
} catch (e) {
|
|
681
422
|
console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
|
|
682
423
|
}
|
|
424
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
|
|
683
425
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
684
426
|
sendEvent(
|
|
685
427
|
{
|
|
@@ -695,7 +437,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
695
437
|
tempParticipant.getId()
|
|
696
438
|
);
|
|
697
439
|
leave(tempParticipant);
|
|
698
|
-
|
|
440
|
+
resolve4();
|
|
699
441
|
}
|
|
700
442
|
}
|
|
701
443
|
}
|
|
@@ -712,18 +454,18 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
712
454
|
});
|
|
713
455
|
setTimeout(async () => {
|
|
714
456
|
if (!isDone) {
|
|
715
|
-
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
|
|
716
|
-
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");
|
|
717
459
|
sendEvent({
|
|
718
460
|
type: "file.failed" /* FILE_FAILED */,
|
|
719
461
|
producerId: tempAgent.getId(),
|
|
720
462
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
721
|
-
payload: { planId, filePath, reason: "Inference timed out" }
|
|
463
|
+
payload: { planId, filePath, reason: "Inference timed out after 120s" }
|
|
722
464
|
}, tempAgent.getId());
|
|
723
465
|
leave(tempAgent);
|
|
724
|
-
|
|
466
|
+
resolve4();
|
|
725
467
|
}
|
|
726
|
-
},
|
|
468
|
+
}, 12e4);
|
|
727
469
|
} catch (error) {
|
|
728
470
|
console.error(`[WorkerAgent:${tempAgent.getId()}] Sync error:`, error);
|
|
729
471
|
if (!isDone) {
|
|
@@ -736,7 +478,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
736
478
|
payload: { planId, filePath, reason: errorMessage || "Sync error" }
|
|
737
479
|
}, tempAgent.getId());
|
|
738
480
|
leave(tempAgent);
|
|
739
|
-
|
|
481
|
+
resolve4();
|
|
740
482
|
}
|
|
741
483
|
}
|
|
742
484
|
});
|
|
@@ -752,7 +494,7 @@ var workOnFileProcessor = {
|
|
|
752
494
|
if (plan) {
|
|
753
495
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
754
496
|
`;
|
|
755
|
-
const catalogEntry =
|
|
497
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
756
498
|
if (catalogEntry) {
|
|
757
499
|
prompt += `
|
|
758
500
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
@@ -858,7 +600,7 @@ ${payload.errors.join("\n")}
|
|
|
858
600
|
if (plan) {
|
|
859
601
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
860
602
|
`;
|
|
861
|
-
const catalogEntry =
|
|
603
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
862
604
|
if (catalogEntry) {
|
|
863
605
|
prompt += `
|
|
864
606
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
@@ -934,6 +676,115 @@ import {
|
|
|
934
676
|
SituationSpecification as SituationSpecification3
|
|
935
677
|
} from "@mozaik-ai/core";
|
|
936
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
|
|
937
788
|
var ConcurrencyQueue2 = class {
|
|
938
789
|
constructor(concurrencyLimit) {
|
|
939
790
|
this.concurrencyLimit = concurrencyLimit;
|
|
@@ -970,15 +821,9 @@ var WhenFileMigrated = class extends SituationSpecification3 {
|
|
|
970
821
|
};
|
|
971
822
|
var WhenReviewCompleted = class extends SituationSpecification3 {
|
|
972
823
|
isSatisfiedBy({ event, participant }) {
|
|
973
|
-
console.log(`[DEBUG:WhenReviewCompleted] Event received -> type: "${event.type}", producerId: "${event.producerId}", participantId: "${participant.getId()}", match: ${event.type === "model.answer" && event.producerId === participant.getId()}`);
|
|
974
824
|
return event.type === "model.answer" && event.producerId === participant.getId();
|
|
975
825
|
}
|
|
976
826
|
};
|
|
977
|
-
var WhenAnythingHappens = class extends SituationSpecification3 {
|
|
978
|
-
isSatisfiedBy() {
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
827
|
var reviewFileProcessor = {
|
|
983
828
|
async apply({ event, participant }) {
|
|
984
829
|
const payload = event.payload;
|
|
@@ -986,10 +831,9 @@ var reviewFileProcessor = {
|
|
|
986
831
|
reviewerQueue.enqueue(async () => {
|
|
987
832
|
return new Promise(async (originalResolve) => {
|
|
988
833
|
let isDone = false;
|
|
989
|
-
const
|
|
834
|
+
const resolve4 = () => {
|
|
990
835
|
if (!isDone) {
|
|
991
836
|
isDone = true;
|
|
992
|
-
console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
|
|
993
837
|
originalResolve();
|
|
994
838
|
}
|
|
995
839
|
};
|
|
@@ -1003,33 +847,22 @@ var reviewFileProcessor = {
|
|
|
1003
847
|
tools: [],
|
|
1004
848
|
// No tools to avoid Gemini thought_signature crash
|
|
1005
849
|
handlers: [
|
|
1006
|
-
// DEBUG: Catch-all handler to log every event this agent receives
|
|
1007
|
-
{
|
|
1008
|
-
specification: new WhenAnythingHappens(),
|
|
1009
|
-
processor: {
|
|
1010
|
-
async apply({ event: anyEvent, participant: anyParticipant }) {
|
|
1011
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}:CATCH_ALL] Event -> type: "${anyEvent.type}", producerId: "${anyEvent.producerId}", myId: "${anyParticipant.getId()}", payload keys: ${Object.keys(anyEvent.payload || {}).join(",")}`);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
850
|
{
|
|
1016
851
|
specification: new WhenReviewCompleted(),
|
|
1017
852
|
processor: {
|
|
1018
853
|
async apply({ event: event2, participant: tempParticipant }) {
|
|
1019
|
-
|
|
854
|
+
if (isDone) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
1020
857
|
const answerItem = event2.payload.answer;
|
|
1021
858
|
const answerText = answerItem?.content?.text || answerItem?.text || null;
|
|
1022
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] answerText:`, answerText);
|
|
1023
859
|
let reviewResult = null;
|
|
1024
860
|
if (answerText) {
|
|
1025
861
|
try {
|
|
1026
862
|
reviewResult = JSON.parse(answerText);
|
|
1027
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Parsed result:`, JSON.stringify(reviewResult));
|
|
1028
863
|
} catch (e) {
|
|
1029
864
|
console.error(`[ReviewerAgent:${reviewerId}] Failed to parse JSON:`, e);
|
|
1030
865
|
}
|
|
1031
|
-
} else {
|
|
1032
|
-
console.warn(`[DEBUG:ReviewerAgent:${reviewerId}] answerItem has no extractable text.`);
|
|
1033
866
|
}
|
|
1034
867
|
if (reviewResult) {
|
|
1035
868
|
const participantId = tempParticipant.getId();
|
|
@@ -1066,28 +899,39 @@ var reviewFileProcessor = {
|
|
|
1066
899
|
}
|
|
1067
900
|
}
|
|
1068
901
|
leave(tempParticipant);
|
|
1069
|
-
|
|
902
|
+
resolve4();
|
|
1070
903
|
}
|
|
1071
904
|
}
|
|
1072
905
|
}
|
|
1073
906
|
]
|
|
1074
907
|
});
|
|
1075
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent created with id: ${tempAgent.getId()}`);
|
|
1076
908
|
join(tempAgent);
|
|
1077
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
|
|
1078
909
|
const runtime = resolveRuntime();
|
|
1079
910
|
const { existsSync: existsSync8 } = await import("fs");
|
|
1080
911
|
if (!existsSync8(payload.filePath)) {
|
|
1081
912
|
const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
|
|
1082
913
|
const planForMissing = await runtime.state.repository.getPlan(payload.planId);
|
|
1083
|
-
const
|
|
914
|
+
const sourceFramework = planForMissing?.profile.source || "react";
|
|
915
|
+
const targetFramework = planForMissing?.profile.target || "next";
|
|
1084
916
|
const shadowRoot = findShadowRoot(payload.filePath);
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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);
|
|
1091
935
|
sendEvent4({
|
|
1092
936
|
type: "file.rejected" /* FILE_REJECTED */,
|
|
1093
937
|
producerId: tempAgent.getId(),
|
|
@@ -1095,27 +939,12 @@ var reviewFileProcessor = {
|
|
|
1095
939
|
payload: {
|
|
1096
940
|
planId: payload.planId,
|
|
1097
941
|
filePath: payload.filePath,
|
|
1098
|
-
errors:
|
|
1099
|
-
"This file lived under src/pages as SPA UI. Next.js reserves src/pages for the Pages Router \u2014 move the tree to a non-reserved folder and import those screens from src/app. Do not delete the UI without a replacement."
|
|
1100
|
-
] : [
|
|
1101
|
-
"The SPA root was deleted before src/app/**/page.tsx existed with real UI. Recreate routing as Next App Router pages that import the original screen components. Do not import App from those pages."
|
|
1102
|
-
]
|
|
942
|
+
errors: classification.errors || ["File does not exist and has no demonstrable replacement"]
|
|
1103
943
|
}
|
|
1104
944
|
}, tempAgent.getId());
|
|
1105
|
-
leave(tempAgent);
|
|
1106
|
-
resolve5();
|
|
1107
|
-
return;
|
|
1108
945
|
}
|
|
1109
|
-
console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} does not exist on disk (likely intentionally deleted/moved). Auto-approving.`);
|
|
1110
|
-
await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
|
|
1111
|
-
sendEvent4({
|
|
1112
|
-
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
1113
|
-
producerId: tempAgent.getId(),
|
|
1114
|
-
occurredAt: /* @__PURE__ */ new Date(),
|
|
1115
|
-
payload: { planId: payload.planId, filePath: payload.filePath }
|
|
1116
|
-
}, tempAgent.getId());
|
|
1117
946
|
leave(tempAgent);
|
|
1118
|
-
|
|
947
|
+
resolve4();
|
|
1119
948
|
return;
|
|
1120
949
|
}
|
|
1121
950
|
if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
|
|
@@ -1139,7 +968,7 @@ var reviewFileProcessor = {
|
|
|
1139
968
|
}
|
|
1140
969
|
}, tempAgent.getId());
|
|
1141
970
|
leave(tempAgent);
|
|
1142
|
-
|
|
971
|
+
resolve4();
|
|
1143
972
|
return;
|
|
1144
973
|
}
|
|
1145
974
|
} catch (e) {
|
|
@@ -1164,7 +993,7 @@ var reviewFileProcessor = {
|
|
|
1164
993
|
payload: { planId: payload.planId, filePath: payload.filePath, errors }
|
|
1165
994
|
}, tempAgent.getId());
|
|
1166
995
|
leave(tempAgent);
|
|
1167
|
-
|
|
996
|
+
resolve4();
|
|
1168
997
|
return;
|
|
1169
998
|
}
|
|
1170
999
|
}
|
|
@@ -1174,7 +1003,7 @@ var reviewFileProcessor = {
|
|
|
1174
1003
|
if (plan) {
|
|
1175
1004
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
1176
1005
|
`;
|
|
1177
|
-
const catalogEntry =
|
|
1006
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
1178
1007
|
if (catalogEntry) {
|
|
1179
1008
|
prompt += `
|
|
1180
1009
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
|
|
@@ -1196,7 +1025,6 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1196
1025
|
const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
1197
1026
|
console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
|
|
1198
1027
|
try {
|
|
1199
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
|
|
1200
1028
|
runLoop(tempAgent.getId(), prompt, {
|
|
1201
1029
|
model: modelToUse,
|
|
1202
1030
|
context: tempAgent.getMemory().getContext(),
|
|
@@ -1214,23 +1042,22 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1214
1042
|
strict: true
|
|
1215
1043
|
}
|
|
1216
1044
|
});
|
|
1217
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
|
|
1218
1045
|
setTimeout(async () => {
|
|
1219
1046
|
if (!isDone) {
|
|
1220
|
-
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after
|
|
1047
|
+
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 120s for ${payload.filePath}. Model never responded.`);
|
|
1221
1048
|
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
1222
1049
|
const runtime2 = resolveRuntime2();
|
|
1223
|
-
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");
|
|
1224
1051
|
sendEvent3({
|
|
1225
1052
|
type: "file.failed" /* FILE_FAILED */,
|
|
1226
1053
|
producerId: tempAgent.getId(),
|
|
1227
1054
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1228
|
-
payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after
|
|
1055
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 120s" }
|
|
1229
1056
|
}, tempAgent.getId());
|
|
1230
1057
|
leave(tempAgent);
|
|
1231
|
-
|
|
1058
|
+
resolve4();
|
|
1232
1059
|
}
|
|
1233
|
-
},
|
|
1060
|
+
}, 12e4);
|
|
1234
1061
|
} catch (error) {
|
|
1235
1062
|
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
1236
1063
|
if (!isDone) {
|
|
@@ -1245,7 +1072,7 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1245
1072
|
payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
|
|
1246
1073
|
}, tempAgent.getId());
|
|
1247
1074
|
leave(tempAgent);
|
|
1248
|
-
|
|
1075
|
+
resolve4();
|
|
1249
1076
|
}
|
|
1250
1077
|
}
|
|
1251
1078
|
});
|
|
@@ -1312,7 +1139,7 @@ var managePackagesProcessor = {
|
|
|
1312
1139
|
await emitPackagesReady("failed");
|
|
1313
1140
|
return;
|
|
1314
1141
|
}
|
|
1315
|
-
const catalogEntry =
|
|
1142
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
1316
1143
|
if (!catalogEntry) {
|
|
1317
1144
|
console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
|
|
1318
1145
|
await emitPackagesReady("failed");
|
|
@@ -1509,10 +1336,26 @@ var startWatchdogProcessor = {
|
|
|
1509
1336
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1510
1337
|
payload: {
|
|
1511
1338
|
planId: p.planId,
|
|
1512
|
-
message: "Coordinator watchdog
|
|
1513
|
-
level: "
|
|
1339
|
+
message: "Coordinator watchdog timed out after 30 minutes. Integration never became ready or the run stalled.",
|
|
1340
|
+
level: "error"
|
|
1514
1341
|
}
|
|
1515
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
|
+
}
|
|
1516
1359
|
return;
|
|
1517
1360
|
}
|
|
1518
1361
|
await emitIntegrationIfReady(producerId, p.planId);
|
|
@@ -1552,7 +1395,7 @@ function log(participantId, planId, message, level = "info") {
|
|
|
1552
1395
|
}
|
|
1553
1396
|
async function shadowPathFor(plan, journal) {
|
|
1554
1397
|
if (journal?.shadowPath) return journal.shadowPath;
|
|
1555
|
-
const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-
|
|
1398
|
+
const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-JHOGUCRS.js");
|
|
1556
1399
|
return new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
1557
1400
|
}
|
|
1558
1401
|
function fallbackReport(params) {
|
|
@@ -1824,17 +1667,19 @@ var completedProcessor = {
|
|
|
1824
1667
|
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1825
1668
|
if (!plan) return;
|
|
1826
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";
|
|
1827
1672
|
const draft = fallbackReport({
|
|
1828
1673
|
source: plan.profile.source,
|
|
1829
1674
|
target: plan.profile.target,
|
|
1830
1675
|
targetPath: plan.targetPath,
|
|
1831
1676
|
runId: plan.runId,
|
|
1832
|
-
phase:
|
|
1677
|
+
phase: effectivePhase,
|
|
1833
1678
|
tasks: plan.tasks || [],
|
|
1834
1679
|
notes: board.notes,
|
|
1835
1680
|
startedAt: board.startedAt
|
|
1836
1681
|
});
|
|
1837
|
-
board.pendingStatusPhase =
|
|
1682
|
+
board.pendingStatusPhase = isFailed ? "failed" : "completed";
|
|
1838
1683
|
try {
|
|
1839
1684
|
const path12 = await import("path");
|
|
1840
1685
|
const dest = path12.join(shadowPath, "MIGRATION.md");
|
|
@@ -1842,7 +1687,7 @@ var completedProcessor = {
|
|
|
1842
1687
|
log(participant.getId(), p.planId, "Reporter draft is still running; Status will be patched when it finishes (no second model call).");
|
|
1843
1688
|
return;
|
|
1844
1689
|
}
|
|
1845
|
-
const result = await writeOrPatchMigrationMd(dest,
|
|
1690
|
+
const result = await writeOrPatchMigrationMd(dest, effectivePhase, draft);
|
|
1846
1691
|
board.pendingStatusPhase = void 0;
|
|
1847
1692
|
log(
|
|
1848
1693
|
participant.getId(),
|
|
@@ -1890,44 +1735,81 @@ async function completeMigration(participantId, planId, message, level) {
|
|
|
1890
1735
|
const runtime = resolveRuntime();
|
|
1891
1736
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1892
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
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (swept > 0) {
|
|
1750
|
+
console.log(
|
|
1751
|
+
`[IntegrationAgent] Swept ${swept} residual failed task(s) \u2192 completed (shadow build passed).`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1893
1754
|
plan.phase = "completed";
|
|
1755
|
+
plan.outcome = "success";
|
|
1894
1756
|
await runtime.state.repository.savePlan(plan);
|
|
1895
1757
|
}
|
|
1896
|
-
sendEvent(
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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"
|
|
1775
|
+
}
|
|
1776
|
+
},
|
|
1777
|
+
participantId
|
|
1778
|
+
);
|
|
1908
1779
|
}
|
|
1909
1780
|
function log2(participantId, planId, message, level = "info") {
|
|
1910
|
-
sendEvent(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
+
);
|
|
1916
1790
|
}
|
|
1917
1791
|
async function failMigration(participantId, planId, message) {
|
|
1918
1792
|
const runtime = resolveRuntime();
|
|
1919
1793
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1920
1794
|
if (plan) {
|
|
1921
1795
|
plan.phase = "failed";
|
|
1796
|
+
plan.outcome = "failed";
|
|
1922
1797
|
await runtime.state.repository.savePlan(plan);
|
|
1923
1798
|
}
|
|
1924
1799
|
log2(participantId, planId, message, "error");
|
|
1925
|
-
sendEvent(
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
+
);
|
|
1931
1813
|
}
|
|
1932
1814
|
async function requeueBrokenFiles(producerId, planId, files, errors) {
|
|
1933
1815
|
const runtime = resolveRuntime();
|
|
@@ -1945,17 +1827,20 @@ async function requeueBrokenFiles(producerId, planId, files, errors) {
|
|
|
1945
1827
|
"pending",
|
|
1946
1828
|
errors.slice(0, 3).join("\n").slice(0, 1500)
|
|
1947
1829
|
);
|
|
1948
|
-
sendEvent(
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
+
);
|
|
1959
1844
|
}
|
|
1960
1845
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1961
1846
|
if (plan) {
|
|
@@ -1970,7 +1855,9 @@ var integrationProcessor = {
|
|
|
1970
1855
|
const producerId = participant.getId();
|
|
1971
1856
|
const locks = resolveRuntime().state.integrationLocks;
|
|
1972
1857
|
if (locks.has(planId)) {
|
|
1973
|
-
console.log(
|
|
1858
|
+
console.log(
|
|
1859
|
+
`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`
|
|
1860
|
+
);
|
|
1974
1861
|
return;
|
|
1975
1862
|
}
|
|
1976
1863
|
locks.add(planId);
|
|
@@ -1982,7 +1869,9 @@ var integrationProcessor = {
|
|
|
1982
1869
|
return;
|
|
1983
1870
|
}
|
|
1984
1871
|
if (plan.phase === "completed" || plan.phase === "failed") {
|
|
1985
|
-
console.log(
|
|
1872
|
+
console.log(
|
|
1873
|
+
`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`
|
|
1874
|
+
);
|
|
1986
1875
|
return;
|
|
1987
1876
|
}
|
|
1988
1877
|
plan.phase = "integration";
|
|
@@ -1998,37 +1887,100 @@ var integrationProcessor = {
|
|
|
1998
1887
|
plan.integrationRounds = currentRound + 1;
|
|
1999
1888
|
await runtime.state.repository.savePlan(plan);
|
|
2000
1889
|
const lastRound = plan.integrationRounds >= MAX_INTEGRATION_ROUNDS;
|
|
2001
|
-
const {
|
|
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");
|
|
2002
1899
|
const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
|
|
2003
|
-
const {
|
|
1900
|
+
const { resolveMigrationCatalog: resolveMigrationCatalog2 } = await import("./src-UC3SN46X.js");
|
|
2004
1901
|
const shadowPath = payload.shadowWorkspacePath || new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
2005
|
-
console.log(
|
|
2006
|
-
|
|
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
|
+
);
|
|
2007
1910
|
console.log(`[IntegrationAgent] npm install starting in ${shadowPath}`);
|
|
2008
1911
|
let lastInstallBeat = Date.now();
|
|
2009
|
-
const install = await runInShadowWorkspace(
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
+
}
|
|
2014
1925
|
}
|
|
2015
|
-
|
|
1926
|
+
);
|
|
2016
1927
|
if (!install.ok) {
|
|
2017
|
-
console.warn(
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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;
|
|
1945
|
+
}
|
|
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;
|
|
2021
1956
|
}
|
|
1957
|
+
log2(
|
|
1958
|
+
producerId,
|
|
1959
|
+
planId,
|
|
1960
|
+
"npm install finished. Next: npm run build in the shadow workspace."
|
|
1961
|
+
);
|
|
2022
1962
|
let lastBuildBeat = Date.now();
|
|
2023
|
-
let build = await runInShadowWorkspace(
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
+
}
|
|
2028
1976
|
}
|
|
2029
|
-
|
|
1977
|
+
);
|
|
2030
1978
|
if (!build.ok) {
|
|
2031
|
-
log2(
|
|
1979
|
+
log2(
|
|
1980
|
+
producerId,
|
|
1981
|
+
planId,
|
|
1982
|
+
"Shadow build failed. IntegrationAgent will attempt local repairs before requeueing files."
|
|
1983
|
+
);
|
|
2032
1984
|
const shadowTools = [
|
|
2033
1985
|
createReadFileTool2(shadowPath),
|
|
2034
1986
|
createWriteFileTool2(shadowPath),
|
|
@@ -2038,7 +1990,7 @@ var integrationProcessor = {
|
|
|
2038
1990
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
2039
1991
|
await new Promise((originalResolve) => {
|
|
2040
1992
|
let isDone = false;
|
|
2041
|
-
const
|
|
1993
|
+
const resolve4 = () => {
|
|
2042
1994
|
if (!isDone) {
|
|
2043
1995
|
isDone = true;
|
|
2044
1996
|
originalResolve();
|
|
@@ -2063,32 +2015,51 @@ Build output:
|
|
|
2063
2015
|
${excerpt}
|
|
2064
2016
|
|
|
2065
2017
|
When you have applied fixes (or cannot fix further), stop. A deterministic rebuild will run after you finish.`;
|
|
2066
|
-
Promise.resolve(
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2018
|
+
Promise.resolve(
|
|
2019
|
+
runLoop(tempAgent.getId(), prompt, {
|
|
2020
|
+
model: modelToUse,
|
|
2021
|
+
context: tempAgent.getMemory().getContext(),
|
|
2022
|
+
tools: shadowTools
|
|
2023
|
+
})
|
|
2024
|
+
).catch((error) => {
|
|
2071
2025
|
console.error("[IntegrationAgent] runLoop error:", error);
|
|
2072
2026
|
leave(tempAgent);
|
|
2073
|
-
|
|
2027
|
+
resolve4();
|
|
2074
2028
|
});
|
|
2075
2029
|
setTimeout(() => {
|
|
2076
2030
|
if (!isDone) {
|
|
2077
|
-
console.warn(
|
|
2031
|
+
console.warn(
|
|
2032
|
+
"[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild."
|
|
2033
|
+
);
|
|
2078
2034
|
leave(tempAgent);
|
|
2079
|
-
|
|
2035
|
+
resolve4();
|
|
2080
2036
|
}
|
|
2081
2037
|
}, REPAIR_TIMEOUT_MS);
|
|
2082
2038
|
});
|
|
2083
2039
|
build = await runInShadowWorkspace(shadowPath, "npm run build");
|
|
2084
2040
|
}
|
|
2085
|
-
const pluginCtx = {
|
|
2086
|
-
|
|
2087
|
-
|
|
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
|
+
);
|
|
2088
2054
|
const buildFailed = !build.ok || !install.ok;
|
|
2089
2055
|
const verifierFailed = structureIssues.length > 0;
|
|
2090
2056
|
if (!buildFailed && !verifierFailed) {
|
|
2091
|
-
await completeMigration(
|
|
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
|
+
);
|
|
2092
2063
|
return;
|
|
2093
2064
|
}
|
|
2094
2065
|
const implicated = parseImplicatedFiles(
|
|
@@ -2115,7 +2086,12 @@ ${build.output.slice(0, 2500)}`]
|
|
|
2115
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.`,
|
|
2116
2087
|
"warning"
|
|
2117
2088
|
);
|
|
2118
|
-
await requeueBrokenFiles(
|
|
2089
|
+
await requeueBrokenFiles(
|
|
2090
|
+
producerId,
|
|
2091
|
+
planId,
|
|
2092
|
+
repairFiles,
|
|
2093
|
+
errors.length > 0 ? errors : ["Shadow verification failed."]
|
|
2094
|
+
);
|
|
2119
2095
|
return;
|
|
2120
2096
|
}
|
|
2121
2097
|
await failMigration(
|
|
@@ -2125,7 +2101,11 @@ ${build.output.slice(0, 2500)}`]
|
|
|
2125
2101
|
);
|
|
2126
2102
|
} catch (error) {
|
|
2127
2103
|
console.error(`[IntegrationAgent] Error:`, error);
|
|
2128
|
-
await failMigration(
|
|
2104
|
+
await failMigration(
|
|
2105
|
+
producerId,
|
|
2106
|
+
planId,
|
|
2107
|
+
"Integration Phase encountered an error. The migration is not successful."
|
|
2108
|
+
);
|
|
2129
2109
|
} finally {
|
|
2130
2110
|
resolveRuntime().state.integrationLocks.delete(planId);
|
|
2131
2111
|
}
|
|
@@ -2781,8 +2761,9 @@ var MigrationRunner = class {
|
|
|
2781
2761
|
*/
|
|
2782
2762
|
async startMigration(request) {
|
|
2783
2763
|
this.ensureInitialized();
|
|
2784
|
-
await this.store.
|
|
2785
|
-
|
|
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);
|
|
2786
2767
|
const runId = `run_${Date.now()}`;
|
|
2787
2768
|
const shadowPath = this.workspace.cloneDirectory(request.targetPath, runId);
|
|
2788
2769
|
const planId = `plan_${randomUUID()}`;
|
|
@@ -2791,6 +2772,7 @@ var MigrationRunner = class {
|
|
|
2791
2772
|
runId,
|
|
2792
2773
|
profile: { source: request.from, target: request.to },
|
|
2793
2774
|
targetPath: request.targetPath,
|
|
2775
|
+
phase: "files",
|
|
2794
2776
|
tasks: [
|
|
2795
2777
|
{ filePath: "system:package_manager", status: "pending" }
|
|
2796
2778
|
],
|
|
@@ -2889,9 +2871,18 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2889
2871
|
}
|
|
2890
2872
|
|
|
2891
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
|
+
}
|
|
2892
2882
|
var program = new Command();
|
|
2893
|
-
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");
|
|
2894
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();
|
|
2895
2886
|
let targetPath = targetPathArg || ".";
|
|
2896
2887
|
const { select, input } = await import("@inquirer/prompts");
|
|
2897
2888
|
let from = options.from;
|
|
@@ -2975,11 +2966,41 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2975
2966
|
spinner.succeed(`Migration dispatched! Plan: ${result.planId}`);
|
|
2976
2967
|
console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
|
|
2977
2968
|
console.log(chalk.blue(`
|
|
2978
|
-
\u23F3
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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
|
+
}
|
|
2983
3004
|
} catch (error) {
|
|
2984
3005
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2985
3006
|
spinner.fail(`Migration failed: ${errorMessage}`);
|
|
@@ -2988,8 +3009,27 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2988
3009
|
});
|
|
2989
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) => {
|
|
2990
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
|
+
}
|
|
2991
3028
|
const integrator = new MigrationIntegrator(new ShadowWorkspace());
|
|
2992
3029
|
const result = await integrator.applyMigration(runId, targetPath);
|
|
3030
|
+
plan.appliedAt = /* @__PURE__ */ new Date();
|
|
3031
|
+
plan.appliedBranch = result.branch;
|
|
3032
|
+
await store.savePlan(plan);
|
|
2993
3033
|
console.log(chalk.green(`\u2705 ${result.message}`));
|
|
2994
3034
|
} catch (error) {
|
|
2995
3035
|
console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -3052,6 +3092,7 @@ program.command("list").description("List past migration plans").action(async ()
|
|
|
3052
3092
|
}
|
|
3053
3093
|
});
|
|
3054
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();
|
|
3055
3096
|
let desiredPort = parseInt(options.port, 10);
|
|
3056
3097
|
const spinner = ora("Starting Metamorph API server...").start();
|
|
3057
3098
|
try {
|