@rebasepro/cli 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +2 -1
  2. package/bin/rebase.js +143 -4
  3. package/dist/{agent-setup-B5AKHOgt.js → agent-setup-D9mHRZ5R.js} +125 -3
  4. package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
  5. package/dist/{api-keys-Tm7zvAPe.js → api-keys-CyJ26KEJ.js} +41 -10
  6. package/dist/api-keys-CyJ26KEJ.js.map +1 -0
  7. package/dist/{apps-BsU_ILbP.js → apps-CnLDV5b6.js} +6 -7
  8. package/dist/{apps-BsU_ILbP.js.map → apps-CnLDV5b6.js.map} +1 -1
  9. package/dist/{atlas-dev-database-OWtqnG7W.js → atlas-dev-database-BODqdCWt.js} +4 -4
  10. package/dist/{atlas-dev-database-OWtqnG7W.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
  11. package/dist/{auth-Vl2hFKXP.js → auth-dII2jCO1.js} +10 -8
  12. package/dist/auth-dII2jCO1.js.map +1 -0
  13. package/dist/{branch-pointer-9DnwkFJJ.js → branch-pointer-BQUF6lYa.js} +1 -1
  14. package/dist/{build-BBgT8EJZ.js → build-Cza7ZQrp.js} +6 -7
  15. package/dist/{build-BBgT8EJZ.js.map → build-Cza7ZQrp.js.map} +1 -1
  16. package/dist/{bundle-DtCCCr4w.js → bundle-C6yEtMJ-.js} +53 -5
  17. package/dist/bundle-C6yEtMJ-.js.map +1 -0
  18. package/dist/bundle.d.ts +14 -0
  19. package/dist/{cloud-BpH0_bmV.js → cloud-BdnmqQaZ.js} +8528 -4342
  20. package/dist/cloud-BdnmqQaZ.js.map +1 -0
  21. package/dist/collections-dir-DlvDhime.js +69 -0
  22. package/dist/collections-dir-DlvDhime.js.map +1 -0
  23. package/dist/commands/agent-setup.d.ts +58 -0
  24. package/dist/commands/api-keys.d.ts +13 -0
  25. package/dist/commands/cloud/activity.d.ts +25 -0
  26. package/dist/commands/cloud/app-key.d.ts +18 -0
  27. package/dist/commands/cloud/auth.d.ts +1 -0
  28. package/dist/commands/cloud/billing-invoices.d.ts +39 -0
  29. package/dist/commands/cloud/claim.d.ts +7 -0
  30. package/dist/commands/cloud/context.d.ts +115 -6
  31. package/dist/commands/cloud/deploy-summary.d.ts +162 -0
  32. package/dist/commands/cloud/deploy.d.ts +64 -0
  33. package/dist/commands/cloud/env.d.ts +24 -0
  34. package/dist/commands/cloud/index.d.ts +13 -1
  35. package/dist/commands/cloud/init.d.ts +143 -0
  36. package/dist/commands/cloud/login-flow.d.ts +51 -0
  37. package/dist/commands/cloud/notifications.d.ts +28 -0
  38. package/dist/commands/cloud/orgs.d.ts +13 -0
  39. package/dist/commands/cloud/previews.d.ts +123 -0
  40. package/dist/commands/cloud/resources.d.ts +68 -14
  41. package/dist/commands/cloud/runtime-logs.d.ts +176 -0
  42. package/dist/commands/cloud/service-key.d.ts +15 -0
  43. package/dist/commands/cloud/token-capabilities.d.ts +61 -8
  44. package/dist/commands/cloud/tokens.d.ts +18 -3
  45. package/dist/commands/cloud/usage.d.ts +63 -0
  46. package/dist/commands/dev.d.ts +22 -0
  47. package/dist/commands/doctor.d.ts +1 -1
  48. package/dist/commands/generate-view.d.ts +94 -0
  49. package/dist/commands/generate_sdk.d.ts +19 -2
  50. package/dist/commands/init.d.ts +20 -0
  51. package/dist/commands/skills.d.ts +4 -3
  52. package/dist/commands/start.d.ts +10 -0
  53. package/dist/commands/storage.d.ts +4 -0
  54. package/dist/commands/upgrade.d.ts +12 -0
  55. package/dist/commands/verify.d.ts +96 -0
  56. package/dist/{consent-ClRHmcxf.js → consent-BD7F29T3.js} +2 -2
  57. package/dist/{consent-ClRHmcxf.js.map → consent-BD7F29T3.js.map} +1 -1
  58. package/dist/{console-CEGm0XNc.js → console-BXB6-8GT.js} +12 -6
  59. package/dist/console-BXB6-8GT.js.map +1 -0
  60. package/dist/{context-CyflUrKg.js → context-GUxkZm-S.js} +482 -26
  61. package/dist/context-GUxkZm-S.js.map +1 -0
  62. package/dist/{daemon-CknkDjd6.js → daemon-Cp7oNXth.js} +160 -5
  63. package/dist/daemon-Cp7oNXth.js.map +1 -0
  64. package/dist/{daemon-entry-CbLRrgzy.js → daemon-entry-zOC1revO.js} +6 -3
  65. package/dist/daemon-entry-zOC1revO.js.map +1 -0
  66. package/dist/{db-CRNpA5mH.js → db-KzlqddGD.js} +101 -54
  67. package/dist/db-KzlqddGD.js.map +1 -0
  68. package/dist/{derive-CsoFGAnK.js → derive-ae9UOoOB.js} +2 -2
  69. package/dist/{derive-CsoFGAnK.js.map → derive-ae9UOoOB.js.map} +1 -1
  70. package/dist/{dev-C0pbMmhS.js → dev-DTqVMgB3.js} +231 -44
  71. package/dist/dev-DTqVMgB3.js.map +1 -0
  72. package/dist/dev-db/daemon.d.ts +41 -0
  73. package/dist/dev-db/pull.d.ts +33 -5
  74. package/dist/dev-db/state.d.ts +9 -0
  75. package/dist/{dev-preflight-unHpiYO1.js → dev-preflight-Cxv3TiD1.js} +73 -27
  76. package/dist/dev-preflight-Cxv3TiD1.js.map +1 -0
  77. package/dist/{dev-server-B1UYifZJ.js → dev-server-OJsUGZEB.js} +14 -3
  78. package/dist/dev-server-OJsUGZEB.js.map +1 -0
  79. package/dist/{doctor-CATRdWm6.js → doctor-BRWOlqeG.js} +14 -12
  80. package/dist/doctor-BRWOlqeG.js.map +1 -0
  81. package/dist/{eject-DuasTr-F.js → eject-DABcDnPN.js} +3 -4
  82. package/dist/{eject-DuasTr-F.js.map → eject-DABcDnPN.js.map} +1 -1
  83. package/dist/{fold-static-hDHtDfTq.js → fold-static-BnAK-anQ.js} +3 -3
  84. package/dist/{fold-static-hDHtDfTq.js.map → fold-static-BnAK-anQ.js.map} +1 -1
  85. package/dist/generate-view-IFwCADvb.js +662 -0
  86. package/dist/generate-view-IFwCADvb.js.map +1 -0
  87. package/dist/{generate_sdk-Cvg5jjJF.js → generate_sdk-BvcWvoUh.js} +49 -13
  88. package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
  89. package/dist/index.es.js +58 -40
  90. package/dist/index.es.js.map +1 -1
  91. package/dist/{init-BWAwHPtz.js → init-CaU6GhwI.js} +50 -62
  92. package/dist/init-CaU6GhwI.js.map +1 -0
  93. package/dist/invoked-command-CG_rIa_o.js +55 -0
  94. package/dist/invoked-command-CG_rIa_o.js.map +1 -0
  95. package/dist/last-dev-database-DpW5dfoX.js +2 -0
  96. package/dist/{manifest-DRp0UVTj.js → manifest-BtzDy7KQ.js} +36 -3
  97. package/dist/{manifest-DRp0UVTj.js.map → manifest-BtzDy7KQ.js.map} +1 -1
  98. package/dist/{normalize-imports-BoBVh5tU.js → normalize-imports-D0zXlM6g.js} +3 -3
  99. package/dist/{normalize-imports-BoBVh5tU.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
  100. package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
  101. package/dist/path-flags-DNvtEi2-.js.map +1 -0
  102. package/dist/{prepare-DdnU70z8.js → prepare-CTw9NEkh.js} +5 -4
  103. package/dist/{prepare-DdnU70z8.js.map → prepare-CTw9NEkh.js.map} +1 -1
  104. package/dist/project-CasK56vN.js +701 -0
  105. package/dist/project-CasK56vN.js.map +1 -0
  106. package/dist/{pull-C3PXduzl.js → pull-DDMpsZ5r.js} +131 -24
  107. package/dist/pull-DDMpsZ5r.js.map +1 -0
  108. package/dist/refusal-ReeeGP8a.js +26 -0
  109. package/dist/refusal-ReeeGP8a.js.map +1 -0
  110. package/dist/resolve-D647F1LA.js +2 -0
  111. package/dist/{resources-BdNRDEAy.js → resources-CPSC5Kcu.js} +4 -5
  112. package/dist/{resources-BdNRDEAy.js.map → resources-CPSC5Kcu.js.map} +1 -1
  113. package/dist/{schema-B0-p6T1O.js → schema-BSXwZSiI.js} +9 -8
  114. package/dist/schema-BSXwZSiI.js.map +1 -0
  115. package/dist/{skills-DfnTeyRz.js → skills-BU2flMOy.js} +52 -8
  116. package/dist/skills-BU2flMOy.js.map +1 -0
  117. package/dist/{start-BmEZkS0n.js → start-mVb6upwY.js} +20 -7
  118. package/dist/start-mVb6upwY.js.map +1 -0
  119. package/dist/{state-CCB-FAQ9.js → state-CYdBvnwA.js} +3 -2
  120. package/dist/state-CYdBvnwA.js.map +1 -0
  121. package/dist/static-syntax-check.d.ts +64 -0
  122. package/dist/{status-BJ26yZOo.js → status-BREvtzJm.js} +8 -9
  123. package/dist/{status-BJ26yZOo.js.map → status-BREvtzJm.js.map} +1 -1
  124. package/dist/{storage-DKPUBTWG.js → storage-DtOXGflp.js} +159 -16
  125. package/dist/storage-DtOXGflp.js.map +1 -0
  126. package/dist/{telemetry-EMFRVRQi.js → telemetry-CgwJPVMO.js} +2 -2
  127. package/dist/{telemetry-EMFRVRQi.js.map → telemetry-CgwJPVMO.js.map} +1 -1
  128. package/dist/{telemetry-m9czTNGR.js → telemetry-DwK5RQJl.js} +4 -4
  129. package/dist/{telemetry-m9czTNGR.js.map → telemetry-DwK5RQJl.js.map} +1 -1
  130. package/dist/{upgrade-DjihWuZL.js → upgrade-B7sVwL8S.js} +26 -4
  131. package/dist/{upgrade-DjihWuZL.js.map → upgrade-B7sVwL8S.js.map} +1 -1
  132. package/dist/utils/args.d.ts +32 -3
  133. package/dist/utils/boot-retry.d.ts +41 -0
  134. package/dist/utils/collections-dir.d.ts +14 -0
  135. package/dist/utils/dev-preflight.d.ts +34 -1
  136. package/dist/utils/dev-server.d.ts +11 -0
  137. package/dist/utils/path-flags.d.ts +2 -1
  138. package/dist/utils/project.d.ts +5 -1
  139. package/dist/utils/tool-stdio.d.ts +1 -1
  140. package/dist/utils/unknown-command.d.ts +4 -0
  141. package/dist/verify-D7UsV_qe.js +730 -0
  142. package/dist/verify-D7UsV_qe.js.map +1 -0
  143. package/package.json +9 -7
  144. package/templates/eject/backend/src/index.ts +9 -1
  145. package/templates/generated-schemas/ecommerce.ts +3 -0
  146. package/templates/overlays/baas/ai-instructions.md +13 -0
  147. package/templates/resource-graphs/blank.json +25 -0
  148. package/templates/resource-graphs/blog.json +28 -0
  149. package/templates/resource-graphs/ecommerce.json +28 -0
  150. package/templates/resource-graphs/headless.json +22 -0
  151. package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
  152. package/templates/template/.claude/settings.json +17 -0
  153. package/templates/template/.env.example +15 -5
  154. package/templates/template/ai-instructions.md +21 -1
  155. package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
  156. package/templates/template/docker-compose.yml +4 -1
  157. package/templates/template/frontend/package.json +1 -0
  158. package/templates/template/frontend/src/App.tsx +10 -9
  159. package/templates/views/AppView.tsx.tmpl +138 -0
  160. package/templates/views/EntityView.tsx.tmpl +64 -0
  161. package/templates/views/index.ts.tmpl +8 -0
  162. package/dist/agent-setup-B5AKHOgt.js.map +0 -1
  163. package/dist/api-keys-Tm7zvAPe.js.map +0 -1
  164. package/dist/args-BX0YD14a.js +0 -266
  165. package/dist/args-BX0YD14a.js.map +0 -1
  166. package/dist/auth-Vl2hFKXP.js.map +0 -1
  167. package/dist/bundle-DtCCCr4w.js.map +0 -1
  168. package/dist/cloud-BpH0_bmV.js.map +0 -1
  169. package/dist/console-CEGm0XNc.js.map +0 -1
  170. package/dist/context-CyflUrKg.js.map +0 -1
  171. package/dist/daemon-CknkDjd6.js.map +0 -1
  172. package/dist/daemon-entry-CbLRrgzy.js.map +0 -1
  173. package/dist/db-CRNpA5mH.js.map +0 -1
  174. package/dist/dev-C0pbMmhS.js.map +0 -1
  175. package/dist/dev-preflight-unHpiYO1.js.map +0 -1
  176. package/dist/dev-server-B1UYifZJ.js.map +0 -1
  177. package/dist/doctor-CATRdWm6.js.map +0 -1
  178. package/dist/generate_sdk-Cvg5jjJF.js.map +0 -1
  179. package/dist/init-BWAwHPtz.js.map +0 -1
  180. package/dist/last-dev-database-c_dKz1S3.js +0 -2
  181. package/dist/path-flags-BFgG9V6y.js.map +0 -1
  182. package/dist/project-Wp2zoyeN.js +0 -349
  183. package/dist/project-Wp2zoyeN.js.map +0 -1
  184. package/dist/pull-C3PXduzl.js.map +0 -1
  185. package/dist/resolve-CNQjsatH.js +0 -2
  186. package/dist/schema-B0-p6T1O.js.map +0 -1
  187. package/dist/skills-DfnTeyRz.js.map +0 -1
  188. package/dist/start-BmEZkS0n.js.map +0 -1
  189. package/dist/state-CCB-FAQ9.js.map +0 -1
  190. package/dist/storage-DKPUBTWG.js.map +0 -1
@@ -1,33 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { d as readEnvFile } from "./project-Wp2zoyeN.js";
2
+ import { d as readEnvFile } from "./project-CasK56vN.js";
3
+ import { t as Refusal } from "./refusal-ReeeGP8a.js";
3
4
  import chalk from "chalk";
4
5
  import fs from "fs";
5
6
  import path from "path";
6
7
  import { ExecaError, execa } from "execa";
7
8
  import net from "net";
8
- //#region src/utils/refusal.ts
9
- /**
10
- * A refusal the CLI raised itself, having said why: nothing broke.
11
- *
12
- * `bin/rebase.js` ends every failure with "Re-run with --debug for the stack
13
- * trace." That is right when something broke inside a command, and noise after
14
- * a check that decided not to run — the compose-port refusal prints the
15
- * container holding the port and how to move, and its stack points at the line
16
- * that decided to refuse. A `UsageError` (`utils/args.ts`) is the same case for
17
- * a command line that was wrong; this is the one for everything else.
18
- *
19
- * Marked with a property rather than left to `instanceof`, for `UsageError`'s
20
- * reason: the reader is `bin/rebase.js`, which imports the bundle, so the class
21
- * it would test against is not the one this file compiled to.
22
- */
23
- var Refusal = class extends Error {
24
- isRefusal = true;
25
- constructor(message) {
26
- super(message);
27
- this.name = "Refusal";
28
- }
29
- };
30
- //#endregion
31
9
  //#region src/utils/dev-preflight.ts
32
10
  /**
33
11
  * Make `rebase dev` the only command a new project needs.
@@ -67,6 +45,7 @@ var Refusal = class extends Error {
67
45
  */
68
46
  var dev_preflight_exports = /* @__PURE__ */ __exportAll({
69
47
  classifyPortPublishers: () => classifyPortPublishers,
48
+ closedDatabaseName: () => closedDatabaseName,
70
49
  composeDatabaseUrl: () => composeDatabaseUrl,
71
50
  composeDeclaresDbService: () => composeDeclaresDbService,
72
51
  composeHostPort: () => composeHostPort,
@@ -301,6 +280,31 @@ async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
301
280
  return false;
302
281
  }
303
282
  /**
283
+ * The database a connection was refused by because it is closed to new
284
+ * connections (`ALLOW_CONNECTIONS false`), or null.
285
+ *
286
+ * The driver's `closedDatabaseName`, for the error a bare `pg` connect gives:
287
+ * the sentence is matched, not SQLSTATE 55000, which also covers failures that
288
+ * have nothing to do with connecting.
289
+ */
290
+ function closedDatabaseName(error) {
291
+ if (!error || typeof error !== "object") return null;
292
+ if ("message" in error && typeof error.message === "string") {
293
+ const named = /database "(.+)" is not currently accepting connections/.exec(error.message);
294
+ if (named) return named[1];
295
+ }
296
+ return "cause" in error ? closedDatabaseName(error.cause) : null;
297
+ }
298
+ /** Open `databaseUrl`'s database again with the driver, as every `rebase db branch` command does first. */
299
+ async function reopenWithDriver(databaseUrl) {
300
+ const { reopenClosedDatabases, reopenedDatabasesNotice } = await import("@rebasepro/server-postgres");
301
+ const result = await reopenClosedDatabases(databaseUrl);
302
+ return {
303
+ reopened: result.reopened.length > 0,
304
+ notice: reopenedDatabasesNotice(result)
305
+ };
306
+ }
307
+ /**
304
308
  * Poll until `SELECT 1` succeeds on this connection string, or the bound runs out.
305
309
  *
306
310
  * The readiness that matters to everything after it. A fresh compose volume
@@ -310,27 +314,56 @@ async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
310
314
  * three. Every error is retried: a reset, "the database system is starting up"
311
315
  * and a refused connection all mean "not yet", and the bound is what turns
312
316
  * "not yet" into an answer.
317
+ *
318
+ * Except one, which is not "not yet": a database closed to new connections
319
+ * (`closedDatabaseName`), which is what a `rebase db branch create --force`
320
+ * killed mid-copy leaves its source as. Waiting cannot open it, and it was
321
+ * waited on for the whole minute. It is opened again with the driver's
322
+ * `reopenClosedDatabases`, once, and then either answers or the wait ends at
323
+ * once, naming it.
313
324
  */
314
325
  async function waitForQueryable(databaseUrl, options = {}) {
315
326
  const { timeoutMs = 6e4, intervalMs = 500 } = options;
316
327
  const { Client } = await import("pg");
317
328
  const deadline = Date.now() + timeoutMs;
318
329
  let lastError = "no attempt was made";
330
+ let healed = false;
319
331
  do {
320
332
  const client = new Client({
321
333
  connectionString: databaseUrl,
322
334
  connectionTimeoutMillis: Math.max(250, Math.min(2e3, deadline - Date.now()))
323
335
  });
324
336
  client.on("error", () => void 0);
337
+ let closed = null;
325
338
  try {
326
339
  await client.connect();
327
340
  await client.query("SELECT 1");
328
341
  return { ready: true };
329
342
  } catch (error) {
330
343
  lastError = error instanceof Error && error.message ? error.message : String(error);
344
+ closed = closedDatabaseName(error);
331
345
  } finally {
332
346
  await client.end().catch(() => void 0);
333
347
  }
348
+ if (closed !== null) {
349
+ if (healed) return {
350
+ ready: false,
351
+ error: lastError,
352
+ closedDatabase: closed
353
+ };
354
+ healed = true;
355
+ const heal = await (options.reopenClosed ?? reopenWithDriver)(databaseUrl).catch(() => ({
356
+ reopened: false,
357
+ notice: []
358
+ }));
359
+ for (const line of heal.notice) options.onNotice?.(line);
360
+ if (!heal.reopened) return {
361
+ ready: false,
362
+ error: lastError,
363
+ closedDatabase: closed
364
+ };
365
+ continue;
366
+ }
334
367
  if (Date.now() + intervalMs >= deadline) break;
335
368
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
336
369
  } while (Date.now() < deadline);
@@ -642,7 +675,20 @@ async function ensureDevDatabase(options) {
642
675
  hint
643
676
  };
644
677
  }
645
- const readiness = await waitForQueryable(databaseUrl, { timeoutMs: options.readyTimeoutMs });
678
+ const readiness = await waitForQueryable(databaseUrl, {
679
+ timeoutMs: options.readyTimeoutMs,
680
+ reopenClosed: options.reopenClosed,
681
+ onNotice: (line) => log(` ${chalk.yellow(line)}`)
682
+ });
683
+ if (!readiness.ready && readiness.closedDatabase !== void 0) {
684
+ const name = readiness.closedDatabase;
685
+ const hint = `Database "${name}" on port ${target.port} is closed to new connections (ALLOW_CONNECTIONS false), and could not be opened again. \`rebase db branch create --force\` closes its source for the copy, and one stopped before it finished leaves it closed; nothing in it is lost. Open it with ${chalk.cyan("rebase db branch list")} (every rebase db branch command opens it first), or run, connected to the server's postgres database: ALTER DATABASE "${name.replace(/"/g, "\"\"")}" ALLOW_CONNECTIONS true;`;
686
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
687
+ return {
688
+ action: "start-failed",
689
+ hint
690
+ };
691
+ }
646
692
  if (!readiness.ready) {
647
693
  const seconds = Math.round((options.readyTimeoutMs ?? 6e4) / 1e3);
648
694
  const hint = `The database container is listening on port ${target.port} but did not answer a query within ${seconds}s (${readiness.error}). Run ${chalk.cyan("docker compose logs db")} to see why, then ${chalk.cyan("rebase db push")}.`;
@@ -678,6 +724,6 @@ async function ensureDevDatabase(options) {
678
724
  }
679
725
  }
680
726
  //#endregion
681
- export { parseLoopbackDsn as a, ensureDevDatabase as i, dev_preflight_exports as n, probeTcp as o, dockerClaimedPorts as r, Refusal as s, composeDatabaseUrl as t };
727
+ export { parseLoopbackDsn as a, ensureDevDatabase as i, dev_preflight_exports as n, probeTcp as o, dockerClaimedPorts as r, composeDatabaseUrl as t };
682
728
 
683
- //# sourceMappingURL=dev-preflight-unHpiYO1.js.map
729
+ //# sourceMappingURL=dev-preflight-Cxv3TiD1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-preflight-Cxv3TiD1.js","names":[],"sources":["../src/utils/dev-preflight.ts"],"sourcesContent":["/**\n * Make `rebase dev` the only command a new project needs.\n *\n * The scaffolded project prints three steps — start the database container,\n * push the schema, start the dev server — and a first-time reader has to\n * perform all three correctly before anything works. Two of them are\n * mechanical, and both fail in ways that do not name themselves: skipping the\n * container gives `ECONNREFUSED` from a stack the reader has never seen, and\n * skipping the push gives a server that starts cleanly and then 500s on the\n * first request because the tables are not there.\n *\n * So `dev` does them. Not always, and not silently — under four conditions that\n * together mean \"this is a throwaway development database on this laptop, and\n * it is not running\":\n *\n * 1. The DSN resolves to a loopback address. A remote database is somebody's\n * real data and is never touched here, whatever else is true.\n * 2. Nothing is listening on it. A database that answers is left completely\n * alone — no push, no inspection, no connection.\n * 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.\n * the one `rebase init` wrote. Without it there is nothing to start and\n * the backend's own error is the better message.\n * 4. Docker is available. If it is not, the manual steps are printed and dev\n * continues — a preflight must never be the reason the command fails.\n *\n * The schema push happens only when this function actually started the\n * container, which is the one moment a local database is known to be new or\n * stale relative to the collections. A `dev` against an already-running\n * database never mutates its schema.\n *\n * One case stops the command instead of stepping aside: the URL is this\n * project's compose database, and Docker says what answers on its port is\n * somebody else's. Carrying on would connect this project to that database,\n * and the best case is a failed login whose advice (\"ALTER USER …\") is for the\n * wrong one. See {@link classifyPortPublishers}.\n */\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { execa, ExecaError } from \"execa\";\n\nimport { readEnvFile } from \"./project\";\nimport { Refusal } from \"./refusal\";\n\n/** Where the preflight stopped, and why. Returned so tests can assert it. */\nexport type PreflightOutcome =\n | { action: \"disabled\" }\n | { action: \"no-dsn\" }\n | { action: \"remote-dsn\"; host: string }\n | { action: \"already-running\"; host: string; port: number }\n | { action: \"no-compose\" }\n | { action: \"no-docker\"; hint: string }\n | { action: \"start-failed\"; hint: string }\n | { action: \"wrong-port\"; hint: string; port: number; composePort: number }\n | { action: \"started\"; port: number; pushed: boolean };\n\n/**\n * Hosts that mean \"this machine\", and so may be provisioned automatically.\n *\n * An exact-match set, never a prefix or substring test: `localhost.example.com`\n * is a perfectly resolvable name that somebody else controls.\n */\nconst LOOPBACK = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"0.0.0.0\"]);\n\n/**\n * The host and port of a Postgres DSN, but only when it points at this machine.\n *\n * Returns null for anything else — a remote host, an unparseable string, a\n * non-postgres scheme. Callers treat null as \"do nothing\", so every parse\n * failure fails closed onto the safe behaviour rather than onto a guess.\n */\nexport function parseLoopbackDsn(dsn: string | undefined): { host: string; port: number } | null {\n if (!dsn) return null;\n\n let url: URL;\n try {\n url = new URL(dsn);\n } catch {\n return null;\n }\n\n if (!/^postgres(ql)?:$/.test(url.protocol)) return null;\n\n // `url.hostname` KEEPS the brackets an IPv6 literal carries in a URL, and\n // `net.connect` rejects them. Normalising here rather than at each use is\n // what stops the two representations diverging: everything downstream gets\n // a host it can dial.\n const host = url.hostname.replace(/^\\[(.+)\\]$/, \"$1\");\n if (!LOOPBACK.has(host)) return null;\n\n const port = url.port ? Number(url.port) : 5432;\n if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;\n\n return { host, port };\n}\n\n/**\n * Whether a compose file declares a `db` service.\n *\n * Deliberately a text scan and not a YAML parse: the CLI has no YAML\n * dependency, and the question is only ever asked of a file this tool wrote.\n * A false negative costs the automation and nothing else — the reader gets the\n * manual steps, which are correct.\n */\nexport function composeDeclaresDbService(yamlText: string): boolean {\n let inServices = false;\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) {\n inServices = true;\n continue;\n }\n // Any other zero-indent key ends the services block.\n if (inServices && /^\\S/.test(line)) {\n inServices = false;\n continue;\n }\n if (inServices && /^\\s{2,}db:\\s*$/.test(line)) return true;\n }\n return false;\n}\n\n/**\n * The connection string the compose file's `db` service will answer on.\n *\n * `--docker` used to be a flag that changed a banner and nothing else: it\n * resolved to `kind: \"docker\"`, and every caller then had no URL to reach the\n * container with, so the backend booted with no `DATABASE_URL` at all and died\n * on the message a project with no database gets. The container it had just\n * been asked for was never started, because the preflight that starts it needs\n * a DSN to decide the database is local and not running.\n *\n * So the URL is derived rather than required: the compose file names the user,\n * the database and the published host port, and `.env` holds the password\n * compose itself interpolates. That is the same string `rebase init` writes as\n * the commented-out `DATABASE_URL`, which is what makes uncommenting that line\n * and passing `--docker` reach the same database.\n *\n * Returns null when the file declares no `db` service, or declares one this\n * scan cannot read — a `--docker` that cannot be honoured has to say so, and\n * a guess would point at somebody else's Postgres on 5432.\n *\n * Same text scan as {@link composeDeclaresDbService}, for the same reason.\n */\nexport function composeDatabaseUrl(\n yamlText: string,\n env: Record<string, string | undefined> = {}\n): string | null {\n const found = scanComposeDbService(yamlText);\n\n const user = expandComposeValue(found.POSTGRES_USER, env);\n const password = expandComposeValue(found.POSTGRES_PASSWORD, env);\n const database = expandComposeValue(found.POSTGRES_DB, env);\n if (!user || !password || !database || !found.hostPort) return null;\n\n return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}` +\n `@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;\n}\n\n/**\n * The host port the compose `db` service publishes, or null.\n *\n * Separate from {@link composeDatabaseUrl} because the port is answerable on\n * its own: the URL needs the user, the password and the database name too, and\n * a compose file missing any of them still says which port a container would\n * listen on. That is the question \"would starting this container answer the\n * DSN in `.env`?\" — see the `wrong-port` branch of {@link ensureDevDatabase},\n * where getting it wrong meant starting a container nobody asked for.\n */\nexport function composeHostPort(yamlText: string): number | null {\n const port = scanComposeDbService(yamlText).hostPort;\n\n return port ? Number(port) : null;\n}\n\n/** The raw values of the compose `db` service, before any expansion. */\nfunction scanComposeDbService(yamlText: string): Record<string, string> & { hostPort?: string } {\n let inServices = false;\n let inDb = false;\n let dbIndent = 0;\n let section: \"environment\" | \"ports\" | null = null;\n const found: Record<string, string> = {};\n let hostPort: string | null = null;\n\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) { inServices = true; continue; }\n if (inServices && /^\\S/.test(line)) { inServices = false; inDb = false; continue; }\n if (!inServices) continue;\n\n const indent = line.length - line.trimStart().length;\n const dbHeader = line.match(/^(\\s{2,})db:\\s*$/);\n if (dbHeader) { inDb = true; dbIndent = dbHeader[1].length; section = null; continue; }\n // A sibling service at the same indent as `db:` ends it.\n if (inDb && indent <= dbIndent) { inDb = false; section = null; continue; }\n if (!inDb) continue;\n\n const key = line.match(/^\\s+(environment|ports):\\s*$/);\n if (key) { section = key[1] as \"environment\" | \"ports\"; continue; }\n // Any other key of the service ends the block we were reading.\n if (/^\\s+[A-Za-z_][\\w-]*:\\s*\\S/.test(line) && !/^\\s+POSTGRES_/.test(line)) {\n if (!/^\\s*-\\s/.test(line.trimStart())) section = null;\n }\n\n if (section === \"environment\") {\n const pair = line.match(/^\\s+(POSTGRES_(?:USER|PASSWORD|DB)):\\s*(.+?)\\s*$/);\n if (pair) found[pair[1]] = pair[2].replace(/^[\"']|[\"']$/g, \"\");\n continue;\n }\n\n if (section === \"ports\" && hostPort === null) {\n // `- \"5435:5432\"`. Only a mapping whose container side is Postgres's\n // own port is the one to dial; anything else is a different service.\n const mapping = line.match(/^\\s*-\\s*[\"']?(?:[\\d.]+:)?(\\d+):5432(?:\\/tcp)?[\"']?\\s*$/);\n if (mapping) hostPort = mapping[1];\n }\n }\n\n return hostPort === null ? found : { ...found, hostPort };\n}\n\n/**\n * `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves\n * them — against the project's `.env`, which is the file compose itself reads.\n */\nfunction expandComposeValue(\n raw: string | undefined,\n env: Record<string, string | undefined>\n): string | null {\n if (!raw) return null;\n const interpolated = raw.replace(/\\$\\{([A-Za-z_][\\w]*)(?::?-([^}]*))?\\}/g, (_all, name, fallback) => {\n const value = env[name];\n if (value !== undefined && value !== \"\") return value;\n return fallback ?? \"\";\n });\n const trimmed = interpolated.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Is something accepting connections there right now?\n *\n * A reset counts as yes: a refused connection means nothing listens, a reset\n * means something accepted it and dropped it — Docker's port proxy, while the\n * Postgres behind it is still starting. On macOS that reset arrives after\n * `connect`; on Linux it can arrive instead of it, and read as \"not listening\"\n * the port wait spun for its whole minute before readiness was ever asked.\n */\nexport function probeTcp(host: string, port: number, timeoutMs = 700): Promise<boolean> {\n return new Promise(resolve => {\n const socket = new net.Socket();\n let settled = false;\n\n const done = (open: boolean): void => {\n if (settled) return;\n settled = true;\n socket.destroy();\n resolve(open);\n };\n\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => done(true));\n socket.once(\"timeout\", () => done(false));\n socket.once(\"error\", (error: Error) => done(\"code\" in error && error.code === \"ECONNRESET\"));\n socket.connect(port, host);\n });\n}\n\n/**\n * Poll until the port answers, or give up.\n *\n * This only answers \"has the container got as far as listening\". It is not\n * readiness: Docker's port proxy accepts on the host port while Postgres inside\n * the container is still running initdb, and resets every connection it cannot\n * forward. {@link waitForQueryable} is the check that a query will work.\n */\nexport async function waitForPort(\n host: string,\n port: number,\n timeoutMs = 60_000,\n intervalMs = 500\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await probeTcp(host, port, 700)) return true;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n }\n return false;\n}\n\n/**\n * Whether a database answered a query, and if not, the last reason it gave —\n * and, for a database closed to new connections, its name.\n */\nexport type QueryReadiness = { ready: true } | { ready: false; error: string; closedDatabase?: string };\n\n/** What opening a closed database again did, and what to say about it. */\nexport interface ClosedDatabaseHeal {\n /** It accepts connections again. */\n reopened: boolean;\n /** The driver's own lines about it (`reopenedDatabasesNotice`). */\n notice: string[];\n}\n\n/**\n * The database a connection was refused by because it is closed to new\n * connections (`ALLOW_CONNECTIONS false`), or null.\n *\n * The driver's `closedDatabaseName`, for the error a bare `pg` connect gives:\n * the sentence is matched, not SQLSTATE 55000, which also covers failures that\n * have nothing to do with connecting.\n */\nexport function closedDatabaseName(error: unknown): string | null {\n if (!error || typeof error !== \"object\") return null;\n if (\"message\" in error && typeof error.message === \"string\") {\n const named = /database \"(.+)\" is not currently accepting connections/.exec(error.message);\n if (named) return named[1];\n }\n return \"cause\" in error ? closedDatabaseName(error.cause) : null;\n}\n\n/** Open `databaseUrl`'s database again with the driver, as every `rebase db branch` command does first. */\nasync function reopenWithDriver(databaseUrl: string): Promise<ClosedDatabaseHeal> {\n const { reopenClosedDatabases, reopenedDatabasesNotice } = await import(\"@rebasepro/server-postgres\");\n const result = await reopenClosedDatabases(databaseUrl);\n return { reopened: result.reopened.length > 0, notice: reopenedDatabasesNotice(result) };\n}\n\n/**\n * Poll until `SELECT 1` succeeds on this connection string, or the bound runs out.\n *\n * The readiness that matters to everything after it. A fresh compose volume\n * answers the TCP probe seconds before it answers a query — the official image\n * runs initdb on a socket-only server, then restarts on TCP — and the schema\n * push that ran in that gap failed on `read ECONNRESET`, three runs out of\n * three. Every error is retried: a reset, \"the database system is starting up\"\n * and a refused connection all mean \"not yet\", and the bound is what turns\n * \"not yet\" into an answer.\n *\n * Except one, which is not \"not yet\": a database closed to new connections\n * (`closedDatabaseName`), which is what a `rebase db branch create --force`\n * killed mid-copy leaves its source as. Waiting cannot open it, and it was\n * waited on for the whole minute. It is opened again with the driver's\n * `reopenClosedDatabases`, once, and then either answers or the wait ends at\n * once, naming it.\n */\nexport async function waitForQueryable(\n databaseUrl: string,\n options: {\n timeoutMs?: number;\n intervalMs?: number;\n /** Opens a closed database again. The driver's `reopenClosedDatabases` by default; injected for tests. */\n reopenClosed?: (databaseUrl: string) => Promise<ClosedDatabaseHeal>;\n /** Where the driver's lines about a database it opened go. */\n onNotice?: (line: string) => void;\n } = {}\n): Promise<QueryReadiness> {\n const { timeoutMs = 60_000, intervalMs = 500 } = options;\n const { Client } = await import(\"pg\");\n const deadline = Date.now() + timeoutMs;\n let lastError = \"no attempt was made\";\n let healed = false;\n\n do {\n const client = new Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: Math.max(250, Math.min(2_000, deadline - Date.now()))\n });\n // A reset can arrive after `connect` settles; unhandled, it would take\n // the process down rather than count as one more \"not yet\".\n client.on(\"error\", () => undefined);\n let closed: string | null = null;\n try {\n await client.connect();\n await client.query(\"SELECT 1\");\n return { ready: true };\n } catch (error) {\n lastError = error instanceof Error && error.message ? error.message : String(error);\n closed = closedDatabaseName(error);\n } finally {\n await client.end().catch(() => undefined);\n }\n if (closed !== null) {\n if (healed) return { ready: false, error: lastError, closedDatabase: closed };\n healed = true;\n const heal = await (options.reopenClosed ?? reopenWithDriver)(databaseUrl).catch(() => ({ reopened: false, notice: [] }));\n for (const line of heal.notice) options.onNotice?.(line);\n if (!heal.reopened) return { ready: false, error: lastError, closedDatabase: closed };\n // Open again: ask straight away rather than after the interval.\n continue;\n }\n if (Date.now() + intervalMs >= deadline) break;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n } while (Date.now() < deadline);\n\n return { ready: false, error: lastError };\n}\n\n/**\n * What a failed schema push printed, as lines to show.\n *\n * `rebase dev` runs the push with its output captured, so a successful start\n * stays quiet. On a failure that captured output is the diagnosis, and it was\n * being dropped: the warning said \"fix the error above\" over nothing at all.\n * The child's stdout and stderr are what is shown — not execa's own message,\n * which leads with the command line, and that carries the database password.\n */\nexport function pushFailureOutput(error: unknown): string[] {\n if (error instanceof ExecaError) {\n const captured = [streamText(error.stdout), streamText(error.stderr)]\n .map(stream => stream.trim())\n .filter(stream => stream.length > 0);\n if (captured.length > 0) return captured.join(\"\\n\").split(/\\r?\\n/);\n return [error.originalMessage || `The push exited with code ${error.exitCode ?? \"unknown\"}.`];\n }\n return [error instanceof Error ? error.message : String(error)];\n}\n\n/** A captured stream as text, whichever form execa's options gave it. */\nfunction streamText(stream: unknown): string {\n if (typeof stream === \"string\") return stream;\n if (stream instanceof Uint8Array) return Buffer.from(stream).toString(\"utf8\");\n if (Array.isArray(stream)) return stream.map(String).join(\"\\n\");\n return \"\";\n}\n\n/** The user and password the compose `db` service creates, expanded against `.env`. */\nfunction composeCredentials(\n yamlText: string,\n env: Record<string, string | undefined>\n): { user: string; password: string } | null {\n const found = scanComposeDbService(yamlText);\n const user = expandComposeValue(found.POSTGRES_USER, env);\n const password = expandComposeValue(found.POSTGRES_PASSWORD, env);\n return user && password ? { user, password } : null;\n}\n\n/**\n * The compose project a file belongs to, named the way `docker compose` names\n * it: the file's top-level `name:`, else the directory, lowercased with\n * everything but `[a-z0-9_-]` dropped.\n *\n * A project name set with `-p` or in the environment is not read: a container\n * started from this directory is recognised by its working directory anyway,\n * and the name is only the fallback for a project that has moved since.\n */\nexport function composeProjectName(yamlText: string, projectRoot: string): string {\n const named = /^name:\\s*[\"']?([^\"'#\\s]+)[\"']?\\s*(?:#.*)?$/m.exec(yamlText)?.[1];\n const raw = named || path.basename(projectRoot);\n return raw.toLowerCase().replace(/[^a-z0-9_-]/g, \"\");\n}\n\n/** Who answers on a host port, as far as Docker can say. */\nexport type PortHolder =\n | { kind: \"this-project\" }\n | { kind: \"container\"; name: string; project?: string; directory?: string }\n | { kind: \"outside-docker\" }\n | { kind: \"unknown\" };\n\n/** One `docker ps` row per container publishing the port: name, then its compose labels. */\nconst PUBLISHER_FORMAT = [\n \"{{.Names}}\",\n \"{{.Label \\\"com.docker.compose.project\\\"}}\",\n \"{{.Label \\\"com.docker.compose.project.working_dir\\\"}}\",\n \"{{.Label \\\"com.docker.compose.service\\\"}}\"\n].join(\"\\t\");\n\n/**\n * Whose container `docker ps --filter publish=<port>` listed, from its rows.\n *\n * This project's when one of them is a `db` service of this compose project:\n * started from this directory, or carrying the project's name (compose treats\n * two directories with one `name:` as one project, so this does too). Any other\n * container is named. No rows at all, from a Docker that answered, means the\n * listener is not a container: a Postgres installed on this machine, say.\n */\nexport function classifyPortPublishers(output: string, projectRoot: string, composeProject: string | null): PortHolder {\n const rows = output.split(/\\r?\\n/)\n .map(line => line.split(\"\\t\").map(field => field.trim()))\n .filter(([name]) => Boolean(name));\n if (rows.length === 0) return { kind: \"outside-docker\" };\n\n const here = canonicalPath(projectRoot);\n for (const [, project, directory, service] of rows) {\n if (service !== \"db\") continue;\n if ((directory && canonicalPath(directory) === here) || (composeProject && project === composeProject)) {\n return { kind: \"this-project\" };\n }\n }\n const [name, project, directory] = rows[0];\n return { kind: \"container\", name, ...(project ? { project } : {}), ...(directory ? { directory } : {}) };\n}\n\nfunction canonicalPath(dir: string): string {\n try {\n return fs.realpathSync(dir);\n } catch {\n return path.resolve(dir);\n }\n}\n\n/** Ask Docker who publishes `port`. `unknown` when Docker cannot be asked. */\nexport async function dockerPortHolder(projectRoot: string, composeProject: string | null, port: number): Promise<PortHolder> {\n try {\n const { stdout } = await execa(\"docker\", [\"ps\", \"--filter\", `publish=${port}`, \"--format\", PUBLISHER_FORMAT], {\n stdio: \"pipe\",\n timeout: 5_000\n });\n return classifyPortPublishers(stdout, projectRoot, composeProject);\n } catch {\n return { kind: \"unknown\" };\n }\n}\n\n/**\n * Every host port in a list of `docker inspect --format '{{json .HostConfig.PortBindings}}'` lines.\n *\n * The configured bindings, which a stopped container keeps: it takes its port\n * back the moment it starts, so the port is not free to give to a new project.\n * `HostPort: \"0\"` asks for any free port and claims none.\n */\nexport function parsePortBindings(output: string): Set<number> {\n const ports = new Set<number>();\n for (const line of output.split(/\\r?\\n/)) {\n if (!line.trim()) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== \"object\") continue;\n const bindingLists: unknown[] = Object.values(parsed);\n for (const bindings of bindingLists) {\n if (!Array.isArray(bindings)) continue;\n for (const binding of bindings) {\n if (!binding || typeof binding !== \"object\" || !(\"HostPort\" in binding)) continue;\n const hostPort = Number(binding.HostPort);\n if (Number.isInteger(hostPort) && hostPort > 0 && hostPort < 65536) ports.add(hostPort);\n }\n }\n }\n return ports;\n}\n\n/**\n * The host ports every container on this machine is bound to, running or\n * stopped — `null` when Docker cannot be asked.\n *\n * A TCP probe sees only what is listening now, and a stopped project's `db`\n * container is not; `docker ps` shows a stopped container's ports only\n * sometimes. The bindings in its config are what it will take back.\n */\nexport async function dockerClaimedPorts(): Promise<Set<number> | null> {\n try {\n const { stdout: ids } = await execa(\"docker\", [\"ps\", \"-aq\"], { stdio: \"pipe\", timeout: 5_000 });\n const containers = ids.split(/\\s+/).filter(Boolean);\n if (containers.length === 0) return new Set();\n // `reject: false`: a container removed since `ps` fails its own entry and\n // exits 1, and every other container's bindings are still on stdout.\n const { stdout } = await execa(\"docker\", [\"inspect\", \"--format\", \"{{json .HostConfig.PortBindings}}\", ...containers], {\n stdio: \"pipe\",\n timeout: 10_000,\n reject: false\n });\n return parsePortBindings(stdout);\n } catch {\n return null;\n }\n}\n\n/**\n * Whether `databaseUrl` is this project's compose database: the port the `db`\n * service publishes, with the user and password it creates.\n *\n * The credentials matter. One Postgres container shared by several projects is\n * an ordinary setup, and such a URL names that container's password — a\n * database somebody chose, which is theirs to point at.\n */\nfunction isComposeDatabaseUrl(databaseUrl: string, port: number, composeText: string, env: Record<string, string | undefined>): boolean {\n if (composeHostPort(composeText) !== port) return false;\n const credentials = composeCredentials(composeText, env);\n if (!credentials) return false;\n try {\n const url = new URL(databaseUrl);\n return decodeURIComponent(url.username) === credentials.user\n && decodeURIComponent(url.password) === credentials.password;\n } catch {\n return false;\n }\n}\n\n/** The first port after `port` that nothing on loopback answers on, for a suggestion. */\nasync function nextQuietPort(port: number): Promise<number | null> {\n for (let candidate = port + 1; candidate < Math.min(port + 50, 65536); candidate++) {\n if (!(await probeTcp(\"127.0.0.1\", candidate, 300))) return candidate;\n }\n return null;\n}\n\n/**\n * Said, then thrown: the compose port answers, and not with this project's\n * database. Returns quietly when the listener is ours, or when Docker cannot\n * say — the behaviour from before this check.\n */\nasync function refuseForeignListener(options: {\n projectRoot: string;\n databaseUrl: string;\n port: number;\n composeText: string;\n whoHoldsPort?: (port: number) => Promise<PortHolder>;\n log: (message: string) => void;\n}): Promise<void> {\n const { projectRoot, databaseUrl, port, composeText, log } = options;\n const env = readEnvFile(projectRoot);\n if (!isComposeDatabaseUrl(databaseUrl, port, composeText, env)) return;\n\n const whoHoldsPort = options.whoHoldsPort\n ?? ((p: number) => dockerPortHolder(projectRoot, composeProjectName(composeText, projectRoot), p));\n const holder = await whoHoldsPort(port);\n if (holder.kind === \"this-project\" || holder.kind === \"unknown\") return;\n\n const who = holder.kind === \"container\"\n ? `another container is listening there: ${holder.name}`\n + (holder.project || holder.directory\n ? ` (${[holder.project ? `compose project ${holder.project}` : \"\", holder.directory ? `in ${holder.directory}` : \"\"]\n .filter(Boolean).join(\", \")})`\n : \"\")\n : \"something outside Docker is listening there, such as a Postgres installed on this machine\";\n const suggestion = await nextQuietPort(port);\n const envSetsUrl = Boolean(env.DATABASE_URL?.trim());\n\n log(\"\");\n log(` ${chalk.yellow(\"⚠\")} ${chalk.bold(`Port ${port} is not this project's database.`)}`);\n for (const line of [\n `docker-compose.yml publishes this project's db service on ${port}, and ${who}.`,\n \"Nothing was started and nothing connected: a login there fails on this project's password, and the fix that failure suggests is for that database.\",\n `Give this project its own port: change \"${port}:5432\" under db: in docker-compose.yml`\n + ` to ${suggestion ? `a free one, such as \"${suggestion}:5432\"` : \"one nothing else uses\"}`\n + (envSetsUrl ? \", and the port in DATABASE_URL in .env.\" : \".\"),\n holder.kind === \"container\"\n ? `Or stop ${holder.name} first: docker stop ${holder.name}`\n : \"Or stop what is listening on it.\"\n ]) {\n log(` ${chalk.gray(line)}`);\n }\n\n // Said in full above, so a `Refusal`: `bin/rebase.js` adds no stack hint.\n throw new Refusal(\n holder.kind === \"container\"\n ? `Port ${port} belongs to ${holder.name}, not to this project's db service. Nothing was started.`\n : `Port ${port} belongs to a server outside Docker, not to this project's db service. Nothing was started.`\n );\n}\n\nexport interface EnsureDevDatabaseOptions {\n projectRoot: string;\n /** The DSN from the project's env file, if it has one. */\n databaseUrl: string | undefined;\n /** Set by `--no-db` or REBASE_DEV_NO_DB. */\n disabled: boolean;\n /** Whether the project has collections worth pushing. */\n hasCollections: boolean;\n /** Runs `rebase db push` for this project. Injected so tests need no database. */\n pushSchema: () => Promise<void>;\n /**\n * Brings the compose `db` service up. Injected for the same reason\n * `pushSchema` is: what happens *after* a start — a push that fails without\n * taking the dev server with it — is the part worth asserting, and it was\n * untestable while the only way in was a real `docker compose up`.\n */\n startDatabase?: (projectRoot: string) => Promise<void>;\n /** How long a started database has to answer `SELECT 1`. Defaults to 60s. */\n readyTimeoutMs?: number;\n /** Opens a database closed to new connections again; see `waitForQueryable`. Injected for tests. */\n reopenClosed?: (databaseUrl: string) => Promise<ClosedDatabaseHeal>;\n /**\n * Who answers on the compose port. Asks Docker by default; injected for the\n * same reason `startDatabase` is.\n */\n whoHoldsPort?: (port: number) => Promise<PortHolder>;\n log?: (message: string) => void;\n}\n\n/**\n * Start the project's development database if it is not running, and give it a\n * schema if this call is what started it.\n */\nexport async function ensureDevDatabase(options: EnsureDevDatabaseOptions): Promise<PreflightOutcome> {\n const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;\n const log = options.log ?? ((message: string) => console.log(message));\n\n if (disabled) return { action: \"disabled\" };\n\n const target = parseLoopbackDsn(databaseUrl);\n if (!databaseUrl) return { action: \"no-dsn\" };\n if (!target) {\n // Not an error, and not worth a warning either: pointing DATABASE_URL at\n // a real database is a completely ordinary thing to do, and the only\n // correct response is to keep away from it.\n let host = \"the configured host\";\n try {\n host = new URL(databaseUrl).hostname || host;\n } catch { /* keep the placeholder */ }\n return { action: \"remote-dsn\", host };\n }\n\n const composePath = path.join(projectRoot, \"docker-compose.yml\");\n const composeText = fs.existsSync(composePath) ? fs.readFileSync(composePath, \"utf8\") : null;\n\n if (await probeTcp(target.host, target.port)) {\n // Something answers. Whatever it is stays untouched; but if this URL is\n // the compose database, it has to be the compose container.\n if (composeText !== null) {\n await refuseForeignListener({\n projectRoot,\n databaseUrl,\n port: target.port,\n composeText,\n whoHoldsPort: options.whoHoldsPort,\n log\n });\n }\n return { action: \"already-running\", host: target.host, port: target.port };\n }\n\n if (composeText === null || !composeDeclaresDbService(composeText)) return { action: \"no-compose\" };\n\n const manualSteps =\n `${chalk.cyan(\"docker compose up -d db\")} then ${chalk.cyan(\"rebase db push\")}`;\n\n // Would starting that container answer *this* DSN? A `.env` naming a port\n // the compose file does not publish is the commonest way to get here, and\n // starting the container then told the reader the exact opposite of the\n // truth: `⚠ The database container did not begin listening on port 3139.\n // Run docker compose up -d db then rebase db push to see why.` The\n // container was up and healthy on 5436; the port in `.env` was what was\n // wrong. And a container nobody asked for was left running.\n const composePort = composeHostPort(composeText);\n if (composePort !== null && composePort !== target.port) {\n const hint =\n `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. `\n + `The compose file's db service publishes ${composePort}, so starting it would not `\n + `answer that URL — nothing was started. Either correct the port in .env, or point `\n + `DATABASE_URL at 127.0.0.1:${composePort}.`;\n log(\"\");\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"wrong-port\", hint, port: target.port, composePort };\n }\n\n log(\"\");\n log(` ${chalk.gray(\"Database not running — starting it…\")}`);\n\n const startDatabase = options.startDatabase ?? (async (root: string) => {\n await execa(\"docker\", [\"compose\", \"up\", \"-d\", \"db\"], { cwd: root, stdio: \"pipe\" });\n });\n\n try {\n await startDatabase(projectRoot);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Two very different failures land here and the reader needs to know\n // which: Docker not being installed at all, and Docker being installed\n // but unable to start this container.\n if (/ENOENT|not found|command not found/i.test(message)) {\n const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"no-docker\", hint };\n }\n const hint = `Could not start the database container. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n if (!(await waitForPort(target.host, target.port))) {\n const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n // Listening is not ready. Nothing is pushed into a database that has not\n // yet answered a query; see `waitForQueryable`.\n const readiness = await waitForQueryable(databaseUrl, {\n timeoutMs: options.readyTimeoutMs,\n reopenClosed: options.reopenClosed,\n onNotice: line => log(` ${chalk.yellow(line)}`)\n });\n if (!readiness.ready && readiness.closedDatabase !== undefined) {\n const name = readiness.closedDatabase;\n const hint = `Database \"${name}\" on port ${target.port} is closed to new connections (ALLOW_CONNECTIONS false), `\n + \"and could not be opened again. `rebase db branch create --force` closes its source for the copy, and one \"\n + \"stopped before it finished leaves it closed; nothing in it is lost. Open it with \"\n + `${chalk.cyan(\"rebase db branch list\")} (every rebase db branch command opens it first), or run, connected `\n + `to the server's postgres database: ALTER DATABASE \"${name.replace(/\"/g, '\"\"')}\" ALLOW_CONNECTIONS true;`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n if (!readiness.ready) {\n const seconds = Math.round((options.readyTimeoutMs ?? 60_000) / 1000);\n const hint = `The database container is listening on port ${target.port} but did not answer a query `\n + `within ${seconds}s (${readiness.error}). Run ${chalk.cyan(\"docker compose logs db\")} to see why, `\n + `then ${chalk.cyan(\"rebase db push\")}.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n log(` ${chalk.green(\"✓\")} ${chalk.gray(`Database ready on port ${target.port}`)}`);\n\n if (!hasCollections) return { action: \"started\", port: target.port, pushed: false };\n\n log(` ${chalk.gray(\"Pushing the schema…\")}`);\n try {\n await pushSchema();\n log(` ${chalk.green(\"✓\")} ${chalk.gray(\"Schema pushed\")}`);\n return { action: \"started\", port: target.port, pushed: true };\n } catch (error) {\n // The push runs with its output captured, so what it printed is shown\n // here, before the line that refers to it. Then the one thing the\n // reader needs next: the server is still starting, and against what.\n for (const line of pushFailureOutput(error)) log(` ${chalk.gray(line)}`);\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan(\"rebase db push\")}.`)}`);\n return { action: \"started\", port: target.port, pushed: false };\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { c as findProjectRoot } from "./project-Wp2zoyeN.js";
1
+ import { c as findProjectRoot } from "./project-CasK56vN.js";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  //#region src/utils/dev-server.ts
@@ -12,6 +12,17 @@ import path from "path";
12
12
  * 3001, for a project whose dev server had been on 3732.
13
13
  */
14
14
  /**
15
+ * The address `rebase dev` is starting its backend on, handed to the
16
+ * `generate-sdk` child it runs.
17
+ *
18
+ * Dev regenerates the SDK at boot, beside the database start and before the
19
+ * backend binds — so before the backend writes `.rebase/state.json`, which is
20
+ * where `generate-sdk` otherwise reads the address for the README's example.
21
+ * Dev knows the port it hands the backend, and the child's environment is the
22
+ * only channel between them.
23
+ */
24
+ var DEV_BACKEND_URL_ENV = "REBASE_DEV_BACKEND_URL";
25
+ /**
15
26
  * The port `rebase dev` derives from the project root path.
16
27
  * Range: 3001–3999 (avoids privileged ports and common services).
17
28
  * Two different project directories will almost always get different ports.
@@ -122,6 +133,6 @@ function serverUnreachableHint(baseUrl, projectRoot = findProjectRoot()) {
122
133
  return lines;
123
134
  }
124
135
  //#endregion
125
- export { serverUnreachableHint as i, getProjectPort as n, runningBackendUrl as r, devServerUrl as t };
136
+ export { serverUnreachableHint as a, runningBackendUrl as i, devServerUrl as n, getProjectPort as r, DEV_BACKEND_URL_ENV as t };
126
137
 
127
- //# sourceMappingURL=dev-server-B1UYifZJ.js.map
138
+ //# sourceMappingURL=dev-server-OJsUGZEB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-server-OJsUGZEB.js","names":[],"sources":["../src/utils/dev-server.ts"],"sourcesContent":["/**\n * Where `rebase dev` serves a project's backend.\n *\n * Not the `PORT` in `.env`: `rebase dev` overrides it with a port derived from\n * the project's path, so two projects on one machine do not collide, and that\n * `PORT` is what `rebase start` listens on. A command that talks to the backend\n * and gets no answer has to be able to say both — its hint named only `.env`'s\n * 3001, for a project whose dev server had been on 3732.\n */\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { findProjectRoot } from \"./project\";\n\n/**\n * The address `rebase dev` is starting its backend on, handed to the\n * `generate-sdk` child it runs.\n *\n * Dev regenerates the SDK at boot, beside the database start and before the\n * backend binds — so before the backend writes `.rebase/state.json`, which is\n * where `generate-sdk` otherwise reads the address for the README's example.\n * Dev knows the port it hands the backend, and the child's environment is the\n * only channel between them.\n */\nexport const DEV_BACKEND_URL_ENV = \"REBASE_DEV_BACKEND_URL\";\n\n/**\n * The port `rebase dev` derives from the project root path.\n * Range: 3001–3999 (avoids privileged ports and common services).\n * Two different project directories will almost always get different ports.\n */\nexport function getProjectPort(projectRoot: string): number {\n let hash = 0;\n for (let i = 0; i < projectRoot.length; i++) {\n hash = ((hash << 5) - hash + projectRoot.charCodeAt(i)) | 0;\n }\n return 3001 + (Math.abs(hash) % 999);\n}\n\n/** A TCP port out of a string or a number, or null. */\nfunction portOf(raw: unknown): number | null {\n const port = typeof raw === \"number\" ? raw : typeof raw === \"string\" ? Number(raw.trim()) : Number.NaN;\n return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;\n}\n\nfunction readText(file: string): string | null {\n try {\n return fs.readFileSync(file, \"utf-8\").trim();\n } catch {\n return null;\n }\n}\n\n/** Is *some* process with this pid running? EPERM means it is, as someone else. */\nfunction processAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * The address of this project's backend while it runs, or `null`.\n *\n * From `.rebase/state.json`, which the server writes when it binds — under\n * `rebase dev` and `rebase start` alike — and only while the pid it names is\n * alive: a backend that was killed leaves the file behind.\n */\nexport function runningBackendUrl(projectRoot: string): string | null {\n const state = readText(path.join(projectRoot, \".rebase\", \"state.json\"));\n if (!state) return null;\n try {\n const parsed: unknown = JSON.parse(state);\n if (typeof parsed !== \"object\" || parsed === null || !(\"pid\" in parsed)) return null;\n const pid = parsed.pid;\n if (typeof pid !== \"number\" || !Number.isInteger(pid) || pid <= 0 || !processAlive(pid)) return null;\n if (\"baseUrl\" in parsed && typeof parsed.baseUrl === \"string\" && parsed.baseUrl.trim()) return parsed.baseUrl.trim();\n const port = \"port\" in parsed ? portOf(parsed.port) : null;\n return port !== null ? `http://localhost:${port}` : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The URL `rebase dev` serves this project on, and how that is known:\n *\n * - `running` — the URL a running dev wrote to `.rebase-dev-url`;\n * - `recorded` — the port the last run left in `.rebase/state.json` or\n * `.rebase-dev-port` (both outlive a dev that did not shut down cleanly);\n * - `derived` — nothing is recorded, so the port it derives for this path.\n *\n * Asked only to describe where dev is; a `PORT` in the shell would move the\n * next run, and is not something a stopped dev left behind.\n */\nexport function devServerUrl(projectRoot: string): { url: string; source: \"running\" | \"recorded\" | \"derived\" } {\n const running = readText(path.join(projectRoot, \".rebase-dev-url\"));\n if (running) return { url: running, source: \"running\" };\n\n const state = readText(path.join(projectRoot, \".rebase\", \"state.json\"));\n if (state) {\n try {\n const parsed: unknown = JSON.parse(state);\n const port = typeof parsed === \"object\" && parsed !== null && \"port\" in parsed ? portOf(parsed.port) : null;\n if (port !== null) return { url: `http://localhost:${port}`, source: \"recorded\" };\n } catch { /* fall through to the port file */ }\n }\n\n const portFile = portOf(readText(path.join(projectRoot, \".rebase-dev-port\"))?.split(/\\s+/)[0]);\n if (portFile !== null) return { url: `http://localhost:${portFile}`, source: \"recorded\" };\n\n return { url: `http://localhost:${getProjectPort(projectRoot)}`, source: \"derived\" };\n}\n\n/**\n * What to say when nothing answered at `baseUrl`.\n *\n * The first line is the question it always asked. The second is added when\n * `rebase dev` serves this project somewhere else, because then `baseUrl` is\n * almost always `.env`'s `PORT`, and a reader who just stopped `rebase dev`\n * reads the number as the one it was using.\n */\nexport function serverUnreachableHint(baseUrl: string, projectRoot: string | null = findProjectRoot()): string[] {\n const lines = [` Is the Rebase server running at ${baseUrl}? Start it with \\`rebase dev\\`, or set REBASE_BASE_URL in .env.`];\n if (!projectRoot) return lines;\n\n const dev = devServerUrl(projectRoot);\n let same = false;\n try {\n same = new URL(dev.url).origin === new URL(baseUrl).origin;\n } catch {\n // An unparseable URL on either side is not the same address.\n }\n if (same) return lines;\n\n const why = dev.source === \"derived\" ? \"the port it picks for this project\" : \"the port it last used\";\n lines.push(` \\`rebase dev\\` serves this project at ${dev.url} (${why}), and this command uses that address while it runs.`);\n\n return lines;\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
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-BX0YD14a.js";
3
- import { d as findBackendApp, g as resolveBackendPaths, p as loadManifest } from "./manifest-DRp0UVTj.js";
1
+ import { f as requireBackendDir, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, o as findEnvFile, p as requireProjectRoot, r as exitDependenciesNotInstalled, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
2
+ import { d as findBackendApp, g as resolveBackendPaths, p as loadManifest } from "./manifest-BtzDy7KQ.js";
4
3
  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-DdnU70z8.js";
7
- import { absolutizeLocalPathArgs, n as argsFromCommand, t as reportSpawnFailure } from "./db-CRNpA5mH.js";
4
+ import { i as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-DNvtEi2-.js";
5
+ import { collectionsDirArgs, resolveCollectionsDir } from "./collections-dir-DlvDhime.js";
6
+ import { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from "./prepare-CTw9NEkh.js";
7
+ import { DATABASE_SOURCE_ENV, absolutizeLocalPathArgs, n as argsFromCommand, t as reportSpawnFailure } from "./db-KzlqddGD.js";
8
8
  import chalk from "chalk";
9
9
  import fs from "fs";
10
10
  import path from "path";
@@ -621,7 +621,7 @@ function hasDeclaredCollections(projectRoot) {
621
621
  * the right failure for a check that only ever reports.
622
622
  */
623
623
  function readDeclaredSlugs(projectRoot) {
624
- const dir = path.join(projectRoot, "config", "collections");
624
+ const dir = resolveCollectionsDir(projectRoot);
625
625
  if (!fs.existsSync(dir)) return [];
626
626
  const slugs = [];
627
627
  let entries;
@@ -686,8 +686,8 @@ var DOCTOR_FLAG_HELP = {
686
686
  * The line handed to the driver, every path on it resolved from where the
687
687
  * command was run — the driver runs in `backend/`. See `utils/path-flags.ts`.
688
688
  */
689
- function doctorDriverArgs(rawArgs, cwd) {
690
- return absolutizeLocalPathArgs(argsFromCommand(rawArgs, "doctor"), cwd, [
689
+ function doctorDriverArgs(rawArgs, cwd, projectRoot) {
690
+ const args = absolutizeLocalPathArgs(argsFromCommand(rawArgs, "doctor"), cwd, [
691
691
  "--collections",
692
692
  "-c",
693
693
  "--schema",
@@ -695,6 +695,7 @@ function doctorDriverArgs(rawArgs, cwd) {
695
695
  "--sdk",
696
696
  "-k"
697
697
  ]);
698
+ return projectRoot ? [...args, ...collectionsDirArgs(projectRoot, args)] : args;
698
699
  }
699
700
  /** `--collections, -c` — the aliases, resolved from the spec rather than retyped. */
700
701
  function doctorFlagLabel(flag) {
@@ -755,6 +756,7 @@ async function doctorCommand(rawArgs) {
755
756
  try {
756
757
  const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
757
758
  Object.assign(env, prepared.env);
759
+ env[DATABASE_SOURCE_ENV] = prepared.database.source;
758
760
  for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));
759
761
  } catch (error) {
760
762
  console.log("");
@@ -771,12 +773,12 @@ async function doctorCommand(rawArgs) {
771
773
  if (pluginCli.endsWith(".ts")) {
772
774
  const tsxBin = resolveTsx(projectRoot);
773
775
  if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
774
- await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
776
+ await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd(), projectRoot)], {
775
777
  cwd: backendDir,
776
778
  stdio: "inherit",
777
779
  env
778
780
  });
779
- } else await execa("node", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
781
+ } else await execa("node", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd(), projectRoot)], {
780
782
  cwd: backendDir,
781
783
  stdio: "inherit",
782
784
  env
@@ -790,4 +792,4 @@ async function doctorCommand(rawArgs) {
790
792
  //#endregion
791
793
  export { doctorCommand };
792
794
 
793
- //# sourceMappingURL=doctor-CATRdWm6.js.map
795
+ //# sourceMappingURL=doctor-BRWOlqeG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-BRWOlqeG.js","names":[],"sources":["../src/utils/libpq-url.ts","../src/doctor-environment.ts","../src/commands/doctor.ts"],"sourcesContent":["/**\n * Detect Postgres connection strings that libpq refuses to parse.\n *\n * libpq splits a URI query parameter on the FIRST `=` and rejects any further\n * one in the same parameter:\n *\n * extra key/value separator \"=\" in URI query parameter: \"options\"\n *\n * That makes a URL like\n *\n * postgresql://u:p@h:5432/db?options=-c%20search_path=public&sslmode=disable\n *\n * unusable by every libpq caller — `pg_dump`/`pg_restore` behind\n * `rebase db backup|restore`, and a plain `psql \"$DATABASE_URL\"`. It is not a\n * version quirk: Postgres 15 through 18 all refuse it.\n *\n * `rebase init` generated exactly that string until 2026-08-18, and the failure\n * is invisible day to day because node-postgres parses URLs itself and accepts\n * it — so `rebase dev` and `rebase db push` work while backups do not. Fixing\n * the generator does nothing for projects that already exist, which is what\n * these functions are for: `rebase doctor` reads them off disk and reports.\n */\n\n/** A query parameter whose value carries a literal `=`. */\nexport interface UnparseableParam {\n /** The parameter name, e.g. \"options\". */\n name: string;\n /** The raw `name=value` text as it appears in the URL. */\n raw: string;\n}\n\n/** Postgres URI schemes. A key/value DSN (\"host=… dbname=…\") is not one. */\nconst POSTGRES_URI_RE = /^postgres(ql)?:\\/\\//i;\n\n/**\n * Return the query parameters libpq would reject, or an empty array.\n *\n * Deliberately not built on `new URL()`: that parser is happy to accept the\n * broken form (it splits on the first `=` and keeps the rest as the value),\n * so it cannot see the defect at all. The rule being checked is libpq's, and\n * it is about the raw text.\n */\nexport function findUnparseableParams(connectionString: string): UnparseableParam[] {\n if (!POSTGRES_URI_RE.test(connectionString)) return [];\n\n const q = connectionString.indexOf(\"?\");\n if (q === -1) return [];\n\n // A fragment is not part of the query.\n const query = connectionString.slice(q + 1).split(\"#\")[0];\n if (!query) return [];\n\n const found: UnparseableParam[] = [];\n for (const part of query.split(\"&\")) {\n if (!part) continue;\n // One separator is the parameter's own; any further one is the defect.\n if (part.split(\"=\").length > 2) {\n found.push({ name: part.slice(0, part.indexOf(\"=\")), raw: part });\n }\n }\n return found;\n}\n\n/**\n * Percent-encode the offending `=` characters, leaving everything else byte for\n * byte as it was.\n *\n * Only the second and later `=` in a parameter are rewritten — the first is the\n * real separator. Nothing else is touched: re-serialising through `URL` would\n * also reorder parameters and turn spaces into `+`, which libpq does not decode,\n * so a \"tidy up\" would trade one unparseable string for another.\n */\nexport function encodeExtraEquals(connectionString: string): string {\n if (findUnparseableParams(connectionString).length === 0) return connectionString;\n\n const q = connectionString.indexOf(\"?\");\n const head = connectionString.slice(0, q + 1);\n const rest = connectionString.slice(q + 1);\n const hash = rest.indexOf(\"#\");\n const query = hash === -1 ? rest : rest.slice(0, hash);\n const fragment = hash === -1 ? \"\" : rest.slice(hash);\n\n const fixed = query\n .split(\"&\")\n .map((part) => {\n const first = part.indexOf(\"=\");\n if (first === -1) return part;\n const name = part.slice(0, first);\n const value = part.slice(first + 1).replace(/=/g, \"%3D\");\n return `${name}=${value}`;\n })\n .join(\"&\");\n\n return `${head}${fixed}${fragment}`;\n}\n\n/** One connection string, found somewhere on disk, that libpq cannot parse. */\nexport interface LibpqUrlFinding {\n /** File it came from, relative to the project root. */\n file: string;\n /** The variable holding it, e.g. \"DATABASE_URL\". */\n variable: string;\n /** Parameter names libpq objects to. */\n params: string[];\n /** The same string with the offending `=` encoded. */\n suggested: string;\n}\n\n/** Variables whose value is a Postgres connection string. */\nconst CONNECTION_VARIABLES = [\"DATABASE_URL\", \"ADMIN_CONNECTION_STRING\"];\n\n/**\n * Scan one file's text for connection strings libpq would reject.\n *\n * Handles both shapes a scaffolded project uses, because both shipped with the\n * defect and a deployed stack is broken by the compose one alone:\n *\n * .env DATABASE_URL=postgresql://…\n * docker-compose.yml DATABASE_URL: postgresql://…\n */\nexport function scanTextForLibpqUrls(file: string, text: string): LibpqUrlFinding[] {\n const findings: LibpqUrlFinding[] = [];\n\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n // Commented-out examples are not what the project runs on.\n if (trimmed.startsWith(\"#\")) continue;\n\n for (const variable of CONNECTION_VARIABLES) {\n // `NAME=value` or `NAME: value`, optionally exported/indented.\n const match = trimmed.match(\n new RegExp(`^(?:export\\\\s+)?${variable}\\\\s*[:=]\\\\s*(.+)$`)\n );\n if (!match) continue;\n\n // Strip surrounding quotes and any trailing comment-free whitespace.\n let value = match[1].trim();\n const quoted = value.match(/^(['\"])(.*)\\1$/);\n if (quoted) value = quoted[2];\n if (!value) continue;\n\n const params = findUnparseableParams(value);\n if (params.length === 0) continue;\n\n findings.push({\n file,\n variable,\n params: params.map((p) => p.name),\n suggested: encodeExtraEquals(value)\n });\n }\n }\n\n return findings;\n}\n","/**\n * The checks that need no database, and that catch what breaks a first run.\n *\n * `rebase doctor` compared three descriptions of a schema, which is the right\n * check for a project that is working and the wrong one for a project that has\n * never worked. Everything that stops a first run — the wrong Node, two\n * lockfiles disagreeing, two collections claiming one slug, a `JWT_SECRET`\n * short enough to be guessed, a half-installed workspace with three versions of\n * `@rebasepro/server` in it — happens before a single table is compared, and\n * doctor said nothing about any of it.\n *\n * Every function here is pure: it takes what it read and returns findings. The\n * reading and the printing are at the edges, in `commands/doctor.ts`, so each\n * check has a fixture that fails it without a filesystem, a project or a\n * database.\n */\n\n/** One thing that is wrong, and what to do about it. */\nexport interface EnvironmentFinding {\n /** Which check produced it, for grouping in the report. */\n check: \"node\" | \"package-manager\" | \"collections\" | \"env\" | \"versions\" | \"atlas\";\n severity: \"error\" | \"warning\";\n message: string;\n fix: string;\n}\n\n// ── Atlas ────────────────────────────────────────────────────────────────\n\n/** What is on disk for `@ariga/atlas`, read by `commands/doctor.ts`. */\nexport interface AtlasBinaryState {\n /** A `node_modules/.bin/atlas` the project's commands would find. */\n onPath: boolean;\n /** `@ariga/atlas/package.json` resolves from the project. */\n packageInstalled: boolean;\n /** The binary the package's `bin` names exists as a file. */\n binaryOnDisk: boolean;\n /** For the remedy: the reader's own package manager. */\n manager: string;\n /**\n * The workspace member the install goes into (`backend` in a scaffold),\n * or absent at a project that is no workspace. A scaffold's root refuses\n * `pnpm add` without `-w`, and atlas belongs to the backend anyway.\n */\n workspaceDir?: string | null;\n}\n\n/** `@ariga/atlas` added as a dev dependency where the project keeps it. */\nfunction atlasAddCommand(manager: string, workspaceDir: string | null | undefined): string {\n const pkg = \"@ariga/atlas\";\n if (!workspaceDir) return `${manager} add -D ${pkg}`;\n switch (manager) {\n case \"npm\": return `npm install -D ${pkg} -w ${workspaceDir}`;\n case \"yarn\": return `yarn --cwd ${workspaceDir} add -D ${pkg}`;\n case \"bun\": return `bun add -d ${pkg} --cwd ${workspaceDir}`;\n default: return `${manager} --dir ${workspaceDir} add -D ${pkg}`;\n }\n}\n\n/**\n * Whether `db push`, `db generate` and `db migrate` can run at all.\n *\n * They shell out to `atlas`, and its absence is invisible until one of them is\n * run: `@ariga/atlas` downloads its platform binary in `preinstall`, pnpm 10+\n * and npm 12+ refuse a dependency's lifecycle scripts unless allowlisted, and\n * the install exits 0 either way. The scaffold allowlists it, so this is a\n * problem for the person who added `@rebasepro/server-postgres` to a project\n * they already had — five lines of `ERR_PNPM_IGNORED_BUILDS` and\n * `Failed to create bin … ENOENT`, several screens up, with nothing saying that\n * a schema push will fail later.\n *\n * Three states, because two of them need opposite advice. The package absent is\n * an install; the package present with no binary is an allowlist; the binary\n * present with no `.bin` shim is a re-install, and telling that reader their\n * build scripts are blocked sends them to a command that does nothing.\n */\nexport function checkAtlasBinary(state: AtlasBinaryState | null): EnvironmentFinding[] {\n if (!state || state.onPath) return [];\n\n if (!state.packageInstalled) {\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"The atlas binary is not on PATH and @ariga/atlas is not installed — \"\n + \"`db push`, `db generate` and `db migrate` will fail.\",\n fix: `Install it: ${atlasAddCommand(state.manager, state.workspaceDir)}, and allow its install script.`\n }];\n }\n\n if (!state.binaryOnDisk) {\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"@ariga/atlas is installed and its binary is missing — its `preinstall` \"\n + \"script was blocked, so `db push` and `db migrate` will fail.\",\n fix: \"Allow the build script (pnpm: `allowBuilds` in pnpm-workspace.yaml or \"\n + \"`pnpm approve-builds`; npm: `allowScripts` in package.json), then re-install.\"\n }];\n }\n\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"The atlas binary is on disk but node_modules/.bin/atlas is missing, \"\n + \"so nothing can invoke it.\",\n fix: \"Re-resolve the tree so the link is recreated: `pnpm install --force`, or delete \"\n + \"node_modules and install again.\"\n }];\n}\n\n// ── Node ─────────────────────────────────────────────────────────────────\n\n/**\n * `20.11.0` → `[20, 11, 0]`, ignoring anything after the patch.\n *\n * A bare major is a version: `>=20` is how every `engines.node` in this repo is\n * written, and requiring a minor here made the whole check return nothing —\n * silently, which is the only way a check like this ever fails.\n */\nfunction parseVersion(raw: string): [number, number, number] | null {\n const match = raw.trim().replace(/^v/, \"\").match(/^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/);\n if (!match) return null;\n return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)];\n}\n\n/** The lowest version a `>=x.y.z` range admits, or null for anything else. */\nexport function minimumFromRange(range: string): [number, number, number] | null {\n const match = range.match(/>=\\s*v?(\\d+(?:\\.\\d+)?(?:\\.\\d+)?)/);\n return match ? parseVersion(match[1]) : null;\n}\n\nfunction isBelow(actual: [number, number, number], minimum: [number, number, number]): boolean {\n for (let i = 0; i < 3; i++) {\n if (actual[i] < minimum[i]) return true;\n if (actual[i] > minimum[i]) return false;\n }\n return false;\n}\n\n/**\n * Is the running Node old enough to break things that will not say so?\n *\n * The symptoms are not \"unsupported Node\". They are a syntax error inside a\n * dependency, a missing global, or — the one that costs the most time — a\n * `node:` builtin that resolves and then behaves differently. Naming the\n * version is the whole check.\n */\nexport function checkNodeVersion(running: string, required: string | undefined): EnvironmentFinding[] {\n if (!required) return [];\n const minimum = minimumFromRange(required);\n const actual = parseVersion(running);\n if (!minimum || !actual) return [];\n if (!isBelow(actual, minimum)) return [];\n\n return [{\n check: \"node\",\n severity: \"error\",\n message: `Node ${running} is running, and Rebase needs ${required}.`,\n fix: `Install Node ${minimum[0]} or newer (nvm install ${minimum[0]}), then reinstall dependencies.`\n }];\n}\n\n// ── Package manager ──────────────────────────────────────────────────────\n\n/** The lockfiles a project may have, and the manager each one belongs to. */\nexport const LOCKFILES: Record<string, string> = {\n \"pnpm-lock.yaml\": \"pnpm\",\n \"package-lock.json\": \"npm\",\n \"yarn.lock\": \"yarn\",\n \"bun.lockb\": \"bun\"\n};\n\n/**\n * Two package managers in one project.\n *\n * A Rebase project is a pnpm workspace, and running `npm install` inside one\n * rewrites `node_modules` into a hoisted layout that pnpm then disagrees with.\n * The result is not an install error: it is `Cannot find module` from inside a\n * dependency, hours later, on a machine where it worked yesterday. The\n * leftover `package-lock.json` is the only durable evidence, and nothing looked\n * at it.\n */\nexport function checkPackageManager(\n lockfilesPresent: string[],\n declared: string | undefined\n): EnvironmentFinding[] {\n const known = lockfilesPresent.filter(file => file in LOCKFILES);\n if (known.length < 2) {\n // One lockfile, or none. A single one that disagrees with\n // `packageManager` is worth saying too.\n const declaredName = declared?.split(\"@\")[0];\n if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) {\n return [{\n check: \"package-manager\",\n severity: \"warning\",\n message: `This project declares packageManager \"${declared}\" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,\n fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`\n }];\n }\n return [];\n }\n\n const managers = known.map(file => LOCKFILES[file]);\n return [{\n check: \"package-manager\",\n severity: \"error\",\n message: `Two package managers have installed here: ${known.join(\" and \")}.`,\n fix: `Keep one. Delete the others and node_modules, then reinstall with ${declared?.split(\"@\")[0] ?? managers[0]}.`\n }];\n}\n\n// ── Collections ──────────────────────────────────────────────────────────\n\n/**\n * Two collections claiming one slug.\n *\n * The slug is the route, the table lookup and the registry key, and the\n * registry keeps the last one registered. So the loser is not reported missing:\n * it is served as the winner, with the winner's properties, under its own name.\n * Every symptom of this points somewhere else.\n */\nexport function checkDuplicateSlugs(\n collections: Array<{ slug?: string; name?: string }>\n): EnvironmentFinding[] {\n const byslug = new Map<string, number>();\n for (const collection of collections) {\n if (!collection.slug) continue;\n byslug.set(collection.slug, (byslug.get(collection.slug) ?? 0) + 1);\n }\n\n return [...byslug]\n .filter(([, count]) => count > 1)\n .map(([slug, count]) => ({\n check: \"collections\" as const,\n severity: \"error\" as const,\n message: `${count} collections declare the slug \"${slug}\". Only the last one registered is served.`,\n fix: `Give each collection its own slug. The one that loses is not reported missing — it answers as the winner.`\n }));\n}\n\n// ── .env ─────────────────────────────────────────────────────────────────\n\n/** What `loadEnv` requires of a JWT secret. Mirrors `rebaseEnvSchema`. */\nexport const MIN_JWT_SECRET_LENGTH = 32;\n\n/**\n * The two `.env` facts that fail late and confusingly.\n *\n * A short `JWT_SECRET` boots fine and is refused by `loadEnv` only in\n * production, which is the worst place to find out. Missing CORS in production\n * is the opposite shape — it does not fail at all; the API simply accepts\n * requests from anywhere.\n *\n * Values are never echoed. This reads a file that holds every credential the\n * project has, and doctor's output goes wherever a terminal goes.\n */\nexport function checkEnvSanity(values: Record<string, string>): EnvironmentFinding[] {\n const findings: EnvironmentFinding[] = [];\n const production = values.NODE_ENV === \"production\";\n\n const secret = values.JWT_SECRET;\n if (secret !== undefined && secret.length > 0 && secret.length < MIN_JWT_SECRET_LENGTH) {\n findings.push({\n check: \"env\",\n severity: production ? \"error\" : \"warning\",\n message: `JWT_SECRET is ${secret.length} characters. The runtime requires at least ${MIN_JWT_SECRET_LENGTH}, and refuses to boot below it in production.`,\n fix: \"Generate one: openssl rand -hex 32\"\n });\n }\n\n if (production && !values.CORS_ORIGINS && !values.FRONTEND_URL) {\n findings.push({\n check: \"env\",\n severity: \"error\",\n message: \"NODE_ENV=production with neither CORS_ORIGINS nor FRONTEND_URL set — the API will accept requests from any origin.\",\n fix: \"Set CORS_ORIGINS to the origins that may call this API.\"\n });\n }\n\n return findings;\n}\n\n/**\n * Parse a `.env` into a plain object.\n *\n * Deliberately not `dotenv`: this must not touch `process.env`, and doctor may\n * be reading a file for a different environment than the one it is running in.\n */\nexport function parseEnvFile(text: string): Record<string, string> {\n const values: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq <= 0) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n let value = trimmed.slice(eq + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n values[key] = value;\n }\n return values;\n}\n\n// ── Version skew ─────────────────────────────────────────────────────────\n\n/** One `@rebasepro/*` dependency, and where it was declared. */\nexport interface DeclaredDependency {\n /** Where it was declared, relative to the project root. */\n file: string;\n name: string;\n /** The range exactly as written. */\n range: string;\n}\n\n/**\n * The same `@rebasepro/*` package pinned to different versions in one project.\n *\n * A Rebase project declares them in several package.json files — the root, the\n * backend, the frontend, the config workspace — and nothing has ever compared\n * them. The failure that produces is not an install error: pnpm installs both,\n * `instanceof` stops working across the two copies, and the symptom is a\n * `CollectionConfig` that fails a type guard written against an identical type.\n *\n * `workspace:` ranges are excluded: inside this repo's own workspaces they all\n * resolve to the same build by construction.\n */\nexport function checkVersionSkew(declared: DeclaredDependency[]): EnvironmentFinding[] {\n const byPackage = new Map<string, Map<string, string[]>>();\n\n for (const dependency of declared) {\n if (!dependency.name.startsWith(\"@rebasepro/\")) continue;\n if (dependency.range.startsWith(\"workspace:\") || dependency.range.startsWith(\"link:\")) continue;\n if (dependency.range === \"*\" || dependency.range === \"latest\") continue;\n\n const ranges = byPackage.get(dependency.name) ?? new Map<string, string[]>();\n ranges.set(dependency.range, [...(ranges.get(dependency.range) ?? []), dependency.file]);\n byPackage.set(dependency.name, ranges);\n }\n\n const findings: EnvironmentFinding[] = [];\n for (const [name, ranges] of byPackage) {\n if (ranges.size < 2) continue;\n const described = [...ranges]\n .map(([range, files]) => `${range} (${files.join(\", \")})`)\n .join(\", \");\n findings.push({\n check: \"versions\",\n severity: \"error\",\n message: `${name} is pinned to different versions in this project: ${described}.`,\n 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.\"\n });\n }\n return findings;\n}\n","/**\n * CLI command: rebase doctor\n *\n * Detects three-way schema drift between collection definitions,\n * the generated Drizzle schema, and the live PostgreSQL database.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n getActiveBackendPlugin,\n resolvePluginCliScript,\n resolveTsx,\n findEnvFile,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { fileURLToPath, pathToFileURL } from \"url\";\nimport { createRequire } from \"module\";\nimport { scanTextForLibpqUrls, type LibpqUrlFinding } from \"../utils/libpq-url\";\nimport { analyseFunctionsDirectory, summarisePortability } from \"../function-portability\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\nimport { collectionsDirArgs, resolveCollectionsDir } from \"../utils/collections-dir\";\nimport { absolutizeLocalPathArgs, DATABASE_SOURCE_ENV } from \"./db\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { loadManifest, findBackendApp, resolveBackendPaths } from \"../manifest\";\nimport { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport {\n checkDuplicateSlugs,\n checkEnvSanity,\n checkAtlasBinary,\n checkNodeVersion,\n checkPackageManager,\n checkVersionSkew,\n LOCKFILES,\n parseEnvFile,\n type AtlasBinaryState,\n type DeclaredDependency,\n type EnvironmentFinding\n} from \"../doctor-environment\";\n\n/**\n * Files that can hold a connection string the project actually runs on.\n *\n * `.env` is what local commands read; the compose files are what a self-hosted\n * stack and its scheduled backup cron read. Both shipped with the defect, and a\n * deployed stack is broken by the compose one even when `.env` has been fixed.\n * `.env.example` is deliberately absent: nothing runs on it.\n */\nconst CONNECTION_FILES = [\n \".env\",\n \".env.local\",\n \"docker-compose.yml\",\n \"docker-compose.yaml\",\n \"docker-compose.custom.yml\"\n];\n\n/**\n * Find connection strings libpq cannot parse, anywhere in the project.\n *\n * Exported for the tests; `envFile` is passed separately because a project may\n * keep its `.env` outside the root (see `findEnvFile`).\n */\nexport function findLibpqUrlProblems(projectRoot: string, envFile?: string | null): LibpqUrlFinding[] {\n const candidates = CONNECTION_FILES.map((f) => path.join(projectRoot, f));\n if (envFile) candidates.push(envFile);\n\n const findings: LibpqUrlFinding[] = [];\n const seen = new Set<string>();\n\n for (const file of candidates) {\n if (seen.has(file)) continue;\n seen.add(file);\n let text: string;\n try {\n if (!fs.existsSync(file)) continue;\n text = fs.readFileSync(file, \"utf-8\");\n } catch {\n // Unreadable is not a finding — doctor reports what it can see.\n continue;\n }\n findings.push(...scanTextForLibpqUrls(path.relative(projectRoot, file) || path.basename(file), text));\n }\n\n return findings;\n}\n\n/**\n * Report unparseable connection strings, if any. Returns true when there were,\n * so the finding reaches the exit code like the environment errors do.\n *\n * Runs before the plugin's drift check and never blocks it. The plugin connects\n * through node-postgres, which parses these URLs happily — so it cannot see this\n * defect, and a project with it will otherwise get a clean bill of health while\n * `rebase db backup` fails.\n */\nfunction reportLibpqUrlProblems(findings: LibpqUrlFinding[]): boolean {\n if (findings.length === 0) return false;\n\n console.log(\"\");\n console.log(chalk.red.bold(\" ✗ Connection string that PostgreSQL's own tools cannot parse\"));\n console.log(\"\");\n for (const f of findings) {\n console.log(` ${chalk.bold(f.file)} → ${chalk.bold(f.variable)}`);\n console.log(chalk.gray(` the \"${f.params.join('\", \"')}\" parameter contains an unencoded \"=\".`));\n console.log(chalk.gray(\" Replace the value with:\"));\n console.log(` ${chalk.cyan(f.suggested)}`);\n console.log(\"\");\n }\n console.log(chalk.gray(\" libpq splits a query parameter on the first \\\"=\\\" and rejects any further\"));\n console.log(chalk.gray(\" one, so this fails:\"));\n console.log(chalk.gray(\" extra key/value separator \\\"=\\\" in URI query parameter\"));\n console.log(\"\");\n console.log(chalk.gray(\" Affects rebase db backup / restore, scheduled backups, and psql.\"));\n console.log(chalk.gray(\" NOT rebase dev or db push — those use a driver that accepts it, which\"));\n console.log(chalk.gray(\" is why a project can look healthy and still have no working backups.\"));\n console.log(chalk.gray(\" Projects scaffolded before 2026-08-18 all carry it.\"));\n console.log(\"\");\n return true;\n}\n\n/**\n * `--help` is answered before the project guard, not after.\n *\n * `doctor` declared no `--help` at all, so the flag fell through to the command\n * body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a\n * project answered \"✗ Could not find a Rebase project root.\" Asking a command\n * what it does is the one question that cannot require being somewhere\n * particular to ask.\n */\n/**\n * What each custom function needs from its host.\n *\n * Prints nothing when there is nothing to say — which is the common case, and\n * the reason it can afford to run every time. When it does print, the ordering\n * is deliberate: the one finding that is a bug *today* comes first, and the\n * rest is a single descriptive line about where these functions could run.\n */\nfunction reportFunctionPortability(projectRoot: string): void {\n let results: ReturnType<typeof analyseFunctionsDirectory>;\n try {\n const backend = findBackendApp(loadManifest(projectRoot).manifest);\n if (!backend) return;\n const paths = resolveBackendPaths(backend.app, projectRoot);\n results = analyseFunctionsDirectory(path.join(projectRoot, paths.functions), projectRoot);\n } catch {\n // A project with no manifest, or one shaped differently — doctor has\n // plenty else to report, and this section is advisory.\n return;\n }\n\n const lines = summarisePortability(results);\n if (lines.length === 0) return;\n\n console.log(chalk.bold(\"\\nCustom functions\"));\n for (const line of lines) {\n console.log(line.trimStart().startsWith(\"⚠\") ? chalk.yellow(line) : chalk.gray(line));\n }\n console.log(chalk.gray(\" See https://rebase.pro/docs/backend/custom-functions#runtime-portability\"));\n}\n\n/**\n * Read everything the environment checks need, then run them.\n *\n * The reading is here and the deciding is in `doctor-environment.ts`, so every\n * check has a fixture that fails it without a project on disk.\n */\nexport function collectEnvironmentFindings(\n projectRoot: string,\n envFile: string | null | undefined\n): EnvironmentFinding[] {\n const findings: EnvironmentFinding[] = [];\n\n // Node, against the engines range the installed CLI declares.\n findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));\n\n // Two package managers in one project.\n const rootPackage = readJson(path.join(projectRoot, \"package.json\"));\n findings.push(...checkPackageManager(\n Object.keys(LOCKFILES).filter(file => fs.existsSync(path.join(projectRoot, file))),\n typeof rootPackage?.packageManager === \"string\" ? rootPackage.packageManager : undefined\n ));\n\n // Two collections claiming one slug. Read as text rather than imported: a\n // collection file can import anything, and doctor must not execute a\n // project's code to tell it two slugs collide.\n findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));\n\n // `.env`, values never echoed.\n if (envFile && fs.existsSync(envFile)) {\n try {\n findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, \"utf-8\"))));\n } catch { /* unreadable is not a finding */ }\n }\n\n // One `@rebasepro/*` package, two versions.\n findings.push(...checkVersionSkew(readDeclaredRebaseDeps(projectRoot)));\n\n // The atlas binary, which every schema command shells out to and whose\n // absence is invisible until one of them is run.\n findings.push(...checkAtlasBinary(readAtlasBinaryState(projectRoot)));\n\n return findings;\n}\n\n/**\n * What is actually on disk for `@ariga/atlas`, from the project's point of view.\n *\n * Three separate questions, because the three states they distinguish need\n * three different remedies — see `checkAtlasBinary`. Directories are searched\n * from the project root upwards and through the workspaces a scaffold has,\n * because `db push` runs from `backend/` and the install may be hoisted to the\n * root.\n */\nexport function readAtlasBinaryState(projectRoot: string): AtlasBinaryState | null {\n const roots = [projectRoot, path.join(projectRoot, \"backend\"), path.join(projectRoot, \"config\")];\n\n const findUpwards = (starts: string[], relative: string): string | null => {\n for (const start of starts) {\n let dir = start;\n for (let i = 0; i < 8; i++) {\n const candidate = path.join(dir, \"node_modules\", relative);\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n }\n return null;\n };\n\n // `@ariga/atlas` is a dependency of `@rebasepro/server-postgres`, not of the\n // project, so under pnpm's isolated layout — the scaffold default — it lives\n // in `.pnpm/` and a directory walk over `node_modules/` never sees it.\n // Resolve through the module graph the way the driver itself does.\n const resolveFrom = (from: string, request: string): string | null => {\n try {\n return createRequire(pathToFileURL(from)).resolve(request);\n } catch {\n return null;\n }\n };\n const bases = roots.map(dir => path.join(dir, \"package.json\")).filter(file => fs.existsSync(file));\n const driverManifests = bases\n .map(base => resolveFrom(base, \"@rebasepro/server-postgres/package.json\"))\n .filter((file): file is string => file !== null);\n\n let manifest: string | null = findUpwards(roots, path.join(\"@ariga\", \"atlas\", \"package.json\"));\n if (!manifest) {\n for (const base of [...bases, ...driverManifests]) {\n manifest = resolveFrom(base, \"@ariga/atlas/package.json\");\n if (manifest) break;\n }\n }\n\n // The `.bin` shim is written beside whichever package declared the\n // dependency, so the driver's own directory is a search root too.\n const onPath = findUpwards(\n [...roots, ...driverManifests.map(file => path.dirname(file))],\n path.join(\".bin\", \"atlas\")\n ) !== null;\n\n let binaryOnDisk = false;\n if (manifest) {\n try {\n const pkg = JSON.parse(fs.readFileSync(manifest, \"utf-8\")) as {\n bin?: string | Record<string, string>;\n };\n const dir = path.dirname(manifest);\n const targets = typeof pkg.bin === \"string\"\n ? [pkg.bin]\n : Object.values(pkg.bin ?? {}).filter((t): t is string => typeof t === \"string\");\n binaryOnDisk = targets.some(target => fs.existsSync(path.resolve(dir, target)));\n } catch { /* unreadable manifest: treat the binary as absent */ }\n }\n\n return {\n onPath,\n packageInstalled: manifest !== null,\n binaryOnDisk,\n manager: readPackageManagerName(projectRoot),\n workspaceDir: fs.existsSync(path.join(projectRoot, \"backend\", \"package.json\")) && isWorkspaceRoot(projectRoot)\n ? \"backend\"\n : null\n };\n}\n\n/** Whether `dir` is a workspace root, where `pnpm add` refuses without `-w`. */\nfunction isWorkspaceRoot(dir: string): boolean {\n if (fs.existsSync(path.join(dir, \"pnpm-workspace.yaml\"))) return true;\n try {\n const manifest: unknown = JSON.parse(fs.readFileSync(path.join(dir, \"package.json\"), \"utf8\"));\n return typeof manifest === \"object\" && manifest !== null && \"workspaces\" in manifest;\n } catch {\n return false;\n }\n}\n\n/** The reader's own package manager, from the lockfile beside their project. */\nfunction readPackageManagerName(projectRoot: string): string {\n for (const [file, manager] of Object.entries(LOCKFILES)) {\n if (fs.existsSync(path.join(projectRoot, file))) return manager;\n }\n return \"pnpm\";\n}\n\n/** The `engines.node` range of the CLI actually running. */\nfunction readCliEngines(): string | undefined {\n try {\n const here = path.dirname(fileURLToPath(import.meta.url));\n for (let dir = here, i = 0; i < 5; i++) {\n const candidate = path.join(dir, \"package.json\");\n if (fs.existsSync(candidate)) {\n const parsed = readJson(candidate);\n if (parsed?.name === \"@rebasepro/cli\") {\n const engines = parsed.engines as { node?: string } | undefined;\n return engines?.node;\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n } catch { /* the check is advisory */ }\n return undefined;\n}\n\nfunction readJson(file: string): Record<string, unknown> | undefined {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf-8\")) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n\n/** Where a project's package.json files live, relative to its root. */\nconst PACKAGE_JSON_LOCATIONS = [\"package.json\", \"backend/package.json\", \"frontend/package.json\", \"config/package.json\"];\n\nfunction readDeclaredRebaseDeps(projectRoot: string): DeclaredDependency[] {\n const declared: DeclaredDependency[] = [];\n for (const relative of PACKAGE_JSON_LOCATIONS) {\n const parsed = readJson(path.join(projectRoot, relative));\n if (!parsed) continue;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"] as const) {\n const block = parsed[field] as Record<string, string> | undefined;\n if (!block) continue;\n for (const [name, range] of Object.entries(block)) {\n if (typeof range === \"string\") declared.push({ file: relative, name, range });\n }\n }\n }\n return declared;\n}\n\n/**\n * Does this project declare its collections in code?\n *\n * `resolveBackendPaths` answers it against the manifest's own `config` path\n * rather than the convention, because a project may move the directory. A\n * project with no manifest, or one shaped differently, is treated as declaring\n * them: the wrong answer there would silently skip the drift report, which is\n * the reason the command exists.\n */\nexport function hasDeclaredCollections(projectRoot: string): boolean {\n try {\n const backend = findBackendApp(loadManifest(projectRoot).manifest);\n if (!backend) return true;\n return resolveBackendPaths(backend.app, projectRoot).hasCollections;\n } catch {\n return true;\n }\n}\n\n/**\n * Every `slug:` a collection file declares, read as text.\n *\n * Deliberately not by importing them: a collection file may import anything the\n * project depends on, and doctor must not run a project's code to tell it two\n * slugs collide. A regex over `slug: \"...\"` finds the literal in every shape\n * the scaffold and the docs use; a computed slug is simply not seen, which is\n * the right failure for a check that only ever reports.\n */\nfunction readDeclaredSlugs(projectRoot: string): Array<{ slug: string }> {\n const dir = resolveCollectionsDir(projectRoot);\n if (!fs.existsSync(dir)) return [];\n const slugs: Array<{ slug: string }> = [];\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n return [];\n }\n for (const entry of entries) {\n if (!/\\.(ts|js|mts|mjs)$/.test(entry) || entry.startsWith(\"index.\")) continue;\n try {\n const text = fs.readFileSync(path.join(dir, entry), \"utf-8\");\n for (const match of text.matchAll(/\\bslug:\\s*[\"'`]([^\"'`]+)[\"'`]/g)) {\n slugs.push({ slug: match[1] });\n }\n } catch { /* unreadable is not a finding */ }\n }\n return slugs;\n}\n\n/** Print what the environment checks found, grouped, or nothing at all. */\nfunction reportEnvironmentFindings(findings: EnvironmentFinding[]): boolean {\n if (findings.length === 0) return false;\n\n console.log(\"\");\n console.log(chalk.bold(\" Environment\"));\n for (const finding of findings) {\n const mark = finding.severity === \"error\" ? chalk.red(\" ✗\") : chalk.yellow(\" ⚠\");\n console.log(`${mark} ${finding.message}`);\n console.log(chalk.gray(` ${finding.fix}`));\n }\n console.log(\"\");\n return findings.some(finding => finding.severity === \"error\");\n}\n\n/**\n * The flags `rebase doctor` takes, and what each is for.\n *\n * One table, read by both the parser and the help page, because they were two\n * things that had to agree and did not: the page listed no flags at all, so\n * `--policies` — the form `cli/index.md` calls \"the form to use as a CI gate\" —\n * was invisible to anyone who ran `rebase doctor --help` to find it. And with\n * nothing parsing the line here, a typo travelled to the driver's permissive\n * parser and was silently dropped, so `rebase doctor --polices` ran the ordinary\n * doctor and reported success on a gate that never ran.\n *\n * The spec mirrors the driver's own (`doctorPluginCommand` in\n * `server-postgres/src/cli.ts`), because the line is relayed to it verbatim.\n */\nexport const DOCTOR_FLAGS = {\n \"--policies\": Boolean,\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--schema\": String,\n \"-s\": \"--schema\",\n \"--sdk\": String,\n \"-k\": \"--sdk\"\n} as const;\n\n/** What each long flag above does, for the Options block. */\nconst DOCTOR_FLAG_HELP: Record<string, string> = {\n \"--policies\": \"Also audit the row-level security policies. The form to use as a CI gate.\",\n \"--collections\": COLLECTIONS_FLAG_HELP,\n \"--schema\": \"Path to the generated Drizzle schema (default: backend/src/schema.generated.ts)\",\n \"--sdk\": \"Path to the generated SDK types (default: generated/sdk/database.types.ts)\"\n};\n\n/**\n * The line handed to the driver, every path on it resolved from where the\n * command was run — the driver runs in `backend/`. See `utils/path-flags.ts`.\n */\nexport function doctorDriverArgs(rawArgs: string[], cwd: string, projectRoot?: string): string[] {\n const args = absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"doctor\"), cwd, [\"--collections\", \"-c\", \"--schema\", \"-s\", \"--sdk\", \"-k\"]);\n return projectRoot ? [...args, ...collectionsDirArgs(projectRoot, args)] : args;\n}\n\n/** `--collections, -c` — the aliases, resolved from the spec rather than retyped. */\nfunction doctorFlagLabel(flag: string): string {\n const aliases = Object.entries(DOCTOR_FLAGS)\n .filter(([, target]) => target === flag)\n .map(([alias]) => alias);\n return [flag, ...aliases].join(\", \");\n}\n\nfunction printDoctorHelp(): void {\n console.log(`\n${chalk.bold(\"rebase doctor\")} — Check a project for drift and misconfiguration\n\n${chalk.green.bold(\"Usage\")}\n rebase doctor [options]\n\n${chalk.green.bold(\"Options\")}\n${Object.keys(DOCTOR_FLAG_HELP)\n .map(flag => ` ${chalk.blue(doctorFlagLabel(flag).padEnd(22))} ${DOCTOR_FLAG_HELP[flag]}`)\n .join(\"\\n\")}\n\nCompares the collections you declare, the generated Drizzle schema, and the\ntables that actually exist, then reports what disagrees and how to reconcile it.\n\nAlso checks the connection strings in .env and the compose files for the\nunencoded \"=\" that makes PostgreSQL's own tools refuse to parse them — which\nbreaks backups and psql while leaving the app itself working.\n\nRun from inside a Rebase project — it reads the project's collections and\nconnects to its database.\n`);\n}\n\nexport async function doctorCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printDoctorHelp();\n return;\n }\n\n // Strictly, before the line is relayed. The driver's own parser is\n // `permissive`, which turns an undeclared flag into a positional rather\n // than an error, so `rebase doctor --polices` ran the ordinary doctor and\n // exited 0 — a CI gate that reported success because the flag asking for it\n // was dropped on the way.\n parseCommandArgs({\n spec: DOCTOR_FLAGS,\n rawArgs,\n commandWords: 1,\n command: \"doctor\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n const backendDir = requireBackendDir(projectRoot);\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir, \"doctor\");\n\n const activePlugin = getActiveBackendPlugin(backendDir);\n if (!activePlugin) {\n console.error(chalk.red(\"✗ Could not detect an active database plugin.\"));\n console.error(chalk.gray(\" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.\"));\n process.exit(1);\n }\n\n const pluginCli = resolvePluginCliScript(backendDir, activePlugin);\n if (!pluginCli) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // Set up environment with DOTENV_CONFIG_PATH\n const envFile = findEnvFile(projectRoot);\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? \"\";\n\n // Reported before the plugin runs: this one needs no database, and if the\n // URL is the problem then anything that tries to connect with it first will\n // fail with a worse message.\n const connectionFailed = reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));\n\n // Same reasoning: no database needed, and one of these findings — a\n // `process.env` read at module scope — is a live failure whose only symptom\n // today is a function quietly missing from `GET /api/functions`.\n reportFunctionPortability(projectRoot);\n\n // Everything that breaks a first run happens before a table is compared:\n // the wrong Node, two lockfiles disagreeing, two collections claiming one\n // slug, a JWT_SECRET short enough to be refused in production, three\n // versions of @rebasepro/server in one workspace. Doctor said nothing about\n // any of it. Reported before the plugin runs, and non-blocking — a project\n // with one of these still deserves its drift report.\n const environmentFailed = reportEnvironmentFindings(collectEnvironmentFindings(projectRoot, envFile));\n // Either is an error a CI gate must see: a connection string libpq cannot\n // parse is a project whose backups have never run.\n const failed = connectionFailed || environmentFailed;\n\n // Which database, resolved the way every other command resolves it.\n //\n // Two of doctor's three phases need one, and both used to key on\n // `DATABASE_URL` alone. On the documented first-run path that variable does\n // not exist — `rebase init` leaves it commented out and the managed\n // database fills the vacuum — so a stock scaffold always ended with\n // `⏭ Collections → Database: skipped (DATABASE_URL not set)` and\n // `⚠ No DATABASE_URL — RLS policies were NOT checked`, then told the reader\n // to set the one variable the quickstart tells them to leave alone. For the\n // command `ai-instructions.md` names as the thing to run before guessing.\n //\n // The managed daemon is started here, exactly as `rebase db url` starts it:\n // a doctor that will not start the database it is asked about can only ever\n // answer \"I did not look\".\n //\n // Last, and never fatal. Everything above needs no database, and a doctor\n // that cannot report what it *can* see because a database would not start\n // is the opposite of the job. A failure here is said out loud and the run\n // continues; the driver then reports its two database phases as skipped,\n // which is the truth.\n try {\n const prepared = await prepareDatabaseEnv(projectRoot, {\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n Object.assign(env, prepared.env);\n // What named the database, as the `db` commands pass it: the driver's\n // \"Cannot connect\" banner picks its fixes by it. Without it, the\n // remembered Docker database, stopped, got \"Verify DATABASE_URL in\n // your .env file\" — a file that had named nothing.\n env[DATABASE_SOURCE_ENV] = prepared.database.source;\n for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));\n } catch (error) {\n console.log(\"\");\n console.error(chalk.yellow(` ⚠ Could not resolve this project's database: ${\n error instanceof Error ? error.message : String(error)}`));\n console.error(chalk.gray(\" The checks that need one are reported below as not run.\"));\n }\n\n // A headless project has no `config/collections` by design — its API is\n // introspected from the live database at boot — and the driver's doctor\n // opens by loading them, finds none, and exits 1. So `rebase doctor` failed\n // on the documented headless path, in 0.17.3 and on main, while every check\n // above it had just passed. Its three phases all compare *against* declared\n // collections, so there is nothing for them to do here; the environment\n // findings are the whole report.\n if (!hasDeclaredCollections(projectRoot)) {\n console.log(\"\");\n console.log(chalk.gray(\n \" ○ Schema drift not checked: this project derives its API from the database — \"\n + \"run `rebase schema introspect` first.\"\n ));\n if (failed) process.exit(1);\n return;\n }\n\n try {\n const isTs = pluginCli.endsWith(\".ts\");\n if (isTs) {\n const tsxBin = resolveTsx(projectRoot);\n if (!tsxBin) {\n exitDependenciesNotInstalled(projectRoot);\n }\n await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd(), projectRoot)], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n } else {\n await execa(\"node\", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd(), projectRoot)], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n }\n } catch (error) {\n // A child that ran and exited non-zero already printed its diagnostics\n // through inherited stdio. A child that never started did not — and\n // \"the tsx symlink is broken\" is exactly the state `doctor` exists to\n // report, so exiting 1 in silence was the worst possible answer.\n reportSpawnFailure(error);\n process.exit(1);\n }\n\n // The plugin exited clean, so the schema agrees with itself. That is not\n // the same as the project being able to run: an environment error is still\n // an error, and a doctor that exits 0 over one is a doctor nobody can gate\n // on.\n if (failed) process.exit(1);\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
- import { p as requireProjectRoot } from "./project-Wp2zoyeN.js";
2
- import { n as parseCommandArgs, r as wantsHelp } from "./args-BX0YD14a.js";
3
- import { d as findBackendApp, g as resolveBackendPaths, o as ManifestError, p as loadManifest, x as writeManifest } from "./manifest-DRp0UVTj.js";
1
+ import { p as requireProjectRoot, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
2
+ import { d as findBackendApp, g as resolveBackendPaths, o as ManifestError, p as loadManifest, x as writeManifest } from "./manifest-BtzDy7KQ.js";
4
3
  import chalk from "chalk";
5
4
  import fs from "fs";
6
5
  import path from "path";
@@ -332,4 +331,4 @@ function restoreBackendScripts(projectRoot) {
332
331
  //#endregion
333
332
  export { ejectCommand };
334
333
 
335
- //# sourceMappingURL=eject-DuasTr-F.js.map
334
+ //# sourceMappingURL=eject-DABcDnPN.js.map