@rebasepro/cli 0.18.1 → 0.19.1-canary.g5a92fa9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.18.1",
3
+ "version": "0.19.1-canary.g5a92fa9",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "keywords": [
6
6
  "cli",
@@ -43,12 +43,12 @@
43
43
  "inquirer": "14.0.2",
44
44
  "jiti": "^2.7.0",
45
45
  "pg": "^8.22.0",
46
- "@rebasepro/agent-skills": "0.18.1",
47
- "@rebasepro/codegen": "0.18.1",
48
- "@rebasepro/client": "0.18.1",
49
- "@rebasepro/server-postgres": "0.18.1",
50
- "@rebasepro/types": "0.18.1",
51
- "@rebasepro/server": "0.18.1"
46
+ "@rebasepro/agent-skills": "0.19.1-canary.g5a92fa9",
47
+ "@rebasepro/codegen": "0.19.1-canary.g5a92fa9",
48
+ "@rebasepro/client": "0.19.1-canary.g5a92fa9",
49
+ "@rebasepro/types": "0.19.1-canary.g5a92fa9",
50
+ "@rebasepro/server": "0.19.1-canary.g5a92fa9",
51
+ "@rebasepro/server-postgres": "0.19.1-canary.g5a92fa9"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.1.2",
@@ -2,17 +2,19 @@ import * as dotenv from "dotenv";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { loadEnv } from "@rebasepro/server";
6
- // This should be `import { loadEnv, z } from "@rebasepro/server"`, and will be
7
- // in the first release that publishes that `z`. `loadEnv({ extend })` composes
8
- // the schema below with the framework's, and a schema built against a second
9
- // copy of zod fails the framework's instanceof checks and is silently dropped —
10
- // so taking `z` from the runtime is the only form that is reliably correct.
11
- // The `@rebasepro/server` version an ejected project pins does not export it
12
- // yet, and a template may only import what the version it pins publishes
13
- // (`pnpm check:templates`); an import of a symbol that is not there fails at
14
- // boot rather than at build. Switch both lines back when the version bumps.
15
- import { z } from "zod";
5
+ // `z` comes from the runtime, not from "zod". `loadEnv({ extend })` composes the
6
+ // schema below with the framework's, and zod recognises a `.default()` by class
7
+ // identity so a schema built against a second copy is not merged, it is
8
+ // rejected field by field. The ejected server then dies at boot on a raw
9
+ // ZodError naming SMTP_PORT, SMTP_SECURE and APP_NAME, every one of which has a
10
+ // default and none of which the operator set.
11
+ //
12
+ // This file imported from "zod" between 0.17.3 and 0.19.0, because the version
13
+ // an ejected project pinned did not publish `z` and a template may only import
14
+ // what its pinned version exports (`pnpm check:templates`). 0.18.0 published it;
15
+ // the workaround outlived its reason and became the defect it was written to
16
+ // avoid, in every project ejected on 0.18.0, 0.18.1 or 0.19.0.
17
+ import { loadEnv, z } from "@rebasepro/server";
16
18
 
17
19
  const __filename = fileURLToPath(import.meta.url);
18
20
  const __dirname = path.dirname(__filename);
@@ -23,14 +23,14 @@ import {
23
23
  import { declaredDataSources, declaredStorageSources } from "@rebasepro/types";
24
24
  // Side-effect import: declaring is what registers, so anything that dropped
25
25
  // this as "unused" would leave the backend with no buckets — silently.
26
- import "../../config/resources.js";
26
+ import "../../config/resources";
27
27
  // {{#collections}}
28
- import { enums, relations, tables } from "./schema.generated.js";
28
+ import { enums, relations, tables } from "./schema.generated";
29
29
  // {{/collections}}
30
- import { storageAuthorize } from "../../config/storage.js";
31
- import { env } from "./env.js";
30
+ import { storageAuthorize } from "../../config/storage";
31
+ import { env } from "./env";
32
32
  // {{#collections}}
33
- import usersCollection from "../../config/collections/users.js";
33
+ import usersCollection from "../../config/collections/users";
34
34
  // {{/collections}}
35
35
 
36
36
  const __filename = fileURLToPath(import.meta.url);
@@ -4,7 +4,7 @@
4
4
  "description": "Rebase BaaS — headless PostgreSQL API",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "tsc"
7
+ "build": "tsc && rebase normalize-imports dist"
8
8
  },
9
9
  "dependencies": {
10
10
  "@rebasepro/server": "workspace:*",
@@ -12,15 +12,19 @@
12
12
  "forceConsistentCasingInFileNames": true,
13
13
  "resolveJsonModule": true,
14
14
  "declaration": true,
15
- "sourceMap": true
15
+ "sourceMap": true,
16
+ "rootDir": ".."
16
17
  },
17
18
  // `functions/` as well as `src/`, because this flavour has nothing in src:
18
19
  // it declares no collections, so there is no generated schema, and the server
19
20
  // entrypoint lives behind `rebase eject`. An include matching only an empty
20
21
  // directory is TS18003 — tsc reports "no inputs" as an error, not a no-op.
21
22
  //
22
- // No `rootDir`: it would have to be `.` for both, and letting tsc infer the
23
- // common root gets there without a second place to keep in sync.
23
+ // `rootDir` is `..`, not `.`: `rebase eject` writes an entrypoint that imports
24
+ // `../../config/resources`, and tsc emits every file it pulls into the
25
+ // program — so the common source directory is the project root and the emit
26
+ // lands at `dist/backend/src/…`, which is what `scripts.start` runs. Inferring
27
+ // it stopped being an option in TypeScript 6 (TS5011).
24
28
  //
25
29
  // `crons/` too: `rebase build` compiles it through a generated tsconfig, but
26
30
  // an ejected project builds with *this* file, so leaving it out meant an
@@ -21,4 +21,4 @@
21
21
  * admin surface and no React.
22
22
  */
23
23
 
24
- export { storageAuthorize } from "./storage.js";
24
+ export { storageAuthorize } from "./storage";
@@ -7,7 +7,7 @@
7
7
  "type": "module",
8
8
  "private": true,
9
9
  "scripts": {
10
- "build": "tsc",
10
+ "build": "tsc && rebase normalize-imports dist",
11
11
  "dev": "tsc --watch",
12
12
  "clean": "rm -rf dist"
13
13
  },
@@ -4,7 +4,7 @@
4
4
  "description": "Rebase backend with PostgreSQL",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "rebase schema generate --collections ../config/collections && tsc"
7
+ "build": "rebase schema generate --collections ../config/collections && tsc && rebase normalize-imports dist"
8
8
  },
