@rebasepro/cli 0.24.0 → 0.24.1-canary.g19596ef

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 (156) hide show
  1. package/bin/rebase.js +3 -0
  2. package/dist/agent-setup-B31F00DC.js +324 -0
  3. package/dist/agent-setup-B31F00DC.js.map +1 -0
  4. package/dist/api-keys-Bl1I9q7M.js +525 -0
  5. package/dist/api-keys-Bl1I9q7M.js.map +1 -0
  6. package/dist/apps-CNgQJjg5.js +340 -0
  7. package/dist/apps-CNgQJjg5.js.map +1 -0
  8. package/dist/args-BnmsEXvu.js +109 -0
  9. package/dist/args-BnmsEXvu.js.map +1 -0
  10. package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
  11. package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
  12. package/dist/auth-aySHEF45.js +414 -0
  13. package/dist/auth-aySHEF45.js.map +1 -0
  14. package/dist/branch-pointer-DvSHBjjz.js +2 -0
  15. package/dist/build-DIHWIqO4.js +336 -0
  16. package/dist/build-DIHWIqO4.js.map +1 -0
  17. package/dist/bundle-CqURVN-K.js +1621 -0
  18. package/dist/bundle-CqURVN-K.js.map +1 -0
  19. package/dist/bundle.d.ts +6 -1
  20. package/dist/cli.d.ts +2 -2
  21. package/dist/cloud-DjByQlkW.js +9667 -0
  22. package/dist/cloud-DjByQlkW.js.map +1 -0
  23. package/dist/collection-drift-BKj_BoMq.js +106 -0
  24. package/dist/collection-drift-BKj_BoMq.js.map +1 -0
  25. package/dist/commands/api-keys.d.ts +2 -0
  26. package/dist/commands/console.d.ts +12 -0
  27. package/dist/commands/db.d.ts +64 -24
  28. package/dist/commands/dev.d.ts +103 -6
  29. package/dist/commands/generate_sdk.d.ts +26 -0
  30. package/dist/commands/init.d.ts +58 -0
  31. package/dist/commands/skills.d.ts +22 -7
  32. package/dist/commands/start.d.ts +15 -0
  33. package/dist/commands/storage.d.ts +46 -0
  34. package/dist/commands/upgrade.d.ts +1 -0
  35. package/dist/consent-ClRHmcxf.js +76 -0
  36. package/dist/consent-ClRHmcxf.js.map +1 -0
  37. package/dist/console-D6SJf8k_.js +102 -0
  38. package/dist/console-D6SJf8k_.js.map +1 -0
  39. package/dist/context-BqhRydBg.js +1234 -0
  40. package/dist/context-BqhRydBg.js.map +1 -0
  41. package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
  42. package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
  43. package/dist/daemon-pzsQwDHa.js +2 -0
  44. package/dist/db-B7RuNuaQ.js +1312 -0
  45. package/dist/db-B7RuNuaQ.js.map +1 -0
  46. package/dist/derive-Dl0z2OWi.js +484 -0
  47. package/dist/derive-Dl0z2OWi.js.map +1 -0
  48. package/dist/dev-CYr_WXan.js +1599 -0
  49. package/dist/dev-CYr_WXan.js.map +1 -0
  50. package/dist/dev-db/atlas-dev-database.d.ts +32 -0
  51. package/dist/dev-db/daemon-entry.d.ts +13 -0
  52. package/dist/dev-db/last-dev-database.d.ts +41 -0
  53. package/dist/dev-db/prepare.d.ts +19 -3
  54. package/dist/dev-db/pull.d.ts +90 -5
  55. package/dist/dev-db/resolve.d.ts +26 -2
  56. package/dist/dev-preflight-B-LYyBf1.js +2 -0
  57. package/dist/dev-server-DG8Tt-uf.js +96 -0
  58. package/dist/dev-server-DG8Tt-uf.js.map +1 -0
  59. package/dist/doctor-BP8cZ-7F.js +771 -0
  60. package/dist/doctor-BP8cZ-7F.js.map +1 -0
  61. package/dist/eject-DizdoecV.js +335 -0
  62. package/dist/eject-DizdoecV.js.map +1 -0
  63. package/dist/fold-static-BC9CYicw.js +201 -0
  64. package/dist/fold-static-BC9CYicw.js.map +1 -0
  65. package/dist/function-portability-BIvlvR9q.js +265 -0
  66. package/dist/function-portability-BIvlvR9q.js.map +1 -0
  67. package/dist/generate_sdk-BPqdJc3Q.js +489 -0
  68. package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
  69. package/dist/index.es.js +151 -24674
  70. package/dist/index.es.js.map +1 -1
  71. package/dist/init-Utstvh2Q.js +1149 -0
  72. package/dist/init-Utstvh2Q.js.map +1 -0
  73. package/dist/last-dev-database-B58g89uA.js +2 -0
  74. package/dist/manifest-DRp0UVTj.js +880 -0
  75. package/dist/manifest-DRp0UVTj.js.map +1 -0
  76. package/dist/manifest.d.ts +15 -3
  77. package/dist/normalize-imports-X0Tgze9y.js +107 -0
  78. package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
  79. package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
  80. package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
  81. package/dist/path-flags-BFgG9V6y.js +91 -0
  82. package/dist/path-flags-BFgG9V6y.js.map +1 -0
  83. package/dist/precompress.d.ts +17 -0
  84. package/dist/prepare-CDN6lSpz.js +1236 -0
  85. package/dist/prepare-CDN6lSpz.js.map +1 -0
  86. package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
  87. package/dist/project-Wp2zoyeN.js.map +1 -0
  88. package/dist/pull-CrGYMEsy.js +458 -0
  89. package/dist/pull-CrGYMEsy.js.map +1 -0
  90. package/dist/resolve-B2O-wKKk.js +2 -0
  91. package/dist/resources-FD7AfZx8.js +183 -0
  92. package/dist/resources-FD7AfZx8.js.map +1 -0
  93. package/dist/schema-DMdr431S.js +125 -0
  94. package/dist/schema-DMdr431S.js.map +1 -0
  95. package/dist/skills-DTFibT1m.js +642 -0
  96. package/dist/skills-DTFibT1m.js.map +1 -0
  97. package/dist/start-DEa-hOBh.js +267 -0
  98. package/dist/start-DEa-hOBh.js.map +1 -0
  99. package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
  100. package/dist/state-Do7ZJYkF.js.map +1 -0
  101. package/dist/status-DxnJTekv.js +352 -0
  102. package/dist/status-DxnJTekv.js.map +1 -0
  103. package/dist/storage-DyVq-f_F.js +303 -0
  104. package/dist/storage-DyVq-f_F.js.map +1 -0
  105. package/dist/telemetry/identity.d.ts +10 -1
  106. package/dist/telemetry-C8Uq8phb.js +129 -0
  107. package/dist/telemetry-C8Uq8phb.js.map +1 -0
  108. package/dist/telemetry-EMFRVRQi.js +351 -0
  109. package/dist/telemetry-EMFRVRQi.js.map +1 -0
  110. package/dist/unknown-command-DNwDcX3w.js +98 -0
  111. package/dist/unknown-command-DNwDcX3w.js.map +1 -0
  112. package/dist/upgrade-CtZtRkTE.js +1332 -0
  113. package/dist/upgrade-CtZtRkTE.js.map +1 -0
  114. package/dist/upgrade.d.ts +24 -3
  115. package/dist/utils/dev-preflight.d.ts +47 -5
  116. package/dist/utils/dev-server.d.ts +30 -0
  117. package/dist/utils/path-flags.d.ts +15 -2
  118. package/dist/utils/project.d.ts +16 -0
  119. package/dist/utils/temp-archive.d.ts +9 -0
  120. package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
  121. package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
  122. package/package.json +7 -7
  123. package/templates/eject/Dockerfile +4 -2
  124. package/templates/eject/backend/src/env.ts +1 -1
  125. package/templates/eject/backend/src/index.ts +29 -4
  126. package/templates/eject/docker-compose.custom.yml +8 -4
  127. package/templates/generated-schemas/blank.ts +40 -0
  128. package/templates/generated-schemas/blog.ts +127 -0
  129. package/templates/generated-schemas/ecommerce.ts +93 -0
  130. package/templates/overlays/baas/config/package.json +1 -0
  131. package/templates/overlays/baas/package.json +1 -0
  132. package/templates/template/.env.example +10 -4
  133. package/templates/template/README.md +11 -4
  134. package/templates/template/ai-instructions.md +3 -2
  135. package/templates/template/config/resources.ts +7 -6
  136. package/templates/template/docker-compose.yml +9 -3
  137. package/templates/template/frontend/vite.config.ts +13 -3
  138. package/templates/template/package.json +1 -0
  139. package/templates/template/scripts/example.ts +2 -1
  140. package/templates/template/tsconfig.json +25 -0
  141. package/dist/branch-pointer-kk9F5FBO.js +0 -111
  142. package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
  143. package/dist/constraints-DKGbNfUW.js +0 -48
  144. package/dist/constraints-DKGbNfUW.js.map +0 -1
  145. package/dist/daemon-C8p32i-n.js +0 -357
  146. package/dist/daemon-C8p32i-n.js.map +0 -1
  147. package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
  148. package/dist/dev-preflight-CNLl4rdo.js +0 -367
  149. package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
  150. package/dist/project-DmaGLmH2.js.map +0 -1
  151. package/dist/pull-Dywm5FWP.js +0 -232
  152. package/dist/pull-Dywm5FWP.js.map +0 -1
  153. package/dist/resolve-BAeRUBhg.js +0 -74
  154. package/dist/resolve-BAeRUBhg.js.map +0 -1
  155. package/dist/state-C59Elrnt.js.map +0 -1
  156. package/templates/template/backend/src/schema.generated.ts +0 -13
