@rebasepro/cli 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +21 -3
  3. package/bin/rebase.js +93 -6
  4. package/dist/branch-pointer-dBiClJ0p.js +111 -0
  5. package/dist/branch-pointer-dBiClJ0p.js.map +1 -0
  6. package/dist/bundle.d.ts +80 -3
  7. package/dist/cli.d.ts +22 -0
  8. package/dist/commands/auth.d.ts +13 -0
  9. package/dist/commands/cloud/action-help.d.ts +0 -10
  10. package/dist/commands/cloud/auth.d.ts +37 -0
  11. package/dist/commands/cloud/context.d.ts +90 -2
  12. package/dist/commands/cloud/databases.d.ts +0 -2
  13. package/dist/commands/cloud/declared-resources.d.ts +3 -0
  14. package/dist/commands/cloud/deploy.d.ts +76 -6
  15. package/dist/commands/cloud/deployments.d.ts +9 -2
  16. package/dist/commands/cloud/index.d.ts +36 -0
  17. package/dist/commands/cloud/link.d.ts +1 -1
  18. package/dist/commands/cloud/orgs.d.ts +6 -0
  19. package/dist/commands/cloud/resources.d.ts +81 -3
  20. package/dist/commands/cloud/settings.d.ts +7 -0
  21. package/dist/commands/db.d.ts +119 -9
  22. package/dist/commands/dev.d.ts +216 -1
  23. package/dist/commands/doctor.d.ts +51 -0
  24. package/dist/commands/init.d.ts +1 -0
  25. package/dist/commands/skills.d.ts +82 -13
  26. package/dist/commands/status.d.ts +1 -0
  27. package/dist/commands/telemetry.d.ts +0 -8
  28. package/dist/{daemon-Bdl4lrdt.js → daemon-YDZK9NZ1.js} +88 -9
  29. package/dist/daemon-YDZK9NZ1.js.map +1 -0
  30. package/dist/{daemon-entry-CmJn83zu.js → daemon-entry-LTFKtpHy.js} +117 -44
  31. package/dist/daemon-entry-LTFKtpHy.js.map +1 -0
  32. package/dist/dev-db/branch-pointer.d.ts +71 -0
  33. package/dist/dev-db/daemon.d.ts +27 -0
  34. package/dist/dev-db/prepare.d.ts +62 -0
  35. package/dist/dev-db/pull.d.ts +28 -0
  36. package/dist/dev-db/resolve.d.ts +45 -6
  37. package/dist/dev-db/state.d.ts +21 -0
  38. package/dist/dev-preflight-CNLl4rdo.js +367 -0
  39. package/dist/dev-preflight-CNLl4rdo.js.map +1 -0
  40. package/dist/doctor-environment.d.ts +131 -0
  41. package/dist/function-portability.d.ts +7 -0
  42. package/dist/index.d.ts +25 -15
  43. package/dist/index.es.js +5594 -1741
  44. package/dist/index.es.js.map +1 -1
  45. package/dist/project-3WCbrrfW.js +499 -0
  46. package/dist/project-3WCbrrfW.js.map +1 -0
  47. package/dist/{pull-DqPRu1te.js → pull-Bj0XnH5s.js} +31 -2
  48. package/dist/pull-Bj0XnH5s.js.map +1 -0
  49. package/dist/resolve-Y56osuQH.js +74 -0
  50. package/dist/resolve-Y56osuQH.js.map +1 -0
  51. package/dist/resources/derive.d.ts +67 -1
  52. package/dist/resources/status.d.ts +128 -0
  53. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  54. package/dist/{state-c0CJ6Kwb.js → state-C59Elrnt.js} +24 -15
  55. package/dist/state-C59Elrnt.js.map +1 -0
  56. package/dist/utils/args.d.ts +18 -0
  57. package/dist/utils/command-words.d.ts +45 -0
  58. package/dist/utils/dev-preflight.d.ts +46 -0
  59. package/dist/utils/package-manager.d.ts +23 -0
  60. package/dist/utils/project.d.ts +66 -0
  61. package/dist/utils/spawn-error.d.ts +1 -0
  62. package/dist/utils/unknown-command.d.ts +26 -0
  63. package/dist/utils/version.d.ts +10 -0
  64. package/package.json +36 -28
  65. package/templates/eject/backend/src/env.ts +9 -0
  66. package/templates/eject/backend/src/index.ts +46 -19
  67. package/templates/eject/config/resources.ts +1 -1
  68. package/templates/overlays/baas/README.md +31 -11
  69. package/templates/overlays/baas/backend/package.json +2 -2
  70. package/templates/overlays/baas/backend/tsconfig.json +1 -1
  71. package/templates/overlays/baas/package.json +5 -2
  72. package/templates/overlays/baas/pnpm-workspace.yaml +13 -0
  73. package/templates/template/.cursorrules +1 -0
  74. package/templates/template/.env.example +91 -8
  75. package/templates/template/.github/copilot-instructions.md +1 -0
  76. package/templates/template/.mcp.json +11 -0
  77. package/templates/template/.windsurfrules +1 -0
  78. package/templates/template/AGENTS.md +1 -0
  79. package/templates/template/CLAUDE.md +2 -1
  80. package/templates/template/README.md +59 -28
  81. package/templates/template/ai-instructions.md +29 -12
  82. package/templates/template/backend/functions/hello.ts +5 -1
  83. package/templates/template/backend/package.json +2 -2
  84. package/templates/template/backend/src/schema.generated.ts +8 -3
  85. package/templates/template/backend/tsconfig.json +1 -1
  86. package/templates/template/config/cms.d.ts +6 -0
  87. package/templates/template/config/package.json +1 -1
  88. package/templates/template/config/resources.ts +42 -3
  89. package/templates/template/config/storage.ts +2 -1
  90. package/templates/template/docker-compose.yml +50 -9
  91. package/templates/template/frontend/index.html +0 -1
  92. package/templates/template/frontend/package.json +1 -2
  93. package/templates/template/frontend/src/App.tsx +8 -2
  94. package/templates/template/frontend/vite.config.ts +8 -0
  95. package/templates/template/gitignore +19 -1
  96. package/templates/template/npmrc +13 -4
  97. package/templates/template/package.json +10 -1
  98. package/templates/template/pnpm-workspace.yaml +17 -0
  99. package/templates/template/scripts/example.ts +4 -1
  100. package/dist/daemon-Bdl4lrdt.js.map +0 -1
  101. package/dist/daemon-entry-CmJn83zu.js.map +0 -1
  102. package/dist/pull-DqPRu1te.js.map +0 -1
  103. package/dist/state-c0CJ6Kwb.js.map +0 -1
