tina4-nodejs 3.13.120 → 3.13.121

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.
@@ -1452,19 +1452,70 @@ export function registerDevTools(server: McpServer): void {
1452
1452
 
1453
1453
  server.registerTool(
1454
1454
  "migration_create",
1455
- (args) => {
1456
- const desc = (args.description as string).replace(/\s+/g, "_").toLowerCase();
1457
- const migrationsDir = path.join(projectRoot, "migrations");
1458
- if (!fs.existsSync(migrationsDir)) {
1459
- fs.mkdirSync(migrationsDir, { recursive: true });
1455
+ async (args) => {
1456
+ // 3.13.121 (ADR-0063): delegate to the CLI's canonical `generate migration`
1457
+ // so the MCP surface emits the SAME `generate_v1_1` envelope (edit_hints[]
1458
+ // + next[]) and the SAME `YYYYMMDDHHMMSS_<slug>.sql` + `.down.sql` file
1459
+ // pair a user gets from `tina4 migrate:create` / `tina4 generate
1460
+ // migration`. Before this the MCP tool wrote a single sequential
1461
+ // `000001_x.sql` with no envelope — two shapes for one operation.
1462
+ //
1463
+ // Dynamic in-process import (parity with migration_status / migration_run
1464
+ // above, which reach `../../orm/src/index.js`) so there is no compile-
1465
+ // time cycle between @tina4/core and @tina4/cli.
1466
+ try {
1467
+ const rawDesc = String(args.description ?? "").trim();
1468
+ if (!rawDesc) return { ok: false, error: "description is required" };
1469
+
1470
+ // Sanitise like the CLI's `migrate:create` front door: lowercase,
1471
+ // non-alphanumeric → "_", trim underscores. `generate migration`
1472
+ // takes its name verbatim by design; the MCP tool is the human-facing
1473
+ // front door, so we sanitise here too.
1474
+ const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1475
+ if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
1476
+
1477
+ // Duplicate-slug guard (parity with PHP + Python MCPs): a `..._<slug>.sql`
1478
+ // that already lives in migrations/ means an earlier `migration_create`
1479
+ // ran for the same request. Surface a clear conflict rather than layering
1480
+ // a second timestamp for the same intent (which would confuse migrate:run).
1481
+ const migrationsDir = path.join(projectRoot, "migrations");
1482
+ if (fs.existsSync(migrationsDir)) {
1483
+ const upSuffix = `_${slug}.sql`;
1484
+ const downSuffix = `_${slug}.down.sql`;
1485
+ const existing = fs.readdirSync(migrationsDir).filter((f) =>
1486
+ (f.endsWith(upSuffix) && !f.endsWith(downSuffix)) || f.endsWith(downSuffix),
1487
+ );
1488
+ if (existing.length > 0) {
1489
+ return {
1490
+ ok: false,
1491
+ error: `A migration with slug "${slug}" already exists`,
1492
+ existing,
1493
+ };
1494
+ }
1495
+ }
1496
+
1497
+ // The generator writes files RELATIVE to process.cwd(). The MCP tool
1498
+ // may be invoked from anywhere, so pin cwd to the resolved projectRoot
1499
+ // for the duration of the call.
1500
+ const originalCwd = process.cwd();
1501
+ try {
1502
+ process.chdir(projectRoot);
1503
+ const gen = await import("../../cli/src/commands/generate.js");
1504
+ // --no-test keeps parity with the CLI's `migrate:create` front door
1505
+ // (a plain migration, no co-emitted test); an operator who wants a
1506
+ // test uses `generate migration` directly.
1507
+ const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
1508
+ const migrationPath = envelope.resolution?.migration_path;
1509
+ const created = migrationPath ? path.basename(migrationPath) : "";
1510
+ return { ok: true, created, resolution: envelope };
1511
+ } finally {
1512
+ process.chdir(originalCwd);
1513
+ }
1514
+ } catch (e) {
1515
+ return { ok: false, error: (e as Error).message };
1460
1516
  }
1461
- const existing = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
1462
- const nextNum = String(existing.length + 1).padStart(6, "0");
1463
- const filename = `${nextNum}_${desc}.sql`;
1464
- fs.writeFileSync(path.join(migrationsDir, filename), `-- Migration: ${args.description}\n`, "utf-8");
1465
- return { created: filename };
1466
1517
  },
1467
- "Create a new migration file",
1518
+ "Create a new migration file (delegates to `generate migration` — emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
1468
1519
  schemaFromParams([{ name: "description", type: "string" }]),
1469
1520
  );
1470
1521