@rebasepro/cli 0.24.0 → 0.24.1-canary.gcb918fc
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-BHvC6Bjs.js +336 -0
- package/dist/build-BHvC6Bjs.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-K6P5IHUb.js +9667 -0
- package/dist/cloud-K6P5IHUb.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/console.d.ts +12 -0
- package/dist/commands/db.d.ts +64 -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/console-Cs00E-dt.js +102 -0
- package/dist/console-Cs00E-dt.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-YbPMFY4Y.js +1252 -0
- package/dist/db-YbPMFY4Y.js.map +1 -0
- package/dist/derive-QMPMcQZJ.js +484 -0
- package/dist/derive-QMPMcQZJ.js.map +1 -0
- package/dist/dev-CJ_dd9uP.js +1593 -0
- package/dist/dev-CJ_dd9uP.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-CIcpWl_L.js +771 -0
- package/dist/doctor-CIcpWl_L.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 +150 -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-3J3UQg5r.js +161 -0
- package/dist/resources-3J3UQg5r.js.map +1 -0
- package/dist/schema-ovO8qJDw.js +125 -0
- package/dist/schema-ovO8qJDw.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-Cfy0TtSb.js +351 -0
- package/dist/status-Cfy0TtSb.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/utils/temp-archive.d.ts +9 -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 +13 -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,1252 @@
|
|
|
1
|
+
import { i as recordEvent } from "./telemetry-CAWNCugs.js";
|
|
2
|
+
import { _ as resolveTsxLoader, f as requireBackendDir, g as resolveTsx, h as resolvePluginCliScript, l as getActiveBackendPlugin, n as dependenciesNotInstalled, o as findEnvFile, p as requireProjectRoot } from "./project-Wp2zoyeN.js";
|
|
3
|
+
import { n as parseCommandArgs } from "./args-BnmsEXvu.js";
|
|
4
|
+
import { t as unknownCommand } from "./unknown-command-DNwDcX3w.js";
|
|
5
|
+
import { r as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-DUBuI2i1.js";
|
|
6
|
+
import { DEV_DATABASE_KIND_ENV, devDatabaseKind } from "./prepare-Btn9Ewbn.js";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import { execa } from "execa";
|
|
13
|
+
//#region src/utils/command-words.ts
|
|
14
|
+
/**
|
|
15
|
+
* The words that name a command, with the flags taken back out.
|
|
16
|
+
*
|
|
17
|
+
* `rawArgs` is the whole of `process.argv`, and reading the command's words at
|
|
18
|
+
* fixed indices off it assumes nothing precedes them. Something routinely does:
|
|
19
|
+
* `--debug` is what `bin/rebase.js` prints after every failure as the thing to
|
|
20
|
+
* re-run with, so it is the single most likely token to appear before a command
|
|
21
|
+
* word, and it shifted every index by one.
|
|
22
|
+
*
|
|
23
|
+
* What that cost is not a bad error message. `rebase db branch switch feature`
|
|
24
|
+
* writes a per-checkout pointer that the CLI owns; the driver, running as a
|
|
25
|
+
* child process, cannot persist it and does not try. The dispatch found
|
|
26
|
+
* `switch` by position, so `rebase --debug db branch switch feature` missed the
|
|
27
|
+
* CLI's branch and handed the line to the driver — which reported success and
|
|
28
|
+
* left the checkout on the main database. Every subsequent `dev`, `push` and
|
|
29
|
+
* `backup` then ran against the wrong database, believing it was the branch,
|
|
30
|
+
* which is the exact failure branching exists to prevent.
|
|
31
|
+
*
|
|
32
|
+
* The words are anchored on the command name rather than taken from position
|
|
33
|
+
* zero, because a flag written in the space form (`--database-url <url>`) leaves
|
|
34
|
+
* its value behind as a bare token and no parser at this level knows which
|
|
35
|
+
* flags take values. Anchoring survives that: the value would have to be the
|
|
36
|
+
* literal command name to fool it.
|
|
37
|
+
*
|
|
38
|
+
* Returns `[]` when the command name is not on the line at all.
|
|
39
|
+
*/
|
|
40
|
+
function commandWords(rawArgs, command) {
|
|
41
|
+
const words = rawArgs.slice(2).filter((token) => !token.startsWith("-"));
|
|
42
|
+
const start = words.indexOf(command);
|
|
43
|
+
return start === -1 ? [] : words.slice(start);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The line to hand a spawned driver, starting at the command word.
|
|
47
|
+
*
|
|
48
|
+
* The same defect one layer down, and it does not degrade gracefully: the
|
|
49
|
+
* driver reads its domain from `args[0]`, so `rebase --debug db push` spawned
|
|
50
|
+
* it with `["--debug", "db", "push"]` and it answered "Unknown domain command:
|
|
51
|
+
* --debug" — for a flag the CLI itself tells you to add after any failure.
|
|
52
|
+
*
|
|
53
|
+
* Tokens written *before* the command word are moved after it rather than
|
|
54
|
+
* dropped. Dropping them would be the same class of bug in the other direction:
|
|
55
|
+
* `rebase --database-url postgres://… db push` would silently lose the flag that
|
|
56
|
+
* says which database to touch. Appending keeps each flag next to its value, and
|
|
57
|
+
* `arg` reads flags wherever they appear.
|
|
58
|
+
*
|
|
59
|
+
* Falls back to `rawArgs.slice(2)` when the command word is not on the line,
|
|
60
|
+
* which is what an internally synthesised argv looks like.
|
|
61
|
+
*/
|
|
62
|
+
function argsFromCommand(rawArgs, command) {
|
|
63
|
+
const tail = rawArgs.slice(2);
|
|
64
|
+
const start = tail.findIndex((token) => token === command);
|
|
65
|
+
if (start <= 0) return [...tail];
|
|
66
|
+
return [...tail.slice(start), ...tail.slice(0, start)];
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/utils/spawn-error.ts
|
|
70
|
+
/**
|
|
71
|
+
* What to say when a spawned child process fails.
|
|
72
|
+
*
|
|
73
|
+
* Two failures arrive through the same `catch` and need opposite treatment:
|
|
74
|
+
*
|
|
75
|
+
* - **The child ran and exited non-zero.** It had inherited stdio, so it has
|
|
76
|
+
* already printed its own diagnostics. execa's "Command failed with exit
|
|
77
|
+
* code 1: …" is a second, worse copy of the same news with the whole argv
|
|
78
|
+
* appended.
|
|
79
|
+
* - **The child never started.** ENOENT on the tsx binary, EACCES on a script
|
|
80
|
+
* that lost its permissions, ENOMEM on a full machine. Nobody printed
|
|
81
|
+
* anything, because nothing ran. `schema.ts` and `doctor.ts` both swallowed
|
|
82
|
+
* these — `catch { process.exit(1); }`, the error not even bound — so
|
|
83
|
+
* `rebase schema generate` against a broken tsx exited 1 in silence, and so
|
|
84
|
+
* did `rebase doctor`, the command whose entire job is to say what is wrong.
|
|
85
|
+
*
|
|
86
|
+
* The tsx symlink surviving a cleaned pnpm store is the ordinary way to get
|
|
87
|
+
* here: `resolveLocalBin` finds the symlink, so "dependencies are not
|
|
88
|
+
* installed" never fires, and the spawn is where it breaks.
|
|
89
|
+
*
|
|
90
|
+
* ## Why the message is not what decides it
|
|
91
|
+
*
|
|
92
|
+
* `db.ts` did make this distinction, by testing the message against
|
|
93
|
+
* `/Command failed|exited with code/i` — and that test is wrong for every case
|
|
94
|
+
* it was written to catch. execa prefixes **all** of them the same way:
|
|
95
|
+
*
|
|
96
|
+
* Command failed with exit code 3: /bin/sh -c 'exit 3'
|
|
97
|
+
* Command failed with ENOENT: /…/tsx x⏎spawn /…/tsx ENOENT
|
|
98
|
+
* Command failed with EACCES: /…/tsx x⏎spawn /…/tsx EACCES
|
|
99
|
+
*
|
|
100
|
+
* So the filter matched the spawn failures too and `db push` was as silent as
|
|
101
|
+
* the other two. The structured fields do distinguish them: a child that ran
|
|
102
|
+
* has a numeric `exitCode` (or a `signal`, if something killed it), and a child
|
|
103
|
+
* that never started has neither — only an errno in `code`.
|
|
104
|
+
*/
|
|
105
|
+
/** Did the process start? Only then has anything else already been printed. */
|
|
106
|
+
function childRan(error) {
|
|
107
|
+
if (!(error instanceof Error)) return false;
|
|
108
|
+
const failure = error;
|
|
109
|
+
return typeof failure.exitCode === "number" || typeof failure.signal === "string";
|
|
110
|
+
}
|
|
111
|
+
function reportSpawnFailure(error) {
|
|
112
|
+
if (childRan(error) || !(error instanceof Error)) return;
|
|
113
|
+
const failure = error;
|
|
114
|
+
const message = typeof failure.originalMessage === "string" ? failure.originalMessage : error.message;
|
|
115
|
+
if (message) console.error(chalk.red(`✗ ${message}`));
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/commands/db.ts
|
|
119
|
+
/**
|
|
120
|
+
* CLI command: rebase db <action>
|
|
121
|
+
*/
|
|
122
|
+
/**
|
|
123
|
+
* The connection string a prepared database actually answers on.
|
|
124
|
+
*
|
|
125
|
+
* One function, because `db url` and `db pull` disagreed about it and the
|
|
126
|
+
* disagreement was a dead command: `prepareDatabaseEnv` returns `env: {}` for a
|
|
127
|
+
* plain external database — the connection string is already somewhere the
|
|
128
|
+
* child looks — so `db pull`'s `prepared.env.DATABASE_URL ?? process.env
|
|
129
|
+
* .DATABASE_URL` was empty for the standard `.env` configuration and answered
|
|
130
|
+
* `✗ No local database to pull into.` on a project whose `rebase db url` had
|
|
131
|
+
* just printed one. Exporting the same URL in the shell made it work, which is
|
|
132
|
+
* the shape of a bug nobody reports as one.
|
|
133
|
+
*
|
|
134
|
+
* `readEnvFile` is passed in rather than imported: this module loads
|
|
135
|
+
* `../utils/project` dynamically everywhere else, and the callers already have
|
|
136
|
+
* it open.
|
|
137
|
+
*/
|
|
138
|
+
function databaseUrlOf(prepared, envFile) {
|
|
139
|
+
const named = (value) => value && value.trim() ? value : void 0;
|
|
140
|
+
return named(prepared.env.DATABASE_URL) ?? named(prepared.database.kind === "managed" ? void 0 : prepared.database.url) ?? named(envFile.DATABASE_URL) ?? named(process.env.DATABASE_URL);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A destination that names a remote store rather than a local path.
|
|
144
|
+
*
|
|
145
|
+
* Matched as "scheme://" generally, not as a list of the schemes we support:
|
|
146
|
+
* an unknown scheme is still not something to join onto a filesystem path, and
|
|
147
|
+
* a Windows drive letter ("C:\backups") has no "//" so it stays a path.
|
|
148
|
+
*/
|
|
149
|
+
var REMOTE_DESTINATION_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
150
|
+
/**
|
|
151
|
+
* The flags whose value is a local path, on every line the CLI relays.
|
|
152
|
+
*
|
|
153
|
+
* All three spellings of the destination, because the driver accepts all
|
|
154
|
+
* three: `backup-cli.ts` maps `--output` and `-o` onto `--out`, and `rebase db
|
|
155
|
+
* backup --help` advertises `--output` as an alias. Only `--out` and `-o` were
|
|
156
|
+
* absolutised once, so the one spelling the help page names was the one that
|
|
157
|
+
* landed in `backend/` — and the success line still echoed the path as typed.
|
|
158
|
+
*
|
|
159
|
+
* And `--collections`, which was not absolutised at all: from the project root,
|
|
160
|
+
* `rebase schema generate --collections ./config/collections` was "not found",
|
|
161
|
+
* while the same spelling worked for `generate-sdk`. See `utils/path-flags.ts`.
|
|
162
|
+
*/
|
|
163
|
+
var LOCAL_PATH_FLAGS = [
|
|
164
|
+
"--out",
|
|
165
|
+
"--output",
|
|
166
|
+
"-o",
|
|
167
|
+
"--collections",
|
|
168
|
+
"-c"
|
|
169
|
+
];
|
|
170
|
+
/**
|
|
171
|
+
* Rewrite local path arguments so they mean what the user typed.
|
|
172
|
+
*
|
|
173
|
+
* The plugin CLI is spawned with `cwd: backendDir` — it has to be, because that
|
|
174
|
+
* is where the plugin and its dependencies resolve from. But the developer runs
|
|
175
|
+
* `rebase db` from the project root, so a relative `--out ./backups` was being
|
|
176
|
+
* resolved against `backend/` and landed in `backend/backups`, while the
|
|
177
|
+
* success line echoed the path as typed. The file was real and the reported
|
|
178
|
+
* location was wrong, which is the worst way for a backup command to behave.
|
|
179
|
+
* `rebase db --help` documents exactly this invocation.
|
|
180
|
+
*
|
|
181
|
+
* Absolutising here rather than inside the plugin keeps the fix where the cwd
|
|
182
|
+
* is actually changed, and leaves the plugin usable on its own terms.
|
|
183
|
+
*/
|
|
184
|
+
function absolutizeLocalPathArgs(args, cwd, pathFlags = LOCAL_PATH_FLAGS) {
|
|
185
|
+
const takesPath = (flag) => pathFlags.includes(flag);
|
|
186
|
+
/**
|
|
187
|
+
* Flags whose next token is a value, not a positional.
|
|
188
|
+
*
|
|
189
|
+
* Mirrors the `arg` specs in `@rebasepro/server-postgres`'s backup CLI.
|
|
190
|
+
* Needed only to find the `db restore` dump argument: without it,
|
|
191
|
+
* `restore --target-db app_restored ./x.dump` treats `app_restored` as the
|
|
192
|
+
* positional — it is the first token not starting with `-` — and turns a
|
|
193
|
+
* database name into a path while leaving the real dump path unresolved.
|
|
194
|
+
*/
|
|
195
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
196
|
+
"--out",
|
|
197
|
+
"--output",
|
|
198
|
+
"-o",
|
|
199
|
+
"--target-db",
|
|
200
|
+
"--exclude-schema",
|
|
201
|
+
"--row-security-role"
|
|
202
|
+
]);
|
|
203
|
+
const out = [...args];
|
|
204
|
+
for (let i = 0; i < out.length; i++) {
|
|
205
|
+
const arg = out[i];
|
|
206
|
+
const eq = arg.indexOf("=");
|
|
207
|
+
if (eq > 0 && takesPath(arg.slice(0, eq))) {
|
|
208
|
+
const value = arg.slice(eq + 1);
|
|
209
|
+
if (value && !REMOTE_DESTINATION_RE.test(value)) out[i] = `${arg.slice(0, eq)}=${path.resolve(cwd, value)}`;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (takesPath(arg)) {
|
|
213
|
+
const value = out[i + 1];
|
|
214
|
+
if (value && !value.startsWith("-") && !REMOTE_DESTINATION_RE.test(value)) {
|
|
215
|
+
out[i + 1] = path.resolve(cwd, value);
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const restoreAt = out.indexOf("restore");
|
|
221
|
+
if (restoreAt !== -1) for (let i = restoreAt + 1; i < out.length; i++) {
|
|
222
|
+
const arg = out[i];
|
|
223
|
+
if (arg.startsWith("-")) {
|
|
224
|
+
if (!arg.includes("=") && VALUE_FLAGS.has(arg)) i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!REMOTE_DESTINATION_RE.test(arg)) out[i] = path.resolve(cwd, arg);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
/**
|
|
234
|
+
* Read `--flag value` or `--flag=value` out of the raw argv.
|
|
235
|
+
*
|
|
236
|
+
* This command forwards its arguments to the driver plugin rather than parsing
|
|
237
|
+
* them, so the two flags the CLI itself acts on are picked out by hand instead
|
|
238
|
+
* of adding a spec that would then have to strip them back out again.
|
|
239
|
+
*/
|
|
240
|
+
function readFlagValue(rawArgs, flag) {
|
|
241
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
242
|
+
const arg = rawArgs[index];
|
|
243
|
+
if (arg === flag) return rawArgs[index + 1] ?? null;
|
|
244
|
+
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Run a database subcommand through the active driver's CLI, throwing on
|
|
250
|
+
* failure instead of exiting.
|
|
251
|
+
*
|
|
252
|
+
* `dbCommand` below turns every failure into `process.exit(1)`, which is right
|
|
253
|
+
* for a command the user invoked directly and wrong for a caller that has more
|
|
254
|
+
* to do afterwards — `rebase dev` runs a schema push during start-up and must
|
|
255
|
+
* survive it failing. Exiting is therefore the wrapper's job, not this
|
|
256
|
+
* function's.
|
|
257
|
+
*
|
|
258
|
+
* The database is resolved *here* rather than in the wrapper, so the schema
|
|
259
|
+
* push `rebase dev` performs during start-up reaches the managed database too.
|
|
260
|
+
*/
|
|
261
|
+
/**
|
|
262
|
+
* The `db` subcommands that plan their work with Atlas.
|
|
263
|
+
*
|
|
264
|
+
* Atlas computes a diff by replaying the desired schema into a **second, empty
|
|
265
|
+
* database** and comparing. That is why these three are listed and `backup` and
|
|
266
|
+
* `restore` are not — those shell out to `pg_dump`, which fails loudly on its
|
|
267
|
+
* own. `branch` is refused too, but for its own reason and with its own
|
|
268
|
+
* message: see {@link refuseBranchOnManagedDatabase}.
|
|
269
|
+
*/
|
|
270
|
+
var ATLAS_BACKED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
271
|
+
"push",
|
|
272
|
+
"generate",
|
|
273
|
+
"migrate"
|
|
274
|
+
]);
|
|
275
|
+
/**
|
|
276
|
+
* A managed-database refusal, raised so the caller decides what it costs.
|
|
277
|
+
*
|
|
278
|
+
* These two guards used to `process.exit(1)` where they stood. That is right
|
|
279
|
+
* for `rebase db push`, typed by a person, and catastrophic for the same code
|
|
280
|
+
* reached from inside `rebase dev`: the dev server's first-boot schema push
|
|
281
|
+
* runs in-process, so an exit there took the whole dev server down — with a
|
|
282
|
+
* `try/catch` around it written expressly to prevent that, which could never
|
|
283
|
+
* run. The first `rebase dev --docker` of every scaffold died this way.
|
|
284
|
+
*
|
|
285
|
+
* So the refusal is a value now. It carries the block it wants printed rather
|
|
286
|
+
* than printing it, because who prints (and whether printing is even right) is
|
|
287
|
+
* the caller's question: `dbCommand` prints it and exits 1, and `rebase dev`
|
|
288
|
+
* keeps serving.
|
|
289
|
+
*/
|
|
290
|
+
var ManagedDatabaseRefusal = class extends Error {
|
|
291
|
+
/** The lines to print, in order, already coloured. */
|
|
292
|
+
lines;
|
|
293
|
+
constructor(summary, lines) {
|
|
294
|
+
super(summary);
|
|
295
|
+
this.name = "ManagedDatabaseRefusal";
|
|
296
|
+
this.lines = lines;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/** Print a refusal exactly as the guards used to print it themselves. */
|
|
300
|
+
function printManagedDatabaseRefusal(refusal) {
|
|
301
|
+
for (const line of refusal.lines) console.error(line);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The variable the driver reads for Atlas's dev database
|
|
305
|
+
* (`ATLAS_DEV_URL_ENV` in `@rebasepro/server-postgres`, which this CLI does not
|
|
306
|
+
* depend on).
|
|
307
|
+
*/
|
|
308
|
+
var ATLAS_DEV_URL_ENV = "REBASE_ATLAS_DEV_URL";
|
|
309
|
+
/** Loaded only when a command needs one: PGlite is 25 MB of WASM. */
|
|
310
|
+
var startAtlasDevDatabase = async () => (await import("./atlas-dev-database-DTMuEScS.js")).startAtlasDevDatabase();
|
|
311
|
+
/**
|
|
312
|
+
* The empty database an Atlas-backed subcommand plans against, on the managed
|
|
313
|
+
* development database.
|
|
314
|
+
*
|
|
315
|
+
* **PGlite serves exactly one database.** Atlas computes a change by replaying
|
|
316
|
+
* the desired schema into a second, empty one; on a real server the driver
|
|
317
|
+
* makes it as `<db>_dev_diff`, but `CREATE DATABASE` against PGlite reports
|
|
318
|
+
* success and creates nothing, so Atlas connected straight back to the
|
|
319
|
+
* database it was comparing and stopped with `connected database is not
|
|
320
|
+
* clean`. These commands were refused outright on the managed database — and
|
|
321
|
+
* with them every way to drop, rename or narrow a column there, since boot
|
|
322
|
+
* only adds.
|
|
323
|
+
*
|
|
324
|
+
* So a throwaway second PGlite is started for the command instead — in
|
|
325
|
+
* memory, in this process, gone when the command ends — and handed to the
|
|
326
|
+
* driver as `REBASE_ATLAS_DEV_URL`. Nothing else about the push changes: the
|
|
327
|
+
* destructive gate is the driver's, and holds as it does everywhere.
|
|
328
|
+
*
|
|
329
|
+
* `undefined` when none is needed: another database kind (the driver makes its
|
|
330
|
+
* own), a subcommand that is not Atlas's, or a `REBASE_ATLAS_DEV_URL` the
|
|
331
|
+
* developer set. When one cannot be started — PGlite is an optional
|
|
332
|
+
* dependency — the old refusal stands, with the reason.
|
|
333
|
+
*/
|
|
334
|
+
async function atlasDevDatabaseFor(rawArgs, kind, env, start = startAtlasDevDatabase) {
|
|
335
|
+
if (kind !== "managed") return void 0;
|
|
336
|
+
const [domain, subcommand] = commandWords(rawArgs, "db");
|
|
337
|
+
if (domain !== "db" || !ATLAS_BACKED_SUBCOMMANDS.has(subcommand ?? "")) return void 0;
|
|
338
|
+
if (env["REBASE_ATLAS_DEV_URL"]?.trim()) return void 0;
|
|
339
|
+
try {
|
|
340
|
+
return await start();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const summary = `rebase db ${subcommand} could not start the database Atlas plans against.`;
|
|
343
|
+
throw new ManagedDatabaseRefusal(summary, [
|
|
344
|
+
"",
|
|
345
|
+
chalk.red(` ✗ ${summary}`),
|
|
346
|
+
chalk.gray(` ${err instanceof Error ? err.message : String(err)}`),
|
|
347
|
+
"",
|
|
348
|
+
chalk.gray(" Atlas plans every change against a second, empty database. On the managed"),
|
|
349
|
+
chalk.gray(" database that is a throwaway PGlite, started for the command."),
|
|
350
|
+
"",
|
|
351
|
+
chalk.gray(` ${chalk.cyan("rebase dev")} still applies your collections at boot, additively. Otherwise,`),
|
|
352
|
+
chalk.gray(` set ${ATLAS_DEV_URL_ENV} to an empty Postgres database Atlas may fill and clear,`),
|
|
353
|
+
chalk.gray(" or point the project at a real Postgres (uncomment DATABASE_URL in .env)."),
|
|
354
|
+
""
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Stop `rebase db branch` before it reports a branch that is not one.
|
|
360
|
+
*
|
|
361
|
+
* **PGlite serves exactly one database**, and this is the second thing that
|
|
362
|
+
* follows from it. `CREATE DATABASE "rb_feature_x" TEMPLATE "postgres"` there
|
|
363
|
+
* writes a `pg_database` catalog row and nothing else. Nothing errors, so the
|
|
364
|
+
* whole feature reports success end to end:
|
|
365
|
+
*
|
|
366
|
+
* $ rebase db branch create feature_x
|
|
367
|
+
* ✓ Branch "feature_x" created successfully.
|
|
368
|
+
* $ rebase db branch list
|
|
369
|
+
* ● feature_x (7.1 MB) — created just now
|
|
370
|
+
*
|
|
371
|
+
* `listBranches` joins `pg_database`, the catalog row is there, and
|
|
372
|
+
* `pg_database_size` answers with the one database's size — so the listing
|
|
373
|
+
* corroborates the lie. Then connecting to `rb_feature_x` reports
|
|
374
|
+
* `current_database()` = `postgres`, and a table created "in the branch" is
|
|
375
|
+
* visible in the parent immediately. Measured on a fresh `rebase init` scaffold.
|
|
376
|
+
*
|
|
377
|
+
* **The branch is the parent.** Every write made in the belief that it is
|
|
378
|
+
* sandboxed lands in the developer's real development database — which is the
|
|
379
|
+
* exact failure branching exists to prevent, announced as a success.
|
|
380
|
+
*
|
|
381
|
+
* So this refuses the whole `branch` domain rather than `create` alone: `list`
|
|
382
|
+
* on the managed database ends with "Create one with: rebase db branch create",
|
|
383
|
+
* an invitation to do the broken thing, and `info` would confirm a size for a
|
|
384
|
+
* database that was never made.
|
|
385
|
+
*
|
|
386
|
+
* Push, generate and migrate had the same problem — Atlas's scratch database
|
|
387
|
+
* is a second database too — and get a throwaway PGlite of their own instead
|
|
388
|
+
* (`atlasDevDatabaseFor` above). A branch cannot: it is meant to keep the
|
|
389
|
+
* project's data, and a throwaway instance has none.
|
|
390
|
+
*/
|
|
391
|
+
function refuseBranchOnManagedDatabase(rawArgs, kind) {
|
|
392
|
+
if (kind !== "managed") return;
|
|
393
|
+
const [domain, subcommand] = commandWords(rawArgs, "db");
|
|
394
|
+
if (domain !== "db" || subcommand !== "branch") return;
|
|
395
|
+
const summary = "rebase db branch does not work on the managed development database.";
|
|
396
|
+
throw new ManagedDatabaseRefusal(summary, [
|
|
397
|
+
"",
|
|
398
|
+
chalk.red(` ✗ ${summary}`),
|
|
399
|
+
"",
|
|
400
|
+
chalk.gray(" Branching copies a database with CREATE DATABASE ... TEMPLATE. The managed"),
|
|
401
|
+
chalk.gray(" database is PGlite, which serves exactly one — the copy would be the"),
|
|
402
|
+
chalk.gray(" original, and every write you meant to sandbox would land in it."),
|
|
403
|
+
"",
|
|
404
|
+
chalk.gray(" Branching needs a real Postgres. Either:"),
|
|
405
|
+
chalk.gray(` ${chalk.cyan("rebase dev --docker")} starts one, and branches work against it`),
|
|
406
|
+
chalk.gray(" or uncomment DATABASE_URL in .env to point at your own."),
|
|
407
|
+
""
|
|
408
|
+
]);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve the active driver's CLI and the environment it should run in.
|
|
412
|
+
*
|
|
413
|
+
* Shared by the two runners below. Deliberately does NOT resolve a database:
|
|
414
|
+
* `schema generate` reads collection files and writes one file, and starting a
|
|
415
|
+
* database to run it would be a side effect nobody asked for.
|
|
416
|
+
*/
|
|
417
|
+
async function resolveDriverCli() {
|
|
418
|
+
const projectRoot = requireProjectRoot();
|
|
419
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
420
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
421
|
+
if (!activePlugin) throw new Error("Could not detect an active database plugin. Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.");
|
|
422
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
423
|
+
if (!pluginCli) throw new Error(dependenciesNotInstalled(projectRoot));
|
|
424
|
+
const envFile = findEnvFile(projectRoot);
|
|
425
|
+
const env = { ...process.env };
|
|
426
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
427
|
+
env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
|
|
428
|
+
return {
|
|
429
|
+
projectRoot,
|
|
430
|
+
backendDir,
|
|
431
|
+
pluginCli,
|
|
432
|
+
env
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The one place this CLI spawns the driver's own CLI.
|
|
437
|
+
*
|
|
438
|
+
* There were three, each with its own copy of "tsx for a `.ts` entry, `node`
|
|
439
|
+
* otherwise". That rule was right only while the driver shipped `src/`; once it
|
|
440
|
+
* shipped `dist/cli.js` all three quietly switched to plain `node`, and the
|
|
441
|
+
* driver loads the PROJECT's TypeScript in process:
|
|
442
|
+
*
|
|
443
|
+
* - `schema stale` reads the collections, could not resolve `./authors` onto
|
|
444
|
+
* `authors.ts`, and answered "⏭ Not checked" while exiting 0;
|
|
445
|
+
* - `db push` does the same in `ensureAuthTables`, so `rebase.users` was never
|
|
446
|
+
* created and the push died at "Applying RLS policies" with
|
|
447
|
+
* `relation "rebase.users" does not exist` — on a virgin database, which is
|
|
448
|
+
* the documented bring-your-own-Postgres first run.
|
|
449
|
+
*
|
|
450
|
+
* Two were fixed and the third was missed, because three copies of a decision
|
|
451
|
+
* are three chances to miss one. There is one now, and a gate holds it at one.
|
|
452
|
+
*
|
|
453
|
+
* tsx whenever it is installed — a devDependency of every scaffold, for exactly
|
|
454
|
+
* this reason. Without it a `.ts` entry cannot run at all; a built entry still
|
|
455
|
+
* runs, and the subcommands that never touch the project's TypeScript still work.
|
|
456
|
+
*
|
|
457
|
+
* tsx's loader in one Node (`node --import`) rather than the `tsx` binary,
|
|
458
|
+
* which is a shell shim and a relay Node in front of the same loader: one Node
|
|
459
|
+
* start instead of two, ~50–100 ms on each of the driver runs every `rebase
|
|
460
|
+
* dev` start and every collection save makes. The binary stays the fallback for
|
|
461
|
+
* a tsx whose loader does not resolve as a package — a global install.
|
|
462
|
+
*/
|
|
463
|
+
async function spawnDriverCli(resolved, childArgs, options = {}) {
|
|
464
|
+
const { projectRoot, backendDir, pluginCli, env } = resolved;
|
|
465
|
+
const stdio = options.quiet ? "pipe" : "inherit";
|
|
466
|
+
const tsxBin = resolveTsx(projectRoot);
|
|
467
|
+
const tsxLoader = resolveTsxLoader(projectRoot);
|
|
468
|
+
if (!tsxBin && !tsxLoader && pluginCli.endsWith(".ts")) throw new Error(dependenciesNotInstalled(projectRoot));
|
|
469
|
+
const [command, ...prefix] = tsxLoader ? [
|
|
470
|
+
process.execPath,
|
|
471
|
+
"--import",
|
|
472
|
+
tsxLoader
|
|
473
|
+
] : [tsxBin ?? "node"];
|
|
474
|
+
await execa(command, [
|
|
475
|
+
...prefix,
|
|
476
|
+
pluginCli,
|
|
477
|
+
...childArgs
|
|
478
|
+
], {
|
|
479
|
+
cwd: backendDir,
|
|
480
|
+
stdio,
|
|
481
|
+
env
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/** @deprecated Use {@link spawnDriverCli}. Kept as the internal name callers use. */
|
|
485
|
+
var execDriverCli = spawnDriverCli;
|
|
486
|
+
/**
|
|
487
|
+
* Run a `schema` subcommand through the active driver's CLI.
|
|
488
|
+
*
|
|
489
|
+
* Separate from {@link runDriverDbCommand} because it must NOT resolve a
|
|
490
|
+
* database. `rebase dev` calls this before the database exists, which is the
|
|
491
|
+
* whole point: the generated schema has to be right before anything reads it.
|
|
492
|
+
* Throws rather than exiting — the caller has more to do.
|
|
493
|
+
*/
|
|
494
|
+
async function runDriverSchemaCommand(rawArgs, options = {}) {
|
|
495
|
+
await execDriverCli(await resolveDriverCli(), rawArgs.slice(2), options);
|
|
496
|
+
}
|
|
497
|
+
async function runDriverDbCommand(rawArgs, options = {}) {
|
|
498
|
+
const projectRoot = requireProjectRoot();
|
|
499
|
+
const backendDir = requireBackendDir(projectRoot);
|
|
500
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
501
|
+
if (!activePlugin) throw new Error("Could not detect an active database plugin. Make sure a package like @rebasepro/server-postgres is installed in backend/package.json.");
|
|
502
|
+
const pluginCli = resolvePluginCliScript(backendDir, activePlugin);
|
|
503
|
+
if (!pluginCli) throw new Error(dependenciesNotInstalled(projectRoot));
|
|
504
|
+
const envFile = findEnvFile(projectRoot);
|
|
505
|
+
const env = { ...process.env };
|
|
506
|
+
if (envFile) env.DOTENV_CONFIG_PATH = envFile;
|
|
507
|
+
env[DEV_DATABASE_KIND_ENV] = devDatabaseKind(projectRoot) ?? "";
|
|
508
|
+
const { resolveDevDatabase } = await import("./resolve-CJLkfFV0.js");
|
|
509
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
510
|
+
refuseBranchOnManagedDatabase(rawArgs, resolveDevDatabase({
|
|
511
|
+
flagUrl: readFlagValue(rawArgs, "--database-url"),
|
|
512
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
513
|
+
env: process.env,
|
|
514
|
+
envFile: readEnvFile(projectRoot)
|
|
515
|
+
}).kind);
|
|
516
|
+
const { prepareDatabaseEnv, managedNotices } = await import("./prepare-Btn9Ewbn.js");
|
|
517
|
+
const prepared = await prepareDatabaseEnv(projectRoot, {
|
|
518
|
+
flagUrl: readFlagValue(rawArgs, "--database-url"),
|
|
519
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
520
|
+
onProgress: (message) => {
|
|
521
|
+
if (!options.quiet) console.log(chalk.gray(` ${message}`));
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
Object.assign(env, prepared.env);
|
|
525
|
+
if (!options.quiet) for (const line of managedNotices(prepared)) console.log(chalk.gray(` ${line}`));
|
|
526
|
+
const atlasDev = await atlasDevDatabaseFor(rawArgs, prepared.database.kind, env);
|
|
527
|
+
if (atlasDev) env[ATLAS_DEV_URL_ENV] = atlasDev.url;
|
|
528
|
+
const childArgs = absolutizeLocalPathArgs(argsFromCommand(rawArgs, "db"), process.cwd());
|
|
529
|
+
try {
|
|
530
|
+
await spawnDriverCli({
|
|
531
|
+
projectRoot,
|
|
532
|
+
backendDir,
|
|
533
|
+
pluginCli,
|
|
534
|
+
env
|
|
535
|
+
}, childArgs, options);
|
|
536
|
+
} finally {
|
|
537
|
+
await atlasDev?.stop();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function dbCommand(subcommand, rawArgs) {
|
|
541
|
+
if (!subcommand || subcommand === "--help" || rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
542
|
+
printDbHelp(subcommand === "--help" ? void 0 : subcommand);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!DB_ACTION_HELP[subcommand]) unknownCommand(subcommand, Object.keys(DB_ACTION_HELP), "db");
|
|
546
|
+
const projectRoot = requireProjectRoot();
|
|
547
|
+
recordEvent("cli.db", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
548
|
+
if (subcommand === "stop" || subcommand === "reset") {
|
|
549
|
+
await manageLocalDatabase(subcommand, projectRoot, rawArgs);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
await ensureDockerDatabase(projectRoot, rawArgs);
|
|
553
|
+
if (subcommand === "pull") {
|
|
554
|
+
await pullIntoLocal(projectRoot, rawArgs);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (subcommand === "url") {
|
|
558
|
+
await printDatabaseUrl(projectRoot, rawArgs);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (subcommand === "branch" && commandWords(rawArgs, "db")[2] === "switch") {
|
|
562
|
+
await switchBranch(projectRoot, rawArgs);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
let driverArgs = rawArgs;
|
|
566
|
+
if (subcommand === "branch") {
|
|
567
|
+
const action = commandWords(rawArgs, "db")[2];
|
|
568
|
+
if (action === "delete") await refuseDeletingActiveBranch(projectRoot, rawArgs);
|
|
569
|
+
if (action && REGISTRY_ACTIONS.has(action)) driverArgs = await onParentDatabase(projectRoot, rawArgs, action);
|
|
570
|
+
}
|
|
571
|
+
refuseMissingCollectionsPath(driverArgs, process.cwd(), requireBackendDir(projectRoot));
|
|
572
|
+
try {
|
|
573
|
+
await runDriverDbCommand(driverArgs);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error instanceof ManagedDatabaseRefusal) {
|
|
576
|
+
printManagedDatabaseRefusal(error);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
reportSpawnFailure(error);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* `rebase db url` — the connection string this project is using, on stdout.
|
|
585
|
+
*
|
|
586
|
+
* The managed database is the one case where nothing on disk names it: `.env`
|
|
587
|
+
* ships `DATABASE_URL` commented out on purpose, the port is derived from the
|
|
588
|
+
* project path, and the data lives under `.rebase/`. A headless project has no
|
|
589
|
+
* admin panel to open either, so before this there was no way to point `psql`,
|
|
590
|
+
* a GUI client or a seeding script at the database `rebase dev` had just
|
|
591
|
+
* created — short of reading the CLI's source.
|
|
592
|
+
*
|
|
593
|
+
* Resolution is the same ordered rule every other command uses, so this prints
|
|
594
|
+
* your own `DATABASE_URL` when you have set one rather than inventing a second
|
|
595
|
+
* answer. Nothing but the URL goes to stdout, so it pipes:
|
|
596
|
+
*
|
|
597
|
+
* psql "$(rebase db url)"
|
|
598
|
+
*
|
|
599
|
+
* It starts the managed database if it is not running, because a connection
|
|
600
|
+
* string for a database nobody is serving is not an answer.
|
|
601
|
+
*/
|
|
602
|
+
async function printDatabaseUrl(projectRoot, rawArgs) {
|
|
603
|
+
parseCommandArgs({
|
|
604
|
+
spec: {
|
|
605
|
+
"--database-url": String,
|
|
606
|
+
"--docker": Boolean
|
|
607
|
+
},
|
|
608
|
+
rawArgs: [...rawArgs],
|
|
609
|
+
commandWords: 2,
|
|
610
|
+
command: "db url",
|
|
611
|
+
maxPositionals: 0
|
|
612
|
+
});
|
|
613
|
+
const { prepareDatabaseEnv } = await import("./prepare-Btn9Ewbn.js");
|
|
614
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
615
|
+
const url = databaseUrlOf(await prepareDatabaseEnv(projectRoot, {
|
|
616
|
+
flagUrl: readFlagValue(rawArgs, "--database-url"),
|
|
617
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
618
|
+
quiet: true,
|
|
619
|
+
onProgress: (message) => console.error(chalk.gray(` ${message}`))
|
|
620
|
+
}), readEnvFile(projectRoot));
|
|
621
|
+
if (!url) {
|
|
622
|
+
console.error(chalk.red(" ✗ This project has no database URL to print."));
|
|
623
|
+
console.error(chalk.gray(" Set DATABASE_URL in .env, or run `rebase dev` to start the managed one."));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
console.log(url);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* `rebase db stop` and `rebase db reset`, for the managed database only.
|
|
630
|
+
*
|
|
631
|
+
* `reset` deletes the data directory rather than dropping schemas: "give me an
|
|
632
|
+
* empty database" is the only thing anyone means by it, and doing it by
|
|
633
|
+
* removing the directory cannot leave a half-dropped schema behind.
|
|
634
|
+
*/
|
|
635
|
+
async function manageLocalDatabase(subcommand, projectRoot, rawArgs) {
|
|
636
|
+
const { resetManagedDatabase, stopManagedDatabase, findRunningDaemon } = await import("./daemon-DBn-shxX.js");
|
|
637
|
+
const { dataDir } = await import("./state-Do7ZJYkF.js").then((n) => n.d);
|
|
638
|
+
const nothingHere = () => {
|
|
639
|
+
const kind = devDatabaseKind(projectRoot);
|
|
640
|
+
const verb = subcommand === "stop" ? "was not running" : "does not exist";
|
|
641
|
+
if (kind && kind !== "managed") return ` The managed development database (PGlite) ${verb}; this project uses its own DATABASE_URL, which is untouched.`;
|
|
642
|
+
return subcommand === "stop" ? " The managed development database (PGlite) was not running." : " There is no managed development database (PGlite) to reset.";
|
|
643
|
+
};
|
|
644
|
+
if (subcommand === "stop") {
|
|
645
|
+
const stopped = await stopManagedDatabase(projectRoot);
|
|
646
|
+
console.log(stopped ? chalk.green("✓ Development database stopped. Data is kept — `rebase dev` will start it again.") : chalk.gray(nothingHere()));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const running = await findRunningDaemon(projectRoot);
|
|
650
|
+
const hasData = fs.existsSync(dataDir(projectRoot));
|
|
651
|
+
if (!running && !hasData) {
|
|
652
|
+
console.log(chalk.gray(nothingHere()));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (!rawArgs.includes("--yes") && !rawArgs.includes("-y")) {
|
|
656
|
+
if (!process.stdin.isTTY) {
|
|
657
|
+
console.error(chalk.red("✗ `rebase db reset` deletes the local database and cannot prompt here."));
|
|
658
|
+
console.error(chalk.yellow(" Re-run with --yes if that is what you intend."));
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
const { confirmed } = await inquirer.prompt([{
|
|
662
|
+
type: "confirm",
|
|
663
|
+
name: "confirmed",
|
|
664
|
+
message: `Delete the development database at ${dataDir(projectRoot)}? This cannot be undone.`,
|
|
665
|
+
default: false
|
|
666
|
+
}]);
|
|
667
|
+
if (!confirmed) {
|
|
668
|
+
console.log(chalk.gray(" Left alone."));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
await resetManagedDatabase(projectRoot);
|
|
673
|
+
console.log(chalk.green("✓ Development database deleted. `rebase dev` will create a fresh one."));
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* `rebase db pull --from <url>` — copy another database's contents into local
|
|
677
|
+
* development.
|
|
678
|
+
*
|
|
679
|
+
* Deliberately one-directional. There is no flag that makes this push, because
|
|
680
|
+
* a tool that can copy in both directions eventually copies in the wrong one,
|
|
681
|
+
* and the wrong one here means overwriting production with a laptop.
|
|
682
|
+
*/
|
|
683
|
+
async function pullIntoLocal(projectRoot, rawArgs) {
|
|
684
|
+
const { anonymizeStatements, describeTarget, dumpArgs, findPgTool, restoreArgs, restoreOutcome } = await import("./pull-Dywm5FWP.js");
|
|
685
|
+
const { prepareDatabaseEnv } = await import("./prepare-Btn9Ewbn.js");
|
|
686
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
687
|
+
const source = readFlagValue(rawArgs, "--from");
|
|
688
|
+
if (!source) {
|
|
689
|
+
console.error(chalk.red("✗ `rebase db pull` needs a source: --from <connection-string>"));
|
|
690
|
+
console.error(chalk.gray(" Example: rebase db pull --from \"$PRODUCTION_DATABASE_URL\""));
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
const schemas = rawArgs.flatMap((arg, index) => arg === "--schema" && rawArgs[index + 1] ? [rawArgs[index + 1]] : []);
|
|
694
|
+
const anonymize = rawArgs.includes("--anonymize");
|
|
695
|
+
for (const tool of ["pg_dump", "pg_restore"]) if (!await findPgTool(tool)) {
|
|
696
|
+
console.error(chalk.red(`✗ \`${tool}\` is not on PATH, and this command cannot run without it.`));
|
|
697
|
+
console.error(chalk.gray(" Install the PostgreSQL client tools, e.g. `brew install libpq`."));
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
const targetFlag = readFlagValue(rawArgs, "--database-url");
|
|
701
|
+
if (targetFlag !== null) {
|
|
702
|
+
console.error(chalk.red("✗ `rebase db pull` does not take --database-url."));
|
|
703
|
+
console.error("");
|
|
704
|
+
console.error(chalk.gray(" The target is always this project's local development database. There is"));
|
|
705
|
+
console.error(chalk.gray(" no flag that makes this write somewhere else, deliberately: a command that"));
|
|
706
|
+
console.error(chalk.gray(" can copy in both directions eventually copies in the wrong one."));
|
|
707
|
+
console.error("");
|
|
708
|
+
console.error(chalk.gray(` To read from another database, that is ${chalk.cyan("--from")}:`));
|
|
709
|
+
console.error(chalk.gray(` rebase db pull --from ${targetFlag}`));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
const prepared = await prepareDatabaseEnv(projectRoot, {
|
|
713
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
714
|
+
onProgress: (message) => console.log(chalk.gray(` ${message}`))
|
|
715
|
+
});
|
|
716
|
+
const target = databaseUrlOf(prepared, readEnvFile(projectRoot)) ?? "";
|
|
717
|
+
if (!target) {
|
|
718
|
+
console.error(chalk.red("✗ No local database to pull into."));
|
|
719
|
+
console.error(chalk.gray(" Set DATABASE_URL in .env, or run `rebase dev` to start the managed one."));
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
const plan = {
|
|
723
|
+
source,
|
|
724
|
+
target,
|
|
725
|
+
anonymize,
|
|
726
|
+
schemas
|
|
727
|
+
};
|
|
728
|
+
console.log("");
|
|
729
|
+
console.log(chalk.bold(" This will replace your local database."));
|
|
730
|
+
console.log(` ${chalk.gray("From")} ${describeTarget(source)}`);
|
|
731
|
+
console.log(` ${chalk.gray("Into")} ${describeTarget(target)}${prepared.dataDir ? chalk.gray(` (${prepared.dataDir})`) : ""}`);
|
|
732
|
+
console.log(` ${chalk.gray("Data")} ${anonymize ? "personal-looking columns will be overwritten after the copy" : chalk.yellow("copied as-is, including any personal data")}`);
|
|
733
|
+
if (schemas.length > 0) console.log(` ${chalk.gray("Schemas")} ${schemas.join(", ")}`);
|
|
734
|
+
console.log("");
|
|
735
|
+
if (!rawArgs.includes("--yes") && !rawArgs.includes("-y")) {
|
|
736
|
+
if (!process.stdin.isTTY) {
|
|
737
|
+
console.error(chalk.red("✗ This replaces the local database and cannot prompt here."));
|
|
738
|
+
console.error(chalk.yellow(" Re-run with --yes if that is what you intend."));
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const { confirmed } = await inquirer.prompt([{
|
|
742
|
+
type: "confirm",
|
|
743
|
+
name: "confirmed",
|
|
744
|
+
message: "Continue?",
|
|
745
|
+
default: false
|
|
746
|
+
}]);
|
|
747
|
+
if (!confirmed) {
|
|
748
|
+
console.log(chalk.gray(" Nothing was changed."));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const dumpFile = path.join(os.tmpdir(), `rebase-pull-${process.pid}.dump`);
|
|
753
|
+
let failure = null;
|
|
754
|
+
try {
|
|
755
|
+
console.log(chalk.gray(" Dumping…"));
|
|
756
|
+
await execa("pg_dump", [
|
|
757
|
+
...dumpArgs(plan),
|
|
758
|
+
"--file",
|
|
759
|
+
dumpFile
|
|
760
|
+
], { stdio: [
|
|
761
|
+
"ignore",
|
|
762
|
+
"inherit",
|
|
763
|
+
"inherit"
|
|
764
|
+
] });
|
|
765
|
+
console.log(chalk.gray(" Restoring…"));
|
|
766
|
+
const restored = restoreOutcome(await execa("pg_restore", restoreArgs(plan, dumpFile), {
|
|
767
|
+
stdin: "ignore",
|
|
768
|
+
stdout: "inherit",
|
|
769
|
+
stderr: ["pipe", "inherit"],
|
|
770
|
+
reject: false
|
|
771
|
+
}));
|
|
772
|
+
failure = restored.failure;
|
|
773
|
+
if (anonymize && restored.started) {
|
|
774
|
+
const { Client } = await import("pg");
|
|
775
|
+
const client = new Client({ connectionString: target });
|
|
776
|
+
await client.connect();
|
|
777
|
+
try {
|
|
778
|
+
const { rows } = await client.query(`SELECT table_schema AS "schema", table_name AS "table",
|
|
779
|
+
column_name AS "column", data_type AS "dataType"
|
|
780
|
+
FROM information_schema.columns
|
|
781
|
+
WHERE table_schema NOT IN ('pg_catalog','information_schema')`);
|
|
782
|
+
const statements = anonymizeStatements(rows);
|
|
783
|
+
for (const statement of statements) await client.query(statement);
|
|
784
|
+
console.log(chalk.green(` ✓ Anonymized ${statements.length} table(s).`));
|
|
785
|
+
console.log(chalk.gray(" Name-based and best-effort: a free-text column may still hold personal data."));
|
|
786
|
+
} finally {
|
|
787
|
+
await client.end();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (!failure) await restoreAppRole(target);
|
|
791
|
+
} finally {
|
|
792
|
+
fs.rmSync(dumpFile, { force: true });
|
|
793
|
+
}
|
|
794
|
+
if (failure) {
|
|
795
|
+
console.error(chalk.red(`✗ ${failure}`));
|
|
796
|
+
console.error(chalk.yellow(` The local database holds an incomplete copy of ${describeTarget(source)}.`));
|
|
797
|
+
console.error(chalk.gray(" Fix what the errors above name — often an extension or a role the source has — and pull again."));
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
console.log(chalk.green("✓ Local database now holds a copy of " + describeTarget(source)));
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Give the restored copy back the privileges `pg_dump` stripped.
|
|
804
|
+
*
|
|
805
|
+
* A pull without this ends on a green tick and hands over a database the
|
|
806
|
+
* application cannot read: `--no-privileges` removes every GRANT, so the copy
|
|
807
|
+
* has the source's RLS policies and none of the grants behind them, and the
|
|
808
|
+
* first query as `rebase_user` fails with `permission denied`. Measured — 68
|
|
809
|
+
* policies and 60 grants in, 68 policies and 0 grants out.
|
|
810
|
+
*
|
|
811
|
+
* `ensureAppRole` is the routine boot runs and `rebase db push` runs, so the
|
|
812
|
+
* pulled database ends up in the state a booted one is in, and there is no
|
|
813
|
+
* second description of these grants to drift. `detectConnectionPosture` gates
|
|
814
|
+
* it the same way `db push` does: a connection that cannot create roles reports
|
|
815
|
+
* that rather than failing the pull, which has already succeeded by this point.
|
|
816
|
+
*
|
|
817
|
+
* Failure here is a warning, not an error. The data is restored and correct; a
|
|
818
|
+
* missing grant is repaired by the next `rebase dev` or `rebase db push`, and
|
|
819
|
+
* saying so beats unwinding a copy that is fine.
|
|
820
|
+
*/
|
|
821
|
+
async function restoreAppRole(target) {
|
|
822
|
+
const { Client } = await import("pg");
|
|
823
|
+
const { detectConnectionPosture, ensureAppRole, REBASE_USER_ROLE } = await import("@rebasepro/server-postgres");
|
|
824
|
+
const { provisionableSchemas } = await import("./pull-Dywm5FWP.js");
|
|
825
|
+
const client = new Client({ connectionString: target });
|
|
826
|
+
await client.connect();
|
|
827
|
+
try {
|
|
828
|
+
const runSql = async (text) => (await client.query(text)).rows;
|
|
829
|
+
if (!(await detectConnectionPosture(runSql)).privileged) {
|
|
830
|
+
console.log(chalk.yellow(` ⚠ Could not re-grant the "${REBASE_USER_ROLE}" role — this connection may not create roles.`));
|
|
831
|
+
console.log(chalk.gray(" The copy has its RLS policies but no privileges behind them;"));
|
|
832
|
+
console.log(chalk.gray(" `rebase dev` or `rebase db push` will provision them."));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const { rows } = await client.query("SELECT nspname AS \"schema\" FROM pg_namespace");
|
|
836
|
+
const schemas = provisionableSchemas(rows);
|
|
837
|
+
await ensureAppRole(runSql, schemas);
|
|
838
|
+
console.log(chalk.gray(` ✓ Re-granted "${REBASE_USER_ROLE}" on ${schemas.length} schema(s) — pg_dump strips privileges.`));
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.log(chalk.yellow(` ⚠ Restored, but could not re-grant "rebase_user": ${error instanceof Error ? error.message : String(error)}`));
|
|
841
|
+
console.log(chalk.gray(" Run `rebase db push` (or start `rebase dev`) to provision it."));
|
|
842
|
+
} finally {
|
|
843
|
+
await client.end();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Start the compose database a `db … --docker` was asked to talk to.
|
|
848
|
+
*
|
|
849
|
+
* `--docker` resolved the compose URL and stopped there, so every `db`
|
|
850
|
+
* subcommand carrying it reached a container nobody had started —
|
|
851
|
+
* `ECONNREFUSED 127.0.0.1:5436`, two lines, no diagnosis — including the
|
|
852
|
+
* commands the managed-database refusals send readers to `--docker` for.
|
|
853
|
+
*
|
|
854
|
+
* `rebase dev --docker`'s preflight, called with the compose URL and nothing
|
|
855
|
+
* to push: this is not a schema step, and `rebase db push --docker` is the
|
|
856
|
+
* push. A failure here is reported and not fatal — the command runs and its
|
|
857
|
+
* own connection error follows, which is the same contract `dev` keeps.
|
|
858
|
+
*/
|
|
859
|
+
async function ensureDockerDatabase(projectRoot, rawArgs) {
|
|
860
|
+
if (!rawArgs.includes("--docker")) return;
|
|
861
|
+
const { ensureDevDatabase } = await import("./dev-preflight-N1evJL3S.js");
|
|
862
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
863
|
+
const { resolveComposeUrl } = await import("./prepare-Btn9Ewbn.js");
|
|
864
|
+
const composeUrl = resolveComposeUrl(projectRoot, readEnvFile(projectRoot));
|
|
865
|
+
if (!composeUrl) return;
|
|
866
|
+
await ensureDevDatabase({
|
|
867
|
+
projectRoot,
|
|
868
|
+
databaseUrl: composeUrl,
|
|
869
|
+
disabled: false,
|
|
870
|
+
hasCollections: false,
|
|
871
|
+
pushSchema: async () => void 0,
|
|
872
|
+
log: (message) => console.log(message)
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* You cannot delete the branch you are standing on.
|
|
877
|
+
*
|
|
878
|
+
* Not a new rule — it was always refused, by a message about a database the
|
|
879
|
+
* developer had never named. After `switch feature_x` the resolved DSN *is*
|
|
880
|
+
* `rb_feature_x`, so `BranchService.deleteBranch` compared it against the
|
|
881
|
+
* connected database, found them equal, and answered `✗ Cannot delete the main
|
|
882
|
+
* database.` The main database is precisely what the developer was not on.
|
|
883
|
+
*
|
|
884
|
+
* Answered here because the pointer is the CLI's: the driver runs as a child
|
|
885
|
+
* process that has never heard of it, and cannot tell "the branch you are on"
|
|
886
|
+
* from "the main database" — those are the same connection string to it.
|
|
887
|
+
*/
|
|
888
|
+
async function refuseDeletingActiveBranch(projectRoot, rawArgs) {
|
|
889
|
+
const [, , , name] = commandWords(rawArgs, "db");
|
|
890
|
+
if (!name) return;
|
|
891
|
+
const { readActiveBranch } = await import("./branch-pointer-DIhKbQLp.js");
|
|
892
|
+
const active = readActiveBranch(projectRoot);
|
|
893
|
+
if (active?.name !== name) return;
|
|
894
|
+
const off = "rebase db branch switch --off";
|
|
895
|
+
const del = `rebase db branch delete ${name}`;
|
|
896
|
+
const column = Math.max(29, del.length) + 4;
|
|
897
|
+
console.error("");
|
|
898
|
+
console.error(chalk.red(`✗ You are on branch "${name}" — leave it before deleting it.`));
|
|
899
|
+
console.error("");
|
|
900
|
+
console.error(chalk.gray(` Every database command in this checkout is pointed at ${active.database},`));
|
|
901
|
+
console.error(chalk.gray(" including this one, so the delete would be asking that database to drop"));
|
|
902
|
+
console.error(chalk.gray(" itself."));
|
|
903
|
+
console.error("");
|
|
904
|
+
console.error(` ${chalk.cyan(off)}${" ".repeat(column - 29)}back to the main database`);
|
|
905
|
+
console.error(` ${chalk.cyan(del)}${" ".repeat(column - del.length)}then this works`);
|
|
906
|
+
console.error("");
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
/** The `branch` actions that read or write the registry — every one but `switch`. */
|
|
910
|
+
var REGISTRY_ACTIONS = /* @__PURE__ */ new Set([
|
|
911
|
+
"list",
|
|
912
|
+
"create",
|
|
913
|
+
"delete",
|
|
914
|
+
"info",
|
|
915
|
+
"prune"
|
|
916
|
+
]);
|
|
917
|
+
/**
|
|
918
|
+
* Every `branch` action runs against the parent, where the registry lives.
|
|
919
|
+
*
|
|
920
|
+
* `rebase.branches` is written in the database branches are made *from*, and a
|
|
921
|
+
* branch is a `CREATE DATABASE … TEMPLATE` copy — so the copy carries a
|
|
922
|
+
* snapshot of that table taken at branch time, and every branch made since is
|
|
923
|
+
* missing from it. After `switch feature_x`, `branch list` connected to
|
|
924
|
+
* `rb_feature_x` and answered `No branches found. Create one with: rebase db
|
|
925
|
+
* branch create <name>` while `branch switch` with no argument, one command
|
|
926
|
+
* earlier, had said `● On branch feature_x`.
|
|
927
|
+
*
|
|
928
|
+
* The same snapshot answered `create`, `delete`, `info` and `prune`: a branch
|
|
929
|
+
* created from a switched checkout was recorded only in the copy, so `list`
|
|
930
|
+
* never showed it and `delete` from the main checkout said "not found" while
|
|
931
|
+
* its database stayed; `info` and `delete` could not find a branch made after
|
|
932
|
+
* the one this checkout is on. The Branches pane in Studio was fixed the same
|
|
933
|
+
* way (`REBASE_BRANCH_PARENT_DATABASE`).
|
|
934
|
+
*
|
|
935
|
+
* `create` still copies what this checkout is on — a branch of the branch, a
|
|
936
|
+
* thing the developer asked for by switching first — so it is given
|
|
937
|
+
* `--from <the branch's database>` unless it names a source itself.
|
|
938
|
+
*
|
|
939
|
+
* Resolved with `branch: null` — the same rule `switch` uses to find what to
|
|
940
|
+
* branch *from* — and relayed as `--database-url`, which the driver honours
|
|
941
|
+
* over anything it would resolve for itself.
|
|
942
|
+
*/
|
|
943
|
+
async function onParentDatabase(projectRoot, rawArgs, action) {
|
|
944
|
+
if (readFlagValue(rawArgs, "--database-url") !== null) return rawArgs;
|
|
945
|
+
const { readActiveBranch } = await import("./branch-pointer-DIhKbQLp.js");
|
|
946
|
+
const active = readActiveBranch(projectRoot);
|
|
947
|
+
if (!active) return rawArgs;
|
|
948
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
949
|
+
const { resolveDevDatabase } = await import("./resolve-CJLkfFV0.js");
|
|
950
|
+
const { resolveComposeUrl } = await import("./prepare-Btn9Ewbn.js");
|
|
951
|
+
const envFile = readEnvFile(projectRoot);
|
|
952
|
+
const parent = resolveDevDatabase({
|
|
953
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
954
|
+
env: process.env,
|
|
955
|
+
envFile,
|
|
956
|
+
branch: null,
|
|
957
|
+
composeUrl: resolveComposeUrl(projectRoot, envFile)
|
|
958
|
+
});
|
|
959
|
+
const url = (parent.kind === "managed" ? null : parent.url)?.trim();
|
|
960
|
+
if (!url) return rawArgs;
|
|
961
|
+
console.log(chalk.gray(` ● On branch ${active.name} — using the main database, where the branch registry lives.`));
|
|
962
|
+
const from = action === "create" && readFlagValue(rawArgs, "--from") === null ? ["--from", active.database] : [];
|
|
963
|
+
return [
|
|
964
|
+
...rawArgs,
|
|
965
|
+
...from,
|
|
966
|
+
"--database-url",
|
|
967
|
+
url
|
|
968
|
+
];
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* `rebase db branch switch <name>` — point this checkout at a branch.
|
|
972
|
+
*
|
|
973
|
+
* The step branching was missing. `create` copied the database in about a
|
|
974
|
+
* second and then printed `Database: rb_feature_auth` and stopped: there was no
|
|
975
|
+
* `switch`, no `--branch` on `rebase dev`, and no `REBASE_BRANCH`. The only way
|
|
976
|
+
* to use a branch was to hand-edit `DATABASE_URL`, while the documentation said
|
|
977
|
+
* "the CLI updates your local development configuration" — it did not, and the
|
|
978
|
+
* `.env` was byte-identical afterwards.
|
|
979
|
+
*
|
|
980
|
+
* Three things it insists on:
|
|
981
|
+
*
|
|
982
|
+
* - **The branch has to exist.** Writing a pointer to a database that was never
|
|
983
|
+
* created turns a typo into a connection error on the *next* command, which
|
|
984
|
+
* is the one place it cannot be explained. Checked against
|
|
985
|
+
* `rebase.branches` and `pg_database` both, because a branch dropped outside
|
|
986
|
+
* Rebase leaves the metadata row behind.
|
|
987
|
+
*
|
|
988
|
+
* - **It says what changed, and how to undo it.** A command that silently
|
|
989
|
+
* redirects every subsequent database operation owes the reader the database
|
|
990
|
+
* name it moved to and the words that move it back.
|
|
991
|
+
*
|
|
992
|
+
* - **It never edits `.env`.** See `branch-pointer.ts`.
|
|
993
|
+
*/
|
|
994
|
+
async function switchBranch(projectRoot, rawArgs) {
|
|
995
|
+
const { branchDatabaseName, clearActiveBranch, databaseNameOf, readActiveBranch, writeActiveBranch } = await import("./branch-pointer-DIhKbQLp.js");
|
|
996
|
+
const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
|
|
997
|
+
const { resolveDevDatabase } = await import("./resolve-CJLkfFV0.js");
|
|
998
|
+
const { resolveComposeUrl } = await import("./prepare-Btn9Ewbn.js");
|
|
999
|
+
const goingOff = rawArgs.includes("--off") || rawArgs.includes("--main");
|
|
1000
|
+
const name = commandWords(rawArgs, "db")[3];
|
|
1001
|
+
const envFile = readEnvFile(projectRoot);
|
|
1002
|
+
const resolved = resolveDevDatabase({
|
|
1003
|
+
flagUrl: readFlagValue(rawArgs, "--database-url"),
|
|
1004
|
+
flagDocker: rawArgs.includes("--docker"),
|
|
1005
|
+
env: process.env,
|
|
1006
|
+
envFile,
|
|
1007
|
+
branch: null,
|
|
1008
|
+
composeUrl: resolveComposeUrl(projectRoot, envFile)
|
|
1009
|
+
});
|
|
1010
|
+
const base = (resolved.kind === "managed" ? null : resolved.url)?.trim() || void 0;
|
|
1011
|
+
if (!name && !goingOff) {
|
|
1012
|
+
const active = readActiveBranch(projectRoot);
|
|
1013
|
+
console.log("");
|
|
1014
|
+
if (active) {
|
|
1015
|
+
console.log(` ${chalk.green("●")} On branch ${chalk.bold(active.name)} ${chalk.gray(`(${active.database})`)}`);
|
|
1016
|
+
console.log(chalk.gray(` Back to the main database: ${chalk.cyan("rebase db branch switch --off")}`));
|
|
1017
|
+
} else {
|
|
1018
|
+
const main = base ? databaseNameOf(base) : null;
|
|
1019
|
+
console.log(` ${chalk.gray("●")} On the main database${main ? chalk.gray(` (${main})`) : ""}`);
|
|
1020
|
+
console.log(chalk.gray(` Switch to a branch: ${chalk.cyan("rebase db branch switch <name>")}`));
|
|
1021
|
+
}
|
|
1022
|
+
console.log("");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (goingOff) {
|
|
1026
|
+
const active = readActiveBranch(projectRoot);
|
|
1027
|
+
clearActiveBranch(projectRoot);
|
|
1028
|
+
console.log("");
|
|
1029
|
+
console.log(active ? chalk.green(` ✓ Back on the main database${base ? ` (${databaseNameOf(base)})` : ""}.`) : chalk.gray(" Already on the main database."));
|
|
1030
|
+
console.log("");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!base) {
|
|
1034
|
+
console.error(chalk.red("✗ There is no database to branch from."));
|
|
1035
|
+
console.error(chalk.gray(resolved.kind === "managed" ? " This project is on the managed development database, which serves exactly one —" : " --docker needs a docker-compose.yml with a db service, and this project has none —"));
|
|
1036
|
+
console.error(chalk.gray(" and branching copies a database. Set DATABASE_URL in .env or in your"));
|
|
1037
|
+
console.error(chalk.gray(" shell, or run `rebase dev --docker`."));
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
const database = branchDatabaseName(name);
|
|
1041
|
+
const { Client } = await import("pg");
|
|
1042
|
+
const client = new Client({ connectionString: base });
|
|
1043
|
+
try {
|
|
1044
|
+
await client.connect();
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.error(chalk.red(`✗ Could not reach the database to check that branch "${name}" exists.`));
|
|
1047
|
+
console.error(chalk.gray(` ${error instanceof Error ? error.message : String(error)}`));
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const { rows } = await client.query(`SELECT EXISTS (SELECT 1 FROM rebase.branches WHERE name = $1) AS registered,
|
|
1052
|
+
EXISTS (SELECT 1 FROM pg_database WHERE datname = $2) AS present`, [name, database]).catch(() => ({ rows: [{
|
|
1053
|
+
registered: false,
|
|
1054
|
+
present: false
|
|
1055
|
+
}] }));
|
|
1056
|
+
const { registered, present } = rows[0] ?? {
|
|
1057
|
+
registered: false,
|
|
1058
|
+
present: false
|
|
1059
|
+
};
|
|
1060
|
+
if (!present) {
|
|
1061
|
+
console.error(chalk.red(`✗ Branch "${name}" does not exist.`));
|
|
1062
|
+
console.error(registered ? chalk.gray(` It is registered but its database (${database}) is gone — someone dropped it outside Rebase.`) : chalk.gray(` ${chalk.cyan("rebase db branch list")} shows the ones that do.`));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
} finally {
|
|
1066
|
+
await client.end();
|
|
1067
|
+
}
|
|
1068
|
+
writeActiveBranch(projectRoot, {
|
|
1069
|
+
name,
|
|
1070
|
+
database
|
|
1071
|
+
});
|
|
1072
|
+
console.log("");
|
|
1073
|
+
console.log(chalk.green(` ✓ Switched to branch "${name}".`));
|
|
1074
|
+
console.log(chalk.gray(` Database: ${database}`));
|
|
1075
|
+
console.log(chalk.gray(" Every rebase command in this checkout now uses it — dev, db push, migrate."));
|
|
1076
|
+
console.log(chalk.gray(` Back to the main database: ${chalk.cyan("rebase db branch switch --off")}`));
|
|
1077
|
+
console.log("");
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* What each `rebase db <action>` does and takes.
|
|
1081
|
+
*
|
|
1082
|
+
* Kept here rather than delegated to the driver because the driver is reached
|
|
1083
|
+
* by *running* it — which is exactly what `--help` must not do. The page is
|
|
1084
|
+
* deliberately short: the authority on a flag is the command's own spec, and
|
|
1085
|
+
* `check-doc-commands.mjs` holds every command written in this repository's
|
|
1086
|
+
* markdown to it. What a reader needs from here is which subcommands exist and
|
|
1087
|
+
* what the destructive ones want before they will run.
|
|
1088
|
+
*/
|
|
1089
|
+
var DB_ACTION_HELP = {
|
|
1090
|
+
push: {
|
|
1091
|
+
usage: "rebase db push [--collections <dir>] [--dry-run] [--allow-destructive] [--yes]",
|
|
1092
|
+
summary: "Apply the schema straight to the database. Development only — it does not write a migration.",
|
|
1093
|
+
notes: [
|
|
1094
|
+
"--dry-run prints the SQL and applies nothing. Read it before you approve it.",
|
|
1095
|
+
"A change that drops data, or re-keys a table (every row gets a new id), needs --allow-destructive.",
|
|
1096
|
+
`--collections <dir>: ${COLLECTIONS_FLAG_HELP}.`
|
|
1097
|
+
]
|
|
1098
|
+
},
|
|
1099
|
+
generate: {
|
|
1100
|
+
usage: "rebase db generate [--collections <dir>]",
|
|
1101
|
+
summary: "Generate the Drizzle schema, the Postgres DDL and a SQL migration file from the collections.",
|
|
1102
|
+
notes: ["A migration that drops or narrows data is named when it is written. It is still written.", `--collections <dir>: ${COLLECTIONS_FLAG_HELP}.`]
|
|
1103
|
+
},
|
|
1104
|
+
migrate: {
|
|
1105
|
+
usage: "rebase db migrate [--baseline <version>] [--refuse-destructive] [amount]",
|
|
1106
|
+
summary: "Run the pending migration files against the database.",
|
|
1107
|
+
notes: [
|
|
1108
|
+
"--refuse-destructive dry-runs the pending files and applies nothing when one drops or narrows data.",
|
|
1109
|
+
"--baseline <version> records that version as already applied and starts from the next one.",
|
|
1110
|
+
"It is what a database Rebase has already booted against needs: boot ensures the schema, so the first migration would try to create tables that are already there.",
|
|
1111
|
+
"The version is the numeric prefix of a file in drizzle/migrations."
|
|
1112
|
+
]
|
|
1113
|
+
},
|
|
1114
|
+
branch: {
|
|
1115
|
+
usage: "rebase db branch <create|list|switch|delete|info|prune> [name]",
|
|
1116
|
+
summary: "Database branching.",
|
|
1117
|
+
notes: [
|
|
1118
|
+
"create <name> [--from <source>] copies an existing database, files and all.",
|
|
1119
|
+
"switch <name> points this checkout at a branch; every later command uses it.",
|
|
1120
|
+
"switch with no name reports where you are; switch --off returns to the main database.",
|
|
1121
|
+
"prune [--older-than <14|14d|2w>] removes branches nothing is using, and the entries that drifted.",
|
|
1122
|
+
"prune --include-dev-diff also removes the Atlas scratch databases `db push` leaves behind.",
|
|
1123
|
+
"prune asks before it drops anything; --yes, -y answers for you, which is the form a CI job wants.",
|
|
1124
|
+
"--force disconnects other sessions first — Postgres refuses to copy or drop a database anything is connected to, usually your own `rebase dev`."
|
|
1125
|
+
]
|
|
1126
|
+
},
|
|
1127
|
+
backup: {
|
|
1128
|
+
usage: "rebase db backup [--out <path|s3://…>]",
|
|
1129
|
+
summary: "Create a pg_dump backup. --out is resolved against the directory you are standing in.",
|
|
1130
|
+
notes: ["--output is accepted as an alias, as it is on `rebase build`.", "`rebase db backup list` lists them, the way `rebase cloud db backup list` does."]
|
|
1131
|
+
},
|
|
1132
|
+
backups: {
|
|
1133
|
+
usage: "rebase db backups list [--out <path|s3://…>]",
|
|
1134
|
+
summary: "List stored backups.",
|
|
1135
|
+
notes: ["`rebase db backup list` is the same command, spelled the cloud family's way."]
|
|
1136
|
+
},
|
|
1137
|
+
url: {
|
|
1138
|
+
usage: "rebase db url [--database-url <url>] [--docker]",
|
|
1139
|
+
summary: "Print the connection string this project uses. Nothing else goes to stdout, so it pipes.",
|
|
1140
|
+
notes: [
|
|
1141
|
+
"psql \"$(rebase db url)\"",
|
|
1142
|
+
"Starts the managed database if it is not already running.",
|
|
1143
|
+
"--database-url and --docker answer for the database `rebase dev` would pick with the same flag."
|
|
1144
|
+
]
|
|
1145
|
+
},
|
|
1146
|
+
restore: {
|
|
1147
|
+
usage: "rebase db restore <dump> [--target-db <name>] [--create-db] --yes",
|
|
1148
|
+
summary: "Restore a backup with pg_restore.",
|
|
1149
|
+
notes: ["Destructive, and refuses to run without --yes."]
|
|
1150
|
+
},
|
|
1151
|
+
pull: {
|
|
1152
|
+
usage: "rebase db pull --from <url> [--schema <name>] [--anonymize] [--yes]",
|
|
1153
|
+
summary: "Copy another database into local development. One-directional by design — it can never push.",
|
|
1154
|
+
notes: ["The target is not selectable: --database-url is refused, so this can never write to a remote database."]
|
|
1155
|
+
},
|
|
1156
|
+
stop: {
|
|
1157
|
+
usage: "rebase db stop",
|
|
1158
|
+
summary: "Stop the managed development database. Data is kept."
|
|
1159
|
+
},
|
|
1160
|
+
reset: {
|
|
1161
|
+
usage: "rebase db reset [--yes]",
|
|
1162
|
+
summary: "Delete the managed development database and start over.",
|
|
1163
|
+
notes: ["Destructive, and the data exists only on this machine."]
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
/** Every subcommand the family dispatches — the help table's keys. */
|
|
1167
|
+
var DB_SUBCOMMANDS = Object.keys(DB_ACTION_HELP);
|
|
1168
|
+
function printDbHelp(action) {
|
|
1169
|
+
const entry = action ? DB_ACTION_HELP[action] : void 0;
|
|
1170
|
+
if (entry) {
|
|
1171
|
+
console.log(`
|
|
1172
|
+
${chalk.bold(`rebase db ${action}`)}
|
|
1173
|
+
|
|
1174
|
+
${entry.summary}
|
|
1175
|
+
|
|
1176
|
+
${chalk.green.bold("Usage")}
|
|
1177
|
+
${chalk.blue(entry.usage)}
|
|
1178
|
+
${entry.notes?.length ? `\n${chalk.green.bold("Notes")}\n${entry.notes.map((n) => ` ${chalk.gray(`• ${n}`)}`).join("\n")}\n` : ""}
|
|
1179
|
+
${chalk.gray("Run `rebase db --help` for every subcommand.")}
|
|
1180
|
+
`);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
console.log(`
|
|
1184
|
+
${chalk.bold("rebase db")} — Database management commands
|
|
1185
|
+
|
|
1186
|
+
${chalk.green.bold("Usage")}
|
|
1187
|
+
rebase db ${chalk.blue("<command>")} [options]
|
|
1188
|
+
|
|
1189
|
+
${chalk.green.bold("Commands")}
|
|
1190
|
+
${chalk.gray("(Commands are provided by your active database driver plugin)")}
|
|
1191
|
+
${chalk.blue.bold("url")} Print the connection string this project uses (pipes into psql)
|
|
1192
|
+
${chalk.blue.bold("pull")} Copy another database into local dev (--from <url>, --anonymize)
|
|
1193
|
+
${chalk.blue.bold("stop")} Stop the managed development database (data is kept)
|
|
1194
|
+
${chalk.blue.bold("reset")} Delete the managed development database and start over
|
|
1195
|
+
${chalk.blue.bold("push")} Apply schema directly to database (development)
|
|
1196
|
+
${chalk.blue.bold("generate")} Generate migration files
|
|
1197
|
+
${chalk.blue.bold("migrate")} Run pending migrations
|
|
1198
|
+
${chalk.blue.bold("branch")} Database branching (create, list, switch, delete, info, prune)
|
|
1199
|
+
${chalk.blue.bold("backup")} Create a backup with pg_dump (--out <path|s3://…>)
|
|
1200
|
+
${chalk.blue.bold("restore")} Restore a backup with pg_restore (destructive; needs --yes)
|
|
1201
|
+
${chalk.blue.bold("backups")} List stored backups (db backup list is the same)
|
|
1202
|
+
|
|
1203
|
+
${chalk.green.bold("Examples")}
|
|
1204
|
+
${dbExamples(devDatabaseKind())}
|
|
1205
|
+
${chalk.gray("# Back up to a local directory, then to object storage")}
|
|
1206
|
+
rebase db backup --out ./backups
|
|
1207
|
+
rebase db backup --out s3://my-private-bucket/backups
|
|
1208
|
+
|
|
1209
|
+
${chalk.gray("# Restore into a fresh database (safe: does not touch the live one)")}
|
|
1210
|
+
rebase db restore ./backups/rebase-app-20260714T030000Z.dump --create-db --target-db app_restored
|
|
1211
|
+
`);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* The examples, which are not the same on the managed development database.
|
|
1215
|
+
*
|
|
1216
|
+
* Every one of the four the help used to lead with — `db push`, `db generate`,
|
|
1217
|
+
* `db migrate`, `db branch` — is refused there, by guards in this same file
|
|
1218
|
+
* that explain exactly why. So on the project the CLI had just scaffolded, the
|
|
1219
|
+
* first thing `rebase db --help` offered was a command that answers with a
|
|
1220
|
+
* refusal, and the reader's most likely conclusion is that their install is
|
|
1221
|
+
* broken.
|
|
1222
|
+
*
|
|
1223
|
+
* Exported for its test.
|
|
1224
|
+
*/
|
|
1225
|
+
function dbExamples(kind) {
|
|
1226
|
+
if (kind === "managed") return ` ${chalk.gray("# Quick development workflow (this project is on the managed database)")}
|
|
1227
|
+
rebase schema generate
|
|
1228
|
+
rebase dev ${chalk.gray("# boot applies your collections to it, additively")}
|
|
1229
|
+
rebase db push ${chalk.gray("# also drops, renames and narrows columns")}
|
|
1230
|
+
|
|
1231
|
+
${chalk.gray("# branch needs a Postgres of your own")}
|
|
1232
|
+
rebase dev --docker ${chalk.gray("# starts one; or uncomment DATABASE_URL in .env")}
|
|
1233
|
+
|
|
1234
|
+
`;
|
|
1235
|
+
return ` ${chalk.gray("# Quick development workflow")}
|
|
1236
|
+
rebase schema generate && rebase db push
|
|
1237
|
+
|
|
1238
|
+
${chalk.gray("# Production migration workflow")}
|
|
1239
|
+
rebase db generate
|
|
1240
|
+
rebase db migrate
|
|
1241
|
+
|
|
1242
|
+
${chalk.gray("# Create a database branch and work on it")}
|
|
1243
|
+
rebase db branch create feature_auth
|
|
1244
|
+
rebase db branch switch feature_auth
|
|
1245
|
+
rebase db branch switch --off
|
|
1246
|
+
|
|
1247
|
+
`;
|
|
1248
|
+
}
|
|
1249
|
+
//#endregion
|
|
1250
|
+
export { DB_SUBCOMMANDS, absolutizeLocalPathArgs, dbCommand, ensureDockerDatabase, argsFromCommand as n, resolveDriverCli, runDriverDbCommand, runDriverSchemaCommand, spawnDriverCli, reportSpawnFailure as t };
|
|
1251
|
+
|
|
1252
|
+
//# sourceMappingURL=db-YbPMFY4Y.js.map
|