@@ -11,9 +11,10 @@
11
11
  *
12
12
  * 1. `--database-url <url>` — said on this command line, wins over everything
13
13
  * 2. `DATABASE_URL` in the shell environment
14
- * 3. `DATABASE_URL` in the project's `.env`
15
- * 4. `--docker` / a manifest preference of `docker`
16
- * 5. the managed PGlite database
14
+ * 3. the database branch this checkout is switched to
15
+ * 4. `DATABASE_URL` in the project's `.env`
16
+ * 5. `--docker` / a manifest preference of `docker`
17
+ * 6. the managed PGlite database
17
18
  *
18
19
  * An explicit connection string always wins. That is the whole point of the
19
20
  * override: someone pointing Rebase at their own Postgres — a colleague's
@@ -21,9 +22,17 @@
21
22
  * with no cleverness in between. The managed database is what fills the vacuum
22
23
  * when nobody has said anything, and it is the only case where the CLI picks.
23
24
  *
25
+ * The branch at (3) is the one addition that is not an explicit connection
26
+ * string, and it sits where it does deliberately. It has to outrank `.env` or
27
+ * `rebase db branch switch` would silently do nothing on every project that
28
+ * sets `DATABASE_URL` — which is every project not on the managed database.
29
+ * It must not outrank (1) or (2), because a flag on this command line and a
30
+ * variable in this shell are more immediate than a switch made yesterday.
31
+ *
24
32
  * {@link resolveDevDatabase} is pure: inputs in, decision out, no filesystem
25
- * and no process. Reading `.env` and starting a daemon happen elsewhere, so
26
- * the ordering above can be tested without either.
33
+ * and no process. Reading `.env`, reading the branch pointer and starting a
34
+ * daemon happen elsewhere, so the ordering above can be tested without any of
35
+ * them.
27
36
  */
28
37
  /** Where the answer came from. Carried so diagnostics can name it. */
29
38
  export type DevDatabaseSource =
@@ -33,6 +42,8 @@ export type DevDatabaseSource =
33
42
  | "environment"
34
43
  /** `DATABASE_URL` in the project's `.env`. */
35
44
  | "env-file"
45
+ /** The branch this checkout is switched to, over the base connection. */
46
+ | "branch"
36
47
  /** `--docker`, or `devDatabase: "docker"` in the manifest. */
37
48
  | "docker"
38
49
  /** Nobody said anything, so the managed database fills in. */
