@rebasepro/cli 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +21 -3
  3. package/bin/rebase.js +93 -6
  4. package/dist/branch-pointer-dBiClJ0p.js +111 -0
  5. package/dist/branch-pointer-dBiClJ0p.js.map +1 -0
  6. package/dist/bundle.d.ts +80 -3
  7. package/dist/cli.d.ts +22 -0
  8. package/dist/commands/auth.d.ts +13 -0
  9. package/dist/commands/cloud/action-help.d.ts +0 -10
  10. package/dist/commands/cloud/auth.d.ts +37 -0
  11. package/dist/commands/cloud/context.d.ts +90 -2
  12. package/dist/commands/cloud/databases.d.ts +0 -2
  13. package/dist/commands/cloud/declared-resources.d.ts +3 -0
  14. package/dist/commands/cloud/deploy.d.ts +76 -6
  15. package/dist/commands/cloud/deployments.d.ts +9 -2
  16. package/dist/commands/cloud/index.d.ts +36 -0
  17. package/dist/commands/cloud/link.d.ts +1 -1
  18. package/dist/commands/cloud/orgs.d.ts +6 -0
  19. package/dist/commands/cloud/resources.d.ts +81 -3
  20. package/dist/commands/cloud/settings.d.ts +7 -0
  21. package/dist/commands/db.d.ts +119 -9
  22. package/dist/commands/dev.d.ts +216 -1
  23. package/dist/commands/doctor.d.ts +51 -0
  24. package/dist/commands/init.d.ts +1 -0
  25. package/dist/commands/skills.d.ts +82 -13
  26. package/dist/commands/status.d.ts +1 -0
  27. package/dist/commands/telemetry.d.ts +0 -8
  28. package/dist/{daemon-Bdl4lrdt.js → daemon-YDZK9NZ1.js} +88 -9
  29. package/dist/daemon-YDZK9NZ1.js.map +1 -0
  30. package/dist/{daemon-entry-CmJn83zu.js → daemon-entry-LTFKtpHy.js} +117 -44
  31. package/dist/daemon-entry-LTFKtpHy.js.map +1 -0
  32. package/dist/dev-db/branch-pointer.d.ts +71 -0
  33. package/dist/dev-db/daemon.d.ts +27 -0
  34. package/dist/dev-db/prepare.d.ts +62 -0
  35. package/dist/dev-db/pull.d.ts +28 -0
  36. package/dist/dev-db/resolve.d.ts +45 -6
  37. package/dist/dev-db/state.d.ts +21 -0
  38. package/dist/dev-preflight-CNLl4rdo.js +367 -0
  39. package/dist/dev-preflight-CNLl4rdo.js.map +1 -0
  40. package/dist/doctor-environment.d.ts +131 -0
  41. package/dist/function-portability.d.ts +7 -0
  42. package/dist/index.d.ts +25 -15
  43. package/dist/index.es.js +5594 -1741
  44. package/dist/index.es.js.map +1 -1
  45. package/dist/project-3WCbrrfW.js +499 -0
  46. package/dist/project-3WCbrrfW.js.map +1 -0
  47. package/dist/{pull-DqPRu1te.js → pull-Bj0XnH5s.js} +31 -2
  48. package/dist/pull-Bj0XnH5s.js.map +1 -0
  49. package/dist/resolve-Y56osuQH.js +74 -0
  50. package/dist/resolve-Y56osuQH.js.map +1 -0
  51. package/dist/resources/derive.d.ts +67 -1
  52. package/dist/resources/status.d.ts +128 -0
  53. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  54. package/dist/{state-c0CJ6Kwb.js → state-C59Elrnt.js} +24 -15
  55. package/dist/state-C59Elrnt.js.map +1 -0
  56. package/dist/utils/args.d.ts +18 -0
  57. package/dist/utils/command-words.d.ts +45 -0
  58. package/dist/utils/dev-preflight.d.ts +46 -0
  59. package/dist/utils/package-manager.d.ts +23 -0
  60. package/dist/utils/project.d.ts +66 -0
  61. package/dist/utils/spawn-error.d.ts +1 -0
  62. package/dist/utils/unknown-command.d.ts +26 -0
  63. package/dist/utils/version.d.ts +10 -0
  64. package/package.json +36 -28
  65. package/templates/eject/backend/src/env.ts +9 -0
  66. package/templates/eject/backend/src/index.ts +46 -19
  67. package/templates/eject/config/resources.ts +1 -1
  68. package/templates/overlays/baas/README.md +31 -11
  69. package/templates/overlays/baas/backend/package.json +2 -2
  70. package/templates/overlays/baas/backend/tsconfig.json +1 -1
  71. package/templates/overlays/baas/package.json +5 -2
  72. package/templates/overlays/baas/pnpm-workspace.yaml +13 -0
  73. package/templates/template/.cursorrules +1 -0
  74. package/templates/template/.env.example +91 -8
  75. package/templates/template/.github/copilot-instructions.md +1 -0
  76. package/templates/template/.mcp.json +11 -0
  77. package/templates/template/.windsurfrules +1 -0
  78. package/templates/template/AGENTS.md +1 -0
  79. package/templates/template/CLAUDE.md +2 -1
  80. package/templates/template/README.md +59 -28
  81. package/templates/template/ai-instructions.md +29 -12
  82. package/templates/template/backend/functions/hello.ts +5 -1
  83. package/templates/template/backend/package.json +2 -2
  84. package/templates/template/backend/src/schema.generated.ts +8 -3
  85. package/templates/template/backend/tsconfig.json +1 -1
  86. package/templates/template/config/cms.d.ts +6 -0
  87. package/templates/template/config/package.json +1 -1
  88. package/templates/template/config/resources.ts +42 -3
  89. package/templates/template/config/storage.ts +2 -1
  90. package/templates/template/docker-compose.yml +50 -9
  91. package/templates/template/frontend/index.html +0 -1
  92. package/templates/template/frontend/package.json +1 -2
  93. package/templates/template/frontend/src/App.tsx +8 -2
  94. package/templates/template/frontend/vite.config.ts +8 -0
  95. package/templates/template/gitignore +19 -1
  96. package/templates/template/npmrc +13 -4
  97. package/templates/template/package.json +10 -1
  98. package/templates/template/pnpm-workspace.yaml +17 -0
  99. package/templates/template/scripts/example.ts +4 -1
  100. package/dist/daemon-Bdl4lrdt.js.map +0 -1
  101. package/dist/daemon-entry-CmJn83zu.js.map +0 -1
  102. package/dist/pull-DqPRu1te.js.map +0 -1
  103. package/dist/state-c0CJ6Kwb.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-YDZK9NZ1.js","names":[],"sources":["../src/dev-db/daemon.ts"],"sourcesContent":["/**\n * Starting, finding and stopping the managed database, from the caller's side.\n *\n * Every command that needs Postgres calls {@link ensureManagedDatabase} and\n * gets a connection string back. Whether that started a process or found one\n * already running is not the caller's business, which is the point: `rebase\n * db push`, `rebase dev` and `rebase studio` in three terminals must all reach\n * the same database without coordinating, because two processes opening one\n * PGlite data directory would corrupt it.\n *\n * The hard part is not starting the daemon; it is deciding whether the one the\n * state file describes is still there. A pid can be recycled after a reboot and\n * a port can be taken by a stranger, so believing either on its own would let\n * Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the\n * daemon to identify itself instead.\n */\n\nimport { randomBytes } from \"crypto\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport {\n acquireStartLock,\n clearState,\n type DaemonState,\n dataDir,\n devDbDir,\n findFreePort,\n pidRunning,\n readState,\n releaseStartLock,\n stateFile\n} from \"./state\";\n\n/** How long to wait for a first boot. PGlite runs initdb on an empty data dir. */\nconst START_TIMEOUT_MS = 60_000;\n/** Poll interval while waiting for the daemon to publish its state file. */\nconst POLL_INTERVAL_MS = 150;\n/** How long the identity handshake may take before we call it dead. */\nconst IDENTITY_TIMEOUT_MS = 1_500;\n\nexport interface ManagedDatabase {\n /** Connection string for this project's managed database. */\n url: string;\n /** Where the data lives, for diagnostics and `--reset`. */\n dataDir: string;\n port: number;\n pid: number;\n /** True when this call started the daemon rather than finding it. */\n started: boolean;\n /**\n * Connection strings for the additional databases the caller asked for,\n * by declared key — `database(\"analytics\")` → `analytics`. Each is its\n * own PGlite instance behind the same daemon.\n */\n additional: Record<string, string>;\n}\n\n/**\n * PGlite's built-in superuser and database.\n *\n * Fixed by PGlite rather than chosen here, and asserted by the socket spike:\n * `session_user` comes back as `postgres`.\n */\nconst MANAGED_USER = \"postgres\";\nconst MANAGED_DATABASE = \"postgres\";\n\n/**\n * The DSN for the managed development database.\n *\n * **`sslmode=disable` is not decoration.** PGlite's socket server speaks no TLS,\n * and libpq-family clients try SSL first — so Atlas, which `rebase db push`\n * shells out to, failed the whole push with\n * `pq: SSL is not enabled on the server`. Its remedy box then told the reader to\n * append `sslmode=disable` to `DATABASE_URL`, which on this path is not set at\n * all: `rebase init` leaves it commented out precisely so the managed database\n * is used. The advice was correct and unfollowable.\n *\n * Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything\n * a driver shells out to later — rather than teaching each one about PGlite.\n */\nexport function managedUrl(port: number): string {\n return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}?sslmode=disable`;\n}\n\n/**\n * Ask the process behind a state record to prove it is the one we wrote down.\n *\n * A pid check alone answers \"is *a* process running\", and a port check alone\n * answers \"is *something* listening\" — after a reboot both say yes about\n * strangers. The daemon answers with the token from its own state file, so a\n * match is the only evidence accepted.\n */\nexport function isDaemonAlive(state: DaemonState): Promise<boolean> {\n if (!pidRunning(state.pid)) return Promise.resolve(false);\n\n return new Promise((resolve) => {\n const socket = new net.Socket();\n let answer = \"\";\n const settle = (alive: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(alive);\n };\n socket.setTimeout(IDENTITY_TIMEOUT_MS);\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.on(\"data\", (chunk) => {\n answer += chunk.toString(\"utf8\");\n if (answer.includes(\"\\n\")) settle(answer.trim() === `rebase-dev-db ${state.token}`);\n });\n socket.once(\"close\", () => settle(answer.trim() === `rebase-dev-db ${state.token}`));\n socket.connect(state.identityPort, \"127.0.0.1\");\n });\n}\n\n/** The running daemon for this project, or `null`. Never starts anything. */\nexport async function findRunningDaemon(projectRoot: string): Promise<DaemonState | null> {\n const state = readState(projectRoot);\n if (!state) return null;\n if (await isDaemonAlive(state)) return state;\n\n // The record describes something that is not there. Clearing it is the\n // whole recovery: the next ensure call starts a fresh daemon, and leaving\n // it would make every subsequent check pay the identity timeout.\n clearState(projectRoot);\n\n return null;\n}\n\n/**\n * Resolve the CLI entry point, so the daemon can be spawned as ourselves.\n *\n * A hidden subcommand rather than a second build entry: under `tsx` this file\n * is `src/dev-db/daemon.ts` and in a published CLI it is bundled into\n * `dist/index.js`, and in both cases the executable to re-invoke is the one\n * already running.\n */\nfunction resolveCliEntry(): string {\n // `process.argv[1]` is `bin/rebase.js` for a real invocation, which is\n // exactly what should be re-run. Under a test runner it is the runner, so\n // fall back to this module's own directory.\n const argvEntry = process.argv[1];\n if (argvEntry && fs.existsSync(argvEntry)) return argvEntry;\n\n return fileURLToPath(new URL(\"../index.js\", import.meta.url));\n}\n\n/**\n * How to re-invoke ourselves, which differs between a published CLI and this\n * repository.\n *\n * A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.\n * Inside the monorepo the entry is TypeScript, which plain `node` cannot load —\n * so the daemon has to be started through the same loader that is running now.\n * Getting this wrong fails as a spawn that exits instantly with a syntax error,\n * which is why the caller reads `pglite.log` on failure.\n */\nexport function resolveSpawn(entry: string): { execPath: string; prefixArgs: string[] } {\n if (!/\\.[cm]?ts$/.test(entry)) return { execPath: process.execPath, prefixArgs: [] };\n\n // Node can load TypeScript when tsx is registered as an import hook. tsx is\n // already a dependency of this package, because `rebase dev` runs the\n // backend through it.\n return { execPath: process.execPath, prefixArgs: [\"--import\", \"tsx\"] };\n}\n\nexport interface EnsureOptions {\n /** Silence the \"starting…\" progress line. */\n quiet?: boolean;\n /** Milliseconds of inactivity before the daemon exits. 0 disables. */\n idleTimeoutMs?: number;\n /** Where progress goes. Injected for tests. */\n onProgress?: (message: string) => void;\n /** Override how the daemon process is launched. For tests. */\n spawn?: { execPath: string; prefixArgs: string[] };\n /**\n * Override the CLI entry to re-invoke. For tests.\n *\n * Necessary because under a test runner `process.argv[1]` is the runner\n * itself, which exists and is therefore accepted by {@link resolveCliEntry}\n * — spawning vitest with `__dev-db-daemon` rather than the CLI.\n */\n entry?: string;\n /**\n * Keys of the additional databases the project declares, beyond the\n * default. Each is served by the daemon on request, so a project that\n * adds `database(\"analytics\")` while the daemon is running gets it\n * without a restart.\n */\n additionalKeys?: readonly string[];\n}\n\n/**\n * Ask the running daemon for one additional database, and get its port.\n *\n * The identity socket doubles as the command channel: the daemon answers\n * with its token first, which is how the caller knows it is talking to the\n * daemon the state file describes and not to a stranger on a recycled port.\n */\nfunction ensureAdditionalDatabase(state: DaemonState, key: string): Promise<number> {\n return new Promise((resolve, reject) => {\n const socket = new net.Socket();\n let answer = \"\";\n let identified = false;\n const fail = (message: string) => {\n socket.removeAllListeners();\n socket.destroy();\n reject(new Error(message));\n };\n socket.setTimeout(START_TIMEOUT_MS);\n socket.once(\"timeout\", () => fail(`The development database did not answer for \"${key}\" within ${Math.round(START_TIMEOUT_MS / 1000)}s.`));\n socket.once(\"error\", (err) => fail(`Could not reach the development database for \"${key}\": ${err.message}`));\n socket.on(\"data\", (chunk) => {\n answer += chunk.toString(\"utf8\");\n let newline = answer.indexOf(\"\\n\");\n while (newline !== -1) {\n const line = answer.slice(0, newline).trim();\n answer = answer.slice(newline + 1);\n if (!identified) {\n if (line !== `rebase-dev-db ${state.token}`) return fail(\"The process on the development database's port is not the daemon.\");\n identified = true;\n socket.write(`ensure ${key}\\n`);\n } else if (line.startsWith(\"ok \")) {\n const port = Number(line.slice(3));\n socket.removeAllListeners();\n socket.destroy();\n if (!Number.isInteger(port) || port <= 0) return reject(new Error(`The development database answered with an invalid port for \"${key}\": ${line}`));\n return resolve(port);\n } else {\n return fail(`The development database could not serve \"${key}\": ${line.replace(/^error\\s*/, \"\")}`);\n }\n newline = answer.indexOf(\"\\n\");\n }\n });\n socket.connect(state.identityPort, \"127.0.0.1\");\n });\n}\n\n/** Every additional database the caller asked for, as connection strings. */\nasync function adoptWithAdditional(state: DaemonState, started: boolean, keys: readonly string[]): Promise<ManagedDatabase> {\n const additional: Record<string, string> = {};\n for (const key of keys) {\n const known = state.databases?.[key];\n const port = known ? known.port : await ensureAdditionalDatabase(state, key);\n additional[key] = managedUrl(port);\n }\n return { ...adopt(state, started), additional };\n}\n\n/**\n * The project's managed database, started if it is not already running.\n *\n * Safe to call concurrently from several commands: the loser of the race finds\n * the winner's state file during its poll and adopts it rather than starting a\n * second daemon.\n */\nexport async function ensureManagedDatabase(\n projectRoot: string,\n options: EnsureOptions = {}\n): Promise<ManagedDatabase> {\n const keys = options.additionalKeys ?? [];\n const existing = await findRunningDaemon(projectRoot);\n if (existing) return adoptWithAdditional(existing, false, keys);\n\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n ensureGitignore(projectRoot);\n\n // Exactly one process may spawn a daemon. Without this, `rebase dev` and\n // `rebase db push` started in the same second both see no state file, both\n // spawn, and two processes open one PGlite data directory — the corruption\n // the single-daemon design exists to prevent.\n if (!acquireStartLock(projectRoot, START_TIMEOUT_MS)) {\n const adopted = await waitForDaemon(projectRoot, START_TIMEOUT_MS);\n if (adopted) return adoptWithAdditional(adopted, false, keys);\n\n // The holder gave up or died without publishing. Falling through to\n // start one ourselves is better than failing: the lock is stale by now,\n // so the next acquire will break it.\n if (!acquireStartLock(projectRoot, 0)) {\n throw new Error(\n \"Another process is starting the development database and did not finish.\\n\" +\n ` Remove ${path.join(devDbDir(projectRoot), \"starting.lock\")} if nothing is running.`\n );\n }\n }\n\n const port = await findFreePort();\n const token = randomBytes(16).toString(\"hex\");\n const entry = options.entry ?? resolveCliEntry();\n\n const spawnPlan = options.spawn ?? resolveSpawn(entry);\n const args = [\n ...spawnPlan.prefixArgs,\n entry,\n \"__dev-db-daemon\",\n \"--project\", projectRoot,\n \"--port\", String(port),\n \"--token\", token\n ];\n if (options.idleTimeoutMs !== undefined) {\n args.push(\"--idle-timeout\", String(options.idleTimeoutMs));\n }\n\n const log = fs.openSync(path.join(devDbDir(projectRoot), \"pglite.log\"), \"a\");\n const child: ChildProcess = spawn(spawnPlan.execPath, args, {\n // Detached with no stdin and its output on a file: the daemon must\n // survive the command that started it, and must not hold the terminal\n // open when that command exits.\n detached: true,\n stdio: [\"ignore\", log, log]\n });\n child.unref();\n\n try {\n const deadline = Date.now() + START_TIMEOUT_MS;\n let announced = false;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return adoptWithAdditional(state, true, keys);\n\n if (!announced && !options.quiet) {\n announced = true;\n options.onProgress?.(\"Starting the development database…\");\n }\n\n // A daemon that died on startup will never publish a state file, and\n // waiting the full minute for that is a bad way to learn it. The log\n // is the only place the reason exists.\n if (child.exitCode !== null && child.exitCode !== 0) {\n throw new Error(\n `The development database failed to start (exit ${child.exitCode}).\\n` +\n ` See ${path.join(devDbDir(projectRoot), \"pglite.log\")} for the reason.`\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n throw new Error(\n `The development database did not start within ${Math.round(START_TIMEOUT_MS / 1000)}s.\\n` +\n ` See ${path.join(devDbDir(projectRoot), \"pglite.log\")} for the reason.\\n` +\n \" To use your own Postgres instead, set DATABASE_URL or pass --database-url.\"\n );\n } finally {\n // Released whether we succeeded, timed out or threw. A lock left behind\n // by a crash is broken by age, but only after a full timeout — which a\n // developer should never have to wait out.\n releaseStartLock(projectRoot);\n }\n}\n\n/** Shape a live state record as the caller's result. */\nfunction adopt(state: DaemonState, started: boolean): ManagedDatabase {\n return {\n url: managedUrl(state.port),\n dataDir: state.dataDir,\n port: state.port,\n pid: state.pid,\n started,\n additional: {}\n };\n}\n\n/** Poll until another process publishes a live daemon, or give up. */\nasync function waitForDaemon(projectRoot: string, timeoutMs: number): Promise<DaemonState | null> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return state;\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n return null;\n}\n\n/** Stop the daemon. Returns false when there was nothing running. */\nexport async function stopManagedDatabase(projectRoot: string): Promise<boolean> {\n const state = await findRunningDaemon(projectRoot);\n if (!state) return false;\n\n try {\n process.kill(state.pid, \"SIGTERM\");\n } catch {\n clearState(projectRoot);\n\n return false;\n }\n\n // Wait for it to clear its own state file, which is how it says it closed\n // the data directory cleanly. A hard kill here would risk the WAL.\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n if (!fs.existsSync(stateFile(projectRoot))) return true;\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n clearState(projectRoot);\n\n return true;\n}\n\n/**\n * Stop the daemon and delete the data directory.\n *\n * Destructive and deliberately not clever: it removes the whole directory\n * rather than dropping schemas, because \"give me an empty database\" is the only\n * thing anyone means by it.\n */\nexport async function resetManagedDatabase(projectRoot: string): Promise<void> {\n await stopManagedDatabase(projectRoot);\n fs.rmSync(dataDir(projectRoot), { recursive: true, force: true });\n // Every additional database too: `pgdata__<key>` beside `pgdata`. \"Give\n // me an empty database\" means all of them — a reset that emptied the\n // default and kept the analytics rows would be a surprise on first read.\n const dir = devDbDir(projectRoot);\n if (fs.existsSync(dir)) {\n for (const entry of fs.readdirSync(dir)) {\n if (entry.startsWith(\"pgdata__\")) fs.rmSync(path.join(dir, entry), { recursive: true, force: true });\n }\n }\n clearState(projectRoot);\n}\n\n/**\n * Keep the generated directory out of git.\n *\n * `.rebase/` holds a Postgres data directory and a log. A project that commits\n * it will push hundreds of megabytes and a database that only makes sense on\n * one machine, so the ignore file is written next to the data rather than\n * relying on the project's root `.gitignore` having been updated.\n */\nfunction ensureGitignore(projectRoot: string): void {\n const target = path.join(devDbDir(projectRoot), \".gitignore\");\n if (fs.existsSync(target)) return;\n fs.writeFileSync(\n target,\n \"# Generated by `rebase dev` — a local Postgres data directory and its log.\\n\" +\n \"# Machine-specific and large; never commit it.\\n*\\n\",\n \"utf8\"\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { a as findFreePort, d as writeState, n as clearState, r as dataDir } from "./state-c0CJ6Kwb.js";
