@rebasepro/cli 0.23.0 → 0.24.1-canary.g19596ef
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/bin/rebase.js +4 -1
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-Bl1I9q7M.js +525 -0
- package/dist/api-keys-Bl1I9q7M.js.map +1 -0
- package/dist/apps-CNgQJjg5.js +340 -0
- package/dist/apps-CNgQJjg5.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
- package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
- package/dist/auth-aySHEF45.js +414 -0
- package/dist/auth-aySHEF45.js.map +1 -0
- package/dist/branch-pointer-DvSHBjjz.js +2 -0
- package/dist/build-DIHWIqO4.js +336 -0
- package/dist/build-DIHWIqO4.js.map +1 -0
- package/dist/bundle-CqURVN-K.js +1621 -0
- package/dist/bundle-CqURVN-K.js.map +1 -0
- package/dist/bundle.d.ts +12 -3
- package/dist/cli.d.ts +23 -2
- package/dist/cloud-DjByQlkW.js +9667 -0
- package/dist/cloud-DjByQlkW.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
- package/dist/commands/agent-setup.d.ts +116 -0
- package/dist/commands/api-keys.d.ts +86 -23
- package/dist/commands/apps.d.ts +17 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/build.d.ts +5 -1
- package/dist/commands/cloud/auth.d.ts +4 -3
- package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
- package/dist/commands/cloud/context.d.ts +44 -5
- package/dist/commands/cloud/cron.d.ts +53 -0
- package/dist/commands/cloud/debug.d.ts +134 -0
- package/dist/commands/cloud/index.d.ts +89 -12
- package/dist/commands/cloud/token-capabilities.d.ts +76 -0
- package/dist/commands/cloud/tokens.d.ts +23 -0
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/db.d.ts +79 -25
- package/dist/commands/dev.d.ts +181 -13
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/generate_sdk.d.ts +29 -2
- package/dist/commands/init.d.ts +115 -14
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/skills.d.ts +126 -7
- package/dist/commands/start.d.ts +44 -0
- package/dist/commands/storage.d.ts +46 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/consent-ClRHmcxf.js +76 -0
- package/dist/consent-ClRHmcxf.js.map +1 -0
- package/dist/console-D6SJf8k_.js +102 -0
- package/dist/console-D6SJf8k_.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
- package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
- package/dist/daemon-pzsQwDHa.js +2 -0
- package/dist/db-B7RuNuaQ.js +1312 -0
- package/dist/db-B7RuNuaQ.js.map +1 -0
- package/dist/derive-Dl0z2OWi.js +484 -0
- package/dist/derive-Dl0z2OWi.js.map +1 -0
- package/dist/dev-CYr_WXan.js +1599 -0
- package/dist/dev-CYr_WXan.js.map +1 -0
- package/dist/dev-db/atlas-dev-database.d.ts +32 -0
- package/dist/dev-db/daemon-entry.d.ts +13 -0
- package/dist/dev-db/daemon.d.ts +10 -0
- package/dist/dev-db/last-dev-database.d.ts +41 -0
- package/dist/dev-db/prepare.d.ts +29 -3
- package/dist/dev-db/pull.d.ts +90 -5
- package/dist/dev-db/resolve.d.ts +26 -2
- package/dist/dev-preflight-B-LYyBf1.js +2 -0
- package/dist/dev-server-DG8Tt-uf.js +96 -0
- package/dist/dev-server-DG8Tt-uf.js.map +1 -0
- package/dist/doctor-BP8cZ-7F.js +771 -0
- package/dist/doctor-BP8cZ-7F.js.map +1 -0
- package/dist/eject-DizdoecV.js +335 -0
- package/dist/eject-DizdoecV.js.map +1 -0
- package/dist/fold-static-BC9CYicw.js +201 -0
- package/dist/fold-static-BC9CYicw.js.map +1 -0
- package/dist/fold-static.d.ts +26 -3
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-BPqdJc3Q.js +489 -0
- package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
- package/dist/index.es.js +199 -22095
- package/dist/index.es.js.map +1 -1
- package/dist/init-Utstvh2Q.js +1149 -0
- package/dist/init-Utstvh2Q.js.map +1 -0
- package/dist/last-dev-database-B58g89uA.js +2 -0
- package/dist/manifest-DRp0UVTj.js +880 -0
- package/dist/manifest-DRp0UVTj.js.map +1 -0
- package/dist/manifest.d.ts +56 -4
- package/dist/normalize-imports-X0Tgze9y.js +107 -0
- package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
- package/dist/package-manager-DSEOwTqM.js +193 -0
- package/dist/package-manager-DSEOwTqM.js.map +1 -0
- package/dist/path-flags-BFgG9V6y.js +91 -0
- package/dist/path-flags-BFgG9V6y.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-CDN6lSpz.js +1236 -0
- package/dist/prepare-CDN6lSpz.js.map +1 -0
- package/dist/{project-3WCbrrfW.js → project-Wp2zoyeN.js} +34 -184
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/pull-CrGYMEsy.js +458 -0
- package/dist/pull-CrGYMEsy.js.map +1 -0
- package/dist/resolve-B2O-wKKk.js +2 -0
- package/dist/resources-FD7AfZx8.js +183 -0
- package/dist/resources-FD7AfZx8.js.map +1 -0
- package/dist/schema-DMdr431S.js +125 -0
- package/dist/schema-DMdr431S.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-DEa-hOBh.js +267 -0
- package/dist/start-DEa-hOBh.js.map +1 -0
- package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
- package/dist/state-Do7ZJYkF.js.map +1 -0
- package/dist/status-DxnJTekv.js +352 -0
- package/dist/status-DxnJTekv.js.map +1 -0
- package/dist/storage-DyVq-f_F.js +303 -0
- package/dist/storage-DyVq-f_F.js.map +1 -0
- package/dist/telemetry/identity.d.ts +10 -1
- package/dist/telemetry-C8Uq8phb.js +129 -0
- package/dist/telemetry-C8Uq8phb.js.map +1 -0
- package/dist/telemetry-EMFRVRQi.js +351 -0
- package/dist/telemetry-EMFRVRQi.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-CtZtRkTE.js +1332 -0
- package/dist/upgrade-CtZtRkTE.js.map +1 -0
- package/dist/upgrade.d.ts +38 -5
- package/dist/utils/dev-preflight.d.ts +47 -5
- package/dist/utils/dev-server.d.ts +30 -0
- package/dist/utils/path-flags.d.ts +31 -0
- package/dist/utils/project.d.ts +16 -0
- package/dist/utils/temp-archive.d.ts +9 -0
- package/dist/version-Buyyo1ut.js +58 -0
- package/dist/version-Buyyo1ut.js.map +1 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +4 -2
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +42 -4
- package/templates/eject/docker-compose.custom.yml +8 -4
- package/templates/generated-schemas/blank.ts +40 -0
- package/templates/generated-schemas/blog.ts +127 -0
- package/templates/generated-schemas/ecommerce.ts +93 -0
- package/templates/overlays/baas/ai-instructions.md +1 -1
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/overlays/baas/config/storage.ts +5 -3
- package/templates/overlays/baas/package.json +1 -0
- package/templates/template/.cursorrules +1 -1
- package/templates/template/.env.example +20 -10
- package/templates/template/.github/copilot-instructions.md +1 -1
- package/templates/template/.mcp.json +2 -2
- package/templates/template/.windsurfrules +1 -1
- package/templates/template/AGENTS.md +1 -1
- package/templates/template/CLAUDE.md +1 -1
- package/templates/template/README.md +21 -15
- package/templates/template/ai-instructions.md +5 -4
- package/templates/template/backend/functions/hello.ts +8 -0
- package/templates/template/backend/package.json +1 -1
- package/templates/template/config/resources.ts +7 -6
- package/templates/template/config/storage.ts +5 -3
- package/templates/template/docker-compose.yml +15 -9
- package/templates/template/frontend/vite.config.ts +13 -3
- package/templates/template/gitignore +6 -1
- package/templates/template/package.json +5 -3
- package/templates/template/scripts/example.ts +6 -4
- package/templates/template/tsconfig.json +25 -0
- package/dist/branch-pointer-dBiClJ0p.js +0 -111
- package/dist/branch-pointer-dBiClJ0p.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-DR9Tk0La.js +0 -355
- package/dist/daemon-DR9Tk0La.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-3WCbrrfW.js.map +0 -1
- package/dist/pull-Dywm5FWP.js +0 -232
- package/dist/pull-Dywm5FWP.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
- package/templates/template/backend/src/schema.generated.ts +0 -13
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-BP8cZ-7F.js","names":[],"sources":["../src/utils/libpq-url.ts","../src/doctor-environment.ts","../src/commands/doctor.ts"],"sourcesContent":["/**\n * Detect Postgres connection strings that libpq refuses to parse.\n *\n * libpq splits a URI query parameter on the FIRST `=` and rejects any further\n * one in the same parameter:\n *\n * extra key/value separator \"=\" in URI query parameter: \"options\"\n *\n * That makes a URL like\n *\n * postgresql://u:p@h:5432/db?options=-c%20search_path=public&sslmode=disable\n *\n * unusable by every libpq caller — `pg_dump`/`pg_restore` behind\n * `rebase db backup|restore`, and a plain `psql \"$DATABASE_URL\"`. It is not a\n * version quirk: Postgres 15 through 18 all refuse it.\n *\n * `rebase init` generated exactly that string until 2026-08-18, and the failure\n * is invisible day to day because node-postgres parses URLs itself and accepts\n * it — so `rebase dev` and `rebase db push` work while backups do not. Fixing\n * the generator does nothing for projects that already exist, which is what\n * these functions are for: `rebase doctor` reads them off disk and reports.\n */\n\n/** A query parameter whose value carries a literal `=`. */\nexport interface UnparseableParam {\n /** The parameter name, e.g. \"options\". */\n name: string;\n /** The raw `name=value` text as it appears in the URL. */\n raw: string;\n}\n\n/** Postgres URI schemes. A key/value DSN (\"host=… dbname=…\") is not one. */\nconst POSTGRES_URI_RE = /^postgres(ql)?:\\/\\//i;\n\n/**\n * Return the query parameters libpq would reject, or an empty array.\n *\n * Deliberately not built on `new URL()`: that parser is happy to accept the\n * broken form (it splits on the first `=` and keeps the rest as the value),\n * so it cannot see the defect at all. The rule being checked is libpq's, and\n * it is about the raw text.\n */\nexport function findUnparseableParams(connectionString: string): UnparseableParam[] {\n if (!POSTGRES_URI_RE.test(connectionString)) return [];\n\n const q = connectionString.indexOf(\"?\");\n if (q === -1) return [];\n\n // A fragment is not part of the query.\n const query = connectionString.slice(q + 1).split(\"#\")[0];\n if (!query) return [];\n\n const found: UnparseableParam[] = [];\n for (const part of query.split(\"&\")) {\n if (!part) continue;\n // One separator is the parameter's own; any further one is the defect.\n if (part.split(\"=\").length > 2) {\n found.push({ name: part.slice(0, part.indexOf(\"=\")), raw: part });\n }\n }\n return found;\n}\n\n/**\n * Percent-encode the offending `=` characters, leaving everything else byte for\n * byte as it was.\n *\n * Only the second and later `=` in a parameter are rewritten — the first is the\n * real separator. Nothing else is touched: re-serialising through `URL` would\n * also reorder parameters and turn spaces into `+`, which libpq does not decode,\n * so a \"tidy up\" would trade one unparseable string for another.\n */\nexport function encodeExtraEquals(connectionString: string): string {\n if (findUnparseableParams(connectionString).length === 0) return connectionString;\n\n const q = connectionString.indexOf(\"?\");\n const head = connectionString.slice(0, q + 1);\n const rest = connectionString.slice(q + 1);\n const hash = rest.indexOf(\"#\");\n const query = hash === -1 ? rest : rest.slice(0, hash);\n const fragment = hash === -1 ? \"\" : rest.slice(hash);\n\n const fixed = query\n .split(\"&\")\n .map((part) => {\n const first = part.indexOf(\"=\");\n if (first === -1) return part;\n const name = part.slice(0, first);\n const value = part.slice(first + 1).replace(/=/g, \"%3D\");\n return `${name}=${value}`;\n })\n .join(\"&\");\n\n return `${head}${fixed}${fragment}`;\n}\n\n/** One connection string, found somewhere on disk, that libpq cannot parse. */\nexport interface LibpqUrlFinding {\n /** File it came from, relative to the project root. */\n file: string;\n /** The variable holding it, e.g. \"DATABASE_URL\". */\n variable: string;\n /** Parameter names libpq objects to. */\n params: string[];\n /** The same string with the offending `=` encoded. */\n suggested: string;\n}\n\n/** Variables whose value is a Postgres connection string. */\nconst CONNECTION_VARIABLES = [\"DATABASE_URL\", \"ADMIN_CONNECTION_STRING\"];\n\n/**\n * Scan one file's text for connection strings libpq would reject.\n *\n * Handles both shapes a scaffolded project uses, because both shipped with the\n * defect and a deployed stack is broken by the compose one alone:\n *\n * .env DATABASE_URL=postgresql://…\n * docker-compose.yml DATABASE_URL: postgresql://…\n */\nexport function scanTextForLibpqUrls(file: string, text: string): LibpqUrlFinding[] {\n const findings: LibpqUrlFinding[] = [];\n\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n // Commented-out examples are not what the project runs on.\n if (trimmed.startsWith(\"#\")) continue;\n\n for (const variable of CONNECTION_VARIABLES) {\n // `NAME=value` or `NAME: value`, optionally exported/indented.\n const match = trimmed.match(\n new RegExp(`^(?:export\\\\s+)?${variable}\\\\s*[:=]\\\\s*(.+)$`)\n );\n if (!match) continue;\n\n // Strip surrounding quotes and any trailing comment-free whitespace.\n let value = match[1].trim();\n const quoted = value.match(/^(['\"])(.*)\\1$/);\n if (quoted) value = quoted[2];\n if (!value) continue;\n\n const params = findUnparseableParams(value);\n if (params.length === 0) continue;\n\n findings.push({\n file,\n variable,\n params: params.map((p) => p.name),\n suggested: encodeExtraEquals(value)\n });\n }\n }\n\n return findings;\n}\n","/**\n * The checks that need no database, and that catch what breaks a first run.\n *\n * `rebase doctor` compared three descriptions of a schema, which is the right\n * check for a project that is working and the wrong one for a project that has\n * never worked. Everything that stops a first run — the wrong Node, two\n * lockfiles disagreeing, two collections claiming one slug, a `JWT_SECRET`\n * short enough to be guessed, a half-installed workspace with three versions of\n * `@rebasepro/server` in it — happens before a single table is compared, and\n * doctor said nothing about any of it.\n *\n * Every function here is pure: it takes what it read and returns findings. The\n * reading and the printing are at the edges, in `commands/doctor.ts`, so each\n * check has a fixture that fails it without a filesystem, a project or a\n * database.\n */\n\n/** One thing that is wrong, and what to do about it. */\nexport interface EnvironmentFinding {\n /** Which check produced it, for grouping in the report. */\n check: \"node\" | \"package-manager\" | \"collections\" | \"env\" | \"versions\" | \"atlas\";\n severity: \"error\" | \"warning\";\n message: string;\n fix: string;\n}\n\n// ── Atlas ────────────────────────────────────────────────────────────────\n\n/** What is on disk for `@ariga/atlas`, read by `commands/doctor.ts`. */\nexport interface AtlasBinaryState {\n /** A `node_modules/.bin/atlas` the project's commands would find. */\n onPath: boolean;\n /** `@ariga/atlas/package.json` resolves from the project. */\n packageInstalled: boolean;\n /** The binary the package's `bin` names exists as a file. */\n binaryOnDisk: boolean;\n /** For the remedy: the reader's own package manager. */\n manager: string;\n}\n\n/**\n * Whether `db push`, `db generate` and `db migrate` can run at all.\n *\n * They shell out to `atlas`, and its absence is invisible until one of them is\n * run: `@ariga/atlas` downloads its platform binary in `preinstall`, pnpm 10+\n * and npm 12+ refuse a dependency's lifecycle scripts unless allowlisted, and\n * the install exits 0 either way. The scaffold allowlists it, so this is a\n * problem for the person who added `@rebasepro/server-postgres` to a project\n * they already had — five lines of `ERR_PNPM_IGNORED_BUILDS` and\n * `Failed to create bin … ENOENT`, several screens up, with nothing saying that\n * a schema push will fail later.\n *\n * Three states, because two of them need opposite advice. The package absent is\n * an install; the package present with no binary is an allowlist; the binary\n * present with no `.bin` shim is a re-install, and telling that reader their\n * build scripts are blocked sends them to a command that does nothing.\n */\nexport function checkAtlasBinary(state: AtlasBinaryState | null): EnvironmentFinding[] {\n if (!state || state.onPath) return [];\n\n if (!state.packageInstalled) {\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"The atlas binary is not on PATH and @ariga/atlas is not installed — \"\n + \"`db push`, `db generate` and `db migrate` will fail.\",\n fix: `Install it: ${state.manager} add -D @ariga/atlas, and allow its install script.`\n }];\n }\n\n if (!state.binaryOnDisk) {\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"@ariga/atlas is installed and its binary is missing — its `preinstall` \"\n + \"script was blocked, so `db push` and `db migrate` will fail.\",\n fix: \"Allow the build script (pnpm: `allowBuilds` in pnpm-workspace.yaml or \"\n + \"`pnpm approve-builds`; npm: `allowScripts` in package.json), then re-install.\"\n }];\n }\n\n return [{\n check: \"atlas\",\n severity: \"warning\",\n message: \"The atlas binary is on disk but node_modules/.bin/atlas is missing, \"\n + \"so nothing can invoke it.\",\n fix: \"Re-resolve the tree so the link is recreated: `pnpm install --force`, or delete \"\n + \"node_modules and install again.\"\n }];\n}\n\n// ── Node ─────────────────────────────────────────────────────────────────\n\n/**\n * `20.11.0` → `[20, 11, 0]`, ignoring anything after the patch.\n *\n * A bare major is a version: `>=20` is how every `engines.node` in this repo is\n * written, and requiring a minor here made the whole check return nothing —\n * silently, which is the only way a check like this ever fails.\n */\nfunction parseVersion(raw: string): [number, number, number] | null {\n const match = raw.trim().replace(/^v/, \"\").match(/^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/);\n if (!match) return null;\n return [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)];\n}\n\n/** The lowest version a `>=x.y.z` range admits, or null for anything else. */\nexport function minimumFromRange(range: string): [number, number, number] | null {\n const match = range.match(/>=\\s*v?(\\d+(?:\\.\\d+)?(?:\\.\\d+)?)/);\n return match ? parseVersion(match[1]) : null;\n}\n\nfunction isBelow(actual: [number, number, number], minimum: [number, number, number]): boolean {\n for (let i = 0; i < 3; i++) {\n if (actual[i] < minimum[i]) return true;\n if (actual[i] > minimum[i]) return false;\n }\n return false;\n}\n\n/**\n * Is the running Node old enough to break things that will not say so?\n *\n * The symptoms are not \"unsupported Node\". They are a syntax error inside a\n * dependency, a missing global, or — the one that costs the most time — a\n * `node:` builtin that resolves and then behaves differently. Naming the\n * version is the whole check.\n */\nexport function checkNodeVersion(running: string, required: string | undefined): EnvironmentFinding[] {\n if (!required) return [];\n const minimum = minimumFromRange(required);\n const actual = parseVersion(running);\n if (!minimum || !actual) return [];\n if (!isBelow(actual, minimum)) return [];\n\n return [{\n check: \"node\",\n severity: \"error\",\n message: `Node ${running} is running, and Rebase needs ${required}.`,\n fix: `Install Node ${minimum[0]} or newer (nvm install ${minimum[0]}), then reinstall dependencies.`\n }];\n}\n\n// ── Package manager ──────────────────────────────────────────────────────\n\n/** The lockfiles a project may have, and the manager each one belongs to. */\nexport const LOCKFILES: Record<string, string> = {\n \"pnpm-lock.yaml\": \"pnpm\",\n \"package-lock.json\": \"npm\",\n \"yarn.lock\": \"yarn\",\n \"bun.lockb\": \"bun\"\n};\n\n/**\n * Two package managers in one project.\n *\n * A Rebase project is a pnpm workspace, and running `npm install` inside one\n * rewrites `node_modules` into a hoisted layout that pnpm then disagrees with.\n * The result is not an install error: it is `Cannot find module` from inside a\n * dependency, hours later, on a machine where it worked yesterday. The\n * leftover `package-lock.json` is the only durable evidence, and nothing looked\n * at it.\n */\nexport function checkPackageManager(\n lockfilesPresent: string[],\n declared: string | undefined\n): EnvironmentFinding[] {\n const known = lockfilesPresent.filter(file => file in LOCKFILES);\n if (known.length < 2) {\n // One lockfile, or none. A single one that disagrees with\n // `packageManager` is worth saying too.\n const declaredName = declared?.split(\"@\")[0];\n if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) {\n return [{\n check: \"package-manager\",\n severity: \"warning\",\n message: `This project declares packageManager \"${declared}\" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,\n fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`\n }];\n }\n return [];\n }\n\n const managers = known.map(file => LOCKFILES[file]);\n return [{\n check: \"package-manager\",\n severity: \"error\",\n message: `Two package managers have installed here: ${known.join(\" and \")}.`,\n fix: `Keep one. Delete the others and node_modules, then reinstall with ${declared?.split(\"@\")[0] ?? managers[0]}.`\n }];\n}\n\n// ── Collections ──────────────────────────────────────────────────────────\n\n/**\n * Two collections claiming one slug.\n *\n * The slug is the route, the table lookup and the registry key, and the\n * registry keeps the last one registered. So the loser is not reported missing:\n * it is served as the winner, with the winner's properties, under its own name.\n * Every symptom of this points somewhere else.\n */\nexport function checkDuplicateSlugs(\n collections: Array<{ slug?: string; name?: string }>\n): EnvironmentFinding[] {\n const byslug = new Map<string, number>();\n for (const collection of collections) {\n if (!collection.slug) continue;\n byslug.set(collection.slug, (byslug.get(collection.slug) ?? 0) + 1);\n }\n\n return [...byslug]\n .filter(([, count]) => count > 1)\n .map(([slug, count]) => ({\n check: \"collections\" as const,\n severity: \"error\" as const,\n message: `${count} collections declare the slug \"${slug}\". Only the last one registered is served.`,\n fix: `Give each collection its own slug. The one that loses is not reported missing — it answers as the winner.`\n }));\n}\n\n// ── .env ─────────────────────────────────────────────────────────────────\n\n/** What `loadEnv` requires of a JWT secret. Mirrors `rebaseEnvSchema`. */\nexport const MIN_JWT_SECRET_LENGTH = 32;\n\n/**\n * The two `.env` facts that fail late and confusingly.\n *\n * A short `JWT_SECRET` boots fine and is refused by `loadEnv` only in\n * production, which is the worst place to find out. Missing CORS in production\n * is the opposite shape — it does not fail at all; the API simply accepts\n * requests from anywhere.\n *\n * Values are never echoed. This reads a file that holds every credential the\n * project has, and doctor's output goes wherever a terminal goes.\n */\nexport function checkEnvSanity(values: Record<string, string>): EnvironmentFinding[] {\n const findings: EnvironmentFinding[] = [];\n const production = values.NODE_ENV === \"production\";\n\n const secret = values.JWT_SECRET;\n if (secret !== undefined && secret.length > 0 && secret.length < MIN_JWT_SECRET_LENGTH) {\n findings.push({\n check: \"env\",\n severity: production ? \"error\" : \"warning\",\n message: `JWT_SECRET is ${secret.length} characters. The runtime requires at least ${MIN_JWT_SECRET_LENGTH}, and refuses to boot below it in production.`,\n fix: \"Generate one: openssl rand -hex 32\"\n });\n }\n\n if (production && !values.CORS_ORIGINS && !values.FRONTEND_URL) {\n findings.push({\n check: \"env\",\n severity: \"error\",\n message: \"NODE_ENV=production with neither CORS_ORIGINS nor FRONTEND_URL set — the API will accept requests from any origin.\",\n fix: \"Set CORS_ORIGINS to the origins that may call this API.\"\n });\n }\n\n return findings;\n}\n\n/**\n * Parse a `.env` into a plain object.\n *\n * Deliberately not `dotenv`: this must not touch `process.env`, and doctor may\n * be reading a file for a different environment than the one it is running in.\n */\nexport function parseEnvFile(text: string): Record<string, string> {\n const values: Record<string, string> = {};\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq <= 0) continue;\n const key = trimmed.slice(0, eq).replace(/^export\\s+/, \"\").trim();\n let value = trimmed.slice(eq + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n values[key] = value;\n }\n return values;\n}\n\n// ── Version skew ─────────────────────────────────────────────────────────\n\n/** One `@rebasepro/*` dependency, and where it was declared. */\nexport interface DeclaredDependency {\n /** Where it was declared, relative to the project root. */\n file: string;\n name: string;\n /** The range exactly as written. */\n range: string;\n}\n\n/**\n * The same `@rebasepro/*` package pinned to different versions in one project.\n *\n * A Rebase project declares them in several package.json files — the root, the\n * backend, the frontend, the config workspace — and nothing has ever compared\n * them. The failure that produces is not an install error: pnpm installs both,\n * `instanceof` stops working across the two copies, and the symptom is a\n * `CollectionConfig` that fails a type guard written against an identical type.\n *\n * `workspace:` ranges are excluded: inside this repo's own workspaces they all\n * resolve to the same build by construction.\n */\nexport function checkVersionSkew(declared: DeclaredDependency[]): EnvironmentFinding[] {\n const byPackage = new Map<string, Map<string, string[]>>();\n\n for (const dependency of declared) {\n if (!dependency.name.startsWith(\"@rebasepro/\")) continue;\n if (dependency.range.startsWith(\"workspace:\") || dependency.range.startsWith(\"link:\")) continue;\n if (dependency.range === \"*\" || dependency.range === \"latest\") continue;\n\n const ranges = byPackage.get(dependency.name) ?? new Map<string, string[]>();\n ranges.set(dependency.range, [...(ranges.get(dependency.range) ?? []), dependency.file]);\n byPackage.set(dependency.name, ranges);\n }\n\n const findings: EnvironmentFinding[] = [];\n for (const [name, ranges] of byPackage) {\n if (ranges.size < 2) continue;\n const described = [...ranges]\n .map(([range, files]) => `${range} (${files.join(\", \")})`)\n .join(\", \");\n findings.push({\n check: \"versions\",\n severity: \"error\",\n message: `${name} is pinned to different versions in this project: ${described}.`,\n fix: \"Run `rebase upgrade` to pin every @rebasepro package to one release and reinstall. Two copies of a Rebase package break `instanceof` between them, which fails as a type guard rejecting its own type.\"\n });\n }\n return findings;\n}\n","/**\n * CLI command: rebase doctor\n *\n * Detects three-way schema drift between collection definitions,\n * the generated Drizzle schema, and the live PostgreSQL database.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n getActiveBackendPlugin,\n resolvePluginCliScript,\n resolveTsx,\n findEnvFile,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { fileURLToPath, pathToFileURL } from \"url\";\nimport { createRequire } from \"module\";\nimport { scanTextForLibpqUrls, type LibpqUrlFinding } from \"../utils/libpq-url\";\nimport { analyseFunctionsDirectory, summarisePortability } from \"../function-portability\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\nimport { absolutizeLocalPathArgs } from \"./db\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { loadManifest, findBackendApp, resolveBackendPaths } from \"../manifest\";\nimport { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport {\n checkDuplicateSlugs,\n checkEnvSanity,\n checkAtlasBinary,\n checkNodeVersion,\n checkPackageManager,\n checkVersionSkew,\n LOCKFILES,\n parseEnvFile,\n type AtlasBinaryState,\n type DeclaredDependency,\n type EnvironmentFinding\n} from \"../doctor-environment\";\n\n/**\n * Files that can hold a connection string the project actually runs on.\n *\n * `.env` is what local commands read; the compose files are what a self-hosted\n * stack and its scheduled backup cron read. Both shipped with the defect, and a\n * deployed stack is broken by the compose one even when `.env` has been fixed.\n * `.env.example` is deliberately absent: nothing runs on it.\n */\nconst CONNECTION_FILES = [\n \".env\",\n \".env.local\",\n \"docker-compose.yml\",\n \"docker-compose.yaml\",\n \"docker-compose.custom.yml\"\n];\n\n/**\n * Find connection strings libpq cannot parse, anywhere in the project.\n *\n * Exported for the tests; `envFile` is passed separately because a project may\n * keep its `.env` outside the root (see `findEnvFile`).\n */\nexport function findLibpqUrlProblems(projectRoot: string, envFile?: string | null): LibpqUrlFinding[] {\n const candidates = CONNECTION_FILES.map((f) => path.join(projectRoot, f));\n if (envFile) candidates.push(envFile);\n\n const findings: LibpqUrlFinding[] = [];\n const seen = new Set<string>();\n\n for (const file of candidates) {\n if (seen.has(file)) continue;\n seen.add(file);\n let text: string;\n try {\n if (!fs.existsSync(file)) continue;\n text = fs.readFileSync(file, \"utf-8\");\n } catch {\n // Unreadable is not a finding — doctor reports what it can see.\n continue;\n }\n findings.push(...scanTextForLibpqUrls(path.relative(projectRoot, file) || path.basename(file), text));\n }\n\n return findings;\n}\n\n/**\n * Report unparseable connection strings, if any. Returns true when there were,\n * so the finding reaches the exit code like the environment errors do.\n *\n * Runs before the plugin's drift check and never blocks it. The plugin connects\n * through node-postgres, which parses these URLs happily — so it cannot see this\n * defect, and a project with it will otherwise get a clean bill of health while\n * `rebase db backup` fails.\n */\nfunction reportLibpqUrlProblems(findings: LibpqUrlFinding[]): boolean {\n if (findings.length === 0) return false;\n\n console.log(\"\");\n console.log(chalk.red.bold(\" ✗ Connection string that PostgreSQL's own tools cannot parse\"));\n console.log(\"\");\n for (const f of findings) {\n console.log(` ${chalk.bold(f.file)} → ${chalk.bold(f.variable)}`);\n console.log(chalk.gray(` the \"${f.params.join('\", \"')}\" parameter contains an unencoded \"=\".`));\n console.log(chalk.gray(\" Replace the value with:\"));\n console.log(` ${chalk.cyan(f.suggested)}`);\n console.log(\"\");\n }\n console.log(chalk.gray(\" libpq splits a query parameter on the first \\\"=\\\" and rejects any further\"));\n console.log(chalk.gray(\" one, so this fails:\"));\n console.log(chalk.gray(\" extra key/value separator \\\"=\\\" in URI query parameter\"));\n console.log(\"\");\n console.log(chalk.gray(\" Affects rebase db backup / restore, scheduled backups, and psql.\"));\n console.log(chalk.gray(\" NOT rebase dev or db push — those use a driver that accepts it, which\"));\n console.log(chalk.gray(\" is why a project can look healthy and still have no working backups.\"));\n console.log(chalk.gray(\" Projects scaffolded before 2026-08-18 all carry it.\"));\n console.log(\"\");\n return true;\n}\n\n/**\n * `--help` is answered before the project guard, not after.\n *\n * `doctor` declared no `--help` at all, so the flag fell through to the command\n * body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a\n * project answered \"✗ Could not find a Rebase project root.\" Asking a command\n * what it does is the one question that cannot require being somewhere\n * particular to ask.\n */\n/**\n * What each custom function needs from its host.\n *\n * Prints nothing when there is nothing to say — which is the common case, and\n * the reason it can afford to run every time. When it does print, the ordering\n * is deliberate: the one finding that is a bug *today* comes first, and the\n * rest is a single descriptive line about where these functions could run.\n */\nfunction reportFunctionPortability(projectRoot: string): void {\n let results: ReturnType<typeof analyseFunctionsDirectory>;\n try {\n const backend = findBackendApp(loadManifest(projectRoot).manifest);\n if (!backend) return;\n const paths = resolveBackendPaths(backend.app, projectRoot);\n results = analyseFunctionsDirectory(path.join(projectRoot, paths.functions), projectRoot);\n } catch {\n // A project with no manifest, or one shaped differently — doctor has\n // plenty else to report, and this section is advisory.\n return;\n }\n\n const lines = summarisePortability(results);\n if (lines.length === 0) return;\n\n console.log(chalk.bold(\"\\nCustom functions\"));\n for (const line of lines) {\n console.log(line.trimStart().startsWith(\"⚠\") ? chalk.yellow(line) : chalk.gray(line));\n }\n console.log(chalk.gray(\" See https://rebase.pro/docs/backend/custom-functions#runtime-portability\"));\n}\n\n/**\n * Read everything the environment checks need, then run them.\n *\n * The reading is here and the deciding is in `doctor-environment.ts`, so every\n * check has a fixture that fails it without a project on disk.\n */\nexport function collectEnvironmentFindings(\n projectRoot: string,\n envFile: string | null | undefined\n): EnvironmentFinding[] {\n const findings: EnvironmentFinding[] = [];\n\n // Node, against the engines range the installed CLI declares.\n findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));\n\n // Two package managers in one project.\n const rootPackage = readJson(path.join(projectRoot, \"package.json\"));\n findings.push(...checkPackageManager(\n Object.keys(LOCKFILES).filter(file => fs.existsSync(path.join(projectRoot, file))),\n typeof rootPackage?.packageManager === \"string\" ? rootPackage.packageManager : undefined\n ));\n\n // Two collections claiming one slug. Read as text rather than imported: a\n // collection file can import anything, and doctor must not execute a\n // project's code to tell it two slugs collide.\n findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));\n\n // `.env`, values never echoed.\n if (envFile && fs.existsSync(envFile)) {\n try {\n findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, \"utf-8\"))));\n } catch { /* unreadable is not a finding */ }\n }\n\n // One `@rebasepro/*` package, two versions.\n findings.push(...checkVersionSkew(readDeclaredRebaseDeps(projectRoot)));\n\n // The atlas binary, which every schema command shells out to and whose\n // absence is invisible until one of them is run.\n findings.push(...checkAtlasBinary(readAtlasBinaryState(projectRoot)));\n\n return findings;\n}\n\n/**\n * What is actually on disk for `@ariga/atlas`, from the project's point of view.\n *\n * Three separate questions, because the three states they distinguish need\n * three different remedies — see `checkAtlasBinary`. Directories are searched\n * from the project root upwards and through the workspaces a scaffold has,\n * because `db push` runs from `backend/` and the install may be hoisted to the\n * root.\n */\nexport function readAtlasBinaryState(projectRoot: string): AtlasBinaryState | null {\n const roots = [projectRoot, path.join(projectRoot, \"backend\"), path.join(projectRoot, \"config\")];\n\n const findUpwards = (starts: string[], relative: string): string | null => {\n for (const start of starts) {\n let dir = start;\n for (let i = 0; i < 8; i++) {\n const candidate = path.join(dir, \"node_modules\", relative);\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n }\n return null;\n };\n\n // `@ariga/atlas` is a dependency of `@rebasepro/server-postgres`, not of the\n // project, so under pnpm's isolated layout — the scaffold default — it lives\n // in `.pnpm/` and a directory walk over `node_modules/` never sees it.\n // Resolve through the module graph the way the driver itself does.\n const resolveFrom = (from: string, request: string): string | null => {\n try {\n return createRequire(pathToFileURL(from)).resolve(request);\n } catch {\n return null;\n }\n };\n const bases = roots.map(dir => path.join(dir, \"package.json\")).filter(file => fs.existsSync(file));\n const driverManifests = bases\n .map(base => resolveFrom(base, \"@rebasepro/server-postgres/package.json\"))\n .filter((file): file is string => file !== null);\n\n let manifest: string | null = findUpwards(roots, path.join(\"@ariga\", \"atlas\", \"package.json\"));\n if (!manifest) {\n for (const base of [...bases, ...driverManifests]) {\n manifest = resolveFrom(base, \"@ariga/atlas/package.json\");\n if (manifest) break;\n }\n }\n\n // The `.bin` shim is written beside whichever package declared the\n // dependency, so the driver's own directory is a search root too.\n const onPath = findUpwards(\n [...roots, ...driverManifests.map(file => path.dirname(file))],\n path.join(\".bin\", \"atlas\")\n ) !== null;\n\n let binaryOnDisk = false;\n if (manifest) {\n try {\n const pkg = JSON.parse(fs.readFileSync(manifest, \"utf-8\")) as {\n bin?: string | Record<string, string>;\n };\n const dir = path.dirname(manifest);\n const targets = typeof pkg.bin === \"string\"\n ? [pkg.bin]\n : Object.values(pkg.bin ?? {}).filter((t): t is string => typeof t === \"string\");\n binaryOnDisk = targets.some(target => fs.existsSync(path.resolve(dir, target)));\n } catch { /* unreadable manifest: treat the binary as absent */ }\n }\n\n return {\n onPath,\n packageInstalled: manifest !== null,\n binaryOnDisk,\n manager: readPackageManagerName(projectRoot)\n };\n}\n\n/** The reader's own package manager, from the lockfile beside their project. */\nfunction readPackageManagerName(projectRoot: string): string {\n for (const [file, manager] of Object.entries(LOCKFILES)) {\n if (fs.existsSync(path.join(projectRoot, file))) return manager;\n }\n return \"pnpm\";\n}\n\n/** The `engines.node` range of the CLI actually running. */\nfunction readCliEngines(): string | undefined {\n try {\n const here = path.dirname(fileURLToPath(import.meta.url));\n for (let dir = here, i = 0; i < 5; i++) {\n const candidate = path.join(dir, \"package.json\");\n if (fs.existsSync(candidate)) {\n const parsed = readJson(candidate);\n if (parsed?.name === \"@rebasepro/cli\") {\n const engines = parsed.engines as { node?: string } | undefined;\n return engines?.node;\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n } catch { /* the check is advisory */ }\n return undefined;\n}\n\nfunction readJson(file: string): Record<string, unknown> | undefined {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf-8\")) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n\n/** Where a project's package.json files live, relative to its root. */\nconst PACKAGE_JSON_LOCATIONS = [\"package.json\", \"backend/package.json\", \"frontend/package.json\", \"config/package.json\"];\n\nfunction readDeclaredRebaseDeps(projectRoot: string): DeclaredDependency[] {\n const declared: DeclaredDependency[] = [];\n for (const relative of PACKAGE_JSON_LOCATIONS) {\n const parsed = readJson(path.join(projectRoot, relative));\n if (!parsed) continue;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"] as const) {\n const block = parsed[field] as Record<string, string> | undefined;\n if (!block) continue;\n for (const [name, range] of Object.entries(block)) {\n if (typeof range === \"string\") declared.push({ file: relative, name, range });\n }\n }\n }\n return declared;\n}\n\n/**\n * Does this project declare its collections in code?\n *\n * `resolveBackendPaths` answers it against the manifest's own `config` path\n * rather than the convention, because a project may move the directory. A\n * project with no manifest, or one shaped differently, is treated as declaring\n * them: the wrong answer there would silently skip the drift report, which is\n * the reason the command exists.\n */\nexport function hasDeclaredCollections(projectRoot: string): boolean {\n try {\n const backend = findBackendApp(loadManifest(projectRoot).manifest);\n if (!backend) return true;\n return resolveBackendPaths(backend.app, projectRoot).hasCollections;\n } catch {\n return true;\n }\n}\n\n/**\n * Every `slug:` a collection file declares, read as text.\n *\n * Deliberately not by importing them: a collection file may import anything the\n * project depends on, and doctor must not run a project's code to tell it two\n * slugs collide. A regex over `slug: \"...\"` finds the literal in every shape\n * the scaffold and the docs use; a computed slug is simply not seen, which is\n * the right failure for a check that only ever reports.\n */\nfunction readDeclaredSlugs(projectRoot: string): Array<{ slug: string }> {\n const dir = path.join(projectRoot, \"config\", \"collections\");\n if (!fs.existsSync(dir)) return [];\n const slugs: Array<{ slug: string }> = [];\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n return [];\n }\n for (const entry of entries) {\n if (!/\\.(ts|js|mts|mjs)$/.test(entry) || entry.startsWith(\"index.\")) continue;\n try {\n const text = fs.readFileSync(path.join(dir, entry), \"utf-8\");\n for (const match of text.matchAll(/\\bslug:\\s*[\"'`]([^\"'`]+)[\"'`]/g)) {\n slugs.push({ slug: match[1] });\n }\n } catch { /* unreadable is not a finding */ }\n }\n return slugs;\n}\n\n/** Print what the environment checks found, grouped, or nothing at all. */\nfunction reportEnvironmentFindings(findings: EnvironmentFinding[]): boolean {\n if (findings.length === 0) return false;\n\n console.log(\"\");\n console.log(chalk.bold(\" Environment\"));\n for (const finding of findings) {\n const mark = finding.severity === \"error\" ? chalk.red(\" ✗\") : chalk.yellow(\" ⚠\");\n console.log(`${mark} ${finding.message}`);\n console.log(chalk.gray(` ${finding.fix}`));\n }\n console.log(\"\");\n return findings.some(finding => finding.severity === \"error\");\n}\n\n/**\n * The flags `rebase doctor` takes, and what each is for.\n *\n * One table, read by both the parser and the help page, because they were two\n * things that had to agree and did not: the page listed no flags at all, so\n * `--policies` — the form `cli/index.md` calls \"the form to use as a CI gate\" —\n * was invisible to anyone who ran `rebase doctor --help` to find it. And with\n * nothing parsing the line here, a typo travelled to the driver's permissive\n * parser and was silently dropped, so `rebase doctor --polices` ran the ordinary\n * doctor and reported success on a gate that never ran.\n *\n * The spec mirrors the driver's own (`doctorPluginCommand` in\n * `server-postgres/src/cli.ts`), because the line is relayed to it verbatim.\n */\nexport const DOCTOR_FLAGS = {\n \"--policies\": Boolean,\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--schema\": String,\n \"-s\": \"--schema\",\n \"--sdk\": String,\n \"-k\": \"--sdk\"\n} as const;\n\n/** What each long flag above does, for the Options block. */\nconst DOCTOR_FLAG_HELP: Record<string, string> = {\n \"--policies\": \"Also audit the row-level security policies. The form to use as a CI gate.\",\n \"--collections\": COLLECTIONS_FLAG_HELP,\n \"--schema\": \"Path to the generated Drizzle schema (default: backend/src/schema.generated.ts)\",\n \"--sdk\": \"Path to the generated SDK types (default: generated/sdk/database.types.ts)\"\n};\n\n/**\n * The line handed to the driver, every path on it resolved from where the\n * command was run — the driver runs in `backend/`. See `utils/path-flags.ts`.\n */\nexport function doctorDriverArgs(rawArgs: string[], cwd: string): string[] {\n return absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"doctor\"), cwd, [\"--collections\", \"-c\", \"--schema\", \"-s\", \"--sdk\", \"-k\"]);\n}\n\n/** `--collections, -c` — the aliases, resolved from the spec rather than retyped. */\nfunction doctorFlagLabel(flag: string): string {\n const aliases = Object.entries(DOCTOR_FLAGS)\n .filter(([, target]) => target === flag)\n .map(([alias]) => alias);\n return [flag, ...aliases].join(\", \");\n}\n\nfunction printDoctorHelp(): void {\n console.log(`\n${chalk.bold(\"rebase doctor\")} — Check a project for drift and misconfiguration\n\n${chalk.green.bold(\"Usage\")}\n rebase doctor [options]\n\n${chalk.green.bold(\"Options\")}\n${Object.keys(DOCTOR_FLAG_HELP)\n .map(flag => ` ${chalk.blue(doctorFlagLabel(flag).padEnd(22))} ${DOCTOR_FLAG_HELP[flag]}`)\n .join(\"\\n\")}\n\nCompares the collections you declare, the generated Drizzle schema, and the\ntables that actually exist, then reports what disagrees and how to reconcile it.\n\nAlso checks the connection strings in .env and the compose files for the\nunencoded \"=\" that makes PostgreSQL's own tools refuse to parse them — which\nbreaks backups and psql while leaving the app itself working.\n\nRun from inside a Rebase project — it reads the project's collections and\nconnects to its database.\n`);\n}\n\nexport async function doctorCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printDoctorHelp();\n return;\n }\n\n // Strictly, before the line is relayed. The driver's own parser is\n // `permissive`, which turns an undeclared flag into a positional rather\n // than an error, so `rebase doctor --polices` ran the ordinary doctor and\n // exited 0 — a CI gate that reported success because the flag asking for it\n // was dropped on the way.\n parseCommandArgs({\n spec: DOCTOR_FLAGS,\n rawArgs,\n commandWords: 1,\n command: \"doctor\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n const backendDir = requireBackendDir(projectRoot);\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir, \"doctor\");\n\n const activePlugin = getActiveBackendPlugin(backendDir);\n if (!activePlugin) {\n console.error(chalk.red(\"✗ Could not detect an active database plugin.\"));\n console.error(chalk.gray(\" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.\"));\n process.exit(1);\n }\n\n const pluginCli = resolvePluginCliScript(backendDir, activePlugin);\n if (!pluginCli) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // Set up environment with DOTENV_CONFIG_PATH\n const envFile = findEnvFile(projectRoot);\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? \"\";\n\n // Reported before the plugin runs: this one needs no database, and if the\n // URL is the problem then anything that tries to connect with it first will\n // fail with a worse message.\n const connectionFailed = reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));\n\n // Same reasoning: no database needed, and one of these findings — a\n // `process.env` read at module scope — is a live failure whose only symptom\n // today is a function quietly missing from `GET /api/functions`.\n reportFunctionPortability(projectRoot);\n\n // Everything that breaks a first run happens before a table is compared:\n // the wrong Node, two lockfiles disagreeing, two collections claiming one\n // slug, a JWT_SECRET short enough to be refused in production, three\n // versions of @rebasepro/server in one workspace. Doctor said nothing about\n // any of it. Reported before the plugin runs, and non-blocking — a project\n // with one of these still deserves its drift report.\n const environmentFailed = reportEnvironmentFindings(collectEnvironmentFindings(projectRoot, envFile));\n // Either is an error a CI gate must see: a connection string libpq cannot\n // parse is a project whose backups have never run.\n const failed = connectionFailed || environmentFailed;\n\n // Which database, resolved the way every other command resolves it.\n //\n // Two of doctor's three phases need one, and both used to key on\n // `DATABASE_URL` alone. On the documented first-run path that variable does\n // not exist — `rebase init` leaves it commented out and the managed\n // database fills the vacuum — so a stock scaffold always ended with\n // `⏭ Collections → Database: skipped (DATABASE_URL not set)` and\n // `⚠ No DATABASE_URL — RLS policies were NOT checked`, then told the reader\n // to set the one variable the quickstart tells them to leave alone. For the\n // command `ai-instructions.md` names as the thing to run before guessing.\n //\n // The managed daemon is started here, exactly as `rebase db url` starts it:\n // a doctor that will not start the database it is asked about can only ever\n // answer \"I did not look\".\n //\n // Last, and never fatal. Everything above needs no database, and a doctor\n // that cannot report what it *can* see because a database would not start\n // is the opposite of the job. A failure here is said out loud and the run\n // continues; the driver then reports its two database phases as skipped,\n // which is the truth.\n try {\n const prepared = await prepareDatabaseEnv(projectRoot, {\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n Object.assign(env, prepared.env);\n for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));\n } catch (error) {\n console.log(\"\");\n console.error(chalk.yellow(` ⚠ Could not resolve this project's database: ${\n error instanceof Error ? error.message : String(error)}`));\n console.error(chalk.gray(\" The checks that need one are reported below as not run.\"));\n }\n\n // A headless project has no `config/collections` by design — its API is\n // introspected from the live database at boot — and the driver's doctor\n // opens by loading them, finds none, and exits 1. So `rebase doctor` failed\n // on the documented headless path, in 0.17.3 and on main, while every check\n // above it had just passed. Its three phases all compare *against* declared\n // collections, so there is nothing for them to do here; the environment\n // findings are the whole report.\n if (!hasDeclaredCollections(projectRoot)) {\n console.log(\"\");\n console.log(chalk.gray(\n \" ○ Schema drift not checked: this project derives its API from the database — \"\n + \"run `rebase schema introspect` first.\"\n ));\n if (failed) process.exit(1);\n return;\n }\n\n try {\n const isTs = pluginCli.endsWith(\".ts\");\n if (isTs) {\n const tsxBin = resolveTsx(projectRoot);\n if (!tsxBin) {\n exitDependenciesNotInstalled(projectRoot);\n }\n await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n } else {\n await execa(\"node\", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n }\n } catch (error) {\n // A child that ran and exited non-zero already printed its diagnostics\n // through inherited stdio. A child that never started did not — and\n // \"the tsx symlink is broken\" is exactly the state `doctor` exists to\n // report, so exiting 1 in silence was the worst possible answer.\n reportSpawnFailure(error);\n process.exit(1);\n }\n\n // The plugin exited clean, so the schema agrees with itself. That is not\n // the same as the project being able to run: an environment error is still\n // an error, and a doctor that exits 0 over one is a doctor nobody can gate\n // on.\n if (failed) process.exit(1);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { p as requireProjectRoot } from "./project-Wp2zoyeN.js";
|
|
2
|
+
import { n as parseCommandArgs, r as wantsHelp } from "./args-BnmsEXvu.js";
|
|
3
|
+
import { d as findBackendApp, g as resolveBackendPaths, o as ManifestError, p as loadManifest, x as writeManifest } from "./manifest-DRp0UVTj.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
//#region src/commands/eject.ts
|
|
9
|
+
/**
|
|
10
|
+
* CLI command: rebase eject
|
|
11
|
+
*
|
|
12
|
+
* The supported route from the managed runtime to a custom one.
|
|
13
|
+
*
|
|
14
|
+
* Without it, `runtime: "custom"` is a mode a user can only reach by
|
|
15
|
+
* hand-writing an entrypoint they have never seen. The template used to solve
|
|
16
|
+
* that by scaffolding `backend/src/index.ts` into every project — ~190 lines
|
|
17
|
+
* configuring CORS, auth, cookies, storage and history, which the managed
|
|
18
|
+
* runtime never loads. It was the most important-looking file in a new project
|
|
19
|
+
* and editing it did nothing.
|
|
20
|
+
*
|
|
21
|
+
* So the file moved here. A managed project does not carry it; a project that
|
|
22
|
+
* asks for it gets it together with the Dockerfile and the manifest change that
|
|
23
|
+
* make it actually run.
|
|
24
|
+
*
|
|
25
|
+
* There is deliberately no `rebase uneject`. Going back is deleting two files
|
|
26
|
+
* and editing one line, and a command that silently discarded a user's server
|
|
27
|
+
* code would be worse than its absence.
|
|
28
|
+
*/
|
|
29
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
/** Walk up to the package root, which holds `templates/`. */
|
|
31
|
+
function findCliRoot(from) {
|
|
32
|
+
const root = path.parse(from).root;
|
|
33
|
+
let dir = from;
|
|
34
|
+
while (dir && dir !== root) {
|
|
35
|
+
if (fs.existsSync(path.join(dir, "templates", "eject"))) return dir;
|
|
36
|
+
dir = path.dirname(dir);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The block names the payload may switch on. A typo has to be an error.
|
|
42
|
+
*
|
|
43
|
+
* Typed as keys of {@link ProjectShape}, which is what makes that true: `as
|
|
44
|
+
* const` alone gave these two string literals and tied them to nothing, so a
|
|
45
|
+
* flag renamed on the interface left this list naming a block that no longer
|
|
46
|
+
* exists, and the template kept switching on it.
|
|
47
|
+
*/
|
|
48
|
+
var SHAPE_FLAGS = ["collections", "frontend"];
|
|
49
|
+
/** Narrow a marker name to a flag, having checked it is one. */
|
|
50
|
+
function isShapeFlag(name) {
|
|
51
|
+
return SHAPE_FLAGS.includes(name);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render one payload file for this project.
|
|
55
|
+
*
|
|
56
|
+
* The payload is one set of files rather than one per flavour, because the
|
|
57
|
+
* flavours differ by about a dozen lines and two copies of a 230-line
|
|
58
|
+
* entrypoint are how copies drift — the payload had already drifted from
|
|
59
|
+
* `app/backend/src/index.ts` over `cronsDir`, which silently stopped every cron
|
|
60
|
+
* job in an ejected project. So both branches live in the file, marked with
|
|
61
|
+
* lines that are comments in TypeScript, YAML and Dockerfiles alike:
|
|
62
|
+
*
|
|
63
|
+
* // {{#collections}}
|
|
64
|
+
* import { tables } from "./schema.generated.js";
|
|
65
|
+
* // {{/collections}}
|
|
66
|
+
* // {{^collections}}
|
|
67
|
+
* // No schema module: this project introspects the database.
|
|
68
|
+
* // {{/collections}}
|
|
69
|
+
*
|
|
70
|
+
* `{{#name}}` keeps its block when the flag is on, `{{^name}}` when it is off,
|
|
71
|
+
* and the marker lines themselves never reach the user. The template stays
|
|
72
|
+
* valid TypeScript with every marker line removed, which is the flavour a
|
|
73
|
+
* typechecker would see.
|
|
74
|
+
*/
|
|
75
|
+
function renderPayload(contents, shape, projectName) {
|
|
76
|
+
const out = [];
|
|
77
|
+
let open = null;
|
|
78
|
+
for (const line of contents.split("\n")) {
|
|
79
|
+
const marker = /^\s*(?:\/\/|#)\s*\{\{([#^/])([A-Za-z]+)\}\}\s*$/.exec(line);
|
|
80
|
+
if (!marker) {
|
|
81
|
+
if (!open || open.keep) out.push(line);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const [, kind, name] = marker;
|
|
85
|
+
if (kind === "/") {
|
|
86
|
+
if (!open || open.name !== name) throw new Error(`Eject template: {{/${name}}} does not close an open block.`);
|
|
87
|
+
open = null;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
|
|
91
|
+
if (!isShapeFlag(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
|
|
92
|
+
open = {
|
|
93
|
+
name,
|
|
94
|
+
keep: kind === "#" ? shape[name] === true : shape[name] !== true
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
98
|
+
return out.join("\n").replace(/\{\{PROJECT_NAME\}\}/g, () => projectName);
|
|
99
|
+
}
|
|
100
|
+
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
101
|
+
var PAYLOAD = [
|
|
102
|
+
{
|
|
103
|
+
from: "backend/src/index.ts",
|
|
104
|
+
to: "backend/src/index.ts",
|
|
105
|
+
overwrite: true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
from: "backend/src/env.ts",
|
|
109
|
+
to: "backend/src/env.ts",
|
|
110
|
+
overwrite: true
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
from: "config/resources.ts",
|
|
114
|
+
to: "config/resources.ts",
|
|
115
|
+
overwrite: false
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
from: "Dockerfile",
|
|
119
|
+
to: "Dockerfile",
|
|
120
|
+
overwrite: false
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
from: "docker-compose.custom.yml",
|
|
124
|
+
to: "docker-compose.custom.yml",
|
|
125
|
+
overwrite: false
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
/**
|
|
129
|
+
* The project's name, for the `{{PROJECT_NAME}}` the payload carries.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to the directory name — a compose project name is cosmetic, and a
|
|
132
|
+
* missing or unreadable package.json is not a reason to refuse to eject.
|
|
133
|
+
*/
|
|
134
|
+
function projectNameOf(projectRoot) {
|
|
135
|
+
try {
|
|
136
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
|
|
137
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim();
|
|
138
|
+
} catch {}
|
|
139
|
+
return path.basename(projectRoot);
|
|
140
|
+
}
|
|
141
|
+
function printHelp() {
|
|
142
|
+
console.log(`
|
|
143
|
+
${chalk.bold("rebase eject")} — take ownership of the server process
|
|
144
|
+
|
|
145
|
+
Writes the backend entrypoint and a Dockerfile into this project, and flips its
|
|
146
|
+
backend to ${chalk.cyan("runtime: \"custom\"")}. From then on this repository builds its own
|
|
147
|
+
image: platform runtime upgrades no longer reach it, and CORS, auth wiring,
|
|
148
|
+
storage and shutdown become yours to configure.
|
|
149
|
+
|
|
150
|
+
${chalk.bold("Usage")}
|
|
151
|
+
rebase eject [app] Take ownership of the server process
|
|
152
|
+
|
|
153
|
+
${chalk.bold("Options")}
|
|
154
|
+
--dry-run List what would change, and change nothing
|
|
155
|
+
--force Replace an existing backend/src/index.ts or
|
|
156
|
+
env.ts, keeping the current file as <name>.bak
|
|
157
|
+
-h, --help Show this help
|
|
158
|
+
`.trim());
|
|
159
|
+
}
|
|
160
|
+
async function ejectCommand(rawArgs = []) {
|
|
161
|
+
if (wantsHelp(rawArgs)) {
|
|
162
|
+
printHelp();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
166
|
+
spec: {
|
|
167
|
+
"--dry-run": Boolean,
|
|
168
|
+
"--force": Boolean
|
|
169
|
+
},
|
|
170
|
+
rawArgs,
|
|
171
|
+
commandWords: 1,
|
|
172
|
+
command: "eject",
|
|
173
|
+
maxPositionals: 1
|
|
174
|
+
});
|
|
175
|
+
const projectRoot = requireProjectRoot();
|
|
176
|
+
const dryRun = Boolean(args["--dry-run"]);
|
|
177
|
+
const force = Boolean(args["--force"]);
|
|
178
|
+
const requested = positionals[0];
|
|
179
|
+
let loaded;
|
|
180
|
+
try {
|
|
181
|
+
loaded = loadManifest(projectRoot);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof ManifestError) {
|
|
184
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
185
|
+
for (const issue of err.issues) console.error(chalk.dim(` ${issue.path}: ${issue.message}`));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
const { manifest } = loaded;
|
|
191
|
+
let appName;
|
|
192
|
+
let app;
|
|
193
|
+
if (requested) {
|
|
194
|
+
const declared = manifest.apps[requested];
|
|
195
|
+
if (!declared) {
|
|
196
|
+
console.error(chalk.red(`✗ No app named "${requested}" in rebase.json.`));
|
|
197
|
+
console.error(chalk.dim(` Declared: ${Object.keys(manifest.apps).join(", ") || "(none)"}`));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
if (declared.type !== "backend") {
|
|
201
|
+
console.error(chalk.red(`✗ "${requested}" is a ${declared.type} app — only a backend can be ejected.`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
appName = requested;
|
|
205
|
+
app = declared;
|
|
206
|
+
} else {
|
|
207
|
+
const backend = findBackendApp(manifest);
|
|
208
|
+
if (!backend) {
|
|
209
|
+
console.error(chalk.red("✗ This repository declares no backend app."));
|
|
210
|
+
console.error(chalk.dim(" Only the repository that declares the backend chooses its runtime."));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
appName = backend.name;
|
|
214
|
+
app = backend.app;
|
|
215
|
+
}
|
|
216
|
+
if (app.runtime === "custom") {
|
|
217
|
+
console.error(chalk.red(`✗ "${appName}" is already ejected — it declares runtime: "custom".`));
|
|
218
|
+
console.error(chalk.dim(` Its entrypoint is ${app.dockerfile ?? "Dockerfile"} and backend/src/index.ts.`));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const cliRoot = findCliRoot(__dirname);
|
|
222
|
+
if (!cliRoot) {
|
|
223
|
+
console.error(chalk.red("✗ Could not locate the eject templates. Reinstall @rebasepro/cli."));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const payloadDir = path.join(cliRoot, "templates", "eject");
|
|
227
|
+
const shape = {
|
|
228
|
+
collections: resolveBackendPaths(app, projectRoot).hasCollections,
|
|
229
|
+
frontend: fs.existsSync(path.join(projectRoot, "frontend"))
|
|
230
|
+
};
|
|
231
|
+
const planned = [];
|
|
232
|
+
const blocked = [];
|
|
233
|
+
for (const file of PAYLOAD) {
|
|
234
|
+
const source = path.join(payloadDir, file.from);
|
|
235
|
+
if (!fs.existsSync(source)) {
|
|
236
|
+
console.error(chalk.red(`✗ The eject template is missing ${file.from}. Reinstall @rebasepro/cli.`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
240
|
+
if (exists && file.overwrite && !force) blocked.push(file.to);
|
|
241
|
+
planned.push({
|
|
242
|
+
to: file.to,
|
|
243
|
+
action: !exists ? "write" : file.overwrite ? "overwrite" : "keep"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (blocked.length > 0) {
|
|
247
|
+
console.error(chalk.red("✗ Ejecting would replace a file this project already has:"));
|
|
248
|
+
for (const item of blocked) console.error(chalk.red(` ${item}`));
|
|
249
|
+
console.error(chalk.dim(" Eject writes its own entrypoint — it does not adopt yours."));
|
|
250
|
+
console.error(chalk.dim(" Move the file aside, or re-run with --force, which keeps the current"));
|
|
251
|
+
console.error(chalk.dim(" contents as <name>.bak."));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
if (dryRun) {
|
|
255
|
+
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
256
|
+
console.log("");
|
|
257
|
+
for (const item of planned) if (item.action === "write") console.log(` ${chalk.green("write")} ${item.to}`);
|
|
258
|
+
else if (item.action === "overwrite") console.log(` ${chalk.yellow("overwrite")} ${item.to} ${chalk.dim(`(kept as ${item.to}.bak)`)}`);
|
|
259
|
+
else console.log(` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
|
|
260
|
+
console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
|
|
261
|
+
if (fs.existsSync(path.join(projectRoot, "backend", "package.json"))) console.log(` ${chalk.green("write")} backend/package.json ${chalk.dim("(main, dev and start scripts)")}`);
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log(chalk.dim("Nothing was changed."));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const projectName = projectNameOf(projectRoot);
|
|
267
|
+
const backups = [];
|
|
268
|
+
for (const [index, file] of PAYLOAD.entries()) {
|
|
269
|
+
if (planned[index].action === "keep") continue;
|
|
270
|
+
const destination = path.join(projectRoot, file.to);
|
|
271
|
+
if (planned[index].action === "overwrite") {
|
|
272
|
+
fs.copyFileSync(destination, `${destination}.bak`);
|
|
273
|
+
backups.push(`${file.to}.bak`);
|
|
274
|
+
}
|
|
275
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
276
|
+
const contents = renderPayload(fs.readFileSync(path.join(payloadDir, file.from), "utf8"), shape, projectName);
|
|
277
|
+
fs.writeFileSync(destination, contents, "utf8");
|
|
278
|
+
}
|
|
279
|
+
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
280
|
+
manifest.apps[appName] = {
|
|
281
|
+
...app,
|
|
282
|
+
runtime: "custom",
|
|
283
|
+
dockerfile,
|
|
284
|
+
port: app.port ?? 8080
|
|
285
|
+
};
|
|
286
|
+
writeManifest(projectRoot, manifest);
|
|
287
|
+
restoreBackendScripts(projectRoot);
|
|
288
|
+
console.log("");
|
|
289
|
+
console.log(chalk.green(`✓ Ejected "${appName}" to a custom runtime.`));
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log(` ${chalk.cyan("backend/src/index.ts".padEnd(26))} your entrypoint — the runtime no longer boots the bundle`);
|
|
292
|
+
console.log(` ${chalk.cyan("backend/src/env.ts".padEnd(26))} the environment it reads`);
|
|
293
|
+
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
294
|
+
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
295
|
+
console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
|
|
296
|
+
for (const backup of backups) console.log(` ${chalk.cyan(backup.padEnd(26))} what was there before`);
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
299
|
+
console.log(chalk.yellow(" upgrades no longer reach this project."));
|
|
300
|
+
if (!shape.collections) {
|
|
301
|
+
console.log(chalk.yellow(" This project declares no collections, so the entrypoint derives them"));
|
|
302
|
+
console.log(chalk.yellow(" from the live database, as the managed runtime did."));
|
|
303
|
+
}
|
|
304
|
+
console.log("");
|
|
305
|
+
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
306
|
+
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
307
|
+
console.log("");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Point the backend workspace's scripts at the entrypoint that now exists.
|
|
311
|
+
*
|
|
312
|
+
* A managed project's backend package deliberately declares no `main` and no
|
|
313
|
+
* `start`: there is no entrypoint to name. Ejecting creates one.
|
|
314
|
+
*/
|
|
315
|
+
function restoreBackendScripts(projectRoot) {
|
|
316
|
+
const packagePath = path.join(projectRoot, "backend", "package.json");
|
|
317
|
+
if (!fs.existsSync(packagePath)) return;
|
|
318
|
+
let parsed;
|
|
319
|
+
try {
|
|
320
|
+
parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
321
|
+
} catch {
|
|
322
|
+
console.log(chalk.yellow(" ⚠ backend/package.json is not valid JSON — its scripts were left alone."));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const scripts = parsed.scripts ?? {};
|
|
326
|
+
parsed.main ??= "src/index.ts";
|
|
327
|
+
scripts.dev ??= "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts";
|
|
328
|
+
scripts.start ??= "node dist/backend/src/index.js";
|
|
329
|
+
parsed.scripts = scripts;
|
|
330
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 4)}\n`, "utf8");
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
export { ejectCommand };
|
|
334
|
+
|
|
335
|
+
//# sourceMappingURL=eject-DizdoecV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eject-DizdoecV.js","names":[],"sources":["../src/commands/eject.ts"],"sourcesContent":["/**\n * CLI command: rebase eject\n *\n * The supported route from the managed runtime to a custom one.\n *\n * Without it, `runtime: \"custom\"` is a mode a user can only reach by\n * hand-writing an entrypoint they have never seen. The template used to solve\n * that by scaffolding `backend/src/index.ts` into every project — ~190 lines\n * configuring CORS, auth, cookies, storage and history, which the managed\n * runtime never loads. It was the most important-looking file in a new project\n * and editing it did nothing.\n *\n * So the file moved here. A managed project does not carry it; a project that\n * asks for it gets it together with the Dockerfile and the manifest change that\n * make it actually run.\n *\n * There is deliberately no `rebase uneject`. Going back is deleting two files\n * and editing one line, and a command that silently discarded a user's server\n * code would be worse than its absence.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport type { RebaseBackendAppConfig } from \"@rebasepro/types\";\nimport { requireProjectRoot } from \"../utils/project\";\nimport { findBackendApp, loadManifest, ManifestError, resolveBackendPaths, writeManifest } from \"../manifest\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/** Walk up to the package root, which holds `templates/`. */\nfunction findCliRoot(from: string): string | null {\n const root = path.parse(from).root;\n let dir = from;\n while (dir && dir !== root) {\n if (fs.existsSync(path.join(dir, \"templates\", \"eject\"))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\n/**\n * The two ways one project's payload differs from another's.\n *\n * Neither is a flag: both were decided at `rebase init` time. `--headless`\n * deletes `config/collections`, `backend/src/schema.generated.ts` and\n * `frontend/`, so a payload that assumes them emits an entrypoint importing\n * three files that are not there and a Dockerfile whose first `COPY` fails.\n */\ninterface ProjectShape {\n /**\n * Collections declared in code, under `<config>/collections`. Without them\n * the server derives its API from the live database instead — the same\n * signal `rebase build` reads.\n */\n collections: boolean;\n /** A `frontend` workspace for the image to build and the entrypoint to serve. */\n frontend: boolean;\n}\n\n/**\n * The block names the payload may switch on. A typo has to be an error.\n *\n * Typed as keys of {@link ProjectShape}, which is what makes that true: `as\n * const` alone gave these two string literals and tied them to nothing, so a\n * flag renamed on the interface left this list naming a block that no longer\n * exists, and the template kept switching on it.\n */\nconst SHAPE_FLAGS: readonly (keyof ProjectShape)[] = [\"collections\", \"frontend\"];\n\n/** Narrow a marker name to a flag, having checked it is one. */\nfunction isShapeFlag(name: string): name is keyof ProjectShape {\n return (SHAPE_FLAGS as readonly string[]).includes(name);\n}\n\n/**\n * Render one payload file for this project.\n *\n * The payload is one set of files rather than one per flavour, because the\n * flavours differ by about a dozen lines and two copies of a 230-line\n * entrypoint are how copies drift — the payload had already drifted from\n * `app/backend/src/index.ts` over `cronsDir`, which silently stopped every cron\n * job in an ejected project. So both branches live in the file, marked with\n * lines that are comments in TypeScript, YAML and Dockerfiles alike:\n *\n * // {{#collections}}\n * import { tables } from \"./schema.generated.js\";\n * // {{/collections}}\n * // {{^collections}}\n * // No schema module: this project introspects the database.\n * // {{/collections}}\n *\n * `{{#name}}` keeps its block when the flag is on, `{{^name}}` when it is off,\n * and the marker lines themselves never reach the user. The template stays\n * valid TypeScript with every marker line removed, which is the flavour a\n * typechecker would see.\n */\nexport function renderPayload(contents: string, shape: ProjectShape, projectName: string): string {\n const out: string[] = [];\n let open: { name: string; keep: boolean } | null = null;\n\n for (const line of contents.split(\"\\n\")) {\n const marker = /^\\s*(?:\\/\\/|#)\\s*\\{\\{([#^/])([A-Za-z]+)\\}\\}\\s*$/.exec(line);\n if (!marker) {\n if (!open || open.keep) out.push(line);\n continue;\n }\n const [, kind, name] = marker;\n if (kind === \"/\") {\n if (!open || open.name !== name) {\n throw new Error(`Eject template: {{/${name}}} does not close an open block.`);\n }\n open = null;\n continue;\n }\n if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);\n if (!isShapeFlag(name)) {\n throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);\n }\n // `shape[name]` directly: the check above narrows `name` to a key of\n // `ProjectShape`, which is what the `as unknown as Record<string,\n // boolean>` on the shape was standing in for — and that cast also meant\n // nothing checked that the block names and the interface agree.\n open = { name,\nkeep: kind === \"#\" ? shape[name] === true : shape[name] !== true };\n }\n\n if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);\n // A replacer function: the name is read from package.json unchecked, and a\n // replacement string would expand any `$&`, `$'` or `$$` in it.\n return out.join(\"\\n\").replace(/\\{\\{PROJECT_NAME\\}\\}/g, () => projectName);\n}\n\n/** Files the eject payload contributes, as `<source> → <destination>`. */\nconst PAYLOAD: { from: string; to: string; overwrite: boolean }[] = [\n // The entrypoint IS the point of ejecting, so it replaces whatever is\n // there — but only with `--force`, and only after keeping the old file as\n // `.bak`. `runtime: \"custom\"` (the guard below) means \"already ejected\",\n // not \"nobody wrote a server here\": a managed project can have a\n // hand-written `backend/src/index.ts` that `rebase dev` has been running.\n { from: \"backend/src/index.ts\",\nto: \"backend/src/index.ts\",\noverwrite: true },\n { from: \"backend/src/env.ts\",\nto: \"backend/src/env.ts\",\noverwrite: true },\n // The declarations the emitted server imports. Never overwritten — a\n // project that already declares its resources owns that file, and replacing\n // it would silently drop every bucket and database it named. Written when\n // absent because the entrypoint imports it unconditionally: an ejected\n // server that cannot resolve its own resources does not start.\n { from: \"config/resources.ts\",\nto: \"config/resources.ts\",\noverwrite: false },\n // Never overwritten: a Dockerfile someone already wrote is theirs, and so is\n // a compose file they have edited. The scaffolded `docker-compose.yml` is\n // deliberately left alone — it runs the managed shape, and going back should\n // stay a one-line change rather than a restore from git.\n { from: \"Dockerfile\",\nto: \"Dockerfile\",\noverwrite: false },\n { from: \"docker-compose.custom.yml\",\nto: \"docker-compose.custom.yml\",\noverwrite: false }\n];\n\n/**\n * The project's name, for the `{{PROJECT_NAME}}` the payload carries.\n *\n * Falls back to the directory name — a compose project name is cosmetic, and a\n * missing or unreadable package.json is not a reason to refuse to eject.\n */\nfunction projectNameOf(projectRoot: string): string {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, \"package.json\"), \"utf8\"));\n if (typeof pkg.name === \"string\" && pkg.name.trim()) return pkg.name.trim();\n } catch {\n // fall through\n }\n return path.basename(projectRoot);\n}\n\nfunction printHelp(): void {\n console.log(`\n${chalk.bold(\"rebase eject\")} — take ownership of the server process\n\nWrites the backend entrypoint and a Dockerfile into this project, and flips its\nbackend to ${chalk.cyan('runtime: \"custom\"')}. From then on this repository builds its own\nimage: platform runtime upgrades no longer reach it, and CORS, auth wiring,\nstorage and shutdown become yours to configure.\n\n${chalk.bold(\"Usage\")}\n rebase eject [app] Take ownership of the server process\n\n${chalk.bold(\"Options\")}\n --dry-run List what would change, and change nothing\n --force Replace an existing backend/src/index.ts or\n env.ts, keeping the current file as <name>.bak\n -h, --help Show this help\n`.trim());\n}\n\nexport async function ejectCommand(rawArgs: string[] = []): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printHelp();\n return;\n }\n\n const { flags: args, positionals } = parseCommandArgs({\n spec: {\n \"--dry-run\": Boolean,\n \"--force\": Boolean\n },\n rawArgs,\n commandWords: 1,\n command: \"eject\",\n maxPositionals: 1\n });\n\n const projectRoot = requireProjectRoot();\n const dryRun = Boolean(args[\"--dry-run\"]);\n const force = Boolean(args[\"--force\"]);\n // Strict parsing rejects an undeclared flag rather than filing it under the\n // positionals, so the first one is the app — there is nothing to filter out.\n const requested = positionals[0];\n\n let loaded;\n try {\n loaded = loadManifest(projectRoot);\n } catch (err) {\n if (err instanceof ManifestError) {\n console.error(chalk.red(`✗ ${err.message}`));\n for (const issue of err.issues) {\n console.error(chalk.dim(` ${issue.path}: ${issue.message}`));\n }\n process.exit(1);\n }\n throw err;\n }\n\n const { manifest } = loaded;\n\n let appName: string;\n let app: RebaseBackendAppConfig;\n\n if (requested) {\n const declared = manifest.apps[requested];\n if (!declared) {\n console.error(chalk.red(`✗ No app named \"${requested}\" in rebase.json.`));\n console.error(chalk.dim(` Declared: ${Object.keys(manifest.apps).join(\", \") || \"(none)\"}`));\n process.exit(1);\n }\n if (declared.type !== \"backend\") {\n // Ejecting is about who runs the server. A static app has no server.\n console.error(chalk.red(`✗ \"${requested}\" is a ${declared.type} app — only a backend can be ejected.`));\n process.exit(1);\n }\n appName = requested;\n app = declared;\n } else {\n const backend = findBackendApp(manifest);\n if (!backend) {\n console.error(chalk.red(\"✗ This repository declares no backend app.\"));\n console.error(chalk.dim(\" Only the repository that declares the backend chooses its runtime.\"));\n process.exit(1);\n }\n appName = backend.name;\n app = backend.app;\n }\n\n if (app.runtime === \"custom\") {\n console.error(chalk.red(`✗ \"${appName}\" is already ejected — it declares runtime: \"custom\".`));\n console.error(chalk.dim(` Its entrypoint is ${app.dockerfile ?? \"Dockerfile\"} and backend/src/index.ts.`));\n process.exit(1);\n }\n\n const cliRoot = findCliRoot(__dirname);\n if (!cliRoot) {\n console.error(chalk.red(\"✗ Could not locate the eject templates. Reinstall @rebasepro/cli.\"));\n process.exit(1);\n }\n const payloadDir = path.join(cliRoot!, \"templates\", \"eject\");\n\n // What the payload has to look like here. Read from the project rather than\n // asked for: `--headless` left no collections and no frontend, and an\n // entrypoint importing either would not compile.\n const paths = resolveBackendPaths(app, projectRoot);\n const shape: ProjectShape = {\n collections: paths.hasCollections,\n frontend: fs.existsSync(path.join(projectRoot, \"frontend\"))\n };\n\n // Decide everything before writing anything, so --dry-run and the real run\n // report the same list and a mid-way failure cannot leave a half-ejected\n // project.\n const planned: { to: string; action: \"write\" | \"keep\" | \"overwrite\" }[] = [];\n const blocked: string[] = [];\n for (const file of PAYLOAD) {\n const source = path.join(payloadDir, file.from);\n if (!fs.existsSync(source)) {\n console.error(chalk.red(`✗ The eject template is missing ${file.from}. Reinstall @rebasepro/cli.`));\n process.exit(1);\n }\n const exists = fs.existsSync(path.join(projectRoot, file.to));\n if (exists && file.overwrite && !force) blocked.push(file.to);\n planned.push({\n to: file.to,\n action: !exists ? \"write\" : file.overwrite ? \"overwrite\" : \"keep\"\n });\n }\n\n if (blocked.length > 0) {\n // A managed backend never loads `backend/src/index.ts`, but `rebase dev`\n // does — it picks the entrypoint by file existence — so this file is very\n // often a server someone is running and editing. Replacing it without\n // being asked is the one unrecoverable thing this command could do.\n console.error(chalk.red(\"✗ Ejecting would replace a file this project already has:\"));\n for (const item of blocked) console.error(chalk.red(` ${item}`));\n console.error(chalk.dim(\" Eject writes its own entrypoint — it does not adopt yours.\"));\n console.error(chalk.dim(\" Move the file aside, or re-run with --force, which keeps the current\"));\n console.error(chalk.dim(\" contents as <name>.bak.\"));\n process.exit(1);\n }\n\n if (dryRun) {\n console.log(chalk.bold(`Would eject \"${appName}\" to a custom runtime:`));\n console.log(\"\");\n for (const item of planned) {\n if (item.action === \"write\") console.log(` ${chalk.green(\"write\")} ${item.to}`);\n else if (item.action === \"overwrite\") {\n console.log(` ${chalk.yellow(\"overwrite\")} ${item.to} ${chalk.dim(`(kept as ${item.to}.bak)`)}`);\n } else console.log(` ${chalk.dim(\"keep\")} ${item.to} ${chalk.dim(\"(already exists)\")}`);\n }\n console.log(` ${chalk.green(\"write\")} rebase.json ${chalk.dim('(runtime: \"custom\")')}`);\n // Not part of the payload table, but it is changed on the real run, and\n // a dry run's whole purpose is to enumerate what changes.\n if (fs.existsSync(path.join(projectRoot, \"backend\", \"package.json\"))) {\n console.log(` ${chalk.green(\"write\")} backend/package.json ${chalk.dim(\"(main, dev and start scripts)\")}`);\n }\n console.log(\"\");\n console.log(chalk.dim(\"Nothing was changed.\"));\n return;\n }\n\n const projectName = projectNameOf(projectRoot);\n const backups: string[] = [];\n for (const [index, file] of PAYLOAD.entries()) {\n if (planned[index].action === \"keep\") continue;\n const destination = path.join(projectRoot, file.to);\n if (planned[index].action === \"overwrite\") {\n fs.copyFileSync(destination, `${destination}.bak`);\n backups.push(`${file.to}.bak`);\n }\n fs.mkdirSync(path.dirname(destination), { recursive: true });\n const contents = renderPayload(\n fs.readFileSync(path.join(payloadDir, file.from), \"utf8\"),\n shape,\n projectName\n );\n fs.writeFileSync(destination, contents, \"utf8\");\n }\n\n const dockerfile = app.dockerfile ?? \"Dockerfile\";\n manifest.apps[appName] = {\n ...app,\n runtime: \"custom\",\n dockerfile,\n port: app.port ?? 8080\n };\n writeManifest(projectRoot, manifest);\n\n // The backend workspace stops being a package the runtime reads and becomes\n // one that is run. Its scripts have to say so, or `npm start` in the image\n // has nothing to call.\n restoreBackendScripts(projectRoot);\n\n console.log(\"\");\n console.log(chalk.green(`✓ Ejected \"${appName}\" to a custom runtime.`));\n console.log(\"\");\n console.log(` ${chalk.cyan(\"backend/src/index.ts\".padEnd(26))} your entrypoint — the runtime no longer boots the bundle`);\n console.log(` ${chalk.cyan(\"backend/src/env.ts\".padEnd(26))} the environment it reads`);\n console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);\n console.log(` ${chalk.cyan(\"docker-compose.custom.yml\".padEnd(26))} runs it`);\n console.log(` ${chalk.cyan(\"rebase.json\".padEnd(26))} runtime: custom`);\n for (const backup of backups) {\n console.log(` ${chalk.cyan(backup.padEnd(26))} what was there before`);\n }\n console.log(\"\");\n console.log(chalk.yellow(\" You now own CORS, auth wiring, storage and shutdown. Platform runtime\"));\n console.log(chalk.yellow(\" upgrades no longer reach this project.\"));\n if (!shape.collections) {\n console.log(chalk.yellow(\" This project declares no collections, so the entrypoint derives them\"));\n console.log(chalk.yellow(\" from the live database, as the managed runtime did.\"));\n }\n console.log(\"\");\n console.log(chalk.dim(` ${chalk.cyan(\"docker compose -f docker-compose.custom.yml up --build\")}`));\n console.log(chalk.dim(\" docker-compose.yml is untouched — it still runs the managed shape if you go back.\"));\n console.log(\"\");\n}\n\n/**\n * Point the backend workspace's scripts at the entrypoint that now exists.\n *\n * A managed project's backend package deliberately declares no `main` and no\n * `start`: there is no entrypoint to name. Ejecting creates one.\n */\nfunction restoreBackendScripts(projectRoot: string): void {\n const packagePath = path.join(projectRoot, \"backend\", \"package.json\");\n if (!fs.existsSync(packagePath)) return;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(fs.readFileSync(packagePath, \"utf8\"));\n } catch {\n // A malformed package.json is the user's to fix, and refusing to finish\n // the eject over it would leave the project half-changed.\n console.log(chalk.yellow(\" ⚠ backend/package.json is not valid JSON — its scripts were left alone.\"));\n return;\n }\n\n const scripts = (parsed.scripts ?? {}) as Record<string, string>;\n parsed.main ??= \"src/index.ts\";\n scripts.dev ??= 'tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts';\n scripts.start ??= \"node dist/backend/src/index.js\";\n parsed.scripts = scripts;\n\n fs.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 4)}\\n`, \"utf8\");\n}\n"],"mappings":"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"}
|