@@ -0,0 +1,771 @@
1
+ import { f as requireBackendDir, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, o as findEnvFile, p as requireProjectRoot, r as exitDependenciesNotInstalled } from "./project-Wp2zoyeN.js";
2
+ import { n as parseCommandArgs, r as wantsHelp } from "./args-BnmsEXvu.js";
3
+ import { d as findBackendApp, g as resolveBackendPaths, p as loadManifest } from "./manifest-DRp0UVTj.js";
4
+ import { n as summarisePortability, t as analyseFunctionsDirectory } from "./function-portability-BIvlvR9q.js";
5
+ import { r as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-BFgG9V6y.js";
6
+ import { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from "./prepare-CDN6lSpz.js";
7
+ import { absolutizeLocalPathArgs, n as argsFromCommand, t as reportSpawnFailure } from "./db-B7RuNuaQ.js";
8
+ import chalk from "chalk";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath, pathToFileURL } from "url";
12
+ import { createRequire } from "module";
13
+ import { execa } from "execa";
14
+ //#region src/utils/libpq-url.ts
15
+ /** Postgres URI schemes. A key/value DSN ("host=… dbname=…") is not one. */
16
+ var POSTGRES_URI_RE = /^postgres(ql)?:\/\//i;
17
+ /**
18
+ * Return the query parameters libpq would reject, or an empty array.
19
+ *
20
+ * Deliberately not built on `new URL()`: that parser is happy to accept the
21
+ * broken form (it splits on the first `=` and keeps the rest as the value),
22
+ * so it cannot see the defect at all. The rule being checked is libpq's, and
23
+ * it is about the raw text.
24
+ */
25
+ function findUnparseableParams(connectionString) {
26
+ if (!POSTGRES_URI_RE.test(connectionString)) return [];
27
+ const q = connectionString.indexOf("?");
28
+ if (q === -1) return [];
29
+ const query = connectionString.slice(q + 1).split("#")[0];
30
+ if (!query) return [];
31
+ const found = [];
32
+ for (const part of query.split("&")) {
33
+ if (!part) continue;
34
+ if (part.split("=").length > 2) found.push({
35
+ name: part.slice(0, part.indexOf("=")),
36
+ raw: part
37
+ });
38
+ }
39
+ return found;
40
+ }
41
+ /**
42
+ * Percent-encode the offending `=` characters, leaving everything else byte for
43
+ * byte as it was.
44
+ *
45
+ * Only the second and later `=` in a parameter are rewritten — the first is the
46
+ * real separator. Nothing else is touched: re-serialising through `URL` would
47
+ * also reorder parameters and turn spaces into `+`, which libpq does not decode,
48
+ * so a "tidy up" would trade one unparseable string for another.
49
+ */
50
+ function encodeExtraEquals(connectionString) {
51
+ if (findUnparseableParams(connectionString).length === 0) return connectionString;
52
+ const q = connectionString.indexOf("?");
53
+ const head = connectionString.slice(0, q + 1);
54
+ const rest = connectionString.slice(q + 1);
55
+ const hash = rest.indexOf("#");
56
+ const query = hash === -1 ? rest : rest.slice(0, hash);
57
+ const fragment = hash === -1 ? "" : rest.slice(hash);
58
+ return `${head}${query.split("&").map((part) => {
59
+ const first = part.indexOf("=");
60
+ if (first === -1) return part;
61
+ return `${part.slice(0, first)}=${part.slice(first + 1).replace(/=/g, "%3D")}`;
62
+ }).join("&")}${fragment}`;
63
+ }
64
+ /** Variables whose value is a Postgres connection string. */
65
+ var CONNECTION_VARIABLES = ["DATABASE_URL", "ADMIN_CONNECTION_STRING"];
66
+ /**
67
+ * Scan one file's text for connection strings libpq would reject.
68
+ *
69
+ * Handles both shapes a scaffolded project uses, because both shipped with the
70
+ * defect and a deployed stack is broken by the compose one alone:
71
+ *
72
+ * .env DATABASE_URL=postgresql://…
73
+ * docker-compose.yml DATABASE_URL: postgresql://…
74
+ */
75
+ function scanTextForLibpqUrls(file, text) {
76
+ const findings = [];
77
+ for (const line of text.split("\n")) {
78
+ const trimmed = line.trim();
79
+ if (trimmed.startsWith("#")) continue;
80
+ for (const variable of CONNECTION_VARIABLES) {
81
+ const match = trimmed.match(new RegExp(`^(?:export\\s+)?${variable}\\s*[:=]\\s*(.+)$`));
82
+ if (!match) continue;
83
+ let value = match[1].trim();
84
+ const quoted = value.match(/^(['"])(.*)\1$/);
85
+ if (quoted) value = quoted[2];
86
+ if (!value) continue;
87
+ const params = findUnparseableParams(value);
88
+ if (params.length === 0) continue;
89
+ findings.push({
90
+ file,
91
+ variable,
92
+ params: params.map((p) => p.name),
93
+ suggested: encodeExtraEquals(value)
94
+ });
95
+ }
96
+ }
97
+ return findings;
98
+ }
99
+ //#endregion
100
+ //#region src/doctor-environment.ts
101
+ /**
102
+ * Whether `db push`, `db generate` and `db migrate` can run at all.
103
+ *
104
+ * They shell out to `atlas`, and its absence is invisible until one of them is
105
+ * run: `@ariga/atlas` downloads its platform binary in `preinstall`, pnpm 10+
106
+ * and npm 12+ refuse a dependency's lifecycle scripts unless allowlisted, and
107
+ * the install exits 0 either way. The scaffold allowlists it, so this is a
108
+ * problem for the person who added `@rebasepro/server-postgres` to a project
109
+ * they already had — five lines of `ERR_PNPM_IGNORED_BUILDS` and
110
+ * `Failed to create bin … ENOENT`, several screens up, with nothing saying that
111
+ * a schema push will fail later.
112
+ *
113
+ * Three states, because two of them need opposite advice. The package absent is
114
+ * an install; the package present with no binary is an allowlist; the binary
115
+ * present with no `.bin` shim is a re-install, and telling that reader their
116
+ * build scripts are blocked sends them to a command that does nothing.
117
+ */
118
+ function checkAtlasBinary(state) {
119
+ if (!state || state.onPath) return [];
120
+ if (!state.packageInstalled) return [{
121
+ check: "atlas",
122
+ severity: "warning",
123
+ message: "The atlas binary is not on PATH and @ariga/atlas is not installed — `db push`, `db generate` and `db migrate` will fail.",
124
+ fix: `Install it: ${state.manager} add -D @ariga/atlas, and allow its install script.`
125
+ }];
126
+ if (!state.binaryOnDisk) return [{
127
+ check: "atlas",
128
+ severity: "warning",
129
+ message: "@ariga/atlas is installed and its binary is missing — its `preinstall` script was blocked, so `db push` and `db migrate` will fail.",
130
+ fix: "Allow the build script (pnpm: `allowBuilds` in pnpm-workspace.yaml or `pnpm approve-builds`; npm: `allowScripts` in package.json), then re-install."
131
+ }];
132
+ return [{
133
+ check: "atlas",
134
+ severity: "warning",
135
+ message: "The atlas binary is on disk but node_modules/.bin/atlas is missing, so nothing can invoke it.",
136
+ fix: "Re-resolve the tree so the link is recreated: `pnpm install --force`, or delete node_modules and install again."
137
+ }];
138
+ }
139
+ /**
140
+ * `20.11.0` → `[20, 11, 0]`, ignoring anything after the patch.
141
+ *
142
+ * A bare major is a version: `>=20` is how every `engines.node` in this repo is
143
+ * written, and requiring a minor here made the whole check return nothing —
144
+ * silently, which is the only way a check like this ever fails.
145
+ */
146
+ function parseVersion(raw) {
147
+ const match = raw.trim().replace(/^v/, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
148
+ if (!match) return null;
149
+ return [
150
+ Number(match[1]),
151
+ Number(match[2] ?? 0),
152
+ Number(match[3] ?? 0)
153
+ ];
154
+ }
155
+ /** The lowest version a `>=x.y.z` range admits, or null for anything else. */
156
+ function minimumFromRange(range) {
157
+ const match = range.match(/>=\s*v?(\d+(?:\.\d+)?(?:\.\d+)?)/);
158
+ return match ? parseVersion(match[1]) : null;
159
+ }
160
+ function isBelow(actual, minimum) {
161
+ for (let i = 0; i < 3; i++) {
162
+ if (actual[i] < minimum[i]) return true;
163
+ if (actual[i] > minimum[i]) return false;
164
+ }
165
+ return false;
166
+ }
167
+ /**
168
+ * Is the running Node old enough to break things that will not say so?
169
+ *
170
+ * The symptoms are not "unsupported Node". They are a syntax error inside a
171
+ * dependency, a missing global, or — the one that costs the most time — a
172
+ * `node:` builtin that resolves and then behaves differently. Naming the
173
+ * version is the whole check.
174
+ */
175
+ function checkNodeVersion(running, required) {
176
+ if (!required) return [];
177
+ const minimum = minimumFromRange(required);
178
+ const actual = parseVersion(running);
179
+ if (!minimum || !actual) return [];
180
+ if (!isBelow(actual, minimum)) return [];
181
+ return [{
182
+ check: "node",
183
+ severity: "error",
184
+ message: `Node ${running} is running, and Rebase needs ${required}.`,
185
+ fix: `Install Node ${minimum[0]} or newer (nvm install ${minimum[0]}), then reinstall dependencies.`
186
+ }];
187
+ }
188
+ /** The lockfiles a project may have, and the manager each one belongs to. */
189
+ var LOCKFILES = {
190
+ "pnpm-lock.yaml": "pnpm",
191
+ "package-lock.json": "npm",
192
+ "yarn.lock": "yarn",
193
+ "bun.lockb": "bun"
194
+ };
195
+ /**
196
+ * Two package managers in one project.
197
+ *
198
+ * A Rebase project is a pnpm workspace, and running `npm install` inside one
199
+ * rewrites `node_modules` into a hoisted layout that pnpm then disagrees with.
200
+ * The result is not an install error: it is `Cannot find module` from inside a
201
+ * dependency, hours later, on a machine where it worked yesterday. The
202
+ * leftover `package-lock.json` is the only durable evidence, and nothing looked
203
+ * at it.
204
+ */
205
+ function checkPackageManager(lockfilesPresent, declared) {
206
+ const known = lockfilesPresent.filter((file) => file in LOCKFILES);
207
+ if (known.length < 2) {
208
+ const declaredName = declared?.split("@")[0];
209
+ if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) return [{
210
+ check: "package-manager",
211
+ severity: "warning",
212
+ message: `This project declares packageManager "${declared}" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,
213
+ fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`
214
+ }];
215
+ return [];
216
+ }
217
+ const managers = known.map((file) => LOCKFILES[file]);
218
+ return [{
219
+ check: "package-manager",
220
+ severity: "error",
221
+ message: `Two package managers have installed here: ${known.join(" and ")}.`,
222
+ fix: `Keep one. Delete the others and node_modules, then reinstall with ${declared?.split("@")[0] ?? managers[0]}.`
223
+ }];
224
+ }
225
+ /**
226
+ * Two collections claiming one slug.
227
+ *
228
+ * The slug is the route, the table lookup and the registry key, and the
229
+ * registry keeps the last one registered. So the loser is not reported missing:
230
+ * it is served as the winner, with the winner's properties, under its own name.
231
+ * Every symptom of this points somewhere else.
232
+ */
233
+ function checkDuplicateSlugs(collections) {
234
+ const byslug = /* @__PURE__ */ new Map();
235
+ for (const collection of collections) {
236
+ if (!collection.slug) continue;
237
+ byslug.set(collection.slug, (byslug.get(collection.slug) ?? 0) + 1);
238
+ }
239
+ return [...byslug].filter(([, count]) => count > 1).map(([slug, count]) => ({
240
+ check: "collections",
241
+ severity: "error",
242
+ message: `${count} collections declare the slug "${slug}". Only the last one registered is served.`,
243
+ fix: `Give each collection its own slug. The one that loses is not reported missing — it answers as the winner.`
244
+ }));
245
+ }
246
+ /**
247
+ * The two `.env` facts that fail late and confusingly.
248
+ *
249
+ * A short `JWT_SECRET` boots fine and is refused by `loadEnv` only in
250
+ * production, which is the worst place to find out. Missing CORS in production
251
+ * is the opposite shape — it does not fail at all; the API simply accepts
252
+ * requests from anywhere.
253
+ *
254
+ * Values are never echoed. This reads a file that holds every credential the
255
+ * project has, and doctor's output goes wherever a terminal goes.
256
+ */
257
+ function checkEnvSanity(values) {
258
+ const findings = [];
259
+ const production = values.NODE_ENV === "production";
260
+ const secret = values.JWT_SECRET;
261
+ if (secret !== void 0 && secret.length > 0 && secret.length < 32) findings.push({
262
+ check: "env",
263
+ severity: production ? "error" : "warning",
264
+ message: `JWT_SECRET is ${secret.length} characters. The runtime requires at least 32, and refuses to boot below it in production.`,
265
+ fix: "Generate one: openssl rand -hex 32"
266
+ });
267
+ if (production && !values.CORS_ORIGINS && !values.FRONTEND_URL) findings.push({
268
+ check: "env",
269
+ severity: "error",
270
+ message: "NODE_ENV=production with neither CORS_ORIGINS nor FRONTEND_URL set — the API will accept requests from any origin.",
271
+ fix: "Set CORS_ORIGINS to the origins that may call this API."
272
+ });
273
+ return findings;
274
+ }
275
+ /**
276
+ * Parse a `.env` into a plain object.
277
+ *
278
+ * Deliberately not `dotenv`: this must not touch `process.env`, and doctor may
279
+ * be reading a file for a different environment than the one it is running in.
280
+ */
281
+ function parseEnvFile(text) {
282
+ const values = {};
283
+ for (const line of text.split("\n")) {
284
+ const trimmed = line.trim();
285
+ if (!trimmed || trimmed.startsWith("#")) continue;
286
+ const eq = trimmed.indexOf("=");
287
+ if (eq <= 0) continue;
288
+ const key = trimmed.slice(0, eq).replace(/^export\s+/, "").trim();
289
+ let value = trimmed.slice(eq + 1).trim();
290
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
291
+ values[key] = value;
292
+ }
293
+ return values;
294
+ }
295
+ /**
296
+ * The same `@rebasepro/*` package pinned to different versions in one project.
297
+ *
298
+ * A Rebase project declares them in several package.json files — the root, the
299
+ * backend, the frontend, the config workspace — and nothing has ever compared
300
+ * them. The failure that produces is not an install error: pnpm installs both,
301
+ * `instanceof` stops working across the two copies, and the symptom is a
302
+ * `CollectionConfig` that fails a type guard written against an identical type.
303
+ *
304
+ * `workspace:` ranges are excluded: inside this repo's own workspaces they all
305
+ * resolve to the same build by construction.
306
+ */
307
+ function checkVersionSkew(declared) {
308
+ const byPackage = /* @__PURE__ */ new Map();
309
+ for (const dependency of declared) {
310
+ if (!dependency.name.startsWith("@rebasepro/")) continue;
311
+ if (dependency.range.startsWith("workspace:") || dependency.range.startsWith("link:")) continue;
312
+ if (dependency.range === "*" || dependency.range === "latest") continue;
313
+ const ranges = byPackage.get(dependency.name) ?? /* @__PURE__ */ new Map();
314
+ ranges.set(dependency.range, [...ranges.get(dependency.range) ?? [], dependency.file]);
315
+ byPackage.set(dependency.name, ranges);
316
+ }
317
+ const findings = [];
318
+ for (const [name, ranges] of byPackage) {
319
+ if (ranges.size < 2) continue;
320
+ const described = [...ranges].map(([range, files]) => `${range} (${files.join(", ")})`).join(", ");
321
+ findings.push({
322
+ check: "versions",
323
+ severity: "error",
324
+ message: `${name} is pinned to different versions in this project: ${described}.`,
325
+ fix: "Run `rebase upgrade` to pin every @rebasepro package to one release and reinstall. Two copies of a Rebase package break `instanceof` between them, which fails as a type guard rejecting its own type."
326
+ });
327
+ }
328
+ return findings;
329
+ }
330
+ //#endregion
331
+ //#region src/commands/doctor.ts
332
+ /**
333
+ * CLI command: rebase doctor
334
+ *
335
+ * Detects three-way schema drift between collection definitions,
336
+ * the generated Drizzle schema, and the live PostgreSQL database.
337
+ */
338
+ /**
339
+ * Files that can hold a connection string the project actually runs on.
340
+ *
341
+ * `.env` is what local commands read; the compose files are what a self-hosted
342
+ * stack and its scheduled backup cron read. Both shipped with the defect, and a
343
+ * deployed stack is broken by the compose one even when `.env` has been fixed.
344
+ * `.env.example` is deliberately absent: nothing runs on it.
345
+ */
346
+ var CONNECTION_FILES = [
347
+ ".env",
348
+ ".env.local",
349
+ "docker-compose.yml",
350
+ "docker-compose.yaml",
351
+ "docker-compose.custom.yml"
352
+ ];
353
+ /**
354
+ * Find connection strings libpq cannot parse, anywhere in the project.
355
+ *
356
+ * Exported for the tests; `envFile` is passed separately because a project may
357
+ * keep its `.env` outside the root (see `findEnvFile`).
358
+ */
359
+ function findLibpqUrlProblems(projectRoot, envFile) {
360
+ const candidates = CONNECTION_FILES.map((f) => path.join(projectRoot, f));
361
+ if (envFile) candidates.push(envFile);
362
+ const findings = [];
363
+ const seen = /* @__PURE__ */ new Set();
364
+ for (const file of candidates) {
365
+ if (seen.has(file)) continue;
366
+ seen.add(file);
367
+ let text;
368
+ try {
369
+ if (!fs.existsSync(file)) continue;
370
+ text = fs.readFileSync(file, "utf-8");
371
+ } catch {
372
+ continue;
373
+ }
374
+ findings.push(...scanTextForLibpqUrls(path.relative(projectRoot, file) || path.basename(file), text));
375
+ }
376
+ return findings;
377
+ }
378
+ /**
379
+ * Report unparseable connection strings, if any. Returns true when there were,
380
+ * so the finding reaches the exit code like the environment errors do.
381
+ *
382
+ * Runs before the plugin's drift check and never blocks it. The plugin connects
383
+ * through node-postgres, which parses these URLs happily — so it cannot see this
384
+ * defect, and a project with it will otherwise get a clean bill of health while
385
+ * `rebase db backup` fails.
386
+ */
387
+ function reportLibpqUrlProblems(findings) {
388
+ if (findings.length === 0) return false;
389
+ console.log("");
390
+ console.log(chalk.red.bold(" ✗ Connection string that PostgreSQL's own tools cannot parse"));
391
+ console.log("");
392
+ for (const f of findings) {
393
+ console.log(` ${chalk.bold(f.file)} → ${chalk.bold(f.variable)}`);
394
+ console.log(chalk.gray(` the "${f.params.join("\", \"")}" parameter contains an unencoded "=".`));
395
+ console.log(chalk.gray(" Replace the value with:"));
396
+ console.log(` ${chalk.cyan(f.suggested)}`);
397
+ console.log("");
398
+ }
399
+ console.log(chalk.gray(" libpq splits a query parameter on the first \"=\" and rejects any further"));
400
+ console.log(chalk.gray(" one, so this fails:"));
401
+ console.log(chalk.gray(" extra key/value separator \"=\" in URI query parameter"));
402
+ console.log("");
403
+ console.log(chalk.gray(" Affects rebase db backup / restore, scheduled backups, and psql."));
404
+ console.log(chalk.gray(" NOT rebase dev or db push — those use a driver that accepts it, which"));
405
+ console.log(chalk.gray(" is why a project can look healthy and still have no working backups."));
406
+ console.log(chalk.gray(" Projects scaffolded before 2026-08-18 all carry it."));
407
+ console.log("");
408
+ return true;
409
+ }
410
+ /**
411
+ * `--help` is answered before the project guard, not after.
412
+ *
413
+ * `doctor` declared no `--help` at all, so the flag fell through to the command
414
+ * body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
415
+ * project answered "✗ Could not find a Rebase project root." Asking a command
416
+ * what it does is the one question that cannot require being somewhere
417
+ * particular to ask.
418
+ */
419
+ /**
420
+ * What each custom function needs from its host.
421
+ *
422
+ * Prints nothing when there is nothing to say — which is the common case, and
423
+ * the reason it can afford to run every time. When it does print, the ordering
424
+ * is deliberate: the one finding that is a bug *today* comes first, and the
425
+ * rest is a single descriptive line about where these functions could run.
426
+ */
427
+ function reportFunctionPortability(projectRoot) {
428
+ let results;
429
+ try {
430
+ const backend = findBackendApp(loadManifest(projectRoot).manifest);
431
+ if (!backend) return;
432
+ const paths = resolveBackendPaths(backend.app, projectRoot);
433
+ results = analyseFunctionsDirectory(path.join(projectRoot, paths.functions), projectRoot);
434
+ } catch {
435
+ return;
436
+ }
437
+ const lines = summarisePortability(results);
438
+ if (lines.length === 0) return;
439
+ console.log(chalk.bold("\nCustom functions"));
440
+ for (const line of lines) console.log(line.trimStart().startsWith("⚠") ? chalk.yellow(line) : chalk.gray(line));
441
+ console.log(chalk.gray(" See https://rebase.pro/docs/backend/custom-functions#runtime-portability"));
442
+ }
443
+ /**
444
+ * Read everything the environment checks need, then run them.
445
+ *
446
+ * The reading is here and the deciding is in `doctor-environment.ts`, so every
447
+ * check has a fixture that fails it without a project on disk.
448
+ */
449
+ function collectEnvironmentFindings(projectRoot, envFile) {
450
+ const findings = [];
451
+ findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));
452
+ const rootPackage = readJson(path.join(projectRoot, "package.json"));
453
+ findings.push(...checkPackageManager(Object.keys(LOCKFILES).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
454
+ findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));
455
+ if (envFile && fs.existsSync(envFile)) try {
456
+ findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, "utf-8"))));
457
+ } catch {}
458
+ findings.push(...checkVersionSkew(readDeclaredRebaseDeps(projectRoot)));
459
+ findings.push(...checkAtlasBinary(readAtlasBinaryState(projectRoot)));
460
+ return findings;
461
+ }
462
+ /**
463
+ * What is actually on disk for `@ariga/atlas`, from the project's point of view.
464
+ *
465
+ * Three separate questions, because the three states they distinguish need
466
+ * three different remedies — see `checkAtlasBinary`. Directories are searched
467
+ * from the project root upwards and through the workspaces a scaffold has,
468
+ * because `db push` runs from `backend/` and the install may be hoisted to the
469
+ * root.
470
+ */
471
+ function readAtlasBinaryState(projectRoot) {
472
+ const roots = [
473
+ projectRoot,
474
+ path.join(projectRoot, "backend"),
475
+ path.join(projectRoot, "config")
476
+ ];
477
+ const findUpwards = (starts, relative) => {
478
+ for (const start of starts) {
479
+ let dir = start;
480
+ for (let i = 0; i < 8; i++) {
481
+ const candidate = path.join(dir, "node_modules", relative);
482
+ if (fs.existsSync(candidate)) return candidate;
483
+ const parent = path.dirname(dir);
484
+ if (parent === dir) break;
485
+ dir = parent;
486
+ }
487
+ }
488
+ return null;
489
+ };
490
+ const resolveFrom = (from, request) => {
491
+ try {
492
+ return createRequire(pathToFileURL(from)).resolve(request);
493
+ } catch {
494
+ return null;
495
+ }
496
+ };
497
+ const bases = roots.map((dir) => path.join(dir, "package.json")).filter((file) => fs.existsSync(file));
498
+ const driverManifests = bases.map((base) => resolveFrom(base, "@rebasepro/server-postgres/package.json")).filter((file) => file !== null);
499
+ let manifest = findUpwards(roots, path.join("@ariga", "atlas", "package.json"));
500
+ if (!manifest) for (const base of [...bases, ...driverManifests]) {
501
+ manifest = resolveFrom(base, "@ariga/atlas/package.json");
502
+ if (manifest) break;
503
+ }
504
+ const onPath = findUpwards([...roots, ...driverManifests.map((file) => path.dirname(file))], path.join(".bin", "atlas")) !== null;
505
+ let binaryOnDisk = false;
506
+ if (manifest) try {
507
+ const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
508
+ const dir = path.dirname(manifest);
509
+ binaryOnDisk = (typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin ?? {}).filter((t) => typeof t === "string")).some((target) => fs.existsSync(path.resolve(dir, target)));
510
+ } catch {}
511
+ return {
512
+ onPath,
513
+ packageInstalled: manifest !== null,
514
+ binaryOnDisk,
515
+ manager: readPackageManagerName(projectRoot)
516
+ };
517
+ }
518
+ /** The reader's own package manager, from the lockfile beside their project. */
519
+ function readPackageManagerName(projectRoot) {
520
+ for (const [file, manager] of Object.entries(LOCKFILES)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
521
+ return "pnpm";
522
+ }
523
+ /** The `engines.node` range of the CLI actually running. */
524
+ function readCliEngines() {
525
+ try {
526
+ const here = path.dirname(fileURLToPath(import.meta.url));
527
+ for (let dir = here, i = 0; i < 5; i++) {
528
+ const candidate = path.join(dir, "package.json");
529
+ if (fs.existsSync(candidate)) {
530
+ const parsed = readJson(candidate);
531
+ if (parsed?.name === "@rebasepro/cli") return parsed.engines?.node;
532
+ }
533
+ const parent = path.dirname(dir);
534
+ if (parent === dir) break;
535
+ dir = parent;
536
+ }
537
+ } catch {}
538
+ }
539
+ function readJson(file) {
540
+ try {
541
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
542
+ } catch {
543
+ return;
544
+ }
545
+ }
546
+ /** Where a project's package.json files live, relative to its root. */
547
+ var PACKAGE_JSON_LOCATIONS = [
548
+ "package.json",
549
+ "backend/package.json",
550
+ "frontend/package.json",
551
+ "config/package.json"
552
+ ];
553
+ function readDeclaredRebaseDeps(projectRoot) {
554
+ const declared = [];
555
+ for (const relative of PACKAGE_JSON_LOCATIONS) {
556
+ const parsed = readJson(path.join(projectRoot, relative));
557
+ if (!parsed) continue;
558
+ for (const field of [
559
+ "dependencies",
560
+ "devDependencies",
561
+ "peerDependencies"
562
+ ]) {
563
+ const block = parsed[field];
564
+ if (!block) continue;
565
+ for (const [name, range] of Object.entries(block)) if (typeof range === "string") declared.push({
566
+ file: relative,
567
+ name,
568
+ range
569
+ });
570
+ }
571
+ }
572
+ return declared;
573
+ }
574
+ /**
575
+ * Does this project declare its collections in code?
576
+ *
577
+ * `resolveBackendPaths` answers it against the manifest's own `config` path
578
+ * rather than the convention, because a project may move the directory. A
579
+ * project with no manifest, or one shaped differently, is treated as declaring
580
+ * them: the wrong answer there would silently skip the drift report, which is
581
+ * the reason the command exists.
582
+ */
583
+ function hasDeclaredCollections(projectRoot) {
584
+ try {
585
+ const backend = findBackendApp(loadManifest(projectRoot).manifest);
586
+ if (!backend) return true;
587
+ return resolveBackendPaths(backend.app, projectRoot).hasCollections;
588
+ } catch {
589
+ return true;
590
+ }
591
+ }
592
+ /**
593
+ * Every `slug:` a collection file declares, read as text.
594
+ *
595
+ * Deliberately not by importing them: a collection file may import anything the
596
+ * project depends on, and doctor must not run a project's code to tell it two
597
+ * slugs collide. A regex over `slug: "..."` finds the literal in every shape
598
+ * the scaffold and the docs use; a computed slug is simply not seen, which is
599
+ * the right failure for a check that only ever reports.
600
+ */
601
+ function readDeclaredSlugs(projectRoot) {
602
+ const dir = path.join(projectRoot, "config", "collections");
603
+ if (!fs.existsSync(dir)) return [];
604
+ const slugs = [];
605
+ let entries;
606
+ try {
607
+ entries = fs.readdirSync(dir);
608
+ } catch {
609
+ return [];
610
+ }
611
+ for (const entry of entries) {
612
+ if (!/\.(ts|js|mts|mjs)$/.test(entry) || entry.startsWith("index.")) continue;
613
+ try {
614
+ const text = fs.readFileSync(path.join(dir, entry), "utf-8");
615
+ for (const match of text.matchAll(/\bslug:\s*["'`]([^"'`]+)["'`]/g)) slugs.push({ slug: match[1] });
616
+ } catch {}
617
+ }
618
+ return slugs;
619
+ }
620
+ /** Print what the environment checks found, grouped, or nothing at all. */
621
+ function reportEnvironmentFindings(findings) {
622
+ if (findings.length === 0) return false;
623
+ console.log("");
624
+ console.log(chalk.bold(" Environment"));
625
+ for (const finding of findings) {
626
+ const mark = finding.severity === "error" ? chalk.red(" ✗") : chalk.yellow(" ⚠");
627
+ console.log(`${mark} ${finding.message}`);
628
+ console.log(chalk.gray(` ${finding.fix}`));
629
+ }
630
+ console.log("");
631
+ return findings.some((finding) => finding.severity === "error");
632
+ }
633
+ /**
634
+ * The flags `rebase doctor` takes, and what each is for.
635
+ *
636
+ * One table, read by both the parser and the help page, because they were two
637
+ * things that had to agree and did not: the page listed no flags at all, so
638
+ * `--policies` — the form `cli/index.md` calls "the form to use as a CI gate" —
639
+ * was invisible to anyone who ran `rebase doctor --help` to find it. And with
640
+ * nothing parsing the line here, a typo travelled to the driver's permissive
641
+ * parser and was silently dropped, so `rebase doctor --polices` ran the ordinary
642
+ * doctor and reported success on a gate that never ran.
643
+ *
644
+ * The spec mirrors the driver's own (`doctorPluginCommand` in
645
+ * `server-postgres/src/cli.ts`), because the line is relayed to it verbatim.
646
+ */
647
+ var DOCTOR_FLAGS = {
648
+ "--policies": Boolean,
649
+ "--collections": String,
650
+ "-c": "--collections",
651
+ "--schema": String,
652
+ "-s": "--schema",
653
+ "--sdk": String,
654
+ "-k": "--sdk"
655
+ };
656
+ /** What each long flag above does, for the Options block. */
657
+ var DOCTOR_FLAG_HELP = {
658
+ "--policies": "Also audit the row-level security policies. The form to use as a CI gate.",
659
+ "--collections": COLLECTIONS_FLAG_HELP,
660
+ "--schema": "Path to the generated Drizzle schema (default: backend/src/schema.generated.ts)",
661
+ "--sdk": "Path to the generated SDK types (default: generated/sdk/database.types.ts)"
662
+ };
663
+ /**
664
+ * The line handed to the driver, every path on it resolved from where the
665
+ * command was run — the driver runs in `backend/`. See `utils/path-flags.ts`.
666
+ */
667
+ function doctorDriverArgs(rawArgs, cwd) {
668
+ return absolutizeLocalPathArgs(argsFromCommand(rawArgs, "doctor"), cwd, [
669
+ "--collections",
670
+ "-c",
671
+ "--schema",
672
+ "-s",
673
+ "--sdk",
674
+ "-k"
675
+ ]);
676
+ }
677
+ /** `--collections, -c` — the aliases, resolved from the spec rather than retyped. */
678
+ function doctorFlagLabel(flag) {
679
+ return [flag, ...Object.entries(DOCTOR_FLAGS).filter(([, target]) => target === flag).map(([alias]) => alias)].join(", ");
680
+ }
681
+ function printDoctorHelp() {
682
+ console.log(`
683
+ ${chalk.bold("rebase doctor")} — Check a project for drift and misconfiguration
684
+
685
+ ${chalk.green.bold("Usage")}
686
+ rebase doctor [options]
687
+
688
+ ${chalk.green.bold("Options")}
689
+ ${Object.keys(DOCTOR_FLAG_HELP).map((flag) => ` ${chalk.blue(doctorFlagLabel(flag).padEnd(22))} ${DOCTOR_FLAG_HELP[flag]}`).join("\n")}
690
+
691
+ Compares the collections you declare, the generated Drizzle schema, and the
692
+ tables that actually exist, then reports what disagrees and how to reconcile it.
693
+
694
+ Also checks the connection strings in .env and the compose files for the
695
+ unencoded "=" that makes PostgreSQL's own tools refuse to parse them — which
696
+ breaks backups and psql while leaving the app itself working.
697
+
698
+ Run from inside a Rebase project — it reads the project's collections and
699
+ connects to its database.
700
+ `);
701
+ }
702
+ async function doctorCommand(rawArgs) {
703
+ if (wantsHelp(rawArgs)) {
704
+ printDoctorHelp();
705
+ return;
706
+ }
707
+ parseCommandArgs({
708
+ spec: DOCTOR_FLAGS,
709
+ rawArgs,
710
+ commandWords: 1,
711
+ command: "doctor",
712
+ maxPositionals: 0
713
+ });
714
+ const projectRoot = requireProjectRoot();
715
+ const backendDir = requireBackendDir(projectRoot);
716
+ refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir, "doctor");
717
+ const activePlugin = getActiveBackendPlugin(backendDir);
718
+ if (!activePlugin) {
719
+ console.error(chalk.red("✗ Could not detect an active database plugin."));
720
+ console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
721
+ process.exit(1);
722
+ }
723
+ const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
724
+ if (!pluginCli) exitDependenciesNotInstalled(projectRoot);
725
+ const envFile = findEnvFile(projectRoot);
726
+ const env = { ...process.env };
727
+ if (envFile) env.DOTENV_CONFIG_PATH = envFile;
728
+ env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
729
+ const connectionFailed = reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));
730
+ reportFunctionPortability(projectRoot);
731
+ const environmentFailed = reportEnvironmentFindings(collectEnvironmentFindings(projectRoot, envFile));
732
+ const failed = connectionFailed || environmentFailed;
733
+ try {
734
+ const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
735
+ Object.assign(env, prepared.env);
736
+ for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));
737
+ } catch (error) {
738
+ console.log("");
739
+ console.error(chalk.yellow(` ⚠ Could not resolve this project's database: ${error instanceof Error ? error.message : String(error)}`));
740
+ console.error(chalk.gray(" The checks that need one are reported below as not run."));
741
+ }
742
+ if (!hasDeclaredCollections(projectRoot)) {
743
+ console.log("");
744
+ console.log(chalk.gray(" ○ Schema drift not checked: this project derives its API from the database — run `rebase schema introspect` first."));
745
+ if (failed) process.exit(1);
746
+ return;
747
+ }
748
+ try {
749
+ if (pluginCli.endsWith(".ts")) {
750
+ const tsxBin = resolveTsx(projectRoot);
751
+ if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
752
+ await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
753
+ cwd: backendDir,
754
+ stdio: "inherit",
755
+ env
756
+ });
757
+ } else await execa("node", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
758
+ cwd: backendDir,
759
+ stdio: "inherit",
760
+ env
761
+ });
762
+ } catch (error) {
763
+ reportSpawnFailure(error);
764
+ process.exit(1);
765
+ }
766
+ if (failed) process.exit(1);
767
+ }
768
+ //#endregion
769
+ export { doctorCommand };
770
+
771
+ //# sourceMappingURL=doctor-BP8cZ-7F.js.map