@rebasepro/cli 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/rebase.js +143 -4
- package/dist/{agent-setup-B5AKHOgt.js → agent-setup-D9mHRZ5R.js} +125 -3
- package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
- package/dist/{api-keys-Tm7zvAPe.js → api-keys-CyJ26KEJ.js} +41 -10
- package/dist/api-keys-CyJ26KEJ.js.map +1 -0
- package/dist/{apps-BsU_ILbP.js → apps-CnLDV5b6.js} +6 -7
- package/dist/{apps-BsU_ILbP.js.map → apps-CnLDV5b6.js.map} +1 -1
- package/dist/{atlas-dev-database-OWtqnG7W.js → atlas-dev-database-BODqdCWt.js} +4 -4
- package/dist/{atlas-dev-database-OWtqnG7W.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
- package/dist/{auth-Vl2hFKXP.js → auth-dII2jCO1.js} +10 -8
- package/dist/auth-dII2jCO1.js.map +1 -0
- package/dist/{branch-pointer-9DnwkFJJ.js → branch-pointer-BQUF6lYa.js} +1 -1
- package/dist/{build-BBgT8EJZ.js → build-Cza7ZQrp.js} +6 -7
- package/dist/{build-BBgT8EJZ.js.map → build-Cza7ZQrp.js.map} +1 -1
- package/dist/{bundle-DtCCCr4w.js → bundle-C6yEtMJ-.js} +53 -5
- package/dist/bundle-C6yEtMJ-.js.map +1 -0
- package/dist/bundle.d.ts +14 -0
- package/dist/{cloud-BpH0_bmV.js → cloud-BdnmqQaZ.js} +8528 -4342
- package/dist/cloud-BdnmqQaZ.js.map +1 -0
- package/dist/collections-dir-DlvDhime.js +69 -0
- package/dist/collections-dir-DlvDhime.js.map +1 -0
- package/dist/commands/agent-setup.d.ts +58 -0
- package/dist/commands/api-keys.d.ts +13 -0
- package/dist/commands/cloud/activity.d.ts +25 -0
- package/dist/commands/cloud/app-key.d.ts +18 -0
- package/dist/commands/cloud/auth.d.ts +1 -0
- package/dist/commands/cloud/billing-invoices.d.ts +39 -0
- package/dist/commands/cloud/claim.d.ts +7 -0
- package/dist/commands/cloud/context.d.ts +115 -6
- package/dist/commands/cloud/deploy-summary.d.ts +162 -0
- package/dist/commands/cloud/deploy.d.ts +64 -0
- package/dist/commands/cloud/env.d.ts +24 -0
- package/dist/commands/cloud/index.d.ts +13 -1
- package/dist/commands/cloud/init.d.ts +143 -0
- package/dist/commands/cloud/login-flow.d.ts +51 -0
- package/dist/commands/cloud/notifications.d.ts +28 -0
- package/dist/commands/cloud/orgs.d.ts +13 -0
- package/dist/commands/cloud/previews.d.ts +123 -0
- package/dist/commands/cloud/resources.d.ts +68 -14
- package/dist/commands/cloud/runtime-logs.d.ts +176 -0
- package/dist/commands/cloud/service-key.d.ts +15 -0
- package/dist/commands/cloud/token-capabilities.d.ts +61 -8
- package/dist/commands/cloud/tokens.d.ts +18 -3
- package/dist/commands/cloud/usage.d.ts +63 -0
- package/dist/commands/dev.d.ts +22 -0
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/generate-view.d.ts +94 -0
- package/dist/commands/generate_sdk.d.ts +19 -2
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/skills.d.ts +4 -3
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/storage.d.ts +4 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/verify.d.ts +96 -0
- package/dist/{consent-ClRHmcxf.js → consent-BD7F29T3.js} +2 -2
- package/dist/{consent-ClRHmcxf.js.map → consent-BD7F29T3.js.map} +1 -1
- package/dist/{console-CEGm0XNc.js → console-BXB6-8GT.js} +12 -6
- package/dist/console-BXB6-8GT.js.map +1 -0
- package/dist/{context-CyflUrKg.js → context-GUxkZm-S.js} +482 -26
- package/dist/context-GUxkZm-S.js.map +1 -0
- package/dist/{daemon-CknkDjd6.js → daemon-Cp7oNXth.js} +160 -5
- package/dist/daemon-Cp7oNXth.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-KzlqddGD.js} +101 -54
- package/dist/db-KzlqddGD.js.map +1 -0
- package/dist/{derive-CsoFGAnK.js → derive-ae9UOoOB.js} +2 -2
- package/dist/{derive-CsoFGAnK.js.map → derive-ae9UOoOB.js.map} +1 -1
- package/dist/{dev-C0pbMmhS.js → dev-DTqVMgB3.js} +231 -44
- package/dist/dev-DTqVMgB3.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-Cxv3TiD1.js} +73 -27
- package/dist/dev-preflight-Cxv3TiD1.js.map +1 -0
- package/dist/{dev-server-B1UYifZJ.js → dev-server-OJsUGZEB.js} +14 -3
- package/dist/dev-server-OJsUGZEB.js.map +1 -0
- package/dist/{doctor-CATRdWm6.js → doctor-BRWOlqeG.js} +14 -12
- package/dist/doctor-BRWOlqeG.js.map +1 -0
- package/dist/{eject-DuasTr-F.js → eject-DABcDnPN.js} +3 -4
- package/dist/{eject-DuasTr-F.js.map → eject-DABcDnPN.js.map} +1 -1
- package/dist/{fold-static-hDHtDfTq.js → fold-static-BnAK-anQ.js} +3 -3
- package/dist/{fold-static-hDHtDfTq.js.map → fold-static-BnAK-anQ.js.map} +1 -1
- package/dist/generate-view-IFwCADvb.js +662 -0
- package/dist/generate-view-IFwCADvb.js.map +1 -0
- package/dist/{generate_sdk-Cvg5jjJF.js → generate_sdk-BvcWvoUh.js} +49 -13
- package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
- package/dist/index.es.js +58 -40
- package/dist/index.es.js.map +1 -1
- package/dist/{init-BWAwHPtz.js → init-CaU6GhwI.js} +50 -62
- package/dist/init-CaU6GhwI.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-DpW5dfoX.js +2 -0
- package/dist/{manifest-DRp0UVTj.js → manifest-BtzDy7KQ.js} +36 -3
- package/dist/{manifest-DRp0UVTj.js.map → manifest-BtzDy7KQ.js.map} +1 -1
- package/dist/{normalize-imports-BoBVh5tU.js → normalize-imports-D0zXlM6g.js} +3 -3
- package/dist/{normalize-imports-BoBVh5tU.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
- package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
- package/dist/path-flags-DNvtEi2-.js.map +1 -0
- package/dist/{prepare-DdnU70z8.js → prepare-CTw9NEkh.js} +5 -4
- package/dist/{prepare-DdnU70z8.js.map → prepare-CTw9NEkh.js.map} +1 -1
- package/dist/project-CasK56vN.js +701 -0
- package/dist/project-CasK56vN.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-D647F1LA.js +2 -0
- package/dist/{resources-BdNRDEAy.js → resources-CPSC5Kcu.js} +4 -5
- package/dist/{resources-BdNRDEAy.js.map → resources-CPSC5Kcu.js.map} +1 -1
- package/dist/{schema-B0-p6T1O.js → schema-BSXwZSiI.js} +9 -8
- package/dist/schema-BSXwZSiI.js.map +1 -0
- package/dist/{skills-DfnTeyRz.js → skills-BU2flMOy.js} +52 -8
- package/dist/skills-BU2flMOy.js.map +1 -0
- package/dist/{start-BmEZkS0n.js → start-mVb6upwY.js} +20 -7
- package/dist/start-mVb6upwY.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/static-syntax-check.d.ts +64 -0
- package/dist/{status-BJ26yZOo.js → status-BREvtzJm.js} +8 -9
- package/dist/{status-BJ26yZOo.js.map → status-BREvtzJm.js.map} +1 -1
- package/dist/{storage-DKPUBTWG.js → storage-DtOXGflp.js} +159 -16
- package/dist/storage-DtOXGflp.js.map +1 -0
- package/dist/{telemetry-EMFRVRQi.js → telemetry-CgwJPVMO.js} +2 -2
- package/dist/{telemetry-EMFRVRQi.js.map → telemetry-CgwJPVMO.js.map} +1 -1
- package/dist/{telemetry-m9czTNGR.js → telemetry-DwK5RQJl.js} +4 -4
- package/dist/{telemetry-m9czTNGR.js.map → telemetry-DwK5RQJl.js.map} +1 -1
- package/dist/{upgrade-DjihWuZL.js → upgrade-B7sVwL8S.js} +26 -4
- package/dist/{upgrade-DjihWuZL.js.map → upgrade-B7sVwL8S.js.map} +1 -1
- package/dist/utils/args.d.ts +32 -3
- package/dist/utils/boot-retry.d.ts +41 -0
- package/dist/utils/collections-dir.d.ts +14 -0
- package/dist/utils/dev-preflight.d.ts +34 -1
- package/dist/utils/dev-server.d.ts +11 -0
- package/dist/utils/path-flags.d.ts +2 -1
- package/dist/utils/project.d.ts +5 -1
- package/dist/utils/tool-stdio.d.ts +1 -1
- package/dist/utils/unknown-command.d.ts +4 -0
- package/dist/verify-D7UsV_qe.js +730 -0
- package/dist/verify-D7UsV_qe.js.map +1 -0
- package/package.json +9 -7
- package/templates/eject/backend/src/index.ts +9 -1
- package/templates/generated-schemas/ecommerce.ts +3 -0
- package/templates/overlays/baas/ai-instructions.md +13 -0
- 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/.claude/hooks/post-edit-collection.sh +99 -0
- package/templates/template/.claude/settings.json +17 -0
- package/templates/template/.env.example +15 -5
- package/templates/template/ai-instructions.md +21 -1
- package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
- package/templates/template/docker-compose.yml +4 -1
- package/templates/template/frontend/package.json +1 -0
- package/templates/template/frontend/src/App.tsx +10 -9
- package/templates/views/AppView.tsx.tmpl +138 -0
- package/templates/views/EntityView.tsx.tmpl +64 -0
- package/templates/views/index.ts.tmpl +8 -0
- package/dist/agent-setup-B5AKHOgt.js.map +0 -1
- package/dist/api-keys-Tm7zvAPe.js.map +0 -1
- package/dist/args-BX0YD14a.js +0 -266
- package/dist/args-BX0YD14a.js.map +0 -1
- package/dist/auth-Vl2hFKXP.js.map +0 -1
- package/dist/bundle-DtCCCr4w.js.map +0 -1
- package/dist/cloud-BpH0_bmV.js.map +0 -1
- package/dist/console-CEGm0XNc.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/path-flags-BFgG9V6y.js.map +0 -1
- 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/schema-B0-p6T1O.js.map +0 -1
- package/dist/skills-DfnTeyRz.js.map +0 -1
- package/dist/start-BmEZkS0n.js.map +0 -1
- package/dist/state-CCB-FAQ9.js.map +0 -1
- package/dist/storage-DKPUBTWG.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-DtOXGflp.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\n/** An orphan scan's record, as `GET /admin/storage/orphans/scans/:id` answers it. */\ninterface OrphanScanState {\n id: string;\n storageId: string;\n runner: \"worker\" | \"in-process\";\n status: \"queued\" | \"running\" | \"completed\" | \"failed\";\n progress: {\n phase: string;\n filesScanned: number;\n rowsScanned: number;\n historyEntriesScanned: number;\n collectionsScanned: number;\n collectionsTotal: number;\n unscanned: { collection: string; reason: string }[];\n };\n result?: Omit<OrphanReport, \"storageId\">;\n error?: string;\n note?: string;\n deletion?: {\n status: \"queued\" | \"running\" | \"completed\" | \"failed\";\n progress: OrphanScanState[\"progress\"] & { candidates: number; deleted: number };\n result?: { deleted: string[]; kept: { key: string; reason: string }[]; failed: { key: string; error: string }[] };\n error?: string;\n note?: string;\n };\n}\n\n/** How often a running scan is asked about. A setting so the tests need not wait. */\nexport const orphanScanPolling = { intervalMs: 1_000 };\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\nconst sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));\n\n/** Whether an error is a backend without the route asked for, as opposed to a source it does not serve. */\nconst isMissingRoute = (error: unknown) => error instanceof RebaseApiError && error.status === 404\n && error.code !== \"UNKNOWN_STORAGE_SOURCE\" && error.code !== \"STORAGE_SCAN_NOT_FOUND\";\n\n/** One line of a running scan's progress, rewritten in place on a terminal. */\nfunction progressLine(text: string, quiet: boolean): void {\n if (!quiet && process.stdout.isTTY) process.stdout.write(`\\r\\x1b[2K ${text}`);\n}\n\nfunction endProgressLine(quiet: boolean): void {\n if (!quiet && process.stdout.isTTY) process.stdout.write(\"\\n\");\n}\n\n/** Ask for a scan's record until `done` says so, printing its progress. */\nasync function pollScan(\n backend: Backend,\n id: string,\n sourceQuery: string,\n done: (state: OrphanScanState) => boolean,\n describe: (state: OrphanScanState) => string,\n quiet: boolean\n): Promise<OrphanScanState> {\n let noted = false;\n for (;;) {\n let state: OrphanScanState;\n try {\n state = await backend.request<OrphanScanState>(\"GET\", `/orphans/scans/${encodeURIComponent(id)}${sourceQuery}`);\n } catch (error: unknown) {\n endProgressLine(quiet);\n fail(\"Failed to read the scan's progress\", error, backend.baseUrl);\n }\n if (done(state)) {\n endProgressLine(quiet);\n return state;\n }\n progressLine(describe(state), quiet);\n const note = state.note ?? state.deletion?.note;\n if (note && !noted && !quiet) {\n noted = true;\n endProgressLine(quiet);\n console.log(chalk.yellow(` ${note}`));\n }\n await sleep(orphanScanPolling.intervalMs);\n }\n}\n\n/**\n * Scan for orphans: started on the backend and asked about until it is done,\n * so a scan longer than any request timeout still finishes. A backend from\n * before scans ran in the background is asked the old way, in one request.\n */\nasync function scanForOrphans(\n backend: Backend,\n query: URLSearchParams,\n sourceQuery: string,\n quiet: boolean\n): Promise<{ report: OrphanReport; scanId?: string }> {\n let started: OrphanScanState;\n try {\n started = await backend.request<OrphanScanState>(\"POST\", `/orphans/scans?${query}`);\n } catch (error: unknown) {\n if (!isMissingRoute(error)) fail(\"Failed to start a storage scan\", error, backend.baseUrl);\n try {\n return { report: await backend.request<OrphanReport>(\"GET\", `/orphans?${query}`) };\n } catch (legacyError: unknown) {\n fail(\"Failed to scan storage\", legacyError, backend.baseUrl);\n }\n }\n if (!quiet && started.runner === \"in-process\") {\n console.log(chalk.gray(\" This backend runs no job queue, so the scan runs inside the server process, in the background. A restart of that process stops it.\"));\n }\n const finished = await pollScan(\n backend,\n started.id,\n sourceQuery,\n state => state.status === \"completed\" || state.status === \"failed\",\n state => state.status === \"queued\"\n ? \"Waiting for the scan to start…\"\n : `Scanning \"${state.storageId}\": ${state.progress.filesScanned} file(s) listed, ${state.progress.rowsScanned} row(s) read, ` +\n `${state.progress.collectionsScanned}/${state.progress.collectionsTotal} collection(s)` +\n (state.progress.unscanned.length > 0 ? `, ${state.progress.unscanned.length} unreadable` : \"\") + \"…\",\n quiet\n );\n if (finished.status === \"failed\" || !finished.result) {\n console.error(chalk.red(`✗ The scan failed: ${finished.error ?? \"no reason was recorded\"}`));\n process.exit(1);\n }\n return { report: { storageId: finished.storageId, ...finished.result }, scanId: finished.id };\n}\n\n/** Delete a completed scan's orphans, in the background, and wait for it. */\nasync function deleteFromScan(\n backend: Backend,\n scanId: string,\n sourceQuery: string,\n quiet: boolean\n): Promise<{ deleted: string[]; failed: { key: string; error: string }[]; kept: { key: string; reason: string }[]; error?: string }> {\n const deleteQuery = new URLSearchParams(sourceQuery.replace(/^\\?/, \"\"));\n deleteQuery.set(\"scan\", scanId);\n try {\n await backend.request(\"DELETE\", `/orphans?${deleteQuery}`);\n } catch (error: unknown) {\n fail(\"Failed to delete unused files\", error, backend.baseUrl);\n }\n const finished = await pollScan(\n backend,\n scanId,\n sourceQuery,\n state => state.deletion?.status === \"completed\" || state.deletion?.status === \"failed\",\n state => {\n const progress = state.deletion?.progress;\n if (!progress || state.deletion?.status === \"queued\") return \"Waiting for the delete to start…\";\n return progress.phase === \"deleting\"\n ? `Deleting: ${progress.deleted} of ${progress.candidates} file(s)…`\n : `Checking every row again before deleting: ${progress.rowsScanned} row(s) read, ` +\n `${progress.collectionsScanned}/${progress.collectionsTotal} collection(s)…`;\n },\n quiet\n );\n const deletion = finished.deletion!;\n return {\n deleted: deletion.result?.deleted ?? [],\n failed: deletion.result?.failed ?? [],\n kept: deletion.result?.kept ?? [],\n ...(deletion.status === \"failed\" ? { error: deletion.error ?? \"no reason was recorded\" } : {})\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 sourceQuery = flags[\"--source\"] ? `?${new URLSearchParams({ storageId: flags[\"--source\"] })}` : \"\";\n const quiet = flags[\"--json\"] === true;\n const backend = connect();\n\n const { report, scanId } = await scanForOrphans(backend, query, sourceQuery, quiet);\n\n if (!flags[\"--delete\"]) {\n if (flags[\"--json\"]) {\n console.log(JSON.stringify({ ...report, ...(scanId ? { scanId } : {}) }, 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 }[]; kept?: { key: string; reason: string }[]; error?: string };\n if (scanId) {\n // The server reads every row again against these files, and asks for\n // each file again right before deleting it: one a row has named since\n // the list was printed, or that was uploaded again, is kept.\n result = await deleteFromScan(backend, scanId, sourceQuery, quiet);\n } else {\n try {\n // A backend from before background scans finds them again, inside the request.\n result = await backend.request(\"DELETE\", `/orphans?${query}`);\n } catch (error: unknown) {\n fail(\"Failed to delete unused files\", error, backend.baseUrl);\n }\n }\n if (flags[\"--json\"]) {\n console.log(JSON.stringify({ storageId: report.storageId, ...result, recent: report.recent }, null, 2));\n } else {\n if (result.error) console.error(chalk.red(` ✗ ${result.error}`));\n console.log(chalk.green(` ✓ Deleted ${result.deleted.length} file(s).`));\n for (const kept of result.kept ?? []) console.log(chalk.yellow(` Kept ${kept.key}: ${kept.reason}`));\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 || result.error) 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 // The source listed again, by a walk of its own: a move reports what its\n // walk saw, and a walk that skipped files once reported success over the\n // half it moved.\n const verified = await listLeftBehind(backend, {\n from,\n to,\n prefix: flags[\"--prefix\"],\n deleteSource: flags[\"--delete-source\"] === true\n }, flags[\"--json\"] === true);\n const reported = new Set(failures.map(failure => failure.key));\n const leftBehind = verified?.filter(entry => !reported.has(entry.key)) ?? [];\n\n if (flags[\"--json\"]) {\n console.log(JSON.stringify({ ...counts, failures, verified: verified !== undefined, notMoved: verified ?? [] }, 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 (verified === undefined) {\n console.log(chalk.gray(\" This backend cannot list the source again (it needs a backend newer than 0.25.1), so the CLI did not check for files the move left behind.\"));\n } else if (leftBehind.length > 0) {\n console.error(chalk.red(` ✗ Listed again, the source holds ${leftBehind.length} more file(s) the move did not report: ${leftBehind.length} file(s) were not moved.`));\n for (const entry of leftBehind) console.error(chalk.red(` ${entry.key}: ${entry.reason}`));\n console.error(chalk.gray(\" Run the same move again to pick them up.\"));\n }\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 || leftBehind.length > 0) process.exitCode = 1;\n}\n\n/**\n * Every file under the moved folder the move did not move, read by the\n * backend's `/move/verify` — or `undefined` from a backend without it.\n */\nasync function listLeftBehind(\n backend: Backend,\n request: { from?: string; to?: string; prefix?: string; deleteSource: boolean },\n quiet: boolean\n): Promise<{ key: string; reason: string }[] | undefined> {\n const notMoved: { key: string; reason: string }[] = [];\n let cursor: string | undefined;\n let checked = 0;\n do {\n let page: { checked: number; notMoved: { key: string; reason: string }[]; cursor?: string };\n try {\n page = await backend.request(\"POST\", \"/move/verify\", { ...request, ...(cursor ? { cursor } : {}), limit: 500 });\n } catch (error: unknown) {\n if (error instanceof RebaseApiError && error.status === 404 && error.code !== \"UNKNOWN_STORAGE_SOURCE\") return undefined;\n fail(\"Failed to list the source again after the move\", error, backend.baseUrl);\n }\n checked += page.checked;\n notMoved.push(...page.notMoved);\n if (!quiet && process.stdout.isTTY) {\n process.stdout.write(`\\r Checking the source again: ${checked} file(s) listed…`);\n }\n cursor = page.cursor;\n } while (cursor);\n return notMoved;\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 The scan runs on the backend in the background — a ${chalk.blue(\"storage.orphans\")} job\n on the job queue, or inside the server process when there is none — and the\n CLI shows its progress until it is done. ${chalk.blue(\"--delete\")} deletes from that scan\n only; the backend reads every row again and asks for each file again first.\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-CasK56vN.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-CgwJPVMO.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-CgwJPVMO.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,6 +1,6 @@
|
|
|
1
|
-
import { a as setConsent, d as configPath, f as readConfig, r as previewEvent, s as readProjectPolicy, t as endpoint } from "./telemetry-
|
|
2
|
-
import {
|
|
3
|
-
import { r as renderPreview, t as describeState } from "./consent-
|
|
1
|
+
import { a as setConsent, d as configPath, f as readConfig, r as previewEvent, s as readProjectPolicy, t as endpoint } from "./telemetry-CgwJPVMO.js";
|
|
2
|
+
import { E as unknownCommand, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
|
|
3
|
+
import { r as renderPreview, t as describeState } from "./consent-BD7F29T3.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
//#region src/commands/telemetry.ts
|
|
6
6
|
/**
|
|
@@ -125,4 +125,4 @@ ${chalk.gray("URLs, hostnames, error messages, stack traces, or exact record cou
|
|
|
125
125
|
//#endregion
|
|
126
126
|
export { TELEMETRY_SUBCOMMANDS, telemetryCommand };
|
|
127
127
|
|
|
128
|
-
//# sourceMappingURL=telemetry-
|
|
128
|
+
//# sourceMappingURL=telemetry-DwK5RQJl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-m9czTNGR.js","names":[],"sources":["../src/commands/telemetry.ts"],"sourcesContent":["import chalk from \"chalk\";\n\nimport { describeState, renderPreview } from \"../telemetry/consent\";\nimport { configPath, endpoint, previewEvent, readConfig, readProjectPolicy, setConsent } from \"../telemetry\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { unknownCommand } from \"../utils/unknown-command\";\n\n/**\n * `rebase telemetry` — the command that makes the rest of it inspectable.\n *\n * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest\n * way to earn it is to stop describing the payload and print it. `show` runs\n * the same builder the sender uses, so what appears here is what would go, not\n * a documentation comment that quietly fell out of date two releases ago.\n */\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const TELEMETRY_SUBCOMMANDS = [\"status\", \"show\", \"enable\", \"disable\"] as const;\n\nexport async function telemetryCommand(rawArgs: string[]): Promise<void> {\n // Checked before parsing, because `--help` is answered by the dispatcher\n // and never by a handler. It used to be checked after a `filter(a =>\n // !a.startsWith(\"-\"))` that stripped every flag off the line, so `rebase\n // telemetry --help` reached `case undefined` and printed the status page.\n if (wantsHelp(rawArgs)) {\n printHelp();\n return;\n }\n\n // Strictly, like every other family. That `filter` was not \"be lenient\": it\n // deleted every `-`-prefixed token before dispatch, so `rebase telemetry\n // --frobnicate` printed the status page and exited 0, and a mistyped\n // `--disable` looked exactly like asking for the status. `parseCommandArgs`\n // is the CLI's one rule for an undeclared flag, and the comment at the top\n // of `utils/args.ts` is why: guessing that an unknown flag was meant as\n // something else is the unsafe branch.\n const { positionals } = parseCommandArgs({\n spec: {},\n rawArgs,\n commandWords: 1,\n command: \"telemetry\",\n maxPositionals: 1\n });\n const subcommand = positionals[0];\n\n switch (subcommand) {\n case \"status\":\n case undefined:\n printStatus();\n return;\n case \"show\":\n printPayload();\n return;\n case \"enable\":\n if (!setConsent(true)) {\n console.error(chalk.red(`Could not write ${configPath()} — sharing was not enabled.`));\n process.exitCode = 1;\n return;\n }\n console.log(chalk.green(\"Anonymous usage sharing enabled.\"));\n console.log(chalk.gray(`Inspect what gets sent with ${chalk.cyan(\"rebase telemetry show\")}.`));\n return;\n case \"disable\":\n if (!setConsent(false)) {\n // The failure direction that matters. Someone turning sharing\n // off must never be left believing it worked — so this is loud,\n // non-zero, and points at the switch that needs no disk.\n console.error(chalk.red(`Could not write ${configPath()} — sharing is still ON.`));\n console.error(chalk.yellow(`Set ${chalk.cyan(\"REBASE_TELEMETRY_DISABLED=1\")} in your environment instead; it needs no file.`));\n process.exitCode = 1;\n return;\n }\n console.log(chalk.gray(\"Anonymous usage sharing disabled. Nothing further will be sent.\"));\n return;\n default:\n // Printing the help and setting exit 1 said nothing about *why*:\n // the page that came back looked like the page `rebase telemetry\n // --help` prints, and the only difference was an exit code nobody\n // reads at a prompt.\n unknownCommand(subcommand, TELEMETRY_SUBCOMMANDS, \"telemetry\");\n }\n}\n\nfunction printStatus(): void {\n console.log(\"\");\n console.log(` Status: ${describeState()}`);\n if (readProjectPolicy() === \"ignored_opt_in\") {\n // Said out loud rather than silently ignored: a maintainer who wrote\n // `\"telemetry\": true` believes they turned something on, and leaving\n // them to believe it is worse than refusing.\n console.log(\"\");\n console.log(chalk.yellow(\" Note: this project's rebase.json sets \\\"telemetry\\\": true, which is ignored.\"));\n console.log(chalk.gray(\" A committed file cannot consent on behalf of everyone who clones it.\"));\n console.log(chalk.gray(` Only \"telemetry\": false is honoured there. Use ${chalk.cyan(\"rebase telemetry enable\")}.`));\n }\n console.log(` Endpoint: ${chalk.gray(endpoint())}`);\n console.log(` Config: ${chalk.gray(configPath())}`);\n console.log(\"\");\n console.log(chalk.gray(` ${chalk.cyan(\"rebase telemetry show\")} prints the exact payload.`));\n console.log(\"\");\n}\n\nfunction printPayload(): void {\n const config = readConfig();\n if (config.enabled !== true) {\n // Deliberately still prints one. The consent prompt points here so the\n // question can stay short, and someone deciding whether to say yes is\n // exactly the person who most needs to see the payload — telling them\n // to enable sharing first, in order to find out what sharing sends,\n // is the wrong way round.\n console.log(\"\");\n console.log(` ${describeState()}`);\n console.log(\"\");\n console.log(chalk.gray(\" Nothing is being sent. This is what WOULD be sent if you said yes:\"));\n console.log(\"\");\n console.log(renderPreview(\"cli.dev\", { first_run: false }).split(\"\\n\").map((l) => \" \" + l).join(\"\\n\"));\n console.log(\"\");\n console.log(chalk.gray(` Start sharing with ${chalk.cyan(\"rebase telemetry enable\")}.`));\n console.log(\"\");\n return;\n }\n\n // A representative event rather than a synthetic one: `cli.dev` is the\n // event that fires most often, so it is the one worth being sure about.\n const event = previewEvent(\"cli.dev\", { first_run: false }, process.cwd());\n console.log(\"\");\n console.log(chalk.gray(\" Sent to \") + chalk.gray(endpoint()) + chalk.gray(\", for example:\"));\n console.log(\"\");\n console.log(JSON.stringify(event, null, 2).split(\"\\n\").map((l) => \" \" + l).join(\"\\n\"));\n console.log(\"\");\n console.log(chalk.gray(\" Both ids are random. Neither is derived from your machine, your\"));\n console.log(chalk.gray(\" hostname, your project name or anything you have typed.\"));\n console.log(\"\");\n}\n\nfunction printHelp(): void {\n console.log(`\n${chalk.bold(\"rebase telemetry\")} — anonymous usage sharing (off until you say yes; ${chalk.cyan(\"rebase init\")} asks)\n\n${chalk.bold(\"Commands\")}\n ${chalk.blue(\"status\")} Whether anything is being shared, and why ${chalk.gray(\"(default)\")}\n ${chalk.blue(\"show\")} Print the exact payload that would be sent\n ${chalk.blue(\"enable\")} Start sharing\n ${chalk.blue(\"disable\")} Stop sharing, permanently\n\n${chalk.bold(\"Project policy\")}\n ${chalk.blue('\"telemetry\": false')} in ${chalk.blue(\"rebase.json\")} disables sharing for everyone who\n clones the repository, overriding each developer's own choice.\n ${chalk.gray('\"telemetry\": true is ignored — a committed file cannot consent for others.')}\n\n${chalk.bold(\"Environment\")}\n ${chalk.blue(\"DO_NOT_TRACK\")} Set to disable, across every tool that honours it\n ${chalk.blue(\"REBASE_TELEMETRY_DISABLED\")} Set to disable Rebase specifically\n ${chalk.blue(\"REBASE_TELEMETRY_ENDPOINT\")} Send elsewhere — your own collector, for instance\n\n${chalk.gray(\"Never shared: project names, paths, collection or table names, database\")}\n${chalk.gray(\"URLs, hostnames, error messages, stack traces, or exact record counts.\")}\n`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"telemetry-DwK5RQJl.js","names":[],"sources":["../src/commands/telemetry.ts"],"sourcesContent":["import chalk from \"chalk\";\n\nimport { describeState, renderPreview } from \"../telemetry/consent\";\nimport { configPath, endpoint, previewEvent, readConfig, readProjectPolicy, setConsent } from \"../telemetry\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { unknownCommand } from \"../utils/unknown-command\";\n\n/**\n * `rebase telemetry` — the command that makes the rest of it inspectable.\n *\n * The whole subsystem asks for trust it cannot otherwise earn, and the cheapest\n * way to earn it is to stop describing the payload and print it. `show` runs\n * the same builder the sender uses, so what appears here is what would go, not\n * a documentation comment that quietly fell out of date two releases ago.\n */\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const TELEMETRY_SUBCOMMANDS = [\"status\", \"show\", \"enable\", \"disable\"] as const;\n\nexport async function telemetryCommand(rawArgs: string[]): Promise<void> {\n // Checked before parsing, because `--help` is answered by the dispatcher\n // and never by a handler. It used to be checked after a `filter(a =>\n // !a.startsWith(\"-\"))` that stripped every flag off the line, so `rebase\n // telemetry --help` reached `case undefined` and printed the status page.\n if (wantsHelp(rawArgs)) {\n printHelp();\n return;\n }\n\n // Strictly, like every other family. That `filter` was not \"be lenient\": it\n // deleted every `-`-prefixed token before dispatch, so `rebase telemetry\n // --frobnicate` printed the status page and exited 0, and a mistyped\n // `--disable` looked exactly like asking for the status. `parseCommandArgs`\n // is the CLI's one rule for an undeclared flag, and the comment at the top\n // of `utils/args.ts` is why: guessing that an unknown flag was meant as\n // something else is the unsafe branch.\n const { positionals } = parseCommandArgs({\n spec: {},\n rawArgs,\n commandWords: 1,\n command: \"telemetry\",\n maxPositionals: 1\n });\n const subcommand = positionals[0];\n\n switch (subcommand) {\n case \"status\":\n case undefined:\n printStatus();\n return;\n case \"show\":\n printPayload();\n return;\n case \"enable\":\n if (!setConsent(true)) {\n console.error(chalk.red(`Could not write ${configPath()} — sharing was not enabled.`));\n process.exitCode = 1;\n return;\n }\n console.log(chalk.green(\"Anonymous usage sharing enabled.\"));\n console.log(chalk.gray(`Inspect what gets sent with ${chalk.cyan(\"rebase telemetry show\")}.`));\n return;\n case \"disable\":\n if (!setConsent(false)) {\n // The failure direction that matters. Someone turning sharing\n // off must never be left believing it worked — so this is loud,\n // non-zero, and points at the switch that needs no disk.\n console.error(chalk.red(`Could not write ${configPath()} — sharing is still ON.`));\n console.error(chalk.yellow(`Set ${chalk.cyan(\"REBASE_TELEMETRY_DISABLED=1\")} in your environment instead; it needs no file.`));\n process.exitCode = 1;\n return;\n }\n console.log(chalk.gray(\"Anonymous usage sharing disabled. Nothing further will be sent.\"));\n return;\n default:\n // Printing the help and setting exit 1 said nothing about *why*:\n // the page that came back looked like the page `rebase telemetry\n // --help` prints, and the only difference was an exit code nobody\n // reads at a prompt.\n unknownCommand(subcommand, TELEMETRY_SUBCOMMANDS, \"telemetry\");\n }\n}\n\nfunction printStatus(): void {\n console.log(\"\");\n console.log(` Status: ${describeState()}`);\n if (readProjectPolicy() === \"ignored_opt_in\") {\n // Said out loud rather than silently ignored: a maintainer who wrote\n // `\"telemetry\": true` believes they turned something on, and leaving\n // them to believe it is worse than refusing.\n console.log(\"\");\n console.log(chalk.yellow(\" Note: this project's rebase.json sets \\\"telemetry\\\": true, which is ignored.\"));\n console.log(chalk.gray(\" A committed file cannot consent on behalf of everyone who clones it.\"));\n console.log(chalk.gray(` Only \"telemetry\": false is honoured there. Use ${chalk.cyan(\"rebase telemetry enable\")}.`));\n }\n console.log(` Endpoint: ${chalk.gray(endpoint())}`);\n console.log(` Config: ${chalk.gray(configPath())}`);\n console.log(\"\");\n console.log(chalk.gray(` ${chalk.cyan(\"rebase telemetry show\")} prints the exact payload.`));\n console.log(\"\");\n}\n\nfunction printPayload(): void {\n const config = readConfig();\n if (config.enabled !== true) {\n // Deliberately still prints one. The consent prompt points here so the\n // question can stay short, and someone deciding whether to say yes is\n // exactly the person who most needs to see the payload — telling them\n // to enable sharing first, in order to find out what sharing sends,\n // is the wrong way round.\n console.log(\"\");\n console.log(` ${describeState()}`);\n console.log(\"\");\n console.log(chalk.gray(\" Nothing is being sent. This is what WOULD be sent if you said yes:\"));\n console.log(\"\");\n console.log(renderPreview(\"cli.dev\", { first_run: false }).split(\"\\n\").map((l) => \" \" + l).join(\"\\n\"));\n console.log(\"\");\n console.log(chalk.gray(` Start sharing with ${chalk.cyan(\"rebase telemetry enable\")}.`));\n console.log(\"\");\n return;\n }\n\n // A representative event rather than a synthetic one: `cli.dev` is the\n // event that fires most often, so it is the one worth being sure about.\n const event = previewEvent(\"cli.dev\", { first_run: false }, process.cwd());\n console.log(\"\");\n console.log(chalk.gray(\" Sent to \") + chalk.gray(endpoint()) + chalk.gray(\", for example:\"));\n console.log(\"\");\n console.log(JSON.stringify(event, null, 2).split(\"\\n\").map((l) => \" \" + l).join(\"\\n\"));\n console.log(\"\");\n console.log(chalk.gray(\" Both ids are random. Neither is derived from your machine, your\"));\n console.log(chalk.gray(\" hostname, your project name or anything you have typed.\"));\n console.log(\"\");\n}\n\nfunction printHelp(): void {\n console.log(`\n${chalk.bold(\"rebase telemetry\")} — anonymous usage sharing (off until you say yes; ${chalk.cyan(\"rebase init\")} asks)\n\n${chalk.bold(\"Commands\")}\n ${chalk.blue(\"status\")} Whether anything is being shared, and why ${chalk.gray(\"(default)\")}\n ${chalk.blue(\"show\")} Print the exact payload that would be sent\n ${chalk.blue(\"enable\")} Start sharing\n ${chalk.blue(\"disable\")} Stop sharing, permanently\n\n${chalk.bold(\"Project policy\")}\n ${chalk.blue('\"telemetry\": false')} in ${chalk.blue(\"rebase.json\")} disables sharing for everyone who\n clones the repository, overriding each developer's own choice.\n ${chalk.gray('\"telemetry\": true is ignored — a committed file cannot consent for others.')}\n\n${chalk.bold(\"Environment\")}\n ${chalk.blue(\"DO_NOT_TRACK\")} Set to disable, across every tool that honours it\n ${chalk.blue(\"REBASE_TELEMETRY_DISABLED\")} Set to disable Rebase specifically\n ${chalk.blue(\"REBASE_TELEMETRY_ENDPOINT\")} Send elsewhere — your own collector, for instance\n\n${chalk.gray(\"Never shared: project names, paths, collection or table names, database\")}\n${chalk.gray(\"URLs, hostnames, error messages, stack traces, or exact record counts.\")}\n`);\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { n as getPMCommands } from "./package-manager-DSEOwTqM.js";
|
|
2
|
-
import { i as failAsJson, p as requireProjectRoot } from "./project-
|
|
3
|
-
import {
|
|
4
|
-
import { o as ManifestError, p as loadManifest } from "./manifest-DRp0UVTj.js";
|
|
2
|
+
import { i as failAsJson, p as requireProjectRoot, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
|
|
3
|
+
import { o as ManifestError, p as loadManifest } from "./manifest-BtzDy7KQ.js";
|
|
5
4
|
import chalk from "chalk";
|
|
6
5
|
import fs from "fs";
|
|
7
6
|
import path from "path";
|
|
@@ -1250,8 +1249,10 @@ async function upgradeCommand(rawArgs, io = defaultIo) {
|
|
|
1250
1249
|
dryRun,
|
|
1251
1250
|
wouldRefuse: dryRun ? downgradeRefusal : null
|
|
1252
1251
|
};
|
|
1252
|
+
const databaseNotice = wrote ? await runningDatabaseNotice(projectRoot, plan.target) : [];
|
|
1253
1253
|
if (json) {
|
|
1254
1254
|
console.log(JSON.stringify(result, null, 2));
|
|
1255
|
+
for (const line of databaseNotice) console.error(line);
|
|
1255
1256
|
return;
|
|
1256
1257
|
}
|
|
1257
1258
|
printOutcome(plan, {
|
|
@@ -1261,6 +1262,27 @@ async function upgradeCommand(rawArgs, io = defaultIo) {
|
|
|
1261
1262
|
installLine,
|
|
1262
1263
|
wouldRefuse: dryRun ? downgradeRefusal : null
|
|
1263
1264
|
});
|
|
1265
|
+
if (databaseNotice.length > 0) {
|
|
1266
|
+
for (const line of databaseNotice) console.log(chalk.yellow(line));
|
|
1267
|
+
console.log("");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* What to say about this project's managed database when an older CLI started
|
|
1272
|
+
* it, or nothing.
|
|
1273
|
+
*
|
|
1274
|
+
* The daemon outlives every command and the database proxy runs inside it, so
|
|
1275
|
+
* moving the pins does not move the proxy: the old one, with whatever the
|
|
1276
|
+
* upgrade fixed in it, kept serving until `rebase db stop` or thirty idle
|
|
1277
|
+
* minutes — and under a running `rebase dev` it never idles. The upgraded CLI
|
|
1278
|
+
* restarts it when nothing is connected (`daemonOutdated`); this says so, and
|
|
1279
|
+
* what to do while something is.
|
|
1280
|
+
*/
|
|
1281
|
+
async function runningDatabaseNotice(projectRoot, target) {
|
|
1282
|
+
const { daemonOutdated, findRunningDaemon } = await import("./daemon-Cp7oNXth.js").then((n) => n.t);
|
|
1283
|
+
const state = await findRunningDaemon(projectRoot);
|
|
1284
|
+
if (!state || !daemonOutdated(state.cliVersion, target)) return [];
|
|
1285
|
+
return [`⚠ This project's development database is still running, started by ${state.cliVersion ? `Rebase CLI ${state.cliVersion}` : "an earlier Rebase CLI"}, and its database proxy is that CLI's until it restarts.`, ` The next command that needs it restarts it on ${target} when nothing is connected to it. While \`rebase dev\` runs, stop it, then run \`rebase db stop\`. Data is kept.`];
|
|
1264
1286
|
}
|
|
1265
1287
|
/** Changes grouped by file, then what was left alone and why, then overrides. */
|
|
1266
1288
|
function printSummary(plan, dryRun) {
|
|
@@ -1337,4 +1359,4 @@ function printOutcome(plan, outcome) {
|
|
|
1337
1359
|
//#endregion
|
|
1338
1360
|
export { upgradeCommand };
|
|
1339
1361
|
|
|
1340
|
-
//# sourceMappingURL=upgrade-
|
|
1362
|
+
//# sourceMappingURL=upgrade-B7sVwL8S.js.map
|