@rebasepro/cli 0.24.0 → 0.24.1-canary.g56af4a4

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 (118) hide show
  1. package/bin/rebase.js +3 -0
  2. package/dist/agent-setup-B31F00DC.js +324 -0
  3. package/dist/agent-setup-B31F00DC.js.map +1 -0
  4. package/dist/api-keys-BPBi5nOF.js +524 -0
  5. package/dist/api-keys-BPBi5nOF.js.map +1 -0
  6. package/dist/apps-CPXNElX4.js +256 -0
  7. package/dist/apps-CPXNElX4.js.map +1 -0
  8. package/dist/args-BnmsEXvu.js +109 -0
  9. package/dist/args-BnmsEXvu.js.map +1 -0
  10. package/dist/atlas-dev-database-DTMuEScS.js +62 -0
  11. package/dist/atlas-dev-database-DTMuEScS.js.map +1 -0
  12. package/dist/auth-DvY94ZuL.js +395 -0
  13. package/dist/auth-DvY94ZuL.js.map +1 -0
  14. package/dist/branch-pointer-DIhKbQLp.js +2 -0
  15. package/dist/build-CFwUx-Ff.js +336 -0
  16. package/dist/build-CFwUx-Ff.js.map +1 -0
  17. package/dist/bundle-BQhP_xiA.js +1617 -0
  18. package/dist/bundle-BQhP_xiA.js.map +1 -0
  19. package/dist/cli.d.ts +2 -2
  20. package/dist/cloud-CkTr-yt0.js +9640 -0
  21. package/dist/cloud-CkTr-yt0.js.map +1 -0
  22. package/dist/collection-drift-BKj_BoMq.js +106 -0
  23. package/dist/collection-drift-BKj_BoMq.js.map +1 -0
  24. package/dist/commands/api-keys.d.ts +2 -0
  25. package/dist/commands/db.d.ts +37 -24
  26. package/dist/commands/dev.d.ts +103 -0
  27. package/dist/commands/generate_sdk.d.ts +26 -0
  28. package/dist/commands/skills.d.ts +22 -7
  29. package/dist/commands/storage.d.ts +46 -0
  30. package/dist/consent-BWHaYx5p.js +76 -0
  31. package/dist/consent-BWHaYx5p.js.map +1 -0
  32. package/dist/context-BqhRydBg.js +1234 -0
  33. package/dist/context-BqhRydBg.js.map +1 -0
  34. package/dist/daemon-DBn-shxX.js +2 -0
  35. package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
  36. package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
  37. package/dist/db-CMpBK3sv.js +1252 -0
  38. package/dist/db-CMpBK3sv.js.map +1 -0
  39. package/dist/derive-RPIsAVO4.js +474 -0
  40. package/dist/derive-RPIsAVO4.js.map +1 -0
  41. package/dist/dev-C9P5Yp9k.js +1593 -0
  42. package/dist/dev-C9P5Yp9k.js.map +1 -0
  43. package/dist/dev-db/atlas-dev-database.d.ts +32 -0
  44. package/dist/dev-db/daemon-entry.d.ts +13 -0
  45. package/dist/dev-preflight-N1evJL3S.js +2 -0
  46. package/dist/doctor-DYs-NqFI.js +771 -0
  47. package/dist/doctor-DYs-NqFI.js.map +1 -0
  48. package/dist/eject-BT6t37D0.js +335 -0
  49. package/dist/eject-BT6t37D0.js.map +1 -0
  50. package/dist/fold-static-C1EtHlfV.js +201 -0
  51. package/dist/fold-static-C1EtHlfV.js.map +1 -0
  52. package/dist/function-portability-BIvlvR9q.js +265 -0
  53. package/dist/function-portability-BIvlvR9q.js.map +1 -0
  54. package/dist/generate_sdk-UV635zSe.js +477 -0
  55. package/dist/generate_sdk-UV635zSe.js.map +1 -0
  56. package/dist/index.es.js +141 -24674
  57. package/dist/index.es.js.map +1 -1
  58. package/dist/init-fXeBcxuN.js +1042 -0
  59. package/dist/init-fXeBcxuN.js.map +1 -0
  60. package/dist/manifest-DzlC60WX.js +871 -0
  61. package/dist/manifest-DzlC60WX.js.map +1 -0
  62. package/dist/normalize-imports-CmVdE9iZ.js +100 -0
  63. package/dist/normalize-imports-CmVdE9iZ.js.map +1 -0
  64. package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
  65. package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
  66. package/dist/path-flags-DUBuI2i1.js +77 -0
  67. package/dist/path-flags-DUBuI2i1.js.map +1 -0
  68. package/dist/precompress.d.ts +17 -0
  69. package/dist/prepare-Btn9Ewbn.js +1052 -0
  70. package/dist/prepare-Btn9Ewbn.js.map +1 -0
  71. package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
  72. package/dist/project-Wp2zoyeN.js.map +1 -0
  73. package/dist/resolve-CJLkfFV0.js +2 -0
  74. package/dist/resources-DT1COn_6.js +161 -0
  75. package/dist/resources-DT1COn_6.js.map +1 -0
  76. package/dist/schema-BDBWvCHu.js +125 -0
  77. package/dist/schema-BDBWvCHu.js.map +1 -0
  78. package/dist/skills-DTFibT1m.js +642 -0
  79. package/dist/skills-DTFibT1m.js.map +1 -0
  80. package/dist/start-rU2e3Kp5.js +228 -0
  81. package/dist/start-rU2e3Kp5.js.map +1 -0
  82. package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
  83. package/dist/state-Do7ZJYkF.js.map +1 -0
  84. package/dist/status-BR9oyi-y.js +351 -0
  85. package/dist/status-BR9oyi-y.js.map +1 -0
  86. package/dist/storage-DyvrSTH3.js +303 -0
  87. package/dist/storage-DyvrSTH3.js.map +1 -0
  88. package/dist/telemetry-CAWNCugs.js +338 -0
  89. package/dist/telemetry-CAWNCugs.js.map +1 -0
  90. package/dist/telemetry-DbLqGIBW.js +129 -0
  91. package/dist/telemetry-DbLqGIBW.js.map +1 -0
  92. package/dist/unknown-command-DNwDcX3w.js +98 -0
  93. package/dist/unknown-command-DNwDcX3w.js.map +1 -0
  94. package/dist/upgrade-D-5-Od96.js +1265 -0
  95. package/dist/upgrade-D-5-Od96.js.map +1 -0
  96. package/dist/utils/project.d.ts +16 -0
  97. package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
  98. package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
  99. package/package.json +7 -7
  100. package/templates/eject/backend/src/env.ts +1 -1
  101. package/templates/eject/backend/src/index.ts +10 -2
  102. package/templates/template/README.md +1 -1
  103. package/templates/template/ai-instructions.md +2 -1
  104. package/templates/template/frontend/vite.config.ts +11 -3
  105. package/templates/template/scripts/example.ts +2 -1
  106. package/dist/branch-pointer-kk9F5FBO.js +0 -111
  107. package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
  108. package/dist/constraints-DKGbNfUW.js +0 -48
  109. package/dist/constraints-DKGbNfUW.js.map +0 -1
  110. package/dist/daemon-C8p32i-n.js +0 -357
  111. package/dist/daemon-C8p32i-n.js.map +0 -1
  112. package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
  113. package/dist/dev-preflight-CNLl4rdo.js +0 -367
  114. package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
  115. package/dist/project-DmaGLmH2.js.map +0 -1
  116. package/dist/resolve-BAeRUBhg.js +0 -74
  117. package/dist/resolve-BAeRUBhg.js.map +0 -1
  118. package/dist/state-C59Elrnt.js.map +0 -1
