@prisma-next/cli 0.3.0-pr.99.6 → 0.3.0

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 (257) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +381 -128
  3. package/dist/agent-skill-mongo.md +106 -0
  4. package/dist/agent-skill-postgres.md +106 -0
  5. package/dist/cli-errors-BDCYR5ap.mjs +4 -0
  6. package/dist/cli-errors-DStABy9d.d.mts +3 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.js +1 -2910
  9. package/dist/cli.mjs +261 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/client-DiUkJAeN.mjs +987 -0
  12. package/dist/client-DiUkJAeN.mjs.map +1 -0
  13. package/dist/commands/contract-emit.d.mts +7 -0
  14. package/dist/commands/contract-emit.d.mts.map +1 -0
  15. package/dist/commands/contract-emit.mjs +9 -0
  16. package/dist/commands/contract-infer.d.mts +7 -0
  17. package/dist/commands/contract-infer.d.mts.map +1 -0
  18. package/dist/commands/contract-infer.mjs +10 -0
  19. package/dist/commands/db-init.d.mts +7 -0
  20. package/dist/commands/db-init.d.mts.map +1 -0
  21. package/dist/commands/db-init.mjs +126 -0
  22. package/dist/commands/db-init.mjs.map +1 -0
  23. package/dist/commands/db-schema.d.mts +7 -0
  24. package/dist/commands/db-schema.d.mts.map +1 -0
  25. package/dist/commands/db-schema.mjs +56 -0
  26. package/dist/commands/db-schema.mjs.map +1 -0
  27. package/dist/commands/db-sign.d.mts +7 -0
  28. package/dist/commands/db-sign.d.mts.map +1 -0
  29. package/dist/commands/db-sign.mjs +137 -0
  30. package/dist/commands/db-sign.mjs.map +1 -0
  31. package/dist/commands/db-update.d.mts +7 -0
  32. package/dist/commands/db-update.d.mts.map +1 -0
  33. package/dist/commands/db-update.mjs +123 -0
  34. package/dist/commands/db-update.mjs.map +1 -0
  35. package/dist/commands/db-verify.d.mts +7 -0
  36. package/dist/commands/db-verify.d.mts.map +1 -0
  37. package/dist/commands/db-verify.mjs +323 -0
  38. package/dist/commands/db-verify.mjs.map +1 -0
  39. package/dist/commands/migration-apply.d.mts +36 -0
  40. package/dist/commands/migration-apply.d.mts.map +1 -0
  41. package/dist/commands/migration-apply.mjs +245 -0
  42. package/dist/commands/migration-apply.mjs.map +1 -0
  43. package/dist/commands/migration-new.d.mts +8 -0
  44. package/dist/commands/migration-new.d.mts.map +1 -0
  45. package/dist/commands/migration-new.mjs +152 -0
  46. package/dist/commands/migration-new.mjs.map +1 -0
  47. package/dist/commands/migration-plan.d.mts +47 -0
  48. package/dist/commands/migration-plan.d.mts.map +1 -0
  49. package/dist/commands/migration-plan.mjs +313 -0
  50. package/dist/commands/migration-plan.mjs.map +1 -0
  51. package/dist/commands/migration-ref.d.mts +43 -0
  52. package/dist/commands/migration-ref.d.mts.map +1 -0
  53. package/dist/commands/migration-ref.mjs +195 -0
  54. package/dist/commands/migration-ref.mjs.map +1 -0
  55. package/dist/commands/migration-show.d.mts +28 -0
  56. package/dist/commands/migration-show.d.mts.map +1 -0
  57. package/dist/commands/migration-show.mjs +140 -0
  58. package/dist/commands/migration-show.mjs.map +1 -0
  59. package/dist/commands/migration-status.d.mts +86 -0
  60. package/dist/commands/migration-status.d.mts.map +1 -0
  61. package/dist/commands/migration-status.mjs +9 -0
  62. package/dist/commands/migration-verify.d.mts +16 -0
  63. package/dist/commands/migration-verify.d.mts.map +1 -0
  64. package/dist/commands/migration-verify.mjs +110 -0
  65. package/dist/commands/migration-verify.mjs.map +1 -0
  66. package/dist/config-loader-C4VXKl8f.mjs +43 -0
  67. package/dist/config-loader-C4VXKl8f.mjs.map +1 -0
  68. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  69. package/dist/config-loader.d.mts.map +1 -0
  70. package/dist/config-loader.mjs +3 -0
  71. package/dist/contract-emit-D2wDXfyo.mjs +191 -0
  72. package/dist/contract-emit-D2wDXfyo.mjs.map +1 -0
  73. package/dist/contract-emit-Zm_sd1wQ.mjs +112 -0
  74. package/dist/contract-emit-Zm_sd1wQ.mjs.map +1 -0
  75. package/dist/contract-emit-kN-IkKTE.mjs +6 -0
  76. package/dist/contract-enrichment-CGW6mm-E.mjs +79 -0
  77. package/dist/contract-enrichment-CGW6mm-E.mjs.map +1 -0
  78. package/dist/contract-infer-DozZT511.mjs +90 -0
  79. package/dist/contract-infer-DozZT511.mjs.map +1 -0
  80. package/dist/exports/config-types.d.mts +2 -0
  81. package/dist/exports/config-types.mjs +3 -0
  82. package/dist/exports/control-api.d.mts +624 -0
  83. package/dist/exports/control-api.d.mts.map +1 -0
  84. package/dist/exports/control-api.mjs +8 -0
  85. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +12 -7
  86. package/dist/exports/index.d.mts.map +1 -0
  87. package/dist/exports/index.mjs +142 -0
  88. package/dist/exports/index.mjs.map +1 -0
  89. package/dist/extract-operation-statements-DZUJNmL3.mjs +13 -0
  90. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +1 -0
  91. package/dist/extract-sql-ddl-DDMX-9mz.mjs +26 -0
  92. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +1 -0
  93. package/dist/framework-components-BAsliT4V.mjs +59 -0
  94. package/dist/framework-components-BAsliT4V.mjs.map +1 -0
  95. package/dist/init-6Pvm_esG.mjs +430 -0
  96. package/dist/init-6Pvm_esG.mjs.map +1 -0
  97. package/dist/inspect-live-schema-BYnhztxZ.mjs +91 -0
  98. package/dist/inspect-live-schema-BYnhztxZ.mjs.map +1 -0
  99. package/dist/migration-command-scaffold-CntCcntR.mjs +105 -0
  100. package/dist/migration-command-scaffold-CntCcntR.mjs.map +1 -0
  101. package/dist/migration-status-CJANY4yr.mjs +1583 -0
  102. package/dist/migration-status-CJANY4yr.mjs.map +1 -0
  103. package/dist/migrations-DTZBYXm1.mjs +173 -0
  104. package/dist/migrations-DTZBYXm1.mjs.map +1 -0
  105. package/dist/progress-adapter-B-YvmcDu.mjs +43 -0
  106. package/dist/progress-adapter-B-YvmcDu.mjs.map +1 -0
  107. package/dist/quick-reference-mongo.md +93 -0
  108. package/dist/quick-reference-postgres.md +91 -0
  109. package/dist/result-handler-oK_vA-Fn.mjs +697 -0
  110. package/dist/result-handler-oK_vA-Fn.mjs.map +1 -0
  111. package/dist/terminal-ui-C5k88MmW.mjs +274 -0
  112. package/dist/terminal-ui-C5k88MmW.mjs.map +1 -0
  113. package/dist/validate-contract-deps-esa-VQ0h.mjs +37 -0
  114. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +1 -0
  115. package/dist/verify-DlFQ2FOw.mjs +385 -0
  116. package/dist/verify-DlFQ2FOw.mjs.map +1 -0
  117. package/package.json +87 -40
  118. package/src/cli.ts +118 -58
  119. package/src/commands/contract-emit.ts +101 -78
  120. package/src/commands/contract-infer-paths.ts +32 -0
  121. package/src/commands/contract-infer.ts +143 -0
  122. package/src/commands/db-init.ts +97 -219
  123. package/src/commands/db-schema.ts +77 -0
  124. package/src/commands/db-sign.ts +46 -73
  125. package/src/commands/db-update.ts +236 -0
  126. package/src/commands/db-verify.ts +409 -119
  127. package/src/commands/init/detect-package-manager.ts +47 -0
  128. package/src/commands/init/index.ts +21 -0
  129. package/src/commands/init/init.ts +203 -0
  130. package/src/commands/init/templates/agent-skill-mongo.md +106 -0
  131. package/src/commands/init/templates/agent-skill-postgres.md +106 -0
  132. package/src/commands/init/templates/agent-skill.ts +19 -0
  133. package/src/commands/init/templates/code-templates.ts +168 -0
  134. package/src/commands/init/templates/quick-reference-mongo.md +93 -0
  135. package/src/commands/init/templates/quick-reference-postgres.md +91 -0
  136. package/src/commands/init/templates/quick-reference.ts +19 -0
  137. package/src/commands/init/templates/render.ts +20 -0
  138. package/src/commands/init/templates/tsconfig.ts +35 -0
  139. package/src/commands/inspect-live-schema.ts +170 -0
  140. package/src/commands/migration-apply.ts +427 -0
  141. package/src/commands/migration-new.ts +260 -0
  142. package/src/commands/migration-plan.ts +519 -0
  143. package/src/commands/migration-ref.ts +305 -0
  144. package/src/commands/migration-show.ts +246 -0
  145. package/src/commands/migration-status.ts +864 -0
  146. package/src/commands/migration-verify.ts +180 -0
  147. package/src/config-loader.ts +13 -3
  148. package/src/control-api/client.ts +205 -183
  149. package/src/control-api/contract-enrichment.ts +119 -0
  150. package/src/control-api/errors.ts +9 -0
  151. package/src/control-api/operations/contract-emit.ts +181 -0
  152. package/src/control-api/operations/db-init.ts +53 -49
  153. package/src/control-api/operations/db-update.ts +220 -0
  154. package/src/control-api/operations/extract-operation-statements.ts +14 -0
  155. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  156. package/src/control-api/operations/migration-apply.ts +191 -0
  157. package/src/control-api/operations/migration-helpers.ts +49 -0
  158. package/src/control-api/types.ts +274 -52
  159. package/src/exports/config-types.ts +4 -3
  160. package/src/exports/control-api.ts +15 -5
  161. package/src/load-ts-contract.ts +30 -19
  162. package/src/utils/cli-errors.ts +14 -8
  163. package/src/utils/command-helpers.ts +302 -3
  164. package/src/utils/formatters/emit.ts +67 -0
  165. package/src/utils/formatters/errors.ts +82 -0
  166. package/src/utils/formatters/graph-migration-mapper.ts +240 -0
  167. package/src/utils/formatters/graph-render.ts +1323 -0
  168. package/src/utils/formatters/graph-types.ts +120 -0
  169. package/src/utils/formatters/help.ts +380 -0
  170. package/src/utils/formatters/helpers.ts +28 -0
  171. package/src/utils/formatters/migrations.ts +346 -0
  172. package/src/utils/formatters/styled.ts +212 -0
  173. package/src/utils/formatters/verify.ts +621 -0
  174. package/src/utils/framework-components.ts +13 -10
  175. package/src/utils/global-flags.ts +41 -23
  176. package/src/utils/migration-command-scaffold.ts +184 -0
  177. package/src/utils/migration-types.ts +12 -0
  178. package/src/utils/progress-adapter.ts +18 -29
  179. package/src/utils/result-handler.ts +12 -13
  180. package/src/utils/shutdown.ts +92 -0
  181. package/src/utils/suggest-command.ts +31 -0
  182. package/src/utils/terminal-ui.ts +276 -0
  183. package/src/utils/validate-contract-deps.ts +49 -0
  184. package/dist/chunk-AGOTG4L3.js +0 -965
  185. package/dist/chunk-AGOTG4L3.js.map +0 -1
  186. package/dist/chunk-HLLI4YL7.js +0 -180
  187. package/dist/chunk-HLLI4YL7.js.map +0 -1
  188. package/dist/chunk-HWYQOCAJ.js +0 -47
  189. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  190. package/dist/chunk-VG2R7DGF.js +0 -735
  191. package/dist/chunk-VG2R7DGF.js.map +0 -1
  192. package/dist/cli.d.ts +0 -2
  193. package/dist/cli.d.ts.map +0 -1
  194. package/dist/cli.js.map +0 -1
  195. package/dist/commands/contract-emit.d.ts +0 -3
  196. package/dist/commands/contract-emit.d.ts.map +0 -1
  197. package/dist/commands/contract-emit.js +0 -10
  198. package/dist/commands/contract-emit.js.map +0 -1
  199. package/dist/commands/db-init.d.ts +0 -3
  200. package/dist/commands/db-init.d.ts.map +0 -1
  201. package/dist/commands/db-init.js +0 -257
  202. package/dist/commands/db-init.js.map +0 -1
  203. package/dist/commands/db-introspect.d.ts +0 -3
  204. package/dist/commands/db-introspect.d.ts.map +0 -1
  205. package/dist/commands/db-introspect.js +0 -155
  206. package/dist/commands/db-introspect.js.map +0 -1
  207. package/dist/commands/db-schema-verify.d.ts +0 -3
  208. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  209. package/dist/commands/db-schema-verify.js +0 -171
  210. package/dist/commands/db-schema-verify.js.map +0 -1
  211. package/dist/commands/db-sign.d.ts +0 -3
  212. package/dist/commands/db-sign.d.ts.map +0 -1
  213. package/dist/commands/db-sign.js +0 -195
  214. package/dist/commands/db-sign.js.map +0 -1
  215. package/dist/commands/db-verify.d.ts +0 -3
  216. package/dist/commands/db-verify.d.ts.map +0 -1
  217. package/dist/commands/db-verify.js +0 -193
  218. package/dist/commands/db-verify.js.map +0 -1
  219. package/dist/config-loader.d.ts.map +0 -1
  220. package/dist/config-loader.js +0 -7
  221. package/dist/config-loader.js.map +0 -1
  222. package/dist/control-api/client.d.ts +0 -13
  223. package/dist/control-api/client.d.ts.map +0 -1
  224. package/dist/control-api/operations/db-init.d.ts +0 -29
  225. package/dist/control-api/operations/db-init.d.ts.map +0 -1
  226. package/dist/control-api/types.d.ts +0 -387
  227. package/dist/control-api/types.d.ts.map +0 -1
  228. package/dist/exports/config-types.d.ts +0 -3
  229. package/dist/exports/config-types.d.ts.map +0 -1
  230. package/dist/exports/config-types.js +0 -6
  231. package/dist/exports/config-types.js.map +0 -1
  232. package/dist/exports/control-api.d.ts +0 -13
  233. package/dist/exports/control-api.d.ts.map +0 -1
  234. package/dist/exports/control-api.js +0 -7
  235. package/dist/exports/control-api.js.map +0 -1
  236. package/dist/exports/index.d.ts +0 -4
  237. package/dist/exports/index.d.ts.map +0 -1
  238. package/dist/exports/index.js +0 -176
  239. package/dist/exports/index.js.map +0 -1
  240. package/dist/load-ts-contract.d.ts.map +0 -1
  241. package/dist/utils/cli-errors.d.ts +0 -7
  242. package/dist/utils/cli-errors.d.ts.map +0 -1
  243. package/dist/utils/command-helpers.d.ts +0 -12
  244. package/dist/utils/command-helpers.d.ts.map +0 -1
  245. package/dist/utils/framework-components.d.ts +0 -70
  246. package/dist/utils/framework-components.d.ts.map +0 -1
  247. package/dist/utils/global-flags.d.ts +0 -25
  248. package/dist/utils/global-flags.d.ts.map +0 -1
  249. package/dist/utils/output.d.ts +0 -142
  250. package/dist/utils/output.d.ts.map +0 -1
  251. package/dist/utils/progress-adapter.d.ts +0 -26
  252. package/dist/utils/progress-adapter.d.ts.map +0 -1
  253. package/dist/utils/result-handler.d.ts +0 -15
  254. package/dist/utils/result-handler.d.ts.map +0 -1
  255. package/src/commands/db-introspect.ts +0 -227
  256. package/src/commands/db-schema-verify.ts +0 -238
  257. package/src/utils/output.ts +0 -1471
