@nikelyh/metamorph 2.0.11 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,969 @@
1
+ import {
2
+ findMigrationCatalogEntry
3
+ } from "./chunk-VEAYUIOZ.js";
4
+
5
+ // ../infrastructure/src/db/SQLiteStateStore.ts
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { join } from "path";
8
+ import { mkdirSync } from "fs";
9
+ var SQLiteStateStore = class {
10
+ db;
11
+ constructor(storageDir = ".metamorph") {
12
+ try {
13
+ mkdirSync(storageDir, { recursive: true });
14
+ } catch (e) {
15
+ }
16
+ const dbPath = join(storageDir, "history.db");
17
+ this.db = new DatabaseSync(dbPath);
18
+ this.initTables();
19
+ }
20
+ /**
21
+ * Creates the necessary tables if they do not exist.
22
+ */
23
+ initTables() {
24
+ this.db.exec(`
25
+ CREATE TABLE IF NOT EXISTS plans (
26
+ id TEXT PRIMARY KEY,
27
+ run_id TEXT,
28
+ target_path TEXT,
29
+ source_framework TEXT,
30
+ target_framework TEXT,
31
+ rules_json TEXT,
32
+ created_at TEXT,
33
+ integration_rounds INTEGER DEFAULT 0,
34
+ phase TEXT
35
+ );
36
+ `);
37
+ try {
38
+ this.db.exec("ALTER TABLE plans ADD COLUMN run_id TEXT");
39
+ } catch (e) {
40
+ }
41
+ try {
42
+ this.db.exec("ALTER TABLE plans ADD COLUMN target_path TEXT");
43
+ } catch (e) {
44
+ }
45
+ try {
46
+ this.db.exec("ALTER TABLE plans ADD COLUMN integration_rounds INTEGER");
47
+ } catch (e) {
48
+ }
49
+ try {
50
+ this.db.exec("ALTER TABLE plans ADD COLUMN phase TEXT");
51
+ } catch (e) {
52
+ }
53
+ try {
54
+ this.db.exec("ALTER TABLE plans ADD COLUMN applied_at TEXT");
55
+ } catch (e) {
56
+ }
57
+ try {
58
+ this.db.exec("ALTER TABLE plans ADD COLUMN applied_branch TEXT");
59
+ } catch (e) {
60
+ }
61
+ this.db.exec(`
62
+ CREATE TABLE IF NOT EXISTS tasks (
63
+ plan_id TEXT,
64
+ file_path TEXT,
65
+ status TEXT,
66
+ dependencies_json TEXT,
67
+ error TEXT,
68
+ PRIMARY KEY (plan_id, file_path),
69
+ FOREIGN KEY (plan_id) REFERENCES plans(id)
70
+ );
71
+ `);
72
+ this.db.exec(`
73
+ CREATE TABLE IF NOT EXISTS events (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ event_name TEXT,
76
+ payload_json TEXT,
77
+ timestamp TEXT
78
+ );
79
+ `);
80
+ }
81
+ async savePlan(plan) {
82
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
+ `);
86
+ stmtPlan.run(
87
+ plan.id,
88
+ plan.runId,
89
+ plan.targetPath || "",
90
+ plan.profile.source,
91
+ plan.profile.target,
92
+ JSON.stringify(plan.profile.rules || []),
93
+ plan.createdAt.toISOString(),
94
+ plan.integrationRounds ?? 0,
95
+ plan.phase || "files",
96
+ plan.appliedAt ? plan.appliedAt.toISOString() : null,
97
+ plan.appliedBranch || null
98
+ );
99
+ const stmtTask = this.db.prepare(`
100
+ INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
101
+ VALUES (?, ?, ?, ?, ?)
102
+ `);
103
+ for (const task of plan.tasks) {
104
+ stmtTask.run(
105
+ plan.id,
106
+ task.filePath,
107
+ task.status,
108
+ JSON.stringify(task.dependencies || []),
109
+ task.error || null
110
+ );
111
+ }
112
+ }
113
+ mapPlan(planRow, taskRows) {
114
+ return {
115
+ id: planRow.id,
116
+ runId: planRow.run_id,
117
+ targetPath: planRow.target_path,
118
+ profile: {
119
+ source: planRow.source_framework,
120
+ target: planRow.target_framework,
121
+ rules: JSON.parse(planRow.rules_json)
122
+ },
123
+ createdAt: new Date(planRow.created_at),
124
+ integrationRounds: planRow.integration_rounds ?? 0,
125
+ phase: planRow.phase || "files",
126
+ appliedAt: planRow.applied_at ? new Date(planRow.applied_at) : void 0,
127
+ appliedBranch: planRow.applied_branch || void 0,
128
+ tasks: taskRows.map((row) => ({
129
+ filePath: row.file_path,
130
+ status: row.status,
131
+ dependencies: JSON.parse(row.dependencies_json),
132
+ error: row.error || void 0
133
+ }))
134
+ };
135
+ }
136
+ async getPlan(id) {
137
+ const stmtPlan = this.db.prepare(`SELECT * FROM plans WHERE id = ?`);
138
+ const planRow = stmtPlan.get(id);
139
+ if (!planRow) {
140
+ return null;
141
+ }
142
+ const stmtTasks = this.db.prepare(`SELECT * FROM tasks WHERE plan_id = ?`);
143
+ const taskRows = stmtTasks.all(id);
144
+ return this.mapPlan(planRow, taskRows);
145
+ }
146
+ async updateTaskStatus(planId, filePath, status, error) {
147
+ const stmt = this.db.prepare(`
148
+ INSERT INTO tasks (plan_id, file_path, status, error, dependencies_json)
149
+ VALUES (?, ?, ?, ?, '[]')
150
+ ON CONFLICT(plan_id, file_path) DO UPDATE
151
+ SET status = excluded.status, error = excluded.error
152
+ `);
153
+ stmt.run(planId, filePath, status, error || null);
154
+ }
155
+ async logEvent(eventName, payload) {
156
+ const stmt = this.db.prepare(`
157
+ INSERT INTO events (event_name, payload_json, timestamp)
158
+ VALUES (?, ?, ?)
159
+ `);
160
+ stmt.run(eventName, JSON.stringify(payload), (/* @__PURE__ */ new Date()).toISOString());
161
+ }
162
+ async getAllPlans() {
163
+ const stmtPlans = this.db.prepare(`SELECT * FROM plans ORDER BY created_at DESC`);
164
+ const planRows = stmtPlans.all();
165
+ const stmtTasks = this.db.prepare(`SELECT * FROM tasks`);
166
+ const taskRows = stmtTasks.all();
167
+ return planRows.map((planRow) => {
168
+ const planTasks = taskRows.filter((t) => t.plan_id === planRow.id);
169
+ return this.mapPlan(planRow, planTasks);
170
+ });
171
+ }
172
+ async getEvents() {
173
+ const stmt = this.db.prepare(`SELECT * FROM events ORDER BY id DESC LIMIT 2000`);
174
+ const rows = stmt.all();
175
+ return rows.map((row) => ({
176
+ id: row.id,
177
+ eventName: row.event_name,
178
+ payload: JSON.parse(row.payload_json),
179
+ timestamp: new Date(row.timestamp)
180
+ }));
181
+ }
182
+ async reset() {
183
+ this.db.exec(`DELETE FROM tasks`);
184
+ this.db.exec(`DELETE FROM plans`);
185
+ this.db.exec(`DELETE FROM events`);
186
+ }
187
+ };
188
+
189
+ // ../infrastructure/src/tools/AstTools.ts
190
+ import { Project } from "ts-morph";
191
+ import { existsSync } from "fs";
192
+ import * as path from "path";
193
+ var project = new Project();
194
+ function assertSandbox(filePath, sandboxDir) {
195
+ const resolved = path.resolve(filePath);
196
+ if (sandboxDir) {
197
+ const resolvedSandbox = path.resolve(sandboxDir);
198
+ if (!resolved.startsWith(resolvedSandbox)) {
199
+ throw new Error(
200
+ `BLOCKED: Path "${resolved}" is outside the sandbox "${resolvedSandbox}". All file operations must stay within the shadow workspace.`
201
+ );
202
+ }
203
+ }
204
+ return resolved;
205
+ }
206
+ function createReadFileTool(sandboxDir) {
207
+ return {
208
+ type: "function",
209
+ name: "read_file",
210
+ description: "Reads the content of a source file using ts-morph.",
211
+ parameters: {
212
+ type: "object",
213
+ properties: {
214
+ filePath: { type: "string", description: "The absolute path to the file to read." }
215
+ },
216
+ required: ["filePath"],
217
+ additionalProperties: false
218
+ },
219
+ strict: true,
220
+ invoke: async ({ filePath }) => {
221
+ const resolvedPath = assertSandbox(filePath, sandboxDir);
222
+ if (!existsSync(resolvedPath)) {
223
+ return { error: `File not found: ${resolvedPath}` };
224
+ }
225
+ let sourceFile = project.getSourceFile(resolvedPath);
226
+ if (!sourceFile) {
227
+ project.addSourceFileAtPath(resolvedPath);
228
+ sourceFile = project.getSourceFileOrThrow(resolvedPath);
229
+ } else {
230
+ await sourceFile.refreshFromFileSystem();
231
+ }
232
+ return {
233
+ filePath: resolvedPath,
234
+ content: sourceFile.getFullText()
235
+ };
236
+ }
237
+ };
238
+ }
239
+ function createWriteFileTool(sandboxDir) {
240
+ return {
241
+ type: "function",
242
+ name: "write_file",
243
+ description: "Overwrites the content of a source file with the provided refactored code using ts-morph.",
244
+ parameters: {
245
+ type: "object",
246
+ properties: {
247
+ filePath: { type: "string", description: "The absolute path to the file to modify." },
248
+ newContent: { type: "string", description: "The entire new source code to write to the file." }
249
+ },
250
+ required: ["filePath", "newContent"],
251
+ additionalProperties: false
252
+ },
253
+ strict: true,
254
+ invoke: async ({ filePath, newContent }) => {
255
+ const resolvedPath = assertSandbox(filePath, sandboxDir);
256
+ let sourceFile = project.getSourceFile(resolvedPath);
257
+ if (!sourceFile) {
258
+ if (existsSync(resolvedPath)) {
259
+ project.addSourceFileAtPath(resolvedPath);
260
+ sourceFile = project.getSourceFileOrThrow(resolvedPath);
261
+ } else {
262
+ sourceFile = project.createSourceFile(resolvedPath, "");
263
+ }
264
+ }
265
+ sourceFile.replaceWithText(newContent);
266
+ await sourceFile.save();
267
+ return {
268
+ success: true,
269
+ message: `File ${resolvedPath} successfully updated.`
270
+ };
271
+ }
272
+ };
273
+ }
274
+ function createRenameFileTool(sandboxDir) {
275
+ return {
276
+ type: "function",
277
+ name: "rename_file",
278
+ description: "Renames a file in the project.",
279
+ parameters: {
280
+ type: "object",
281
+ properties: {
282
+ oldPath: { type: "string", description: "Current absolute path of the file." },
283
+ newPath: { type: "string", description: "New absolute path for the file." }
284
+ },
285
+ required: ["oldPath", "newPath"],
286
+ additionalProperties: false
287
+ },
288
+ strict: true,
289
+ invoke: async ({ oldPath, newPath }) => {
290
+ const resolvedOld = assertSandbox(oldPath, sandboxDir);
291
+ const resolvedNew = assertSandbox(newPath, sandboxDir);
292
+ const { renameSync, existsSync: fsExists } = await import("fs");
293
+ if (!fsExists(resolvedOld)) {
294
+ return { error: `File not found: ${resolvedOld}` };
295
+ }
296
+ const { mkdirSync: mkdirSync2 } = await import("fs");
297
+ const { dirname } = await import("path");
298
+ mkdirSync2(dirname(resolvedNew), { recursive: true });
299
+ renameSync(resolvedOld, resolvedNew);
300
+ const sourceFile = project.getSourceFile(resolvedOld);
301
+ if (sourceFile) {
302
+ project.removeSourceFile(sourceFile);
303
+ if (fsExists(resolvedNew)) {
304
+ project.addSourceFileAtPath(resolvedNew);
305
+ }
306
+ }
307
+ return { success: true, message: `File renamed from ${resolvedOld} to ${resolvedNew}.` };
308
+ }
309
+ };
310
+ }
311
+ function createCreateFileTool(sandboxDir) {
312
+ return {
313
+ type: "function",
314
+ name: "create_file",
315
+ description: "Creates a new file with the specified content.",
316
+ parameters: {
317
+ type: "object",
318
+ properties: {
319
+ filePath: { type: "string", description: "Absolute path of the new file to create." },
320
+ content: { type: "string", description: "The content of the new file." }
321
+ },
322
+ required: ["filePath", "content"],
323
+ additionalProperties: false
324
+ },
325
+ strict: true,
326
+ invoke: async ({ filePath, content }) => {
327
+ const resolvedPath = assertSandbox(filePath, sandboxDir);
328
+ const { writeFileSync, mkdirSync: mkdirSync2 } = await import("fs");
329
+ const { dirname } = await import("path");
330
+ mkdirSync2(dirname(resolvedPath), { recursive: true });
331
+ writeFileSync(resolvedPath, content, "utf-8");
332
+ project.addSourceFileAtPath(resolvedPath);
333
+ return { success: true, message: `File created at ${resolvedPath}.` };
334
+ }
335
+ };
336
+ }
337
+ function createDeleteFileTool(sandboxDir) {
338
+ return {
339
+ type: "function",
340
+ name: "delete_file",
341
+ description: "Deletes a file from the project.",
342
+ parameters: {
343
+ type: "object",
344
+ properties: {
345
+ filePath: { type: "string", description: "Absolute path of the file to delete." }
346
+ },
347
+ required: ["filePath"],
348
+ additionalProperties: false
349
+ },
350
+ strict: true,
351
+ invoke: async ({ filePath }) => {
352
+ const resolvedPath = assertSandbox(filePath, sandboxDir);
353
+ const { renameSync, existsSync: fsExists } = await import("fs");
354
+ if (!fsExists(resolvedPath)) {
355
+ return { error: `File not found: ${resolvedPath}` };
356
+ }
357
+ const obsoletePath = `${resolvedPath}.obsolete`;
358
+ renameSync(resolvedPath, obsoletePath);
359
+ const sourceFile = project.getSourceFile(resolvedPath);
360
+ if (sourceFile) {
361
+ project.removeSourceFile(sourceFile);
362
+ }
363
+ return { success: true, message: `File marked as obsolete at ${obsoletePath} instead of deleting.` };
364
+ }
365
+ };
366
+ }
367
+ function createListDirectoryTool(sandboxDir) {
368
+ return {
369
+ type: "function",
370
+ name: "list_directory",
371
+ description: "Lists files and folders in a directory inside the shadow workspace. Use this to find existing screens, entry files, or imports instead of guessing paths.",
372
+ parameters: {
373
+ type: "object",
374
+ properties: {
375
+ dirPath: { type: "string", description: "Absolute directory path inside the shadow workspace." }
376
+ },
377
+ required: ["dirPath"],
378
+ additionalProperties: false
379
+ },
380
+ strict: true,
381
+ invoke: async ({ dirPath }) => {
382
+ const resolvedPath = assertSandbox(dirPath, sandboxDir);
383
+ const { readdirSync, statSync: statSync2, existsSync: fsExists } = await import("fs");
384
+ if (!fsExists(resolvedPath)) {
385
+ return { error: `Directory not found: ${resolvedPath}` };
386
+ }
387
+ if (!statSync2(resolvedPath).isDirectory()) {
388
+ return { error: `Not a directory: ${resolvedPath}` };
389
+ }
390
+ const entries = readdirSync(resolvedPath, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").slice(0, 80).map((entry) => ({
391
+ name: entry.name,
392
+ path: path.join(resolvedPath, entry.name),
393
+ kind: entry.isDirectory() ? "dir" : "file"
394
+ }));
395
+ return { dirPath: resolvedPath, entries };
396
+ }
397
+ };
398
+ }
399
+
400
+ // ../infrastructure/src/workspace/ShadowWorkspace.ts
401
+ import fs from "fs-extra";
402
+ import { join as join3, resolve as resolve2 } from "path";
403
+ var ShadowWorkspace = class {
404
+ baseDir;
405
+ constructor(basePath = ".metamorph/shadow") {
406
+ this.baseDir = resolve2(process.cwd(), basePath);
407
+ }
408
+ /**
409
+ * Clones a target directory into the shadow workspace.
410
+ * @param targetPath The absolute or relative path to the directory to clone.
411
+ * @param runId A unique identifier for this run (e.g. timestamp).
412
+ * @returns The absolute path to the cloned directory inside the shadow workspace.
413
+ */
414
+ cloneDirectory(targetPath, runId) {
415
+ const sourceDir = resolve2(process.cwd(), targetPath);
416
+ if (!fs.existsSync(sourceDir)) {
417
+ throw new Error(`Target directory does not exist: ${sourceDir}`);
418
+ }
419
+ const shadowDir = join3(this.baseDir, runId);
420
+ if (fs.existsSync(shadowDir)) {
421
+ fs.emptyDirSync(shadowDir);
422
+ }
423
+ const ignoredDirs = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
424
+ const items = fs.readdirSync(sourceDir);
425
+ for (const item of items) {
426
+ if (ignoredDirs.includes(item)) {
427
+ continue;
428
+ }
429
+ const itemSrc = join3(sourceDir, item);
430
+ const itemDest = join3(shadowDir, item);
431
+ fs.copySync(itemSrc, itemDest, {
432
+ filter: (src) => !ignoredDirs.some((dir) => src.includes(`/${dir}/`) || src.endsWith(`/${dir}`) || src.includes(`\\${dir}\\`) || src.endsWith(`\\${dir}`))
433
+ });
434
+ }
435
+ return shadowDir;
436
+ }
437
+ /**
438
+ * Removes the shadow workspace for a given run, effectively
439
+ * discarding any in-progress migration work.
440
+ */
441
+ rollback(runId) {
442
+ const shadowDir = join3(this.baseDir, runId);
443
+ if (fs.existsSync(shadowDir)) {
444
+ fs.removeSync(shadowDir);
445
+ }
446
+ }
447
+ /**
448
+ * Returns the absolute path of the shadow dir for a given runId.
449
+ */
450
+ getShadowPath(runId) {
451
+ return join3(this.baseDir, runId);
452
+ }
453
+ /**
454
+ * Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
455
+ * @param keepCount Number of recent runs to keep (default: 3)
456
+ */
457
+ cleanupOldRuns(keepCount = 3) {
458
+ if (!fs.existsSync(this.baseDir)) return;
459
+ const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
460
+ runs.sort();
461
+ if (runs.length > keepCount) {
462
+ const toDelete = runs.slice(0, runs.length - keepCount);
463
+ for (const run of toDelete) {
464
+ this.rollback(run);
465
+ }
466
+ }
467
+ }
468
+ };
469
+
470
+ // ../infrastructure/src/workspace/MigrationIntegrator.ts
471
+ import fs2 from "fs-extra";
472
+ import * as path2 from "path";
473
+ import { execFileSync } from "child_process";
474
+ var COPY_IGNORED = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
475
+ var GITIGNORE_ENTRIES = [".metamorph/", ".next/", "node_modules/", "dist/", "build/", "coverage/", ".env"];
476
+ var MigrationIntegrator = class {
477
+ shadowWorkspace;
478
+ constructor(shadowWorkspace) {
479
+ this.shadowWorkspace = shadowWorkspace;
480
+ }
481
+ /**
482
+ * Applies the migrated code from the shadow workspace back to the target directory.
483
+ * Requires a Git repository that actually versions the target (not a parent repo that gitignores it).
484
+ */
485
+ async applyMigration(runId, targetPath) {
486
+ const shadowDir = this.shadowWorkspace.getShadowPath(runId);
487
+ if (!fs2.existsSync(shadowDir)) {
488
+ throw new Error(`Shadow run not found: ${shadowDir}`);
489
+ }
490
+ const resolvedTarget = path2.resolve(process.cwd(), targetPath);
491
+ const gitRoot = this.resolveGitRoot(resolvedTarget);
492
+ if (!gitRoot) {
493
+ throw new Error(
494
+ "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
+ );
496
+ }
497
+ if (this.isIgnoredByGit(gitRoot, resolvedTarget)) {
498
+ throw new Error(
499
+ `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.`
500
+ );
501
+ }
502
+ return this.applyWithGit(runId, shadowDir, resolvedTarget, gitRoot, `metamorph/${runId}`);
503
+ }
504
+ /**
505
+ * Walks up from targetPath via `git rev-parse --show-toplevel`.
506
+ */
507
+ resolveGitRoot(targetPath) {
508
+ try {
509
+ const root = this.git(["rev-parse", "--show-toplevel"], targetPath).trim();
510
+ return root || null;
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+ isIgnoredByGit(gitRoot, targetPath) {
516
+ try {
517
+ execFileSync("git", ["check-ignore", "-q", "--", targetPath], {
518
+ cwd: gitRoot,
519
+ stdio: "ignore"
520
+ });
521
+ return true;
522
+ } catch {
523
+ return false;
524
+ }
525
+ }
526
+ git(args, cwd) {
527
+ try {
528
+ return execFileSync("git", args, {
529
+ cwd,
530
+ encoding: "utf-8",
531
+ stdio: ["ignore", "pipe", "pipe"]
532
+ });
533
+ } catch (error) {
534
+ const err = error;
535
+ const detail = [err.stderr, err.stdout, err.message].map((part) => (Buffer.isBuffer(part) ? part.toString("utf-8") : part) || "").filter(Boolean).join("\n").trim();
536
+ throw new Error(`git ${args.join(" ")} failed: ${detail || "unknown git error"}`);
537
+ }
538
+ }
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
+ async applyWithGit(runId, shadowDir, targetPath, gitRoot, branchName) {
555
+ try {
556
+ this.git(["checkout", "-B", branchName], gitRoot);
557
+ } catch (e) {
558
+ if (e instanceof Error) {
559
+ throw new Error(`Failed to create/reset git branch ${branchName}: ${e.message}`);
560
+ }
561
+ throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
562
+ }
563
+ this.copyShadowToTarget(shadowDir, targetPath);
564
+ this.ensureApplyGitignore(gitRoot);
565
+ const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
566
+ const addPath = relativeTarget === "" ? "." : relativeTarget;
567
+ try {
568
+ try {
569
+ this.git(["-c", "advice.addIgnoredFile=false", "add", "-A", "--", addPath], gitRoot);
570
+ } catch (addError) {
571
+ const detail = addError instanceof Error ? addError.message : String(addError);
572
+ const ignoredOnly = /paths are ignored by one of your \.gitignore/i.test(detail) && !/not a git repository|index\.lock|permission denied|tell me who you are/i.test(detail);
573
+ if (!ignoredOnly) {
574
+ throw addError;
575
+ }
576
+ }
577
+ this.git(
578
+ ["-c", "advice.addIgnoredFile=false", "commit", "--allow-empty", "-m", `chore: apply metamorph ai migration (${runId})`],
579
+ gitRoot
580
+ );
581
+ } catch (e) {
582
+ const detail = e instanceof Error ? e.message : String(e);
583
+ if (/tell me who you are|user\.email|user\.name/i.test(detail)) {
584
+ throw new Error(
585
+ `Failed to commit to branch ${branchName}: Git user.name / user.email are not set. Run git config user.email and git config user.name in this repo, then retry apply.`
586
+ );
587
+ }
588
+ throw new Error(`Failed to commit changes to branch ${branchName}: ${detail}`);
589
+ }
590
+ return {
591
+ message: `Successfully applied migration via Git to branch: ${branchName}`,
592
+ gitUsed: true,
593
+ branch: branchName
594
+ };
595
+ }
596
+ copyShadowToTarget(shadowDir, targetPath) {
597
+ if (fs2.existsSync(targetPath)) {
598
+ const targetItems = fs2.readdirSync(targetPath);
599
+ for (const item of targetItems) {
600
+ if (COPY_IGNORED.includes(item)) continue;
601
+ fs2.removeSync(path2.join(targetPath, item));
602
+ }
603
+ }
604
+ const shadowItems = fs2.readdirSync(shadowDir);
605
+ for (const item of shadowItems) {
606
+ if (COPY_IGNORED.includes(item)) continue;
607
+ const itemSrc = path2.join(shadowDir, item);
608
+ const itemDest = path2.join(targetPath, item);
609
+ fs2.copySync(itemSrc, itemDest, { overwrite: true });
610
+ }
611
+ }
612
+ };
613
+
614
+ // ../infrastructure/src/server/api.ts
615
+ async function createApiServer(store, migrationRunner) {
616
+ const express = (await import("express")).default;
617
+ const cors = (await import("cors")).default;
618
+ const app = express();
619
+ app.use(cors());
620
+ app.use(express.json());
621
+ app.get("/api/plans", async (_req, res) => {
622
+ try {
623
+ const plans = await store.getAllPlans();
624
+ res.json(plans);
625
+ } catch (error) {
626
+ if (error instanceof Error) {
627
+ res.status(500).json({ error: error.message });
628
+ } else {
629
+ res.status(500).json({ error: String(error) });
630
+ }
631
+ }
632
+ });
633
+ app.get("/api/events", async (_req, res) => {
634
+ try {
635
+ const events = await store.getEvents();
636
+ res.json(events);
637
+ } catch (error) {
638
+ if (error instanceof Error) {
639
+ res.status(500).json({ error: error.message });
640
+ } else {
641
+ res.status(500).json({ error: String(error) });
642
+ }
643
+ }
644
+ });
645
+ app.get("/api/browse", async (req, res) => {
646
+ try {
647
+ const { readdirSync, statSync: statSync2 } = await import("fs");
648
+ const { resolve: resolve5, join: join7 } = await import("path");
649
+ const targetPath = req.query.path ? resolve5(String(req.query.path)) : resolve5(".");
650
+ const entries = readdirSync(targetPath).filter(
651
+ (name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist" && name !== "build"
652
+ ).map((name) => {
653
+ const full = join7(targetPath, name);
654
+ try {
655
+ const stat = statSync2(full);
656
+ return {
657
+ name,
658
+ path: full,
659
+ isDirectory: stat.isDirectory()
660
+ };
661
+ } catch {
662
+ return null;
663
+ }
664
+ }).filter(Boolean);
665
+ res.json({
666
+ current: targetPath,
667
+ parent: resolve5(targetPath, ".."),
668
+ entries
669
+ });
670
+ } catch (error) {
671
+ if (error instanceof Error) {
672
+ res.status(500).json({ error: error.message });
673
+ } else {
674
+ res.status(500).json({ error: String(error) });
675
+ }
676
+ }
677
+ });
678
+ app.get("/api/detect", async (req, res) => {
679
+ try {
680
+ const { detectTechnologies } = await import("./TechDetector-5YP44RVR.js");
681
+ const targetPath = req.query.path ? String(req.query.path) : ".";
682
+ const detected = detectTechnologies(targetPath);
683
+ res.json({
684
+ detected,
685
+ primary: detected.length > 0 ? detected[0].framework : null
686
+ });
687
+ } catch (error) {
688
+ if (error instanceof Error) {
689
+ res.status(500).json({ error: error.message });
690
+ } else {
691
+ res.status(500).json({ error: String(error) });
692
+ }
693
+ }
694
+ });
695
+ if (migrationRunner) {
696
+ app.post("/api/migrations/start", async (req, res) => {
697
+ try {
698
+ const { targetPath, from, to } = req.body;
699
+ if (!targetPath || !from || !to) {
700
+ res.status(400).json({
701
+ error: "Missing required fields: targetPath, from, to"
702
+ });
703
+ return;
704
+ }
705
+ const catalogEntry = findMigrationCatalogEntry(from, to);
706
+ if (!catalogEntry) {
707
+ res.status(400).json({
708
+ error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
709
+ });
710
+ return;
711
+ }
712
+ const result = await migrationRunner.startMigration({
713
+ targetPath,
714
+ from,
715
+ to
716
+ });
717
+ res.json({ ok: true, ...result });
718
+ } catch (error) {
719
+ if (error instanceof Error) {
720
+ res.status(500).json({ error: error.message });
721
+ } else {
722
+ res.status(500).json({ error: String(error) });
723
+ }
724
+ }
725
+ });
726
+ app.post("/api/migrations/apply", async (req, res) => {
727
+ try {
728
+ const { runId, targetPath } = req.body;
729
+ if (!runId || !targetPath) {
730
+ res.status(400).json({
731
+ error: "Missing required fields: runId, targetPath"
732
+ });
733
+ return;
734
+ }
735
+ const integrator = new MigrationIntegrator(migrationRunner.workspace);
736
+ const result = await integrator.applyMigration(runId, targetPath);
737
+ const plans = await store.getAllPlans();
738
+ const plan = plans.find((item) => item.runId === runId);
739
+ if (plan) {
740
+ plan.appliedAt = /* @__PURE__ */ new Date();
741
+ plan.appliedBranch = result.branch;
742
+ await store.savePlan(plan);
743
+ }
744
+ await store.logEvent("migration.applied", { runId, branch: result.branch, targetPath });
745
+ res.json({ ok: true, ...result });
746
+ } catch (error) {
747
+ if (error instanceof Error) {
748
+ res.status(500).json({ error: error.message });
749
+ } else {
750
+ res.status(500).json({ error: String(error) });
751
+ }
752
+ }
753
+ });
754
+ app.post("/api/migrations/rollback", async (req, res) => {
755
+ try {
756
+ const { runId } = req.body;
757
+ if (!runId) {
758
+ res.status(400).json({
759
+ error: "Missing required field: runId"
760
+ });
761
+ return;
762
+ }
763
+ await migrationRunner.rollbackMigration(runId);
764
+ res.json({ ok: true, message: `Rolled back run: ${runId}` });
765
+ } catch (error) {
766
+ if (error instanceof Error) {
767
+ res.status(500).json({ error: error.message });
768
+ } else {
769
+ res.status(500).json({ error: String(error) });
770
+ }
771
+ }
772
+ });
773
+ app.post("/api/migrations/reset", async (_req, res) => {
774
+ try {
775
+ await store.reset();
776
+ res.json({ ok: true, message: "Migration state reset successfully" });
777
+ } catch (error) {
778
+ if (error instanceof Error) {
779
+ res.status(500).json({ error: error.message });
780
+ } else {
781
+ res.status(500).json({ error: String(error) });
782
+ }
783
+ }
784
+ });
785
+ }
786
+ const path5 = await import("path");
787
+ const fs4 = await import("fs");
788
+ let publicDir = path5.join(import.meta.dirname, "public");
789
+ if (!fs4.existsSync(publicDir)) {
790
+ publicDir = path5.join(import.meta.dirname, "../public");
791
+ }
792
+ if (fs4.existsSync(publicDir)) {
793
+ app.use(express.static(publicDir));
794
+ app.get(/^(.*)$/, (_req, res) => {
795
+ res.sendFile(path5.join(publicDir, "index.html"));
796
+ });
797
+ }
798
+ return app;
799
+ }
800
+
801
+ // ../infrastructure/src/tools/LinterTools.ts
802
+ import { Project as Project2 } from "ts-morph";
803
+ import * as path3 from "path";
804
+ var createCheckProjectDiagnosticsTool = (shadowBase) => ({
805
+ type: "function",
806
+ name: "check_project_diagnostics",
807
+ description: "Checks the project for TypeScript errors like broken imports/exports. Call this to see if the project has cross-file integration issues. Returns a list of errors.",
808
+ parameters: {
809
+ type: "object",
810
+ properties: {}
811
+ // No params needed, it checks the whole shadow workspace
812
+ },
813
+ strict: true,
814
+ invoke: async () => {
815
+ try {
816
+ const tsConfigPath = path3.join(shadowBase, "tsconfig.json");
817
+ let project2;
818
+ try {
819
+ project2 = new Project2({
820
+ tsConfigFilePath: tsConfigPath,
821
+ skipAddingFilesFromTsConfig: false
822
+ });
823
+ } catch (err) {
824
+ project2 = new Project2();
825
+ }
826
+ project2.addSourceFilesAtPaths(path3.join(shadowBase, "src/**/*.ts"));
827
+ project2.addSourceFilesAtPaths(path3.join(shadowBase, "src/**/*.tsx"));
828
+ const diagnostics = project2.getPreEmitDiagnostics();
829
+ const relevantErrors = diagnostics.filter((d) => {
830
+ const code = d.getCode();
831
+ const message = d.getMessageText().toString();
832
+ if (code === 17004) return false;
833
+ if (code === 2307 || code === 2792) {
834
+ if (message.includes("'.") || message.includes("'/") || message.includes("'@/")) {
835
+ return true;
836
+ }
837
+ return false;
838
+ }
839
+ if ([2304, 2305, 2306, 2614, 2552, 2322, 2786].includes(code)) {
840
+ return true;
841
+ }
842
+ return false;
843
+ });
844
+ if (relevantErrors.length === 0) {
845
+ return "No local import/export errors found! The project integration looks solid.";
846
+ }
847
+ const formattedErrors = relevantErrors.map((d) => {
848
+ const file = d.getSourceFile();
849
+ const start = d.getStart();
850
+ const line = file && start !== void 0 ? file.getLineAndColumnAtPos(start).line : "unknown";
851
+ const filePath = file ? path3.relative(shadowBase, file.getFilePath()) : "unknown path";
852
+ return `[TS${d.getCode()}] ${filePath}:${line} - ${d.getMessageText().toString()}`;
853
+ }).join("\n");
854
+ return `Found ${relevantErrors.length} integration errors:
855
+ ${formattedErrors}`;
856
+ } catch (e) {
857
+ return `Failed to run diagnostics: ${e.message}. Note: if tsconfig.json is missing, you may need to check files manually.`;
858
+ }
859
+ }
860
+ });
861
+
862
+ // ../infrastructure/src/tools/BuildTools.ts
863
+ import { spawn } from "child_process";
864
+ import * as fs3 from "fs";
865
+ import * as path4 from "path";
866
+ async function runInShadowWorkspace(shadowBase, command, onChunk) {
867
+ return new Promise((resolve5) => {
868
+ const child = spawn(command, {
869
+ cwd: shadowBase,
870
+ shell: true,
871
+ env: {
872
+ ...process.env,
873
+ CI: "true",
874
+ NPM_CONFIG_WORKSPACES: "false",
875
+ NPM_CONFIG_FUND: "false",
876
+ NPM_CONFIG_AUDIT: "false",
877
+ npm_config_progress: "false"
878
+ }
879
+ });
880
+ let output = "";
881
+ const append = (buf) => {
882
+ const text = buf.toString("utf-8");
883
+ output += text;
884
+ onChunk?.(text);
885
+ };
886
+ child.stdout?.on("data", append);
887
+ child.stderr?.on("data", append);
888
+ const timeout = setTimeout(() => {
889
+ child.kill("SIGTERM");
890
+ resolve5({
891
+ ok: false,
892
+ output: `${output}
893
+ Command timed out after 5 minutes: ${command}`
894
+ });
895
+ }, 3e5);
896
+ child.on("error", (error) => {
897
+ clearTimeout(timeout);
898
+ resolve5({ ok: false, output: `${output}
899
+ ${error.message}` });
900
+ });
901
+ child.on("close", (code) => {
902
+ clearTimeout(timeout);
903
+ resolve5({ ok: code === 0, output: output.trim() });
904
+ });
905
+ });
906
+ }
907
+ function parseImplicatedFiles(output, shadowPath) {
908
+ const found = /* @__PURE__ */ new Set();
909
+ const root = path4.resolve(shadowPath);
910
+ const ext = String.raw`tsx|ts|jsx|js|mjs|cjs|css`;
911
+ const patterns = [
912
+ new RegExp(String.raw`(?:^|[\s'"(\`])((?:\.{1,2}[\\/])?[^\s:'"()<>]+\.(?:${ext}))`, "g"),
913
+ new RegExp(String.raw`\b([A-Za-z]:[\\/][^\s'"()<>]+\.(?:${ext}))`, "g")
914
+ ];
915
+ const consider = (raw) => {
916
+ const stripped = raw.replace(/[\\/]+$/, "").replace(/:[\d]+(?::[\d]+)?$/, "");
917
+ const abs = /^[A-Za-z]:[\\/]/.test(stripped) || path4.isAbsolute(stripped) ? stripped : path4.join(shadowPath, stripped.replace(/^\.[\\/]/, ""));
918
+ const resolved = path4.resolve(abs);
919
+ const inside = resolved === root || resolved.startsWith(root + path4.sep);
920
+ if (!inside) return;
921
+ if (fs3.existsSync(resolved) && fs3.statSync(resolved).isFile()) found.add(resolved);
922
+ };
923
+ for (const re of patterns) {
924
+ let match;
925
+ while ((match = re.exec(output)) !== null) consider(match[1]);
926
+ }
927
+ return [...found];
928
+ }
929
+ var createRunBuildTool = (shadowBase) => ({
930
+ type: "function",
931
+ name: "run_project_build",
932
+ description: `Runs a command (typically "npm install" or "npm run build") in the shadow workspace for this migration run. Never runs against the user's original tree.`,
933
+ parameters: {
934
+ type: "object",
935
+ properties: {
936
+ command: {
937
+ type: "string",
938
+ description: 'The command to run. Use "npm install" then "npm run build".'
939
+ }
940
+ },
941
+ required: ["command"]
942
+ },
943
+ strict: true,
944
+ invoke: async (args) => {
945
+ const result = await runInShadowWorkspace(shadowBase, args.command || "npm run build", (chunk) => {
946
+ process.stdout.write(chunk);
947
+ });
948
+ return result.ok ? `Build Output:
949
+ ${result.output}` : `Build Failed!
950
+ ${result.output}`;
951
+ }
952
+ });
953
+
954
+ export {
955
+ SQLiteStateStore,
956
+ createReadFileTool,
957
+ createWriteFileTool,
958
+ createRenameFileTool,
959
+ createCreateFileTool,
960
+ createDeleteFileTool,
961
+ createListDirectoryTool,
962
+ ShadowWorkspace,
963
+ MigrationIntegrator,
964
+ createApiServer,
965
+ createCheckProjectDiagnosticsTool,
966
+ runInShadowWorkspace,
967
+ parseImplicatedFiles,
968
+ createRunBuildTool
969
+ };