@prisma-next/cli 0.12.0 → 0.13.0-dev.1

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 (213) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
  5. package/dist/client-CJzuo5wX.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
  7. package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +4 -5
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -3
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -6
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -7
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +37 -3
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +298 -12
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +55 -13
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +3 -2
  33. package/dist/commands/migration-graph.d.mts +17 -8
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +185 -2
  36. package/dist/commands/migration-graph.mjs.map +1 -0
  37. package/dist/commands/migration-list.d.mts +26 -27
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -190
  40. package/dist/commands/migration-log.d.mts +9 -19
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +1 -137
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +6 -5
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -1
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +17 -21
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +24 -36
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +42 -144
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +3 -759
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +4 -4
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/commands/telemetry/index.d.mts +7 -0
  61. package/dist/commands/telemetry/index.d.mts.map +1 -0
  62. package/dist/commands/telemetry/index.mjs +2 -0
  63. package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
  64. package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
  65. package/dist/config-loader.mjs +1 -1
  66. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
  67. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
  68. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
  69. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
  70. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
  71. package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
  72. package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
  73. package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
  74. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
  75. package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
  76. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
  77. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
  78. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +5 -3
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +3 -3
  83. package/dist/exports/index.mjs +1 -1
  84. package/dist/exports/index.mjs.map +1 -1
  85. package/dist/exports/init-output.d.mts +1 -3
  86. package/dist/exports/init-output.d.mts.map +1 -1
  87. package/dist/exports/init-output.mjs +1 -1
  88. package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
  89. package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
  90. package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
  91. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
  92. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
  93. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
  94. package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
  95. package/dist/init-0HwB-Vh8.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
  97. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
  98. package/dist/migration-check-soB5uZEQ.mjs +573 -0
  99. package/dist/migration-check-soB5uZEQ.mjs.map +1 -0
  100. package/dist/migration-cli.mjs +1 -1
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
  104. package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
  105. package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
  106. package/dist/migration-list-DlJJ_38Z.mjs +230 -0
  107. package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
  108. package/dist/migration-log-CG0qQAFm.mjs +222 -0
  109. package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
  110. package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
  111. package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
  112. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
  113. package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
  114. package/dist/migration-status-CgWSoI_g.mjs +446 -0
  115. package/dist/migration-status-CgWSoI_g.mjs.map +1 -0
  116. package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
  117. package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
  118. package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
  119. package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
  120. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
  121. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
  122. package/dist/schemas-CeGMYFYX.d.mts +191 -0
  123. package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
  124. package/dist/schemas-KhXMzNA_.mjs +112 -0
  125. package/dist/schemas-KhXMzNA_.mjs.map +1 -0
  126. package/dist/telemetry-BIM4beEO.mjs +122 -0
  127. package/dist/telemetry-BIM4beEO.mjs.map +1 -0
  128. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
  129. package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
  130. package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
  131. package/dist/types-C_tYiJYx.d.mts.map +1 -0
  132. package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
  133. package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
  134. package/package.json +26 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/contract-infer.ts +2 -2
  137. package/src/commands/db-update.ts +7 -1
  138. package/src/commands/init/index.ts +6 -35
  139. package/src/commands/init/init.ts +1 -14
  140. package/src/commands/init/inputs.ts +0 -75
  141. package/src/commands/inspect-live-schema.ts +10 -0
  142. package/src/commands/json/schemas.ts +195 -0
  143. package/src/commands/migrate.ts +527 -8
  144. package/src/commands/migration-check.ts +469 -134
  145. package/src/commands/migration-graph.ts +164 -91
  146. package/src/commands/migration-list.ts +72 -39
  147. package/src/commands/migration-log.ts +52 -102
  148. package/src/commands/migration-new.ts +2 -1
  149. package/src/commands/migration-plan.ts +2 -1
  150. package/src/commands/migration-show.ts +31 -66
  151. package/src/commands/migration-status-overlay.ts +61 -0
  152. package/src/commands/migration-status.ts +458 -1066
  153. package/src/commands/telemetry/index.ts +107 -0
  154. package/src/commands/telemetry/status.ts +67 -0
  155. package/src/control-api/client.ts +70 -9
  156. package/src/control-api/operations/contract-emit.ts +22 -2
  157. package/src/control-api/operations/db-init.ts +6 -3
  158. package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
  159. package/src/control-api/operations/db-update.ts +7 -4
  160. package/src/control-api/operations/db-verify.ts +15 -5
  161. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  162. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  163. package/src/control-api/types.ts +56 -29
  164. package/src/utils/cli-errors.ts +70 -2
  165. package/src/utils/formatters/errors.ts +11 -0
  166. package/src/utils/formatters/migration-graph-command-render.ts +239 -0
  167. package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
  168. package/src/utils/formatters/migration-graph-labels.ts +408 -0
  169. package/src/utils/formatters/migration-graph-model.ts +103 -0
  170. package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
  171. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  172. package/src/utils/formatters/migration-graph-space-render.ts +188 -0
  173. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  174. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  175. package/src/utils/formatters/migration-list-render.ts +135 -71
  176. package/src/utils/formatters/migration-list-styler.ts +46 -5
  177. package/src/utils/formatters/migration-list-types.ts +5 -21
  178. package/src/utils/formatters/migration-log-table.ts +205 -0
  179. package/src/utils/formatters/migrations.ts +33 -11
  180. package/src/utils/global-flags.ts +35 -0
  181. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  182. package/src/utils/legend.ts +38 -0
  183. package/src/utils/migration-path-target.ts +60 -0
  184. package/src/utils/telemetry.ts +68 -32
  185. package/dist/client-KgJorIvG.mjs.map +0 -1
  186. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  187. package/dist/commands/migration-list.mjs.map +0 -1
  188. package/dist/commands/migration-log.mjs.map +0 -1
  189. package/dist/commands/migration-status.mjs.map +0 -1
  190. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  191. package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
  192. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  193. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  194. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  195. package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
  196. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  197. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  198. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  199. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  200. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  201. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  202. package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
  203. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  204. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  205. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  206. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  207. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  208. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  209. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  210. package/src/utils/formatters/graph-render.ts +0 -1323
  211. package/src/utils/formatters/graph-types.ts +0 -120
  212. package/src/utils/formatters/migration-graph-layout.ts +0 -1119
  213. package/src/utils/formatters/migration-graph-tree-render.ts +0 -459