@@ -41,10 +52,20 @@ export type DevDatabase = {
41
52
  kind: "external";
42
53
  /** The connection string, exactly as given. Never rewritten. */
43
54
  url: string;
44
- source: Extract<DevDatabaseSource, "flag" | "environment" | "env-file">;
55
+ source: Extract<DevDatabaseSource, "flag" | "environment" | "env-file" | "branch">;
56
+ /** The branch name, when this checkout is switched to one. */
57
+ branch?: string;
45
58
  } | {
46
59
  kind: "docker";
47
60
  source: "docker";
61
+ /**
62
+ * The compose `db` service's connection string, derived by the caller.
63
+ *
64
+ * Null when the project has no compose file to derive one from, which
65
+ * is a `--docker` that cannot be honoured rather than a fallback: the
66
+ * caller says so instead of guessing at `localhost:5432`.
67
+ */
68
+ url: string | null;
48
69
  } | {
49
70
  kind: "managed";
50
71
  source: "managed";
@@ -60,6 +81,24 @@ export interface ResolveDevDatabaseInput {
60
81
  envFile?: Record<string, string> | null;
61
82
  /** `devDatabase` from `rebase.json`, if the project recorded a preference. */
62
83
  manifestPreference?: "managed" | "docker" | null;
84
+ /**
85
+ * The compose `db` service's connection string, when the project has one.
86
+ *
87
+ * Passed in rather than read here so this function stays pure. Only
88
+ * consulted for the Docker case; an explicit `DATABASE_URL` still wins.
89
+ */
90
+ composeUrl?: string | null;
91
+ /**
92
+ * The branch this checkout is switched to, already resolved to a URL.
93
+ *
94
+ * Resolved by the caller rather than here so this stays pure: deriving it
95
+ * needs the base connection string, which needs the `.env` this function is
96
+ * handed rather than reads.
97
+ */
98
+ branch?: {
99
+ name: string;
100
+ url: string;
101
+ } | null;
63
102
  }
64
103
  export declare function resolveDevDatabase(input?: ResolveDevDatabaseInput): DevDatabase;
65
104
  /** One line for the startup banner, naming both the database and why. */
@@ -49,9 +49,30 @@ export interface DaemonState {
49
49
  token: string;
50
50
  /** Loopback port that answers the identity check. */
51
51
  identityPort: number;
52
+ /**
53
+ * The additional databases this daemon serves, by declared key.
54
+ *
55
+ * One PGlite instance each — PGlite is one database per instance, so
56
+ * `database("analytics")` is a second data directory and a second port,
57
+ * not a `CREATE DATABASE`. Started on demand when a command asks for the
58
+ * key over the identity socket, and recorded here so `rebase status` and
59
+ * a reset can see them. The default database is the fields above.
60
+ */
61
+ databases?: Record<string, {
62
+ port: number;
63
+ dataDir: string;
64
+ }>;
52
65
  }
53
66
  export declare function devDbDir(projectRoot: string): string;
54
67
  export declare function dataDir(projectRoot: string): string;
68
+ /**
69
+ * The data directory for an additional declared database.
70
+ *
71
+ * `pgdata__analytics`: the key spelled the way its variable is, so the
72
+ * directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the
73
+ * same thing.
74
+ */
75
+ export declare function additionalDataDir(projectRoot: string, key: string): string;
55
76
  export declare function stateFile(projectRoot: string): string;
56
77
  export declare function startLockFile(projectRoot: string): string;
57
78
  /**
@@ -0,0 +1,367 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import chalk from "chalk";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import net from "net";
6
+ import { execa } from "execa";
7
+ //#region src/utils/dev-preflight.ts
8
+ /**
9
+ * Make `rebase dev` the only command a new project needs.
10
+ *
11
+ * The scaffolded project prints three steps — start the database container,
12
+ * push the schema, start the dev server — and a first-time reader has to
13
+ * perform all three correctly before anything works. Two of them are
14
+ * mechanical, and both fail in ways that do not name themselves: skipping the
15
+ * container gives `ECONNREFUSED` from a stack the reader has never seen, and
16
+ * skipping the push gives a server that starts cleanly and then 500s on the
17
+ * first request because the tables are not there.
18
+ *
19
+ * So `dev` does them. Not always, and not silently — under four conditions that
20
+ * together mean "this is a throwaway development database on this laptop, and
21
+ * it is not running":
22
+ *
23
+ * 1. The DSN resolves to a loopback address. A remote database is somebody's
24
+ * real data and is never touched here, whatever else is true.
25
+ * 2. Nothing is listening on it. A database that answers is left completely
26
+ * alone — no push, no inspection, no connection.
27
+ * 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.
28
+ * the one `rebase init` wrote. Without it there is nothing to start and
29
+ * the backend's own error is the better message.
30
+ * 4. Docker is available. If it is not, the manual steps are printed and dev
31
+ * continues — a preflight must never be the reason the command fails.
32
+ *
33
+ * The schema push happens only when this function actually started the
34
+ * container, which is the one moment a local database is known to be new or
35
+ * stale relative to the collections. A `dev` against an already-running
36
+ * database never mutates its schema.
37
+ */
38
+ var dev_preflight_exports = /* @__PURE__ */ __exportAll({
39
+ composeDatabaseUrl: () => composeDatabaseUrl,
40
+ composeDeclaresDbService: () => composeDeclaresDbService,
41
+ composeHostPort: () => composeHostPort,
42
+ ensureDevDatabase: () => ensureDevDatabase,
43
+ parseLoopbackDsn: () => parseLoopbackDsn,
44
+ probeTcp: () => probeTcp,
45
+ waitForPort: () => waitForPort
46
+ });
47
+ /**
48
+ * Hosts that mean "this machine", and so may be provisioned automatically.
49
+ *
50
+ * An exact-match set, never a prefix or substring test: `localhost.example.com`
51
+ * is a perfectly resolvable name that somebody else controls.
52
+ */
53
+ var LOOPBACK = /* @__PURE__ */ new Set([
54
+ "localhost",
55
+ "127.0.0.1",
56
+ "::1",
57
+ "0.0.0.0"
58
+ ]);
59
+ /**
60
+ * The host and port of a Postgres DSN, but only when it points at this machine.
61
+ *
62
+ * Returns null for anything else — a remote host, an unparseable string, a
63
+ * non-postgres scheme. Callers treat null as "do nothing", so every parse
64
+ * failure fails closed onto the safe behaviour rather than onto a guess.
65
+ */
66
+ function parseLoopbackDsn(dsn) {
67
+ if (!dsn) return null;
68
+ let url;
69
+ try {
70
+ url = new URL(dsn);
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (!/^postgres(ql)?:$/.test(url.protocol)) return null;
75
+ const host = url.hostname.replace(/^\[(.+)\]$/, "$1");
76
+ if (!LOOPBACK.has(host)) return null;
77
+ const port = url.port ? Number(url.port) : 5432;
78
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
79
+ return {
80
+ host,
81
+ port
82
+ };
83
+ }
84
+ /**
85
+ * Whether a compose file declares a `db` service.
86
+ *
87
+ * Deliberately a text scan and not a YAML parse: the CLI has no YAML
88
+ * dependency, and the question is only ever asked of a file this tool wrote.
89
+ * A false negative costs the automation and nothing else — the reader gets the
90
+ * manual steps, which are correct.
91
+ */
92
+ function composeDeclaresDbService(yamlText) {
93
+ let inServices = false;
94
+ for (const rawLine of yamlText.split(/\r?\n/)) {
95
+ const line = rawLine.replace(/\t/g, " ");
96
+ if (/^\s*#/.test(line) || line.trim() === "") continue;
97
+ if (/^services:\s*$/.test(line)) {
98
+ inServices = true;
99
+ continue;
100
+ }
101
+ if (inServices && /^\S/.test(line)) {
102
+ inServices = false;
103
+ continue;
104
+ }
105
+ if (inServices && /^\s{2,}db:\s*$/.test(line)) return true;
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * The connection string the compose file's `db` service will answer on.
111
+ *
112
+ * `--docker` used to be a flag that changed a banner and nothing else: it
113
+ * resolved to `kind: "docker"`, and every caller then had no URL to reach the
114
+ * container with, so the backend booted with no `DATABASE_URL` at all and died
115
+ * on the message a project with no database gets. The container it had just
116
+ * been asked for was never started, because the preflight that starts it needs
117
+ * a DSN to decide the database is local and not running.
118
+ *
119
+ * So the URL is derived rather than required: the compose file names the user,
120
+ * the database and the published host port, and `.env` holds the password
121
+ * compose itself interpolates. That is the same string `rebase init` writes as
122
+ * the commented-out `DATABASE_URL`, which is what makes uncommenting that line
123
+ * and passing `--docker` reach the same database.
124
+ *
125
+ * Returns null when the file declares no `db` service, or declares one this
126
+ * scan cannot read — a `--docker` that cannot be honoured has to say so, and
127
+ * a guess would point at somebody else's Postgres on 5432.
128
+ *
129
+ * Same text scan as {@link composeDeclaresDbService}, for the same reason.
130
+ */
131
+ function composeDatabaseUrl(yamlText, env = {}) {
132
+ const found = scanComposeDbService(yamlText);
133
+ const user = expandComposeValue(found.POSTGRES_USER, env);
134
+ const password = expandComposeValue(found.POSTGRES_PASSWORD, env);
135
+ const database = expandComposeValue(found.POSTGRES_DB, env);
136
+ if (!user || !password || !database || !found.hostPort) return null;
137
+ return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;
138
+ }
139
+ /**
140
+ * The host port the compose `db` service publishes, or null.
141
+ *
142
+ * Separate from {@link composeDatabaseUrl} because the port is answerable on
143
+ * its own: the URL needs the user, the password and the database name too, and
144
+ * a compose file missing any of them still says which port a container would
145
+ * listen on. That is the question "would starting this container answer the
146
+ * DSN in `.env`?" — see the `wrong-port` branch of {@link ensureDevDatabase},
147
+ * where getting it wrong meant starting a container nobody asked for.
148
+ */
149
+ function composeHostPort(yamlText) {
150
+ const port = scanComposeDbService(yamlText).hostPort;
151
+ return port ? Number(port) : null;
152
+ }
153
+ /** The raw values of the compose `db` service, before any expansion. */
154
+ function scanComposeDbService(yamlText) {
155
+ let inServices = false;
156
+ let inDb = false;
157
+ let dbIndent = 0;
158
+ let section = null;
159
+ const found = {};
160
+ let hostPort = null;
161
+ for (const rawLine of yamlText.split(/\r?\n/)) {
162
+ const line = rawLine.replace(/\t/g, " ");
163
+ if (/^\s*#/.test(line) || line.trim() === "") continue;
164
+ if (/^services:\s*$/.test(line)) {
165
+ inServices = true;
166
+ continue;
167
+ }
168
+ if (inServices && /^\S/.test(line)) {
169
+ inServices = false;
170
+ inDb = false;
171
+ continue;
172
+ }
173
+ if (!inServices) continue;
174
+ const indent = line.length - line.trimStart().length;
175
+ const dbHeader = line.match(/^(\s{2,})db:\s*$/);
176
+ if (dbHeader) {
177
+ inDb = true;
178
+ dbIndent = dbHeader[1].length;
179
+ section = null;
180
+ continue;
181
+ }
182
+ if (inDb && indent <= dbIndent) {
183
+ inDb = false;
184
+ section = null;
185
+ continue;
186
+ }
187
+ if (!inDb) continue;
188
+ const key = line.match(/^\s+(environment|ports):\s*$/);
189
+ if (key) {
190
+ section = key[1];
191
+ continue;
192
+ }
193
+ if (/^\s+[A-Za-z_][\w-]*:\s*\S/.test(line) && !/^\s+POSTGRES_/.test(line)) {
194
+ if (!/^\s*-\s/.test(line.trimStart())) section = null;
195
+ }
196
+ if (section === "environment") {
197
+ const pair = line.match(/^\s+(POSTGRES_(?:USER|PASSWORD|DB)):\s*(.+?)\s*$/);
198
+ if (pair) found[pair[1]] = pair[2].replace(/^["']|["']$/g, "");
199
+ continue;
200
+ }
201
+ if (section === "ports" && hostPort === null) {
202
+ const mapping = line.match(/^\s*-\s*["']?(?:[\d.]+:)?(\d+):5432(?:\/tcp)?["']?\s*$/);
203
+ if (mapping) hostPort = mapping[1];
204
+ }
205
+ }
206
+ return hostPort === null ? found : {
207
+ ...found,
208
+ hostPort
209
+ };
210
+ }
211
+ /**
212
+ * `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves
213
+ * them — against the project's `.env`, which is the file compose itself reads.
214
+ */
215
+ function expandComposeValue(raw, env) {
216
+ if (!raw) return null;
217
+ const trimmed = raw.replace(/\$\{([A-Za-z_][\w]*)(?::?-([^}]*))?\}/g, (_all, name, fallback) => {
218
+ const value = env[name];
219
+ if (value !== void 0 && value !== "") return value;
220
+ return fallback ?? "";
221
+ }).trim();
222
+ return trimmed.length > 0 ? trimmed : null;
223
+ }
224
+ /** Is something accepting connections there right now? */
225
+ function probeTcp(host, port, timeoutMs = 700) {
226
+ return new Promise((resolve) => {
227
+ const socket = new net.Socket();
228
+ let settled = false;
229
+ const done = (open) => {
230
+ if (settled) return;
231
+ settled = true;
232
+ socket.destroy();
233
+ resolve(open);
234
+ };
235
+ socket.setTimeout(timeoutMs);
236
+ socket.once("connect", () => done(true));
237
+ socket.once("timeout", () => done(false));
238
+ socket.once("error", () => done(false));
239
+ socket.connect(port, host);
240
+ });
241
+ }
242
+ /**
243
+ * Poll until the port answers, or give up.
244
+ *
245
+ * Postgres publishes its port before it is ready to accept queries, so the
246
+ * caller still has to tolerate a first connection that is refused at the
247
+ * protocol level. This only answers "has the container got as far as listening",
248
+ * which is the part that takes the seconds.
249
+ */
250
+ async function waitForPort(host, port, timeoutMs = 6e4, intervalMs = 500) {
251
+ const deadline = Date.now() + timeoutMs;
252
+ while (Date.now() < deadline) {
253
+ if (await probeTcp(host, port, 700)) return true;
254
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
255
+ }
256
+ return false;
257
+ }
258
+ /**
259
+ * Start the project's development database if it is not running, and give it a
260
+ * schema if this call is what started it.
261
+ */
262
+ async function ensureDevDatabase(options) {
263
+ const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;
264
+ const log = options.log ?? ((message) => console.log(message));
265
+ if (disabled) return { action: "disabled" };
266
+ const target = parseLoopbackDsn(databaseUrl);
267
+ if (!databaseUrl) return { action: "no-dsn" };
268
+ if (!target) {
269
+ let host = "the configured host";
270
+ try {
271
+ host = new URL(databaseUrl).hostname || host;
272
+ } catch {}
273
+ return {
274
+ action: "remote-dsn",
275
+ host
276
+ };
277
+ }
278
+ if (await probeTcp(target.host, target.port)) return {
279
+ action: "already-running",
280
+ host: target.host,
281
+ port: target.port
282
+ };
283
+ const composePath = path.join(projectRoot, "docker-compose.yml");
284
+ if (!fs.existsSync(composePath)) return { action: "no-compose" };
285
+ const composeText = fs.readFileSync(composePath, "utf8");
286
+ if (!composeDeclaresDbService(composeText)) return { action: "no-compose" };
287
+ const manualSteps = `${chalk.cyan("docker compose up -d db")} then ${chalk.cyan("rebase db push")}`;
288
+ const composePort = composeHostPort(composeText);
289
+ if (composePort !== null && composePort !== target.port) {
290
+ const hint = `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. The compose file's db service publishes ${composePort}, so starting it would not answer that URL — nothing was started. Either correct the port in .env, or point DATABASE_URL at 127.0.0.1:${composePort}.`;
291
+ log("");
292
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
293
+ return {
294
+ action: "wrong-port",
295
+ hint,
296
+ port: target.port,
297
+ composePort
298
+ };
299
+ }
300
+ log("");
301
+ log(` ${chalk.gray("Database not running — starting it…")}`);
302
+ const startDatabase = options.startDatabase ?? (async (root) => {
303
+ await execa("docker", [
304
+ "compose",
305
+ "up",
306
+ "-d",
307
+ "db"
308
+ ], {
309
+ cwd: root,
310
+ stdio: "pipe"
311
+ });
312
+ });
313
+ try {
314
+ await startDatabase(projectRoot);
315
+ } catch (error) {
316
+ const message = error instanceof Error ? error.message : String(error);
317
+ if (/ENOENT|not found|command not found/i.test(message)) {
318
+ const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;
319
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
320
+ return {
321
+ action: "no-docker",
322
+ hint
323
+ };
324
+ }
325
+ const hint = `Could not start the database container. Run ${manualSteps} to see why.`;
326
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
327
+ return {
328
+ action: "start-failed",
329
+ hint
330
+ };
331
+ }
332
+ if (!await waitForPort(target.host, target.port)) {
333
+ const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;
334
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(hint)}`);
335
+ return {
336
+ action: "start-failed",
337
+ hint
338
+ };
339
+ }
340
+ log(` ${chalk.green("✓")} ${chalk.gray(`Database ready on port ${target.port}`)}`);
341
+ if (!hasCollections) return {
342
+ action: "started",
343
+ port: target.port,
344
+ pushed: false
345
+ };
346
+ log(` ${chalk.gray("Pushing the schema…")}`);
347
+ try {
348
+ await pushSchema();
349
+ log(` ${chalk.green("✓")} ${chalk.gray("Schema pushed")}`);
350
+ return {
351
+ action: "started",
352
+ port: target.port,
353
+ pushed: true
354
+ };
355
+ } catch {
356
+ log(` ${chalk.yellow("⚠")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan("rebase db push")}.`)}`);
357
+ return {
358
+ action: "started",
359
+ port: target.port,
360
+ pushed: false
361
+ };
362
+ }
363
+ }
364
+ //#endregion
365
+ export { probeTcp as i, dev_preflight_exports as n, ensureDevDatabase as r, composeDatabaseUrl as t };
366
+
367
+ //# sourceMappingURL=dev-preflight-CNLl4rdo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-preflight-CNLl4rdo.js","names":[],"sources":["../src/utils/dev-preflight.ts"],"sourcesContent":["/**\n * Make `rebase dev` the only command a new project needs.\n *\n * The scaffolded project prints three steps — start the database container,\n * push the schema, start the dev server — and a first-time reader has to\n * perform all three correctly before anything works. Two of them are\n * mechanical, and both fail in ways that do not name themselves: skipping the\n * container gives `ECONNREFUSED` from a stack the reader has never seen, and\n * skipping the push gives a server that starts cleanly and then 500s on the\n * first request because the tables are not there.\n *\n * So `dev` does them. Not always, and not silently — under four conditions that\n * together mean \"this is a throwaway development database on this laptop, and\n * it is not running\":\n *\n * 1. The DSN resolves to a loopback address. A remote database is somebody's\n * real data and is never touched here, whatever else is true.\n * 2. Nothing is listening on it. A database that answers is left completely\n * alone — no push, no inspection, no connection.\n * 3. The project has a `docker-compose.yml` declaring a `db` service, i.e.\n * the one `rebase init` wrote. Without it there is nothing to start and\n * the backend's own error is the better message.\n * 4. Docker is available. If it is not, the manual steps are printed and dev\n * continues — a preflight must never be the reason the command fails.\n *\n * The schema push happens only when this function actually started the\n * container, which is the one moment a local database is known to be new or\n * stale relative to the collections. A `dev` against an already-running\n * database never mutates its schema.\n */\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { execa } from \"execa\";\n\n/** Where the preflight stopped, and why. Returned so tests can assert it. */\nexport type PreflightOutcome =\n | { action: \"disabled\" }\n | { action: \"no-dsn\" }\n | { action: \"remote-dsn\"; host: string }\n | { action: \"already-running\"; host: string; port: number }\n | { action: \"no-compose\" }\n | { action: \"no-docker\"; hint: string }\n | { action: \"start-failed\"; hint: string }\n | { action: \"wrong-port\"; hint: string; port: number; composePort: number }\n | { action: \"started\"; port: number; pushed: boolean };\n\n/**\n * Hosts that mean \"this machine\", and so may be provisioned automatically.\n *\n * An exact-match set, never a prefix or substring test: `localhost.example.com`\n * is a perfectly resolvable name that somebody else controls.\n */\nconst LOOPBACK = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"0.0.0.0\"]);\n\n/**\n * The host and port of a Postgres DSN, but only when it points at this machine.\n *\n * Returns null for anything else — a remote host, an unparseable string, a\n * non-postgres scheme. Callers treat null as \"do nothing\", so every parse\n * failure fails closed onto the safe behaviour rather than onto a guess.\n */\nexport function parseLoopbackDsn(dsn: string | undefined): { host: string; port: number } | null {\n if (!dsn) return null;\n\n let url: URL;\n try {\n url = new URL(dsn);\n } catch {\n return null;\n }\n\n if (!/^postgres(ql)?:$/.test(url.protocol)) return null;\n\n // `url.hostname` KEEPS the brackets an IPv6 literal carries in a URL, and\n // `net.connect` rejects them. Normalising here rather than at each use is\n // what stops the two representations diverging: everything downstream gets\n // a host it can dial.\n const host = url.hostname.replace(/^\\[(.+)\\]$/, \"$1\");\n if (!LOOPBACK.has(host)) return null;\n\n const port = url.port ? Number(url.port) : 5432;\n if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;\n\n return { host, port };\n}\n\n/**\n * Whether a compose file declares a `db` service.\n *\n * Deliberately a text scan and not a YAML parse: the CLI has no YAML\n * dependency, and the question is only ever asked of a file this tool wrote.\n * A false negative costs the automation and nothing else — the reader gets the\n * manual steps, which are correct.\n */\nexport function composeDeclaresDbService(yamlText: string): boolean {\n let inServices = false;\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) {\n inServices = true;\n continue;\n }\n // Any other zero-indent key ends the services block.\n if (inServices && /^\\S/.test(line)) {\n inServices = false;\n continue;\n }\n if (inServices && /^\\s{2,}db:\\s*$/.test(line)) return true;\n }\n return false;\n}\n\n/**\n * The connection string the compose file's `db` service will answer on.\n *\n * `--docker` used to be a flag that changed a banner and nothing else: it\n * resolved to `kind: \"docker\"`, and every caller then had no URL to reach the\n * container with, so the backend booted with no `DATABASE_URL` at all and died\n * on the message a project with no database gets. The container it had just\n * been asked for was never started, because the preflight that starts it needs\n * a DSN to decide the database is local and not running.\n *\n * So the URL is derived rather than required: the compose file names the user,\n * the database and the published host port, and `.env` holds the password\n * compose itself interpolates. That is the same string `rebase init` writes as\n * the commented-out `DATABASE_URL`, which is what makes uncommenting that line\n * and passing `--docker` reach the same database.\n *\n * Returns null when the file declares no `db` service, or declares one this\n * scan cannot read — a `--docker` that cannot be honoured has to say so, and\n * a guess would point at somebody else's Postgres on 5432.\n *\n * Same text scan as {@link composeDeclaresDbService}, for the same reason.\n */\nexport function composeDatabaseUrl(\n yamlText: string,\n env: Record<string, string | undefined> = {}\n): string | null {\n const found = scanComposeDbService(yamlText);\n\n const user = expandComposeValue(found.POSTGRES_USER, env);\n const password = expandComposeValue(found.POSTGRES_PASSWORD, env);\n const database = expandComposeValue(found.POSTGRES_DB, env);\n if (!user || !password || !database || !found.hostPort) return null;\n\n return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}` +\n `@127.0.0.1:${found.hostPort}/${database}?options=-c%20search_path%3Dpublic&sslmode=disable`;\n}\n\n/**\n * The host port the compose `db` service publishes, or null.\n *\n * Separate from {@link composeDatabaseUrl} because the port is answerable on\n * its own: the URL needs the user, the password and the database name too, and\n * a compose file missing any of them still says which port a container would\n * listen on. That is the question \"would starting this container answer the\n * DSN in `.env`?\" — see the `wrong-port` branch of {@link ensureDevDatabase},\n * where getting it wrong meant starting a container nobody asked for.\n */\nexport function composeHostPort(yamlText: string): number | null {\n const port = scanComposeDbService(yamlText).hostPort;\n\n return port ? Number(port) : null;\n}\n\n/** The raw values of the compose `db` service, before any expansion. */\nfunction scanComposeDbService(yamlText: string): Record<string, string> & { hostPort?: string } {\n let inServices = false;\n let inDb = false;\n let dbIndent = 0;\n let section: \"environment\" | \"ports\" | null = null;\n const found: Record<string, string> = {};\n let hostPort: string | null = null;\n\n for (const rawLine of yamlText.split(/\\r?\\n/)) {\n const line = rawLine.replace(/\\t/g, \" \");\n if (/^\\s*#/.test(line) || line.trim() === \"\") continue;\n\n if (/^services:\\s*$/.test(line)) { inServices = true; continue; }\n if (inServices && /^\\S/.test(line)) { inServices = false; inDb = false; continue; }\n if (!inServices) continue;\n\n const indent = line.length - line.trimStart().length;\n const dbHeader = line.match(/^(\\s{2,})db:\\s*$/);\n if (dbHeader) { inDb = true; dbIndent = dbHeader[1].length; section = null; continue; }\n // A sibling service at the same indent as `db:` ends it.\n if (inDb && indent <= dbIndent) { inDb = false; section = null; continue; }\n if (!inDb) continue;\n\n const key = line.match(/^\\s+(environment|ports):\\s*$/);\n if (key) { section = key[1] as \"environment\" | \"ports\"; continue; }\n // Any other key of the service ends the block we were reading.\n if (/^\\s+[A-Za-z_][\\w-]*:\\s*\\S/.test(line) && !/^\\s+POSTGRES_/.test(line)) {\n if (!/^\\s*-\\s/.test(line.trimStart())) section = null;\n }\n\n if (section === \"environment\") {\n const pair = line.match(/^\\s+(POSTGRES_(?:USER|PASSWORD|DB)):\\s*(.+?)\\s*$/);\n if (pair) found[pair[1]] = pair[2].replace(/^[\"']|[\"']$/g, \"\");\n continue;\n }\n\n if (section === \"ports\" && hostPort === null) {\n // `- \"5435:5432\"`. Only a mapping whose container side is Postgres's\n // own port is the one to dial; anything else is a different service.\n const mapping = line.match(/^\\s*-\\s*[\"']?(?:[\\d.]+:)?(\\d+):5432(?:\\/tcp)?[\"']?\\s*$/);\n if (mapping) hostPort = mapping[1];\n }\n }\n\n return hostPort === null ? found : { ...found, hostPort };\n}\n\n/**\n * `${VAR:-default}`, `${VAR}` and a literal, resolved the way compose resolves\n * them — against the project's `.env`, which is the file compose itself reads.\n */\nfunction expandComposeValue(\n raw: string | undefined,\n env: Record<string, string | undefined>\n): string | null {\n if (!raw) return null;\n const interpolated = raw.replace(/\\$\\{([A-Za-z_][\\w]*)(?::?-([^}]*))?\\}/g, (_all, name, fallback) => {\n const value = env[name];\n if (value !== undefined && value !== \"\") return value;\n return fallback ?? \"\";\n });\n const trimmed = interpolated.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/** Is something accepting connections there right now? */\nexport function probeTcp(host: string, port: number, timeoutMs = 700): Promise<boolean> {\n return new Promise(resolve => {\n const socket = new net.Socket();\n let settled = false;\n\n const done = (open: boolean): void => {\n if (settled) return;\n settled = true;\n socket.destroy();\n resolve(open);\n };\n\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => done(true));\n socket.once(\"timeout\", () => done(false));\n socket.once(\"error\", () => done(false));\n socket.connect(port, host);\n });\n}\n\n/**\n * Poll until the port answers, or give up.\n *\n * Postgres publishes its port before it is ready to accept queries, so the\n * caller still has to tolerate a first connection that is refused at the\n * protocol level. This only answers \"has the container got as far as listening\",\n * which is the part that takes the seconds.\n */\nexport async function waitForPort(\n host: string,\n port: number,\n timeoutMs = 60_000,\n intervalMs = 500\n): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await probeTcp(host, port, 700)) return true;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n }\n return false;\n}\n\nexport interface EnsureDevDatabaseOptions {\n projectRoot: string;\n /** The DSN from the project's env file, if it has one. */\n databaseUrl: string | undefined;\n /** Set by `--no-db` or REBASE_DEV_NO_DB. */\n disabled: boolean;\n /** Whether the project has collections worth pushing. */\n hasCollections: boolean;\n /** Runs `rebase db push` for this project. Injected so tests need no database. */\n pushSchema: () => Promise<void>;\n /**\n * Brings the compose `db` service up. Injected for the same reason\n * `pushSchema` is: what happens *after* a start — a push that fails without\n * taking the dev server with it — is the part worth asserting, and it was\n * untestable while the only way in was a real `docker compose up`.\n */\n startDatabase?: (projectRoot: string) => Promise<void>;\n log?: (message: string) => void;\n}\n\n/**\n * Start the project's development database if it is not running, and give it a\n * schema if this call is what started it.\n */\nexport async function ensureDevDatabase(options: EnsureDevDatabaseOptions): Promise<PreflightOutcome> {\n const { projectRoot, databaseUrl, disabled, hasCollections, pushSchema } = options;\n const log = options.log ?? ((message: string) => console.log(message));\n\n if (disabled) return { action: \"disabled\" };\n\n const target = parseLoopbackDsn(databaseUrl);\n if (!databaseUrl) return { action: \"no-dsn\" };\n if (!target) {\n // Not an error, and not worth a warning either: pointing DATABASE_URL at\n // a real database is a completely ordinary thing to do, and the only\n // correct response is to keep away from it.\n let host = \"the configured host\";\n try {\n host = new URL(databaseUrl).hostname || host;\n } catch { /* keep the placeholder */ }\n return { action: \"remote-dsn\", host };\n }\n\n if (await probeTcp(target.host, target.port)) {\n return { action: \"already-running\", host: target.host, port: target.port };\n }\n\n const composePath = path.join(projectRoot, \"docker-compose.yml\");\n if (!fs.existsSync(composePath)) return { action: \"no-compose\" };\n const composeText = fs.readFileSync(composePath, \"utf8\");\n if (!composeDeclaresDbService(composeText)) return { action: \"no-compose\" };\n\n const manualSteps =\n `${chalk.cyan(\"docker compose up -d db\")} then ${chalk.cyan(\"rebase db push\")}`;\n\n // Would starting that container answer *this* DSN? A `.env` naming a port\n // the compose file does not publish is the commonest way to get here, and\n // starting the container then told the reader the exact opposite of the\n // truth: `⚠ The database container did not begin listening on port 3139.\n // Run docker compose up -d db then rebase db push to see why.` The\n // container was up and healthy on 5436; the port in `.env` was what was\n // wrong. And a container nobody asked for was left running.\n const composePort = composeHostPort(composeText);\n if (composePort !== null && composePort !== target.port) {\n const hint =\n `DATABASE_URL points at ${target.host}:${target.port} and nothing listens there. `\n + `The compose file's db service publishes ${composePort}, so starting it would not `\n + `answer that URL — nothing was started. Either correct the port in .env, or point `\n + `DATABASE_URL at 127.0.0.1:${composePort}.`;\n log(\"\");\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"wrong-port\", hint, port: target.port, composePort };\n }\n\n log(\"\");\n log(` ${chalk.gray(\"Database not running — starting it…\")}`);\n\n const startDatabase = options.startDatabase ?? (async (root: string) => {\n await execa(\"docker\", [\"compose\", \"up\", \"-d\", \"db\"], { cwd: root, stdio: \"pipe\" });\n });\n\n try {\n await startDatabase(projectRoot);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Two very different failures land here and the reader needs to know\n // which: Docker not being installed at all, and Docker being installed\n // but unable to start this container.\n if (/ENOENT|not found|command not found/i.test(message)) {\n const hint = `Docker was not found. Start a Postgres yourself, then run ${manualSteps}.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"no-docker\", hint };\n }\n const hint = `Could not start the database container. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n if (!(await waitForPort(target.host, target.port))) {\n const hint = `The database container did not begin listening on port ${target.port}. Run ${manualSteps} to see why.`;\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(hint)}`);\n return { action: \"start-failed\", hint };\n }\n\n log(` ${chalk.green(\"✓\")} ${chalk.gray(`Database ready on port ${target.port}`)}`);\n\n if (!hasCollections) return { action: \"started\", port: target.port, pushed: false };\n\n log(` ${chalk.gray(\"Pushing the schema…\")}`);\n try {\n await pushSchema();\n log(` ${chalk.green(\"✓\")} ${chalk.gray(\"Schema pushed\")}`);\n return { action: \"started\", port: target.port, pushed: true };\n } catch {\n // The push prints its own diagnostics through inherited stdio, so\n // repeating them here would only bury them. What is added is the one\n // thing the reader now needs: the server is still starting, and against\n // what.\n log(` ${chalk.yellow(\"⚠\")} ${chalk.gray(`Schema push failed. The database is running; fix the error above and run ${chalk.cyan(\"rebase db push\")}.`)}`);\n return { action: \"started\", port: target.port, pushed: false };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The checks that need no database, and that catch what breaks a first run.
3
+ *
4
+ * `rebase doctor` compared three descriptions of a schema, which is the right
5
+ * check for a project that is working and the wrong one for a project that has
6
+ * never worked. Everything that stops a first run — the wrong Node, two
7
+ * lockfiles disagreeing, two collections claiming one slug, a `JWT_SECRET`
8
+ * short enough to be guessed, a half-installed workspace with three versions of
9
+ * `@rebasepro/server` in it — happens before a single table is compared, and
10
+ * doctor said nothing about any of it.
11
+ *
12
+ * Every function here is pure: it takes what it read and returns findings. The
13
+ * reading and the printing are at the edges, in `commands/doctor.ts`, so each
14
+ * check has a fixture that fails it without a filesystem, a project or a
15
+ * database.
16
+ */
17
+ /** One thing that is wrong, and what to do about it. */
18
+ export interface EnvironmentFinding {
19
+ /** Which check produced it, for grouping in the report. */
20
+ check: "node" | "package-manager" | "collections" | "env" | "versions" | "atlas";
21
+ severity: "error" | "warning";
22
+ message: string;
23
+ fix: string;
24
+ }
25
+ /** What is on disk for `@ariga/atlas`, read by `commands/doctor.ts`. */
26
+ export interface AtlasBinaryState {
27
+ /** A `node_modules/.bin/atlas` the project's commands would find. */
28
+ onPath: boolean;
29
+ /** `@ariga/atlas/package.json` resolves from the project. */
30
+ packageInstalled: boolean;
31
+ /** The binary the package's `bin` names exists as a file. */
32
+ binaryOnDisk: boolean;
33
+ /** For the remedy: the reader's own package manager. */
34
+ manager: string;
35
+ }
36
+ /**
37
+ * Whether `db push`, `db generate` and `db migrate` can run at all.
38
+ *
39
+ * They shell out to `atlas`, and its absence is invisible until one of them is
40
+ * run: `@ariga/atlas` downloads its platform binary in `preinstall`, pnpm 10+
41
+ * and npm 12+ refuse a dependency's lifecycle scripts unless allowlisted, and
42
+ * the install exits 0 either way. The scaffold allowlists it, so this is a
43
+ * problem for the person who added `@rebasepro/server-postgres` to a project
44
+ * they already had — five lines of `ERR_PNPM_IGNORED_BUILDS` and
45
+ * `Failed to create bin … ENOENT`, several screens up, with nothing saying that
46
+ * a schema push will fail later.
47
+ *
48
+ * Three states, because two of them need opposite advice. The package absent is
49
+ * an install; the package present with no binary is an allowlist; the binary
50
+ * present with no `.bin` shim is a re-install, and telling that reader their
51
+ * build scripts are blocked sends them to a command that does nothing.
52
+ */
53
+ export declare function checkAtlasBinary(state: AtlasBinaryState | null): EnvironmentFinding[];
54
+ /** The lowest version a `>=x.y.z` range admits, or null for anything else. */
55
+ export declare function minimumFromRange(range: string): [number, number, number] | null;
56
+ /**
57
+ * Is the running Node old enough to break things that will not say so?
58
+ *
59
+ * The symptoms are not "unsupported Node". They are a syntax error inside a
60
+ * dependency, a missing global, or — the one that costs the most time — a
61
+ * `node:` builtin that resolves and then behaves differently. Naming the
62
+ * version is the whole check.
63
+ */
64
+ export declare function checkNodeVersion(running: string, required: string | undefined): EnvironmentFinding[];
65
+ /** The lockfiles a project may have, and the manager each one belongs to. */
66
+ export declare const LOCKFILES: Record<string, string>;
67
+ /**
68
+ * Two package managers in one project.
69
+ *
70
+ * A Rebase project is a pnpm workspace, and running `npm install` inside one
71
+ * rewrites `node_modules` into a hoisted layout that pnpm then disagrees with.
72
+ * The result is not an install error: it is `Cannot find module` from inside a
73
+ * dependency, hours later, on a machine where it worked yesterday. The
74
+ * leftover `package-lock.json` is the only durable evidence, and nothing looked
75
+ * at it.
76
+ */
77
+ export declare function checkPackageManager(lockfilesPresent: string[], declared: string | undefined): EnvironmentFinding[];
78
+ /**
79
+ * Two collections claiming one slug.
80
+ *
81
+ * The slug is the route, the table lookup and the registry key, and the
82
+ * registry keeps the last one registered. So the loser is not reported missing:
83
+ * it is served as the winner, with the winner's properties, under its own name.
84
+ * Every symptom of this points somewhere else.
85
+ */
86
+ export declare function checkDuplicateSlugs(collections: Array<{
87
+ slug?: string;
88
+ name?: string;
89
+ }>): EnvironmentFinding[];
90
+ /** What `loadEnv` requires of a JWT secret. Mirrors `rebaseEnvSchema`. */
91
+ export declare const MIN_JWT_SECRET_LENGTH = 32;
92
+ /**
93
+ * The two `.env` facts that fail late and confusingly.
94
+ *
95
+ * A short `JWT_SECRET` boots fine and is refused by `loadEnv` only in
96
+ * production, which is the worst place to find out. Missing CORS in production
97
+ * is the opposite shape — it does not fail at all; the API simply accepts
98
+ * requests from anywhere.
99
+ *
100
+ * Values are never echoed. This reads a file that holds every credential the
101
+ * project has, and doctor's output goes wherever a terminal goes.
102
+ */
103
+ export declare function checkEnvSanity(values: Record<string, string>): EnvironmentFinding[];
104
+ /**
105
+ * Parse a `.env` into a plain object.
106
+ *
107
+ * Deliberately not `dotenv`: this must not touch `process.env`, and doctor may
108
+ * be reading a file for a different environment than the one it is running in.
109
+ */
110
+ export declare function parseEnvFile(text: string): Record<string, string>;
111
+ /** One `@rebasepro/*` dependency, and where it was declared. */
112
+ export interface DeclaredDependency {
113
+ /** Where it was declared, relative to the project root. */
114
+ file: string;
115
+ name: string;
116
+ /** The range exactly as written. */
117
+ range: string;
118
+ }
119
+ /**
120
+ * The same `@rebasepro/*` package pinned to different versions in one project.
121
+ *
122
+ * A Rebase project declares them in several package.json files — the root, the
123
+ * backend, the frontend, the config workspace — and nothing has ever compared
124
+ * them. The failure that produces is not an install error: pnpm installs both,
125
+ * `instanceof` stops working across the two copies, and the symptom is a
126
+ * `CollectionConfig` that fails a type guard written against an identical type.
127
+ *
128
+ * `workspace:` ranges are excluded: inside this repo's own workspaces they all
129
+ * resolve to the same build by construction.
130
+ */
131
+ export declare function checkVersionSkew(declared: DeclaredDependency[]): EnvironmentFinding[];