@prisma-next/migration-tools 0.4.0-dev.6 → 0.4.0-dev.8

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.
Files changed (34) hide show
  1. package/dist/attestation-DnebS4XZ.mjs +64 -0
  2. package/dist/attestation-DnebS4XZ.mjs.map +1 -0
  3. package/dist/{constants-DARNL_LD.mjs → constants-BRi0X7B_.mjs} +1 -1
  4. package/dist/{constants-DARNL_LD.mjs.map → constants-BRi0X7B_.mjs.map} +1 -1
  5. package/dist/exports/attestation.d.mts +1 -1
  6. package/dist/exports/attestation.mjs +3 -63
  7. package/dist/exports/constants.mjs +1 -1
  8. package/dist/exports/dag.d.mts +1 -1
  9. package/dist/exports/dag.mjs +1 -1
  10. package/dist/exports/io.d.mts +14 -2
  11. package/dist/exports/io.d.mts.map +1 -1
  12. package/dist/exports/io.mjs +2 -2
  13. package/dist/exports/migration-ts.d.mts +27 -18
  14. package/dist/exports/migration-ts.d.mts.map +1 -1
  15. package/dist/exports/migration-ts.mjs +39 -85
  16. package/dist/exports/migration-ts.mjs.map +1 -1
  17. package/dist/exports/migration.d.mts +35 -6
  18. package/dist/exports/migration.d.mts.map +1 -1
  19. package/dist/exports/migration.mjs +83 -29
  20. package/dist/exports/migration.mjs.map +1 -1
  21. package/dist/exports/types.d.mts +1 -1
  22. package/dist/{io-BO18-Evu.mjs → io-Cun81AIZ.mjs} +24 -3
  23. package/dist/io-Cun81AIZ.mjs.map +1 -0
  24. package/dist/{types-DXjq7Fum.d.mts → types-D2uX4ql7.d.mts} +1 -1
  25. package/dist/{types-DXjq7Fum.d.mts.map → types-D2uX4ql7.d.mts.map} +1 -1
  26. package/package.json +4 -4
  27. package/src/exports/io.ts +1 -0
  28. package/src/exports/migration-ts.ts +3 -6
  29. package/src/io.ts +26 -1
  30. package/src/migration-base.ts +123 -35
  31. package/src/migration-ts.ts +27 -144
  32. package/src/runtime-detection.ts +18 -0
  33. package/dist/exports/attestation.mjs.map +0 -1
  34. package/dist/io-BO18-Evu.mjs.map +0 -1
@@ -1,6 +1,9 @@
1
+ import "../io-Cun81AIZ.mjs";
2
+ import { n as computeMigrationId } from "../attestation-DnebS4XZ.mjs";
1
3
  import { type } from "arktype";
2
4
  import { dirname, join } from "pathe";
3
- import { realpathSync, writeFileSync } from "node:fs";
5
+ import { ifDefined } from "@prisma-next/utils/defined";
6
+ import { readFileSync, realpathSync, writeFileSync } from "node:fs";
4
7
  import { fileURLToPath } from "node:url";
5
8
 
6
9
  //#region src/migration-base.ts
@@ -8,15 +11,30 @@ const MigrationMetaSchema = type({
8
11
  from: "string",
9
12
  to: "string",
10
13
  "kind?": "'regular' | 'baseline'",
11
- "labels?": "string[]"
14
+ "labels?": type("string").array()
12
15
  });