2
1
  import { n as PGLITE_EXTENSIONS } from "./constraints-DKGbNfUW.js";
2
+ import { f as writeState, i as dataDir, n as additionalDataDir, o as findFreePort, r as clearState } from "./state-C59Elrnt.js";
3
3
  import fs from "fs";
4
4
  import net from "net";
5
5
  //#region src/dev-db/notification-proxy.ts
@@ -297,11 +297,30 @@ async function loadExtensions() {
297
297
  * Rebase send a migration to a stranger. So the daemon publishes a token on a
298
298
  * second loopback port and the answer is only yes when the token matches.
299
299
  */
300
- function startIdentityServer(token, onConnection) {
300
+ /**
301
+ * How long a client has to send a command after the identity line, before
302
+ * the daemon closes the socket. The liveness check never sends one and
303
+ * closes on its own; this is for a client that connected and hung.
304
+ */
305
+ var COMMAND_TIMEOUT_MS = 2e3;
306
+ function startIdentityServer(token, onConnection, onCommand) {
301
307
  return new Promise((resolve, reject) => {
302
308
  const server = net.createServer((socket) => {
303
309
  onConnection();
304
- socket.end(`rebase-dev-db ${token}\n`);
310
+ socket.write(`rebase-dev-db ${token}\n`);
311
+ let buffered = "";
312
+ const timer = setTimeout(() => socket.end(), COMMAND_TIMEOUT_MS);
313
+ socket.on("data", (chunk) => {
314
+ buffered += chunk.toString("utf8");
315
+ const newline = buffered.indexOf("\n");
316
+ if (newline === -1) return;
317
+ clearTimeout(timer);
318
+ const command = buffered.slice(0, newline).trim();
319
+ buffered = "";
320
+ onCommand(command).then((reply) => socket.end(`${reply}\n`)).catch((err) => socket.end(`error ${err instanceof Error ? err.message : String(err)}\n`));
321
+ });
322
+ socket.on("error", () => clearTimeout(timer));
323
+ socket.on("close", () => clearTimeout(timer));
305
324
  });
306
325
  server.once("error", reject);
307
326
  server.listen(0, "127.0.0.1", () => resolve(server));
@@ -313,64 +332,118 @@ async function runDaemon(args) {
313
332
  const { PGlite } = await import("@electric-sql/pglite");
314
333
  const { PGLiteSocketServer } = await import("@electric-sql/pglite-socket");
315
334
  const extensions = await loadExtensions();
316
- const db = await PGlite.create({
317
- dataDir: directory,
318
- extensions
319
- });
320
- const upstreamPort = await findFreePort();
321
- const server = new PGLiteSocketServer({
322
- db,
323
- port: upstreamPort,
324
- host: "127.0.0.1",
325
- maxConnections: 4
326
- });
327
- await server.start();
328
- const proxy = new NotificationProxy({
329
- listenPort: args.port,
330
- upstreamPort,
331
- onNotification: (channel, _payload, copies) => {
332
- if (copies > 0) process.stdout.write(`dev-db: relayed notification on ${channel} to ${copies} client(s)\n`);
333
- }
334
- });
335
- await proxy.start();
335
+ const startInstance = async (instanceDir, listenPort, label) => {
336
+ fs.mkdirSync(instanceDir, { recursive: true });
337
+ const db = await PGlite.create({
338
+ dataDir: instanceDir,
339
+ extensions
340
+ });
341
+ const upstreamPort = await findFreePort();
342
+ const server = new PGLiteSocketServer({
343
+ db,
344
+ port: upstreamPort,
345
+ host: "127.0.0.1",
346
+ maxConnections: 4
347
+ });
348
+ await server.start();
349
+ const proxy = new NotificationProxy({
350
+ listenPort,
351
+ upstreamPort,
352
+ onNotification: (channel, _payload, copies) => {
353
+ if (copies > 0) process.stdout.write(`dev-db${label}: relayed notification on ${channel} to ${copies} client(s)\n`);
354
+ }
355
+ });
356
+ await proxy.start();
357
+ return {
358
+ db,
359
+ server,
360
+ proxy,
361
+ port: listenPort,
362
+ dataDir: instanceDir
363
+ };
364
+ };
365
+ const primary = await startInstance(directory, args.port, "");
366
+ /** Additional declared databases, by key. Started on demand. */
367
+ const additional = /* @__PURE__ */ new Map();
368
+ /** In-flight starts, so two commands asking for one key start one instance. */
369
+ const starting = /* @__PURE__ */ new Map();
336
370
  let idleSince = Date.now();
371
+ const publishState = (identityPort) => {
372
+ const databases = {};
373
+ for (const [key, instance] of additional) databases[key] = {
374
+ port: instance.port,
375
+ dataDir: instance.dataDir
376
+ };
377
+ writeState(args.projectRoot, {
378
+ port: args.port,
379
+ pid: process.pid,
380
+ dataDir: directory,
381
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
382
+ token: args.token,
383
+ identityPort,
384
+ ...additional.size > 0 ? { databases } : {}
385
+ });
386
+ };
387
+ let identityPort = 0;
388
+ const ensureAdditional = async (key) => {
389
+ const existing = additional.get(key);
390
+ if (existing) return existing;
391
+ const inFlight = starting.get(key);
392
+ if (inFlight) return inFlight;
393
+ const promise = (async () => {
394
+ const instance = await startInstance(additionalDataDir(args.projectRoot, key), await findFreePort(), ` [${key}]`);
395
+ additional.set(key, instance);
396
+ publishState(identityPort);
397
+ process.stdout.write(`dev-db [${key}]: ready on 127.0.0.1:${instance.port}\n`);
398
+ return instance;
399
+ })();
400
+ starting.set(key, promise);
401
+ try {
402
+ return await promise;
403
+ } finally {
404
+ starting.delete(key);
405
+ }
406
+ };
337
407
  const identity = await startIdentityServer(args.token, () => {
338
408
  idleSince = null;
409
+ }, async (command) => {
410
+ const match = /^ensure\s+(\S+)$/.exec(command);
411
+ if (!match) return `error unknown command "${command}"`;
412
+ const key = match[1];
413
+ if (!/[a-z0-9]/i.test(key) || key.includes("/") || key.includes("..")) return `error invalid key "${key}"`;
414
+ return `ok ${(await ensureAdditional(key)).port}`;
339
415
  });
340
416
  const identityAddress = identity.address();
341
- const identityPort = identityAddress !== null && typeof identityAddress !== "string" ? identityAddress.port : 0;
342
- writeState(args.projectRoot, {
343
- port: args.port,
344
- pid: process.pid,
345
- dataDir: directory,
346
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
347
- token: args.token,
348
- identityPort
349
- });
417
+ identityPort = identityAddress !== null && typeof identityAddress !== "string" ? identityAddress.port : 0;
418
+ publishState(identityPort);
350
419
  let shuttingDown = false;
351
420
  const shutdown = async (reason) => {
352
421
  if (shuttingDown) return;
353
422
  shuttingDown = true;
354
423
  process.stdout.write(`dev-db: stopping (${reason})\n`);
355
424
  clearState(args.projectRoot);
356
- try {
357
- await proxy.stop();
358
- } catch {}
359
- try {
360
- await server.stop();
361
- } catch {}
362
425
  identity.close();
363
- try {
364
- await db.close();
365
- } catch {}
426
+ for (const instance of [primary, ...additional.values()]) {
427
+ try {
428
+ await instance.proxy.stop();
429
+ } catch {}
430
+ try {
431
+ await instance.server.stop();
432
+ } catch {}
433
+ try {
434
+ await instance.db.close();
435
+ } catch {}
436
+ }
366
437
  process.exit(0);
367
438
  };
368
439
  process.on("SIGINT", () => void shutdown("SIGINT"));
369
440
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
370
441
  process.on("disconnect", () => {});
371
442
  if (args.idleTimeoutMs > 0) setInterval(() => {
372
- const stats = server.getStats();
373
- if (stats.activeConnections > 0 || stats.queuedQueries > 0) {
443
+ if ([primary, ...additional.values()].some((instance) => {
444
+ const stats = instance.server.getStats();
445
+ return stats.activeConnections > 0 || stats.queuedQueries > 0;
446
+ })) {
374
447
  idleSince = null;
375
448
  return;
376
449
  }
@@ -385,4 +458,4 @@ async function runDaemon(args) {
385
458
  //#endregion
386
459
  export { parseDaemonArgs, runDaemon };
387
460
 
388
- //# sourceMappingURL=daemon-entry-CmJn83zu.js.map
461
+ //# sourceMappingURL=daemon-entry-LTFKtpHy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-entry-LTFKtpHy.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_EXTENSIONS\n} from \"./constraints\";\nimport { NotificationProxy } from \"./notification-proxy\";\nimport { additionalDataDir, 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.\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 *\n * Every failure here is loud for that reason, including the one that is new:\n * pgvector arrives from a package of its own, so unlike the contrib bundles it\n * can be absent while PGlite itself is fine.\n */\nasync function loadExtensions(): Promise<Record<string, unknown>> {\n const extensions: Record<string, unknown> = {};\n for (const extension of PGLITE_EXTENSIONS) {\n let module: Record<string, unknown>;\n try {\n module = (await import(extension.module)) as Record<string, unknown>;\n } catch (err) {\n throw new Error(\n `Could not load \"${extension.module}\", which supplies the ${extension.name} extension: ` +\n `${err instanceof Error ? err.message : String(err)}\\n` +\n \"It is an optional dependency of @rebasepro/cli — reinstall the project, \" +\n \"or run `rebase dev --docker` to use a real Postgres instead.\"\n );\n }\n const bundle = module[extension.export];\n if (!bundle) {\n throw new Error(\n `${extension.module} did not export \"${extension.export}\". ` +\n \"The installed PGlite version may not ship this extension.\"\n );\n }\n extensions[extension.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 */\n/**\n * How long a client has to send a command after the identity line, before\n * the daemon closes the socket. The liveness check never sends one and\n * closes on its own; this is for a client that connected and hung.\n */\nconst COMMAND_TIMEOUT_MS = 2_000;\n\n/**\n * One command a client may send after reading the identity line.\n *\n * `ensure <key>` answers `ok <port>` once a PGlite instance for that declared\n * database is serving, starting it if it is not. A daemon started before a\n * second database was declared serves it without a restart, which matters\n * because `rebase studio` in another terminal is connected to the first.\n */\ntype CommandHandler = (command: string) => Promise<string>;\n\nfunction startIdentityServer(token: string, onConnection: () => void, onCommand: CommandHandler): Promise<net.Server> {\n return new Promise((resolve, reject) => {\n const server = net.createServer((socket) => {\n onConnection();\n socket.write(`rebase-dev-db ${token}\\n`);\n let buffered = \"\";\n const timer = setTimeout(() => socket.end(), COMMAND_TIMEOUT_MS);\n socket.on(\"data\", (chunk) => {\n buffered += chunk.toString(\"utf8\");\n const newline = buffered.indexOf(\"\\n\");\n if (newline === -1) return;\n clearTimeout(timer);\n const command = buffered.slice(0, newline).trim();\n buffered = \"\";\n onCommand(command)\n .then((reply) => socket.end(`${reply}\\n`))\n .catch((err) => socket.end(`error ${err instanceof Error ? err.message : String(err)}\\n`));\n });\n socket.on(\"error\", () => clearTimeout(timer));\n socket.on(\"close\", () => clearTimeout(timer));\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\n /** One PGlite instance, its socket server and the proxy clients reach it through. */\n interface Instance {\n db: { close(): Promise<void> };\n server: { stop(): Promise<void>; getStats(): { activeConnections: number; queuedQueries: number } };\n proxy: NotificationProxy;\n port: number;\n dataDir: string;\n }\n\n // The socket server listens privately; clients reach it through the\n // notification proxy on the public port. Realtime does not work\n // otherwise — PGlite is one session, so a NotificationResponse is handed\n // to whichever socket is reading rather than to the one that issued\n // LISTEN. See `notification-proxy.ts` for the measurements.\n const startInstance = async (instanceDir: string, listenPort: number, label: string): Promise<Instance> => {\n fs.mkdirSync(instanceDir, { recursive: true });\n const db = (await PGlite.create({ dataDir: instanceDir, extensions })) as { close(): Promise<void> };\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 const proxy = new NotificationProxy({\n listenPort,\n upstreamPort,\n onNotification: (channel, _payload, copies) => {\n if (copies > 0) process.stdout.write(`dev-db${label}: relayed notification on ${channel} to ${copies} client(s)\\n`);\n }\n });\n await proxy.start();\n return { db, server, proxy, port: listenPort, dataDir: instanceDir };\n };\n\n const primary = await startInstance(directory, args.port, \"\");\n /** Additional declared databases, by key. Started on demand. */\n const additional = new Map<string, Instance>();\n /** In-flight starts, so two commands asking for one key start one instance. */\n const starting = new Map<string, Promise<Instance>>();\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\n const publishState = (identityPort: number) => {\n const databases: Record<string, { port: number; dataDir: string }> = {};\n for (const [key, instance] of additional) databases[key] = { port: instance.port, dataDir: instance.dataDir };\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 ...(additional.size > 0 ? { databases } : {})\n });\n };\n\n let identityPort = 0;\n const ensureAdditional = async (key: string): Promise<Instance> => {\n const existing = additional.get(key);\n if (existing) return existing;\n const inFlight = starting.get(key);\n if (inFlight) return inFlight;\n const promise = (async () => {\n const instance = await startInstance(additionalDataDir(args.projectRoot, key), await findFreePort(), ` [${key}]`);\n additional.set(key, instance);\n publishState(identityPort);\n process.stdout.write(`dev-db [${key}]: ready on 127.0.0.1:${instance.port}\\n`);\n return instance;\n })();\n starting.set(key, promise);\n try {\n return await promise;\n } finally {\n starting.delete(key);\n }\n };\n\n const identity = await startIdentityServer(args.token, () => {\n idleSince = null;\n }, async (command) => {\n const match = /^ensure\\s+(\\S+)$/.exec(command);\n if (!match) return `error unknown command \"${command}\"`;\n const key = match[1];\n // The key becomes a directory name and a variable suffix; the same\n // rule the declaration passed applies here, so nothing a declaration\n // could not say reaches the filesystem.\n if (!/[a-z0-9]/i.test(key) || key.includes(\"/\") || key.includes(\"..\")) return `error invalid key \"${key}\"`;\n const instance = await ensureAdditional(key);\n return `ok ${instance.port}`;\n });\n const identityAddress = identity.address();\n identityPort = identityAddress !== null && typeof identityAddress !== \"string\" ? identityAddress.port : 0;\n\n publishState(identityPort);\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 identity.close();\n for (const instance of [primary, ...additional.values()]) {\n try {\n await instance.proxy.stop();\n } catch { /* already down */ }\n try {\n await instance.server.stop();\n } catch { /* already down */ }\n try {\n await instance.db.close();\n } catch { /* already closed */ }\n }\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 busy = [primary, ...additional.values()].some((instance) => {\n const stats = instance.server.getStats();\n return stats.activeConnections > 0 || stats.queuedQueries > 0;\n });\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,71 @@
1
+ /**
2
+ * Which database branch this checkout is working on.
3
+ *
4
+ * `rebase db branch create` made a full copy of the database in about a second
5
+ * and then stopped one step short of being a feature: there was no `switch`, no
6
+ * `--branch` on `rebase dev`, and nothing wrote the branch anywhere a command
7
+ * would read it. The only way to *use* a branch was to hand-edit
8
+ * `DATABASE_URL`, and the documentation claimed otherwise — "the CLI updates
9
+ * your local development configuration" — which it did not; the `.env` was
10
+ * byte-identical afterwards.
11
+ *
12
+ * This is that missing step, and the shape of it is chosen to avoid three
13
+ * things:
14
+ *
15
+ * 1. **It does not touch `.env`.** That file holds secrets, comments and the
16
+ * developer's own ordering, and a command that rewrites it in place will
17
+ * eventually lose something that was not its business. The branch is
18
+ * recorded beside the other per-checkout state in `.rebase/`, which is
19
+ * already gitignored — a branch is a fact about one machine, never about
20
+ * the project.
21
+ *
22
+ * 2. **It stores a name, not a connection string.** The credentials stay in
23
+ * exactly one place; the URL is derived by swapping the database name on
24
+ * the base one at resolution time. A stored URL would be a second copy of
25
+ * the password, and would go stale the moment `.env` changed.
26
+ *
27
+ * 3. **It does not outrank an explicit instruction.** `--database-url` and a
28
+ * `DATABASE_URL` in the shell are more immediate and more specific than a
29
+ * switch made yesterday, so they still win. The pointer sits directly above
30
+ * the project's `.env`: it is a deliberate, persistent choice, and `.env` is
31
+ * the project default it is meant to override. Anything less and `switch`
32
+ * would silently do nothing on every project that sets `DATABASE_URL` —
33
+ * which is every project not using the managed database.
34
+ */
35
+ /** Where the pointer lives, next to the rest of the per-checkout state. */
36
+ export declare function branchPointerPath(projectRoot: string): string;
37
+ export interface ActiveBranch {
38
+ /** The name the developer typed. */
39
+ name: string;
40
+ /** The PostgreSQL database that name refers to. */
41
+ database: string;
42
+ }
43
+ /**
44
+ * The prefix `BranchService` puts on every branch database.
45
+ *
46
+ * Duplicated from the driver rather than imported because the CLI must answer
47
+ * "which database" without loading a database driver — `rebase dev` reads this
48
+ * before it knows which plugin the project uses. It is covered by a test that
49
+ * fails if the driver's prefix moves.
50
+ */
51
+ export declare const BRANCH_DB_PREFIX = "rb_";
52
+ export declare function branchDatabaseName(name: string): string;
53
+ /** Read the active branch, or null when the checkout is on the main database. */
54
+ export declare function readActiveBranch(projectRoot: string): ActiveBranch | null;
55
+ export declare function writeActiveBranch(projectRoot: string, branch: ActiveBranch): void;
56
+ /** Return to the main database. Absent is success — this is idempotent. */
57
+ export declare function clearActiveBranch(projectRoot: string): void;
58
+ /**
59
+ * The base connection string with its database swapped for the branch's.
60
+ *
61
+ * Everything else is carried across untouched — credentials, host, port, and
62
+ * the query string, which is load-bearing here: `?sslmode=disable` is what a
63
+ * local Docker Postgres needs, and dropping it turns a switch into a TLS error
64
+ * that says nothing about branches.
65
+ *
66
+ * Returns null for a connection string that cannot be parsed, so the caller can
67
+ * say so plainly rather than emitting a mangled URL that fails later.
68
+ */
69
+ export declare function branchUrl(baseUrl: string, database: string): string | null;
70
+ /** The database name in a connection string, for reporting what you are on. */
71
+ export declare function databaseNameOf(connectionString: string): string | null;
@@ -24,7 +24,27 @@ export interface ManagedDatabase {
24
24
  pid: number;
25
25
  /** True when this call started the daemon rather than finding it. */
26
26
  started: boolean;
27
+ /**
28
+ * Connection strings for the additional databases the caller asked for,
29
+ * by declared key — `database("analytics")` → `analytics`. Each is its
30
+ * own PGlite instance behind the same daemon.
31
+ */
32
+ additional: Record<string, string>;
27
33
  }
34
+ /**
35
+ * The DSN for the managed development database.
36
+ *
37
+ * **`sslmode=disable` is not decoration.** PGlite's socket server speaks no TLS,
38
+ * and libpq-family clients try SSL first — so Atlas, which `rebase db push`
39
+ * shells out to, failed the whole push with
40
+ * `pq: SSL is not enabled on the server`. Its remedy box then told the reader to
41
+ * append `sslmode=disable` to `DATABASE_URL`, which on this path is not set at
42
+ * all: `rebase init` leaves it commented out precisely so the managed database
43
+ * is used. The advice was correct and unfollowable.
44
+ *
45
+ * Putting it in the URL fixes every consumer at once — Atlas, `pg`, and anything
46
+ * a driver shells out to later — rather than teaching each one about PGlite.
47
+ */
28
48
  export declare function managedUrl(port: number): string;
29
49
  /**
30
50
  * Ask the process behind a state record to prove it is the one we wrote down.
@@ -71,6 +91,13 @@ export interface EnsureOptions {
71
91
  * — spawning vitest with `__dev-db-daemon` rather than the CLI.
72
92
  */
73
93
  entry?: string;
94
+ /**
95
+ * Keys of the additional databases the project declares, beyond the
96
+ * default. Each is served by the daemon on request, so a project that
97
+ * adds `database("analytics")` while the daemon is running gets it
98
+ * without a restart.
99
+ */
100
+ additionalKeys?: readonly string[];
74
101
  }
75
102
  /**
76
103
  * The project's managed database, started if it is not already running.
@@ -28,6 +28,15 @@ export interface PrepareOptions {
28
28
  quiet?: boolean;
29
29
  /** Where human-facing lines go. Defaults to stdout via the caller. */
30
30
  onProgress?: (message: string) => void;
31
+ /**
32
+ * Keys of the additional databases the project declares —
33
+ * `database("analytics")` → `analytics`. On the managed path each gets
34
+ * its own PGlite instance and a `DATABASE_URL__<KEY>` in the child's
35
+ * environment, unless the developer set that variable themselves. On an
36
+ * external database nothing is added: whoever chose the database binds
37
+ * the rest of them.
38
+ */
39
+ additionalKeys?: readonly string[];
31
40
  }
32
41
  export interface PreparedDatabase {
33
42
  /** What the resolver chose, for the banner and for tests. */
@@ -45,7 +54,60 @@ export interface PreparedDatabase {
45
54
  dataDir?: string;
46
55
  /** True when this call started the managed database rather than finding it. */
47
56
  startedDaemon?: boolean;
57
+ /** The additional managed databases, by declared key, when any were asked for. */
58
+ additional?: Record<string, string>;
48
59
  }
60
+ /**
61
+ * The branch this checkout is switched to, as a connection string.
62
+ *
63
+ * Derived rather than stored: the pointer holds a name, and the credentials
64
+ * stay in the one place the developer wrote them. The base is the project's
65
+ * `.env` — a switch is a statement about *this project's* database, so a
66
+ * `DATABASE_URL` that happens to be in the shell is not the thing being
67
+ * branched, and it outranks the branch anyway.
68
+ *
69
+ * Returns null when there is no branch, and also when there is no base to swap
70
+ * the database name on: a project with no `DATABASE_URL` is on the managed
71
+ * database, where branching does not work at all.
72
+ *
73
+ * Exported for its tests. Asserting this through {@link prepareDatabaseEnv}
74
+ * would start a real PGlite daemon to check a decision made before any daemon
75
+ * is involved.
76
+ */
77
+ export declare function resolveActiveBranch(projectRoot: string, envFile: Record<string, string>): {
78
+ name: string;
79
+ url: string;
80
+ } | null;
81
+ /**
82
+ * The compose `db` service's connection string for this project, or null.
83
+ *
84
+ * Read here rather than in `resolve.ts` so the ordering there stays pure, and
85
+ * exported so `rebase dev` can hand the same URL to the preflight that starts
86
+ * the container — the preflight decides "local, and not running" from a DSN,
87
+ * so without one `--docker` asked for a container nothing ever started.
88
+ */
89
+ export declare function resolveComposeUrl(projectRoot: string, envFile: Record<string, string>): string | null;
90
+ /**
91
+ * Which database a project is on, decided without starting anything.
92
+ *
93
+ * `resolveDevDatabase` is pure, so the answer is available to help text and to
94
+ * a driver child alike — and neither should pay for a daemon to learn it. The
95
+ * driver cannot work it out for itself: on the managed path `DATABASE_URL` is a
96
+ * perfectly ordinary connection string to a Postgres on loopback, so a child
97
+ * handed only that has no way to know that `rebase db generate` will be refused.
98
+ *
99
+ * Null when there is no project here, which is legal — `rebase db --help` is
100
+ * answered outside one — and when anything at all goes wrong reading the
101
+ * project, because help text must not fail to print over an unreadable `.env`.
102
+ */
103
+ export declare function devDatabaseKind(projectRoot?: string | null): DevDatabase["kind"] | null;
104
+ /**
105
+ * The variable a driver child reads {@link devDatabaseKind} from.
106
+ *
107
+ * One name, exported, because three commands build that child's environment and
108
+ * a fourth reads it in another package.
109
+ */
110
+ export declare const DEV_DATABASE_KIND_ENV = "REBASE_DEV_DATABASE_KIND";
49
111
  /**
50
112
  * Resolve, start if needed, and describe the database for this command.
51
113
  *
@@ -60,6 +60,34 @@ export interface ColumnRef {
60
60
  export declare function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[];
61
61
  /** `UPDATE` statements for one anonymization pass, in a stable order. */
62
62
  export declare function anonymizeStatements(columns: readonly ColumnRef[]): string[];
63
+ /**
64
+ * Schemas the restored copy needs the app role provisioned on.
65
+ *
66
+ * `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives
67
+ * with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no
68
+ * privileges behind them. Measured on a 30-table project: 68 policies restored,
69
+ * 14 tables with RLS on, and **0** grants to `rebase_user` — where the source
70
+ * had 60. Reading one table as the role Rebase serves every request through:
71
+ *
72
+ * source: 6
73
+ * copy: ERROR: permission denied for table leads
74
+ *
75
+ * The dump flags are right and stay: without `--no-owner`/`--no-privileges`
76
+ * every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails
77
+ * against roles that do not exist on a laptop, and buries the real output. The
78
+ * repair belongs after the restore, and belongs to `ensureAppRole` — the same
79
+ * routine boot and `rebase db push` call — rather than to a second list of
80
+ * grants written here.
81
+ *
82
+ * Which schemas: every non-system schema the restored database actually has.
83
+ * Boot knows its `managedSchemas` from the collections; a restore knows only
84
+ * what arrived, and a source may carry schemas this project does not declare.
85
+ * `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to
86
+ * grant on and PostgreSQL would refuse anyway.
87
+ */
88
+ export declare function provisionableSchemas(rows: readonly {
89
+ schema: string;
90
+ }[]): string[];
63
91
  /** Host and database of a connection string, with no credentials in it. */
64
92
  export declare function describeTarget(connectionString: string): string;
65
93
  export interface PullPlan {