@rebasepro/cli 0.24.0 → 0.24.1-canary.g19596ef
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rebase.js +3 -0
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-Bl1I9q7M.js +525 -0
- package/dist/api-keys-Bl1I9q7M.js.map +1 -0
- package/dist/apps-CNgQJjg5.js +340 -0
- package/dist/apps-CNgQJjg5.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
- package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
- package/dist/auth-aySHEF45.js +414 -0
- package/dist/auth-aySHEF45.js.map +1 -0
- package/dist/branch-pointer-DvSHBjjz.js +2 -0
- package/dist/build-DIHWIqO4.js +336 -0
- package/dist/build-DIHWIqO4.js.map +1 -0
- package/dist/bundle-CqURVN-K.js +1621 -0
- package/dist/bundle-CqURVN-K.js.map +1 -0
- package/dist/bundle.d.ts +6 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cloud-DjByQlkW.js +9667 -0
- package/dist/cloud-DjByQlkW.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/api-keys.d.ts +2 -0
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/db.d.ts +64 -24
- package/dist/commands/dev.d.ts +103 -6
- package/dist/commands/generate_sdk.d.ts +26 -0
- package/dist/commands/init.d.ts +58 -0
- package/dist/commands/skills.d.ts +22 -7
- package/dist/commands/start.d.ts +15 -0
- package/dist/commands/storage.d.ts +46 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/consent-ClRHmcxf.js +76 -0
- package/dist/consent-ClRHmcxf.js.map +1 -0
- package/dist/console-D6SJf8k_.js +102 -0
- package/dist/console-D6SJf8k_.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
- package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
- package/dist/daemon-pzsQwDHa.js +2 -0
- package/dist/db-B7RuNuaQ.js +1312 -0
- package/dist/db-B7RuNuaQ.js.map +1 -0
- package/dist/derive-Dl0z2OWi.js +484 -0
- package/dist/derive-Dl0z2OWi.js.map +1 -0
- package/dist/dev-CYr_WXan.js +1599 -0
- package/dist/dev-CYr_WXan.js.map +1 -0
- package/dist/dev-db/atlas-dev-database.d.ts +32 -0
- package/dist/dev-db/daemon-entry.d.ts +13 -0
- package/dist/dev-db/last-dev-database.d.ts +41 -0
- package/dist/dev-db/prepare.d.ts +19 -3
- package/dist/dev-db/pull.d.ts +90 -5
- package/dist/dev-db/resolve.d.ts +26 -2
- package/dist/dev-preflight-B-LYyBf1.js +2 -0
- package/dist/dev-server-DG8Tt-uf.js +96 -0
- package/dist/dev-server-DG8Tt-uf.js.map +1 -0
- package/dist/doctor-BP8cZ-7F.js +771 -0
- package/dist/doctor-BP8cZ-7F.js.map +1 -0
- package/dist/eject-DizdoecV.js +335 -0
- package/dist/eject-DizdoecV.js.map +1 -0
- package/dist/fold-static-BC9CYicw.js +201 -0
- package/dist/fold-static-BC9CYicw.js.map +1 -0
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-BPqdJc3Q.js +489 -0
- package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
- package/dist/index.es.js +151 -24674
- package/dist/index.es.js.map +1 -1
- package/dist/init-Utstvh2Q.js +1149 -0
- package/dist/init-Utstvh2Q.js.map +1 -0
- package/dist/last-dev-database-B58g89uA.js +2 -0
- package/dist/manifest-DRp0UVTj.js +880 -0
- package/dist/manifest-DRp0UVTj.js.map +1 -0
- package/dist/manifest.d.ts +15 -3
- package/dist/normalize-imports-X0Tgze9y.js +107 -0
- package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
- package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
- package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
- package/dist/path-flags-BFgG9V6y.js +91 -0
- package/dist/path-flags-BFgG9V6y.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-CDN6lSpz.js +1236 -0
- package/dist/prepare-CDN6lSpz.js.map +1 -0
- package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/pull-CrGYMEsy.js +458 -0
- package/dist/pull-CrGYMEsy.js.map +1 -0
- package/dist/resolve-B2O-wKKk.js +2 -0
- package/dist/resources-FD7AfZx8.js +183 -0
- package/dist/resources-FD7AfZx8.js.map +1 -0
- package/dist/schema-DMdr431S.js +125 -0
- package/dist/schema-DMdr431S.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-DEa-hOBh.js +267 -0
- package/dist/start-DEa-hOBh.js.map +1 -0
- package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
- package/dist/state-Do7ZJYkF.js.map +1 -0
- package/dist/status-DxnJTekv.js +352 -0
- package/dist/status-DxnJTekv.js.map +1 -0
- package/dist/storage-DyVq-f_F.js +303 -0
- package/dist/storage-DyVq-f_F.js.map +1 -0
- package/dist/telemetry/identity.d.ts +10 -1
- package/dist/telemetry-C8Uq8phb.js +129 -0
- package/dist/telemetry-C8Uq8phb.js.map +1 -0
- package/dist/telemetry-EMFRVRQi.js +351 -0
- package/dist/telemetry-EMFRVRQi.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-CtZtRkTE.js +1332 -0
- package/dist/upgrade-CtZtRkTE.js.map +1 -0
- package/dist/upgrade.d.ts +24 -3
- package/dist/utils/dev-preflight.d.ts +47 -5
- package/dist/utils/dev-server.d.ts +30 -0
- package/dist/utils/path-flags.d.ts +15 -2
- package/dist/utils/project.d.ts +16 -0
- package/dist/utils/temp-archive.d.ts +9 -0
- package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
- package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
- package/package.json +7 -7
- package/templates/eject/Dockerfile +4 -2
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +29 -4
- package/templates/eject/docker-compose.custom.yml +8 -4
- package/templates/generated-schemas/blank.ts +40 -0
- package/templates/generated-schemas/blog.ts +127 -0
- package/templates/generated-schemas/ecommerce.ts +93 -0
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/overlays/baas/package.json +1 -0
- package/templates/template/.env.example +10 -4
- package/templates/template/README.md +11 -4
- package/templates/template/ai-instructions.md +3 -2
- package/templates/template/config/resources.ts +7 -6
- package/templates/template/docker-compose.yml +9 -3
- package/templates/template/frontend/vite.config.ts +13 -3
- package/templates/template/package.json +1 -0
- package/templates/template/scripts/example.ts +2 -1
- package/templates/template/tsconfig.json +25 -0
- package/dist/branch-pointer-kk9F5FBO.js +0 -111
- package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-C8p32i-n.js +0 -357
- package/dist/daemon-C8p32i-n.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-DmaGLmH2.js.map +0 -1
- package/dist/pull-Dywm5FWP.js +0 -232
- package/dist/pull-Dywm5FWP.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
- package/templates/template/backend/src/schema.generated.ts +0 -13
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import net from "net";
|
|
6
|
-
import { execa } from "execa";
|
|
7
|
-
//#region src/utils/dev-preflight.ts
|
|
8
|
-
/**
|
|
9
|
-
* Make `rebase dev` the only command a new project needs.
|
|
10
|
-
*
|
|
11
|
-
* The scaffolded project prints three steps — start the database container,
|
|
12
|
-
* push the schema, start the dev server — and a first-time reader has to
|
|
13
|
-
* perform all three correctly before anything works. Two of them are
|
|
14
|
-
* mechanical, and both fail in ways that do not name themselves: skipping the
|
|
15
|
-
* container gives `ECONNREFUSED` from a stack the reader has never seen, and
|
|
16
|
-
* skipping the push gives a server that starts cleanly and then 500s on the
|
|
17
|
-
* first request because the tables are not there.
|
|
18
|
-
*
|
|
19
|
-
* So `dev` does them. Not always, and not silently — under four conditions that
|
|
20
|
-
* together mean "this is a throwaway development database on this laptop, and
|
|
21
|
-
* it is not running":
|
|
22
|
-
*
|
|
23
|
-
* 1. The DSN resolves to a loopback address. A remote database is somebody's
|
|
24
|
-
* real data and is never touched here, whatever else is true.
|
|
25
|
-
* 2. Nothing is listening on it. A database that answers is left completely
|
|
26
|
-
* alone — no push, no inspection, no connection.
|
|
27
|
-
* 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.
|
|
28
|
-
* the one `rebase init` wrote. Without it there is nothing to start and
|
|
29
|
-
* the backend's own error is the better message.
|
|
30
|
-
* 4. Docker is available. If it is not, the manual steps are printed and dev
|
|
31
|
-
* continues — a preflight must never be the reason the command fails.
|
|
32
|
-
*
|
|
33
|
-
* The schema push happens only when this function actually started the
|
|
34
|
-
* container, which is the one moment a local database is known to be new or
|
|
35
|
-
* stale relative to the collections. A `dev` against an already-running
|
|
36
|
-
* database never mutates its schema.
|
|
37
|
-
*/
|
|
38
|
-
var dev_preflight_exports = /* @__PURE__ */ __exportAll({
|
|
39
|
-
composeDatabaseUrl: () => composeDatabaseUrl,
|
|
40
|
-
composeDeclaresDbService: () => composeDeclaresDbService,
|
|
41
|
-
composeHostPort: () => composeHostPort,
|
|
42
|
-
ensureDevDatabase: () => ensureDevDatabase,
|
|
43
|
-
parseLoopbackDsn: () => parseLoopbackDsn,
|
|
44
|
-
probeTcp: () => probeTcp,
|
|
45
|
-
waitForPort: () => waitForPort
|
|
46
|
-
});
|
|
47
|
-
/**
|
|
48
|
-
* Hosts that mean "this machine", and so may be provisioned automatically.
|
|
49
|
-
*
|
|
50
|
-
* An exact-match set, never a prefix or substring test: `localhost.example.com`
|
|
51
|
-
* is a perfectly resolvable name that somebody else controls.
|
|
52
|
-
*/
|
|
53
|
-
var LOOPBACK = /* @__PURE__ */ new Set([
|
|
54
|
-
"localhost",
|
|
55
|
-
"127.0.0.1",
|
|
56
|
-
"::1",
|
|
57
|
-
"0.0.0.0"
|
|
58
|
-
]);
|
|
59
|
-
/**
|
|
60
|
-
* The host and port of a Postgres DSN, but only when it points at this machine.
|
|
61
|
-
*
|
|
62
|
-
* Returns null for anything else — a remote host, an unparseable string, a
|
|
63
|
-
* non-postgres scheme. Callers treat null as "do nothing", so every parse
|
|
64
|
-
* failure fails closed onto the safe behaviour rather than onto a guess.
|
|
65
|
-
*/
|
|
66
|
-
function parseLoopbackDsn(dsn) {
|
|
67
|
-
if (!dsn) return null;
|
|
68
|
-
let url;
|
|
69
|
-
try {
|
|
70
|
-
url = new URL(dsn);
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (!/^postgres(ql)?:$/.test(url.protocol)) return null;
|
|
75
|
-
const host = url.hostname.replace(/^\[(.+)\]$/, "$1");
|
|
76
|
-
if (!LOOPBACK.has(host)) return null;
|
|
77
|
-
const port = url.port ? Number(url.port) : 5432;
|
|
78
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
79
|
-
return {
|
|
80
|
-
host,
|
|
81
|
-
port
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Whether a compose file declares a `db` service.
|
|
86
|
-
*
|
|
87
|
-
* Deliberately a text scan and not a YAML parse: the CLI has no YAML
|
|
88
|
-
* dependency, and the question is only ever asked of a file this tool wrote.
|
|
89
|
-
* A false negative costs the automation and nothing else — the reader gets the
|
|
90
|
-
* manual steps, which are correct.
|
|
91
|
-
*/
|
|
92
|
-
function composeDeclaresDbService(yamlText) {
|
|
93
|
-
let inServices = false;
|
|
94
|
-
for (const rawLine of yamlText.split(/\r?\n/)) {
|
|
95
|
-
const line = rawLine.replace(/\t/g, " ");
|
|
96
|
-
if (/^\s*#/.test(line) || line.trim() === "") continue;
|
|
97
|
-
if (/^services:\s*$/.test(line)) {
|
|
98
|
-
inServices = true;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (inServices && /^\S/.test(line)) {
|
|
102
|
-
inServices = false;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (inServices && /^\s{2,}db:\s*$/.test(line)) return true;
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* The connection string the compose file's `db` service will answer on.
|
|
111
|
-
*
|
|
112
|
-
* `--docker` used to be a flag that changed a banner and nothing else: it
|
|
113
|
-
* resolved to `kind: "docker"`, and every caller then had no URL to reach the
|
|
114
|
-
* container with, so the backend booted with no `DATABASE_URL` at all and died
|
|
115
|
-
* on the message a project with no database gets. The container it had just
|
|
116
|
-
* been asked for was never started, because the preflight that starts it needs
|
|
117
|
-
* a DSN to decide the database is local and not running.
|
|
118
|
-
*
|
|
119
|
-
* So the URL is derived rather than required: the compose file names the user,
|
|
120
|
-
* the database and the published host port, and `.env` holds the password
|
|
121
|
-
* compose itself interpolates. That is the same string `rebase init` writes as
|
|
122
|
-
* the commented-out `DATABASE_URL`, which is what makes uncommenting that line
|
|
123
|
-
* and passing `--docker` reach the same database.
|
|
124
|
-
*
|
|
125
|
-
* Returns null when the file declares no `db` service, or declares one this
|
|
126
|
-
* scan cannot read — a `--docker` that cannot be honoured has to say so, and
|
|
127
|
-
* a guess would point at somebody else's Postgres on 5432.
|
|
128
|
-
*
|
|
129
|
-
* Same text scan as {@link composeDeclaresDbService}, for the same reason.
|
|
130
|
-
*/
|
|
131
|
-
function composeDatabaseUrl(yamlText, env = {}) {
|
|
132
|
-
const found = scanComposeDbService(yamlText);
|
|
133
|
-
const user = expandComposeValue(found.POSTGRES_USER, env);
|
|
134
|
-
const password = expandComposeValue(found.POSTGRES_PASSWORD, env);
|
|
135
|
-
const database = expandComposeValue(found.POSTGRES_DB, env);
|
|
136
|
-
if (!user || !password || !database || !found.hostPort) return null;
|
|
137
|
-
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* The host port the compose `db` service publishes, or null.
|
|
141
|
-
*
|
|
142
|
-
* Separate from {@link composeDatabaseUrl} because the port is answerable on
|
|
143
|
-
* its own: the URL needs the user, the password and the database name too, and
|
|
144
|
-
* a compose file missing any of them still says which port a container would
|
|
145
|
-
* listen on. That is the question "would starting this container answer the
|
|
146
|
-
* DSN in `.env`?" — see the `wrong-port` branch of {@link ensureDevDatabase},
|
|
147
|
-
* where getting it wrong meant starting a container nobody asked for.
|
|
148
|
-
*/
|
|
149
|
-
function composeHostPort(yamlText) {
|
|
150
|
-
const port = scanComposeDbService(yamlText).hostPort;
|
|
151
|
-
return port ? Number(port) : null;
|
|
152
|
-
}
|
|
153
|
-
/** The raw values of the compose `db` service, before any expansion. */
|
|
154
|
-
function scanComposeDbService(yamlText) {
|
|
155
|
-
let inServices = false;
|
|
156
|
-
let inDb = false;
|
|
157
|
-
let dbIndent = 0;
|
|
158
|
-
let section = null;
|
|
159
|
-
const found = {};
|
|
160
|
-
let hostPort = null;
|
|
161
|
-
for (const rawLine of yamlText.split(/\r?\n/)) {
|
|
162
|
-
const line = rawLine.replace(/\t/g, " ");
|
|
163
|
-
if (/^\s*#/.test(line) || line.trim() === "") continue;
|
|
164
|
-
if (/^services:\s*$/.test(line)) {
|
|
165
|
-
inServices = true;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (inServices && /^\S/.test(line)) {
|
|
169
|
-
inServices = false;
|
|
170
|
-
inDb = false;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (!inServices) continue;
|
|
174
|
-
const indent = line.length - line.trimStart().length;
|
|
175
|
-
const dbHeader = line.match(/^(\s{2,})db:\s*$/);
|
|
176
|
-
if (dbHeader) {
|
|
177
|
-
inDb = true;
|
|
178
|
-
dbIndent = dbHeader[1].length;
|
|
179
|
-
section = null;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (inDb && indent <= dbIndent) {
|
|
183
|
-
inDb = false;
|
|
184
|
-
section = null;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (!inDb) continue;
|
|
188
|
-
const key = line.match(/^\s+(environment|ports):\s*$/);
|
|
189
|
-
if (key) {
|
|
190
|
-
section = key[1];
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (/^\s+[A-Za-z_][\w-]*:\s*\S/.test(line) && !/^\s+POSTGRES_/.test(line)) {
|
|
194
|
-
if (!/^\s*-\s/.test(line.trimStart())) section = null;
|
|
195
|
-
}
|
|
196
|
-
if (section === "environment") {
|
|
197
|
-
const pair = line.match(/^\s+(POSTGRES_(?:USER|PASSWORD|DB)):\s*(.+?)\s*$/);
|
|
198
|
-
if (pair) found[pair[1]] = pair[2].replace(/^["']|["']$/g, "");
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (section === "ports" && hostPort === null) {
|
|
202
|
-
const mapping = line.match(/^\s*-\s*["']?(?:[\d.]+:)?(\d+):5432(?:\/tcp)?["']?\s*$/);
|
|
203
|
-
if (mapping) hostPort = mapping[1];
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return hostPort === null ? found : {
|
|
207
|
-
...found,
|
|
208
|
-
hostPort
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves
|
|
213
|
-
* them — against the project's `.env`, which is the file compose itself reads.
|
|
214
|
-
*/
|
|
215
|
-
function expandComposeValue(raw, env) {
|
|
216
|
-
if (!raw) return null;
|
|
217
|
-
const trimmed = raw.replace(/\$\{([A-Za-z_][\w]*)(?::?-([^}]*))?\}/g, (_all, name, fallback) => {
|
|
218
|
-
const value = env[name];
|
|
219
|
-
if (value !== void 0 && value !== "") return value;
|
|
220
|
-
return fallback ?? "";
|
|
221
|
-
}).trim();
|
|
222
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
223
|
-
}
|
|
224
|
-
/** Is something accepting connections there right now? */
|
|
225
|
-
function probeTcp(host, port, timeoutMs = 700) {
|
|
226
|
-
return new Promise((resolve) => {
|
|
227
|
-
const socket = new net.Socket();
|
|
228
|
-
let settled = false;
|
|
229
|
-
const done = (open) => {
|
|
230
|
-
if (settled) return;
|
|
231
|
-
settled = true;
|
|
232
|
-
socket.destroy();
|
|
233
|
-
resolve(open);
|
|
234
|
-
};
|
|
235
|
-
socket.setTimeout(timeoutMs);
|
|
236
|
-
socket.once("connect", () => done(true));
|
|
237
|
-
socket.once("timeout", () => done(false));
|
|
238
|
-
socket.once("error", () => done(false));
|
|
239
|
-
socket.connect(port, host);
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Poll until the port answers, or give up.
|
|
244
|
-
*
|
|
245
|
-
* Postgres publishes its port before it is ready to accept queries, so the
|
|
246
|
-
* caller still has to tolerate a first connection that is refused at the
|
|
247
|
-
* protocol level. This only answers "has the container got as far as listening",
|
|
248
|
-
* which is the part that takes the seconds.
|
|
249
|
-
*/
|
|
250
|
-
async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
|
|
251
|
-
const deadline = Date.now() + timeoutMs;
|
|
252
|
-
while (Date.now() < deadline) {
|
|
253
|
-
if (await probeTcp(host, port, 700)) return true;
|
|
254
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
255
|
-
}
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Start the project's development database if it is not running, and give it a
|
|
260
|
-
* schema if this call is what started it.
|
|
261
|
-
*/
|
|
262
|
-
async function ensureDevDatabase(options) {
|
|
263
|
-
const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;
|
|
264
|
-
const log = options.log ?? ((message) => console.log(message));
|
|
265
|
-
if (disabled) return { action: "disabled" };
|
|
266
|
-
const target = parseLoopbackDsn(databaseUrl);
|
|
267
|
-
if (!databaseUrl) return { action: "no-dsn" };
|
|
268
|
-
if (!target) {
|
|
269
|
-
let host = "the configured host";
|
|
270
|
-
try {
|
|
271
|
-
host = new URL(databaseUrl).hostname || host;
|
|
272
|
-
} catch {}
|
|
273
|
-
return {
|
|
274
|
-
action: "remote-dsn",
|
|
275
|
-
host
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (await probeTcp(target.host, target.port)) return {
|
|
279
|
-
action: "already-running",
|
|
280
|
-
host: target.host,
|
|
281
|
-
port: target.port
|
|
282
|
-
};
|
|
283
|
-
const composePath = path.join(projectRoot, "docker-compose.yml");
|
|
284
|
-
if (!fs.existsSync(composePath)) return { action: "no-compose" };
|
|
285
|
-
const composeText = fs.readFileSync(composePath, "utf8");
|
|
286
|
-
if (!composeDeclaresDbService(composeText)) return { action: "no-compose" };
|
|
287
|
-
const manualSteps = `${chalk.cyan("docker compose up -d db")} then ${chalk.cyan("rebase db push")}`;
|
|
288
|
-
const composePort = composeHostPort(composeText);
|
|
289
|
-
if (composePort !== null && composePort !== target.port) {
|
|
290
|
-
const hint = `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. The compose file's db service publishes ${composePort}, so starting it would not answer that URL — nothing was started. Either correct the port in .env, or point DATABASE_URL at 127.0.0.1:${composePort}.`;
|
|
291
|
-
log("");
|
|
292
|
-
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
293
|
-
return {
|
|
294
|
-
action: "wrong-port",
|
|
295
|
-
hint,
|
|
296
|
-
port: target.port,
|
|
297
|
-
composePort
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
log("");
|
|
301
|
-
log(` ${chalk.gray("Database not running — starting it…")}`);
|
|
302
|
-
const startDatabase = options.startDatabase ?? (async (root) => {
|
|
303
|
-
await execa("docker", [
|
|
304
|
-
"compose",
|
|
305
|
-
"up",
|
|
306
|
-
"-d",
|
|
307
|
-
"db"
|
|
308
|
-
], {
|
|
309
|
-
cwd: root,
|
|
310
|
-
stdio: "pipe"
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
try {
|
|
314
|
-
await startDatabase(projectRoot);
|
|
315
|
-
} catch (error) {
|
|
316
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
-
if (/ENOENT|not found|command not found/i.test(message)) {
|
|
318
|
-
const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;
|
|
319
|
-
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
320
|
-
return {
|
|
321
|
-
action: "no-docker",
|
|
322
|
-
hint
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
const hint = `Could not start the database container. Run ${manualSteps} to see why.`;
|
|
326
|
-
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
327
|
-
return {
|
|
328
|
-
action: "start-failed",
|
|
329
|
-
hint
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
if (!await waitForPort(target.host, target.port)) {
|
|
333
|
-
const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;
|
|
334
|
-
log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
|
|
335
|
-
return {
|
|
336
|
-
action: "start-failed",
|
|
337
|
-
hint
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
log(` ${chalk.green("✓")} ${chalk.gray(`Database ready on port ${target.port}`)}`);
|
|
341
|
-
if (!hasCollections) return {
|
|
342
|
-
action: "started",
|
|
343
|
-
port: target.port,
|
|
344
|
-
pushed: false
|
|
345
|
-
};
|
|
346
|
-
log(` ${chalk.gray("Pushing the schema…")}`);
|
|
347
|
-
try {
|
|
348
|
-
await pushSchema();
|
|
349
|
-
log(` ${chalk.green("✓")} ${chalk.gray("Schema pushed")}`);
|
|
350
|
-
return {
|
|
351
|
-
action: "started",
|
|
352
|
-
port: target.port,
|
|
353
|
-
pushed: true
|
|
354
|
-
};
|
|
355
|
-
} catch {
|
|
356
|
-
log(` ${chalk.yellow("⚠")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan("rebase db push")}.`)}`);
|
|
357
|
-
return {
|
|
358
|
-
action: "started",
|
|
359
|
-
port: target.port,
|
|
360
|
-
pushed: false
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
|
-
export { probeTcp as i, dev_preflight_exports as n, ensureDevDatabase as r, composeDatabaseUrl as t };
|
|
366
|
-
|
|
367
|
-
//# sourceMappingURL=dev-preflight-CNLl4rdo.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dev-preflight-CNLl4rdo.js","names":[],"sources":["../src/utils/dev-preflight.ts"],"sourcesContent":["/**\n * Make `rebase dev` the only command a new project needs.\n *\n * The scaffolded project prints three steps — start the database container,\n * push the schema, start the dev server — and a first-time reader has to\n * perform all three correctly before anything works. Two of them are\n * mechanical, and both fail in ways that do not name themselves: skipping the\n * container gives `ECONNREFUSED` from a stack the reader has never seen, and\n * skipping the push gives a server that starts cleanly and then 500s on the\n * first request because the tables are not there.\n *\n * So `dev` does them. Not always, and not silently — under four conditions that\n * together mean \"this is a throwaway development database on this laptop, and\n * it is not running\":\n *\n * 1. The DSN resolves to a loopback address. A remote database is somebody's\n * real data and is never touched here, whatever else is true.\n * 2. Nothing is listening on it. A database that answers is left completely\n * alone — no push, no inspection, no connection.\n * 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.\n * the one `rebase init` wrote. Without it there is nothing to start and\n * the backend's own error is the better message.\n * 4. Docker is available. If it is not, the manual steps are printed and dev\n * continues — a preflight must never be the reason the command fails.\n *\n * The schema push happens only when this function actually started the\n * container, which is the one moment a local database is known to be new or\n * stale relative to the collections. A `dev` against an already-running\n * database never mutates its schema.\n */\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { execa } from \"execa\";\n\n/** Where the preflight stopped, and why. Returned so tests can assert it. */\nexport type PreflightOutcome =\n | { action: \"disabled\" }\n | { action: \"no-dsn\" }\n | { action: \"remote-dsn\"; host: string }\n | { action: \"already-running\"; host: string; port: number }\n | { action: \"no-compose\" }\n | { action: \"no-docker\"; hint: string }\n | { action: \"start-failed\"; hint: string }\n | { action: \"wrong-port\"; hint: string; port: number; composePort: number }\n | { action: \"started\"; port: number; pushed: boolean };\n\n/**\n * Hosts that mean \"this machine\", and so may be provisioned automatically.\n *\n * An exact-match set, never a prefix or substring test: `localhost.example.com`\n * is a perfectly resolvable name that somebody else controls.\n */\nconst LOOPBACK = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"0.0.0.0\"]);\n\n/**\n * The host and port of a Postgres DSN, but only when it points at this machine.\n *\n * Returns null for anything else — a remote host, an unparseable string, a\n * non-postgres scheme. Callers treat null as \"do nothing\", so every parse\n * failure fails closed onto the safe behaviour rather than onto a guess.\n */\nexport function parseLoopbackDsn(dsn: string | undefined): { host: string; port: number } | null {\n if (!dsn) return null;\n\n let url: URL;\n try {\n url = new URL(dsn);\n } catch {\n return null;\n }\n\n if (!/^postgres(ql)?:$/.test(url.protocol)) return null;\n\n // `url.hostname` KEEPS the brackets an IPv6 literal carries in a URL, and\n // `net.connect` rejects them. Normalising here rather than at each use is\n // what stops the two representations diverging: everything downstream gets\n // a host it can dial.\n const host = url.hostname.replace(/^\\[(.+)\\]$/, \"$1\");\n if (!LOOPBACK.has(host)) return null;\n\n const port = url.port ? Number(url.port) : 5432;\n if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;\n\n return { host, port };\n}\n\n/**\n * Whether a compose file declares a `db` service.\n *\n * Deliberately a text scan and not a YAML parse: the CLI has no YAML\n * dependency, and the question is only ever asked of a file this tool wrote.\n * A false negative costs the automation and nothing else — the reader gets the\n * manual steps, which are correct.\n */\nexport function composeDeclaresDbService(yamlText: string): boolean {\n let inServices = false;\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) {\n inServices = true;\n continue;\n }\n // Any other zero-indent key ends the services block.\n if (inServices && /^\\S/.test(line)) {\n inServices = false;\n continue;\n }\n if (inServices && /^\\s{2,}db:\\s*$/.test(line)) return true;\n }\n return false;\n}\n\n/**\n * The connection string the compose file's `db` service will answer on.\n *\n * `--docker` used to be a flag that changed a banner and nothing else: it\n * resolved to `kind: \"docker\"`, and every caller then had no URL to reach the\n * container with, so the backend booted with no `DATABASE_URL` at all and died\n * on the message a project with no database gets. The container it had just\n * been asked for was never started, because the preflight that starts it needs\n * a DSN to decide the database is local and not running.\n *\n * So the URL is derived rather than required: the compose file names the user,\n * the database and the published host port, and `.env` holds the password\n * compose itself interpolates. That is the same string `rebase init` writes as\n * the commented-out `DATABASE_URL`, which is what makes uncommenting that line\n * and passing `--docker` reach the same database.\n *\n * Returns null when the file declares no `db` service, or declares one this\n * scan cannot read — a `--docker` that cannot be honoured has to say so, and\n * a guess would point at somebody else's Postgres on 5432.\n *\n * Same text scan as {@link composeDeclaresDbService}, for the same reason.\n */\nexport function composeDatabaseUrl(\n yamlText: string,\n env: Record<string, string | undefined> = {}\n): string | null {\n const found = scanComposeDbService(yamlText);\n\n const user = expandComposeValue(found.POSTGRES_USER, env);\n const password = expandComposeValue(found.POSTGRES_PASSWORD, env);\n const database = expandComposeValue(found.POSTGRES_DB, env);\n if (!user || !password || !database || !found.hostPort) return null;\n\n return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}` +\n `@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;\n}\n\n/**\n * The host port the compose `db` service publishes, or null.\n *\n * Separate from {@link composeDatabaseUrl} because the port is answerable on\n * its own: the URL needs the user, the password and the database name too, and\n * a compose file missing any of them still says which port a container would\n * listen on. That is the question \"would starting this container answer the\n * DSN in `.env`?\" — see the `wrong-port` branch of {@link ensureDevDatabase},\n * where getting it wrong meant starting a container nobody asked for.\n */\nexport function composeHostPort(yamlText: string): number | null {\n const port = scanComposeDbService(yamlText).hostPort;\n\n return port ? Number(port) : null;\n}\n\n/** The raw values of the compose `db` service, before any expansion. */\nfunction scanComposeDbService(yamlText: string): Record<string, string> & { hostPort?: string } {\n let inServices = false;\n let inDb = false;\n let dbIndent = 0;\n let section: \"environment\" | \"ports\" | null = null;\n const found: Record<string, string> = {};\n let hostPort: string | null = null;\n\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) { inServices = true; continue; }\n if (inServices && /^\\S/.test(line)) { inServices = false; inDb = false; continue; }\n if (!inServices) continue;\n\n const indent = line.length - line.trimStart().length;\n const dbHeader = line.match(/^(\\s{2,})db:\\s*$/);\n if (dbHeader) { inDb = true; dbIndent = dbHeader[1].length; section = null; continue; }\n // A sibling service at the same indent as `db:` ends it.\n if (inDb && indent <= dbIndent) { inDb = false; section = null; continue; }\n if (!inDb) continue;\n\n const key = line.match(/^\\s+(environment|ports):\\s*$/);\n if (key) { section = key[1] as \"environment\" | \"ports\"; continue; }\n // Any other key of the service ends the block we were reading.\n if (/^\\s+[A-Za-z_][\\w-]*:\\s*\\S/.test(line) && !/^\\s+POSTGRES_/.test(line)) {\n if (!/^\\s*-\\s/.test(line.trimStart())) section = null;\n }\n\n if (section === \"environment\") {\n const pair = line.match(/^\\s+(POSTGRES_(?:USER|PASSWORD|DB)):\\s*(.+?)\\s*$/);\n if (pair) found[pair[1]] = pair[2].replace(/^[\"']|[\"']$/g, \"\");\n continue;\n }\n\n if (section === \"ports\" && hostPort === null) {\n // `- \"5435:5432\"`. Only a mapping whose container side is Postgres's\n // own port is the one to dial; anything else is a different service.\n const mapping = line.match(/^\\s*-\\s*[\"']?(?:[\\d.]+:)?(\\d+):5432(?:\\/tcp)?[\"']?\\s*$/);\n if (mapping) hostPort = mapping[1];\n }\n }\n\n return hostPort === null ? found : { ...found, hostPort };\n}\n\n/**\n * `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves\n * them — against the project's `.env`, which is the file compose itself reads.\n */\nfunction expandComposeValue(\n raw: string | undefined,\n env: Record<string, string | undefined>\n): string | null {\n if (!raw) return null;\n const interpolated = raw.replace(/\\$\\{([A-Za-z_][\\w]*)(?::?-([^}]*))?\\}/g, (_all, name, fallback) => {\n const value = env[name];\n if (value !== undefined && value !== \"\") return value;\n return fallback ?? \"\";\n });\n const trimmed = interpolated.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/** Is something accepting connections there right now? */\nexport function probeTcp(host: string, port: number, timeoutMs = 700): Promise<boolean> {\n return new Promise(resolve => {\n const socket = new net.Socket();\n let settled = false;\n\n const done = (open: boolean): void => {\n if (settled) return;\n settled = true;\n socket.destroy();\n resolve(open);\n };\n\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => done(true));\n socket.once(\"timeout\", () => done(false));\n socket.once(\"error\", () => done(false));\n socket.connect(port, host);\n });\n}\n\n/**\n * Poll until the port answers, or give up.\n *\n * Postgres publishes its port before it is ready to accept queries, so the\n * caller still has to tolerate a first connection that is refused at the\n * protocol level. This only answers \"has the container got as far as listening\",\n * which is the part that takes the seconds.\n */\nexport async function waitForPort(\n host: string,\n port: number,\n timeoutMs = 60_000,\n intervalMs = 500\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await probeTcp(host, port, 700)) return true;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n }\n return false;\n}\n\nexport interface EnsureDevDatabaseOptions {\n projectRoot: string;\n /** The DSN from the project's env file, if it has one. */\n databaseUrl: string | undefined;\n /** Set by `--no-db` or REBASE_DEV_NO_DB. */\n disabled: boolean;\n /** Whether the project has collections worth pushing. */\n hasCollections: boolean;\n /** Runs `rebase db push` for this project. Injected so tests need no database. */\n pushSchema: () => Promise<void>;\n /**\n * Brings the compose `db` service up. Injected for the same reason\n * `pushSchema` is: what happens *after* a start — a push that fails without\n * taking the dev server with it — is the part worth asserting, and it was\n * untestable while the only way in was a real `docker compose up`.\n */\n startDatabase?: (projectRoot: string) => Promise<void>;\n log?: (message: string) => void;\n}\n\n/**\n * Start the project's development database if it is not running, and give it a\n * schema if this call is what started it.\n */\nexport async function ensureDevDatabase(options: EnsureDevDatabaseOptions): Promise<PreflightOutcome> {\n const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;\n const log = options.log ?? ((message: string) => console.log(message));\n\n if (disabled) return { action: \"disabled\" };\n\n const target = parseLoopbackDsn(databaseUrl);\n if (!databaseUrl) return { action: \"no-dsn\" };\n if (!target) {\n // Not an error, and not worth a warning either: pointing DATABASE_URL at\n // a real database is a completely ordinary thing to do, and the only\n // correct response is to keep away from it.\n let host = \"the configured host\";\n try {\n host = new URL(databaseUrl).hostname || host;\n } catch { /* keep the placeholder */ }\n return { action: \"remote-dsn\", host };\n }\n\n if (await probeTcp(target.host, target.port)) {\n return { action: \"already-running\", host: target.host, port: target.port };\n }\n\n const composePath = path.join(projectRoot, \"docker-compose.yml\");\n if (!fs.existsSync(composePath)) return { action: \"no-compose\" };\n const composeText = fs.readFileSync(composePath, \"utf8\");\n if (!composeDeclaresDbService(composeText)) return { action: \"no-compose\" };\n\n const manualSteps =\n `${chalk.cyan(\"docker compose up -d db\")} then ${chalk.cyan(\"rebase db push\")}`;\n\n // Would starting that container answer *this* DSN? A `.env` naming a port\n // the compose file does not publish is the commonest way to get here, and\n // starting the container then told the reader the exact opposite of the\n // truth: `⚠ The database container did not begin listening on port 3139.\n // Run docker compose up -d db then rebase db push to see why.` The\n // container was up and healthy on 5436; the port in `.env` was what was\n // wrong. And a container nobody asked for was left running.\n const composePort = composeHostPort(composeText);\n if (composePort !== null && composePort !== target.port) {\n const hint =\n `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. `\n + `The compose file's db service publishes ${composePort}, so starting it would not `\n + `answer that URL — nothing was started. Either correct the port in .env, or point `\n + `DATABASE_URL at 127.0.0.1:${composePort}.`;\n log(\"\");\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"wrong-port\", hint, port: target.port, composePort };\n }\n\n log(\"\");\n log(` ${chalk.gray(\"Database not running — starting it…\")}`);\n\n const startDatabase = options.startDatabase ?? (async (root: string) => {\n await execa(\"docker\", [\"compose\", \"up\", \"-d\", \"db\"], { cwd: root, stdio: \"pipe\" });\n });\n\n try {\n await startDatabase(projectRoot);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Two very different failures land here and the reader needs to know\n // which: Docker not being installed at all, and Docker being installed\n // but unable to start this container.\n if (/ENOENT|not found|command not found/i.test(message)) {\n const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"no-docker\", hint };\n }\n const hint = `Could not start the database container. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n if (!(await waitForPort(target.host, target.port))) {\n const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n log(` ${chalk.green(\"✓\")} ${chalk.gray(`Database ready on port ${target.port}`)}`);\n\n if (!hasCollections) return { action: \"started\", port: target.port, pushed: false };\n\n log(` ${chalk.gray(\"Pushing the schema…\")}`);\n try {\n await pushSchema();\n log(` ${chalk.green(\"✓\")} ${chalk.gray(\"Schema pushed\")}`);\n return { action: \"started\", port: target.port, pushed: true };\n } catch {\n // The push prints its own diagnostics through inherited stdio, so\n // repeating them here would only bury them. What is added is the one\n // thing the reader now needs: the server is still starting, and against\n // what.\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan(\"rebase db push\")}.`)}`);\n return { action: \"started\", port: target.port, pushed: false };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"project-DmaGLmH2.js","names":[],"sources":["../src/utils/project.ts"],"sourcesContent":["/**\n * Project discovery utilities for the Rebase CLI.\n *\n * These helpers locate the project root, backend directory, .env file,\n * and local binaries — used by all CLI command modules.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execSync } from \"child_process\";\nimport dotenv from \"dotenv\";\nimport chalk from \"chalk\";\nimport { detectPackageManager, getPMCommands } from \"./package-manager\";\n\n/** The authored project manifest. Its presence alone marks a project root. */\nexport const MANIFEST_FILENAME = \"rebase.json\";\n\n/**\n * Walk up from `startDir` to find the Rebase project root.\n *\n * A directory is the root when it holds a `rebase.json`, or when it holds a\n * `package.json` that either lists `backend` as a workspace or sits beside both\n * `backend/` and `config/`.\n *\n * `rebase.json` is checked first and needs no `package.json` beside it, because\n * the conventions below all describe a repository that *contains the backend*.\n * A repository holding only a frontend — the normal shape once a project's apps\n * live in separate repositories — matches none of them, so without this the\n * tooling could not run there at all.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): string | null {\n let dir = path.resolve(startDir);\n const root = path.parse(dir).root;\n\n while (dir !== root) {\n if (fs.existsSync(path.join(dir, MANIFEST_FILENAME))) {\n return dir;\n }\n\n const pkgPath = path.join(dir, \"package.json\");\n\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n // Check for workspace-based project (monorepo root)\n if (pkg.workspaces && Array.isArray(pkg.workspaces)) {\n const hasBackend = pkg.workspaces.some((w: string) =>\n w === \"backend\"\n );\n if (hasBackend) return dir;\n }\n } catch {\n // ignore parse errors\n }\n\n // Check for sibling backend directory\n if (fs.existsSync(path.join(dir, \"backend\")) && fs.existsSync(path.join(dir, \"config\"))) {\n return dir;\n }\n }\n\n dir = path.dirname(dir);\n }\n\n return null;\n}\n\n/**\n * Locate the backend directory within the project root.\n */\nexport function findBackendDir(projectRoot: string): string | null {\n const backendDir = path.join(projectRoot, \"backend\");\n return fs.existsSync(backendDir) ? backendDir : null;\n}\n\n/**\n * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.\n */\nexport function getActiveBackendPlugin(backendDir: string): string | null {\n const pkgPath = path.join(backendDir, \"package.json\");\n if (!fs.existsSync(pkgPath)) return null;\n\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n const deps = { ...pkg.dependencies,\n...pkg.devDependencies };\n\n // Collect all @rebasepro/server-* driver plugins (exclude server itself)\n const candidates = Object.keys(deps).filter(\n dep => dep.startsWith(\"@rebasepro/server-\") && dep !== \"@rebasepro/server\"\n );\n\n if (candidates.length === 0) return null;\n\n // Prefer server-postgres — it's the primary supported driver\n if (candidates.includes(\"@rebasepro/server-postgres\")) {\n return \"@rebasepro/server-postgres\";\n }\n\n // Fallback: return the first candidate that actually has a CLI entry point\n for (const candidate of candidates) {\n if (resolvePluginCliScript(backendDir, candidate)) {\n return candidate;\n }\n }\n\n // Last resort: return whatever we found\n return candidates[0];\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\n/**\n * Resolve the active plugin's CLI script.\n */\nexport function resolvePluginCliScript(backendDir: string, pluginName: string): string | null {\n const candidates: string[] = [];\n\n // Walk up from the backend dir: pnpm links the plugin into\n // backend/node_modules, while npm workspaces hoist it to the project (or an\n // enclosing monorepo) root.\n let dir = path.resolve(backendDir);\n const fsRoot = path.parse(dir).root;\n while (dir !== fsRoot) {\n candidates.push(\n path.join(dir, \"node_modules\", pluginName, \"src\", \"cli.ts\"),\n path.join(dir, \"node_modules\", pluginName, \"dist\", \"cli.js\")\n );\n dir = path.dirname(dir);\n }\n\n candidates.push(\n // For monorepo dev mode:\n path.resolve(backendDir, \"..\", \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\")\n );\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * Locate the frontend directory within the project root.\n */\nexport function findFrontendDir(projectRoot: string): string | null {\n const frontendDir = path.join(projectRoot, \"frontend\");\n return fs.existsSync(frontendDir) ? frontendDir : null;\n}\n\n/**\n * Find the .env file. Checks the project root first, then backend.\n */\nexport function findEnvFile(projectRoot: string): string | null {\n const candidates = [\n path.join(projectRoot, \".env\"),\n path.join(projectRoot, \"backend\", \".env\")\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n\n/**\n * Read the project's `.env` into a plain object.\n *\n * One reader, because there were four: `dotenv` in `start`, a hand-rolled\n * `indexOf(\"=\")` loop in `api-keys`, a single-key regex in `auth`, and its own\n * splitting in `cloud env`. `dotenv` is a declared dependency of this package,\n * so the other three existed for no reason and disagreed with the correct one\n * on the two things people actually write in a `.env`:\n *\n * - `export KEY=value`, which the hand-rolled parser keyed as\n * `export KEY` — so the command reported the key as unset while it was\n * right there in the file;\n * - `KEY=value # comment`, whose comment travelled into the value and then\n * into an `Authorization` header, coming back as a 401 with nothing\n * pointing at the cause.\n *\n * Returns `{}` when the project has no `.env`, so callers can treat \"absent\"\n * and \"empty\" alike.\n */\nexport function readEnvFile(projectRoot: string): Record<string, string> {\n const envFile = findEnvFile(projectRoot);\n if (!envFile || !fs.existsSync(envFile)) return {};\n try {\n return dotenv.parse(fs.readFileSync(envFile, \"utf-8\"));\n } catch {\n // An unreadable or malformed file is not worth failing a command over;\n // the caller reports the missing value it was looking for.\n return {};\n }\n}\n\n/**\n * Resolve a binary from the project's node_modules/.bin.\n * Checks backend, root, parent monorepo root, then falls back to PATH.\n */\nexport function resolveLocalBin(projectRoot: string, binName: string): string | null {\n const candidates = [\n path.join(projectRoot, \"backend\", \"node_modules\", \".bin\", binName),\n path.join(projectRoot, \"node_modules\", \".bin\", binName)\n ];\n\n // Also check parent directories (for monorepo setups where app/ is nested)\n let parent = path.dirname(projectRoot);\n const rootDir = path.parse(parent).root;\n while (parent !== rootDir) {\n candidates.push(path.join(parent, \"node_modules\", \".bin\", binName));\n parent = path.dirname(parent);\n }\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n // Fall back to globally installed binary via which\n try {\n const globalPath = execSync(`which ${binName}`, { encoding: \"utf-8\" }).trim();\n if (globalPath && fs.existsSync(globalPath)) return globalPath;\n } catch {\n // not found globally\n }\n\n return null;\n}\n\n/**\n * Resolve the tsx binary. Checks backend node_modules first, then root.\n */\nexport function resolveTsx(projectRoot: string): string | null {\n return resolveLocalBin(projectRoot, \"tsx\");\n}\n\n/**\n * Validate that a resolved tsx binary actually has an intact installation.\n *\n * `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)\n * exists. If the pnpm content-addressable store was cleaned or a previous\n * install was interrupted, the symlink can exist while critical files inside\n * the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a\n * confusing MODULE_NOT_FOUND error at runtime.\n *\n * This function follows the symlink, walks up to find the tsx package root\n * (`package.json` with `name: \"tsx\"`), and verifies that `dist/preflight.cjs`\n * is present. Returns `null` when the installation looks healthy, or an\n * error description string when it appears corrupted.\n */\nexport function validateTsxInstallation(tsxBinPath: string): string | null {\n try {\n // Follow the symlink chain to the real tsx entry script\n const realPath = fs.realpathSync(tsxBinPath);\n\n // Walk up from the real binary to locate the tsx package root\n let dir = path.dirname(realPath);\n const fsRoot = path.parse(dir).root;\n for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {\n const pkgPath = path.join(dir, \"package.json\");\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n if (pkg.name === \"tsx\") {\n // Found the tsx package root — verify critical preload file\n const preflightPath = path.join(dir, \"dist\", \"preflight.cjs\");\n if (!fs.existsSync(preflightPath)) {\n return `tsx package at ${dir} is missing dist/preflight.cjs`;\n }\n return null; // Installation looks healthy\n }\n } catch {\n // Malformed package.json — keep walking\n }\n }\n dir = path.dirname(dir);\n }\n\n // Could not determine tsx root — don't block, assume valid\n return null;\n } catch (err) {\n // realpathSync throws if the symlink target is completely gone\n return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;\n }\n}\n\n/**\n * The one sentence a command says when the project's dependencies are missing.\n *\n * Six commands each had their own wording for the same state, and none of them\n * named the remedy:\n *\n * ✗ Could not find CLI entry point for @rebasepro/server-postgres.\n * ✗ Could not find tsx binary.\n * ✗ Could not find tsx binary for backend.\n *\n * All three mean \"you have not installed yet\" — `getActiveBackendPlugin` has\n * already read the driver out of `backend/package.json` by the time the first\n * one fires, so the package is declared and simply not on disk, and `tsx` is a\n * devDependency of every scaffold. But they read as *Rebase* being broken, and\n * they name an internal path or a binary the developer never asked for rather\n * than the command that fixes it. That is the whole of the failure someone sees\n * on a fresh clone, where `node_modules/` is the one thing a checkout does not\n * carry.\n *\n * So: one sentence, naming the package manager this project uses and the\n * directory to run it in — the working directory is usually neither.\n */\nexport function dependenciesNotInstalled(projectRoot: string): string {\n const install = getPMCommands(detectPackageManager(projectRoot)).install.join(\" \");\n return `Dependencies are not installed — run \\`${install}\\` in ${projectRoot}`;\n}\n\n/**\n * Print {@link dependenciesNotInstalled} and exit 1.\n *\n * For the five commands that report and stop. `db.ts` throws instead, because\n * its caller adds the `✗` and the exit itself.\n */\nexport function exitDependenciesNotInstalled(projectRoot: string): never {\n console.error(chalk.red(`✗ ${dependenciesNotInstalled(projectRoot)}`));\n process.exit(1);\n}\n\n/** One problem inside a refusal, when the command found several. */\nexport interface JsonFailureIssue {\n /** Where it is — a config path, a manifest key, a file. */\n path?: string;\n message: string;\n}\n\n/**\n * The refusal envelope, for a caller that asked for JSON.\n *\n * Same shape as the cloud family's `fail()` — `{ error: { message, code, hint } }`\n * — because there is one CLI and a caller should not have to know which half of\n * it answered. `code` is what a caller branches on and is never absent; the\n * cloud family's reasoning about that applies here unchanged. `issues` carries\n * the per-problem detail a human reader gets as a bullet list, because a\n * refusal that says \"2 problem(s)\" and does not say which two is not\n * machine-readable in any useful sense.\n *\n * On **stdout**, like every other `--json` result: the contract those commands\n * make is that stdout holds one JSON value, and a caller that pipes stdout to a\n * parser must get a parseable refusal rather than an empty stream and a\n * human sentence it never sees. That contract used to hold for exactly one\n * failure of each command — the one that goes through `requireProjectRoot` —\n * while every other exit of the *same* command wrote human text to stderr and\n * left stdout empty. So `rebase status --json` was parseable outside a project\n * and unparseable inside a broken one, which is the case a caller actually has\n * to handle.\n *\n * The flag is read off `process.argv` because the failure happens inside a\n * helper the command calls before it has parsed anything. Coarse — a literal\n * `--json` as some other flag's value would count — and worth it: the failure\n * mode of being coarse is a JSON error where a human one was wanted, and the\n * failure mode of not doing it is an unparseable stream.\n */\nexport function failAsJson(\n message: string,\n code: string,\n hint?: string,\n issues?: JsonFailureIssue[]\n): never {\n const error: Record<string, unknown> = { message, code };\n if (hint !== undefined) error.hint = hint;\n if (issues !== undefined) error.issues = issues;\n console.log(JSON.stringify({ error }, null, 2));\n process.exit(1);\n}\n\n/** Did the command line ask for machine-readable output? */\nexport function wantsJsonOutput(argv: readonly string[] = process.argv): boolean {\n return argv.includes(\"--json\");\n}\n\n/**\n * Require the project root or exit with a helpful error.\n */\nexport function requireProjectRoot(): string {\n const root = findProjectRoot();\n if (!root) {\n if (wantsJsonOutput()) {\n // `rebase status --json` outside a project wrote four grey lines to\n // stderr and nothing at all to stdout, so the agent or CI step that\n // asked for JSON got an empty parse and no reason.\n failAsJson(\n \"Could not find a Rebase project root.\",\n \"no_project_root\",\n `Looked in this directory and every parent for a ${MANIFEST_FILENAME}, `\n + \"a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"\n );\n }\n // Name what is actually looked for, in the order `findProjectRoot`\n // looks for it. The old wording (\"backend/, frontend/, and config/\")\n // described neither the manifest — which is the primary marker and the\n // only one `rebase init` writes — nor a `--headless` project, which has\n // no frontend at all and would read this as \"you are in the wrong\n // place\" while standing in the right one.\n console.error(chalk.red(\"✗ Could not find a Rebase project root.\"));\n console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));\n console.error(chalk.gray(\" a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"));\n console.error(chalk.gray(\" Run this from inside a project, or create one with `rebase init`.\"));\n process.exit(1);\n }\n return root;\n}\n\n/**\n * Require the backend directory or exit with a helpful error.\n */\nexport function requireBackendDir(projectRoot: string): string {\n const backendDir = findBackendDir(projectRoot);\n if (!backendDir) {\n if (wantsJsonOutput()) {\n failAsJson(\n \"Could not find a backend/ directory.\",\n \"no_backend_dir\",\n `Expected at: ${path.join(projectRoot, \"backend\")}`\n );\n }\n console.error(chalk.red(\"✗ Could not find a backend/ directory.\"));\n console.error(chalk.gray(` Expected at: ${path.join(projectRoot, \"backend\")}`));\n process.exit(1);\n }\n return backendDir;\n}\n"],"mappings":"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"}
|