16
+ /**
17
+ * Base class for class-flow migrations.
18
+ *
19
+ * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
20
+ * runner can consume it directly via `targetId`, `operations`, `origin`, and
21
+ * `destination`. The manifest-shaped inputs come from `describe()`, which
22
+ * every migration must implement — `migration.json` is required for a
23
+ * migration to be valid.
24
+ */
13
25
  var Migration = class {
14
- describe() {}
26
+ get origin() {
27
+ const from = this.describe().from;
28
+ return from === "" ? null : { storageHash: from };
29
+ }
30
+ get destination() {
31
+ return { storageHash: this.describe().to };
32
+ }
15
33
  /**
16
34
  * Entrypoint guard for migration files. When called at module scope,
17
- * detects whether the file is being run directly (e.g. `tsx migration.ts`)
18
- * and if so, serializes the migration plan to `ops.json` (and optionally
19
- * `migration.json`) in the same directory. When the file is imported by
35
+ * detects whether the file is being run directly (e.g. `node migration.ts`)
36
+ * and if so, serializes the migration plan to `ops.json` and
37
+ * `migration.json` in the same directory. When the file is imported by
20
38
  * another module, this is a no-op.
21
39
  *
22
40
  * Usage (at module scope, after the class definition):
@@ -54,7 +72,7 @@ var Migration = class {
54
72
  };
55
73
  function printHelp() {
56
74
  process.stdout.write([
57
- "Usage: tsx <migration-file> [options]",
75
+ "Usage: node <migration-file> [options]",
58
76
  "",
59
77
  "Options:",
60
78
  " --dry-run Print operations to stdout without writing files",
@@ -62,42 +80,78 @@ function printHelp() {
62
80
  ""
63
81
  ].join("\n"));
64
82
  }
65
- function buildManifest(meta) {
66
- return {
83
+ /**
84
+ * Build the attested manifest written by `Migration.run()`.
85
+ *
86
+ * When a `migration.json` already exists in the directory (the common case:
87
+ * the package was scaffolded by `migration plan`), preserve the contract
88
+ * bookends, hints, labels, and `createdAt` set there — those fields are
89
+ * owned by the CLI scaffolder, not the authored class. Only the
90
+ * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
91
+ * change as the author iterates. When no manifest exists yet (a bare
92
+ * `migration.ts` run from scratch), synthesize a minimal but
93
+ * schema-conformant manifest so the resulting package can still be read,
94
+ * verified, and applied.
95
+ *
96
+ * In both cases the `migrationId` is recomputed against the current
97
+ * manifest + ops so the on-disk artifacts are always fully attested — no
98
+ * draft (`migrationId: null`) ever leaves this function.
99
+ */
100
+ function buildAttestedManifest(migrationDir, meta, ops) {
101
+ const existing = readExistingManifest(join(migrationDir, "migration.json"));
102
+ const baseManifest = {
67
103
  migrationId: null,
68
104
  from: meta.from,
69
105
  to: meta.to,
70
106
  kind: meta.kind ?? "regular",
71
- labels: meta.labels ?? [],
72
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
107
+ labels: meta.labels ?? existing?.labels ?? [],
108
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
109
+ fromContract: existing?.fromContract ?? null,
110
+ toContract: existing?.toContract ?? { storage: { storageHash: meta.to } },
111
+ hints: existing?.hints ?? {
112
+ used: [],
113
+ applied: [],
114
+ plannerVersion: "2.0.0",
115
+ planningStrategy: "class-based"
116
+ },
117
+ ...ifDefined("authorship", existing?.authorship)
118
+ };
119
+ const migrationId = computeMigrationId(baseManifest, ops);
120
+ return {
121
+ ...baseManifest,
122
+ migrationId
73
123
  };
74
124
  }
125
+ function readExistingManifest(manifestPath) {
126
+ let raw;
127
+ try {
128
+ raw = readFileSync(manifestPath, "utf-8");
129
+ } catch {
130
+ return null;
131
+ }
132
+ try {
133
+ return JSON.parse(raw);
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
75
138
  function serializeMigration(MigrationClass, migrationDir, dryRun) {
76
139
  const instance = new MigrationClass();
77
- const ops = instance.plan();
78
- if (!Array.isArray(ops)) throw new Error("plan() must return an array of operations");
140
+ const ops = instance.operations;
141
+ if (!Array.isArray(ops)) throw new Error("operations must be an array");
79
142
  const serializedOps = JSON.stringify(ops, null, 2);
80
- let manifest;
81
- if (typeof instance.describe === "function") {
82
- const rawMeta = instance.describe();
83
- if (rawMeta !== void 0) {
84
- const parsed = MigrationMetaSchema(rawMeta);
85
- if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
86
- manifest = buildManifest(parsed);
87
- }
88
- }
143
+ const parsed = MigrationMetaSchema(instance.describe());
144
+ if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
145
+ const manifest = buildAttestedManifest(migrationDir, parsed, ops);
89
146
  if (dryRun) {
90
- if (manifest) {
91
- process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
92
- process.stdout.write("--- ops.json ---\n");
93
- }
147
+ process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
148
+ process.stdout.write("--- ops.json ---\n");
94
149
  process.stdout.write(`${serializedOps}\n`);
95
150
  return;
96
151
  }
97
152
  writeFileSync(join(migrationDir, "ops.json"), serializedOps);
98
- if (manifest) writeFileSync(join(migrationDir, "migration.json"), JSON.stringify(manifest, null, 2));
99
- const files = manifest ? "ops.json + migration.json" : "ops.json";
100
- process.stdout.write(`Wrote ${files} to ${migrationDir}\n`);
153
+ writeFileSync(join(migrationDir, "migration.json"), JSON.stringify(manifest, null, 2));
154
+ process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
101
155
  }
102
156
 
103
157
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":["isEntrypoint: boolean","manifest: Record<string, unknown> | undefined","rawMeta: unknown"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { type } from 'arktype';\nimport { dirname, join } from 'pathe';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': 'string[]',\n});\n\nexport abstract class Migration<TOperation = unknown> {\n abstract plan(): TOperation[];\n\n describe(): MigrationMeta | undefined {\n return undefined;\n }\n\n /**\n * Entrypoint guard for migration files. When called at module scope,\n * detects whether the file is being run directly (e.g. `tsx migration.ts`)\n * and if so, serializes the migration plan to `ops.json` (and optionally\n * `migration.json`) in the same directory. When the file is imported by\n * another module, this is a no-op.\n *\n * Usage (at module scope, after the class definition):\n *\n * class MyMigration extends Migration { ... }\n * export default MyMigration;\n * Migration.run(import.meta.url, MyMigration);\n */\n static run(importMetaUrl: string, MigrationClass: new () => Migration): void {\n if (!importMetaUrl) return;\n\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return;\n\n let isEntrypoint: boolean;\n try {\n isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return;\n }\n if (!isEntrypoint) return;\n\n const args = process.argv.slice(2);\n\n if (args.includes('--help')) {\n printHelp();\n return;\n }\n\n const dryRun = args.includes('--dry-run');\n const migrationDir = dirname(metaFilename);\n\n try {\n serializeMigration(MigrationClass, migrationDir, dryRun);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n }\n }\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: tsx <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\nfunction buildManifest(meta: MigrationMeta): Record<string, unknown> {\n return {\n migrationId: null,\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? [],\n createdAt: new Date().toISOString(),\n };\n}\n\nfunction serializeMigration(\n MigrationClass: new () => Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const instance = new MigrationClass();\n\n const ops = instance.plan();\n\n if (!Array.isArray(ops)) {\n throw new Error('plan() must return an array of operations');\n }\n\n const serializedOps = JSON.stringify(ops, null, 2);\n\n let manifest: Record<string, unknown> | undefined;\n if (typeof instance.describe === 'function') {\n const rawMeta: unknown = instance.describe();\n if (rawMeta !== undefined) {\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n manifest = buildManifest(parsed);\n }\n }\n\n if (dryRun) {\n if (manifest) {\n process.stdout.write(`--- migration.json ---\\n${JSON.stringify(manifest, null, 2)}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n }\n process.stdout.write(`${serializedOps}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), serializedOps);\n if (manifest) {\n writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));\n }\n\n const files = manifest ? 'ops.json + migration.json' : 'ops.json';\n process.stdout.write(`Wrote ${files} to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;AAYA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW;CACZ,CAAC;AAEF,IAAsB,YAAtB,MAAsD;CAGpD,WAAsC;;;;;;;;;;;;;;CAiBtC,OAAO,IAAI,eAAuB,gBAA2C;AAC3E,MAAI,CAAC,cAAe;EAEpB,MAAM,eAAe,cAAc,cAAc;EACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,MAAI,CAAC,MAAO;EAEZ,IAAIA;AACJ,MAAI;AACF,kBAAe,aAAa,aAAa,KAAK,aAAa,MAAM;UAC3D;AACN;;AAEF,MAAI,CAAC,aAAc;EAEnB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,MAAI,KAAK,SAAS,SAAS,EAAE;AAC3B,cAAW;AACX;;EAGF,MAAM,SAAS,KAAK,SAAS,YAAY;EACzC,MAAM,eAAe,QAAQ,aAAa;AAE1C,MAAI;AACF,sBAAmB,gBAAgB,cAAc,OAAO;WACjD,KAAK;AACZ,WAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAC7E,WAAQ,WAAW;;;;AAKzB,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;AAGH,SAAS,cAAc,MAA8C;AACnE,QAAO;EACL,aAAa;EACb,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,EAAE;EACzB,4BAAW,IAAI,MAAM,EAAC,aAAa;EACpC;;AAGH,SAAS,mBACP,gBACA,cACA,QACM;CACN,MAAM,WAAW,IAAI,gBAAgB;CAErC,MAAM,MAAM,SAAS,MAAM;AAE3B,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,4CAA4C;CAG9D,MAAM,gBAAgB,KAAK,UAAU,KAAK,MAAM,EAAE;CAElD,IAAIC;AACJ,KAAI,OAAO,SAAS,aAAa,YAAY;EAC3C,MAAMC,UAAmB,SAAS,UAAU;AAC5C,MAAI,YAAY,QAAW;GACzB,MAAM,SAAS,oBAAoB,QAAQ;AAC3C,OAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;AAE5E,cAAW,cAAc,OAAO;;;AAIpC,KAAI,QAAQ;AACV,MAAI,UAAU;AACZ,WAAQ,OAAO,MAAM,2BAA2B,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,IAAI;AACtF,WAAQ,OAAO,MAAM,qBAAqB;;AAE5C,UAAQ,OAAO,MAAM,GAAG,cAAc,IAAI;AAC1C;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,cAAc;AAC5D,KAAI,SACF,eAAc,KAAK,cAAc,iBAAiB,EAAE,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;CAGxF,MAAM,QAAQ,WAAW,8BAA8B;AACvD,SAAQ,OAAO,MAAM,SAAS,MAAM,MAAM,aAAa,IAAI"}
1
+ {"version":3,"file":"migration.mjs","names":["isEntrypoint: boolean","baseManifest: MigrationManifest","raw: string"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { readFileSync, realpathSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { dirname, join } from 'pathe';\nimport { computeMigrationId } from './attestation';\nimport type { MigrationManifest, MigrationOps } from './types';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for class-flow migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The manifest-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation>\n implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n\n /**\n * Entrypoint guard for migration files. When called at module scope,\n * detects whether the file is being run directly (e.g. `node migration.ts`)\n * and if so, serializes the migration plan to `ops.json` and\n * `migration.json` in the same directory. When the file is imported by\n * another module, this is a no-op.\n *\n * Usage (at module scope, after the class definition):\n *\n * class MyMigration extends Migration { ... }\n * export default MyMigration;\n * Migration.run(import.meta.url, MyMigration);\n */\n static run(importMetaUrl: string, MigrationClass: new () => Migration): void {\n if (!importMetaUrl) return;\n\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return;\n\n let isEntrypoint: boolean;\n try {\n isEntrypoint = realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return;\n }\n if (!isEntrypoint) return;\n\n const args = process.argv.slice(2);\n\n if (args.includes('--help')) {\n printHelp();\n return;\n }\n\n const dryRun = args.includes('--dry-run');\n const migrationDir = dirname(metaFilename);\n\n try {\n serializeMigration(MigrationClass, migrationDir, dryRun);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 1;\n }\n }\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * Build the attested manifest written by `Migration.run()`.\n *\n * When a `migration.json` already exists in the directory (the common case:\n * the package was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no manifest exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant manifest so the resulting package can still be read,\n * verified, and applied.\n *\n * In both cases the `migrationId` is recomputed against the current\n * manifest + ops so the on-disk artifacts are always fully attested — no\n * draft (`migrationId: null`) ever leaves this function.\n */\nfunction buildAttestedManifest(\n migrationDir: string,\n meta: MigrationMeta,\n ops: MigrationOps,\n): MigrationManifest {\n const existing = readExistingManifest(join(migrationDir, 'migration.json'));\n\n const baseManifest: MigrationManifest = {\n migrationId: null,\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded manifest exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationId`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: existing?.hints ?? {\n used: [],\n applied: [],\n plannerVersion: '2.0.0',\n planningStrategy: 'class-based',\n },\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationId = computeMigrationId(baseManifest, ops);\n return { ...baseManifest, migrationId };\n}\n\nfunction readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {\n let raw: string;\n try {\n raw = readFileSync(manifestPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationManifest>;\n } catch {\n return null;\n }\n}\n\nfunction serializeMigration(\n MigrationClass: new () => Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const instance = new MigrationClass();\n\n const ops = instance.operations;\n\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const serializedOps = JSON.stringify(ops, null, 2);\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const manifest = buildAttestedManifest(migrationDir, parsed, ops);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${JSON.stringify(manifest, null, 2)}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${serializedOps}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), serializedOps);\n writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as DraftMigrationManifest, c as MigrationHints, d as isAttested, f as isDraft, i as DraftMigrationBundle, l as MigrationManifest, n as AttestedMigrationManifest, o as MigrationChainEntry, r as BaseMigrationBundle, s as MigrationGraph, t as AttestedMigrationBundle, u as MigrationOps } from "../types-DXjq7Fum.mjs";
1
+ import { a as DraftMigrationManifest, c as MigrationHints, d as isAttested, f as isDraft, i as DraftMigrationBundle, l as MigrationManifest, n as AttestedMigrationManifest, o as MigrationChainEntry, r as BaseMigrationBundle, s as MigrationGraph, t as AttestedMigrationBundle, u as MigrationOps } from "../types-D2uX4ql7.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { a as errorInvalidJson, d as errorMissingFile, o as errorInvalidManifest, r as errorDirectoryExists, u as errorInvalidSlug } from "./errors-C_XuSbX7.mjs";
2
- import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import { type } from "arktype";
4
4
  import { basename, dirname, join } from "pathe";
5
5
 
@@ -50,6 +50,27 @@ async function writeMigrationPackage(dir, manifest, ops) {
50
50
  await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: "wx" });
51
51
  await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
52
52
  }
53
+ /**
54
+ * Copy the destination contract artifacts (`contract.json` and the
55
+ * colocated `contract.d.ts`) into the migration package directory so
56
+ * authors of the scaffolded `migration.ts` can import the typed
57
+ * contract relative to the migration directory
58
+ * (`import type { Contract } from './contract'`).
59
+ *
60
+ * A missing `.d.ts` is tolerated (only the `.json` is required) so the
61
+ * helper stays usable in tests that hand-roll a bare `contract.json`.
62
+ * A missing `contract.json` — or any other I/O failure — throws.
63
+ */
64
+ async function copyContractToMigrationDir(packageDir, contractJsonPath) {
65
+ await copyFile(contractJsonPath, join(packageDir, "contract.json"));
66
+ const dtsPath = `${contractJsonPath.slice(0, -5)}.d.ts`;
67
+ try {
68
+ await copyFile(dtsPath, join(packageDir, "contract.d.ts"));
69
+ } catch (error) {
70
+ if (hasErrnoCode(error, "ENOENT")) return;
71
+ throw error;
72
+ }
73
+ }
53
74
  async function writeMigrationManifest(dir, manifest) {
54
75
  await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
55
76
  }
@@ -132,5 +153,5 @@ function formatMigrationDirName(timestamp, slug) {
132
153
  }
133
154
 
134
155
  //#endregion
135
- export { writeMigrationOps as a, writeMigrationManifest as i, readMigrationPackage as n, writeMigrationPackage as o, readMigrationsDir as r, formatMigrationDirName as t };
136
- //# sourceMappingURL=io-BO18-Evu.mjs.map
156
+ export { writeMigrationManifest as a, readMigrationsDir as i, formatMigrationDirName as n, writeMigrationOps as o, readMigrationPackage as r, writeMigrationPackage as s, copyContractToMigrationDir as t };
157
+ //# sourceMappingURL=io-Cun81AIZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io-Cun81AIZ.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: BaseMigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { BaseMigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nconst MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n planningStrategy: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string | null',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n manifest: MigrationManifest,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: 'wx' });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy the destination contract artifacts (`contract.json` and the\n * colocated `contract.d.ts`) into the migration package directory so\n * authors of the scaffolded `migration.ts` can import the typed\n * contract relative to the migration directory\n * (`import type { Contract } from './contract'`).\n *\n * A missing `.d.ts` is tolerated (only the `.json` is required) so the\n * helper stays usable in tests that hand-roll a bare `contract.json`.\n * A missing `contract.json` — or any other I/O failure — throws.\n */\nexport async function copyContractToMigrationDir(\n packageDir: string,\n contractJsonPath: string,\n): Promise<void> {\n await copyFile(contractJsonPath, join(packageDir, 'contract.json'));\n const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;\n try {\n await copyFile(dtsPath, join(packageDir, 'contract.d.ts'));\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nexport async function writeMigrationManifest(\n dir: string,\n manifest: MigrationManifest,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<BaseMigrationBundle> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, dir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, dir);\n }\n throw error;\n }\n\n let manifest: MigrationManifest;\n try {\n manifest = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateManifest(manifest, manifestPath);\n validateOps(ops, opsPath);\n\n return {\n dirName: basename(dir),\n dirPath: dir,\n manifest,\n ops,\n };\n}\n\nfunction validateManifest(\n manifest: unknown,\n filePath: string,\n): asserts manifest is MigrationManifest {\n const result = MigrationManifestSchema(manifest);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly BaseMigrationBundle[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: BaseMigrationBundle[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
@@ -97,4 +97,4 @@ declare function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigr
97
97
  declare function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle;
98
98
  //#endregion
99
99
  export { DraftMigrationManifest as a, MigrationHints as c, isAttested as d, isDraft as f, DraftMigrationBundle as i, MigrationManifest as l, AttestedMigrationManifest as n, MigrationChainEntry as o, BaseMigrationBundle as r, MigrationGraph as s, AttestedMigrationBundle as t, MigrationOps as u };
100
- //# sourceMappingURL=types-DXjq7Fum.d.mts.map
100
+ //# sourceMappingURL=types-D2uX4ql7.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-DXjq7Fum.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAUrB,SAAA,cAAA,EAAqB,MAAA;EAIN,SAAA,gBAAA,EAAA,MAAA;;;;AAczB;AAQA,UA1BU,qBAAA,CA0BiC;EAS/B,SAAA,IAAA,EAAA,MAAiB;EAEjB,SAAA,EAAA,EAAA,MAAY;EAMP,SAAA,IAAA,EAAA,SAAmB,GAAA,UAGf;EAQJ,SAAA,YAAA,EAlDQ,QAkDgB,GAAA,IACpB;EAGJ,SAAA,UAAA,EArDM,QAqDe;EAQrB,SAAA,KAAA,EA5DC,cA4DkB;EASnB,SAAA,MAAA,EAAc,SAAA,MAAA,EAAA;EACb,SAAA,UAAA,CAAA,EAAA;IACoC,SAAA,MAAA,CAAA,EAAA,MAAA;IAA7B,SAAA,KAAA,CAAA,EAAA,MAAA;EAC6B,CAAA;EAA7B,SAAA,SAAA,CAAA,EAAA;IACqB,SAAA,KAAA,EAAA,MAAA;IAApB,SAAA,KAAA,EAAA,MAAA;EAAW,CAAA,GAAA,IAAA;EAOrB,SAAA,SAAU,EAAA,MAAS;AAInC;;;;;;UAxEiB,sBAAA,SAA+B;;;;;;;UAQ/B,yBAAA,SAAkC;;;;;;;;KASvC,iBAAA,GAAoB,yBAAyB;KAE7C,YAAA,YAAwB;;;;;UAMnB,mBAAA;;;qBAGI;gBACL;;;;;;UAOC,uBAAA,SAAgC;qBAC5B;;UAGJ,oBAAA,SAA6B;qBACzB;;;;;;UAOJ,mBAAA;;;;;;;;UASA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB;;;;;;iBAO9B,UAAA,SAAmB,gCAAgC;iBAInD,OAAA,SAAgB,gCAAgC"}
1
+ {"version":3,"file":"types-D2uX4ql7.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;UAGiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAUrB,SAAA,cAAA,EAAqB,MAAA;EAIN,SAAA,gBAAA,EAAA,MAAA;;;;AAczB;AAQA,UA1BU,qBAAA,CA0BiC;EAS/B,SAAA,IAAA,EAAA,MAAiB;EAEjB,SAAA,EAAA,EAAA,MAAY;EAMP,SAAA,IAAA,EAAA,SAAmB,GAAA,UAGf;EAQJ,SAAA,YAAA,EAlDQ,QAkDgB,GAAA,IACpB;EAGJ,SAAA,UAAA,EArDM,QAqDe;EAQrB,SAAA,KAAA,EA5DC,cA4DkB;EASnB,SAAA,MAAA,EAAc,SAAA,MAAA,EAAA;EACb,SAAA,UAAA,CAAA,EAAA;IACoC,SAAA,MAAA,CAAA,EAAA,MAAA;IAA7B,SAAA,KAAA,CAAA,EAAA,MAAA;EAC6B,CAAA;EAA7B,SAAA,SAAA,CAAA,EAAA;IACqB,SAAA,KAAA,EAAA,MAAA;IAApB,SAAA,KAAA,EAAA,MAAA;EAAW,CAAA,GAAA,IAAA;EAOrB,SAAA,SAAU,EAAA,MAAS;AAInC;;;;;;UAxEiB,sBAAA,SAA+B;;;;;;;UAQ/B,yBAAA,SAAkC;;;;;;;;KASvC,iBAAA,GAAoB,yBAAyB;KAE7C,YAAA,YAAwB;;;;;UAMnB,mBAAA;;;qBAGI;gBACL;;;;;;UAOC,uBAAA,SAAgC;qBAC5B;;UAGJ,oBAAA,SAA6B;qBACzB;;;;;;UAOJ,mBAAA;;;;;;;;UASA,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;0BAC5B,oBAAoB;;;;;;iBAO9B,UAAA,SAAmB,gCAAgC;iBAInD,OAAA,SAAgB,gCAAgC"}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.4.0-dev.6",
3
+ "version": "0.4.0-dev.8",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "On-disk migration persistence, attestation, and chain reconstruction for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.1.29",
9
9
  "pathe": "^2.0.3",
10
- "@prisma-next/contract": "0.4.0-dev.6",
11
- "@prisma-next/framework-components": "0.4.0-dev.6",
12
- "@prisma-next/utils": "0.4.0-dev.6"
10
+ "@prisma-next/framework-components": "0.4.0-dev.8",
11
+ "@prisma-next/contract": "0.4.0-dev.8",
12
+ "@prisma-next/utils": "0.4.0-dev.8"
13
13
  },
14
14
  "devDependencies": {
15
15
  "tsdown": "0.18.4",
package/src/exports/io.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {
2
+ copyContractToMigrationDir,
2
3
  formatMigrationDirName,
3
4
  readMigrationPackage,
4
5
  readMigrationsDir,
@@ -1,6 +1,3 @@
1
- export type { ScaffoldOptions } from '../migration-ts';
2
- export {
3
- evaluateMigrationTs,
4
- hasMigrationTs,
5
- scaffoldMigrationTs,
6
- } from '../migration-ts';
1
+ export { evaluateMigrationTs, hasMigrationTs, writeMigrationTs } from '../migration-ts';
2
+ export type { ScaffoldRuntime } from '../runtime-detection';
3
+ export { detectScaffoldRuntime, shebangLineFor } from '../runtime-detection';
package/src/io.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
1
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
2
2
  import { type } from 'arktype';
3
3
  import { basename, dirname, join } from 'pathe';
4
4
  import {
@@ -74,6 +74,31 @@ export async function writeMigrationPackage(
74
74
  await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });
75
75
  }
76
76
 
77
+ /**
78
+ * Copy the destination contract artifacts (`contract.json` and the
79
+ * colocated `contract.d.ts`) into the migration package directory so
80
+ * authors of the scaffolded `migration.ts` can import the typed
81
+ * contract relative to the migration directory
82
+ * (`import type { Contract } from './contract'`).
83
+ *
84
+ * A missing `.d.ts` is tolerated (only the `.json` is required) so the
85
+ * helper stays usable in tests that hand-roll a bare `contract.json`.
86
+ * A missing `contract.json` — or any other I/O failure — throws.
87
+ */
88
+ export async function copyContractToMigrationDir(
89
+ packageDir: string,
90
+ contractJsonPath: string,
91
+ ): Promise<void> {
92
+ await copyFile(contractJsonPath, join(packageDir, 'contract.json'));
93
+ const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;
94
+ try {
95
+ await copyFile(dtsPath, join(packageDir, 'contract.d.ts'));
96
+ } catch (error) {
97
+ if (hasErrnoCode(error, 'ENOENT')) return;
98
+ throw error;
99
+ }
100
+ }
101
+
77
102
  export async function writeMigrationManifest(
78
103
  dir: string,
79
104
  manifest: MigrationManifest,
@@ -1,7 +1,15 @@
1
- import { realpathSync, writeFileSync } from 'node:fs';
1
+ import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import type { Contract } from '@prisma-next/contract/types';
4
+ import type {
5
+ MigrationPlan,
6
+ MigrationPlanOperation,
7
+ } from '@prisma-next/framework-components/control';
8
+ import { ifDefined } from '@prisma-next/utils/defined';
3
9
  import { type } from 'arktype';
4
10
  import { dirname, join } from 'pathe';
11
+ import { computeMigrationId } from './attestation';
12
+ import type { MigrationManifest, MigrationOps } from './types';
5
13
 
6
14
  export interface MigrationMeta {
7
15
  readonly from: string;
@@ -14,21 +22,56 @@ const MigrationMetaSchema = type({
14
22
  from: 'string',
15
23
  to: 'string',
16
24
  'kind?': "'regular' | 'baseline'",
17
- 'labels?': 'string[]',
25
+ 'labels?': type('string').array(),
18
26
  });
19
27
 
20
- export abstract class Migration<TOperation = unknown> {
21
- abstract plan(): TOperation[];
28
+ /**
29
+ * Base class for class-flow migrations.
30
+ *
31
+ * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
32
+ * runner can consume it directly via `targetId`, `operations`, `origin`, and
33
+ * `destination`. The manifest-shaped inputs come from `describe()`, which
34
+ * every migration must implement — `migration.json` is required for a
35
+ * migration to be valid.
36
+ */
37
+ export abstract class Migration<TOperation extends MigrationPlanOperation = MigrationPlanOperation>
38
+ implements MigrationPlan
39
+ {
40
+ abstract readonly targetId: string;
22
41
 
23
- describe(): MigrationMeta | undefined {
24
- return undefined;
42
+ /**
43
+ * Ordered list of operations this migration performs.
44
+ *
45
+ * Implemented as a getter so that subclasses can either precompute the list
46
+ * in their constructor or build it lazily per access.
47
+ */
48
+ abstract get operations(): readonly TOperation[];
49
+
50
+ /**
51
+ * Metadata inputs used to build `migration.json` and to derive the plan's
52
+ * origin/destination identities. Every migration must provide this —
53
+ * omitting it would produce an invalid on-disk migration package.
54
+ */
55
+ abstract describe(): MigrationMeta;
56
+
57
+ get origin(): { readonly storageHash: string } | null {
58
+ const from = this.describe().from;
59
+ // An empty `from` represents a migration with no prior origin (e.g.
60
+ // initial baseline, or an in-process plan that was never persisted).
61
+ // Surface that as a null origin so runners treat the plan as
62
+ // origin-less rather than matching against an empty storage hash.
63
+ return from === '' ? null : { storageHash: from };
64
+ }
65
+
66
+ get destination(): { readonly storageHash: string } {
67
+ return { storageHash: this.describe().to };
25
68
  }
26
69
 
27
70
  /**
28
71
  * Entrypoint guard for migration files. When called at module scope,
29
- * detects whether the file is being run directly (e.g. `tsx migration.ts`)
30
- * and if so, serializes the migration plan to `ops.json` (and optionally
31
- * `migration.json`) in the same directory. When the file is imported by
72
+ * detects whether the file is being run directly (e.g. `node migration.ts`)
73
+ * and if so, serializes the migration plan to `ops.json` and
74
+ * `migration.json` in the same directory. When the file is imported by
32
75
  * another module, this is a no-op.
33
76
  *
34
77
  * Usage (at module scope, after the class definition):
@@ -74,7 +117,7 @@ export abstract class Migration<TOperation = unknown> {
74
117
  function printHelp(): void {
75
118
  process.stdout.write(
76
119
  [
77
- 'Usage: tsx <migration-file> [options]',
120
+ 'Usage: node <migration-file> [options]',
78
121
  '',
79
122
  'Options:',
80
123
  ' --dry-run Print operations to stdout without writing files',
@@ -84,15 +127,69 @@ function printHelp(): void {
84
127
  );
85
128
  }
86
129
 
87
- function buildManifest(meta: MigrationMeta): Record<string, unknown> {
88
- return {
130
+ /**
131
+ * Build the attested manifest written by `Migration.run()`.
132
+ *
133
+ * When a `migration.json` already exists in the directory (the common case:
134
+ * the package was scaffolded by `migration plan`), preserve the contract
135
+ * bookends, hints, labels, and `createdAt` set there — those fields are
136
+ * owned by the CLI scaffolder, not the authored class. Only the
137
+ * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
138
+ * change as the author iterates. When no manifest exists yet (a bare
139
+ * `migration.ts` run from scratch), synthesize a minimal but
140
+ * schema-conformant manifest so the resulting package can still be read,
141
+ * verified, and applied.
142
+ *
143
+ * In both cases the `migrationId` is recomputed against the current
144
+ * manifest + ops so the on-disk artifacts are always fully attested — no
145
+ * draft (`migrationId: null`) ever leaves this function.
146
+ */
147
+ function buildAttestedManifest(
148
+ migrationDir: string,
149
+ meta: MigrationMeta,
150
+ ops: MigrationOps,
151
+ ): MigrationManifest {
152
+ const existing = readExistingManifest(join(migrationDir, 'migration.json'));
153
+
154
+ const baseManifest: MigrationManifest = {
89
155
  migrationId: null,
90
156
  from: meta.from,
91
157
  to: meta.to,
92
158
  kind: meta.kind ?? 'regular',
93
- labels: meta.labels ?? [],
94
- createdAt: new Date().toISOString(),
159
+ labels: meta.labels ?? existing?.labels ?? [],
160
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
161
+ fromContract: existing?.fromContract ?? null,
162
+ // When no scaffolded manifest exists we synthesize a minimal contract
163
+ // stub so the package is still readable end-to-end. The cast is
164
+ // intentional: only the storage bookend matters for hash computation
165
+ // (everything else is stripped by `computeMigrationId`), and a real
166
+ // contract bookend would only be available after `migration plan`.
167
+ toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
168
+ hints: existing?.hints ?? {
169
+ used: [],
170
+ applied: [],
171
+ plannerVersion: '2.0.0',
172
+ planningStrategy: 'class-based',
173
+ },
174
+ ...ifDefined('authorship', existing?.authorship),
95
175
  };
176
+
177
+ const migrationId = computeMigrationId(baseManifest, ops);
178
+ return { ...baseManifest, migrationId };
179
+ }
180
+
181
+ function readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {
182
+ let raw: string;
183
+ try {
184
+ raw = readFileSync(manifestPath, 'utf-8');
185
+ } catch {
186
+ return null;
187
+ }
188
+ try {
189
+ return JSON.parse(raw) as Partial<MigrationManifest>;
190
+ } catch {
191
+ return null;
192
+ }
96
193
  }
97
194
 
98
195
  function serializeMigration(
@@ -102,40 +199,31 @@ function serializeMigration(
102
199
  ): void {
103
200
  const instance = new MigrationClass();
104
201
 
105
- const ops = instance.plan();
202
+ const ops = instance.operations;
106
203
 
107
204
  if (!Array.isArray(ops)) {
108
- throw new Error('plan() must return an array of operations');
205
+ throw new Error('operations must be an array');
109
206
  }
110
207
 
111
208
  const serializedOps = JSON.stringify(ops, null, 2);
112
209
 
113
- let manifest: Record<string, unknown> | undefined;
114
- if (typeof instance.describe === 'function') {
115
- const rawMeta: unknown = instance.describe();
116
- if (rawMeta !== undefined) {
117
- const parsed = MigrationMetaSchema(rawMeta);
118
- if (parsed instanceof type.errors) {
119
- throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
120
- }
121
- manifest = buildManifest(parsed);
122
- }
210
+ const rawMeta: unknown = instance.describe();
211
+ const parsed = MigrationMetaSchema(rawMeta);
212
+ if (parsed instanceof type.errors) {
213
+ throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
123
214
  }
124
215
 
216
+ const manifest = buildAttestedManifest(migrationDir, parsed, ops);
217
+
125
218
  if (dryRun) {
126
- if (manifest) {
127
- process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
128
- process.stdout.write('--- ops.json ---\n');
129
- }
219
+ process.stdout.write(`--- migration.json ---\n${JSON.stringify(manifest, null, 2)}\n`);
220
+ process.stdout.write('--- ops.json ---\n');
130
221
  process.stdout.write(`${serializedOps}\n`);
131
222
  return;
132
223
  }
133
224
 
134
225
  writeFileSync(join(migrationDir, 'ops.json'), serializedOps);
135
- if (manifest) {
136
- writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));
137
- }
226
+ writeFileSync(join(migrationDir, 'migration.json'), JSON.stringify(manifest, null, 2));
138
227
 
139
- const files = manifest ? 'ops.json + migration.json' : 'ops.json';
140
- process.stdout.write(`Wrote ${files} to ${migrationDir}\n`);
228
+ process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\n`);
141
229
  }