@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":"project-CasK56vN.js","names":[],"sources":["../src/utils/redact.ts","../src/utils/unknown-command.ts","../src/utils/args.ts","../src/utils/project.ts"],"sourcesContent":["/**\n * Connection-string passwords, replaced before a line is printed.\n *\n * A refusal that repeats what was typed repeats a password with it, and these\n * lines land in terminals, CI logs and issues. `user:***@` is the form every\n * other message uses: the driver's `redactConnectionPasswords` in\n * `@rebasepro/server-postgres`, which its entry does not export, and this\n * mirrors. Covers the user-info password and a `password=` parameter.\n */\nexport function redactConnectionPasswords(text: string): string {\n return text\n .replace(/(\\b[a-z][a-z0-9+.-]*:\\/\\/[^\\s/?#@'\"]*):[^\\s/?#'\"]*@/gi, \"$1:***@\")\n .replace(/(\\bpassword\\s*=\\s*)('[^']*'|[^\\s&'\"]+)/gi, \"$1***\");\n}\n","/**\n * What every command family says when it does not recognise a subcommand.\n *\n * Six families each did it differently, and the differences were not stylistic:\n *\n * rebase statsu \"Unknown command: statsu\" + the whole top-level help\n * rebase auth foo \"Unknown auth command: foo\" + the whole auth help\n * rebase apps foo \"Unknown subcommand: foo\" — which command's?\n * rebase skills foo \"Unknown skills subcommand: foo\" + the whole help\n * rebase telemetry x the help, silently, with exit code 1 and no error line\n * rebase cloud debug x both a JSON error *and* the human one, in JSON mode\n *\n * Dumping a screen of help after an error is the wrong shape: the one line that\n * says what happened scrolls off the top, and in a CI log it is buried in\n * forty. None of them offered the correction, which for a typo is the entire\n * answer — `statsu` is one transposition from `status`.\n *\n * So: one line, the near-miss when there is one, and where to read the rest.\n */\nimport chalk from \"chalk\";\n\nimport { redactConnectionPasswords } from \"./redact\";\n\n/**\n * Damerau–Levenshtein distance, capped by the caller.\n *\n * The transposition is the reason this is not plain Levenshtein: `statsu` for\n * `status` and `craete` for `create` are the two most common typing errors\n * there are, and both are distance 2 without it — the same as two unrelated\n * edits, which is too far to suggest anything at all.\n */\nfunction editDistance(a: string, b: string): number {\n const rows = a.length + 1;\n const cols = b.length + 1;\n const d: number[][] = Array.from({ length: rows }, () => new Array<number>(cols).fill(0));\n\n for (let i = 0; i < rows; i++) d[i][0] = i;\n for (let j = 0; j < cols; j++) d[0][j] = j;\n\n for (let i = 1; i < rows; i++) {\n for (let j = 1; j < cols; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n d[i][j] = Math.min(\n d[i - 1][j] + 1,\n d[i][j - 1] + 1,\n d[i - 1][j - 1] + cost\n );\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);\n }\n }\n }\n return d[a.length][b.length];\n}\n\n/**\n * The one command the user probably meant, or nothing.\n *\n * Tight on purpose. A suggestion that is wrong is worse than none: it sends\n * someone to read the help for a command that was never the answer, and on a\n * family like `db` — where `push` and `pull` are one edit apart — a loose\n * threshold would confidently propose the destructive neighbour of what they\n * typed. So: one edit for a short word, two for a long one, and a prefix match\n * always counts (`rebase cloud dep` for `deploy`).\n */\nexport function suggestCommand(typed: string, known: readonly string[]): string | undefined {\n const word = typed.toLowerCase();\n if (!word) return undefined;\n\n // An abbreviation, but only when it names exactly one command. \"res\" is a\n // prefix of both `reset` and `restore`, and picking whichever comes first\n // in the list would be a coin toss dressed up as an answer.\n if (word.length >= 3) {\n const prefixed = known.filter(name => name.toLowerCase().startsWith(word));\n if (prefixed.length === 1) return prefixed[0];\n }\n\n const budget = word.length <= 4 ? 1 : 2;\n let best: { name: string; distance: number } | undefined;\n\n for (const candidate of known) {\n const distance = editDistance(word, candidate.toLowerCase());\n if (distance > budget) continue;\n if (!best || distance < best.distance) best = { name: candidate, distance };\n }\n\n return best?.name;\n}\n\n/**\n * The family command a word names exactly, as `<family> <command>`, or nothing.\n *\n * `rebase deploy` is `rebase cloud deploy`: the word is a whole command one\n * level down. Exact only — a near miss there is a guess about both the family\n * and the command.\n */\nfunction familyCommand(typed: string, families: Readonly<Record<string, readonly string[]>>): string | undefined {\n const word = typed.toLowerCase();\n for (const [family, commands] of Object.entries(families)) {\n const match = commands.find(name => name.toLowerCase() === word);\n if (match) return `${family} ${match}`;\n }\n return undefined;\n}\n\n/**\n * The line itself, without printing it — so callers that must answer in JSON\n * (the cloud family, whose `fail()` writes a machine-readable envelope) use the\n * same words as the ones that print.\n *\n * `family` is empty for the top level: `rebase --help`, not `rebase --help`.\n * `families` are the command families below this level, by name, whose\n * commands a word may be one of.\n *\n * The typed word is repeated with any connection-string password masked: a URL\n * pasted where the command goes (`rebase db \"postgres://u:pw@h/db\"`) is a\n * word like any other here, and this line lands in terminals and CI logs.\n */\nexport function unknownCommandMessage(\n typed: string | undefined,\n known: readonly string[],\n family = \"\",\n families: Readonly<Record<string, readonly string[]>> = {}\n): string {\n const what = family ? `${family} command` : \"command\";\n const suggestion = typed ? suggestCommand(typed, known) ?? familyCommand(typed, families) : undefined;\n const help = family ? `rebase ${family} --help` : \"rebase --help\";\n const shown = redactConnectionPasswords(typed ?? \"\");\n\n return suggestion\n ? `Unknown ${what} \"${shown}\" — did you mean \\`${suggestion}\\`? Run \\`${help}\\` for the rest.`\n : `Unknown ${what} \"${shown}\". Run \\`${help}\\` for the ones there are.`;\n}\n\n/**\n * Print the line to stderr and exit 1.\n *\n * stderr, not stdout: a mistyped command is a failure, and the families with a\n * `--json` mode guarantee that stdout holds only their envelope.\n */\nexport function unknownCommand(\n typed: string | undefined,\n known: readonly string[],\n family = \"\",\n families: Readonly<Record<string, readonly string[]>> = {}\n): never {\n console.error(chalk.red(`✗ ${unknownCommandMessage(typed, known, family, families)}`));\n process.exit(1);\n}\n","/**\n * Argument parsing for the commands that take positional arguments.\n *\n * Every command in the small group used to parse its own line the same way:\n * `arg(spec, { argv: rawArgs.slice(4), permissive: true })`, then read `_[0]`\n * and `_[1]`. Both halves of that are wrong in a way that costs data.\n *\n * - **`permissive: true` turns an unknown flag into a positional.** `arg`\n * pushes an undeclared flag into `_` as a bare token, so `_[1]` is whatever\n * came second on the line, flag or not. `rebase auth reset-password\n * bob@example.com --debug` set Bob's password to the literal `--debug` — and\n * `--debug` is what `bin/rebase.js` prints after *every* failure as the thing\n * to re-run with, so the single most likely next keystroke after a failed\n * reset was the one that reset the account to a two-word string.\n * - **`slice(4)` assumes the command words are at fixed indices.** They are\n * not: a flag before the command shifts everything, so `rebase --debug auth\n * reset-password bob@example.com NewPass1!` read the email as\n * `reset-password` and the password as `bob@example.com`.\n *\n * So: parse the *whole* line — `rawArgs` is `process.argv` — against a spec\n * strictly, with no permissive mode. `arg` then consumes every flag wherever it\n * appears and rejects the ones nobody declared, which leaves `_` holding the\n * command words followed by the real positionals, in order and at a known\n * offset. An unrecognised flag becomes an error naming the command's help,\n * which is the only safe answer: the alternative is guessing that it was meant\n * as a value.\n *\n * `commands/cloud/index.ts` resolves its positionals against its own spec for\n * the same reason; this is that idea for the commands whose positionals are\n * credentials rather than resource names.\n */\nimport arg from \"arg\";\n\nimport { redactConnectionPasswords } from \"./redact\";\nimport { suggestCommand } from \"./unknown-command\";\n\n/**\n * Flags accepted on top of whatever a command declares.\n *\n * `--debug` is read by `bin/rebase.js` off `process.argv` and never by a\n * command, but it has to be *declared* somewhere or strict parsing rejects the\n * exact flag the CLI tells people to add. `--help`/`-h` are answered by each\n * command's dispatcher before any work happens.\n */\nexport const GLOBAL_COMMAND_FLAGS = {\n \"--debug\": Boolean,\n \"--help\": Boolean,\n \"-h\": \"--help\"\n} as const;\n\n/**\n * The declared long flag an undeclared one was probably meant to be: one edit\n * or one swap away (`--form` → `--from`), or an abbreviation of exactly one\n * (`--anon` → `--anonymize`).\n */\nfunction closestDeclaredFlag(typed: string, declared: readonly string[]): string | undefined {\n if (!typed.startsWith(\"--\")) return undefined;\n const near = suggestCommand(typed.slice(2), declared.filter(flag => flag.startsWith(\"--\")).map(flag => flag.slice(2)));\n return near ? `--${near}` : undefined;\n}\n\n/** Past this many, a refusal names the help page rather than listing every flag. */\nconst LISTED_FLAGS_MAX = 8;\n\n/**\n * The sentence an undeclared flag is refused with — the one `db backup`,\n * `db restore` and `db branch` say, so one CLI answers a typo one way:\n *\n * `rebase db pull` has no option --anonymise — did you mean --anonymize?\n * Nothing was run. See `rebase db pull --help`.\n *\n * With nothing close it lists what the command takes, when that list is short\n * enough to read.\n *\n * `typed` is the argument as `arg` refused it, and only its flag part is\n * repeated: a value given inline (`--fro=postgres://u:pw@h/db`) is not. One\n * argument holding a flag and its value, quoted together, is named as that,\n * and what it quotes is password-masked: `\"--fro postgres://u:pw@h/db\"` was\n * refused with the whole URL, password included, in the middle of a garbled\n * sentence.\n *\n * Exported for the commands that refuse a flag outside `parseCommandArgs`.\n */\nexport function unknownOptionMessage(command: string, typed: string, spec: Readonly<Record<string, unknown>>): string {\n const declared = Object.keys({ ...GLOBAL_COMMAND_FLAGS, ...spec });\n const name = typed.split(/[=\\s]/)[0];\n const near = closestDeclaredFlag(name, declared);\n const tail = ` Nothing was run. See \\`rebase ${command} --help\\`.`;\n\n if (/\\s/.test(typed.trim())) {\n const flag = declared.includes(name) ? name : near;\n const how = flag\n ? ` Write them as two arguments, ${flag} <value>, or as ${flag}=<value>.`\n : \" Write a flag and its value as two arguments.\";\n return `\\`rebase ${command}\\` has no option \"${redactConnectionPasswords(typed)}\": ` +\n `that is a flag and a value in one argument, quoted together.${how}${tail}`;\n }\n\n const own = Object.keys(spec).filter(flag => flag.startsWith(\"--\") && !(flag in GLOBAL_COMMAND_FLAGS));\n const takes = near\n ? ` — did you mean ${near}?`\n : own.length === 0\n ? \" — it takes no options.\"\n : own.length <= LISTED_FLAGS_MAX ? ` — it takes ${own.join(\", \")}.` : \".\";\n\n return `\\`rebase ${command}\\` has no option ${name}${takes}${tail}`;\n}\n\n/** What a flag resolves to, through its alias (`\"-y\": \"--yes\"`). */\nfunction handlerOf(spec: Readonly<Record<string, unknown>>, flag: string): unknown {\n const declared = spec[flag];\n return typeof declared === \"string\" ? spec[declared] : declared;\n}\n\n/** A flag that takes no value: on when given, off when left out. */\nfunction isSwitch(handler: unknown): boolean {\n return handler === Boolean || (Array.isArray(handler) && handler[0] === Boolean);\n}\n\n/** The values a switch reads as on, and as off, after its `=`. */\nconst SWITCH_ON = /^(?:true|1)$/i;\nconst SWITCH_OFF = /^(?:false|0)$/i;\n\n/**\n * A line with every `--switch=value` resolved: `=true` or `=1` is the switch\n * alone, `=false` or `=0` drops it, and any other value is refused.\n *\n * `arg` drops the value of a flag that takes none and turns the flag on, so\n * `--anonymize=false` anonymized and `--yes=false` answered yes. `=true` kept\n * working by that same accident, and scripts write it, so the two readings a\n * value can have are honoured and nothing else is guessed at. Past `--` every\n * word is a positional, so nothing there is read.\n *\n * @throws UsageError naming the switch and the values it takes.\n */\nexport function resolveSwitchValues(\n spec: Readonly<Record<string, unknown>>,\n argv: readonly string[],\n command: string\n): string[] {\n const resolved: string[] = [];\n for (let index = 0; index < argv.length; index += 1) {\n const word = argv[index];\n if (word === \"--\") {\n resolved.push(...argv.slice(index));\n break;\n }\n const flag = word.startsWith(\"--\") && word.includes(\"=\") ? word.slice(0, word.indexOf(\"=\")) : null;\n if (flag === null || !isSwitch(handlerOf(spec, flag))) {\n resolved.push(word);\n continue;\n }\n const value = word.slice(flag.length + 1);\n if (SWITCH_ON.test(value)) resolved.push(flag);\n else if (!SWITCH_OFF.test(value)) throw new UsageError(switchValueMessage(command, flag, word));\n }\n return resolved;\n}\n\n/**\n * Whether a switch is on in a line read by hand: given alone, or as `=true`\n * or `=1`, before any `--`. The reading {@link resolveSwitchValues} gives the\n * parsed flags, for the code that looks at `rawArgs` itself.\n */\nexport function switchOn(rawArgs: readonly string[], flag: string): boolean {\n for (const word of rawArgs) {\n if (word === \"--\") return false;\n if (word === flag) return true;\n if (word.startsWith(`${flag}=`) && SWITCH_ON.test(word.slice(flag.length + 1))) return true;\n }\n return false;\n}\n\n/** The sentence a switch given a value it cannot read is refused with. */\nexport function switchValueMessage(command: string, flag: string, typed: string): string {\n return `\\`rebase ${command}\\` cannot read ${redactConnectionPasswords(typed)}: ${flag} is a switch, ` +\n `and after its = it takes true or 1 to turn it on, false or 0 to turn it off. ` +\n `Nothing was run. See \\`rebase ${command} --help\\`.`;\n}\n\nexport interface ParsedCommand<S extends arg.Spec> {\n /** The declared flags, as `arg` resolved them. */\n flags: arg.Result<S>;\n /** What is left after the command words — never a flag. */\n positionals: string[];\n /** `--help` or `-h` appeared anywhere on the line. */\n help: boolean;\n}\n\n/** Did the line ask for help? Answered before dispatch, never by a handler. */\nexport function wantsHelp(rawArgs: string[]): boolean {\n return rawArgs.includes(\"--help\") || rawArgs.includes(\"-h\");\n}\n\n/**\n * The command line was wrong — nothing ran, and there is no stack to want.\n *\n * `bin/rebase.js` ends every failure with \"Re-run with --debug for the stack\n * trace.\", which is right when something broke inside a command and absurd for\n * a typo: the stack points at `arg` and this file, neither of which the person\n * who mistyped `--ouput` has any interest in. Worse, `--debug` is a flag, so\n * the suggested next command is *another* command line — and the one thing we\n * have just established is that the command line is what went wrong.\n *\n * Marked with a property rather than left to `instanceof`, because the reader\n * is `bin/rebase.js`: it imports the bundle, so the class object it would test\n * against is not the one this file compiled to.\n */\nexport class UsageError extends Error {\n readonly isUsageError = true;\n\n constructor(message: string) {\n super(message);\n this.name = \"UsageError\";\n }\n}\n\n/**\n * Resolve a command's flags and positionals from the full `process.argv`.\n *\n * `commandWords` is how many words name the command itself — 2 for\n * `auth reset-password`, 1 for `start` — and is applied to the *parsed*\n * positionals rather than to `argv`, so a flag placed before the command no\n * longer shifts them.\n *\n * `command` names the command in error messages, e.g. `auth reset-password`.\n *\n * Throws on an unknown flag, on a switch given a value other than true,\n * false, 1 or 0 (`--yes=maybe`), on a positional that looks like a flag, and\n * on more positionals than the command takes. `bin/rebase.js` turns each into\n * a one-line `✗ …` and exit 1.\n */\nexport function parseCommandArgs<S extends arg.Spec>({\n spec,\n rawArgs,\n commandWords,\n command,\n maxPositionals\n}: {\n spec: S;\n rawArgs: string[];\n commandWords: number;\n command: string;\n maxPositionals?: number;\n}): ParsedCommand<S> {\n let parsed: arg.Result<S> & { \"--help\"?: boolean };\n const merged = {\n ...GLOBAL_COMMAND_FLAGS,\n ...spec\n };\n const argv = resolveSwitchValues(merged, rawArgs.slice(2), command);\n try {\n parsed = arg(merged, { argv }) as arg.Result<S> & { \"--help\"?: boolean };\n } catch (err) {\n if (err instanceof Error && (err as arg.ArgError).code === \"ARG_UNKNOWN_OPTION\") {\n // arg's wording: `unknown or unexpected option: --form`, naming\n // the argument up to its first `=`, past a space in it too. The\n // word it came from is what was typed.\n const named = /option: ([\\s\\S]+)$/.exec(err.message)?.[1] ?? \"\";\n const typed = rawArgs.slice(2).find(word => word === named || word.startsWith(`${named}=`)) ?? named;\n throw new UsageError(unknownOptionMessage(command, typed, spec));\n }\n throw err;\n }\n\n const positionals = parsed._.slice(commandWords);\n\n for (const value of positionals) {\n // Only reachable past a `--` separator or for a bare `-`, since anything\n // else beginning with `-` was either declared or already rejected above.\n // Still refused rather than accepted: a value that looks like a flag is\n // as likely to be a typo as an argument, and one of these positions is a\n // password.\n if (value.startsWith(\"-\")) {\n throw new UsageError(\n `\\`${redactConnectionPasswords(value)}\\` looks like an option, not a value — pass it with an explicit flag ` +\n `(\\`rebase ${command} --help\\`).`\n );\n }\n }\n\n if (maxPositionals !== undefined && positionals.length > maxPositionals) {\n // Redacted: a connection string passed where a flag's value belongs\n // (`rebase db pull \"$PROD\"`) is still a password.\n throw new UsageError(\n `rebase ${command} takes ${maxPositionals} argument${maxPositionals === 1 ? \"\" : \"s\"}, ` +\n `got ${positionals.length}: ${redactConnectionPasswords(positionals.join(\" \"))}`\n );\n }\n\n return {\n flags: parsed,\n positionals,\n help: Boolean(parsed[\"--help\"])\n };\n}\n","/**\n * Project discovery utilities for the Rebase CLI.\n *\n * These helpers locate the project root, backend directory, .env file,\n * and local binaries — used by all CLI command modules.\n */\nimport fs from \"fs\";\nimport { createRequire } from \"module\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { execSync } from \"child_process\";\nimport dotenv from \"dotenv\";\nimport chalk from \"chalk\";\nimport { detectPackageManager, getPMCommands } from \"./package-manager\";\nimport { switchOn } from \"./args\";\n\n/** The authored project manifest. Its presence alone marks a project root. */\nexport const MANIFEST_FILENAME = \"rebase.json\";\n\n/**\n * Walk up from `startDir` to find the Rebase project root.\n *\n * A directory is the root when it holds a `rebase.json`, or when it holds a\n * `package.json` that either lists `backend` as a workspace or sits beside both\n * `backend/` and `config/`.\n *\n * `rebase.json` is checked first and needs no `package.json` beside it, because\n * the conventions below all describe a repository that *contains the backend*.\n * A repository holding only a frontend — the normal shape once a project's apps\n * live in separate repositories — matches none of them, so without this the\n * tooling could not run there at all.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): string | null {\n let dir = path.resolve(startDir);\n const root = path.parse(dir).root;\n\n while (dir !== root) {\n if (fs.existsSync(path.join(dir, MANIFEST_FILENAME))) {\n return dir;\n }\n\n const pkgPath = path.join(dir, \"package.json\");\n\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n // Check for workspace-based project (monorepo root)\n if (pkg.workspaces && Array.isArray(pkg.workspaces)) {\n const hasBackend = pkg.workspaces.some((w: string) =>\n w === \"backend\"\n );\n if (hasBackend) return dir;\n }\n } catch {\n // ignore parse errors\n }\n\n // Check for sibling backend directory\n if (fs.existsSync(path.join(dir, \"backend\")) && fs.existsSync(path.join(dir, \"config\"))) {\n return dir;\n }\n }\n\n dir = path.dirname(dir);\n }\n\n return null;\n}\n\n/**\n * Locate the backend directory within the project root.\n */\nexport function findBackendDir(projectRoot: string): string | null {\n const backendDir = path.join(projectRoot, \"backend\");\n return fs.existsSync(backendDir) ? backendDir : null;\n}\n\n/**\n * Detect the active backend plugin (e.g. @rebasepro/server-postgres) from the backend's package.json.\n */\nexport function getActiveBackendPlugin(backendDir: string): string | null {\n const pkgPath = path.join(backendDir, \"package.json\");\n if (!fs.existsSync(pkgPath)) return null;\n\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n const deps = { ...pkg.dependencies,\n...pkg.devDependencies };\n\n // Collect all @rebasepro/server-* driver plugins (exclude server itself)\n const candidates = Object.keys(deps).filter(\n dep => dep.startsWith(\"@rebasepro/server-\") && dep !== \"@rebasepro/server\"\n );\n\n if (candidates.length === 0) return null;\n\n // Prefer server-postgres — it's the primary supported driver\n if (candidates.includes(\"@rebasepro/server-postgres\")) {\n return \"@rebasepro/server-postgres\";\n }\n\n // Fallback: return the first candidate that actually has a CLI entry point\n for (const candidate of candidates) {\n if (resolvePluginCliScript(backendDir, candidate)) {\n return candidate;\n }\n }\n\n // Last resort: return whatever we found\n return candidates[0];\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\n/**\n * Resolve the active plugin's CLI script.\n */\nexport function resolvePluginCliScript(backendDir: string, pluginName: string): string | null {\n const candidates: string[] = [];\n\n // Walk up from the backend dir: pnpm links the plugin into\n // backend/node_modules, while npm workspaces hoist it to the project (or an\n // enclosing monorepo) root.\n let dir = path.resolve(backendDir);\n const fsRoot = path.parse(dir).root;\n while (dir !== fsRoot) {\n candidates.push(\n path.join(dir, \"node_modules\", pluginName, \"src\", \"cli.ts\"),\n path.join(dir, \"node_modules\", pluginName, \"dist\", \"cli.js\")\n );\n dir = path.dirname(dir);\n }\n\n candidates.push(\n // For monorepo dev mode:\n path.resolve(backendDir, \"..\", \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\"),\n path.resolve(backendDir, \"..\", \"packages\", pluginName.replace(\"@rebasepro/\", \"\"), \"src\", \"cli.ts\")\n );\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * Locate the frontend directory within the project root.\n */\nexport function findFrontendDir(projectRoot: string): string | null {\n const frontendDir = path.join(projectRoot, \"frontend\");\n return fs.existsSync(frontendDir) ? frontendDir : null;\n}\n\n/**\n * Find the .env file. Checks the project root first, then backend.\n */\nexport function findEnvFile(projectRoot: string): string | null {\n const candidates = [\n path.join(projectRoot, \".env\"),\n path.join(projectRoot, \"backend\", \".env\")\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n return null;\n}\n\n/**\n * Read the project's `.env` into a plain object.\n *\n * One reader, because there were four: `dotenv` in `start`, a hand-rolled\n * `indexOf(\"=\")` loop in `api-keys`, a single-key regex in `auth`, and its own\n * splitting in `cloud env`. `dotenv` is a declared dependency of this package,\n * so the other three existed for no reason and disagreed with the correct one\n * on the two things people actually write in a `.env`:\n *\n * - `export KEY=value`, which the hand-rolled parser keyed as\n * `export KEY` — so the command reported the key as unset while it was\n * right there in the file;\n * - `KEY=value # comment`, whose comment travelled into the value and then\n * into an `Authorization` header, coming back as a 401 with nothing\n * pointing at the cause.\n *\n * Returns `{}` when the project has no `.env`, so callers can treat \"absent\"\n * and \"empty\" alike.\n */\nexport function readEnvFile(projectRoot: string): Record<string, string> {\n const envFile = findEnvFile(projectRoot);\n if (!envFile || !fs.existsSync(envFile)) return {};\n try {\n return dotenv.parse(fs.readFileSync(envFile, \"utf-8\"));\n } catch {\n // An unreadable or malformed file is not worth failing a command over;\n // the caller reports the missing value it was looking for.\n return {};\n }\n}\n\n/**\n * Resolve a binary from the project's node_modules/.bin.\n * Checks backend, root, parent monorepo root, then falls back to PATH.\n */\nexport function resolveLocalBin(projectRoot: string, binName: string): string | null {\n const candidates = [\n path.join(projectRoot, \"backend\", \"node_modules\", \".bin\", binName),\n path.join(projectRoot, \"node_modules\", \".bin\", binName)\n ];\n\n // Also check parent directories (for monorepo setups where app/ is nested)\n let parent = path.dirname(projectRoot);\n const rootDir = path.parse(parent).root;\n while (parent !== rootDir) {\n candidates.push(path.join(parent, \"node_modules\", \".bin\", binName));\n parent = path.dirname(parent);\n }\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n\n // Fall back to globally installed binary via which\n try {\n const globalPath = execSync(`which ${binName}`, { encoding: \"utf-8\" }).trim();\n if (globalPath && fs.existsSync(globalPath)) return globalPath;\n } catch {\n // not found globally\n }\n\n return null;\n}\n\n/**\n * Resolve the tsx binary. Checks backend node_modules first, then root.\n */\nexport function resolveTsx(projectRoot: string): string | null {\n return resolveLocalBin(projectRoot, \"tsx\");\n}\n\n/**\n * tsx's loader, as the URL `node --import` takes, or null when none resolves.\n *\n * For running a TypeScript entry in one Node process. The `tsx` binary is a\n * relay — pnpm's `.bin/tsx` shell shim, then tsx's CLI in one Node, which\n * spawns a second Node with this same loader to run the script — so spawning\n * through it costs a shell and a Node start (~50–100 ms) on every hop, and\n * `rebase dev` pays a hop on every start and every collection save.\n *\n * Resolved the way Node resolves the package, from the project first — the\n * backend, where a scaffold declares tsx, then the root and everything above it\n * — so the tsx used is the one {@link resolveTsx} would have found; then from\n * this package, as the repository's own tests resolve `tsx/cli`. A URL rather\n * than a path: a Windows path is not a valid `--import` specifier.\n */\nexport function resolveTsxLoader(projectRoot: string): string | null {\n const bases = [\n path.join(projectRoot, \"backend\", \"noop.js\"),\n path.join(projectRoot, \"noop.js\"),\n import.meta.url\n ];\n for (const base of bases) {\n try {\n return pathToFileURL(createRequire(base).resolve(\"tsx\")).href;\n } catch {\n // Not resolvable from here; try the next base.\n }\n }\n return null;\n}\n\n/**\n * Validate that a resolved tsx binary actually has an intact installation.\n *\n * `resolveLocalBin` only checks whether `node_modules/.bin/tsx` (a symlink)\n * exists. If the pnpm content-addressable store was cleaned or a previous\n * install was interrupted, the symlink can exist while critical files inside\n * the tsx package (e.g. `dist/preflight.cjs`) are missing — causing a\n * confusing MODULE_NOT_FOUND error at runtime.\n *\n * This function follows the symlink, walks up to find the tsx package root\n * (`package.json` with `name: \"tsx\"`), and verifies that `dist/preflight.cjs`\n * is present. Returns `null` when the installation looks healthy, or an\n * error description string when it appears corrupted.\n */\nexport function validateTsxInstallation(tsxBinPath: string): string | null {\n try {\n // Follow the symlink chain to the real tsx entry script\n const realPath = fs.realpathSync(tsxBinPath);\n\n // Walk up from the real binary to locate the tsx package root\n let dir = path.dirname(realPath);\n const fsRoot = path.parse(dir).root;\n for (let depth = 0; depth < 10 && dir !== fsRoot; depth++) {\n const pkgPath = path.join(dir, \"package.json\");\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n if (pkg.name === \"tsx\") {\n // Found the tsx package root — verify critical preload file\n const preflightPath = path.join(dir, \"dist\", \"preflight.cjs\");\n if (!fs.existsSync(preflightPath)) {\n return `tsx package at ${dir} is missing dist/preflight.cjs`;\n }\n return null; // Installation looks healthy\n }\n } catch {\n // Malformed package.json — keep walking\n }\n }\n dir = path.dirname(dir);\n }\n\n // Could not determine tsx root — don't block, assume valid\n return null;\n } catch (err) {\n // realpathSync throws if the symlink target is completely gone\n return `tsx binary symlink is broken: ${err instanceof Error ? err.message : String(err)}`;\n }\n}\n\n/**\n * The one sentence a command says when the project's dependencies are missing.\n *\n * Six commands each had their own wording for the same state, and none of them\n * named the remedy:\n *\n * ✗ Could not find CLI entry point for @rebasepro/server-postgres.\n * ✗ Could not find tsx binary.\n * ✗ Could not find tsx binary for backend.\n *\n * All three mean \"you have not installed yet\" — `getActiveBackendPlugin` has\n * already read the driver out of `backend/package.json` by the time the first\n * one fires, so the package is declared and simply not on disk, and `tsx` is a\n * devDependency of every scaffold. But they read as *Rebase* being broken, and\n * they name an internal path or a binary the developer never asked for rather\n * than the command that fixes it. That is the whole of the failure someone sees\n * on a fresh clone, where `node_modules/` is the one thing a checkout does not\n * carry.\n *\n * So: one sentence, naming the package manager this project uses and the\n * directory to run it in — the working directory is usually neither.\n */\nexport function dependenciesNotInstalled(projectRoot: string): string {\n const install = getPMCommands(detectPackageManager(projectRoot)).install.join(\" \");\n return `Dependencies are not installed — run \\`${install}\\` in ${projectRoot}`;\n}\n\n/**\n * Print {@link dependenciesNotInstalled} and exit 1.\n *\n * For the five commands that report and stop. `db.ts` throws instead, because\n * its caller adds the `✗` and the exit itself.\n */\nexport function exitDependenciesNotInstalled(projectRoot: string): never {\n console.error(chalk.red(`✗ ${dependenciesNotInstalled(projectRoot)}`));\n process.exit(1);\n}\n\n/** One problem inside a refusal, when the command found several. */\nexport interface JsonFailureIssue {\n /** Where it is — a config path, a manifest key, a file. */\n path?: string;\n message: string;\n}\n\n/**\n * The refusal envelope, for a caller that asked for JSON.\n *\n * Same shape as the cloud family's `fail()` — `{ error: { message, code, hint } }`\n * — because there is one CLI and a caller should not have to know which half of\n * it answered. `code` is what a caller branches on and is never absent; the\n * cloud family's reasoning about that applies here unchanged. `issues` carries\n * the per-problem detail a human reader gets as a bullet list, because a\n * refusal that says \"2 problem(s)\" and does not say which two is not\n * machine-readable in any useful sense.\n *\n * On **stdout**, like every other `--json` result: the contract those commands\n * make is that stdout holds one JSON value, and a caller that pipes stdout to a\n * parser must get a parseable refusal rather than an empty stream and a\n * human sentence it never sees. That contract used to hold for exactly one\n * failure of each command — the one that goes through `requireProjectRoot` —\n * while every other exit of the *same* command wrote human text to stderr and\n * left stdout empty. So `rebase status --json` was parseable outside a project\n * and unparseable inside a broken one, which is the case a caller actually has\n * to handle.\n *\n * The flag is read off `process.argv` because the failure happens inside a\n * helper the command calls before it has parsed anything. Coarse — a literal\n * `--json` as some other flag's value would count — and worth it: the failure\n * mode of being coarse is a JSON error where a human one was wanted, and the\n * failure mode of not doing it is an unparseable stream.\n */\nexport function failAsJson(\n message: string,\n code: string,\n hint?: string,\n issues?: JsonFailureIssue[]\n): never {\n const error: Record<string, unknown> = { message, code };\n if (hint !== undefined) error.hint = hint;\n if (issues !== undefined) error.issues = issues;\n console.log(JSON.stringify({ error }, null, 2));\n process.exit(1);\n}\n\n/**\n * Did the command line ask for machine-readable output? Read as the parser\n * reads a switch: `--json`, `--json=true` and `--json=1` ask for it,\n * `--json=false` and `--json=0` do not.\n */\nexport function wantsJsonOutput(argv: readonly string[] = process.argv): boolean {\n return switchOn(argv, \"--json\");\n}\n\n/**\n * Require the project root or exit with a helpful error.\n */\nexport function requireProjectRoot(): string {\n const root = findProjectRoot();\n if (!root) {\n if (wantsJsonOutput()) {\n // `rebase status --json` outside a project wrote four grey lines to\n // stderr and nothing at all to stdout, so the agent or CI step that\n // asked for JSON got an empty parse and no reason.\n failAsJson(\n \"Could not find a Rebase project root.\",\n \"no_project_root\",\n `Looked in this directory and every parent for a ${MANIFEST_FILENAME}, `\n + \"a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"\n );\n }\n // Name what is actually looked for, in the order `findProjectRoot`\n // looks for it. The old wording (\"backend/, frontend/, and config/\")\n // described neither the manifest — which is the primary marker and the\n // only one `rebase init` writes — nor a `--headless` project, which has\n // no frontend at all and would read this as \"you are in the wrong\n // place\" while standing in the right one.\n console.error(chalk.red(\"✗ Could not find a Rebase project root.\"));\n console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));\n console.error(chalk.gray(\" a package.json with a \\\"backend\\\" workspace, or a backend/ next to a config/.\"));\n console.error(chalk.gray(\" Run this from inside a project, or create one with `rebase init`.\"));\n process.exit(1);\n }\n return root;\n}\n\n/**\n * Require the backend directory or exit with a helpful error.\n */\nexport function requireBackendDir(projectRoot: string): string {\n const backendDir = findBackendDir(projectRoot);\n if (!backendDir) {\n if (wantsJsonOutput()) {\n failAsJson(\n \"Could not find a backend/ directory.\",\n \"no_backend_dir\",\n `Expected at: ${path.join(projectRoot, \"backend\")}`\n );\n }\n console.error(chalk.red(\"✗ Could not find a backend/ directory.\"));\n console.error(chalk.gray(` Expected at: ${path.join(projectRoot, \"backend\")}`));\n process.exit(1);\n }\n return backendDir;\n}\n"],"mappings":"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"}
|
|
@@ -2,6 +2,80 @@ import { r as PGLITE_EXTENSIONS } from "./constraints-CtiylsGf.js";
|
|
|
2
2
|
import { i as hostOf, n as connectionErrorText, t as banner } from "./connection-banner-DbLJzWf4.js";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { execa } from "execa";
|
|
5
|
+
//#region src/dev-db/pull.ts
|
|
6
|
+
/**
|
|
7
|
+
* `rebase db pull` — copy a database's contents into local development.
|
|
8
|
+
*
|
|
9
|
+
* The common case is production into local, and the reason it exists is that
|
|
10
|
+
* the alternative is worse: without it people hand-roll a `pg_dump | psql` and
|
|
11
|
+
* get the flags wrong in ways that either fail loudly at 2am or, more often,
|
|
12
|
+
* quietly restore half a schema.
|
|
13
|
+
*
|
|
14
|
+
* Three things this command insists on, because copying a production database
|
|
15
|
+
* onto a laptop is a data-protection event whether or not anyone calls it one:
|
|
16
|
+
*
|
|
17
|
+
* 1. **It says what it is about to do, in full, before doing it** — which
|
|
18
|
+
* database it will read, which one it will overwrite, and where the data will
|
|
19
|
+
* come to rest on disk. The target path matters: people forget that
|
|
20
|
+
* `.rebase/pgdata` is a directory their backup software may be indexing.
|
|
21
|
+
*
|
|
22
|
+
* 2. **It refuses to run unattended without being told to.** The target is
|
|
23
|
+
* destroyed, so a mistyped `--from` with no confirmation would take the
|
|
24
|
+
* developer's working database with it.
|
|
25
|
+
*
|
|
26
|
+
* 3. **It will not write to a remote database.** The target is always the local
|
|
27
|
+
* development database; there is no flag that makes this push. A tool that
|
|
28
|
+
* can copy in both directions eventually copies in the wrong one.
|
|
29
|
+
*
|
|
30
|
+
* Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not
|
|
31
|
+
* an obviously safe one — the flag nobody types is the flag nobody gets. It is
|
|
32
|
+
* a best-effort pass over columns whose *names* look like personal data, and
|
|
33
|
+
* {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in
|
|
34
|
+
* a column called `notes`, and this file says so rather than implying a
|
|
35
|
+
* guarantee it cannot keep.
|
|
36
|
+
*
|
|
37
|
+
* What it does guarantee is that it never half-happens: {@link anonymizeCopy}
|
|
38
|
+
* runs the pass in one transaction, and when the pass fails it empties the
|
|
39
|
+
* tables it would have rewritten rather than leave their real values behind.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Tables whose rows are people, by name: `rebase.users`, the blog scaffold's
|
|
43
|
+
* `authors`, and the usual spellings of a project's own. Singular or plural.
|
|
44
|
+
*/
|
|
45
|
+
var PERSON_TABLES = /^(users?|user_profiles?|profiles?|people|persons?|members?|customers?|contacts?|employees?|authors?)$/i;
|
|
46
|
+
/** An email address column, by name: the email rule's own pattern. */
|
|
47
|
+
var EMAIL_COLUMN = /^(.*_)?e?mail(_.*)?$/i;
|
|
48
|
+
/** The column types a replacement string fits. */
|
|
49
|
+
var TEXTUAL_TYPES = /* @__PURE__ */ new Set([
|
|
50
|
+
"text",
|
|
51
|
+
"character varying",
|
|
52
|
+
"varchar",
|
|
53
|
+
"character",
|
|
54
|
+
"char",
|
|
55
|
+
"citext"
|
|
56
|
+
]);
|
|
57
|
+
function isTextual(column) {
|
|
58
|
+
return TEXTUAL_TYPES.has(column.dataType.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
/** `"schema"."table"`, the key a column's table is known by. */
|
|
61
|
+
function tableKey(column) {
|
|
62
|
+
return `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The tables whose rows are people: those {@link PERSON_TABLES} names, and any
|
|
66
|
+
* that holds an email address column.
|
|
67
|
+
*
|
|
68
|
+
* The second is what reaches the tables no list of names could: a row with an
|
|
69
|
+
* address of its own is a person's, so its `name` is one too. The blog
|
|
70
|
+
* scaffold's `authors` (`name`, `email`, `picture`) arrived with every name
|
|
71
|
+
* as it was. A table of organisations with a contact address gets its `name`
|
|
72
|
+
* redacted too, which costs a local copy little.
|
|
73
|
+
*/
|
|
74
|
+
function peopleTables(columns) {
|
|
75
|
+
const people = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const column of columns) if (PERSON_TABLES.test(column.table) || EMAIL_COLUMN.test(column.column) && isTextual(column)) people.add(tableKey(column));
|
|
77
|
+
return people;
|
|
78
|
+
}
|
|
5
79
|
/**
|
|
6
80
|
* Column-name patterns the anonymizer overwrites.
|
|
7
81
|
*
|
|
@@ -11,7 +85,7 @@ import { execa } from "execa";
|
|
|
11
85
|
*/
|
|
12
86
|
var ANONYMIZE_PATTERNS = [
|
|
13
87
|
{
|
|
14
|
-
pattern:
|
|
88
|
+
pattern: EMAIL_COLUMN,
|
|
15
89
|
value: null,
|
|
16
90
|
prefix: "user-",
|
|
17
91
|
suffix: "@example.invalid"
|
|
@@ -30,7 +104,7 @@ var ANONYMIZE_PATTERNS = [
|
|
|
30
104
|
},
|
|
31
105
|
{
|
|
32
106
|
pattern: /^name$/i,
|
|
33
|
-
|
|
107
|
+
peopleOnly: true,
|
|
34
108
|
value: "Redacted",
|
|
35
109
|
prefix: "Redacted-",
|
|
36
110
|
suffix: ""
|
|
@@ -60,12 +134,9 @@ var ANONYMIZE_PATTERNS = [
|
|
|
60
134
|
suffix: ""
|
|
61
135
|
}
|
|
62
136
|
];
|
|
63
|
-
/** The rule that overwrites this column, if any.
|
|
64
|
-
function ruleFor(columnName,
|
|
65
|
-
return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName) && (!rule.
|
|
66
|
-
}
|
|
67
|
-
function shouldAnonymize(columnName, table) {
|
|
68
|
-
return ruleFor(columnName, table) !== void 0;
|
|
137
|
+
/** The rule that overwrites this column, if any. A people-only rule applies only on a table of people. */
|
|
138
|
+
function ruleFor(columnName, onPeopleTable) {
|
|
139
|
+
return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName) && (!rule.peopleOnly || onPeopleTable));
|
|
69
140
|
}
|
|
70
141
|
/** Random hex of this length, per row. `gen_random_uuid()` is core since PostgreSQL 13. */
|
|
71
142
|
function randomHex(length) {
|
|
@@ -89,8 +160,8 @@ var MIN_UNIQUE_HEX = 12;
|
|
|
89
160
|
* kept to at least {@link MIN_UNIQUE_HEX} digits while there is room for them.
|
|
90
161
|
* A NULL stays NULL: an empty optional field is not personal data.
|
|
91
162
|
*/
|
|
92
|
-
function replacementFor(column) {
|
|
93
|
-
const rule = ruleFor(column.column,
|
|
163
|
+
function replacementFor(column, onPeopleTable = PERSON_TABLES.test(column.table)) {
|
|
164
|
+
const rule = ruleFor(column.column, onPeopleTable);
|
|
94
165
|
if (!rule) return null;
|
|
95
166
|
const limit = typeof column.maxLength === "number" && column.maxLength > 0 ? column.maxLength : null;
|
|
96
167
|
let value;
|
|
@@ -119,29 +190,23 @@ function quoteIdentifier(name) {
|
|
|
119
190
|
* whole pass on a technicality.
|
|
120
191
|
*/
|
|
121
192
|
function anonymizableColumns(columns) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
"character varying",
|
|
125
|
-
"varchar",
|
|
126
|
-
"character",
|
|
127
|
-
"char",
|
|
128
|
-
"citext"
|
|
129
|
-
]);
|
|
130
|
-
return columns.filter((column) => shouldAnonymize(column.column, column.table) && textual.has(column.dataType.toLowerCase()));
|
|
193
|
+
const people = peopleTables(columns);
|
|
194
|
+
return columns.filter((column) => isTextual(column) && ruleFor(column.column, people.has(tableKey(column))) !== void 0);
|
|
131
195
|
}
|
|
132
196
|
/** `"schema"."table"` for each table the pass rewrites, sorted. */
|
|
133
197
|
function anonymizedTables(columns) {
|
|
134
|
-
return [...new Set(anonymizableColumns(columns).map(
|
|
198
|
+
return [...new Set(anonymizableColumns(columns).map(tableKey))].sort();
|
|
135
199
|
}
|
|
136
200
|
/** `UPDATE` statements for one anonymization pass, in a stable order. */
|
|
137
201
|
function anonymizeStatements(columns) {
|
|
202
|
+
const people = peopleTables(columns);
|
|
138
203
|
const byTable = /* @__PURE__ */ new Map();
|
|
139
204
|
for (const column of anonymizableColumns(columns)) {
|
|
140
|
-
const key =
|
|
205
|
+
const key = tableKey(column);
|
|
141
206
|
byTable.set(key, [...byTable.get(key) ?? [], column]);
|
|
142
207
|
}
|
|
143
208
|
return [...byTable.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([table, cols]) => {
|
|
144
|
-
return `UPDATE ${table} SET ${[...cols].sort((a, b) => a.column.localeCompare(b.column)).map((column) => `${quoteIdentifier(column.column)} = ${replacementFor(column)}`).join(", ")};`;
|
|
209
|
+
return `UPDATE ${table} SET ${[...cols].sort((a, b) => a.column.localeCompare(b.column)).map((column) => `${quoteIdentifier(column.column)} = ${replacementFor(column, people.has(table))}`).join(", ")};`;
|
|
145
210
|
});
|
|
146
211
|
}
|
|
147
212
|
/**
|
|
@@ -279,6 +344,43 @@ function provisionableSchemas(rows) {
|
|
|
279
344
|
const skip = (name) => name === "information_schema" || name.startsWith("pg_");
|
|
280
345
|
return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();
|
|
281
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* libpq's `keyword=value` form (`host=db.example.com port=5432 dbname=app`):
|
|
349
|
+
* one or more pairs, a value bare or single-quoted with `\'` and `\\` escapes.
|
|
350
|
+
*/
|
|
351
|
+
var KEYWORD_VALUE_DSN = /^\s*[a-z_]+\s*=\s*(?:'(?:[^'\\]|\\.)*'|[^\s']+)(?:\s+[a-z_]+\s*=\s*(?:'(?:[^'\\]|\\.)*'|[^\s']+))*\s*$/i;
|
|
352
|
+
/** One keyword's value in a `keyword=value` connection string, unquoted. */
|
|
353
|
+
function keywordValue(dsn, keyword) {
|
|
354
|
+
const match = new RegExp(`(?:^|\\s)${keyword}\\s*=\\s*('(?:[^'\\\\]|\\\\.)*'|[^\\s']+)`, "i").exec(dsn);
|
|
355
|
+
if (!match) return null;
|
|
356
|
+
const value = match[1];
|
|
357
|
+
return value.startsWith("'") ? value.slice(1, -1).replace(/\\(.)/g, "$1") : value;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Why a `--from` value is not a connection string pg_dump reads, or null
|
|
361
|
+
* when it is one: a `postgres://` or `postgresql://` URL, or libpq's
|
|
362
|
+
* `keyword=value` pairs.
|
|
363
|
+
*
|
|
364
|
+
* Asked before anything else, because the answer is known from the string:
|
|
365
|
+
* `--from garbage --yes` printed "This will replace your local database.
|
|
366
|
+
* From (unparseable connection string)" and started dumping before pg_dump
|
|
367
|
+
* refused it. The value is never quoted back: it may carry a password in a
|
|
368
|
+
* shape no redaction recognises.
|
|
369
|
+
*/
|
|
370
|
+
function sourceProblem(source) {
|
|
371
|
+
const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(source)?.[1]?.toLowerCase();
|
|
372
|
+
if (scheme !== void 0) {
|
|
373
|
+
if (scheme !== "postgres" && scheme !== "postgresql") return `it is a ${scheme}:// URL, and a pull copies from PostgreSQL (postgres:// or postgresql://)`;
|
|
374
|
+
try {
|
|
375
|
+
new URL(source);
|
|
376
|
+
return null;
|
|
377
|
+
} catch {
|
|
378
|
+
return "it starts like a postgres:// URL but is not a valid one";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (KEYWORD_VALUE_DSN.test(source)) return null;
|
|
382
|
+
return "it is neither a postgres:// URL nor libpq keyword=value pairs";
|
|
383
|
+
}
|
|
282
384
|
/** Host and database of a connection string, with no credentials in it. */
|
|
283
385
|
function describeTarget(connectionString) {
|
|
284
386
|
try {
|
|
@@ -286,6 +388,11 @@ function describeTarget(connectionString) {
|
|
|
286
388
|
const database = url.pathname.replace(/^\//, "") || "(default)";
|
|
287
389
|
return `${url.hostname}${url.port ? `:${url.port}` : ""}/${database}`;
|
|
288
390
|
} catch {
|
|
391
|
+
if (KEYWORD_VALUE_DSN.test(connectionString)) {
|
|
392
|
+
const host = keywordValue(connectionString, "host") ?? keywordValue(connectionString, "hostaddr") ?? "(local socket)";
|
|
393
|
+
const port = keywordValue(connectionString, "port");
|
|
394
|
+
return `${host}${port ? `:${port}` : ""}/${keywordValue(connectionString, "dbname") ?? "(default)"}`;
|
|
395
|
+
}
|
|
289
396
|
return "(unparseable connection string)";
|
|
290
397
|
}
|
|
291
398
|
}
|
|
@@ -725,6 +832,6 @@ function describeRestoreFailure(outcome, source) {
|
|
|
725
832
|
];
|
|
726
833
|
}
|
|
727
834
|
//#endregion
|
|
728
|
-
export { anonymizeCopy, describeRestoreFailure, describeTarget, dumpSource, findPgTool, provisionableSchemas, restoreCopy };
|
|
835
|
+
export { anonymizeCopy, describeRestoreFailure, describeTarget, dumpSource, findPgTool, provisionableSchemas, restoreCopy, sourceProblem };
|
|
729
836
|
|
|
730
|
-
//# sourceMappingURL=pull-
|
|
837
|
+
//# sourceMappingURL=pull-DDMpsZ5r.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull-DDMpsZ5r.js","names":[],"sources":["../src/dev-db/pull.ts"],"sourcesContent":["/**\n * `rebase db pull` — copy a database's contents into local development.\n *\n * The common case is production into local, and the reason it exists is that\n * the alternative is worse: without it people hand-roll a `pg_dump | psql` and\n * get the flags wrong in ways that either fail loudly at 2am or, more often,\n * quietly restore half a schema.\n *\n * Three things this command insists on, because copying a production database\n * onto a laptop is a data-protection event whether or not anyone calls it one:\n *\n * 1. **It says what it is about to do, in full, before doing it** — which\n * database it will read, which one it will overwrite, and where the data will\n * come to rest on disk. The target path matters: people forget that\n * `.rebase/pgdata` is a directory their backup software may be indexing.\n *\n * 2. **It refuses to run unattended without being told to.** The target is\n * destroyed, so a mistyped `--from` with no confirmation would take the\n * developer's working database with it.\n *\n * 3. **It will not write to a remote database.** The target is always the local\n * development database; there is no flag that makes this push. A tool that\n * can copy in both directions eventually copies in the wrong one.\n *\n * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not\n * an obviously safe one — the flag nobody types is the flag nobody gets. It is\n * a best-effort pass over columns whose *names* look like personal data, and\n * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in\n * a column called `notes`, and this file says so rather than implying a\n * guarantee it cannot keep.\n *\n * What it does guarantee is that it never half-happens: {@link anonymizeCopy}\n * runs the pass in one transaction, and when the pass fails it empties the\n * tables it would have rewritten rather than leave their real values behind.\n */\n\nimport { execa } from \"execa\";\nimport fs from \"fs\";\nimport type { Client } from \"pg\";\n\nimport { banner, connectionErrorText, hostOf } from \"./connection-banner\";\nimport { PGLITE_EXTENSIONS } from \"./constraints\";\n\n/**\n * How one kind of personal-looking column is overwritten.\n *\n * Two forms, because a constant is the most readable replacement and is wrong\n * for exactly one kind of column: a unique one. Every `token_hash` becoming\n * `'REDACTED'` is what `UNIQUE(token_hash)` refused on the second row of\n * `rebase.refresh_tokens` — so on any project where someone had signed in twice.\n */\nexport interface AnonymizeRule {\n pattern: RegExp;\n /**\n * Only on a table of people (see {@link isPeopleTable}), when set. For a\n * column name that is personal on one table and not on another: a user's\n * `name` is a person's, a tag's is not.\n */\n peopleOnly?: true;\n /** The value for a column no unique index covers, or null to always use the unique form. */\n value: string | null;\n /** The unique form is `prefix` + random hex + `suffix`, cut to fit the column. */\n prefix: string;\n suffix: string;\n}\n\n/**\n * Tables whose rows are people, by name: `rebase.users`, the blog scaffold's\n * `authors`, and the usual spellings of a project's own. Singular or plural.\n */\nconst PERSON_TABLES = /^(users?|user_profiles?|profiles?|people|persons?|members?|customers?|contacts?|employees?|authors?)$/i;\n\n/** An email address column, by name: the email rule's own pattern. */\nconst EMAIL_COLUMN = /^(.*_)?e?mail(_.*)?$/i;\n\n/** The column types a replacement string fits. */\nconst TEXTUAL_TYPES = new Set([\"text\", \"character varying\", \"varchar\", \"character\", \"char\", \"citext\"]);\n\nfunction isTextual(column: ColumnRef): boolean {\n return TEXTUAL_TYPES.has(column.dataType.toLowerCase());\n}\n\n/** `\"schema\".\"table\"`, the key a column's table is known by. */\nfunction tableKey(column: ColumnRef): string {\n return `${quoteIdentifier(column.schema)}.${quoteIdentifier(column.table)}`;\n}\n\n/**\n * The tables whose rows are people: those {@link PERSON_TABLES} names, and any\n * that holds an email address column.\n *\n * The second is what reaches the tables no list of names could: a row with an\n * address of its own is a person's, so its `name` is one too. The blog\n * scaffold's `authors` (`name`, `email`, `picture`) arrived with every name\n * as it was. A table of organisations with a contact address gets its `name`\n * redacted too, which costs a local copy little.\n */\nexport function peopleTables(columns: readonly ColumnRef[]): Set<string> {\n const people = new Set<string>();\n for (const column of columns) {\n if (PERSON_TABLES.test(column.table) || (EMAIL_COLUMN.test(column.column) && isTextual(column))) {\n people.add(tableKey(column));\n }\n }\n return people;\n}\n\n/**\n * Column-name patterns the anonymizer overwrites.\n *\n * Names, not contents: inspecting values would be slower, and would still miss\n * the same things. This is a reasonable-effort measure for making a local copy\n * less dangerous, and it is not a compliance control.\n */\nexport const ANONYMIZE_PATTERNS: readonly AnonymizeRule[] = [\n // Emails are unique in practice even where nothing enforces it, so they\n // always get a value of their own.\n { pattern: EMAIL_COLUMN, value: null, prefix: \"user-\", suffix: \"@example.invalid\" },\n { pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i, value: \"+10000000000\", prefix: \"+1-\", suffix: \"\" },\n // `display_name` is how `rebase.users` holds every user's name.\n {\n pattern: /^(.*_)?(display_name|first_name|last_name|full_name|middle_name|family_name|surname|given_name)(_.*)?$/i,\n value: \"Redacted\",\n prefix: \"Redacted-\",\n suffix: \"\"\n },\n // A bare `name` is a person's only on a table of people.\n { pattern: /^name$/i, peopleOnly: true, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i, value: \"Redacted\", prefix: \"Redacted-\", suffix: \"\" },\n { pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" },\n { pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i, value: \"REDACTED\", prefix: \"REDACTED-\", suffix: \"\" }\n];\n\n/** The rule that overwrites this column, if any. A people-only rule applies only on a table of people. */\nfunction ruleFor(columnName: string, onPeopleTable: boolean): AnonymizeRule | undefined {\n return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName) && (!rule.peopleOnly || onPeopleTable));\n}\n\n/**\n * Whether a column of this name is overwritten. A table is judged a table of\n * people by its name here; {@link anonymizableColumns}, which sees the\n * table's other columns, also counts one that holds an email address.\n */\nexport function shouldAnonymize(columnName: string, table?: string): boolean {\n return ruleFor(columnName, table !== undefined && PERSON_TABLES.test(table)) !== undefined;\n}\n\n/** A text-ish column the anonymizer can overwrite without a type error. */\nexport interface ColumnRef {\n schema: string;\n table: string;\n column: string;\n dataType: string;\n /** `character_maximum_length`: null or absent for an unbounded type. */\n maxLength?: number | null;\n /** Covered by a unique index — a primary key, a UNIQUE constraint, or a unique expression index. */\n unique?: boolean;\n}\n\n/** Random hex of this length, per row. `gen_random_uuid()` is core since PostgreSQL 13. */\nfunction randomHex(length: number): string {\n return `left(replace(gen_random_uuid()::text, '-', ''), ${length})`;\n}\n\nfunction sqlLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Fewest random hex digits a unique replacement keeps. Twelve is 16^12 values:\n * a collision needs millions of rows, where eight would need tens of thousands.\n */\nconst MIN_UNIQUE_HEX = 12;\n\n/**\n * The SQL expression that overwrites one column, or null when no rule names it.\n *\n * Unique per row when an index makes the column unique, and always cut to the\n * column's declared length — `zip char(5)` refused `'Redacted'` outright, and a\n * failed statement now empties the tables rather than skip one. To fit a short\n * unique column the prefix goes first, then the suffix, and the random part is\n * kept to at least {@link MIN_UNIQUE_HEX} digits while there is room for them.\n * A NULL stays NULL: an empty optional field is not personal data.\n */\nexport function replacementFor(column: ColumnRef, onPeopleTable = PERSON_TABLES.test(column.table)): string | null {\n const rule = ruleFor(column.column, onPeopleTable);\n if (!rule) return null;\n const limit = typeof column.maxLength === \"number\" && column.maxLength > 0 ? column.maxLength : null;\n\n let value: string;\n if (rule.value !== null && !column.unique) {\n value = sqlLiteral(limit === null ? rule.value : rule.value.slice(0, limit));\n } else {\n const shapes = [[rule.prefix, rule.suffix], [\"\", rule.suffix], [\"\", \"\"]];\n const fitting = shapes.find(([prefix, suffix]) =>\n limit === null || limit - prefix.length - suffix.length >= MIN_UNIQUE_HEX);\n const [prefix, suffix] = fitting ?? [\"\", \"\"];\n const hex = Math.min(32, limit === null ? 32 : limit - prefix.length - suffix.length);\n value = `concat(${sqlLiteral(prefix)}, ${randomHex(Math.max(1, hex))}, ${sqlLiteral(suffix)})`;\n }\n\n const name = quoteIdentifier(column.column);\n return `CASE WHEN ${name} IS NULL THEN NULL ELSE ${value} END`;\n}\n\n/** A double-quoted identifier, with any `\"` in it doubled — a table called `order` or `a\"b` still works. */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Anonymizable columns: name looks personal, and the type can hold the\n * replacement.\n *\n * The type check is what stops this generating `UPDATE … SET user_id =\n * 'Redacted'` for an integer column called `user_id_email_seq` and failing the\n * whole pass on a technicality.\n */\nexport function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[] {\n const people = peopleTables(columns);\n\n return columns.filter((column) =>\n isTextual(column) && ruleFor(column.column, people.has(tableKey(column))) !== undefined);\n}\n\n/** `\"schema\".\"table\"` for each table the pass rewrites, sorted. */\nexport function anonymizedTables(columns: readonly ColumnRef[]): string[] {\n return [...new Set(anonymizableColumns(columns).map(tableKey))].sort();\n}\n\n/** `UPDATE` statements for one anonymization pass, in a stable order. */\nexport function anonymizeStatements(columns: readonly ColumnRef[]): string[] {\n const people = peopleTables(columns);\n const byTable = new Map<string, ColumnRef[]>();\n for (const column of anonymizableColumns(columns)) {\n const key = tableKey(column);\n byTable.set(key, [...(byTable.get(key) ?? []), column]);\n }\n\n return [...byTable.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([table, cols]) => {\n const assignments = [...cols]\n .sort((a, b) => a.column.localeCompare(b.column))\n .map((column) => `${quoteIdentifier(column.column)} = ${replacementFor(column, people.has(table))}`)\n .join(\", \");\n\n return `UPDATE ${table} SET ${assignments};`;\n });\n}\n\n/**\n * Every column the pass could overwrite, with what decides how.\n *\n * Base tables only — a view has an `email` column too, and is not something to\n * UPDATE. Generated columns are skipped for the same reason: `email_domain\n * GENERATED ALWAYS AS (…)` matches by name and refuses any value but DEFAULT,\n * and it follows the column it is generated from anyway. An extension type such\n * as `citext` reports `USER-DEFINED`, so its own name is what is compared.\n *\n * `unique` comes from the indexes, through `pg_depend` as well as `indkey`: a\n * unique index on `lower(email)` names no column in `indkey`, and is exactly\n * what a constant replacement would violate.\n */\nexport const ANONYMIZE_COLUMNS_SQL = `\nWITH unique_columns AS (\n SELECT DISTINCT i.indrelid AS relid, a.attname AS column_name\n FROM pg_index i\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum > 0 AND NOT a.attisdropped\n WHERE i.indisunique\n AND (a.attnum = ANY (i.indkey)\n OR EXISTS (SELECT 1 FROM pg_depend d\n WHERE d.classid = 'pg_class'::regclass AND d.objid = i.indexrelid\n AND d.refclassid = 'pg_class'::regclass AND d.refobjid = i.indrelid\n AND d.refobjsubid = a.attnum))\n)\nSELECT c.table_schema AS \"schema\",\n c.table_name AS \"table\",\n c.column_name AS \"column\",\n CASE WHEN c.data_type = 'USER-DEFINED' THEN c.udt_name ELSE c.data_type END AS \"dataType\",\n c.character_maximum_length AS \"maxLength\",\n (u.column_name IS NOT NULL) AS \"unique\"\n FROM information_schema.columns c\n JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name\n LEFT JOIN unique_columns u\n ON u.relid = format('%I.%I', c.table_schema, c.table_name)::regclass AND u.column_name = c.column_name\n WHERE t.table_type = 'BASE TABLE'\n AND c.is_generated = 'NEVER'\n AND c.table_schema NOT IN ('pg_catalog', 'information_schema')`;\n\n/** How {@link anonymizeCopy} ended. */\nexport type AnonymizeOutcome =\n | { ok: true; tables: string[] }\n | {\n ok: false;\n /** Why the pass failed. Nothing it wrote was kept. */\n error: string;\n /** The tables emptied instead, so none of their real values are left. */\n emptied: string[];\n /** Why they could not be emptied, when they could not: the copy then holds real data. */\n emptyError: string | null;\n };\n\nfunction errorText(error: unknown): string {\n return error instanceof Error && error.message ? error.message : String(error);\n}\n\n/**\n * Overwrite the personal-looking columns of a restored copy, all or nothing.\n *\n * One transaction, so a failure part-way leaves no table half done — the old\n * pass ran its UPDATEs one by one, so a failure on `rebase.refresh_tokens` left\n * `public.authors` anonymized and `rebase.users` holding every real email.\n *\n * A rollback alone would be worse than that, though: it puts back the real\n * value of every column, in a copy the developer asked to be anonymized. So on\n * failure the tables the pass would have rewritten are emptied (`TRUNCATE …\n * CASCADE`, which takes the rows that reference them too), and the outcome\n * says which — or, if even that failed, says the copy holds real data, so the\n * caller can say it louder.\n *\n * `row_security = off` makes a role that RLS would filter fail the pass instead\n * of rewriting only the rows its policies show it, which looked like success.\n */\nexport async function anonymizeCopy(client: Client): Promise<AnonymizeOutcome> {\n let listed = false;\n let tables: string[] = [];\n try {\n const { rows } = await client.query<ColumnRef>(ANONYMIZE_COLUMNS_SQL);\n tables = anonymizedTables(rows);\n listed = true;\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL row_security = off\");\n for (const statement of anonymizeStatements(rows)) await client.query(statement);\n await client.query(\"COMMIT\");\n\n return { ok: true, tables };\n } catch (error) {\n const reason = errorText(error);\n await client.query(\"ROLLBACK\").catch(() => undefined);\n if (!listed) {\n return { ok: false, error: reason, emptied: [], emptyError: \"the tables to empty could not be listed\" };\n }\n if (tables.length === 0) return { ok: false, error: reason, emptied: [], emptyError: null };\n try {\n await client.query(`TRUNCATE TABLE ${tables.join(\", \")} CASCADE`);\n\n return { ok: false, error: reason, emptied: tables, emptyError: null };\n } catch (emptyError) {\n return { ok: false, error: reason, emptied: [], emptyError: errorText(emptyError) };\n }\n }\n}\n\n/**\n * Schemas the restored copy needs the app role provisioned on.\n *\n * `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives\n * with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no\n * privileges behind them. Measured on a 30-table project: 68 policies restored,\n * 14 tables with RLS on, and **0** grants to `rebase_user` — where the source\n * had 60. Reading one table as the role Rebase serves every request through:\n *\n * source: 6\n * copy: ERROR: permission denied for table leads\n *\n * The dump flags are right and stay: without `--no-owner`/`--no-privileges`\n * every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails\n * against roles that do not exist on a laptop, and buries the real output. The\n * repair belongs after the restore, and belongs to `ensureAppRole` — the same\n * routine boot and `rebase db push` call — rather than to a second list of\n * grants written here.\n *\n * Which schemas: every non-system schema the restored database actually has.\n * Boot knows its `managedSchemas` from the collections; a restore knows only\n * what arrived, and a source may carry schemas this project does not declare.\n * `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to\n * grant on and PostgreSQL would refuse anyway.\n */\nexport function provisionableSchemas(rows: readonly { schema: string }[]): string[] {\n const skip = (name: string) => name === \"information_schema\" || name.startsWith(\"pg_\");\n\n return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();\n}\n\n/**\n * libpq's `keyword=value` form (`host=db.example.com port=5432 dbname=app`):\n * one or more pairs, a value bare or single-quoted with `\\'` and `\\\\` escapes.\n */\nconst KEYWORD_VALUE_DSN = /^\\s*[a-z_]+\\s*=\\s*(?:'(?:[^'\\\\]|\\\\.)*'|[^\\s']+)(?:\\s+[a-z_]+\\s*=\\s*(?:'(?:[^'\\\\]|\\\\.)*'|[^\\s']+))*\\s*$/i;\n\n/** One keyword's value in a `keyword=value` connection string, unquoted. */\nfunction keywordValue(dsn: string, keyword: string): string | null {\n const match = new RegExp(`(?:^|\\\\s)${keyword}\\\\s*=\\\\s*('(?:[^'\\\\\\\\]|\\\\\\\\.)*'|[^\\\\s']+)`, \"i\").exec(dsn);\n if (!match) return null;\n const value = match[1];\n return value.startsWith(\"'\") ? value.slice(1, -1).replace(/\\\\(.)/g, \"$1\") : value;\n}\n\n/**\n * Why a `--from` value is not a connection string pg_dump reads, or null\n * when it is one: a `postgres://` or `postgresql://` URL, or libpq's\n * `keyword=value` pairs.\n *\n * Asked before anything else, because the answer is known from the string:\n * `--from garbage --yes` printed \"This will replace your local database.\n * From (unparseable connection string)\" and started dumping before pg_dump\n * refused it. The value is never quoted back: it may carry a password in a\n * shape no redaction recognises.\n */\nexport function sourceProblem(source: string): string | null {\n const scheme = /^([a-z][a-z0-9+.-]*):\\/\\//i.exec(source)?.[1]?.toLowerCase();\n if (scheme !== undefined) {\n if (scheme !== \"postgres\" && scheme !== \"postgresql\") {\n return `it is a ${scheme}:// URL, and a pull copies from PostgreSQL (postgres:// or postgresql://)`;\n }\n try {\n new URL(source);\n return null;\n } catch {\n return \"it starts like a postgres:// URL but is not a valid one\";\n }\n }\n if (KEYWORD_VALUE_DSN.test(source)) return null;\n return \"it is neither a postgres:// URL nor libpq keyword=value pairs\";\n}\n\n/** Host and database of a connection string, with no credentials in it. */\nexport function describeTarget(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n const database = url.pathname.replace(/^\\//, \"\") || \"(default)\";\n\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}/${database}`;\n } catch {\n if (KEYWORD_VALUE_DSN.test(connectionString)) {\n const host = keywordValue(connectionString, \"host\") ?? keywordValue(connectionString, \"hostaddr\") ?? \"(local socket)\";\n const port = keywordValue(connectionString, \"port\");\n return `${host}${port ? `:${port}` : \"\"}/${keywordValue(connectionString, \"dbname\") ?? \"(default)\"}`;\n }\n // Never echo the raw string: it carries a password, and this line is\n // printed to a terminal people paste into issues.\n return \"(unparseable connection string)\";\n }\n}\n\n/**\n * What the local database runs on.\n *\n * `pglite` is the managed development database: one PGlite session behind a\n * socket server. Two things a restore relies on are not there. The server runs\n * each protocol message on its own, so the `CopyData` that follows a `COPY …\n * FROM stdin` never reaches the backend waiting for it: the COPY fails on\n * \"unexpected EOF\", and the session stops answering anyone until the daemon\n * is killed. And every connection shares that one session, so what pg_restore\n * `SET`s for itself stays set for everyone after it disconnects.\n */\nexport type PullTargetEngine = \"postgres\" | \"pglite\";\n\nexport interface PullPlan {\n /** Where the data comes from. */\n source: string;\n /** Where it lands. Always local. */\n target: string;\n anonymize: boolean;\n /** Schemas to copy. Empty means every non-system schema. */\n schemas: string[];\n /**\n * What the target runs on, which decides how the dump carries its rows.\n * Absent means `postgres`. {@link dumpSource} reads it off the target.\n */\n targetEngine?: PullTargetEngine;\n}\n\n/**\n * Rows per `INSERT` in a dump for the managed database.\n *\n * One per statement restored 100k rows in 8s; a hundred took 0.8s. A hundred\n * 512KB rows, a 100MB statement, still went in.\n */\nconst ROWS_PER_INSERT = 100;\n\n/**\n * `pg_dump` arguments for the source.\n *\n * `--no-owner` and `--no-acl` because the roles on a production server do not\n * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and\n * buries the real output in noise. `--format=custom` so `pg_restore` can be told\n * to continue past errors selectively rather than all-or-nothing.\n *\n * For the managed database the rows travel as `INSERT`s, which PGlite takes\n * like any other statement, instead of `COPY` data, which it cannot take at\n * all — see {@link PullTargetEngine}. The copy is the same; only how the rows\n * are written in the dump differs.\n */\nexport function dumpArgs(plan: PullPlan): string[] {\n const args = [\"--format=custom\", \"--no-owner\", \"--no-acl\", \"--no-privileges\"];\n if (plan.targetEngine === \"pglite\") args.push(`--rows-per-insert=${ROWS_PER_INSERT}`);\n for (const schema of plan.schemas) args.push(\"--schema\", schema);\n args.push(\"--dbname\", splitPassword(plan.source).connectionString);\n\n return args;\n}\n\n/** The environment `pg_dump` runs with: the source's password, if it has one. */\nexport function dumpEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.source);\n}\n\n/** The environment `pg_restore` runs with: the target's password, if it has one. */\nexport function restoreEnv(plan: PullPlan): Record<string, string> {\n return passwordEnv(plan.target);\n}\n\nfunction passwordEnv(connectionString: string): Record<string, string> {\n const { password } = splitPassword(connectionString);\n return password === null ? {} : { PGPASSWORD: password };\n}\n\n/**\n * A libpq connection string without its password, and the password.\n *\n * The password goes in `PGPASSWORD`, never on a command line, where every\n * process listing can read it and every failure prints it back (execa's error\n * repeats the command). The same rule `db backup` follows; this mirrors\n * `splitPassword` in `@rebasepro/server-postgres`'s connection-string utils,\n * which is not exported. Covers the user-info password and a `password=`\n * parameter; anything that does not parse as a postgres URL is left alone.\n */\nfunction splitPassword(connectionString: string): { connectionString: string; password: string | null } {\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n return { connectionString, password: null };\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return { connectionString, password: null };\n\n const fromUserInfo = url.password ? decodePercent(url.password) : null;\n const fromParameter = url.searchParams.get(\"password\");\n if (fromUserInfo === null && fromParameter === null) return { connectionString, password: null };\n\n url.password = \"\";\n if (fromParameter !== null) {\n url.searchParams.delete(\"password\");\n // By hand: URLSearchParams writes a space as `+`, which libpq reads literally.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n }\n return { connectionString: url.toString(), password: fromUserInfo ?? fromParameter };\n}\n\nfunction decodePercent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/**\n * `pg_restore` arguments for the target.\n *\n * It restores into schemas {@link restoreCopy} has already emptied, so\n * `--clean --if-exists` drops nothing that is there; it stays for whatever a\n * dump holds outside them — an extension, say — which would otherwise fail as\n * already existing. `--no-owner` for the same reason as the dump.\n */\nexport function restoreArgs(plan: PullPlan, dumpFile: string): string[] {\n return [\"--clean\", \"--if-exists\", \"--no-owner\", \"--no-privileges\", \"--dbname\", splitPassword(plan.target).connectionString, dumpFile];\n}\n\n/**\n * Is this PostgreSQL client tool on PATH, and what version? Both are checked\n * before anything destructive: discovering `pg_restore` is missing after the\n * dump has been taken — or after `--clean` has emptied the target — is the\n * worst possible ordering.\n */\nexport async function findPgTool(tool: \"pg_dump\" | \"pg_restore\"): Promise<string | null> {\n try {\n const { stdout } = await execa(tool, [\"--version\"]);\n\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\n/** PGlite names itself in `version()`: `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`. */\nfunction engineOf(version: string): PullTargetEngine {\n return /\\bPGlite\\b/i.test(version) ? \"pglite\" : \"postgres\";\n}\n\nconst NOTHING_CHANGED = \"Nothing was changed: the local database is as it was.\";\n\n/** What the target database runs on, or why it could not be asked. */\nasync function readTargetEngine(target: string): Promise<{ engine: PullTargetEngine } | { error: unknown }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n const { rows } = await client.query<{ version: string }>(\"SELECT version() AS version\");\n\n return { engine: engineOf(rows[0]?.version ?? \"\") };\n } catch (error) {\n return { error };\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/** How {@link dumpSource} ended. */\nexport type DumpOutcome =\n | {\n ok: true;\n /** The plan the dump was taken for, with its target's engine read: what {@link restoreCopy} takes. */\n plan: PullPlan;\n }\n | {\n ok: false;\n /** What failed and what to do, as plain lines for the caller to print. Nothing was changed. */\n lines: string[];\n };\n\n/**\n * `pg_dump` the source into `dumpFile`, in the form the target can restore.\n *\n * It asks the target what it runs on first — the managed database cannot take\n * `COPY` data, see {@link PullTargetEngine} — which also finds a local\n * database that is not there before anything is read from the source.\n *\n * pg_dump's stderr is kept rather than shown as it arrives, so a failure is\n * reported once, in the reader's terms: an unreachable source, or credentials\n * it refused, get the banner every database command prints for a connection\n * that failed, and anything else is pg_dump's own words with a line saying\n * nothing was changed. A dump that succeeds with warnings still shows them.\n * A dump that fails leaves no file behind.\n */\nexport async function dumpSource(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n try {\n const outcome = await dumpInto(plan, dumpFile);\n if (!outcome.ok) fs.rmSync(dumpFile, { force: true });\n\n return outcome;\n } catch (error) {\n fs.rmSync(dumpFile, { force: true });\n throw error;\n }\n}\n\nasync function dumpInto(plan: PullPlan, dumpFile: string): Promise<DumpOutcome> {\n let targetEngine = plan.targetEngine;\n if (!targetEngine) {\n const read = await readTargetEngine(plan.target);\n if (\"error\" in read) {\n return {\n ok: false,\n lines: banner(`Cannot connect to PostgreSQL at ${hostOf(plan.target)}`,\n `The driver said: ${connectionErrorText(read.error)}`, [\n \"That is the local database the pull would replace, and nothing\",\n \"was read from the source.\",\n \"Start it — `rebase dev` starts the managed one, `rebase dev --docker`\",\n \"Postgres in Docker — or check DATABASE_URL in .env, then pull again.\"\n ])\n };\n }\n targetEngine = read.engine;\n }\n\n const planned: PullPlan = { ...plan, targetEngine };\n const run = await execa(\"pg_dump\", [...dumpArgs(planned), \"--file\", dumpFile], {\n env: dumpEnv(planned),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: \"pipe\",\n reject: false\n });\n if (run.exitCode === 0) {\n if (run.stderr.trim()) process.stderr.write(`${run.stderr.trimEnd()}\\n`);\n\n return { ok: true, plan: planned };\n }\n\n return { ok: false, lines: describeDumpFailure(plan.source, run) };\n}\n\n/**\n * Why `pg_dump` could not read the source, as plain lines for the caller to\n * print. The dump is taken before anything local is touched, so every one of\n * them ends the same way: nothing was changed.\n */\nexport function describeDumpFailure(source: string, run: RestoreRun): string[] {\n if (run.signal) return [`✗ pg_dump was stopped by ${run.signal} before it finished.`, NOTHING_CHANGED];\n if (run.exitCode === undefined) {\n return [\n `✗ pg_dump could not be started (${run.code ?? \"unknown error\"}).`,\n NOTHING_CHANGED,\n \"Check that the PostgreSQL client tools are installed (`pg_dump --version`).\"\n ];\n }\n\n const stderr = (run.stderr ?? \"\").trim();\n const host = hostOf(source);\n const error = /pg_dump: error: (.+)/.exec(stderr)?.[1]?.trim() ?? null;\n const said = error ? `pg_dump said: ${error}` : null;\n\n if (/could not translate host name|could not connect to server|connection to server .* failed: (?:Connection refused|timeout expired|No route to host|Network is unreachable|Operation timed out|Host is down)/i.test(stderr)) {\n return banner(`Cannot connect to PostgreSQL at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the host and port in --from, and that this machine can reach\",\n \"that server: a VPN, an allow-list, or an SSH tunnel.\"\n ]);\n }\n if (/password authentication failed|no pg_hba\\.conf entry|role \"[^\"]+\" does not exist/i.test(stderr)) {\n let user = \"unknown\";\n try {\n user = decodePercent(new URL(source).username) || \"unknown\";\n } catch { /* named as unknown */ }\n return banner(`Authentication failed for user \"${user}\" at ${host}`, said, [\n \"PostgreSQL rejected the credentials --from carries.\",\n NOTHING_CHANGED,\n \"Check the user and password in --from.\"\n ]);\n }\n // The server answered and has no database of that name: a typo in\n // --from, usually, or a database not created yet.\n const missing = /database \"([^\"]+)\" does not exist/i.exec(stderr)?.[1];\n if (missing) {\n return banner(`Database \"${missing}\" does not exist at ${host}`, said, [\n \"That is the database --from names, which the pull reads.\",\n NOTHING_CHANGED,\n \"Check the database name in --from.\"\n ]);\n }\n\n const versions = /server version mismatch/i.test(stderr)\n ? [\"pg_dump is older than the server: install client tools at least as new as it (`brew install libpq`).\"]\n : [];\n return [\n ...(stderr ? stderr.split(\"\\n\") : []),\n `✗ pg_dump could not dump ${describeTarget(source)} (exit code ${run.exitCode}).`,\n NOTHING_CHANGED,\n ...versions\n ];\n}\n\n/** The fields of a finished `pg_restore` (or `pg_dump`) run that decide {@link restoreOutcome}. */\nexport interface RestoreRun {\n exitCode?: number;\n signal?: string;\n /** A spawn failure's code — `ENOENT` when the binary vanished. */\n code?: string;\n stderr?: string;\n}\n\n/** What a `pg_restore` run says about the copy. */\nexport interface RestoreRunOutcome {\n /** Whether anything can have arrived — what decides whether an anonymization pass has data to redact. */\n started: boolean;\n /** Null when the copy is complete. */\n failure: string | null;\n /**\n * What to do about the failure. \"Pull again\" only where a second pull is\n * what finishes the job: everything else repeats until its cause is fixed.\n */\n advice: string | null;\n}\n\n/**\n * What a finished `pg_restore` says about the copy.\n *\n * The target was emptied before anything was written, so every way this can\n * end short of exit 0 leaves a partial or empty local database, and none of\n * them may be reported as a copy. A non-zero exit is pg_restore saying it\n * skipped statements — \"errors ignored on restore: N\" — and whatever those\n * statements created (a table, its rows, an RLS policy) is missing locally.\n *\n * Restoring into an emptied database, those errors come from what the source\n * holds that this Postgres cannot create — an extension it does not have, a\n * feature of a newer server — and a second pull meets them again. So the\n * advice names that, rather than \"pull again\", which sent people in a loop.\n * On the managed database an extension cannot be installed at all, so the\n * advice there is Postgres in Docker.\n */\nexport function restoreOutcome(run: RestoreRun, engine: PullTargetEngine = \"postgres\"): RestoreRunOutcome {\n if (run.signal) {\n return {\n started: true,\n failure: `pg_restore was stopped by ${run.signal} before it finished.`,\n advice: \"It was interrupted, not refused: pull again to finish the copy.\"\n };\n }\n if (run.exitCode === undefined) {\n return {\n started: false,\n failure: `pg_restore could not be started (${run.code ?? \"unknown error\"}).`,\n advice: \"Check that the PostgreSQL client tools are installed (`pg_restore --version`), then pull again.\"\n };\n }\n if (run.exitCode === 0) return { started: true, failure: null, advice: null };\n\n const ignored = /errors ignored on restore: (\\d+)/.exec(run.stderr ?? \"\");\n const repeats = engine === \"pglite\"\n ? \"Pulling again repeats these errors until what they name is fixed — most often an extension the \"\n + `source uses that the managed database does not have (it has ${PGLITE_EXTENSIONS.map(extension => extension.name).join(\", \")}). `\n + \"Pull into Postgres in Docker instead (`rebase db pull --docker`), or pull only the schemas that do \"\n + \"not need it (--schema).\"\n : \"Pulling again repeats these errors until what they name is fixed — most often an extension \"\n + \"the source uses that this Postgres does not have installed. Install it here, or pull only the schemas \"\n + \"that do not need it (--schema).\";\n\n return {\n started: true,\n failure: ignored\n ? `pg_restore could not restore ${ignored[1]} object(s) — the errors are above.`\n : `pg_restore exited with code ${run.exitCode} — the errors are above.`,\n advice: repeats\n };\n}\n\n/** How {@link restoreCopy} ended. */\nexport interface RestoreOutcome extends RestoreRunOutcome {\n /** Whether the local database was emptied first. False means it is exactly as it was. */\n cleared: boolean;\n}\n\n/**\n * The local schemas a pull empties before it restores: every one of the local\n * database's own for a whole-database pull, and those it copies for a\n * `--schema` pull. `pg_catalog`, `information_schema` and the `pg_*` internals\n * never — see {@link provisionableSchemas}.\n */\nexport function schemasToClear(present: readonly { schema: string }[], planned: readonly string[]): string[] {\n const local = provisionableSchemas(present);\n\n return planned.length === 0 ? local : local.filter((schema) => planned.includes(schema));\n}\n\n/**\n * The statements that empty those schemas, in one transaction.\n *\n * `CASCADE`, which `pg_restore --clean` never uses: a table the source lacks —\n * `rebase.atlas_schema_revisions` from `rebase db migrate`, a collection added\n * locally that references a pulled table — is exactly what blocked its plain\n * `DROP SCHEMA` and `DROP TABLE`. `public` is put back empty, with the access\n * every role has to it by default, because a dump does not create a default\n * `public` and every table in it would otherwise have nowhere to go.\n */\nexport function clearStatements(schemas: readonly string[]): string[] {\n const statements = schemas.map((schema) => `DROP SCHEMA IF EXISTS ${quoteIdentifier(schema)} CASCADE`);\n if (schemas.includes(\"public\")) statements.push(\"CREATE SCHEMA public\", \"GRANT USAGE ON SCHEMA public TO PUBLIC\");\n\n return statements;\n}\n\n/**\n * How long emptying the local database waits for a lock before giving up.\n *\n * A `rebase dev` serving from the database holds connections, and one inside\n * a transaction would otherwise make the pull hang without a word.\n */\nconst CLEAR_LOCK_TIMEOUT = \"15s\";\n\n/** Why emptying the local database failed, in the reader's terms, and what fixes it. */\nfunction clearFailure(error: unknown): RestoreOutcome {\n const code = typeof error === \"object\" && error !== null && \"code\" in error ? String(error.code) : \"\";\n const reason = errorText(error);\n const advice = code === \"55P03\"\n ? \"Something holds a lock on it — usually `rebase dev` serving from it. Stop that, then pull again.\"\n : code === \"42501\"\n ? \"This connection may not drop those schemas. Connect as the role that owns them — the one \"\n + \"that created them — and pull with that.\"\n : \"Fix what the error names; pulling again before that repeats it.\";\n\n return {\n cleared: false,\n started: false,\n failure: `Could not empty the local database before restoring: ${reason}`,\n advice\n };\n}\n\n/**\n * Replace the local database's contents with a dump: empty the schemas it\n * holds, then `pg_restore` into them.\n *\n * Only ever `plan.target`, the local development database the command\n * resolved — the pull's one destination, which no flag can change. Emptying it\n * is one transaction: if any schema cannot be dropped, none is, nothing is\n * restored, and the database is as it was.\n *\n * On the managed database the dump must be one {@link dumpSource} took for it,\n * with its rows as INSERTs: a COPY would leave the daemon answering nobody, so\n * one is refused before anything is emptied. And afterwards its one session —\n * every connection's — is given back its own settings: pg_restore leaves\n * `search_path` empty and `row_security` off behind it, which a running\n * `rebase dev` would otherwise query under.\n */\nexport async function restoreCopy(plan: PullPlan, dumpFile: string): Promise<RestoreOutcome> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: plan.target });\n client.on(\"error\", () => undefined);\n let engine: PullTargetEngine;\n try {\n await client.connect();\n engine = engineOf((await client.query<{ version: string }>(\"SELECT version() AS version\")).rows[0]?.version ?? \"\");\n if (engine === \"pglite\" && plan.targetEngine !== \"pglite\") {\n return {\n cleared: false,\n started: false,\n failure: \"The managed development database cannot take this dump: its rows are COPY data, which PGlite's socket server cannot receive.\",\n advice: \"Pull into Postgres in Docker instead: `rebase db pull --docker`.\"\n };\n }\n const { rows } = await client.query<{ schema: string }>(\"SELECT nspname AS \\\"schema\\\" FROM pg_namespace\");\n await client.query(\"BEGIN\");\n await client.query(`SET LOCAL lock_timeout = '${CLEAR_LOCK_TIMEOUT}'`);\n for (const statement of clearStatements(schemasToClear(rows, plan.schemas))) await client.query(statement);\n await client.query(\"COMMIT\");\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n return clearFailure(error);\n } finally {\n await client.end().catch(() => undefined);\n }\n\n // stderr is shown as it arrives and kept, for the count in its summary\n // line. Not rejected: every way this ends is judged by `restoreOutcome`.\n const run = await execa(\"pg_restore\", restoreArgs(plan, dumpFile), {\n env: restoreEnv(plan),\n stdin: \"ignore\",\n stdout: \"inherit\",\n stderr: [\"pipe\", \"inherit\"],\n reject: false\n });\n\n if (engine === \"pglite\") await resetSharedSession(plan.target);\n\n return { cleared: true, ...restoreOutcome(run, engine) };\n}\n\n/**\n * Put the managed database's one session back to its own settings.\n *\n * `RESET ALL` and `DEALLOCATE ALL`, not `DISCARD ALL`: the session is every\n * connection's, and `DISCARD` would also drop the `LISTEN` a running\n * `rebase dev`'s realtime holds on it. The same pair the backup service runs\n * after its tools (`resetSharedSession` in server-postgres' backup-service). Best effort: a daemon that cannot take this cannot take the\n * re-grant that follows either, and that step reports it.\n */\nasync function resetSharedSession(target: string): Promise<void> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: target, connectionTimeoutMillis: 10_000, query_timeout: 15_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n await client.query(\"RESET ALL\");\n await client.query(\"DEALLOCATE ALL\");\n } catch {\n // Reported by the next step that connects; see above.\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * The lines that report a failed restore: what failed, what state the local\n * database is in now, and what to do. Plain text, for the caller to colour.\n */\nexport function describeRestoreFailure(outcome: RestoreOutcome, source: string): string[] {\n const state = outcome.started\n ? `The local database holds an incomplete copy of ${source}.`\n : outcome.cleared\n ? \"The local database was emptied, and nothing was restored into it.\"\n : \"Nothing was restored, and the local database is as it was.\";\n\n return [outcome.failure ?? \"The restore did not complete.\", state, ...(outcome.advice ? [outcome.advice] : [])];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/utils/refusal.ts
|
|
2
|
+
/**
|
|
3
|
+
* A refusal the CLI raised itself, having said why: nothing broke.
|
|
4
|
+
*
|
|
5
|
+
* `bin/rebase.js` ends every failure with "Re-run with --debug for the stack
|
|
6
|
+
* trace." That is right when something broke inside a command, and noise after
|
|
7
|
+
* a check that decided not to run — the compose-port refusal prints the
|
|
8
|
+
* container holding the port and how to move, and its stack points at the line
|
|
9
|
+
* that decided to refuse. A `UsageError` (`utils/args.ts`) is the same case for
|
|
10
|
+
* a command line that was wrong; this is the one for everything else.
|
|
11
|
+
*
|
|
12
|
+
* Marked with a property rather than left to `instanceof`, for `UsageError`'s
|
|
13
|
+
* reason: the reader is `bin/rebase.js`, which imports the bundle, so the class
|
|
14
|
+
* it would test against is not the one this file compiled to.
|
|
15
|
+
*/
|
|
16
|
+
var Refusal = class extends Error {
|
|
17
|
+
isRefusal = true;
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "Refusal";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Refusal as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=refusal-ReeeGP8a.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal-ReeeGP8a.js","names":[],"sources":["../src/utils/refusal.ts"],"sourcesContent":["/**\n * A refusal the CLI raised itself, having said why: nothing broke.\n *\n * `bin/rebase.js` ends every failure with \"Re-run with --debug for the stack\n * trace.\" That is right when something broke inside a command, and noise after\n * a check that decided not to run — the compose-port refusal prints the\n * container holding the port and how to move, and its stack points at the line\n * that decided to refuse. A `UsageError` (`utils/args.ts`) is the same case for\n * a command line that was wrong; this is the one for everything else.\n *\n * Marked with a property rather than left to `instanceof`, for `UsageError`'s\n * reason: the reader is `bin/rebase.js`, which imports the bundle, so the class\n * it would test against is not the one this file compiled to.\n */\nexport class Refusal extends Error {\n readonly isRefusal = true;\n\n constructor(message: string) {\n super(message);\n this.name = \"Refusal\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,IAAa,UAAb,cAA6B,MAAM;CAC/B,YAAqB;CAErB,YAAY,SAAiB;EACzB,MAAM,OAAO;EACb,KAAK,OAAO;CAChB;AACJ"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { i as failAsJson, p as requireProjectRoot } from "./project-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { a as projectResourceGraph, l as writeResourceGraphFile, n as deriveOptionsFor, o as readResourceGraphFile, r as deriveResourceGraph, s as serializeResourceGraph, t as RESOURCE_GRAPH_FILENAME } from "./derive-CsoFGAnK.js";
|
|
1
|
+
import { i as failAsJson, p as requireProjectRoot, w as wantsHelp, x as parseCommandArgs } from "./project-CasK56vN.js";
|
|
2
|
+
import { d as findBackendApp, o as ManifestError, p as loadManifest } from "./manifest-BtzDy7KQ.js";
|
|
3
|
+
import { a as projectResourceGraph, l as writeResourceGraphFile, n as deriveOptionsFor, o as readResourceGraphFile, r as deriveResourceGraph, s as serializeResourceGraph, t as RESOURCE_GRAPH_FILENAME } from "./derive-ae9UOoOB.js";
|
|
5
4
|
import chalk from "chalk";
|
|
6
5
|
import path from "path";
|
|
7
6
|
import { declaredSubscriptions } from "@rebasepro/types";
|
|
@@ -180,4 +179,4 @@ async function resourcesCommand(rawArgs) {
|
|
|
180
179
|
//#endregion
|
|
181
180
|
export { resourcesCommand };
|
|
182
181
|
|
|
183
|
-
//# sourceMappingURL=resources-
|
|
182
|
+
//# sourceMappingURL=resources-CPSC5Kcu.js.map
|