@prisma-next/cli 0.5.0-dev.21 → 0.5.0-dev.23

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.
@@ -1 +1 @@
1
- {"version":3,"file":"migration-cli.d.mts","names":[],"sources":["../src/migration-cli.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;KAqEY,oBAAA,2BAA+C;;;;;;;;;;;;;cAiE9C,YAAA;;;;;;;;;;;;;oDAa6C,uBAAuB"}
1
+ {"version":3,"file":"migration-cli.d.mts","names":[],"sources":["../src/migration-cli.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;KAsEY,oBAAA,2BAA+C;;;;;;;;;;;;;cAiE9C,YAAA;;;;;;;;;;;;;oDAa6C,uBAAuB"}
@@ -4,6 +4,7 @@ import { dirname, join } from "pathe";
4
4
  import { createControlStack } from "@prisma-next/framework-components/control";
5
5
  import { errorMigrationTargetMismatch } from "@prisma-next/errors/migration";
6
6
  import { readFileSync, writeFileSync } from "node:fs";
7
+ import { MigrationToolsError, errorInvalidJson } from "@prisma-next/migration-tools/errors";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp } from "@prisma-next/migration-tools/migration";
9
10
 
@@ -125,7 +126,7 @@ var MigrationCLI = class {
125
126
  });
126
127
  serializeMigrationToDisk(new MigrationClass(createControlStack(config)), migrationDir, args.dryRun);
127
128
  } catch (err) {
128
- if (CliStructuredError.is(err)) process.stderr.write(`${err.message}: ${err.why}\n`);
129
+ if (CliStructuredError.is(err) || MigrationToolsError.is(err)) process.stderr.write(`${err.message}: ${err.why}\n`);
129
130
  else process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
130
131
  process.exitCode = 1;
131
132
  }
@@ -133,17 +134,24 @@ var MigrationCLI = class {
133
134
  };
134
135
  /**
135
136
  * Read a previously-scaffolded `migration.json` from disk, returning
136
- * `null` when the file is missing or unparseable. The CLI feeds this into
137
- * `buildMigrationArtifacts` so the pure builder can preserve fields owned
138
- * by `migration plan` (contract bookends, hints, labels, `createdAt`)
139
- * across re-emits.
137
+ * `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`
138
+ * when the file is present but cannot be parsed as JSON. The CLI feeds
139
+ * this into `buildMigrationArtifacts` so the pure builder can preserve
140
+ * fields owned by `migration plan` (contract bookends, hints, labels,
141
+ * `createdAt`) across re-emits.
140
142
  *
141
- * Author-time path: this loader is non-verifying by design. Hash mismatch
142
- * is the *expected* outcome of a re-author (the developer's source
143
- * changes invalidate the prior hash by construction), and verification
144
- * here would block legitimate regenerations. Apply-time consumers always
145
- * route through the verifying `readMigrationPackage` in
146
- * `@prisma-next/migration-tools/io` instead.
143
+ * Author-time path: this loader still does not verify the manifest hash
144
+ * or schema that is the apply-time loader's job. Hash mismatch is the
145
+ * *expected* outcome of a re-author (the developer's source changes
146
+ * invalidate the prior hash by construction), and verification here
147
+ * would block legitimate regenerations. Syntactic JSON-parse failure,
148
+ * however, is now surfaced rather than swallowed: a malformed
149
+ * `migration.json` indicates either a hand-edit gone wrong or partial
150
+ * write, and silently rebuilding from `describe()` would discard the
151
+ * user's on-disk content (preserved bookends, hints, labels,
152
+ * `createdAt`) without any indication something was wrong on disk.
153
+ * Apply-time consumers always route through the verifying
154
+ * `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.
147
155
  */
148
156
  function readExistingMetadata(metadataPath) {
149
157
  let raw;
@@ -154,8 +162,8 @@ function readExistingMetadata(metadataPath) {
154
162
  }
155
163
  try {
156
164
  return JSON.parse(raw);
157
- } catch {
158
- return null;
165
+ } catch (e) {
166
+ throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));
159
167
  }
160
168
  }
