@nikelyh/metamorph 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +58 -31
- package/bin/metamorph.js +0 -0
- package/dist/NextMigrationHints-BGO2W3K4.js +32 -0
- package/dist/ProjectDetector-WU3OD6WT.js +16 -0
- package/dist/{chunk-OXXEISZT.js → chunk-APRESSUR.js} +103 -44
- package/dist/chunk-H2LZEPNI.js +352 -0
- package/dist/{chunk-VEAYUIOZ.js → chunk-WLHSZWET.js} +39 -0
- package/dist/chunk-XLD7NXYU.js +528 -0
- package/dist/index.js +624 -520
- package/dist/public/assets/index-BX5N2qs-.js +63 -0
- package/dist/public/assets/index-C7odDrsP.css +1 -0
- package/dist/public/icons/angular.svg +1 -1
- package/dist/public/icons/expressjs.svg +1 -1
- package/dist/public/icons/fastify.svg +1 -1
- package/dist/public/icons/hapi.svg +1 -14
- package/dist/public/icons/nestjs.svg +1 -1
- package/dist/public/icons/nextjs.svg +1 -1
- package/dist/public/icons/react.svg +1 -1
- package/dist/public/icons/svelte.svg +1 -21
- package/dist/public/icons/vue.svg +1 -1
- package/dist/public/icons.svg +1 -24
- package/dist/public/index.html +2 -2
- package/dist/{src-2UXWWNPF.js → src-Q3DGKZEQ.js} +20 -4
- package/dist/{src-UC3SN46X.js → src-YPCIVBLZ.js} +3 -1
- package/package.json +18 -5
- package/dist/TechDetector-5YP44RVR.js +0 -6
- package/dist/chunk-3QN2Q75J.js +0 -101
- package/dist/public/assets/index-DDJRaEaa.css +0 -1
- package/dist/public/assets/index-PNJ1umlc.js +0 -63
package/dist/index.js
CHANGED
|
@@ -12,14 +12,17 @@ import {
|
|
|
12
12
|
createRenameFileTool,
|
|
13
13
|
createRunBuildTool,
|
|
14
14
|
createWriteFileTool
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-APRESSUR.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
formatCatalogRules,
|
|
18
|
+
resolveMigrationCatalog,
|
|
19
|
+
resolvePackageManagerCommands
|
|
20
|
+
} from "./chunk-WLHSZWET.js";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
detectMonorepo,
|
|
23
|
+
inspectManifest,
|
|
24
|
+
inspectProject
|
|
25
|
+
} from "./chunk-XLD7NXYU.js";
|
|
23
26
|
import {
|
|
24
27
|
MetamorphState,
|
|
25
28
|
initializeRuntime,
|
|
@@ -33,6 +36,16 @@ import {
|
|
|
33
36
|
collectFileHints,
|
|
34
37
|
registerMigrationPlugin
|
|
35
38
|
} from "./chunk-O2QLEES3.js";
|
|
39
|
+
import {
|
|
40
|
+
analyzeReactToNextStructure,
|
|
41
|
+
findShadowRoot,
|
|
42
|
+
hasNextAppRoutes,
|
|
43
|
+
isFsdPageComponent,
|
|
44
|
+
isSpaPagesTreeFile,
|
|
45
|
+
isSpaRootComponent,
|
|
46
|
+
listAppRouterFiles,
|
|
47
|
+
nextMigrationFileHint
|
|
48
|
+
} from "./chunk-H2LZEPNI.js";
|
|
36
49
|
|
|
37
50
|
// src/index.ts
|
|
38
51
|
import { Command } from "commander";
|
|
@@ -118,7 +131,7 @@ import {
|
|
|
118
131
|
createAgent as createAgent2,
|
|
119
132
|
SituationSpecification as SituationSpecification2
|
|
120
133
|
} from "@mozaik-ai/core";
|
|
121
|
-
import * as
|
|
134
|
+
import * as fs3 from "fs";
|
|
122
135
|
|
|
123
136
|
// ../application/src/utils/FileTreeBuilder.ts
|
|
124
137
|
import * as path2 from "path";
|
|
@@ -180,312 +193,15 @@ function buildFileTree(filePaths) {
|
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
// ../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
196
|
import * as fs2 from "fs";
|
|
188
197
|
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
198
|
var MAX_FILES = 8;
|
|
483
199
|
var MAX_FILE_CHARS = 4e3;
|
|
484
200
|
var MAX_TOTAL_CHARS = 24e3;
|
|
485
201
|
function readTruncated(filePath) {
|
|
486
202
|
try {
|
|
487
|
-
if (!
|
|
488
|
-
const raw =
|
|
203
|
+
if (!fs2.existsSync(filePath) || !fs2.statSync(filePath).isFile()) return null;
|
|
204
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
489
205
|
if (raw.length <= MAX_FILE_CHARS) return raw;
|
|
490
206
|
return `${raw.slice(0, MAX_FILE_CHARS)}
|
|
491
207
|
|
|
@@ -494,18 +210,18 @@ function readTruncated(filePath) {
|
|
|
494
210
|
return null;
|
|
495
211
|
}
|
|
496
212
|
}
|
|
497
|
-
function
|
|
213
|
+
function findSrcRoot(filePath) {
|
|
498
214
|
const normalized = filePath.replace(/\\/g, "/");
|
|
499
215
|
const idx = normalized.lastIndexOf("/src/");
|
|
500
216
|
if (idx === -1) return null;
|
|
501
217
|
return filePath.slice(0, idx + 4);
|
|
502
218
|
}
|
|
503
219
|
function resolveImport(fromFile, spec) {
|
|
504
|
-
const dir =
|
|
220
|
+
const dir = path3.dirname(fromFile);
|
|
505
221
|
const base = spec.startsWith("@/") ? (() => {
|
|
506
|
-
const src =
|
|
507
|
-
return src ?
|
|
508
|
-
})() :
|
|
222
|
+
const src = findSrcRoot(fromFile);
|
|
223
|
+
return src ? path3.join(src, spec.slice(2)) : path3.resolve(dir, spec);
|
|
224
|
+
})() : path3.resolve(dir, spec);
|
|
509
225
|
const candidates = [
|
|
510
226
|
base,
|
|
511
227
|
`${base}.tsx`,
|
|
@@ -514,12 +230,12 @@ function resolveImport(fromFile, spec) {
|
|
|
514
230
|
`${base}.js`,
|
|
515
231
|
`${base}.vue`,
|
|
516
232
|
`${base}.svelte`,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
233
|
+
path3.join(base, "index.tsx"),
|
|
234
|
+
path3.join(base, "index.ts"),
|
|
235
|
+
path3.join(base, "index.vue"),
|
|
236
|
+
path3.join(base, "index.svelte")
|
|
521
237
|
];
|
|
522
|
-
return candidates.find((file) =>
|
|
238
|
+
return candidates.find((file) => fs2.existsSync(file) && fs2.statSync(file).isFile()) || null;
|
|
523
239
|
}
|
|
524
240
|
function localImportSpecs(content) {
|
|
525
241
|
const specs = [];
|
|
@@ -530,8 +246,8 @@ function localImportSpecs(content) {
|
|
|
530
246
|
}
|
|
531
247
|
function listNearbyModules(fromFile, limit) {
|
|
532
248
|
const shadow = findShadowRoot(fromFile);
|
|
533
|
-
const src =
|
|
534
|
-
if (!
|
|
249
|
+
const src = findSrcRoot(fromFile) || (shadow ? path3.join(shadow, "src") : path3.dirname(fromFile));
|
|
250
|
+
if (!fs2.existsSync(src)) return [];
|
|
535
251
|
const interesting = /(?:Page|page|View|Screen|layout|Layout|main|App|routes|Routes|app\.routes)\.(t|j)sx?$|\.(vue|svelte)$/;
|
|
536
252
|
const out = [];
|
|
537
253
|
const stack = [src];
|
|
@@ -539,13 +255,13 @@ function listNearbyModules(fromFile, limit) {
|
|
|
539
255
|
const dir = stack.pop();
|
|
540
256
|
let entries;
|
|
541
257
|
try {
|
|
542
|
-
entries =
|
|
258
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
543
259
|
} catch {
|
|
544
260
|
continue;
|
|
545
261
|
}
|
|
546
262
|
for (const entry of entries) {
|
|
547
263
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
548
|
-
const full =
|
|
264
|
+
const full = path3.join(dir, entry.name);
|
|
549
265
|
if (entry.isDirectory()) stack.push(full);
|
|
550
266
|
else if (interesting.test(entry.name)) out.push(full);
|
|
551
267
|
if (out.length >= limit) break;
|
|
@@ -575,15 +291,15 @@ ${body}
|
|
|
575
291
|
`);
|
|
576
292
|
}
|
|
577
293
|
const importSpecs = self ? localImportSpecs(self) : [];
|
|
578
|
-
const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !==
|
|
294
|
+
const resolved = importSpecs.map((spec) => resolveImport(filePath, spec)).filter((file) => Boolean(file && file !== path3.resolve(filePath)));
|
|
579
295
|
const unique = [...new Set(resolved)].slice(0, MAX_FILES);
|
|
580
296
|
for (const dep of unique) {
|
|
581
297
|
const body = readTruncated(dep);
|
|
582
298
|
if (body) push(`imported: ${dep}`, body);
|
|
583
299
|
}
|
|
584
|
-
const nearby = listNearbyModules(filePath, 24).filter((file) =>
|
|
300
|
+
const nearby = listNearbyModules(filePath, 24).filter((file) => path3.resolve(file) !== path3.resolve(filePath));
|
|
585
301
|
if (nearby.length > 0) {
|
|
586
|
-
const listing = nearby.map((file) =>
|
|
302
|
+
const listing = nearby.map((file) => path3.relative(findShadowRoot(filePath) || path3.dirname(filePath), file)).join("\n");
|
|
587
303
|
sections.push(`### Nearby screen / bootstrap files (paths only \u2014 read before inventing a new UI)
|
|
588
304
|
${listing}
|
|
589
305
|
`);
|
|
@@ -594,6 +310,12 @@ Evidence from disk (read this before editing; match existing exports and callbac
|
|
|
594
310
|
${sections.join("\n")}`;
|
|
595
311
|
}
|
|
596
312
|
|
|
313
|
+
// ../application/src/utils/workerCompletion.ts
|
|
314
|
+
var MISSING_AFTER_WORKER = "File missing after worker inference (missing_after_worker)";
|
|
315
|
+
function workerCompletionKind(fileExists) {
|
|
316
|
+
return fileExists ? "migrated" : "failed_missing";
|
|
317
|
+
}
|
|
318
|
+
|
|
597
319
|
// ../application/src/agents/WorkerAgent.ts
|
|
598
320
|
var MAX_RETRIES = 2;
|
|
599
321
|
var ConcurrencyQueue = class {
|
|
@@ -648,7 +370,7 @@ var WhenInferenceCompleted = class extends SituationSpecification2 {
|
|
|
648
370
|
async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
649
371
|
return new Promise(async (originalResolve) => {
|
|
650
372
|
let isDone = false;
|
|
651
|
-
const
|
|
373
|
+
const resolve4 = () => {
|
|
652
374
|
if (!isDone) {
|
|
653
375
|
isDone = true;
|
|
654
376
|
originalResolve();
|
|
@@ -658,28 +380,51 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
658
380
|
const repository = runtime.state.repository;
|
|
659
381
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
660
382
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
383
|
+
const { findShadowRoot: findShadowRoot2 } = await import("./NextMigrationHints-BGO2W3K4.js");
|
|
384
|
+
const shadowRoot = findShadowRoot2(filePath);
|
|
385
|
+
let workerTools = participant.getTools();
|
|
386
|
+
if (shadowRoot) {
|
|
387
|
+
const { createShadowTools } = await import("./src-Q3DGKZEQ.js");
|
|
388
|
+
workerTools = createShadowTools(shadowRoot);
|
|
389
|
+
}
|
|
661
390
|
const tempAgent = createMozaikAgent({
|
|
662
391
|
name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
|
|
663
392
|
capabilities: ["code_refactoring", "inference"],
|
|
664
393
|
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:
|
|
394
|
+
tools: workerTools,
|
|
666
395
|
handlers: [
|
|
667
396
|
{
|
|
668
397
|
specification: new WhenInferenceCompleted(),
|
|
669
398
|
processor: {
|
|
670
|
-
async apply({
|
|
671
|
-
|
|
672
|
-
|
|
399
|
+
async apply({ participant: tempParticipant }) {
|
|
400
|
+
if (isDone) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const exists = fs3.existsSync(filePath);
|
|
404
|
+
if (workerCompletionKind(exists) === "failed_missing") {
|
|
405
|
+
console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk after inference: ${filePath}`);
|
|
406
|
+
await repository.updateTaskStatus(planId, filePath, "failed", MISSING_AFTER_WORKER);
|
|
407
|
+
sendEvent(
|
|
408
|
+
{
|
|
409
|
+
type: "file.failed" /* FILE_FAILED */,
|
|
410
|
+
producerId: tempParticipant.getId(),
|
|
411
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
412
|
+
payload: { planId, filePath, reason: MISSING_AFTER_WORKER }
|
|
413
|
+
},
|
|
414
|
+
tempParticipant.getId()
|
|
415
|
+
);
|
|
416
|
+
leave(tempParticipant);
|
|
417
|
+
resolve4();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
let fileContent = "";
|
|
673
421
|
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
|
-
}
|
|
422
|
+
fileContent = fs3.readFileSync(filePath, "utf-8");
|
|
423
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
|
|
680
424
|
} catch (e) {
|
|
681
425
|
console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
|
|
682
426
|
}
|
|
427
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
|
|
683
428
|
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
684
429
|
sendEvent(
|
|
685
430
|
{
|
|
@@ -695,7 +440,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
695
440
|
tempParticipant.getId()
|
|
696
441
|
);
|
|
697
442
|
leave(tempParticipant);
|
|
698
|
-
|
|
443
|
+
resolve4();
|
|
699
444
|
}
|
|
700
445
|
}
|
|
701
446
|
}
|
|
@@ -712,18 +457,18 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
712
457
|
});
|
|
713
458
|
setTimeout(async () => {
|
|
714
459
|
if (!isDone) {
|
|
715
|
-
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
|
|
716
|
-
await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
|
|
460
|
+
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out after 120s.`);
|
|
461
|
+
await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out after 120s");
|
|
717
462
|
sendEvent({
|
|
718
463
|
type: "file.failed" /* FILE_FAILED */,
|
|
719
464
|
producerId: tempAgent.getId(),
|
|
720
465
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
721
|
-
payload: { planId, filePath, reason: "Inference timed out" }
|
|
466
|
+
payload: { planId, filePath, reason: "Inference timed out after 120s" }
|
|
722
467
|
}, tempAgent.getId());
|
|
723
468
|
leave(tempAgent);
|
|
724
|
-
|
|
469
|
+
resolve4();
|
|
725
470
|
}
|
|
726
|
-
},
|
|
471
|
+
}, 12e4);
|
|
727
472
|
} catch (error) {
|
|
728
473
|
console.error(`[WorkerAgent:${tempAgent.getId()}] Sync error:`, error);
|
|
729
474
|
if (!isDone) {
|
|
@@ -736,7 +481,7 @@ async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
|
736
481
|
payload: { planId, filePath, reason: errorMessage || "Sync error" }
|
|
737
482
|
}, tempAgent.getId());
|
|
738
483
|
leave(tempAgent);
|
|
739
|
-
|
|
484
|
+
resolve4();
|
|
740
485
|
}
|
|
741
486
|
}
|
|
742
487
|
});
|
|
@@ -752,7 +497,7 @@ var workOnFileProcessor = {
|
|
|
752
497
|
if (plan) {
|
|
753
498
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
754
499
|
`;
|
|
755
|
-
const catalogEntry =
|
|
500
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
756
501
|
if (catalogEntry) {
|
|
757
502
|
prompt += `
|
|
758
503
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
@@ -858,7 +603,7 @@ ${payload.errors.join("\n")}
|
|
|
858
603
|
if (plan) {
|
|
859
604
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
860
605
|
`;
|
|
861
|
-
const catalogEntry =
|
|
606
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
862
607
|
if (catalogEntry) {
|
|
863
608
|
prompt += `
|
|
864
609
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair):
|
|
@@ -934,6 +679,115 @@ import {
|
|
|
934
679
|
SituationSpecification as SituationSpecification3
|
|
935
680
|
} from "@mozaik-ai/core";
|
|
936
681
|
import * as path5 from "path";
|
|
682
|
+
|
|
683
|
+
// ../application/src/utils/classifyMissingFile.ts
|
|
684
|
+
import * as fs4 from "fs";
|
|
685
|
+
import * as path4 from "path";
|
|
686
|
+
var NEXT_CONFIG_NAMES = [
|
|
687
|
+
"next.config.js",
|
|
688
|
+
"next.config.mjs",
|
|
689
|
+
"next.config.ts",
|
|
690
|
+
"next.config.cjs"
|
|
691
|
+
];
|
|
692
|
+
var CONFIG_FILE_PATTERN = /[/\\](vite\.config|craco\.config|webpack\.config)\.(js|ts|mjs|cjs)$/i;
|
|
693
|
+
function classifyMissingFile(input) {
|
|
694
|
+
const { filePath, target } = input;
|
|
695
|
+
const fileExists = input.fileExists || fs4.existsSync;
|
|
696
|
+
const shadowRoot = input.shadowRoot !== void 0 ? input.shadowRoot : findShadowRoot(filePath);
|
|
697
|
+
const normalizedTarget = target.toLowerCase();
|
|
698
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
699
|
+
if (CONFIG_FILE_PATTERN.test(filePath)) {
|
|
700
|
+
if (normalizedTarget === "next" && shadowRoot) {
|
|
701
|
+
const hasNextConfig = NEXT_CONFIG_NAMES.some(
|
|
702
|
+
(cfg) => fileExists(path4.join(shadowRoot, cfg))
|
|
703
|
+
);
|
|
704
|
+
if (hasNextConfig) {
|
|
705
|
+
return {
|
|
706
|
+
verdict: "approved",
|
|
707
|
+
reason: `Config file removed because a Next.js configuration exists in shadow workspace.`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
verdict: "rejected",
|
|
712
|
+
errors: [
|
|
713
|
+
`Configuration file was removed, but no Next.js configuration (next.config.js/mjs/ts) was found in the project root.`
|
|
714
|
+
]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (/[/\\]index\.html$/i.test(filePath)) {
|
|
719
|
+
if (normalizedTarget === "next" && shadowRoot) {
|
|
720
|
+
const layoutCandidates = [
|
|
721
|
+
path4.join(shadowRoot, "src", "app", "layout.tsx"),
|
|
722
|
+
path4.join(shadowRoot, "src", "app", "layout.jsx"),
|
|
723
|
+
path4.join(shadowRoot, "src", "app", "layout.js"),
|
|
724
|
+
path4.join(shadowRoot, "app", "layout.tsx"),
|
|
725
|
+
path4.join(shadowRoot, "app", "layout.jsx"),
|
|
726
|
+
path4.join(shadowRoot, "app", "layout.js"),
|
|
727
|
+
path4.join(shadowRoot, "src", "pages", "_document.tsx"),
|
|
728
|
+
path4.join(shadowRoot, "pages", "_document.tsx")
|
|
729
|
+
];
|
|
730
|
+
const hasLayout = layoutCandidates.some((candidate) => fileExists(candidate));
|
|
731
|
+
if (hasLayout) {
|
|
732
|
+
return {
|
|
733
|
+
verdict: "approved",
|
|
734
|
+
reason: `index.html was removed because Next.js root layout or _document exists.`
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
verdict: "rejected",
|
|
739
|
+
errors: [
|
|
740
|
+
`index.html was removed, but no Next.js root layout (app/layout.tsx) or _document was created to provide the HTML structure.`
|
|
741
|
+
]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (isSpaRootComponent(filePath)) {
|
|
746
|
+
if (normalizedTarget === "next") {
|
|
747
|
+
const hasRoutes = shadowRoot ? hasNextAppRoutes(shadowRoot) : false;
|
|
748
|
+
if (hasRoutes) {
|
|
749
|
+
return {
|
|
750
|
+
verdict: "approved",
|
|
751
|
+
reason: `SPA root component was removed because Next.js App Router routes exist.`
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
verdict: "rejected",
|
|
756
|
+
errors: [
|
|
757
|
+
`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.`
|
|
758
|
+
]
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (isFsdPageComponent(filePath) || isSpaPagesTreeFile(filePath)) {
|
|
763
|
+
if (normalizedTarget === "next") {
|
|
764
|
+
const viewsReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1views$1");
|
|
765
|
+
const screensReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1screens$1");
|
|
766
|
+
const compReplacement = filePath.replace(/([/\\])src\1pages\1/, "$1src$1components$1pages$1");
|
|
767
|
+
if (fileExists(viewsReplacement) || fileExists(screensReplacement) || fileExists(compReplacement)) {
|
|
768
|
+
return {
|
|
769
|
+
verdict: "approved",
|
|
770
|
+
reason: `File moved out of src/pages into a non-reserved directory for Next.js.`
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
verdict: "rejected",
|
|
775
|
+
errors: [
|
|
776
|
+
`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.`
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const fileName = path4.basename(filePath);
|
|
782
|
+
return {
|
|
783
|
+
verdict: "rejected",
|
|
784
|
+
errors: [
|
|
785
|
+
`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.`
|
|
786
|
+
]
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ../application/src/agents/ReviewerAgent.ts
|
|
937
791
|
var ConcurrencyQueue2 = class {
|
|
938
792
|
constructor(concurrencyLimit) {
|
|
939
793
|
this.concurrencyLimit = concurrencyLimit;
|
|
@@ -970,15 +824,9 @@ var WhenFileMigrated = class extends SituationSpecification3 {
|
|
|
970
824
|
};
|
|
971
825
|
var WhenReviewCompleted = class extends SituationSpecification3 {
|
|
972
826
|
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
827
|
return event.type === "model.answer" && event.producerId === participant.getId();
|
|
975
828
|
}
|
|
976
829
|
};
|
|
977
|
-
var WhenAnythingHappens = class extends SituationSpecification3 {
|
|
978
|
-
isSatisfiedBy() {
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
830
|
var reviewFileProcessor = {
|
|
983
831
|
async apply({ event, participant }) {
|
|
984
832
|
const payload = event.payload;
|
|
@@ -986,10 +834,9 @@ var reviewFileProcessor = {
|
|
|
986
834
|
reviewerQueue.enqueue(async () => {
|
|
987
835
|
return new Promise(async (originalResolve) => {
|
|
988
836
|
let isDone = false;
|
|
989
|
-
const
|
|
837
|
+
const resolve4 = () => {
|
|
990
838
|
if (!isDone) {
|
|
991
839
|
isDone = true;
|
|
992
|
-
console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
|
|
993
840
|
originalResolve();
|
|
994
841
|
}
|
|
995
842
|
};
|
|
@@ -1003,33 +850,22 @@ var reviewFileProcessor = {
|
|
|
1003
850
|
tools: [],
|
|
1004
851
|
// No tools to avoid Gemini thought_signature crash
|
|
1005
852
|
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
853
|
{
|
|
1016
854
|
specification: new WhenReviewCompleted(),
|
|
1017
855
|
processor: {
|
|
1018
856
|
async apply({ event: event2, participant: tempParticipant }) {
|
|
1019
|
-
|
|
857
|
+
if (isDone) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
1020
860
|
const answerItem = event2.payload.answer;
|
|
1021
861
|
const answerText = answerItem?.content?.text || answerItem?.text || null;
|
|
1022
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] answerText:`, answerText);
|
|
1023
862
|
let reviewResult = null;
|
|
1024
863
|
if (answerText) {
|
|
1025
864
|
try {
|
|
1026
865
|
reviewResult = JSON.parse(answerText);
|
|
1027
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Parsed result:`, JSON.stringify(reviewResult));
|
|
1028
866
|
} catch (e) {
|
|
1029
867
|
console.error(`[ReviewerAgent:${reviewerId}] Failed to parse JSON:`, e);
|
|
1030
868
|
}
|
|
1031
|
-
} else {
|
|
1032
|
-
console.warn(`[DEBUG:ReviewerAgent:${reviewerId}] answerItem has no extractable text.`);
|
|
1033
869
|
}
|
|
1034
870
|
if (reviewResult) {
|
|
1035
871
|
const participantId = tempParticipant.getId();
|
|
@@ -1066,28 +902,39 @@ var reviewFileProcessor = {
|
|
|
1066
902
|
}
|
|
1067
903
|
}
|
|
1068
904
|
leave(tempParticipant);
|
|
1069
|
-
|
|
905
|
+
resolve4();
|
|
1070
906
|
}
|
|
1071
907
|
}
|
|
1072
908
|
}
|
|
1073
909
|
]
|
|
1074
910
|
});
|
|
1075
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent created with id: ${tempAgent.getId()}`);
|
|
1076
911
|
join(tempAgent);
|
|
1077
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
|
|
1078
912
|
const runtime = resolveRuntime();
|
|
1079
913
|
const { existsSync: existsSync8 } = await import("fs");
|
|
1080
914
|
if (!existsSync8(payload.filePath)) {
|
|
1081
915
|
const { sendEvent: sendEvent4 } = await import("./runtime-L7VOQROE.js");
|
|
1082
916
|
const planForMissing = await runtime.state.repository.getPlan(payload.planId);
|
|
1083
|
-
const
|
|
917
|
+
const sourceFramework = planForMissing?.profile.source || "react";
|
|
918
|
+
const targetFramework = planForMissing?.profile.target || "next";
|
|
1084
919
|
const shadowRoot = findShadowRoot(payload.filePath);
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
920
|
+
const classification = classifyMissingFile({
|
|
921
|
+
filePath: payload.filePath,
|
|
922
|
+
source: sourceFramework,
|
|
923
|
+
target: targetFramework,
|
|
924
|
+
shadowRoot,
|
|
925
|
+
fileExists: existsSync8
|
|
926
|
+
});
|
|
927
|
+
if (classification.verdict === "approved") {
|
|
928
|
+
console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} approved: ${classification.reason}`);
|
|
929
|
+
await runtime.state.repository.updateTaskStatus(payload.planId, payload.filePath, "completed");
|
|
930
|
+
sendEvent4({
|
|
931
|
+
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
932
|
+
producerId: tempAgent.getId(),
|
|
933
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
934
|
+
payload: { planId: payload.planId, filePath: payload.filePath }
|
|
935
|
+
}, tempAgent.getId());
|
|
936
|
+
} else {
|
|
937
|
+
console.log(`[ReviewerAgent:${reviewerId}] Missing ${payload.filePath} rejected:`, classification.errors);
|
|
1091
938
|
sendEvent4({
|
|
1092
939
|
type: "file.rejected" /* FILE_REJECTED */,
|
|
1093
940
|
producerId: tempAgent.getId(),
|
|
@@ -1095,27 +942,12 @@ var reviewFileProcessor = {
|
|
|
1095
942
|
payload: {
|
|
1096
943
|
planId: payload.planId,
|
|
1097
944
|
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
|
-
]
|
|
945
|
+
errors: classification.errors || ["File does not exist and has no demonstrable replacement"]
|
|
1103
946
|
}
|
|
1104
947
|
}, tempAgent.getId());
|
|
1105
|
-
leave(tempAgent);
|
|
1106
|
-
resolve5();
|
|
1107
|
-
return;
|
|
1108
948
|
}
|
|
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
949
|
leave(tempAgent);
|
|
1118
|
-
|
|
950
|
+
resolve4();
|
|
1119
951
|
return;
|
|
1120
952
|
}
|
|
1121
953
|
if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
|
|
@@ -1139,7 +971,7 @@ var reviewFileProcessor = {
|
|
|
1139
971
|
}
|
|
1140
972
|
}, tempAgent.getId());
|
|
1141
973
|
leave(tempAgent);
|
|
1142
|
-
|
|
974
|
+
resolve4();
|
|
1143
975
|
return;
|
|
1144
976
|
}
|
|
1145
977
|
} catch (e) {
|
|
@@ -1164,7 +996,7 @@ var reviewFileProcessor = {
|
|
|
1164
996
|
payload: { planId: payload.planId, filePath: payload.filePath, errors }
|
|
1165
997
|
}, tempAgent.getId());
|
|
1166
998
|
leave(tempAgent);
|
|
1167
|
-
|
|
999
|
+
resolve4();
|
|
1168
1000
|
return;
|
|
1169
1001
|
}
|
|
1170
1002
|
}
|
|
@@ -1174,7 +1006,7 @@ var reviewFileProcessor = {
|
|
|
1174
1006
|
if (plan) {
|
|
1175
1007
|
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
1176
1008
|
`;
|
|
1177
|
-
const catalogEntry =
|
|
1009
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
1178
1010
|
if (catalogEntry) {
|
|
1179
1011
|
prompt += `
|
|
1180
1012
|
Architectural rules (all \u2192 ${catalogEntry.layer} \u2192 frameworks \u2192 this pair; apply only if relevant to this file):
|
|
@@ -1196,7 +1028,6 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1196
1028
|
const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
1197
1029
|
console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
|
|
1198
1030
|
try {
|
|
1199
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
|
|
1200
1031
|
runLoop(tempAgent.getId(), prompt, {
|
|
1201
1032
|
model: modelToUse,
|
|
1202
1033
|
context: tempAgent.getMemory().getContext(),
|
|
@@ -1214,23 +1045,22 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1214
1045
|
strict: true
|
|
1215
1046
|
}
|
|
1216
1047
|
});
|
|
1217
|
-
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
|
|
1218
1048
|
setTimeout(async () => {
|
|
1219
1049
|
if (!isDone) {
|
|
1220
|
-
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after
|
|
1050
|
+
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 120s for ${payload.filePath}. Model never responded.`);
|
|
1221
1051
|
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-L7VOQROE.js");
|
|
1222
1052
|
const runtime2 = resolveRuntime2();
|
|
1223
|
-
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after
|
|
1053
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 120s");
|
|
1224
1054
|
sendEvent3({
|
|
1225
1055
|
type: "file.failed" /* FILE_FAILED */,
|
|
1226
1056
|
producerId: tempAgent.getId(),
|
|
1227
1057
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1228
|
-
payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after
|
|
1058
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: "Inference timed out after 120s" }
|
|
1229
1059
|
}, tempAgent.getId());
|
|
1230
1060
|
leave(tempAgent);
|
|
1231
|
-
|
|
1061
|
+
resolve4();
|
|
1232
1062
|
}
|
|
1233
|
-
},
|
|
1063
|
+
}, 12e4);
|
|
1234
1064
|
} catch (error) {
|
|
1235
1065
|
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
1236
1066
|
if (!isDone) {
|
|
@@ -1245,7 +1075,7 @@ Compare the diff to imported modules in the evidence. If props/exports do not ma
|
|
|
1245
1075
|
payload: { planId: payload.planId, filePath: payload.filePath, reason: errorMessage || "Sync error" }
|
|
1246
1076
|
}, tempAgent.getId());
|
|
1247
1077
|
leave(tempAgent);
|
|
1248
|
-
|
|
1078
|
+
resolve4();
|
|
1249
1079
|
}
|
|
1250
1080
|
}
|
|
1251
1081
|
});
|
|
@@ -1312,7 +1142,7 @@ var managePackagesProcessor = {
|
|
|
1312
1142
|
await emitPackagesReady("failed");
|
|
1313
1143
|
return;
|
|
1314
1144
|
}
|
|
1315
|
-
const catalogEntry =
|
|
1145
|
+
const catalogEntry = resolveMigrationCatalog(plan.profile.source, plan.profile.target);
|
|
1316
1146
|
if (!catalogEntry) {
|
|
1317
1147
|
console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
|
|
1318
1148
|
await emitPackagesReady("failed");
|
|
@@ -1389,7 +1219,7 @@ var managePackagesProcessor = {
|
|
|
1389
1219
|
type: "system.log" /* SYSTEM_LOG */,
|
|
1390
1220
|
producerId: participant.getId(),
|
|
1391
1221
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1392
|
-
payload: { planId: payload.planId, message: "
|
|
1222
|
+
payload: { planId: payload.planId, message: "Dependencies updated in package.json: target framework libraries and build scripts configured.", level: "info" }
|
|
1393
1223
|
}, participant.getId());
|
|
1394
1224
|
} catch (err) {
|
|
1395
1225
|
console.error(`[PackageManagerAgent] Error editing package.json:`, err);
|
|
@@ -1490,7 +1320,7 @@ var startWatchdogProcessor = {
|
|
|
1490
1320
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1491
1321
|
payload: {
|
|
1492
1322
|
planId: p.planId,
|
|
1493
|
-
message: "
|
|
1323
|
+
message: "Swarm coordinator active: orchestrating agent tasks and monitoring workflow progress.",
|
|
1494
1324
|
level: "info"
|
|
1495
1325
|
}
|
|
1496
1326
|
}, producerId);
|
|
@@ -1509,10 +1339,26 @@ var startWatchdogProcessor = {
|
|
|
1509
1339
|
occurredAt: /* @__PURE__ */ new Date(),
|
|
1510
1340
|
payload: {
|
|
1511
1341
|
planId: p.planId,
|
|
1512
|
-
message: "
|
|
1513
|
-
level: "
|
|
1342
|
+
message: "Migration session timed out after 30 minutes: task completion could not be verified.",
|
|
1343
|
+
level: "error"
|
|
1514
1344
|
}
|
|
1515
1345
|
}, producerId);
|
|
1346
|
+
const currentPlan = await runtime.state.repository.getPlan(p.planId);
|
|
1347
|
+
if (currentPlan && currentPlan.phase !== "completed" && currentPlan.phase !== "failed") {
|
|
1348
|
+
currentPlan.phase = "failed";
|
|
1349
|
+
currentPlan.outcome = "failed";
|
|
1350
|
+
await runtime.state.repository.savePlan(currentPlan);
|
|
1351
|
+
sendEvent({
|
|
1352
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1353
|
+
producerId,
|
|
1354
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1355
|
+
payload: {
|
|
1356
|
+
planId: p.planId,
|
|
1357
|
+
outcome: "failed",
|
|
1358
|
+
reason: "Migration timed out after 30 minutes watchdog budget."
|
|
1359
|
+
}
|
|
1360
|
+
}, producerId);
|
|
1361
|
+
}
|
|
1516
1362
|
return;
|
|
1517
1363
|
}
|
|
1518
1364
|
await emitIntegrationIfReady(producerId, p.planId);
|
|
@@ -1552,7 +1398,7 @@ function log(participantId, planId, message, level = "info") {
|
|
|
1552
1398
|
}
|
|
1553
1399
|
async function shadowPathFor(plan, journal) {
|
|
1554
1400
|
if (journal?.shadowPath) return journal.shadowPath;
|
|
1555
|
-
const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-
|
|
1401
|
+
const { ShadowWorkspace: ShadowWorkspace2 } = await import("./src-Q3DGKZEQ.js");
|
|
1556
1402
|
return new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
1557
1403
|
}
|
|
1558
1404
|
function fallbackReport(params) {
|
|
@@ -1561,10 +1407,15 @@ function fallbackReport(params) {
|
|
|
1561
1407
|
const rejected = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal");
|
|
1562
1408
|
const fileLines = params.tasks.filter((t) => !t.filePath.startsWith("system:")).slice(0, 40).map((t) => `- \`${t.filePath}\` \u2014 ${t.status}${t.error ? ` (${t.error})` : ""}`).join("\n");
|
|
1563
1409
|
const riskLines = rejected.slice(0, 12).map((n) => `- \`${n.filePath}\`: ${n.detail || n.outcome}`).join("\n");
|
|
1410
|
+
const pm = params.packageManager || "npm";
|
|
1411
|
+
const devCmd = pm === "yarn" ? "yarn dev" : `${pm} run dev`;
|
|
1412
|
+
const buildCmd = pm === "yarn" ? "yarn build" : `${pm} run build`;
|
|
1564
1413
|
return `# What changed
|
|
1565
1414
|
|
|
1566
1415
|
Metamorph migrated this project from **${params.source}** to **${params.target}**.
|
|
1567
|
-
${params.phase === "failed" ?
|
|
1416
|
+
${params.phase === "failed" ? `
|
|
1417
|
+
**Status:** shadow \`${buildCmd}\` (or catalog checks) did not pass. Treat this as an incomplete migration.
|
|
1418
|
+
` : ""}
|
|
1568
1419
|
Your original files were not overwritten until you click **Apply Migration**. After apply, the result lives on a git branch named like \`metamorph/${params.runId}\`.
|
|
1569
1420
|
|
|
1570
1421
|
## How to run it
|
|
@@ -1572,8 +1423,8 @@ Your original files were not overwritten until you click **Apply Migration**. Af
|
|
|
1572
1423
|
1. Apply the migration in the dashboard (or keep working in the shadow workspace).
|
|
1573
1424
|
2. From the project folder:
|
|
1574
1425
|
\`\`\`bash
|
|
1575
|
-
|
|
1576
|
-
|
|
1426
|
+
${pm} install
|
|
1427
|
+
${devCmd}
|
|
1577
1428
|
\`\`\`
|
|
1578
1429
|
3. Open the URL the framework prints (\`next dev\` \u2192 port 3000, Vite \u2192 5173).
|
|
1579
1430
|
|
|
@@ -1705,6 +1556,8 @@ var fatalProcessor = {
|
|
|
1705
1556
|
};
|
|
1706
1557
|
function draftPrompt(params) {
|
|
1707
1558
|
const notable = params.notes.filter((n) => n.outcome === "rejected" || n.outcome === "fatal").slice(-15).map((n) => `${n.filePath}: ${n.detail || n.outcome}`).join("\n");
|
|
1559
|
+
const pm = params.plan.packageManager || "npm";
|
|
1560
|
+
const devCmd = pm === "yarn" ? "yarn dev" : `${pm} run dev`;
|
|
1708
1561
|
return `Write a user-facing migration briefing to this exact path:
|
|
1709
1562
|
${params.reportAbs}
|
|
1710
1563
|
|
|
@@ -1717,10 +1570,10 @@ Run id: ${params.plan.runId}
|
|
|
1717
1570
|
|
|
1718
1571
|
Include these sections, in this order, with markdown headings:
|
|
1719
1572
|
1. What changed \u2014 1-2 short paragraphs in plain language
|
|
1720
|
-
2. How to run it \u2014 concrete commands (
|
|
1573
|
+
2. How to run it \u2014 concrete commands (${pm} install, ${devCmd}, which port if you can infer from package.json)
|
|
1721
1574
|
3. What we touched \u2014 group by area (routes, styles, config, dependencies). Do not dump every file as a checklist unless there are under 12 files.
|
|
1722
1575
|
4. Watch out for \u2014 honest risks from the notes below and from reading package.json / the new entry files
|
|
1723
|
-
5. Next steps after Apply \u2014 git branch checkout if they applied,
|
|
1576
|
+
5. Next steps after Apply \u2014 git branch checkout if they applied, ${pm} install on the real project
|
|
1724
1577
|
|
|
1725
1578
|
You may read package.json, README, src/app/layout.tsx, src/main.tsx, index.html, or MIGRATION.md if they exist.
|
|
1726
1579
|
|
|
@@ -1753,7 +1606,8 @@ var draftDuringInstallProcessor = {
|
|
|
1753
1606
|
phase: plan.phase,
|
|
1754
1607
|
tasks: plan.tasks || [],
|
|
1755
1608
|
notes: board.notes,
|
|
1756
|
-
startedAt: board.startedAt
|
|
1609
|
+
startedAt: board.startedAt,
|
|
1610
|
+
packageManager: plan.packageManager
|
|
1757
1611
|
});
|
|
1758
1612
|
const tools = participant.getTools();
|
|
1759
1613
|
if (tools.length === 0) {
|
|
@@ -1824,34 +1678,37 @@ var completedProcessor = {
|
|
|
1824
1678
|
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
1825
1679
|
if (!plan) return;
|
|
1826
1680
|
const shadowPath = await shadowPathFor(plan, board);
|
|
1681
|
+
const isFailed = plan.phase === "failed" || plan.outcome === "failed" || p.outcome === "failed";
|
|
1682
|
+
const effectivePhase = isFailed ? "failed" : plan.phase || "completed";
|
|
1827
1683
|
const draft = fallbackReport({
|
|
1828
1684
|
source: plan.profile.source,
|
|
1829
1685
|
target: plan.profile.target,
|
|
1830
1686
|
targetPath: plan.targetPath,
|
|
1831
1687
|
runId: plan.runId,
|
|
1832
|
-
phase:
|
|
1688
|
+
phase: effectivePhase,
|
|
1833
1689
|
tasks: plan.tasks || [],
|
|
1834
1690
|
notes: board.notes,
|
|
1835
|
-
startedAt: board.startedAt
|
|
1691
|
+
startedAt: board.startedAt,
|
|
1692
|
+
packageManager: plan.packageManager
|
|
1836
1693
|
});
|
|
1837
|
-
board.pendingStatusPhase =
|
|
1694
|
+
board.pendingStatusPhase = isFailed ? "failed" : "completed";
|
|
1838
1695
|
try {
|
|
1839
1696
|
const path12 = await import("path");
|
|
1840
1697
|
const dest = path12.join(shadowPath, "MIGRATION.md");
|
|
1841
1698
|
if (board.reportLoopActive) {
|
|
1842
|
-
log(participant.getId(), p.planId, "
|
|
1699
|
+
log(participant.getId(), p.planId, "Compiling comprehensive migration briefing in the background...");
|
|
1843
1700
|
return;
|
|
1844
1701
|
}
|
|
1845
|
-
const result = await writeOrPatchMigrationMd(dest,
|
|
1702
|
+
const result = await writeOrPatchMigrationMd(dest, effectivePhase, draft);
|
|
1846
1703
|
board.pendingStatusPhase = void 0;
|
|
1847
1704
|
log(
|
|
1848
1705
|
participant.getId(),
|
|
1849
1706
|
p.planId,
|
|
1850
|
-
result === "patched" ? "
|
|
1707
|
+
result === "patched" ? "Updated MIGRATION.md with final build verification status." : "Migration report ready in MIGRATION.md: review changes and run Apply to merge into your project."
|
|
1851
1708
|
);
|
|
1852
1709
|
} catch (error) {
|
|
1853
1710
|
console.error("[ReporterAgent] Could not ensure MIGRATION.md:", error);
|
|
1854
|
-
log(participant.getId(), p.planId, "
|
|
1711
|
+
log(participant.getId(), p.planId, "Could not generate MIGRATION.md report.", "warning");
|
|
1855
1712
|
}
|
|
1856
1713
|
}
|
|
1857
1714
|
};
|
|
@@ -1890,44 +1747,81 @@ async function completeMigration(participantId, planId, message, level) {
|
|
|
1890
1747
|
const runtime = resolveRuntime();
|
|
1891
1748
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1892
1749
|
if (plan) {
|
|
1750
|
+
let swept = 0;
|
|
1751
|
+
for (const task of plan.tasks) {
|
|
1752
|
+
if (task.status === "failed" && !task.filePath.startsWith("system:")) {
|
|
1753
|
+
await runtime.state.repository.updateTaskStatus(
|
|
1754
|
+
planId,
|
|
1755
|
+
task.filePath,
|
|
1756
|
+
"completed"
|
|
1757
|
+
);
|
|
1758
|
+
swept++;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (swept > 0) {
|
|
1762
|
+
console.log(
|
|
1763
|
+
`[IntegrationAgent] Swept ${swept} residual failed task(s) \u2192 completed (shadow build passed).`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1893
1766
|
plan.phase = "completed";
|
|
1767
|
+
plan.outcome = "success";
|
|
1894
1768
|
await runtime.state.repository.savePlan(plan);
|
|
1895
1769
|
}
|
|
1896
|
-
sendEvent(
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1770
|
+
sendEvent(
|
|
1771
|
+
{
|
|
1772
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1773
|
+
producerId: participantId,
|
|
1774
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1775
|
+
payload: { planId, message, level }
|
|
1776
|
+
},
|
|
1777
|
+
participantId
|
|
1778
|
+
);
|
|
1779
|
+
sendEvent(
|
|
1780
|
+
{
|
|
1781
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1782
|
+
producerId: participantId,
|
|
1783
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1784
|
+
payload: {
|
|
1785
|
+
planId,
|
|
1786
|
+
outcome: "success"
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
participantId
|
|
1790
|
+
);
|
|
1908
1791
|
}
|
|
1909
1792
|
function log2(participantId, planId, message, level = "info") {
|
|
1910
|
-
sendEvent(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1793
|
+
sendEvent(
|
|
1794
|
+
{
|
|
1795
|
+
type: "system.log" /* SYSTEM_LOG */,
|
|
1796
|
+
producerId: participantId,
|
|
1797
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1798
|
+
payload: { planId, message, level }
|
|
1799
|
+
},
|
|
1800
|
+
participantId
|
|
1801
|
+
);
|
|
1916
1802
|
}
|
|
1917
1803
|
async function failMigration(participantId, planId, message) {
|
|
1918
1804
|
const runtime = resolveRuntime();
|
|
1919
1805
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1920
1806
|
if (plan) {
|
|
1921
1807
|
plan.phase = "failed";
|
|
1808
|
+
plan.outcome = "failed";
|
|
1922
1809
|
await runtime.state.repository.savePlan(plan);
|
|
1923
1810
|
}
|
|
1924
1811
|
log2(participantId, planId, message, "error");
|
|
1925
|
-
sendEvent(
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1812
|
+
sendEvent(
|
|
1813
|
+
{
|
|
1814
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
1815
|
+
producerId: participantId,
|
|
1816
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1817
|
+
payload: {
|
|
1818
|
+
planId,
|
|
1819
|
+
outcome: "failed",
|
|
1820
|
+
reason: message
|
|
1821
|
+
}
|
|
1822
|
+
},
|
|
1823
|
+
participantId
|
|
1824
|
+
);
|
|
1931
1825
|
}
|
|
1932
1826
|
async function requeueBrokenFiles(producerId, planId, files, errors) {
|
|
1933
1827
|
const runtime = resolveRuntime();
|
|
@@ -1945,17 +1839,20 @@ async function requeueBrokenFiles(producerId, planId, files, errors) {
|
|
|
1945
1839
|
"pending",
|
|
1946
1840
|
errors.slice(0, 3).join("\n").slice(0, 1500)
|
|
1947
1841
|
);
|
|
1948
|
-
sendEvent(
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1842
|
+
sendEvent(
|
|
1843
|
+
{
|
|
1844
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
1845
|
+
producerId,
|
|
1846
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1847
|
+
payload: {
|
|
1848
|
+
planId,
|
|
1849
|
+
filePath,
|
|
1850
|
+
errors,
|
|
1851
|
+
source: "integration"
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
producerId
|
|
1855
|
+
);
|
|
1959
1856
|
}
|
|
1960
1857
|
const plan = await runtime.state.repository.getPlan(planId);
|
|
1961
1858
|
if (plan) {
|
|
@@ -1970,7 +1867,9 @@ var integrationProcessor = {
|
|
|
1970
1867
|
const producerId = participant.getId();
|
|
1971
1868
|
const locks = resolveRuntime().state.integrationLocks;
|
|
1972
1869
|
if (locks.has(planId)) {
|
|
1973
|
-
console.log(
|
|
1870
|
+
console.log(
|
|
1871
|
+
`[IntegrationAgent] Integration already running for ${planId}. Ignoring duplicate phase event.`
|
|
1872
|
+
);
|
|
1974
1873
|
return;
|
|
1975
1874
|
}
|
|
1976
1875
|
locks.add(planId);
|
|
@@ -1982,7 +1881,9 @@ var integrationProcessor = {
|
|
|
1982
1881
|
return;
|
|
1983
1882
|
}
|
|
1984
1883
|
if (plan.phase === "completed" || plan.phase === "failed") {
|
|
1985
|
-
console.log(
|
|
1884
|
+
console.log(
|
|
1885
|
+
`[IntegrationAgent] Plan ${planId} already finished (${plan.phase}). Ignoring extra integration phase.`
|
|
1886
|
+
);
|
|
1986
1887
|
return;
|
|
1987
1888
|
}
|
|
1988
1889
|
plan.phase = "integration";
|
|
@@ -1998,37 +1899,101 @@ var integrationProcessor = {
|
|
|
1998
1899
|
plan.integrationRounds = currentRound + 1;
|
|
1999
1900
|
await runtime.state.repository.savePlan(plan);
|
|
2000
1901
|
const lastRound = plan.integrationRounds >= MAX_INTEGRATION_ROUNDS;
|
|
2001
|
-
const {
|
|
1902
|
+
const {
|
|
1903
|
+
ShadowWorkspace: ShadowWorkspace2,
|
|
1904
|
+
createReadFileTool: createReadFileTool2,
|
|
1905
|
+
createWriteFileTool: createWriteFileTool2,
|
|
1906
|
+
createListDirectoryTool: createListDirectoryTool2,
|
|
1907
|
+
createRunBuildTool: createRunBuildTool2,
|
|
1908
|
+
runInShadowWorkspace,
|
|
1909
|
+
parseImplicatedFiles
|
|
1910
|
+
} = await import("./src-Q3DGKZEQ.js");
|
|
2002
1911
|
const { collectShadowIssues, collectRepairTargets } = await import("./registry-7WZ5RTGE.js");
|
|
2003
|
-
const {
|
|
1912
|
+
const { resolveMigrationCatalog: resolveMigrationCatalog2 } = await import("./src-YPCIVBLZ.js");
|
|
2004
1913
|
const shadowPath = payload.shadowWorkspacePath || new ShadowWorkspace2().getShadowPath(plan.runId);
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
1914
|
+
const pmCommands = resolvePackageManagerCommands(plan.packageManager);
|
|
1915
|
+
console.log(
|
|
1916
|
+
`[IntegrationAgent] Shadow build round ${plan.integrationRounds}/${MAX_INTEGRATION_ROUNDS} in ${shadowPath}`
|
|
1917
|
+
);
|
|
1918
|
+
log2(
|
|
1919
|
+
producerId,
|
|
1920
|
+
planId,
|
|
1921
|
+
`Source files transformed. Installing dependencies in shadow workspace with ${pmCommands.cleanInstall}...`
|
|
1922
|
+
);
|
|
1923
|
+
console.log(`[IntegrationAgent] ${pmCommands.cleanInstall} starting in ${shadowPath}`);
|
|
2008
1924
|
let lastInstallBeat = Date.now();
|
|
2009
|
-
const install = await runInShadowWorkspace(
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
1925
|
+
const install = await runInShadowWorkspace(
|
|
1926
|
+
shadowPath,
|
|
1927
|
+
pmCommands.cleanInstall,
|
|
1928
|
+
(chunk) => {
|
|
1929
|
+
process.stdout.write(chunk);
|
|
1930
|
+
if (Date.now() - lastInstallBeat >= 15e3) {
|
|
1931
|
+
lastInstallBeat = Date.now();
|
|
1932
|
+
log2(
|
|
1933
|
+
producerId,
|
|
1934
|
+
planId,
|
|
1935
|
+
`${pmCommands.cleanInstall} is in progress in the shadow workspace...`
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
2014
1938
|
}
|
|
2015
|
-
|
|
1939
|
+
);
|
|
2016
1940
|
if (!install.ok) {
|
|
2017
|
-
console.warn(
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
1941
|
+
console.warn(
|
|
1942
|
+
`[IntegrationAgent] ${pmCommands.cleanInstall} failed in shadow:`,
|
|
1943
|
+
install.output.slice(0, 2e3)
|
|
1944
|
+
);
|
|
1945
|
+
log2(
|
|
1946
|
+
producerId,
|
|
1947
|
+
planId,
|
|
1948
|
+
`${pmCommands.cleanInstall} failed in the shadow workspace. Halting to diagnose dependency conflicts.`,
|
|
1949
|
+
"error"
|
|
1950
|
+
);
|
|
1951
|
+
if (lastRound) {
|
|
1952
|
+
await failMigration(
|
|
1953
|
+
producerId,
|
|
1954
|
+
planId,
|
|
1955
|
+
`${pmCommands.cleanInstall} failed in the shadow workspace after ${MAX_INTEGRATION_ROUNDS} integration rounds. The migration could not complete.`
|
|
1956
|
+
);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const path12 = await import("path");
|
|
1960
|
+
const pkgPath = path12.join(shadowPath, "package.json");
|
|
1961
|
+
await requeueBrokenFiles(
|
|
1962
|
+
producerId,
|
|
1963
|
+
planId,
|
|
1964
|
+
[pkgPath],
|
|
1965
|
+
[`${pmCommands.cleanInstall} failed in shadow workspace:
|
|
1966
|
+
${install.output.slice(0, 2500)}`]
|
|
1967
|
+
);
|
|
1968
|
+
return;
|
|
2021
1969
|
}
|
|
1970
|
+
log2(
|
|
1971
|
+
producerId,
|
|
1972
|
+
planId,
|
|
1973
|
+
`Dependencies installed successfully. Running verification build (${pmCommands.runBuild})...`
|
|
1974
|
+
);
|
|
2022
1975
|
let lastBuildBeat = Date.now();
|
|
2023
|
-
let build = await runInShadowWorkspace(
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
1976
|
+
let build = await runInShadowWorkspace(
|
|
1977
|
+
shadowPath,
|
|
1978
|
+
pmCommands.runBuild,
|
|
1979
|
+
(chunk) => {
|
|
1980
|
+
process.stdout.write(chunk);
|
|
1981
|
+
if (Date.now() - lastBuildBeat >= 15e3) {
|
|
1982
|
+
lastBuildBeat = Date.now();
|
|
1983
|
+
log2(
|
|
1984
|
+
producerId,
|
|
1985
|
+
planId,
|
|
1986
|
+
`${pmCommands.runBuild} is in progress in the shadow workspace...`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
2028
1989
|
}
|
|
2029
|
-
|
|
1990
|
+
);
|
|
2030
1991
|
if (!build.ok) {
|
|
2031
|
-
log2(
|
|
1992
|
+
log2(
|
|
1993
|
+
producerId,
|
|
1994
|
+
planId,
|
|
1995
|
+
"Verification build encountered errors. Analyzing diagnostics for automated repair..."
|
|
1996
|
+
);
|
|
2032
1997
|
const shadowTools = [
|
|
2033
1998
|
createReadFileTool2(shadowPath),
|
|
2034
1999
|
createWriteFileTool2(shadowPath),
|
|
@@ -2038,7 +2003,7 @@ var integrationProcessor = {
|
|
|
2038
2003
|
const { createAgent: createMozaikAgent } = await import("@mozaik-ai/core");
|
|
2039
2004
|
await new Promise((originalResolve) => {
|
|
2040
2005
|
let isDone = false;
|
|
2041
|
-
const
|
|
2006
|
+
const resolve4 = () => {
|
|
2042
2007
|
if (!isDone) {
|
|
2043
2008
|
isDone = true;
|
|
2044
2009
|
originalResolve();
|
|
@@ -2063,32 +2028,51 @@ Build output:
|
|
|
2063
2028
|
${excerpt}
|
|
2064
2029
|
|
|
2065
2030
|
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
|
-
|
|
2031
|
+
Promise.resolve(
|
|
2032
|
+
runLoop(tempAgent.getId(), prompt, {
|
|
2033
|
+
model: modelToUse,
|
|
2034
|
+
context: tempAgent.getMemory().getContext(),
|
|
2035
|
+
tools: shadowTools
|
|
2036
|
+
})
|
|
2037
|
+
).catch((error) => {
|
|
2071
2038
|
console.error("[IntegrationAgent] runLoop error:", error);
|
|
2072
2039
|
leave(tempAgent);
|
|
2073
|
-
|
|
2040
|
+
resolve4();
|
|
2074
2041
|
});
|
|
2075
2042
|
setTimeout(() => {
|
|
2076
2043
|
if (!isDone) {
|
|
2077
|
-
console.warn(
|
|
2044
|
+
console.warn(
|
|
2045
|
+
"[IntegrationAgent] Repair loop timed out; continuing with deterministic rebuild."
|
|
2046
|
+
);
|
|
2078
2047
|
leave(tempAgent);
|
|
2079
|
-
|
|
2048
|
+
resolve4();
|
|
2080
2049
|
}
|
|
2081
2050
|
}, REPAIR_TIMEOUT_MS);
|
|
2082
2051
|
});
|
|
2083
2052
|
build = await runInShadowWorkspace(shadowPath, "npm run build");
|
|
2084
2053
|
}
|
|
2085
|
-
const pluginCtx = {
|
|
2086
|
-
|
|
2087
|
-
|
|
2054
|
+
const pluginCtx = {
|
|
2055
|
+
source: plan.profile.source,
|
|
2056
|
+
target: plan.profile.target
|
|
2057
|
+
};
|
|
2058
|
+
const catalogEntry = resolveMigrationCatalog2(
|
|
2059
|
+
plan.profile.source,
|
|
2060
|
+
plan.profile.target
|
|
2061
|
+
);
|
|
2062
|
+
const structureIssues = collectShadowIssues(
|
|
2063
|
+
shadowPath,
|
|
2064
|
+
pluginCtx,
|
|
2065
|
+
catalogEntry?.layer
|
|
2066
|
+
);
|
|
2088
2067
|
const buildFailed = !build.ok || !install.ok;
|
|
2089
2068
|
const verifierFailed = structureIssues.length > 0;
|
|
2090
2069
|
if (!buildFailed && !verifierFailed) {
|
|
2091
|
-
await completeMigration(
|
|
2070
|
+
await completeMigration(
|
|
2071
|
+
producerId,
|
|
2072
|
+
planId,
|
|
2073
|
+
"Verification build succeeded cleanly. Generating final migration report and instructions...",
|
|
2074
|
+
"info"
|
|
2075
|
+
);
|
|
2092
2076
|
return;
|
|
2093
2077
|
}
|
|
2094
2078
|
const implicated = parseImplicatedFiles(
|
|
@@ -2103,29 +2087,38 @@ When you have applied fixes (or cannot fix further), stop. A deterministic rebui
|
|
|
2103
2087
|
});
|
|
2104
2088
|
const errors = [
|
|
2105
2089
|
...structureIssues.flatMap((issue) => issue.errors),
|
|
2106
|
-
...install.ok ? [] : [
|
|
2090
|
+
...install.ok ? [] : [`${pmCommands.cleanInstall} failed:
|
|
2107
2091
|
${install.output.slice(0, 1500)}`],
|
|
2108
|
-
...build.ok ? [] : [
|
|
2092
|
+
...build.ok ? [] : [`${pmCommands.runBuild} failed:
|
|
2109
2093
|
${build.output.slice(0, 2500)}`]
|
|
2110
2094
|
];
|
|
2111
2095
|
if (!lastRound && repairFiles.length > 0) {
|
|
2112
2096
|
log2(
|
|
2113
2097
|
producerId,
|
|
2114
2098
|
planId,
|
|
2115
|
-
buildFailed ?
|
|
2099
|
+
buildFailed ? `Verification build failed. Reopening implicated files for automated repair...` : `Structure verifier identified ${structureIssues.length} architectural issue(s). Reopening files for repair...`,
|
|
2116
2100
|
"warning"
|
|
2117
2101
|
);
|
|
2118
|
-
await requeueBrokenFiles(
|
|
2102
|
+
await requeueBrokenFiles(
|
|
2103
|
+
producerId,
|
|
2104
|
+
planId,
|
|
2105
|
+
repairFiles,
|
|
2106
|
+
errors.length > 0 ? errors : ["Shadow verification failed."]
|
|
2107
|
+
);
|
|
2119
2108
|
return;
|
|
2120
2109
|
}
|
|
2121
2110
|
await failMigration(
|
|
2122
2111
|
producerId,
|
|
2123
2112
|
planId,
|
|
2124
|
-
lastRound ? `
|
|
2113
|
+
lastRound ? `Verification build did not pass after ${MAX_INTEGRATION_ROUNDS} integration rounds. Please inspect MIGRATION.md for diagnostic details.` : "Shadow verification failed and no repair targets could be assigned."
|
|
2125
2114
|
);
|
|
2126
2115
|
} catch (error) {
|
|
2127
2116
|
console.error(`[IntegrationAgent] Error:`, error);
|
|
2128
|
-
await failMigration(
|
|
2117
|
+
await failMigration(
|
|
2118
|
+
producerId,
|
|
2119
|
+
planId,
|
|
2120
|
+
"Integration Phase encountered an error. The migration is not successful."
|
|
2121
|
+
);
|
|
2129
2122
|
} finally {
|
|
2130
2123
|
resolveRuntime().state.integrationLocks.delete(planId);
|
|
2131
2124
|
}
|
|
@@ -2781,16 +2774,21 @@ var MigrationRunner = class {
|
|
|
2781
2774
|
*/
|
|
2782
2775
|
async startMigration(request) {
|
|
2783
2776
|
this.ensureInitialized();
|
|
2784
|
-
await this.store.
|
|
2785
|
-
|
|
2777
|
+
const existing = await this.store.getAllPlans();
|
|
2778
|
+
const protectedRunIds = existing.filter((p) => p.appliedAt).map((p) => p.runId);
|
|
2779
|
+
this.workspace.cleanupOldRuns(3, protectedRunIds);
|
|
2786
2780
|
const runId = `run_${Date.now()}`;
|
|
2787
2781
|
const shadowPath = this.workspace.cloneDirectory(request.targetPath, runId);
|
|
2788
2782
|
const planId = `plan_${randomUUID()}`;
|
|
2783
|
+
const manifest = inspectManifest(request.targetPath);
|
|
2784
|
+
const resolvedPackageManager = request.packageManager || manifest.packageManager || "npm";
|
|
2789
2785
|
const plan = {
|
|
2790
2786
|
id: planId,
|
|
2791
2787
|
runId,
|
|
2792
2788
|
profile: { source: request.from, target: request.to },
|
|
2793
2789
|
targetPath: request.targetPath,
|
|
2790
|
+
packageManager: resolvedPackageManager,
|
|
2791
|
+
phase: "files",
|
|
2794
2792
|
tasks: [
|
|
2795
2793
|
{ filePath: "system:package_manager", status: "pending" }
|
|
2796
2794
|
],
|
|
@@ -2809,7 +2807,8 @@ var MigrationRunner = class {
|
|
|
2809
2807
|
payload: {
|
|
2810
2808
|
planId,
|
|
2811
2809
|
profile: plan.profile,
|
|
2812
|
-
shadowWorkspacePath: shadowPath
|
|
2810
|
+
shadowWorkspacePath: shadowPath,
|
|
2811
|
+
packageManager: resolvedPackageManager
|
|
2813
2812
|
}
|
|
2814
2813
|
},
|
|
2815
2814
|
dispatcherId
|
|
@@ -2889,9 +2888,18 @@ function bootstrapMetamorph(repository, tools = []) {
|
|
|
2889
2888
|
}
|
|
2890
2889
|
|
|
2891
2890
|
// src/index.ts
|
|
2891
|
+
function logStartupContext() {
|
|
2892
|
+
const isDev = process.env.METAMORPH_DEV === "1";
|
|
2893
|
+
console.log(chalk.gray(`[Metamorph] CWD: ${process.cwd()}`));
|
|
2894
|
+
console.log(chalk.gray(`[Metamorph] Binary: ${import.meta.url}`));
|
|
2895
|
+
if (!isDev && import.meta.url.includes("/src/")) {
|
|
2896
|
+
console.log(chalk.yellow(`[Metamorph] Notice: Running directly from source without METAMORPH_DEV=1.`));
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2892
2899
|
var program = new Command();
|
|
2893
|
-
program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("1.
|
|
2900
|
+
program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("2.1.2");
|
|
2894
2901
|
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) => {
|
|
2902
|
+
logStartupContext();
|
|
2895
2903
|
let targetPath = targetPathArg || ".";
|
|
2896
2904
|
const { select, input } = await import("@inquirer/prompts");
|
|
2897
2905
|
let from = options.from;
|
|
@@ -2906,15 +2914,42 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2906
2914
|
angular: ["react", "next", "vue", "svelte"],
|
|
2907
2915
|
svelte: ["react", "next", "vue", "angular"]
|
|
2908
2916
|
};
|
|
2917
|
+
const monorepo = detectMonorepo(targetPath);
|
|
2918
|
+
if (monorepo.isMonorepo && monorepo.packages.length > 0) {
|
|
2919
|
+
console.log(chalk.cyan(`
|
|
2920
|
+
\u{1F4E6} Monorepo detected (${monorepo.tool || "workspaces"} with ${monorepo.packages.length} packages).`));
|
|
2921
|
+
const chosenPath = await select({
|
|
2922
|
+
message: "Select the workspace package you want to migrate:",
|
|
2923
|
+
choices: [
|
|
2924
|
+
...monorepo.packages.map((p) => ({
|
|
2925
|
+
name: `${p.name} (${p.relativePath})`,
|
|
2926
|
+
value: p.absolutePath
|
|
2927
|
+
})),
|
|
2928
|
+
{
|
|
2929
|
+
name: `Whole root directory (${targetPath})`,
|
|
2930
|
+
value: targetPath
|
|
2931
|
+
}
|
|
2932
|
+
]
|
|
2933
|
+
});
|
|
2934
|
+
targetPath = chosenPath;
|
|
2935
|
+
}
|
|
2909
2936
|
if (!from) {
|
|
2910
2937
|
console.log(chalk.gray(`
|
|
2911
2938
|
\u{1F50D} Scanning directory: ${targetPath}`));
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
2914
|
-
if (
|
|
2939
|
+
const profiles = inspectProject(targetPath);
|
|
2940
|
+
const supportedProfiles = profiles.filter((p) => Object.keys(SUPPORTED_MIGRATIONS).includes(p.framework));
|
|
2941
|
+
if (supportedProfiles.length > 0) {
|
|
2915
2942
|
from = await select({
|
|
2916
2943
|
message: "What technology do you want to migrate FROM?",
|
|
2917
|
-
choices:
|
|
2944
|
+
choices: supportedProfiles.map((p) => {
|
|
2945
|
+
const details = [
|
|
2946
|
+
p.variant !== "none" ? p.variant : null,
|
|
2947
|
+
p.bundler !== "unknown" ? p.bundler : null,
|
|
2948
|
+
p.language === "typescript" ? "TS" : "JS"
|
|
2949
|
+
].filter(Boolean).join(" | ");
|
|
2950
|
+
const label = details ? `${p.framework} (${details} - ${p.confidence}%)` : `${p.framework} (${p.confidence}%)`;
|
|
2951
|
+
return { name: label, value: p.framework };
|
|
2952
|
+
})
|
|
2918
2953
|
});
|
|
2919
2954
|
} else {
|
|
2920
2955
|
console.log(chalk.yellow(`No supported technologies detected in the current directory.`));
|
|
@@ -2975,11 +3010,41 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2975
3010
|
spinner.succeed(`Migration dispatched! Plan: ${result.planId}`);
|
|
2976
3011
|
console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
|
|
2977
3012
|
console.log(chalk.blue(`
|
|
2978
|
-
\u23F3
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3013
|
+
\u23F3 Swarm is executing migration in shadow workspace...`));
|
|
3014
|
+
const waitSpinner = ora("Waiting for swarm to complete migration...").start();
|
|
3015
|
+
const startTime = Date.now();
|
|
3016
|
+
const MAX_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
3017
|
+
while (true) {
|
|
3018
|
+
if (Date.now() - startTime >= MAX_TIMEOUT_MS) {
|
|
3019
|
+
waitSpinner.fail(chalk.red(`Migration timed out after 30 minutes.`));
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
}
|
|
3022
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
3023
|
+
const plan = await store.getPlan(result.planId);
|
|
3024
|
+
if (!plan) continue;
|
|
3025
|
+
const terminalTasks = plan.tasks.filter((t) => t.status === "completed" || t.status === "failed");
|
|
3026
|
+
const inProgressTasks = plan.tasks.filter((t) => t.status === "in_progress");
|
|
3027
|
+
const pendingTasks = plan.tasks.filter((t) => t.status === "pending");
|
|
3028
|
+
waitSpinner.text = `Phase: ${plan.phase || "files"} | Tasks: ${terminalTasks.length}/${plan.tasks.length} terminal (${inProgressTasks.length} in progress, ${pendingTasks.length} pending)`;
|
|
3029
|
+
if (plan.outcome === "success") {
|
|
3030
|
+
waitSpinner.succeed(chalk.green(`
|
|
3031
|
+
\u2705 Migration finished successfully in shadow workspace!`));
|
|
3032
|
+
console.log(chalk.white(`
|
|
3033
|
+
Next steps:`));
|
|
3034
|
+
console.log(chalk.gray(` 1. Review changes in shadow workspace: `) + chalk.cyan(result.shadowPath));
|
|
3035
|
+
console.log(chalk.gray(` 2. Apply changes to a dedicated Git branch: `) + chalk.cyan(`metamorph apply ${result.runId} ${targetPath}
|
|
3036
|
+
`));
|
|
3037
|
+
process.exit(0);
|
|
3038
|
+
}
|
|
3039
|
+
if (plan.phase === "failed" || plan.outcome === "failed") {
|
|
3040
|
+
waitSpinner.fail(chalk.red(`
|
|
3041
|
+
\u274C Migration failed in shadow workspace.`));
|
|
3042
|
+
console.log(chalk.yellow(`Check .metamorph/shadow/${result.runId}/MIGRATION.md or run 'metamorph ui' for failure details.`));
|
|
3043
|
+
console.log(chalk.gray(`Your original codebase in "${targetPath}" remains completely untouched.
|
|
3044
|
+
`));
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
2983
3048
|
} catch (error) {
|
|
2984
3049
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2985
3050
|
spinner.fail(`Migration failed: ${errorMessage}`);
|
|
@@ -2988,8 +3053,27 @@ program.command("run [path]").description("Run a migration on the specified dire
|
|
|
2988
3053
|
});
|
|
2989
3054
|
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
3055
|
try {
|
|
3056
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
3057
|
+
const plans = await store.getAllPlans();
|
|
3058
|
+
const plan = plans.find((p) => p.runId === runId);
|
|
3059
|
+
if (!plan) {
|
|
3060
|
+
throw new Error(`Plan for run "${runId}" not found in database.`);
|
|
3061
|
+
}
|
|
3062
|
+
if (plan.phase !== "completed" || plan.outcome !== "success") {
|
|
3063
|
+
throw new Error(
|
|
3064
|
+
`Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
if (plan.appliedAt) {
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
2991
3072
|
const integrator = new MigrationIntegrator(new ShadowWorkspace());
|
|
2992
3073
|
const result = await integrator.applyMigration(runId, targetPath);
|
|
3074
|
+
plan.appliedAt = /* @__PURE__ */ new Date();
|
|
3075
|
+
plan.appliedBranch = result.branch;
|
|
3076
|
+
await store.savePlan(plan);
|
|
2993
3077
|
console.log(chalk.green(`\u2705 ${result.message}`));
|
|
2994
3078
|
} catch (error) {
|
|
2995
3079
|
console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -3017,16 +3101,35 @@ program.command("reset").description("Clear all migration history and events fro
|
|
|
3017
3101
|
process.exit(1);
|
|
3018
3102
|
}
|
|
3019
3103
|
});
|
|
3020
|
-
program.command("detect [path]").description("Detect frameworks and
|
|
3104
|
+
program.command("detect [path]").description("Detect frameworks, architectural variants and monorepo structure in a directory").action(async (targetPath) => {
|
|
3021
3105
|
try {
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
|
|
3106
|
+
const resolved = targetPath || ".";
|
|
3107
|
+
const monorepo = detectMonorepo(resolved);
|
|
3108
|
+
if (monorepo.isMonorepo) {
|
|
3109
|
+
console.log(chalk.cyan(`
|
|
3110
|
+
\u{1F4E6} Monorepo Detected: ${monorepo.tool || "workspaces"}`));
|
|
3111
|
+
console.log(chalk.gray(`Root: ${monorepo.rootPath}`));
|
|
3112
|
+
console.log(chalk.gray(`Workspace Packages (${monorepo.packages.length}):`));
|
|
3113
|
+
monorepo.packages.forEach((p) => console.log(` \u2022 ${chalk.white(p.name)}: ${chalk.gray(p.relativePath)}`));
|
|
3114
|
+
}
|
|
3115
|
+
const profiles = inspectProject(resolved);
|
|
3116
|
+
if (profiles.length === 0) {
|
|
3117
|
+
console.log(chalk.yellow(`
|
|
3118
|
+
No supported technologies detected.`));
|
|
3025
3119
|
} else {
|
|
3026
3120
|
console.log(chalk.blue(`
|
|
3027
|
-
\u{1F50D} Detected Technologies:`));
|
|
3028
|
-
|
|
3029
|
-
|
|
3121
|
+
\u{1F50D} Detected Technologies & Architecture:`));
|
|
3122
|
+
profiles.forEach((p) => {
|
|
3123
|
+
console.log(`
|
|
3124
|
+
\u2022 ${chalk.bold.green(p.framework)} (Confidence: ${p.confidence}%)`);
|
|
3125
|
+
console.log(` Category: ${chalk.cyan(p.category)} | Variant: ${chalk.cyan(p.variant)} | Bundler: ${chalk.cyan(p.bundler)} | Lang: ${chalk.cyan(p.language.toUpperCase())}`);
|
|
3126
|
+
if (p.subsumedDependencies.length > 0) {
|
|
3127
|
+
console.log(` Subsumed Dependencies: ${chalk.yellow(p.subsumedDependencies.join(", "))}`);
|
|
3128
|
+
}
|
|
3129
|
+
console.log(` Suggested Targets: ${chalk.magenta(p.suggestedTargets.join(", "))}`);
|
|
3130
|
+
console.log(` Evidence: ${chalk.gray(p.evidence.join("; "))}`);
|
|
3131
|
+
});
|
|
3132
|
+
console.log("");
|
|
3030
3133
|
}
|
|
3031
3134
|
} catch (error) {
|
|
3032
3135
|
console.error(chalk.red(`\u274C Detection failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
@@ -3052,6 +3155,7 @@ program.command("list").description("List past migration plans").action(async ()
|
|
|
3052
3155
|
}
|
|
3053
3156
|
});
|
|
3054
3157
|
program.command("ui").description("Start the Metamorph Dashboard API server").option("-p, --port <number>", "Port for the API server", "9876").action(async (options) => {
|
|
3158
|
+
logStartupContext();
|
|
3055
3159
|
let desiredPort = parseInt(options.port, 10);
|
|
3056
3160
|
const spinner = ora("Starting Metamorph API server...").start();
|
|
3057
3161
|
try {
|