@@ -1,6 +1,39 @@
1
1
  import { bold, cyan, cyanBright, dim, green, yellow } from 'colorette';
2
2
  import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
3
3
 
4
+ export type MigrationListStylerWithMarkers = MigrationListStyler & {
5
+ markers(names: readonly string[]): string;
6
+ };
7
+
8
+ function hasMarkersFormatter(
9
+ styler: MigrationListStyler,
10
+ ): styler is MigrationListStylerWithMarkers {
11
+ return 'markers' in styler && typeof styler.markers === 'function';
12
+ }
13
+
14
+ function styleMarkerName(name: string): string {
15
+ return name === CONTRACT_MARKER_NAME ? bold(green(name)) : green(name);
16
+ }
17
+
18
+ function plainMarkers(names: readonly string[]): string {
19
+ return names.map((name) => `@${name}`).join(' ');
20
+ }
21
+
22
+ export function formatContractNodeOverlays(
23
+ styler: MigrationListStyler,
24
+ markers: readonly string[],
25
+ refs: readonly string[],
26
+ ): string {
27
+ const parts: string[] = [];
28
+ if (markers.length > 0) {
29
+ parts.push(hasMarkersFormatter(styler) ? styler.markers(markers) : plainMarkers(markers));
30
+ }
31
+ if (refs.length > 0) {
32
+ parts.push(styler.refs(refs));
33
+ }
34
+ return parts.join(' ');
35
+ }
36
+
4
37
  /**
5
38
  * The current contract overlay marker. Unlike user refs, this names the user's
6
39
  * declared desired state — the implicit base/target for `plan` / `migrate` —
@@ -10,7 +43,7 @@ import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migr
10
43
  export const CONTRACT_MARKER_NAME = 'contract';
11
44
 
12
45
  function styleRefName(name: string): string {
13
- return name === CONTRACT_MARKER_NAME ? bold(green(name)) : green(name);
46
+ return green(name);
14
47
  }
15
48
 
16
49
  /**
@@ -29,17 +62,21 @@ function styleRefName(name: string): string {
29
62
  * - `glyph` (`→` / `⟲` / `∅`): dim
30
63
  * - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
31
64
  * - `invariants` (`{...}`): yellow
32
- * - `refs` (`(...)`): green; the `contract` desired-state marker inside is
33
- * green-bold (the active ref is bolded separately by the tree styler)
65
+ * - `markers` (`@contract @db`): green; the `contract` desired-state marker is
66
+ * green-bold (`db` is plain green); the `@` sigil is applied to each name
67
+ * - `refs` (`(...)`): green (the active ref is bolded separately by the tree styler)
34
68
  * - `spaceHeading` (`<spaceId>:`): bold
35
69
  * - `summary`: dim
36
70
  * - `emptyState`: dim
37
71
  */