161
169
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"migration-cli.mjs","names":["configPath: string | undefined","raw: string"],"sources":["../src/migration-cli.ts"],"sourcesContent":["/**\n * The migration-file CLI interface: the actor invoked when the author runs\n * `node migration.ts` directly.\n *\n * Naming: this is *not* a \"migration runner\" in the apply-time sense. The\n * apply-time runner is the thing `prisma-next migration apply` uses to\n * execute migration JSON ops against a database. `MigrationCLI` is the\n * tiny CLI surface owned by an authored `migration.ts` file: parse the\n * file's argv, load the project's `prisma-next.config.ts`, assemble a\n * `ControlStack`, instantiate the migration class, and serialize.\n *\n * The user authors a migration class, then calls\n * `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope\n * after the class definition. When the file is invoked as a node\n * entrypoint (`node migration.ts`), the CLI:\n *\n * 1. Detects whether the file is the direct entrypoint (no-op when imported).\n * 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`).\n * 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`\n * the CLI commands use, walking up from the migration file's directory.\n * 4. Probe-instantiates the migration class without a stack so it can read\n * `targetId` and verify it matches `config.target.targetId`\n * (`PN-MIG-2006` on mismatch) before any stack-driven adapter\n * construction runs.\n * 5. Assembles a `ControlStack` from the loaded config descriptors and\n * constructs the migration with that stack.\n * 6. Reads any previously-scaffolded `migration.json`, then calls\n * `buildMigrationArtifacts` from `@prisma-next/migration-tools` to\n * produce in-memory `ops.json` + `migration.json` content. Persists\n * the result to disk (or prints in dry-run mode).\n *\n * File I/O lives here, in `@prisma-next/cli`: this is the only place\n * that legitimately combines config loading, stack assembly, and\n * on-disk persistence. `@prisma-next/migration-tools` owns the pure\n * conversion from a `Migration` instance to artifact strings; `Migration`\n * stays a pure abstract class.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';\nimport { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';\nimport { createControlStack } from '@prisma-next/framework-components/control';\nimport type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';\nimport {\n buildMigrationArtifacts,\n isDirectEntrypoint,\n type Migration,\n printMigrationHelp,\n} from '@prisma-next/migration-tools/migration';\nimport { dirname, join } from 'pathe';\nimport { loadConfig } from './config-loader';\n\n/**\n * Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses\n * accept an optional `ControlStack` in their constructor (each subclass\n * narrows the stack to its own family/target generics); the CLI always\n * passes one assembled from the loaded config. We use a rest-args `any[]`\n * constructor signature so that subclass constructors with narrower\n * parameter types remain assignable - constructor type compatibility in\n * TS is contravariant in the parameter, and a wider `unknown` parameter\n * on the alias side would reject any narrower subclass signature.\n *\n * The CLI only ever passes one argument (`new MigrationClass(stack)`);\n * the rest-arity is purely a type-compatibility concession for subclass\n * constructors that declare narrower parameter types, not an extension\n * point for additional construction arguments.\n */\n// biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures\nexport type MigrationConstructor = new (...args: any[]) => Migration;\n\ninterface ParsedArgs {\n readonly help: boolean;\n readonly dryRun: boolean;\n readonly configPath: string | undefined;\n}\n\n/**\n * Parse the subset of `process.argv` that `MigrationCLI.run` cares about.\n * Recognised flags: `--help`, `--dry-run`, `--config <path>` /\n * `--config=<path>`. Unknown flags are ignored to keep the surface\n * forgiving for ad-hoc tooling that wraps a migration file.\n *\n * Throws `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`) when\n * `--config` is missing its path argument or is followed by another flag\n * (e.g. `--config --dry-run`); silently consuming the next flag would\n * either drop dry-run handling or serialize against the wrong project.\n *\n * NOTE: this hand-rolled parser is a known wart, tracked separately by\n * TML-2318 (\"Migration CLI: replace handrolled arg parser with shared\n * CLI library\"). Until that lands the surface is intentionally tiny.\n */\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n let help = false;\n let dryRun = false;\n let configPath: string | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === '--help' || arg === '-h') {\n help = true;\n } else if (arg === '--dry-run') {\n dryRun = true;\n } else if (arg === '--config') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw errorMigrationCliInvalidConfigArg();\n }\n if (next.startsWith('-')) {\n throw errorMigrationCliInvalidConfigArg({ nextToken: next });\n }\n configPath = next;\n i++;\n } else if (arg.startsWith('--config=')) {\n configPath = arg.slice('--config='.length);\n }\n }\n\n return { help, dryRun, configPath };\n}\n\n/**\n * The CLI surface invoked by an authored `migration.ts` file. Exposed as\n * a class with a static `run` method (rather than a free function) to\n * give the concept a stable identity in the ubiquitous language: this is\n * the \"migration-file CLI\", distinct from the apply-time runner that\n * executes migration JSON ops.\n *\n * Currently a single static method. Future surface (e.g. a programmatic\n * `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can\n * land here without changing the import shape used by every authored\n * migration.\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.\nexport class MigrationCLI {\n /**\n * Orchestrates a class-flow `migration.ts` script run. Awaitable:\n * callers may `await MigrationCLI.run(...)` to surface async failures\n * from config loading, but the typical usage pattern (top-level call\n * after the class definition) does not require awaiting because\n * node's module evaluation keeps the promise alive until completion.\n *\n * Any throwable inside this function must surface through the internal\n * try/catch — script callers do not await, so an unhandled rejection\n * would silently exit 0. Treat the try/catch as load-bearing for the\n * no-await usage pattern.\n */\n static async run(importMetaUrl: string, MigrationClass: MigrationConstructor): Promise<void> {\n if (!importMetaUrl) return;\n if (!isDirectEntrypoint(importMetaUrl)) return;\n\n try {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n printMigrationHelp();\n return;\n }\n\n const migrationFile = fileURLToPath(importMetaUrl);\n const migrationDir = dirname(migrationFile);\n\n const config = await loadConfig(args.configPath);\n\n // Probe-instantiate without a stack so we can read `targetId` before\n // any target-specific constructor side effects (e.g.\n // `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete\n // subclasses are required to accept the no-arg form; the abstract\n // `Migration` constructor declares `stack?` and target subclasses\n // (Postgres, Mongo) propagate that optionality. This makes the\n // target-mismatch guard fail fast with `PN-MIG-2006` before any\n // stack-driven adapter construction begins, even if the wrong-target\n // adapter's `create` would otherwise succeed and silently misshapen\n // the stored adapter cast.\n const probe = new MigrationClass();\n\n if (probe.targetId !== config.target.targetId) {\n throw errorMigrationTargetMismatch({\n migrationTargetId: probe.targetId,\n configTargetId: config.target.targetId,\n });\n }\n\n const stack = createControlStack(config);\n const instance = new MigrationClass(stack);\n\n serializeMigrationToDisk(instance, migrationDir, args.dryRun);\n } catch (err) {\n if (CliStructuredError.is(err)) {\n process.stderr.write(`${err.message}: ${err.why}\\n`);\n } else {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = 1;\n }\n }\n}\n\n/**\n * Read a previously-scaffolded `migration.json` from disk, returning\n * `null` when the file is missing or unparseable. The CLI feeds this into\n * `buildMigrationArtifacts` so the pure builder can preserve fields owned\n * by `migration plan` (contract bookends, hints, labels, `createdAt`)\n * across re-emits.\n *\n * Author-time path: this loader is non-verifying by design. Hash mismatch\n * is the *expected* outcome of a re-author (the developer's source\n * changes invalidate the prior hash by construction), and verification\n * here would block legitimate regenerations. Apply-time consumers always\n * route through the verifying `readMigrationPackage` in\n * `@prisma-next/migration-tools/io` instead.\n */\nfunction readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {\n let raw: string;\n try {\n raw = readFileSync(metadataPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationMetadata>;\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a migration instance's artifacts to `migrationDir`. In\n * `dryRun` mode the artifacts are printed to stdout (with the same\n * `--- migration.json --- / --- ops.json ---` framing the legacy\n * `serializeMigration` helper used) and no files are written. Otherwise\n * `ops.json` and `migration.json` are written next to `migration.ts` and\n * a confirmation line is printed.\n *\n * File I/O lives in the CLI rather than `@prisma-next/migration-tools`\n * so the migration-tools package stays focused on the pure\n * `Migration` → in-memory artifact conversion. The CLI is the only\n * legitimate site for combining config loading, stack assembly, and\n * filesystem persistence.\n */\nfunction serializeMigrationToDisk(\n instance: Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const metadataPath = join(migrationDir, 'migration.json');\n const existing = readExistingMetadata(metadataPath);\n const { opsJson, metadataJson } = buildMigrationArtifacts(instance, existing);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${metadataJson}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${opsJson}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), opsJson);\n writeFileSync(metadataPath, metadataJson);\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,SAAS,UAAU,MAAqC;CACtD,IAAI,OAAO;CACX,IAAI,SAAS;CACb,IAAIA;AAEJ,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,YAAY,QAAQ,KAC9B,QAAO;WACE,QAAQ,YACjB,UAAS;WACA,QAAQ,YAAY;GAC7B,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,OACX,OAAM,mCAAmC;AAE3C,OAAI,KAAK,WAAW,IAAI,CACtB,OAAM,kCAAkC,EAAE,WAAW,MAAM,CAAC;AAE9D,gBAAa;AACb;aACS,IAAI,WAAW,YAAY,CACpC,cAAa,IAAI,MAAM,EAAmB;;AAI9C,QAAO;EAAE;EAAM;EAAQ;EAAY;;;;;;;;;;;;;;AAgBrC,IAAa,eAAb,MAA0B;;;;;;;;;;;;;CAaxB,aAAa,IAAI,eAAuB,gBAAqD;AAC3F,MAAI,CAAC,cAAe;AACpB,MAAI,CAAC,mBAAmB,cAAc,CAAE;AAExC,MAAI;GACF,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,CAAC;AAE7C,OAAI,KAAK,MAAM;AACb,wBAAoB;AACpB;;GAIF,MAAM,eAAe,QADC,cAAc,cAAc,CACP;GAE3C,MAAM,SAAS,MAAM,WAAW,KAAK,WAAW;GAYhD,MAAM,QAAQ,IAAI,gBAAgB;AAElC,OAAI,MAAM,aAAa,OAAO,OAAO,SACnC,OAAM,6BAA6B;IACjC,mBAAmB,MAAM;IACzB,gBAAgB,OAAO,OAAO;IAC/B,CAAC;AAMJ,4BAFiB,IAAI,eADP,mBAAmB,OAAO,CACE,EAEP,cAAc,KAAK,OAAO;WACtD,KAAK;AACZ,OAAI,mBAAmB,GAAG,IAAI,CAC5B,SAAQ,OAAO,MAAM,GAAG,IAAI,QAAQ,IAAI,IAAI,IAAI,IAAI;OAEpD,SAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAE/E,WAAQ,WAAW;;;;;;;;;;;;;;;;;;AAmBzB,SAAS,qBAAqB,cAAyD;CACrF,IAAIC;AACJ,KAAI;AACF,QAAM,aAAa,cAAc,QAAQ;SACnC;AACN,SAAO;;AAET,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;;;;;;;;;;;;;;;AAkBX,SAAS,yBACP,UACA,cACA,QACM;CACN,MAAM,eAAe,KAAK,cAAc,iBAAiB;CAEzD,MAAM,EAAE,SAAS,iBAAiB,wBAAwB,UADzC,qBAAqB,aAAa,CAC0B;AAE7E,KAAI,QAAQ;AACV,UAAQ,OAAO,MAAM,2BAA2B,aAAa,IAAI;AACjE,UAAQ,OAAO,MAAM,qBAAqB;AAC1C,UAAQ,OAAO,MAAM,GAAG,QAAQ,IAAI;AACpC;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,QAAQ;AACtD,eAAc,cAAc,aAAa;AAEzC,SAAQ,OAAO,MAAM,sCAAsC,aAAa,IAAI"}
1
+ {"version":3,"file":"migration-cli.mjs","names":["configPath: string | undefined","raw: string"],"sources":["../src/migration-cli.ts"],"sourcesContent":["/**\n * The migration-file CLI interface: the actor invoked when the author runs\n * `node migration.ts` directly.\n *\n * Naming: this is *not* a \"migration runner\" in the apply-time sense. The\n * apply-time runner is the thing `prisma-next migration apply` uses to\n * execute migration JSON ops against a database. `MigrationCLI` is the\n * tiny CLI surface owned by an authored `migration.ts` file: parse the\n * file's argv, load the project's `prisma-next.config.ts`, assemble a\n * `ControlStack`, instantiate the migration class, and serialize.\n *\n * The user authors a migration class, then calls\n * `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope\n * after the class definition. When the file is invoked as a node\n * entrypoint (`node migration.ts`), the CLI:\n *\n * 1. Detects whether the file is the direct entrypoint (no-op when imported).\n * 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`).\n * 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`\n * the CLI commands use, walking up from the migration file's directory.\n * 4. Probe-instantiates the migration class without a stack so it can read\n * `targetId` and verify it matches `config.target.targetId`\n * (`PN-MIG-2006` on mismatch) before any stack-driven adapter\n * construction runs.\n * 5. Assembles a `ControlStack` from the loaded config descriptors and\n * constructs the migration with that stack.\n * 6. Reads any previously-scaffolded `migration.json`, then calls\n * `buildMigrationArtifacts` from `@prisma-next/migration-tools` to\n * produce in-memory `ops.json` + `migration.json` content. Persists\n * the result to disk (or prints in dry-run mode).\n *\n * File I/O lives here, in `@prisma-next/cli`: this is the only place\n * that legitimately combines config loading, stack assembly, and\n * on-disk persistence. `@prisma-next/migration-tools` owns the pure\n * conversion from a `Migration` instance to artifact strings; `Migration`\n * stays a pure abstract class.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';\nimport { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';\nimport { createControlStack } from '@prisma-next/framework-components/control';\nimport { errorInvalidJson, MigrationToolsError } from '@prisma-next/migration-tools/errors';\nimport type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';\nimport {\n buildMigrationArtifacts,\n isDirectEntrypoint,\n type Migration,\n printMigrationHelp,\n} from '@prisma-next/migration-tools/migration';\nimport { dirname, join } from 'pathe';\nimport { loadConfig } from './config-loader';\n\n/**\n * Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses\n * accept an optional `ControlStack` in their constructor (each subclass\n * narrows the stack to its own family/target generics); the CLI always\n * passes one assembled from the loaded config. We use a rest-args `any[]`\n * constructor signature so that subclass constructors with narrower\n * parameter types remain assignable - constructor type compatibility in\n * TS is contravariant in the parameter, and a wider `unknown` parameter\n * on the alias side would reject any narrower subclass signature.\n *\n * The CLI only ever passes one argument (`new MigrationClass(stack)`);\n * the rest-arity is purely a type-compatibility concession for subclass\n * constructors that declare narrower parameter types, not an extension\n * point for additional construction arguments.\n */\n// biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures\nexport type MigrationConstructor = new (...args: any[]) => Migration;\n\ninterface ParsedArgs {\n readonly help: boolean;\n readonly dryRun: boolean;\n readonly configPath: string | undefined;\n}\n\n/**\n * Parse the subset of `process.argv` that `MigrationCLI.run` cares about.\n * Recognised flags: `--help`, `--dry-run`, `--config <path>` /\n * `--config=<path>`. Unknown flags are ignored to keep the surface\n * forgiving for ad-hoc tooling that wraps a migration file.\n *\n * Throws `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`) when\n * `--config` is missing its path argument or is followed by another flag\n * (e.g. `--config --dry-run`); silently consuming the next flag would\n * either drop dry-run handling or serialize against the wrong project.\n *\n * NOTE: this hand-rolled parser is a known wart, tracked separately by\n * TML-2318 (\"Migration CLI: replace handrolled arg parser with shared\n * CLI library\"). Until that lands the surface is intentionally tiny.\n */\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n let help = false;\n let dryRun = false;\n let configPath: string | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === '--help' || arg === '-h') {\n help = true;\n } else if (arg === '--dry-run') {\n dryRun = true;\n } else if (arg === '--config') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw errorMigrationCliInvalidConfigArg();\n }\n if (next.startsWith('-')) {\n throw errorMigrationCliInvalidConfigArg({ nextToken: next });\n }\n configPath = next;\n i++;\n } else if (arg.startsWith('--config=')) {\n configPath = arg.slice('--config='.length);\n }\n }\n\n return { help, dryRun, configPath };\n}\n\n/**\n * The CLI surface invoked by an authored `migration.ts` file. Exposed as\n * a class with a static `run` method (rather than a free function) to\n * give the concept a stable identity in the ubiquitous language: this is\n * the \"migration-file CLI\", distinct from the apply-time runner that\n * executes migration JSON ops.\n *\n * Currently a single static method. Future surface (e.g. a programmatic\n * `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can\n * land here without changing the import shape used by every authored\n * migration.\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.\nexport class MigrationCLI {\n /**\n * Orchestrates a class-flow `migration.ts` script run. Awaitable:\n * callers may `await MigrationCLI.run(...)` to surface async failures\n * from config loading, but the typical usage pattern (top-level call\n * after the class definition) does not require awaiting because\n * node's module evaluation keeps the promise alive until completion.\n *\n * Any throwable inside this function must surface through the internal\n * try/catch — script callers do not await, so an unhandled rejection\n * would silently exit 0. Treat the try/catch as load-bearing for the\n * no-await usage pattern.\n */\n static async run(importMetaUrl: string, MigrationClass: MigrationConstructor): Promise<void> {\n if (!importMetaUrl) return;\n if (!isDirectEntrypoint(importMetaUrl)) return;\n\n try {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n printMigrationHelp();\n return;\n }\n\n const migrationFile = fileURLToPath(importMetaUrl);\n const migrationDir = dirname(migrationFile);\n\n const config = await loadConfig(args.configPath);\n\n // Probe-instantiate without a stack so we can read `targetId` before\n // any target-specific constructor side effects (e.g.\n // `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete\n // subclasses are required to accept the no-arg form; the abstract\n // `Migration` constructor declares `stack?` and target subclasses\n // (Postgres, Mongo) propagate that optionality. This makes the\n // target-mismatch guard fail fast with `PN-MIG-2006` before any\n // stack-driven adapter construction begins, even if the wrong-target\n // adapter's `create` would otherwise succeed and silently misshapen\n // the stored adapter cast.\n const probe = new MigrationClass();\n\n if (probe.targetId !== config.target.targetId) {\n throw errorMigrationTargetMismatch({\n migrationTargetId: probe.targetId,\n configTargetId: config.target.targetId,\n });\n }\n\n const stack = createControlStack(config);\n const instance = new MigrationClass(stack);\n\n serializeMigrationToDisk(instance, migrationDir, args.dryRun);\n } catch (err) {\n if (CliStructuredError.is(err) || MigrationToolsError.is(err)) {\n process.stderr.write(`${err.message}: ${err.why}\\n`);\n } else {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = 1;\n }\n }\n}\n\n/**\n * Read a previously-scaffolded `migration.json` from disk, returning\n * `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`\n * when the file is present but cannot be parsed as JSON. The CLI feeds\n * this into `buildMigrationArtifacts` so the pure builder can preserve\n * fields owned by `migration plan` (contract bookends, hints, labels,\n * `createdAt`) across re-emits.\n *\n * Author-time path: this loader still does not verify the manifest hash\n * or schema — that is the apply-time loader's job. Hash mismatch is the\n * *expected* outcome of a re-author (the developer's source changes\n * invalidate the prior hash by construction), and verification here\n * would block legitimate regenerations. Syntactic JSON-parse failure,\n * however, is now surfaced rather than swallowed: a malformed\n * `migration.json` indicates either a hand-edit gone wrong or partial\n * write, and silently rebuilding from `describe()` would discard the\n * user's on-disk content (preserved bookends, hints, labels,\n * `createdAt`) without any indication something was wrong on disk.\n * Apply-time consumers always route through the verifying\n * `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.\n */\nfunction readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {\n let raw: string;\n try {\n raw = readFileSync(metadataPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationMetadata>;\n } catch (e) {\n throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));\n }\n}\n\n/**\n * Persist a migration instance's artifacts to `migrationDir`. In\n * `dryRun` mode the artifacts are printed to stdout (with the same\n * `--- migration.json --- / --- ops.json ---` framing the legacy\n * `serializeMigration` helper used) and no files are written. Otherwise\n * `ops.json` and `migration.json` are written next to `migration.ts` and\n * a confirmation line is printed.\n *\n * File I/O lives in the CLI rather than `@prisma-next/migration-tools`\n * so the migration-tools package stays focused on the pure\n * `Migration` → in-memory artifact conversion. The CLI is the only\n * legitimate site for combining config loading, stack assembly, and\n * filesystem persistence.\n */\nfunction serializeMigrationToDisk(\n instance: Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const metadataPath = join(migrationDir, 'migration.json');\n const existing = readExistingMetadata(metadataPath);\n const { opsJson, metadataJson } = buildMigrationArtifacts(instance, existing);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${metadataJson}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${opsJson}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), opsJson);\n writeFileSync(metadataPath, metadataJson);\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAS,UAAU,MAAqC;CACtD,IAAI,OAAO;CACX,IAAI,SAAS;CACb,IAAIA;AAEJ,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,YAAY,QAAQ,KAC9B,QAAO;WACE,QAAQ,YACjB,UAAS;WACA,QAAQ,YAAY;GAC7B,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,OACX,OAAM,mCAAmC;AAE3C,OAAI,KAAK,WAAW,IAAI,CACtB,OAAM,kCAAkC,EAAE,WAAW,MAAM,CAAC;AAE9D,gBAAa;AACb;aACS,IAAI,WAAW,YAAY,CACpC,cAAa,IAAI,MAAM,EAAmB;;AAI9C,QAAO;EAAE;EAAM;EAAQ;EAAY;;;;;;;;;;;;;;AAgBrC,IAAa,eAAb,MAA0B;;;;;;;;;;;;;CAaxB,aAAa,IAAI,eAAuB,gBAAqD;AAC3F,MAAI,CAAC,cAAe;AACpB,MAAI,CAAC,mBAAmB,cAAc,CAAE;AAExC,MAAI;GACF,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,CAAC;AAE7C,OAAI,KAAK,MAAM;AACb,wBAAoB;AACpB;;GAIF,MAAM,eAAe,QADC,cAAc,cAAc,CACP;GAE3C,MAAM,SAAS,MAAM,WAAW,KAAK,WAAW;GAYhD,MAAM,QAAQ,IAAI,gBAAgB;AAElC,OAAI,MAAM,aAAa,OAAO,OAAO,SACnC,OAAM,6BAA6B;IACjC,mBAAmB,MAAM;IACzB,gBAAgB,OAAO,OAAO;IAC/B,CAAC;AAMJ,4BAFiB,IAAI,eADP,mBAAmB,OAAO,CACE,EAEP,cAAc,KAAK,OAAO;WACtD,KAAK;AACZ,OAAI,mBAAmB,GAAG,IAAI,IAAI,oBAAoB,GAAG,IAAI,CAC3D,SAAQ,OAAO,MAAM,GAAG,IAAI,QAAQ,IAAI,IAAI,IAAI,IAAI;OAEpD,SAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAE/E,WAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;AA0BzB,SAAS,qBAAqB,cAAyD;CACrF,IAAIC;AACJ,KAAI;AACF,QAAM,aAAa,cAAc,QAAQ;SACnC;AACN,SAAO;;AAET,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,GAAG;AACV,QAAM,iBAAiB,cAAc,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAAC;;;;;;;;;;;;;;;;;AAkBpF,SAAS,yBACP,UACA,cACA,QACM;CACN,MAAM,eAAe,KAAK,cAAc,iBAAiB;CAEzD,MAAM,EAAE,SAAS,iBAAiB,wBAAwB,UADzC,qBAAqB,aAAa,CAC0B;AAE7E,KAAI,QAAQ;AACV,UAAQ,OAAO,MAAM,2BAA2B,aAAa,IAAI;AACjE,UAAQ,OAAO,MAAM,qBAAqB;AAC1C,UAAQ,OAAO,MAAM,GAAG,QAAQ,IAAI;AACpC;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,QAAQ;AACtD,eAAc,cAAc,aAAa;AAEzC,SAAQ,OAAO,MAAM,sCAAsC,aAAa,IAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-next/cli",
3
- "version": "0.5.0-dev.21",
3
+ "version": "0.5.0-dev.23",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "files": [
@@ -25,26 +25,26 @@
25
25
  "string-width": "^8.2.0",
26
26
  "strip-ansi": "^7.1.2",
27
27
  "wrap-ansi": "^10.0.0",
28
- "@prisma-next/config": "0.5.0-dev.21",
29
- "@prisma-next/contract": "0.5.0-dev.21",
30
- "@prisma-next/emitter": "0.5.0-dev.21",
31
- "@prisma-next/framework-components": "0.5.0-dev.21",
32
- "@prisma-next/migration-tools": "0.5.0-dev.21",
33
- "@prisma-next/errors": "0.5.0-dev.21",
34
- "@prisma-next/psl-printer": "0.5.0-dev.21",
35
- "@prisma-next/utils": "0.5.0-dev.21"
28
+ "@prisma-next/config": "0.5.0-dev.23",
29
+ "@prisma-next/contract": "0.5.0-dev.23",
30
+ "@prisma-next/emitter": "0.5.0-dev.23",
31
+ "@prisma-next/errors": "0.5.0-dev.23",
32
+ "@prisma-next/psl-printer": "0.5.0-dev.23",
33
+ "@prisma-next/utils": "0.5.0-dev.23",
34
+ "@prisma-next/migration-tools": "0.5.0-dev.23",
35
+ "@prisma-next/framework-components": "0.5.0-dev.23"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "24.10.4",
39
39
  "tsdown": "0.18.4",
40
40
  "typescript": "5.9.3",
41
41
  "vitest": "4.0.17",
42
- "@prisma-next/sql-contract": "0.5.0-dev.21",
43
- "@prisma-next/sql-contract-emitter": "0.5.0-dev.21",
44
- "@prisma-next/sql-contract-ts": "0.5.0-dev.21",
45
- "@prisma-next/sql-operations": "0.5.0-dev.21",
46
- "@prisma-next/sql-runtime": "0.5.0-dev.21",
42
+ "@prisma-next/sql-contract": "0.5.0-dev.23",
43
+ "@prisma-next/sql-contract-emitter": "0.5.0-dev.23",
44
+ "@prisma-next/sql-contract-ts": "0.5.0-dev.23",
45
+ "@prisma-next/sql-operations": "0.5.0-dev.23",
47
46
  "@prisma-next/test-utils": "0.0.1",
47
+ "@prisma-next/sql-runtime": "0.5.0-dev.23",
48
48
  "@prisma-next/tsconfig": "0.0.0",
49
49
  "@prisma-next/tsdown": "0.0.0"
50
50
  },
@@ -41,6 +41,7 @@ import { fileURLToPath } from 'node:url';
41
41
  import { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';
42
42
  import { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';
43
43
  import { createControlStack } from '@prisma-next/framework-components/control';
44
+ import { errorInvalidJson, MigrationToolsError } from '@prisma-next/migration-tools/errors';
44
45
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
45
46
  import {
46
47
  buildMigrationArtifacts,
@@ -186,7 +187,7 @@ export class MigrationCLI {
186
187
 
187
188
  serializeMigrationToDisk(instance, migrationDir, args.dryRun);
188
189
  } catch (err) {
189
- if (CliStructuredError.is(err)) {
190
+ if (CliStructuredError.is(err) || MigrationToolsError.is(err)) {
190
191
  process.stderr.write(`${err.message}: ${err.why}\n`);
191
192
  } else {
192
193
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
@@ -198,17 +199,24 @@ export class MigrationCLI {
198
199
 
199
200
  /**
200
201
  * Read a previously-scaffolded `migration.json` from disk, returning
201
- * `null` when the file is missing or unparseable. The CLI feeds this into
202
- * `buildMigrationArtifacts` so the pure builder can preserve fields owned
203
- * by `migration plan` (contract bookends, hints, labels, `createdAt`)
204
- * across re-emits.
202
+ * `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`
203
+ * when the file is present but cannot be parsed as JSON. The CLI feeds
204
+ * this into `buildMigrationArtifacts` so the pure builder can preserve
205
+ * fields owned by `migration plan` (contract bookends, hints, labels,
206
+ * `createdAt`) across re-emits.
205
207
  *
206
- * Author-time path: this loader is non-verifying by design. Hash mismatch
207
- * is the *expected* outcome of a re-author (the developer's source
208
- * changes invalidate the prior hash by construction), and verification
209
- * here would block legitimate regenerations. Apply-time consumers always
210
- * route through the verifying `readMigrationPackage` in
211
- * `@prisma-next/migration-tools/io` instead.
208
+ * Author-time path: this loader still does not verify the manifest hash
209
+ * or schema that is the apply-time loader's job. Hash mismatch is the
210
+ * *expected* outcome of a re-author (the developer's source changes
211
+ * invalidate the prior hash by construction), and verification here
212
+ * would block legitimate regenerations. Syntactic JSON-parse failure,
213
+ * however, is now surfaced rather than swallowed: a malformed
214
+ * `migration.json` indicates either a hand-edit gone wrong or partial
215
+ * write, and silently rebuilding from `describe()` would discard the
216
+ * user's on-disk content (preserved bookends, hints, labels,
217
+ * `createdAt`) without any indication something was wrong on disk.
218
+ * Apply-time consumers always route through the verifying
219
+ * `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.
212
220
  */
213
221
  function readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {
214
222
  let raw: string;
@@ -219,8 +227,8 @@ function readExistingMetadata(metadataPath: string): Partial<MigrationMetadata>
219
227
  }
220
228
  try {
221
229
  return JSON.parse(raw) as Partial<MigrationMetadata>;
222
- } catch {
223
- return null;
230
+ } catch (e) {
231
+ throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));
224
232
  }
225
233
  }
226
234