@prisma-next/cli 0.3.0-dev.11 → 0.3.0-dev.114

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 (215) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +470 -134
  3. package/dist/cli-errors-ByGuoqNj.mjs +3 -0
  4. package/dist/cli-errors-D6HxRn3A.d.mts +2 -0
  5. package/dist/cli.d.mts +1 -0
  6. package/dist/cli.js +1 -2350
  7. package/dist/cli.mjs +235 -0
  8. package/dist/cli.mjs.map +1 -0
  9. package/dist/client-612RJJD_.mjs +1069 -0
  10. package/dist/client-612RJJD_.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts +7 -0
  12. package/dist/commands/contract-emit.d.mts.map +1 -0
  13. package/dist/commands/contract-emit.mjs +4 -0
  14. package/dist/commands/contract-infer.d.mts +7 -0
  15. package/dist/commands/contract-infer.d.mts.map +1 -0
  16. package/dist/commands/contract-infer.mjs +4 -0
  17. package/dist/commands/db-init.d.mts +7 -0
  18. package/dist/commands/db-init.d.mts.map +1 -0
  19. package/dist/commands/db-init.mjs +124 -0
  20. package/dist/commands/db-init.mjs.map +1 -0
  21. package/dist/commands/db-schema.d.mts +7 -0
  22. package/dist/commands/db-schema.d.mts.map +1 -0
  23. package/dist/commands/db-schema.mjs +52 -0
  24. package/dist/commands/db-schema.mjs.map +1 -0
  25. package/dist/commands/db-sign.d.mts +7 -0
  26. package/dist/commands/db-sign.d.mts.map +1 -0
  27. package/dist/commands/db-sign.mjs +135 -0
  28. package/dist/commands/db-sign.mjs.map +1 -0
  29. package/dist/commands/db-update.d.mts +7 -0
  30. package/dist/commands/db-update.d.mts.map +1 -0
  31. package/dist/commands/db-update.mjs +121 -0
  32. package/dist/commands/db-update.mjs.map +1 -0
  33. package/dist/commands/db-verify.d.mts +7 -0
  34. package/dist/commands/db-verify.d.mts.map +1 -0
  35. package/dist/commands/db-verify.mjs +310 -0
  36. package/dist/commands/db-verify.mjs.map +1 -0
  37. package/dist/commands/migration-apply.d.mts +36 -0
  38. package/dist/commands/migration-apply.d.mts.map +1 -0
  39. package/dist/commands/migration-apply.mjs +240 -0
  40. package/dist/commands/migration-apply.mjs.map +1 -0
  41. package/dist/commands/migration-plan.d.mts +47 -0
  42. package/dist/commands/migration-plan.d.mts.map +1 -0
  43. package/dist/commands/migration-plan.mjs +288 -0
  44. package/dist/commands/migration-plan.mjs.map +1 -0
  45. package/dist/commands/migration-ref.d.mts +43 -0
  46. package/dist/commands/migration-ref.d.mts.map +1 -0
  47. package/dist/commands/migration-ref.mjs +194 -0
  48. package/dist/commands/migration-ref.mjs.map +1 -0
  49. package/dist/commands/migration-show.d.mts +28 -0
  50. package/dist/commands/migration-show.d.mts.map +1 -0
  51. package/dist/commands/migration-show.mjs +139 -0
  52. package/dist/commands/migration-show.mjs.map +1 -0
  53. package/dist/commands/migration-status.d.mts +85 -0
  54. package/dist/commands/migration-status.d.mts.map +1 -0
  55. package/dist/commands/migration-status.mjs +4 -0
  56. package/dist/commands/migration-verify.d.mts +16 -0
  57. package/dist/commands/migration-verify.d.mts.map +1 -0
  58. package/dist/commands/migration-verify.mjs +87 -0
  59. package/dist/commands/migration-verify.mjs.map +1 -0
  60. package/dist/config-loader-d_KF19Tw.mjs +43 -0
  61. package/dist/config-loader-d_KF19Tw.mjs.map +1 -0
  62. package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
  63. package/dist/config-loader.d.mts.map +1 -0
  64. package/dist/config-loader.mjs +3 -0
  65. package/dist/contract-emit-CVv7dbQ9.mjs +187 -0
  66. package/dist/contract-emit-CVv7dbQ9.mjs.map +1 -0
  67. package/dist/contract-infer-Bvw8u8Eu.mjs +83 -0
  68. package/dist/contract-infer-Bvw8u8Eu.mjs.map +1 -0
  69. package/dist/exports/config-types.d.mts +2 -0
  70. package/dist/exports/config-types.mjs +3 -0
  71. package/dist/exports/control-api.d.mts +626 -0
  72. package/dist/exports/control-api.d.mts.map +1 -0
  73. package/dist/exports/control-api.mjs +107 -0
  74. package/dist/exports/control-api.mjs.map +1 -0
  75. package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +10 -5
  76. package/dist/exports/index.d.mts.map +1 -0
  77. package/dist/exports/index.mjs +130 -0
  78. package/dist/exports/index.mjs.map +1 -0
  79. package/dist/extract-sql-ddl-Jf5blEO0.mjs +26 -0
  80. package/dist/extract-sql-ddl-Jf5blEO0.mjs.map +1 -0
  81. package/dist/framework-components-M2j-qPfr.mjs +59 -0
  82. package/dist/framework-components-M2j-qPfr.mjs.map +1 -0
  83. package/dist/inspect-live-schema-BQe5i4YE.mjs +90 -0
  84. package/dist/inspect-live-schema-BQe5i4YE.mjs.map +1 -0
  85. package/dist/migration-command-scaffold-SLrjcKXS.mjs +104 -0
  86. package/dist/migration-command-scaffold-SLrjcKXS.mjs.map +1 -0
  87. package/dist/migration-status-B7OVZ-Ka.mjs +1576 -0
  88. package/dist/migration-status-B7OVZ-Ka.mjs.map +1 -0
  89. package/dist/migrations-Db_ea9eE.mjs +173 -0
  90. package/dist/migrations-Db_ea9eE.mjs.map +1 -0
  91. package/dist/progress-adapter-DRNe2idZ.mjs +43 -0
  92. package/dist/progress-adapter-DRNe2idZ.mjs.map +1 -0
  93. package/dist/terminal-ui-DAcMBRKf.mjs +980 -0
  94. package/dist/terminal-ui-DAcMBRKf.mjs.map +1 -0
  95. package/dist/verify-DXKxBFvU.mjs +385 -0
  96. package/dist/verify-DXKxBFvU.mjs.map +1 -0
  97. package/package.json +88 -43
  98. package/src/cli.ts +109 -58
  99. package/src/commands/contract-emit.ts +236 -143
  100. package/src/commands/contract-infer-paths.ts +32 -0
  101. package/src/commands/contract-infer.ts +131 -0
  102. package/src/commands/db-init.ts +211 -425
  103. package/src/commands/db-schema.ts +77 -0
  104. package/src/commands/db-sign.ts +207 -228
  105. package/src/commands/db-update.ts +236 -0
  106. package/src/commands/db-verify.ts +484 -186
  107. package/src/commands/inspect-live-schema.ts +171 -0
  108. package/src/commands/migration-apply.ts +416 -0
  109. package/src/commands/migration-plan.ts +451 -0
  110. package/src/commands/migration-ref.ts +305 -0
  111. package/src/commands/migration-show.ts +246 -0
  112. package/src/commands/migration-status.ts +838 -0
  113. package/src/commands/migration-verify.ts +134 -0
  114. package/src/config-loader.ts +13 -3
  115. package/src/control-api/client.ts +614 -0
  116. package/src/control-api/contract-enrichment.ts +135 -0
  117. package/src/control-api/errors.ts +9 -0
  118. package/src/control-api/operations/contract-emit.ts +173 -0
  119. package/src/control-api/operations/db-init.ts +286 -0
  120. package/src/control-api/operations/db-update.ts +221 -0
  121. package/src/control-api/operations/extract-sql-ddl.ts +47 -0
  122. package/src/control-api/operations/migration-apply.ts +194 -0
  123. package/src/control-api/operations/migration-helpers.ts +49 -0
  124. package/src/control-api/types.ts +683 -0
  125. package/src/exports/config-types.ts +4 -3
  126. package/src/exports/control-api.ts +56 -0
  127. package/src/load-ts-contract.ts +16 -11
  128. package/src/utils/cli-errors.ts +5 -2
  129. package/src/utils/command-helpers.ts +293 -3
  130. package/src/utils/formatters/emit.ts +67 -0
  131. package/src/utils/formatters/errors.ts +82 -0
  132. package/src/utils/formatters/graph-migration-mapper.ts +220 -0
  133. package/src/utils/formatters/graph-render.ts +1317 -0
  134. package/src/utils/formatters/graph-types.ts +114 -0
  135. package/src/utils/formatters/help.ts +380 -0
  136. package/src/utils/formatters/helpers.ts +28 -0
  137. package/src/utils/formatters/migrations.ts +346 -0
  138. package/src/utils/formatters/styled.ts +212 -0
  139. package/src/utils/formatters/verify.ts +620 -0
  140. package/src/utils/global-flags.ts +41 -23
  141. package/src/utils/migration-command-scaffold.ts +187 -0
  142. package/src/utils/migration-types.ts +12 -0
  143. package/src/utils/progress-adapter.ts +75 -0
  144. package/src/utils/result-handler.ts +12 -13
  145. package/src/utils/shutdown.ts +92 -0
  146. package/src/utils/suggest-command.ts +31 -0
  147. package/src/utils/terminal-ui.ts +276 -0
  148. package/dist/chunk-BZMBKEEQ.js +0 -997
  149. package/dist/chunk-BZMBKEEQ.js.map +0 -1
  150. package/dist/chunk-CVNWLFXO.js +0 -91
  151. package/dist/chunk-CVNWLFXO.js.map +0 -1
  152. package/dist/chunk-HWYQOCAJ.js +0 -47
  153. package/dist/chunk-HWYQOCAJ.js.map +0 -1
  154. package/dist/chunk-QUPBU4KV.js +0 -131
  155. package/dist/chunk-QUPBU4KV.js.map +0 -1
  156. package/dist/cli.d.ts +0 -2
  157. package/dist/cli.d.ts.map +0 -1
  158. package/dist/cli.js.map +0 -1
  159. package/dist/commands/contract-emit.d.ts +0 -3
  160. package/dist/commands/contract-emit.d.ts.map +0 -1
  161. package/dist/commands/contract-emit.js +0 -9
  162. package/dist/commands/contract-emit.js.map +0 -1
  163. package/dist/commands/db-init.d.ts +0 -3
  164. package/dist/commands/db-init.d.ts.map +0 -1
  165. package/dist/commands/db-init.js +0 -337
  166. package/dist/commands/db-init.js.map +0 -1
  167. package/dist/commands/db-introspect.d.ts +0 -3
  168. package/dist/commands/db-introspect.d.ts.map +0 -1
  169. package/dist/commands/db-introspect.js +0 -186
  170. package/dist/commands/db-introspect.js.map +0 -1
  171. package/dist/commands/db-schema-verify.d.ts +0 -3
  172. package/dist/commands/db-schema-verify.d.ts.map +0 -1
  173. package/dist/commands/db-schema-verify.js +0 -160
  174. package/dist/commands/db-schema-verify.js.map +0 -1
  175. package/dist/commands/db-sign.d.ts +0 -3
  176. package/dist/commands/db-sign.d.ts.map +0 -1
  177. package/dist/commands/db-sign.js +0 -195
  178. package/dist/commands/db-sign.js.map +0 -1
  179. package/dist/commands/db-verify.d.ts +0 -3
  180. package/dist/commands/db-verify.d.ts.map +0 -1
  181. package/dist/commands/db-verify.js +0 -169
  182. package/dist/commands/db-verify.js.map +0 -1
  183. package/dist/config-loader.d.ts.map +0 -1
  184. package/dist/config-loader.js +0 -7
  185. package/dist/config-loader.js.map +0 -1
  186. package/dist/exports/config-types.d.ts +0 -3
  187. package/dist/exports/config-types.d.ts.map +0 -1
  188. package/dist/exports/config-types.js +0 -6
  189. package/dist/exports/config-types.js.map +0 -1
  190. package/dist/exports/index.d.ts +0 -4
  191. package/dist/exports/index.d.ts.map +0 -1
  192. package/dist/exports/index.js +0 -175
  193. package/dist/exports/index.js.map +0 -1
  194. package/dist/load-ts-contract.d.ts.map +0 -1
  195. package/dist/utils/action.d.ts +0 -16
  196. package/dist/utils/action.d.ts.map +0 -1
  197. package/dist/utils/cli-errors.d.ts +0 -7
  198. package/dist/utils/cli-errors.d.ts.map +0 -1
  199. package/dist/utils/command-helpers.d.ts +0 -12
  200. package/dist/utils/command-helpers.d.ts.map +0 -1
  201. package/dist/utils/framework-components.d.ts +0 -70
  202. package/dist/utils/framework-components.d.ts.map +0 -1
  203. package/dist/utils/global-flags.d.ts +0 -25
  204. package/dist/utils/global-flags.d.ts.map +0 -1
  205. package/dist/utils/output.d.ts +0 -142
  206. package/dist/utils/output.d.ts.map +0 -1
  207. package/dist/utils/result-handler.d.ts +0 -15
  208. package/dist/utils/result-handler.d.ts.map +0 -1
  209. package/dist/utils/spinner.d.ts +0 -29
  210. package/dist/utils/spinner.d.ts.map +0 -1
  211. package/src/commands/db-introspect.ts +0 -256
  212. package/src/commands/db-schema-verify.ts +0 -232
  213. package/src/utils/action.ts +0 -43
  214. package/src/utils/output.ts +0 -1471
  215. package/src/utils/spinner.ts +0 -67