@@ -1,367 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import chalk from "chalk";
3
- import path from "path";
4
- import fs from "fs";
5
- import net from "net";
6
- import { execa } from "execa";
7
- //#region src/utils/dev-preflight.ts
8
- /**
9
- * Make `rebase dev` the only command a new project needs.
10
- *
11
- * The scaffolded project prints three steps — start the database container,
12
- * push the schema, start the dev server — and a first-time reader has to
13
- * perform all three correctly before anything works. Two of them are
14
- * mechanical, and both fail in ways that do not name themselves: skipping the
15
- * container gives `ECONNREFUSED` from a stack the reader has never seen, and
16
- * skipping the push gives a server that starts cleanly and then 500s on the
17
- * first request because the tables are not there.
18
- *
19
- * So `dev` does them. Not always, and not silently — under four conditions that
20
- * together mean "this is a throwaway development database on this laptop, and
21
- * it is not running":
22
- *
23
- * 1. The DSN resolves to a loopback address. A remote database is somebody's
24
- * real data and is never touched here, whatever else is true.
25
- * 2. Nothing is listening on it. A database that answers is left completely
26
- * alone — no push, no inspection, no connection.
27
- * 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.
28
- * the one `rebase init` wrote. Without it there is nothing to start and
29
- * the backend's own error is the better message.
30
- * 4. Docker is available. If it is not, the manual steps are printed and dev
31
- * continues — a preflight must never be the reason the command fails.
32
- *
33
- * The schema push happens only when this function actually started the
34
- * container, which is the one moment a local database is known to be new or
35
- * stale relative to the collections. A `dev` against an already-running
36
- * database never mutates its schema.
37
- */
38
- var dev_preflight_exports = /* @__PURE__ */ __exportAll({
39
- composeDatabaseUrl: () => composeDatabaseUrl,
40
- composeDeclaresDbService: () => composeDeclaresDbService,
41
- composeHostPort: () => composeHostPort,
42
- ensureDevDatabase: () => ensureDevDatabase,
43
- parseLoopbackDsn: () => parseLoopbackDsn,
44
- probeTcp: () => probeTcp,
45
- waitForPort: () => waitForPort
46
- });
47
- /**
48
- * Hosts that mean "this machine", and so may be provisioned automatically.
49
- *
50
- * An exact-match set, never a prefix or substring test: `localhost.example.com`
51
- * is a perfectly resolvable name that somebody else controls.
52
- */
53
- var LOOPBACK = /* @__PURE__ */ new Set([
54
- "localhost",
55
- "127.0.0.1",
56
- "::1",
57
- "0.0.0.0"
58
- ]);
59
- /**
60
- * The host and port of a Postgres DSN, but only when it points at this machine.
61
- *
62
- * Returns null for anything else — a remote host, an unparseable string, a
63
- * non-postgres scheme. Callers treat null as "do nothing", so every parse
64
- * failure fails closed onto the safe behaviour rather than onto a guess.
65
- */
66
- function parseLoopbackDsn(dsn) {
67
- if (!dsn) return null;
68
- let url;
69
- try {
70
- url = new URL(dsn);
71
- } catch {
72
- return null;
73
- }
74
- if (!/^postgres(ql)?:$/.test(url.protocol)) return null;
75
- const host = url.hostname.replace(/^\[(.+)\]$/, "$1");
76
- if (!LOOPBACK.has(host)) return null;
77
- const port = url.port ? Number(url.port) : 5432;
78
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
79
- return {
80
- host,
81
- port
82
- };
83
- }
84
- /**
85
- * Whether a compose file declares a `db` service.
86
- *
87
- * Deliberately a text scan and not a YAML parse: the CLI has no YAML
88
- * dependency, and the question is only ever asked of a file this tool wrote.
89
- * A false negative costs the automation and nothing else — the reader gets the
90
- * manual steps, which are correct.
91
- */
92
- function composeDeclaresDbService(yamlText) {
93
- let inServices = false;
94
- for (const rawLine of yamlText.split(/\r?\n/)) {
95
- const line = rawLine.replace(/\t/g, " ");
96
- if (/^\s*#/.test(line) || line.trim() === "") continue;
97
- if (/^services:\s*$/.test(line)) {
98
- inServices = true;
99
- continue;
100
- }
101
- if (inServices && /^\S/.test(line)) {
102
- inServices = false;
103
- continue;
104
- }
105
- if (inServices && /^\s{2,}db:\s*$/.test(line)) return true;
106
- }
107
- return false;
108
- }
109
- /**
110
- * The connection string the compose file's `db` service will answer on.
111
- *
112
- * `--docker` used to be a flag that changed a banner and nothing else: it
113
- * resolved to `kind: "docker"`, and every caller then had no URL to reach the
114
- * container with, so the backend booted with no `DATABASE_URL` at all and died
115
- * on the message a project with no database gets. The container it had just
116
- * been asked for was never started, because the preflight that starts it needs
117
- * a DSN to decide the database is local and not running.
118
- *
119
- * So the URL is derived rather than required: the compose file names the user,
120
- * the database and the published host port, and `.env` holds the password
121
- * compose itself interpolates. That is the same string `rebase init` writes as
122
- * the commented-out `DATABASE_URL`, which is what makes uncommenting that line
123
- * and passing `--docker` reach the same database.
124
- *
125
- * Returns null when the file declares no `db` service, or declares one this
126
- * scan cannot read — a `--docker` that cannot be honoured has to say so, and
127
- * a guess would point at somebody else's Postgres on 5432.
128
- *
129
- * Same text scan as {@link composeDeclaresDbService}, for the same reason.
130
- */
131
- function composeDatabaseUrl(yamlText, env = {}) {
132
- const found = scanComposeDbService(yamlText);
133
- const user = expandComposeValue(found.POSTGRES_USER, env);
134
- const password = expandComposeValue(found.POSTGRES_PASSWORD, env);
135
- const database = expandComposeValue(found.POSTGRES_DB, env);
136
- if (!user || !password || !database || !found.hostPort) return null;
137
- return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;
138
- }
139
- /**
140
- * The host port the compose `db` service publishes, or null.
141
- *
142
- * Separate from {@link composeDatabaseUrl} because the port is answerable on
143
- * its own: the URL needs the user, the password and the database name too, and
144
- * a compose file missing any of them still says which port a container would
145
- * listen on. That is the question "would starting this container answer the
146
- * DSN in `.env`?" — see the `wrong-port` branch of {@link ensureDevDatabase},
147
- * where getting it wrong meant starting a container nobody asked for.
148
- */
149
- function composeHostPort(yamlText) {
150
- const port = scanComposeDbService(yamlText).hostPort;
151
- return port ? Number(port) : null;
152
- }
153
- /** The raw values of the compose `db` service, before any expansion. */
154
- function scanComposeDbService(yamlText) {
155
- let inServices = false;
156
- let inDb = false;
157
- let dbIndent = 0;
158
- let section = null;
159
- const found = {};
160
- let hostPort = null;
161
- for (const rawLine of yamlText.split(/\r?\n/)) {
162
- const line = rawLine.replace(/\t/g, " ");
163
- if (/^\s*#/.test(line) || line.trim() === "") continue;
164
- if (/^services:\s*$/.test(line)) {
165
- inServices = true;
166
- continue;
167
- }
168
- if (inServices && /^\S/.test(line)) {
169
- inServices = false;
170
- inDb = false;
171
- continue;
172
- }
173
- if (!inServices) continue;
174
- const indent = line.length - line.trimStart().length;
175
- const dbHeader = line.match(/^(\s{2,})db:\s*$/);
176
- if (dbHeader) {
177
- inDb = true;
178
- dbIndent = dbHeader[1].length;
179
- section = null;
180
- continue;
181
- }
182
- if (inDb && indent <= dbIndent) {
183
- inDb = false;
184
- section = null;
185
- continue;
186
- }
187
- if (!inDb) continue;
188
- const key = line.match(/^\s+(environment|ports):\s*$/);
189
- if (key) {
190
- section = key[1];
191
- continue;
192
- }
193
- if (/^\s+[A-Za-z_][\w-]*:\s*\S/.test(line) && !/^\s+POSTGRES_/.test(line)) {
194
- if (!/^\s*-\s/.test(line.trimStart())) section = null;
195
- }
196
- if (section === "environment") {
197
- const pair = line.match(/^\s+(POSTGRES_(?:USER|PASSWORD|DB)):\s*(.+?)\s*$/);
198
- if (pair) found[pair[1]] = pair[2].replace(/^["']|["']$/g, "");
199
- continue;
200
- }
201
- if (section === "ports" && hostPort === null) {
202
- const mapping = line.match(/^\s*-\s*["']?(?:[\d.]+:)?(\d+):5432(?:\/tcp)?["']?\s*$/);
203
- if (mapping) hostPort = mapping[1];
204
- }
205
- }
206
- return hostPort === null ? found : {
207
- ...found,
208
- hostPort
209
- };
210
- }
211
- /**
212
- * `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves
213
- * them — against the project's `.env`, which is the file compose itself reads.
214
- */
215
- function expandComposeValue(raw, env) {
216
- if (!raw) return null;
217
- const trimmed = raw.replace(/\$\{([A-Za-z_][\w]*)(?::?-([^}]*))?\}/g, (_all, name, fallback) => {
218
- const value = env[name];
219
- if (value !== void 0 && value !== "") return value;
220
- return fallback ?? "";
221
- }).trim();
222
- return trimmed.length > 0 ? trimmed : null;
223
- }
224
- /** Is something accepting connections there right now? */
225
- function probeTcp(host, port, timeoutMs = 700) {
226
- return new Promise((resolve) => {
227
- const socket = new net.Socket();
228
- let settled = false;
229
- const done = (open) => {
230
- if (settled) return;
231
- settled = true;
232
- socket.destroy();
233
- resolve(open);
234
- };
235
- socket.setTimeout(timeoutMs);
236
- socket.once("connect", () => done(true));
237
- socket.once("timeout", () => done(false));
238
- socket.once("error", () => done(false));
239
- socket.connect(port, host);
240
- });
241
- }
242
- /**
243
- * Poll until the port answers, or give up.
244
- *
245
- * Postgres publishes its port before it is ready to accept queries, so the
246
- * caller still has to tolerate a first connection that is refused at the
247
- * protocol level. This only answers "has the container got as far as listening",
248
- * which is the part that takes the seconds.
249
- */
250
- async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
251
- const deadline = Date.now() + timeoutMs;
252
- while (Date.now() < deadline) {
253
- if (await probeTcp(host, port, 700)) return true;
254
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
255
- }
256
- return false;
257
- }
258
- /**
259
- * Start the project's development database if it is not running, and give it a
260
- * schema if this call is what started it.
261
- */
262
- async function ensureDevDatabase(options) {
263
- const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;
264
- const log = options.log ?? ((message) => console.log(message));
265
- if (disabled) return { action: "disabled" };
266
- const target = parseLoopbackDsn(databaseUrl);
267
- if (!databaseUrl) return { action: "no-dsn" };
268
- if (!target) {
269
- let host = "the configured host";
270
- try {
271
- host = new URL(databaseUrl).hostname || host;
272
- } catch {}
273
- return {
274
- action: "remote-dsn",
275
- host
276
- };
277
- }
278
- if (await probeTcp(target.host, target.port)) return {
279
- action: "already-running",
280
- host: target.host,
281
- port: target.port
282
- };
283
- const composePath = path.join(projectRoot, "docker-compose.yml");
284
- if (!fs.existsSync(composePath)) return { action: "no-compose" };
285
- const composeText = fs.readFileSync(composePath, "utf8");
286
- if (!composeDeclaresDbService(composeText)) return { action: "no-compose" };
287
- const manualSteps = `${chalk.cyan("docker compose up -d db")} then ${chalk.cyan("rebase db push")}`;
288
- const composePort = composeHostPort(composeText);
289
- if (composePort !== null && composePort !== target.port) {
290
- const hint = `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. The compose file's db service publishes ${composePort}, so starting it would not answer that URL — nothing was started. Either correct the port in .env, or point DATABASE_URL at 127.0.0.1:${composePort}.`;
291
- log("");
292
- log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
293
- return {
294
- action: "wrong-port",
295
- hint,
296
- port: target.port,
297
- composePort
298
- };
299
- }
300
- log("");
301
- log(` ${chalk.gray("Database not running — starting it…")}`);
302
- const startDatabase = options.startDatabase ?? (async (root) => {
303
- await execa("docker", [
304
- "compose",
305
- "up",
306
- "-d",
307
- "db"
308
- ], {
309
- cwd: root,
310
- stdio: "pipe"
311
- });
312
- });
313
- try {
314
- await startDatabase(projectRoot);
315
- } catch (error) {
316
- const message = error instanceof Error ? error.message : String(error);
317
- if (/ENOENT|not found|command not found/i.test(message)) {
318
- const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;
319
- log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
320
- return {
321
- action: "no-docker",
322
- hint
323
- };
324
- }
325
- const hint = `Could not start the database container. Run ${manualSteps} to see why.`;
326
- log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
327
- return {
328
- action: "start-failed",
329
- hint
330
- };
331
- }
332
- if (!await waitForPort(target.host, target.port)) {
333
- const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;
334
- log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
335
- return {
336
- action: "start-failed",
337
- hint
338
- };
339
- }
340
- log(` ${chalk.green("✓")} ${chalk.gray(`Database ready on port ${target.port}`)}`);
341
- if (!hasCollections) return {
342
- action: "started",
343
- port: target.port,
344
- pushed: false
345
- };
346
- log(` ${chalk.gray("Pushing the schema…")}`);
347
- try {
348
- await pushSchema();
349
- log(` ${chalk.green("✓")} ${chalk.gray("Schema pushed")}`);
350
- return {
351
- action: "started",
352
- port: target.port,
353
- pushed: true
354
- };
355
- } catch {
356
- log(` ${chalk.yellow("⚠")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan("rebase db push")}.`)}`);
357
- return {
358
- action: "started",
359
- port: target.port,
360
- pushed: false
361
- };
362
- }
363
- }
364
- //#endregion
365
- export { probeTcp as i, dev_preflight_exports as n, ensureDevDatabase as r, composeDatabaseUrl as t };
366
-
367
- //# sourceMappingURL=dev-preflight-CNLl4rdo.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dev-preflight-CNLl4rdo.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 */\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { execa } from \"execa\";\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/** Is something accepting connections there right now? */\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\", () => done(false));\n socket.connect(port, host);\n });\n}\n\n/**\n * Poll until the port answers, or give up.\n *\n * Postgres publishes its port before it is ready to accept queries, so the\n * caller still has to tolerate a first connection that is refused at the\n * protocol level. This only answers \"has the container got as far as listening\",\n * which is the part that takes the seconds.\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\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 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 if (await probeTcp(target.host, target.port)) {\n return { action: \"already-running\", host: target.host, port: target.port };\n }\n\n const composePath = path.join(projectRoot, \"docker-compose.yml\");\n if (!fs.existsSync(composePath)) return { action: \"no-compose\" };\n const composeText = fs.readFileSync(composePath, \"utf8\");\n if (!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 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 {\n // The push prints its own diagnostics through inherited stdio, so\n // repeating them here would only bury them. What is added is the one\n // thing the reader now needs: the server is still starting, and against\n // what.\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 +0,0 @@
1
- {"version":3,"file":"project-DmaGLmH2.js","names":[],"sources":["../src/utils/project.ts"],"sourcesContent":["/**\n * Project discovery utilities for the Rebase CLI.\n *\n * These helpers locate the project root, backend directory, .env file,\n * and local binaries — used by all CLI command modules.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execSync } from \"child_process\";\nimport dotenv from \"dotenv\";\nimport chalk from \"chalk\";\nimport { detectPackageManager, getPMCommands } from \"./package-manager\";\n\n/** The authored project manifest. Its presence alone marks a project root. */\nexport const MANIFEST_FILENAME = \"rebase.json\";\n\n/**\n * Walk up from `startDir` to find the Rebase project root.\n *\n * A directory is the root when it holds a `rebase.json`, or when it holds a\n * `package.json` that either lists `backend` as a workspace or sits beside both\n * `backend/` and `config/`.\n *\n * `rebase.json` is checked first and needs no `package.json` beside it, because\n * the conventions below all describe a repository that *contains the backend*.\n * A repository holding only a frontend — the normal shape once a project's apps\n * live in separate repositories — matches none of them, so without this the\n * tooling could not run there at all.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): string | null {\n let dir = path.resolve(startDir);\n const root = path.parse(dir).root;\n\n while (dir !== root) {\n if (fs.existsSync(path.join(dir, MANIFEST_FILENAME))) {\n return dir;\n }\n\n const pkgPath = path.join(dir, \"package.json\");\n\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n // Check for workspace-based project (monorepo root)\n if (pkg.workspaces && Array.isArray(pkg.workspaces)) {\n const hasBackend = pkg.workspaces.some((w: string) =>\n w === \"backend\"\n );\n if (hasBackend) return dir;\n }\n } catch {\n // ignore parse errors\n }\n\n // Check for sibling backend directory\n if (fs.existsSync(path.join(dir, \"backend\")) && fs.existsSync(path.join(dir, \"config\"))) {\n return dir;\n }\n }\n\n dir = path.dirname(dir);\n }\n\n return null;\n}\n\n/**\n * Locate the backend directory within the project root.\n */\nexport function findBackendDir(projectRoot: string): string | null {\n const backendDir = path.join(projectRoot, \"backend\");\n return fs.existsSync(backendDir) ? backendDir : null;\n}\n\n/**\n * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.\n */\nexport function getActiveBackendPlugin(backendDir: string): string | null {\n const pkgPath = path.join(backendDir, \"package.json\");\n if (!fs.existsSync(pkgPath)) return null;\n\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n const deps = { ...pkg.dependencies,\n...pkg.devDependencies };\n\n // Collect all @rebasepro/server-* driver plugins (exclude server itself)\n const candidates = Object.keys(deps).filter(\n dep => dep.startsWith(\"@rebasepro/server-\") && dep !== \"@rebasepro/server\"\n );\n\n if (candidates.length === 0) return null;\n\n // Prefer server-postgres — it's the primary supported driver\n if (candidates.includes(\"@rebasepro/server-postgres\")) {\n return \"@rebasepro/server-postgres\";\n }\n\n // Fallback: return the first candidate that actually has a CLI entry point\n for (const candidate of candidates) {\n if (resolvePluginCliScript(backendDir, candidate)) {\n return candidate;\n }\n }\n\n // Last resort: return whatever we found\n return candidates[0];\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\n/**\n * Resolve the active plugin's CLI script.\n */\nexport function resolvePluginCliScript(backendDir: string, pluginName: string): string | null {\n const candidates: string[] = [];\n\n // Walk up from the backend dir: pnpm links the plugin into\n // backend/node_modules, while npm workspaces hoist it to the project (or an\n // enclosing monorepo) root.\n let dir = path.resolve(backendDir);\n const fsRoot = path.parse(dir).root;\n while (dir !== fsRoot) {\n candidates.push(\n path.join(dir, \"node_modules\", pluginName, \"src\", \"cli.ts\"),\n path.join(dir, \"node_modules\", pluginName, \"dist\", \"cli.js\")\n );\n dir = path.dirname(dir);\n }\n\n candidates.push(\n // For monorepo dev mode:\n path.resolve(backendDir, \"..\", \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\")\n );\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * Locate the frontend directory within the project root.\n */\nexport function findFrontendDir(projectRoot: string): string | null {\n const frontendDir = path.join(projectRoot, \"frontend\");\n return fs.existsSync(frontendDir) ? frontendDir : null;\n}\n\n/**\n * Find the .env file. Checks the project root first, then backend.\n */\nexport function findEnvFile(projectRoot: string): string | null {\n const candidates = [\n path.join(projectRoot, \".env\"),\n path.join(projectRoot, \"backend\", \".env\")\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n\n/**\n * Read the project's `.env` into a plain object.\n *\n * One reader, because there were four: `dotenv` in `start`, a hand-rolled\n * `indexOf(\"=\")` loop in `api-keys`, a single-key regex in `auth`, and its own\n * splitting in `cloud env`. `dotenv` is a declared dependency of this package,\n * so the other three existed for no reason and disagreed with the correct one\n * on the two things people actually write in a `.env`:\n *\n * - `export KEY=value`, which the hand-rolled parser keyed as\n * `export KEY` — so the command reported the key as unset while it was\n * right there in the file;\n * - `KEY=value # comment`, whose comment travelled into the value and then\n * into an `Authorization` header, coming back as a 401 with nothing\n * pointing at the cause.\n *\n * Returns `{}` when the project has no `.env`, so callers can treat \"absent\"\n * and \"empty\" alike.\n */\nexport function readEnvFile(projectRoot: string): Record<string, string> {\n const envFile = findEnvFile(projectRoot);\n if (!envFile || !fs.existsSync(envFile)) return {};\n try {\n return dotenv.parse(fs.readFileSync(envFile, \"utf-8\"));\n } catch {\n // An unreadable or malformed file is not worth failing a command over;\n // the caller reports the missing value it was looking for.\n return {};\n }\n}\n\n/**\n * Resolve a binary from the project's node_modules/.bin.\n * Checks backend, root, parent monorepo root, then falls back to PATH.\n */\nexport function resolveLocalBin(projectRoot: string, binName: string): string | null {\n const candidates = [\n path.join(projectRoot, \"backend\", \"node_modules\", \".bin\", binName),\n path.join(projectRoot, \"node_modules\", \".bin\", binName)\n ];\n\n // Also check parent directories (for monorepo setups where app/ is nested)\n let parent = path.dirname(projectRoot);\n const rootDir = path.parse(parent).root;\n while (parent !== rootDir) {\n candidates.push(path.join(parent, \"node_modules\", \".bin\", binName));\n parent = path.dirname(parent);\n }\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n // Fall back to globally installed binary via which\n try {\n const globalPath = execSync(`which ${binName}`, { encoding: \"utf-8\" }).trim();\n if (globalPath && fs.existsSync(globalPath)) return globalPath;\n } catch {\n // not found globally\n }\n\n return null;\n}\n\n/**\n * Resolve the tsx binary. Checks backend node_modules first, then root.\n */\nexport function resolveTsx(projectRoot: string): string | null {\n return resolveLocalBin(projectRoot, \"tsx\");\n}\n\n/**\n * Validate that a resolved tsx binary actually has an intact installation.\n *\n * `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)\n * exists. If the pnpm content-addressable store was cleaned or a previous\n * install was interrupted, the symlink can exist while critical files inside\n * the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a\n * confusing MODULE_NOT_FOUND error at runtime.\n *\n * This function follows the symlink, walks up to find the tsx package root\n * (`package.json` with `name: \"tsx\"`), and verifies that `dist/preflight.cjs`\n * is present. Returns `null` when the installation looks healthy, or an\n * error description string when it appears corrupted.\n */\nexport function validateTsxInstallation(tsxBinPath: string): string | null {\n try {\n // Follow the symlink chain to the real tsx entry script\n const realPath = fs.realpathSync(tsxBinPath);\n\n // Walk up from the real binary to locate the tsx package root\n let dir = path.dirname(realPath);\n const fsRoot = path.parse(dir).root;\n for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {\n const pkgPath = path.join(dir, \"package.json\");\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n if (pkg.name === \"tsx\") {\n // Found the tsx package root — verify critical preload file\n const preflightPath = path.join(dir, \"dist\", \"preflight.cjs\");\n if (!fs.existsSync(preflightPath)) {\n return `tsx package at ${dir} is missing dist/preflight.cjs`;\n }\n return null; // Installation looks healthy\n }\n } catch {\n // Malformed package.json — keep walking\n }\n }\n dir = path.dirname(dir);\n }\n\n // Could not determine tsx root — don't block, assume valid\n return null;\n } catch (err) {\n // realpathSync throws if the symlink target is completely gone\n return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;\n }\n}\n\n/**\n * The one sentence a command says when the project's dependencies are missing.\n *\n * Six commands each had their own wording for the same state, and none of them\n * named the remedy:\n *\n * ✗ Could not find CLI entry point for @rebasepro/server-postgres.\n * ✗ Could not find tsx binary.\n * ✗ Could not find tsx binary for backend.\n *\n * All three mean \"you have not installed yet\" — `getActiveBackendPlugin` has\n * already read the driver out of `backend/package.json` by the time the first\n * one fires, so the package is declared and simply not on disk, and `tsx` is a\n * devDependency of every scaffold. But they read as *Rebase* being broken, and\n * they name an internal path or a binary the developer never asked for rather\n * than the command that fixes it. That is the whole of the failure someone sees\n * on a fresh clone, where `node_modules/` is the one thing a checkout does not\n * carry.\n *\n * So: one sentence, naming the package manager this project uses and the\n * directory to run it in — the working directory is usually neither.\n */\nexport function dependenciesNotInstalled(projectRoot: string): string {\n const install = getPMCommands(detectPackageManager(projectRoot)).install.join(\" \");\n return `Dependencies are not installed — run \\`${install}\\` in ${projectRoot}`;\n}\n\n/**\n * Print {@link dependenciesNotInstalled} and exit 1.\n *\n * For the five commands that report and stop. `db.ts` throws instead, because\n * its caller adds the `✗` and the exit itself.\n */\nexport function exitDependenciesNotInstalled(projectRoot: string): never {\n console.error(chalk.red(`✗ ${dependenciesNotInstalled(projectRoot)}`));\n process.exit(1);\n}\n\n/** One problem inside a refusal, when the command found several. */\nexport interface JsonFailureIssue {\n /** Where it is — a config path, a manifest key, a file. */\n path?: string;\n message: string;\n}\n\n/**\n * The refusal envelope, for a caller that asked for JSON.\n *\n * Same shape as the cloud family's `fail()` — `{ error: { message, code, hint } }`\n * — because there is one CLI and a caller should not have to know which half of\n * it answered. `code` is what a caller branches on and is never absent; the\n * cloud family's reasoning about that applies here unchanged. `issues` carries\n * the per-problem detail a human reader gets as a bullet list, because a\n * refusal that says \"2 problem(s)\" and does not say which two is not\n * machine-readable in any useful sense.\n *\n * On **stdout**, like every other `--json` result: the contract those commands\n * make is that stdout holds one JSON value, and a caller that pipes stdout to a\n * parser must get a parseable refusal rather than an empty stream and a\n * human sentence it never sees. That contract used to hold for exactly one\n * failure of each command — the one that goes through `requireProjectRoot` —\n * while every other exit of the *same* command wrote human text to stderr and\n * left stdout empty. So `rebase status --json` was parseable outside a project\n * and unparseable inside a broken one, which is the case a caller actually has\n * to handle.\n *\n * The flag is read off `process.argv` because the failure happens inside a\n * helper the command calls before it has parsed anything. Coarse — a literal\n * `--json` as some other flag's value would count — and worth it: the failure\n * mode of being coarse is a JSON error where a human one was wanted, and the\n * failure mode of not doing it is an unparseable stream.\n */\nexport function failAsJson(\n message: string,\n code: string,\n hint?: string,\n issues?: JsonFailureIssue[]\n): never {\n const error: Record<string, unknown> = { message, code };\n if (hint !== undefined) error.hint = hint;\n if (issues !== undefined) error.issues = issues;\n console.log(JSON.stringify({ error }, null, 2));\n process.exit(1);\n}\n\n/** Did the command line ask for machine-readable output? */\nexport function wantsJsonOutput(argv: readonly string[] = process.argv): boolean {\n return argv.includes(\"--json\");\n}\n\n/**\n * Require the project root or exit with a helpful error.\n */\nexport function requireProjectRoot(): string {\n const root = findProjectRoot();\n if (!root) {\n if (wantsJsonOutput()) {\n // `rebase status --json` outside a project wrote four grey lines to\n // stderr and nothing at all to stdout, so the agent or CI step that\n // asked for JSON got an empty parse and no reason.\n failAsJson(\n \"Could not find a Rebase project root.\",\n \"no_project_root\",\n `Looked in this directory and every parent for a ${MANIFEST_FILENAME}, `\n + \"a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"\n );\n }\n // Name what is actually looked for, in the order `findProjectRoot`\n // looks for it. The old wording (\"backend/, frontend/, and config/\")\n // described neither the manifest — which is the primary marker and the\n // only one `rebase init` writes — nor a `--headless` project, which has\n // no frontend at all and would read this as \"you are in the wrong\n // place\" while standing in the right one.\n console.error(chalk.red(\"✗ Could not find a Rebase project root.\"));\n console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));\n console.error(chalk.gray(\" a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"));\n console.error(chalk.gray(\" Run this from inside a project, or create one with `rebase init`.\"));\n process.exit(1);\n }\n return root;\n}\n\n/**\n * Require the backend directory or exit with a helpful error.\n */\nexport function requireBackendDir(projectRoot: string): string {\n const backendDir = findBackendDir(projectRoot);\n if (!backendDir) {\n if (wantsJsonOutput()) {\n failAsJson(\n \"Could not find a backend/ directory.\",\n \"no_backend_dir\",\n `Expected at: ${path.join(projectRoot, \"backend\")}`\n );\n }\n console.error(chalk.red(\"✗ Could not find a backend/ directory.\"));\n console.error(chalk.gray(` Expected at: ${path.join(projectRoot, \"backend\")}`));\n process.exit(1);\n }\n return backendDir;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,IAAa,oBAAoB;;;;;;;;;;;;;;AAejC,SAAgB,gBAAgB,WAAmB,QAAQ,IAAI,GAAkB;CAC7E,IAAI,MAAM,KAAK,QAAQ,QAAQ;CAC/B,MAAM,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;CAE7B,OAAO,QAAQ,MAAM;EACjB,IAAI,GAAG,WAAW,KAAK,KAAK,KAAA,aAAsB,CAAC,GAC/C,OAAO;EAGX,MAAM,UAAU,KAAK,KAAK,KAAK,cAAc;EAE7C,IAAI,GAAG,WAAW,OAAO,GAAG;GACxB,IAAI;IACA,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,SAAS,OAAO,CAAC;IAExD,IAAI,IAAI,cAAc,MAAM,QAAQ,IAAI,UAAU;SAC3B,IAAI,WAAW,MAAM,MACpC,MAAM,SAEN,GAAY,OAAO;IAAA;GAE/B,QAAQ,CAER;GAGA,IAAI,GAAG,WAAW,KAAK,KAAK,KAAK,SAAS,CAAC,KAAK,GAAG,WAAW,KAAK,KAAK,KAAK,QAAQ,CAAC,GAClF,OAAO;EAEf;EAEA,MAAM,KAAK,QAAQ,GAAG;CAC1B;CAEA,OAAO;AACX;;;;AAKA,SAAgB,eAAe,aAAoC;CAC/D,MAAM,aAAa,KAAK,KAAK,aAAa,SAAS;CACnD,OAAO,GAAG,WAAW,UAAU,IAAI,aAAa;AACpD;;;;AAKA,SAAgB,uBAAuB,YAAmC;CACtE,MAAM,UAAU,KAAK,KAAK,YAAY,cAAc;CACpD,IAAI,CAAC,GAAG,WAAW,OAAO,GAAG,OAAO;CAEpC,IAAI;EACA,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,SAAS,OAAO,CAAC;EACxD,MAAM,OAAO;GAAE,GAAG,IAAI;GAC9B,GAAG,IAAI;EAAgB;EAGf,MAAM,aAAa,OAAO,KAAK,IAAI,CAAC,CAAC,QACjC,QAAO,IAAI,WAAW,oBAAoB,KAAK,QAAQ,mBAC3D;EAEA,IAAI,WAAW,WAAW,GAAG,OAAO;EAGpC,IAAI,WAAW,SAAS,4BAA4B,GAChD,OAAO;EAIX,KAAK,MAAM,aAAa,YACpB,IAAI,uBAAuB,YAAY,SAAS,GAC5C,OAAO;EAKf,OAAO,WAAW;CACtB,QAAQ,CAER;CACA,OAAO;AACX;;;;AAKA,SAAgB,uBAAuB,YAAoB,YAAmC;CAC1F,MAAM,aAAuB,CAAC;CAK9B,IAAI,MAAM,KAAK,QAAQ,UAAU;CACjC,MAAM,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;CAC/B,OAAO,QAAQ,QAAQ;EACnB,WAAW,KACP,KAAK,KAAK,KAAK,gBAAgB,YAAY,OAAO,QAAQ,GAC1D,KAAK,KAAK,KAAK,gBAAgB,YAAY,QAAQ,QAAQ,CAC/D;EACA,MAAM,KAAK,QAAQ,GAAG;CAC1B;CAEA,WAAW,KAEP,KAAK,QAAQ,YAAY,MAAM,MAAM,MAAM,YAAY,WAAW,QAAQ,eAAe,EAAE,GAAG,OAAO,QAAQ,GAC7G,KAAK,QAAQ,YAAY,MAAM,MAAM,YAAY,WAAW,QAAQ,eAAe,EAAE,GAAG,OAAO,QAAQ,GACvG,KAAK,QAAQ,YAAY,MAAM,YAAY,WAAW,QAAQ,eAAe,EAAE,GAAG,OAAO,QAAQ,CACrG;CAEA,KAAK,MAAM,aAAa,YACpB,IAAI,GAAG,WAAW,SAAS,GAAG,OAAO;CAEzC,OAAO;AACX;;;;AAKA,SAAgB,gBAAgB,aAAoC;CAChE,MAAM,cAAc,KAAK,KAAK,aAAa,UAAU;CACrD,OAAO,GAAG,WAAW,WAAW,IAAI,cAAc;AACtD;;;;AAKA,SAAgB,YAAY,aAAoC;CAC5D,MAAM,aAAa,CACf,KAAK,KAAK,aAAa,MAAM,GAC7B,KAAK,KAAK,aAAa,WAAW,MAAM,CAC5C;CAEA,KAAK,MAAM,aAAa,YACpB,IAAI,GAAG,WAAW,SAAS,GAAG,OAAO;CAGzC,OAAO;AACX;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,YAAY,aAA6C;CACrE,MAAM,UAAU,YAAY,WAAW;CACvC,IAAI,CAAC,WAAW,CAAC,GAAG,WAAW,OAAO,GAAG,OAAO,CAAC;CACjD,IAAI;EACA,OAAO,OAAO,MAAM,GAAG,aAAa,SAAS,OAAO,CAAC;CACzD,QAAQ;EAGJ,OAAO,CAAC;CACZ;AACJ;;;;;AAMA,SAAgB,gBAAgB,aAAqB,SAAgC;CACjF,MAAM,aAAa,CACf,KAAK,KAAK,aAAa,WAAW,gBAAgB,QAAQ,OAAO,GACjE,KAAK,KAAK,aAAa,gBAAgB,QAAQ,OAAO,CAC1D;CAGA,IAAI,SAAS,KAAK,QAAQ,WAAW;CACrC,MAAM,UAAU,KAAK,MAAM,MAAM,CAAC,CAAC;CACnC,OAAO,WAAW,SAAS;EACvB,WAAW,KAAK,KAAK,KAAK,QAAQ,gBAAgB,QAAQ,OAAO,CAAC;EAClE,SAAS,KAAK,QAAQ,MAAM;CAChC;CAEA,KAAK,MAAM,aAAa,YACpB,IAAI,GAAG,WAAW,SAAS,GAAG,OAAO;CAIzC,IAAI;EACA,MAAM,aAAa,SAAS,SAAS,WAAW,EAAE,UAAU,QAAQ,CAAC,CAAC,CAAC,KAAK;EAC5E,IAAI,cAAc,GAAG,WAAW,UAAU,GAAG,OAAO;CACxD,QAAQ,CAER;CAEA,OAAO;AACX;;;;AAKA,SAAgB,WAAW,aAAoC;CAC3D,OAAO,gBAAgB,aAAa,KAAK;AAC7C;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,YAAmC;CACvE,IAAI;EAEA,MAAM,WAAW,GAAG,aAAa,UAAU;EAG3C,IAAI,MAAM,KAAK,QAAQ,QAAQ;EAC/B,MAAM,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;EAC/B,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,QAAQ,SAAS;GACvD,MAAM,UAAU,KAAK,KAAK,KAAK,cAAc;GAC7C,IAAI,GAAG,WAAW,OAAO,GACrB,IAAI;IAEA,IADY,KAAK,MAAM,GAAG,aAAa,SAAS,OAAO,CACnD,CAAA,CAAI,SAAS,OAAO;KAEpB,MAAM,gBAAgB,KAAK,KAAK,KAAK,QAAQ,eAAe;KAC5D,IAAI,CAAC,GAAG,WAAW,aAAa,GAC5B,OAAO,kBAAkB,IAAI;KAEjC,OAAO;IACX;GACJ,QAAQ,CAER;GAEJ,MAAM,KAAK,QAAQ,GAAG;EAC1B;EAGA,OAAO;CACX,SAAS,KAAK;EAEV,OAAO,iCAAiC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC3F;AACJ;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,yBAAyB,aAA6B;CAElE,OAAO,0CADS,cAAc,qBAAqB,WAAW,CAAC,CAAC,CAAC,QAAQ,KAAK,GAC7B,EAAQ,QAAQ;AACrE;;;;;;;AAQA,SAAgB,6BAA6B,aAA4B;CACrE,QAAQ,MAAM,MAAM,IAAI,KAAK,yBAAyB,WAAW,GAAG,CAAC;CACrE,QAAQ,KAAK,CAAC;AAClB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,WACZ,SACA,MACA,MACA,QACK;CACL,MAAM,QAAiC;EAAE;EAAS;CAAK;CACvD,IAAI,SAAS,KAAA,GAAW,MAAM,OAAO;CACrC,IAAI,WAAW,KAAA,GAAW,MAAM,SAAS;CACzC,QAAQ,IAAI,KAAK,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC9C,QAAQ,KAAK,CAAC;AAClB;;AAGA,SAAgB,gBAAgB,OAA0B,QAAQ,MAAe;CAC7E,OAAO,KAAK,SAAS,QAAQ;AACjC;;;;AAKA,SAAgB,qBAA6B;CACzC,MAAM,OAAO,gBAAgB;CAC7B,IAAI,CAAC,MAAM;EACP,IAAI,gBAAgB,GAIhB,WACI,yCACA,mBACA,mDAAmD,kBAAkB,8EAEzE;EAQJ,QAAQ,MAAM,MAAM,IAAI,yCAAyC,CAAC;EAClE,QAAQ,MAAM,MAAM,KAAK,qDAAqD,kBAAkB,EAAE,CAAC;EACnG,QAAQ,MAAM,MAAM,KAAK,iFAAiF,CAAC;EAC3G,QAAQ,MAAM,MAAM,KAAK,qEAAqE,CAAC;EAC/F,QAAQ,KAAK,CAAC;CAClB;CACA,OAAO;AACX;;;;AAKA,SAAgB,kBAAkB,aAA6B;CAC3D,MAAM,aAAa,eAAe,WAAW;CAC7C,IAAI,CAAC,YAAY;EACb,IAAI,gBAAgB,GAChB,WACI,wCACA,kBACA,gBAAgB,KAAK,KAAK,aAAa,SAAS,GACpD;EAEJ,QAAQ,MAAM,MAAM,IAAI,wCAAwC,CAAC;EACjE,QAAQ,MAAM,MAAM,KAAK,kBAAkB,KAAK,KAAK,aAAa,SAAS,GAAG,CAAC;EAC/E,QAAQ,KAAK,CAAC;CAClB;CACA,OAAO;AACX"}
@@ -1,74 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- //#region src/dev-db/resolve.ts
3
- var resolve_exports = /* @__PURE__ */ __exportAll({
4
- describeDevDatabase: () => describeDevDatabase,
5
- resolveDevDatabase: () => resolveDevDatabase
6
- });
7
- /**
8
- * A value that is present but empty is treated as absent.
9
- *
10
- * `DATABASE_URL=` in a `.env` is what a developer writes when they mean "not
11
- * this one" — honouring it literally would hand an empty connection string to
12
- * libpq and produce an error about a missing host, which explains nothing.
13
- */
14
- function present(value) {
15
- if (typeof value !== "string") return null;
16
- const trimmed = value.trim();
17
- return trimmed.length > 0 ? trimmed : null;
18
- }
19
- function resolveDevDatabase(input = {}) {
20
- const flagUrl = present(input.flagUrl);
21
- if (flagUrl) return {
22
- kind: "external",
23
- url: flagUrl,
24
- source: "flag"
25
- };
26
- const fromEnvironment = present(input.env?.DATABASE_URL);
27
- if (fromEnvironment) return {
28
- kind: "external",
29
- url: fromEnvironment,
30
- source: "environment"
31
- };
32
- const fromBranch = present(input.branch?.url);
33
- if (fromBranch && input.branch) return {
34
- kind: "external",
35
- url: fromBranch,
36
- source: "branch",
37
- branch: input.branch.name
38
- };
39
- const fromEnvFile = present(input.envFile?.DATABASE_URL);
40
- if (fromEnvFile) return {
41
- kind: "external",
42
- url: fromEnvFile,
43
- source: "env-file"
44
- };
45
- if (input.flagDocker) return {
46
- kind: "docker",
47
- source: "docker",
48
- url: present(input.composeUrl)
49
- };
50
- return {
51
- kind: "managed",
52
- source: "managed"
53
- };
54
- }
55
- /** One line for the startup banner, naming both the database and why. */
56
- function describeDevDatabase(database) {
57
- switch (database.kind) {
58
- case "external":
59
- switch (database.source) {
60
- case "flag": return "your database (--database-url)";
61
- case "environment": return "your database (DATABASE_URL in the environment)";
62
- case "env-file": return "your database (DATABASE_URL in .env)";
63
- case "branch": return `branch "${database.branch}" (rebase db branch switch)`;
64
- }
65
- break;
66
- case "docker": return "Postgres in Docker";
67
- case "managed": return "the managed development database (PGlite)";
68
- }
69
- throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);
70
- }
71
- //#endregion
72
- export { resolveDevDatabase as n, resolve_exports as r, describeDevDatabase as t };
73
-
74
- //# sourceMappingURL=resolve-BAeRUBhg.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolve-BAeRUBhg.js","names":[],"sources":["../src/dev-db/resolve.ts"],"sourcesContent":["/**\n * Which database a command should talk to, decided in one place.\n *\n * Before this existed every command that needed Postgres read `DATABASE_URL`\n * for itself, which was fine while there was exactly one answer. Introducing a\n * managed database makes the question real: a project may have no\n * `DATABASE_URL` at all and still expect `rebase db push` to work, and a\n * project that *does* set one must never be quietly redirected somewhere else.\n *\n * So the rule is ordered and boring, and the order is the promise:\n *\n * 1. `--database-url <url>` — said on this command line, wins over everything\n * 2. `DATABASE_URL` in the shell environment\n * 3. the database branch this checkout is switched to\n * 4. `DATABASE_URL` in the project's `.env`\n * 5. `--docker`\n * 6. the managed PGlite database\n *\n * An explicit connection string always wins. That is the whole point of the\n * override: someone pointing Rebase at their own Postgres — a colleague's\n * staging box, a Neon branch, a container they manage — must get exactly that,\n * with no cleverness in between. The managed database is what fills the vacuum\n * when nobody has said anything, and it is the only case where the CLI picks.\n *\n * The branch at (3) is the one addition that is not an explicit connection\n * string, and it sits where it does deliberately. It has to outrank `.env` or\n * `rebase db branch switch` would silently do nothing on every project that\n * sets `DATABASE_URL` — which is every project not on the managed database.\n * It must not outrank (1) or (2), because a flag on this command line and a\n * variable in this shell are more immediate than a switch made yesterday.\n *\n * {@link resolveDevDatabase} is pure: inputs in, decision out, no filesystem\n * and no process. Reading `.env`, reading the branch pointer and starting a\n * daemon happen elsewhere, so the ordering above can be tested without any of\n * them.\n */\n\n/** Where the answer came from. Carried so diagnostics can name it. */\nexport type DevDatabaseSource =\n /** `--database-url` on the command line. */\n | \"flag\"\n /** `DATABASE_URL` in the shell environment. */\n | \"environment\"\n /** `DATABASE_URL` in the project's `.env`. */\n | \"env-file\"\n /** The branch this checkout is switched to, over the base connection. */\n | \"branch\"\n /** `--docker`. */\n | \"docker\"\n /** Nobody said anything, so the managed database fills in. */\n | \"managed\";\n\nexport type DevDatabase =\n | {\n kind: \"external\";\n /** The connection string, exactly as given. Never rewritten. */\n url: string;\n source: Extract<DevDatabaseSource, \"flag\" | \"environment\" | \"env-file\" | \"branch\">;\n /** The branch name, when this checkout is switched to one. */\n branch?: string;\n }\n | {\n kind: \"docker\";\n source: \"docker\";\n /**\n * The compose `db` service's connection string, derived by the caller.\n *\n * Null when the project has no compose file to derive one from, which\n * is a `--docker` that cannot be honoured rather than a fallback: the\n * caller says so instead of guessing at `localhost:5432`.\n */\n url: string | null;\n }\n | {\n kind: \"managed\";\n source: \"managed\";\n };\n\nexport interface ResolveDevDatabaseInput {\n /** `--database-url <url>`, if given. */\n flagUrl?: string | null;\n /** `--docker`, if given. */\n flagDocker?: boolean;\n /** The shell environment. Only `DATABASE_URL` is read. */\n env?: Record<string, string | undefined>;\n /** Parsed `.env` from the project root. Only `DATABASE_URL` is read. */\n envFile?: Record<string, string> | null;\n /**\n * The compose `db` service's connection string, when the project has one.\n *\n * Passed in rather than read here so this function stays pure. Only\n * consulted for the Docker case; an explicit `DATABASE_URL` still wins.\n */\n composeUrl?: string | null;\n /**\n * The branch this checkout is switched to, already resolved to a URL.\n *\n * Resolved by the caller rather than here so this stays pure: deriving it\n * needs the base connection string, which needs the `.env` this function is\n * handed rather than reads.\n */\n branch?: { name: string; url: string } | null;\n}\n\n/**\n * A value that is present but empty is treated as absent.\n *\n * `DATABASE_URL=` in a `.env` is what a developer writes when they mean \"not\n * this one\" — honouring it literally would hand an empty connection string to\n * libpq and produce an error about a missing host, which explains nothing.\n */\nfunction present(value: string | undefined | null): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n\n return trimmed.length > 0 ? trimmed : null;\n}\n\nexport function resolveDevDatabase(input: ResolveDevDatabaseInput = {}): DevDatabase {\n const flagUrl = present(input.flagUrl);\n if (flagUrl) return { kind: \"external\", url: flagUrl, source: \"flag\" };\n\n const fromEnvironment = present(input.env?.DATABASE_URL);\n if (fromEnvironment) return { kind: \"external\", url: fromEnvironment, source: \"environment\" };\n\n // Above `.env`, below anything said explicitly — see the ordering note at\n // the top of this file.\n const fromBranch = present(input.branch?.url);\n if (fromBranch && input.branch) {\n return { kind: \"external\", url: fromBranch, source: \"branch\", branch: input.branch.name };\n }\n\n const fromEnvFile = present(input.envFile?.DATABASE_URL);\n if (fromEnvFile) return { kind: \"external\", url: fromEnvFile, source: \"env-file\" };\n\n // Only consulted once every explicit connection string is exhausted: asking\n // for Docker is a choice about *how to get* a database, not which one, so a\n // DATABASE_URL that already names one outranks it.\n if (input.flagDocker) {\n return { kind: \"docker\", source: \"docker\", url: present(input.composeUrl) };\n }\n\n return { kind: \"managed\", source: \"managed\" };\n}\n\n/** One line for the startup banner, naming both the database and why. */\nexport function describeDevDatabase(database: DevDatabase): string {\n switch (database.kind) {\n case \"external\":\n switch (database.source) {\n case \"flag\":\n return \"your database (--database-url)\";\n case \"environment\":\n return \"your database (DATABASE_URL in the environment)\";\n case \"env-file\":\n return \"your database (DATABASE_URL in .env)\";\n case \"branch\":\n return `branch \"${database.branch}\" (rebase db branch switch)`;\n }\n break;\n case \"docker\":\n return \"Postgres in Docker\";\n case \"managed\":\n return \"the managed development database (PGlite)\";\n }\n\n // Unreachable while the union is exhaustive; kept so a future variant fails\n // loudly in review rather than printing \"undefined\" to a user.\n throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);\n}\n"],"mappings":";;;;;;;;;;;;;AA+GA,SAAS,QAAQ,OAAiD;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,KAAK;CAE3B,OAAO,QAAQ,SAAS,IAAI,UAAU;AAC1C;AAEA,SAAgB,mBAAmB,QAAiC,CAAC,GAAgB;CACjF,MAAM,UAAU,QAAQ,MAAM,OAAO;CACrC,IAAI,SAAS,OAAO;EAAE,MAAM;EAAY,KAAK;EAAS,QAAQ;CAAO;CAErE,MAAM,kBAAkB,QAAQ,MAAM,KAAK,YAAY;CACvD,IAAI,iBAAiB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAiB,QAAQ;CAAc;CAI5F,MAAM,aAAa,QAAQ,MAAM,QAAQ,GAAG;CAC5C,IAAI,cAAc,MAAM,QACpB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAY,QAAQ;EAAU,QAAQ,MAAM,OAAO;CAAK;CAG5F,MAAM,cAAc,QAAQ,MAAM,SAAS,YAAY;CACvD,IAAI,aAAa,OAAO;EAAE,MAAM;EAAY,KAAK;EAAa,QAAQ;CAAW;CAKjF,IAAI,MAAM,YACN,OAAO;EAAE,MAAM;EAAU,QAAQ;EAAU,KAAK,QAAQ,MAAM,UAAU;CAAE;CAG9E,OAAO;EAAE,MAAM;EAAW,QAAQ;CAAU;AAChD;;AAGA,SAAgB,oBAAoB,UAA+B;CAC/D,QAAQ,SAAS,MAAjB;EACI,KAAK;GACD,QAAQ,SAAS,QAAjB;IACI,KAAK,QACD,OAAO;IACX,KAAK,eACD,OAAO;IACX,KAAK,YACD,OAAO;IACX,KAAK,UACD,OAAO,WAAW,SAAS,OAAO;GAC1C;GACA;EACJ,KAAK,UACD,OAAO;EACX,KAAK,WACD,OAAO;CACf;CAIA,MAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,QAAQ,GAAG;AACzE"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"state-C59Elrnt.js","names":[],"sources":["../src/dev-db/state.ts"],"sourcesContent":["/**\n * The managed database's state file, and the rules for trusting it.\n *\n * The daemon outlives the command that started it — `rebase db push` in one\n * terminal and `rebase dev` in another have to reach the *same* PGlite, because\n * two processes opening one data directory would corrupt it. So the daemon\n * records where it is, and every command reads that record.\n *\n * A record on disk is a claim, not a fact. The process it names may have been\n * killed, the machine may have rebooted and handed the pid to something else,\n * and the port may now belong to a stranger. {@link readState} therefore only\n * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and\n * it asks the daemon rather than the operating system.\n */\n\nimport fs from \"fs\";\nimport { resourceEnvSuffix } from \"@rebasepro/types\";\nimport net from \"net\";\nimport path from \"path\";\n\n/** Everything under here is generated and gitignored. */\nexport const DEV_DB_DIR = \".rebase\";\n/** PGlite's own data directory. Deleting it is what `--reset` means. */\nexport const DATA_DIR_NAME = \"pgdata\";\n/** The record the daemon writes once it is accepting connections. */\nexport const STATE_FILE_NAME = \"pglite.json\";\n/**\n * Held by whoever is currently starting a daemon.\n *\n * Without it, `rebase dev` and `rebase db push` started in the same second both\n * see no state file, both spawn, and two processes open one PGlite data\n * directory — the exact corruption the single-daemon design exists to prevent.\n * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to\n * show up; the harmful way is a damaged database.\n */\nexport const START_LOCK_NAME = \"starting.lock\";\n\nexport interface DaemonState {\n /** TCP port the socket server is listening on, chosen when it started. */\n port: number;\n /** The daemon process. Used only as a fast negative check. */\n pid: number;\n /** Absolute path of the PGlite data directory this daemon has open. */\n dataDir: string;\n /** ISO timestamp, for diagnostics. */\n startedAt: string;\n /**\n * A random token the daemon also answers with over the wire, on\n * {@link identityPort}.\n *\n * Without it, \"is the daemon alive?\" degrades to \"is something listening on\n * that port?\", which is a different question and answers yes for whatever\n * process happened to take the port after a reboot. Rebase would then send\n * migrations to a stranger.\n */\n token: string;\n /** Loopback port that answers the identity check. */\n identityPort: number;\n /**\n * The additional databases this daemon serves, by declared key.\n *\n * One PGlite instance each — PGlite is one database per instance, so\n * `database(\"analytics\")` is a second data directory and a second port,\n * not a `CREATE DATABASE`. Started on demand when a command asks for the\n * key over the identity socket, and recorded here so `rebase status` and\n * a reset can see them. The default database is the fields above.\n */\n databases?: Record<string, { port: number; dataDir: string }>;\n}\n\nexport function devDbDir(projectRoot: string): string {\n return path.join(projectRoot, DEV_DB_DIR);\n}\n\nexport function dataDir(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), DATA_DIR_NAME);\n}\n\n/**\n * The data directory for an additional declared database.\n *\n * `pgdata__analytics`: the key spelled the way its variable is, so the\n * directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the\n * same thing.\n */\nexport function additionalDataDir(projectRoot: string, key: string): string {\n return path.join(devDbDir(projectRoot), `${DATA_DIR_NAME}${resourceEnvSuffix(key).toLowerCase()}`);\n}\n\nexport function stateFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), STATE_FILE_NAME);\n}\n\nexport function startLockFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), START_LOCK_NAME);\n}\n\n/**\n * Take the start lock, or report that somebody else holds it.\n *\n * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so\n * exactly one of two racing processes can succeed no matter how close together\n * they arrive.\n *\n * A lock older than `staleAfterMs` is broken rather than waited on — the holder\n * may have been killed between creating it and starting anything, and a\n * developer should never have to know this file exists in order to unstick\n * their project.\n */\nexport function acquireStartLock(projectRoot: string, staleAfterMs: number): boolean {\n const target = startLockFile(projectRoot);\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n\n const attempt = (): boolean => {\n try {\n const handle = fs.openSync(target, \"wx\");\n fs.writeSync(handle, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(handle);\n\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\n return false;\n }\n };\n\n if (attempt()) return true;\n\n try {\n // Clamped at zero: a filesystem whose timestamp granularity rounds the\n // mtime *up* reports a negative age for a lock created moments ago,\n // and a negative age is below every threshold — so `staleAfterMs: 0`,\n // which means \"break any lock\", would refuse to break one.\n const age = Math.max(0, Date.now() - fs.statSync(target).mtimeMs);\n if (age < staleAfterMs) return false;\n fs.unlinkSync(target);\n } catch {\n // Vanished under us, which means the holder finished. Either way the\n // next attempt is the answer.\n }\n\n return attempt();\n}\n\nexport function releaseStartLock(projectRoot: string): void {\n try {\n fs.unlinkSync(startLockFile(projectRoot));\n } catch {\n // Already released is the desired end state.\n }\n}\n\n/**\n * Parse the record, or `null` for anything that is not one.\n *\n * Every failure is the same answer — absent — because every failure has the\n * same remedy: start a daemon. A corrupt state file is not worth an error\n * message to a user who never wrote it.\n */\nexport function readState(projectRoot: string): DaemonState | null {\n let raw: string;\n try {\n raw = fs.readFileSync(stateFile(projectRoot), \"utf8\");\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as Partial<DaemonState>;\n if (\n typeof parsed.port !== \"number\" ||\n !Number.isInteger(parsed.port) ||\n parsed.port <= 0 ||\n parsed.port > 65535 ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.dataDir !== \"string\" ||\n typeof parsed.token !== \"string\" ||\n parsed.token.length === 0 ||\n typeof parsed.identityPort !== \"number\" ||\n !Number.isInteger(parsed.identityPort) ||\n parsed.identityPort <= 0 ||\n parsed.identityPort > 65535\n ) {\n return null;\n }\n\n // Only well-formed entries survive: a malformed one is treated as\n // absent, and the next ensure asks the daemon for it again.\n const databases: Record<string, { port: number; dataDir: string }> = {};\n if (parsed.databases && typeof parsed.databases === \"object\") {\n for (const [key, entry] of Object.entries(parsed.databases)) {\n if (\n entry && typeof entry.port === \"number\" && Number.isInteger(entry.port)\n && entry.port > 0 && entry.port <= 65535 && typeof entry.dataDir === \"string\"\n ) {\n databases[key] = { port: entry.port, dataDir: entry.dataDir };\n }\n }\n }\n\n return {\n port: parsed.port,\n pid: parsed.pid,\n dataDir: parsed.dataDir,\n startedAt: typeof parsed.startedAt === \"string\" ? parsed.startedAt : \"\",\n token: parsed.token,\n identityPort: parsed.identityPort,\n ...(Object.keys(databases).length > 0 ? { databases } : {})\n };\n } catch {\n return null;\n }\n}\n\nexport function writeState(projectRoot: string, state: DaemonState): void {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n // Written whole then moved, so a reader never sees half a record — commands\n // poll this file while the daemon is starting.\n const target = stateFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n}\n\nexport function clearState(projectRoot: string): void {\n try {\n fs.unlinkSync(stateFile(projectRoot));\n } catch {\n // Already gone is the desired end state.\n }\n}\n\n/** Is *some* process with this pid running? A fast, cheap negative check. */\nexport function pidRunning(pid: number): boolean {\n try {\n // Signal 0 performs the permission and existence checks without\n // delivering anything.\n process.kill(pid, 0);\n\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which for our\n // purposes is still \"running\".\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Can a TCP connection be opened to this port on loopback? */\nexport function portAccepting(port: number, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = new net.Socket();\n const settle = (answer: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(answer);\n };\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => settle(true));\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.connect(port, \"127.0.0.1\");\n });\n}\n\n/**\n * Ask a port for a free one, then hand back the number.\n *\n * Deliberately not the probe `rebase init` uses: that one has a documented\n * failure where a port is free to probe and unusable to publish. This binds on\n * loopback only, which is also where the daemon listens, so a port that binds\n * here binds there.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close(() => reject(new Error(\"Could not determine a free port.\")));\n\n return;\n }\n const { port } = address;\n server.close(() => resolve(port));\n });\n });\n}\n"],"mappings":"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"}