@prisma-next/cli 0.12.0-dev.39 → 0.12.0-dev.40

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 (92) hide show
  1. package/dist/cli.mjs +12 -12
  2. package/dist/{client-CcqChq9N.mjs → client-BNdG504y.mjs} +4 -4
  3. package/dist/{client-CcqChq9N.mjs.map → client-BNdG504y.mjs.map} +1 -1
  4. package/dist/{command-helpers-DK_5ItoJ.mjs → command-helpers-xvg9oq4T.mjs} +21 -3
  5. package/dist/command-helpers-xvg9oq4T.mjs.map +1 -0
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.mjs +4 -4
  9. package/dist/commands/db-schema.mjs +3 -3
  10. package/dist/commands/db-sign.mjs +4 -4
  11. package/dist/commands/db-update.mjs +5 -5
  12. package/dist/commands/db-verify.mjs +1 -1
  13. package/dist/commands/migrate.mjs +5 -6
  14. package/dist/commands/migrate.mjs.map +1 -1
  15. package/dist/commands/migration-check.d.mts +55 -1
  16. package/dist/commands/migration-check.d.mts.map +1 -1
  17. package/dist/commands/migration-check.mjs +2 -2
  18. package/dist/commands/migration-graph.d.mts +13 -1
  19. package/dist/commands/migration-graph.d.mts.map +1 -1
  20. package/dist/commands/migration-graph.mjs +15 -16
  21. package/dist/commands/migration-graph.mjs.map +1 -1
  22. package/dist/commands/migration-list.mjs +1 -1
  23. package/dist/commands/migration-log.d.mts +18 -1
  24. package/dist/commands/migration-log.d.mts.map +1 -1
  25. package/dist/commands/migration-log.mjs +1 -1
  26. package/dist/commands/migration-new.mjs +3 -3
  27. package/dist/commands/migration-plan.mjs +1 -1
  28. package/dist/commands/migration-show.d.mts +1 -4
  29. package/dist/commands/migration-show.d.mts.map +1 -1
  30. package/dist/commands/migration-show.mjs +12 -23
  31. package/dist/commands/migration-show.mjs.map +1 -1
  32. package/dist/commands/migration-status.d.mts.map +1 -1
  33. package/dist/commands/migration-status.mjs +1 -1
  34. package/dist/commands/ref.mjs +3 -3
  35. package/dist/commands/telemetry/index.mjs +1 -1
  36. package/dist/{contract-at-errors-DG3kjgoz.mjs → contract-at-errors-Wj3u4Xco.mjs} +2 -2
  37. package/dist/{contract-at-errors-DG3kjgoz.mjs.map → contract-at-errors-Wj3u4Xco.mjs.map} +1 -1
  38. package/dist/{contract-emit-C0Bs0VRj.mjs → contract-emit-COg18szA.mjs} +3 -3
  39. package/dist/{contract-emit-C0Bs0VRj.mjs.map → contract-emit-COg18szA.mjs.map} +1 -1
  40. package/dist/{contract-emit-BO0l6fnT.mjs → contract-emit-KyJNQK5-.mjs} +3 -3
  41. package/dist/{contract-emit-BO0l6fnT.mjs.map → contract-emit-KyJNQK5-.mjs.map} +1 -1
  42. package/dist/{contract-infer-DaHH7CGX.mjs → contract-infer-IEp0227u.mjs} +3 -3
  43. package/dist/{contract-infer-DaHH7CGX.mjs.map → contract-infer-IEp0227u.mjs.map} +1 -1
  44. package/dist/{contract-space-aggregate-loader-Dbr3-jHF.mjs → contract-space-aggregate-loader-BdRPfM3Q.mjs} +62 -4
  45. package/dist/{contract-space-aggregate-loader-Dbr3-jHF.mjs.map → contract-space-aggregate-loader-BdRPfM3Q.mjs.map} +1 -1
  46. package/dist/{db-verify-DppLAhYT.mjs → db-verify-C9k5KAyI.mjs} +4 -4
  47. package/dist/{db-verify-DppLAhYT.mjs.map → db-verify-C9k5KAyI.mjs.map} +1 -1
  48. package/dist/exports/control-api.mjs +2 -2
  49. package/dist/exports/index.mjs +1 -1
  50. package/dist/{framework-components-CxOVKAAh.mjs → framework-components-Be4inY3I.mjs} +2 -2
  51. package/dist/{framework-components-CxOVKAAh.mjs.map → framework-components-Be4inY3I.mjs.map} +1 -1
  52. package/dist/{init-R272pxux.mjs → init-BIxw3l7t.mjs} +3 -3
  53. package/dist/{init-R272pxux.mjs.map → init-BIxw3l7t.mjs.map} +1 -1
  54. package/dist/{inspect-live-schema-CfOSPCmR.mjs → inspect-live-schema-DXUFGQDe.mjs} +3 -3
  55. package/dist/{inspect-live-schema-CfOSPCmR.mjs.map → inspect-live-schema-DXUFGQDe.mjs.map} +1 -1
  56. package/dist/{migration-check-Dc0cOhKH.mjs → migration-check-CUavU7U9.mjs} +236 -88
  57. package/dist/migration-check-CUavU7U9.mjs.map +1 -0
  58. package/dist/{migration-command-scaffold-CIxCqBXa.mjs → migration-command-scaffold-omgKpt3K.mjs} +3 -3
  59. package/dist/{migration-command-scaffold-CIxCqBXa.mjs.map → migration-command-scaffold-omgKpt3K.mjs.map} +1 -1
  60. package/dist/{migration-graph-space-render-dmLLWift.mjs → migration-graph-space-render-ByJ83gxp.mjs} +2 -2
  61. package/dist/{migration-graph-space-render-dmLLWift.mjs.map → migration-graph-space-render-ByJ83gxp.mjs.map} +1 -1
  62. package/dist/{migration-list-C5sXrl0U.mjs → migration-list-jK6QeczE.mjs} +4 -4
  63. package/dist/{migration-list-C5sXrl0U.mjs.map → migration-list-jK6QeczE.mjs.map} +1 -1
  64. package/dist/{migration-log-CwP41Cke.mjs → migration-log-CW0EjxSr.mjs} +39 -27
  65. package/dist/migration-log-CW0EjxSr.mjs.map +1 -0
  66. package/dist/migration-path-target-DqcrbOis.mjs +24 -0
  67. package/dist/migration-path-target-DqcrbOis.mjs.map +1 -0
  68. package/dist/{migration-plan-CeTjQOIG.mjs → migration-plan-NHdlUwPG.mjs} +5 -6
  69. package/dist/{migration-plan-CeTjQOIG.mjs.map → migration-plan-NHdlUwPG.mjs.map} +1 -1
  70. package/dist/{migration-status-GMRiiLFP.mjs → migration-status-GZ6XfbWs.mjs} +21 -14
  71. package/dist/migration-status-GZ6XfbWs.mjs.map +1 -0
  72. package/dist/{ref-advancement-V1o-9LVK.mjs → ref-advancement-CJY9zOv7.mjs} +1 -1
  73. package/dist/{ref-advancement-V1o-9LVK.mjs.map → ref-advancement-CJY9zOv7.mjs.map} +1 -1
  74. package/dist/{telemetry-S-NGi9U6.mjs → telemetry-DQP0BvKv.mjs} +2 -2
  75. package/dist/{telemetry-S-NGi9U6.mjs.map → telemetry-DQP0BvKv.mjs.map} +1 -1
  76. package/dist/{verify-BdI-BgYi.mjs → verify-tvHRBBVP.mjs} +2 -2
  77. package/dist/{verify-BdI-BgYi.mjs.map → verify-tvHRBBVP.mjs.map} +1 -1
  78. package/package.json +18 -18
  79. package/src/commands/migration-check.ts +340 -117
  80. package/src/commands/migration-graph.ts +31 -19
  81. package/src/commands/migration-log.ts +33 -16
  82. package/src/commands/migration-show.ts +10 -38
  83. package/src/commands/migration-status.ts +19 -11
  84. package/src/utils/cli-errors.ts +28 -0
  85. package/src/utils/formatters/migration-log-table.ts +27 -17
  86. package/src/utils/migration-path-target.ts +39 -0
  87. package/dist/command-helpers-DK_5ItoJ.mjs.map +0 -1
  88. package/dist/extension-pack-inputs-IDvjRCi3.mjs +0 -62
  89. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +0 -1
  90. package/dist/migration-check-Dc0cOhKH.mjs.map +0 -1
  91. package/dist/migration-log-CwP41Cke.mjs.map +0 -1
  92. package/dist/migration-status-GMRiiLFP.mjs.map +0 -1