@@ -0,0 +1,864 @@
1
+ import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
+ import {
4
+ findPath,
5
+ findPathWithDecision,
6
+ findReachableLeaves,
7
+ } from '@prisma-next/migration-tools/dag';
8
+ import type { Refs } from '@prisma-next/migration-tools/refs';
9
+ import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
10
+ import type {
11
+ AttestedMigrationBundle,
12
+ DraftMigrationBundle,
13
+ MigrationChainEntry,
14
+ MigrationGraph,
15
+ } from '@prisma-next/migration-tools/types';
16
+ import { MigrationToolsError } from '@prisma-next/migration-tools/types';
17
+ import { ifDefined } from '@prisma-next/utils/defined';
18
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
19
+ import { cyan, dim, magenta, yellow } from 'colorette';
20
+ import { Command } from 'commander';
21
+
22
+ import { loadConfig } from '../config-loader';
23
+ import { createControlClient } from '../control-api/client';
24
+ import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
25
+ import {
26
+ addGlobalOptions,
27
+ loadAllBundles,
28
+ maskConnectionUrl,
29
+ readContractEnvelope,
30
+ resolveMigrationPaths,
31
+ setCommandDescriptions,
32
+ setCommandExamples,
33
+ toPathDecisionResult,
34
+ } from '../utils/command-helpers';
35
+ import {
36
+ type EdgeStatus,
37
+ type EdgeStatusKind,
38
+ migrationGraphToRenderInput,
39
+ } from '../utils/formatters/graph-migration-mapper';
40
+ import {
41
+ extractRelevantSubgraph,
42
+ graphRenderer,
43
+ isLinearGraph,
44
+ } from '../utils/formatters/graph-render';
45
+ import { formatStyledHeader } from '../utils/formatters/styled';
46
+ import type { CommonCommandOptions } from '../utils/global-flags';
47
+ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
48
+ import type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
49
+ import { handleResult } from '../utils/result-handler';
50
+ import { TerminalUI } from '../utils/terminal-ui';
51
+
52
+ interface MigrationStatusOptions extends CommonCommandOptions {
53
+ readonly db?: string;
54
+ readonly config?: string;
55
+ readonly ref?: string;
56
+ readonly graph?: boolean;
57
+ readonly limit?: string;
58
+ readonly all?: boolean;
59
+ }
60
+
61
+ export interface MigrationStatusEntry {
62
+ readonly dirName: string;
63
+ readonly from: string;
64
+ readonly to: string;
65
+ readonly migrationId: string | null;
66
+ readonly operationCount: number;
67
+ readonly operationSummary: string;
68
+ readonly hasDestructive: boolean;
69
+ readonly status: EdgeStatusKind | 'unknown';
70
+ }
71
+
72
+ export type { StatusDiagnostic, StatusRef } from '../utils/migration-types';
73
+
74
+ export interface MigrationStatusResult {
75
+ readonly ok: true;
76
+ readonly mode: 'online' | 'offline';
77
+ readonly migrations: readonly MigrationStatusEntry[];
78
+ readonly markerHash?: string;
79
+ readonly targetHash: string;
80
+ readonly contractHash: string;
81
+ readonly refs?: readonly StatusRef[];
82
+ readonly pathDecision?: {
83
+ readonly fromHash: string;
84
+ readonly toHash: string;
85
+ readonly alternativeCount: number;
86
+ readonly tieBreakReasons: readonly string[];
87
+ readonly refName?: string;
88
+ readonly selectedPath: readonly {
89
+ readonly dirName: string;
90
+ readonly migrationId: string | null;
91
+ readonly from: string;
92
+ readonly to: string;
93
+ }[];
94
+ };
95
+ readonly summary: string;
96
+ readonly diagnostics: readonly StatusDiagnostic[];
97
+ readonly graph?: MigrationGraph;
98
+ readonly bundles?: readonly AttestedMigrationBundle[];
99
+ readonly drafts?: readonly DraftMigrationBundle[];
100
+ readonly edgeStatuses?: readonly EdgeStatus[];
101
+ readonly activeRefHash?: string;
102
+ readonly activeRefName?: string;
103
+ readonly diverged?: boolean;
104
+ }
105
+
106
+ function summarizeOps(ops: readonly MigrationPlanOperation[]): {
107
+ summary: string;
108
+ hasDestructive: boolean;
109
+ } {
110
+ if (ops.length === 0) return { summary: '0 ops', hasDestructive: false };
111
+
112
+ const classes = new Map<string, number>();
113
+ for (const op of ops) {
114
+ classes.set(op.operationClass, (classes.get(op.operationClass) ?? 0) + 1);
115
+ }
116
+
117
+ const hasDestructive = classes.has('destructive');
118
+ const count = ops.length;
119
+ const noun = count === 1 ? 'op' : 'ops';
120
+
121
+ if (classes.size === 1) {
122
+ const cls = [...classes.keys()][0]!;
123
+ return { summary: `${count} ${noun} (all ${cls})`, hasDestructive };
124
+ }
125
+
126
+ const destructiveCount = classes.get('destructive');
127
+ if (destructiveCount) {
128
+ return { summary: `${count} ${noun} (${destructiveCount} destructive)`, hasDestructive };
129
+ }
130
+
131
+ const parts = [...classes.entries()].map(([cls, n]) => `${n} ${cls}`);
132
+ return { summary: `${count} ${noun} (${parts.join(', ')})`, hasDestructive };
133
+ }
134
+
135
+ /**
136
+ * Derive per-edge status across the full graph using path analysis.
137
+ *
138
+ * - **applied**: edge is on the path from root to the DB marker
139
+ * - **pending**: edge is on the path from the DB marker to the target
140
+ * (and the marker is reachable from root, i.e. it's on the same branch)
141
+ * - **unreachable**: edge is on the path from root to the target but the DB
142
+ * marker is on a different branch — `apply` can't reach these edges
143
+ * without the DB first moving to this branch
144
+ *
145
+ * Returns statuses only for edges that have a known status (skips offline
146
+ * and edges not on any relevant path).
147
+ *
148
+ * @internal Exported for testing only.
149
+ */
150
+ export function deriveEdgeStatuses(
151
+ graph: MigrationGraph,
152
+ targetHash: string,
153
+ contractHash: string,
154
+ markerHash: string | undefined,
155
+ mode: 'online' | 'offline',
156
+ ): EdgeStatus[] {
157
+ if (mode === 'offline') return [];
158
+
159
+ const edgeKey = (e: MigrationChainEntry) => `${e.from}\0${e.to}`;
160
+
161
+ // No marker = empty DB — treat root as the marker (nothing applied, everything pending)
162
+ const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
163
+
164
+ const appliedPath =
165
+ markerHash !== undefined ? findPath(graph, EMPTY_CONTRACT_HASH, markerHash) : null;
166
+
167
+ const pendingPath = findPath(graph, effectiveMarker, targetHash);
168
+ const targetPath = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
169
+
170
+ const statuses: EdgeStatus[] = [];
171
+ const assignedKeys = new Set<string>();
172
+
173
+ // Applied edges (root → marker)
174
+ if (appliedPath) {
175
+ for (const e of appliedPath) {
176
+ assignedKeys.add(edgeKey(e));
177
+ statuses.push({ dirName: e.dirName, status: 'applied' });
178
+ }
179
+ }
180
+
181
+ // Pending edges (marker → target)
182
+ if (pendingPath) {
183
+ for (const e of pendingPath) {
184
+ assignedKeys.add(edgeKey(e));
185
+ statuses.push({ dirName: e.dirName, status: 'pending' });
186
+ }
187
+ }
188
+
189
+ // Pending edges beyond the target: target → contract (when target is a ref
190
+ // and the contract is reachable from it)
191
+ if (
192
+ contractHash !== EMPTY_CONTRACT_HASH &&
193
+ contractHash !== targetHash &&
194
+ graph.nodes.has(contractHash)
195
+ ) {
196
+ const beyondTarget = findPath(graph, targetHash, contractHash);
197
+ if (beyondTarget) {
198
+ for (const e of beyondTarget) {
199
+ if (!assignedKeys.has(edgeKey(e))) {
200
+ assignedKeys.add(edgeKey(e));
201
+ statuses.push({ dirName: e.dirName, status: 'pending' });
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Unreachable edges: on the path from root to the target but neither applied
208
+ // nor pending. This covers two cases:
209
+ // 1. Marker can't reach target at all (different branch entirely)
210
+ // 2. Marker reaches target via a different route, leaving some root→target
211
+ // edges orphaned (e.g. a fork where one branch was applied and apply
212
+ // will continue through the other)
213
+ if (targetPath) {
214
+ for (const e of targetPath) {
215
+ if (!assignedKeys.has(edgeKey(e))) {
216
+ statuses.push({ dirName: e.dirName, status: 'unreachable' });
217
+ }
218
+ }
219
+ }
220
+
221
+ return statuses;
222
+ }
223
+
224
+ /**
225
+ * @param mode — 'online' if we connected to the database, 'offline' otherwise
226
+ * @param markerHash — the marker hash from the database, or undefined if no marker row / offline
227
+ */
228
+ function buildMigrationEntries(
229
+ chain: readonly MigrationChainEntry[],
230
+ packages: readonly AttestedMigrationBundle[],
231
+ mode: 'online' | 'offline',
232
+ markerHash: string | undefined,
233
+ edgeStatuses?: readonly EdgeStatus[],
234
+ ): MigrationStatusEntry[] {
235
+ const pkgByDirName = new Map(packages.map((p) => [p.dirName, p]));
236
+ const statusByDirName = edgeStatuses
237
+ ? new Map(edgeStatuses.map((e) => [e.dirName, e.status]))
238
+ : undefined;
239
+
240
+ const markerInChain = markerHash === undefined || chain.some((e) => e.to === markerHash);
241
+
242
+ const entries: MigrationStatusEntry[] = [];
243
+ let reachedMarker = mode === 'online' && markerHash === undefined;
244
+
245
+ for (const migration of chain) {
246
+ const pkg = pkgByDirName.get(migration.dirName);
247
+ const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
248
+ const { summary, hasDestructive } = summarizeOps(ops);
249
+
250
+ let status: EdgeStatusKind | 'unknown';
251
+ const edgeStatus = statusByDirName?.get(migration.dirName);
252
+ if (edgeStatus) {
253
+ status = edgeStatus;
254
+ } else if (mode === 'offline' || !markerInChain) {
255
+ status = 'unknown';
256
+ } else if (reachedMarker) {
257
+ status = 'pending';
258
+ } else {
259
+ status = 'applied';
260
+ }
261
+
262
+ entries.push({
263
+ dirName: migration.dirName,
264
+ from: migration.from,
265
+ to: migration.to,
266
+ migrationId: migration.migrationId,
267
+ operationCount: ops.length,
268
+ operationSummary: summary,
269
+ hasDestructive,
270
+ status,
271
+ });
272
+
273
+ if (!reachedMarker && migration.to === markerHash) {
274
+ reachedMarker = true;
275
+ }
276
+ }
277
+
278
+ return entries;
279
+ }
280
+
281
+ /**
282
+ * Resolve the migration chain to display in status output.
283
+ *
284
+ * When offline or the marker is at EMPTY, the chain is simply the shortest
285
+ * path from EMPTY to the target — all structural paths are equivalent per
286
+ * the spec, so the deterministic shortest path is the canonical display.
287
+ *
288
+ * When online with a non-empty marker, the chain routes *through* the marker:
289
+ * EMPTY→marker (applied history) + marker→target (pending edges). This ensures
290
+ * the displayed chain includes the marker node so applied/pending status is
291
+ * correct. Without this, BFS from EMPTY to target could pick a shortest path
292
+ * that bypasses the marker entirely (e.g. in a diamond graph), causing the
293
+ * marker to appear "diverged" when it isn't.
294
+ */
295
+ function resolveDisplayChain(
296
+ graph: MigrationGraph,
297
+ targetHash: string,
298
+ markerHash: string | undefined,
299
+ ): readonly MigrationChainEntry[] | null {
300
+ if (markerHash === undefined) {
301
+ return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
302
+ }
303
+
304
+ const toMarker = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);
305
+ // Marker unreachable from EMPTY — show the target chain anyway.
306
+ // The caller detects this via markerInChain and emits a divergence diagnostic.
307
+ if (!toMarker) return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
308
+
309
+ if (markerHash === targetHash) return toMarker;
310
+
311
+ const fromMarker = findPath(graph, markerHash, targetHash);
312
+ if (fromMarker) return [...toMarker, ...fromMarker];
313
+
314
+ // Marker is ahead of target (or on a disconnected branch).
315
+ // Try the inverse: target→marker. If it succeeds, the marker is ahead —
316
+ // show the full chain from EMPTY through the target and on to the marker.
317
+ const toTarget = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
318
+ if (!toTarget) return null;
319
+
320
+ const targetToMarker = findPath(graph, targetHash, markerHash);
321
+ if (targetToMarker) return [...toTarget, ...targetToMarker];
322
+
323
+ // Genuinely disconnected — show EMPTY→target; caller handles divergence diagnostic.
324
+ return toTarget;
325
+ }
326
+
327
+ const DEFAULT_LIMIT = 10;
328
+
329
+ function determineLimit(opts: MigrationStatusOptions) {
330
+ if (opts.all) {
331
+ // No limit
332
+ return;
333
+ }
334
+ if (!opts.limit) {
335
+ return DEFAULT_LIMIT;
336
+ }
337
+ const parsed = Number.parseInt(opts.limit, 10);
338
+ if (Number.isNaN(parsed)) {
339
+ return DEFAULT_LIMIT;
340
+ }
341
+ return parsed;
342
+ }
343
+
344
+ async function executeMigrationStatusCommand(
345
+ options: MigrationStatusOptions,
346
+ flags: GlobalFlags,
347
+ ui: TerminalUI,
348
+ ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
349
+ const config = await loadConfig(options.config);
350
+ const { configPath, migrationsDir, migrationsRelative, refsPath } = resolveMigrationPaths(
351
+ options.config,
352
+ config,
353
+ );
354
+
355
+ const dbConnection = options.db ?? config.db?.connection;
356
+ const hasDriver = !!config.driver;
357
+
358
+ let activeRefName: string | undefined;
359
+ let activeRefHash: string | undefined;
360
+ let allRefs: Refs = {};
361
+ try {
362
+ allRefs = await readRefs(refsPath);
363
+ } catch (error) {
364
+ if (MigrationToolsError.is(error)) {
365
+ return notOk(
366
+ errorRuntime(error.message, {
367
+ why: error.why,
368
+ fix: error.fix,
369
+ meta: { code: error.code },
370
+ }),
371
+ );
372
+ }
373
+ throw error;
374
+ }
375
+
376
+ if (options.ref) {
377
+ activeRefName = options.ref;
378
+ const refHash = allRefs[activeRefName];
379
+ if (refHash) {
380
+ activeRefHash = refHash;
381
+ } else {
382
+ try {
383
+ activeRefHash = resolveRef(allRefs, activeRefName);
384
+ } catch (error) {
385
+ if (MigrationToolsError.is(error)) {
386
+ return notOk(
387
+ errorRuntime(error.message, {
388
+ why: error.why,
389
+ fix: error.fix,
390
+ meta: { code: error.code },
391
+ }),
392
+ );
393
+ }
394
+ throw error;
395
+ }
396
+ }
397
+ }
398
+
399
+ const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, hash]) => ({
400
+ name,
401
+ hash,
402
+ active: name === activeRefName,
403
+ }));
404
+
405
+ if (!flags.json && !flags.quiet) {
406
+ const details: Array<{ label: string; value: string }> = [
407
+ { label: 'config', value: configPath },
408
+ { label: 'migrations', value: migrationsRelative },
409
+ ];
410
+ if (dbConnection && hasDriver) {
411
+ details.push({ label: 'database', value: maskConnectionUrl(String(dbConnection)) });
412
+ }
413
+ if (activeRefName) {
414
+ details.push({ label: 'ref', value: activeRefName });
415
+ }
416
+ const header = formatStyledHeader({
417
+ command: 'migration status',
418
+ description: 'Show migration history and applied status',
419
+ details,
420
+ flags,
421
+ });
422
+ ui.stderr(header);
423
+ }
424
+
425
+ const diagnostics: StatusDiagnostic[] = [];
426
+ let contractHash: string = EMPTY_CONTRACT_HASH;
427
+ try {
428
+ const envelope = await readContractEnvelope(config);
429
+ contractHash = envelope.storageHash;
430
+ } catch (error) {
431
+ diagnostics.push({
432
+ code: 'CONTRACT.UNREADABLE',
433
+ severity: 'warn',
434
+ message: `Could not read contract: ${error instanceof Error ? error.message : 'unknown error'}`,
435
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
436
+ });
437
+ }
438
+
439
+ let attested: readonly AttestedMigrationBundle[];
440
+ let drafts: readonly DraftMigrationBundle[];
441
+ let graph: MigrationGraph;
442
+ try {
443
+ ({ attested, drafts, graph } = await loadAllBundles(migrationsDir));
444
+ } catch (error) {
445
+ if (MigrationToolsError.is(error)) {
446
+ return notOk(
447
+ errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
448
+ );
449
+ }
450
+ return notOk(
451
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
452
+ why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
453
+ }),
454
+ );
455
+ }
456
+
457
+ if (drafts.length > 0) {
458
+ diagnostics.push({
459
+ code: 'MIGRATION.DRAFTS',
460
+ severity: 'warn',
461
+ message: `${drafts.length} draft migration(s) found: ${drafts.map((d) => d.dirName).join(', ')}`,
462
+ hints: [
463
+ "Run 'prisma-next migration verify --dir <path>' to attest draft migrations before applying",
464
+ ],
465
+ });
466
+ }
467
+
468
+ if (attested.length === 0) {
469
+ if (contractHash !== EMPTY_CONTRACT_HASH) {
470
+ diagnostics.push({
471
+ code: 'CONTRACT.AHEAD',
472
+ severity: 'warn',
473
+ message: 'No migration exists for the current contract',
474
+ hints: [
475
+ "Run 'prisma-next migration plan' to generate a migration for the current contract",
476
+ ],
477
+ });
478
+ }
479
+ return ok({
480
+ ok: true,
481
+ mode: dbConnection && hasDriver ? 'online' : 'offline',
482
+ migrations: [],
483
+ targetHash: EMPTY_CONTRACT_HASH,
484
+ contractHash,
485
+ summary: 'No migrations found',
486
+ diagnostics,
487
+ });
488
+ }
489
+
490
+ let targetHash: string | undefined;
491
+
492
+ if (activeRefHash) {
493
+ targetHash = activeRefHash;
494
+ } else if (graph.nodes.has(contractHash)) {
495
+ targetHash = contractHash;
496
+ } else {
497
+ const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
498
+ if (leaves.length === 1) {
499
+ targetHash = leaves[0];
500
+ } else {
501
+ diagnostics.push({
502
+ code: 'MIGRATION.DIVERGED',
503
+ severity: 'warn',
504
+ message: 'There are multiple valid migration paths — you must select a target',
505
+ hints: [
506
+ "Use '--ref <name>' to select a target",
507
+ "Or 'prisma-next migration ref set <name> <hash>' to create one",
508
+ ],
509
+ });
510
+ }
511
+ }
512
+
513
+ let markerHash: string | undefined;
514
+ let mode: 'online' | 'offline' = 'offline';
515
+
516
+ if (dbConnection && hasDriver) {
517
+ const client = createControlClient({
518
+ family: config.family,
519
+ target: config.target,
520
+ adapter: config.adapter,
521
+ driver: config.driver,
522
+ extensionPacks: config.extensionPacks ?? [],
523
+ });
524
+ try {
525
+ await client.connect(dbConnection);
526
+ markerHash = (await client.readMarker())?.storageHash;
527
+ mode = 'online';
528
+ } catch {
529
+ if (!flags.json && !flags.quiet) {
530
+ ui.warn('Could not connect to database — showing offline status');
531
+ }
532
+ } finally {
533
+ await client.close();
534
+ }
535
+ }
536
+
537
+ // Marker exists but is not in the migration graph and doesn't match the
538
+ // contract hash. The DB is at an unknown state relative to the graph.
539
+ // Bail out early with a clear diagnostic instead of rendering a confusing
540
+ // graph with no statuses.
541
+ //
542
+ // When marker === contract (both off-graph), the DB matches the current
543
+ // contract — proceed normally; the detached contract node will carry both
544
+ // the db and contract markers.
545
+ if (
546
+ mode === 'online' &&
547
+ markerHash !== undefined &&
548
+ !graph.nodes.has(markerHash) &&
549
+ markerHash !== contractHash
550
+ ) {
551
+ const hints: string[] = [];
552
+ if (graph.nodes.has(contractHash)) {
553
+ hints.push(
554
+ "Run 'prisma-next db sign' to overwrite the marker if the database already matches the contract",
555
+ "Run 'prisma-next db update' to push the current contract to the database",
556
+ "Run 'prisma-next contract infer' to make your contract match the database",
557
+ "Run 'prisma-next db verify' to inspect the database state",
558
+ );
559
+ } else {
560
+ hints.push(
561
+ "Run 'prisma-next db update' to push the current contract to the database",
562
+ "Run 'prisma-next contract infer' to make your contract match the database",
563
+ "Run 'prisma-next db verify' to inspect the database state",
564
+ );
565
+ }
566
+ diagnostics.push({
567
+ code: 'MIGRATION.MARKER_NOT_IN_HISTORY',
568
+ severity: 'warn',
569
+ message:
570
+ 'Database was updated outside the migration system (marker does not match any migration)',
571
+ hints,
572
+ });
573
+ return ok({
574
+ ok: true,
575
+ mode,
576
+ migrations: [],
577
+ targetHash: EMPTY_CONTRACT_HASH,
578
+ contractHash,
579
+ summary: `${attested.length} migration(s) on disk`,
580
+ diagnostics,
581
+ markerHash,
582
+ ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
583
+ });
584
+ }
585
+
586
+ if (mode === 'online' && markerHash === undefined) {
587
+ diagnostics.push({
588
+ code: 'MIGRATION.NO_MARKER',
589
+ severity: 'warn',
590
+ message: 'Database has not been initialized — no migration marker found',
591
+ hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
592
+ });
593
+ }
594
+
595
+ // Contract diagnostic — fires when no migration produces the current contract hash.
596
+ // Suppressed when: (a) graph is diverged (MIGRATION.DIVERGED already guides the user),
597
+ // (b) marker === contract and both off-graph (marker-not-in-graph diagnostic covers it).
598
+ if (
599
+ targetHash &&
600
+ contractHash !== EMPTY_CONTRACT_HASH &&
601
+ !graph.nodes.has(contractHash) &&
602
+ markerHash !== contractHash
603
+ ) {
604
+ diagnostics.push({
605
+ code: 'CONTRACT.AHEAD',
606
+ severity: 'warn',
607
+ message: 'Contract has changed since the last migration was planned',
608
+ hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"],
609
+ });
610
+ }
611
+
612
+ if (!targetHash) {
613
+ return ok({
614
+ ok: true,
615
+ mode,
616
+ migrations: [],
617
+ targetHash: EMPTY_CONTRACT_HASH,
618
+ contractHash,
619
+ summary: `${attested.length} migration(s) on disk`,
620
+ diagnostics,
621
+ ...ifDefined('markerHash', markerHash),
622
+ ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
623
+ graph,
624
+ bundles: attested,
625
+ diverged: true,
626
+ });
627
+ }
628
+
629
+ const chain = resolveDisplayChain(graph, targetHash, markerHash);
630
+
631
+ if (!chain) {
632
+ return notOk(
633
+ errorRuntime('Cannot reconstruct migration history', {
634
+ why: `No path from ${EMPTY_CONTRACT_HASH} to target ${targetHash}`,
635
+ fix: 'The migration history may have gaps. Check the migrations directory for missing or corrupted packages.',
636
+ }),
637
+ );
638
+ }
639
+
640
+ const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
641
+ const entries = buildMigrationEntries(chain, attested, mode, markerHash, edgeStatuses);
642
+
643
+ const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
644
+ const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
645
+
646
+ let summary: string;
647
+ if (mode === 'online') {
648
+ if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
649
+ summary = `${attested.length} migration(s) on disk`;
650
+ } else if (activeRefHash && markerHash !== undefined) {
651
+ summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
652
+ } else if (pendingCount === 0) {
653
+ summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
654
+ } else if (markerHash === undefined) {
655
+ summary = `${pendingCount} pending migration(s) — database has no marker`;
656
+ } else {
657
+ summary = `${pendingCount} pending migration(s) — run 'prisma-next migration apply' to apply`;
658
+ }
659
+ } else {
660
+ summary = `${entries.length} migration(s) on disk`;
661
+ }
662
+
663
+ if (mode === 'online') {
664
+ if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
665
+ diagnostics.push({
666
+ code: 'MIGRATION.MARKER_NOT_IN_HISTORY',
667
+ severity: 'warn',
668
+ message:
669
+ 'Database matches the current contract but was updated directly (not via migration apply)',
670
+ hints: ["Run 'prisma-next migration plan' to plan a migration to your current contract"],
671
+ });
672
+ } else if (pendingCount > 0) {
673
+ diagnostics.push({
674
+ code: 'MIGRATION.DATABASE_BEHIND',
675
+ severity: 'info',
676
+ message: `${pendingCount} migration(s) pending`,
677
+ hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
678
+ });
679
+ } else {
680
+ diagnostics.push({
681
+ code: 'MIGRATION.UP_TO_DATE',
682
+ severity: 'info',
683
+ message: 'Database is up to date',
684
+ hints: [],
685
+ });
686
+ }
687
+ }
688
+
689
+ let pathDecision: MigrationStatusResult['pathDecision'];
690
+ if (mode === 'online' && markerHash !== undefined) {
691
+ const decision = findPathWithDecision(graph, markerHash, targetHash, activeRefName);
692
+ if (decision) {
693
+ pathDecision = toPathDecisionResult(decision);
694
+ }
695
+ }
696
+
697
+ const result: MigrationStatusResult = {
698
+ ok: true,
699
+ mode,
700
+ migrations: entries,
701
+ targetHash,
702
+ contractHash,
703
+ summary,
704
+ diagnostics,
705
+ ...ifDefined('markerHash', markerHash),
706
+ ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
707
+ ...ifDefined('pathDecision', pathDecision),
708
+ graph,
709
+ bundles: attested,
710
+ ...(drafts.length > 0 ? { drafts } : {}),
711
+ edgeStatuses,
712
+ ...ifDefined('activeRefHash', activeRefHash),
713
+ ...ifDefined('activeRefName', activeRefName),
714
+ };
715
+ return ok(result);
716
+ }
717
+
718
+ export function createMigrationStatusCommand(): Command {
719
+ const command = new Command('status');
720
+ setCommandDescriptions(
721
+ command,
722
+ 'Show migration history and applied status',
723
+ 'Displays the migration history in order. When a database connection\n' +
724
+ 'is available, shows which migrations are applied and which are pending.\n' +
725
+ 'Without a database connection, shows the history from disk only.',
726
+ );
727
+ setCommandExamples(command, [
728
+ 'prisma-next migration status',
729
+ 'prisma-next migration status --db $DATABASE_URL',
730
+ ]);
731
+ addGlobalOptions(command)
732
+ .option('--db <url>', 'Database connection string')
733
+ .option('--config <path>', 'Path to prisma-next.config.ts')
734
+ .option('--ref <name>', 'Target ref name from migrations/refs/')
735
+ .option('--graph', 'Show the full migration graph with all branches')
736
+ .option('--limit <n>', 'Maximum number of migrations to display (default: 10)')
737
+ .option('--all', 'Show full history (disables truncation)')
738
+ .action(async (options: MigrationStatusOptions) => {
739
+ const flags = parseGlobalFlags(options);
740
+
741
+ const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
742
+
743
+ const result = await executeMigrationStatusCommand(options, flags, ui);
744
+
745
+ const exitCode = handleResult(result, flags, ui, (statusResult) => {
746
+ if (flags.json) {
747
+ const {
748
+ graph: _g,
749
+ bundles: _b,
750
+ edgeStatuses: _es,
751
+ activeRefHash: _arh,
752
+ activeRefName: _arn,
753
+ diverged: _d,
754
+ ...jsonResult
755
+ } = statusResult;
756
+ ui.output(JSON.stringify(jsonResult, null, 2));
757
+ } else if (!flags.quiet) {
758
+ const colorize = flags.color !== false;
759
+
760
+ if (statusResult.graph) {
761
+ const limit = determineLimit(options);
762
+ const renderInput = migrationGraphToRenderInput({
763
+ graph: statusResult.graph,
764
+ mode: statusResult.mode,
765
+ markerHash: statusResult.markerHash,
766
+ contractHash: statusResult.contractHash,
767
+ refs: statusResult.refs,
768
+ activeRefHash: statusResult.activeRefHash,
769
+ activeRefName: statusResult.activeRefName,
770
+ edgeStatuses: statusResult.edgeStatuses,
771
+ draftEdges: statusResult.drafts?.map((d) => ({
772
+ from: d.manifest.from,
773
+ to: d.manifest.to,
774
+ dirName: d.dirName,
775
+ })),
776
+ });
777
+
778
+ const graphToRender =
779
+ options.graph || statusResult.diverged
780
+ ? renderInput.graph
781
+ : extractRelevantSubgraph(renderInput.graph, renderInput.relevantPaths);
782
+ const dagreOptions =
783
+ !options.graph && isLinearGraph(graphToRender) ? { ranksep: 1 } : undefined;
784
+ const renderOptions = {
785
+ ...renderInput.options,
786
+ colorize,
787
+ ...ifDefined('limit', limit),
788
+ ...ifDefined('dagreOptions', dagreOptions),
789
+ };
790
+ const graphOutput = graphRenderer.render(graphToRender, renderOptions);
791
+ ui.log(graphOutput);
792
+ if (statusResult.mode === 'online') {
793
+ ui.log(formatLegend(colorize));
794
+ }
795
+ }
796
+ ui.log('');
797
+ ui.log(formatStatusSummary(statusResult, colorize));
798
+ }
799
+ });
800
+
801
+ process.exit(exitCode);
802
+ });
803
+
804
+ return command;
805
+ }
806
+
807
+ function formatLegend(colorize: boolean): string {
808
+ const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
809
+ const parts = [
810
+ `${c(cyan, '✓')} applied`,
811
+ `${c(yellow, '⧗')} pending`,
812
+ `${c(magenta, '✗')} unreachable`,
813
+ ];
814
+ return c(dim, parts.join(' '));
815
+ }
816
+
817
+ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
818
+ const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
819
+ const lines: string[] = [];
820
+
821
+ const hasUnknown = result.migrations.some((e) => e.status === 'unknown');
822
+ const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
823
+
824
+ const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
825
+
826
+ if (result.mode === 'online') {
827
+ if (hasUnknown || hasWarnings) {
828
+ lines.push(`${c(yellow, '⚠')} ${result.summary}`);
829
+ } else if (pendingCount === 0) {
830
+ lines.push(`${c(cyan, '✔')} ${result.summary}`);
831
+ } else {
832
+ lines.push(`${c(yellow, '⧗')} ${result.summary}`);
833
+ }
834
+ } else {
835
+ lines.push(result.summary);
836
+ }
837
+
838
+ const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
839
+ for (const diag of warnings) {
840
+ lines.push(`${c(yellow, '⚠')} ${diag.message}`);
841
+ for (const hint of diag.hints) {
842
+ lines.push(` ${c(dim, hint)}`);
843
+ }
844
+ }
845
+
846
+ return lines.join('\n');
847
+ }
848
+
849
+ function summarizeRefDistance(
850
+ graph: MigrationGraph,
851
+ markerHash: string,
852
+ refHash: string,
853
+ refName: string,
854
+ ): string {
855
+ if (markerHash === refHash) return `At ref "${refName}" target`;
856
+
857
+ const pathToRef = findPath(graph, markerHash, refHash);
858
+ if (pathToRef) return `${pathToRef.length} migration(s) behind ref "${refName}"`;
859
+
860
+ const pathFromRef = findPath(graph, refHash, markerHash);
861
+ if (pathFromRef) return `${pathFromRef.length} migration(s) ahead of ref "${refName}"`;
862
+
863
+ return `No path between database marker and ref "${refName}" target`;
864
+ }