@rebasepro/cli 0.16.0 → 0.16.1-canary.g041c925

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 (50) hide show
  1. package/dist/bundle.d.ts +28 -2
  2. package/dist/commands/build.d.ts +10 -0
  3. package/dist/commands/cloud/deployments.d.ts +41 -0
  4. package/dist/commands/cloud/projects.d.ts +14 -4
  5. package/dist/commands/cloud/resources.d.ts +10 -1
  6. package/dist/commands/db.d.ts +16 -0
  7. package/dist/commands/dev.d.ts +11 -0
  8. package/dist/commands/doctor.d.ts +1 -1
  9. package/dist/commands/init.d.ts +1 -1
  10. package/dist/commands/resources.d.ts +1 -0
  11. package/dist/constraints-BK1_4vci.js +80 -0
  12. package/dist/constraints-BK1_4vci.js.map +1 -0
  13. package/dist/daemon-Bdl4lrdt.js +252 -0
  14. package/dist/daemon-Bdl4lrdt.js.map +1 -0
  15. package/dist/daemon-entry-Brq-S8XX.js +378 -0
  16. package/dist/daemon-entry-Brq-S8XX.js.map +1 -0
  17. package/dist/dev-db/__fixtures__/cli-entry.d.ts +1 -0
  18. package/dist/dev-db/constraints.d.ts +98 -0
  19. package/dist/dev-db/daemon-entry.d.ts +35 -0
  20. package/dist/dev-db/daemon.d.ts +92 -0
  21. package/dist/dev-db/notification-proxy.d.ts +102 -0
  22. package/dist/dev-db/prepare.d.ts +63 -0
  23. package/dist/dev-db/pull.d.ts +92 -0
  24. package/dist/dev-db/resolve.d.ts +66 -0
  25. package/dist/dev-db/state.d.ts +93 -0
  26. package/dist/function-portability.d.ts +45 -0
  27. package/dist/index.d.ts +17 -17
  28. package/dist/index.es.js +5699 -4122
  29. package/dist/index.es.js.map +1 -1
  30. package/dist/manifest.d.ts +24 -1
  31. package/dist/pull-DqPRu1te.js +167 -0
  32. package/dist/pull-DqPRu1te.js.map +1 -0
  33. package/dist/resources/derive.d.ts +47 -0
  34. package/dist/resources/eject-infra-command.d.ts +7 -0
  35. package/dist/resources/eject-infra.d.ts +46 -0
  36. package/dist/state-c0CJ6Kwb.js +190 -0
  37. package/dist/state-c0CJ6Kwb.js.map +1 -0
  38. package/dist/telemetry/consent.d.ts +1 -1
  39. package/dist/telemetry/index.d.ts +7 -7
  40. package/dist/utils/dev-preflight.d.ts +73 -0
  41. package/package.json +13 -8
  42. package/templates/eject/backend/src/index.ts +15 -8
  43. package/templates/eject/config/resources.ts +24 -0
  44. package/templates/template/AGENTS.md +1 -1
  45. package/templates/template/CLAUDE.md +1 -1
  46. package/templates/template/README.md +1 -1
  47. package/templates/template/ai-instructions.md +5 -2
  48. package/templates/template/backend/functions/hello.ts +43 -22
  49. package/templates/template/docker-compose.yml +10 -1
  50. package/templates/template/gitignore +1 -0