@@ -1,15 +1,15 @@
1
1
  import type { LedgerEntryRecord } from '@prisma-next/contract/types';
2
2
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
3
4
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
5
  import { Command } from 'commander';
5
6
  import { loadConfig } from '../config-loader';
6
7
  import { createControlClient } from '../control-api/client';
7
8
  import {
8
9
  CliStructuredError,
9
- errorDatabaseConnectionRequired,
10
- errorDriverRequired,
11
10
  errorUnexpected,
12
11
  mapMigrationToolsError,
12
+ requireLiveDatabase,
13
13
  } from '../utils/cli-errors';
14
14
  import {
15
15
  addGlobalOptions,
@@ -24,6 +24,7 @@ import { createAnsiMigrationListStyler } from '../utils/formatters/migration-lis
24
24
  import {
25
25
  MIGRATION_LOG_EMPTY_MESSAGE,
26
26
  renderMigrationLogTable,
27
+ type SerializedLedgerEntryRecord,
27
28
  serializeLedgerEntriesForJson,
28
29
  } from '../utils/formatters/migration-log-table';
29
30
  import { formatStyledHeader } from '../utils/formatters/styled';
@@ -36,6 +37,12 @@ interface MigrationLogOptions extends CommonCommandOptions {
36
37
  readonly db?: string;
37
38
  readonly config?: string;
38
39
  readonly utc?: boolean;
40
+ readonly ascii?: boolean;
41
+ }
42
+
43
+ export interface MigrationLogResult {
44
+ readonly ok: true;
45
+ readonly entries: readonly SerializedLedgerEntryRecord[];
39
46
  }
40
47
 
41
48
  export async function executeMigrationLogCommand(
@@ -47,16 +54,14 @@ export async function executeMigrationLogCommand(
47
54
  const { configPath } = resolveMigrationPaths(options.config, config);
48
55
 
49
56
  const dbConnection = options.db ?? config.db?.connection;
50
- if (!dbConnection) {
51
- return notOk(
52
- errorDatabaseConnectionRequired({
53
- why: `Database connection is required for migration log (set db.connection in ${configPath}, or pass --db <url>)`,
54
- commandName: 'migration log',
55
- }),
56
- );
57
- }
58
- if (!config.driver) {
59
- return notOk(errorDriverRequired({ why: 'Config.driver is required for migration log' }));
57
+ const missingDb = requireLiveDatabase({
58
+ dbConnection,
59
+ hasDriver: !!config.driver,
60
+ why: `migration log needs a database connection and driver to read the ledger (set db.connection in ${configPath}, or pass --db <url>)`,
61
+ commandName: 'migration log',
62
+ });
63
+ if (missingDb) {
64
+ return notOk(missingDb);
60
65
  }
61
66
  if (!targetSupportsMigrations(config.target)) {
62
67
  return notOk(errorUnexpected('Target does not support migrations'));
@@ -81,7 +86,7 @@ export async function executeMigrationLogCommand(
81
86
  family: config.family,
82
87
  target: config.target,
83
88
  adapter: config.adapter,
84
- driver: config.driver,
89
+ ...ifDefined('driver', config.driver),
85
90
  extensionPacks: config.extensionPacks ?? [],
86
91
  });
87
92
 
@@ -108,7 +113,8 @@ export function createMigrationLogCommand(): Command {
108
113
  command,
109
114
  'Show executed migration history',
110
115
  'Reads the database ledger and displays every applied migration edge\n' +
111
- 'in chronological order, including rollbacks and re-applies.',
116
+ 'in chronological order, including rollbacks and re-applies, merged\n' +
117
+ 'across all contract spaces. Requires a database connection.',
112
118
  );
113
119
  setCommandExamples(command, [
114
120
  'prisma-next migration log --db $DATABASE_URL',
@@ -125,19 +131,30 @@ export function createMigrationLogCommand(): Command {
125
131
  .option('--db <url>', 'Database connection string')
126
132
  .option('--config <path>', 'Path to prisma-next.config.ts')
127
133
  .option('--utc', 'Render human timestamps in UTC instead of local time')
134
+ .option('--ascii', 'Use ASCII glyphs (pipe-friendly)')
128
135
  .action(async (options: MigrationLogOptions) => {
129
136
  const flags = parseGlobalFlagsOrExit(options);
130
137
  const ui = createTerminalUI(flags);
131
138
  const result = await executeMigrationLogCommand(options, flags, ui);
132
139
  const exitCode = handleResult(result, flags, ui, (entries) => {
133
140
  if (flags.json) {
134
- ui.output(JSON.stringify(serializeLedgerEntriesForJson(entries), null, 2));
141
+ const result: MigrationLogResult = {
142
+ ok: true,
143
+ entries: serializeLedgerEntriesForJson(entries),
144
+ };
145
+ ui.output(JSON.stringify(result, null, 2));
135
146
  } else if (!flags.quiet) {
136
147
  if (entries.length === 0) {
137
148
  ui.output(MIGRATION_LOG_EMPTY_MESSAGE);
138
149
  } else {
139
150
  const styler = createAnsiMigrationListStyler({ useColor: ui.useColor });
140
- ui.output(renderMigrationLogTable(entries, { utc: options.utc === true, styler }));
151
+ ui.output(
152
+ renderMigrationLogTable(entries, {
153
+ utc: options.utc === true,
154
+ styler,
155
+ glyphMode: ui.resolveGlyphMode(options.ascii === true),
156
+ }),
157
+ );
141
158
  }
142
159
  }
143
160
  });
@@ -13,7 +13,7 @@ import { castAs } from '@prisma-next/utils/casts';
13
13
  import { ifDefined } from '@prisma-next/utils/defined';
14
14
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
15
15
  import { Command } from 'commander';
16
- import { isAbsolute, relative, resolve } from 'pathe';
16
+ import { relative } from 'pathe';
17
17
  import { loadConfig } from '../config-loader';
18
18
  import { createControlClient } from '../control-api/client';
19
19
  import {
@@ -36,6 +36,11 @@ import { formatMigrationShowOutput } from '../utils/formatters/migrations';
36
36
  import { formatStyledHeader } from '../utils/formatters/styled';
37
37
  import type { CommonCommandOptions } from '../utils/global-flags';
38
38
  import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
39
+ import {
40
+ findPackageByDirPath,
41
+ looksLikePath,
42
+ resolveAppTargetPath,
43
+ } from '../utils/migration-path-target';
39
44
  import { handleResult } from '../utils/result-handler';
40
45
  import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
41
46
 
@@ -65,33 +70,6 @@ export interface MigrationShowResult {
65
70
  readonly migration: MigrationShowPresent;
66
71
  }
67
72
 
68
- function looksLikePath(target: string): boolean {
69
- return target.includes('/') || target.includes('\\');
70
- }
71
-
72
- export function resolveAppTargetPath(
73
- target: string,
74
- appMigrationsDir: string,
75
- appMigrationsRelative: string,
76
- ): Result<string, CliStructuredError> {
77
- const targetPath = resolve(target);
78
- const relativeToApp = relative(appMigrationsDir, targetPath);
79
- const isOutsideAppDir =
80
- relativeToApp === '' ||
81
- relativeToApp === '.' ||
82
- relativeToApp.startsWith('..') ||
83
- isAbsolute(relativeToApp);
84
- if (isOutsideAppDir) {
85
- return notOk(
86
- errorRuntime('Target must point to an app-space migration', {
87
- why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
88
- fix: 'Pass an app-space migration directory or use a hash prefix.',
89
- }),
90
- );
91
- }
92
- return ok(targetPath);
93
- }
94
-
95
73
  function pkgToPresent(
96
74
  spaceId: string,
97
75
  pkg: OnDiskMigrationPackage,
@@ -117,14 +95,6 @@ function pkgToPresent(
117
95
  };
118
96
  }
119
97
 
120
- function findPackageByDirPath(
121
- packages: readonly OnDiskMigrationPackage[],
122
- resolvedDirPath: string,
123
- ): OnDiskMigrationPackage | undefined {
124
- const normalized = resolve(resolvedDirPath);
125
- return packages.find((p) => resolve(p.dirPath) === normalized);
126
- }
127
-
128
98
  async function executeMigrationShowCommand(
129
99
  target: string,
130
100
  options: MigrationShowOptions,
@@ -260,11 +230,13 @@ export function createMigrationShowCommand(): Command {
260
230
  command,
261
231
  'Display migration package contents',
262
232
  'Shows the operations, statement preview, and metadata for one app-space migration.\n' +
263
- 'Accepts a directory path, directory name, or hash prefix.',
233
+ 'Accepts a directory path, directory name, or hash prefix.\n' +
234
+ 'Offline — does not consult the database.',
264
235
  );
265
236
  setCommandExamples(command, [
266
237
  'prisma-next migration show 20260101_100000_add_user',
267
238
  'prisma-next migration show sha256:a1b2c3',
239
+ 'prisma-next migration show 20260101_100000_add_user --json',
268
240
  ]);
269
241
  setCommandSeeAlso(command, [
270
242
  { verb: 'migration status', oneLiner: 'Show migration path and pending status' },
@@ -273,7 +245,7 @@ export function createMigrationShowCommand(): Command {
273
245
  { verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
274
246
  ]);
275
247
  addGlobalOptions(command)
276
- .argument('<target>', 'Migration reference: directory name, hash/prefix, or path')
248
+ .argument('<target>', 'Migration reference: directory name, hash/prefix, ref, or path')
277
249
  .option('--config <path>', 'Path to prisma-next.config.ts')
278
250
  .action(async (target: string, options: MigrationShowOptions) => {
279
251
  const flags = parseGlobalFlagsOrExit(options);
@@ -21,10 +21,10 @@ import { loadConfig } from '../config-loader';
21
21
  import { createControlClient } from '../control-api/client';
22
22
  import {
23
23
  CliStructuredError,
24
- errorDatabaseConnectionRequired,
25
24
  errorUnexpected,
26
25
  mapMigrationToolsError,
27
26
  mapRefResolutionError,
27
+ requireLiveDatabase,
28
28
  } from '../utils/cli-errors';
29
29
  import {
30
30
  addGlobalOptions,
@@ -76,6 +76,7 @@ interface MigrationStatusOptions extends CommonCommandOptions {
76
76
  readonly from?: string;
77
77
  readonly space?: string;
78
78
  readonly legend?: boolean;
79
+ readonly ascii?: boolean;
79
80
  }
80
81
 
81
82
  export interface MigrationStatusMigrationEntry extends MigrationListEntry {
@@ -271,13 +272,16 @@ async function executeMigrationStatusCommand(
271
272
  const hasDriver = !!config.driver;
272
273
  const usingFromOverride = options.from !== undefined;
273
274
 
274
- if (!usingFromOverride && (!dbConnection || !hasDriver)) {
275
- return notOk(
276
- errorDatabaseConnectionRequired({
277
- why: 'migration status needs a database connection to read the marker and ledger (or pass --from for offline path preview)',
278
- retryCommand: 'prisma-next migration status --from <contract>',
279
- }),
280
- );
275
+ if (!usingFromOverride) {
276
+ const missingDb = requireLiveDatabase({
277
+ dbConnection,
278
+ hasDriver,
279
+ why: 'migration status needs a database connection to read the marker and ledger (or pass --from for offline path preview)',
280
+ retryCommand: 'prisma-next migration status --from <contract>',
281
+ });
282
+ if (missingDb) {
283
+ return notOk(missingDb);
284
+ }
281
285
  }
282
286
 
283
287
  let allRefs: Refs = {};
@@ -374,7 +378,7 @@ async function executeMigrationStatusCommand(
374
378
  ui.stderr(
375
379
  renderMigrationGraphLegend({
376
380
  colorize: flags.color !== false,
377
- glyphMode: ui.resolveGlyphMode(false),
381
+ glyphMode: ui.resolveGlyphMode(options.ascii === true),
378
382
  }),
379
383
  );
380
384
  ui.stderr('');
@@ -449,7 +453,7 @@ async function executeMigrationStatusCommand(
449
453
 
450
454
  const showAppliedOverlay = connected && !usingFromOverride;
451
455
  const showDbMarker = connected && !usingFromOverride;
452
- const glyphMode = ui.resolveGlyphMode(false);
456
+ const glyphMode = ui.resolveGlyphMode(options.ascii === true);
453
457
  const colorize = flags.color !== false;
454
458
 
455
459
  const statusSpaces: MigrationStatusSpaceResult[] = [];
@@ -630,7 +634,8 @@ export function createMigrationStatusCommand(): Command {
630
634
  command,
631
635
  'Show migration path and pending status',
632
636
  'Shows which migrations are pending between the database marker and\n' +
633
- 'the target contract. Requires a database connection for live status.\n' +
637
+ 'the target contract. Requires a database connection.\n' +
638
+ 'Pass --from for an offline path preview without a database.\n' +
634
639
  'Use `migration graph` for topology, `migration log` for history,\n' +
635
640
  'and `migration list` for on-disk enumeration.',
636
641
  );
@@ -638,6 +643,8 @@ export function createMigrationStatusCommand(): Command {
638
643
  'prisma-next migration status --db $DATABASE_URL',
639
644
  'prisma-next migration status --to production --db $DATABASE_URL',
640
645
  'prisma-next migration status --from sha256:abc --to production',
646
+ 'prisma-next migration status --from sha256:abc --to production --json',
647
+ 'prisma-next migration status --ascii --from sha256:abc --to production',
641
648
  'prisma-next migration status --legend --from sha256:abc --to production',
642
649
  ]);
643
650
  setCommandSeeAlso(command, [
@@ -659,6 +666,7 @@ export function createMigrationStatusCommand(): Command {
659
666
  'Origin contract reference; same grammar as --to. Supplying --from switches to offline path computation.',
660
667
  )
661
668
  .option('--legend', 'Print a key for the tree glyphs and lane colors')
669
+ .option('--ascii', 'Use ASCII glyphs (pipe-friendly)')
662
670
  .action(async (options: MigrationStatusOptions) => {
663
671
  const flags = parseGlobalFlagsOrExit(options);
664
672
  const ui = createTerminalUI(flags);
@@ -28,6 +28,7 @@ import {
28
28
  import { errorRuntime } from '@prisma-next/errors/execution';
29
29
  import type { MigrationToolsError } from '@prisma-next/migration-tools/errors';
30
30
  import type { RefResolutionError } from '@prisma-next/migration-tools/ref-resolution';
31
+ import { ifDefined } from '@prisma-next/utils/defined';
31
32
  import type { MigrateFailure } from '../control-api/types';
32
33
 
33
34
  export {
@@ -342,6 +343,33 @@ export function mapMigrationToolsError(error: MigrationToolsError): CliStructure
342
343
  });
343
344
  }
344
345
 
346
+ /**
347
+ * Shared "needs a live database" precondition for read verbs that consult the
348
+ * marker/ledger (`migration log`, `migration status`). A command needs both a
349
+ * connection string and a control-plane driver; either missing yields the same
350
+ * `PN-CLI-4005` envelope with `meta.missingFlags` (canonical long-form flags
351
+ * per CLI Style Guide §Errors) so callers can react programmatically. Returns
352
+ * `null` when both are present.
353
+ */
354
+ export function requireLiveDatabase(args: {
355
+ readonly dbConnection: unknown;
356
+ readonly hasDriver: boolean;
357
+ readonly why: string;
358
+ readonly commandName?: string;
359
+ readonly retryCommand?: string;
360
+ }): CliStructuredError | null {
361
+ if (args.dbConnection && args.hasDriver) {
362
+ return null;
363
+ }
364
+ const missingFlags = args.dbConnection ? [] : ['--db'];
365
+ return errorDatabaseConnectionRequired({
366
+ why: args.why,
367
+ missingFlags,
368
+ ...ifDefined('commandName', args.commandName),
369
+ ...ifDefined('retryCommand', args.retryCommand),
370
+ });
371
+ }
372
+
345
373
  /**
346
374
  * Maps a `RefResolutionError` from the contract/migration reference
347
375
  * resolver into a CLI structured error envelope.
@@ -1,9 +1,10 @@
1
1
  import type { LedgerEntryRecord } from '@prisma-next/contract/types';
2
2
  import stringWidth from 'string-width';
3
+ import type { GlyphMode } from '../glyph-mode';
3
4
  import {
4
5
  abbreviateContractHash,
5
- MIGRATION_LIST_EMPTY_SOURCE,
6
- MIGRATION_LIST_FORWARD_EDGE_GLYPH,
6
+ migrationListEmptySource,
7
+ migrationListForwardArrow,
7
8
  } from './migration-list-data-column';
8
9
  import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
9
10
 
@@ -12,6 +13,7 @@ export type LedgerTimestampMode = 'local' | 'utc' | 'iso';
12
13
  export interface RenderMigrationLogTableOptions {
13
14
  readonly utc?: boolean;
14
15
  readonly styler?: MigrationListStyler;
16
+ readonly glyphMode?: GlyphMode;
15
17
  }
16
18
 
17
19
  export interface SerializedLedgerEntryRecord {
@@ -31,6 +33,7 @@ const HEADING_CHANGE = 'Change';
31
33
  const HEADING_OPS = 'Ops';
32
34
  const COLUMN_SEPARATOR = ' ';
33
35
  const DIVIDER_CHAR = '─';
36
+ const ASCII_DIVIDER_CHAR = '-';
34
37
 
35
38
  export function sortLedgerEntries(entries: readonly LedgerEntryRecord[]): LedgerEntryRecord[] {
36
39
  return [...entries].sort((left, right) => {
@@ -65,27 +68,32 @@ export function formatLedgerAppliedAt(date: Date, mode: LedgerTimestampMode): st
65
68
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())} ${sign}${offsetHours}:${offsetMins}`;
66
69
  }
67
70
 
68
- export function formatHashEndpoint(hash: string | null): string {
71
+ export function formatHashEndpoint(hash: string | null, glyphMode: GlyphMode = 'unicode'): string {
69
72
  if (hash === null) {
70
- return MIGRATION_LIST_EMPTY_SOURCE;
73
+ return migrationListEmptySource(glyphMode);
71
74
  }
72
75
  return abbreviateContractHash(hash);
73
76
  }
74
77
 
75
- export function formatHashTransition(from: string | null, to: string): string {
76
- return `${formatHashEndpoint(from)} ${MIGRATION_LIST_FORWARD_EDGE_GLYPH} ${abbreviateContractHash(to)}`;
78
+ export function formatHashTransition(
79
+ from: string | null,
80
+ to: string,
81
+ glyphMode: GlyphMode = 'unicode',
82
+ ): string {
83
+ return `${formatHashEndpoint(from, glyphMode)} ${migrationListForwardArrow(glyphMode)} ${abbreviateContractHash(to)}`;
77
84
  }
78
85
 
79
86
  export function styleHashTransition(
80
87
  from: string | null,
81
88
  to: string,
82
89
  styler: MigrationListStyler,
90
+ glyphMode: GlyphMode = 'unicode',
83
91
  ): string {
84
92
  const fromPart =
85
93
  from === null
86
- ? styler.glyph(MIGRATION_LIST_EMPTY_SOURCE)
94
+ ? styler.glyph(migrationListEmptySource(glyphMode))
87
95
  : styler.sourceHash(abbreviateContractHash(from));
88
- const arrow = styler.glyph(MIGRATION_LIST_FORWARD_EDGE_GLYPH);
96
+ const arrow = styler.glyph(migrationListForwardArrow(glyphMode));
89
97
  const dest = styler.destHash(abbreviateContractHash(to));
90
98
  return `${fromPart} ${arrow} ${dest}`;
91
99
  }
@@ -99,8 +107,8 @@ function columnWidth(values: readonly string[]): number {
99
107
  return values.reduce((max, value) => Math.max(max, stringWidth(value)), 0);
100
108
  }
101
109
 
102
- function padDividerCell(valueWidth: number): string {
103
- return DIVIDER_CHAR.repeat(valueWidth + 2);
110
+ function padDividerCell(valueWidth: number, dividerChar: string): string {
111
+ return dividerChar.repeat(valueWidth + 2);
104
112
  }
105
113
 
106
114
  function padTextCell(value: string, valueWidth: number): string {
@@ -122,13 +130,15 @@ export function renderMigrationLogTable(
122
130
  }
123
131
 
124
132
  const styler = options.styler ?? IDENTITY_MIGRATION_LIST_STYLER;
133
+ const glyphMode = options.glyphMode ?? 'unicode';
134
+ const dividerChar = glyphMode === 'ascii' ? ASCII_DIVIDER_CHAR : DIVIDER_CHAR;
125
135
  const showSpace = new Set(sorted.map((entry) => entry.space)).size > 1;
126
136
  const timestampMode: LedgerTimestampMode = options.utc ? 'utc' : 'local';
127
137
  const rows = sorted.map((entry) => ({
128
138
  appliedAt: formatLedgerAppliedAt(entry.appliedAt, timestampMode),
129
139
  space: entry.space,
130
140
  migrationName: entry.migrationName,
131
- transition: formatHashTransition(entry.from, entry.to),
141
+ transition: formatHashTransition(entry.from, entry.to, glyphMode),
132
142
  ops: `${entry.operationCount} ops`,
133
143
  from: entry.from,
134
144
  to: entry.to,
@@ -151,14 +161,14 @@ export function renderMigrationLogTable(
151
161
  );
152
162
  const heading = headingParts.join(COLUMN_SEPARATOR);
153
163
 
154
- const dividerParts = [padDividerCell(appliedAtWidth)];
164
+ const dividerParts = [padDividerCell(appliedAtWidth, dividerChar)];
155
165
  if (showSpace) {
156
- dividerParts.push(padDividerCell(spaceWidth));
166
+ dividerParts.push(padDividerCell(spaceWidth, dividerChar));
157
167
  }
158
168
  dividerParts.push(
159
- padDividerCell(nameWidth),
160
- padDividerCell(transitionWidth),
161
- padDividerCell(opsWidth),
169
+ padDividerCell(nameWidth, dividerChar),
170
+ padDividerCell(transitionWidth, dividerChar),
171
+ padDividerCell(opsWidth, dividerChar),
162
172
  );
163
173
  const divider = dividerParts.map((cell) => styler.summary(cell)).join(COLUMN_SEPARATOR);
164
174
 
@@ -169,7 +179,7 @@ export function renderMigrationLogTable(
169
179
  }
170
180
  parts.push(
171
181
  padTextCell(styler.dirName(row.migrationName), nameWidth),
172
- padTextCell(styleHashTransition(row.from, row.to, styler), transitionWidth),
182
+ padTextCell(styleHashTransition(row.from, row.to, styler, glyphMode), transitionWidth),
173
183
  padOpsCell(row.ops, opsWidth),
174
184
  );
175
185
  return parts.join(COLUMN_SEPARATOR);
@@ -0,0 +1,39 @@
1
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
2
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
3
+ import { isAbsolute, relative, resolve } from 'pathe';
4
+ import { type CliStructuredError, errorRuntime } from './cli-errors';
5
+
6
+ export function looksLikePath(target: string): boolean {
7
+ return target.includes('/') || target.includes('\\');
8
+ }
9
+
10
+ export function resolveAppTargetPath(
11
+ target: string,
12
+ appMigrationsDir: string,
13
+ appMigrationsRelative: string,
14
+ ): Result<string, CliStructuredError> {
15
+ const targetPath = resolve(target);
16
+ const relativeToApp = relative(appMigrationsDir, targetPath);
17
+ const isOutsideAppDir =
18
+ relativeToApp === '' ||
19
+ relativeToApp === '.' ||
20
+ relativeToApp.startsWith('..') ||
21
+ isAbsolute(relativeToApp);
22
+ if (isOutsideAppDir) {
23
+ return notOk(
24
+ errorRuntime('Target must point to an app-space migration', {
25
+ why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
26
+ fix: 'Pass an app-space migration directory or use a hash prefix.',
27
+ }),
28
+ );
29
+ }
30
+ return ok(targetPath);
31
+ }
32
+
33
+ export function findPackageByDirPath(
34
+ packages: readonly OnDiskMigrationPackage[],
35
+ resolvedDirPath: string,
36
+ ): OnDiskMigrationPackage | undefined {
37
+ const normalized = resolve(resolvedDirPath);
38
+ return packages.find((p) => resolve(p.dirPath) === normalized);
39
+ }