9
9
  "dependencies": {
10
10
  "{{PROJECT_NAME}}-config": "*",
@@ -5,6 +5,12 @@
5
5
  "moduleResolution": "bundler",
6
6
  "lib": ["ES2022"],
7
7
  "outDir": "./dist",
8
+ // The program includes `../config/**`, so tsc's common source directory is
9
+ // the project root and the emit already lands at `dist/backend/src/…` —
10
+ // which is what `scripts.start` runs. Saying so explicitly is required from
11
+ // TypeScript 6, which refuses to infer it (TS5011), and changes nothing
12
+ // about the layout.
13
+ "rootDir": "..",
8
14
  "strict": true,
9
15
  "esModuleInterop": true,
10
16
  "allowSyntheticDefaultImports": true,
@@ -1,7 +1,7 @@
1
- import postsCollection from "./posts.js";
2
- import authorsCollection from "./authors.js";
3
- import tagsCollection from "./tags.js";
4
- import usersCollection from "./users.js";
1
+ import postsCollection from "./posts";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
+ import usersCollection from "./users";
5
5
  import type { SecurityRule } from "@rebasepro/types";
6
6
 
7
7
  export const collections = [postsCollection, authorsCollection, tagsCollection, usersCollection];
@@ -1,6 +1,6 @@
1
1
  import { defineCollection } from "@rebasepro/cms-types";
2
- import authorsCollection from "./authors.js";
3
- import tagsCollection from "./tags.js";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
4
 
5
5
  const postsCollection = defineCollection({
6
6
  name: "Posts",
@@ -1,5 +1,5 @@
1
1
  // Resolves after `rebase init` copies this file into config/collections/,
2
2
  // next to the shared users.ts — not from inside presets/, which never runs.
3
- import usersCollection from "./users.js";
3
+ import usersCollection from "./users";
4
4
 
5
5
  export const collections = [usersCollection];
@@ -1,8 +1,8 @@
1
- import productsCollection from "./products.js";
2
- import categoriesCollection from "./categories.js";
3
- import ordersCollection from "./orders.js";
1
+ import productsCollection from "./products";
2
+ import categoriesCollection from "./categories";
3
+ import ordersCollection from "./orders";
4
4
  // Resolves after `rebase init` copies this file into config/collections/,
5
5
  // next to the shared users.ts — not from inside presets/, which never runs.
6
- import usersCollection from "./users.js";
6
+ import usersCollection from "./users";
7
7
 
8
8
  export const collections = [productsCollection, categoriesCollection, ordersCollection, usersCollection];
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from "@rebasepro/cms-types";
2
- import categoriesCollection from "./categories.js";
2
+ import categoriesCollection from "./categories";
3
3
 
4
4
  const productsCollection = defineCollection({
5
5
  name: "Products",
@@ -3,10 +3,10 @@
3
3
  * Collections defined here are used by both the frontend and backend
4
4
  */
5
5
 
6
- export { collections } from "./collections/index.js";
6
+ export { collections } from "./collections/index";
7
7
 
8
8
  // Who may read, write, delete and list files in storage. Exported from here
9
9
  // because that is where the runtime and `rebase cloud deploy` look for it — the
10
10
  // bundle manifest records whether this export exists, so a managed deploy can be
11
11
  // refused with a readable message instead of turning into a boot crash-loop.
12
- export { storageAuthorize } from "./storage.js";
12
+ export { storageAuthorize } from "./storage";
@@ -7,7 +7,7 @@
7
7
  "type": "module",
8
8
  "private": true,
9
9
  "scripts": {
10
- "build": "tsc",
10
+ "build": "tsc && rebase normalize-imports dist",
11
11
  "dev": "tsc --watch",
12
12
  "clean": "rm -rf dist"
13
13
  },
@@ -1 +0,0 @@
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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"}