@rebasepro/cli 0.24.0 → 0.24.1-canary.gcb918fc
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.
- package/bin/rebase.js +3 -0
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-BPBi5nOF.js +524 -0
- package/dist/api-keys-BPBi5nOF.js.map +1 -0
- package/dist/apps-CPXNElX4.js +256 -0
- package/dist/apps-CPXNElX4.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-DTMuEScS.js +62 -0
- package/dist/atlas-dev-database-DTMuEScS.js.map +1 -0
- package/dist/auth-DvY94ZuL.js +395 -0
- package/dist/auth-DvY94ZuL.js.map +1 -0
- package/dist/branch-pointer-DIhKbQLp.js +2 -0
- package/dist/build-BHvC6Bjs.js +336 -0
- package/dist/build-BHvC6Bjs.js.map +1 -0
- package/dist/bundle-BQhP_xiA.js +1617 -0
- package/dist/bundle-BQhP_xiA.js.map +1 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cloud-K6P5IHUb.js +9667 -0
- package/dist/cloud-K6P5IHUb.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/api-keys.d.ts +2 -0
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/db.d.ts +64 -24
- package/dist/commands/dev.d.ts +103 -0
- package/dist/commands/generate_sdk.d.ts +26 -0
- package/dist/commands/skills.d.ts +22 -7
- package/dist/commands/storage.d.ts +46 -0
- package/dist/consent-BWHaYx5p.js +76 -0
- package/dist/consent-BWHaYx5p.js.map +1 -0
- package/dist/console-Cs00E-dt.js +102 -0
- package/dist/console-Cs00E-dt.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/daemon-DBn-shxX.js +2 -0
- package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
- package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
- package/dist/db-YbPMFY4Y.js +1252 -0
- package/dist/db-YbPMFY4Y.js.map +1 -0
- package/dist/derive-QMPMcQZJ.js +484 -0
- package/dist/derive-QMPMcQZJ.js.map +1 -0
- package/dist/dev-CJ_dd9uP.js +1593 -0
- package/dist/dev-CJ_dd9uP.js.map +1 -0
- package/dist/dev-db/atlas-dev-database.d.ts +32 -0
- package/dist/dev-db/daemon-entry.d.ts +13 -0
- package/dist/dev-preflight-N1evJL3S.js +2 -0
- package/dist/doctor-CIcpWl_L.js +771 -0
- package/dist/doctor-CIcpWl_L.js.map +1 -0
- package/dist/eject-BT6t37D0.js +335 -0
- package/dist/eject-BT6t37D0.js.map +1 -0
- package/dist/fold-static-C1EtHlfV.js +201 -0
- package/dist/fold-static-C1EtHlfV.js.map +1 -0
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-UV635zSe.js +477 -0
- package/dist/generate_sdk-UV635zSe.js.map +1 -0
- package/dist/index.es.js +150 -24674
- package/dist/index.es.js.map +1 -1
- package/dist/init-fXeBcxuN.js +1042 -0
- package/dist/init-fXeBcxuN.js.map +1 -0
- package/dist/manifest-DzlC60WX.js +871 -0
- package/dist/manifest-DzlC60WX.js.map +1 -0
- package/dist/normalize-imports-CmVdE9iZ.js +100 -0
- package/dist/normalize-imports-CmVdE9iZ.js.map +1 -0
- package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
- package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
- package/dist/path-flags-DUBuI2i1.js +77 -0
- package/dist/path-flags-DUBuI2i1.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-Btn9Ewbn.js +1052 -0
- package/dist/prepare-Btn9Ewbn.js.map +1 -0
- package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/resolve-CJLkfFV0.js +2 -0
- package/dist/resources-3J3UQg5r.js +161 -0
- package/dist/resources-3J3UQg5r.js.map +1 -0
- package/dist/schema-ovO8qJDw.js +125 -0
- package/dist/schema-ovO8qJDw.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-rU2e3Kp5.js +228 -0
- package/dist/start-rU2e3Kp5.js.map +1 -0
- package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
- package/dist/state-Do7ZJYkF.js.map +1 -0
- package/dist/status-Cfy0TtSb.js +351 -0
- package/dist/status-Cfy0TtSb.js.map +1 -0
- package/dist/storage-DyvrSTH3.js +303 -0
- package/dist/storage-DyvrSTH3.js.map +1 -0
- package/dist/telemetry-CAWNCugs.js +338 -0
- package/dist/telemetry-CAWNCugs.js.map +1 -0
- package/dist/telemetry-DbLqGIBW.js +129 -0
- package/dist/telemetry-DbLqGIBW.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-D-5-Od96.js +1265 -0
- package/dist/upgrade-D-5-Od96.js.map +1 -0
- package/dist/utils/project.d.ts +16 -0
- package/dist/utils/temp-archive.d.ts +9 -0
- package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
- package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
- package/package.json +7 -7
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +10 -2
- package/templates/template/README.md +1 -1
- package/templates/template/ai-instructions.md +2 -1
- package/templates/template/frontend/vite.config.ts +13 -3
- package/templates/template/scripts/example.ts +2 -1
- package/dist/branch-pointer-kk9F5FBO.js +0 -111
- package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-C8p32i-n.js +0 -357
- package/dist/daemon-C8p32i-n.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-DmaGLmH2.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
import { c as findProjectRoot, d as readEnvFile } from "./project-Wp2zoyeN.js";
|
|
2
|
+
import { a as devDbDir, c as readState, i as dataDir, l as releaseStartLock, o as findFreePort, p as MANAGED_LIMITATIONS, r as clearState, s as pidRunning, t as acquireStartLock, u as stateFile } from "./state-Do7ZJYkF.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { resourceEnvSuffix } from "@rebasepro/types";
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
import net from "net";
|
|
12
|
+
//#region src/utils/dev-preflight.ts
|
|
13
|
+
/**
|
|
14
|
+
* Make `rebase dev` the only command a new project needs.
|
|
15
|
+
*
|
|
16
|
+
* The scaffolded project prints three steps — start the database container,
|
|
17
|
+
* push the schema, start the dev server — and a first-time reader has to
|
|
18
|
+
* perform all three correctly before anything works. Two of them are
|
|
19
|
+
* mechanical, and both fail in ways that do not name themselves: skipping the
|
|
20
|
+
* container gives `ECONNREFUSED` from a stack the reader has never seen, and
|
|
21
|
+
* skipping the push gives a server that starts cleanly and then 500s on the
|
|
22
|
+
* first request because the tables are not there.
|
|
23
|
+
*
|
|
24
|
+
* So `dev` does them. Not always, and not silently — under four conditions that
|
|
25
|
+
* together mean "this is a throwaway development database on this laptop, and
|
|
26
|
+
* it is not running":
|
|
27
|
+
*
|
|
28
|
+
* 1. The DSN resolves to a loopback address. A remote database is somebody's
|
|
29
|
+
* real data and is never touched here, whatever else is true.
|
|
30
|
+
* 2. Nothing is listening on it. A database that answers is left completely
|
|
31
|
+
* alone — no push, no inspection, no connection.
|
|
32
|
+
* 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.
|
|
33
|
+
* the one `rebase init` wrote. Without it there is nothing to start and
|
|
34
|
+
* the backend's own error is the better message.
|
|
35
|
+
* 4. Docker is available. If it is not, the manual steps are printed and dev
|
|
36
|
+
* continues — a preflight must never be the reason the command fails.
|
|
37
|
+
*
|
|
38
|
+
* The schema push happens only when this function actually started the
|
|
39
|
+
* container, which is the one moment a local database is known to be new or
|
|
40
|
+
* stale relative to the collections. A `dev` against an already-running
|
|
41
|
+
* database never mutates its schema.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Hosts that mean "this machine", and so may be provisioned automatically.
|
|
45
|
+
*
|
|
46
|
+
* An exact-match set, never a prefix or substring test: `localhost.example.com`
|
|
47
|
+
* is a perfectly resolvable name that somebody else controls.
|
|
48
|
+
*/
|
|
49
|
+
var LOOPBACK = /* @__PURE__ */ new Set([
|
|
50
|
+
"localhost",
|
|
51
|
+
"127.0.0.1",
|
|
52
|
+
"::1",
|
|
53
|
+
"0.0.0.0"
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* The host and port of a Postgres DSN, but only when it points at this machine.
|
|
57
|
+
*
|
|
58
|
+
* Returns null for anything else — a remote host, an unparseable string, a
|
|
59
|
+
* non-postgres scheme. Callers treat null as "do nothing", so every parse
|
|
60
|
+
* failure fails closed onto the safe behaviour rather than onto a guess.
|
|
61
|
+
*/
|
|
62
|
+
function parseLoopbackDsn(dsn) {
|
|
63
|
+
if (!dsn) return null;
|
|
64
|
+
let url;
|
|
65
|
+
try {
|
|
66
|
+
url = new URL(dsn);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!/^postgres(ql)?:$/.test(url.protocol)) return null;
|
|
71
|
+
const host = url.hostname.replace(/^\[(.+)\]$/, "$1");
|
|
72
|
+
if (!LOOPBACK.has(host)) return null;
|
|
73
|
+
const port = url.port ? Number(url.port) : 5432;
|
|
74
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
75
|
+
return {
|
|
76
|
+
host,
|
|
77
|
+
port
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a compose file declares a `db` service.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately a text scan and not a YAML parse: the CLI has no YAML
|
|
84
|
+
* dependency, and the question is only ever asked of a file this tool wrote.
|
|
85
|
+
* A false negative costs the automation and nothing else — the reader gets the
|
|
86
|
+
* manual steps, which are correct.
|
|
87
|
+
*/
|
|
88
|
+
function composeDeclaresDbService(yamlText) {
|
|
89
|
+
let inServices = false;
|
|
90
|
+
for (const rawLine of yamlText.split(/\r?\n/)) {
|
|
91
|
+
const line = rawLine.replace(/\t/g, " ");
|
|
92
|
+
if (/^\s*#/.test(line) || line.trim() === "") continue;
|
|
93
|
+
if (/^services:\s*$/.test(line)) {
|
|
94
|
+
inServices = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (inServices && /^\S/.test(line)) {
|
|
98
|
+
inServices = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (inServices && /^\s{2,}db:\s*$/.test(line)) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The connection string the compose file's `db` service will answer on.
|
|
107
|
+
*
|
|
108
|
+
* `--docker` used to be a flag that changed a banner and nothing else: it
|
|
109
|
+
* resolved to `kind: "docker"`, and every caller then had no URL to reach the
|
|
110
|
+
* container with, so the backend booted with no `DATABASE_URL` at all and died
|
|
111
|
+
* on the message a project with no database gets. The container it had just
|
|
112
|
+
* been asked for was never started, because the preflight that starts it needs
|
|
113
|
+
* a DSN to decide the database is local and not running.
|
|
114
|
+
*
|
|
115
|
+
* So the URL is derived rather than required: the compose file names the user,
|
|
116
|
+
* the database and the published host port, and `.env` holds the password
|
|
117
|
+
* compose itself interpolates. That is the same string `rebase init` writes as
|
|
118
|
+
* the commented-out `DATABASE_URL`, which is what makes uncommenting that line
|
|
119
|
+
* and passing `--docker` reach the same database.
|
|
120
|
+
*
|
|
121
|
+
* Returns null when the file declares no `db` service, or declares one this
|
|
122
|
+
* scan cannot read — a `--docker` that cannot be honoured has to say so, and
|
|
123
|
+
* a guess would point at somebody else's Postgres on 5432.
|
|
124
|
+
*
|
|
125
|
+
* Same text scan as {@link composeDeclaresDbService}, for the same reason.
|
|
126
|
+
*/
|
|
127
|
+
function composeDatabaseUrl(yamlText, env = {}) {
|
|
128
|
+
const found = scanComposeDbService(yamlText);
|
|
129
|
+
const user = expandComposeValue(found.POSTGRES_USER, env);
|
|
130
|
+
const password = expandComposeValue(found.POSTGRES_PASSWORD, env);
|
|
131
|
+
const database = expandComposeValue(found.POSTGRES_DB, env);
|
|
132
|
+
if (!user || !password || !database || !found.hostPort) return null;
|
|
133
|
+
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The host port the compose `db` service publishes, or null.
|
|
137
|
+
*
|
|
138
|
+
* Separate from {@link composeDatabaseUrl} because the port is answerable on
|
|
139
|
+
* its own: the URL needs the user, the password and the database name too, and
|
|
140
|
+
* a compose file missing any of them still says which port a container would
|
|
141
|
+
* listen on. That is the question "would starting this container answer the
|
|
142
|
+
* DSN in `.env`?" — see the `wrong-port` branch of {@link ensureDevDatabase},
|
|
143
|
+
* where getting it wrong meant starting a container nobody asked for.
|
|
144
|
+
*/
|
|
145
|
+
function composeHostPort(yamlText) {
|
|
146
|
+
const port = scanComposeDbService(yamlText).hostPort;
|
|
147
|
+
return port ? Number(port) : null;
|
|
148
|
+
}
|
|
149
|
+
/** The raw values of the compose `db` service, before any expansion. */
|
|
150
|
+
function scanComposeDbService(yamlText) {
|
|
151
|
+
let inServices = false;
|
|
152
|
+
let inDb = false;
|
|
153
|
+
let dbIndent = 0;
|
|
154
|
+
let section = null;
|
|
155
|
+
const found = {};
|
|
156
|
+
let hostPort = null;
|
|
157
|
+
for (const rawLine of yamlText.split(/\r?\n/)) {
|
|
158
|
+
const line = rawLine.replace(/\t/g, " ");
|
|
159
|
+
if (/^\s*#/.test(line) || line.trim() === "") continue;
|
|
160
|
+
if (/^services:\s*$/.test(line)) {
|
|
161
|
+
inServices = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (inServices && /^\S/.test(line)) {
|
|
165
|
+
inServices = false;
|
|
166
|
+
inDb = false;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!inServices) continue;
|
|
170
|
+
const indent = line.length - line.trimStart().length;
|
|
171
|
+
const dbHeader = line.match(/^(\s{2,})db:\s*$/);
|
|
172
|
+
if (dbHeader) {
|
|
173
|
+
inDb = true;
|
|
174
|
+
dbIndent = dbHeader[1].length;
|
|
175
|
+
section = null;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (inDb && indent <= dbIndent) {
|
|
179
|
+
inDb = false;
|
|
180
|
+
section = null;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!inDb) continue;
|
|
184
|
+
const key = line.match(/^\s+(environment|ports):\s*$/);
|
|
185
|
+
if (key) {
|
|
186
|
+
section = key[1];
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (/^\s+[A-Za-z_][\w-]*:\s*\S/.test(line) && !/^\s+POSTGRES_/.test(line)) {
|
|
190
|
+
if (!/^\s*-\s/.test(line.trimStart())) section = null;
|
|
191
|
+
}
|
|
192
|
+
if (section === "environment") {
|
|
193
|
+
const pair = line.match(/^\s+(POSTGRES_(?:USER|PASSWORD|DB)):\s*(.+?)\s*$/);
|
|
194
|
+
if (pair) found[pair[1]] = pair[2].replace(/^["']|["']$/g, "");
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (section === "ports" && hostPort === null) {
|
|
198
|
+
const mapping = line.match(/^\s*-\s*["']?(?:[\d.]+:)?(\d+):5432(?:\/tcp)?["']?\s*$/);
|
|
199
|
+
if (mapping) hostPort = mapping[1];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return hostPort === null ? found : {
|
|
203
|
+
...found,
|
|
204
|
+
hostPort
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves
|
|
209
|
+
* them — against the project's `.env`, which is the file compose itself reads.
|
|
210
|
+
*/
|
|
211
|
+
function expandComposeValue(raw, env) {
|
|
212
|
+
if (!raw) return null;
|
|
213
|
+
const trimmed = raw.replace(/\$\{([A-Za-z_][\w]*)(?::?-([^}]*))?\}/g, (_all, name, fallback) => {
|
|
214
|
+
const value = env[name];
|
|
215
|
+
if (value !== void 0 && value !== "") return value;
|
|
216
|
+
return fallback ?? "";
|
|
217
|
+
}).trim();
|
|
218
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
219
|
+
}
|
|
220
|
+
/** Is something accepting connections there right now? */
|
|
221
|
+
function probeTcp(host, port, timeoutMs = 700) {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
const socket = new net.Socket();
|
|
224
|
+
let settled = false;
|
|
225
|
+
const done = (open) => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
socket.destroy();
|
|
229
|
+
resolve(open);
|
|
230
|
+
};
|
|
231
|
+
socket.setTimeout(timeoutMs);
|
|
232
|
+
socket.once("connect", () => done(true));
|
|
233
|
+
socket.once("timeout", () => done(false));
|
|
234
|
+
socket.once("error", () => done(false));
|
|
235
|
+
socket.connect(port, host);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Poll until the port answers, or give up.
|
|
240
|
+
*
|
|
241
|
+
* Postgres publishes its port before it is ready to accept queries, so the
|
|
242
|
+
* caller still has to tolerate a first connection that is refused at the
|
|
243
|
+
* protocol level. This only answers "has the container got as far as listening",
|
|
244
|
+
* which is the part that takes the seconds.
|
|
245
|
+
*/
|
|
246
|
+
async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
|
|
247
|
+
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
while (Date.now() < deadline) {
|
|
249
|
+
if (await probeTcp(host, port, 700)) return true;
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Start the project's development database if it is not running, and give it a
|
|
256
|
+
* schema if this call is what started it.
|
|
257
|
+
*/
|
|
258
|
+
async function ensureDevDatabase(options) {
|
|
259
|
+
const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;
|
|
260
|
+
const log = options.log ?? ((message) => console.log(message));
|
|
261
|
+
if (disabled) return { action: "disabled" };
|
|
262
|
+
const target = parseLoopbackDsn(databaseUrl);
|
|
263
|
+
if (!databaseUrl) return { action: "no-dsn" };
|
|
264
|
+
if (!target) {
|
|
265
|
+
let host = "the configured host";
|
|
266
|
+
try {
|
|
267
|
+
host = new URL(databaseUrl).hostname || host;
|
|
268
|
+
} catch {}
|
|
269
|
+
return {
|
|
270
|
+
action: "remote-dsn",
|
|
271
|
+
host
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (await probeTcp(target.host, target.port)) return {
|
|
275
|
+
action: "already-running",
|
|
276
|
+
host: target.host,
|
|
277
|
+
port: target.port
|
|
278
|
+
};
|
|
279
|
+
const composePath = path.join(projectRoot, "docker-compose.yml");
|
|
280
|
+
if (!fs.existsSync(composePath)) return { action: "no-compose" };
|
|
281
|
+
const composeText = fs.readFileSync(composePath, "utf8");
|
|
282
|
+
if (!composeDeclaresDbService(composeText)) return { action: "no-compose" };
|
|
283
|
+
const manualSteps = `${chalk.cyan("docker compose up -d db")} then ${chalk.cyan("rebase db push")}`;
|
|
284
|
+
const composePort = composeHostPort(composeText);
|
|
285
|
+
if (composePort !== null && composePort !== target.port) {
|
|
286
|
+
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}.`;
|
|
287
|
+
log("");
|
|
288
|
+
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
289
|
+
return {
|
|
290
|
+
action: "wrong-port",
|
|
291
|
+
hint,
|
|
292
|
+
port: target.port,
|
|
293
|
+
composePort
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
log("");
|
|
297
|
+
log(` ${chalk.gray("Database not running — starting it…")}`);
|
|
298
|
+
const startDatabase = options.startDatabase ?? (async (root) => {
|
|
299
|
+
await execa("docker", [
|
|
300
|
+
"compose",
|
|
301
|
+
"up",
|
|
302
|
+
"-d",
|
|
303
|
+
"db"
|
|
304
|
+
], {
|
|
305
|
+
cwd: root,
|
|
306
|
+
stdio: "pipe"
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
await startDatabase(projectRoot);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
313
|
+
if (/ENOENT|not found|command not found/i.test(message)) {
|
|
314
|
+
const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;
|
|
315
|
+
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
316
|
+
return {
|
|
317
|
+
action: "no-docker",
|
|
318
|
+
hint
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const hint = `Could not start the database container. Run ${manualSteps} to see why.`;
|
|
322
|
+
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
323
|
+
return {
|
|
324
|
+
action: "start-failed",
|
|
325
|
+
hint
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (!await waitForPort(target.host, target.port)) {
|
|
329
|
+
const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;
|
|
330
|
+
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
331
|
+
return {
|
|
332
|
+
action: "start-failed",
|
|
333
|
+
hint
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
log(` ${chalk.green("✓")} ${chalk.gray(`Database ready on port ${target.port}`)}`);
|
|
337
|
+
if (!hasCollections) return {
|
|
338
|
+
action: "started",
|
|
339
|
+
port: target.port,
|
|
340
|
+
pushed: false
|
|
341
|
+
};
|
|
342
|
+
log(` ${chalk.gray("Pushing the schema…")}`);
|
|
343
|
+
try {
|
|
344
|
+
await pushSchema();
|
|
345
|
+
log(` ${chalk.green("✓")} ${chalk.gray("Schema pushed")}`);
|
|
346
|
+
return {
|
|
347
|
+
action: "started",
|
|
348
|
+
port: target.port,
|
|
349
|
+
pushed: true
|
|
350
|
+
};
|
|
351
|
+
} catch {
|
|
352
|
+
log(` ${chalk.yellow("⚠")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan("rebase db push")}.`)}`);
|
|
353
|
+
return {
|
|
354
|
+
action: "started",
|
|
355
|
+
port: target.port,
|
|
356
|
+
pushed: false
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/dev-db/branch-pointer.ts
|
|
362
|
+
/**
|
|
363
|
+
* Which database branch this checkout is working on.
|
|
364
|
+
*
|
|
365
|
+
* `rebase db branch create` made a full copy of the database in about a second
|
|
366
|
+
* and then stopped one step short of being a feature: there was no `switch`, no
|
|
367
|
+
* `--branch` on `rebase dev`, and nothing wrote the branch anywhere a command
|
|
368
|
+
* would read it. The only way to *use* a branch was to hand-edit
|
|
369
|
+
* `DATABASE_URL`, and the documentation claimed otherwise — "the CLI updates
|
|
370
|
+
* your local development configuration" — which it did not; the `.env` was
|
|
371
|
+
* byte-identical afterwards.
|
|
372
|
+
*
|
|
373
|
+
* This is that missing step, and the shape of it is chosen to avoid three
|
|
374
|
+
* things:
|
|
375
|
+
*
|
|
376
|
+
* 1. **It does not touch `.env`.** That file holds secrets, comments and the
|
|
377
|
+
* developer's own ordering, and a command that rewrites it in place will
|
|
378
|
+
* eventually lose something that was not its business. The branch is
|
|
379
|
+
* recorded beside the other per-checkout state in `.rebase/`, which is
|
|
380
|
+
* already gitignored — a branch is a fact about one machine, never about
|
|
381
|
+
* the project.
|
|
382
|
+
*
|
|
383
|
+
* 2. **It stores a name, not a connection string.** The credentials stay in
|
|
384
|
+
* exactly one place; the URL is derived by swapping the database name on
|
|
385
|
+
* the base one at resolution time. A stored URL would be a second copy of
|
|
386
|
+
* the password, and would go stale the moment `.env` changed.
|
|
387
|
+
*
|
|
388
|
+
* 3. **It does not outrank an explicit instruction.** `--database-url` and a
|
|
389
|
+
* `DATABASE_URL` in the shell are more immediate and more specific than a
|
|
390
|
+
* switch made yesterday, so they still win. The pointer sits directly above
|
|
391
|
+
* the project's `.env`: it is a deliberate, persistent choice, and `.env` is
|
|
392
|
+
* the project default it is meant to override. Anything less and `switch`
|
|
393
|
+
* would silently do nothing on every project that sets `DATABASE_URL` —
|
|
394
|
+
* which is every project not using the managed database.
|
|
395
|
+
*/
|
|
396
|
+
/** Where the pointer lives, next to the rest of the per-checkout state. */
|
|
397
|
+
function branchPointerPath(projectRoot) {
|
|
398
|
+
return path.join(projectRoot, ".rebase", "branch.json");
|
|
399
|
+
}
|
|
400
|
+
function branchDatabaseName(name) {
|
|
401
|
+
return `rb_${name}`;
|
|
402
|
+
}
|
|
403
|
+
/** Read the active branch, or null when the checkout is on the main database. */
|
|
404
|
+
function readActiveBranch(projectRoot) {
|
|
405
|
+
try {
|
|
406
|
+
const raw = fs.readFileSync(branchPointerPath(projectRoot), "utf-8");
|
|
407
|
+
const parsed = JSON.parse(raw);
|
|
408
|
+
if (typeof parsed.name !== "string" || typeof parsed.database !== "string") return null;
|
|
409
|
+
if (!parsed.name || !parsed.database) return null;
|
|
410
|
+
return {
|
|
411
|
+
name: parsed.name,
|
|
412
|
+
database: parsed.database
|
|
413
|
+
};
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function writeActiveBranch(projectRoot, branch) {
|
|
419
|
+
const file = branchPointerPath(projectRoot);
|
|
420
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
421
|
+
fs.writeFileSync(file, `${JSON.stringify(branch, null, 2)}\n`, "utf-8");
|
|
422
|
+
}
|
|
423
|
+
/** Return to the main database. Absent is success — this is idempotent. */
|
|
424
|
+
function clearActiveBranch(projectRoot) {
|
|
425
|
+
fs.rmSync(branchPointerPath(projectRoot), { force: true });
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The base connection string with its database swapped for the branch's.
|
|
429
|
+
*
|
|
430
|
+
* Everything else is carried across untouched — credentials, host, port, and
|
|
431
|
+
* the query string, which is load-bearing here: `?sslmode=disable` is what a
|
|
432
|
+
* local Docker Postgres needs, and dropping it turns a switch into a TLS error
|
|
433
|
+
* that says nothing about branches.
|
|
434
|
+
*
|
|
435
|
+
* Returns null for a connection string that cannot be parsed, so the caller can
|
|
436
|
+
* say so plainly rather than emitting a mangled URL that fails later.
|
|
437
|
+
*/
|
|
438
|
+
function branchUrl(baseUrl, database) {
|
|
439
|
+
try {
|
|
440
|
+
const url = new URL(baseUrl);
|
|
441
|
+
url.pathname = `/${database}`;
|
|
442
|
+
return url.toString();
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** The database name in a connection string, for reporting what you are on. */
|
|
448
|
+
function databaseNameOf(connectionString) {
|
|
449
|
+
try {
|
|
450
|
+
return new URL(connectionString).pathname.replace(/^\//, "") || null;
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/dev-db/daemon.ts
|
|
457
|
+
/**
|
|
458
|
+
* Starting, finding and stopping the managed database, from the caller's side.
|
|
459
|
+
*
|
|
460
|
+
* Every command that needs Postgres calls {@link ensureManagedDatabase} and
|
|
461
|
+
* gets a connection string back. Whether that started a process or found one
|
|
462
|
+
* already running is not the caller's business, which is the point: `rebase
|
|
463
|
+
* db push`, `rebase dev` and `rebase studio` in three terminals must all reach
|
|
464
|
+
* the same database without coordinating, because two processes opening one
|
|
465
|
+
* PGlite data directory would corrupt it.
|
|
466
|
+
*
|
|
467
|
+
* The hard part is not starting the daemon; it is deciding whether the one the
|
|
468
|
+
* state file describes is still there. A pid can be recycled after a reboot and
|
|
469
|
+
* a port can be taken by a stranger, so believing either on its own would let
|
|
470
|
+
* Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the
|
|
471
|
+
* daemon to identify itself instead.
|
|
472
|
+
*/
|
|
473
|
+
/** How long to wait for a first boot. PGlite runs initdb on an empty data dir. */
|
|
474
|
+
var START_TIMEOUT_MS = 6e4;
|
|
475
|
+
/** Poll interval while waiting for the daemon to publish its state file. */
|
|
476
|
+
var POLL_INTERVAL_MS = 150;
|
|
477
|
+
/** How long the identity handshake may take before we call it dead. */
|
|
478
|
+
var IDENTITY_TIMEOUT_MS = 1500;
|
|
479
|
+
/**
|
|
480
|
+
* PGlite's built-in superuser and database.
|
|
481
|
+
*
|
|
482
|
+
* Fixed by PGlite rather than chosen here, and asserted by the socket spike:
|
|
483
|
+
* `session_user` comes back as `postgres`.
|
|
484
|
+
*/
|
|
485
|
+
var MANAGED_USER = "postgres";
|
|
486
|
+
var MANAGED_DATABASE = "postgres";
|
|
487
|
+
/**
|
|
488
|
+
* The DSN for the managed development database.
|
|
489
|
+
*
|
|
490
|
+
* **`sslmode=disable` is not decoration.** PGlite's socket server speaks no TLS,
|
|
491
|
+
* and libpq-family clients try SSL first — so Atlas, which `rebase db push`
|
|
492
|
+
* shells out to, failed the whole push with
|
|
493
|
+
* `pq: SSL is not enabled on the server`. Its remedy box then told the reader to
|
|
494
|
+
* append `sslmode=disable` to `DATABASE_URL`, which on this path is not set at
|
|
495
|
+
* all: `rebase init` leaves it commented out precisely so the managed database
|
|
496
|
+
* is used. The advice was correct and unfollowable.
|
|
497
|
+
*
|
|
498
|
+
* Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything
|
|
499
|
+
* a driver shells out to later — rather than teaching each one about PGlite.
|
|
500
|
+
*/
|
|
501
|
+
/**
|
|
502
|
+
* The end of `pglite.log`, for an error message that has to carry its own reason.
|
|
503
|
+
*
|
|
504
|
+
* The daemon writes its output to a file because it is detached — it has to
|
|
505
|
+
* outlive the command that started it. That is right, and it means every
|
|
506
|
+
* startup failure used to be reported as a path: "see pglite.log for the
|
|
507
|
+
* reason". On a laptop that is a fair trade. In CI it is not a trade at all,
|
|
508
|
+
* because the runner discards the workspace: the log named in the message no
|
|
509
|
+
* longer exists by the time anybody reads the message, so a failed run says
|
|
510
|
+
* only that something failed.
|
|
511
|
+
*
|
|
512
|
+
* So the tail comes with the error. Bounded, because a log that has been
|
|
513
|
+
* appended to across many runs is not something to paste in full.
|
|
514
|
+
*/
|
|
515
|
+
function pgliteLogTail(projectRoot, lines = 12) {
|
|
516
|
+
try {
|
|
517
|
+
const contents = fs.readFileSync(path.join(devDbDir(projectRoot), "pglite.log"), "utf8").trimEnd();
|
|
518
|
+
if (!contents) return " (pglite.log is empty — the daemon wrote nothing before exiting)";
|
|
519
|
+
return contents.split("\n").slice(-lines).map((line) => ` ${line}`).join("\n");
|
|
520
|
+
} catch {
|
|
521
|
+
return " (pglite.log could not be read)";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function managedUrl(port) {
|
|
525
|
+
return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}?sslmode=disable`;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Ask the process behind a state record to prove it is the one we wrote down.
|
|
529
|
+
*
|
|
530
|
+
* A pid check alone answers "is *a* process running", and a port check alone
|
|
531
|
+
* answers "is *something* listening" — after a reboot both say yes about
|
|
532
|
+
* strangers. The daemon answers with the token from its own state file, so a
|
|
533
|
+
* match is the only evidence accepted.
|
|
534
|
+
*/
|
|
535
|
+
function isDaemonAlive(state) {
|
|
536
|
+
if (!pidRunning(state.pid)) return Promise.resolve(false);
|
|
537
|
+
return new Promise((resolve) => {
|
|
538
|
+
const socket = new net.Socket();
|
|
539
|
+
let answer = "";
|
|
540
|
+
const settle = (alive) => {
|
|
541
|
+
socket.removeAllListeners();
|
|
542
|
+
socket.destroy();
|
|
543
|
+
resolve(alive);
|
|
544
|
+
};
|
|
545
|
+
socket.setTimeout(IDENTITY_TIMEOUT_MS);
|
|
546
|
+
socket.once("timeout", () => settle(false));
|
|
547
|
+
socket.once("error", () => settle(false));
|
|
548
|
+
socket.on("data", (chunk) => {
|
|
549
|
+
answer += chunk.toString("utf8");
|
|
550
|
+
if (answer.includes("\n")) settle(answer.trim() === `rebase-dev-db ${state.token}`);
|
|
551
|
+
});
|
|
552
|
+
socket.once("close", () => settle(answer.trim() === `rebase-dev-db ${state.token}`));
|
|
553
|
+
socket.connect(state.identityPort, "127.0.0.1");
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/** The running daemon for this project, or `null`. Never starts anything. */
|
|
557
|
+
async function findRunningDaemon(projectRoot) {
|
|
558
|
+
const state = readState(projectRoot);
|
|
559
|
+
if (!state) return null;
|
|
560
|
+
if (await isDaemonAlive(state)) return state;
|
|
561
|
+
clearState(projectRoot);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Resolve the CLI entry point, so the daemon can be spawned as ourselves.
|
|
566
|
+
*
|
|
567
|
+
* A hidden subcommand rather than a second build entry: under `tsx` this file
|
|
568
|
+
* is `src/dev-db/daemon.ts` and in a published CLI it is bundled into
|
|
569
|
+
* `dist/index.js`, and in both cases the executable to re-invoke is the one
|
|
570
|
+
* already running. `rebase dev` re-runs itself the same way to regenerate the
|
|
571
|
+
* SDK types.
|
|
572
|
+
*/
|
|
573
|
+
function resolveCliEntry() {
|
|
574
|
+
const argvEntry = process.argv[1];
|
|
575
|
+
if (argvEntry && fs.existsSync(argvEntry)) return argvEntry;
|
|
576
|
+
return fileURLToPath(new URL("data:video/mp2t;base64,LyoqCiAqIFRoZSBDTEkncyBwdWJsaWMgZW50cnkg4oCUIGRlbGliZXJhdGVseSBzbWFsbC4KICoKICogVGhpcyBmaWxlIHVzZWQgdG8gYGV4cG9ydCAqYCBmcm9tIHNpeHRlZW4gbW9kdWxlczogZXZlcnkgY29tbWFuZCBmdW5jdGlvbiwKICogYGRldGVjdFBhY2thZ2VNYW5hZ2VyYCwgYGZpbmRQcm9qZWN0Um9vdGAsIHRoZSB3aG9sZSBvZiBgY29tbWFuZHMvY2xvdWRgLiBOb25lCiAqIG9mIGl0IHdhcyBpbXBvcnRlZCBieSBhbnl0aGluZywgaW4gdGhpcyByZXBvc2l0b3J5IG9yIHRoZSBjb250cm9sIHBsYW5lLCBhbmQKICogbm9uZSBvZiBpdCB3YXMgbWVhbnQgdG8gYmU6IGBAcmViYXNlcHJvL2NsaWAgaXMgYSBwcm9ncmFtLCBhbmQgYSBwcm9ncmFtJ3MKICogaW50ZXJuYWxzIGJlY29tZSBhbiBBUEkgdGhlIG1vbWVudCB0aGV5IGFyZSBleHBvcnRlZCBhbmQgcHVibGlzaGVkLiBUaGUKICogYmFycmVsIHdhcyB3cml0dGVuIHRoZSB3YXkgYmFycmVscyBnZXQgd3JpdHRlbiDigJQgb25lIGxpbmUgcGVyIGZpbGUg4oCUIGFuZCB0aGUKICogc3VyZmFjZSBpdCBjcmVhdGVkIHdhcyBhbiBhY2NpZGVudCBvZiB0aGF0IGNvbnZlbnRpb24gcmF0aGVyIHRoYW4gYSBkZWNpc2lvbi4KICoKICogVGhyZWUgdGhpbmdzIGFyZSBleHBvcnRlZCwgZWFjaCBmb3IgYSBzdGF0ZWQgcmVhc29uLgogKi8KCi8qKgogKiBXaGF0IGBiaW4vcmViYXNlLmpzYCBjYWxscy4gVGhlIGJpbmFyeSBpbXBvcnRzIGBkaXN0L2luZGV4LmVzLmpzYCBhbmQgaW52b2tlcwogKiBgZW50cnkoKWAsIHNvIHRoaXMgaXMgdGhlIG9uZSBleHBvcnQgdGhlIENMSSBjYW5ub3QgcnVuIHdpdGhvdXQuCiAqLwpleHBvcnQgeyBlbnRyeSB9IGZyb20gIi4vY2xpIjsKCi8qKgogKiBUaGUgbWFuaWZlc3QgYW5kIGJ1bmRsZSBjb250cmFjdHMuCiAqCiAqIEV4cG9ydGVkIGJlY2F1c2UgdmFsaWRhdGluZyB0aGVtIGlzIG5vdCBvbmx5IHRoZSBDTEkncyBqb2I6IGEgY29udHJvbCBwbGFuZQogKiBoYXMgdG8gZGVjaWRlIHdoZXRoZXIgYSBzdWJtaXR0ZWQgYnVuZGxlIGNhbiBydW4gYmVmb3JlIGl0IGRlcGxveXMgaXQsIGFuZCBpdAogKiBtdXN0IHJlYWNoIHRoYXQgdmVyZGljdCB3aXRoIHRoZSBzYW1lIGNvZGUgdGhhdCBwcm9kdWNlZCB0aGUgYXJ0aWZhY3QuCiAqLwpleHBvcnQgKiBmcm9tICIuL21hbmlmZXN0IjsKZXhwb3J0ICogZnJvbSAiLi9idW5kbGUiOwo=", "" + import.meta.url));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* How to re-invoke ourselves, which differs between a published CLI and this
|
|
580
|
+
* repository.
|
|
581
|
+
*
|
|
582
|
+
* A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.
|
|
583
|
+
* Inside the monorepo the entry is TypeScript, which plain `node` cannot load —
|
|
584
|
+
* so the daemon has to be started through the same loader that is running now.
|
|
585
|
+
* Getting this wrong fails as a spawn that exits instantly with a syntax error,
|
|
586
|
+
* which is why the caller reads `pglite.log` on failure.
|
|
587
|
+
*/
|
|
588
|
+
function resolveSpawn(entry) {
|
|
589
|
+
if (!/\.[cm]?ts$/.test(entry)) return {
|
|
590
|
+
execPath: process.execPath,
|
|
591
|
+
prefixArgs: []
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
execPath: process.execPath,
|
|
595
|
+
prefixArgs: ["--import", "tsx"]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Ask the running daemon for one additional database, and get its port.
|
|
600
|
+
*
|
|
601
|
+
* The identity socket doubles as the command channel: the daemon answers
|
|
602
|
+
* with its token first, which is how the caller knows it is talking to the
|
|
603
|
+
* daemon the state file describes and not to a stranger on a recycled port.
|
|
604
|
+
*/
|
|
605
|
+
function ensureAdditionalDatabase(state, key) {
|
|
606
|
+
return new Promise((resolve, reject) => {
|
|
607
|
+
const socket = new net.Socket();
|
|
608
|
+
let answer = "";
|
|
609
|
+
let identified = false;
|
|
610
|
+
const fail = (message) => {
|
|
611
|
+
socket.removeAllListeners();
|
|
612
|
+
socket.destroy();
|
|
613
|
+
reject(new Error(message));
|
|
614
|
+
};
|
|
615
|
+
socket.setTimeout(START_TIMEOUT_MS);
|
|
616
|
+
socket.once("timeout", () => fail(`The development database did not answer for "${key}" within ${Math.round(START_TIMEOUT_MS / 1e3)}s.`));
|
|
617
|
+
socket.once("error", (err) => fail(`Could not reach the development database for "${key}": ${err.message}`));
|
|
618
|
+
socket.on("data", (chunk) => {
|
|
619
|
+
answer += chunk.toString("utf8");
|
|
620
|
+
let newline = answer.indexOf("\n");
|
|
621
|
+
while (newline !== -1) {
|
|
622
|
+
const line = answer.slice(0, newline).trim();
|
|
623
|
+
answer = answer.slice(newline + 1);
|
|
624
|
+
if (!identified) {
|
|
625
|
+
if (line !== `rebase-dev-db ${state.token}`) return fail("The process on the development database's port is not the daemon.");
|
|
626
|
+
identified = true;
|
|
627
|
+
socket.write(`ensure ${key}\n`);
|
|
628
|
+
} else if (line.startsWith("ok ")) {
|
|
629
|
+
const port = Number(line.slice(3));
|
|
630
|
+
socket.removeAllListeners();
|
|
631
|
+
socket.destroy();
|
|
632
|
+
if (!Number.isInteger(port) || port <= 0) return reject(/* @__PURE__ */ new Error(`The development database answered with an invalid port for "${key}": ${line}`));
|
|
633
|
+
return resolve(port);
|
|
634
|
+
} else return fail(`The development database could not serve "${key}": ${line.replace(/^error\s*/, "")}`);
|
|
635
|
+
newline = answer.indexOf("\n");
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
socket.connect(state.identityPort, "127.0.0.1");
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
/** Every additional database the caller asked for, as connection strings. */
|
|
642
|
+
async function adoptWithAdditional(state, started, keys) {
|
|
643
|
+
const additional = {};
|
|
644
|
+
for (const key of keys) {
|
|
645
|
+
const known = state.databases?.[key];
|
|
646
|
+
additional[key] = managedUrl(known ? known.port : await ensureAdditionalDatabase(state, key));
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
...adopt(state, started),
|
|
650
|
+
additional
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The project's managed database, started if it is not already running.
|
|
655
|
+
*
|
|
656
|
+
* Safe to call concurrently from several commands: the loser of the race finds
|
|
657
|
+
* the winner's state file during its poll and adopts it rather than starting a
|
|
658
|
+
* second daemon.
|
|
659
|
+
*/
|
|
660
|
+
async function ensureManagedDatabase(projectRoot, options = {}) {
|
|
661
|
+
const keys = options.additionalKeys ?? [];
|
|
662
|
+
const existing = await findRunningDaemon(projectRoot);
|
|
663
|
+
if (existing) return adoptWithAdditional(existing, false, keys);
|
|
664
|
+
fs.mkdirSync(devDbDir(projectRoot), { recursive: true });
|
|
665
|
+
ensureGitignore(projectRoot);
|
|
666
|
+
if (!acquireStartLock(projectRoot, START_TIMEOUT_MS)) {
|
|
667
|
+
const adopted = await waitForDaemon(projectRoot, START_TIMEOUT_MS);
|
|
668
|
+
if (adopted) return adoptWithAdditional(adopted, false, keys);
|
|
669
|
+
if (!acquireStartLock(projectRoot, 0)) throw new Error(`Another process is starting the development database and did not finish.
|
|
670
|
+
Remove ${path.join(devDbDir(projectRoot), "starting.lock")} if nothing is running.`);
|
|
671
|
+
}
|
|
672
|
+
const port = await findFreePort();
|
|
673
|
+
const token = randomBytes(16).toString("hex");
|
|
674
|
+
const entry = options.entry ?? resolveCliEntry();
|
|
675
|
+
const spawnPlan = options.spawn ?? resolveSpawn(entry);
|
|
676
|
+
const args = [
|
|
677
|
+
...spawnPlan.prefixArgs,
|
|
678
|
+
entry,
|
|
679
|
+
"__dev-db-daemon",
|
|
680
|
+
"--project",
|
|
681
|
+
projectRoot,
|
|
682
|
+
"--port",
|
|
683
|
+
String(port),
|
|
684
|
+
"--token",
|
|
685
|
+
token
|
|
686
|
+
];
|
|
687
|
+
if (options.idleTimeoutMs !== void 0) args.push("--idle-timeout", String(options.idleTimeoutMs));
|
|
688
|
+
const log = fs.openSync(path.join(devDbDir(projectRoot), "pglite.log"), "a");
|
|
689
|
+
const child = spawn(spawnPlan.execPath, args, {
|
|
690
|
+
detached: true,
|
|
691
|
+
stdio: [
|
|
692
|
+
"ignore",
|
|
693
|
+
log,
|
|
694
|
+
log
|
|
695
|
+
]
|
|
696
|
+
});
|
|
697
|
+
child.unref();
|
|
698
|
+
try {
|
|
699
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
700
|
+
let announced = false;
|
|
701
|
+
while (Date.now() < deadline) {
|
|
702
|
+
const state = readState(projectRoot);
|
|
703
|
+
if (state && await isDaemonAlive(state)) return adoptWithAdditional(state, true, keys);
|
|
704
|
+
if (!announced && !options.quiet) {
|
|
705
|
+
announced = true;
|
|
706
|
+
options.onProgress?.("Starting the development database…");
|
|
707
|
+
}
|
|
708
|
+
if (child.exitCode !== null && child.exitCode !== 0) throw new Error(`The development database failed to start (exit ${child.exitCode}).\n ${path.join(devDbDir(projectRoot), "pglite.log")} ends:\n` + pgliteLogTail(projectRoot));
|
|
709
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
710
|
+
}
|
|
711
|
+
throw new Error(`The development database did not start within ${Math.round(START_TIMEOUT_MS / 1e3)}s.\n ${path.join(devDbDir(projectRoot), "pglite.log")} ends:\n` + pgliteLogTail(projectRoot) + "\n To use your own Postgres instead, set DATABASE_URL or pass --database-url.");
|
|
712
|
+
} finally {
|
|
713
|
+
releaseStartLock(projectRoot);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/** Shape a live state record as the caller's result. */
|
|
717
|
+
function adopt(state, started) {
|
|
718
|
+
return {
|
|
719
|
+
url: managedUrl(state.port),
|
|
720
|
+
dataDir: state.dataDir,
|
|
721
|
+
port: state.port,
|
|
722
|
+
pid: state.pid,
|
|
723
|
+
started,
|
|
724
|
+
additional: {}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/** Poll until another process publishes a live daemon, or give up. */
|
|
728
|
+
async function waitForDaemon(projectRoot, timeoutMs) {
|
|
729
|
+
const deadline = Date.now() + timeoutMs;
|
|
730
|
+
while (Date.now() < deadline) {
|
|
731
|
+
const state = readState(projectRoot);
|
|
732
|
+
if (state && await isDaemonAlive(state)) return state;
|
|
733
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
/** Stop the daemon. Returns false when there was nothing running. */
|
|
738
|
+
async function stopManagedDatabase(projectRoot) {
|
|
739
|
+
const state = await findRunningDaemon(projectRoot);
|
|
740
|
+
if (!state) return false;
|
|
741
|
+
try {
|
|
742
|
+
process.kill(state.pid, "SIGTERM");
|
|
743
|
+
} catch {
|
|
744
|
+
clearState(projectRoot);
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
const deadline = Date.now() + 1e4;
|
|
748
|
+
while (Date.now() < deadline) {
|
|
749
|
+
if (!fs.existsSync(stateFile(projectRoot))) return true;
|
|
750
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
751
|
+
}
|
|
752
|
+
clearState(projectRoot);
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Stop the daemon and delete the data directory.
|
|
757
|
+
*
|
|
758
|
+
* Destructive and deliberately not clever: it removes the whole directory
|
|
759
|
+
* rather than dropping schemas, because "give me an empty database" is the only
|
|
760
|
+
* thing anyone means by it.
|
|
761
|
+
*/
|
|
762
|
+
async function resetManagedDatabase(projectRoot) {
|
|
763
|
+
await stopManagedDatabase(projectRoot);
|
|
764
|
+
fs.rmSync(dataDir(projectRoot), {
|
|
765
|
+
recursive: true,
|
|
766
|
+
force: true
|
|
767
|
+
});
|
|
768
|
+
const dir = devDbDir(projectRoot);
|
|
769
|
+
if (fs.existsSync(dir)) {
|
|
770
|
+
for (const entry of fs.readdirSync(dir)) if (entry.startsWith("pgdata__")) fs.rmSync(path.join(dir, entry), {
|
|
771
|
+
recursive: true,
|
|
772
|
+
force: true
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
clearState(projectRoot);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Keep the generated directory out of git.
|
|
779
|
+
*
|
|
780
|
+
* `.rebase/` holds a Postgres data directory and a log. A project that commits
|
|
781
|
+
* it will push hundreds of megabytes and a database that only makes sense on
|
|
782
|
+
* one machine, so the ignore file is written next to the data rather than
|
|
783
|
+
* relying on the project's root `.gitignore` having been updated.
|
|
784
|
+
*/
|
|
785
|
+
function ensureGitignore(projectRoot) {
|
|
786
|
+
const target = path.join(devDbDir(projectRoot), ".gitignore");
|
|
787
|
+
if (fs.existsSync(target)) return;
|
|
788
|
+
fs.writeFileSync(target, "# Generated by `rebase dev` — a local Postgres data directory and its log.\n# Machine-specific and large; never commit it.\n*\n", "utf8");
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/dev-db/resolve.ts
|
|
792
|
+
/**
|
|
793
|
+
* A value that is present but empty is treated as absent.
|
|
794
|
+
*
|
|
795
|
+
* `DATABASE_URL=` in a `.env` is what a developer writes when they mean "not
|
|
796
|
+
* this one" — honouring it literally would hand an empty connection string to
|
|
797
|
+
* libpq and produce an error about a missing host, which explains nothing.
|
|
798
|
+
*/
|
|
799
|
+
function present(value) {
|
|
800
|
+
if (typeof value !== "string") return null;
|
|
801
|
+
const trimmed = value.trim();
|
|
802
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
803
|
+
}
|
|
804
|
+
function resolveDevDatabase(input = {}) {
|
|
805
|
+
const flagUrl = present(input.flagUrl);
|
|
806
|
+
if (flagUrl) return {
|
|
807
|
+
kind: "external",
|
|
808
|
+
url: flagUrl,
|
|
809
|
+
source: "flag"
|
|
810
|
+
};
|
|
811
|
+
const fromEnvironment = present(input.env?.DATABASE_URL);
|
|
812
|
+
if (fromEnvironment) return {
|
|
813
|
+
kind: "external",
|
|
814
|
+
url: fromEnvironment,
|
|
815
|
+
source: "environment"
|
|
816
|
+
};
|
|
817
|
+
const fromBranch = present(input.branch?.url);
|
|
818
|
+
if (fromBranch && input.branch) return {
|
|
819
|
+
kind: "external",
|
|
820
|
+
url: fromBranch,
|
|
821
|
+
source: "branch",
|
|
822
|
+
branch: input.branch.name
|
|
823
|
+
};
|
|
824
|
+
const fromEnvFile = present(input.envFile?.DATABASE_URL);
|
|
825
|
+
if (fromEnvFile) return {
|
|
826
|
+
kind: "external",
|
|
827
|
+
url: fromEnvFile,
|
|
828
|
+
source: "env-file"
|
|
829
|
+
};
|
|
830
|
+
if (input.flagDocker) return {
|
|
831
|
+
kind: "docker",
|
|
832
|
+
source: "docker",
|
|
833
|
+
url: present(input.composeUrl)
|
|
834
|
+
};
|
|
835
|
+
return {
|
|
836
|
+
kind: "managed",
|
|
837
|
+
source: "managed"
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/** One line for the startup banner, naming both the database and why. */
|
|
841
|
+
function describeDevDatabase(database) {
|
|
842
|
+
switch (database.kind) {
|
|
843
|
+
case "external":
|
|
844
|
+
switch (database.source) {
|
|
845
|
+
case "flag": return "your database (--database-url)";
|
|
846
|
+
case "environment": return "your database (DATABASE_URL in the environment)";
|
|
847
|
+
case "env-file": return "your database (DATABASE_URL in .env)";
|
|
848
|
+
case "branch": return `branch "${database.branch}" (rebase db branch switch)`;
|
|
849
|
+
}
|
|
850
|
+
break;
|
|
851
|
+
case "docker": return "Postgres in Docker";
|
|
852
|
+
case "managed": return "the managed development database (PGlite)";
|
|
853
|
+
}
|
|
854
|
+
throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/dev-db/prepare.ts
|
|
858
|
+
/**
|
|
859
|
+
* The one place a command asks "which database, and how do I reach it?".
|
|
860
|
+
*
|
|
861
|
+
* Every command that touches Postgres — `dev`, and the whole `db` namespace
|
|
862
|
+
* through the driver plugin — goes through {@link prepareDatabaseEnv}. It
|
|
863
|
+
* resolves the ordering in `resolve.ts`, starts the managed database if that is
|
|
864
|
+
* what the ordering chose, and hands back the environment additions the child
|
|
865
|
+
* process needs.
|
|
866
|
+
*
|
|
867
|
+
* Two things it deliberately does *not* do:
|
|
868
|
+
*
|
|
869
|
+
* - **It never overwrites an existing `DATABASE_URL`.** When the developer has
|
|
870
|
+
* named a database, the returned environment is empty and the child inherits
|
|
871
|
+
* exactly what it would have inherited before this feature existed. A
|
|
872
|
+
* migration must never be redirected away from the database its author meant.
|
|
873
|
+
*
|
|
874
|
+
* - **It never starts anything for a command that is not going to connect.**
|
|
875
|
+
* `--help` and argument errors are handled by the caller before this is
|
|
876
|
+
* reached, because booting a Postgres to print usage would be absurd.
|
|
877
|
+
*/
|
|
878
|
+
/**
|
|
879
|
+
* The branch this checkout is switched to, as a connection string.
|
|
880
|
+
*
|
|
881
|
+
* Derived rather than stored: the pointer holds a name, and the credentials
|
|
882
|
+
* stay in the one place the developer wrote them. The base is the project's
|
|
883
|
+
* `.env` — a switch is a statement about *this project's* database, so a
|
|
884
|
+
* `DATABASE_URL` that happens to be in the shell is not the thing being
|
|
885
|
+
* branched, and it outranks the branch anyway.
|
|
886
|
+
*
|
|
887
|
+
* Returns null when there is no branch, and also when there is no base to swap
|
|
888
|
+
* the database name on: a project with no `DATABASE_URL` is on the managed
|
|
889
|
+
* database, where branching does not work at all.
|
|
890
|
+
*
|
|
891
|
+
* Exported for its tests. Asserting this through {@link prepareDatabaseEnv}
|
|
892
|
+
* would start a real PGlite daemon to check a decision made before any daemon
|
|
893
|
+
* is involved.
|
|
894
|
+
*/
|
|
895
|
+
function resolveActiveBranch(projectRoot, envFile) {
|
|
896
|
+
const active = readActiveBranch(projectRoot);
|
|
897
|
+
if (!active) return null;
|
|
898
|
+
const base = envFile.DATABASE_URL?.trim();
|
|
899
|
+
if (!base) return null;
|
|
900
|
+
const url = branchUrl(base, active.database);
|
|
901
|
+
return url ? {
|
|
902
|
+
name: active.name,
|
|
903
|
+
url
|
|
904
|
+
} : null;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* The compose `db` service's connection string for this project, or null.
|
|
908
|
+
*
|
|
909
|
+
* Read here rather than in `resolve.ts` so the ordering there stays pure, and
|
|
910
|
+
* exported so `rebase dev` can hand the same URL to the preflight that starts
|
|
911
|
+
* the container — the preflight decides "local, and not running" from a DSN,
|
|
912
|
+
* so without one `--docker` asked for a container nothing ever started.
|
|
913
|
+
*/
|
|
914
|
+
function resolveComposeUrl(projectRoot, envFile) {
|
|
915
|
+
const composePath = path.join(projectRoot, "docker-compose.yml");
|
|
916
|
+
if (!fs.existsSync(composePath)) return null;
|
|
917
|
+
try {
|
|
918
|
+
return composeDatabaseUrl(fs.readFileSync(composePath, "utf8"), envFile);
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Which database a project is on, decided without starting anything.
|
|
925
|
+
*
|
|
926
|
+
* `resolveDevDatabase` is pure, so the answer is available to help text and to
|
|
927
|
+
* a driver child alike — and neither should pay for a daemon to learn it. The
|
|
928
|
+
* driver cannot work it out for itself: on the managed path `DATABASE_URL` is a
|
|
929
|
+
* perfectly ordinary connection string to a Postgres on loopback, so a child
|
|
930
|
+
* handed only that has no way to know that `rebase db generate` will be refused.
|
|
931
|
+
*
|
|
932
|
+
* Null when there is no project here, which is legal — `rebase db --help` is
|
|
933
|
+
* answered outside one — and when anything at all goes wrong reading the
|
|
934
|
+
* project, because help text must not fail to print over an unreadable `.env`.
|
|
935
|
+
*/
|
|
936
|
+
function devDatabaseKind(projectRoot) {
|
|
937
|
+
try {
|
|
938
|
+
const root = projectRoot ?? findProjectRoot();
|
|
939
|
+
if (!root) return null;
|
|
940
|
+
const envFile = readEnvFile(root);
|
|
941
|
+
return resolveDevDatabase({
|
|
942
|
+
env: process.env,
|
|
943
|
+
envFile,
|
|
944
|
+
branch: resolveActiveBranch(root, envFile),
|
|
945
|
+
composeUrl: resolveComposeUrl(root, envFile)
|
|
946
|
+
}).kind;
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* The variable a driver child reads {@link devDatabaseKind} from.
|
|
953
|
+
*
|
|
954
|
+
* One name, exported, because three commands build that child's environment and
|
|
955
|
+
* a fourth reads it in another package.
|
|
956
|
+
*/
|
|
957
|
+
var DEV_DATABASE_KIND_ENV = "REBASE_DEV_DATABASE_KIND";
|
|
958
|
+
/**
|
|
959
|
+
* The variable that tells a server started on a branch which database it was
|
|
960
|
+
* branched from.
|
|
961
|
+
*
|
|
962
|
+
* The branch registry lives in that database, and a branch database holds
|
|
963
|
+
* only a snapshot of it taken when it was copied — so a server on a branch
|
|
964
|
+
* keeps its branches, and copies new ones from, the database named here. A
|
|
965
|
+
* branch URL does not say which that is; only this checkout's `.env` does.
|
|
966
|
+
*/
|
|
967
|
+
var BRANCH_PARENT_DATABASE_ENV = "REBASE_BRANCH_PARENT_DATABASE";
|
|
968
|
+
/**
|
|
969
|
+
* Resolve, start if needed, and describe the database for this command.
|
|
970
|
+
*
|
|
971
|
+
* `projectRoot` is where the managed database's data lives, so two projects on
|
|
972
|
+
* one machine get two databases without either being told about the other.
|
|
973
|
+
*/
|
|
974
|
+
async function prepareDatabaseEnv(projectRoot, options = {}) {
|
|
975
|
+
const envFile = readEnvFile(projectRoot);
|
|
976
|
+
const database = resolveDevDatabase({
|
|
977
|
+
flagUrl: options.flagUrl,
|
|
978
|
+
flagDocker: options.flagDocker,
|
|
979
|
+
env: process.env,
|
|
980
|
+
envFile,
|
|
981
|
+
branch: resolveActiveBranch(projectRoot, envFile),
|
|
982
|
+
composeUrl: resolveComposeUrl(projectRoot, envFile)
|
|
983
|
+
});
|
|
984
|
+
const description = describeDevDatabase(database);
|
|
985
|
+
if (database.kind === "external" && (database.source === "branch" || database.source === "flag")) {
|
|
986
|
+
const parent = database.source === "branch" ? databaseNameOf(envFile.DATABASE_URL?.trim() ?? "") : null;
|
|
987
|
+
return {
|
|
988
|
+
database,
|
|
989
|
+
env: {
|
|
990
|
+
DATABASE_URL: database.url,
|
|
991
|
+
...parent ? { [BRANCH_PARENT_DATABASE_ENV]: parent } : {}
|
|
992
|
+
},
|
|
993
|
+
description
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
if (database.kind === "docker") {
|
|
997
|
+
if (!database.url) throw new Error("--docker needs a docker-compose.yml with a db service in this project, and this one has none that names POSTGRES_USER, POSTGRES_DB and a published port. Set DATABASE_URL in .env to point at the database you mean instead.");
|
|
998
|
+
return {
|
|
999
|
+
database,
|
|
1000
|
+
env: { DATABASE_URL: database.url },
|
|
1001
|
+
description
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (database.kind !== "managed") return {
|
|
1005
|
+
database,
|
|
1006
|
+
env: {},
|
|
1007
|
+
description
|
|
1008
|
+
};
|
|
1009
|
+
const unbound = (options.additionalKeys ?? []).filter((key) => {
|
|
1010
|
+
const name = `DATABASE_URL${resourceEnvSuffix(key)}`;
|
|
1011
|
+
return !process.env[name] && !envFile[name];
|
|
1012
|
+
});
|
|
1013
|
+
const managed = await ensureManagedDatabase(projectRoot, {
|
|
1014
|
+
quiet: options.quiet,
|
|
1015
|
+
onProgress: options.onProgress,
|
|
1016
|
+
additionalKeys: unbound
|
|
1017
|
+
});
|
|
1018
|
+
const additionalEnv = {};
|
|
1019
|
+
for (const [key, url] of Object.entries(managed.additional)) additionalEnv[`DATABASE_URL${resourceEnvSuffix(key)}`] = url;
|
|
1020
|
+
return {
|
|
1021
|
+
database,
|
|
1022
|
+
description,
|
|
1023
|
+
dataDir: managed.dataDir,
|
|
1024
|
+
startedDaemon: managed.started,
|
|
1025
|
+
additional: managed.additional,
|
|
1026
|
+
env: {
|
|
1027
|
+
DATABASE_URL: managed.url,
|
|
1028
|
+
REBASE_DB_POOL_MAX: String(1),
|
|
1029
|
+
...additionalEnv
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* The lines to print about a managed database, in the order to print them.
|
|
1035
|
+
*
|
|
1036
|
+
* Returned rather than printed so the caller decides where they go — `dev` has
|
|
1037
|
+
* a banner, `db push` has a single line above its own output — and so a test can
|
|
1038
|
+
* assert on them without capturing a stream.
|
|
1039
|
+
*/
|
|
1040
|
+
function managedNotices(prepared) {
|
|
1041
|
+
if (prepared.database.kind !== "managed") return [];
|
|
1042
|
+
const lines = [`Using ${prepared.description}.`];
|
|
1043
|
+
if (prepared.dataDir) lines.push(`Data: ${prepared.dataDir}`);
|
|
1044
|
+
for (const key of Object.keys(prepared.additional ?? {})) lines.push(`Also serving database "${key}" as DATABASE_URL${resourceEnvSuffix(key)}`);
|
|
1045
|
+
for (const limitation of MANAGED_LIMITATIONS) lines.push(`${limitation.summary} ${limitation.remedy}`);
|
|
1046
|
+
lines.push("To use your own Postgres instead, set DATABASE_URL in .env.");
|
|
1047
|
+
return lines;
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
export { parseLoopbackDsn as C, DEV_DATABASE_KIND_ENV, ensureDevDatabase as S, waitForPort as T, readActiveBranch as _, isDaemonAlive as a, composeDeclaresDbService as b, resetManagedDatabase as c, stopManagedDatabase as d, devDatabaseKind, branchDatabaseName as f, databaseNameOf as g, clearActiveBranch as h, findRunningDaemon as i, resolveCliEntry as l, branchUrl as m, managedNotices, resolveDevDatabase as n, managedUrl as o, branchPointerPath as p, prepareDatabaseEnv, ensureManagedDatabase as r, resolveActiveBranch, resolveComposeUrl, pgliteLogTail as s, describeDevDatabase as t, resolveSpawn as u, writeActiveBranch as v, probeTcp as w, composeHostPort as x, composeDatabaseUrl as y };
|
|
1051
|
+
|
|
1052
|
+
//# sourceMappingURL=prepare-Btn9Ewbn.js.map
|