@rebasepro/cli 0.25.0 → 0.25.1
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/dist/{agent-setup-B5AKHOgt.js → agent-setup-BYb2Tbdz.js} +2 -2
- package/dist/{agent-setup-B5AKHOgt.js.map → agent-setup-BYb2Tbdz.js.map} +1 -1
- package/dist/{api-keys-Tm7zvAPe.js → api-keys-CSMbsmAn.js} +3 -4
- package/dist/{api-keys-Tm7zvAPe.js.map → api-keys-CSMbsmAn.js.map} +1 -1
- package/dist/{apps-BsU_ILbP.js → apps-iBXNZhVN.js} +6 -7
- package/dist/{apps-BsU_ILbP.js.map → apps-iBXNZhVN.js.map} +1 -1
- package/dist/{atlas-dev-database-OWtqnG7W.js → atlas-dev-database-BusbHP6n.js} +4 -4
- package/dist/{atlas-dev-database-OWtqnG7W.js.map → atlas-dev-database-BusbHP6n.js.map} +1 -1
- package/dist/{auth-Vl2hFKXP.js → auth-DRsTWPky.js} +6 -7
- package/dist/{auth-Vl2hFKXP.js.map → auth-DRsTWPky.js.map} +1 -1
- package/dist/{branch-pointer-9DnwkFJJ.js → branch-pointer-Bn_FjNld.js} +1 -1
- package/dist/{build-BBgT8EJZ.js → build-et7Rccxi.js} +6 -7
- package/dist/{build-BBgT8EJZ.js.map → build-et7Rccxi.js.map} +1 -1
- package/dist/{bundle-DtCCCr4w.js → bundle-DlqkDYSe.js} +3 -3
- package/dist/{bundle-DtCCCr4w.js.map → bundle-DlqkDYSe.js.map} +1 -1
- package/dist/{cloud-BpH0_bmV.js → cloud-B-ZIjHrA.js} +10 -11
- package/dist/{cloud-BpH0_bmV.js.map → cloud-B-ZIjHrA.js.map} +1 -1
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/generate_sdk.d.ts +4 -2
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/{consent-ClRHmcxf.js → consent-BaB-eQKG.js} +2 -2
- package/dist/{consent-ClRHmcxf.js.map → consent-BaB-eQKG.js.map} +1 -1
- package/dist/{console-CEGm0XNc.js → console-BlD9kjFW.js} +4 -4
- package/dist/{console-CEGm0XNc.js.map → console-BlD9kjFW.js.map} +1 -1
- package/dist/{context-CyflUrKg.js → context-DHiiiJ5C.js} +3 -4
- package/dist/context-DHiiiJ5C.js.map +1 -0
- package/dist/{daemon-CknkDjd6.js → daemon-DDMRbz4T.js} +160 -5
- package/dist/daemon-DDMRbz4T.js.map +1 -0
- package/dist/{daemon-entry-CbLRrgzy.js → daemon-entry-zOC1revO.js} +6 -3
- package/dist/daemon-entry-zOC1revO.js.map +1 -0
- package/dist/{db-CRNpA5mH.js → db-9P6MnmdC.js} +90 -50
- package/dist/db-9P6MnmdC.js.map +1 -0
- package/dist/{derive-CsoFGAnK.js → derive-MTdClnkc.js} +2 -2
- package/dist/{derive-CsoFGAnK.js.map → derive-MTdClnkc.js.map} +1 -1
- package/dist/{dev-C0pbMmhS.js → dev-BDN_OKXA.js} +183 -23
- package/dist/dev-BDN_OKXA.js.map +1 -0
- package/dist/dev-db/daemon.d.ts +41 -0
- package/dist/dev-db/pull.d.ts +33 -5
- package/dist/dev-db/state.d.ts +9 -0
- package/dist/{dev-preflight-unHpiYO1.js → dev-preflight-FT9GbJVj.js} +73 -27
- package/dist/dev-preflight-FT9GbJVj.js.map +1 -0
- package/dist/{dev-server-B1UYifZJ.js → dev-server-CIZwAfZa.js} +14 -3
- package/dist/dev-server-CIZwAfZa.js.map +1 -0
- package/dist/{doctor-CATRdWm6.js → doctor-D8YXz7df.js} +6 -6
- package/dist/doctor-D8YXz7df.js.map +1 -0
- package/dist/{eject-DuasTr-F.js → eject-izG2KjMo.js} +3 -4
- package/dist/{eject-DuasTr-F.js.map → eject-izG2KjMo.js.map} +1 -1
- package/dist/{fold-static-hDHtDfTq.js → fold-static-CybSay_W.js} +3 -3
- package/dist/{fold-static-hDHtDfTq.js.map → fold-static-CybSay_W.js.map} +1 -1
- package/dist/{generate_sdk-Cvg5jjJF.js → generate_sdk-B4R65oUV.js} +21 -11
- package/dist/generate_sdk-B4R65oUV.js.map +1 -0
- package/dist/index.es.js +36 -37
- package/dist/index.es.js.map +1 -1
- package/dist/{init-BWAwHPtz.js → init-DYaeppEN.js} +50 -62
- package/dist/init-DYaeppEN.js.map +1 -0
- package/dist/invoked-command-CG_rIa_o.js +55 -0
- package/dist/invoked-command-CG_rIa_o.js.map +1 -0
- package/dist/last-dev-database-B_75fxZC.js +2 -0
- package/dist/{manifest-DRp0UVTj.js → manifest-nvPdLlJY.js} +2 -2
- package/dist/{manifest-DRp0UVTj.js.map → manifest-nvPdLlJY.js.map} +1 -1
- package/dist/{normalize-imports-BoBVh5tU.js → normalize-imports-BYw8ad87.js} +3 -3
- package/dist/{normalize-imports-BoBVh5tU.js.map → normalize-imports-BYw8ad87.js.map} +1 -1
- package/dist/{prepare-DdnU70z8.js → prepare-1rVNd2PF.js} +5 -4
- package/dist/{prepare-DdnU70z8.js.map → prepare-1rVNd2PF.js.map} +1 -1
- package/dist/project-g4K6kV1t.js +701 -0
- package/dist/project-g4K6kV1t.js.map +1 -0
- package/dist/{pull-C3PXduzl.js → pull-DDMpsZ5r.js} +131 -24
- package/dist/pull-DDMpsZ5r.js.map +1 -0
- package/dist/refusal-ReeeGP8a.js +26 -0
- package/dist/refusal-ReeeGP8a.js.map +1 -0
- package/dist/resolve-BQUAC3Wi.js +2 -0
- package/dist/{resources-BdNRDEAy.js → resources-CovP8AZ-.js} +4 -5
- package/dist/{resources-BdNRDEAy.js.map → resources-CovP8AZ-.js.map} +1 -1
- package/dist/{schema-B0-p6T1O.js → schema-nuxvvc50.js} +5 -6
- package/dist/{schema-B0-p6T1O.js.map → schema-nuxvvc50.js.map} +1 -1
- package/dist/{skills-DfnTeyRz.js → skills-DeUIsZcT.js} +3 -4
- package/dist/{skills-DfnTeyRz.js.map → skills-DeUIsZcT.js.map} +1 -1
- package/dist/{start-BmEZkS0n.js → start-RzZqDroW.js} +20 -7
- package/dist/start-RzZqDroW.js.map +1 -0
- package/dist/{state-CCB-FAQ9.js → state-CYdBvnwA.js} +3 -2
- package/dist/state-CYdBvnwA.js.map +1 -0
- package/dist/{status-BJ26yZOo.js → status-D9Zm88NZ.js} +8 -9
- package/dist/{status-BJ26yZOo.js.map → status-D9Zm88NZ.js.map} +1 -1
- package/dist/{storage-DKPUBTWG.js → storage-BB3umRfw.js} +3 -4
- package/dist/{storage-DKPUBTWG.js.map → storage-BB3umRfw.js.map} +1 -1
- package/dist/{telemetry-EMFRVRQi.js → telemetry-HfU5uAh1.js} +2 -2
- package/dist/{telemetry-EMFRVRQi.js.map → telemetry-HfU5uAh1.js.map} +1 -1
- package/dist/{telemetry-m9czTNGR.js → telemetry-TseNLk6X.js} +4 -4
- package/dist/{telemetry-m9czTNGR.js.map → telemetry-TseNLk6X.js.map} +1 -1
- package/dist/{upgrade-DjihWuZL.js → upgrade-apiPKKE_.js} +26 -4
- package/dist/{upgrade-DjihWuZL.js.map → upgrade-apiPKKE_.js.map} +1 -1
- package/dist/utils/args.d.ts +32 -3
- package/dist/utils/boot-retry.d.ts +41 -0
- package/dist/utils/dev-preflight.d.ts +34 -1
- package/dist/utils/dev-server.d.ts +11 -0
- package/dist/utils/project.d.ts +5 -1
- package/dist/utils/unknown-command.d.ts +4 -0
- package/package.json +7 -7
- package/templates/resource-graphs/blank.json +25 -0
- package/templates/resource-graphs/blog.json +28 -0
- package/templates/resource-graphs/ecommerce.json +28 -0
- package/templates/resource-graphs/headless.json +22 -0
- package/templates/template/.env.example +5 -4
- package/templates/template/frontend/src/App.tsx +10 -9
- package/dist/args-BX0YD14a.js +0 -266
- package/dist/args-BX0YD14a.js.map +0 -1
- package/dist/context-CyflUrKg.js.map +0 -1
- package/dist/daemon-CknkDjd6.js.map +0 -1
- package/dist/daemon-entry-CbLRrgzy.js.map +0 -1
- package/dist/db-CRNpA5mH.js.map +0 -1
- package/dist/dev-C0pbMmhS.js.map +0 -1
- package/dist/dev-preflight-unHpiYO1.js.map +0 -1
- package/dist/dev-server-B1UYifZJ.js.map +0 -1
- package/dist/doctor-CATRdWm6.js.map +0 -1
- package/dist/generate_sdk-Cvg5jjJF.js.map +0 -1
- package/dist/init-BWAwHPtz.js.map +0 -1
- package/dist/last-dev-database-c_dKz1S3.js +0 -2
- package/dist/project-Wp2zoyeN.js +0 -349
- package/dist/project-Wp2zoyeN.js.map +0 -1
- package/dist/pull-C3PXduzl.js.map +0 -1
- package/dist/resolve-CNQjsatH.js +0 -2
- package/dist/start-BmEZkS0n.js.map +0 -1
- package/dist/state-CCB-FAQ9.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-CYdBvnwA.js","names":[],"sources":["../src/dev-db/state.ts"],"sourcesContent":["/**\n * The managed database's state file, and the rules for trusting it.\n *\n * The daemon outlives the command that started it — `rebase db push` in one\n * terminal and `rebase dev` in another have to reach the *same* PGlite, because\n * two processes opening one data directory would corrupt it. So the daemon\n * records where it is, and every command reads that record.\n *\n * A record on disk is a claim, not a fact. The process it names may have been\n * killed, the machine may have rebooted and handed the pid to something else,\n * and the port may now belong to a stranger. {@link readState} therefore only\n * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and\n * it asks the daemon rather than the operating system.\n */\n\nimport { createHash } from \"crypto\";\nimport fs from \"fs\";\nimport { DEFAULT_RESOURCE_KEY, resourceEnvSuffix } from \"@rebasepro/types\";\nimport net from \"net\";\nimport path from \"path\";\n\n/** Everything under here is generated and gitignored. */\nexport const DEV_DB_DIR = \".rebase\";\n/** PGlite's own data directory. Deleting it is what `--reset` means. */\nexport const DATA_DIR_NAME = \"pgdata\";\n/** The record the daemon writes once it is accepting connections. */\nexport const STATE_FILE_NAME = \"pglite.json\";\n/**\n * Held by whoever is currently starting a daemon.\n *\n * Without it, `rebase dev` and `rebase db push` started in the same second both\n * see no state file, both spawn, and two processes open one PGlite data\n * directory — the exact corruption the single-daemon design exists to prevent.\n * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to\n * show up; the harmful way is a damaged database.\n */\nexport const START_LOCK_NAME = \"starting.lock\";\n/**\n * The port each managed database last listened on, by key.\n *\n * Kept apart from the state file because it has to outlive the daemon: the\n * state file is cleared on every stop, and it is what a stopped daemon's\n * successor reads to come back where the last one was. See\n * {@link chooseManagedPort}.\n */\nexport const PORTS_FILE_NAME = \"pglite-ports.json\";\n\nexport interface DaemonState {\n /** TCP port the socket server is listening on, chosen when it started. */\n port: number;\n /** The daemon process. Used only as a fast negative check. */\n pid: number;\n /** Absolute path of the PGlite data directory this daemon has open. */\n dataDir: string;\n /** ISO timestamp, for diagnostics. */\n startedAt: string;\n /**\n * A random token the daemon also answers with over the wire, on\n * {@link identityPort}.\n *\n * Without it, \"is the daemon alive?\" degrades to \"is something listening on\n * that port?\", which is a different question and answers yes for whatever\n * process happened to take the port after a reboot. Rebase would then send\n * migrations to a stranger.\n */\n token: string;\n /** Loopback port that answers the identity check. */\n identityPort: number;\n /**\n * The additional databases this daemon serves, by declared key.\n *\n * One PGlite instance each — PGlite is one database per instance, so\n * `database(\"analytics\")` is a second data directory and a second port,\n * not a `CREATE DATABASE`. Started on demand when a command asks for the\n * key over the identity socket, and recorded here so `rebase status` and\n * a reset can see them. The default database is the fields above.\n */\n databases?: Record<string, { port: number; dataDir: string }>;\n /**\n * The version of the CLI that started this daemon.\n *\n * The daemon outlives the command that started it, and the database proxy\n * runs inside it — so after `rebase upgrade` the old proxy, and its bugs,\n * kept serving until `rebase db stop` or thirty idle minutes. A daemon\n * started before this was recorded has none; see `daemonOutdated`.\n */\n cliVersion?: string;\n}\n\nexport function devDbDir(projectRoot: string): string {\n return path.join(projectRoot, DEV_DB_DIR);\n}\n\nexport function dataDir(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), DATA_DIR_NAME);\n}\n\n/**\n * The data directory for an additional declared database.\n *\n * `pgdata__analytics`: the key spelled the way its variable is, so the\n * directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the\n * same thing.\n */\nexport function additionalDataDir(projectRoot: string, key: string): string {\n return path.join(devDbDir(projectRoot), `${DATA_DIR_NAME}${resourceEnvSuffix(key).toLowerCase()}`);\n}\n\nexport function stateFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), STATE_FILE_NAME);\n}\n\nexport function startLockFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), START_LOCK_NAME);\n}\n\n/**\n * Take the start lock, or report that somebody else holds it.\n *\n * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so\n * exactly one of two racing processes can succeed no matter how close together\n * they arrive.\n *\n * A lock older than `staleAfterMs` is broken rather than waited on — the holder\n * may have been killed between creating it and starting anything, and a\n * developer should never have to know this file exists in order to unstick\n * their project.\n */\nexport function acquireStartLock(projectRoot: string, staleAfterMs: number): boolean {\n const target = startLockFile(projectRoot);\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n\n const attempt = (): boolean => {\n try {\n const handle = fs.openSync(target, \"wx\");\n fs.writeSync(handle, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(handle);\n\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\n return false;\n }\n };\n\n if (attempt()) return true;\n\n try {\n // Clamped at zero: a filesystem whose timestamp granularity rounds the\n // mtime *up* reports a negative age for a lock created moments ago,\n // and a negative age is below every threshold — so `staleAfterMs: 0`,\n // which means \"break any lock\", would refuse to break one.\n const age = Math.max(0, Date.now() - fs.statSync(target).mtimeMs);\n if (age < staleAfterMs) return false;\n fs.unlinkSync(target);\n } catch {\n // Vanished under us, which means the holder finished. Either way the\n // next attempt is the answer.\n }\n\n return attempt();\n}\n\nexport function releaseStartLock(projectRoot: string): void {\n try {\n fs.unlinkSync(startLockFile(projectRoot));\n } catch {\n // Already released is the desired end state.\n }\n}\n\n/**\n * Parse the record, or `null` for anything that is not one.\n *\n * Every failure is the same answer — absent — because every failure has the\n * same remedy: start a daemon. A corrupt state file is not worth an error\n * message to a user who never wrote it.\n */\nexport function readState(projectRoot: string): DaemonState | null {\n let raw: string;\n try {\n raw = fs.readFileSync(stateFile(projectRoot), \"utf8\");\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as Partial<DaemonState>;\n if (\n typeof parsed.port !== \"number\" ||\n !Number.isInteger(parsed.port) ||\n parsed.port <= 0 ||\n parsed.port > 65535 ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.dataDir !== \"string\" ||\n typeof parsed.token !== \"string\" ||\n parsed.token.length === 0 ||\n typeof parsed.identityPort !== \"number\" ||\n !Number.isInteger(parsed.identityPort) ||\n parsed.identityPort <= 0 ||\n parsed.identityPort > 65535\n ) {\n return null;\n }\n\n // Only well-formed entries survive: a malformed one is treated as\n // absent, and the next ensure asks the daemon for it again.\n const databases: Record<string, { port: number; dataDir: string }> = {};\n if (parsed.databases && typeof parsed.databases === \"object\") {\n for (const [key, entry] of Object.entries(parsed.databases)) {\n if (\n entry && typeof entry.port === \"number\" && Number.isInteger(entry.port)\n && entry.port > 0 && entry.port <= 65535 && typeof entry.dataDir === \"string\"\n ) {\n databases[key] = { port: entry.port, dataDir: entry.dataDir };\n }\n }\n }\n\n return {\n port: parsed.port,\n pid: parsed.pid,\n dataDir: parsed.dataDir,\n startedAt: typeof parsed.startedAt === \"string\" ? parsed.startedAt : \"\",\n token: parsed.token,\n identityPort: parsed.identityPort,\n ...(Object.keys(databases).length > 0 ? { databases } : {}),\n ...(typeof parsed.cliVersion === \"string\" && parsed.cliVersion.trim() ? { cliVersion: parsed.cliVersion.trim() } : {})\n };\n } catch {\n return null;\n }\n}\n\nexport function writeState(projectRoot: string, state: DaemonState): void {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n // Written whole then moved, so a reader never sees half a record — commands\n // poll this file while the daemon is starting.\n const target = stateFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n}\n\nexport function clearState(projectRoot: string): void {\n try {\n fs.unlinkSync(stateFile(projectRoot));\n } catch {\n // Already gone is the desired end state.\n }\n}\n\n/** Is *some* process with this pid running? A fast, cheap negative check. */\nexport function pidRunning(pid: number): boolean {\n try {\n // Signal 0 performs the permission and existence checks without\n // delivering anything.\n process.kill(pid, 0);\n\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which for our\n // purposes is still \"running\".\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Can a TCP connection be opened to this port on loopback? */\nexport function portAccepting(port: number, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = new net.Socket();\n const settle = (answer: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(answer);\n };\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => settle(true));\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.connect(port, \"127.0.0.1\");\n });\n}\n\n/**\n * Ask a port for a free one, then hand back the number.\n *\n * Deliberately not the probe `rebase init` uses: that one has a documented\n * failure where a port is free to probe and unusable to publish. This binds on\n * loopback only, which is also where the daemon listens, so a port that binds\n * here binds there.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close(() => reject(new Error(\"Could not determine a free port.\")));\n\n return;\n }\n const { port } = address;\n server.close(() => resolve(port));\n });\n });\n}\n\n/** Where a project's derived database port falls: 25432–26431. */\nconst MANAGED_PORT_FIRST = 25432;\nconst MANAGED_PORT_SPAN = 1000;\n\n/**\n * The port a project's managed database tries first when nothing is recorded:\n * derived from the project path, and for an additional database from its key\n * too, so two projects — and a project's two databases — rarely ask for the\n * same one.\n *\n * Below every ephemeral range (Linux hands out 32768 and up, macOS 49152 and\n * up), so an outgoing connection's source port never sits on it, and clear of\n * the ports `rebase dev` derives for the backend (3001–3999), the admin panel\n * (5173–5372) and the compose database (5433–5632).\n */\nexport function derivedManagedPort(projectRoot: string, key: string = DEFAULT_RESOURCE_KEY): number {\n const seed = key === DEFAULT_RESOURCE_KEY ? path.resolve(projectRoot) : `${path.resolve(projectRoot)}\\0${key}`;\n const digest = createHash(\"sha256\").update(seed).digest();\n return MANAGED_PORT_FIRST + (digest.readUInt32BE(0) % MANAGED_PORT_SPAN);\n}\n\nfunction portsFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), PORTS_FILE_NAME);\n}\n\n/** The port each database last listened on, by key; empty when nothing is recorded or the record is unreadable. */\nfunction readRecordedPorts(projectRoot: string): Record<string, number> {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(portsFile(projectRoot), \"utf8\"));\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return {};\n const ports: Record<string, number> = {};\n for (const [key, port] of Object.entries(parsed)) {\n if (typeof port === \"number\" && Number.isInteger(port) && port > 0 && port <= 65535) ports[key] = port;\n }\n return ports;\n } catch {\n return {};\n }\n}\n\n/**\n * Remember where these databases listen, merged into what is recorded.\n *\n * Best effort: a `.rebase/` that cannot be written costs the next start its\n * preference, not the start itself.\n */\nexport function recordManagedPorts(projectRoot: string, ports: Record<string, number>): void {\n try {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n const merged = { ...readRecordedPorts(projectRoot), ...ports };\n const target = portsFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(merged, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n } catch {\n // See above.\n }\n}\n\n/** Can a server listen on this loopback port right now? */\nexport function portBindable(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer();\n server.once(\"error\", () => resolve(false));\n server.listen(port, \"127.0.0.1\", () => server.close(() => resolve(true)));\n });\n}\n\n/**\n * The port a managed database should listen on: where it listened last time,\n * else the one derived from the project path, else any free one.\n *\n * Stable on purpose. A backend holds the URL it was started with, and a pool\n * reconnects to the same address on its own — so a database that comes back on\n * the port it had is one a running `rebase dev` recovers from without a\n * restart. It used to be whatever the operating system handed out, and a `rebase\n * db stop` followed by any command that starts it again left the backend\n * dialling a port nothing listened on, answering 503 until dev was restarted.\n *\n * @param canBind stands in for {@link portBindable} in the tests.\n */\nexport async function chooseManagedPort(\n projectRoot: string,\n key: string = DEFAULT_RESOURCE_KEY,\n canBind: (port: number) => Promise<boolean> = portBindable\n): Promise<number> {\n const candidates = [readRecordedPorts(projectRoot)[key], derivedManagedPort(projectRoot, key)];\n for (const port of new Set(candidates.filter((candidate): candidate is number => candidate !== undefined))) {\n if (await canBind(port)) return port;\n }\n return findFreePort();\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { d as readEnvFile, i as failAsJson, p as requireProjectRoot } from "./project-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { describeTarget } from "./pull-C3PXduzl.js";
|
|
1
|
+
import { C as wantsHelp, b as parseCommandArgs, d as readEnvFile, i as failAsJson, p as requireProjectRoot } from "./project-g4K6kV1t.js";
|
|
2
|
+
import { d as findBackendApp, g as resolveBackendPaths, o as ManifestError, p as loadManifest } from "./manifest-nvPdLlJY.js";
|
|
3
|
+
import { c as withImplicitDefaults, n as deriveOptionsFor, o as readResourceGraphFile, r as deriveResourceGraph, s as serializeResourceGraph, t as RESOURCE_GRAPH_FILENAME } from "./derive-MTdClnkc.js";
|
|
4
|
+
import { a as parseLoopbackDsn, o as probeTcp } from "./dev-preflight-FT9GbJVj.js";
|
|
5
|
+
import { i as managedUrl, r as findRunningDaemon } from "./daemon-DDMRbz4T.js";
|
|
6
|
+
import { resolveProjectDatabaseAndBranch } from "./prepare-1rVNd2PF.js";
|
|
7
|
+
import { describeTarget } from "./pull-DDMpsZ5r.js";
|
|
9
8
|
import chalk from "chalk";
|
|
10
9
|
import fs from "fs";
|
|
11
10
|
import path from "path";
|
|
@@ -428,4 +427,4 @@ async function statusCommand(rawArgs) {
|
|
|
428
427
|
//#endregion
|
|
429
428
|
export { statusCommand };
|
|
430
429
|
|
|
431
|
-
//# sourceMappingURL=status-
|
|
430
|
+
//# sourceMappingURL=status-D9Zm88NZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-BJ26yZOo.js","names":[],"sources":["../src/resources/status.ts","../src/commands/status.ts"],"sourcesContent":["/**\n * What a project declared, what the environment binds it to, and whether the\n * two meet.\n *\n * ## Why this exists\n *\n * Every piece of this was already knowable and none of it was visible in one\n * place. `rebase resources` lists what you declared. The environment holds the\n * variables. The resolvers decide whether a source is usable. So the question a\n * developer actually has — *\"why does uploading to `media` answer 501\"* — could\n * only be answered by knowing the suffix rule, deriving the variable name by\n * hand, and checking it against a `.env` they had to find first.\n *\n * The answer is mechanical, so it should be printed rather than taught.\n *\n * ## Why it calls the resolvers instead of re-deciding\n *\n * The verdict comes from `resolveDataSources` and `resolveStorageBackend` — the\n * same functions that run at boot. A status view that decided for itself what\n * \"configured\" means would be a second implementation of the rule, and the\n * first time it disagreed it would be reassuring a developer about a deployment\n * that is about to refuse to start.\n *\n * The variable *names* come from each kind's `envBases`, and the verdicts from\n * the resolver `@rebasepro/server` registers per kind — one object per kind,\n * held to `envBases` by `resource-env-bases.test.ts`. A kind this view has\n * never heard of is judged by whatever resolver its package registered.\n */\nimport { withImplicitDefaults } from \"./derive\";\nimport {\n DEFAULT_RESOURCE_KEY,\n envBasesForResource,\n resourceEnvSuffix,\n resourceToDataSource,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\n\n/** One environment variable a resource reads, and whether it is there. */\nexport interface ResourceBinding {\n /** The variable this resource reads, suffix included. */\n name: string;\n set: boolean;\n /**\n * The account-scoped name consulted when `name` is unset.\n *\n * Only provider-level bindings have one, and only when the resource named\n * an `account`. Shown because a developer looking at an unset\n * `S3_ACCESS_KEY_ID__MEDIA` needs to know it is not the whole story.\n */\n fallback?: { name: string; set: boolean };\n}\n\n/** Whether a declared resource can actually be reached. */\nexport type ResourceState =\n /** Bound and usable. */\n | \"ready\"\n /** Nothing set for it. Legal for a bucket, fatal for a database. */\n | \"unconfigured\"\n /** Set, and set wrongly. The deployment refuses to start. */\n | \"broken\";\n\nexport interface ResourceStatus {\n kind: string;\n key: string;\n engine: string;\n transport: string;\n /** Present when this resource shares another's credentials. */\n account?: string;\n /** True for a resource nobody declared, which exists anyway. */\n implicit: boolean;\n bindings: ResourceBinding[];\n state: ResourceState;\n /** What this means, in the terms the developer will meet it in. */\n detail: string;\n /** Set when a local directory stands in for this engine, in development. */\n standsIn?: string;\n /** What in the project reaches this resource, from the graph. */\n usedBy?: readonly string[];\n}\n\nexport type EnvBag = Record<string, string | undefined>;\n\n/** Set, and not set to the empty string — which the resolvers treat as absent. */\nfunction isSet(env: EnvBag, name: string): boolean {\n const value = env[name];\n return typeof value === \"string\" && value !== \"\";\n}\n\n/**\n * The bindings one declaration reads, in the order a reader should scan them.\n *\n * A `direct`-transport resource gets none: the browser reaches it with a\n * provider SDK and the backend binds nothing for it, so listing variables would\n * invite someone to set variables that are never read.\n */\nexport function bindingsFor(\n declaration: ResourceDeclaration,\n env: EnvBag,\n accountScopedBases: readonly string[]\n): ResourceBinding[] {\n if (declaration.transport !== \"server\") return [];\n\n const suffix = resourceEnvSuffix(declaration.key);\n const account = typeof declaration.options.account === \"string\"\n ? resourceEnvSuffix(declaration.options.account)\n : undefined;\n\n return envBasesForResource(declaration).map((base) => {\n const name = `${base}${suffix}`;\n const binding: ResourceBinding = { name, set: isSet(env, name) };\n\n // Only the provider-level bindings fall back, and only for a resource\n // that named an account. The bucket name never does — it is what\n // distinguishes one source from another.\n if (account !== undefined && !binding.set && accountScopedBases.includes(base)) {\n binding.fallback = { name: `${base}${account}`, set: isSet(env, `${base}${account}`) };\n }\n return binding;\n });\n}\n\n/**\n * What the managed development database covers, when a project is on it.\n *\n * `rebase init` leaves `DATABASE_URL` commented out on purpose and the managed\n * PGlite fills the vacuum — the documented first-run state. Judging that state\n * by the environment alone produced `○ (default) postgres · DATABASE_URL not\n * set` and the remedy \"set DATABASE_URL\", at the same moment `rebase db url`\n * was printing a working connection string for the same project and\n * `rebase dev` was serving from it. The status view was reading the one place\n * the answer deliberately is not.\n *\n * Resolved by the caller, because deciding it needs the project's `.env`, its\n * branch pointer and its compose file, and this module stays free of all three.\n */\nexport interface ManagedDatabaseStatus {\n /**\n * The daemon's connection string, or null when it is not running.\n *\n * Null is not an error: the managed database exists per project and starts\n * on demand. It only means there is nothing to run the whole-set check\n * against, and the row says so rather than implying a misconfiguration.\n */\n url: string | null;\n}\n\n/**\n * The compose `db` service, when a project is on Postgres in Docker.\n *\n * `rebase dev --docker` is remembered, and every command follows it with the\n * scaffold's `DATABASE_URL` still commented out — so, judged by `.env` alone,\n * this view said the default database had no connection string while `rebase\n * dev --docker` served the project from it. Unlike the managed database it\n * stands in for the default only: `dev --docker` hands the backend\n * `DATABASE_URL` and nothing else, so a second declared database still needs\n * its own variable.\n */\nexport interface DockerDatabaseStatus {\n /** The compose service's connection string, which the runtime is given as `DATABASE_URL`. */\n url: string;\n /** What the default database's row says it is. */\n detail: string;\n}\n\n/**\n * The resolver-side of status, as `@rebasepro/server` registers it per kind.\n *\n * Passed in rather than imported so this module stays testable without a\n * server, and so a kind registered by a plugin is judged by the resolver the\n * plugin brought with it rather than by a switch here.\n */\nexport interface StatusResolvers {\n /** `resourceResolver(kind)` from `@rebasepro/server`. */\n resolverFor: (kind: string) => {\n accountScoped?: readonly string[];\n resolve: (\n declaration: ResourceDeclaration,\n env: EnvBag,\n context: { production: boolean; defaultBasePath: string }\n ) => { state: \"ready\" | \"unbound\" | \"blocked\" | \"code\"; detail?: string; standsIn?: string };\n } | undefined;\n /** `resolveDataSources` from `@rebasepro/server`, for the whole-set check. */\n resolveDataSources: (env: EnvBag, definitions: unknown) => unknown;\n /** Whether the process being judged is production. Status judges for development by default. */\n production?: boolean;\n /**\n * Set when this project runs on the managed development database.\n *\n * Absent for a project that named its own connection string, and absent in\n * production — where an unset `DATABASE_URL` is the failure it has always\n * been.\n */\n managedDatabase?: ManagedDatabaseStatus;\n /** Set when this project runs on Postgres in Docker. Absent in production, like the managed one. */\n dockerDatabase?: DockerDatabaseStatus;\n}\n\n/**\n * The environment plus the managed database's URL, for the whole-set check.\n *\n * A copy: the bindings a reader sees are computed against the real `.env`, so\n * nothing here can make the view claim a variable is set that is not. This is\n * only what `resolveDataSources` is handed, and it is handed it so the check\n * judges the environment the runtime would actually get rather than throwing\n * \"Data source \"(default)\" has no connection string\" about a database that is\n * running and reachable.\n */\nfunction withManagedUrls(env: EnvBag, graph: ResourceGraph, url: string): EnvBag {\n const filled: EnvBag = { ...env };\n const keys = [\n DEFAULT_RESOURCE_KEY,\n ...graph.resources.filter(r => r.kind === \"database\").map(r => r.key)\n ];\n for (const key of keys) {\n const name = `DATABASE_URL${resourceEnvSuffix(key)}`;\n if (!isSet(filled, name)) filled[name] = url;\n }\n\n return filled;\n}\n\n/**\n * The environment with the default database's URL filled in, when `.env` leaves it out.\n *\n * What a database's resolver is judged against. The resolver checks a source\n * alongside the default one, so a second database bound by its own\n * `DATABASE_URL__ANALYTICS` was marked ✗ with `Data source \"(default)\" has no\n * connection string` — on a project whose default the managed database or the\n * compose service provides, and whose boot worked. Judged against the\n * environment the runtime actually gets, it is judged on its own string.\n */\nfunction withDefaultUrl(env: EnvBag, url: string | null | undefined): EnvBag {\n if (!url || isSet(env, \"DATABASE_URL\")) return env;\n\n return { ...env, DATABASE_URL: url };\n}\n\n/**\n * Resolve every declared resource against an environment.\n *\n * Each kind's verdict comes from the resolver `@rebasepro/server` registered\n * for it — the same one boot calls — so this view cannot say \"ready\" about a\n * binding boot would refuse. A kind with no resolver is reported as such\n * rather than skipped: a resource nobody shows is one nobody remembers to\n * configure, and a kind this runtime cannot bind is exactly what boot will\n * refuse by name.\n *\n * `resolveDataSources` is then run once over the whole set, so anything it\n * would refuse across sources (a driver package it cannot resolve for a custom\n * engine) is reported rather than missed by a view that only looked per row.\n */\nexport function computeStatus(\n graph: ResourceGraph,\n env: EnvBag,\n resolvers: StatusResolvers\n): { resources: ResourceStatus[]; blocked?: string } {\n const resources: ResourceStatus[] = [];\n const production = resolvers.production ?? false;\n const managed = resolvers.managedDatabase;\n const docker = resolvers.dockerDatabase;\n /** The default database's URL as the runtime gets it, when `.env` does not name one. */\n const devDefaultUrl = managed?.url ?? docker?.url ?? null;\n /** Database rows the managed database answered for, so `blocked` can skip them. */\n let managedCovered = 0;\n\n for (const { declaration, implicit } of withImplicitDefaults(graph)) {\n const resolver = resolvers.resolverFor(declaration.kind);\n const bindings = bindingsFor(declaration, env, resolver?.accountScoped ?? []);\n const base: Omit<ResourceStatus, \"state\" | \"detail\"> = {\n kind: declaration.kind,\n key: declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n implicit,\n bindings,\n ...(declaration.usedBy && declaration.usedBy.length > 0 ? { usedBy: declaration.usedBy } : {})\n };\n\n if (!resolver) {\n resources.push({\n ...base,\n state: \"broken\",\n detail: `this runtime has no resolver for a \"${declaration.kind}\" — boot refuses it by name. ` +\n \"Upgrade @rebasepro/server, or remove the declaration.\"\n });\n continue;\n }\n\n // Answered before the resolver, because the resolver reads the\n // environment and the whole point of the managed database is that the\n // environment is deliberately empty. Only for a database nobody bound:\n // a developer who set `DATABASE_URL__ANALYTICS` to a warehouse of their\n // own has said which database that is, and the managed one fills a\n // vacuum, never a choice.\n if (managed && declaration.kind === \"database\" && bindings.every(b => !b.set && !b.fallback?.set)) {\n managedCovered += 1;\n resources.push({\n ...base,\n state: \"ready\",\n detail: managed.url\n ? \"the managed development database (PGlite), this project only\"\n : \"the managed development database (PGlite), this project only — \"\n + \"not running; start it with `rebase dev`\"\n });\n continue;\n }\n\n // The default database on Docker: the compose service, unless `.env`\n // names one itself.\n if (docker && declaration.kind === \"database\" && declaration.key === DEFAULT_RESOURCE_KEY\n && bindings.every(b => !b.set && !b.fallback?.set)) {\n resources.push({ ...base, state: \"ready\", detail: docker.detail });\n continue;\n }\n\n const judged = declaration.kind === \"database\" ? withDefaultUrl(env, devDefaultUrl) : env;\n const verdict = resolver.resolve(declaration, judged, { production, defaultBasePath: \"uploads\" });\n switch (verdict.state) {\n case \"ready\":\n resources.push({\n ...base,\n state: \"ready\",\n ...(verdict.standsIn ? { standsIn: verdict.standsIn } : {}),\n detail: verdict.detail ?? \"\"\n });\n break;\n case \"code\":\n resources.push({ ...base, state: \"ready\", detail: verdict.detail ?? \"needs no environment configuration\" });\n break;\n case \"unbound\":\n resources.push({ ...base, state: \"unconfigured\", detail: verdict.detail ?? \"declared, not configured\" });\n break;\n case \"blocked\":\n resources.push({\n ...base,\n // A database the environment names nothing for is the\n // ordinary first-run state, and reads as unconfigured; a\n // binding that is set and set wrongly is broken.\n state: implicit || bindings.every(b => !b.set && !b.fallback?.set) ? \"unconfigured\" : \"broken\",\n detail: verdict.detail ?? \"boot refuses this binding\"\n });\n break;\n }\n }\n\n // Only asked when every row already looks fine. Its failures overlap with\n // the per-resource ones, and a banner repeating a line printed two rows\n // above teaches nothing — what it is here for is the failure no row models,\n // such as a custom engine whose driver package cannot be resolved.\n let blocked: string | undefined;\n // Not run when the managed database answered for a row and is not running:\n // there is no connection string to check the set against, and inventing one\n // to keep the check formally \"run\" is how a green tick gets printed for\n // work nobody did. The row says the daemon is down; that is the finding.\n const managedSetUnknown = managedCovered > 0 && !managed?.url;\n if (resources.every(r => r.state === \"ready\") && !managedSetUnknown) {\n try {\n resolvers.resolveDataSources(\n // The managed URL for the rows it answered for, so the whole-set\n // check judges the same environment the runtime would get.\n managed?.url ? withManagedUrls(env, graph, managed.url) : withDefaultUrl(env, docker?.url),\n graph.resources.filter(r => r.kind === \"database\").map(resourceToDataSource)\n );\n } catch (err) {\n blocked = err instanceof Error ? err.message : String(err);\n }\n }\n\n return { resources, ...(blocked !== undefined ? { blocked } : {}) };\n}\n","/**\n * `rebase status` — what this project is, and whether it can reach its things.\n *\n * The model a developer has to hold is small and was nowhere visible:\n *\n * rebase.json where the code is, and who runs the server\n * config/resources.ts what the project needs\n * the environment how to reach each of them\n *\n * Everything else — `rebase.resources.json`, the bundle manifest — is a\n * generated copy of the middle one, for readers that cannot run the code. A\n * developer never writes them and should not have to think about them.\n *\n * This command prints the three together, resolved. It exists because the\n * question people actually arrive with — \"why does uploading to `media` answer\n * 501\" — was answerable only by knowing the `<BASE>__<KEY>` suffix rule,\n * deriving the variable name by hand, and checking it against a `.env` they had\n * to find first. Every step of that is mechanical, so it is printed instead.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { failAsJson, readEnvFile, requireProjectRoot } from \"../utils/project\";\nimport { findBackendApp, loadManifest, ManifestError, resolveBackendPaths } from \"../manifest\";\nimport { deriveOptionsFor, deriveResourceGraph, RESOURCE_GRAPH_FILENAME, readResourceGraphFile, serializeResourceGraph } from \"../resources/derive\";\nimport { computeStatus, type DockerDatabaseStatus, type ManagedDatabaseStatus, type ResourceStatus } from \"../resources/status\";\nimport { findRunningDaemon, managedUrl } from \"../dev-db/daemon\";\nimport { type ResolvedBranch, resolveProjectDatabaseAndBranch } from \"../dev-db/prepare\";\nimport { describeTarget } from \"../dev-db/pull\";\nimport { parseLoopbackDsn, probeTcp } from \"../utils/dev-preflight\";\nimport type { RebaseBackendAppConfig } from \"@rebasepro/types\";\n\nfunction usage(): void {\n console.log(`\n${chalk.bold(\"rebase status\")} — what this project declares, and whether it is configured\n\n${chalk.bold(\"Usage\")}\n rebase status Show every resource and the variables it reads\n rebase status --json The same, as JSON\n\n${chalk.bold(\"Reads\")}\n rebase.json which apps exist, and who owns the server process\n <config>/resources.ts the databases, buckets, topics and queues declared\n backend/crons, backend/functions the crons and functions, by filename\n .env whether each one's variables are set\n`);\n}\n\nconst MARK = {\n ready: chalk.green(\"✓\"),\n unconfigured: chalk.yellow(\"○\"),\n broken: chalk.red(\"✗\")\n} as const;\n\nfunction printResource(status: ResourceStatus): void {\n const name = status.key === \"(default)\" ? chalk.bold(\"(default)\") : chalk.bold(status.key);\n const tags = [chalk.gray(status.engine)];\n if (status.transport !== \"server\") tags.push(chalk.yellow(status.transport));\n if (status.account) tags.push(chalk.gray(`account:${status.account}`));\n if (status.implicit) tags.push(chalk.gray(\"implicit\"));\n // A stand-in is not a binding. It is shown in yellow beside a green tick:\n // uploads work here, and the same declaration answers 501 in production\n // until the variable is set.\n if (status.standsIn) tags.push(chalk.yellow(`local stand-in for ${status.standsIn}`));\n\n console.log(` ${MARK[status.state]} ${name} ${tags.join(\" · \")}`);\n if (status.usedBy && status.usedBy.length > 0) {\n console.log(` ${chalk.gray(\"used by\")} ${chalk.gray(status.usedBy.join(\", \"))}`);\n }\n\n let hiddenOptional = 0;\n for (const binding of status.bindings) {\n if (binding.set) {\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.name)}`);\n } else if (binding.fallback?.set) {\n // Not a gap: the account form is the variable this bucket is\n // actually reading, and showing only the unset per-key name would\n // send someone to set the one they were deliberately avoiding.\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.fallback.name)} ${chalk.gray(`(shared, for ${binding.name})`)}`);\n } else if (status.state === \"ready\") {\n // It resolved without this one, so this one is optional — which is\n // the resolver's answer, not a second table of required names kept\n // here. Listing seven unset optionals under a working database is\n // how a status view teaches people to skim past the line that\n // matters.\n hiddenOptional += 1;\n } else {\n const also = binding.fallback ? chalk.gray(` or ${binding.fallback.name}`) : \"\";\n console.log(` ${chalk.gray(\"·\")} ${chalk.gray(binding.name)}${also} ${chalk.gray(\"not set\")}`);\n }\n }\n if (hiddenOptional > 0) {\n console.log(chalk.gray(` ${hiddenOptional} optional variable${hiddenOptional === 1 ? \"\" : \"s\"} not set`));\n }\n\n if (status.detail) {\n const colour = status.state === \"broken\" ? chalk.red : status.state === \"unconfigured\" ? chalk.yellow : chalk.gray;\n console.log(` ${colour(`└ ${status.detail}`)}`);\n }\n}\n\n/** Which database `rebase status` reports the project on, and what it hands the status view about it. */\nexport interface StatusDatabase {\n managedDatabase?: ManagedDatabaseStatus;\n dockerDatabase?: DockerDatabaseStatus;\n /** The branch this checkout is on, when the database is one. */\n branch: ResolvedBranch | null;\n /** A switched-to branch the managed database cannot serve. */\n ignoredBranch: string | null;\n}\n\n/**\n * Which database this project is on, decided by the same ordered rule\n * `rebase dev` and `rebase db url` use — and read from the project's `.env`,\n * not from this shell: `rebase status` asks what *this project* is on.\n *\n * Nothing is started. `status` is a question, and a question that boots a\n * 180 MB Postgres to answer itself is a different command. So a managed\n * database that is not running is reported as exactly that, rather than as a\n * project with no database; and a compose database likewise, after a probe of\n * its port.\n *\n * `envFile` is the parsed `.env`; production stands nothing in, because there\n * an unset `DATABASE_URL` is the failure it has always been.\n *\n * Exported for its tests.\n */\nexport async function statusDatabase(projectRoot: string, envFile: Record<string, string>): Promise<StatusDatabase> {\n const { database, branch, ignoredBranch } = resolveProjectDatabaseAndBranch(projectRoot, { env: {} });\n if (envFile.NODE_ENV === \"production\") return { branch, ignoredBranch };\n\n if (database.kind === \"managed\") {\n const daemon = await findRunningDaemon(projectRoot);\n return { managedDatabase: { url: daemon ? managedUrl(daemon.port) : null }, branch, ignoredBranch };\n }\n\n if (database.kind === \"docker\" && database.url) {\n const target = parseLoopbackDsn(database.url);\n const listening = target ? await probeTcp(target.host, target.port) : true;\n const which = branch ? `branch \"${branch.name}\" (${branch.database}) of Postgres in Docker` : \"Postgres in Docker\";\n return {\n dockerDatabase: {\n url: database.url,\n detail: which + \", as the last `rebase dev --docker` did · \" + describeTarget(database.url)\n + (listening ? \"\" : \" — not running; `rebase dev --docker` starts it\")\n },\n branch,\n ignoredBranch\n };\n }\n\n return { branch, ignoredBranch };\n}\n\nexport async function statusCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) return usage();\n\n const { flags } = parseCommandArgs({\n spec: { \"--json\": Boolean },\n rawArgs,\n commandWords: 1,\n command: \"status\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n\n // Every exit below has a `--json` arm, because `--json` means stdout holds\n // one JSON object on *every* exit of the command. It used to mean that for\n // the one failure `requireProjectRoot` owns and nothing else, so a caller\n // parsing stdout got an envelope outside a project and an empty stream\n // inside a broken one — the case it actually has to handle.\n const asJson = Boolean(flags[\"--json\"]);\n\n let backend: { name: string; app: RebaseBackendAppConfig };\n try {\n const loaded = loadManifest(projectRoot);\n const found = findBackendApp(loaded.manifest);\n if (!found) {\n if (asJson) {\n failAsJson(\n \"This project declares no backend app, so it declares no resources.\",\n \"no_backend_app\",\n \"Add a backend app to rebase.json, or run `rebase apps init`.\"\n );\n }\n console.error(chalk.red(\"This project declares no backend app, so it declares no resources.\"));\n process.exitCode = 1;\n return;\n }\n backend = found;\n } catch (err) {\n if (err instanceof ManifestError) {\n if (asJson) {\n failAsJson(err.message, \"manifest_invalid\", \"Fix rebase.json, then run this again.\",\n err.issues.map(issue => ({ path: issue.path, message: issue.message })));\n }\n console.error(chalk.red(err.message));\n for (const issue of err.issues) console.error(` ${chalk.gray(issue.path)} ${issue.message}`);\n process.exitCode = 1;\n return;\n }\n throw err;\n }\n\n const { name: backendName, app: backendApp } = backend;\n const paths = resolveBackendPaths(backendApp, projectRoot);\n\n const { graph, issues } = await deriveResourceGraph(deriveOptionsFor(projectRoot, backendApp));\n if (issues.length > 0) {\n if (asJson) {\n failAsJson(\n `${issues.length} problem(s) in the declared resources.`,\n \"resource_declaration_invalid\",\n \"Fix the declarations the issues name, then run this again.\",\n issues.map(issue => ({ path: issue.path, message: issue.message }))\n );\n }\n console.error(chalk.red(`\\n✗ ${issues.length} problem(s) in the declared resources:\\n`));\n for (const issue of issues) console.error(` ${chalk.bold(issue.path)} ${issue.message}`);\n console.error(\"\");\n process.exitCode = 1;\n return;\n }\n\n // The project's own `.env`, not this process's environment: `rebase status`\n // is asking what a deployment of THIS repository would do, and the shell a\n // developer happens to be standing in is not that.\n const env = readEnvFile(projectRoot);\n\n const production = env.NODE_ENV === \"production\";\n const { managedDatabase, dockerDatabase, branch, ignoredBranch } = await statusDatabase(projectRoot, env);\n\n const server = await import(\"@rebasepro/server\");\n const { resources, blocked } = computeStatus(graph, env, {\n resolverFor: server.resourceResolver as never,\n resolveDataSources: server.resolveDataSources as never,\n // Judged as production when the .env says so; a stand-in local\n // directory for an unbound bucket is only ever offered to development.\n production,\n ...(managedDatabase ? { managedDatabase } : {}),\n ...(dockerDatabase ? { dockerDatabase } : {})\n });\n\n if (asJson) {\n console.log(JSON.stringify({\n backend: backendName,\n runtime: backendApp.runtime,\n ...(branch ? { branch: { name: branch.name, database: branch.database } } : {}),\n resources,\n blocked\n }, null, 2));\n return;\n }\n\n const ownership = backendApp.runtime === \"managed\"\n ? \"Rebase's runtime boots your bundle\"\n : \"this project builds its own image and entrypoint\";\n\n console.log(\"\");\n console.log(` ${chalk.bold(backendName)} ${chalk.gray(\"·\")} ${chalk.cyan(backendApp.runtime)} ${chalk.gray(ownership)}`);\n // Named whether or not it exists, and said plainly when it does not. A\n // project with no `resources.ts` is legal — it has one default database and\n // one default bucket — but \"declared in <a file you do not have>\" reads as\n // a broken install rather than as the default it is.\n const resourcesFile = path.join(paths.config, \"resources.ts\");\n const declaresAnything = fs.existsSync(path.join(projectRoot, resourcesFile));\n console.log(` ${chalk.gray(\"declared in\")} ${chalk.gray(resourcesFile)}${\n declaresAnything ? \"\" : chalk.gray(\" (none — the defaults below are implicit)\")}`);\n console.log(` ${chalk.gray(\"configured by\")} ${chalk.gray(\".env\")}`);\n // Every database command in this checkout uses the branch, so the view\n // says so: a switch made yesterday is easy to forget.\n if (branch) {\n console.log(` ${chalk.gray(\"on branch\")} ${chalk.bold(branch.name)} ${chalk.gray(`(${branch.database})`)}`\n + chalk.gray(` — ${chalk.cyan(\"rebase db branch switch --off\")} returns to ${branch.parent ?? \"the main database\"}`));\n } else if (ignoredBranch) {\n console.log(` ${chalk.yellow(\"○\")} ${chalk.gray(`switched to branch \"${ignoredBranch}\", which the managed database cannot serve — it uses the main one`)}`);\n }\n\n const kinds = [...new Set(resources.map(r => r.kind))];\n for (const kind of kinds) {\n console.log(\"\");\n console.log(` ${chalk.bold.underline(`${kind}s`)}`);\n for (const status of resources.filter(r => r.kind === kind)) printResource(status);\n }\n\n if (blocked) {\n console.log(\"\");\n console.log(` ${chalk.red(\"✗\")} ${chalk.red(blocked)}`);\n }\n\n // Only worth a line when it is wrong. A generated file that matches its\n // declarations is not news, and a status view that reports its own\n // bookkeeping as an item trains people to skim past the items that matter.\n //\n // Nor is one that has not been written yet: `rebase build` writes it on\n // every build and says \"commit it\" when it changes, so a project that has\n // never been built — every fresh scaffold — has nothing wrong with it.\n // `rebase resources --check` is the gate that refuses a missing one.\n const committed = readResourceGraphFile(projectRoot);\n if (committed !== null && committed !== serializeResourceGraph(graph)) {\n console.log(\"\");\n console.log(` ${chalk.yellow(\"○\")} ${RESOURCE_GRAPH_FILENAME} is stale — ${chalk.blue(\"rebase resources --write\")}`);\n console.log(chalk.gray(\" a host reads it to decide what to provision before it runs anything\"));\n }\n\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"status-D9Zm88NZ.js","names":[],"sources":["../src/resources/status.ts","../src/commands/status.ts"],"sourcesContent":["/**\n * What a project declared, what the environment binds it to, and whether the\n * two meet.\n *\n * ## Why this exists\n *\n * Every piece of this was already knowable and none of it was visible in one\n * place. `rebase resources` lists what you declared. The environment holds the\n * variables. The resolvers decide whether a source is usable. So the question a\n * developer actually has — *\"why does uploading to `media` answer 501\"* — could\n * only be answered by knowing the suffix rule, deriving the variable name by\n * hand, and checking it against a `.env` they had to find first.\n *\n * The answer is mechanical, so it should be printed rather than taught.\n *\n * ## Why it calls the resolvers instead of re-deciding\n *\n * The verdict comes from `resolveDataSources` and `resolveStorageBackend` — the\n * same functions that run at boot. A status view that decided for itself what\n * \"configured\" means would be a second implementation of the rule, and the\n * first time it disagreed it would be reassuring a developer about a deployment\n * that is about to refuse to start.\n *\n * The variable *names* come from each kind's `envBases`, and the verdicts from\n * the resolver `@rebasepro/server` registers per kind — one object per kind,\n * held to `envBases` by `resource-env-bases.test.ts`. A kind this view has\n * never heard of is judged by whatever resolver its package registered.\n */\nimport { withImplicitDefaults } from \"./derive\";\nimport {\n DEFAULT_RESOURCE_KEY,\n envBasesForResource,\n resourceEnvSuffix,\n resourceToDataSource,\n type ResourceDeclaration,\n type ResourceGraph\n} from \"@rebasepro/types\";\n\n/** One environment variable a resource reads, and whether it is there. */\nexport interface ResourceBinding {\n /** The variable this resource reads, suffix included. */\n name: string;\n set: boolean;\n /**\n * The account-scoped name consulted when `name` is unset.\n *\n * Only provider-level bindings have one, and only when the resource named\n * an `account`. Shown because a developer looking at an unset\n * `S3_ACCESS_KEY_ID__MEDIA` needs to know it is not the whole story.\n */\n fallback?: { name: string; set: boolean };\n}\n\n/** Whether a declared resource can actually be reached. */\nexport type ResourceState =\n /** Bound and usable. */\n | \"ready\"\n /** Nothing set for it. Legal for a bucket, fatal for a database. */\n | \"unconfigured\"\n /** Set, and set wrongly. The deployment refuses to start. */\n | \"broken\";\n\nexport interface ResourceStatus {\n kind: string;\n key: string;\n engine: string;\n transport: string;\n /** Present when this resource shares another's credentials. */\n account?: string;\n /** True for a resource nobody declared, which exists anyway. */\n implicit: boolean;\n bindings: ResourceBinding[];\n state: ResourceState;\n /** What this means, in the terms the developer will meet it in. */\n detail: string;\n /** Set when a local directory stands in for this engine, in development. */\n standsIn?: string;\n /** What in the project reaches this resource, from the graph. */\n usedBy?: readonly string[];\n}\n\nexport type EnvBag = Record<string, string | undefined>;\n\n/** Set, and not set to the empty string — which the resolvers treat as absent. */\nfunction isSet(env: EnvBag, name: string): boolean {\n const value = env[name];\n return typeof value === \"string\" && value !== \"\";\n}\n\n/**\n * The bindings one declaration reads, in the order a reader should scan them.\n *\n * A `direct`-transport resource gets none: the browser reaches it with a\n * provider SDK and the backend binds nothing for it, so listing variables would\n * invite someone to set variables that are never read.\n */\nexport function bindingsFor(\n declaration: ResourceDeclaration,\n env: EnvBag,\n accountScopedBases: readonly string[]\n): ResourceBinding[] {\n if (declaration.transport !== \"server\") return [];\n\n const suffix = resourceEnvSuffix(declaration.key);\n const account = typeof declaration.options.account === \"string\"\n ? resourceEnvSuffix(declaration.options.account)\n : undefined;\n\n return envBasesForResource(declaration).map((base) => {\n const name = `${base}${suffix}`;\n const binding: ResourceBinding = { name, set: isSet(env, name) };\n\n // Only the provider-level bindings fall back, and only for a resource\n // that named an account. The bucket name never does — it is what\n // distinguishes one source from another.\n if (account !== undefined && !binding.set && accountScopedBases.includes(base)) {\n binding.fallback = { name: `${base}${account}`, set: isSet(env, `${base}${account}`) };\n }\n return binding;\n });\n}\n\n/**\n * What the managed development database covers, when a project is on it.\n *\n * `rebase init` leaves `DATABASE_URL` commented out on purpose and the managed\n * PGlite fills the vacuum — the documented first-run state. Judging that state\n * by the environment alone produced `○ (default) postgres · DATABASE_URL not\n * set` and the remedy \"set DATABASE_URL\", at the same moment `rebase db url`\n * was printing a working connection string for the same project and\n * `rebase dev` was serving from it. The status view was reading the one place\n * the answer deliberately is not.\n *\n * Resolved by the caller, because deciding it needs the project's `.env`, its\n * branch pointer and its compose file, and this module stays free of all three.\n */\nexport interface ManagedDatabaseStatus {\n /**\n * The daemon's connection string, or null when it is not running.\n *\n * Null is not an error: the managed database exists per project and starts\n * on demand. It only means there is nothing to run the whole-set check\n * against, and the row says so rather than implying a misconfiguration.\n */\n url: string | null;\n}\n\n/**\n * The compose `db` service, when a project is on Postgres in Docker.\n *\n * `rebase dev --docker` is remembered, and every command follows it with the\n * scaffold's `DATABASE_URL` still commented out — so, judged by `.env` alone,\n * this view said the default database had no connection string while `rebase\n * dev --docker` served the project from it. Unlike the managed database it\n * stands in for the default only: `dev --docker` hands the backend\n * `DATABASE_URL` and nothing else, so a second declared database still needs\n * its own variable.\n */\nexport interface DockerDatabaseStatus {\n /** The compose service's connection string, which the runtime is given as `DATABASE_URL`. */\n url: string;\n /** What the default database's row says it is. */\n detail: string;\n}\n\n/**\n * The resolver-side of status, as `@rebasepro/server` registers it per kind.\n *\n * Passed in rather than imported so this module stays testable without a\n * server, and so a kind registered by a plugin is judged by the resolver the\n * plugin brought with it rather than by a switch here.\n */\nexport interface StatusResolvers {\n /** `resourceResolver(kind)` from `@rebasepro/server`. */\n resolverFor: (kind: string) => {\n accountScoped?: readonly string[];\n resolve: (\n declaration: ResourceDeclaration,\n env: EnvBag,\n context: { production: boolean; defaultBasePath: string }\n ) => { state: \"ready\" | \"unbound\" | \"blocked\" | \"code\"; detail?: string; standsIn?: string };\n } | undefined;\n /** `resolveDataSources` from `@rebasepro/server`, for the whole-set check. */\n resolveDataSources: (env: EnvBag, definitions: unknown) => unknown;\n /** Whether the process being judged is production. Status judges for development by default. */\n production?: boolean;\n /**\n * Set when this project runs on the managed development database.\n *\n * Absent for a project that named its own connection string, and absent in\n * production — where an unset `DATABASE_URL` is the failure it has always\n * been.\n */\n managedDatabase?: ManagedDatabaseStatus;\n /** Set when this project runs on Postgres in Docker. Absent in production, like the managed one. */\n dockerDatabase?: DockerDatabaseStatus;\n}\n\n/**\n * The environment plus the managed database's URL, for the whole-set check.\n *\n * A copy: the bindings a reader sees are computed against the real `.env`, so\n * nothing here can make the view claim a variable is set that is not. This is\n * only what `resolveDataSources` is handed, and it is handed it so the check\n * judges the environment the runtime would actually get rather than throwing\n * \"Data source \"(default)\" has no connection string\" about a database that is\n * running and reachable.\n */\nfunction withManagedUrls(env: EnvBag, graph: ResourceGraph, url: string): EnvBag {\n const filled: EnvBag = { ...env };\n const keys = [\n DEFAULT_RESOURCE_KEY,\n ...graph.resources.filter(r => r.kind === \"database\").map(r => r.key)\n ];\n for (const key of keys) {\n const name = `DATABASE_URL${resourceEnvSuffix(key)}`;\n if (!isSet(filled, name)) filled[name] = url;\n }\n\n return filled;\n}\n\n/**\n * The environment with the default database's URL filled in, when `.env` leaves it out.\n *\n * What a database's resolver is judged against. The resolver checks a source\n * alongside the default one, so a second database bound by its own\n * `DATABASE_URL__ANALYTICS` was marked ✗ with `Data source \"(default)\" has no\n * connection string` — on a project whose default the managed database or the\n * compose service provides, and whose boot worked. Judged against the\n * environment the runtime actually gets, it is judged on its own string.\n */\nfunction withDefaultUrl(env: EnvBag, url: string | null | undefined): EnvBag {\n if (!url || isSet(env, \"DATABASE_URL\")) return env;\n\n return { ...env, DATABASE_URL: url };\n}\n\n/**\n * Resolve every declared resource against an environment.\n *\n * Each kind's verdict comes from the resolver `@rebasepro/server` registered\n * for it — the same one boot calls — so this view cannot say \"ready\" about a\n * binding boot would refuse. A kind with no resolver is reported as such\n * rather than skipped: a resource nobody shows is one nobody remembers to\n * configure, and a kind this runtime cannot bind is exactly what boot will\n * refuse by name.\n *\n * `resolveDataSources` is then run once over the whole set, so anything it\n * would refuse across sources (a driver package it cannot resolve for a custom\n * engine) is reported rather than missed by a view that only looked per row.\n */\nexport function computeStatus(\n graph: ResourceGraph,\n env: EnvBag,\n resolvers: StatusResolvers\n): { resources: ResourceStatus[]; blocked?: string } {\n const resources: ResourceStatus[] = [];\n const production = resolvers.production ?? false;\n const managed = resolvers.managedDatabase;\n const docker = resolvers.dockerDatabase;\n /** The default database's URL as the runtime gets it, when `.env` does not name one. */\n const devDefaultUrl = managed?.url ?? docker?.url ?? null;\n /** Database rows the managed database answered for, so `blocked` can skip them. */\n let managedCovered = 0;\n\n for (const { declaration, implicit } of withImplicitDefaults(graph)) {\n const resolver = resolvers.resolverFor(declaration.kind);\n const bindings = bindingsFor(declaration, env, resolver?.accountScoped ?? []);\n const base: Omit<ResourceStatus, \"state\" | \"detail\"> = {\n kind: declaration.kind,\n key: declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n implicit,\n bindings,\n ...(declaration.usedBy && declaration.usedBy.length > 0 ? { usedBy: declaration.usedBy } : {})\n };\n\n if (!resolver) {\n resources.push({\n ...base,\n state: \"broken\",\n detail: `this runtime has no resolver for a \"${declaration.kind}\" — boot refuses it by name. ` +\n \"Upgrade @rebasepro/server, or remove the declaration.\"\n });\n continue;\n }\n\n // Answered before the resolver, because the resolver reads the\n // environment and the whole point of the managed database is that the\n // environment is deliberately empty. Only for a database nobody bound:\n // a developer who set `DATABASE_URL__ANALYTICS` to a warehouse of their\n // own has said which database that is, and the managed one fills a\n // vacuum, never a choice.\n if (managed && declaration.kind === \"database\" && bindings.every(b => !b.set && !b.fallback?.set)) {\n managedCovered += 1;\n resources.push({\n ...base,\n state: \"ready\",\n detail: managed.url\n ? \"the managed development database (PGlite), this project only\"\n : \"the managed development database (PGlite), this project only — \"\n + \"not running; start it with `rebase dev`\"\n });\n continue;\n }\n\n // The default database on Docker: the compose service, unless `.env`\n // names one itself.\n if (docker && declaration.kind === \"database\" && declaration.key === DEFAULT_RESOURCE_KEY\n && bindings.every(b => !b.set && !b.fallback?.set)) {\n resources.push({ ...base, state: \"ready\", detail: docker.detail });\n continue;\n }\n\n const judged = declaration.kind === \"database\" ? withDefaultUrl(env, devDefaultUrl) : env;\n const verdict = resolver.resolve(declaration, judged, { production, defaultBasePath: \"uploads\" });\n switch (verdict.state) {\n case \"ready\":\n resources.push({\n ...base,\n state: \"ready\",\n ...(verdict.standsIn ? { standsIn: verdict.standsIn } : {}),\n detail: verdict.detail ?? \"\"\n });\n break;\n case \"code\":\n resources.push({ ...base, state: \"ready\", detail: verdict.detail ?? \"needs no environment configuration\" });\n break;\n case \"unbound\":\n resources.push({ ...base, state: \"unconfigured\", detail: verdict.detail ?? \"declared, not configured\" });\n break;\n case \"blocked\":\n resources.push({\n ...base,\n // A database the environment names nothing for is the\n // ordinary first-run state, and reads as unconfigured; a\n // binding that is set and set wrongly is broken.\n state: implicit || bindings.every(b => !b.set && !b.fallback?.set) ? \"unconfigured\" : \"broken\",\n detail: verdict.detail ?? \"boot refuses this binding\"\n });\n break;\n }\n }\n\n // Only asked when every row already looks fine. Its failures overlap with\n // the per-resource ones, and a banner repeating a line printed two rows\n // above teaches nothing — what it is here for is the failure no row models,\n // such as a custom engine whose driver package cannot be resolved.\n let blocked: string | undefined;\n // Not run when the managed database answered for a row and is not running:\n // there is no connection string to check the set against, and inventing one\n // to keep the check formally \"run\" is how a green tick gets printed for\n // work nobody did. The row says the daemon is down; that is the finding.\n const managedSetUnknown = managedCovered > 0 && !managed?.url;\n if (resources.every(r => r.state === \"ready\") && !managedSetUnknown) {\n try {\n resolvers.resolveDataSources(\n // The managed URL for the rows it answered for, so the whole-set\n // check judges the same environment the runtime would get.\n managed?.url ? withManagedUrls(env, graph, managed.url) : withDefaultUrl(env, docker?.url),\n graph.resources.filter(r => r.kind === \"database\").map(resourceToDataSource)\n );\n } catch (err) {\n blocked = err instanceof Error ? err.message : String(err);\n }\n }\n\n return { resources, ...(blocked !== undefined ? { blocked } : {}) };\n}\n","/**\n * `rebase status` — what this project is, and whether it can reach its things.\n *\n * The model a developer has to hold is small and was nowhere visible:\n *\n * rebase.json where the code is, and who runs the server\n * config/resources.ts what the project needs\n * the environment how to reach each of them\n *\n * Everything else — `rebase.resources.json`, the bundle manifest — is a\n * generated copy of the middle one, for readers that cannot run the code. A\n * developer never writes them and should not have to think about them.\n *\n * This command prints the three together, resolved. It exists because the\n * question people actually arrive with — \"why does uploading to `media` answer\n * 501\" — was answerable only by knowing the `<BASE>__<KEY>` suffix rule,\n * deriving the variable name by hand, and checking it against a `.env` they had\n * to find first. Every step of that is mechanical, so it is printed instead.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { failAsJson, readEnvFile, requireProjectRoot } from \"../utils/project\";\nimport { findBackendApp, loadManifest, ManifestError, resolveBackendPaths } from \"../manifest\";\nimport { deriveOptionsFor, deriveResourceGraph, RESOURCE_GRAPH_FILENAME, readResourceGraphFile, serializeResourceGraph } from \"../resources/derive\";\nimport { computeStatus, type DockerDatabaseStatus, type ManagedDatabaseStatus, type ResourceStatus } from \"../resources/status\";\nimport { findRunningDaemon, managedUrl } from \"../dev-db/daemon\";\nimport { type ResolvedBranch, resolveProjectDatabaseAndBranch } from \"../dev-db/prepare\";\nimport { describeTarget } from \"../dev-db/pull\";\nimport { parseLoopbackDsn, probeTcp } from \"../utils/dev-preflight\";\nimport type { RebaseBackendAppConfig } from \"@rebasepro/types\";\n\nfunction usage(): void {\n console.log(`\n${chalk.bold(\"rebase status\")} — what this project declares, and whether it is configured\n\n${chalk.bold(\"Usage\")}\n rebase status Show every resource and the variables it reads\n rebase status --json The same, as JSON\n\n${chalk.bold(\"Reads\")}\n rebase.json which apps exist, and who owns the server process\n <config>/resources.ts the databases, buckets, topics and queues declared\n backend/crons, backend/functions the crons and functions, by filename\n .env whether each one's variables are set\n`);\n}\n\nconst MARK = {\n ready: chalk.green(\"✓\"),\n unconfigured: chalk.yellow(\"○\"),\n broken: chalk.red(\"✗\")\n} as const;\n\nfunction printResource(status: ResourceStatus): void {\n const name = status.key === \"(default)\" ? chalk.bold(\"(default)\") : chalk.bold(status.key);\n const tags = [chalk.gray(status.engine)];\n if (status.transport !== \"server\") tags.push(chalk.yellow(status.transport));\n if (status.account) tags.push(chalk.gray(`account:${status.account}`));\n if (status.implicit) tags.push(chalk.gray(\"implicit\"));\n // A stand-in is not a binding. It is shown in yellow beside a green tick:\n // uploads work here, and the same declaration answers 501 in production\n // until the variable is set.\n if (status.standsIn) tags.push(chalk.yellow(`local stand-in for ${status.standsIn}`));\n\n console.log(` ${MARK[status.state]} ${name} ${tags.join(\" · \")}`);\n if (status.usedBy && status.usedBy.length > 0) {\n console.log(` ${chalk.gray(\"used by\")} ${chalk.gray(status.usedBy.join(\", \"))}`);\n }\n\n let hiddenOptional = 0;\n for (const binding of status.bindings) {\n if (binding.set) {\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.name)}`);\n } else if (binding.fallback?.set) {\n // Not a gap: the account form is the variable this bucket is\n // actually reading, and showing only the unset per-key name would\n // send someone to set the one they were deliberately avoiding.\n console.log(` ${chalk.green(\"✓\")} ${chalk.gray(binding.fallback.name)} ${chalk.gray(`(shared, for ${binding.name})`)}`);\n } else if (status.state === \"ready\") {\n // It resolved without this one, so this one is optional — which is\n // the resolver's answer, not a second table of required names kept\n // here. Listing seven unset optionals under a working database is\n // how a status view teaches people to skim past the line that\n // matters.\n hiddenOptional += 1;\n } else {\n const also = binding.fallback ? chalk.gray(` or ${binding.fallback.name}`) : \"\";\n console.log(` ${chalk.gray(\"·\")} ${chalk.gray(binding.name)}${also} ${chalk.gray(\"not set\")}`);\n }\n }\n if (hiddenOptional > 0) {\n console.log(chalk.gray(` ${hiddenOptional} optional variable${hiddenOptional === 1 ? \"\" : \"s\"} not set`));\n }\n\n if (status.detail) {\n const colour = status.state === \"broken\" ? chalk.red : status.state === \"unconfigured\" ? chalk.yellow : chalk.gray;\n console.log(` ${colour(`└ ${status.detail}`)}`);\n }\n}\n\n/** Which database `rebase status` reports the project on, and what it hands the status view about it. */\nexport interface StatusDatabase {\n managedDatabase?: ManagedDatabaseStatus;\n dockerDatabase?: DockerDatabaseStatus;\n /** The branch this checkout is on, when the database is one. */\n branch: ResolvedBranch | null;\n /** A switched-to branch the managed database cannot serve. */\n ignoredBranch: string | null;\n}\n\n/**\n * Which database this project is on, decided by the same ordered rule\n * `rebase dev` and `rebase db url` use — and read from the project's `.env`,\n * not from this shell: `rebase status` asks what *this project* is on.\n *\n * Nothing is started. `status` is a question, and a question that boots a\n * 180 MB Postgres to answer itself is a different command. So a managed\n * database that is not running is reported as exactly that, rather than as a\n * project with no database; and a compose database likewise, after a probe of\n * its port.\n *\n * `envFile` is the parsed `.env`; production stands nothing in, because there\n * an unset `DATABASE_URL` is the failure it has always been.\n *\n * Exported for its tests.\n */\nexport async function statusDatabase(projectRoot: string, envFile: Record<string, string>): Promise<StatusDatabase> {\n const { database, branch, ignoredBranch } = resolveProjectDatabaseAndBranch(projectRoot, { env: {} });\n if (envFile.NODE_ENV === \"production\") return { branch, ignoredBranch };\n\n if (database.kind === \"managed\") {\n const daemon = await findRunningDaemon(projectRoot);\n return { managedDatabase: { url: daemon ? managedUrl(daemon.port) : null }, branch, ignoredBranch };\n }\n\n if (database.kind === \"docker\" && database.url) {\n const target = parseLoopbackDsn(database.url);\n const listening = target ? await probeTcp(target.host, target.port) : true;\n const which = branch ? `branch \"${branch.name}\" (${branch.database}) of Postgres in Docker` : \"Postgres in Docker\";\n return {\n dockerDatabase: {\n url: database.url,\n detail: which + \", as the last `rebase dev --docker` did · \" + describeTarget(database.url)\n + (listening ? \"\" : \" — not running; `rebase dev --docker` starts it\")\n },\n branch,\n ignoredBranch\n };\n }\n\n return { branch, ignoredBranch };\n}\n\nexport async function statusCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) return usage();\n\n const { flags } = parseCommandArgs({\n spec: { \"--json\": Boolean },\n rawArgs,\n commandWords: 1,\n command: \"status\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n\n // Every exit below has a `--json` arm, because `--json` means stdout holds\n // one JSON object on *every* exit of the command. It used to mean that for\n // the one failure `requireProjectRoot` owns and nothing else, so a caller\n // parsing stdout got an envelope outside a project and an empty stream\n // inside a broken one — the case it actually has to handle.\n const asJson = Boolean(flags[\"--json\"]);\n\n let backend: { name: string; app: RebaseBackendAppConfig };\n try {\n const loaded = loadManifest(projectRoot);\n const found = findBackendApp(loaded.manifest);\n if (!found) {\n if (asJson) {\n failAsJson(\n \"This project declares no backend app, so it declares no resources.\",\n \"no_backend_app\",\n \"Add a backend app to rebase.json, or run `rebase apps init`.\"\n );\n }\n console.error(chalk.red(\"This project declares no backend app, so it declares no resources.\"));\n process.exitCode = 1;\n return;\n }\n backend = found;\n } catch (err) {\n if (err instanceof ManifestError) {\n if (asJson) {\n failAsJson(err.message, \"manifest_invalid\", \"Fix rebase.json, then run this again.\",\n err.issues.map(issue => ({ path: issue.path, message: issue.message })));\n }\n console.error(chalk.red(err.message));\n for (const issue of err.issues) console.error(` ${chalk.gray(issue.path)} ${issue.message}`);\n process.exitCode = 1;\n return;\n }\n throw err;\n }\n\n const { name: backendName, app: backendApp } = backend;\n const paths = resolveBackendPaths(backendApp, projectRoot);\n\n const { graph, issues } = await deriveResourceGraph(deriveOptionsFor(projectRoot, backendApp));\n if (issues.length > 0) {\n if (asJson) {\n failAsJson(\n `${issues.length} problem(s) in the declared resources.`,\n \"resource_declaration_invalid\",\n \"Fix the declarations the issues name, then run this again.\",\n issues.map(issue => ({ path: issue.path, message: issue.message }))\n );\n }\n console.error(chalk.red(`\\n✗ ${issues.length} problem(s) in the declared resources:\\n`));\n for (const issue of issues) console.error(` ${chalk.bold(issue.path)} ${issue.message}`);\n console.error(\"\");\n process.exitCode = 1;\n return;\n }\n\n // The project's own `.env`, not this process's environment: `rebase status`\n // is asking what a deployment of THIS repository would do, and the shell a\n // developer happens to be standing in is not that.\n const env = readEnvFile(projectRoot);\n\n const production = env.NODE_ENV === \"production\";\n const { managedDatabase, dockerDatabase, branch, ignoredBranch } = await statusDatabase(projectRoot, env);\n\n const server = await import(\"@rebasepro/server\");\n const { resources, blocked } = computeStatus(graph, env, {\n resolverFor: server.resourceResolver as never,\n resolveDataSources: server.resolveDataSources as never,\n // Judged as production when the .env says so; a stand-in local\n // directory for an unbound bucket is only ever offered to development.\n production,\n ...(managedDatabase ? { managedDatabase } : {}),\n ...(dockerDatabase ? { dockerDatabase } : {})\n });\n\n if (asJson) {\n console.log(JSON.stringify({\n backend: backendName,\n runtime: backendApp.runtime,\n ...(branch ? { branch: { name: branch.name, database: branch.database } } : {}),\n resources,\n blocked\n }, null, 2));\n return;\n }\n\n const ownership = backendApp.runtime === \"managed\"\n ? \"Rebase's runtime boots your bundle\"\n : \"this project builds its own image and entrypoint\";\n\n console.log(\"\");\n console.log(` ${chalk.bold(backendName)} ${chalk.gray(\"·\")} ${chalk.cyan(backendApp.runtime)} ${chalk.gray(ownership)}`);\n // Named whether or not it exists, and said plainly when it does not. A\n // project with no `resources.ts` is legal — it has one default database and\n // one default bucket — but \"declared in <a file you do not have>\" reads as\n // a broken install rather than as the default it is.\n const resourcesFile = path.join(paths.config, \"resources.ts\");\n const declaresAnything = fs.existsSync(path.join(projectRoot, resourcesFile));\n console.log(` ${chalk.gray(\"declared in\")} ${chalk.gray(resourcesFile)}${\n declaresAnything ? \"\" : chalk.gray(\" (none — the defaults below are implicit)\")}`);\n console.log(` ${chalk.gray(\"configured by\")} ${chalk.gray(\".env\")}`);\n // Every database command in this checkout uses the branch, so the view\n // says so: a switch made yesterday is easy to forget.\n if (branch) {\n console.log(` ${chalk.gray(\"on branch\")} ${chalk.bold(branch.name)} ${chalk.gray(`(${branch.database})`)}`\n + chalk.gray(` — ${chalk.cyan(\"rebase db branch switch --off\")} returns to ${branch.parent ?? \"the main database\"}`));\n } else if (ignoredBranch) {\n console.log(` ${chalk.yellow(\"○\")} ${chalk.gray(`switched to branch \"${ignoredBranch}\", which the managed database cannot serve — it uses the main one`)}`);\n }\n\n const kinds = [...new Set(resources.map(r => r.kind))];\n for (const kind of kinds) {\n console.log(\"\");\n console.log(` ${chalk.bold.underline(`${kind}s`)}`);\n for (const status of resources.filter(r => r.kind === kind)) printResource(status);\n }\n\n if (blocked) {\n console.log(\"\");\n console.log(` ${chalk.red(\"✗\")} ${chalk.red(blocked)}`);\n }\n\n // Only worth a line when it is wrong. A generated file that matches its\n // declarations is not news, and a status view that reports its own\n // bookkeeping as an item trains people to skim past the items that matter.\n //\n // Nor is one that has not been written yet: `rebase build` writes it on\n // every build and says \"commit it\" when it changes, so a project that has\n // never been built — every fresh scaffold — has nothing wrong with it.\n // `rebase resources --check` is the gate that refuses a missing one.\n const committed = readResourceGraphFile(projectRoot);\n if (committed !== null && committed !== serializeResourceGraph(graph)) {\n console.log(\"\");\n console.log(` ${chalk.yellow(\"○\")} ${RESOURCE_GRAPH_FILENAME} is stale — ${chalk.blue(\"rebase resources --write\")}`);\n console.log(chalk.gray(\" a host reads it to decide what to provision before it runs anything\"));\n }\n\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { d as readEnvFile, p as requireProjectRoot } from "./project-
|
|
2
|
-
import { a as unknownCommand, n as parseCommandArgs, r as wantsHelp, t as UsageError } from "./args-BX0YD14a.js";
|
|
1
|
+
import { C as wantsHelp, T as unknownCommand, b as parseCommandArgs, d as readEnvFile, p as requireProjectRoot, y as UsageError } from "./project-g4K6kV1t.js";
|
|
3
2
|
import { t as cliUserAgent } from "./version-Buyyo1ut.js";
|
|
4
|
-
import { describeFailure, requireBackendUrl } from "./api-keys-
|
|
3
|
+
import { describeFailure, requireBackendUrl } from "./api-keys-CSMbsmAn.js";
|
|
5
4
|
import chalk from "chalk";
|
|
6
5
|
import { RebaseApiError } from "@rebasepro/client";
|
|
7
6
|
import readline from "node:readline/promises";
|
|
@@ -303,4 +302,4 @@ ${chalk.green.bold("Notes")}
|
|
|
303
302
|
//#endregion
|
|
304
303
|
export { STORAGE_SUBCOMMANDS, storageCommand };
|
|
305
304
|
|
|
306
|
-
//# sourceMappingURL=storage-
|
|
305
|
+
//# sourceMappingURL=storage-BB3umRfw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-DKPUBTWG.js","names":[],"sources":["../src/commands/storage.ts"],"sourcesContent":["/**\n * CLI command: rebase storage <action>\n *\n * Whole-store maintenance through the running backend's `/api/admin/storage`,\n * authenticated with the service key from `.env`.\n *\n * Subcommands:\n * orphans — Stored files no row names; `--delete` deletes them\n * move — Copy a folder of files to another storage source, same keys\n */\nimport chalk from \"chalk\";\nimport readline from \"node:readline/promises\";\nimport { RebaseApiError } from \"@rebasepro/client\";\nimport { requireProjectRoot, readEnvFile } from \"../utils/project\";\nimport { parseCommandArgs, UsageError, wantsHelp } from \"../utils/args\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { cliUserAgent } from \"../utils/version\";\nimport { describeFailure, requireBackendUrl } from \"./api-keys\";\n\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const STORAGE_SUBCOMMANDS = [\"orphans\", \"move\"] as const;\n\n/** The flags `rebase storage orphans` takes. */\nexport const ORPHANS_FLAGS = {\n \"--source\": String,\n \"--older-than\": Number,\n \"--delete\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\",\n \"--json\": Boolean\n} as const;\n\n/** The flags `rebase storage move` takes. */\nexport const MOVE_FLAGS = {\n \"--from\": String,\n \"--to\": String,\n \"--prefix\": String,\n \"--delete-source\": Boolean,\n \"--json\": Boolean\n} as const;\n\n/** One stored object, as the backend lists it. */\ninterface StoredObjectEntry {\n key: string;\n size?: number;\n updated?: string;\n}\n\n/** What `GET /admin/storage/orphans` answers. */\nexport interface OrphanReport {\n storageId: string;\n objects: number;\n orphans: StoredObjectEntry[];\n recent: number;\n scanned: string[];\n unscanned: { collection: string; reason: string }[];\n}\n\ninterface MovedObject {\n key: string;\n outcome: \"copied\" | \"exists\" | \"failed\";\n deleted?: boolean;\n error?: string;\n}\n\ninterface Backend {\n baseUrl: string;\n request<T>(method: string, path: string, body?: unknown): Promise<T>;\n}\n\n/** The project's backend, holding the service key — the `admin` role, every scope. */\nfunction connect(): Backend {\n const projectRoot = requireProjectRoot();\n const env = readEnvFile(projectRoot);\n const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;\n if (!serviceKey) {\n console.error(chalk.red(\"✗ REBASE_SERVICE_KEY not found in .env — required for storage maintenance.\"));\n process.exit(1);\n }\n // Only an address this project recorded or the developer named: the\n // request carries the service key. See `resolveBackendAddress`.\n const baseUrl = requireBackendUrl(env, projectRoot).replace(/\\/+$/, \"\");\n return {\n baseUrl,\n async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/api/admin/storage${path}`, {\n method,\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Accept\": \"application/json\",\n \"User-Agent\": cliUserAgent(),\n ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" })\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) })\n });\n } catch (error: unknown) {\n throw new RebaseApiError(error instanceof Error ? error.message : String(error), { status: 0, code: \"NETWORK_ERROR\" });\n }\n const answer: unknown = await response.json().catch(() => undefined);\n if (!response.ok) {\n const error = answer && typeof answer === \"object\" && \"error\" in answer && answer.error && typeof answer.error === \"object\"\n ? answer.error as { message?: unknown; code?: unknown; details?: unknown }\n : undefined;\n throw new RebaseApiError(\n typeof error?.message === \"string\" ? error.message : `The server answered ${response.status}.`,\n { status: response.status, code: typeof error?.code === \"string\" ? error.code : undefined, details: error?.details }\n );\n }\n return (answer as { data: T }).data;\n }\n };\n}\n\nfunction fail(action: string, error: unknown, baseUrl: string): never {\n for (const line of describeFailure(action, error, baseUrl)) {\n console.error(line.startsWith(\"✗\") ? chalk.red(line) : chalk.gray(line));\n }\n if (error instanceof RebaseApiError && error.status === 404 && error.code !== \"UNKNOWN_STORAGE_SOURCE\") {\n console.error(chalk.gray(\" This backend has no storage maintenance routes: they arrived in 0.25, and need storage configured.\"));\n }\n process.exit(1);\n}\n\n/** Bytes, as a person reads them. */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;\n}\n\n/**\n * The lines that describe an orphan scan. Exported so its tests can read them\n * without a server.\n */\nexport function describeOrphanReport(report: OrphanReport, olderThanHours: number): string[] {\n const lines: string[] = [];\n const bytes = report.orphans.reduce((sum, orphan) => sum + (orphan.size ?? 0), 0);\n lines.push(` ${report.objects} file(s) in source \"${report.storageId}\", ${report.scanned.length} collection(s) read.`);\n if (report.orphans.length === 0) {\n lines.push(\" No file older than \" + `${olderThanHours}h is unused.`);\n } else {\n lines.push(` ${report.orphans.length} file(s) no row names, ${formatBytes(bytes)}:`);\n for (const orphan of report.orphans) {\n lines.push(` ${orphan.key}${orphan.size !== undefined ? ` ${formatBytes(orphan.size)}` : \"\"}${orphan.updated ? ` ${orphan.updated}` : \"\"}`);\n }\n }\n if (report.recent > 0) {\n const one = report.recent === 1;\n const more = report.orphans.length > 0 ? \"more \" : \"\";\n lines.push(` ${report.recent} ${more}unused ${one ? \"file is\" : \"files are\"} left out: ` +\n `${one ? \"it is\" : \"they are\"} newer than ${olderThanHours}h, or ${one ? \"its\" : \"their\"} age is unknown, ` +\n \"and an upload can be saved before the row that names it.\");\n }\n for (const entry of report.unscanned) {\n lines.push(` ⚠ ${entry.collection} could not be read (${entry.reason}): a file only it names is listed above.`);\n }\n return lines;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });\n try {\n return /^y(es)?$/i.test((await prompt.question(question)).trim());\n } finally {\n prompt.close();\n }\n}\n\nasync function orphans(rawArgs: string[]): Promise<void> {\n const { flags } = parseCommandArgs({ spec: ORPHANS_FLAGS, rawArgs, commandWords: 2, command: \"storage orphans\", maxPositionals: 0 });\n const olderThan = flags[\"--older-than\"] ?? 24;\n if (!Number.isFinite(olderThan) || olderThan < 0) {\n throw new UsageError(\"--older-than takes a number of hours, 0 or more.\");\n }\n const query = new URLSearchParams({ olderThanHours: String(olderThan) });\n if (flags[\"--source\"]) query.set(\"storageId\", flags[\"--source\"]);\n const backend = connect();\n\n let report: OrphanReport;\n try {\n report = await backend.request<OrphanReport>(\"GET\", `/orphans?${query}`);\n } catch (error: unknown) {\n fail(\"Failed to scan storage\", error, backend.baseUrl);\n }\n\n if (!flags[\"--delete\"]) {\n if (flags[\"--json\"]) {\n console.log(JSON.stringify(report, null, 2));\n return;\n }\n console.log(\"\");\n for (const line of describeOrphanReport(report, olderThan)) console.log(line);\n if (report.orphans.length > 0) {\n console.log(\"\");\n console.log(chalk.gray(\" Nothing was deleted. Run again with --delete to delete them.\"));\n }\n console.log(\"\");\n return;\n }\n\n if (!flags[\"--json\"]) {\n console.log(\"\");\n for (const line of describeOrphanReport(report, olderThan)) console.log(line);\n console.log(\"\");\n }\n if (report.orphans.length === 0) return;\n if (report.unscanned.length > 0) {\n console.error(chalk.red(\"✗ Not deleting: some collections could not be read, so a file they name would look unused.\"));\n process.exit(1);\n }\n if (!flags[\"--yes\"]) {\n if (!process.stdin.isTTY) {\n throw new UsageError(\"--delete deletes files for good; pass --yes to confirm when there is no terminal to ask.\");\n }\n if (!await confirm(` Delete these ${report.orphans.length} file(s) for good? [y/N] `)) {\n console.log(chalk.gray(\" Nothing was deleted.\"));\n return;\n }\n }\n\n let result: { deleted: string[]; failed: { key: string; error: string }[] };\n try {\n // The server finds them again before it deletes: a file a row has\n // named since the list was printed is not deleted.\n result = await backend.request(\"DELETE\", `/orphans?${query}`);\n } catch (error: unknown) {\n fail(\"Failed to delete unused files\", error, backend.baseUrl);\n }\n if (flags[\"--json\"]) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(chalk.green(` ✓ Deleted ${result.deleted.length} file(s).`));\n for (const failure of result.failed) console.error(chalk.red(` ✗ ${failure.key}: ${failure.error}`));\n console.log(\"\");\n }\n if (result.failed.length > 0) process.exitCode = 1;\n}\n\nasync function move(rawArgs: string[]): Promise<void> {\n const { flags } = parseCommandArgs({ spec: MOVE_FLAGS, rawArgs, commandWords: 2, command: \"storage move\", maxPositionals: 0 });\n const from = flags[\"--from\"];\n const to = flags[\"--to\"];\n if (!from && !to) {\n throw new UsageError(\"Name the sources: --from <source> --to <source>. The default source is \\\"(default)\\\", and either may be left out for it.\");\n }\n const backend = connect();\n const counts = { copied: 0, exists: 0, failed: 0, deleted: 0 };\n const failures: MovedObject[] = [];\n let cursor: string | undefined;\n do {\n let page: { moved: MovedObject[]; cursor?: string };\n try {\n page = await backend.request(\"POST\", \"/move\", {\n from,\n to,\n prefix: flags[\"--prefix\"],\n cursor,\n deleteSource: flags[\"--delete-source\"] === true\n });\n } catch (error: unknown) {\n fail(\"Failed to move files\", error, backend.baseUrl);\n }\n for (const object of page.moved) {\n counts[object.outcome]++;\n if (object.deleted) counts.deleted++;\n if (object.outcome === \"failed\") failures.push(object);\n }\n if (!flags[\"--json\"] && process.stdout.isTTY) {\n process.stdout.write(`\\r ${counts.copied} copied, ${counts.exists} already there, ${counts.failed} failed…`);\n }\n cursor = page.cursor;\n } while (cursor);\n\n if (flags[\"--json\"]) {\n console.log(JSON.stringify({ ...counts, failures }, null, 2));\n } else {\n if (process.stdout.isTTY) process.stdout.write(\"\\n\");\n console.log(chalk.green(` ✓ ${counts.copied} copied, ${counts.exists} already there` +\n `${flags[\"--delete-source\"] ? `, ${counts.deleted} deleted from the source` : \"\"}.`));\n for (const failure of failures) console.error(chalk.red(` ✗ ${failure.key}: ${failure.error ?? \"failed\"}`));\n if (counts.copied + counts.exists > 0 && !flags[\"--delete-source\"]) {\n console.log(chalk.gray(\" The source keeps its copies. Point the property at the new source, then run again with --delete-source.\"));\n }\n }\n if (failures.length > 0) process.exitCode = 1;\n}\n\nexport async function storageCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printStorageHelp();\n return;\n }\n switch (subcommand) {\n case \"orphans\":\n await orphans(rawArgs);\n break;\n case \"move\":\n await move(rawArgs);\n break;\n default:\n unknownCommand(subcommand, STORAGE_SUBCOMMANDS, \"storage\");\n }\n}\n\nfunction printStorageHelp(): void {\n console.log(`\n${chalk.bold(\"rebase storage\")} — Maintain stored files, through the running backend\n\n${chalk.green.bold(\"Usage\")}\n rebase storage ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.blue.bold(\"orphans\")} List the stored files no row names\n ${chalk.blue.bold(\"move\")} Copy a folder of files to another storage source, same keys\n\n${chalk.green.bold(\"orphans Options\")}\n ${chalk.blue(\"--source\")} Storage source key ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--older-than\")} Hours a file must be old to count ${chalk.gray(\"(default: 24)\")}\n ${chalk.blue(\"--delete\")} Delete them, after asking\n ${chalk.blue(\"--yes, -y\")} Do not ask ${chalk.gray(\"(required with --delete when there is no terminal)\")}\n ${chalk.blue(\"--json\")} Print the report as JSON\n\n${chalk.green.bold(\"move Options\")}\n ${chalk.blue(\"--from\")} Source to copy from ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--to\")} Source to copy to ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--prefix\")} Folder to copy ${chalk.gray(\"(default: everything)\")}\n ${chalk.blue(\"--delete-source\")} Delete each file from the source once the target holds it\n ${chalk.blue(\"--json\")} Print the counts as JSON\n\n${chalk.green.bold(\"Notes\")}\n A file is unused when no string in any row of any collection names it —\n a key, a URL, a ${chalk.blue(\"rebase-storage:\")} reference in text — whatever property\n or source holds it. Files newer than ${chalk.blue(\"--older-than\")} are left out: the admin\n panel uploads a file before the row that names it is saved. Files your own\n tables name, outside collections, are not seen: check before deleting.\n\n Both read ${chalk.blue(\"REBASE_SERVICE_KEY\")} from .env and need the backend running.\n`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage-BB3umRfw.js","names":[],"sources":["../src/commands/storage.ts"],"sourcesContent":["/**\n * CLI command: rebase storage <action>\n *\n * Whole-store maintenance through the running backend's `/api/admin/storage`,\n * authenticated with the service key from `.env`.\n *\n * Subcommands:\n * orphans — Stored files no row names; `--delete` deletes them\n * move — Copy a folder of files to another storage source, same keys\n */\nimport chalk from \"chalk\";\nimport readline from \"node:readline/promises\";\nimport { RebaseApiError } from \"@rebasepro/client\";\nimport { requireProjectRoot, readEnvFile } from \"../utils/project\";\nimport { parseCommandArgs, UsageError, wantsHelp } from \"../utils/args\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { cliUserAgent } from \"../utils/version\";\nimport { describeFailure, requireBackendUrl } from \"./api-keys\";\n\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const STORAGE_SUBCOMMANDS = [\"orphans\", \"move\"] as const;\n\n/** The flags `rebase storage orphans` takes. */\nexport const ORPHANS_FLAGS = {\n \"--source\": String,\n \"--older-than\": Number,\n \"--delete\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\",\n \"--json\": Boolean\n} as const;\n\n/** The flags `rebase storage move` takes. */\nexport const MOVE_FLAGS = {\n \"--from\": String,\n \"--to\": String,\n \"--prefix\": String,\n \"--delete-source\": Boolean,\n \"--json\": Boolean\n} as const;\n\n/** One stored object, as the backend lists it. */\ninterface StoredObjectEntry {\n key: string;\n size?: number;\n updated?: string;\n}\n\n/** What `GET /admin/storage/orphans` answers. */\nexport interface OrphanReport {\n storageId: string;\n objects: number;\n orphans: StoredObjectEntry[];\n recent: number;\n scanned: string[];\n unscanned: { collection: string; reason: string }[];\n}\n\ninterface MovedObject {\n key: string;\n outcome: \"copied\" | \"exists\" | \"failed\";\n deleted?: boolean;\n error?: string;\n}\n\ninterface Backend {\n baseUrl: string;\n request<T>(method: string, path: string, body?: unknown): Promise<T>;\n}\n\n/** The project's backend, holding the service key — the `admin` role, every scope. */\nfunction connect(): Backend {\n const projectRoot = requireProjectRoot();\n const env = readEnvFile(projectRoot);\n const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;\n if (!serviceKey) {\n console.error(chalk.red(\"✗ REBASE_SERVICE_KEY not found in .env — required for storage maintenance.\"));\n process.exit(1);\n }\n // Only an address this project recorded or the developer named: the\n // request carries the service key. See `resolveBackendAddress`.\n const baseUrl = requireBackendUrl(env, projectRoot).replace(/\\/+$/, \"\");\n return {\n baseUrl,\n async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/api/admin/storage${path}`, {\n method,\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Accept\": \"application/json\",\n \"User-Agent\": cliUserAgent(),\n ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" })\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) })\n });\n } catch (error: unknown) {\n throw new RebaseApiError(error instanceof Error ? error.message : String(error), { status: 0, code: \"NETWORK_ERROR\" });\n }\n const answer: unknown = await response.json().catch(() => undefined);\n if (!response.ok) {\n const error = answer && typeof answer === \"object\" && \"error\" in answer && answer.error && typeof answer.error === \"object\"\n ? answer.error as { message?: unknown; code?: unknown; details?: unknown }\n : undefined;\n throw new RebaseApiError(\n typeof error?.message === \"string\" ? error.message : `The server answered ${response.status}.`,\n { status: response.status, code: typeof error?.code === \"string\" ? error.code : undefined, details: error?.details }\n );\n }\n return (answer as { data: T }).data;\n }\n };\n}\n\nfunction fail(action: string, error: unknown, baseUrl: string): never {\n for (const line of describeFailure(action, error, baseUrl)) {\n console.error(line.startsWith(\"✗\") ? chalk.red(line) : chalk.gray(line));\n }\n if (error instanceof RebaseApiError && error.status === 404 && error.code !== \"UNKNOWN_STORAGE_SOURCE\") {\n console.error(chalk.gray(\" This backend has no storage maintenance routes: they arrived in 0.25, and need storage configured.\"));\n }\n process.exit(1);\n}\n\n/** Bytes, as a person reads them. */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;\n}\n\n/**\n * The lines that describe an orphan scan. Exported so its tests can read them\n * without a server.\n */\nexport function describeOrphanReport(report: OrphanReport, olderThanHours: number): string[] {\n const lines: string[] = [];\n const bytes = report.orphans.reduce((sum, orphan) => sum + (orphan.size ?? 0), 0);\n lines.push(` ${report.objects} file(s) in source \"${report.storageId}\", ${report.scanned.length} collection(s) read.`);\n if (report.orphans.length === 0) {\n lines.push(\" No file older than \" + `${olderThanHours}h is unused.`);\n } else {\n lines.push(` ${report.orphans.length} file(s) no row names, ${formatBytes(bytes)}:`);\n for (const orphan of report.orphans) {\n lines.push(` ${orphan.key}${orphan.size !== undefined ? ` ${formatBytes(orphan.size)}` : \"\"}${orphan.updated ? ` ${orphan.updated}` : \"\"}`);\n }\n }\n if (report.recent > 0) {\n const one = report.recent === 1;\n const more = report.orphans.length > 0 ? \"more \" : \"\";\n lines.push(` ${report.recent} ${more}unused ${one ? \"file is\" : \"files are\"} left out: ` +\n `${one ? \"it is\" : \"they are\"} newer than ${olderThanHours}h, or ${one ? \"its\" : \"their\"} age is unknown, ` +\n \"and an upload can be saved before the row that names it.\");\n }\n for (const entry of report.unscanned) {\n lines.push(` ⚠ ${entry.collection} could not be read (${entry.reason}): a file only it names is listed above.`);\n }\n return lines;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });\n try {\n return /^y(es)?$/i.test((await prompt.question(question)).trim());\n } finally {\n prompt.close();\n }\n}\n\nasync function orphans(rawArgs: string[]): Promise<void> {\n const { flags } = parseCommandArgs({ spec: ORPHANS_FLAGS, rawArgs, commandWords: 2, command: \"storage orphans\", maxPositionals: 0 });\n const olderThan = flags[\"--older-than\"] ?? 24;\n if (!Number.isFinite(olderThan) || olderThan < 0) {\n throw new UsageError(\"--older-than takes a number of hours, 0 or more.\");\n }\n const query = new URLSearchParams({ olderThanHours: String(olderThan) });\n if (flags[\"--source\"]) query.set(\"storageId\", flags[\"--source\"]);\n const backend = connect();\n\n let report: OrphanReport;\n try {\n report = await backend.request<OrphanReport>(\"GET\", `/orphans?${query}`);\n } catch (error: unknown) {\n fail(\"Failed to scan storage\", error, backend.baseUrl);\n }\n\n if (!flags[\"--delete\"]) {\n if (flags[\"--json\"]) {\n console.log(JSON.stringify(report, null, 2));\n return;\n }\n console.log(\"\");\n for (const line of describeOrphanReport(report, olderThan)) console.log(line);\n if (report.orphans.length > 0) {\n console.log(\"\");\n console.log(chalk.gray(\" Nothing was deleted. Run again with --delete to delete them.\"));\n }\n console.log(\"\");\n return;\n }\n\n if (!flags[\"--json\"]) {\n console.log(\"\");\n for (const line of describeOrphanReport(report, olderThan)) console.log(line);\n console.log(\"\");\n }\n if (report.orphans.length === 0) return;\n if (report.unscanned.length > 0) {\n console.error(chalk.red(\"✗ Not deleting: some collections could not be read, so a file they name would look unused.\"));\n process.exit(1);\n }\n if (!flags[\"--yes\"]) {\n if (!process.stdin.isTTY) {\n throw new UsageError(\"--delete deletes files for good; pass --yes to confirm when there is no terminal to ask.\");\n }\n if (!await confirm(` Delete these ${report.orphans.length} file(s) for good? [y/N] `)) {\n console.log(chalk.gray(\" Nothing was deleted.\"));\n return;\n }\n }\n\n let result: { deleted: string[]; failed: { key: string; error: string }[] };\n try {\n // The server finds them again before it deletes: a file a row has\n // named since the list was printed is not deleted.\n result = await backend.request(\"DELETE\", `/orphans?${query}`);\n } catch (error: unknown) {\n fail(\"Failed to delete unused files\", error, backend.baseUrl);\n }\n if (flags[\"--json\"]) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(chalk.green(` ✓ Deleted ${result.deleted.length} file(s).`));\n for (const failure of result.failed) console.error(chalk.red(` ✗ ${failure.key}: ${failure.error}`));\n console.log(\"\");\n }\n if (result.failed.length > 0) process.exitCode = 1;\n}\n\nasync function move(rawArgs: string[]): Promise<void> {\n const { flags } = parseCommandArgs({ spec: MOVE_FLAGS, rawArgs, commandWords: 2, command: \"storage move\", maxPositionals: 0 });\n const from = flags[\"--from\"];\n const to = flags[\"--to\"];\n if (!from && !to) {\n throw new UsageError(\"Name the sources: --from <source> --to <source>. The default source is \\\"(default)\\\", and either may be left out for it.\");\n }\n const backend = connect();\n const counts = { copied: 0, exists: 0, failed: 0, deleted: 0 };\n const failures: MovedObject[] = [];\n let cursor: string | undefined;\n do {\n let page: { moved: MovedObject[]; cursor?: string };\n try {\n page = await backend.request(\"POST\", \"/move\", {\n from,\n to,\n prefix: flags[\"--prefix\"],\n cursor,\n deleteSource: flags[\"--delete-source\"] === true\n });\n } catch (error: unknown) {\n fail(\"Failed to move files\", error, backend.baseUrl);\n }\n for (const object of page.moved) {\n counts[object.outcome]++;\n if (object.deleted) counts.deleted++;\n if (object.outcome === \"failed\") failures.push(object);\n }\n if (!flags[\"--json\"] && process.stdout.isTTY) {\n process.stdout.write(`\\r ${counts.copied} copied, ${counts.exists} already there, ${counts.failed} failed…`);\n }\n cursor = page.cursor;\n } while (cursor);\n\n if (flags[\"--json\"]) {\n console.log(JSON.stringify({ ...counts, failures }, null, 2));\n } else {\n if (process.stdout.isTTY) process.stdout.write(\"\\n\");\n console.log(chalk.green(` ✓ ${counts.copied} copied, ${counts.exists} already there` +\n `${flags[\"--delete-source\"] ? `, ${counts.deleted} deleted from the source` : \"\"}.`));\n for (const failure of failures) console.error(chalk.red(` ✗ ${failure.key}: ${failure.error ?? \"failed\"}`));\n if (counts.copied + counts.exists > 0 && !flags[\"--delete-source\"]) {\n console.log(chalk.gray(\" The source keeps its copies. Point the property at the new source, then run again with --delete-source.\"));\n }\n }\n if (failures.length > 0) process.exitCode = 1;\n}\n\nexport async function storageCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printStorageHelp();\n return;\n }\n switch (subcommand) {\n case \"orphans\":\n await orphans(rawArgs);\n break;\n case \"move\":\n await move(rawArgs);\n break;\n default:\n unknownCommand(subcommand, STORAGE_SUBCOMMANDS, \"storage\");\n }\n}\n\nfunction printStorageHelp(): void {\n console.log(`\n${chalk.bold(\"rebase storage\")} — Maintain stored files, through the running backend\n\n${chalk.green.bold(\"Usage\")}\n rebase storage ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.blue.bold(\"orphans\")} List the stored files no row names\n ${chalk.blue.bold(\"move\")} Copy a folder of files to another storage source, same keys\n\n${chalk.green.bold(\"orphans Options\")}\n ${chalk.blue(\"--source\")} Storage source key ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--older-than\")} Hours a file must be old to count ${chalk.gray(\"(default: 24)\")}\n ${chalk.blue(\"--delete\")} Delete them, after asking\n ${chalk.blue(\"--yes, -y\")} Do not ask ${chalk.gray(\"(required with --delete when there is no terminal)\")}\n ${chalk.blue(\"--json\")} Print the report as JSON\n\n${chalk.green.bold(\"move Options\")}\n ${chalk.blue(\"--from\")} Source to copy from ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--to\")} Source to copy to ${chalk.gray(\"(default: the default source)\")}\n ${chalk.blue(\"--prefix\")} Folder to copy ${chalk.gray(\"(default: everything)\")}\n ${chalk.blue(\"--delete-source\")} Delete each file from the source once the target holds it\n ${chalk.blue(\"--json\")} Print the counts as JSON\n\n${chalk.green.bold(\"Notes\")}\n A file is unused when no string in any row of any collection names it —\n a key, a URL, a ${chalk.blue(\"rebase-storage:\")} reference in text — whatever property\n or source holds it. Files newer than ${chalk.blue(\"--older-than\")} are left out: the admin\n panel uploads a file before the row that names it is saved. Files your own\n tables name, outside collections, are not seen: check before deleting.\n\n Both read ${chalk.blue(\"REBASE_SERVICE_KEY\")} from .env and need the backend running.\n`);\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as findProjectRoot, t as MANIFEST_FILENAME } from "./project-
|
|
1
|
+
import { c as findProjectRoot, t as MANIFEST_FILENAME } from "./project-g4K6kV1t.js";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
@@ -348,4 +348,4 @@ async function recordEvent(event, properties = {}, options = {}) {
|
|
|
348
348
|
//#endregion
|
|
349
349
|
export { setConsent as a, buildEvent as c, configPath as d, readConfig as f, recordEvent as i, durationBucket as l, isEnabled as n, suppressionReason as o, previewEvent as r, readProjectPolicy as s, endpoint as t, errorClass as u };
|
|
350
350
|
|
|
351
|
-
//# sourceMappingURL=telemetry-
|
|
351
|
+
//# sourceMappingURL=telemetry-HfU5uAh1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-EMFRVRQi.js","names":[],"sources":["../src/telemetry/identity.ts","../src/telemetry/payload.ts","../src/telemetry/project.ts","../src/telemetry/index.ts"],"sourcesContent":["import fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport crypto from \"crypto\";\n\n/**\n * Who is reporting, and where that is remembered.\n *\n * Two identifiers, because they answer two different questions and conflating\n * them would make the answers wrong:\n *\n * - `machineId` — one developer's machine. Lives beside the cloud credentials\n * in `~/.rebase/`, so it survives across every project they scaffold.\n * - `projectId` — one *checkout*. Lives in the project's gitignored\n * `.rebase/`, in a file of its own, `telemetry-project.json`, so it lasts\n * as long as the checkout does.\n *\n * Without the split, a developer trying five templates in an afternoon reads as\n * five installations. With it, that is one machine and five projects, which is\n * the shape the funnel actually needs.\n *\n * ## Why `projectId` is not committed\n *\n * `rebase.json` is checked in, and a stable identifier sitting in a public\n * repository is a correlation handle for anyone who finds it — it ties a named\n * organisation to whatever that id did. `.rebase/` is gitignored, so the id\n * stays on the machine that generated it. It also makes the unit honest: what\n * is being counted is a developer working on a project, not the project.\n *\n * ## Why both are random\n *\n * Neither is derived from a hostname, a database name, an email or a machine\n * fingerprint. Hashing any of those only *looks* anonymous — the space of real\n * hostnames is small enough to brute-force a salted hash back to the original\n * in minutes. A random UUID carries no information at all and does the same job.\n */\n\n/** Bumped when the stored shape changes in a way old CLIs cannot read. */\nexport const TELEMETRY_CONFIG_VERSION = 1;\n\nexport type TelemetryConfig = {\n version: number;\n /**\n * `undefined` means never asked — which is *not* the same as declining, and\n * is the only state in which anything may prompt.\n */\n enabled?: boolean;\n machineId?: string;\n /** When the choice was made, so a later release can tell stale consent from fresh. */\n decidedAt?: string;\n};\n\nexport function configPath(): string {\n return path.join(os.homedir(), \".rebase\", \"telemetry.json\");\n}\n\nexport function readConfig(): TelemetryConfig {\n try {\n const raw = JSON.parse(fs.readFileSync(configPath(), \"utf-8\"));\n if (raw && typeof raw === \"object\") {\n return {\n version: typeof raw.version === \"number\" ? raw.version : TELEMETRY_CONFIG_VERSION,\n enabled: typeof raw.enabled === \"boolean\" ? raw.enabled : undefined,\n machineId: typeof raw.machineId === \"string\" ? raw.machineId : undefined,\n decidedAt: typeof raw.decidedAt === \"string\" ? raw.decidedAt : undefined\n };\n }\n } catch {\n // Missing, unreadable or corrupt all mean the same thing: we have no\n // record of a decision, so nothing may be sent.\n }\n return { version: TELEMETRY_CONFIG_VERSION };\n}\n\nexport function writeConfig(config: TelemetryConfig): void {\n const file = configPath();\n fs.mkdirSync(path.dirname(file), { recursive: true });\n // 0600: it sits beside `credentials.json`, and a world-readable identifier\n // on a shared machine is a way to link one developer's activity to another's.\n fs.writeFileSync(file, JSON.stringify({ ...config,\nversion: TELEMETRY_CONFIG_VERSION }, null, 2), {\n encoding: \"utf-8\",\n mode: 0o600\n });\n}\n\n/**\n * The machine's id, generating and persisting one on first use.\n *\n * Only called once consent exists — an id written before the user agreed would\n * be a record we had no right to create, even unsent.\n */\nexport function ensureMachineId(): string {\n const config = readConfig();\n if (config.machineId) return config.machineId;\n const machineId = crypto.randomUUID();\n writeConfig({ ...config,\nmachineId });\n return machineId;\n}\n\n/** Where a checkout keeps its id, under the project's gitignored `.rebase/`. */\nexport const PROJECT_ID_FILE = \"telemetry-project.json\";\n\n/**\n * The checkout's id, generating one if this project has none.\n *\n * The directory is created when it is missing, but **only** where a\n * `rebase.json` proves this really is a project root. An earlier version\n * required `.rebase/` to already exist, on the assumption that `rebase init`\n * created it — it does not. It is written only when a scaffold is linked to\n * Rebase Cloud, so every self-hosted project reported no `projectId` at all,\n * for ever. That silently broke the funnel this id exists for, on exactly the\n * population the telemetry is meant to learn about.\n *\n * The `rebase.json` check is what keeps the fix from being \"scatter `.rebase/`\n * wherever a command happens to run\": no manifest, no project, no directory,\n * and the event simply reports no project.\n *\n * A file of its own, written by nothing else. The id used to be a key in\n * `.rebase/state.json`, which is the dev server's runtime file: rewritten\n * from scratch on every start and deleted on every shutdown. So every\n * restart of `rebase dev` minted a new id, and one checkout read as a new\n * project each time — the error this split exists to prevent, by another way.\n */\nexport function ensureProjectId(projectRoot: string): string | undefined {\n if (!fs.existsSync(path.join(projectRoot, \"rebase.json\"))) return undefined;\n\n const dir = path.join(projectRoot, \".rebase\");\n try {\n fs.mkdirSync(dir, { recursive: true });\n } catch {\n return undefined;\n }\n\n const file = path.join(dir, PROJECT_ID_FILE);\n if (fs.existsSync(file)) return readProjectId(file);\n\n const projectId = crypto.randomUUID();\n try {\n // `wx`: of two commands minting the first id at once, one writes and\n // the other reads what it wrote.\n fs.writeFileSync(file, `${JSON.stringify({ projectId }, null, 2)}\\n`, { encoding: \"utf-8\", flag: \"wx\", mode: 0o600 });\n } catch {\n return fs.existsSync(file) ? readProjectId(file) : undefined;\n }\n return projectId;\n}\n\n/**\n * The id a checkout's file holds. A file that does not parse is not ours to\n * repair — overwriting it would quietly start a second id for the same\n * checkout — so the event reports no project instead.\n */\nfunction readProjectId(file: string): string | undefined {\n try {\n const raw = JSON.parse(fs.readFileSync(file, \"utf-8\"));\n if (raw && typeof raw === \"object\" && typeof raw.projectId === \"string\") return raw.projectId;\n } catch {\n // Unreadable or corrupt: see above.\n }\n return undefined;\n}\n","import os from \"os\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\n/**\n * Every field that may ever leave the machine, and nothing else.\n *\n * ## The rule this file exists to enforce\n *\n * **No value here may be free text.** Every property is either a number, a\n * boolean, or a string drawn from a set enumerated in this file. That is not\n * fussiness — free text is the mechanism by which telemetry leaks. An error\n * message carries `/Users/francesco/rebase/...`, which is a real person's name.\n * A collection name carries a customer's product vocabulary. A database URL\n * carries a password. None of those are things anyone intends to send; they\n * arrive by being interpolated into a string that a well-meaning field was\n * happy to accept.\n *\n * So the payload is closed by construction. Adding a field means adding it\n * here, where the review question — \"can this ever contain something the user\n * typed?\" — is unavoidable.\n *\n * ## What is deliberately absent\n *\n * Project names, directory paths, collection or table names, hostnames,\n * database URLs, error messages, stack traces, email addresses, exact row or\n * user counts. Anything that would let a payload be traced to an organisation\n * rather than to an anonymous id.\n */\n\n/**\n * Bumped whenever a field is added, removed or changes meaning.\n *\n * Sent with every event so the receiving end can reject or migrate old shapes,\n * and so `rebase telemetry show` can state which contract the user agreed to.\n */\nexport const TELEMETRY_SCHEMA_VERSION = 1;\n\n/** The events the CLI reports. Closed set — a name not listed cannot be sent. */\nexport type TelemetryEventName =\n | \"cli.init\"\n | \"cli.dev\"\n | \"cli.deploy\"\n // Named for the command group, not for one of its subcommands. These were\n // `cli.schema_generate` and `cli.db_push` and were sent for *every*\n // subcommand of their group — so `rebase db restore` was recorded as a\n // push, and anything counting pushes counted every database command. The\n // subcommand travels as a property, where it can be read.\n | \"cli.schema\"\n | \"cli.db\"\n | \"cli.error\";\n\n/** A single non-free-text value. */\nexport type TelemetryValue = string | number | boolean;\n\nexport type TelemetryEvent = {\n schema: number;\n event: TelemetryEventName;\n /** Random per machine — see identity.ts on why it is not derived. */\n machineId: string;\n /** Random per checkout. Absent when the command ran outside a project. */\n projectId?: string;\n /** The CLI's own version, so old releases can be told apart. */\n cliVersion: string;\n /** Coarse platform facts, for the support matrix. */\n nodeMajor: number;\n platform: NodeJS.Platform;\n arch: string;\n /** UTC, second precision. Not a device clock reading anyone can fingerprint. */\n at: string;\n properties: Record<string, TelemetryValue>;\n};\n\n/**\n * Buckets rather than exact counts.\n *\n * An exact figure is a surprisingly good fingerprint — \"this install has 37\n * collections\" narrows the field a great deal when combined with a version and\n * a driver. A bucket answers \"roughly how big\" without doing that.\n */\nexport function bucket(value: number): string {\n if (!Number.isFinite(value) || value < 0) return \"unknown\";\n if (value === 0) return \"0\";\n if (value <= 5) return \"1-5\";\n if (value <= 20) return \"6-20\";\n if (value <= 50) return \"21-50\";\n if (value <= 200) return \"51-200\";\n return \"200+\";\n}\n\n/** Duration in coarse bands — enough to see \"slow\", not enough to fingerprint. */\nexport function durationBucket(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"unknown\";\n if (ms < 5_000) return \"<5s\";\n if (ms < 30_000) return \"5-30s\";\n if (ms < 120_000) return \"30-120s\";\n return \"120s+\";\n}\n\n/**\n * An error reduced to something safe to transmit.\n *\n * The message and the stack are discarded, always. What survives is the\n * constructor name and, for the errors that carry one, a `code` — both of which\n * come from the program rather than from anything the user typed or named.\n * `EACCES` is useful and safe; \"cannot write /Users/francesco/clients/acme\" is\n * neither.\n */\nexport function errorClass(error: unknown): string {\n if (error && typeof error === \"object\") {\n const code = (error as { code?: unknown }).code;\n if (typeof code === \"string\" && /^[A-Z][A-Z0-9_]{1,31}$/.test(code)) return code;\n const name = (error as { name?: unknown }).name;\n if (typeof name === \"string\" && /^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) return name;\n }\n return \"Unknown\";\n}\n\n/**\n * Drop anything that is not a permitted value type, and clamp strings.\n *\n * The last line of defence rather than the first. Every call site is supposed\n * to pass enumerated values; this is what stops a future one that forgets from\n * turning into an incident. Strings are capped at 64 characters because no\n * legitimate enumerated value is longer, and a path or a message always is.\n */\n/**\n * Key names that shadow an `Object.prototype` member.\n *\n * `__proto__` is already excluded by the pattern (it starts with an\n * underscore), and assigning any of these to an object literal shadows rather\n * than pollutes — so nothing is exploitable here. They are refused because a\n * stored property called `constructor` is a trap for every consumer downstream\n * that reaches for `properties.constructor` and gets a string.\n */\nconst RESERVED_KEYS = new Set([\"constructor\", \"prototype\", \"hasownproperty\", \"tostring\", \"valueof\"]);\n\nexport function sanitize(properties: Record<string, unknown>): Record<string, TelemetryValue> {\n const out: Record<string, TelemetryValue> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || RESERVED_KEYS.has(key.toLowerCase())) continue;\n if (typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value))) {\n out[key] = value;\n } else if (typeof value === \"string\" && value.length > 0 && value.length <= 64) {\n // Anything with a separator in it is a path, a URL or a sentence —\n // never one of our enumerated tokens.\n if (!/[/\\\\@:\\s]/.test(value)) out[key] = value;\n }\n }\n return out;\n}\n\n/**\n * The CLI's own version, read by walking up to this package's manifest.\n *\n * The obvious `require(\"../../package.json\")` was wrong everywhere, not only on\n * one install path: `vite build` bundles this module into `dist/index.es.js`, so\n * the specifier resolves relative to `<pkg>/dist/` and lands on\n * `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or\n * the monorepo. Every event ever sent carried `cliVersion: \"unknown\"`, which is\n * the one field that makes the rest of a payload interpretable.\n *\n * So walk up and check the manifest's `name` rather than counting directory\n * levels: the count differs between `src/telemetry/` and the bundled `dist/`,\n * and a wrong count fails silently by finding *some* package.json — the nearest\n * dependency's, under a hoisted layout. Matching the name cannot do that.\n */\nfunction cliVersion(): string {\n try {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n const root = path.parse(dir).root;\n while (dir && dir !== root) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n const pkg = JSON.parse(fs.readFileSync(manifest, \"utf-8\"));\n if (pkg?.name === \"@rebasepro/cli\" && typeof pkg.version === \"string\" && pkg.version) {\n return pkg.version;\n }\n }\n dir = path.dirname(dir);\n }\n } catch {\n // Fall through — a version we cannot read is reported as such.\n }\n return \"unknown\";\n}\n\nexport function buildEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown>,\n identity: { machineId: string; projectId?: string }\n): TelemetryEvent {\n return {\n schema: TELEMETRY_SCHEMA_VERSION,\n event,\n machineId: identity.machineId,\n projectId: identity.projectId,\n cliVersion: cliVersion(),\n nodeMajor: Number(process.versions.node.split(\".\")[0]) || 0,\n platform: os.platform(),\n arch: os.arch(),\n at: new Date().toISOString().replace(/\\.\\d{3}Z$/, \"Z\"),\n properties: sanitize(properties)\n };\n}\n","import fs from \"fs\";\nimport path from \"path\";\n\nimport { findProjectRoot, MANIFEST_FILENAME } from \"../utils/project\";\n\n/**\n * A repository's own policy on usage sharing.\n *\n * ## Why this is deliberately one-directional\n *\n * `rebase.json` is committed, so whatever it says applies to everyone who\n * clones the repository — including people who have never seen our prompt. That\n * makes it the right place for exactly one of the two answers:\n *\n * - **`false` is honoured**, and beats an individual's opt-in. An organisation\n * restricting its own repository is setting policy for work done on its\n * behalf, which is theirs to set. It is the same shape as a committed\n * `.npmrc`.\n * - **`true` is ignored**, and says so. It would be one developer answering a\n * privacy question for every colleague who later clones the repo — consent\n * by proxy, which is the precise thing opt-in exists to prevent.\n *\n * The asymmetry is the whole feature. A symmetric flag would be a worse version\n * of the machine-level setting, and a quietly-obeyed `true` would be a way to\n * enrol people without asking them.\n *\n * Read straight from the file rather than through `parseManifest`, for two\n * reasons: the parser builds a typed object and drops keys it does not model,\n * and a manifest too malformed to parse must still be able to switch telemetry\n * off. A repository that says no should be obeyed even when it is broken.\n */\nexport type ProjectTelemetryPolicy =\n /** `\"telemetry\": false` — suppress, regardless of the machine setting. */\n | \"opt_out\"\n /** `\"telemetry\": true` — refused; the machine setting still decides. */\n | \"ignored_opt_in\"\n /** No manifest, no key, or an unreadable one. */\n | \"unset\";\n\nexport function readProjectPolicy(startDir: string = process.cwd()): ProjectTelemetryPolicy {\n try {\n const root = findProjectRoot(startDir);\n if (!root) return \"unset\";\n\n const file = path.join(root, MANIFEST_FILENAME);\n if (!fs.existsSync(file)) return \"unset\";\n\n // A regex rather than JSON.parse: a manifest with a trailing comma or a\n // half-finished edit still parses badly but reads clearly, and \"your\n // JSON is malformed\" is not a reason to start sending data the file\n // plainly asks us not to send.\n const raw = fs.readFileSync(file, \"utf-8\");\n const match = /\"telemetry\"\\s*:\\s*(true|false)/.exec(raw);\n if (!match) return \"unset\";\n return match[1] === \"false\" ? \"opt_out\" : \"ignored_opt_in\";\n } catch {\n return \"unset\";\n }\n}\n","import { ensureMachineId, ensureProjectId, readConfig, writeConfig } from \"./identity\";\nimport { buildEvent, TelemetryEvent, TelemetryEventName } from \"./payload\";\nimport { readProjectPolicy } from \"./project\";\nimport { parseEnvBoolean } from \"@rebasepro/types\";\n\nexport { TELEMETRY_SCHEMA_VERSION, bucket, durationBucket, errorClass, buildEvent, sanitize } from \"./payload\";\nexport type { TelemetryEvent, TelemetryEventName, TelemetryValue } from \"./payload\";\nexport { configPath, readConfig, writeConfig } from \"./identity\";\nexport type { TelemetryConfig } from \"./identity\";\nexport { readProjectPolicy } from \"./project\";\nexport type { ProjectTelemetryPolicy } from \"./project\";\n\n/**\n * Where events go. Overridable so a fork can point at its own collector, and so\n * the tests never touch the network.\n */\nexport const DEFAULT_TELEMETRY_ENDPOINT = \"https://app.rebase.pro/api/functions/telemetry\";\n\nexport function endpoint(): string {\n return process.env.REBASE_TELEMETRY_ENDPOINT?.trim() || DEFAULT_TELEMETRY_ENDPOINT;\n}\n\n/**\n * Set to anything but a spelled no.\n *\n * The conventions behind these variables make presence the signal —\n * `CI=woodpecker` is a CI run, `DO_NOT_TRACK=please` is an answer — so a value\n * the parser does not recognise still counts, and the refusal holds. Only the\n * platform's spellings of no turn one off. It was `!== \"0\"` for two of them and\n * `!== \"false\"` for `CI`, so `CI=0` read as a runner.\n */\nfunction setAndNotNo(value: string | undefined): boolean {\n return value !== undefined && value.trim() !== \"\" && parseEnvBoolean(value) !== false;\n}\n\n/** Why nothing would be sent right now, or `null` when it would. */\nexport type SuppressionReason =\n | \"not_asked\"\n | \"declined\"\n | \"do_not_track\"\n | \"rebase_telemetry_disabled\"\n | \"ci\"\n | \"project_opt_out\";\n\n/**\n * The one function that decides whether anything leaves the machine.\n *\n * Every path is a refusal except the last, which is the point: consent is\n * opt-in, so the default answer at every branch — never asked, unreadable\n * config, a set env var, a CI runner — is no.\n *\n * `DO_NOT_TRACK` is honoured because it is the cross-tool convention\n * (consoledonottrack.com); a user who has set it globally has already answered\n * this question and should not be asked again by us.\n *\n * `CI` is refused for a different reason: a build runner is not a person, and\n * counting one is both useless and misleading. A single pipeline re-running on\n * every push would otherwise outweigh every real developer in the data.\n *\n * A project's own `\"telemetry\": false` is checked *before* the machine setting,\n * because it has to beat an individual opt-in to be worth anything — see\n * project.ts on why the reverse is refused.\n */\nexport function suppressionReason(\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd()\n): SuppressionReason | null {\n if (setAndNotNo(env.DO_NOT_TRACK)) return \"do_not_track\";\n if (setAndNotNo(env.REBASE_TELEMETRY_DISABLED)) return \"rebase_telemetry_disabled\";\n if (setAndNotNo(env.CI)) return \"ci\";\n if (readProjectPolicy(cwd) === \"opt_out\") return \"project_opt_out\";\n\n const config = readConfig();\n if (config.enabled === undefined) return \"not_asked\";\n if (!config.enabled) return \"declined\";\n return null;\n}\n\nexport function isEnabled(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): boolean {\n return suppressionReason(env, cwd) === null;\n}\n\n/**\n * Record the user's answer. `false` is final — nothing prompts again.\n *\n * Returns whether the choice could actually be persisted. A read-only or full\n * home directory makes `writeConfig` throw, and the direction of that failure\n * matters enormously: someone turning sharing **off** who sees a stack trace,\n * or worse sees nothing, is left sharing. The caller is expected to say so and\n * point at `REBASE_TELEMETRY_DISABLED`, which needs no disk.\n */\nexport function setConsent(enabled: boolean): boolean {\n const config = readConfig();\n try {\n writeConfig({\n ...config,\n enabled,\n decidedAt: new Date().toISOString(),\n // An id is minted only on acceptance. Generating one at decline\n // time would create the very record the user just refused.\n machineId: enabled ? (config.machineId ?? undefined) : undefined\n });\n if (enabled) ensureMachineId();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Build the event that *would* be sent, without sending it.\n *\n * This is what `rebase telemetry show` prints, and it is deliberately the same\n * function the sender uses — a preview assembled by separate code is a promise\n * that drifts. Returns `null` when nothing would be sent, so the command can\n * say why instead of showing a payload that is never going anywhere.\n */\nexport function previewEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown> = {},\n projectRoot?: string\n): TelemetryEvent | null {\n const config = readConfig();\n if (!config.machineId) return null;\n return buildEvent(event, properties, {\n machineId: config.machineId,\n projectId: projectRoot ? ensureProjectId(projectRoot) : undefined\n });\n}\n\n/**\n * Send one event, or quietly do nothing.\n *\n * Three properties this must hold, in order of importance:\n *\n * 1. **It never throws.** Telemetry failing is not a reason for `rebase dev`\n * to fail. Every error is swallowed.\n * 2. **It never blocks meaningfully.** A two-second ceiling, after which the\n * command carries on regardless — a collector having a bad day must not\n * become the CLI hanging.\n * 3. **It never sends without consent.** Enforced here rather than at the call\n * sites, so a new call site cannot get it wrong.\n */\nexport async function recordEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown> = {},\n options: { projectRoot?: string; timeoutMs?: number } = {}\n): Promise<void> {\n try {\n // The project's own policy is read relative to the project being acted\n // on, not the shell's cwd — `rebase dev` run from a subdirectory must\n // still see its own repository's opt-out.\n if (!isEnabled(process.env, options.projectRoot ?? process.cwd())) return;\n\n const machineId = ensureMachineId();\n const projectId = options.projectRoot ? ensureProjectId(options.projectRoot) : undefined;\n const payload = buildEvent(event, properties, { machineId,\nprojectId });\n\n const abort = new AbortController();\n const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 2000);\n try {\n await fetch(endpoint(), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: abort.signal\n });\n } finally {\n clearTimeout(timer);\n }\n } catch {\n // Deliberately silent. A warning here would be worse than useless: it\n // draws attention to a subsystem the user opted into and cannot act on,\n // in the middle of output they were actually reading.\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"telemetry-HfU5uAh1.js","names":[],"sources":["../src/telemetry/identity.ts","../src/telemetry/payload.ts","../src/telemetry/project.ts","../src/telemetry/index.ts"],"sourcesContent":["import fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport crypto from \"crypto\";\n\n/**\n * Who is reporting, and where that is remembered.\n *\n * Two identifiers, because they answer two different questions and conflating\n * them would make the answers wrong:\n *\n * - `machineId` — one developer's machine. Lives beside the cloud credentials\n * in `~/.rebase/`, so it survives across every project they scaffold.\n * - `projectId` — one *checkout*. Lives in the project's gitignored\n * `.rebase/`, in a file of its own, `telemetry-project.json`, so it lasts\n * as long as the checkout does.\n *\n * Without the split, a developer trying five templates in an afternoon reads as\n * five installations. With it, that is one machine and five projects, which is\n * the shape the funnel actually needs.\n *\n * ## Why `projectId` is not committed\n *\n * `rebase.json` is checked in, and a stable identifier sitting in a public\n * repository is a correlation handle for anyone who finds it — it ties a named\n * organisation to whatever that id did. `.rebase/` is gitignored, so the id\n * stays on the machine that generated it. It also makes the unit honest: what\n * is being counted is a developer working on a project, not the project.\n *\n * ## Why both are random\n *\n * Neither is derived from a hostname, a database name, an email or a machine\n * fingerprint. Hashing any of those only *looks* anonymous — the space of real\n * hostnames is small enough to brute-force a salted hash back to the original\n * in minutes. A random UUID carries no information at all and does the same job.\n */\n\n/** Bumped when the stored shape changes in a way old CLIs cannot read. */\nexport const TELEMETRY_CONFIG_VERSION = 1;\n\nexport type TelemetryConfig = {\n version: number;\n /**\n * `undefined` means never asked — which is *not* the same as declining, and\n * is the only state in which anything may prompt.\n */\n enabled?: boolean;\n machineId?: string;\n /** When the choice was made, so a later release can tell stale consent from fresh. */\n decidedAt?: string;\n};\n\nexport function configPath(): string {\n return path.join(os.homedir(), \".rebase\", \"telemetry.json\");\n}\n\nexport function readConfig(): TelemetryConfig {\n try {\n const raw = JSON.parse(fs.readFileSync(configPath(), \"utf-8\"));\n if (raw && typeof raw === \"object\") {\n return {\n version: typeof raw.version === \"number\" ? raw.version : TELEMETRY_CONFIG_VERSION,\n enabled: typeof raw.enabled === \"boolean\" ? raw.enabled : undefined,\n machineId: typeof raw.machineId === \"string\" ? raw.machineId : undefined,\n decidedAt: typeof raw.decidedAt === \"string\" ? raw.decidedAt : undefined\n };\n }\n } catch {\n // Missing, unreadable or corrupt all mean the same thing: we have no\n // record of a decision, so nothing may be sent.\n }\n return { version: TELEMETRY_CONFIG_VERSION };\n}\n\nexport function writeConfig(config: TelemetryConfig): void {\n const file = configPath();\n fs.mkdirSync(path.dirname(file), { recursive: true });\n // 0600: it sits beside `credentials.json`, and a world-readable identifier\n // on a shared machine is a way to link one developer's activity to another's.\n fs.writeFileSync(file, JSON.stringify({ ...config,\nversion: TELEMETRY_CONFIG_VERSION }, null, 2), {\n encoding: \"utf-8\",\n mode: 0o600\n });\n}\n\n/**\n * The machine's id, generating and persisting one on first use.\n *\n * Only called once consent exists — an id written before the user agreed would\n * be a record we had no right to create, even unsent.\n */\nexport function ensureMachineId(): string {\n const config = readConfig();\n if (config.machineId) return config.machineId;\n const machineId = crypto.randomUUID();\n writeConfig({ ...config,\nmachineId });\n return machineId;\n}\n\n/** Where a checkout keeps its id, under the project's gitignored `.rebase/`. */\nexport const PROJECT_ID_FILE = \"telemetry-project.json\";\n\n/**\n * The checkout's id, generating one if this project has none.\n *\n * The directory is created when it is missing, but **only** where a\n * `rebase.json` proves this really is a project root. An earlier version\n * required `.rebase/` to already exist, on the assumption that `rebase init`\n * created it — it does not. It is written only when a scaffold is linked to\n * Rebase Cloud, so every self-hosted project reported no `projectId` at all,\n * for ever. That silently broke the funnel this id exists for, on exactly the\n * population the telemetry is meant to learn about.\n *\n * The `rebase.json` check is what keeps the fix from being \"scatter `.rebase/`\n * wherever a command happens to run\": no manifest, no project, no directory,\n * and the event simply reports no project.\n *\n * A file of its own, written by nothing else. The id used to be a key in\n * `.rebase/state.json`, which is the dev server's runtime file: rewritten\n * from scratch on every start and deleted on every shutdown. So every\n * restart of `rebase dev` minted a new id, and one checkout read as a new\n * project each time — the error this split exists to prevent, by another way.\n */\nexport function ensureProjectId(projectRoot: string): string | undefined {\n if (!fs.existsSync(path.join(projectRoot, \"rebase.json\"))) return undefined;\n\n const dir = path.join(projectRoot, \".rebase\");\n try {\n fs.mkdirSync(dir, { recursive: true });\n } catch {\n return undefined;\n }\n\n const file = path.join(dir, PROJECT_ID_FILE);\n if (fs.existsSync(file)) return readProjectId(file);\n\n const projectId = crypto.randomUUID();\n try {\n // `wx`: of two commands minting the first id at once, one writes and\n // the other reads what it wrote.\n fs.writeFileSync(file, `${JSON.stringify({ projectId }, null, 2)}\\n`, { encoding: \"utf-8\", flag: \"wx\", mode: 0o600 });\n } catch {\n return fs.existsSync(file) ? readProjectId(file) : undefined;\n }\n return projectId;\n}\n\n/**\n * The id a checkout's file holds. A file that does not parse is not ours to\n * repair — overwriting it would quietly start a second id for the same\n * checkout — so the event reports no project instead.\n */\nfunction readProjectId(file: string): string | undefined {\n try {\n const raw = JSON.parse(fs.readFileSync(file, \"utf-8\"));\n if (raw && typeof raw === \"object\" && typeof raw.projectId === \"string\") return raw.projectId;\n } catch {\n // Unreadable or corrupt: see above.\n }\n return undefined;\n}\n","import os from \"os\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\n/**\n * Every field that may ever leave the machine, and nothing else.\n *\n * ## The rule this file exists to enforce\n *\n * **No value here may be free text.** Every property is either a number, a\n * boolean, or a string drawn from a set enumerated in this file. That is not\n * fussiness — free text is the mechanism by which telemetry leaks. An error\n * message carries `/Users/francesco/rebase/...`, which is a real person's name.\n * A collection name carries a customer's product vocabulary. A database URL\n * carries a password. None of those are things anyone intends to send; they\n * arrive by being interpolated into a string that a well-meaning field was\n * happy to accept.\n *\n * So the payload is closed by construction. Adding a field means adding it\n * here, where the review question — \"can this ever contain something the user\n * typed?\" — is unavoidable.\n *\n * ## What is deliberately absent\n *\n * Project names, directory paths, collection or table names, hostnames,\n * database URLs, error messages, stack traces, email addresses, exact row or\n * user counts. Anything that would let a payload be traced to an organisation\n * rather than to an anonymous id.\n */\n\n/**\n * Bumped whenever a field is added, removed or changes meaning.\n *\n * Sent with every event so the receiving end can reject or migrate old shapes,\n * and so `rebase telemetry show` can state which contract the user agreed to.\n */\nexport const TELEMETRY_SCHEMA_VERSION = 1;\n\n/** The events the CLI reports. Closed set — a name not listed cannot be sent. */\nexport type TelemetryEventName =\n | \"cli.init\"\n | \"cli.dev\"\n | \"cli.deploy\"\n // Named for the command group, not for one of its subcommands. These were\n // `cli.schema_generate` and `cli.db_push` and were sent for *every*\n // subcommand of their group — so `rebase db restore` was recorded as a\n // push, and anything counting pushes counted every database command. The\n // subcommand travels as a property, where it can be read.\n | \"cli.schema\"\n | \"cli.db\"\n | \"cli.error\";\n\n/** A single non-free-text value. */\nexport type TelemetryValue = string | number | boolean;\n\nexport type TelemetryEvent = {\n schema: number;\n event: TelemetryEventName;\n /** Random per machine — see identity.ts on why it is not derived. */\n machineId: string;\n /** Random per checkout. Absent when the command ran outside a project. */\n projectId?: string;\n /** The CLI's own version, so old releases can be told apart. */\n cliVersion: string;\n /** Coarse platform facts, for the support matrix. */\n nodeMajor: number;\n platform: NodeJS.Platform;\n arch: string;\n /** UTC, second precision. Not a device clock reading anyone can fingerprint. */\n at: string;\n properties: Record<string, TelemetryValue>;\n};\n\n/**\n * Buckets rather than exact counts.\n *\n * An exact figure is a surprisingly good fingerprint — \"this install has 37\n * collections\" narrows the field a great deal when combined with a version and\n * a driver. A bucket answers \"roughly how big\" without doing that.\n */\nexport function bucket(value: number): string {\n if (!Number.isFinite(value) || value < 0) return \"unknown\";\n if (value === 0) return \"0\";\n if (value <= 5) return \"1-5\";\n if (value <= 20) return \"6-20\";\n if (value <= 50) return \"21-50\";\n if (value <= 200) return \"51-200\";\n return \"200+\";\n}\n\n/** Duration in coarse bands — enough to see \"slow\", not enough to fingerprint. */\nexport function durationBucket(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"unknown\";\n if (ms < 5_000) return \"<5s\";\n if (ms < 30_000) return \"5-30s\";\n if (ms < 120_000) return \"30-120s\";\n return \"120s+\";\n}\n\n/**\n * An error reduced to something safe to transmit.\n *\n * The message and the stack are discarded, always. What survives is the\n * constructor name and, for the errors that carry one, a `code` — both of which\n * come from the program rather than from anything the user typed or named.\n * `EACCES` is useful and safe; \"cannot write /Users/francesco/clients/acme\" is\n * neither.\n */\nexport function errorClass(error: unknown): string {\n if (error && typeof error === \"object\") {\n const code = (error as { code?: unknown }).code;\n if (typeof code === \"string\" && /^[A-Z][A-Z0-9_]{1,31}$/.test(code)) return code;\n const name = (error as { name?: unknown }).name;\n if (typeof name === \"string\" && /^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) return name;\n }\n return \"Unknown\";\n}\n\n/**\n * Drop anything that is not a permitted value type, and clamp strings.\n *\n * The last line of defence rather than the first. Every call site is supposed\n * to pass enumerated values; this is what stops a future one that forgets from\n * turning into an incident. Strings are capped at 64 characters because no\n * legitimate enumerated value is longer, and a path or a message always is.\n */\n/**\n * Key names that shadow an `Object.prototype` member.\n *\n * `__proto__` is already excluded by the pattern (it starts with an\n * underscore), and assigning any of these to an object literal shadows rather\n * than pollutes — so nothing is exploitable here. They are refused because a\n * stored property called `constructor` is a trap for every consumer downstream\n * that reaches for `properties.constructor` and gets a string.\n */\nconst RESERVED_KEYS = new Set([\"constructor\", \"prototype\", \"hasownproperty\", \"tostring\", \"valueof\"]);\n\nexport function sanitize(properties: Record<string, unknown>): Record<string, TelemetryValue> {\n const out: Record<string, TelemetryValue> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || RESERVED_KEYS.has(key.toLowerCase())) continue;\n if (typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value))) {\n out[key] = value;\n } else if (typeof value === \"string\" && value.length > 0 && value.length <= 64) {\n // Anything with a separator in it is a path, a URL or a sentence —\n // never one of our enumerated tokens.\n if (!/[/\\\\@:\\s]/.test(value)) out[key] = value;\n }\n }\n return out;\n}\n\n/**\n * The CLI's own version, read by walking up to this package's manifest.\n *\n * The obvious `require(\"../../package.json\")` was wrong everywhere, not only on\n * one install path: `vite build` bundles this module into `dist/index.es.js`, so\n * the specifier resolves relative to `<pkg>/dist/` and lands on\n * `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or\n * the monorepo. Every event ever sent carried `cliVersion: \"unknown\"`, which is\n * the one field that makes the rest of a payload interpretable.\n *\n * So walk up and check the manifest's `name` rather than counting directory\n * levels: the count differs between `src/telemetry/` and the bundled `dist/`,\n * and a wrong count fails silently by finding *some* package.json — the nearest\n * dependency's, under a hoisted layout. Matching the name cannot do that.\n */\nfunction cliVersion(): string {\n try {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n const root = path.parse(dir).root;\n while (dir && dir !== root) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n const pkg = JSON.parse(fs.readFileSync(manifest, \"utf-8\"));\n if (pkg?.name === \"@rebasepro/cli\" && typeof pkg.version === \"string\" && pkg.version) {\n return pkg.version;\n }\n }\n dir = path.dirname(dir);\n }\n } catch {\n // Fall through — a version we cannot read is reported as such.\n }\n return \"unknown\";\n}\n\nexport function buildEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown>,\n identity: { machineId: string; projectId?: string }\n): TelemetryEvent {\n return {\n schema: TELEMETRY_SCHEMA_VERSION,\n event,\n machineId: identity.machineId,\n projectId: identity.projectId,\n cliVersion: cliVersion(),\n nodeMajor: Number(process.versions.node.split(\".\")[0]) || 0,\n platform: os.platform(),\n arch: os.arch(),\n at: new Date().toISOString().replace(/\\.\\d{3}Z$/, \"Z\"),\n properties: sanitize(properties)\n };\n}\n","import fs from \"fs\";\nimport path from \"path\";\n\nimport { findProjectRoot, MANIFEST_FILENAME } from \"../utils/project\";\n\n/**\n * A repository's own policy on usage sharing.\n *\n * ## Why this is deliberately one-directional\n *\n * `rebase.json` is committed, so whatever it says applies to everyone who\n * clones the repository — including people who have never seen our prompt. That\n * makes it the right place for exactly one of the two answers:\n *\n * - **`false` is honoured**, and beats an individual's opt-in. An organisation\n * restricting its own repository is setting policy for work done on its\n * behalf, which is theirs to set. It is the same shape as a committed\n * `.npmrc`.\n * - **`true` is ignored**, and says so. It would be one developer answering a\n * privacy question for every colleague who later clones the repo — consent\n * by proxy, which is the precise thing opt-in exists to prevent.\n *\n * The asymmetry is the whole feature. A symmetric flag would be a worse version\n * of the machine-level setting, and a quietly-obeyed `true` would be a way to\n * enrol people without asking them.\n *\n * Read straight from the file rather than through `parseManifest`, for two\n * reasons: the parser builds a typed object and drops keys it does not model,\n * and a manifest too malformed to parse must still be able to switch telemetry\n * off. A repository that says no should be obeyed even when it is broken.\n */\nexport type ProjectTelemetryPolicy =\n /** `\"telemetry\": false` — suppress, regardless of the machine setting. */\n | \"opt_out\"\n /** `\"telemetry\": true` — refused; the machine setting still decides. */\n | \"ignored_opt_in\"\n /** No manifest, no key, or an unreadable one. */\n | \"unset\";\n\nexport function readProjectPolicy(startDir: string = process.cwd()): ProjectTelemetryPolicy {\n try {\n const root = findProjectRoot(startDir);\n if (!root) return \"unset\";\n\n const file = path.join(root, MANIFEST_FILENAME);\n if (!fs.existsSync(file)) return \"unset\";\n\n // A regex rather than JSON.parse: a manifest with a trailing comma or a\n // half-finished edit still parses badly but reads clearly, and \"your\n // JSON is malformed\" is not a reason to start sending data the file\n // plainly asks us not to send.\n const raw = fs.readFileSync(file, \"utf-8\");\n const match = /\"telemetry\"\\s*:\\s*(true|false)/.exec(raw);\n if (!match) return \"unset\";\n return match[1] === \"false\" ? \"opt_out\" : \"ignored_opt_in\";\n } catch {\n return \"unset\";\n }\n}\n","import { ensureMachineId, ensureProjectId, readConfig, writeConfig } from \"./identity\";\nimport { buildEvent, TelemetryEvent, TelemetryEventName } from \"./payload\";\nimport { readProjectPolicy } from \"./project\";\nimport { parseEnvBoolean } from \"@rebasepro/types\";\n\nexport { TELEMETRY_SCHEMA_VERSION, bucket, durationBucket, errorClass, buildEvent, sanitize } from \"./payload\";\nexport type { TelemetryEvent, TelemetryEventName, TelemetryValue } from \"./payload\";\nexport { configPath, readConfig, writeConfig } from \"./identity\";\nexport type { TelemetryConfig } from \"./identity\";\nexport { readProjectPolicy } from \"./project\";\nexport type { ProjectTelemetryPolicy } from \"./project\";\n\n/**\n * Where events go. Overridable so a fork can point at its own collector, and so\n * the tests never touch the network.\n */\nexport const DEFAULT_TELEMETRY_ENDPOINT = \"https://app.rebase.pro/api/functions/telemetry\";\n\nexport function endpoint(): string {\n return process.env.REBASE_TELEMETRY_ENDPOINT?.trim() || DEFAULT_TELEMETRY_ENDPOINT;\n}\n\n/**\n * Set to anything but a spelled no.\n *\n * The conventions behind these variables make presence the signal —\n * `CI=woodpecker` is a CI run, `DO_NOT_TRACK=please` is an answer — so a value\n * the parser does not recognise still counts, and the refusal holds. Only the\n * platform's spellings of no turn one off. It was `!== \"0\"` for two of them and\n * `!== \"false\"` for `CI`, so `CI=0` read as a runner.\n */\nfunction setAndNotNo(value: string | undefined): boolean {\n return value !== undefined && value.trim() !== \"\" && parseEnvBoolean(value) !== false;\n}\n\n/** Why nothing would be sent right now, or `null` when it would. */\nexport type SuppressionReason =\n | \"not_asked\"\n | \"declined\"\n | \"do_not_track\"\n | \"rebase_telemetry_disabled\"\n | \"ci\"\n | \"project_opt_out\";\n\n/**\n * The one function that decides whether anything leaves the machine.\n *\n * Every path is a refusal except the last, which is the point: consent is\n * opt-in, so the default answer at every branch — never asked, unreadable\n * config, a set env var, a CI runner — is no.\n *\n * `DO_NOT_TRACK` is honoured because it is the cross-tool convention\n * (consoledonottrack.com); a user who has set it globally has already answered\n * this question and should not be asked again by us.\n *\n * `CI` is refused for a different reason: a build runner is not a person, and\n * counting one is both useless and misleading. A single pipeline re-running on\n * every push would otherwise outweigh every real developer in the data.\n *\n * A project's own `\"telemetry\": false` is checked *before* the machine setting,\n * because it has to beat an individual opt-in to be worth anything — see\n * project.ts on why the reverse is refused.\n */\nexport function suppressionReason(\n env: NodeJS.ProcessEnv = process.env,\n cwd: string = process.cwd()\n): SuppressionReason | null {\n if (setAndNotNo(env.DO_NOT_TRACK)) return \"do_not_track\";\n if (setAndNotNo(env.REBASE_TELEMETRY_DISABLED)) return \"rebase_telemetry_disabled\";\n if (setAndNotNo(env.CI)) return \"ci\";\n if (readProjectPolicy(cwd) === \"opt_out\") return \"project_opt_out\";\n\n const config = readConfig();\n if (config.enabled === undefined) return \"not_asked\";\n if (!config.enabled) return \"declined\";\n return null;\n}\n\nexport function isEnabled(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): boolean {\n return suppressionReason(env, cwd) === null;\n}\n\n/**\n * Record the user's answer. `false` is final — nothing prompts again.\n *\n * Returns whether the choice could actually be persisted. A read-only or full\n * home directory makes `writeConfig` throw, and the direction of that failure\n * matters enormously: someone turning sharing **off** who sees a stack trace,\n * or worse sees nothing, is left sharing. The caller is expected to say so and\n * point at `REBASE_TELEMETRY_DISABLED`, which needs no disk.\n */\nexport function setConsent(enabled: boolean): boolean {\n const config = readConfig();\n try {\n writeConfig({\n ...config,\n enabled,\n decidedAt: new Date().toISOString(),\n // An id is minted only on acceptance. Generating one at decline\n // time would create the very record the user just refused.\n machineId: enabled ? (config.machineId ?? undefined) : undefined\n });\n if (enabled) ensureMachineId();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Build the event that *would* be sent, without sending it.\n *\n * This is what `rebase telemetry show` prints, and it is deliberately the same\n * function the sender uses — a preview assembled by separate code is a promise\n * that drifts. Returns `null` when nothing would be sent, so the command can\n * say why instead of showing a payload that is never going anywhere.\n */\nexport function previewEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown> = {},\n projectRoot?: string\n): TelemetryEvent | null {\n const config = readConfig();\n if (!config.machineId) return null;\n return buildEvent(event, properties, {\n machineId: config.machineId,\n projectId: projectRoot ? ensureProjectId(projectRoot) : undefined\n });\n}\n\n/**\n * Send one event, or quietly do nothing.\n *\n * Three properties this must hold, in order of importance:\n *\n * 1. **It never throws.** Telemetry failing is not a reason for `rebase dev`\n * to fail. Every error is swallowed.\n * 2. **It never blocks meaningfully.** A two-second ceiling, after which the\n * command carries on regardless — a collector having a bad day must not\n * become the CLI hanging.\n * 3. **It never sends without consent.** Enforced here rather than at the call\n * sites, so a new call site cannot get it wrong.\n */\nexport async function recordEvent(\n event: TelemetryEventName,\n properties: Record<string, unknown> = {},\n options: { projectRoot?: string; timeoutMs?: number } = {}\n): Promise<void> {\n try {\n // The project's own policy is read relative to the project being acted\n // on, not the shell's cwd — `rebase dev` run from a subdirectory must\n // still see its own repository's opt-out.\n if (!isEnabled(process.env, options.projectRoot ?? process.cwd())) return;\n\n const machineId = ensureMachineId();\n const projectId = options.projectRoot ? ensureProjectId(options.projectRoot) : undefined;\n const payload = buildEvent(event, properties, { machineId,\nprojectId });\n\n const abort = new AbortController();\n const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 2000);\n try {\n await fetch(endpoint(), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: abort.signal\n });\n } finally {\n clearTimeout(timer);\n }\n } catch {\n // Deliberately silent. A warning here would be worse than useless: it\n // draws attention to a subsystem the user opted into and cannot act on,\n // in the middle of output they were actually reading.\n }\n}\n"],"mappings":"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"}
|