@rebasepro/cli 0.24.0 → 0.24.1-canary.g56af4a4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rebase.js +3 -0
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-BPBi5nOF.js +524 -0
- package/dist/api-keys-BPBi5nOF.js.map +1 -0
- package/dist/apps-CPXNElX4.js +256 -0
- package/dist/apps-CPXNElX4.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-DTMuEScS.js +62 -0
- package/dist/atlas-dev-database-DTMuEScS.js.map +1 -0
- package/dist/auth-DvY94ZuL.js +395 -0
- package/dist/auth-DvY94ZuL.js.map +1 -0
- package/dist/branch-pointer-DIhKbQLp.js +2 -0
- package/dist/build-CFwUx-Ff.js +336 -0
- package/dist/build-CFwUx-Ff.js.map +1 -0
- package/dist/bundle-BQhP_xiA.js +1617 -0
- package/dist/bundle-BQhP_xiA.js.map +1 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cloud-CkTr-yt0.js +9640 -0
- package/dist/cloud-CkTr-yt0.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/api-keys.d.ts +2 -0
- package/dist/commands/db.d.ts +37 -24
- package/dist/commands/dev.d.ts +103 -0
- package/dist/commands/generate_sdk.d.ts +26 -0
- package/dist/commands/skills.d.ts +22 -7
- package/dist/commands/storage.d.ts +46 -0
- package/dist/consent-BWHaYx5p.js +76 -0
- package/dist/consent-BWHaYx5p.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/daemon-DBn-shxX.js +2 -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/db-CMpBK3sv.js +1252 -0
- package/dist/db-CMpBK3sv.js.map +1 -0
- package/dist/derive-RPIsAVO4.js +474 -0
- package/dist/derive-RPIsAVO4.js.map +1 -0
- package/dist/dev-C9P5Yp9k.js +1593 -0
- package/dist/dev-C9P5Yp9k.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-preflight-N1evJL3S.js +2 -0
- package/dist/doctor-DYs-NqFI.js +771 -0
- package/dist/doctor-DYs-NqFI.js.map +1 -0
- package/dist/eject-BT6t37D0.js +335 -0
- package/dist/eject-BT6t37D0.js.map +1 -0
- package/dist/fold-static-C1EtHlfV.js +201 -0
- package/dist/fold-static-C1EtHlfV.js.map +1 -0
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-UV635zSe.js +477 -0
- package/dist/generate_sdk-UV635zSe.js.map +1 -0
- package/dist/index.es.js +141 -24674
- package/dist/index.es.js.map +1 -1
- package/dist/init-fXeBcxuN.js +1042 -0
- package/dist/init-fXeBcxuN.js.map +1 -0
- package/dist/manifest-DzlC60WX.js +871 -0
- package/dist/manifest-DzlC60WX.js.map +1 -0
- package/dist/normalize-imports-CmVdE9iZ.js +100 -0
- package/dist/normalize-imports-CmVdE9iZ.js.map +1 -0
- package/dist/{package-manager-CXQevsE1.js → package-manager-DSEOwTqM.js} +2 -2
- package/dist/{package-manager-CXQevsE1.js.map → package-manager-DSEOwTqM.js.map} +1 -1
- package/dist/path-flags-DUBuI2i1.js +77 -0
- package/dist/path-flags-DUBuI2i1.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-Btn9Ewbn.js +1052 -0
- package/dist/prepare-Btn9Ewbn.js.map +1 -0
- package/dist/{project-DmaGLmH2.js → project-Wp2zoyeN.js} +33 -4
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/resolve-CJLkfFV0.js +2 -0
- package/dist/resources-DT1COn_6.js +161 -0
- package/dist/resources-DT1COn_6.js.map +1 -0
- package/dist/schema-BDBWvCHu.js +125 -0
- package/dist/schema-BDBWvCHu.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-rU2e3Kp5.js +228 -0
- package/dist/start-rU2e3Kp5.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-BR9oyi-y.js +351 -0
- package/dist/status-BR9oyi-y.js.map +1 -0
- package/dist/storage-DyvrSTH3.js +303 -0
- package/dist/storage-DyvrSTH3.js.map +1 -0
- package/dist/telemetry-CAWNCugs.js +338 -0
- package/dist/telemetry-CAWNCugs.js.map +1 -0
- package/dist/telemetry-DbLqGIBW.js +129 -0
- package/dist/telemetry-DbLqGIBW.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-D-5-Od96.js +1265 -0
- package/dist/upgrade-D-5-Od96.js.map +1 -0
- package/dist/utils/project.d.ts +16 -0
- package/dist/{version-DsgBmP22.js → version-Buyyo1ut.js} +2 -2
- package/dist/{version-DsgBmP22.js.map → version-Buyyo1ut.js.map} +1 -1
- package/package.json +7 -7
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +10 -2
- package/templates/template/README.md +1 -1
- package/templates/template/ai-instructions.md +2 -1
- package/templates/template/frontend/vite.config.ts +11 -3
- package/templates/template/scripts/example.ts +2 -1
- package/dist/branch-pointer-kk9F5FBO.js +0 -111
- package/dist/branch-pointer-kk9F5FBO.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-C8p32i-n.js +0 -357
- package/dist/daemon-C8p32i-n.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-DmaGLmH2.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { f as requireBackendDir, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, o as findEnvFile, p as requireProjectRoot, r as exitDependenciesNotInstalled } from "./project-Wp2zoyeN.js";
|
|
2
|
+
import { n as parseCommandArgs, r as wantsHelp } from "./args-BnmsEXvu.js";
|
|
3
|
+
import { d as findBackendApp, g as resolveBackendPaths, p as loadManifest } from "./manifest-DzlC60WX.js";
|
|
4
|
+
import { n as summarisePortability, t as analyseFunctionsDirectory } from "./function-portability-BIvlvR9q.js";
|
|
5
|
+
import { r as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-DUBuI2i1.js";
|
|
6
|
+
import { DEV_DATABASE_KIND_ENV, devDatabaseKind, managedNotices, prepareDatabaseEnv } from "./prepare-Btn9Ewbn.js";
|
|
7
|
+
import { absolutizeLocalPathArgs, n as reportSpawnFailure, t as argsFromCommand } from "./db-CMpBK3sv.js";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
import { execa } from "execa";
|
|
14
|
+
//#region src/utils/libpq-url.ts
|
|
15
|
+
/** Postgres URI schemes. A key/value DSN ("host=… dbname=…") is not one. */
|
|
16
|
+
var POSTGRES_URI_RE = /^postgres(ql)?:\/\//i;
|
|
17
|
+
/**
|
|
18
|
+
* Return the query parameters libpq would reject, or an empty array.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately not built on `new URL()`: that parser is happy to accept the
|
|
21
|
+
* broken form (it splits on the first `=` and keeps the rest as the value),
|
|
22
|
+
* so it cannot see the defect at all. The rule being checked is libpq's, and
|
|
23
|
+
* it is about the raw text.
|
|
24
|
+
*/
|
|
25
|
+
function findUnparseableParams(connectionString) {
|
|
26
|
+
if (!POSTGRES_URI_RE.test(connectionString)) return [];
|
|
27
|
+
const q = connectionString.indexOf("?");
|
|
28
|
+
if (q === -1) return [];
|
|
29
|
+
const query = connectionString.slice(q + 1).split("#")[0];
|
|
30
|
+
if (!query) return [];
|
|
31
|
+
const found = [];
|
|
32
|
+
for (const part of query.split("&")) {
|
|
33
|
+
if (!part) continue;
|
|
34
|
+
if (part.split("=").length > 2) found.push({
|
|
35
|
+
name: part.slice(0, part.indexOf("=")),
|
|
36
|
+
raw: part
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Percent-encode the offending `=` characters, leaving everything else byte for
|
|
43
|
+
* byte as it was.
|
|
44
|
+
*
|
|
45
|
+
* Only the second and later `=` in a parameter are rewritten — the first is the
|
|
46
|
+
* real separator. Nothing else is touched: re-serialising through `URL` would
|
|
47
|
+
* also reorder parameters and turn spaces into `+`, which libpq does not decode,
|
|
48
|
+
* so a "tidy up" would trade one unparseable string for another.
|
|
49
|
+
*/
|
|
50
|
+
function encodeExtraEquals(connectionString) {
|
|
51
|
+
if (findUnparseableParams(connectionString).length === 0) return connectionString;
|
|
52
|
+
const q = connectionString.indexOf("?");
|
|
53
|
+
const head = connectionString.slice(0, q + 1);
|
|
54
|
+
const rest = connectionString.slice(q + 1);
|
|
55
|
+
const hash = rest.indexOf("#");
|
|
56
|
+
const query = hash === -1 ? rest : rest.slice(0, hash);
|
|
57
|
+
const fragment = hash === -1 ? "" : rest.slice(hash);
|
|
58
|
+
return `${head}${query.split("&").map((part) => {
|
|
59
|
+
const first = part.indexOf("=");
|
|
60
|
+
if (first === -1) return part;
|
|
61
|
+
return `${part.slice(0, first)}=${part.slice(first + 1).replace(/=/g, "%3D")}`;
|
|
62
|
+
}).join("&")}${fragment}`;
|
|
63
|
+
}
|
|
64
|
+
/** Variables whose value is a Postgres connection string. */
|
|
65
|
+
var CONNECTION_VARIABLES = ["DATABASE_URL", "ADMIN_CONNECTION_STRING"];
|
|
66
|
+
/**
|
|
67
|
+
* Scan one file's text for connection strings libpq would reject.
|
|
68
|
+
*
|
|
69
|
+
* Handles both shapes a scaffolded project uses, because both shipped with the
|
|
70
|
+
* defect and a deployed stack is broken by the compose one alone:
|
|
71
|
+
*
|
|
72
|
+
* .env DATABASE_URL=postgresql://…
|
|
73
|
+
* docker-compose.yml DATABASE_URL: postgresql://…
|
|
74
|
+
*/
|
|
75
|
+
function scanTextForLibpqUrls(file, text) {
|
|
76
|
+
const findings = [];
|
|
77
|
+
for (const line of text.split("\n")) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (trimmed.startsWith("#")) continue;
|
|
80
|
+
for (const variable of CONNECTION_VARIABLES) {
|
|
81
|
+
const match = trimmed.match(new RegExp(`^(?:export\\s+)?${variable}\\s*[:=]\\s*(.+)$`));
|
|
82
|
+
if (!match) continue;
|
|
83
|
+
let value = match[1].trim();
|
|
84
|
+
const quoted = value.match(/^(['"])(.*)\1$/);
|
|
85
|
+
if (quoted) value = quoted[2];
|
|
86
|
+
if (!value) continue;
|
|
87
|
+
const params = findUnparseableParams(value);
|
|
88
|
+
if (params.length === 0) continue;
|
|
89
|
+
findings.push({
|
|
90
|
+
file,
|
|
91
|
+
variable,
|
|
92
|
+
params: params.map((p) => p.name),
|
|
93
|
+
suggested: encodeExtraEquals(value)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/doctor-environment.ts
|
|
101
|
+
/**
|
|
102
|
+
* Whether `db push`, `db generate` and `db migrate` can run at all.
|
|
103
|
+
*
|
|
104
|
+
* They shell out to `atlas`, and its absence is invisible until one of them is
|
|
105
|
+
* run: `@ariga/atlas` downloads its platform binary in `preinstall`, pnpm 10+
|
|
106
|
+
* and npm 12+ refuse a dependency's lifecycle scripts unless allowlisted, and
|
|
107
|
+
* the install exits 0 either way. The scaffold allowlists it, so this is a
|
|
108
|
+
* problem for the person who added `@rebasepro/server-postgres` to a project
|
|
109
|
+
* they already had — five lines of `ERR_PNPM_IGNORED_BUILDS` and
|
|
110
|
+
* `Failed to create bin … ENOENT`, several screens up, with nothing saying that
|
|
111
|
+
* a schema push will fail later.
|
|
112
|
+
*
|
|
113
|
+
* Three states, because two of them need opposite advice. The package absent is
|
|
114
|
+
* an install; the package present with no binary is an allowlist; the binary
|
|
115
|
+
* present with no `.bin` shim is a re-install, and telling that reader their
|
|
116
|
+
* build scripts are blocked sends them to a command that does nothing.
|
|
117
|
+
*/
|
|
118
|
+
function checkAtlasBinary(state) {
|
|
119
|
+
if (!state || state.onPath) return [];
|
|
120
|
+
if (!state.packageInstalled) return [{
|
|
121
|
+
check: "atlas",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message: "The atlas binary is not on PATH and @ariga/atlas is not installed — `db push`, `db generate` and `db migrate` will fail.",
|
|
124
|
+
fix: `Install it: ${state.manager} add -D @ariga/atlas, and allow its install script.`
|
|
125
|
+
}];
|
|
126
|
+
if (!state.binaryOnDisk) return [{
|
|
127
|
+
check: "atlas",
|
|
128
|
+
severity: "warning",
|
|
129
|
+
message: "@ariga/atlas is installed and its binary is missing — its `preinstall` script was blocked, so `db push` and `db migrate` will fail.",
|
|
130
|
+
fix: "Allow the build script (pnpm: `allowBuilds` in pnpm-workspace.yaml or `pnpm approve-builds`; npm: `allowScripts` in package.json), then re-install."
|
|
131
|
+
}];
|
|
132
|
+
return [{
|
|
133
|
+
check: "atlas",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
message: "The atlas binary is on disk but node_modules/.bin/atlas is missing, so nothing can invoke it.",
|
|
136
|
+
fix: "Re-resolve the tree so the link is recreated: `pnpm install --force`, or delete node_modules and install again."
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* `20.11.0` → `[20, 11, 0]`, ignoring anything after the patch.
|
|
141
|
+
*
|
|
142
|
+
* A bare major is a version: `>=20` is how every `engines.node` in this repo is
|
|
143
|
+
* written, and requiring a minor here made the whole check return nothing —
|
|
144
|
+
* silently, which is the only way a check like this ever fails.
|
|
145
|
+
*/
|
|
146
|
+
function parseVersion(raw) {
|
|
147
|
+
const match = raw.trim().replace(/^v/, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
148
|
+
if (!match) return null;
|
|
149
|
+
return [
|
|
150
|
+
Number(match[1]),
|
|
151
|
+
Number(match[2] ?? 0),
|
|
152
|
+
Number(match[3] ?? 0)
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
/** The lowest version a `>=x.y.z` range admits, or null for anything else. */
|
|
156
|
+
function minimumFromRange(range) {
|
|
157
|
+
const match = range.match(/>=\s*v?(\d+(?:\.\d+)?(?:\.\d+)?)/);
|
|
158
|
+
return match ? parseVersion(match[1]) : null;
|
|
159
|
+
}
|
|
160
|
+
function isBelow(actual, minimum) {
|
|
161
|
+
for (let i = 0; i < 3; i++) {
|
|
162
|
+
if (actual[i] < minimum[i]) return true;
|
|
163
|
+
if (actual[i] > minimum[i]) return false;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Is the running Node old enough to break things that will not say so?
|
|
169
|
+
*
|
|
170
|
+
* The symptoms are not "unsupported Node". They are a syntax error inside a
|
|
171
|
+
* dependency, a missing global, or — the one that costs the most time — a
|
|
172
|
+
* `node:` builtin that resolves and then behaves differently. Naming the
|
|
173
|
+
* version is the whole check.
|
|
174
|
+
*/
|
|
175
|
+
function checkNodeVersion(running, required) {
|
|
176
|
+
if (!required) return [];
|
|
177
|
+
const minimum = minimumFromRange(required);
|
|
178
|
+
const actual = parseVersion(running);
|
|
179
|
+
if (!minimum || !actual) return [];
|
|
180
|
+
if (!isBelow(actual, minimum)) return [];
|
|
181
|
+
return [{
|
|
182
|
+
check: "node",
|
|
183
|
+
severity: "error",
|
|
184
|
+
message: `Node ${running} is running, and Rebase needs ${required}.`,
|
|
185
|
+
fix: `Install Node ${minimum[0]} or newer (nvm install ${minimum[0]}), then reinstall dependencies.`
|
|
186
|
+
}];
|
|
187
|
+
}
|
|
188
|
+
/** The lockfiles a project may have, and the manager each one belongs to. */
|
|
189
|
+
var LOCKFILES = {
|
|
190
|
+
"pnpm-lock.yaml": "pnpm",
|
|
191
|
+
"package-lock.json": "npm",
|
|
192
|
+
"yarn.lock": "yarn",
|
|
193
|
+
"bun.lockb": "bun"
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Two package managers in one project.
|
|
197
|
+
*
|
|
198
|
+
* A Rebase project is a pnpm workspace, and running `npm install` inside one
|
|
199
|
+
* rewrites `node_modules` into a hoisted layout that pnpm then disagrees with.
|
|
200
|
+
* The result is not an install error: it is `Cannot find module` from inside a
|
|
201
|
+
* dependency, hours later, on a machine where it worked yesterday. The
|
|
202
|
+
* leftover `package-lock.json` is the only durable evidence, and nothing looked
|
|
203
|
+
* at it.
|
|
204
|
+
*/
|
|
205
|
+
function checkPackageManager(lockfilesPresent, declared) {
|
|
206
|
+
const known = lockfilesPresent.filter((file) => file in LOCKFILES);
|
|
207
|
+
if (known.length < 2) {
|
|
208
|
+
const declaredName = declared?.split("@")[0];
|
|
209
|
+
if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) return [{
|
|
210
|
+
check: "package-manager",
|
|
211
|
+
severity: "warning",
|
|
212
|
+
message: `This project declares packageManager "${declared}" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,
|
|
213
|
+
fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`
|
|
214
|
+
}];
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const managers = known.map((file) => LOCKFILES[file]);
|
|
218
|
+
return [{
|
|
219
|
+
check: "package-manager",
|
|
220
|
+
severity: "error",
|
|
221
|
+
message: `Two package managers have installed here: ${known.join(" and ")}.`,
|
|
222
|
+
fix: `Keep one. Delete the others and node_modules, then reinstall with ${declared?.split("@")[0] ?? managers[0]}.`
|
|
223
|
+
}];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Two collections claiming one slug.
|
|
227
|
+
*
|
|
228
|
+
* The slug is the route, the table lookup and the registry key, and the
|
|
229
|
+
* registry keeps the last one registered. So the loser is not reported missing:
|
|
230
|
+
* it is served as the winner, with the winner's properties, under its own name.
|
|
231
|
+
* Every symptom of this points somewhere else.
|
|
232
|
+
*/
|
|
233
|
+
function checkDuplicateSlugs(collections) {
|
|
234
|
+
const byslug = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const collection of collections) {
|
|
236
|
+
if (!collection.slug) continue;
|
|
237
|
+
byslug.set(collection.slug, (byslug.get(collection.slug) ?? 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
return [...byslug].filter(([, count]) => count > 1).map(([slug, count]) => ({
|
|
240
|
+
check: "collections",
|
|
241
|
+
severity: "error",
|
|
242
|
+
message: `${count} collections declare the slug "${slug}". Only the last one registered is served.`,
|
|
243
|
+
fix: `Give each collection its own slug. The one that loses is not reported missing — it answers as the winner.`
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The two `.env` facts that fail late and confusingly.
|
|
248
|
+
*
|
|
249
|
+
* A short `JWT_SECRET` boots fine and is refused by `loadEnv` only in
|
|
250
|
+
* production, which is the worst place to find out. Missing CORS in production
|
|
251
|
+
* is the opposite shape — it does not fail at all; the API simply accepts
|
|
252
|
+
* requests from anywhere.
|
|
253
|
+
*
|
|
254
|
+
* Values are never echoed. This reads a file that holds every credential the
|
|
255
|
+
* project has, and doctor's output goes wherever a terminal goes.
|
|
256
|
+
*/
|
|
257
|
+
function checkEnvSanity(values) {
|
|
258
|
+
const findings = [];
|
|
259
|
+
const production = values.NODE_ENV === "production";
|
|
260
|
+
const secret = values.JWT_SECRET;
|
|
261
|
+
if (secret !== void 0 && secret.length > 0 && secret.length < 32) findings.push({
|
|
262
|
+
check: "env",
|
|
263
|
+
severity: production ? "error" : "warning",
|
|
264
|
+
message: `JWT_SECRET is ${secret.length} characters. The runtime requires at least 32, and refuses to boot below it in production.`,
|
|
265
|
+
fix: "Generate one: openssl rand -hex 32"
|
|
266
|
+
});
|
|
267
|
+
if (production && !values.CORS_ORIGINS && !values.FRONTEND_URL) findings.push({
|
|
268
|
+
check: "env",
|
|
269
|
+
severity: "error",
|
|
270
|
+
message: "NODE_ENV=production with neither CORS_ORIGINS nor FRONTEND_URL set — the API will accept requests from any origin.",
|
|
271
|
+
fix: "Set CORS_ORIGINS to the origins that may call this API."
|
|
272
|
+
});
|
|
273
|
+
return findings;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Parse a `.env` into a plain object.
|
|
277
|
+
*
|
|
278
|
+
* Deliberately not `dotenv`: this must not touch `process.env`, and doctor may
|
|
279
|
+
* be reading a file for a different environment than the one it is running in.
|
|
280
|
+
*/
|
|
281
|
+
function parseEnvFile(text) {
|
|
282
|
+
const values = {};
|
|
283
|
+
for (const line of text.split("\n")) {
|
|
284
|
+
const trimmed = line.trim();
|
|
285
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
286
|
+
const eq = trimmed.indexOf("=");
|
|
287
|
+
if (eq <= 0) continue;
|
|
288
|
+
const key = trimmed.slice(0, eq).replace(/^export\s+/, "").trim();
|
|
289
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
290
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
291
|
+
values[key] = value;
|
|
292
|
+
}
|
|
293
|
+
return values;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The same `@rebasepro/*` package pinned to different versions in one project.
|
|
297
|
+
*
|
|
298
|
+
* A Rebase project declares them in several package.json files — the root, the
|
|
299
|
+
* backend, the frontend, the config workspace — and nothing has ever compared
|
|
300
|
+
* them. The failure that produces is not an install error: pnpm installs both,
|
|
301
|
+
* `instanceof` stops working across the two copies, and the symptom is a
|
|
302
|
+
* `CollectionConfig` that fails a type guard written against an identical type.
|
|
303
|
+
*
|
|
304
|
+
* `workspace:` ranges are excluded: inside this repo's own workspaces they all
|
|
305
|
+
* resolve to the same build by construction.
|
|
306
|
+
*/
|
|
307
|
+
function checkVersionSkew(declared) {
|
|
308
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const dependency of declared) {
|
|
310
|
+
if (!dependency.name.startsWith("@rebasepro/")) continue;
|
|
311
|
+
if (dependency.range.startsWith("workspace:") || dependency.range.startsWith("link:")) continue;
|
|
312
|
+
if (dependency.range === "*" || dependency.range === "latest") continue;
|
|
313
|
+
const ranges = byPackage.get(dependency.name) ?? /* @__PURE__ */ new Map();
|
|
314
|
+
ranges.set(dependency.range, [...ranges.get(dependency.range) ?? [], dependency.file]);
|
|
315
|
+
byPackage.set(dependency.name, ranges);
|
|
316
|
+
}
|
|
317
|
+
const findings = [];
|
|
318
|
+
for (const [name, ranges] of byPackage) {
|
|
319
|
+
if (ranges.size < 2) continue;
|
|
320
|
+
const described = [...ranges].map(([range, files]) => `${range} (${files.join(", ")})`).join(", ");
|
|
321
|
+
findings.push({
|
|
322
|
+
check: "versions",
|
|
323
|
+
severity: "error",
|
|
324
|
+
message: `${name} is pinned to different versions in this project: ${described}.`,
|
|
325
|
+
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."
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return findings;
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/commands/doctor.ts
|
|
332
|
+
/**
|
|
333
|
+
* CLI command: rebase doctor
|
|
334
|
+
*
|
|
335
|
+
* Detects three-way schema drift between collection definitions,
|
|
336
|
+
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* Files that can hold a connection string the project actually runs on.
|
|
340
|
+
*
|
|
341
|
+
* `.env` is what local commands read; the compose files are what a self-hosted
|
|
342
|
+
* stack and its scheduled backup cron read. Both shipped with the defect, and a
|
|
343
|
+
* deployed stack is broken by the compose one even when `.env` has been fixed.
|
|
344
|
+
* `.env.example` is deliberately absent: nothing runs on it.
|
|
345
|
+
*/
|
|
346
|
+
var CONNECTION_FILES = [
|
|
347
|
+
".env",
|
|
348
|
+
".env.local",
|
|
349
|
+
"docker-compose.yml",
|
|
350
|
+
"docker-compose.yaml",
|
|
351
|
+
"docker-compose.custom.yml"
|
|
352
|
+
];
|
|
353
|
+
/**
|
|
354
|
+
* Find connection strings libpq cannot parse, anywhere in the project.
|
|
355
|
+
*
|
|
356
|
+
* Exported for the tests; `envFile` is passed separately because a project may
|
|
357
|
+
* keep its `.env` outside the root (see `findEnvFile`).
|
|
358
|
+
*/
|
|
359
|
+
function findLibpqUrlProblems(projectRoot, envFile) {
|
|
360
|
+
const candidates = CONNECTION_FILES.map((f) => path.join(projectRoot, f));
|
|
361
|
+
if (envFile) candidates.push(envFile);
|
|
362
|
+
const findings = [];
|
|
363
|
+
const seen = /* @__PURE__ */ new Set();
|
|
364
|
+
for (const file of candidates) {
|
|
365
|
+
if (seen.has(file)) continue;
|
|
366
|
+
seen.add(file);
|
|
367
|
+
let text;
|
|
368
|
+
try {
|
|
369
|
+
if (!fs.existsSync(file)) continue;
|
|
370
|
+
text = fs.readFileSync(file, "utf-8");
|
|
371
|
+
} catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
findings.push(...scanTextForLibpqUrls(path.relative(projectRoot, file) || path.basename(file), text));
|
|
375
|
+
}
|
|
376
|
+
return findings;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Report unparseable connection strings, if any. Returns true when there were,
|
|
380
|
+
* so the finding reaches the exit code like the environment errors do.
|
|
381
|
+
*
|
|
382
|
+
* Runs before the plugin's drift check and never blocks it. The plugin connects
|
|
383
|
+
* through node-postgres, which parses these URLs happily — so it cannot see this
|
|
384
|
+
* defect, and a project with it will otherwise get a clean bill of health while
|
|
385
|
+
* `rebase db backup` fails.
|
|
386
|
+
*/
|
|
387
|
+
function reportLibpqUrlProblems(findings) {
|
|
388
|
+
if (findings.length === 0) return false;
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(chalk.red.bold(" ✗ Connection string that PostgreSQL's own tools cannot parse"));
|
|
391
|
+
console.log("");
|
|
392
|
+
for (const f of findings) {
|
|
393
|
+
console.log(` ${chalk.bold(f.file)} → ${chalk.bold(f.variable)}`);
|
|
394
|
+
console.log(chalk.gray(` the "${f.params.join("\", \"")}" parameter contains an unencoded "=".`));
|
|
395
|
+
console.log(chalk.gray(" Replace the value with:"));
|
|
396
|
+
console.log(` ${chalk.cyan(f.suggested)}`);
|
|
397
|
+
console.log("");
|
|
398
|
+
}
|
|
399
|
+
console.log(chalk.gray(" libpq splits a query parameter on the first \"=\" and rejects any further"));
|
|
400
|
+
console.log(chalk.gray(" one, so this fails:"));
|
|
401
|
+
console.log(chalk.gray(" extra key/value separator \"=\" in URI query parameter"));
|
|
402
|
+
console.log("");
|
|
403
|
+
console.log(chalk.gray(" Affects rebase db backup / restore, scheduled backups, and psql."));
|
|
404
|
+
console.log(chalk.gray(" NOT rebase dev or db push — those use a driver that accepts it, which"));
|
|
405
|
+
console.log(chalk.gray(" is why a project can look healthy and still have no working backups."));
|
|
406
|
+
console.log(chalk.gray(" Projects scaffolded before 2026-08-18 all carry it."));
|
|
407
|
+
console.log("");
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* `--help` is answered before the project guard, not after.
|
|
412
|
+
*
|
|
413
|
+
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
414
|
+
* body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
|
|
415
|
+
* project answered "✗ Could not find a Rebase project root." Asking a command
|
|
416
|
+
* what it does is the one question that cannot require being somewhere
|
|
417
|
+
* particular to ask.
|
|
418
|
+
*/
|
|
419
|
+
/**
|
|
420
|
+
* What each custom function needs from its host.
|
|
421
|
+
*
|
|
422
|
+
* Prints nothing when there is nothing to say — which is the common case, and
|
|
423
|
+
* the reason it can afford to run every time. When it does print, the ordering
|
|
424
|
+
* is deliberate: the one finding that is a bug *today* comes first, and the
|
|
425
|
+
* rest is a single descriptive line about where these functions could run.
|
|
426
|
+
*/
|
|
427
|
+
function reportFunctionPortability(projectRoot) {
|
|
428
|
+
let results;
|
|
429
|
+
try {
|
|
430
|
+
const backend = findBackendApp(loadManifest(projectRoot).manifest);
|
|
431
|
+
if (!backend) return;
|
|
432
|
+
const paths = resolveBackendPaths(backend.app, projectRoot);
|
|
433
|
+
results = analyseFunctionsDirectory(path.join(projectRoot, paths.functions), projectRoot);
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const lines = summarisePortability(results);
|
|
438
|
+
if (lines.length === 0) return;
|
|
439
|
+
console.log(chalk.bold("\nCustom functions"));
|
|
440
|
+
for (const line of lines) console.log(line.trimStart().startsWith("⚠") ? chalk.yellow(line) : chalk.gray(line));
|
|
441
|
+
console.log(chalk.gray(" See https://rebase.pro/docs/backend/custom-functions#runtime-portability"));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Read everything the environment checks need, then run them.
|
|
445
|
+
*
|
|
446
|
+
* The reading is here and the deciding is in `doctor-environment.ts`, so every
|
|
447
|
+
* check has a fixture that fails it without a project on disk.
|
|
448
|
+
*/
|
|
449
|
+
function collectEnvironmentFindings(projectRoot, envFile) {
|
|
450
|
+
const findings = [];
|
|
451
|
+
findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));
|
|
452
|
+
const rootPackage = readJson(path.join(projectRoot, "package.json"));
|
|
453
|
+
findings.push(...checkPackageManager(Object.keys(LOCKFILES).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
|
|
454
|
+
findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));
|
|
455
|
+
if (envFile && fs.existsSync(envFile)) try {
|
|
456
|
+
findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, "utf-8"))));
|
|
457
|
+
} catch {}
|
|
458
|
+
findings.push(...checkVersionSkew(readDeclaredRebaseDeps(projectRoot)));
|
|
459
|
+
findings.push(...checkAtlasBinary(readAtlasBinaryState(projectRoot)));
|
|
460
|
+
return findings;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* What is actually on disk for `@ariga/atlas`, from the project's point of view.
|
|
464
|
+
*
|
|
465
|
+
* Three separate questions, because the three states they distinguish need
|
|
466
|
+
* three different remedies — see `checkAtlasBinary`. Directories are searched
|
|
467
|
+
* from the project root upwards and through the workspaces a scaffold has,
|
|
468
|
+
* because `db push` runs from `backend/` and the install may be hoisted to the
|
|
469
|
+
* root.
|
|
470
|
+
*/
|
|
471
|
+
function readAtlasBinaryState(projectRoot) {
|
|
472
|
+
const roots = [
|
|
473
|
+
projectRoot,
|
|
474
|
+
path.join(projectRoot, "backend"),
|
|
475
|
+
path.join(projectRoot, "config")
|
|
476
|
+
];
|
|
477
|
+
const findUpwards = (starts, relative) => {
|
|
478
|
+
for (const start of starts) {
|
|
479
|
+
let dir = start;
|
|
480
|
+
for (let i = 0; i < 8; i++) {
|
|
481
|
+
const candidate = path.join(dir, "node_modules", relative);
|
|
482
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
483
|
+
const parent = path.dirname(dir);
|
|
484
|
+
if (parent === dir) break;
|
|
485
|
+
dir = parent;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
};
|
|
490
|
+
const resolveFrom = (from, request) => {
|
|
491
|
+
try {
|
|
492
|
+
return createRequire(pathToFileURL(from)).resolve(request);
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const bases = roots.map((dir) => path.join(dir, "package.json")).filter((file) => fs.existsSync(file));
|
|
498
|
+
const driverManifests = bases.map((base) => resolveFrom(base, "@rebasepro/server-postgres/package.json")).filter((file) => file !== null);
|
|
499
|
+
let manifest = findUpwards(roots, path.join("@ariga", "atlas", "package.json"));
|
|
500
|
+
if (!manifest) for (const base of [...bases, ...driverManifests]) {
|
|
501
|
+
manifest = resolveFrom(base, "@ariga/atlas/package.json");
|
|
502
|
+
if (manifest) break;
|
|
503
|
+
}
|
|
504
|
+
const onPath = findUpwards([...roots, ...driverManifests.map((file) => path.dirname(file))], path.join(".bin", "atlas")) !== null;
|
|
505
|
+
let binaryOnDisk = false;
|
|
506
|
+
if (manifest) try {
|
|
507
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
508
|
+
const dir = path.dirname(manifest);
|
|
509
|
+
binaryOnDisk = (typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin ?? {}).filter((t) => typeof t === "string")).some((target) => fs.existsSync(path.resolve(dir, target)));
|
|
510
|
+
} catch {}
|
|
511
|
+
return {
|
|
512
|
+
onPath,
|
|
513
|
+
packageInstalled: manifest !== null,
|
|
514
|
+
binaryOnDisk,
|
|
515
|
+
manager: readPackageManagerName(projectRoot)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/** The reader's own package manager, from the lockfile beside their project. */
|
|
519
|
+
function readPackageManagerName(projectRoot) {
|
|
520
|
+
for (const [file, manager] of Object.entries(LOCKFILES)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
|
|
521
|
+
return "pnpm";
|
|
522
|
+
}
|
|
523
|
+
/** The `engines.node` range of the CLI actually running. */
|
|
524
|
+
function readCliEngines() {
|
|
525
|
+
try {
|
|
526
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
527
|
+
for (let dir = here, i = 0; i < 5; i++) {
|
|
528
|
+
const candidate = path.join(dir, "package.json");
|
|
529
|
+
if (fs.existsSync(candidate)) {
|
|
530
|
+
const parsed = readJson(candidate);
|
|
531
|
+
if (parsed?.name === "@rebasepro/cli") return parsed.engines?.node;
|
|
532
|
+
}
|
|
533
|
+
const parent = path.dirname(dir);
|
|
534
|
+
if (parent === dir) break;
|
|
535
|
+
dir = parent;
|
|
536
|
+
}
|
|
537
|
+
} catch {}
|
|
538
|
+
}
|
|
539
|
+
function readJson(file) {
|
|
540
|
+
try {
|
|
541
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
542
|
+
} catch {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** Where a project's package.json files live, relative to its root. */
|
|
547
|
+
var PACKAGE_JSON_LOCATIONS = [
|
|
548
|
+
"package.json",
|
|
549
|
+
"backend/package.json",
|
|
550
|
+
"frontend/package.json",
|
|
551
|
+
"config/package.json"
|
|
552
|
+
];
|
|
553
|
+
function readDeclaredRebaseDeps(projectRoot) {
|
|
554
|
+
const declared = [];
|
|
555
|
+
for (const relative of PACKAGE_JSON_LOCATIONS) {
|
|
556
|
+
const parsed = readJson(path.join(projectRoot, relative));
|
|
557
|
+
if (!parsed) continue;
|
|
558
|
+
for (const field of [
|
|
559
|
+
"dependencies",
|
|
560
|
+
"devDependencies",
|
|
561
|
+
"peerDependencies"
|
|
562
|
+
]) {
|
|
563
|
+
const block = parsed[field];
|
|
564
|
+
if (!block) continue;
|
|
565
|
+
for (const [name, range] of Object.entries(block)) if (typeof range === "string") declared.push({
|
|
566
|
+
file: relative,
|
|
567
|
+
name,
|
|
568
|
+
range
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return declared;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Does this project declare its collections in code?
|
|
576
|
+
*
|
|
577
|
+
* `resolveBackendPaths` answers it against the manifest's own `config` path
|
|
578
|
+
* rather than the convention, because a project may move the directory. A
|
|
579
|
+
* project with no manifest, or one shaped differently, is treated as declaring
|
|
580
|
+
* them: the wrong answer there would silently skip the drift report, which is
|
|
581
|
+
* the reason the command exists.
|
|
582
|
+
*/
|
|
583
|
+
function hasDeclaredCollections(projectRoot) {
|
|
584
|
+
try {
|
|
585
|
+
const backend = findBackendApp(loadManifest(projectRoot).manifest);
|
|
586
|
+
if (!backend) return true;
|
|
587
|
+
return resolveBackendPaths(backend.app, projectRoot).hasCollections;
|
|
588
|
+
} catch {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Every `slug:` a collection file declares, read as text.
|
|
594
|
+
*
|
|
595
|
+
* Deliberately not by importing them: a collection file may import anything the
|
|
596
|
+
* project depends on, and doctor must not run a project's code to tell it two
|
|
597
|
+
* slugs collide. A regex over `slug: "..."` finds the literal in every shape
|
|
598
|
+
* the scaffold and the docs use; a computed slug is simply not seen, which is
|
|
599
|
+
* the right failure for a check that only ever reports.
|
|
600
|
+
*/
|
|
601
|
+
function readDeclaredSlugs(projectRoot) {
|
|
602
|
+
const dir = path.join(projectRoot, "config", "collections");
|
|
603
|
+
if (!fs.existsSync(dir)) return [];
|
|
604
|
+
const slugs = [];
|
|
605
|
+
let entries;
|
|
606
|
+
try {
|
|
607
|
+
entries = fs.readdirSync(dir);
|
|
608
|
+
} catch {
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (!/\.(ts|js|mts|mjs)$/.test(entry) || entry.startsWith("index.")) continue;
|
|
613
|
+
try {
|
|
614
|
+
const text = fs.readFileSync(path.join(dir, entry), "utf-8");
|
|
615
|
+
for (const match of text.matchAll(/\bslug:\s*["'`]([^"'`]+)["'`]/g)) slugs.push({ slug: match[1] });
|
|
616
|
+
} catch {}
|
|
617
|
+
}
|
|
618
|
+
return slugs;
|
|
619
|
+
}
|
|
620
|
+
/** Print what the environment checks found, grouped, or nothing at all. */
|
|
621
|
+
function reportEnvironmentFindings(findings) {
|
|
622
|
+
if (findings.length === 0) return false;
|
|
623
|
+
console.log("");
|
|
624
|
+
console.log(chalk.bold(" Environment"));
|
|
625
|
+
for (const finding of findings) {
|
|
626
|
+
const mark = finding.severity === "error" ? chalk.red(" ✗") : chalk.yellow(" ⚠");
|
|
627
|
+
console.log(`${mark} ${finding.message}`);
|
|
628
|
+
console.log(chalk.gray(` ${finding.fix}`));
|
|
629
|
+
}
|
|
630
|
+
console.log("");
|
|
631
|
+
return findings.some((finding) => finding.severity === "error");
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The flags `rebase doctor` takes, and what each is for.
|
|
635
|
+
*
|
|
636
|
+
* One table, read by both the parser and the help page, because they were two
|
|
637
|
+
* things that had to agree and did not: the page listed no flags at all, so
|
|
638
|
+
* `--policies` — the form `cli/index.md` calls "the form to use as a CI gate" —
|
|
639
|
+
* was invisible to anyone who ran `rebase doctor --help` to find it. And with
|
|
640
|
+
* nothing parsing the line here, a typo travelled to the driver's permissive
|
|
641
|
+
* parser and was silently dropped, so `rebase doctor --polices` ran the ordinary
|
|
642
|
+
* doctor and reported success on a gate that never ran.
|
|
643
|
+
*
|
|
644
|
+
* The spec mirrors the driver's own (`doctorPluginCommand` in
|
|
645
|
+
* `server-postgres/src/cli.ts`), because the line is relayed to it verbatim.
|
|
646
|
+
*/
|
|
647
|
+
var DOCTOR_FLAGS = {
|
|
648
|
+
"--policies": Boolean,
|
|
649
|
+
"--collections": String,
|
|
650
|
+
"-c": "--collections",
|
|
651
|
+
"--schema": String,
|
|
652
|
+
"-s": "--schema",
|
|
653
|
+
"--sdk": String,
|
|
654
|
+
"-k": "--sdk"
|
|
655
|
+
};
|
|
656
|
+
/** What each long flag above does, for the Options block. */
|
|
657
|
+
var DOCTOR_FLAG_HELP = {
|
|
658
|
+
"--policies": "Also audit the row-level security policies. The form to use as a CI gate.",
|
|
659
|
+
"--collections": COLLECTIONS_FLAG_HELP,
|
|
660
|
+
"--schema": "Path to the generated Drizzle schema (default: backend/src/schema.generated.ts)",
|
|
661
|
+
"--sdk": "Path to the generated SDK types (default: generated/sdk/database.types.ts)"
|
|
662
|
+
};
|
|
663
|
+
/**
|
|
664
|
+
* The line handed to the driver, every path on it resolved from where the
|
|
665
|
+
* command was run — the driver runs in `backend/`. See `utils/path-flags.ts`.
|
|
666
|
+
*/
|
|
667
|
+
function doctorDriverArgs(rawArgs, cwd) {
|
|
668
|
+
return absolutizeLocalPathArgs(argsFromCommand(rawArgs, "doctor"), cwd, [
|
|
669
|
+
"--collections",
|
|
670
|
+
"-c",
|
|
671
|
+
"--schema",
|
|
672
|
+
"-s",
|
|
673
|
+
"--sdk",
|
|
674
|
+
"-k"
|
|
675
|
+
]);
|
|
676
|
+
}
|
|
677
|
+
/** `--collections, -c` — the aliases, resolved from the spec rather than retyped. */
|
|
678
|
+
function doctorFlagLabel(flag) {
|
|
679
|
+
return [flag, ...Object.entries(DOCTOR_FLAGS).filter(([, target]) => target === flag).map(([alias]) => alias)].join(", ");
|
|
680
|
+
}
|
|
681
|
+
function printDoctorHelp() {
|
|
682
|
+
console.log(`
|
|
683
|
+
${chalk.bold("rebase doctor")} — Check a project for drift and misconfiguration
|
|
684
|
+
|
|
685
|
+
${chalk.green.bold("Usage")}
|
|
686
|
+
rebase doctor [options]
|
|
687
|
+
|
|
688
|
+
${chalk.green.bold("Options")}
|
|
689
|
+
${Object.keys(DOCTOR_FLAG_HELP).map((flag) => ` ${chalk.blue(doctorFlagLabel(flag).padEnd(22))} ${DOCTOR_FLAG_HELP[flag]}`).join("\n")}
|
|
690
|
+
|
|
691
|
+
Compares the collections you declare, the generated Drizzle schema, and the
|
|
692
|
+
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
693
|
+
|
|
694
|
+
Also checks the connection strings in .env and the compose files for the
|
|
695
|
+
unencoded "=" that makes PostgreSQL's own tools refuse to parse them — which
|
|
696
|
+
breaks backups and psql while leaving the app itself working.
|
|
697
|
+
|
|
698
|
+
Run from inside a Rebase project — it reads the project's collections and
|
|
699
|
+
connects to its database.
|
|
700
|
+
`);
|
|
701
|
+
}
|
|
702
|
+
async function doctorCommand(rawArgs) {
|
|
703
|
+
if (wantsHelp(rawArgs)) {
|
|
704
|
+
printDoctorHelp();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
parseCommandArgs({
|
|
708
|
+
spec: DOCTOR_FLAGS,
|
|
709
|
+
rawArgs,
|
|
710
|
+
commandWords: 1,
|
|
711
|
+
command: "doctor",
|
|
712
|
+
maxPositionals: 0
|
|
713
|
+
});
|
|
714
|
+
const projectRoot = requireProjectRoot();
|
|
715
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
716
|
+
refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir);
|
|
717
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
718
|
+
if (!activePlugin) {
|
|
719
|
+
console.error(chalk.red("✗ Could not detect an active database plugin."));
|
|
720
|
+
console.error(chalk.gray(" Make sure a package like @rebasepro/server-postgres is installed in backend/package.json."));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
724
|
+
if (!pluginCli) exitDependenciesNotInstalled(projectRoot);
|
|
725
|
+
const envFile = findEnvFile(projectRoot);
|
|
726
|
+
const env = { ...process.env };
|
|
727
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
728
|
+
env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
|
|
729
|
+
const connectionFailed = reportLibpqUrlProblems(findLibpqUrlProblems(projectRoot, envFile));
|
|
730
|
+
reportFunctionPortability(projectRoot);
|
|
731
|
+
const environmentFailed = reportEnvironmentFindings(collectEnvironmentFindings(projectRoot, envFile));
|
|
732
|
+
const failed = connectionFailed || environmentFailed;
|
|
733
|
+
try {
|
|
734
|
+
const prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
|
|
735
|
+
Object.assign(env, prepared.env);
|
|
736
|
+
for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.log("");
|
|
739
|
+
console.error(chalk.yellow(` ⚠ Could not resolve this project's database: ${error instanceof Error ? error.message : String(error)}`));
|
|
740
|
+
console.error(chalk.gray(" The checks that need one are reported below as not run."));
|
|
741
|
+
}
|
|
742
|
+
if (!hasDeclaredCollections(projectRoot)) {
|
|
743
|
+
console.log("");
|
|
744
|
+
console.log(chalk.gray(" ○ Schema drift not checked: this project derives its API from the database — run `rebase schema introspect` first."));
|
|
745
|
+
if (failed) process.exit(1);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
if (pluginCli.endsWith(".ts")) {
|
|
750
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
751
|
+
if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
|
|
752
|
+
await execa(tsxBin, [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
|
|
753
|
+
cwd: backendDir,
|
|
754
|
+
stdio: "inherit",
|
|
755
|
+
env
|
|
756
|
+
});
|
|
757
|
+
} else await execa("node", [pluginCli, ...doctorDriverArgs(rawArgs, process.cwd())], {
|
|
758
|
+
cwd: backendDir,
|
|
759
|
+
stdio: "inherit",
|
|
760
|
+
env
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
reportSpawnFailure(error);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
if (failed) process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
export { doctorCommand };
|
|
770
|
+
|
|
771
|
+
//# sourceMappingURL=doctor-DYs-NqFI.js.map
|