@rebasepro/server-postgres 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/BranchService-ByLfM_Tt.js +360 -0
  4. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  5. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  6. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  7. package/dist/arg-Dni7MzLB.js +87 -0
  8. package/dist/arg-Dni7MzLB.js.map +1 -0
  9. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  10. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  11. package/dist/backup-argv.d.ts +19 -0
  12. package/dist/backup-cli-oM9WQVlV.js +408 -0
  13. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  14. package/dist/backup-service-Br3tqwsy.js +715 -0
  15. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  16. package/dist/branch-argv.d.ts +28 -0
  17. package/dist/branch-prune.d.ts +77 -0
  18. package/dist/cli-collections-path.d.ts +31 -0
  19. package/dist/cli-errors-DxN4yzdH.js +657 -0
  20. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  21. package/dist/cli-errors.d.ts +123 -0
  22. package/dist/cli-flags.d.ts +101 -0
  23. package/dist/cli-helpers.d.ts +94 -10
  24. package/dist/cli-output-CNdMql-L.js +47 -0
  25. package/dist/cli-output-CNdMql-L.js.map +1 -0
  26. package/dist/cli.js +2020 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/collection-index-BHgT_TKd.js +276 -0
  29. package/dist/collection-index-BHgT_TKd.js.map +1 -0
  30. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  31. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  32. package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
  33. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  34. package/dist/databasePoolManager.d.ts +27 -0
  35. package/dist/doctor-D7HWmGzs.js +974 -0
  36. package/dist/doctor-D7HWmGzs.js.map +1 -0
  37. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
  38. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  39. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  40. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  41. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  42. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  43. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  44. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  45. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  46. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
  48. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  49. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  50. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.es.js +7423 -1133
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  55. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  56. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  57. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  58. package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
  59. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  60. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
  61. package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
  62. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  63. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  64. package/dist/schema/column-type-drift.d.ts +79 -0
  65. package/dist/schema/doctor-cli.js +129 -0
  66. package/dist/schema/doctor-cli.js.map +1 -0
  67. package/dist/schema/doctor.d.ts +42 -0
  68. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  69. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  70. package/dist/schema/generate-drizzle-schema.js +24 -0
  71. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  72. package/dist/schema/generate-next-step.d.ts +30 -0
  73. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  74. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  75. package/dist/schema/generate-postgres-ddl.js +138 -0
  76. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  77. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  78. package/dist/schema/introspect-db-queries.d.ts +34 -0
  79. package/dist/schema/introspect-db.js +866 -0
  80. package/dist/schema/introspect-db.js.map +1 -0
  81. package/dist/security/rls-enforcement.d.ts +63 -2
  82. package/dist/services/BranchService.d.ts +86 -1
  83. package/dist/services/collection-helpers.d.ts +1 -1
  84. package/dist/services/realtimeService.d.ts +30 -2
  85. package/dist/services/row-pipeline.d.ts +12 -1
  86. package/dist/source-Br7L7GOI.js +410 -0
  87. package/dist/source-Br7L7GOI.js.map +1 -0
  88. package/dist/utils/pg-error-utils.d.ts +3 -1
  89. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  90. package/dist/vector-index-DkWxm2de.js +309 -0
  91. package/dist/vector-index-DkWxm2de.js.map +1 -0
  92. package/package.json +32 -27
  93. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  94. package/dist/collection-index-BRUg10H5.js +0 -5690
  95. package/dist/collection-index-BRUg10H5.js.map +0 -1
  96. package/dist/connection-GOKU3Hu5.js.map +0 -1
  97. package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
  98. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  99. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  100. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  101. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  102. package/dist/src-DiB5RP2Z.js +0 -541
  103. package/dist/src-DiB5RP2Z.js.map +0 -1
  104. package/dist/websocket-BZ4H5wUz.js +0 -8198
  105. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  106. package/src/PostgresAdapter.ts +0 -91
  107. package/src/PostgresBackendDriver.ts +0 -1857
  108. package/src/PostgresBootstrapper.ts +0 -1157
  109. package/src/auth/ensure-tables.ts +0 -896
  110. package/src/auth/schema-version.ts +0 -260
  111. package/src/auth/services.ts +0 -1610
  112. package/src/backup/backup-cli.ts +0 -520
  113. package/src/backup/backup-cron.ts +0 -212
  114. package/src/backup/backup-logic.ts +0 -93
  115. package/src/backup/backup-service.ts +0 -485
  116. package/src/backup/index.ts +0 -12
  117. package/src/backup/pg-tools.ts +0 -403
  118. package/src/backup/retention.ts +0 -86
  119. package/src/cli-errors.ts +0 -265
  120. package/src/cli-helpers.ts +0 -569
  121. package/src/cli-output.ts +0 -43
  122. package/src/cli.ts +0 -1339
  123. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  124. package/src/collections/buildRegistry.ts +0 -68
  125. package/src/collections/validate-relations.ts +0 -402
  126. package/src/connection.ts +0 -333
  127. package/src/data-transformer.ts +0 -845
  128. package/src/databasePoolManager.ts +0 -95
  129. package/src/history/HistoryService.ts +0 -254
  130. package/src/history/ensure-history-table.ts +0 -53
  131. package/src/index.ts +0 -19
  132. package/src/interfaces.ts +0 -60
  133. package/src/module-dir.ts +0 -7
  134. package/src/schema/atlas-argv.ts +0 -94
  135. package/src/schema/auth-schema.ts +0 -298
  136. package/src/schema/auth-users-columns.ts +0 -131
  137. package/src/schema/carved-out-migration.ts +0 -404
  138. package/src/schema/classify-change.ts +0 -436
  139. package/src/schema/collection-index.ts +0 -427
  140. package/src/schema/collections-schema-version.ts +0 -103
  141. package/src/schema/destructive-sql.ts +0 -94
  142. package/src/schema/doctor-cli.ts +0 -66
  143. package/src/schema/doctor-policy-checks.ts +0 -105
  144. package/src/schema/doctor.ts +0 -837
  145. package/src/schema/drizzle-ddl.ts +0 -33
  146. package/src/schema/dynamic-tables.test.ts +0 -302
  147. package/src/schema/dynamic-tables.ts +0 -293
  148. package/src/schema/ensure-collection-policies.ts +0 -198
  149. package/src/schema/ensure-collection-tables.test.ts +0 -518
  150. package/src/schema/ensure-collection-tables.ts +0 -1267
  151. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  152. package/src/schema/generate-drizzle-schema.ts +0 -143
  153. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  154. package/src/schema/generate-postgres-ddl.ts +0 -180
  155. package/src/schema/generate-schema-commit.ts +0 -267
  156. package/src/schema/generated-schema-staleness.ts +0 -284
  157. package/src/schema/introspect-db-constraints.ts +0 -385
  158. package/src/schema/introspect-db-inference.ts +0 -255
  159. package/src/schema/introspect-db-logic.ts +0 -1387
  160. package/src/schema/introspect-db-naming.ts +0 -15
  161. package/src/schema/introspect-db-project.ts +0 -78
  162. package/src/schema/introspect-db-queries.ts +0 -326
  163. package/src/schema/introspect-db-structure.ts +0 -670
  164. package/src/schema/introspect-db-types.ts +0 -56
  165. package/src/schema/introspect-db.ts +0 -257
  166. package/src/schema/introspect-runtime.test.ts +0 -260
  167. package/src/schema/introspect-runtime.ts +0 -327
  168. package/src/schema/non-sql-collections.test.ts +0 -131
  169. package/src/schema/rls-bootstrap-sql.ts +0 -288
  170. package/src/schema/search-column.ts +0 -643
  171. package/src/schema/test-schema.ts +0 -11
  172. package/src/schema/vector-index.ts +0 -491
  173. package/src/security/anonymous-grants.test.ts +0 -73
  174. package/src/security/policy-drift.test.ts +0 -435
  175. package/src/security/policy-drift.ts +0 -473
  176. package/src/security/rls-enforcement.ts +0 -505
  177. package/src/services/BranchService.ts +0 -326
  178. package/src/services/FetchService.ts +0 -2115
  179. package/src/services/PersistService.ts +0 -557
  180. package/src/services/RelationService.ts +0 -978
  181. package/src/services/RelationWriteService.ts +0 -653
  182. package/src/services/cdc/CdcListener.ts +0 -103
  183. package/src/services/cdc/junction-tables.ts +0 -91
  184. package/src/services/cdc/trigger-cdc.ts +0 -173
  185. package/src/services/channel-bus/ChannelBus.ts +0 -44
  186. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  187. package/src/services/channel-bus/index.ts +0 -114
  188. package/src/services/channel-history.ts +0 -424
  189. package/src/services/channel-presence.ts +0 -185
  190. package/src/services/collection-helpers.ts +0 -420
  191. package/src/services/dataService.ts +0 -256
  192. package/src/services/index.ts +0 -14
  193. package/src/services/junction-writes.ts +0 -295
  194. package/src/services/nested-path.ts +0 -145
  195. package/src/services/pg-notify-listener.ts +0 -151
  196. package/src/services/realtimeService.ts +0 -2545
  197. package/src/services/row-pipeline.ts +0 -240
  198. package/src/services/write-denial.ts +0 -55
  199. package/src/types.ts +0 -4
  200. package/src/utils/connection-string.ts +0 -58
  201. package/src/utils/drizzle-conditions.ts +0 -2743
  202. package/src/utils/pg-array-null-patch.ts +0 -42
  203. package/src/utils/pg-error-utils.ts +0 -391
  204. package/src/websocket.ts +0 -855