@@ -0,0 +1,1576 @@
1
+ import { t as loadConfig } from "./config-loader-d_KF19Tw.mjs";
2
+ import { _ as errorUnexpected, m as errorRuntime } from "./cli-errors-ByGuoqNj.mjs";
3
+ import { t as createControlClient } from "./client-612RJJD_.mjs";
4
+ import { c as readContractEnvelope, f as setCommandDescriptions, g as parseGlobalFlags, h as toPathDecisionResult, i as addGlobalOptions, o as loadMigrationBundles, p as setCommandExamples, r as handleResult, s as maskConnectionUrl, t as TerminalUI, u as resolveMigrationPaths, y as formatStyledHeader } from "./terminal-ui-DAcMBRKf.mjs";
5
+ import { Command } from "commander";
6
+ import { notOk, ok } from "@prisma-next/utils/result";
7
+ import { ifDefined } from "@prisma-next/utils/defined";
8
+ import { EMPTY_CONTRACT_HASH } from "@prisma-next/core-control-plane/constants";
9
+ import { findPath, findPathWithDecision, findReachableLeaves } from "@prisma-next/migration-tools/dag";
10
+ import { MigrationToolsError } from "@prisma-next/migration-tools/types";
11
+ import { bold, cyan, dim, magenta, yellow } from "colorette";
12
+ import { readRefs, resolveRef } from "@prisma-next/migration-tools/refs";
13
+ import dagre from "@dagrejs/dagre";
14
+
15
+ //#region src/utils/formatters/graph-types.ts
16
+ /**
17
+ * Immutable directed graph with adjacency-list indexing.
18
+ *
19
+ * Built once from flat arrays of nodes and edges, then passed around as
20
+ * the primary graph representation for rendering, traversal, truncation,
21
+ * and subgraph extraction.
22
+ */
23
+ var RenderGraph = class {
24
+ nodes;
25
+ edges;
26
+ /** Forward adjacency: node id → outgoing edges. */
27
+ forward;
28
+ /** Set of node ids that have at least one incoming edge. */
29
+ incomingNodes;
30
+ /** Node lookup by id. */
31
+ nodeById;
32
+ constructor(nodes, edges) {
33
+ this.nodes = nodes;
34
+ this.edges = edges;
35
+ const fwd = /* @__PURE__ */ new Map();
36
+ const inc = /* @__PURE__ */ new Set();
37
+ const byId = /* @__PURE__ */ new Map();
38
+ for (const n of nodes) byId.set(n.id, n);
39
+ for (const e of edges) {
40
+ const list = fwd.get(e.from);
41
+ if (list) list.push(e);
42
+ else fwd.set(e.from, [e]);
43
+ inc.add(e.to);
44
+ }
45
+ this.forward = fwd;
46
+ this.incomingNodes = inc;
47
+ this.nodeById = byId;
48
+ }
49
+ /** Outgoing edges from a node (empty array if none). */
50
+ outgoing(nodeId) {
51
+ return this.forward.get(nodeId) ?? [];
52
+ }
53
+ };
54
+
55
+ //#endregion
56
+ //#region src/utils/formatters/graph-migration-mapper.ts
57
+ /**
58
+ * Maps MigrationGraph + status info to the generic graph renderer types.
59
+ */
60
+ const STATUS_ICON = {
61
+ applied: " ✓",
62
+ pending: " ⧗",
63
+ unreachable: " ✗"
64
+ };
65
+ /** Shorten a contract hash for display: strip sha256: prefix, take 7 chars. */
66
+ function shortHash(hash) {
67
+ return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, 7);
68
+ }
69
+ function toShortId(hash) {
70
+ return hash === EMPTY_CONTRACT_HASH ? "∅" : shortHash(hash);
71
+ }
72
+ /**
73
+ * Convert a MigrationGraph + status info into the generic graph renderer types.
74
+ */
75
+ function migrationGraphToRenderInput(input) {
76
+ const { graph, mode, markerHash, contractHash, refs, activeRefHash, edgeStatuses } = input;
77
+ const statusByDirName = new Map(edgeStatuses?.map((e) => [e.dirName, e.status]));
78
+ const nodeList = [];
79
+ for (const nodeId of graph.nodes) {
80
+ const markers = [];
81
+ if (mode === "online" && markerHash === nodeId) markers.push({ kind: "db" });
82
+ if (refs) {
83
+ for (const ref of refs) if (ref.hash === nodeId) markers.push({
84
+ kind: "ref",
85
+ name: ref.name,
86
+ active: ref.active
87
+ });
88
+ }
89
+ if (contractHash === nodeId && contractHash !== EMPTY_CONTRACT_HASH) markers.push({
90
+ kind: "contract",
91
+ planned: true
92
+ });
93
+ nodeList.push({
94
+ id: toShortId(nodeId),
95
+ markers: markers.length > 0 ? markers : void 0
96
+ });
97
+ }
98
+ if (contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash)) {
99
+ const detachedMarkers = [];
100
+ if (mode === "online" && markerHash === contractHash) detachedMarkers.push({ kind: "db" });
101
+ detachedMarkers.push({
102
+ kind: "contract",
103
+ planned: false
104
+ });
105
+ nodeList.push({
106
+ id: shortHash(contractHash),
107
+ markers: detachedMarkers,
108
+ style: "detached"
109
+ });
110
+ }
111
+ const edgeList = [];
112
+ for (const [, entries] of graph.forwardChain) for (const entry of entries) {
113
+ const status = statusByDirName.get(entry.dirName);
114
+ const icon = status ? STATUS_ICON[status] : "";
115
+ const label = `${entry.dirName}${icon}`;
116
+ edgeList.push({
117
+ from: toShortId(entry.from),
118
+ to: toShortId(entry.to),
119
+ label,
120
+ ...ifDefined("colorHint", status)
121
+ });
122
+ }
123
+ const relevantPaths = [];
124
+ const rootId = EMPTY_CONTRACT_HASH;
125
+ function addPathFromRoot(targetHash) {
126
+ if (!graph.nodes.has(targetHash)) return;
127
+ const raw = findPath(graph, rootId, targetHash);
128
+ if (raw && raw.length > 0) relevantPaths.push([toShortId(rootId), ...raw.map((e) => toShortId(e.to))]);
129
+ }
130
+ function addPathBetween(fromHash, toHash) {
131
+ if (!graph.nodes.has(fromHash) || !graph.nodes.has(toHash)) return;
132
+ const raw = findPath(graph, fromHash, toHash);
133
+ if (raw && raw.length > 0) relevantPaths.push([toShortId(fromHash), ...raw.map((e) => toShortId(e.to))]);
134
+ }
135
+ if (mode === "online" && markerHash) addPathFromRoot(markerHash);
136
+ if (activeRefHash && activeRefHash !== markerHash) addPathFromRoot(activeRefHash);
137
+ if (contractHash !== EMPTY_CONTRACT_HASH) {
138
+ let contractReached = false;
139
+ if (markerHash && markerHash !== contractHash) {
140
+ if (findPath(graph, markerHash, contractHash)) {
141
+ addPathBetween(markerHash, contractHash);
142
+ contractReached = true;
143
+ }
144
+ }
145
+ if (activeRefHash && activeRefHash !== markerHash && activeRefHash !== contractHash) {
146
+ if (findPath(graph, activeRefHash, contractHash)) {
147
+ addPathBetween(activeRefHash, contractHash);
148
+ contractReached = true;
149
+ }
150
+ }
151
+ if (!contractReached && contractHash !== (markerHash ?? activeRefHash)) addPathFromRoot(contractHash);
152
+ }
153
+ if (relevantPaths.length === 0) addPathFromRoot([...graph.forwardChain.values()].flat().pop()?.to ?? EMPTY_CONTRACT_HASH);
154
+ let spineTargetHash;
155
+ if (activeRefHash && graph.nodes.has(activeRefHash)) spineTargetHash = activeRefHash;
156
+ else if (contractHash !== EMPTY_CONTRACT_HASH && graph.nodes.has(contractHash)) spineTargetHash = contractHash;
157
+ else spineTargetHash = [...graph.forwardChain.values()].flat().pop()?.to ?? EMPTY_CONTRACT_HASH;
158
+ return {
159
+ graph: new RenderGraph(nodeList, edgeList),
160
+ options: {
161
+ spineTarget: toShortId(spineTargetHash),
162
+ rootId: "∅",
163
+ colorize: true
164
+ },
165
+ relevantPaths
166
+ };
167
+ }
168
+
169
+ //#endregion
170
+ //#region src/utils/formatters/graph-render.ts
171
+ /**
172
+ * Terminal graph renderer.
173
+ *
174
+ * Renders directed graphs as ASCII/box-drawing art for terminal output. Uses
175
+ * dagre for automatic layout (rank assignment + coordinate placement), then
176
+ * stamps the result onto a {@link CharGrid} — a sparse character canvas that
177
+ * resolves box-drawing junctions, color priority, and label placement.
178
+ *
179
+ * ## Rendering pipeline
180
+ *
181
+ * 1. **Layout** — dagre assigns (x, y) coordinates to nodes and polyline
182
+ * control points to edges. We use `rankdir: 'TB'` (top-to-bottom).
183
+ * 2. **Orthogonalization** — dagre's polylines may contain diagonal segments.
184
+ * {@link selectBestVariant} resolves each diagonal into an L-shaped bend,
185
+ * enumerating all 2^N combinations and picking the variant with fewest
186
+ * corners and shortest total length.
187
+ * 3. **Edge stamping** — orthogonal segments are stamped onto the CharGrid as
188
+ * directional bitmasks. The grid resolves overlapping directions into the
189
+ * correct box-drawing character (│, ─, ┌, ┼, etc.).
190
+ * 4. **Label placement** — edge labels are placed adjacent to their polyline
191
+ * segments, preferring horizontal (branch-specific) segments over shared
192
+ * vertical trunks to avoid ambiguity.
193
+ * 5. **Arrowheads** — ▾ ▴ ◂ ▸ placed one cell before the terminal point.
194
+ * 6. **Node stamping** — `○ nodeId` with inline marker tags (db, contract,
195
+ * ref names).
196
+ * 7. **Elided indicator** — when truncation is active, `┊ (N earlier
197
+ * migrations)` is stamped above the visible root.
198
+ * 8. **Detached nodes** — rendered below the graph with `◇` and a dotted
199
+ * connector.
200
+ *
201
+ * ## Graph filtering
202
+ *
203
+ * The caller controls what graph is rendered: the full graph, or a subgraph
204
+ * extracted via {@link extractRelevantSubgraph} (union of relevant paths).
205
+ * The renderer itself is agnostic — it renders whatever graph it receives.
206
+ *
207
+ * Truncation is supported via `options.limit`.
208
+ *
209
+ * ## Color accessibility
210
+ *
211
+ * Uses a CVD-safe palette — no red/green contrast. Shape and icon always
212
+ * carry meaning; color only reinforces.
213
+ */
214
+ function segment(from, to) {
215
+ return {
216
+ from,
217
+ to
218
+ };
219
+ }
220
+ function isVertical(seg) {
221
+ return seg.from.x === seg.to.x;
222
+ }
223
+ function manhattanLength(seg) {
224
+ return Math.abs(seg.to.x - seg.from.x) + Math.abs(seg.to.y - seg.from.y);
225
+ }
226
+ /** Rotating palette for ref marker names, cycling through these for each ref. */
227
+ const REF_COLORS = [
228
+ yellow,
229
+ magenta,
230
+ bold,
231
+ cyan
232
+ ];
233
+ /** Build the color palette, respecting the `colorize` flag. When false, all color functions become identity. */
234
+ function buildColors(colorize) {
235
+ const c = (fn) => colorize ? fn : (s) => s;
236
+ return {
237
+ spine: c(cyan),
238
+ branch: c(dim),
239
+ backward: c(magenta),
240
+ applied: c(cyan),
241
+ pending: c(yellow),
242
+ unreachable: c(magenta),
243
+ node: c(cyan),
244
+ label: c(dim),
245
+ marker: c(bold),
246
+ ref: (index) => c(REF_COLORS[index % REF_COLORS.length])
247
+ };
248
+ }
249
+ /** Map a `colorHint` value to its color function, or `undefined` for no hint. */
250
+ function resolveHintColor(hint, colors) {
251
+ if (hint === "applied") return colors.applied;
252
+ if (hint === "pending") return colors.pending;
253
+ if (hint === "unreachable") return colors.unreachable;
254
+ }
255
+ /**
256
+ * Edge drawing priorities — higher priority wins when edges overlap on the
257
+ * same grid cell. Backward edges are drawn on top so rollback paths remain
258
+ * visible over spine and branch edges.
259
+ */
260
+ const PRIORITY = {
261
+ branch: 1,
262
+ spine: 2,
263
+ backward: 3
264
+ };
265
+ const DIR = {
266
+ up: 1,
267
+ down: 2,
268
+ left: 4,
269
+ right: 8
270
+ };
271
+ /** Arrow characters for edge termination (one cell before the target node). */
272
+ const ARROW = {
273
+ up: "▴",
274
+ down: "▾",
275
+ left: "◂",
276
+ right: "▸"
277
+ };
278
+ /** Maps a direction bitmask to its box-drawing character. */
279
+ const BOX_CHAR = {
280
+ 0: " ",
281
+ [DIR.up]: "│",
282
+ [DIR.down]: "│",
283
+ [DIR.up | DIR.down]: "│",
284
+ [DIR.left]: "─",
285
+ [DIR.right]: "─",
286
+ [DIR.left | DIR.right]: "─",
287
+ [DIR.down | DIR.right]: "┌",
288
+ [DIR.down | DIR.left]: "┐",
289
+ [DIR.up | DIR.right]: "└",
290
+ [DIR.up | DIR.left]: "┘",
291
+ [DIR.up | DIR.down | DIR.right]: "├",
292
+ [DIR.up | DIR.down | DIR.left]: "┤",
293
+ [DIR.left | DIR.right | DIR.down]: "┬",
294
+ [DIR.left | DIR.right | DIR.up]: "┴",
295
+ [DIR.up | DIR.down | DIR.left | DIR.right]: "┼"
296
+ };
297
+ /**
298
+ * Convert a node's markers into renderable inline tags.
299
+ *
300
+ * - `db` → `◆ db`
301
+ * - `contract` → `◆ contract` (applied) or `◇ contract` (planned)
302
+ * - `ref` → the ref name, colored from the rotating {@link REF_COLORS} palette
303
+ * - `custom` → the custom label
304
+ */
305
+ function buildInlineTags(markers, colors) {
306
+ const tags = [];
307
+ const refNames = markers.filter((m) => m.kind === "ref").map((m) => m.name);
308
+ for (const m of markers) if (m.kind === "db") tags.push({
309
+ text: "◆ db",
310
+ color: colors.marker
311
+ });
312
+ else if (m.kind === "contract") tags.push({
313
+ text: m.planned ? "◆ contract" : "◇ contract",
314
+ color: colors.marker
315
+ });
316
+ else if (m.kind === "ref") tags.push({
317
+ text: m.name,
318
+ color: colors.ref(refNames.indexOf(m.name))
319
+ });
320
+ else if (m.kind === "custom") tags.push({
321
+ text: m.label,
322
+ color: colors.marker
323
+ });
324
+ return tags;
325
+ }
326
+ /** Total character width of inline tags including leading spaces (0 if no tags). */
327
+ function inlineTagsWidth(tags) {
328
+ if (tags.length === 0) return 0;
329
+ return tags.reduce((w, t) => w + 1 + t.text.length, 0);
330
+ }
331
+ /**
332
+ * Sparse character canvas for terminal graph rendering.
333
+ *
334
+ * Coordinates are unbounded integers — the grid auto-expands as content is
335
+ * added and trims to the bounding box on {@link render}.
336
+ */
337
+ var CharGrid = class {
338
+ connections = /* @__PURE__ */ new Map();
339
+ cellColors = /* @__PURE__ */ new Map();
340
+ chars = /* @__PURE__ */ new Map();
341
+ reserved = /* @__PURE__ */ new Set();
342
+ minX = Number.POSITIVE_INFINITY;
343
+ maxX = Number.NEGATIVE_INFINITY;
344
+ minY = Number.POSITIVE_INFINITY;
345
+ maxY = Number.NEGATIVE_INFINITY;
346
+ key(x, y) {
347
+ return `${x},${y}`;
348
+ }
349
+ /** Expand the bounding box to include (x, y). */
350
+ touch(x, y) {
351
+ if (x < this.minX) this.minX = x;
352
+ if (x > this.maxX) this.maxX = x;
353
+ if (y < this.minY) this.minY = y;
354
+ if (y > this.maxY) this.maxY = y;
355
+ }
356
+ /**
357
+ * Add a directional connection at (x, y). Multiple calls at the same cell
358
+ * are OR'd together — e.g. `addConnection(x, y, UP)` then
359
+ * `addConnection(x, y, RIGHT)` produces a └ corner. Color follows
360
+ * priority: higher-priority edges win at shared cells.
361
+ */
362
+ addConnection(x, y, dir, color, priority = PRIORITY.branch) {
363
+ this.touch(x, y);
364
+ const k = this.key(x, y);
365
+ this.connections.set(k, (this.connections.get(k) ?? 0) | dir);
366
+ const existing = this.cellColors.get(k);
367
+ if (!existing || priority >= existing.priority) this.cellColors.set(k, {
368
+ color,
369
+ priority
370
+ });
371
+ }
372
+ /** Stamp a horizontal edge segment from x1 to x2 at row y. */
373
+ markHorizontal(y, x1, x2, color, priority) {
374
+ const lo = Math.min(x1, x2);
375
+ const hi = Math.max(x1, x2);
376
+ if (lo === hi) return;
377
+ this.addConnection(lo, y, DIR.right, color, priority);
378
+ for (let x = lo + 1; x < hi; x++) this.addConnection(x, y, DIR.left | DIR.right, color, priority);
379
+ this.addConnection(hi, y, DIR.left, color, priority);
380
+ }
381
+ /** Stamp a vertical edge segment from y1 to y2 at column x. */
382
+ markVertical(x, y1, y2, color, priority) {
383
+ const lo = Math.min(y1, y2);
384
+ const hi = Math.max(y1, y2);
385
+ if (lo === hi) return;
386
+ this.addConnection(x, lo, DIR.down, color, priority);
387
+ for (let y = lo + 1; y < hi; y++) this.addConnection(x, y, DIR.up | DIR.down, color, priority);
388
+ this.addConnection(x, hi, DIR.up, color, priority);
389
+ }
390
+ /** Place literal text at (x, y). Each character occupies one cell. Text stamps override connections. */
391
+ stampText(x, y, text, color) {
392
+ for (let i = 0; i < text.length; i++) {
393
+ const cx = x + i;
394
+ this.touch(cx, y);
395
+ this.chars.set(this.key(cx, y), {
396
+ ch: text[i],
397
+ color
398
+ });
399
+ }
400
+ }
401
+ /** True if (x, y) has stamped text or is in a reserved area (node labels). */
402
+ hasLabel(x, y) {
403
+ return this.chars.has(this.key(x, y)) || this.reserved.has(this.key(x, y));
404
+ }
405
+ /** True if (x, y) has any directional connection (an edge passes through). */
406
+ hasConnection(x, y) {
407
+ return (this.connections.get(this.key(x, y)) ?? 0) !== 0;
408
+ }
409
+ /** True if (x, y) has stamped text (not just a reserved area). */
410
+ hasText(x, y) {
411
+ return this.chars.has(this.key(x, y));
412
+ }
413
+ /** Reserve a horizontal span so label placement avoids it. Used for node id + marker regions. */
414
+ reserveArea(x, y, width) {
415
+ for (let i = 0; i < width; i++) this.reserved.add(this.key(x + i, y));
416
+ }
417
+ /** The largest y coordinate with content — used for positioning detached nodes below the graph. */
418
+ getMaxY() {
419
+ return this.maxY;
420
+ }
421
+ /**
422
+ * Render the grid to a multi-line string.
423
+ *
424
+ * Iterates row by row over the bounding box, resolving each cell to either
425
+ * its stamped text character or the box-drawing character for its
426
+ * connection bitmask. Consecutive characters with the same color are
427
+ * batched into a single ANSI-wrapped run for efficiency.
428
+ */
429
+ render() {
430
+ if (this.minX === Number.POSITIVE_INFINITY) return "(empty)";
431
+ const rows = [];
432
+ for (let y = this.minY; y <= this.maxY; y++) {
433
+ let row = "";
434
+ let runChars = "";
435
+ let runColor;
436
+ const flush = () => {
437
+ if (runChars.length === 0) return;
438
+ row += runColor ? runColor(runChars) : runChars;
439
+ runChars = "";
440
+ };
441
+ for (let x = this.minX; x <= this.maxX; x++) {
442
+ const k = this.key(x, y);
443
+ let ch;
444
+ let color;
445
+ const label = this.chars.get(k);
446
+ if (label) {
447
+ ch = label.ch;
448
+ color = label.color;
449
+ } else {
450
+ const conn = this.connections.get(k) ?? 0;
451
+ ch = BOX_CHAR[conn] ?? " ";
452
+ color = conn === 0 ? void 0 : this.cellColors.get(k)?.color;
453
+ }
454
+ if (color !== runColor) {
455
+ flush();
456
+ runColor = color;
457
+ }
458
+ runChars += ch;
459
+ }
460
+ flush();
461
+ rows.push(row.trimEnd());
462
+ }
463
+ while (rows.length > 0 && rows[rows.length - 1] === "") rows.pop();
464
+ return rows.join("\n");
465
+ }
466
+ };
467
+ /**
468
+ * Find the set of edge keys (`"from→to"`) on the shortest path from
469
+ * `rootId` to `targetId`. Used to color spine edges distinctly from
470
+ * branch edges in the rendered output.
471
+ *
472
+ * Returns an empty set if no path exists.
473
+ */
474
+ function findSpineEdges(graph, rootId, targetId) {
475
+ const visited = new Set([rootId]);
476
+ const parent = /* @__PURE__ */ new Map();
477
+ const queue = [rootId];
478
+ while (queue.length > 0) {
479
+ const current = queue.shift();
480
+ if (current === targetId) {
481
+ const spineEdges = /* @__PURE__ */ new Set();
482
+ let node = targetId;
483
+ while (parent.has(node)) {
484
+ const edge = parent.get(node);
485
+ spineEdges.add(`${edge.from}→${edge.to}`);
486
+ node = edge.from;
487
+ }
488
+ return spineEdges;
489
+ }
490
+ for (const edge of graph.outgoing(current)) if (!visited.has(edge.to)) {
491
+ visited.add(edge.to);
492
+ parent.set(edge.to, edge);
493
+ queue.push(edge.to);
494
+ }
495
+ }
496
+ return /* @__PURE__ */ new Set();
497
+ }
498
+ /**
499
+ * Prepend `src` and append `tgt` to dagre's control points, round to
500
+ * integers, and deduplicate consecutive identical points.
501
+ */
502
+ function prepareRawPoints(src, dagrePoints, tgt) {
503
+ const rounded = [
504
+ src,
505
+ ...dagrePoints,
506
+ tgt
507
+ ].map((p) => ({
508
+ x: Math.round(p.x),
509
+ y: Math.round(p.y)
510
+ }));
511
+ const deduped = [rounded[0]];
512
+ for (let i = 1; i < rounded.length; i++) {
513
+ const prev = deduped[deduped.length - 1];
514
+ const curr = rounded[i];
515
+ if (curr.x !== prev.x || curr.y !== prev.y) deduped.push(curr);
516
+ }
517
+ return deduped;
518
+ }
519
+ function countDiagonals(points) {
520
+ let count = 0;
521
+ for (let i = 1; i < points.length; i++) {
522
+ const prev = points[i - 1];
523
+ const curr = points[i];
524
+ if (prev.x !== curr.x && prev.y !== curr.y) count++;
525
+ }
526
+ return count;
527
+ }
528
+ /**
529
+ * Decode a bitmask into an array of bend directions.
530
+ *
531
+ * Each bit selects how one diagonal is converted to an orthogonal corner:
532
+ * 0 → horizontal-first, 1 → vertical-first. Used by {@link selectBestVariant}
533
+ * to enumerate all 2^N combinations.
534
+ */
535
+ function bitsToBends(bits, count) {
536
+ const bends = [];
537
+ for (let k = 0; k < count; k++) bends.push(bits >> k & 1 ? "vertical-first" : "horizontal-first");
538
+ return bends;
539
+ }
540
+ /**
541
+ * Build one polyline variant by resolving each diagonal with the given bend direction.
542
+ *
543
+ * Diagonals are detected inline: when the last emitted point and the next
544
+ * input point differ in both x and y, the segment is diagonal and is
545
+ * resolved using the next value from `bends`. The result is deduplicated
546
+ * to remove zero-length segments created when a bend coincides with an
547
+ * adjacent point.
548
+ */
549
+ function buildVariant(points, bends) {
550
+ if (points.length < 2) return points;
551
+ let bendIdx = 0;
552
+ const result = [points[0]];
553
+ for (let i = 1; i < points.length; i++) {
554
+ const prev = result[result.length - 1];
555
+ const curr = points[i];
556
+ if (prev.x === curr.x || prev.y === curr.y) result.push(curr);
557
+ else {
558
+ if ((bends[bendIdx++] ?? "horizontal-first") === "horizontal-first") result.push({
559
+ x: curr.x,
560
+ y: prev.y
561
+ });
562
+ else result.push({
563
+ x: prev.x,
564
+ y: curr.y
565
+ });
566
+ result.push(curr);
567
+ }
568
+ }
569
+ const final = [result[0]];
570
+ for (let i = 1; i < result.length; i++) {
571
+ const prev = final[final.length - 1];
572
+ const curr = result[i];
573
+ if (curr.x !== prev.x || curr.y !== prev.y) final.push(curr);
574
+ }
575
+ return final;
576
+ }
577
+ /** Count the number of direction changes (corners) in an orthogonal polyline. */
578
+ function countCorners(poly) {
579
+ let corners = 0;
580
+ for (let i = 1; i < poly.length - 1; i++) {
581
+ const a = poly[i - 1];
582
+ const b = poly[i];
583
+ const c = poly[i + 1];
584
+ if (a.x === b.x !== (b.x === c.x)) corners++;
585
+ }
586
+ return corners;
587
+ }
588
+ /** Manhattan length of a polyline (sum of absolute x and y deltas). */
589
+ function polyLength(poly) {
590
+ let len = 0;
591
+ for (let i = 0; i < poly.length - 1; i++) len += Math.abs(poly[i + 1].x - poly[i].x) + Math.abs(poly[i + 1].y - poly[i].y);
592
+ return len;
593
+ }
594
+ /**
595
+ * Find the best (x, y) position to place an edge label adjacent to its
596
+ * polyline. Returns null if no collision-free position exists.
597
+ *
598
+ * @param poly - The orthogonalized polyline for the edge.
599
+ * @param label - The label text to place.
600
+ * @param grid - The character grid (used for collision checks).
601
+ * @param srcY - Y coordinate of the source node (for adjacency penalty).
602
+ */
603
+ function findLabelPlacement(poly, label, grid, srcY) {
604
+ const segments = polyToSegments(poly);
605
+ let best;
606
+ for (const seg of segments) {
607
+ const candidates = segmentLabelCandidates(seg, label.length);
608
+ for (const pos of candidates) {
609
+ if (labelCollides(grid, pos.x, pos.y, label)) continue;
610
+ const score = scoreLabelCandidate(pos, seg, segments, label, grid, srcY);
611
+ if (!best || score > best.score) best = {
612
+ x: pos.x,
613
+ y: pos.y,
614
+ score
615
+ };
616
+ }
617
+ }
618
+ return best;
619
+ }
620
+ /** Convert a polyline into non-zero-length segments. */
621
+ function polyToSegments(poly) {
622
+ const segments = [];
623
+ for (let i = 0; i < poly.length - 1; i++) {
624
+ const seg = segment(poly[i], poly[i + 1]);
625
+ if (manhattanLength(seg) > 0) segments.push(seg);
626
+ }
627
+ return segments;
628
+ }
629
+ /**
630
+ * Generate all candidate (x, y) positions for placing a label adjacent
631
+ * to a single segment. Positions are perpendicular to the segment:
632
+ *
633
+ * - Vertical segments: left and right, at every y along the segment.
634
+ * - Horizontal segments: above and below, at every x where the label fits.
635
+ */
636
+ function segmentLabelCandidates(seg, labelLen) {
637
+ const candidates = [];
638
+ if (isVertical(seg)) {
639
+ const minY = Math.min(seg.from.y, seg.to.y);
640
+ const maxY = Math.max(seg.from.y, seg.to.y);
641
+ for (const x of [seg.from.x + 2, seg.from.x - labelLen - 1]) for (let y = minY; y <= maxY; y++) candidates.push({
642
+ x,
643
+ y
644
+ });
645
+ } else {
646
+ const minX = Math.min(seg.from.x, seg.to.x);
647
+ const maxX = Math.max(seg.from.x, seg.to.x);
648
+ for (const dy of [-1, 1]) {
649
+ const y = seg.from.y + dy;
650
+ for (let x = minX; x <= maxX - labelLen + 1; x++) candidates.push({
651
+ x,
652
+ y
653
+ });
654
+ }
655
+ }
656
+ return candidates;
657
+ }
658
+ /**
659
+ * Score a candidate label position. Higher is better.
660
+ *
661
+ * Combines: segment length, surrounding whitespace, distance from
662
+ * segment midpoint, source-node proximity penalty, and segment-position
663
+ * bonus (horizontal/later segments preferred when the edge has bends).
664
+ *
665
+ * @param pos - Candidate position (top-left corner of the label text).
666
+ * @param seg - The segment this candidate is adjacent to.
667
+ * @param allSegments - All segments of the edge polyline (for segment-position bonus).
668
+ * @param label - The label text (used for width and whitespace probing).
669
+ * @param grid - The character grid (used for whitespace checks).
670
+ * @param srcY - Y coordinate of the edge's source node (penalizes labels
671
+ * that would appear to belong to the node rather than the edge).
672
+ */
673
+ function scoreLabelCandidate(pos, seg, allSegments, label, grid, srcY) {
674
+ const len = manhattanLength(seg);
675
+ const midX = Math.round((seg.from.x + seg.to.x) / 2);
676
+ const midY = Math.round((seg.from.y + seg.to.y) / 2);
677
+ let score = len;
678
+ for (let dy = 1; dy <= 2; dy++) {
679
+ if (!rowHasContent(grid, pos.x, pos.y - dy, label.length)) score += 3;
680
+ if (!rowHasContent(grid, pos.x, pos.y + dy, label.length)) score += 3;
681
+ }
682
+ const labelCenterX = pos.x + Math.floor(label.length / 2);
683
+ score -= (Math.abs(labelCenterX - midX) + Math.abs(pos.y - midY)) * 2;
684
+ const labelCenterY = pos.y + Math.floor(label.length / 2);
685
+ score -= (Math.abs(labelCenterY - midY) + Math.abs(pos.x - midX)) * 2;
686
+ if (isVertical(seg) && pos.x > seg.from.x) score += 10;
687
+ if (srcY !== void 0 && Math.abs(pos.y - srcY) <= 1) score -= 20;
688
+ if (allSegments.some((s) => !isVertical(s))) {
689
+ if (!isVertical(seg)) score += 15;
690
+ const segIndex = allSegments.indexOf(seg);
691
+ score += segIndex / allSegments.length * 5;
692
+ }
693
+ return score;
694
+ }
695
+ /** True if any cell in the horizontal span [x, x+width) at row y has content. */
696
+ function rowHasContent(grid, x, y, width) {
697
+ for (let i = 0; i < width; i++) if (grid.hasLabel(x + i, y) || grid.hasConnection(x + i, y)) return true;
698
+ return false;
699
+ }
700
+ /**
701
+ * Check if placing `text` at (x, y) would collide with existing content.
702
+ * Checks one cell of padding on each side to keep labels visually separated.
703
+ */
704
+ function labelCollides(grid, x, y, text) {
705
+ for (let i = -1; i <= text.length; i++) {
706
+ const cx = x + i;
707
+ if (grid.hasLabel(cx, y)) return true;
708
+ if (i >= 0 && i < text.length && grid.hasConnection(cx, y)) return true;
709
+ }
710
+ return false;
711
+ }
712
+ /**
713
+ * Resolve dagre's polyline into the best orthogonal variant and find the
714
+ * optimal label position in a single pass.
715
+ *
716
+ * Enumerates all 2^N diagonal resolutions, scores each by:
717
+ * - `corners * 10` — fewer corners preferred
718
+ * - `+ manhattan length` — shorter paths preferred
719
+ * - `+ 100` penalty if the label couldn't be placed
720
+ *
721
+ * For edges with no diagonals, the polyline is used as-is.
722
+ */
723
+ function selectBestVariant(src, dagrePoints, tgt, label, grid) {
724
+ const rawPoints = prepareRawPoints(src, dagrePoints, tgt);
725
+ const diagCount = countDiagonals(rawPoints);
726
+ if (diagCount === 0) {
727
+ const poly = buildVariant(rawPoints, []);
728
+ return {
729
+ poly,
730
+ labelPos: label ? findLabelPlacement(poly, label, grid, src.y) : void 0
731
+ };
732
+ }
733
+ const numVariants = 1 << diagCount;
734
+ let bestPoly = null;
735
+ let bestLabel;
736
+ let bestScore = Number.POSITIVE_INFINITY;
737
+ for (let bits = 0; bits < numVariants; bits++) {
738
+ const poly = buildVariant(rawPoints, bitsToBends(bits, diagCount));
739
+ const corners = countCorners(poly);
740
+ const len = polyLength(poly);
741
+ const labelPos = label ? findLabelPlacement(poly, label, grid, src.y) : void 0;
742
+ const labelPenalty = label && !labelPos ? 100 : 0;
743
+ const score = corners * 10 + len + labelPenalty;
744
+ if (score < bestScore) {
745
+ bestScore = score;
746
+ bestPoly = poly;
747
+ bestLabel = labelPos;
748
+ }
749
+ }
750
+ return {
751
+ poly: bestPoly ?? buildVariant(rawPoints, bitsToBends(0, diagCount)),
752
+ labelPos: bestLabel
753
+ };
754
+ }
755
+ /**
756
+ * Extract the subgraph covering the union of multiple paths.
757
+ *
758
+ * Each path is an ordered list of node ids (root → target). The result
759
+ * contains every node on any path plus every forward edge between
760
+ * consecutive nodes on any path. Detached nodes are always included.
761
+ *
762
+ * When all paths overlap (the common case), the result is identical to
763
+ * a single-path extract. When paths diverge (e.g. DB marker on a
764
+ * different branch than the contract), the result naturally includes the
765
+ * fork and both branches — exactly the minimal information needed.
766
+ */
767
+ function extractRelevantSubgraph(graph, paths) {
768
+ const nodeSet = /* @__PURE__ */ new Set();
769
+ const edgePairs = /* @__PURE__ */ new Set();
770
+ for (const path of paths) for (let i = 0; i < path.length; i++) {
771
+ nodeSet.add(path[i]);
772
+ if (i > 0) edgePairs.add(`${path[i - 1]}\0${path[i]}`);
773
+ }
774
+ return new RenderGraph(graph.nodes.filter((n) => nodeSet.has(n.id) || n.style === "detached"), graph.edges.filter((e) => edgePairs.has(`${e.from}\0${e.to}`)));
775
+ }
776
+ /**
777
+ * Truncate a graph to the last `limit` spine edges from the spine target.
778
+ * The window expands to include any node carrying a db or contract marker
779
+ * so those are never truncated away.
780
+ *
781
+ * For the full graph: keeps all branches that fork from the visible spine window.
782
+ * For the spine view: caller should call extractSubgraph first, then truncate.
783
+ */
784
+ function truncateGraph(graph, spine, limit) {
785
+ if (spine.length <= 1 || limit >= spine.length - 1) return {
786
+ graph,
787
+ elidedCount: 0,
788
+ spine
789
+ };
790
+ let earliestMarkerIdx = spine.length;
791
+ for (let i = 0; i < spine.length; i++) if (graph.nodeById.get(spine[i])?.markers?.some((m) => m.kind === "db" || m.kind === "contract")) {
792
+ earliestMarkerIdx = i;
793
+ break;
794
+ }
795
+ const markerDistance = spine.length - 1 - earliestMarkerIdx;
796
+ const effectiveEdges = Math.max(limit, markerDistance);
797
+ if (effectiveEdges >= spine.length - 1) return {
798
+ graph,
799
+ elidedCount: 0,
800
+ spine
801
+ };
802
+ const keepFromIdx = spine.length - 1 - effectiveEdges;
803
+ const truncatedSpine = spine.slice(keepFromIdx);
804
+ const visibleSpineSet = new Set(truncatedSpine);
805
+ const reachable = new Set(visibleSpineSet);
806
+ const queue = [...truncatedSpine];
807
+ while (queue.length > 0) {
808
+ const current = queue.shift();
809
+ for (const edge of graph.outgoing(current)) if (!reachable.has(edge.to)) {
810
+ reachable.add(edge.to);
811
+ queue.push(edge.to);
812
+ }
813
+ }
814
+ for (const n of graph.nodes) if (n.style === "detached") reachable.add(n.id);
815
+ const truncatedNodes = graph.nodes.filter((n) => reachable.has(n.id));
816
+ const truncatedEdges = graph.edges.filter((e) => reachable.has(e.from) && reachable.has(e.to));
817
+ const elidedCount = spine.length - 1 - effectiveEdges;
818
+ return {
819
+ graph: new RenderGraph(truncatedNodes, truncatedEdges),
820
+ elidedCount,
821
+ spine: truncatedSpine
822
+ };
823
+ }
824
+ /**
825
+ * After truncation the original root may not be in the visible graph.
826
+ * Find the first node with no incoming edges as a fallback root.
827
+ */
828
+ function findVisibleRoot(graph, layoutNodes) {
829
+ return layoutNodes.find((n) => !graph.incomingNodes.has(n.id))?.id ?? layoutNodes[0]?.id ?? "∅";
830
+ }
831
+ /**
832
+ * The main rendering pipeline: dagre layout → edge stamping → label
833
+ * placement → arrowheads → nodes → elided indicator → detached nodes.
834
+ *
835
+ * Called by {@link render} after optional truncation. Receives nodes/edges
836
+ * and produces the final multi-line string.
837
+ *
838
+ * @param graph - The graph to render (may include detached nodes, which are
839
+ * rendered below the main graph rather than laid out by dagre).
840
+ * @param options - Render options (rootId, spineTarget, colorize).
841
+ * @param elidedCount - If > 0, a `┊ (N earlier migrations)` indicator
842
+ * is stamped above the visible root node.
843
+ */
844
+ function layoutAndRender(graph, options, elidedCount = 0) {
845
+ const colors = buildColors(options.colorize ?? true);
846
+ const layoutNodes = graph.nodes.filter((n) => n.style !== "detached");
847
+ const layoutNodeIds = new Set(layoutNodes.map((n) => n.id));
848
+ const requestedRoot = options.rootId ?? layoutNodes[0]?.id ?? "∅";
849
+ const spineEdgeKeys = findSpineEdges(graph, layoutNodeIds.has(requestedRoot) ? requestedRoot : findVisibleRoot(graph, layoutNodes), options.spineTarget);
850
+ const g = new dagre.graphlib.Graph({ multigraph: true });
851
+ g.setGraph({
852
+ rankdir: "TB",
853
+ ranksep: 4,
854
+ nodesep: 6,
855
+ marginx: 2,
856
+ marginy: 1,
857
+ ...options.dagreOptions
858
+ });
859
+ g.setDefaultEdgeLabel(() => ({}));
860
+ for (const node of layoutNodes) {
861
+ const tagWidth = inlineTagsWidth(buildInlineTags(node.markers ?? [], colors));
862
+ g.setNode(node.id, {
863
+ width: node.id.length + 6 + tagWidth,
864
+ height: 1
865
+ });
866
+ }
867
+ const edgeNames = [];
868
+ for (let i = 0; i < graph.edges.length; i++) {
869
+ const edge = graph.edges[i];
870
+ const fromDetached = graph.nodeById.get(edge.from)?.style === "detached";
871
+ const toDetached = graph.nodeById.get(edge.to)?.style === "detached";
872
+ if (fromDetached || toDetached) {
873
+ edgeNames.push("");
874
+ continue;
875
+ }
876
+ const name = `e${i}`;
877
+ edgeNames.push(name);
878
+ g.setEdge(edge.from, edge.to, { label: edge.label ?? "" }, name);
879
+ }
880
+ dagre.layout(g);
881
+ const nodePos = /* @__PURE__ */ new Map();
882
+ for (const id of g.nodes()) {
883
+ const n = g.node(id);
884
+ nodePos.set(id, {
885
+ x: Math.round(n.x),
886
+ y: Math.round(n.y)
887
+ });
888
+ }
889
+ const grid = new CharGrid();
890
+ for (const node of layoutNodes) {
891
+ const pos = nodePos.get(node.id);
892
+ if (!pos) continue;
893
+ const tagWidth = inlineTagsWidth(buildInlineTags(node.markers ?? [], colors));
894
+ grid.reserveArea(pos.x - 1, pos.y, node.id.length + 4 + tagWidth);
895
+ }
896
+ const edgeEntries = [];
897
+ for (let i = 0; i < graph.edges.length; i++) {
898
+ const edge = graph.edges[i];
899
+ const name = edgeNames[i];
900
+ if (!name || !nodePos.has(edge.from) || !nodePos.has(edge.to)) continue;
901
+ const src = nodePos.get(edge.from);
902
+ const tgt = nodePos.get(edge.to);
903
+ const dagrePoints = g.edge({
904
+ v: edge.from,
905
+ w: edge.to,
906
+ name
907
+ })?.points ?? [];
908
+ const isBackward = tgt.y < src.y;
909
+ const isSpine = spineEdgeKeys.has(`${edge.from}→${edge.to}`);
910
+ const role = isBackward ? "backward" : isSpine ? "spine" : "branch";
911
+ const edgeColor = resolveHintColor(edge.colorHint, colors) ?? (role === "backward" ? colors.backward : role === "spine" ? colors.spine : colors.branch);
912
+ const priority = role === "backward" ? PRIORITY.backward : role === "spine" ? PRIORITY.spine : PRIORITY.branch;
913
+ edgeEntries.push({
914
+ idx: i,
915
+ edge,
916
+ dagrePoints,
917
+ src,
918
+ tgt,
919
+ role,
920
+ edgeColor,
921
+ priority
922
+ });
923
+ }
924
+ const drawnEdges = [];
925
+ for (const entry of edgeEntries) {
926
+ const { edge, dagrePoints, src, tgt, edgeColor, priority } = entry;
927
+ const { poly } = selectBestVariant(src, dagrePoints, tgt, edge.label, grid);
928
+ for (let j = 0; j < poly.length - 1; j++) {
929
+ const a = poly[j];
930
+ const b = poly[j + 1];
931
+ if (a.y === b.y) grid.markHorizontal(a.y, a.x, b.x, edgeColor, priority);
932
+ else if (a.x === b.x) grid.markVertical(a.x, a.y, b.y, edgeColor, priority);
933
+ }
934
+ drawnEdges.push({
935
+ edge,
936
+ poly,
937
+ role: entry.role,
938
+ srcY: src.y
939
+ });
940
+ }
941
+ const labelOrder = [...drawnEdges].map((de, i) => ({
942
+ ...de,
943
+ i
944
+ })).filter((de) => de.edge.label).sort((a, b) => (b.edge.label?.length ?? 0) - (a.edge.label?.length ?? 0));
945
+ for (const { edge, poly, role, srcY } of labelOrder) {
946
+ if (!edge.label) continue;
947
+ const labelPos = findLabelPlacement(poly, edge.label, grid, srcY);
948
+ if (labelPos) {
949
+ const labelColor = resolveHintColor(edge.colorHint, colors) ?? (role === "backward" ? colors.backward : role === "spine" ? colors.spine : colors.label);
950
+ grid.stampText(labelPos.x, labelPos.y, edge.label, labelColor);
951
+ }
952
+ }
953
+ for (const { edge, poly, role } of drawnEdges) {
954
+ if (poly.length < 2) continue;
955
+ const last = poly[poly.length - 1];
956
+ const prev = poly[poly.length - 2];
957
+ const edgeColor = resolveHintColor(edge.colorHint, colors) ?? (role === "backward" ? colors.backward : role === "spine" ? colors.spine : colors.branch);
958
+ let ax;
959
+ let ay;
960
+ let arrow;
961
+ if (prev.x === last.x) if (last.y > prev.y) {
962
+ ax = last.x;
963
+ ay = last.y - 1;
964
+ arrow = ARROW.down;
965
+ } else {
966
+ ax = last.x;
967
+ ay = last.y + 1;
968
+ arrow = ARROW.up;
969
+ }
970
+ else if (last.x > prev.x) {
971
+ ax = last.x - 1;
972
+ ay = last.y;
973
+ arrow = ARROW.right;
974
+ } else {
975
+ ax = last.x + 1;
976
+ ay = last.y;
977
+ arrow = ARROW.left;
978
+ }
979
+ if (ax !== void 0 && ay !== void 0 && arrow && !grid.hasText(ax, ay)) grid.stampText(ax, ay, arrow, edgeColor);
980
+ }
981
+ const spineNodeIds = /* @__PURE__ */ new Set();
982
+ for (const key of spineEdgeKeys) {
983
+ const [from, to] = key.split("→");
984
+ if (from) spineNodeIds.add(from);
985
+ if (to) spineNodeIds.add(to);
986
+ }
987
+ for (const node of layoutNodes) {
988
+ const pos = nodePos.get(node.id);
989
+ if (!pos) continue;
990
+ const isSpineNode = spineNodeIds.has(node.id);
991
+ const nodeColor = isSpineNode ? colors.spine : colors.branch;
992
+ grid.stampText(pos.x, pos.y, "○", nodeColor);
993
+ grid.stampText(pos.x + 1, pos.y, " ");
994
+ const hasMarkers = node.markers && node.markers.length > 0;
995
+ grid.stampText(pos.x + 2, pos.y, node.id, isSpineNode || hasMarkers ? bold : dim);
996
+ const tags = buildInlineTags(node.markers ?? [], colors);
997
+ if (tags.length > 0) {
998
+ let bx = pos.x + 2 + node.id.length;
999
+ for (const tag of tags) {
1000
+ grid.stampText(bx, pos.y, " ");
1001
+ bx++;
1002
+ grid.stampText(bx, pos.y, tag.text, tag.color);
1003
+ bx += tag.text.length;
1004
+ }
1005
+ }
1006
+ }
1007
+ if (elidedCount > 0) {
1008
+ const topNodeId = layoutNodes.find((n) => !graph.incomingNodes.has(n.id))?.id ?? layoutNodes[0]?.id;
1009
+ const rootPos = topNodeId ? nodePos.get(topNodeId) : void 0;
1010
+ if (rootPos) {
1011
+ const label = elidedCount === 1 ? "1 earlier migration" : `${elidedCount} earlier migrations`;
1012
+ const topY = rootPos.y - 3;
1013
+ grid.stampText(rootPos.x, topY, "┊", colors.label);
1014
+ grid.stampText(rootPos.x, topY + 1, "┊", colors.label);
1015
+ grid.stampText(rootPos.x + 2, topY + 1, `(${label})`, colors.label);
1016
+ grid.stampText(rootPos.x, topY + 2, "┊", colors.label);
1017
+ }
1018
+ }
1019
+ const detachedNodes = graph.nodes.filter((n) => n.style === "detached");
1020
+ if (detachedNodes.length > 0) {
1021
+ let bottomNodeX = nodePos.values().next().value?.x ?? 0;
1022
+ let bottomNodeY = -1;
1023
+ for (const [, pos] of nodePos) if (pos.y > bottomNodeY) {
1024
+ bottomNodeY = pos.y;
1025
+ bottomNodeX = pos.x;
1026
+ }
1027
+ const spineX = bottomNodeX;
1028
+ let bottomY = grid.getMaxY() + 1;
1029
+ for (const node of detachedNodes) {
1030
+ grid.stampText(spineX, bottomY, "┊", colors.branch);
1031
+ bottomY++;
1032
+ grid.stampText(spineX, bottomY, "◇", colors.branch);
1033
+ grid.stampText(spineX + 2, bottomY, node.id, dim);
1034
+ const tags = buildInlineTags(node.markers ?? [], colors);
1035
+ if (tags.length > 0) {
1036
+ let bx = spineX + 2 + node.id.length;
1037
+ for (const tag of tags) {
1038
+ grid.stampText(bx, bottomY, " ");
1039
+ bx++;
1040
+ grid.stampText(bx, bottomY, tag.text, tag.color);
1041
+ bx += tag.text.length;
1042
+ }
1043
+ }
1044
+ bottomY++;
1045
+ }
1046
+ }
1047
+ return grid.render();
1048
+ }
1049
+ /**
1050
+ * BFS to find the ordered node path from `rootId` to `targetId`.
1051
+ * Used for truncation — the spine path determines which edges to keep.
1052
+ *
1053
+ * Returns `[rootId]` if no path exists.
1054
+ */
1055
+ function findSpinePath(graph, rootId, targetId) {
1056
+ const visited = new Set([rootId]);
1057
+ const parent = /* @__PURE__ */ new Map();
1058
+ const queue = [rootId];
1059
+ while (queue.length > 0) {
1060
+ const current = queue.shift();
1061
+ if (current === targetId) {
1062
+ const path = [];
1063
+ let node = targetId;
1064
+ while (node !== rootId) {
1065
+ path.unshift(node);
1066
+ node = parent.get(node);
1067
+ }
1068
+ path.unshift(rootId);
1069
+ return path;
1070
+ }
1071
+ for (const edge of graph.outgoing(current)) if (!visited.has(edge.to)) {
1072
+ visited.add(edge.to);
1073
+ parent.set(edge.to, current);
1074
+ queue.push(edge.to);
1075
+ }
1076
+ }
1077
+ return [rootId];
1078
+ }
1079
+ /**
1080
+ * Render a graph with optional truncation.
1081
+ *
1082
+ * The caller decides what to pass in: the full graph for `--graph`, or a
1083
+ * subgraph extracted via {@link extractRelevantSubgraph} for the default view.
1084
+ */
1085
+ function render(graph, options) {
1086
+ if (options.limit !== void 0) {
1087
+ const { graph: truncated, elidedCount } = truncateGraph(graph, findSpinePath(graph, options.rootId ?? graph.nodes[0]?.id ?? "∅", options.spineTarget), options.limit);
1088
+ return layoutAndRender(truncated, options, elidedCount);
1089
+ }
1090
+ return layoutAndRender(graph, options);
1091
+ }
1092
+ const graphRenderer = { render };
1093
+ /** True if the graph is a single linear chain (no branching), ignoring detached nodes. */
1094
+ function isLinearGraph(graph) {
1095
+ for (const node of graph.nodes) {
1096
+ if (node.style === "detached") continue;
1097
+ if (graph.outgoing(node.id).length > 1) return false;
1098
+ }
1099
+ return true;
1100
+ }
1101
+
1102
+ //#endregion
1103
+ //#region src/commands/migration-status.ts
1104
+ function summarizeOps(ops) {
1105
+ if (ops.length === 0) return {
1106
+ summary: "0 ops",
1107
+ hasDestructive: false
1108
+ };
1109
+ const classes = /* @__PURE__ */ new Map();
1110
+ for (const op of ops) classes.set(op.operationClass, (classes.get(op.operationClass) ?? 0) + 1);
1111
+ const hasDestructive = classes.has("destructive");
1112
+ const count = ops.length;
1113
+ const noun = count === 1 ? "op" : "ops";
1114
+ if (classes.size === 1) return {
1115
+ summary: `${count} ${noun} (all ${[...classes.keys()][0]})`,
1116
+ hasDestructive
1117
+ };
1118
+ const destructiveCount = classes.get("destructive");
1119
+ if (destructiveCount) return {
1120
+ summary: `${count} ${noun} (${destructiveCount} destructive)`,
1121
+ hasDestructive
1122
+ };
1123
+ return {
1124
+ summary: `${count} ${noun} (${[...classes.entries()].map(([cls, n]) => `${n} ${cls}`).join(", ")})`,
1125
+ hasDestructive
1126
+ };
1127
+ }
1128
+ /**
1129
+ * Derive per-edge status across the full graph using path analysis.
1130
+ *
1131
+ * - **applied**: edge is on the path from root to the DB marker
1132
+ * - **pending**: edge is on the path from the DB marker to the target
1133
+ * (and the marker is reachable from root, i.e. it's on the same branch)
1134
+ * - **unreachable**: edge is on the path from root to the target but the DB
1135
+ * marker is on a different branch — `apply` can't reach these edges
1136
+ * without the DB first moving to this branch
1137
+ *
1138
+ * Returns statuses only for edges that have a known status (skips offline
1139
+ * and edges not on any relevant path).
1140
+ *
1141
+ * @internal Exported for testing only.
1142
+ */
1143
+ function deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode) {
1144
+ if (mode === "offline") return [];
1145
+ const edgeKey = (e) => `${e.from}\0${e.to}`;
1146
+ const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
1147
+ const appliedPath = markerHash !== void 0 ? findPath(graph, EMPTY_CONTRACT_HASH, markerHash) : null;
1148
+ const pendingPath = findPath(graph, effectiveMarker, targetHash);
1149
+ const targetPath = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
1150
+ const statuses = [];
1151
+ const assignedKeys = /* @__PURE__ */ new Set();
1152
+ if (appliedPath) for (const e of appliedPath) {
1153
+ assignedKeys.add(edgeKey(e));
1154
+ statuses.push({
1155
+ dirName: e.dirName,
1156
+ status: "applied"
1157
+ });
1158
+ }
1159
+ if (pendingPath) for (const e of pendingPath) {
1160
+ assignedKeys.add(edgeKey(e));
1161
+ statuses.push({
1162
+ dirName: e.dirName,
1163
+ status: "pending"
1164
+ });
1165
+ }
1166
+ if (contractHash !== EMPTY_CONTRACT_HASH && contractHash !== targetHash && graph.nodes.has(contractHash)) {
1167
+ const beyondTarget = findPath(graph, targetHash, contractHash);
1168
+ if (beyondTarget) {
1169
+ for (const e of beyondTarget) if (!assignedKeys.has(edgeKey(e))) {
1170
+ assignedKeys.add(edgeKey(e));
1171
+ statuses.push({
1172
+ dirName: e.dirName,
1173
+ status: "pending"
1174
+ });
1175
+ }
1176
+ }
1177
+ }
1178
+ if (targetPath) {
1179
+ for (const e of targetPath) if (!assignedKeys.has(edgeKey(e))) statuses.push({
1180
+ dirName: e.dirName,
1181
+ status: "unreachable"
1182
+ });
1183
+ }
1184
+ return statuses;
1185
+ }
1186
+ /**
1187
+ * @param mode — 'online' if we connected to the database, 'offline' otherwise
1188
+ * @param markerHash — the marker hash from the database, or undefined if no marker row / offline
1189
+ */
1190
+ function buildMigrationEntries(chain, packages, mode, markerHash, edgeStatuses) {
1191
+ const pkgByDirName = new Map(packages.map((p) => [p.dirName, p]));
1192
+ const statusByDirName = edgeStatuses ? new Map(edgeStatuses.map((e) => [e.dirName, e.status])) : void 0;
1193
+ const markerInChain = markerHash === void 0 || chain.some((e) => e.to === markerHash);
1194
+ const entries = [];
1195
+ let reachedMarker = mode === "online" && markerHash === void 0;
1196
+ for (const migration of chain) {
1197
+ const ops = pkgByDirName.get(migration.dirName)?.ops ?? [];
1198
+ const { summary, hasDestructive } = summarizeOps(ops);
1199
+ let status;
1200
+ const edgeStatus = statusByDirName?.get(migration.dirName);
1201
+ if (edgeStatus) status = edgeStatus;
1202
+ else if (mode === "offline" || !markerInChain) status = "unknown";
1203
+ else if (reachedMarker) status = "pending";
1204
+ else status = "applied";
1205
+ entries.push({
1206
+ dirName: migration.dirName,
1207
+ from: migration.from,
1208
+ to: migration.to,
1209
+ migrationId: migration.migrationId,
1210
+ operationCount: ops.length,
1211
+ operationSummary: summary,
1212
+ hasDestructive,
1213
+ status
1214
+ });
1215
+ if (!reachedMarker && migration.to === markerHash) reachedMarker = true;
1216
+ }
1217
+ return entries;
1218
+ }
1219
+ /**
1220
+ * Resolve the migration chain to display in status output.
1221
+ *
1222
+ * When offline or the marker is at EMPTY, the chain is simply the shortest
1223
+ * path from EMPTY to the target — all structural paths are equivalent per
1224
+ * the spec, so the deterministic shortest path is the canonical display.
1225
+ *
1226
+ * When online with a non-empty marker, the chain routes *through* the marker:
1227
+ * EMPTY→marker (applied history) + marker→target (pending edges). This ensures
1228
+ * the displayed chain includes the marker node so applied/pending status is
1229
+ * correct. Without this, BFS from EMPTY to target could pick a shortest path
1230
+ * that bypasses the marker entirely (e.g. in a diamond graph), causing the
1231
+ * marker to appear "diverged" when it isn't.
1232
+ */
1233
+ function resolveDisplayChain(graph, targetHash, markerHash) {
1234
+ if (markerHash === void 0) return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
1235
+ const toMarker = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);
1236
+ if (!toMarker) return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
1237
+ if (markerHash === targetHash) return toMarker;
1238
+ const fromMarker = findPath(graph, markerHash, targetHash);
1239
+ if (fromMarker) return [...toMarker, ...fromMarker];
1240
+ const toTarget = findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
1241
+ if (!toTarget) return null;
1242
+ const targetToMarker = findPath(graph, targetHash, markerHash);
1243
+ if (targetToMarker) return [...toTarget, ...targetToMarker];
1244
+ return toTarget;
1245
+ }
1246
+ const DEFAULT_LIMIT = 10;
1247
+ function determineLimit(opts) {
1248
+ if (opts.all) return;
1249
+ if (!opts.limit) return DEFAULT_LIMIT;
1250
+ const parsed = Number.parseInt(opts.limit, 10);
1251
+ if (Number.isNaN(parsed)) return DEFAULT_LIMIT;
1252
+ return parsed;
1253
+ }
1254
+ async function executeMigrationStatusCommand(options, flags, ui) {
1255
+ const config = await loadConfig(options.config);
1256
+ const { configPath, migrationsDir, migrationsRelative, refsPath } = resolveMigrationPaths(options.config, config);
1257
+ const dbConnection = options.db ?? config.db?.connection;
1258
+ const hasDriver = !!config.driver;
1259
+ let activeRefName;
1260
+ let activeRefHash;
1261
+ let allRefs = {};
1262
+ try {
1263
+ allRefs = await readRefs(refsPath);
1264
+ } catch (error) {
1265
+ if (MigrationToolsError.is(error)) return notOk(errorRuntime(error.message, {
1266
+ why: error.why,
1267
+ fix: error.fix,
1268
+ meta: { code: error.code }
1269
+ }));
1270
+ throw error;
1271
+ }
1272
+ if (options.ref) {
1273
+ activeRefName = options.ref;
1274
+ try {
1275
+ activeRefHash = resolveRef(allRefs, activeRefName);
1276
+ } catch (error) {
1277
+ if (MigrationToolsError.is(error)) return notOk(errorRuntime(error.message, {
1278
+ why: error.why,
1279
+ fix: error.fix,
1280
+ meta: { code: error.code }
1281
+ }));
1282
+ throw error;
1283
+ }
1284
+ }
1285
+ const statusRefs = Object.entries(allRefs).map(([name, hash]) => ({
1286
+ name,
1287
+ hash,
1288
+ active: name === activeRefName
1289
+ }));
1290
+ if (!flags.json && !flags.quiet) {
1291
+ const details = [{
1292
+ label: "config",
1293
+ value: configPath
1294
+ }, {
1295
+ label: "migrations",
1296
+ value: migrationsRelative
1297
+ }];
1298
+ if (dbConnection && hasDriver) details.push({
1299
+ label: "database",
1300
+ value: maskConnectionUrl(String(dbConnection))
1301
+ });
1302
+ if (activeRefName) details.push({
1303
+ label: "ref",
1304
+ value: activeRefName
1305
+ });
1306
+ const header = formatStyledHeader({
1307
+ command: "migration status",
1308
+ description: "Show migration history and applied status",
1309
+ details,
1310
+ flags
1311
+ });
1312
+ ui.stderr(header);
1313
+ }
1314
+ const diagnostics = [];
1315
+ let contractHash = EMPTY_CONTRACT_HASH;
1316
+ try {
1317
+ contractHash = (await readContractEnvelope(config)).storageHash;
1318
+ } catch (error) {
1319
+ diagnostics.push({
1320
+ code: "CONTRACT.UNREADABLE",
1321
+ severity: "warn",
1322
+ message: `Could not read contract: ${error instanceof Error ? error.message : "unknown error"}`,
1323
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"]
1324
+ });
1325
+ }
1326
+ let attested;
1327
+ let graph;
1328
+ try {
1329
+ ({bundles: attested, graph} = await loadMigrationBundles(migrationsDir));
1330
+ } catch (error) {
1331
+ if (MigrationToolsError.is(error)) return notOk(errorRuntime(error.message, {
1332
+ why: error.why,
1333
+ fix: error.fix,
1334
+ meta: { code: error.code }
1335
+ }));
1336
+ return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}` }));
1337
+ }
1338
+ if (attested.length === 0) {
1339
+ if (contractHash !== EMPTY_CONTRACT_HASH) diagnostics.push({
1340
+ code: "CONTRACT.AHEAD",
1341
+ severity: "warn",
1342
+ message: "No migration exists for the current contract",
1343
+ hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"]
1344
+ });
1345
+ return ok({
1346
+ ok: true,
1347
+ mode: dbConnection && hasDriver ? "online" : "offline",
1348
+ migrations: [],
1349
+ targetHash: EMPTY_CONTRACT_HASH,
1350
+ contractHash,
1351
+ summary: "No migrations found",
1352
+ diagnostics
1353
+ });
1354
+ }
1355
+ let targetHash;
1356
+ if (activeRefHash) targetHash = activeRefHash;
1357
+ else if (graph.nodes.has(contractHash)) targetHash = contractHash;
1358
+ else {
1359
+ const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);
1360
+ if (leaves.length === 1) targetHash = leaves[0];
1361
+ else diagnostics.push({
1362
+ code: "MIGRATION.DIVERGED",
1363
+ severity: "warn",
1364
+ message: "There are multiple valid migration paths — you must select a target",
1365
+ hints: ["Use '--ref <name>' to select a target", "Or 'prisma-next migration ref set <name> <hash>' to create one"]
1366
+ });
1367
+ }
1368
+ let markerHash;
1369
+ let mode = "offline";
1370
+ if (dbConnection && hasDriver) {
1371
+ const client = createControlClient({
1372
+ family: config.family,
1373
+ target: config.target,
1374
+ adapter: config.adapter,
1375
+ driver: config.driver,
1376
+ extensionPacks: config.extensionPacks ?? []
1377
+ });
1378
+ try {
1379
+ await client.connect(dbConnection);
1380
+ markerHash = (await client.readMarker())?.storageHash;
1381
+ mode = "online";
1382
+ } catch {
1383
+ if (!flags.json && !flags.quiet) ui.warn("Could not connect to database — showing offline status");
1384
+ } finally {
1385
+ await client.close();
1386
+ }
1387
+ }
1388
+ if (mode === "online" && markerHash !== void 0 && !graph.nodes.has(markerHash) && markerHash !== contractHash) {
1389
+ const hints = [];
1390
+ if (graph.nodes.has(contractHash)) hints.push("Run 'prisma-next db sign' to overwrite the marker if the database already matches the contract", "Run 'prisma-next db update' to push the current contract to the database", "Run 'prisma-next contract infer' to make your contract match the database", "Run 'prisma-next db verify' to inspect the database state");
1391
+ else hints.push("Run 'prisma-next db update' to push the current contract to the database", "Run 'prisma-next contract infer' to make your contract match the database", "Run 'prisma-next db verify' to inspect the database state");
1392
+ diagnostics.push({
1393
+ code: "MIGRATION.MARKER_NOT_IN_HISTORY",
1394
+ severity: "warn",
1395
+ message: "Database was updated outside the migration system (marker does not match any migration)",
1396
+ hints
1397
+ });
1398
+ return ok({
1399
+ ok: true,
1400
+ mode,
1401
+ migrations: [],
1402
+ targetHash: EMPTY_CONTRACT_HASH,
1403
+ contractHash,
1404
+ summary: `${attested.length} migration(s) on disk`,
1405
+ diagnostics,
1406
+ markerHash,
1407
+ ...statusRefs.length > 0 ? { refs: statusRefs } : {}
1408
+ });
1409
+ }
1410
+ if (mode === "online" && markerHash === void 0) diagnostics.push({
1411
+ code: "MIGRATION.NO_MARKER",
1412
+ severity: "warn",
1413
+ message: "Database has not been initialized — no migration marker found",
1414
+ hints: ["Run 'prisma-next migration apply' to apply pending migrations"]
1415
+ });
1416
+ if (targetHash && contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash) && markerHash !== contractHash) diagnostics.push({
1417
+ code: "CONTRACT.AHEAD",
1418
+ severity: "warn",
1419
+ message: "Contract has changed since the last migration was planned",
1420
+ hints: ["Run 'prisma-next migration plan' to generate a migration for the current contract"]
1421
+ });
1422
+ if (!targetHash) return ok({
1423
+ ok: true,
1424
+ mode,
1425
+ migrations: [],
1426
+ targetHash: EMPTY_CONTRACT_HASH,
1427
+ contractHash,
1428
+ summary: `${attested.length} migration(s) on disk`,
1429
+ diagnostics,
1430
+ ...ifDefined("markerHash", markerHash),
1431
+ ...statusRefs.length > 0 ? { refs: statusRefs } : {},
1432
+ graph,
1433
+ bundles: attested,
1434
+ diverged: true
1435
+ });
1436
+ const chain = resolveDisplayChain(graph, targetHash, markerHash);
1437
+ if (!chain) return notOk(errorRuntime("Cannot reconstruct migration history", {
1438
+ why: `No path from ${EMPTY_CONTRACT_HASH} to target ${targetHash}`,
1439
+ fix: "The migration history may have gaps. Check the migrations directory for missing or corrupted packages."
1440
+ }));
1441
+ const edgeStatuses = deriveEdgeStatuses(graph, targetHash, contractHash, markerHash, mode);
1442
+ const entries = buildMigrationEntries(chain, attested, mode, markerHash, edgeStatuses);
1443
+ const pendingCount = edgeStatuses.filter((e) => e.status === "pending").length;
1444
+ const appliedCount = edgeStatuses.filter((e) => e.status === "applied").length;
1445
+ let summary;
1446
+ if (mode === "online") if (markerHash !== void 0 && !graph.nodes.has(markerHash) && markerHash === contractHash) summary = `${attested.length} migration(s) on disk`;
1447
+ else if (activeRefHash && markerHash !== void 0) summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName);
1448
+ else if (pendingCount === 0) summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? "s" : ""} applied)`;
1449
+ else if (markerHash === void 0) summary = `${pendingCount} pending migration(s) — database has no marker`;
1450
+ else summary = `${pendingCount} pending migration(s) — run 'prisma-next migration apply' to apply`;
1451
+ else summary = `${entries.length} migration(s) on disk`;
1452
+ if (mode === "online") if (markerHash !== void 0 && !graph.nodes.has(markerHash) && markerHash === contractHash) diagnostics.push({
1453
+ code: "MIGRATION.MARKER_NOT_IN_HISTORY",
1454
+ severity: "warn",
1455
+ message: "Database matches the current contract but was updated directly (not via migration apply)",
1456
+ hints: ["Run 'prisma-next migration plan' to plan a migration to your current contract"]
1457
+ });
1458
+ else if (pendingCount > 0) diagnostics.push({
1459
+ code: "MIGRATION.DATABASE_BEHIND",
1460
+ severity: "info",
1461
+ message: `${pendingCount} migration(s) pending`,
1462
+ hints: ["Run 'prisma-next migration apply' to apply pending migrations"]
1463
+ });
1464
+ else diagnostics.push({
1465
+ code: "MIGRATION.UP_TO_DATE",
1466
+ severity: "info",
1467
+ message: "Database is up to date",
1468
+ hints: []
1469
+ });
1470
+ let pathDecision;
1471
+ if (mode === "online" && markerHash !== void 0) {
1472
+ const decision = findPathWithDecision(graph, markerHash, targetHash, activeRefName);
1473
+ if (decision) pathDecision = toPathDecisionResult(decision);
1474
+ }
1475
+ return ok({
1476
+ ok: true,
1477
+ mode,
1478
+ migrations: entries,
1479
+ targetHash,
1480
+ contractHash,
1481
+ summary,
1482
+ diagnostics,
1483
+ ...ifDefined("markerHash", markerHash),
1484
+ ...statusRefs.length > 0 ? { refs: statusRefs } : {},
1485
+ ...ifDefined("pathDecision", pathDecision),
1486
+ graph,
1487
+ bundles: attested,
1488
+ edgeStatuses,
1489
+ ...ifDefined("activeRefHash", activeRefHash),
1490
+ ...ifDefined("activeRefName", activeRefName)
1491
+ });
1492
+ }
1493
+ function createMigrationStatusCommand() {
1494
+ const command = new Command("status");
1495
+ setCommandDescriptions(command, "Show migration history and applied status", "Displays the migration history in order. When a database connection\nis available, shows which migrations are applied and which are pending.\nWithout a database connection, shows the history from disk only.");
1496
+ setCommandExamples(command, ["prisma-next migration status", "prisma-next migration status --db $DATABASE_URL"]);
1497
+ addGlobalOptions(command).option("--db <url>", "Database connection string").option("--config <path>", "Path to prisma-next.config.ts").option("--ref <name>", "Target ref name from migrations/refs.json").option("--graph", "Show the full migration graph with all branches").option("--limit <n>", "Maximum number of migrations to display (default: 10)").option("--all", "Show full history (disables truncation)").action(async (options) => {
1498
+ const flags = parseGlobalFlags(options);
1499
+ const ui = new TerminalUI({
1500
+ color: flags.color,
1501
+ interactive: flags.interactive
1502
+ });
1503
+ const exitCode = handleResult(await executeMigrationStatusCommand(options, flags, ui), flags, ui, (statusResult) => {
1504
+ if (flags.json) {
1505
+ const { graph: _g, bundles: _b, edgeStatuses: _es, activeRefHash: _arh, activeRefName: _arn, diverged: _d, ...jsonResult } = statusResult;
1506
+ ui.output(JSON.stringify(jsonResult, null, 2));
1507
+ } else if (!flags.quiet) {
1508
+ const colorize = flags.color !== false;
1509
+ if (statusResult.graph) {
1510
+ const limit = determineLimit(options);
1511
+ const renderInput = migrationGraphToRenderInput({
1512
+ graph: statusResult.graph,
1513
+ mode: statusResult.mode,
1514
+ markerHash: statusResult.markerHash,
1515
+ contractHash: statusResult.contractHash,
1516
+ refs: statusResult.refs,
1517
+ activeRefHash: statusResult.activeRefHash,
1518
+ activeRefName: statusResult.activeRefName,
1519
+ edgeStatuses: statusResult.edgeStatuses
1520
+ });
1521
+ const graphToRender = options.graph || statusResult.diverged ? renderInput.graph : extractRelevantSubgraph(renderInput.graph, renderInput.relevantPaths);
1522
+ const renderOptions = {
1523
+ ...renderInput.options,
1524
+ colorize,
1525
+ ...ifDefined("limit", limit),
1526
+ ...isLinearGraph(graphToRender) ? { dagreOptions: { ranksep: 1 } } : {}
1527
+ };
1528
+ const graphOutput = graphRenderer.render(graphToRender, renderOptions);
1529
+ ui.log(graphOutput);
1530
+ if (statusResult.mode === "online") ui.log(formatLegend(colorize));
1531
+ }
1532
+ ui.log("");
1533
+ ui.log(formatStatusSummary(statusResult, colorize));
1534
+ }
1535
+ });
1536
+ process.exit(exitCode);
1537
+ });
1538
+ return command;
1539
+ }
1540
+ function formatLegend(colorize) {
1541
+ const c = (fn, s) => colorize ? fn(s) : s;
1542
+ return c(dim, [
1543
+ `${c(cyan, "✓")} applied`,
1544
+ `${c(yellow, "⧗")} pending`,
1545
+ `${c(magenta, "✗")} unreachable`
1546
+ ].join(" "));
1547
+ }
1548
+ function formatStatusSummary(result, colorize) {
1549
+ const c = (fn, s) => colorize ? fn(s) : s;
1550
+ const lines = [];
1551
+ const hasUnknown = result.migrations.some((e) => e.status === "unknown");
1552
+ const pendingCount = result.migrations.filter((e) => e.status === "pending").length;
1553
+ const hasWarnings = result.diagnostics?.some((d) => d.severity === "warn") ?? false;
1554
+ if (result.mode === "online") if (hasUnknown || hasWarnings) lines.push(`${c(yellow, "⚠")} ${result.summary}`);
1555
+ else if (pendingCount === 0) lines.push(`${c(cyan, "✔")} ${result.summary}`);
1556
+ else lines.push(`${c(yellow, "⧗")} ${result.summary}`);
1557
+ else lines.push(result.summary);
1558
+ const warnings = result.diagnostics?.filter((d) => d.severity === "warn") ?? [];
1559
+ for (const diag of warnings) {
1560
+ lines.push(`${c(yellow, "⚠")} ${diag.message}`);
1561
+ for (const hint of diag.hints) lines.push(` ${c(dim, hint)}`);
1562
+ }
1563
+ return lines.join("\n");
1564
+ }
1565
+ function summarizeRefDistance(graph, markerHash, refHash, refName) {
1566
+ if (markerHash === refHash) return `At ref "${refName}" target`;
1567
+ const pathToRef = findPath(graph, markerHash, refHash);
1568
+ if (pathToRef) return `${pathToRef.length} migration(s) behind ref "${refName}"`;
1569
+ const pathFromRef = findPath(graph, refHash, markerHash);
1570
+ if (pathFromRef) return `${pathFromRef.length} migration(s) ahead of ref "${refName}"`;
1571
+ return `No path between database marker and ref "${refName}" target`;
1572
+ }
1573
+
1574
+ //#endregion
1575
+ export { deriveEdgeStatuses as n, createMigrationStatusCommand as t };
1576
+ //# sourceMappingURL=migration-status-B7OVZ-Ka.mjs.map