@@ -0,0 +1,378 @@
1
+ import { a as findFreePort, d as writeState, n as clearState, r as dataDir } from "./state-c0CJ6Kwb.js";
2
+ import { n as PGLITE_EXTENSION_NAMES } from "./constraints-BK1_4vci.js";
3
+ import fs from "fs";
4
+ import net from "net";
5
+ //#region src/dev-db/notification-proxy.ts
6
+ /**
7
+ * A transparent Postgres proxy that puts LISTEN/NOTIFY back.
8
+ *
9
+ * Without this, realtime does not work against the managed database — and it
10
+ * fails silently, which is worse than failing. The reason is specific and
11
+ * measurable:
12
+ *
13
+ * PGlite is a *single* backend session, and `PGLiteSocketServer` multiplexes
14
+ * every client connection onto it. `LISTEN` is therefore session-wide: whichever
15
+ * client issues it arms the whole database. But a `NotificationResponse` is an
16
+ * asynchronous message with no request to answer, so the multiplexer hands it to
17
+ * whichever socket happens to be reading the protocol stream at that moment —
18
+ * which is the client that *caused* the notification, not the one that asked for
19
+ * it.
20
+ *
21
+ * Measured against pglite-socket 0.2.9:
22
+ *
23
+ * LISTEN and NOTIFY on one connection → delivered
24
+ * trigger-fired pg_notify, same connection → delivered
25
+ * another connection causes the notify → NOT delivered to the listener
26
+ * …and the same notification IS delivered to the notifier, which never asked
27
+ *
28
+ * The realtime engine listens on a dedicated connection and the writes come
29
+ * from request connections, so it is exactly the broken case, every time.
30
+ *
31
+ * The fix is to stop treating a notification as belonging to one connection,
32
+ * which for a single-session database is the truth anyway: this proxy watches
33
+ * the server→client direction, and every `NotificationResponse` frame it sees is
34
+ * copied to every other connected client. A client that never issued `LISTEN`
35
+ * may receive one it did not ask for; `pg` raises a `notification` event nobody
36
+ * has subscribed to, which costs nothing. A client that *did* ask now always
37
+ * gets it, which is the whole point.
38
+ *
39
+ * Two properties make this safe rather than clever:
40
+ *
41
+ * - **It never parses SQL and never rewrites a byte.** Frames are forwarded
42
+ * verbatim; the only edit is delivering a copy of one to more sockets.
43
+ * - **Injection only happens on a message boundary.** The server→client stream
44
+ * is reassembled into whole protocol messages before anything is written on,
45
+ * so an injected frame can never land inside another message.
46
+ *
47
+ * This exists only for the managed development database. Against a real Postgres
48
+ * there is no proxy, because there is no defect to correct.
49
+ */
50
+ /** `NotificationResponse`. The one message type this proxy treats specially. */
51
+ var NOTIFICATION_RESPONSE = 65;
52
+ /**
53
+ * The SSL negotiation request, which is the one thing on the wire that is not
54
+ * a typed message.
55
+ *
56
+ * A client may open with an 8-byte `SSLRequest` (length 8, code 80877103), and
57
+ * the server answers with a *single untyped byte* — `N` or `S`. Feeding that
58
+ * byte to a parser expecting `type + Int32 length` would desynchronise the
59
+ * stream for the rest of the connection, so it is recognised and passed through.
60
+ */
61
+ var SSL_REQUEST_LENGTH = 8;
62
+ var SSL_REQUEST_CODE = 80877103;
63
+ function isSslRequest(chunk) {
64
+ return chunk.length >= SSL_REQUEST_LENGTH && chunk.readInt32BE(0) === SSL_REQUEST_LENGTH && chunk.readInt32BE(4) === SSL_REQUEST_CODE;
65
+ }
66
+ /**
67
+ * Reassembles a server→client byte stream into whole protocol messages.
68
+ *
69
+ * Every backend message is `Int8 type` + `Int32 length` + payload, where the
70
+ * length counts itself but not the type byte. Anything shorter than a full
71
+ * message is held until the rest arrives — TCP offers no guarantee that a
72
+ * message arrives in one chunk, and a proxy that assumed otherwise would inject
73
+ * into the middle of a row description under load.
74
+ */
75
+ var BackendMessageParser = class {
76
+ buffered = Buffer.alloc(0);
77
+ /** Set once the untyped SSL negotiation byte has been dealt with. */
78
+ awaitingSslReply = false;
79
+ expectSslReply() {
80
+ this.awaitingSslReply = true;
81
+ }
82
+ /** Feed bytes in; get whole messages out, in order. */
83
+ push(chunk) {
84
+ const messages = [];
85
+ this.buffered = this.buffered.length === 0 ? chunk : Buffer.concat([this.buffered, chunk]);
86
+ if (this.awaitingSslReply && this.buffered.length >= 1) {
87
+ messages.push(this.buffered.subarray(0, 1));
88
+ this.buffered = this.buffered.subarray(1);
89
+ this.awaitingSslReply = false;
90
+ }
91
+ while (this.buffered.length >= 5) {
92
+ const length = this.buffered.readInt32BE(1);
93
+ if (length < 4) break;
94
+ const total = length + 1;
95
+ if (this.buffered.length < total) break;
96
+ messages.push(this.buffered.subarray(0, total));
97
+ this.buffered = this.buffered.subarray(total);
98
+ }
99
+ return messages;
100
+ }
101
+ /** Bytes held back because they are not yet a whole message. */
102
+ get pending() {
103
+ return this.buffered.length;
104
+ }
105
+ };
106
+ function isNotificationFrame(message) {
107
+ return message.length > 0 && message[0] === NOTIFICATION_RESPONSE;
108
+ }
109
+ /** Channel and payload of a NotificationResponse, for logging and tests. */
110
+ function decodeNotification(message) {
111
+ if (!isNotificationFrame(message) || message.length < 10) return null;
112
+ const body = message.subarray(9);
113
+ const split = body.indexOf(0);
114
+ if (split === -1) return null;
115
+ const channel = body.subarray(0, split).toString("utf8");
116
+ const rest = body.subarray(split + 1);
117
+ const end = rest.indexOf(0);
118
+ return {
119
+ channel,
120
+ payload: (end === -1 ? rest : rest.subarray(0, end)).toString("utf8")
121
+ };
122
+ }
123
+ /**
124
+ * The proxy itself.
125
+ *
126
+ * One upstream connection per client connection, so the multiplexer downstream
127
+ * sees exactly what it would have seen without the proxy.
128
+ */
129
+ var NotificationProxy = class {
130
+ options;
131
+ server = null;
132
+ connections = /* @__PURE__ */ new Set();
133
+ constructor(options) {
134
+ this.options = options;
135
+ }
136
+ get connectionCount() {
137
+ return this.connections.size;
138
+ }
139
+ start() {
140
+ const host = this.options.host ?? "127.0.0.1";
141
+ return new Promise((resolve, reject) => {
142
+ const server = net.createServer((client) => this.accept(client, host));
143
+ server.once("error", reject);
144
+ server.listen(this.options.listenPort, host, () => {
145
+ this.server = server;
146
+ resolve();
147
+ });
148
+ });
149
+ }
150
+ accept(client, host) {
151
+ const upstream = net.connect(this.options.upstreamPort, host);
152
+ const connection = {
153
+ client,
154
+ upstream,
155
+ parser: new BackendMessageParser()
156
+ };
157
+ this.connections.add(connection);
158
+ client.setNoDelay(true);
159
+ upstream.setNoDelay(true);
160
+ client.on("data", (chunk) => {
161
+ if (isSslRequest(chunk)) connection.parser.expectSslReply();
162
+ upstream.write(chunk);
163
+ });
164
+ upstream.on("data", (chunk) => {
165
+ for (const message of connection.parser.push(chunk)) {
166
+ client.write(message);
167
+ if (isNotificationFrame(message)) this.broadcast(message, connection);
168
+ }
169
+ });
170
+ const close = () => {
171
+ this.connections.delete(connection);
172
+ client.destroy();
173
+ upstream.destroy();
174
+ };
175
+ client.on("close", close);
176
+ client.on("error", close);
177
+ upstream.on("close", close);
178
+ upstream.on("error", close);
179
+ }
180
+ /**
181
+ * Copy a notification to every other client.
182
+ *
183
+ * Written directly rather than through a parser: it is already a whole
184
+ * message, and every other socket is only ever written whole messages, so
185
+ * there is no boundary to land inside.
186
+ */
187
+ broadcast(message, origin) {
188
+ let copies = 0;
189
+ for (const connection of this.connections) {
190
+ if (connection === origin) continue;
191
+ if (connection.client.destroyed || !connection.client.writable) continue;
192
+ connection.client.write(message);
193
+ copies += 1;
194
+ }
195
+ const decoded = decodeNotification(message);
196
+ if (decoded) this.options.onNotification?.(decoded.channel, decoded.payload, copies);
197
+ }
198
+ async stop() {
199
+ for (const connection of [...this.connections]) {
200
+ connection.client.destroy();
201
+ connection.upstream.destroy();
202
+ }
203
+ this.connections.clear();
204
+ const server = this.server;
205
+ this.server = null;
206
+ if (!server) return;
207
+ await new Promise((resolve) => server.close(() => resolve()));
208
+ }
209
+ };
210
+ //#endregion
211
+ //#region src/dev-db/daemon-entry.ts
212
+ /**
213
+ * The managed database process: one PGlite instance behind a Postgres socket.
214
+ *
215
+ * Runs as `rebase __dev-db-daemon`, a hidden subcommand rather than a separate
216
+ * build entry point, so the same resolution works from `src` under tsx and from
217
+ * the bundled `dist` a published CLI ships — there is no second file for a
218
+ * build config to forget.
219
+ *
220
+ * It is deliberately detached from whoever started it. `rebase db push` in one
221
+ * terminal and `rebase dev` in another must reach the same database, because
222
+ * two processes opening one PGlite data directory would corrupt it, so the
223
+ * daemon belongs to the *project* rather than to a command. What starts it is
224
+ * incidental; what stops it is an explicit `rebase db stop`, an idle timeout,
225
+ * or the machine going away.
226
+ *
227
+ * PGlite is imported dynamically. It is an optional dependency carrying a 25MB
228
+ * WASM build, and the cost of that must fall only on someone who actually uses
229
+ * the managed database — never on `rebase init`, and never on a CLI startup
230
+ * that is about to print help.
231
+ */
232
+ /** Shut down after this long with nothing connected. */
233
+ var DEFAULT_IDLE_TIMEOUT_MS = 30 * 6e4;
234
+ /** How often to check for idleness. */
235
+ var IDLE_CHECK_INTERVAL_MS = 6e4;
236
+ /**
237
+ * `--project <dir> --port <n> --token <t> [--idle-timeout <ms>]`.
238
+ *
239
+ * Every field is required and unvalidated input is fatal: this process is
240
+ * spawned by the CLI, never typed by a person, so a malformed argument is a bug
241
+ * in the caller and guessing would hide it.
242
+ */
243
+ function parseDaemonArgs(argv) {
244
+ const take = (flag) => {
245
+ const index = argv.indexOf(flag);
246
+ return index >= 0 && index + 1 < argv.length ? argv[index + 1] : null;
247
+ };
248
+ const projectRoot = take("--project");
249
+ const port = Number(take("--port"));
250
+ const token = take("--token");
251
+ const idleRaw = take("--idle-timeout");
252
+ if (!projectRoot) throw new Error("__dev-db-daemon: --project is required");
253
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("__dev-db-daemon: --port must be a valid port");
254
+ if (!token) throw new Error("__dev-db-daemon: --token is required");
255
+ const idleTimeoutMs = idleRaw === null ? DEFAULT_IDLE_TIMEOUT_MS : Number(idleRaw);
256
+ if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs < 0) throw new Error("__dev-db-daemon: --idle-timeout must be a non-negative number of milliseconds");
257
+ return {
258
+ projectRoot,
259
+ port,
260
+ token,
261
+ idleTimeoutMs
262
+ };
263
+ }
264
+ /**
265
+ * Load the extension bundles PGlite needs by name.
266
+ *
267
+ * `CREATE EXTENSION pg_trgm` cannot install anything on its own here — PGlite
268
+ * resolves extensions from bundles handed to the constructor, and a missing one
269
+ * fails at migration time with `extension "pg_trgm" is not available`, which
270
+ * reads like a broken database rather than a missing import.
271
+ */
272
+ async function loadExtensions() {
273
+ const extensions = {};
274
+ for (const name of PGLITE_EXTENSION_NAMES) {
275
+ const bundle = (await import(`@electric-sql/pglite/contrib/${name}`))[name];
276
+ if (!bundle) throw new Error(`@electric-sql/pglite/contrib/${name} did not export "${name}". The installed PGlite version may not ship this extension.`);
277
+ extensions[name] = bundle;
278
+ }
279
+ return extensions;
280
+ }
281
+ /**
282
+ * A tiny sidecar listener that answers one question: "are you the daemon this
283
+ * state file describes?"
284
+ *
285
+ * Liveness cannot be answered by the pid — after a reboot the number belongs to
286
+ * something else — nor by the port alone, for the same reason. Both would let
287
+ * Rebase send a migration to a stranger. So the daemon publishes a token on a
288
+ * second loopback port and the answer is only yes when the token matches.
289
+ */
290
+ function startIdentityServer(token, onConnection) {
291
+ return new Promise((resolve, reject) => {
292
+ const server = net.createServer((socket) => {
293
+ onConnection();
294
+ socket.end(`rebase-dev-db ${token}\n`);
295
+ });
296
+ server.once("error", reject);
297
+ server.listen(0, "127.0.0.1", () => resolve(server));
298
+ });
299
+ }
300
+ async function runDaemon(args) {
301
+ const directory = dataDir(args.projectRoot);
302
+ fs.mkdirSync(directory, { recursive: true });
303
+ const { PGlite } = await import("@electric-sql/pglite");
304
+ const { PGLiteSocketServer } = await import("@electric-sql/pglite-socket");
305
+ const extensions = await loadExtensions();
306
+ const db = await PGlite.create({
307
+ dataDir: directory,
308
+ extensions
309
+ });
310
+ const upstreamPort = await findFreePort();
311
+ const server = new PGLiteSocketServer({
312
+ db,
313
+ port: upstreamPort,
314
+ host: "127.0.0.1",
315
+ maxConnections: 4
316
+ });
317
+ await server.start();
318
+ const proxy = new NotificationProxy({
319
+ listenPort: args.port,
320
+ upstreamPort,
321
+ onNotification: (channel, _payload, copies) => {
322
+ if (copies > 0) process.stdout.write(`dev-db: relayed notification on ${channel} to ${copies} client(s)\n`);
323
+ }
324
+ });
325
+ await proxy.start();
326
+ let idleSince = Date.now();
327
+ const identity = await startIdentityServer(args.token, () => {
328
+ idleSince = null;
329
+ });
330
+ const identityAddress = identity.address();
331
+ const identityPort = identityAddress !== null && typeof identityAddress !== "string" ? identityAddress.port : 0;
332
+ writeState(args.projectRoot, {
333
+ port: args.port,
334
+ pid: process.pid,
335
+ dataDir: directory,
336
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
337
+ token: args.token,
338
+ identityPort
339
+ });
340
+ let shuttingDown = false;
341
+ const shutdown = async (reason) => {
342
+ if (shuttingDown) return;
343
+ shuttingDown = true;
344
+ process.stdout.write(`dev-db: stopping (${reason})\n`);
345
+ clearState(args.projectRoot);
346
+ try {
347
+ await proxy.stop();
348
+ } catch {}
349
+ try {
350
+ await server.stop();
351
+ } catch {}
352
+ identity.close();
353
+ try {
354
+ await db.close();
355
+ } catch {}
356
+ process.exit(0);
357
+ };
358
+ process.on("SIGINT", () => void shutdown("SIGINT"));
359
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
360
+ process.on("disconnect", () => {});
361
+ if (args.idleTimeoutMs > 0) setInterval(() => {
362
+ const stats = server.getStats();
363
+ if (stats.activeConnections > 0 || stats.queuedQueries > 0) {
364
+ idleSince = null;
365
+ return;
366
+ }
367
+ if (idleSince === null) {
368
+ idleSince = Date.now();
369
+ return;
370
+ }
371
+ if (Date.now() - idleSince >= args.idleTimeoutMs) shutdown(`idle for ${Math.round(args.idleTimeoutMs / 6e4)} minutes`);
372
+ }, IDLE_CHECK_INTERVAL_MS).unref();
373
+ process.stdout.write(`dev-db: ready on 127.0.0.1:${args.port}\n`);
374
+ }
375
+ //#endregion
376
+ export { parseDaemonArgs, runDaemon };
377
+
378
+ //# sourceMappingURL=daemon-entry-Brq-S8XX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-entry-Brq-S8XX.js","names":[],"sources":["../src/dev-db/notification-proxy.ts","../src/dev-db/daemon-entry.ts"],"sourcesContent":["/**\n * A transparent Postgres proxy that puts LISTEN/NOTIFY back.\n *\n * Without this, realtime does not work against the managed database — and it\n * fails silently, which is worse than failing. The reason is specific and\n * measurable:\n *\n * PGlite is a *single* backend session, and `PGLiteSocketServer` multiplexes\n * every client connection onto it. `LISTEN` is therefore session-wide: whichever\n * client issues it arms the whole database. But a `NotificationResponse` is an\n * asynchronous message with no request to answer, so the multiplexer hands it to\n * whichever socket happens to be reading the protocol stream at that moment —\n * which is the client that *caused* the notification, not the one that asked for\n * it.\n *\n * Measured against pglite-socket 0.2.9:\n *\n * LISTEN and NOTIFY on one connection → delivered\n * trigger-fired pg_notify, same connection → delivered\n * another connection causes the notify → NOT delivered to the listener\n * …and the same notification IS delivered to the notifier, which never asked\n *\n * The realtime engine listens on a dedicated connection and the writes come\n * from request connections, so it is exactly the broken case, every time.\n *\n * The fix is to stop treating a notification as belonging to one connection,\n * which for a single-session database is the truth anyway: this proxy watches\n * the server→client direction, and every `NotificationResponse` frame it sees is\n * copied to every other connected client. A client that never issued `LISTEN`\n * may receive one it did not ask for; `pg` raises a `notification` event nobody\n * has subscribed to, which costs nothing. A client that *did* ask now always\n * gets it, which is the whole point.\n *\n * Two properties make this safe rather than clever:\n *\n * - **It never parses SQL and never rewrites a byte.** Frames are forwarded\n * verbatim; the only edit is delivering a copy of one to more sockets.\n * - **Injection only happens on a message boundary.** The server→client stream\n * is reassembled into whole protocol messages before anything is written on,\n * so an injected frame can never land inside another message.\n *\n * This exists only for the managed development database. Against a real Postgres\n * there is no proxy, because there is no defect to correct.\n */\n\nimport net from \"net\";\n\n/** `NotificationResponse`. The one message type this proxy treats specially. */\nconst NOTIFICATION_RESPONSE = 0x41; // 'A'\n\n/**\n * The SSL negotiation request, which is the one thing on the wire that is not\n * a typed message.\n *\n * A client may open with an 8-byte `SSLRequest` (length 8, code 80877103), and\n * the server answers with a *single untyped byte* — `N` or `S`. Feeding that\n * byte to a parser expecting `type + Int32 length` would desynchronise the\n * stream for the rest of the connection, so it is recognised and passed through.\n */\nconst SSL_REQUEST_LENGTH = 8;\nconst SSL_REQUEST_CODE = 80877103;\n\nfunction isSslRequest(chunk: Buffer): boolean {\n return (\n chunk.length >= SSL_REQUEST_LENGTH &&\n chunk.readInt32BE(0) === SSL_REQUEST_LENGTH &&\n chunk.readInt32BE(4) === SSL_REQUEST_CODE\n );\n}\n\n/**\n * Reassembles a server→client byte stream into whole protocol messages.\n *\n * Every backend message is `Int8 type` + `Int32 length` + payload, where the\n * length counts itself but not the type byte. Anything shorter than a full\n * message is held until the rest arrives — TCP offers no guarantee that a\n * message arrives in one chunk, and a proxy that assumed otherwise would inject\n * into the middle of a row description under load.\n */\nexport class BackendMessageParser {\n private buffered: Buffer = Buffer.alloc(0);\n /** Set once the untyped SSL negotiation byte has been dealt with. */\n private awaitingSslReply = false;\n\n expectSslReply(): void {\n this.awaitingSslReply = true;\n }\n\n /** Feed bytes in; get whole messages out, in order. */\n push(chunk: Buffer): Buffer[] {\n const messages: Buffer[] = [];\n this.buffered = this.buffered.length === 0 ? chunk : Buffer.concat([this.buffered, chunk]);\n\n if (this.awaitingSslReply && this.buffered.length >= 1) {\n // Single untyped byte: 'N' (no SSL) or 'S' (proceed).\n messages.push(this.buffered.subarray(0, 1));\n this.buffered = this.buffered.subarray(1);\n this.awaitingSslReply = false;\n }\n\n while (this.buffered.length >= 5) {\n const length = this.buffered.readInt32BE(1);\n // A length below 4 cannot describe itself; the stream is not one we\n // understand, so stop parsing and let the rest through untouched\n // rather than guessing.\n if (length < 4) break;\n const total = length + 1;\n if (this.buffered.length < total) break;\n messages.push(this.buffered.subarray(0, total));\n this.buffered = this.buffered.subarray(total);\n }\n\n return messages;\n }\n\n /** Bytes held back because they are not yet a whole message. */\n get pending(): number {\n return this.buffered.length;\n }\n}\n\nexport function isNotificationFrame(message: Buffer): boolean {\n return message.length > 0 && message[0] === NOTIFICATION_RESPONSE;\n}\n\n/** Channel and payload of a NotificationResponse, for logging and tests. */\nexport function decodeNotification(message: Buffer): { channel: string; payload: string } | null {\n if (!isNotificationFrame(message) || message.length < 10) return null;\n // 1 type byte + 4 length + 4 process id, then two null-terminated strings.\n const body = message.subarray(9);\n const split = body.indexOf(0);\n if (split === -1) return null;\n const channel = body.subarray(0, split).toString(\"utf8\");\n const rest = body.subarray(split + 1);\n const end = rest.indexOf(0);\n\n return { channel, payload: (end === -1 ? rest : rest.subarray(0, end)).toString(\"utf8\") };\n}\n\nexport interface NotificationProxyOptions {\n /** Port clients connect to. */\n listenPort: number;\n /** Port the real PGlite socket server is on. */\n upstreamPort: number;\n host?: string;\n /** Called for every notification broadcast. For diagnostics and tests. */\n onNotification?: (channel: string, payload: string, copies: number) => void;\n}\n\ninterface Connection {\n client: net.Socket;\n upstream: net.Socket;\n parser: BackendMessageParser;\n}\n\n/**\n * The proxy itself.\n *\n * One upstream connection per client connection, so the multiplexer downstream\n * sees exactly what it would have seen without the proxy.\n */\nexport class NotificationProxy {\n private server: net.Server | null = null;\n private readonly connections = new Set<Connection>();\n\n constructor(private readonly options: NotificationProxyOptions) {}\n\n get connectionCount(): number {\n return this.connections.size;\n }\n\n start(): Promise<void> {\n const host = this.options.host ?? \"127.0.0.1\";\n\n return new Promise((resolve, reject) => {\n const server = net.createServer((client) => this.accept(client, host));\n server.once(\"error\", reject);\n server.listen(this.options.listenPort, host, () => {\n this.server = server;\n resolve();\n });\n });\n }\n\n private accept(client: net.Socket, host: string): void {\n const upstream = net.connect(this.options.upstreamPort, host);\n const connection: Connection = { client, upstream, parser: new BackendMessageParser() };\n this.connections.add(connection);\n\n // Nagle would batch a notification behind nothing at all, adding latency\n // to the one message whose entire value is arriving promptly.\n client.setNoDelay(true);\n upstream.setNoDelay(true);\n\n client.on(\"data\", (chunk: Buffer) => {\n if (isSslRequest(chunk)) connection.parser.expectSslReply();\n upstream.write(chunk);\n });\n\n upstream.on(\"data\", (chunk: Buffer) => {\n for (const message of connection.parser.push(chunk)) {\n client.write(message);\n if (isNotificationFrame(message)) this.broadcast(message, connection);\n }\n });\n\n const close = () => {\n this.connections.delete(connection);\n client.destroy();\n upstream.destroy();\n };\n client.on(\"close\", close);\n client.on(\"error\", close);\n upstream.on(\"close\", close);\n upstream.on(\"error\", close);\n }\n\n /**\n * Copy a notification to every other client.\n *\n * Written directly rather than through a parser: it is already a whole\n * message, and every other socket is only ever written whole messages, so\n * there is no boundary to land inside.\n */\n private broadcast(message: Buffer, origin: Connection): void {\n let copies = 0;\n for (const connection of this.connections) {\n if (connection === origin) continue;\n if (connection.client.destroyed || !connection.client.writable) continue;\n connection.client.write(message);\n copies += 1;\n }\n\n const decoded = decodeNotification(message);\n if (decoded) this.options.onNotification?.(decoded.channel, decoded.payload, copies);\n }\n\n async stop(): Promise<void> {\n for (const connection of [...this.connections]) {\n connection.client.destroy();\n connection.upstream.destroy();\n }\n this.connections.clear();\n\n const server = this.server;\n this.server = null;\n if (!server) return;\n\n await new Promise<void>((resolve) => server.close(() => resolve()));\n }\n}\n","/**\n * The managed database process: one PGlite instance behind a Postgres socket.\n *\n * Runs as `rebase __dev-db-daemon`, a hidden subcommand rather than a separate\n * build entry point, so the same resolution works from `src` under tsx and from\n * the bundled `dist` a published CLI ships — there is no second file for a\n * build config to forget.\n *\n * It is deliberately detached from whoever started it. `rebase db push` in one\n * terminal and `rebase dev` in another must reach the same database, because\n * two processes opening one PGlite data directory would corrupt it, so the\n * daemon belongs to the *project* rather than to a command. What starts it is\n * incidental; what stops it is an explicit `rebase db stop`, an idle timeout,\n * or the machine going away.\n *\n * PGlite is imported dynamically. It is an optional dependency carrying a 25MB\n * WASM build, and the cost of that must fall only on someone who actually uses\n * the managed database — never on `rebase init`, and never on a CLI startup\n * that is about to print help.\n */\n\nimport fs from \"fs\";\nimport net from \"net\";\n\nimport {\n MANAGED_SERVER_MAX_CONNECTIONS,\n PGLITE_EXTENSION_NAMES\n} from \"./constraints\";\nimport { NotificationProxy } from \"./notification-proxy\";\nimport { clearState, dataDir, findFreePort, writeState } from \"./state\";\n\n/** Shut down after this long with nothing connected. */\nconst DEFAULT_IDLE_TIMEOUT_MS = 30 * 60_000;\n\n/** How often to check for idleness. */\nconst IDLE_CHECK_INTERVAL_MS = 60_000;\n\nexport interface DaemonArgs {\n projectRoot: string;\n port: number;\n token: string;\n idleTimeoutMs: number;\n}\n\n/**\n * `--project <dir> --port <n> --token <t> [--idle-timeout <ms>]`.\n *\n * Every field is required and unvalidated input is fatal: this process is\n * spawned by the CLI, never typed by a person, so a malformed argument is a bug\n * in the caller and guessing would hide it.\n */\nexport function parseDaemonArgs(argv: readonly string[]): DaemonArgs {\n const take = (flag: string): string | null => {\n const index = argv.indexOf(flag);\n\n return index >= 0 && index + 1 < argv.length ? argv[index + 1] : null;\n };\n\n const projectRoot = take(\"--project\");\n const port = Number(take(\"--port\"));\n const token = take(\"--token\");\n const idleRaw = take(\"--idle-timeout\");\n\n if (!projectRoot) throw new Error(\"__dev-db-daemon: --project is required\");\n if (!Number.isInteger(port) || port <= 0 || port > 65535) {\n throw new Error(\"__dev-db-daemon: --port must be a valid port\");\n }\n if (!token) throw new Error(\"__dev-db-daemon: --token is required\");\n\n const idleTimeoutMs = idleRaw === null ? DEFAULT_IDLE_TIMEOUT_MS : Number(idleRaw);\n if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs < 0) {\n throw new Error(\"__dev-db-daemon: --idle-timeout must be a non-negative number of milliseconds\");\n }\n\n return { projectRoot, port, token, idleTimeoutMs };\n}\n\n/**\n * Load the extension bundles PGlite needs by name.\n *\n * `CREATE EXTENSION pg_trgm` cannot install anything on its own here — PGlite\n * resolves extensions from bundles handed to the constructor, and a missing one\n * fails at migration time with `extension \"pg_trgm\" is not available`, which\n * reads like a broken database rather than a missing import.\n */\nasync function loadExtensions(): Promise<Record<string, unknown>> {\n const extensions: Record<string, unknown> = {};\n for (const name of PGLITE_EXTENSION_NAMES) {\n const module = (await import(`@electric-sql/pglite/contrib/${name}`)) as Record<string, unknown>;\n const bundle = module[name];\n if (!bundle) {\n throw new Error(\n `@electric-sql/pglite/contrib/${name} did not export \"${name}\". ` +\n \"The installed PGlite version may not ship this extension.\"\n );\n }\n extensions[name] = bundle;\n }\n\n return extensions;\n}\n\n/**\n * A tiny sidecar listener that answers one question: \"are you the daemon this\n * state file describes?\"\n *\n * Liveness cannot be answered by the pid — after a reboot the number belongs to\n * something else — nor by the port alone, for the same reason. Both would let\n * Rebase send a migration to a stranger. So the daemon publishes a token on a\n * second loopback port and the answer is only yes when the token matches.\n */\nfunction startIdentityServer(token: string, onConnection: () => void): Promise<net.Server> {\n return new Promise((resolve, reject) => {\n const server = net.createServer((socket) => {\n onConnection();\n socket.end(`rebase-dev-db ${token}\\n`);\n });\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => resolve(server));\n });\n}\n\nexport async function runDaemon(args: DaemonArgs): Promise<void> {\n const directory = dataDir(args.projectRoot);\n fs.mkdirSync(directory, { recursive: true });\n\n const { PGlite } = (await import(\"@electric-sql/pglite\")) as {\n PGlite: { create(options: unknown): Promise<unknown> };\n };\n const { PGLiteSocketServer } = (await import(\"@electric-sql/pglite-socket\")) as {\n PGLiteSocketServer: new (options: unknown) => {\n start(): Promise<void>;\n stop(): Promise<void>;\n getStats(): { activeConnections: number; queuedQueries: number };\n };\n };\n\n const extensions = await loadExtensions();\n const db = (await PGlite.create({ dataDir: directory, extensions })) as { close(): Promise<void> };\n\n // The socket server listens privately; clients reach it through the\n // notification proxy on `args.port`. Realtime does not work otherwise —\n // PGlite is one session, so a NotificationResponse is handed to whichever\n // socket is reading rather than to the one that issued LISTEN. See\n // `notification-proxy.ts` for the measurements.\n const upstreamPort = await findFreePort();\n const server = new PGLiteSocketServer({\n db,\n port: upstreamPort,\n host: \"127.0.0.1\",\n // Above the client pool limit so a second *non-transactional* client is\n // refused with a connection error rather than deadlocking the\n // multiplexer. See `constraints.ts` — the pool limit is what actually\n // prevents overlapping transactions.\n maxConnections: MANAGED_SERVER_MAX_CONNECTIONS\n });\n await server.start();\n\n const proxy = new NotificationProxy({\n listenPort: args.port,\n upstreamPort,\n onNotification: (channel, _payload, copies) => {\n if (copies > 0) process.stdout.write(`dev-db: relayed notification on ${channel} to ${copies} client(s)\\n`);\n }\n });\n await proxy.start();\n\n // \"Idle\" means nothing is connected to the *database*. An earlier version\n // tracked identity pings instead, which meant a daemon serving queries\n // steadily for an hour would decide it was idle and shut down under a\n // running dev server.\n let idleSince: number | null = Date.now();\n const identity = await startIdentityServer(args.token, () => {\n idleSince = null;\n });\n const identityAddress = identity.address();\n const identityPort = identityAddress !== null && typeof identityAddress !== \"string\" ? identityAddress.port : 0;\n\n writeState(args.projectRoot, {\n port: args.port,\n pid: process.pid,\n dataDir: directory,\n startedAt: new Date().toISOString(),\n token: args.token,\n identityPort\n });\n\n let shuttingDown = false;\n const shutdown = async (reason: string) => {\n if (shuttingDown) return;\n shuttingDown = true;\n process.stdout.write(`dev-db: stopping (${reason})\\n`);\n // The state file goes first: a command that reads it during shutdown\n // should conclude \"not running\" and start a fresh daemon, rather than\n // connect to a socket that is closing under it.\n clearState(args.projectRoot);\n try {\n await proxy.stop();\n } catch { /* already down */ }\n try {\n await server.stop();\n } catch { /* already down */ }\n identity.close();\n try {\n await db.close();\n } catch { /* already closed */ }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => void shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => void shutdown(\"SIGTERM\"));\n // The parent going away must not take the database with it — the daemon\n // belongs to the project. But an orphan with nobody left to serve should\n // not outlive the session either, which is what the idle timer is for.\n process.on(\"disconnect\", () => { /* detached on purpose */ });\n\n if (args.idleTimeoutMs > 0) {\n const timer = setInterval(() => {\n const stats = server.getStats();\n const busy = stats.activeConnections > 0 || stats.queuedQueries > 0;\n if (busy) {\n idleSince = null;\n\n return;\n }\n if (idleSince === null) {\n idleSince = Date.now();\n\n return;\n }\n if (Date.now() - idleSince >= args.idleTimeoutMs) {\n void shutdown(`idle for ${Math.round(args.idleTimeoutMs / 60_000)} minutes`);\n }\n }, IDLE_CHECK_INTERVAL_MS);\n timer.unref();\n }\n\n process.stdout.write(`dev-db: ready on 127.0.0.1:${args.port}\\n`);\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ /**
2
+ * What PGlite can and cannot do as a development database, measured rather
3
+ * than assumed.
4
+ *
5
+ * Everything in this directory is shaped by four facts, each established by
6
+ * running it against `@electric-sql/pglite` 0.5.6 and
7
+ * `@electric-sql/pglite-socket` 0.2.9 rather than by reading their docs. They
8
+ * are recorded here because two of them are silent failures — the kind that
9
+ * make a developer lose an evening to a feature that reports success and does
10
+ * nothing.
11
+ *
12
+ * 1. **It is really PostgreSQL 18.3.** `select version()` over the socket
13
+ * returns `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32`, which is the same
14
+ * major as the `postgres:18-alpine` the eject template ships. So a dev
15
+ * database here and a compose database there are the same Postgres, and
16
+ * schema behaviour does not diverge between them.
17
+ *
18
+ * 2. **`pg_trgm` and `unaccent` are available**, which is what search
19
+ * collections need. They are not installed by a bare `CREATE EXTENSION`,
20
+ * though — PGlite ships them as separate bundles that must be passed to the
21
+ * constructor, and without that `CREATE EXTENSION pg_trgm` fails with
22
+ * `extension "pg_trgm" is not available`. {@link PGLITE_EXTENSIONS} is that
23
+ * list, and it has to stay in step with what the schema generator emits.
24
+ *
25
+ * 3. **RLS is enforced exactly as it is on a real server.** With
26
+ * `SET LOCAL ROLE "rebase_user"` inside a transaction — which is how
27
+ * `PostgresBackendDriver` isolates every request — `current_user` becomes
28
+ * the restricted role, `session_user` stays the owner, and a policy using
29
+ * `current_setting('app.tenant')` filters rows correctly, including under
30
+ * `FORCE ROW LEVEL SECURITY`. Measured: an owner saw 3 rows and the
31
+ * role-switched transaction saw 2, with the cross-tenant probe returning 0.
32
+ * This is the one that mattered most: a dev database that quietly failed to
33
+ * apply RLS would give false confidence about the product's central claim.
34
+ *
35
+ * 4. **Concurrency is the real limit, and it fails badly.** PGlite is a single
36
+ * session, and `PGLiteSocketServer` multiplexes connections onto it. Two
37
+ * pooled clients that hold *overlapping transactions* deadlock — not error,
38
+ * hang — which is precisely what a request-per-transaction server does under
39
+ * any concurrent load. {@link MANAGED_POOL_MAX} is the answer: one client
40
+ * connection, so requests queue in the pool instead of deadlocking in the
41
+ * multiplexer. Measured: with a pool of 1, four concurrent queries and a
42
+ * role-switched RLS transaction all pass; with a pool of 5 the same script
43
+ * hangs indefinitely.
44
+ *
45
+ * 5. **LISTEN/NOTIFY needed repairing, and now works.** A notification is an
46
+ * asynchronous message with no request to answer, and the multiplexer hands
47
+ * it to whichever socket is reading rather than to the one that issued
48
+ * `LISTEN` — so a dedicated listener connection, which is exactly how the
49
+ * realtime engine works, received nothing while the *writer* received
50
+ * notifications it never asked for. `notification-proxy.ts` corrects that by
51
+ * copying every `NotificationResponse` frame to every client, which for a
52
+ * single-session database is simply the truth. Realtime therefore works
53
+ * against the managed database, with no change to the server: it does
54
+ * ordinary `LISTEN` over ordinary libpq.
55
+ */
56
+ /**
57
+ * Extensions to hand PGlite's constructor.
58
+ *
59
+ * `CREATE EXTENSION` alone cannot install these — PGlite resolves them from
60
+ * bundles supplied at construction time, so anything missing here is missing
61
+ * from the database no matter what the migration says.
62
+ */
63
+ export declare const PGLITE_EXTENSION_NAMES: readonly ["pg_trgm", "unaccent"];
64
+ /**
65
+ * Client connections the managed database tolerates: exactly one.
66
+ *
67
+ * Not a tuning choice. Two concurrent transactions over the socket
68
+ * multiplexer deadlock, and a request-per-transaction server produces those
69
+ * the moment two requests overlap. One connection converts that deadlock into
70
+ * ordinary queueing, which is slower and correct.
71
+ */
72
+ export declare const MANAGED_POOL_MAX = 1;
73
+ /**
74
+ * Connections the socket server will accept.
75
+ *
76
+ * Above {@link MANAGED_POOL_MAX} so that a second *non-transactional* client —
77
+ * `rebase db push` in another terminal while `rebase dev` runs — is refused
78
+ * with a connection error rather than corrupting the multiplexer. The pool
79
+ * limit is what prevents overlapping transactions; this only stops a stampede.
80
+ */
81
+ export declare const MANAGED_SERVER_MAX_CONNECTIONS = 4;
82
+ /** What a managed PGlite database cannot do, in the words the user needs. */
83
+ export interface ManagedLimitation {
84
+ /** Stable id, so a warning can be suppressed or tested for. */
85
+ id: string;
86
+ /** One line, naming the feature rather than the mechanism. */
87
+ summary: string;
88
+ /** What to do instead. Always a concrete command. */
89
+ remedy: string;
90
+ }
91
+ /**
92
+ * Announced at startup, every time, rather than discovered.
93
+ *
94
+ * A developer who does not know realtime is off will read the silence as a bug
95
+ * in their own code, which is a worse outcome than not offering the managed
96
+ * database at all.
97
+ */
98
+ export declare const MANAGED_LIMITATIONS: readonly ManagedLimitation[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The managed database process: one PGlite instance behind a Postgres socket.
3
+ *
4
+ * Runs as `rebase __dev-db-daemon`, a hidden subcommand rather than a separate
5
+ * build entry point, so the same resolution works from `src` under tsx and from
6
+ * the bundled `dist` a published CLI ships — there is no second file for a
7
+ * build config to forget.
8
+ *
9
+ * It is deliberately detached from whoever started it. `rebase db push` in one
10
+ * terminal and `rebase dev` in another must reach the same database, because
11
+ * two processes opening one PGlite data directory would corrupt it, so the
12
+ * daemon belongs to the *project* rather than to a command. What starts it is
13
+ * incidental; what stops it is an explicit `rebase db stop`, an idle timeout,
14
+ * or the machine going away.
15
+ *
16
+ * PGlite is imported dynamically. It is an optional dependency carrying a 25MB
17
+ * WASM build, and the cost of that must fall only on someone who actually uses
18
+ * the managed database — never on `rebase init`, and never on a CLI startup
19
+ * that is about to print help.
20
+ */
21
+ export interface DaemonArgs {
22
+ projectRoot: string;
23
+ port: number;
24
+ token: string;
25
+ idleTimeoutMs: number;
26
+ }
27
+ /**
28
+ * `--project <dir> --port <n> --token <t> [--idle-timeout <ms>]`.
29
+ *
30
+ * Every field is required and unvalidated input is fatal: this process is
31
+ * spawned by the CLI, never typed by a person, so a malformed argument is a bug
32
+ * in the caller and guessing would hide it.
33
+ */
34
+ export declare function parseDaemonArgs(argv: readonly string[]): DaemonArgs;
35
+ export declare function runDaemon(args: DaemonArgs): Promise<void>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Starting, finding and stopping the managed database, from the caller's side.
3
+ *
4
+ * Every command that needs Postgres calls {@link ensureManagedDatabase} and
5
+ * gets a connection string back. Whether that started a process or found one
6
+ * already running is not the caller's business, which is the point: `rebase
7
+ * db push`, `rebase dev` and `rebase studio` in three terminals must all reach
8
+ * the same database without coordinating, because two processes opening one
9
+ * PGlite data directory would corrupt it.
10
+ *
11
+ * The hard part is not starting the daemon; it is deciding whether the one the
12
+ * state file describes is still there. A pid can be recycled after a reboot and
13
+ * a port can be taken by a stranger, so believing either on its own would let
14
+ * Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the
15
+ * daemon to identify itself instead.
16
+ */
17
+ import { type DaemonState } from "./state.js";
18
+ export interface ManagedDatabase {
19
+ /** Connection string for this project's managed database. */
20
+ url: string;
21
+ /** Where the data lives, for diagnostics and `--reset`. */
22
+ dataDir: string;
23
+ port: number;
24
+ pid: number;
25
+ /** True when this call started the daemon rather than finding it. */
26
+ started: boolean;
27
+ }
28
+ export declare function managedUrl(port: number): string;
29
+ /**
30
+ * Ask the process behind a state record to prove it is the one we wrote down.
31
+ *
32
+ * A pid check alone answers "is *a* process running", and a port check alone
33
+ * answers "is *something* listening" — after a reboot both say yes about
34
+ * strangers. The daemon answers with the token from its own state file, so a
35
+ * match is the only evidence accepted.
36
+ */
37
+ export declare function isDaemonAlive(state: DaemonState): Promise<boolean>;
38
+ /** The running daemon for this project, or `null`. Never starts anything. */
39
+ export declare function findRunningDaemon(projectRoot: string): Promise<DaemonState | null>;
40
+ /**
41
+ * How to re-invoke ourselves, which differs between a published CLI and this
42
+ * repository.
43
+ *
44
+ * A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.
45
+ * Inside the monorepo the entry is TypeScript, which plain `node` cannot load —
46
+ * so the daemon has to be started through the same loader that is running now.
47
+ * Getting this wrong fails as a spawn that exits instantly with a syntax error,
48
+ * which is why the caller reads `pglite.log` on failure.
49
+ */
50
+ export declare function resolveSpawn(entry: string): {
51
+ execPath: string;
52
+ prefixArgs: string[];
53
+ };
54
+ export interface EnsureOptions {
55
+ /** Silence the "starting…" progress line. */
56
+ quiet?: boolean;
57
+ /** Milliseconds of inactivity before the daemon exits. 0 disables. */
58
+ idleTimeoutMs?: number;
59
+ /** Where progress goes. Injected for tests. */
60
+ onProgress?: (message: string) => void;
61
+ /** Override how the daemon process is launched. For tests. */
62
+ spawn?: {
63
+ execPath: string;
64
+ prefixArgs: string[];
65
+ };
66
+ /**
67
+ * Override the CLI entry to re-invoke. For tests.
68
+ *
69
+ * Necessary because under a test runner `process.argv[1]` is the runner
70
+ * itself, which exists and is therefore accepted by {@link resolveCliEntry}
71
+ * — spawning vitest with `__dev-db-daemon` rather than the CLI.
72
+ */
73
+ entry?: string;
74
+ }
75
+ /**
76
+ * The project's managed database, started if it is not already running.
77
+ *
78
+ * Safe to call concurrently from several commands: the loser of the race finds
79
+ * the winner's state file during its poll and adopts it rather than starting a
80
+ * second daemon.
81
+ */
82
+ export declare function ensureManagedDatabase(projectRoot: string, options?: EnsureOptions): Promise<ManagedDatabase>;
83
+ /** Stop the daemon. Returns false when there was nothing running. */
84
+ export declare function stopManagedDatabase(projectRoot: string): Promise<boolean>;
85
+ /**
86
+ * Stop the daemon and delete the data directory.
87
+ *
88
+ * Destructive and deliberately not clever: it removes the whole directory
89
+ * rather than dropping schemas, because "give me an empty database" is the only
90
+ * thing anyone means by it.
91
+ */
92
+ export declare function resetManagedDatabase(projectRoot: string): Promise<void>;