@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
resolveMigrationCatalog
|
|
3
3
|
} from "./chunk-VEAYUIOZ.js";
|
|
4
4
|
|
|
5
5
|
// ../infrastructure/src/db/SQLiteStateStore.ts
|
|
@@ -50,6 +50,10 @@ var SQLiteStateStore = class {
|
|
|
50
50
|
this.db.exec("ALTER TABLE plans ADD COLUMN phase TEXT");
|
|
51
51
|
} catch (e) {
|
|
52
52
|
}
|
|
53
|
+
try {
|
|
54
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN outcome TEXT");
|
|
55
|
+
} catch (e) {
|
|
56
|
+
}
|
|
53
57
|
try {
|
|
54
58
|
this.db.exec("ALTER TABLE plans ADD COLUMN applied_at TEXT");
|
|
55
59
|
} catch (e) {
|
|
@@ -80,8 +84,8 @@ var SQLiteStateStore = class {
|
|
|
80
84
|
}
|
|
81
85
|
async savePlan(plan) {
|
|
82
86
|
const stmtPlan = this.db.prepare(`
|
|
83
|
-
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, applied_at, applied_branch)
|
|
84
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
87
|
+
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, outcome, applied_at, applied_branch)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
85
89
|
`);
|
|
86
90
|
stmtPlan.run(
|
|
87
91
|
plan.id,
|
|
@@ -93,6 +97,7 @@ var SQLiteStateStore = class {
|
|
|
93
97
|
plan.createdAt.toISOString(),
|
|
94
98
|
plan.integrationRounds ?? 0,
|
|
95
99
|
plan.phase || "files",
|
|
100
|
+
plan.outcome || null,
|
|
96
101
|
plan.appliedAt ? plan.appliedAt.toISOString() : null,
|
|
97
102
|
plan.appliedBranch || null
|
|
98
103
|
);
|
|
@@ -123,6 +128,7 @@ var SQLiteStateStore = class {
|
|
|
123
128
|
createdAt: new Date(planRow.created_at),
|
|
124
129
|
integrationRounds: planRow.integration_rounds ?? 0,
|
|
125
130
|
phase: planRow.phase || "files",
|
|
131
|
+
outcome: planRow.outcome || void 0,
|
|
126
132
|
appliedAt: planRow.applied_at ? new Date(planRow.applied_at) : void 0,
|
|
127
133
|
appliedBranch: planRow.applied_branch || void 0,
|
|
128
134
|
tasks: taskRows.map((row) => ({
|
|
@@ -195,7 +201,8 @@ function assertSandbox(filePath, sandboxDir) {
|
|
|
195
201
|
const resolved = path.resolve(filePath);
|
|
196
202
|
if (sandboxDir) {
|
|
197
203
|
const resolvedSandbox = path.resolve(sandboxDir);
|
|
198
|
-
|
|
204
|
+
const isInside = resolved === resolvedSandbox || resolved.startsWith(resolvedSandbox + path.sep);
|
|
205
|
+
if (!isInside) {
|
|
199
206
|
throw new Error(
|
|
200
207
|
`BLOCKED: Path "${resolved}" is outside the sandbox "${resolvedSandbox}". All file operations must stay within the shadow workspace.`
|
|
201
208
|
);
|
|
@@ -396,6 +403,16 @@ function createListDirectoryTool(sandboxDir) {
|
|
|
396
403
|
}
|
|
397
404
|
};
|
|
398
405
|
}
|
|
406
|
+
function createShadowTools(shadowDir) {
|
|
407
|
+
return [
|
|
408
|
+
createReadFileTool(shadowDir),
|
|
409
|
+
createWriteFileTool(shadowDir),
|
|
410
|
+
createRenameFileTool(shadowDir),
|
|
411
|
+
createCreateFileTool(shadowDir),
|
|
412
|
+
createDeleteFileTool(shadowDir),
|
|
413
|
+
createListDirectoryTool(shadowDir)
|
|
414
|
+
];
|
|
415
|
+
}
|
|
399
416
|
|
|
400
417
|
// ../infrastructure/src/workspace/ShadowWorkspace.ts
|
|
401
418
|
import fs from "fs-extra";
|
|
@@ -454,15 +471,16 @@ var ShadowWorkspace = class {
|
|
|
454
471
|
* Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
|
|
455
472
|
* @param keepCount Number of recent runs to keep (default: 3)
|
|
456
473
|
*/
|
|
457
|
-
cleanupOldRuns(keepCount = 3) {
|
|
474
|
+
cleanupOldRuns(keepCount = 3, protectedRunIds = []) {
|
|
458
475
|
if (!fs.existsSync(this.baseDir)) return;
|
|
476
|
+
const protectedSet = new Set(protectedRunIds);
|
|
459
477
|
const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
|
|
460
478
|
runs.sort();
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
const unprotected = runs.filter((run) => !protectedSet.has(run));
|
|
480
|
+
if (unprotected.length <= keepCount) return;
|
|
481
|
+
const toDelete = unprotected.slice(0, unprotected.length - keepCount);
|
|
482
|
+
for (const run of toDelete) {
|
|
483
|
+
this.rollback(run);
|
|
466
484
|
}
|
|
467
485
|
}
|
|
468
486
|
};
|
|
@@ -472,7 +490,11 @@ import fs2 from "fs-extra";
|
|
|
472
490
|
import * as path2 from "path";
|
|
473
491
|
import { execFileSync } from "child_process";
|
|
474
492
|
var COPY_IGNORED = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
|
|
475
|
-
|
|
493
|
+
function isCopyIgnored(itemName) {
|
|
494
|
+
if (COPY_IGNORED.includes(itemName)) return true;
|
|
495
|
+
if (itemName === ".env" || itemName.startsWith(".env.")) return true;
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
476
498
|
var MigrationIntegrator = class {
|
|
477
499
|
shadowWorkspace;
|
|
478
500
|
constructor(shadowWorkspace) {
|
|
@@ -494,6 +516,20 @@ var MigrationIntegrator = class {
|
|
|
494
516
|
"Metamorph apply requires a Git repository. Run this from a project that already has git (including monorepo packages whose .git lives in a parent folder). Nested git init is not supported."
|
|
495
517
|
);
|
|
496
518
|
}
|
|
519
|
+
const rootPkgJson = path2.join(gitRoot, "package.json");
|
|
520
|
+
if (fs2.existsSync(rootPkgJson)) {
|
|
521
|
+
try {
|
|
522
|
+
const pkg = JSON.parse(fs2.readFileSync(rootPkgJson, "utf-8"));
|
|
523
|
+
const normalizedTarget = resolvedTarget.replace(/\\/g, "/");
|
|
524
|
+
if (pkg.name === "metamorph-monorepo" && normalizedTarget.includes("/scratch/")) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`BLOCKED: Cannot apply to "${resolvedTarget}" because it is inside the development scratch/ directory of the Metamorph repository. Use a standalone git repository for user project migrations.`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
if (e instanceof Error && e.message.startsWith("BLOCKED:")) throw e;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
497
533
|
if (this.isIgnoredByGit(gitRoot, resolvedTarget)) {
|
|
498
534
|
throw new Error(
|
|
499
535
|
`Cannot apply via Git: "${resolvedTarget}" is ignored by the repository at "${gitRoot}" (.gitignore). Metamorph will not create branches on a parent repo for ignored sandboxes. Use a standalone git project, or a tracked package inside a monorepo.`
|
|
@@ -536,22 +572,17 @@ var MigrationIntegrator = class {
|
|
|
536
572
|
throw new Error(`git ${args.join(" ")} failed: ${detail || "unknown git error"}`);
|
|
537
573
|
}
|
|
538
574
|
}
|
|
539
|
-
ensureApplyGitignore(gitRoot) {
|
|
540
|
-
const gitignorePath = path2.join(gitRoot, ".gitignore");
|
|
541
|
-
const existing = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
|
|
542
|
-
const missing = GITIGNORE_ENTRIES.filter((entry) => {
|
|
543
|
-
const name = entry.replace(/\/$/, "").replace(".", "\\.");
|
|
544
|
-
const pattern = new RegExp(`(^|\\n)${name}(/)?(\\n|$)`);
|
|
545
|
-
return !pattern.test(existing.replace(/\r/g, "\n"));
|
|
546
|
-
});
|
|
547
|
-
if (missing.length === 0) return;
|
|
548
|
-
const block = `
|
|
549
|
-
# Metamorph apply
|
|
550
|
-
${missing.join("\n")}
|
|
551
|
-
`;
|
|
552
|
-
fs2.appendFileSync(gitignorePath, block, "utf-8");
|
|
553
|
-
}
|
|
554
575
|
async applyWithGit(runId, shadowDir, targetPath, gitRoot, branchName) {
|
|
576
|
+
const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
|
|
577
|
+
const addPath = relativeTarget === "" ? "." : relativeTarget;
|
|
578
|
+
const dirtyStatus = this.git(["status", "--porcelain", "--", addPath], gitRoot).trim();
|
|
579
|
+
if (dirtyStatus) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Cannot apply migration: the target directory has uncommitted changes:
|
|
582
|
+
${dirtyStatus}
|
|
583
|
+
Please commit or stash your changes before applying.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
555
586
|
try {
|
|
556
587
|
this.git(["checkout", "-B", branchName], gitRoot);
|
|
557
588
|
} catch (e) {
|
|
@@ -561,9 +592,6 @@ ${missing.join("\n")}
|
|
|
561
592
|
throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
|
|
562
593
|
}
|
|
563
594
|
this.copyShadowToTarget(shadowDir, targetPath);
|
|
564
|
-
this.ensureApplyGitignore(gitRoot);
|
|
565
|
-
const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
|
|
566
|
-
const addPath = relativeTarget === "" ? "." : relativeTarget;
|
|
567
595
|
try {
|
|
568
596
|
try {
|
|
569
597
|
this.git(["-c", "advice.addIgnoredFile=false", "add", "-A", "--", addPath], gitRoot);
|
|
@@ -597,13 +625,13 @@ ${missing.join("\n")}
|
|
|
597
625
|
if (fs2.existsSync(targetPath)) {
|
|
598
626
|
const targetItems = fs2.readdirSync(targetPath);
|
|
599
627
|
for (const item of targetItems) {
|
|
600
|
-
if (
|
|
628
|
+
if (isCopyIgnored(item)) continue;
|
|
601
629
|
fs2.removeSync(path2.join(targetPath, item));
|
|
602
630
|
}
|
|
603
631
|
}
|
|
604
632
|
const shadowItems = fs2.readdirSync(shadowDir);
|
|
605
633
|
for (const item of shadowItems) {
|
|
606
|
-
if (
|
|
634
|
+
if (isCopyIgnored(item)) continue;
|
|
607
635
|
const itemSrc = path2.join(shadowDir, item);
|
|
608
636
|
const itemDest = path2.join(targetPath, item);
|
|
609
637
|
fs2.copySync(itemSrc, itemDest, { overwrite: true });
|
|
@@ -702,7 +730,7 @@ async function createApiServer(store, migrationRunner) {
|
|
|
702
730
|
});
|
|
703
731
|
return;
|
|
704
732
|
}
|
|
705
|
-
const catalogEntry =
|
|
733
|
+
const catalogEntry = resolveMigrationCatalog(from, to);
|
|
706
734
|
if (!catalogEntry) {
|
|
707
735
|
res.status(400).json({
|
|
708
736
|
error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
|
|
@@ -732,15 +760,29 @@ async function createApiServer(store, migrationRunner) {
|
|
|
732
760
|
});
|
|
733
761
|
return;
|
|
734
762
|
}
|
|
735
|
-
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
736
|
-
const result = await integrator.applyMigration(runId, targetPath);
|
|
737
763
|
const plans = await store.getAllPlans();
|
|
738
764
|
const plan = plans.find((item) => item.runId === runId);
|
|
739
|
-
if (plan) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
await store.savePlan(plan);
|
|
765
|
+
if (!plan) {
|
|
766
|
+
res.status(404).json({ error: `Plan for run "${runId}" not found in database.` });
|
|
767
|
+
return;
|
|
743
768
|
}
|
|
769
|
+
if (plan.phase !== "completed" || plan.outcome !== "success") {
|
|
770
|
+
res.status(400).json({
|
|
771
|
+
error: `Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (plan.appliedAt) {
|
|
776
|
+
res.status(400).json({
|
|
777
|
+
error: `Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
|
|
778
|
+
});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
782
|
+
const result = await integrator.applyMigration(runId, targetPath);
|
|
783
|
+
plan.appliedAt = /* @__PURE__ */ new Date();
|
|
784
|
+
plan.appliedBranch = result.branch;
|
|
785
|
+
await store.savePlan(plan);
|
|
744
786
|
await store.logEvent("migration.applied", { runId, branch: result.branch, targetPath });
|
|
745
787
|
res.json({ ok: true, ...result });
|
|
746
788
|
} catch (error) {
|
|
@@ -918,7 +960,11 @@ function parseImplicatedFiles(output, shadowPath) {
|
|
|
918
960
|
const resolved = path4.resolve(abs);
|
|
919
961
|
const inside = resolved === root || resolved.startsWith(root + path4.sep);
|
|
920
962
|
if (!inside) return;
|
|
921
|
-
if (fs3.existsSync(resolved)
|
|
963
|
+
if (fs3.existsSync(resolved)) {
|
|
964
|
+
if (fs3.statSync(resolved).isFile()) found.add(resolved);
|
|
965
|
+
} else {
|
|
966
|
+
found.add(resolved);
|
|
967
|
+
}
|
|
922
968
|
};
|
|
923
969
|
for (const re of patterns) {
|
|
924
970
|
let match;
|
|
@@ -953,13 +999,16 @@ ${result.output}`;
|
|
|
953
999
|
|
|
954
1000
|
export {
|
|
955
1001
|
SQLiteStateStore,
|
|
1002
|
+
assertSandbox,
|
|
956
1003
|
createReadFileTool,
|
|
957
1004
|
createWriteFileTool,
|
|
958
1005
|
createRenameFileTool,
|
|
959
1006
|
createCreateFileTool,
|
|
960
1007
|
createDeleteFileTool,
|
|
961
1008
|
createListDirectoryTool,
|
|
1009
|
+
createShadowTools,
|
|
962
1010
|
ShadowWorkspace,
|
|
1011
|
+
isCopyIgnored,
|
|
963
1012
|
MigrationIntegrator,
|
|
964
1013
|
createApiServer,
|
|
965
1014
|
createCheckProjectDiagnosticsTool,
|