38
72
  export function createAnsiMigrationListStyler(opts: {
39
73
  readonly useColor: boolean;
40
- }): MigrationListStyler {
74
+ }): MigrationListStylerWithMarkers {
41
75
  if (!opts.useColor) {
42
- return IDENTITY_MIGRATION_LIST_STYLER;
76
+ return {
77
+ ...IDENTITY_MIGRATION_LIST_STYLER,
78
+ markers: plainMarkers,
79
+ };
43
80
  }
44
81
  return {
45
82
  // Kind glyphs stay bright in both flat and graph views; lanes carry the dim gutter.
@@ -50,6 +87,10 @@ export function createAnsiMigrationListStyler(opts: {
50
87
  glyph: (text) => dim(text),
51
88
  lane: (text) => dim(text),
52
89
  invariants: (ids) => yellow(`{${ids.join(', ')}}`),
90
+ markers: (names) => {
91
+ const sigil = green('@');
92
+ return names.map((name) => sigil + styleMarkerName(name)).join(' ');
93
+ },
53
94
  refs: (names) => {
54
95
  const open = green('(');
55
96
  const close = green(')');
@@ -1,21 +1,5 @@
1
- export interface MigrationListEntry {
2
- readonly dirName: string;
3
- readonly from: string | null;
4
- readonly to: string;
5
- readonly migrationHash: string;
6
- readonly operationCount: number;
7
- readonly createdAt: string;
8
- readonly refs: readonly string[];
9
- readonly providedInvariants: readonly string[];
10
- }
11
-
12
- export interface MigrationSpaceListEntry {
13
- readonly spaceId: string;
14
- readonly migrations: readonly MigrationListEntry[];
15
- }
16
-
17
- export interface MigrationListResult {
18
- readonly ok: true;
19
- readonly spaces: readonly MigrationSpaceListEntry[];
20
- readonly summary: string;
21
- }
1
+ export type {
2
+ MigrationEntry as MigrationListEntry,
3
+ MigrationListResult,
4
+ MigrationSpaceListEntry,
5
+ } from '../../commands/json/schemas';
@@ -0,0 +1,205 @@
1
+ import type { LedgerEntryRecord } from '@prisma-next/contract/types';
2
+ import stringWidth from 'string-width';
3
+ import type { GlyphMode } from '../glyph-mode';
4
+ import {
5
+ abbreviateContractHash,
6
+ migrationListEmptySource,
7
+ migrationListForwardArrow,
8
+ } from './migration-list-data-column';
9
+ import { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';
10
+
11
+ export type LedgerTimestampMode = 'local' | 'utc' | 'iso';
12
+
13
+ export interface RenderMigrationLogTableOptions {
14
+ readonly utc?: boolean;
15
+ readonly styler?: MigrationListStyler;
16
+ readonly glyphMode?: GlyphMode;
17
+ }
18
+
19
+ export interface SerializedLedgerEntryRecord {
20
+ readonly space: string;
21
+ readonly name: string;
22
+ readonly hash: string;
23
+ readonly fromContract: string | null;
24
+ readonly toContract: string;
25
+ readonly appliedAt: string;
26
+ readonly operationCount: number;
27
+ }
28
+
29
+ const HEADING_APPLIED_AT = 'Applied at';
30
+ const HEADING_SPACE = 'Space';
31
+ const HEADING_MIGRATION = 'Migration';
32
+ const HEADING_CHANGE = 'Change';
33
+ const HEADING_OPS = 'Ops';
34
+ const COLUMN_SEPARATOR = ' ';
35
+ const DIVIDER_CHAR = '─';
36
+ const ASCII_DIVIDER_CHAR = '-';
37
+
38
+ export function sortLedgerEntries(entries: readonly LedgerEntryRecord[]): LedgerEntryRecord[] {
39
+ return [...entries].sort((left, right) => {
40
+ const timeDiff = left.appliedAt.getTime() - right.appliedAt.getTime();
41
+ if (timeDiff !== 0) {
42
+ return timeDiff;
43
+ }
44
+ const spaceDiff = left.space.localeCompare(right.space);
45
+ if (spaceDiff !== 0) {
46
+ return spaceDiff;
47
+ }
48
+ return left.migrationName.localeCompare(right.migrationName);
49
+ });
50
+ }
51
+
52
+ function pad2(value: number): string {
53
+ return String(value).padStart(2, '0');
54
+ }
55
+
56
+ export function formatLedgerAppliedAt(date: Date, mode: LedgerTimestampMode): string {
57
+ if (mode === 'iso') {
58
+ return date.toISOString();
59
+ }
60
+ if (mode === 'utc') {
61
+ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}Z`;
62
+ }
63
+ const offsetMinutes = -date.getTimezoneOffset();
64
+ const sign = offsetMinutes >= 0 ? '+' : '-';
65
+ const absoluteOffset = Math.abs(offsetMinutes);
66
+ const offsetHours = pad2(Math.floor(absoluteOffset / 60));
67
+ const offsetMins = pad2(absoluteOffset % 60);
68
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())} ${sign}${offsetHours}:${offsetMins}`;
69
+ }
70
+
71
+ export function formatHashEndpoint(hash: string | null, glyphMode: GlyphMode = 'unicode'): string {
72
+ if (hash === null) {
73
+ return migrationListEmptySource(glyphMode);
74
+ }
75
+ return abbreviateContractHash(hash);
76
+ }
77
+
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)}`;
84
+ }
85
+
86
+ export function styleHashTransition(
87
+ from: string | null,
88
+ to: string,
89
+ styler: MigrationListStyler,
90
+ glyphMode: GlyphMode = 'unicode',
91
+ ): string {
92
+ const fromPart =
93
+ from === null
94
+ ? styler.glyph(migrationListEmptySource(glyphMode))
95
+ : styler.sourceHash(abbreviateContractHash(from));
96
+ const arrow = styler.glyph(migrationListForwardArrow(glyphMode));
97
+ const dest = styler.destHash(abbreviateContractHash(to));
98
+ return `${fromPart} ${arrow} ${dest}`;
99
+ }
100
+
101
+ function padVisible(text: string, targetWidth: number): string {
102
+ const padding = Math.max(0, targetWidth - stringWidth(text));
103
+ return text + ' '.repeat(padding);
104
+ }
105
+
106
+ function columnWidth(values: readonly string[]): number {
107
+ return values.reduce((max, value) => Math.max(max, stringWidth(value)), 0);
108
+ }
109
+
110
+ function padDividerCell(valueWidth: number, dividerChar: string): string {
111
+ return dividerChar.repeat(valueWidth + 2);
112
+ }
113
+
114
+ function padTextCell(value: string, valueWidth: number): string {
115
+ return ` ${padVisible(value, valueWidth)} `;
116
+ }
117
+
118
+ function padOpsCell(value: string, valueWidth: number): string {
119
+ const padding = Math.max(0, valueWidth - stringWidth(value));
120
+ return ` ${' '.repeat(padding)}${value} `;
121
+ }
122
+
123
+ export function renderMigrationLogTable(
124
+ entries: readonly LedgerEntryRecord[],
125
+ options: RenderMigrationLogTableOptions = {},
126
+ ): string {
127
+ const sorted = sortLedgerEntries(entries);
128
+ if (sorted.length === 0) {
129
+ return '';
130
+ }
131
+
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;
135
+ const showSpace = new Set(sorted.map((entry) => entry.space)).size > 1;
136
+ const timestampMode: LedgerTimestampMode = options.utc ? 'utc' : 'local';
137
+ const rows = sorted.map((entry) => ({
138
+ appliedAt: formatLedgerAppliedAt(entry.appliedAt, timestampMode),
139
+ space: entry.space,
140
+ migrationName: entry.migrationName,
141
+ transition: formatHashTransition(entry.from, entry.to, glyphMode),
142
+ ops: `${entry.operationCount} ops`,
143
+ from: entry.from,
144
+ to: entry.to,
145
+ }));
146
+
147
+ const appliedAtWidth = columnWidth([HEADING_APPLIED_AT, ...rows.map((row) => row.appliedAt)]);
148
+ const spaceWidth = showSpace ? columnWidth([HEADING_SPACE, ...rows.map((row) => row.space)]) : 0;
149
+ const nameWidth = columnWidth([HEADING_MIGRATION, ...rows.map((row) => row.migrationName)]);
150
+ const transitionWidth = columnWidth([HEADING_CHANGE, ...rows.map((row) => row.transition)]);
151
+ const opsWidth = columnWidth([HEADING_OPS, ...rows.map((row) => row.ops)]);
152
+
153
+ const headingParts = [padTextCell(HEADING_APPLIED_AT, appliedAtWidth)];
154
+ if (showSpace) {
155
+ headingParts.push(padTextCell(HEADING_SPACE, spaceWidth));
156
+ }
157
+ headingParts.push(
158
+ padTextCell(HEADING_MIGRATION, nameWidth),
159
+ padTextCell(HEADING_CHANGE, transitionWidth),
160
+ padOpsCell(HEADING_OPS, opsWidth),
161
+ );
162
+ const heading = headingParts.join(COLUMN_SEPARATOR);
163
+
164
+ const dividerParts = [padDividerCell(appliedAtWidth, dividerChar)];
165
+ if (showSpace) {
166
+ dividerParts.push(padDividerCell(spaceWidth, dividerChar));
167
+ }
168
+ dividerParts.push(
169
+ padDividerCell(nameWidth, dividerChar),
170
+ padDividerCell(transitionWidth, dividerChar),
171
+ padDividerCell(opsWidth, dividerChar),
172
+ );
173
+ const divider = dividerParts.map((cell) => styler.summary(cell)).join(COLUMN_SEPARATOR);
174
+
175
+ const dataRows = rows.map((row) => {
176
+ const parts = [padTextCell(row.appliedAt, appliedAtWidth)];
177
+ if (showSpace) {
178
+ parts.push(padTextCell(row.space, spaceWidth));
179
+ }
180
+ parts.push(
181
+ padTextCell(styler.dirName(row.migrationName), nameWidth),
182
+ padTextCell(styleHashTransition(row.from, row.to, styler, glyphMode), transitionWidth),
183
+ padOpsCell(row.ops, opsWidth),
184
+ );
185
+ return parts.join(COLUMN_SEPARATOR);
186
+ });
187
+
188
+ return [heading, divider, ...dataRows].join('\n');
189
+ }
190
+
191
+ export function serializeLedgerEntriesForJson(
192
+ entries: readonly LedgerEntryRecord[],
193
+ ): SerializedLedgerEntryRecord[] {
194
+ return sortLedgerEntries(entries).map((entry) => ({
195
+ space: entry.space,
196
+ name: entry.migrationName,
197
+ hash: entry.migrationHash,
198
+ fromContract: entry.from,
199
+ toContract: entry.to,
200
+ appliedAt: formatLedgerAppliedAt(entry.appliedAt, 'iso'),
201
+ operationCount: entry.operationCount,
202
+ }));
203
+ }
204
+
205
+ export const MIGRATION_LOG_EMPTY_MESSAGE = 'No migrations have been applied to this database.';
@@ -1,4 +1,7 @@
1
- import type { OperationPreview } from '@prisma-next/framework-components/control';
1
+ import type {
2
+ MigrationPlannerConflict,
3
+ OperationPreview,
4
+ } from '@prisma-next/framework-components/control';
2
5
  import { cyan, green, yellow } from 'colorette';
3
6
 
4
7
  import type { PerSpaceExecutionEntry } from '../../control-api/types';
@@ -85,11 +88,24 @@ export interface MigrationCommandResult {
85
88
  readonly advancedRef?: { readonly name: string; readonly hash: string } | null;
86
89
  readonly plannedAdvanceRef?: { readonly name: string; readonly hash: string } | null;
87
90
  readonly summary: string;
91
+ readonly warnings?: readonly MigrationPlannerConflict[];
88
92
  readonly timings: {
89
93
  readonly total: number;
90
94
  };
91
95
  }
92
96
 
97
+ export function formatPlannerWarningsBlock(
98
+ warnings: readonly MigrationPlannerConflict[],
99
+ useColor: boolean,
100
+ ): readonly string[] {
101
+ const formatDimText = (text: string) => formatDim(useColor, text);
102
+ const lines: string[] = ['', 'Warnings:'];
103
+ for (const warning of warnings) {
104
+ lines.push(` ${formatDimText(`- ${warning.summary}`)}`);
105
+ }
106
+ return lines;
107
+ }
108
+
93
109
  /**
94
110
  * Render the shared per-space execution block consumed by the `db init`
95
111
  * / `db update` / `migrate` summaries. Always shows: space
@@ -165,6 +181,10 @@ export function formatMigrationPlanOutput(
165
181
  lines.push(`${formatGreen('✔')} Planned ${operationCount} operation(s)`);
166
182
  }
167
183
 
184
+ if (result.warnings && result.warnings.length > 0) {
185
+ lines.push(...formatPlannerWarningsBlock(result.warnings, useColor));
186
+ }
187
+
168
188
  const formatYellow = createColorFormatter(useColor, yellow);
169
189
 
170
190
  // Per-space breakdown takes precedence over the flat ops tree when
@@ -315,11 +335,10 @@ export function formatMigrationApplyCommandOutput(
315
335
  }
316
336
 
317
337
  interface MigrationShowPresent {
318
- readonly dirName: string;
319
- readonly dirPath: string;
320
- readonly from: string | null;
321
- readonly to: string;
322
- readonly migrationHash: string;
338
+ readonly name: string;
339
+ readonly fromContract: string | null;
340
+ readonly toContract: string;
341
+ readonly hash: string;
323
342
  readonly createdAt: string;
324
343
  readonly operations: readonly {
325
344
  readonly id: string;
@@ -327,7 +346,6 @@ interface MigrationShowPresent {
327
346
  readonly operationClass: string;
328
347
  }[];
329
348
  readonly preview: OperationPreview;
330
- readonly summary: string;
331
349
  }
332
350
 
333
351
  interface MigrationShowResult {
@@ -340,10 +358,10 @@ function formatSpaceShowBlock(space: MigrationShowPresent, useColor: boolean): r
340
358
  const formatDimText = (text: string) => formatDim(useColor, text);
341
359
 
342
360
  const lines: string[] = [];
343
- lines.push(`${formatGreen('✔')} ${space.dirName}`);
344
- lines.push(`${formatDimText(` from: ${space.from ?? '(baseline)'}`)}`);
345
- lines.push(`${formatDimText(` to: ${space.to}`)}`);
346
- lines.push(`${formatDimText(` migrationHash: ${space.migrationHash}`)}`);
361
+ lines.push(`${formatGreen('✔')} ${space.name}`);
362
+ lines.push(`${formatDimText(` from: ${space.fromContract ?? '(baseline)'}`)}`);
363
+ lines.push(`${formatDimText(` to: ${space.toContract}`)}`);
364
+ lines.push(`${formatDimText(` hash: ${space.hash}`)}`);
347
365
  lines.push(`${formatDimText(` created: ${space.createdAt}`)}`);
348
366
 
349
367
  lines.push('');
@@ -427,6 +445,10 @@ export function formatMigrationApplyOutput(
427
445
  lines.push(`${formatGreen('✔')} Applied ${executed} operation(s)`);
428
446
  }
429
447
 
448
+ if (result.warnings && result.warnings.length > 0) {
449
+ lines.push(...formatPlannerWarningsBlock(result.warnings, useColor));
450
+ }
451
+
430
452
  // Per-space breakdown — replaces the single ambiguous `Signature:`
431
453
  // line with a per-space marker + ops listing.
432
454
  if (result.perSpace && result.perSpace.length > 0) {
@@ -180,3 +180,38 @@ export function parseGlobalFlags(options: CommonCommandOptions): GlobalFlags {
180
180
 
181
181
  return flags as GlobalFlags;
182
182
  }
183
+
184
+ /**
185
+ * Bridges the two TTY checks (stdout via `flags`, stdin via
186
+ * `process.stdin.isTTY`) into the `canPrompt` boolean the interactive
187
+ * `init` flow consumes.
188
+ *
189
+ * Per the [Style Guide § Interactivity](../../../../../../../docs/CLI%20Style%20Guide.md#interactivity):
190
+ *
191
+ * - `flags.interactive` governs *decoration* (TerminalUI, intro/outro,
192
+ * spinners) and is derived from stdout-TTY by `parseGlobalFlags`,
193
+ * honouring `--interactive` / `--no-interactive`.
194
+ * - Prompting additionally requires a stdin TTY — closing stdin is a
195
+ * common signal in CI / agent environments even when stdout stays
196
+ * attached.
197
+ * - `--interactive` is the explicit override: when the user passes it,
198
+ * we honour it (e.g. testing flows where stdin is stubbed).
199
+ *
200
+ * Single source of truth for the interactive-prompt decision: both the
201
+ * `init` action handler and the preAction telemetry bridge derive
202
+ * prompt-eligibility from this helper so they cannot drift. Lives in
203
+ * `global-flags` (alongside `parseGlobalFlags`) to keep
204
+ * `utils/telemetry` and `commands/init/index` free of an import cycle.
205
+ *
206
+ * Exported so callers and tests can derive the same value without
207
+ * touching `process` globals.
208
+ */
209
+ export function deriveCanPrompt(opts: {
210
+ readonly flagsInteractive: boolean | undefined;
211
+ readonly optionInteractive: boolean | undefined;
212
+ readonly stdinIsTTY: boolean;
213
+ }): boolean {
214
+ if (opts.optionInteractive === true) return true;
215
+ if (opts.flagsInteractive === false) return false;
216
+ return opts.stdinIsTTY;
217
+ }
@@ -1,12 +1,8 @@
1
1
  import type { IntegrityViolation } from '@prisma-next/migration-tools/aggregate';
2
2
  import { join, relative } from 'pathe';
3
+ import type { CheckFailure } from '../commands/json/schemas';
3
4
 
4
- export interface CheckFailure {
5
- readonly pnCode: string;
6
- readonly where: string;
7
- readonly why: string;
8
- readonly fix: string;
9
- }
5
+ export type { CheckFailure } from '../commands/json/schemas';
10
6
 
11
7
  function migrationPathRelative(dirPath: string): string {
12
8
  return relative(process.cwd(), dirPath);
@@ -34,7 +30,8 @@ export function integrityViolationToCheckFailure(
34
30
  switch (violation.kind) {
35
31
  case 'hashMismatch':
36
32
  return {
37
- pnCode: 'PN-MIG-CHECK-001',
33
+ space: violation.spaceId,
34
+ code: 'PN-MIG-CHECK-001',
38
35
  where: migrationFileRelative(
39
36
  join(migrationsDir, violation.spaceId, violation.dirName),
40
37
  'migration.json',
@@ -44,84 +41,96 @@ export function integrityViolationToCheckFailure(
44
41
  };
45
42
  case 'providedInvariantsMismatch':
46
43
  return {
47
- pnCode: 'PN-MIG-CHECK-002',
44
+ space: violation.spaceId,
45
+ code: 'PN-MIG-CHECK-002',
48
46
  where: packageRelative(violation.spaceId, violation.dirName),
49
47
  why: `Migration "${violation.dirName}" providedInvariants in migration.json disagrees with ops.json.`,
50
48
  fix: 'Re-emit the migration package so migration.json and ops.json agree.',
51
49
  };
52
50
  case 'packageUnloadable':
53
51
  return {
54
- pnCode: 'PN-MIG-CHECK-002',
52
+ space: violation.spaceId,
53
+ code: 'PN-MIG-CHECK-002',
55
54
  where: packageRelative(violation.spaceId, violation.dirName),
56
55
  why: `Migration "${violation.dirName}" could not be loaded: ${violation.detail}`,
57
56
  fix: 'Re-emit the migration package or restore from version control.',
58
57
  };
59
58
  case 'sameSourceAndTarget':
60
59
  return {
61
- pnCode: 'PN-MIG-CHECK-007',
60
+ space: violation.spaceId,
61
+ code: 'PN-MIG-CHECK-007',
62
62
  where: packageRelative(violation.spaceId, violation.dirName),
63
63
  why: `Migration "${violation.dirName}" in space "${violation.spaceId}" has source equal to target (${violation.hash}) with no data invariant — a true no-op self-edge.`,
64
64
  fix: 'Add a data operation if this self-edge was meant to carry a data invariant, or delete the migration if it is a true no-op.',
65
65
  };
66
66
  case 'orphanSpaceDir':
67
67
  return {
68
- pnCode: 'PN-MIG-CHECK-008',
68
+ space: violation.spaceId,
69
+ code: 'PN-MIG-CHECK-008',
69
70
  where: spaceRelative(violation.spaceId),
70
71
  why: `Contract-space directory "${violation.spaceId}" exists on disk but no extension declares it.`,
71
72
  fix: 'Remove the orphan directory, or declare the extension in `extensionPacks`.',
72
73
  };
73
74
  case 'declaredButUnmigrated':
74
75
  return {
75
- pnCode: 'PN-MIG-CHECK-009',
76
+ space: violation.spaceId,
77
+ code: 'PN-MIG-CHECK-009',
76
78
  where: spaceRelative(violation.spaceId),
77
79
  why: `Extension "${violation.spaceId}" is declared in \`extensionPacks\` but has no on-disk migrations directory.`,
78
80
  fix: 'Re-emit the extension contract-space artefacts with `prisma-next contract emit` and migration planning, or remove the extension from `extensionPacks` if it is unused.',
79
81
  };
80
82
  case 'headRefMissing':
81
83
  return {
82
- pnCode: 'PN-MIG-CHECK-010',
84
+ space: violation.spaceId,
85
+ code: 'PN-MIG-CHECK-010',
83
86
  where: refRelative(violation.spaceId, 'head'),
84
87
  why: `Head ref \`refs/head.json\` is missing for contract space "${violation.spaceId}".`,
85
88
  fix: 'Re-emit the contract-space migrations and head ref artefacts, or restore `refs/head.json` from version control.',
86
89
  };
87
90
  case 'headRefNotInGraph':
88
91
  return {
89
- pnCode: 'PN-MIG-CHECK-011',
92
+ space: violation.spaceId,
93
+ code: 'PN-MIG-CHECK-011',
90
94
  where: refRelative(violation.spaceId, 'head'),
91
95
  why: `Head ref ${violation.hash} for contract space "${violation.spaceId}" is not present in its migration graph.`,
92
96
  fix: 'Re-emit the contract space migrations, or restore the missing migration package.',
93
97
  };
94
98
  case 'refUnreadable':
95
99
  return {
96
- pnCode: 'PN-MIG-CHECK-012',
100
+ space: violation.spaceId,
101
+ code: 'PN-MIG-CHECK-012',
97
102
  where: refRelative(violation.spaceId, violation.refName),
98
103
  why: `Ref "${violation.refName}" for contract space "${violation.spaceId}" is unreadable: ${violation.detail}`,
99
104
  fix: 'Repair or remove the corrupt ref file.',
100
105
  };
101
106
  case 'targetMismatch':
102
107
  return {
103
- pnCode: 'PN-MIG-CHECK-013',
108
+ space: violation.spaceId,
109
+ code: 'PN-MIG-CHECK-013',
104
110
  where: spaceRelative(violation.spaceId),
105
111
  why: `Contract space "${violation.spaceId}" targets "${violation.actual}" but the project targets "${violation.expected}".`,
106
112
  fix: 'Update the extension to target the configured database, or change the project target.',
107
113
  };
108
114
  case 'disjointness':
109
115
  return {
110
- pnCode: 'PN-MIG-CHECK-014',
116
+ space: 'app',
117
+ code: 'PN-MIG-CHECK-014',
111
118
  where: migrationPathRelative(migrationsDir),
112
119
  why: `Storage element "${violation.element}" is claimed by multiple contract spaces: ${violation.claimedBy.join(', ')}.`,
113
120
  fix: 'Update the contracts so each storage element is owned by exactly one contract space.',
114
121
  };
115
122
  case 'contractUnreadable':
116
123
  return {
117
- pnCode: 'PN-MIG-CHECK-015',
124
+ space: violation.spaceId,
125
+ code: 'PN-MIG-CHECK-015',
118
126
  where: migrationFileRelative(join(migrationsDir, violation.spaceId), 'contract.json'),
119
127
  why: `Contract for space "${violation.spaceId}" is unreadable: ${violation.detail}`,
120
128
  fix: 'Re-emit the extension contract artefacts, or fix the descriptor producing the invalid contract.',
121
129
  };
122
130
  case 'duplicateMigrationHash':
123
131
  return {
124
- pnCode: 'PN-MIG-CHECK-016',
132
+ space: violation.spaceId,
133
+ code: 'PN-MIG-CHECK-016',
125
134
  where: spaceRelative(violation.spaceId),
126
135
  why: `Multiple migrations in space "${violation.spaceId}" share migrationHash "${violation.migrationHash}" (${violation.dirNames.join(', ')}).`,
127
136
  fix: 'Re-emit one of the conflicting packages so each migrationHash is unique.',
@@ -0,0 +1,38 @@
1
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
2
+ import { type CliStructuredError, errorLegendHumanOnly } from './cli-errors';
3
+ import type { GlobalFlags } from './global-flags';
4
+
5
+ export interface LegendCliOptions {
6
+ readonly legend?: boolean;
7
+ readonly dot?: boolean;
8
+ }
9
+
10
+ /**
11
+ * The legend is decoration printed alongside the command header on stderr, so
12
+ * it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
13
+ * `--quiet`) exactly as the header is.
14
+ */
15
+ export function shouldShowLegend(options: LegendCliOptions, flags: GlobalFlags): boolean {
16
+ return (
17
+ options.legend === true && options.dot !== true && flags.json !== true && flags.quiet !== true
18
+ );
19
+ }
20
+
21
+ export function validateLegendOptions(
22
+ options: LegendCliOptions,
23
+ flags: GlobalFlags,
24
+ ): Result<void, CliStructuredError> {
25
+ if (options.legend !== true) {
26
+ return ok(undefined);
27
+ }
28
+ if (flags.json === true) {
29
+ return notOk(errorLegendHumanOnly('--json'));
30
+ }
31
+ if (flags.quiet === true) {
32
+ return notOk(errorLegendHumanOnly('--quiet'));
33
+ }
34
+ if (options.dot === true) {
35
+ return notOk(errorLegendHumanOnly('--dot'));
36
+ }
37
+ return ok(undefined);
38
+ }
@@ -0,0 +1,60 @@
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
+ /**
34
+ * Resolve a filesystem-path target to the migration dir that contains it,
35
+ * searching each in-scope space's `migrationsDir`. A path is explicit, so
36
+ * it can belong to at most one space — returns the first match, or `null`
37
+ * when the path falls outside every space dir.
38
+ */
39
+ export function resolveTargetPathAcrossSpaces(
40
+ target: string,
41
+ spaces: ReadonlyArray<{ readonly migrationsDir: string }>,
42
+ ): string | null {
43
+ const targetPath = resolve(target);
44
+ for (const space of spaces) {
45
+ const rel = relative(space.migrationsDir, targetPath);
46
+ const isOutside = rel === '' || rel === '.' || rel.startsWith('..') || isAbsolute(rel);
47
+ if (!isOutside) {
48
+ return targetPath;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ export function findPackageByDirPath(
55
+ packages: readonly OnDiskMigrationPackage[],
56
+ resolvedDirPath: string,
57
+ ): OnDiskMigrationPackage | undefined {
58
+ const normalized = resolve(resolvedDirPath);
59
+ return packages.find((p) => resolve(p.dirPath) === normalized);
60
+ }