package/dist/cli.js ADDED
@@ -0,0 +1,2020 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { a as __toESM } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { i as diagnoseDbError, r as diagnoseAtlasFailure, s as reportCommandFailure, t as checkDatabaseConnectivity } from "./cli-errors-DxN4yzdH.js";
5
+ import { n as outError, r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
6
+ import { C as resolveLocalBin, S as readVectorDdl, T as forLibpq, _ as getSearchExcludes, a as ExcludeIntrospectionError, b as promptConfirm, c as describeBuildScriptRemedy, d as detectProjectPackageManager, f as diagnoseMissingBin, g as getForeignIndexExcludes, h as getDevDatabaseUrl, l as describeDevAddCommand, m as ensureDevDatabaseExists, o as applySearchDdl, p as dropDevDatabase, r as dropLegacyAuthSchema, s as applyVectorDdl, t as RLS_BOOTSTRAP_SQL, u as describeReinstallCommand, v as getTableExcludes, w as execa, x as readSearchDdl, y as getVectorExcludes } from "./rls-bootstrap-sql-DLDnUjtx.js";
7
+ import { t as chalk } from "./source-Br7L7GOI.js";
8
+ import { t as require_arg } from "./arg-Dni7MzLB.js";
9
+ import { logger } from "@rebasepro/server";
10
+ import { formatRelativeTime } from "@rebasepro/utils";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ //#region src/schema/destructive-sql.ts
15
+ var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
16
+ /**
17
+ * Pure helpers that classify an Atlas declarative-apply plan as destructive
18
+ * or not, and decide what `rebase db push` should do about it.
19
+ *
20
+ * `db push` runs `atlas schema apply` to make the live database match the
21
+ * generated `schema.sql`. Removing a collection field compiles to
22
+ * `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
23
+ * We first run the apply with `--dry-run` to obtain the planned SQL, scan it
24
+ * here, and refuse to auto-approve anything destructive without an explicit
25
+ * opt-in.
26
+ *
27
+ * Everything in this file is side-effect free so it can be unit-tested
28
+ * without Atlas or a database.
29
+ */
30
+ /**
31
+ * SQL fragments that destroy data or data-bearing objects. Matched
32
+ * case-insensitively against each statement of the plan. `IF EXISTS` /
33
+ * whitespace variations are tolerated by the regexes below.
34
+ */
35
+ var DESTRUCTIVE_PATTERNS = [
36
+ {
37
+ label: "DROP TABLE",
38
+ re: /\bDROP\s+TABLE\b/i
39
+ },
40
+ {
41
+ label: "DROP COLUMN",
42
+ re: /\bDROP\s+COLUMN\b/i
43
+ },
44
+ {
45
+ label: "DROP SCHEMA",
46
+ re: /\bDROP\s+SCHEMA\b/i
47
+ },
48
+ {
49
+ label: "DROP VIEW",
50
+ re: /\bDROP\s+(MATERIALIZED\s+)?VIEW\b/i
51
+ },
52
+ {
53
+ label: "DROP TYPE",
54
+ re: /\bDROP\s+TYPE\b/i
55
+ },
56
+ {
57
+ label: "TRUNCATE",
58
+ re: /\bTRUNCATE\b/i
59
+ }
60
+ ];
61
+ /**
62
+ * Split a SQL script into individual statements, dropping blank lines and
63
+ * `--` comment lines. Deliberately simple: Atlas emits one plain statement
64
+ * per `;`, without string literals that contain semicolons in a schema DDL
65
+ * plan, so a naive split is safe and keeps this dependency-free.
66
+ */
67
+ function splitSqlStatements(sql) {
68
+ return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
69
+ }
70
+ /**
71
+ * Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
72
+ * the statements that would destroy data. An empty array means the plan is
73
+ * safe to auto-approve.
74
+ */
75
+ function detectDestructiveStatements(planSql) {
76
+ const found = [];
77
+ for (const statement of splitSqlStatements(planSql)) for (const { label, re } of DESTRUCTIVE_PATTERNS) if (re.test(statement)) {
78
+ found.push({
79
+ statement,
80
+ kind: label
81
+ });
82
+ break;
83
+ }
84
+ return found;
85
+ }
86
+ /**
87
+ * Decide how `db push` should proceed given the plan's destructiveness and
88
+ * the invocation context.
89
+ *
90
+ * - No destructive statements → `apply` (safe to auto-approve).
91
+ * - Destructive + `--allow-destructive` → `apply` (operator opted in).
92
+ * - Destructive + interactive TTY → `confirm` (prompt before applying).
93
+ * - Destructive + non-interactive → `refuse` (never silently drop data in
94
+ * CI / scripts / agents).
95
+ */
96
+ function decidePushSafety(opts) {
97
+ if (opts.destructiveCount === 0) return "apply";
98
+ if (opts.allowDestructive) return "apply";
99
+ return opts.interactive ? "confirm" : "refuse";
100
+ }
101
+ //#endregion
102
+ //#region src/schema/carved-out-migration.ts
103
+ /**
104
+ * Keep the objects Rebase carved out of Atlas's view out of the migrations
105
+ * Atlas writes.
106
+ *
107
+ * `db push` protects them with `--exclude`. `atlas migrate diff` has no such
108
+ * flag — measured on the pinned 1.2.3, `--exclude` is rejected outright with
109
+ * `unknown flag`, and the `exclude` attribute of an `atlas.hcl` `env` block is
110
+ * accepted and then silently ignored on that path. So the diff sees the search
111
+ * column that the migration directory's own appended DDL created when it
112
+ * replayed, does not see it in `schema.sql`, and plans a drop:
113
+ *
114
+ * ALTER TABLE "public"."talents" DROP COLUMN "search_vector";
115
+ *
116
+ * That statement is removed here, after Atlas has written the file and before
117
+ * anything is appended to it — so the input is always Atlas's own plain output,
118
+ * never the dollar-quoted helper bodies that get appended afterwards.
119
+ *
120
+ * The drop does not arrive alone. Atlas folds every change to one table into a
121
+ * single statement, so a real edit in the same run produces
122
+ *
123
+ * ALTER TABLE "public"."talents"
124
+ * DROP COLUMN "search_vector", DROP COLUMN "interests", ADD COLUMN "headline" text NULL;
125
+ *
126
+ * and dropping the whole statement would silently throw away a migration the
127
+ * project asked for. The removal is therefore per *clause*.
128
+ *
129
+ * Fail-safe throughout: a statement that mentions a carved-out object but does
130
+ * not match a shape this understands is left exactly as it is and reported as
131
+ * `unhandled`, so the caller can say so out loud. A spurious drop the developer
132
+ * can see beats a migration quietly rewritten wrong.
133
+ */
134
+ /** Anything Postgres accepts in an unquoted identifier past ASCII. */
135
+ var IDENT = "(?:\"(?:[^\"]|\"\")*\"|[A-Za-z_\\u0080-\\uffff][\\w$\\u0080-\\uffff]*)";
136
+ var QUALIFIED = `${IDENT}(?:\\s*\\.\\s*${IDENT})*`;
137
+ var ALTER_TABLE_RE = new RegExp(`^ALTER\\s+TABLE\\s+(?:ONLY\\s+)?(${QUALIFIED})\\s+([\\s\\S]+)$`, "i");
138
+ var DROP_COLUMN_RE = new RegExp(`^DROP\\s+(?:COLUMN\\s+)?(?:IF\\s+EXISTS\\s+)?(${IDENT})$`, "i");
139
+ var DROP_INDEX_RE = new RegExp(`^DROP\\s+INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+EXISTS\\s+)?(${QUALIFIED})$`, "i");
140
+ /**
141
+ * Does this statement destroy anything at all?
142
+ *
143
+ * The gate on reporting. A statement that merely *names* a carved-out object —
144
+ * `COMMENT ON COLUMN … "search_vector"` — is not a problem and must not be
145
+ * reported as one, because an unrewritable drop stops the caller outright.
146
+ */
147
+ var MENTIONS_DROP = /\bDROP\b/i;
148
+ /**
149
+ * Split `schema.table.object` patterns. Anything not in three parts is
150
+ * ignored rather than guessed at — the two-part form is the one Atlas itself
151
+ * silently mis-reads, and repeating that mistake here would be worse than
152
+ * doing nothing.
153
+ */
154
+ function parseExcludePatterns(patterns) {
155
+ const parsed = [];
156
+ for (const pattern of patterns) {
157
+ const parts = pattern.split(".");
158
+ if (parts.length !== 3) continue;
159
+ if (parts.some((p) => p.length === 0)) continue;
160
+ parsed.push({
161
+ schema: parts[0],
162
+ table: parts[1],
163
+ object: parts[2]
164
+ });
165
+ }
166
+ return parsed;
167
+ }
168
+ /** `"public"` and `public` are the same name; `"Public"` is not `public`. */
169
+ function unquoteIdentifier(raw) {
170
+ const trimmed = raw.trim();
171
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) return trimmed.slice(1, -1).replace(/""/g, "\"");
172
+ return trimmed.toLowerCase();
173
+ }
174
+ /** Split a dotted identifier at its top-level dots, respecting quoting. */
175
+ function splitQualifiedName(raw) {
176
+ const parts = [];
177
+ let current = "";
178
+ let inQuote = false;
179
+ for (let i = 0; i < raw.length; i++) {
180
+ const ch = raw[i];
181
+ if (inQuote) {
182
+ if (ch === "\"") {
183
+ if (raw[i + 1] === "\"") {
184
+ current += "\"\"";
185
+ i++;
186
+ continue;
187
+ }
188
+ inQuote = false;
189
+ }
190
+ current += ch;
191
+ continue;
192
+ }
193
+ if (ch === "\"") {
194
+ inQuote = true;
195
+ current += ch;
196
+ continue;
197
+ }
198
+ if (ch === ".") {
199
+ parts.push(current);
200
+ current = "";
201
+ continue;
202
+ }
203
+ current += ch;
204
+ }
205
+ if (inQuote) return null;
206
+ parts.push(current);
207
+ return parts.map(unquoteIdentifier);
208
+ }
209
+ /**
210
+ * Walk the script and locate every statement. Quote-, comment- and
211
+ * dollar-aware: the appended halves of a migration are not passed here, but a
212
+ * splitter that shreds a `DO $tag$ ... $tag$` block is a trap worth not
213
+ * leaving behind — it is how the derived-names renderer broke.
214
+ *
215
+ * Returns null if the text ends inside a quote or a dollar-quoted body, or
216
+ * trails off in a statement with no semicolon. Either means this cannot be
217
+ * reasoned about safely.
218
+ */
219
+ function locateStatements(sql) {
220
+ const statements = [];
221
+ let i = 0;
222
+ let statementStart = 0;
223
+ let sawCode = false;
224
+ const pushStatement = (endExclusive) => {
225
+ const leading = sql.slice(statementStart, endExclusive);
226
+ let consumed = 0;
227
+ for (const line of leading.split("\n")) {
228
+ const trimmed = line.trim();
229
+ if (trimmed === "" || trimmed.startsWith("--")) {
230
+ consumed += line.length + 1;
231
+ continue;
232
+ }
233
+ break;
234
+ }
235
+ const bodyStart = statementStart + consumed;
236
+ statements.push({
237
+ start: statementStart,
238
+ end: endExclusive,
239
+ bodyStart,
240
+ body: sql.slice(bodyStart, endExclusive).replace(/;\s*$/, "").trim()
241
+ });
242
+ };
243
+ while (i < sql.length) {
244
+ const ch = sql[i];
245
+ if (ch === "-" && sql[i + 1] === "-") {
246
+ const nl = sql.indexOf("\n", i);
247
+ i = nl === -1 ? sql.length : nl + 1;
248
+ continue;
249
+ }
250
+ if (ch === "/" && sql[i + 1] === "*") {
251
+ const close = sql.indexOf("*/", i + 2);
252
+ if (close === -1) return null;
253
+ i = close + 2;
254
+ sawCode = true;
255
+ continue;
256
+ }
257
+ if (ch === "'" || ch === "\"") {
258
+ const quote = ch;
259
+ i++;
260
+ let closed = false;
261
+ while (i < sql.length) {
262
+ if (sql[i] === quote) {
263
+ if (sql[i + 1] === quote) {
264
+ i += 2;
265
+ continue;
266
+ }
267
+ i++;
268
+ closed = true;
269
+ break;
270
+ }
271
+ i++;
272
+ }
273
+ if (!closed) return null;
274
+ sawCode = true;
275
+ continue;
276
+ }
277
+ if (ch === "$") {
278
+ const tag = /^\$(?:[A-Za-z_\u0080-\uffff][\w\u0080-\uffff]*)?\$/.exec(sql.slice(i));
279
+ if (tag) {
280
+ const close = sql.indexOf(tag[0], i + tag[0].length);
281
+ if (close === -1) return null;
282
+ i = close + tag[0].length;
283
+ sawCode = true;
284
+ continue;
285
+ }
286
+ }
287
+ if (ch === ";") {
288
+ pushStatement(i + 1);
289
+ i++;
290
+ statementStart = i;
291
+ sawCode = false;
292
+ continue;
293
+ }
294
+ if (!/\s/.test(ch)) sawCode = true;
295
+ i++;
296
+ }
297
+ if (sawCode) return null;
298
+ return statements;
299
+ }
300
+ /** Split an ALTER TABLE action list at its top-level commas. */
301
+ function splitClauses(actions) {
302
+ const clauses = [];
303
+ let depth = 0;
304
+ let current = "";
305
+ let i = 0;
306
+ while (i < actions.length) {
307
+ const ch = actions[i];
308
+ if (ch === "'" || ch === "\"") {
309
+ const quote = ch;
310
+ current += ch;
311
+ i++;
312
+ let closed = false;
313
+ while (i < actions.length) {
314
+ current += actions[i];
315
+ if (actions[i] === quote) {
316
+ if (actions[i + 1] === quote) {
317
+ current += actions[i + 1];
318
+ i += 2;
319
+ continue;
320
+ }
321
+ i++;
322
+ closed = true;
323
+ break;
324
+ }
325
+ i++;
326
+ }
327
+ if (!closed) return null;
328
+ continue;
329
+ }
330
+ if (ch === "(") {
331
+ depth++;
332
+ current += ch;
333
+ i++;
334
+ continue;
335
+ }
336
+ if (ch === ")") {
337
+ depth--;
338
+ current += ch;
339
+ i++;
340
+ continue;
341
+ }
342
+ if (ch === "," && depth === 0) {
343
+ clauses.push(current);
344
+ current = "";
345
+ i++;
346
+ continue;
347
+ }
348
+ current += ch;
349
+ i++;
350
+ }
351
+ if (depth !== 0) return null;
352
+ clauses.push(current);
353
+ return clauses.map((c) => c.trim()).filter((c) => c.length > 0);
354
+ }
355
+ /**
356
+ * Remove every clause and statement that would drop one of `patterns`.
357
+ *
358
+ * `patterns` are the `schema.table.object` strings the CLI already builds for
359
+ * Atlas's `--exclude`, so the diff path and the apply path protect exactly the
360
+ * same set by construction.
361
+ */
362
+ function stripCarvedOutStatements(migrationSql, patterns) {
363
+ const carved = parseExcludePatterns(patterns);
364
+ if (carved.length === 0) return {
365
+ sql: migrationSql,
366
+ removed: [],
367
+ unhandled: [],
368
+ empty: isBlank(migrationSql)
369
+ };
370
+ const located = locateStatements(migrationSql);
371
+ if (located === null) return {
372
+ sql: migrationSql,
373
+ removed: [],
374
+ unhandled: MENTIONS_DROP.test(migrationSql) && carved.some((c) => migrationSql.includes(c.object)) ? [migrationSql.trim()] : [],
375
+ empty: isBlank(migrationSql)
376
+ };
377
+ const removed = [];
378
+ const unhandled = [];
379
+ const edits = [];
380
+ for (const statement of located) {
381
+ if (!carved.some((c) => statement.body.includes(c.object))) continue;
382
+ const dropIndex = DROP_INDEX_RE.exec(statement.body);
383
+ if (dropIndex) {
384
+ const parts = splitQualifiedName(dropIndex[1]);
385
+ const name = parts?.[parts.length - 1];
386
+ const schema = parts && parts.length > 1 ? parts[parts.length - 2] : void 0;
387
+ if (name !== void 0 && carved.some((c) => c.object === name && (schema === void 0 || c.schema === schema))) {
388
+ removed.push(statement.body);
389
+ edits.push({
390
+ start: statement.start,
391
+ end: statement.end,
392
+ replacement: ""
393
+ });
394
+ } else unhandled.push(statement.body);
395
+ continue;
396
+ }
397
+ const alter = ALTER_TABLE_RE.exec(statement.body);
398
+ if (!alter) {
399
+ if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
400
+ continue;
401
+ }
402
+ const target = splitQualifiedName(alter[1]);
403
+ if (!target) {
404
+ if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
405
+ continue;
406
+ }
407
+ const table = target[target.length - 1];
408
+ const schema = target.length > 1 ? target[target.length - 2] : void 0;
409
+ const clauses = splitClauses(alter[2]);
410
+ if (!clauses) {
411
+ if (MENTIONS_DROP.test(statement.body)) unhandled.push(statement.body);
412
+ continue;
413
+ }
414
+ const survivors = [];
415
+ let removedHere = 0;
416
+ let unhandledHere = false;
417
+ for (const clause of clauses) {
418
+ const drop = DROP_COLUMN_RE.exec(clause);
419
+ const column = drop ? unquoteIdentifier(drop[1]) : void 0;
420
+ if (column !== void 0 && carved.some((c) => c.object === column && c.table === table && (schema === void 0 || c.schema === schema))) {
421
+ removed.push(`ALTER TABLE ${alter[1]}: ${clause}`);
422
+ removedHere++;
423
+ continue;
424
+ }
425
+ if (MENTIONS_DROP.test(clause) && carved.some((c) => clause.includes(c.object))) unhandledHere = true;
426
+ survivors.push(clause);
427
+ }
428
+ if (unhandledHere) unhandled.push(statement.body);
429
+ if (removedHere === 0) continue;
430
+ if (survivors.length === 0) edits.push({
431
+ start: statement.start,
432
+ end: statement.end,
433
+ replacement: ""
434
+ });
435
+ else edits.push({
436
+ start: statement.bodyStart,
437
+ end: statement.end,
438
+ replacement: `ALTER TABLE ${alter[1]} ${survivors.join(", ")};`
439
+ });
440
+ }
441
+ let sql = migrationSql;
442
+ for (const edit of edits.sort((a, b) => b.start - a.start)) sql = sql.slice(0, edit.start) + edit.replacement + sql.slice(edit.end);
443
+ if (edits.length > 0) sql = tidy(sql);
444
+ return {
445
+ sql,
446
+ removed,
447
+ unhandled,
448
+ empty: isBlank(sql)
449
+ };
450
+ }
451
+ /** Only comments and whitespace left — nothing a migration would do. */
452
+ function isBlank(sql) {
453
+ return sql.split("\n").every((line) => line.trim() === "" || line.trim().startsWith("--"));
454
+ }
455
+ /** Collapse the blank runs an excision leaves behind. */
456
+ function tidy(sql) {
457
+ const trimmed = sql.replace(/\n{3,}/g, "\n\n").replace(/^\s*\n/, "").trimEnd();
458
+ return trimmed.length === 0 ? "" : `${trimmed}\n`;
459
+ }
460
+ //#endregion
461
+ //#region src/schema/atlas-argv.ts
462
+ /**
463
+ * Does this Atlas invocation accept `--exclude`?
464
+ *
465
+ * Only `atlas schema apply` does, on the pinned 1.2.3. Measured against the
466
+ * binary: `migrate diff` and `migrate apply` both answer `unknown flag:
467
+ * --exclude`, and neither lists it in `--help` (`migrate apply` offers
468
+ * `--url/--dir/--format/--revisions-schema/--dry-run/--lock-name/--lock-timeout/`
469
+ * `--skip-lock/--baseline/--to-version/--tx-mode/--exec-order/--allow-dirty`).
470
+ * Hence `domain === "schema"` and not just the subcommand.
471
+ *
472
+ * The `exclude` attribute of an `atlas.hcl` `env` block is not a way round it
473
+ * either: measured, it is accepted and then ignored on the diff path, which is
474
+ * the silent-no-match shape that costs the most time to notice.
475
+ *
476
+ * So the diff keeps the carved-out objects out of the migration afterwards
477
+ * instead, with `stripCarvedOutStatements`.
478
+ */
479
+ function acceptsExcludeFlag(domain, args) {
480
+ return domain === "schema" && args.includes("apply");
481
+ }
482
+ /** Assemble the full argv, connection flags and all. */
483
+ function buildAtlasArgs(invocation) {
484
+ const { domain, args, url, devUrl, excludes = [] } = invocation;
485
+ const argv = [domain, ...args];
486
+ if (domain === "schema") {
487
+ if (args.includes("apply")) argv.push("--url", url, "--dev-url", devUrl);
488
+ else if (args.includes("clean") || args.includes("inspect")) argv.push("--url", url);
489
+ } else if (domain === "migrate") {
490
+ if (args.includes("diff")) argv.push("--dev-url", devUrl);
491
+ else if (args.includes("apply") || args.includes("status")) {
492
+ argv.push("--url", url, "--revisions-schema", "rebase");
493
+ if (args.includes("apply") && !args.includes("--baseline")) argv.push("--allow-dirty");
494
+ }
495
+ }
496
+ if (acceptsExcludeFlag(domain, args)) for (const exclude of excludes) argv.push("--exclude", exclude);
497
+ return argv;
498
+ }
499
+ //#endregion
500
+ //#region src/branch-argv.ts
501
+ /**
502
+ * Argument shapes for `rebase db branch`, with no dependencies.
503
+ *
504
+ * Its own module for the same reason `schema/atlas-argv.ts` is: `cli.ts` pulls
505
+ * in chalk, execa and the driver runtime, so a unit test that imports it cannot
506
+ * run. Argv parsing is the part most worth testing and the part least in need
507
+ * of any of that.
508
+ */
509
+ /**
510
+ * Words on a `db branch` command line that no argument accounts for.
511
+ *
512
+ * `rebase db branch create alpha beta` created a branch called `alpha` and
513
+ * threw `beta` away without a word. The shapes that produces are all quiet and
514
+ * all wrong: an unquoted name (`create my feature` → `my`), a flag written
515
+ * without its dashes (`create feat from main` → `feat`), a shell that split
516
+ * something you thought was one token. In each case the command succeeds and
517
+ * the branch is not the one asked for — and branch names are the thing you
518
+ * later type to switch, delete, or point a deploy at.
519
+ *
520
+ * Anything starting with `-` is left alone rather than validated. This runs on
521
+ * every branch subcommand, including ones added later with flags this function
522
+ * has never heard of, and rejecting an unrecognised flag here would break them
523
+ * from a distance. The value after a flag that takes one is skipped for the
524
+ * same reason: it is that flag's argument, not a stray word.
525
+ *
526
+ * @param words The command's own arguments: action, name, then the rest.
527
+ */
528
+ function unexpectedBranchArgs(words) {
529
+ const extras = [];
530
+ let seenName = false;
531
+ for (let index = 1; index < words.length; index += 1) {
532
+ const word = words[index];
533
+ if (word.startsWith("-")) {
534
+ if (TAKES_A_VALUE.has(word.split("=")[0])) index += 1;
535
+ continue;
536
+ }
537
+ if (!seenName) {
538
+ seenName = true;
539
+ continue;
540
+ }
541
+ extras.push(word);
542
+ }
543
+ return extras;
544
+ }
545
+ /**
546
+ * Branch-line flags whose next word belongs to them.
547
+ *
548
+ * `--from` was the only one listed, and `--database-url` is the one that bit:
549
+ * `rebase db branch list --database-url postgres://…` — which the CLI itself
550
+ * appends when the checkout is on a branch, so `rebase.branches` is read in the
551
+ * parent where it lives — answered `✗ Unexpected argument: postgres://…`. The
552
+ * `=` form carries its value in the same word and needs no skip.
553
+ */
554
+ var TAKES_A_VALUE = /* @__PURE__ */ new Set([
555
+ "--from",
556
+ "--older-than",
557
+ "--database-url"
558
+ ]);
559
+ //#endregion
560
+ //#region src/cli-flags.ts
561
+ /**
562
+ * The flags each driver command takes, in one place, and the check that
563
+ * enforces them.
564
+ *
565
+ * Every parser in `cli.ts` runs `arg(..., { permissive: true })`, which does not
566
+ * mean "be lenient" — it means **an undeclared flag becomes a positional**. So
567
+ * `rebase db push --alow-destructive` did not fail: the typo landed in `_`, the
568
+ * push ran with the destructive gate still closed, and the developer read the
569
+ * refusal as Rebase ignoring the flag they had just typed. `rebase schema
570
+ * generate --ouput src/schema.ts` was worse — it wrote the default path and
571
+ * said nothing, so the next build compiled a file nobody had regenerated.
572
+ *
573
+ * The check has to live at the entry point rather than in those parsers, and
574
+ * that is not a detail: `db push` and `db generate` re-enter `schemaCommand`
575
+ * and `generatePostgresDdlCommand` with the *db* line, so a strict parser
576
+ * inside either of them would reject `--allow-destructive` on a line where it
577
+ * is correct. One validation, once, against the spec for the command the user
578
+ * actually named; the inner parsers keep reading an already-validated line.
579
+ *
580
+ * Commands with their own argument handling — `db branch` (`unexpectedBranchArgs`),
581
+ * `db backup`, `db restore`, `db backups` — are deliberately absent: they own
582
+ * their spec, and a second list here would be one more thing to keep in step.
583
+ * An absent entry is "not checked here", never "takes nothing".
584
+ */
585
+ /**
586
+ * Flags that reach this process on a line it did not compose.
587
+ *
588
+ * `rebase db …` hands the driver the user's whole line: the CLI *reads*
589
+ * `--database-url` and `--docker` to resolve the database and then relays them
590
+ * verbatim, and `--debug` is what `bin/rebase.js` prints after every failure as
591
+ * the thing to re-run with. Rejecting any of the three would make the driver
592
+ * refuse commands the CLI documents.
593
+ */
594
+ var RELAYED_FLAGS = {
595
+ "--database-url": String,
596
+ "--docker": Boolean,
597
+ "--debug": Boolean,
598
+ "--help": Boolean,
599
+ "-h": "--help"
600
+ };
601
+ /** Keyed by `"<domain> <subcommand>"`, the way the user types it. */
602
+ var DRIVER_FLAG_SPECS = {
603
+ "db push": {
604
+ "--collections": String,
605
+ "-c": "--collections",
606
+ "--dry-run": Boolean,
607
+ "--allow-destructive": Boolean,
608
+ "--yes": Boolean,
609
+ "-y": "--yes"
610
+ },
611
+ "db generate": {
612
+ "--collections": String,
613
+ "-c": "--collections"
614
+ },
615
+ "db migrate": { "--baseline": String },
616
+ "schema generate": {
617
+ "--collections": String,
618
+ "-c": "--collections",
619
+ "--output": String,
620
+ "-o": "--output",
621
+ "--out": "--output",
622
+ "--watch": Boolean,
623
+ "-w": "--watch"
624
+ },
625
+ "schema introspect": {
626
+ "--output": String,
627
+ "-o": "--output",
628
+ "--out": "--output",
629
+ "--collections": String,
630
+ "-c": "--collections",
631
+ "--force": Boolean,
632
+ "-f": "--force",
633
+ "--schema": String
634
+ },
635
+ "schema stale": {
636
+ "--collections": String,
637
+ "-c": "--collections",
638
+ "--output": String,
639
+ "-o": "--output",
640
+ "--out": "--output",
641
+ "--fix": Boolean
642
+ }
643
+ };
644
+ /**
645
+ * The `--collections` path the user typed, or `null` if they typed none.
646
+ *
647
+ * Read once, at the entry point, because every command that takes it re-enters
648
+ * the generators with the same line and each of them resolved it again — which
649
+ * is how "Collections path not found" came to be printed four times before the
650
+ * real output, and how a path that does not exist got as far as *writing* an
651
+ * empty schema.
652
+ *
653
+ * Permissive, and it has to be: {@link assertKnownFlags} has already judged the
654
+ * line, and `db push` carries flags the `--collections` spec does not name.
655
+ */
656
+ function collectionsPathIn(args) {
657
+ try {
658
+ return (0, import_arg.default)({
659
+ "--collections": String,
660
+ "-c": "--collections"
661
+ }, {
662
+ argv: args.slice(2),
663
+ permissive: true
664
+ })["--collections"] ?? null;
665
+ } catch {
666
+ return null;
667
+ }
668
+ }
669
+ /**
670
+ * Reject a flag the named command does not take.
671
+ *
672
+ * `args` is the driver's whole line — `["db", "push", …]` — so the flags are
673
+ * read from `args.slice(2)`, past the two command words.
674
+ *
675
+ * Throws rather than exiting: `runPluginCommand`'s caller already turns a
676
+ * thrown error into one red line and exit 1, and a thrown error is what the
677
+ * tests can see.
678
+ */
679
+ function assertKnownFlags(domain, subcommand, args) {
680
+ if (!subcommand) return;
681
+ const spec = DRIVER_FLAG_SPECS[`${domain} ${subcommand}`];
682
+ if (!spec) return;
683
+ try {
684
+ (0, import_arg.default)({
685
+ ...RELAYED_FLAGS,
686
+ ...spec
687
+ }, { argv: args.slice(2) });
688
+ } catch (err) {
689
+ if (err instanceof Error && err.code === "ARG_UNKNOWN_OPTION") throw new Error(`${err.message} — run \`rebase ${domain} --help\` for the options \`${domain} ${subcommand}\` takes.`);
690
+ throw err;
691
+ }
692
+ }
693
+ //#endregion
694
+ //#region src/cli-collections-path.ts
695
+ /**
696
+ * `--collections <dir>`, checked once, before anything is written.
697
+ *
698
+ * A path that does not resolve used to warn and carry on. Four times over —
699
+ * `schemaCommand`, `generatePostgresDdlCommand` and the Atlas argv assembly
700
+ * each re-enter the loader with the same line — and then both generators wrote
701
+ * an *empty* schema: `drizzle/schema.sql` truncated to `CREATE SCHEMA IF NOT
702
+ * EXISTS "rebase";`, `src/schema.generated.ts` to ten lines. Both are committed
703
+ * artifacts. The push that followed planned a `DROP TABLE` for every table in
704
+ * the database, and the only thing between a typo and that was the destructive
705
+ * gate.
706
+ *
707
+ * The check has to be here rather than inside `loadCollectionsForCli`, which is
708
+ * deliberately forgiving: its callers use it to narrow what Atlas may touch,
709
+ * and making a broken *file* fatal there would block a push over something
710
+ * unrelated. "Does the directory the user named exist" is a different question,
711
+ * it was answerable before the first write, and the code already knew.
712
+ */
713
+ /**
714
+ * Thrown rather than exited, so the tests can see it and the entry point owns
715
+ * the exit code.
716
+ *
717
+ * `alreadyReported` is read by `reportCommandFailure`: the full diagnosis is on
718
+ * stderr by the time this is thrown, and the point of the whole change is that
719
+ * it is printed once.
720
+ */
721
+ var CollectionsPathMissing = class extends Error {
722
+ typed;
723
+ resolved;
724
+ alreadyReported = true;
725
+ constructor(typed, resolved) {
726
+ super(`Collections path not found: "${typed}"`);
727
+ this.typed = typed;
728
+ this.resolved = resolved;
729
+ this.name = "CollectionsPathMissing";
730
+ }
731
+ };
732
+ /**
733
+ * The whole message, in one place, because it is printed by two callers.
734
+ *
735
+ * Names what was typed, what it resolved to, and the working directory it
736
+ * resolved against — a relative `--collections` is resolved against the cwd,
737
+ * and the generated npm script runs with `cwd: backend/`, so "the path is
738
+ * right and the directory is wrong" is the commonest way to arrive here.
739
+ */
740
+ function describeMissingCollectionsPath(typed, resolved) {
741
+ return [
742
+ chalk.red(`✗ Collections path not found: "${typed}"`),
743
+ chalk.gray(` Resolved to: ${resolved}`),
744
+ chalk.gray(` (relative to cwd: ${process.cwd()})`),
745
+ "",
746
+ chalk.gray(" Nothing was generated and nothing was applied — a schema generated from a"),
747
+ chalk.gray(" directory that is not there is an empty one, and applying it would drop"),
748
+ chalk.gray(" every table it does not describe."),
749
+ "",
750
+ chalk.gray(" Pass an absolute path, or one relative to where you run the command.")
751
+ ].join("\n");
752
+ }
753
+ /**
754
+ * Refuse a `--collections` path that does not exist.
755
+ *
756
+ * `null` means the flag was not given: the default (`../config/collections`) is
757
+ * left to the loader, which warns and continues — a project may legitimately
758
+ * have no collections directory, and a headless scaffold does.
759
+ */
760
+ function assertCollectionsPathExists(typed) {
761
+ if (typed === null) return;
762
+ const resolved = path.resolve(process.cwd(), typed);
763
+ if (fs.existsSync(resolved)) return;
764
+ outError("");
765
+ outError(describeMissingCollectionsPath(typed, resolved));
766
+ outError("");
767
+ throw new CollectionsPathMissing(typed, resolved);
768
+ }
769
+ //#endregion
770
+ //#region src/backup-argv.ts
771
+ /**
772
+ * @param rawArgs the driver's whole line, `["db", "backup", …]`.
773
+ */
774
+ function backupActionOf(rawArgs) {
775
+ return rawArgs[2] === "list" ? "list" : "create";
776
+ }
777
+ //#endregion
778
+ //#region src/branch-prune.ts
779
+ /** True when nothing at all needs doing. */
780
+ function planIsEmpty(plan, includeDevDiff) {
781
+ return plan.staleRows.length === 0 && plan.orphanDatabases.length === 0 && plan.expired.length === 0 && (!includeDevDiff || plan.devDiff.length === 0);
782
+ }
783
+ /**
784
+ * Age in whole days, floored.
785
+ *
786
+ * Floored rather than rounded so `--older-than 7` never catches something six
787
+ * and a half days old: a prune that removes more than it said it would is worse
788
+ * than one that waits another twelve hours.
789
+ */
790
+ function ageInDays(createdAt, now) {
791
+ return Math.floor((now.getTime() - createdAt.getTime()) / 864e5);
792
+ }
793
+ /**
794
+ * Parse `--older-than`. Accepts a bare number of days, or `7d` / `2w`.
795
+ *
796
+ * Returns null for anything else, so the caller refuses rather than guessing —
797
+ * silently reading `--older-than 7h` as seven *days* would delete a week of
798
+ * work.
799
+ */
800
+ function parseOlderThan(value) {
801
+ const match = /^(\d+)\s*([dw])?$/i.exec(value.trim());
802
+ if (!match) return null;
803
+ const amount = Number(match[1]);
804
+ if (!Number.isFinite(amount)) return null;
805
+ return match[2]?.toLowerCase() === "w" ? amount * 7 : amount;
806
+ }
807
+ function planPrune(input, options = {}) {
808
+ const now = options.now ?? /* @__PURE__ */ new Date();
809
+ const present = new Set(input.databases);
810
+ const registered = new Set(input.rows.map((row) => row.dbName));
811
+ return {
812
+ staleRows: input.rows.filter((row) => !present.has(row.dbName)),
813
+ orphanDatabases: input.databases.filter((name) => name.startsWith(input.branchPrefix) && !registered.has(name)).sort(),
814
+ expired: options.olderThanDays == null ? [] : input.rows.filter((row) => present.has(row.dbName)).map((branch) => ({
815
+ branch,
816
+ ageDays: ageInDays(branch.createdAt, now)
817
+ })).filter((entry) => entry.ageDays >= options.olderThanDays).sort((a, b) => b.ageDays - a.ageDays),
818
+ devDiff: input.databases.filter((name) => name.endsWith("_dev_diff")).sort()
819
+ };
820
+ }
821
+ //#endregion
822
+ //#region src/cli.ts
823
+ var __cliDirname = path.dirname(fileURLToPath(import.meta.url));
824
+ /**
825
+ * A script this CLI spawns, and how to run it.
826
+ *
827
+ * Four commands — `schema generate`, the DDL plan, `introspect` and the schema
828
+ * doctor — run as child processes, located by path relative to this file. That
829
+ * path used to be `<dir>/schema/<name>.ts`, which worked only because `files`
830
+ * shipped `src`. When the tarball stopped shipping `src`, every one of those
831
+ * commands broke for every npm consumer at once: the file was simply absent,
832
+ * and the parent CLI reported the absence as "Dependencies are not installed"
833
+ * at projects that had installed perfectly. 0.18.0 shipped that way — a
834
+ * scaffolded project's first `rebase dev` could not regenerate its schema and
835
+ * every `GET /api/data/*` answered `Table not found for collection 'posts'`.
836
+ *
837
+ * The build emits each of them beside this file, so a published copy carries
838
+ * the command surface as an artifact. Source still wins where there is no
839
+ * build, which is how the monorepo runs before `pnpm build`.
840
+ *
841
+ * **The interpreter is tsx either way, and that is not incidental.** These
842
+ * scripts load the *project's* collection files, which are TypeScript and
843
+ * import each other as `./authors.js` — the extension TypeScript tells you to
844
+ * write. Node resolves that literally and fails on a file that does not exist;
845
+ * tsx maps it back. Running the built `.js` under plain `node` therefore fails
846
+ * on the user's code rather than on ours, one directory further from anything
847
+ * the error mentions.
848
+ */
849
+ function resolveChildScript(name) {
850
+ const built = path.join(__cliDirname, "schema", `${name}.js`);
851
+ const source = path.join(__cliDirname, "schema", `${name}.ts`);
852
+ const script = fs.existsSync(built) ? built : fs.existsSync(source) ? source : null;
853
+ if (!script) return {
854
+ ok: false,
855
+ reason: "script"
856
+ };
857
+ const tsxBin = resolveLocalBin("tsx");
858
+ if (!tsxBin) return {
859
+ ok: false,
860
+ reason: "tsx"
861
+ };
862
+ return {
863
+ ok: true,
864
+ bin: tsxBin,
865
+ script
866
+ };
867
+ }
868
+ /** What to print when {@link resolveChildScript} cannot produce a command. */
869
+ function childScriptError(what, reason) {
870
+ if (reason === "tsx") return `✗ Could not find the tsx binary, which ${what} needs to read your collection files. Install your project's dependencies and try again.`;
871
+ return `✗ The driver's ${what} is missing. This copy of @rebasepro/server-postgres is incomplete — reinstall it, or run its build if you are working in the monorepo.`;
872
+ }
873
+ async function loadEnv() {
874
+ try {
875
+ const dotenv = await import("dotenv");
876
+ const envPaths = [
877
+ process.env.DOTENV_CONFIG_PATH,
878
+ path.resolve(process.cwd(), ".env"),
879
+ path.resolve(process.cwd(), "../.env"),
880
+ path.resolve(process.cwd(), "../../.env")
881
+ ].filter(Boolean);
882
+ for (const p of envPaths) if (fs.existsSync(p)) {
883
+ const parsed = dotenv.config({
884
+ path: p,
885
+ quiet: true
886
+ });
887
+ if (parsed.parsed) {
888
+ for (const [key, val] of Object.entries(parsed.parsed)) if (process.env[key] === void 0) process.env[key] = val;
889
+ break;
890
+ }
891
+ }
892
+ } catch {}
893
+ }
894
+ async function runPluginCommand(args) {
895
+ if (process.env.DOTENV_CONFIG_QUIET === void 0) process.env.DOTENV_CONFIG_QUIET = "true";
896
+ await loadEnv();
897
+ const domain = args[0];
898
+ const subcommand = args[1];
899
+ assertKnownFlags(domain, subcommand, args);
900
+ assertCollectionsPathExists(collectionsPathIn(args));
901
+ if (domain === "db") await dbCommand(subcommand, args);
902
+ else if (domain === "schema") await schemaCommand(subcommand, args);
903
+ else if (domain === "doctor") await doctorPluginCommand(args);
904
+ else {
905
+ outError(chalk.red(`Unknown domain command: ${domain}`));
906
+ process.exit(1);
907
+ }
908
+ }
909
+ /** The migration files on disk, sorted, or none if the directory is absent. */
910
+ function listMigrationFiles() {
911
+ const dir = path.resolve(process.cwd(), "drizzle", "migrations");
912
+ if (!fs.existsSync(dir)) return [];
913
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
914
+ }
915
+ /**
916
+ * The newest migration's version — the numeric prefix Atlas records.
917
+ *
918
+ * `20260906101530_init.sql` → `20260906101530`. Named in the baseline remedy so
919
+ * the command it prints can be typed rather than worked out.
920
+ */
921
+ function latestMigrationVersion() {
922
+ const newest = listMigrationFiles().at(-1);
923
+ if (!newest) return null;
924
+ const match = /^(\d+)/.exec(newest);
925
+ return match ? match[1] : newest.replace(/\.sql$/, "");
926
+ }
927
+ /**
928
+ * Take Atlas's carve-outs back out of the migration `migrate diff` just wrote.
929
+ *
930
+ * The apply path hands Atlas `--exclude` and it never sees the search or vector
931
+ * columns. The diff path has no such flag — `atlas migrate diff` rejects
932
+ * `--exclude` outright, and an `atlas.hcl` `env` block's `exclude` is accepted
933
+ * and then ignored — so Atlas replays the migration directory (which builds
934
+ * both columns, because that DDL is appended to migrations), does not find them
935
+ * in `schema.sql`, and plans `DROP COLUMN`. Applied, that would take search and
936
+ * the embeddings away from every database the migration reaches.
937
+ *
938
+ * Returns whether a migration Atlas just wrote is still there afterwards: a
939
+ * file whose only content was the spurious drop is deleted, because an empty
940
+ * migration is worse than none — it takes a slot in the revision history and
941
+ * the caller would append the carved-out DDL and the policies to it.
942
+ *
943
+ * @returns true when a new migration survives and may be appended to.
944
+ */
945
+ async function stripCarvedOutFromNewestMigration(collectionsPath) {
946
+ const patterns = [...await getSearchExcludes(collectionsPath), ...await getVectorExcludes(collectionsPath)];
947
+ if (patterns.length === 0) return true;
948
+ const newest = listMigrationFiles().at(-1);
949
+ if (!newest) return false;
950
+ const file = path.resolve(process.cwd(), "drizzle", "migrations", newest);
951
+ const result = stripCarvedOutStatements(fs.readFileSync(file, "utf-8"), patterns);
952
+ if (result.unhandled.length > 0) {
953
+ outError(chalk.red(`\n ✗ ${newest} drops an object Rebase manages outside Atlas, in a form the CLI`));
954
+ outError(chalk.red(" could not remove:"));
955
+ for (const statement of result.unhandled) outError(chalk.gray(` ${statement.replace(/\s+/g, " ")}`));
956
+ outError(chalk.gray(" Left as Atlas wrote it. Delete those statements by hand before running\n `rebase db migrate` — and please report them: rewriting this is meant to\n be automatic."));
957
+ process.exit(1);
958
+ }
959
+ if (result.removed.length === 0) return true;
960
+ if (result.empty) {
961
+ fs.rmSync(file);
962
+ out(chalk.gray(` ✓ Dropped ${newest} — it planned nothing but the removal of columns\n Atlas cannot see and Rebase applies itself.`));
963
+ } else {
964
+ fs.writeFileSync(file, result.sql, "utf-8");
965
+ out(chalk.gray(` ✓ Kept Rebase's own columns out of ${newest} (${result.removed.length} statement(s))`));
966
+ }
967
+ await runAtlas("migrate", [
968
+ "hash",
969
+ "--dir",
970
+ "file://drizzle/migrations"
971
+ ], collectionsPath);
972
+ return !result.empty;
973
+ }
974
+ async function dbCommand(subcommand, rawArgs) {
975
+ const VALID_ACTIONS = [
976
+ "push",
977
+ "generate",
978
+ "migrate",
979
+ "branch",
980
+ "backup",
981
+ "restore",
982
+ "backups"
983
+ ];
984
+ if (!subcommand || !VALID_ACTIONS.includes(subcommand)) {
985
+ outError(chalk.red(`Unknown db command. Valid: ${VALID_ACTIONS.join(", ")}`));
986
+ process.exit(1);
987
+ }
988
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
989
+ out("");
990
+ out(chalk.bold(` rebase db ${subcommand}`));
991
+ out(chalk.gray(" Run `rebase db --help` for the full page — this is the driver's own entry point."));
992
+ out("");
993
+ return;
994
+ }
995
+ if (subcommand === "branch") {
996
+ await branchCommand(rawArgs);
997
+ return;
998
+ }
999
+ if (subcommand === "backup" || subcommand === "restore" || subcommand === "backups") {
1000
+ const { backupCommand, restoreCommand, backupsCommand } = await import("./backup-cli-oM9WQVlV.js");
1001
+ if (subcommand === "backup" && backupActionOf(rawArgs) === "list") await backupsCommand(rawArgs);
1002
+ else if (subcommand === "backup") await backupCommand(rawArgs);
1003
+ else if (subcommand === "restore") await restoreCommand(rawArgs);
1004
+ else await backupsCommand(rawArgs);
1005
+ return;
1006
+ }
1007
+ const argsList = (0, import_arg.default)({
1008
+ "--collections": String,
1009
+ "--allow-destructive": Boolean,
1010
+ "--dry-run": Boolean,
1011
+ "--yes": Boolean,
1012
+ "--baseline": String,
1013
+ "-c": "--collections",
1014
+ "-y": "--yes"
1015
+ }, {
1016
+ argv: rawArgs.slice(2),
1017
+ permissive: true
1018
+ });
1019
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1020
+ if (subcommand === "generate") {
1021
+ out("");
1022
+ out(chalk.bold(" 📦 Rebase DB Generate"));
1023
+ out(chalk.gray(" Step 1/2: Generating Drizzle schema & Postgres DDL from collections..."));
1024
+ out("");
1025
+ await schemaCommand("generate", rawArgs);
1026
+ await generatePostgresDdlCommand(rawArgs);
1027
+ out("");
1028
+ out(chalk.gray(" Step 2/2: Generating SQL migration files with Atlas..."));
1029
+ out("");
1030
+ const migrationName = argsList._[0] || "migration";
1031
+ const migrationsBefore = listMigrationFiles();
1032
+ await runAtlas("migrate", [
1033
+ "diff",
1034
+ migrationName,
1035
+ "--dir",
1036
+ "file://drizzle/migrations",
1037
+ "--to",
1038
+ "file://drizzle/schema.sql"
1039
+ ], collectionsPath);
1040
+ let wroteNewMigration = listMigrationFiles().length > migrationsBefore.length;
1041
+ if (wroteNewMigration) wroteNewMigration = await stripCarvedOutFromNewestMigration(collectionsPath);
1042
+ try {
1043
+ const migrationsDir = path.resolve(process.cwd(), "drizzle", "migrations");
1044
+ if (fs.existsSync(migrationsDir) && wroteNewMigration) {
1045
+ const sqlFiles = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
1046
+ if (sqlFiles.length > 0) {
1047
+ const newestMigrationFile = path.join(migrationsDir, sqlFiles[sqlFiles.length - 1]);
1048
+ let migrationContent = fs.readFileSync(newestMigrationFile, "utf-8");
1049
+ migrationContent = migrationContent.replace(/CREATE SCHEMA (?!IF NOT EXISTS)("[^"]+");/g, "CREATE SCHEMA IF NOT EXISTS $1;");
1050
+ const vectorContent = readVectorDdl();
1051
+ if (vectorContent) {
1052
+ migrationContent = `${migrationContent}\n\n${vectorContent}`;
1053
+ out(chalk.gray(" ✓ Appended vector DDL to the migration"));
1054
+ }
1055
+ const searchContent = readSearchDdl();
1056
+ if (searchContent) {
1057
+ migrationContent = `${migrationContent}\n\n${searchContent}`;
1058
+ out(chalk.gray(" ✓ Appended search DDL to the migration"));
1059
+ }
1060
+ fs.writeFileSync(newestMigrationFile, migrationContent, "utf-8");
1061
+ const policiesFile = path.resolve(process.cwd(), "drizzle", "policies.sql");
1062
+ if (fs.existsSync(policiesFile)) {
1063
+ const policiesContent = fs.readFileSync(policiesFile, "utf-8");
1064
+ fs.appendFileSync(newestMigrationFile, "\n\n" + RLS_BOOTSTRAP_SQL + "\n" + policiesContent);
1065
+ out(chalk.gray(` ✓ Appended RLS policies to migration file: ${path.basename(newestMigrationFile)}`));
1066
+ out(chalk.gray(" Re-hashing migration files..."));
1067
+ await runAtlas("migrate", [
1068
+ "hash",
1069
+ "--dir",
1070
+ "file://drizzle/migrations"
1071
+ ], collectionsPath);
1072
+ out(chalk.gray(" ✓ Migration directory checksum updated successfully."));
1073
+ }
1074
+ }
1075
+ }
1076
+ } catch (err) {
1077
+ outWarn(chalk.yellow(` ⚠️ Failed to append policies or re-hash migration: ${err instanceof Error ? err.message : String(err)}`));
1078
+ }
1079
+ if (!wroteNewMigration) out(chalk.gray(" ℹ No migration written — nothing in this change is visible to Atlas.\n Search, vector and RLS DDL are applied by `rebase db push`, and at boot by\n the schema ensure. For a migration-only deployment, add drizzle/search.sql,\n drizzle/vector.sql and drizzle/policies.sql to a migration by hand."));
1080
+ out("");
1081
+ out(` You can now run ${chalk.bold.green("rebase db migrate")} to apply the migrations to your database.`);
1082
+ out("");
1083
+ } else {
1084
+ out("");
1085
+ out(chalk.bold(` 🗄️ Rebase DB ${subcommand.charAt(0).toUpperCase() + subcommand.slice(1)}`));
1086
+ out("");
1087
+ if (subcommand === "push") {
1088
+ out(chalk.gray(" Step 1/3: Generating Drizzle schema & Postgres DDL from collections..."));
1089
+ out("");
1090
+ await schemaCommand("generate", rawArgs);
1091
+ await generatePostgresDdlCommand(rawArgs);
1092
+ out("");
1093
+ const dryRun = argsList["--dry-run"] === true;
1094
+ out(chalk.gray(dryRun ? " Step 2/3: Planning the change against the database (nothing is applied)..." : " Step 2/3: Pushing schema to database with Atlas..."));
1095
+ out("");
1096
+ const databaseUrl = process.env.DATABASE_URL;
1097
+ if (databaseUrl && !dryRun) await ensureAuthSchemaAndFunctions(databaseUrl);
1098
+ const plan = await runAtlas("schema", [
1099
+ "apply",
1100
+ "--to",
1101
+ "file://drizzle/schema.sql",
1102
+ "--dry-run"
1103
+ ], collectionsPath, { captureStdout: true });
1104
+ const destructive = detectDestructiveStatements(plan);
1105
+ if (dryRun) {
1106
+ out(chalk.gray(" Step 3/3: --dry-run — nothing was applied."));
1107
+ out("");
1108
+ if (plan.trim()) {
1109
+ out(chalk.bold(" Planned changes:"));
1110
+ out(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1111
+ } else out(chalk.green(" ✓ No changes: the database already matches these collections."));
1112
+ out("");
1113
+ if (destructive.length > 0) {
1114
+ outWarn(chalk.yellow(` ⚠️ ${destructive.length} of those DESTROY data:`));
1115
+ for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1116
+ outWarn("");
1117
+ outWarn(chalk.yellow(" Applying them needs `rebase db push --allow-destructive`. Back up first: rebase db backup"));
1118
+ outWarn("");
1119
+ }
1120
+ out(chalk.gray(" The auth schema step was skipped, because it writes. A real push runs it first."));
1121
+ out("");
1122
+ return;
1123
+ }
1124
+ const allowDestructive = argsList["--allow-destructive"] === true || argsList["--yes"] === true;
1125
+ const decision = decidePushSafety({
1126
+ destructiveCount: destructive.length,
1127
+ allowDestructive,
1128
+ interactive: process.stdin.isTTY === true
1129
+ });
1130
+ if (destructive.length > 0) {
1131
+ outWarn(chalk.yellow(` ⚠️ This push includes ${destructive.length} destructive change(s) that will DESTROY data:`));
1132
+ outWarn("");
1133
+ for (const d of destructive) outWarn(chalk.red(` ${d.kind}: `) + chalk.gray(d.statement.replace(/\s+/g, " ")));
1134
+ outWarn("");
1135
+ outWarn(chalk.yellow(" Full planned changes:"));
1136
+ outWarn(chalk.gray(plan.trim().split("\n").map((l) => ` ${l}`).join("\n")));
1137
+ outWarn("");
1138
+ if (decision === "refuse") {
1139
+ outError(chalk.red(" ✗ Aborting: destructive changes require confirmation."));
1140
+ outError(chalk.gray(" Re-run interactively, or pass --allow-destructive to proceed. Back up first: rebase db backup"));
1141
+ process.exit(1);
1142
+ }
1143
+ if (decision === "confirm") {
1144
+ if (!await promptConfirm(chalk.yellow(" Type 'yes' to apply these destructive changes (this cannot be undone): "))) {
1145
+ out(chalk.gray(" Aborted. No changes were made."));
1146
+ process.exit(1);
1147
+ }
1148
+ } else outWarn(chalk.yellow(" Proceeding because --allow-destructive was passed."));
1149
+ }
1150
+ await runAtlas("schema", [
1151
+ "apply",
1152
+ "--to",
1153
+ "file://drizzle/schema.sql",
1154
+ "--auto-approve"
1155
+ ], collectionsPath);
1156
+ out("");
1157
+ if (databaseUrl) {
1158
+ await ensureAuthTables(databaseUrl, collectionsPath);
1159
+ await applyVectorDdl(databaseUrl);
1160
+ await applySearchDdl(databaseUrl);
1161
+ await applyPolicies(databaseUrl);
1162
+ await reconcilePolicies(databaseUrl, collectionsPath);
1163
+ await ensureRlsUserRole(databaseUrl);
1164
+ await retireLegacyAuthSchema(databaseUrl);
1165
+ if (scratchDatabaseCreatedHere) await dropDevDatabase(databaseUrl, getDevDatabaseUrl(databaseUrl));
1166
+ } else outWarn(chalk.yellow(" ⚠️ DATABASE_URL not found in environment, skipping RLS policies application."));
1167
+ } else if (subcommand === "migrate") {
1168
+ const databaseUrl = process.env.DATABASE_URL;
1169
+ if (databaseUrl) await ensureAuthSchemaAndFunctions(databaseUrl);
1170
+ const extraArgs = argsList._.filter((arg) => arg !== "migrate");
1171
+ const baseline = argsList["--baseline"];
1172
+ if (baseline) {
1173
+ out(chalk.gray(` Recording ${baseline} as already applied, then migrating from the next one.`));
1174
+ out("");
1175
+ }
1176
+ await runAtlas("migrate", [
1177
+ "apply",
1178
+ "--dir",
1179
+ "file://drizzle/migrations",
1180
+ ...baseline ? ["--baseline", baseline] : [],
1181
+ ...extraArgs
1182
+ ], collectionsPath);
1183
+ if (databaseUrl) {
1184
+ await ensureRlsUserRole(databaseUrl);
1185
+ await retireLegacyAuthSchema(databaseUrl);
1186
+ }
1187
+ }
1188
+ out("");
1189
+ out(chalk.green(` ✓ rebase db ${subcommand} completed successfully.`));
1190
+ out("");
1191
+ }
1192
+ }
1193
+ async function ensureAuthSchemaAndFunctions(databaseUrl) {
1194
+ try {
1195
+ const { Client } = await import("pg");
1196
+ const client = new Client({ connectionString: databaseUrl });
1197
+ await client.connect();
1198
+ try {
1199
+ await client.query(RLS_BOOTSTRAP_SQL);
1200
+ } finally {
1201
+ await client.end();
1202
+ }
1203
+ } catch (err) {
1204
+ outWarn(chalk.yellow(` ⚠️ Failed to bootstrap the RLS helper functions: ${err instanceof Error ? err.message : String(err)}`));
1205
+ }
1206
+ }
1207
+ /**
1208
+ * Create the framework auth tables (e.g. `rebase.users`) that the generated RLS
1209
+ * policies reference, before `applyPolicies` runs. Mirrors what the server does
1210
+ * at boot (`PostgresBootstrapper.initializeAuth` → `ensureAuthTablesExist`).
1211
+ *
1212
+ * Without this, `rebase db push` against a database that has never booted the
1213
+ * server fails while applying policies with `relation "rebase.users" does not
1214
+ * exist` — the documented first-run does `db push` *before* the first `dev`.
1215
+ * `ensureAuthTablesExist` is idempotent (CREATE TABLE IF NOT EXISTS), so it is
1216
+ * safe to run on every push and harmless once the server has also created them.
1217
+ */
1218
+ async function ensureAuthTables(databaseUrl, collectionsPath) {
1219
+ try {
1220
+ const { drizzle } = await import("drizzle-orm/node-postgres");
1221
+ const { ensureAuthTablesExist } = await import("./ensure-tables-CIojK-Rx.js").then((n) => n.n);
1222
+ const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
1223
+ const { Client } = await import("pg");
1224
+ const authCollection = (await loadCollections(path.resolve(process.cwd(), collectionsPath))).find((c) => {
1225
+ const a = c.auth;
1226
+ return a === true || typeof a === "object" && a !== null && a.enabled === true;
1227
+ });
1228
+ const client = new Client({ connectionString: databaseUrl });
1229
+ await client.connect();
1230
+ try {
1231
+ await ensureAuthTablesExist(drizzle(client), authCollection);
1232
+ } finally {
1233
+ await client.end();
1234
+ }
1235
+ } catch (err) {
1236
+ outWarn(chalk.yellow(` ⚠️ Failed to ensure framework auth tables: ${err instanceof Error ? err.message : String(err)}`));
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Provision the restricted `rebase_user` role right after schema changes land,
1241
+ * so grants cover freshly created tables (default privileges cover future
1242
+ * ones). Only needed — and only possible without extra setup — when the
1243
+ * connection would bypass RLS (superuser / BYPASSRLS / table owner).
1244
+ */
1245
+ async function ensureRlsUserRole(databaseUrl) {
1246
+ const schemas = ["public", "rebase"];
1247
+ let rls;
1248
+ try {
1249
+ rls = await import("./rls-enforcement-DeYEjYLt.js").then((n) => n.a);
1250
+ } catch (err) {
1251
+ outError(chalk.red(`\n ✗ Could not load the RLS provisioning module: ${err instanceof Error ? err.message : String(err)}`));
1252
+ return;
1253
+ }
1254
+ try {
1255
+ const { Client } = await import("pg");
1256
+ const client = new Client({ connectionString: databaseUrl });
1257
+ await client.connect();
1258
+ try {
1259
+ const runSql = async (text) => (await client.query(text)).rows;
1260
+ if ((await rls.detectConnectionPosture(runSql)).privileged) {
1261
+ await rls.ensureAppRole(runSql, schemas);
1262
+ out(chalk.gray(` ✓ RLS role "${rls.REBASE_USER_ROLE}" provisioned/refreshed.`));
1263
+ }
1264
+ } finally {
1265
+ await client.end();
1266
+ }
1267
+ } catch (err) {
1268
+ outError(chalk.red(`\n ✗ The schema change was applied, but the "${rls.REBASE_USER_ROLE}" role could not be provisioned: ${err instanceof Error ? err.message : String(err)}`));
1269
+ outError(chalk.gray(rls.appRoleSetupInstructions("your database user", schemas)));
1270
+ }
1271
+ }
1272
+ /**
1273
+ * Retire the pre-1.0 `auth` schema, once nothing depends on it any more.
1274
+ *
1275
+ * Runs last on purpose. Postgres refuses to drop a function an RLS policy still
1276
+ * calls, so this only succeeds after the policies above have been rewritten to
1277
+ * `rebase.uid()`. See `dropLegacyAuthSchema` for what it reports when something
1278
+ * hand-written is still holding the schema open, and DROP_LEGACY_AUTH_SCHEMA_SQL
1279
+ * for the guards that keep it off a Supabase `auth` schema.
1280
+ */
1281
+ async function retireLegacyAuthSchema(databaseUrl) {
1282
+ try {
1283
+ const { Client } = await import("pg");
1284
+ const client = new Client({ connectionString: databaseUrl });
1285
+ await client.connect();
1286
+ try {
1287
+ await dropLegacyAuthSchema(async (text) => (await client.query(text)).rows, {
1288
+ info: (m) => out(chalk.gray(` ${m}`)),
1289
+ warn: (m) => outWarn(chalk.yellow(` ⚠️ ${m}`))
1290
+ });
1291
+ } finally {
1292
+ await client.end();
1293
+ }
1294
+ } catch {}
1295
+ }
1296
+ async function applyPolicies(databaseUrl) {
1297
+ try {
1298
+ const policiesPath = path.resolve(process.cwd(), "drizzle", "policies.sql");
1299
+ if (!fs.existsSync(policiesPath)) return;
1300
+ out(chalk.gray(" Step 3/3: Applying RLS policies to database..."));
1301
+ out("");
1302
+ const policiesContent = fs.readFileSync(policiesPath, "utf-8");
1303
+ const { Client } = await import("pg");
1304
+ const client = new Client({ connectionString: databaseUrl });
1305
+ await client.connect();
1306
+ try {
1307
+ await client.query(policiesContent);
1308
+ out(chalk.green(" ✓ RLS policies applied successfully."));
1309
+ } finally {
1310
+ await client.end();
1311
+ }
1312
+ } catch (err) {
1313
+ const hint = diagnoseDbError(err, databaseUrl);
1314
+ if (hint) outError(hint);
1315
+ else outError(chalk.red(` ✗ Failed to apply RLS policies: ${err instanceof Error ? err.message : String(err)}`));
1316
+ process.exit(1);
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Remove the policies an earlier push superseded but never dropped.
1321
+ *
1322
+ * `policies.sql` only DROPs the names it is about to CREATE, and a rule's
1323
+ * generated name contains a hash of its own semantics — so editing a rule
1324
+ * writes a *new* policy and abandons the old one. Postgres ORs PERMISSIVE
1325
+ * policies together, which makes an abandoned grant outrank every tightening
1326
+ * that replaced it, and push reported success the whole time.
1327
+ *
1328
+ * Runs after `applyPolicies` so the current policies are already in place: the
1329
+ * drift check then sees exactly the set that should survive, and anything else
1330
+ * on a managed table is by definition left over.
1331
+ */
1332
+ async function reconcilePolicies(databaseUrl, collectionsPath) {
1333
+ try {
1334
+ const { checkPolicyDrift, dropOrphanedPolicies, formatPolicyDrift, hasDrift } = await import("./policy-drift-kNJ6X2Uj.js").then((n) => n.a);
1335
+ const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
1336
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1337
+ const { Client } = await import("pg");
1338
+ const client = new Client({ connectionString: databaseUrl });
1339
+ await client.connect();
1340
+ try {
1341
+ const drift = await checkPolicyDrift(client, collections);
1342
+ const { dropped, kept } = await dropOrphanedPolicies(client, drift, collections);
1343
+ for (const p of dropped) out(chalk.gray(` ✓ Dropped superseded policy "${p.name}" on ${p.schema}.${p.table}`));
1344
+ if (dropped.length > 0) out(chalk.green(` ✓ Removed ${dropped.length} superseded RLS ${dropped.length === 1 ? "policy" : "policies"}.`));
1345
+ if (kept.length > 0) {
1346
+ outWarn(chalk.yellow(" ⚠️ Policies in the database that no collection describes:"));
1347
+ for (const p of kept) outWarn(chalk.yellow(` • ${p.schema}.${p.table} → "${p.name}" (${p.command} TO ${p.roles.join(", ")})`));
1348
+ outWarn(chalk.yellow(" These still grant access. Drop them by hand if they are stale."));
1349
+ }
1350
+ const remaining = {
1351
+ ...drift,
1352
+ orphaned: kept
1353
+ };
1354
+ if (hasDrift(remaining) && (remaining.missing.length > 0 || remaining.diverged.length > 0)) {
1355
+ outWarn(chalk.yellow(" ⚠️ RLS policies do not match your collections:"));
1356
+ outWarn(formatPolicyDrift({
1357
+ ...remaining,
1358
+ orphaned: []
1359
+ }));
1360
+ }
1361
+ } finally {
1362
+ await client.end();
1363
+ }
1364
+ } catch (err) {
1365
+ outWarn(chalk.yellow(` ⚠️ Could not reconcile RLS policies: ${err instanceof Error ? err.message : String(err)}`));
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Flags the `branch` subcommands take.
1370
+ *
1371
+ * Declared as an `arg` spec rather than picked out of argv by hand, which is
1372
+ * both how the rest of this file parses flags and how the documentation
1373
+ * verifier discovers them: it reads `arg({ … })` specs out of this source, so a
1374
+ * flag parsed any other way is one it reports as unrunnable in every page that
1375
+ * documents it. Hand-rolled parsing here cost exactly that — `--older-than`
1376
+ * worked and the docs check called it a command a reader cannot run.
1377
+ */
1378
+ var BRANCH_FLAGS = {
1379
+ "--older-than": String,
1380
+ "--include-dev-diff": Boolean,
1381
+ "--from": String,
1382
+ "--yes": Boolean,
1383
+ "-y": "--yes"
1384
+ };
1385
+ async function branchCommand(rawArgs) {
1386
+ const branchAction = rawArgs[2];
1387
+ if (!branchAction || branchAction === "--help") {
1388
+ printBranchHelp();
1389
+ return;
1390
+ }
1391
+ const extras = unexpectedBranchArgs(rawArgs.slice(2));
1392
+ if (extras.length > 0) {
1393
+ outError("");
1394
+ outError(chalk.red(` ✗ Unexpected argument${extras.length > 1 ? "s" : ""}: ${extras.join(" ")}`));
1395
+ outError("");
1396
+ outError(chalk.gray(` ${chalk.bold("rebase db branch")} takes one name. A branch name may contain letters,`));
1397
+ outError(chalk.gray(" digits, underscores and hyphens — no spaces."));
1398
+ if (rawArgs[3]) outError(chalk.gray(` Did you mean: ${chalk.cyan(`rebase db branch ${branchAction} ${[rawArgs[3], ...extras].join("-")}`)}`));
1399
+ outError("");
1400
+ process.exit(1);
1401
+ }
1402
+ try {
1403
+ const dotenv = await import("dotenv");
1404
+ const envPath = process.env.DOTENV_CONFIG_PATH;
1405
+ if (envPath) dotenv.config({
1406
+ path: envPath,
1407
+ quiet: true
1408
+ });
1409
+ else dotenv.config({ quiet: true });
1410
+ } catch {}
1411
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
1412
+ if (!databaseUrl) {
1413
+ outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
1414
+ process.exit(1);
1415
+ }
1416
+ const { DatabasePoolManager } = await import("./databasePoolManager-Bj5FbeAs.js").then((n) => n.n);
1417
+ const { BranchService } = await import("./BranchService-ByLfM_Tt.js").then((n) => n.n);
1418
+ const { drizzle } = await import("drizzle-orm/node-postgres");
1419
+ const { Pool } = await import("pg");
1420
+ const pool = new Pool({
1421
+ connectionString: databaseUrl,
1422
+ max: 3
1423
+ });
1424
+ const db = drizzle(pool);
1425
+ const poolManager = new DatabasePoolManager(databaseUrl);
1426
+ const branchService = new BranchService(db, poolManager);
1427
+ await branchService.ensureBranchMetadataTable();
1428
+ try {
1429
+ switch (branchAction) {
1430
+ case "create": {
1431
+ const name = rawArgs[3];
1432
+ if (!name) {
1433
+ outError(chalk.red("✗ Branch name is required."));
1434
+ out(chalk.gray(" Usage: rebase db branch create <name> [--from <source>]"));
1435
+ process.exit(1);
1436
+ }
1437
+ let source;
1438
+ const fromIdx = rawArgs.indexOf("--from");
1439
+ if (fromIdx !== -1 && rawArgs[fromIdx + 1]) source = rawArgs[fromIdx + 1];
1440
+ const force = rawArgs.includes("--force");
1441
+ out("");
1442
+ out(chalk.bold(" 🌿 Creating database branch..."));
1443
+ out(chalk.gray(` Name: ${name}`));
1444
+ if (source) out(chalk.gray(` Source: ${source}`));
1445
+ if (force) out(chalk.gray(" Force: other connections to the source will be disconnected"));
1446
+ out("");
1447
+ const branch = await branchService.createBranch(name, {
1448
+ source,
1449
+ force
1450
+ });
1451
+ out(chalk.green(` ✓ Branch "${branch.name}" created successfully.`));
1452
+ out(chalk.gray(` Database: rb_${branch.name}`));
1453
+ out(chalk.gray(` Parent: ${branch.parentDatabase}`));
1454
+ out("");
1455
+ break;
1456
+ }
1457
+ case "list": {
1458
+ const branches = await branchService.listBranches();
1459
+ out("");
1460
+ if (branches.length === 0) out(chalk.gray(" No branches found. Create one with: rebase db branch create <name>"));
1461
+ else {
1462
+ out(chalk.bold(` 🌿 ${branches.length} branch(es):`));
1463
+ out("");
1464
+ for (const b of branches) {
1465
+ const size = b.sizeBytes != null ? chalk.gray(` (${formatBytes(b.sizeBytes)})`) : "";
1466
+ const age = chalk.gray(` — created ${timeAgo(b.createdAt)}`);
1467
+ out(` ${chalk.green("●")} ${chalk.bold(b.name)}${size}${age}`);
1468
+ out(chalk.gray(` from ${b.parentDatabase}`));
1469
+ }
1470
+ }
1471
+ out("");
1472
+ break;
1473
+ }
1474
+ case "delete": {
1475
+ const name = rawArgs[3];
1476
+ if (!name) {
1477
+ outError(chalk.red("✗ Branch name is required."));
1478
+ out(chalk.gray(" Usage: rebase db branch delete <name>"));
1479
+ process.exit(1);
1480
+ }
1481
+ out("");
1482
+ out(chalk.bold(` 🗑️ Deleting branch "${name}"...`));
1483
+ await branchService.deleteBranch(name, { force: rawArgs.includes("--force") });
1484
+ out(chalk.green(` ✓ Branch "${name}" deleted.`));
1485
+ out("");
1486
+ break;
1487
+ }
1488
+ case "info": {
1489
+ const name = rawArgs[3];
1490
+ if (!name) {
1491
+ outError(chalk.red("✗ Branch name is required."));
1492
+ out(chalk.gray(" Usage: rebase db branch info <name>"));
1493
+ process.exit(1);
1494
+ }
1495
+ const info = await branchService.getBranchInfo(name);
1496
+ out("");
1497
+ if (!info) {
1498
+ outError(chalk.red(` ✗ Branch "${name}" not found.`));
1499
+ out("");
1500
+ process.exit(1);
1501
+ } else {
1502
+ out(chalk.bold(` 🌿 Branch: ${info.name}`));
1503
+ out(chalk.gray(` Database: rb_${info.name}`));
1504
+ out(chalk.gray(` Parent: ${info.parentDatabase}`));
1505
+ out(chalk.gray(` Created: ${info.createdAt.toISOString()}`));
1506
+ if (info.sizeBytes != null) out(chalk.gray(` Size: ${formatBytes(info.sizeBytes)}`));
1507
+ }
1508
+ out("");
1509
+ break;
1510
+ }
1511
+ case "prune": {
1512
+ const parsed = (0, import_arg.default)(BRANCH_FLAGS, {
1513
+ argv: rawArgs.slice(3),
1514
+ permissive: true
1515
+ });
1516
+ const olderThanRaw = parsed["--older-than"] ?? null;
1517
+ const olderThanDays = olderThanRaw == null ? null : parseOlderThan(olderThanRaw);
1518
+ if (olderThanRaw != null && olderThanDays == null) {
1519
+ outError(chalk.red(` ✗ --older-than "${olderThanRaw}" is not a duration.`));
1520
+ out(chalk.gray(" Use a number of days (14), or 14d / 2w."));
1521
+ process.exit(1);
1522
+ }
1523
+ const includeDevDiff = parsed["--include-dev-diff"] === true;
1524
+ const { rows, databases } = await branchService.pruneCandidates();
1525
+ const plan = planPrune({
1526
+ rows,
1527
+ databases,
1528
+ branchPrefix: "rb_"
1529
+ }, { olderThanDays });
1530
+ out("");
1531
+ if (planIsEmpty(plan, includeDevDiff)) {
1532
+ out(chalk.gray(" Nothing to prune."));
1533
+ if (!olderThanDays && rows.length > 0) out(chalk.gray(` ${rows.length} branch(es) are healthy — --older-than 14 also removes old ones.`));
1534
+ if (!includeDevDiff && plan.devDiff.length > 0) {
1535
+ out(chalk.gray(` ${plan.devDiff.length} Atlas scratch database(s) left over from db push;`));
1536
+ out(chalk.gray(" --include-dev-diff removes those too."));
1537
+ }
1538
+ out("");
1539
+ break;
1540
+ }
1541
+ out(chalk.bold(" 🧹 Prune plan"));
1542
+ out("");
1543
+ for (const row of plan.staleRows) out(` ${chalk.yellow("○")} ${chalk.bold(row.name)} ${chalk.gray("— registered, but its database is gone; the entry will be removed")}`);
1544
+ for (const dbName of plan.orphanDatabases) out(` ${chalk.yellow("○")} ${chalk.bold(dbName)} ${chalk.gray("— a branch database with no entry; the database will be dropped")}`);
1545
+ for (const { branch, ageDays } of plan.expired) out(` ${chalk.red("●")} ${chalk.bold(branch.name)} ${chalk.gray(`— ${ageDays} days old; branch and database will be dropped`)}`);
1546
+ if (includeDevDiff) for (const dbName of plan.devDiff) out(` ${chalk.red("●")} ${chalk.bold(dbName)} ${chalk.gray("— Atlas scratch database; will be dropped")}`);
1547
+ else if (plan.devDiff.length > 0) out(chalk.gray(` (${plan.devDiff.length} Atlas scratch database(s) not listed — --include-dev-diff)`));
1548
+ out("");
1549
+ if (parsed["--yes"] !== true) {
1550
+ if (!await promptConfirm(" Remove these? (y/N) ")) {
1551
+ out(chalk.gray(" Nothing was changed."));
1552
+ out("");
1553
+ break;
1554
+ }
1555
+ }
1556
+ let removed = 0;
1557
+ for (const row of plan.staleRows) {
1558
+ await branchService.forgetBranchRow(row.name);
1559
+ removed += 1;
1560
+ }
1561
+ for (const dbName of plan.orphanDatabases) {
1562
+ await branchService.dropDatabase(dbName);
1563
+ removed += 1;
1564
+ }
1565
+ for (const { branch } of plan.expired) {
1566
+ await branchService.deleteBranch(branch.name);
1567
+ removed += 1;
1568
+ }
1569
+ if (includeDevDiff) for (const dbName of plan.devDiff) {
1570
+ await branchService.dropDatabase(dbName);
1571
+ removed += 1;
1572
+ }
1573
+ out(chalk.green(` ✓ Pruned ${removed} item(s).`));
1574
+ out("");
1575
+ break;
1576
+ }
1577
+ default:
1578
+ outError(chalk.red(`Unknown branch action: "${branchAction}".`));
1579
+ printBranchHelp();
1580
+ process.exit(1);
1581
+ }
1582
+ } finally {
1583
+ await poolManager.shutdown();
1584
+ await pool.end();
1585
+ }
1586
+ }
1587
+ function printBranchHelp() {
1588
+ out(`
1589
+ ${chalk.bold("rebase db branch")} — Database branching commands
1590
+
1591
+ ${chalk.green.bold("Usage")}
1592
+ rebase db branch ${chalk.blue("<command>")} [options]
1593
+
1594
+ ${chalk.green.bold("Commands")}
1595
+ ${chalk.blue.bold("create")} <name> [--from <source>] Create a new branch
1596
+ ${chalk.blue.bold("list")} List all branches
1597
+ ${chalk.blue.bold("switch")} [<name>|--off] Point this checkout at a branch
1598
+ ${chalk.blue.bold("delete")} <name> Delete a branch
1599
+ ${chalk.blue.bold("info")} <name> Show branch details
1600
+ ${chalk.blue.bold("prune")} [--older-than <14|14d|2w>] Remove branches nothing is using
1601
+
1602
+ ${chalk.green.bold("Why prune")}
1603
+ Every branch is a full-size copy — CREATE DATABASE ... TEMPLATE duplicates the
1604
+ files on disk, so five branches of a 100GB database cost 500GB. Prune also
1605
+ finds the two ways branches drift: an entry whose database was dropped
1606
+ outside Rebase, and a branch database whose entry never got written.
1607
+
1608
+ ${chalk.green.bold("Options")}
1609
+ ${chalk.blue.bold("--force")} Disconnect other sessions first.
1610
+ Postgres refuses to copy or drop a database
1611
+ anything else is connected to — usually your
1612
+ own \`rebase dev\`.
1613
+ ${chalk.blue.bold("--include-dev-diff")} Also prune the Atlas scratch databases
1614
+ \`db push\` leaves behind.
1615
+ ${chalk.blue.bold("--yes, -y")} Skip prune's confirmation prompt. Dropping a
1616
+ branch is not undoable, so this is the flag
1617
+ a CI job passes and a person usually should not.
1618
+
1619
+ ${chalk.green.bold("Examples")}
1620
+ ${chalk.gray("# Create a branch and work on it")}
1621
+ rebase db branch create feature_auth
1622
+ rebase db branch switch feature_auth
1623
+
1624
+ ${chalk.gray("# Create a branch from a specific source")}
1625
+ rebase db branch create staging --from production
1626
+
1627
+ ${chalk.gray("# Remove branches older than two weeks, and anything orphaned")}
1628
+ rebase db branch prune --older-than 2w
1629
+
1630
+ ${chalk.gray("# List all branches")}
1631
+ rebase db branch list
1632
+
1633
+ ${chalk.gray("# Delete a branch")}
1634
+ rebase db branch delete feature_auth
1635
+ `);
1636
+ }
1637
+ function formatBytes(bytes) {
1638
+ if (bytes < 1024) return `${bytes} B`;
1639
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1640
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1641
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
1642
+ }
1643
+ /** A branch's age, for "…created 6d ago". */
1644
+ function timeAgo(date) {
1645
+ return formatRelativeTime(date, { maxMs: Number.POSITIVE_INFINITY }) ?? "unknown";
1646
+ }
1647
+ /** Set when this process created the Atlas scratch database; see `ensureDevDatabaseExists`. */
1648
+ var scratchDatabaseCreatedHere = false;
1649
+ async function runAtlas(domain, args, collectionsPath, opts = {}) {
1650
+ const atlasBin = resolveLocalBin("atlas");
1651
+ if (!atlasBin) {
1652
+ outError(chalk.red("\n✗ The atlas binary is missing, so the schema cannot be applied.\n"));
1653
+ const manager = detectProjectPackageManager();
1654
+ const diagnosis = diagnoseMissingBin("@ariga/atlas");
1655
+ if (diagnosis === "bin-link-missing") {
1656
+ outError(chalk.yellow(" @ariga/atlas and its binary are both on disk — only the\n node_modules/.bin/atlas link that puts it on PATH is missing.\n"));
1657
+ outError(chalk.gray(" Usually a half-written install: the link is created before the script that\n downloads the target, so an interrupted or copied tree keeps the binary and\n loses the shim.\n"));
1658
+ outError(" Fix it with:\n");
1659
+ outError(chalk.bold(` ${describeReinstallCommand(manager)}\n`));
1660
+ } else if (diagnosis === "build-script-blocked") {
1661
+ outError(chalk.yellow(" @ariga/atlas IS installed — only its binary is missing.\n"));
1662
+ outError(chalk.gray(" It downloads that binary in a `preinstall` script, and pnpm 10+ and npm 12+\n do not run a dependency's scripts unless you allow it. The install still\n exits 0, so the only sign is a line several screens up: pnpm's\n `Ignored build scripts: @ariga/atlas`, or npm's `install scripts blocked`.\n"));
1663
+ outError(" Fix it with either:\n");
1664
+ for (const line of describeBuildScriptRemedy("@ariga/atlas", manager)) outError(line ? chalk.bold(line) : "");
1665
+ outError("");
1666
+ } else {
1667
+ outError(chalk.gray(" It is not installed in this project.\n"));
1668
+ outError(" Install it with:\n");
1669
+ outError(chalk.bold(` ${describeDevAddCommand("@ariga/atlas", manager)}\n`));
1670
+ outError(chalk.gray(" If the install then reports blocked or ignored build scripts, allow them too —\n the package carries a `preinstall` script that fetches the binary.\n"));
1671
+ }
1672
+ process.exit(1);
1673
+ }
1674
+ const env = { ...process.env };
1675
+ try {
1676
+ const dotenv = await import("dotenv");
1677
+ const envPaths = [
1678
+ process.env.DOTENV_CONFIG_PATH,
1679
+ path.resolve(process.cwd(), ".env"),
1680
+ path.resolve(process.cwd(), "../.env"),
1681
+ path.resolve(process.cwd(), "../../.env")
1682
+ ].filter(Boolean);
1683
+ for (const p of envPaths) if (fs.existsSync(p)) {
1684
+ const parsed = dotenv.config({
1685
+ path: p,
1686
+ quiet: true
1687
+ });
1688
+ if (parsed.parsed) {
1689
+ for (const [key, val] of Object.entries(parsed.parsed)) if (env[key] === void 0) env[key] = val;
1690
+ break;
1691
+ }
1692
+ }
1693
+ } catch {}
1694
+ const databaseUrl = env.DATABASE_URL;
1695
+ if (!databaseUrl) {
1696
+ outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
1697
+ process.exit(1);
1698
+ }
1699
+ await checkDatabaseConnectivity(databaseUrl);
1700
+ const devDatabaseUrl = getDevDatabaseUrl(databaseUrl);
1701
+ if (await ensureDevDatabaseExists(databaseUrl, devDatabaseUrl)) scratchDatabaseCreatedHere = true;
1702
+ const atlasUrl = forLibpq(databaseUrl);
1703
+ const atlasDevUrl = forLibpq(devDatabaseUrl);
1704
+ const excludes = [];
1705
+ if (collectionsPath && acceptsExcludeFlag(domain, args)) {
1706
+ excludes.push(...await getSearchExcludes(collectionsPath));
1707
+ excludes.push(...await getVectorExcludes(collectionsPath));
1708
+ try {
1709
+ excludes.push(...await getTableExcludes(databaseUrl, collectionsPath));
1710
+ } catch (err) {
1711
+ if (err instanceof ExcludeIntrospectionError) {
1712
+ outError(chalk.red("\n✗ Aborting push: could not determine which tables to protect."));
1713
+ outError(chalk.gray(` ${err.message}`));
1714
+ outError(chalk.gray(" Refusing to apply — a partial exclude list could drop tables Rebase does not manage."));
1715
+ const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
1716
+ if (hint) outError(hint);
1717
+ process.exit(1);
1718
+ }
1719
+ throw err;
1720
+ }
1721
+ try {
1722
+ excludes.push(...await getForeignIndexExcludes(databaseUrl, collectionsPath));
1723
+ } catch (err) {
1724
+ if (err instanceof ExcludeIntrospectionError) {
1725
+ outError(chalk.red("\n✗ Aborting push: could not determine which indexes to protect."));
1726
+ outError(chalk.gray(` ${err.message}`));
1727
+ outError(chalk.gray(" Refusing to apply — a partial exclude list could drop an index Rebase does not manage."));
1728
+ const hint = diagnoseDbError(err.cause ?? err, databaseUrl);
1729
+ if (hint) outError(hint);
1730
+ process.exit(1);
1731
+ }
1732
+ throw err;
1733
+ }
1734
+ }
1735
+ const subprocess = execa(atlasBin, buildAtlasArgs({
1736
+ domain,
1737
+ args,
1738
+ url: atlasUrl,
1739
+ devUrl: atlasDevUrl,
1740
+ excludes
1741
+ }), {
1742
+ cwd: process.cwd(),
1743
+ stdout: opts.captureStdout ? "pipe" : "inherit",
1744
+ stderr: "pipe",
1745
+ env
1746
+ });
1747
+ let stdoutText = "";
1748
+ if (opts.captureStdout) subprocess.stdout?.on("data", (chunk) => {
1749
+ stdoutText += chunk.toString();
1750
+ });
1751
+ let stderrText = "";
1752
+ subprocess.stderr?.on("data", (chunk) => {
1753
+ const text = chunk.toString();
1754
+ stderrText += text;
1755
+ process.stderr.write(text);
1756
+ });
1757
+ try {
1758
+ await subprocess;
1759
+ } catch {
1760
+ outError(chalk.red(`\n✗ atlas ${domain} ${args.join(" ")} failed.\n`));
1761
+ const hint = await diagnoseAtlasFailure({
1762
+ domain,
1763
+ args,
1764
+ stderr: stderrText,
1765
+ databaseUrl,
1766
+ latestMigrationVersion: latestMigrationVersion()
1767
+ }) ?? diagnoseDbError({ message: stderrText }, databaseUrl);
1768
+ if (hint) outError(hint);
1769
+ process.exit(1);
1770
+ }
1771
+ if (opts.captureStdout) return stdoutText.trim().length > 0 ? stdoutText : stderrText;
1772
+ return "";
1773
+ }
1774
+ async function generatePostgresDdlCommand(rawArgs) {
1775
+ const argsList = (0, import_arg.default)({
1776
+ "--collections": String,
1777
+ "--output": String,
1778
+ "-c": "--collections",
1779
+ "-o": "--output"
1780
+ }, {
1781
+ argv: rawArgs.slice(2),
1782
+ permissive: true
1783
+ });
1784
+ const ddl = resolveChildScript("generate-postgres-ddl");
1785
+ if (!ddl.ok) {
1786
+ outError(chalk.red(childScriptError("DDL generator", ddl.reason)));
1787
+ process.exit(1);
1788
+ }
1789
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1790
+ const outputPath = argsList["--output"] || path.join("drizzle", "schema.sql");
1791
+ const cmdParts = [
1792
+ ddl.bin,
1793
+ ddl.script,
1794
+ `--collections=${collectionsPath}`,
1795
+ `--output=${outputPath}`
1796
+ ];
1797
+ try {
1798
+ await execa(cmdParts[0], cmdParts.slice(1), {
1799
+ cwd: process.cwd(),
1800
+ stdio: "inherit",
1801
+ env: { ...process.env }
1802
+ });
1803
+ } catch (err) {
1804
+ if (typeof err.exitCode === "number") outError(chalk.red("✗ Postgres DDL generation failed — nothing was applied."));
1805
+ else outError(chalk.red(`✗ Failed to run Postgres DDL generator: ${err instanceof Error ? err.message : String(err)}`));
1806
+ process.exit(1);
1807
+ }
1808
+ }
1809
+ /**
1810
+ * `schema stale [--fix]` — is the generated Drizzle schema older than the rule
1811
+ * that derives its foreign-key names?
1812
+ *
1813
+ * This exists for one upgrade path and is worth the command. 0.13 derives
1814
+ * `category_id` where 0.12 derived `categorie_id`; boot-ensure renames the
1815
+ * database column to match, and the project's checked-in
1816
+ * `backend/src/schema.generated.ts` is then wrong in a way nothing the developer
1817
+ * did would explain. Relation validation refuses to boot on it, permanently,
1818
+ * because the rename has already been applied and will not run again.
1819
+ *
1820
+ * `rebase dev` calls this with `--fix` before starting the backend, so the
1821
+ * upgrade behaves the way the release note says it does: the column moves and the
1822
+ * project keeps working. Without `--fix` it reports and exits non-zero, which is
1823
+ * what a build or a CI step wants.
1824
+ */
1825
+ async function schemaStaleCommand(rawArgs) {
1826
+ const argsList = (0, import_arg.default)({
1827
+ "--collections": String,
1828
+ "--output": String,
1829
+ "--fix": Boolean,
1830
+ "-c": "--collections",
1831
+ "-o": "--output",
1832
+ "--out": "--output"
1833
+ }, {
1834
+ argv: rawArgs.slice(2),
1835
+ permissive: true
1836
+ });
1837
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1838
+ const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
1839
+ const schemaFile = path.resolve(process.cwd(), outputPath);
1840
+ const fix = Boolean(argsList["--fix"]);
1841
+ const generatedExists = fs.existsSync(schemaFile);
1842
+ const { loadCollections } = await import("./doctor-D7HWmGzs.js").then((n) => n.t);
1843
+ const { findLegacyForeignKeyNames, staleVerdict } = await import("./generated-schema-staleness-DQdfJl2A.js");
1844
+ let stale = [];
1845
+ let unreadable;
1846
+ if (generatedExists) try {
1847
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
1848
+ stale = findLegacyForeignKeyNames(fs.readFileSync(schemaFile, "utf8"), collections);
1849
+ } catch (err) {
1850
+ unreadable = err instanceof Error ? err.message : String(err);
1851
+ logger.debug(`schema stale: skipped (${unreadable})`);
1852
+ }
1853
+ const verdict = staleVerdict({
1854
+ outputPath,
1855
+ generatedExists,
1856
+ stale,
1857
+ unreadable,
1858
+ fix
1859
+ });
1860
+ for (const line of verdict.lines) if (!line) out("");
1861
+ else if (line.includes("⚠️")) outWarn(chalk.yellow(line));
1862
+ else if (verdict.exitCode !== 0) outError(chalk.red(line));
1863
+ else if (line.includes("✓")) out(chalk.green(line));
1864
+ else out(chalk.gray(line));
1865
+ if (verdict.exitCode !== 0) process.exit(verdict.exitCode);
1866
+ if (!verdict.regenerate) return;
1867
+ await schemaCommand("generate", [
1868
+ "schema",
1869
+ "generate",
1870
+ `--collections=${collectionsPath}`,
1871
+ `--output=${outputPath}`
1872
+ ]);
1873
+ }
1874
+ async function schemaCommand(subcommand, rawArgs) {
1875
+ if (subcommand && (rawArgs.includes("--help") || rawArgs.includes("-h"))) {
1876
+ out("");
1877
+ out(chalk.bold(` rebase schema ${subcommand}`));
1878
+ out(chalk.gray(" Run `rebase schema --help` for the full page — this is the driver's own entry point."));
1879
+ out("");
1880
+ return;
1881
+ }
1882
+ if (subcommand === "stale") {
1883
+ await schemaStaleCommand(rawArgs);
1884
+ return;
1885
+ }
1886
+ if (subcommand === "generate") {
1887
+ const argsList = (0, import_arg.default)({
1888
+ "--collections": String,
1889
+ "--output": String,
1890
+ "--watch": Boolean,
1891
+ "-c": "--collections",
1892
+ "-o": "--output",
1893
+ "--out": "--output",
1894
+ "-w": "--watch"
1895
+ }, {
1896
+ argv: rawArgs.slice(2),
1897
+ permissive: true
1898
+ });
1899
+ const generator = resolveChildScript("generate-drizzle-schema");
1900
+ if (!generator.ok) {
1901
+ outError(chalk.red(childScriptError("schema generator", generator.reason)));
1902
+ process.exit(1);
1903
+ }
1904
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
1905
+ const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
1906
+ const watch = argsList["--watch"] || false;
1907
+ out("");
1908
+ out(chalk.bold(" 🔧 Rebase Schema Generator"));
1909
+ out("");
1910
+ const cmdParts = [
1911
+ generator.bin,
1912
+ generator.script,
1913
+ `--collections=${collectionsPath}`,
1914
+ `--output=${outputPath}`
1915
+ ];
1916
+ if (watch) cmdParts.push("--watch");
1917
+ try {
1918
+ await execa(cmdParts[0], cmdParts.slice(1), {
1919
+ cwd: process.cwd(),
1920
+ stdio: "inherit",
1921
+ env: { ...process.env }
1922
+ });
1923
+ } catch (err) {
1924
+ if (typeof err.exitCode === "number") outError(chalk.red("✗ Drizzle schema generation failed — nothing was applied."));
1925
+ else outError(chalk.red(`✗ Failed to run schema generator: ${err instanceof Error ? err.message : String(err)}`));
1926
+ process.exit(1);
1927
+ }
1928
+ } else if (subcommand === "introspect") {
1929
+ const argsList = (0, import_arg.default)({
1930
+ "--output": String,
1931
+ "--collections": String,
1932
+ "--force": Boolean,
1933
+ "--schema": String,
1934
+ "-o": "--output",
1935
+ "--out": "--output",
1936
+ "-c": "--collections",
1937
+ "-f": "--force"
1938
+ }, {
1939
+ argv: rawArgs.slice(2),
1940
+ permissive: true
1941
+ });
1942
+ const introspect = resolveChildScript("introspect-db");
1943
+ if (!introspect.ok) {
1944
+ outError(chalk.red(childScriptError("introspection script", introspect.reason)));
1945
+ process.exit(1);
1946
+ }
1947
+ const outputPath = argsList["--output"] || argsList["--collections"] || path.join("..", "config", "collections");
1948
+ out("");
1949
+ out(chalk.bold(" 🔍 Rebase Schema Introspector"));
1950
+ out("");
1951
+ const cmdParts = [
1952
+ introspect.bin,
1953
+ introspect.script,
1954
+ `--output=${outputPath}`,
1955
+ ...argsList["--force"] ? ["--force"] : [],
1956
+ ...argsList["--schema"] ? [`--schema=${argsList["--schema"]}`] : []
1957
+ ];
1958
+ try {
1959
+ await execa(cmdParts[0], cmdParts.slice(1), {
1960
+ cwd: process.cwd(),
1961
+ stdio: "inherit",
1962
+ env: { ...process.env }
1963
+ });
1964
+ } catch (err) {
1965
+ outError(chalk.red(`✗ Failed to run schema introspector: ${err instanceof Error ? err.message : String(err)}`));
1966
+ process.exit(1);
1967
+ }
1968
+ } else {
1969
+ outError(chalk.red("Unknown schema command."));
1970
+ process.exit(1);
1971
+ }
1972
+ }
1973
+ async function doctorPluginCommand(rawArgs) {
1974
+ const parsedArgs = (0, import_arg.default)({
1975
+ "--collections": String,
1976
+ "--schema": String,
1977
+ "--sdk": String,
1978
+ "--policies": Boolean,
1979
+ "-c": "--collections",
1980
+ "-s": "--schema",
1981
+ "-k": "--sdk"
1982
+ }, {
1983
+ argv: rawArgs.slice(1),
1984
+ permissive: true
1985
+ });
1986
+ const doctor = resolveChildScript("doctor-cli");
1987
+ if (!doctor.ok) {
1988
+ outError(chalk.red(childScriptError("doctor script", doctor.reason)));
1989
+ process.exit(1);
1990
+ }
1991
+ const collectionsPath = parsedArgs["--collections"] || path.join("..", "config", "collections");
1992
+ const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
1993
+ const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
1994
+ const cmdParts = [
1995
+ doctor.bin,
1996
+ doctor.script,
1997
+ `--collections=${collectionsPath}`,
1998
+ `--schema=${schemaPath}`,
1999
+ `--sdk=${sdkPath}`,
2000
+ ...parsedArgs["--policies"] ? ["--policies"] : []
2001
+ ];
2002
+ try {
2003
+ await execa(cmdParts[0], cmdParts.slice(1), {
2004
+ cwd: process.cwd(),
2005
+ stdio: "inherit",
2006
+ env: { ...process.env }
2007
+ });
2008
+ } catch {
2009
+ process.exit(1);
2010
+ }
2011
+ }
2012
+ var argv1Real = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
2013
+ if (import.meta.url === `file://${argv1Real}`) runPluginCommand(process.argv.slice(2)).catch((error) => {
2014
+ reportCommandFailure(error);
2015
+ process.exit(1);
2016
+ });
2017
+ //#endregion
2018
+ export { runPluginCommand };
2019
+
2020
+ //# sourceMappingURL=cli.js.map