@rebasepro/cli 0.13.0 → 0.13.1-canary.g1822133
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/dist/bundle.d.ts +4 -3
- package/dist/commands/api-keys.d.ts +51 -0
- package/dist/commands/auth.d.ts +62 -0
- package/dist/commands/cloud/context.d.ts +143 -10
- package/dist/commands/cloud/databases.d.ts +39 -0
- package/dist/commands/cloud/debug.d.ts +1 -0
- package/dist/commands/cloud/deployments.d.ts +22 -0
- package/dist/commands/cloud/domains.d.ts +10 -0
- package/dist/commands/cloud/env.d.ts +51 -0
- package/dist/commands/cloud/extensions.d.ts +21 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/projects.d.ts +29 -0
- package/dist/commands/cloud/resources.d.ts +14 -0
- package/dist/commands/cloud/settings.d.ts +1 -0
- package/dist/commands/dev.d.ts +17 -6
- package/dist/commands/eject.d.ts +42 -0
- package/dist/commands/init.d.ts +67 -0
- package/dist/commands/skills.d.ts +81 -0
- package/dist/fold-static.d.ts +47 -0
- package/dist/index.es.js +2272 -765
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +16 -1
- package/dist/telemetry/payload.d.ts +1 -1
- package/dist/utils/args.d.ts +76 -0
- package/dist/utils/collection-drift.d.ts +27 -0
- package/dist/utils/project.d.ts +20 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +29 -4
- package/templates/eject/backend/src/index.ts +60 -8
- package/templates/eject/docker-compose.custom.yml +13 -5
- package/templates/overlays/baas/backend/tsconfig.json +6 -1
- package/templates/overlays/baas/config/index.ts +9 -0
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/template/.env.example +13 -4
- package/templates/template/backend/functions/hello.ts +8 -4
- package/templates/template/backend/tsconfig.json +6 -1
- package/templates/template/config/package.json +1 -0
- package/templates/template/docker-compose.yml +4 -4
- package/templates/template/frontend/vite.config.ts +33 -2
package/dist/index.es.js
CHANGED
|
@@ -12,9 +12,10 @@ import crypto from "crypto";
|
|
|
12
12
|
import { execSync, spawn, spawnSync } from "child_process";
|
|
13
13
|
import os from "os";
|
|
14
14
|
import { createRebaseClient } from "@rebasepro/client";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
|
+
import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
|
|
15
18
|
import { createRequire } from "module";
|
|
16
|
-
import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
|
-
import { generateSDK } from "@rebasepro/codegen";
|
|
18
19
|
//#region src/utils/package-manager.ts
|
|
19
20
|
/**
|
|
20
21
|
* Package manager detection and command abstraction.
|
|
@@ -190,6 +191,90 @@ function getPMCommands(pm) {
|
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
//#endregion
|
|
194
|
+
//#region src/utils/args.ts
|
|
195
|
+
/**
|
|
196
|
+
* Argument parsing for the commands that take positional arguments.
|
|
197
|
+
*
|
|
198
|
+
* Every command in the small group used to parse its own line the same way:
|
|
199
|
+
* `arg(spec, { argv: rawArgs.slice(4), permissive: true })`, then read `_[0]`
|
|
200
|
+
* and `_[1]`. Both halves of that are wrong in a way that costs data.
|
|
201
|
+
*
|
|
202
|
+
* - **`permissive: true` turns an unknown flag into a positional.** `arg`
|
|
203
|
+
* pushes an undeclared flag into `_` as a bare token, so `_[1]` is whatever
|
|
204
|
+
* came second on the line, flag or not. `rebase auth reset-password
|
|
205
|
+
* bob@example.com --debug` set Bob's password to the literal `--debug` — and
|
|
206
|
+
* `--debug` is what `bin/rebase.js` prints after *every* failure as the thing
|
|
207
|
+
* to re-run with, so the single most likely next keystroke after a failed
|
|
208
|
+
* reset was the one that reset the account to a two-word string.
|
|
209
|
+
* - **`slice(4)` assumes the command words are at fixed indices.** They are
|
|
210
|
+
* not: a flag before the command shifts everything, so `rebase --debug auth
|
|
211
|
+
* reset-password bob@example.com NewPass1!` read the email as
|
|
212
|
+
* `reset-password` and the password as `bob@example.com`.
|
|
213
|
+
*
|
|
214
|
+
* So: parse the *whole* line — `rawArgs` is `process.argv` — against a spec
|
|
215
|
+
* strictly, with no permissive mode. `arg` then consumes every flag wherever it
|
|
216
|
+
* appears and rejects the ones nobody declared, which leaves `_` holding the
|
|
217
|
+
* command words followed by the real positionals, in order and at a known
|
|
218
|
+
* offset. An unrecognised flag becomes an error naming the command's help,
|
|
219
|
+
* which is the only safe answer: the alternative is guessing that it was meant
|
|
220
|
+
* as a value.
|
|
221
|
+
*
|
|
222
|
+
* `commands/cloud/index.ts` resolves its positionals against its own spec for
|
|
223
|
+
* the same reason; this is that idea for the commands whose positionals are
|
|
224
|
+
* credentials rather than resource names.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Flags accepted on top of whatever a command declares.
|
|
228
|
+
*
|
|
229
|
+
* `--debug` is read by `bin/rebase.js` off `process.argv` and never by a
|
|
230
|
+
* command, but it has to be *declared* somewhere or strict parsing rejects the
|
|
231
|
+
* exact flag the CLI tells people to add. `--help`/`-h` are answered by each
|
|
232
|
+
* command's dispatcher before any work happens.
|
|
233
|
+
*/
|
|
234
|
+
var GLOBAL_COMMAND_FLAGS = {
|
|
235
|
+
"--debug": Boolean,
|
|
236
|
+
"--help": Boolean,
|
|
237
|
+
"-h": "--help"
|
|
238
|
+
};
|
|
239
|
+
/** Did the line ask for help? Answered before dispatch, never by a handler. */
|
|
240
|
+
function wantsHelp(rawArgs) {
|
|
241
|
+
return rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a command's flags and positionals from the full `process.argv`.
|
|
245
|
+
*
|
|
246
|
+
* `commandWords` is how many words name the command itself — 2 for
|
|
247
|
+
* `auth reset-password`, 1 for `start` — and is applied to the *parsed*
|
|
248
|
+
* positionals rather than to `argv`, so a flag placed before the command no
|
|
249
|
+
* longer shifts them.
|
|
250
|
+
*
|
|
251
|
+
* `command` names the command in error messages, e.g. `auth reset-password`.
|
|
252
|
+
*
|
|
253
|
+
* Throws on an unknown flag, on a positional that looks like a flag, and on
|
|
254
|
+
* more positionals than the command takes. `bin/rebase.js` turns each into a
|
|
255
|
+
* one-line `✗ …` and exit 1.
|
|
256
|
+
*/
|
|
257
|
+
function parseCommandArgs({ spec, rawArgs, commandWords, command, maxPositionals }) {
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = arg({
|
|
261
|
+
...GLOBAL_COMMAND_FLAGS,
|
|
262
|
+
...spec
|
|
263
|
+
}, { argv: rawArgs.slice(2) });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof Error && err.code === "ARG_UNKNOWN_OPTION") throw new Error(`${err.message} — run \`rebase ${command} --help\` for the options it takes.`);
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
const positionals = parsed._.slice(commandWords);
|
|
269
|
+
for (const value of positionals) if (value.startsWith("-")) throw new Error(`\`${value}\` looks like an option, not a value — pass it with an explicit flag (\`rebase ${command} --help\`).`);
|
|
270
|
+
if (maxPositionals !== void 0 && positionals.length > maxPositionals) throw new Error(`rebase ${command} takes ${maxPositionals} argument${maxPositionals === 1 ? "" : "s"}, got ${positionals.length}: ${positionals.join(" ")}`);
|
|
271
|
+
return {
|
|
272
|
+
flags: parsed,
|
|
273
|
+
positionals,
|
|
274
|
+
help: Boolean(parsed["--help"])
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
193
278
|
//#region src/utils/project.ts
|
|
194
279
|
/**
|
|
195
280
|
* Project discovery utilities for the Rebase CLI.
|
|
@@ -289,6 +374,34 @@ function findEnvFile(projectRoot) {
|
|
|
289
374
|
return null;
|
|
290
375
|
}
|
|
291
376
|
/**
|
|
377
|
+
* Read the project's `.env` into a plain object.
|
|
378
|
+
*
|
|
379
|
+
* One reader, because there were four: `dotenv` in `start`, a hand-rolled
|
|
380
|
+
* `indexOf("=")` loop in `api-keys`, a single-key regex in `auth`, and its own
|
|
381
|
+
* splitting in `cloud env`. `dotenv` is a declared dependency of this package,
|
|
382
|
+
* so the other three existed for no reason and disagreed with the correct one
|
|
383
|
+
* on the two things people actually write in a `.env`:
|
|
384
|
+
*
|
|
385
|
+
* - `export KEY=value`, which the hand-rolled parser keyed as
|
|
386
|
+
* `export KEY` — so the command reported the key as unset while it was
|
|
387
|
+
* right there in the file;
|
|
388
|
+
* - `KEY=value # comment`, whose comment travelled into the value and then
|
|
389
|
+
* into an `Authorization` header, coming back as a 401 with nothing
|
|
390
|
+
* pointing at the cause.
|
|
391
|
+
*
|
|
392
|
+
* Returns `{}` when the project has no `.env`, so callers can treat "absent"
|
|
393
|
+
* and "empty" alike.
|
|
394
|
+
*/
|
|
395
|
+
function readEnvFile(projectRoot) {
|
|
396
|
+
const envFile = findEnvFile(projectRoot);
|
|
397
|
+
if (!envFile || !fs.existsSync(envFile)) return {};
|
|
398
|
+
try {
|
|
399
|
+
return dotenv.parse(fs.readFileSync(envFile, "utf-8"));
|
|
400
|
+
} catch {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
292
405
|
* Resolve a binary from the project's node_modules/.bin.
|
|
293
406
|
* Checks backend, root, parent monorepo root, then falls back to PATH.
|
|
294
407
|
*/
|
|
@@ -497,7 +610,7 @@ function normalizeUrl(url) {
|
|
|
497
610
|
function createCloudClient(url) {
|
|
498
611
|
return createRebaseClient({
|
|
499
612
|
baseUrl: url,
|
|
500
|
-
|
|
613
|
+
realtime: false,
|
|
501
614
|
auth: {
|
|
502
615
|
storage: createFileAuthStorage(url),
|
|
503
616
|
persistSession: true,
|
|
@@ -516,11 +629,11 @@ async function requireClient(rawArgs) {
|
|
|
516
629
|
const url = resolveCloudUrl(rawArgs);
|
|
517
630
|
const client = createCloudClient(url);
|
|
518
631
|
const session = client.auth.getSession();
|
|
519
|
-
if (!session || !session.accessToken) fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first
|
|
632
|
+
if (!session || !session.accessToken) fail(`Not logged in to ${chalk.cyan(url)}.`, `Run ${chalk.bold("rebase cloud login")} first.`, "not_logged_in");
|
|
520
633
|
if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
|
|
521
634
|
await client.auth.refreshSession();
|
|
522
635
|
} catch {
|
|
523
|
-
fail(`Your session for ${chalk.cyan(url)} has expired.`, `Run ${chalk.bold("rebase cloud login")} to sign in again
|
|
636
|
+
fail(`Your session for ${chalk.cyan(url)} has expired.`, `Run ${chalk.bold("rebase cloud login")} to sign in again.`, "session_expired");
|
|
524
637
|
}
|
|
525
638
|
return {
|
|
526
639
|
client,
|
|
@@ -672,7 +785,7 @@ function requireProjectRef(rawArgs) {
|
|
|
672
785
|
if (parsed["--project"]) return parsed["--project"];
|
|
673
786
|
const link = readLink();
|
|
674
787
|
if (link?.projectId) return link.projectId;
|
|
675
|
-
fail("No project specified and this directory is not linked.", `Pass ${chalk.bold("--project <slug>")} or run ${chalk.bold("rebase cloud link")}
|
|
788
|
+
fail("No project specified and this directory is not linked.", `Pass ${chalk.bold("--project <slug>")} or run ${chalk.bold("rebase cloud link")}.`, "no_project");
|
|
676
789
|
}
|
|
677
790
|
/**
|
|
678
791
|
* Resolve a project reference — slug or UUID — to the internal id the API
|
|
@@ -691,7 +804,7 @@ async function lookupProjectId(ref, client) {
|
|
|
691
804
|
/** Like `lookupProjectId`, but exits with guidance when the ref matches nothing. */
|
|
692
805
|
async function resolveProjectRef(ref, client) {
|
|
693
806
|
const id = await lookupProjectId(ref, client);
|
|
694
|
-
if (id === void 0) fail(`No project with slug ${chalk.bold(ref)}.`, `List yours with ${chalk.bold("rebase cloud projects")}
|
|
807
|
+
if (id === void 0) fail(`No project with slug ${chalk.bold(ref)}.`, `List yours with ${chalk.bold("rebase cloud projects")}.`, "project_not_found");
|
|
695
808
|
return id;
|
|
696
809
|
}
|
|
697
810
|
/** `requireProjectRef` + `resolveProjectRef` in one step. */
|
|
@@ -760,6 +873,28 @@ function emit(human, json) {
|
|
|
760
873
|
else human();
|
|
761
874
|
}
|
|
762
875
|
/**
|
|
876
|
+
* Print a help page — the human one, or a machine-readable description of the
|
|
877
|
+
* same command in JSON mode.
|
|
878
|
+
*
|
|
879
|
+
* `--help` is the one place where "stdout is not a TTY" is a weak signal: a
|
|
880
|
+
* person runs `rebase cloud db --help | less` and wants the page. But the rule
|
|
881
|
+
* this family promises is that stdout carries one JSON value whenever it is not
|
|
882
|
+
* a terminal, and a help page is the easiest possible thing to describe
|
|
883
|
+
* structurally — so rather than carve out an exception, help answers the same
|
|
884
|
+
* question in the reader's own language. For an agent, `--help` piped is then a
|
|
885
|
+
* discovery call rather than 60 lines of ANSI to scrape.
|
|
886
|
+
*
|
|
887
|
+
* `env` shipped this shape first, alone; this generalises it so every group
|
|
888
|
+
* answers the same way.
|
|
889
|
+
*/
|
|
890
|
+
function emitHelp(command, actions, human, extra = {}) {
|
|
891
|
+
emit(human, {
|
|
892
|
+
command,
|
|
893
|
+
actions,
|
|
894
|
+
...extra
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
763
898
|
* Print a warning (+ optional hint) — in every output mode, always to stderr.
|
|
764
899
|
*
|
|
765
900
|
* `emit` is for a command's *result*, and JSON mode legitimately replaces the
|
|
@@ -788,12 +923,27 @@ function warn(message, hint) {
|
|
|
788
923
|
console.error(chalk.yellow(` ⚠ ${message}`));
|
|
789
924
|
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
790
925
|
}
|
|
791
|
-
/**
|
|
926
|
+
/**
|
|
927
|
+
* Print an error (+ optional hint) and exit non-zero. Never returns.
|
|
928
|
+
*
|
|
929
|
+
* `code` is the field a caller branches on, and it defaults to `"error"` rather
|
|
930
|
+
* than `null`. An envelope whose only machine-readable field is null is not
|
|
931
|
+
* machine-readable — `{"error":{"message":"No project specified…","code":null}}`
|
|
932
|
+
* forced the very substring-matching on `message` that the envelope exists to
|
|
933
|
+
* make unnecessary, and `message` is the field most likely to be reworded.
|
|
934
|
+
*
|
|
935
|
+
* `"error"` is deliberately a poor code: it says "this refusal has not been
|
|
936
|
+
* classified yet" without ever being absent. Anything a caller might plausibly
|
|
937
|
+
* want to distinguish — `usage`, `not_found`, `unauthenticated` — passes a real
|
|
938
|
+
* one. Codes are part of the CLI's contract once shipped; see
|
|
939
|
+
* `cloud-reporting.test.ts`, which pins the ones commands are documented to
|
|
940
|
+
* return.
|
|
941
|
+
*/
|
|
792
942
|
function fail(message, hint, code) {
|
|
793
943
|
if (JSON_MODE) {
|
|
794
944
|
printJson({ error: {
|
|
795
945
|
message: stripAnsi(message),
|
|
796
|
-
code: code ??
|
|
946
|
+
code: code ?? "error",
|
|
797
947
|
hint: hint ? stripAnsi(hint) : void 0
|
|
798
948
|
} });
|
|
799
949
|
process.exit(1);
|
|
@@ -822,26 +972,139 @@ async function confirmDestructive(opts) {
|
|
|
822
972
|
message: opts.prompt
|
|
823
973
|
}]);
|
|
824
974
|
if (!confirmed) {
|
|
825
|
-
console.
|
|
975
|
+
console.error(chalk.gray(" Aborted."));
|
|
826
976
|
process.exit(0);
|
|
827
977
|
}
|
|
828
978
|
}
|
|
829
979
|
/**
|
|
830
|
-
*
|
|
980
|
+
* Refuse, rather than prompt, when there is nobody to answer.
|
|
981
|
+
*
|
|
982
|
+
* `confirmDestructive` has always done this for yes/no confirmations. The
|
|
983
|
+
* *value* prompts had no such guard: `cloud login`, `cloud link`, `cloud use`,
|
|
984
|
+
* `cloud orgs create` and `cloud db create` all called `inquirer.prompt`
|
|
985
|
+
* unconditionally, so piping any of them — which is how an agent runs every
|
|
986
|
+
* command in this family — parked the process on a prompt reading from a stdin
|
|
987
|
+
* that was never going to produce a line. A hang is the worst failure mode
|
|
988
|
+
* available here: no output, no exit code, nothing to retry on.
|
|
989
|
+
*
|
|
990
|
+
* @param what what the prompt would have asked for, e.g. "an email and password"
|
|
991
|
+
* @param flags the flags that supply it non-interactively
|
|
992
|
+
*/
|
|
993
|
+
function requireInteractive(what, flags) {
|
|
994
|
+
if (JSON_MODE || process.stdin.isTTY !== true) fail(`This command needs ${what}, and there is no terminal to ask on.`, `Pass ${chalk.bold(flags)}.`, "input_required");
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Resolve one cloud command's flags and ARGUMENTS from the full `process.argv`.
|
|
998
|
+
*
|
|
999
|
+
* This replaces `cloudPositionals`, which was `rawArgs.slice(3).filter(a =>
|
|
1000
|
+
* !a.startsWith("-"))`. Dropping `-`-prefixed tokens looks like it solves the
|
|
1001
|
+
* permissive-parse problem and does not: a flag that takes a VALUE leaves the
|
|
1002
|
+
* value behind, an ordinary word in the argument position that no filter can
|
|
1003
|
+
* tell from a real one. `--project` is the flag every one of these commands
|
|
1004
|
+
* documents, so the failure was reachable from the help page:
|
|
1005
|
+
*
|
|
1006
|
+
* rebase cloud env unset -p acme → removed the variable "acme"
|
|
1007
|
+
* rebase cloud env set KEY -p acme → stored the value "acme"
|
|
1008
|
+
* rebase cloud domains add -p acme → registered the domain "acme"
|
|
1009
|
+
* rebase cloud webhooks delete -p acme 42 → deleted webhook "acme", not 42
|
|
1010
|
+
* rebase cloud cancel -p acme → cancelled deployment id "acme"
|
|
1011
|
+
*
|
|
1012
|
+
* The filter's other half is quieter. A flag nobody declared *is* dropped by
|
|
1013
|
+
* it — but only from the operands, never from the run: nothing rejects it, so
|
|
1014
|
+
* the command proceeds with the argument missing or defaulted. `db backup
|
|
1015
|
+
* --dry-run` listed backups, `domains remove --dry-run` detached the domain,
|
|
1016
|
+
* and `env set KEY=v --secrett` stored the value as an ordinary variable that
|
|
1017
|
+
* `env reveal` will hand back. The one place an undeclared flag became the
|
|
1018
|
+
* argument outright is `projects info|delete`, which resolved its id through
|
|
1019
|
+
* `positionals()` instead — that skips only LEADING `-` tokens, so `projects
|
|
1020
|
+
* delete --force` looked up a project named "--force".
|
|
1021
|
+
*
|
|
1022
|
+
* So: parse the whole line strictly, through the same `parseCommandArgs` the
|
|
1023
|
+
* non-cloud commands use — `arg` then consumes each declared flag *with its
|
|
1024
|
+
* value* wherever it appears, and rejects the undeclared, leaving `_` holding
|
|
1025
|
+
* the command words followed by the real arguments. `commandWords` counts from
|
|
1026
|
+
* `cloud` itself (`cloud env set` ⇒ 3), and is applied to the parsed
|
|
1027
|
+
* positionals, so a flag written before the group shifts nothing.
|
|
1028
|
+
*
|
|
1029
|
+
* Two things this adds over calling `parseCommandArgs` directly, and the reason
|
|
1030
|
+
* it is worth a wrapper:
|
|
1031
|
+
*
|
|
1032
|
+
* - `GLOBAL_CLOUD_FLAGS` is merged in. `--json`, `--yes` and `--project` may
|
|
1033
|
+
* appear anywhere on a cloud line including before the group, so a strict
|
|
1034
|
+
* parse that did not declare them would reject the CLI's own documented
|
|
1035
|
+
* usage. (`parseCommandArgs` adds `--debug`/`--help` on top of that.)
|
|
1036
|
+
* - A parse error is reported through `fail`, not thrown. A throw reaches
|
|
1037
|
+
* `bin/rebase.js`, which prints `✗ …` to stderr — which is right for every
|
|
1038
|
+
* other command and wrong here: `rebase cloud` is in JSON mode whenever
|
|
1039
|
+
* stdout is not a TTY, i.e. always for the agents this family is built for,
|
|
1040
|
+
* and it promises them exactly one JSON value. `fail` keeps that promise,
|
|
1041
|
+
* with the same `usage` code the other refusals in this family use.
|
|
1042
|
+
*/
|
|
1043
|
+
function parseCloudArgs(opts) {
|
|
1044
|
+
const spec = {
|
|
1045
|
+
...GLOBAL_CLOUD_FLAGS,
|
|
1046
|
+
...opts.spec
|
|
1047
|
+
};
|
|
1048
|
+
try {
|
|
1049
|
+
const parsed = parseCommandArgs({
|
|
1050
|
+
...opts,
|
|
1051
|
+
spec
|
|
1052
|
+
});
|
|
1053
|
+
return {
|
|
1054
|
+
flags: parsed.flags,
|
|
1055
|
+
positionals: parsed.positionals
|
|
1056
|
+
};
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
fail(err instanceof Error ? err.message : String(err), void 0, "usage");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Announce an outcome — "Logged in as …", "Deleted project …".
|
|
831
1063
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
1064
|
+
* On **stderr**, in both modes. It reads like a result and is not one: the
|
|
1065
|
+
* result is the JSON value (or the table) on stdout, and every JSON payload in
|
|
1066
|
+
* this family already carries `success: true`. Leaving this on stdout meant a
|
|
1067
|
+
* successful `rebase cloud link | jq` was handed a green tick followed by an
|
|
1068
|
+
* object — one stream, two syntaxes, and only the second parseable.
|
|
1069
|
+
*
|
|
1070
|
+
* It stays visible in JSON mode, unlike `note`: an agent that got a `success`
|
|
1071
|
+
* line on a command it expected to refuse has learned something.
|
|
837
1072
|
*/
|
|
838
|
-
function cloudPositionals(rawArgs) {
|
|
839
|
-
return rawArgs.slice(3).filter((a) => !a.startsWith("-"));
|
|
840
|
-
}
|
|
841
1073
|
function success(message) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1074
|
+
if (JSON_MODE) {
|
|
1075
|
+
process.stderr.write(`${stripAnsi(message)}\n`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
console.error("");
|
|
1079
|
+
console.error(chalk.bold.green(` ✓ ${message}`));
|
|
1080
|
+
console.error("");
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Narrate progress, or point at the next step — "Signing in to …", "Redeploy
|
|
1084
|
+
* for the tenant to pick this up".
|
|
1085
|
+
*
|
|
1086
|
+
* stderr, and **suppressed entirely in JSON mode**. This is the one helper that
|
|
1087
|
+
* a mode may silence, and the distinction from `warn` is worth keeping sharp:
|
|
1088
|
+
*
|
|
1089
|
+
* - A warning is a *condition*. It is as true when piped as when watched, so
|
|
1090
|
+
* silencing it hides something the caller would want to know. `warn` never
|
|
1091
|
+
* silences.
|
|
1092
|
+
* - A note is *hand-holding*. "Next: run `rebase generate-sdk`" tells a person
|
|
1093
|
+
* what to type; the agent reading the JSON already has the same information
|
|
1094
|
+
* structurally, or does not need it. Printing it anyway is transcript noise.
|
|
1095
|
+
*
|
|
1096
|
+
* When in doubt it is a warning. The cost of a needless warning is a line; the
|
|
1097
|
+
* cost of a swallowed one is the deploy that ejected a project off the managed
|
|
1098
|
+
* runtime and said so only to a terminal nobody was looking at.
|
|
1099
|
+
*/
|
|
1100
|
+
function note(message, indent = " ") {
|
|
1101
|
+
if (JSON_MODE) return;
|
|
1102
|
+
console.error(`${indent}${message}`);
|
|
1103
|
+
}
|
|
1104
|
+
/** A blank spacer line on the narration stream. No-op in JSON mode. */
|
|
1105
|
+
function noteBlank() {
|
|
1106
|
+
if (JSON_MODE) return;
|
|
1107
|
+
console.error("");
|
|
845
1108
|
}
|
|
846
1109
|
/** Colorize a deployment / resource status token. */
|
|
847
1110
|
function colorStatus(status) {
|
|
@@ -879,7 +1142,7 @@ function reportError(e, context) {
|
|
|
879
1142
|
if (JSON_MODE) {
|
|
880
1143
|
printJson({ error: {
|
|
881
1144
|
message: err?.message ? stripAnsi(err.message) : String(e),
|
|
882
|
-
code: err?.code ??
|
|
1145
|
+
code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
|
|
883
1146
|
status: err?.status ?? null,
|
|
884
1147
|
context
|
|
885
1148
|
} });
|
|
@@ -888,13 +1151,18 @@ function reportError(e, context) {
|
|
|
888
1151
|
fail(`${context}${err?.status ? ` (${err.status})` : ""}: ${err?.message ?? String(e)}`);
|
|
889
1152
|
}
|
|
890
1153
|
/**
|
|
891
|
-
* Open a URL in the user's default browser (best effort). Always
|
|
892
|
-
* first so it stays usable over SSH or when no browser is available.
|
|
1154
|
+
* Open a URL in the user's default browser (best effort). Always announces the
|
|
1155
|
+
* URL first so it stays usable over SSH or when no browser is available.
|
|
1156
|
+
*
|
|
1157
|
+
* The announcement is narration, not the result — it goes to stderr, and in
|
|
1158
|
+
* JSON mode it is silent. Every caller `emit`s the same URL in its payload, so
|
|
1159
|
+
* a machine reader gets it from the one place it is guaranteed to be parseable
|
|
1160
|
+
* rather than from a line that happens to end in a URL.
|
|
893
1161
|
*/
|
|
894
1162
|
function openUrl(target, label = "Opening") {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1163
|
+
noteBlank();
|
|
1164
|
+
note(`${label} ${chalk.cyan(target)}`);
|
|
1165
|
+
noteBlank();
|
|
898
1166
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
899
1167
|
try {
|
|
900
1168
|
const child = spawn(opener, [target], {
|
|
@@ -949,13 +1217,35 @@ function sanitize(properties) {
|
|
|
949
1217
|
}
|
|
950
1218
|
return out;
|
|
951
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* The CLI's own version, read by walking up to this package's manifest.
|
|
1222
|
+
*
|
|
1223
|
+
* The obvious `require("../../package.json")` was wrong everywhere, not only on
|
|
1224
|
+
* one install path: `vite build` bundles this module into `dist/index.es.js`, so
|
|
1225
|
+
* the specifier resolves relative to `<pkg>/dist/` and lands on
|
|
1226
|
+
* `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or
|
|
1227
|
+
* the monorepo. Every event ever sent carried `cliVersion: "unknown"`, which is
|
|
1228
|
+
* the one field that makes the rest of a payload interpretable.
|
|
1229
|
+
*
|
|
1230
|
+
* So walk up and check the manifest's `name` rather than counting directory
|
|
1231
|
+
* levels: the count differs between `src/telemetry/` and the bundled `dist/`,
|
|
1232
|
+
* and a wrong count fails silently by finding *some* package.json — the nearest
|
|
1233
|
+
* dependency's, under a hoisted layout. Matching the name cannot do that.
|
|
1234
|
+
*/
|
|
952
1235
|
function cliVersion() {
|
|
953
1236
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1237
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1238
|
+
const root = path.parse(dir).root;
|
|
1239
|
+
while (dir && dir !== root) {
|
|
1240
|
+
const manifest = path.join(dir, "package.json");
|
|
1241
|
+
if (fs.existsSync(manifest)) {
|
|
1242
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1243
|
+
if (pkg?.name === "@rebasepro/cli" && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1244
|
+
}
|
|
1245
|
+
dir = path.dirname(dir);
|
|
1246
|
+
}
|
|
1247
|
+
} catch {}
|
|
1248
|
+
return "unknown";
|
|
959
1249
|
}
|
|
960
1250
|
function buildEvent(event, properties, identity) {
|
|
961
1251
|
return {
|
|
@@ -1455,7 +1745,7 @@ ${chalk.bold("Examples")}
|
|
|
1455
1745
|
`);
|
|
1456
1746
|
}
|
|
1457
1747
|
async function createRebaseApp(rawArgs) {
|
|
1458
|
-
if (
|
|
1748
|
+
if (wantsHelp(rawArgs)) {
|
|
1459
1749
|
printInitHelp();
|
|
1460
1750
|
return;
|
|
1461
1751
|
}
|
|
@@ -1464,26 +1754,31 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
|
|
|
1464
1754
|
`);
|
|
1465
1755
|
await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
|
|
1466
1756
|
}
|
|
1757
|
+
/** The flags `rebase init` takes. */
|
|
1758
|
+
var INIT_FLAGS = {
|
|
1759
|
+
"--git": Boolean,
|
|
1760
|
+
"--install": Boolean,
|
|
1761
|
+
"--database-url": String,
|
|
1762
|
+
"--introspect": Boolean,
|
|
1763
|
+
"--template": String,
|
|
1764
|
+
"--headless": Boolean,
|
|
1765
|
+
"--project": String,
|
|
1766
|
+
"--setup-key": String,
|
|
1767
|
+
"--yes": Boolean,
|
|
1768
|
+
"-g": "--git",
|
|
1769
|
+
"-i": "--install",
|
|
1770
|
+
"-t": "--template",
|
|
1771
|
+
"-y": "--yes"
|
|
1772
|
+
};
|
|
1467
1773
|
async function promptForOptions(rawArgs, pm) {
|
|
1468
|
-
const args =
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
"
|
|
1473
|
-
|
|
1474
|
-
"--headless": Boolean,
|
|
1475
|
-
"--project": String,
|
|
1476
|
-
"--setup-key": String,
|
|
1477
|
-
"--yes": Boolean,
|
|
1478
|
-
"-g": "--git",
|
|
1479
|
-
"-i": "--install",
|
|
1480
|
-
"-t": "--template",
|
|
1481
|
-
"-y": "--yes"
|
|
1482
|
-
}, {
|
|
1483
|
-
argv: rawArgs.slice(3),
|
|
1484
|
-
permissive: true
|
|
1774
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
1775
|
+
spec: INIT_FLAGS,
|
|
1776
|
+
rawArgs,
|
|
1777
|
+
commandWords: 1,
|
|
1778
|
+
command: "init",
|
|
1779
|
+
maxPositionals: 1
|
|
1485
1780
|
});
|
|
1486
|
-
const nameArg =
|
|
1781
|
+
const nameArg = positionals[0];
|
|
1487
1782
|
const isNonInteractive = args["--yes"] || false;
|
|
1488
1783
|
if (nameArg) {
|
|
1489
1784
|
const resolvedName = path.basename(path.resolve(process.cwd(), nameArg));
|
|
@@ -1606,6 +1901,49 @@ async function linkScaffoldToCloud(options) {
|
|
|
1606
1901
|
console.warn(chalk.yellow(` ${linkLater}`));
|
|
1607
1902
|
}
|
|
1608
1903
|
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Make the initial commit, after everything that writes into the project has run.
|
|
1906
|
+
*
|
|
1907
|
+
* It used to happen immediately after `git init`, which is before dependency
|
|
1908
|
+
* installation and before introspection — so `init --git --install` ended on a
|
|
1909
|
+
* dirty tree whose only untracked file was `pnpm-lock.yaml`. A lockfile is
|
|
1910
|
+
* precisely the thing that should be in a project's first commit, and a brand
|
|
1911
|
+
* new scaffold whose first `git status` is dirty invites the reader to conclude
|
|
1912
|
+
* the lockfile is deliberately ignored and never commit it at all.
|
|
1913
|
+
*
|
|
1914
|
+
* Introspection has the same shape: it generates `config/collections` and
|
|
1915
|
+
* `schema.generated.ts`, which belong in the commit describing the scaffold that
|
|
1916
|
+
* produced them.
|
|
1917
|
+
*
|
|
1918
|
+
* `git init` stays where it was. Creating the repository early costs nothing and
|
|
1919
|
+
* means a failed install still leaves the user a repository to commit into.
|
|
1920
|
+
*/
|
|
1921
|
+
async function commitScaffold(targetDirectory) {
|
|
1922
|
+
try {
|
|
1923
|
+
await execa("git", ["add", "-A"], { cwd: targetDirectory });
|
|
1924
|
+
let identity = {};
|
|
1925
|
+
try {
|
|
1926
|
+
await execa("git", ["config", "user.email"], { cwd: targetDirectory });
|
|
1927
|
+
} catch {
|
|
1928
|
+
identity = {
|
|
1929
|
+
GIT_AUTHOR_NAME: "Rebase",
|
|
1930
|
+
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1931
|
+
GIT_COMMITTER_NAME: "Rebase",
|
|
1932
|
+
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
await execa("git", [
|
|
1936
|
+
"commit",
|
|
1937
|
+
"-m",
|
|
1938
|
+
"Initial commit from Rebase"
|
|
1939
|
+
], {
|
|
1940
|
+
cwd: targetDirectory,
|
|
1941
|
+
env: identity
|
|
1942
|
+
});
|
|
1943
|
+
} catch {
|
|
1944
|
+
console.warn(chalk.yellow(" Warning: Failed to create the initial commit"));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1609
1947
|
async function createProject$1(options) {
|
|
1610
1948
|
const startedAt = Date.now();
|
|
1611
1949
|
if (fs.existsSync(options.targetDirectory)) {
|
|
@@ -1645,6 +1983,7 @@ async function createProject$1(options) {
|
|
|
1645
1983
|
await applyHeadless(options.targetDirectory, options.headless);
|
|
1646
1984
|
await replacePlaceholders(options);
|
|
1647
1985
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1986
|
+
let gitInitialized = false;
|
|
1648
1987
|
if (options.git) {
|
|
1649
1988
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
1650
1989
|
try {
|
|
@@ -1656,26 +1995,7 @@ async function createProject$1(options) {
|
|
|
1656
1995
|
"refs/heads/main"
|
|
1657
1996
|
], { cwd: options.targetDirectory });
|
|
1658
1997
|
} catch {}
|
|
1659
|
-
|
|
1660
|
-
let identity = {};
|
|
1661
|
-
try {
|
|
1662
|
-
await execa("git", ["config", "user.email"], { cwd: options.targetDirectory });
|
|
1663
|
-
} catch {
|
|
1664
|
-
identity = {
|
|
1665
|
-
GIT_AUTHOR_NAME: "Rebase",
|
|
1666
|
-
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1667
|
-
GIT_COMMITTER_NAME: "Rebase",
|
|
1668
|
-
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
await execa("git", [
|
|
1672
|
-
"commit",
|
|
1673
|
-
"-m",
|
|
1674
|
-
"Initial commit from Rebase"
|
|
1675
|
-
], {
|
|
1676
|
-
cwd: options.targetDirectory,
|
|
1677
|
-
env: identity
|
|
1678
|
-
});
|
|
1998
|
+
gitInitialized = true;
|
|
1679
1999
|
} catch {
|
|
1680
2000
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1681
2001
|
}
|
|
@@ -1732,6 +2052,7 @@ async function createProject$1(options) {
|
|
|
1732
2052
|
console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
|
|
1733
2053
|
}
|
|
1734
2054
|
}
|
|
2055
|
+
if (gitInitialized) await commitScaffold(options.targetDirectory);
|
|
1735
2056
|
await linkScaffoldToCloud(options);
|
|
1736
2057
|
console.log("");
|
|
1737
2058
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
@@ -1789,7 +2110,7 @@ async function createProject$1(options) {
|
|
|
1789
2110
|
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
1790
2111
|
}
|
|
1791
2112
|
console.log("");
|
|
1792
|
-
console.log(isBaas ? chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
2113
|
+
console.log(isBaas ? introspected ? chalk.gray("This starts a headless API (Hono + PostgreSQL) over the collections just ") + chalk.gray("generated from your database in config/collections. Edit them to change what the ") + chalk.gray("API exposes; docs are at /api/swagger.") : chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
1793
2114
|
console.log("");
|
|
1794
2115
|
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
1795
2116
|
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
@@ -1976,18 +2297,51 @@ project directory ${path.basename(options.targetDirectory)}/ was created and is
|
|
|
1976
2297
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1977
2298
|
}
|
|
1978
2299
|
}
|
|
1979
|
-
|
|
2300
|
+
/** `undefined` binds the wildcard address, which is a different question — see isPortAvailable. */
|
|
2301
|
+
function canBind(port, host) {
|
|
1980
2302
|
return new Promise((resolve) => {
|
|
1981
2303
|
const server = net.createServer();
|
|
1982
|
-
server.once("error", () => {
|
|
1983
|
-
resolve(
|
|
2304
|
+
server.once("error", (err) => {
|
|
2305
|
+
resolve(err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
1984
2306
|
});
|
|
1985
2307
|
server.once("listening", () => {
|
|
1986
2308
|
server.close(() => resolve(true));
|
|
1987
2309
|
});
|
|
1988
|
-
server.listen(port);
|
|
2310
|
+
if (host === void 0) server.listen(port);
|
|
2311
|
+
else server.listen(port, host);
|
|
1989
2312
|
});
|
|
1990
2313
|
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Whether `port` is free — on the wildcard address *and* on both loopback addresses.
|
|
2316
|
+
*
|
|
2317
|
+
* All three, because on macOS/BSD a successful bind does not mean the port is
|
|
2318
|
+
* unused. Sockets carry `SO_REUSEADDR` (Node sets it), under which a wildcard
|
|
2319
|
+
* bind and a specific-address bind on the same port do not conflict — in either
|
|
2320
|
+
* direction. So each probe alone has a blind spot, and they are different ones:
|
|
2321
|
+
*
|
|
2322
|
+
* - **Wildcard only** (what this used to do) misses a server bound to
|
|
2323
|
+
* `127.0.0.1` and `[::1]` — a Homebrew or Postgres.app install, i.e. most
|
|
2324
|
+
* developer machines. 5432 was reported free while it was already serving
|
|
2325
|
+
* another project's database. Docker then published `*:5432` for the same
|
|
2326
|
+
* reason and the container started cleanly, with no "port already allocated"
|
|
2327
|
+
* error anywhere to hint at the collision. `DATABASE_URL` pointed at
|
|
2328
|
+
* `localhost:5432`, `localhost` resolves to `::1` first, and every command
|
|
2329
|
+
* reported success while reading and writing the *pre-existing* database — a
|
|
2330
|
+
* `db push` would have created tables, roles and RLS policies inside it.
|
|
2331
|
+
*
|
|
2332
|
+
* - **Loopback only** misses the opposite case: Docker Desktop publishes a
|
|
2333
|
+
* container's port on `*`, and a specific-address bind succeeds right past it.
|
|
2334
|
+
* That port is free to probe and unusable to publish, so `docker compose up -d
|
|
2335
|
+
* db` fails on "Bind for 0.0.0.0:PORT failed: port is already allocated" —
|
|
2336
|
+
* loudly, but only after the project has been generated around the bad port.
|
|
2337
|
+
*
|
|
2338
|
+
* Requiring all three costs three sockets and leaves neither gap. The wildcard
|
|
2339
|
+
* bind is the one the container itself has to make; the loopback binds are the
|
|
2340
|
+
* addresses `DATABASE_URL` will actually name.
|
|
2341
|
+
*/
|
|
2342
|
+
async function isPortAvailable(port) {
|
|
2343
|
+
return await canBind(port) && await canBind(port, "127.0.0.1") && await canBind(port, "::1");
|
|
2344
|
+
}
|
|
1991
2345
|
async function findAvailablePort(startPort) {
|
|
1992
2346
|
let port = startPort;
|
|
1993
2347
|
while (!await isPortAvailable(port)) port++;
|
|
@@ -2013,11 +2367,38 @@ function readCliVersion() {
|
|
|
2013
2367
|
} catch {}
|
|
2014
2368
|
return "latest";
|
|
2015
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* The runtime image tag to pin, given the version of the CLI doing the scaffolding.
|
|
2372
|
+
*
|
|
2373
|
+
* Only a stable release publishes `rebasepro/server` — a multi-arch build on
|
|
2374
|
+
* every push to main would cost minutes per commit for an image nobody pulls.
|
|
2375
|
+
* So pinning a prerelease CLI's own version writes a tag that cannot exist, and
|
|
2376
|
+
* `docker compose up` fails on `manifest unknown`, which is the same dead end
|
|
2377
|
+
* as the missing-repository bug this pinning was added to prevent.
|
|
2378
|
+
*
|
|
2379
|
+
* A prerelease therefore falls back to `latest`, which is correct rather than
|
|
2380
|
+
* merely available: a bundle's manifest declares the runtime range it needs
|
|
2381
|
+
* (`^1`), the image supplies only `@rebasepro/server`, and the framework a
|
|
2382
|
+
* bundle runs is installed from its own `deps.declared` at boot. The current
|
|
2383
|
+
* stable runtime boots a canary bundle by design.
|
|
2384
|
+
*
|
|
2385
|
+
* A floating tag is a real cost — it is what pinning exists to avoid — so say
|
|
2386
|
+
* so in the file rather than leaving a reader to discover it.
|
|
2387
|
+
*/
|
|
2388
|
+
function resolveRuntimeImageTag(cliVersion) {
|
|
2389
|
+
if (/^\d+\.\d+\.\d+-/.test(cliVersion)) return {
|
|
2390
|
+
tag: "latest",
|
|
2391
|
+
note: `# Scaffolded by a prerelease CLI (${cliVersion}), which publishes no runtime image,\n# so this floats to the newest stable runtime. Pin an exact version once you
|
|
2392
|
+
# deploy: a moving tag changes what you are running with no version changing.`
|
|
2393
|
+
};
|
|
2394
|
+
return { tag: cliVersion };
|
|
2395
|
+
}
|
|
2016
2396
|
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
2017
2397
|
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
2018
2398
|
const envPath = path.join(targetDirectory, ".env");
|
|
2019
2399
|
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
2020
2400
|
fs.copyFileSync(envExamplePath, envPath);
|
|
2401
|
+
fs.chmodSync(envPath, 384);
|
|
2021
2402
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
2022
2403
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
2023
2404
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
@@ -2031,8 +2412,10 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2031
2412
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
2032
2413
|
const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
|
|
2033
2414
|
envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
|
|
2034
|
-
|
|
2035
|
-
|
|
2415
|
+
envContent = envContent.replace(/^#?\s*VITE_API_URL=.*$/m, "VITE_API_URL=");
|
|
2416
|
+
const { tag: runtimeVersion, note } = resolveRuntimeImageTag(readCliVersion());
|
|
2417
|
+
const pinned = `${note ? `${note}\n` : ""}REBASE_VERSION=${runtimeVersion}`;
|
|
2418
|
+
envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, pinned) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\n${pinned}\n`;
|
|
2036
2419
|
if (databaseUrl) {
|
|
2037
2420
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
2038
2421
|
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
@@ -2040,7 +2423,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2040
2423
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2041
2424
|
} else {
|
|
2042
2425
|
const dbPort = await findAvailablePort(5432);
|
|
2043
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://
|
|
2426
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2044
2427
|
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
2045
2428
|
if (fs.existsSync(dockerComposePath)) {
|
|
2046
2429
|
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
@@ -2048,7 +2431,11 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2048
2431
|
fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
|
|
2049
2432
|
}
|
|
2050
2433
|
}
|
|
2051
|
-
fs.writeFileSync(envPath, envContent,
|
|
2434
|
+
fs.writeFileSync(envPath, envContent, {
|
|
2435
|
+
encoding: "utf-8",
|
|
2436
|
+
mode: 384
|
|
2437
|
+
});
|
|
2438
|
+
fs.chmodSync(envPath, 384);
|
|
2052
2439
|
}
|
|
2053
2440
|
}
|
|
2054
2441
|
//#endregion
|
|
@@ -2318,7 +2705,17 @@ async function generateSdkCommand(args) {
|
|
|
2318
2705
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
2319
2706
|
console.log("");
|
|
2320
2707
|
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
2321
|
-
|
|
2708
|
+
let files;
|
|
2709
|
+
try {
|
|
2710
|
+
files = generateSDK(collections);
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
if (err instanceof CodegenError) {
|
|
2713
|
+
console.log("");
|
|
2714
|
+
console.log(chalk.red(` ✗ ${err.message}`));
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
throw err;
|
|
2718
|
+
}
|
|
2322
2719
|
const schemaVersion = remoteSchemaVersion ?? computeSchemaVersion(collections);
|
|
2323
2720
|
files.push({
|
|
2324
2721
|
path: "schema.meta.ts",
|
|
@@ -2330,7 +2727,6 @@ async function generateSdkCommand(args) {
|
|
|
2330
2727
|
// curl -s <api-url>/api/meta/schema-version
|
|
2331
2728
|
//
|
|
2332
2729
|
export const SCHEMA_VERSION = ${JSON.stringify(schemaVersion)};
|
|
2333
|
-
export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOString())};
|
|
2334
2730
|
`
|
|
2335
2731
|
});
|
|
2336
2732
|
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
@@ -2352,8 +2748,9 @@ export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOS
|
|
|
2352
2748
|
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
2353
2749
|
console.log(chalk.gray(" });"));
|
|
2354
2750
|
console.log("");
|
|
2355
|
-
|
|
2356
|
-
|
|
2751
|
+
const exampleAccessor = toSafeIdentifier(exampleSlug);
|
|
2752
|
+
const exampleAccess = isIdentifierLike(exampleAccessor) ? `rebase.data.${exampleAccessor}` : `rebase.data[${JSON.stringify(exampleAccessor)}]`;
|
|
2753
|
+
console.log(chalk.gray(` const { data } = await ${exampleAccess}.find();`));
|
|
2357
2754
|
console.log("");
|
|
2358
2755
|
}
|
|
2359
2756
|
//#endregion
|
|
@@ -2367,7 +2764,7 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
2367
2764
|
return;
|
|
2368
2765
|
}
|
|
2369
2766
|
const projectRoot = requireProjectRoot();
|
|
2370
|
-
recordEvent("cli.
|
|
2767
|
+
recordEvent("cli.schema", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2371
2768
|
const backendDir = requireBackendDir(projectRoot);
|
|
2372
2769
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2373
2770
|
if (!activePlugin) {
|
|
@@ -2436,7 +2833,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2436
2833
|
return;
|
|
2437
2834
|
}
|
|
2438
2835
|
const projectRoot = requireProjectRoot();
|
|
2439
|
-
recordEvent("cli.
|
|
2836
|
+
recordEvent("cli.db", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2440
2837
|
const backendDir = requireBackendDir(projectRoot);
|
|
2441
2838
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2442
2839
|
if (!activePlugin) {
|
|
@@ -2834,13 +3231,20 @@ function validateManifest(raw) {
|
|
|
2834
3231
|
byPath.set(at, name);
|
|
2835
3232
|
}
|
|
2836
3233
|
const storage = validateStorageSources(raw.storage, issues);
|
|
3234
|
+
let telemetry;
|
|
3235
|
+
if (raw.telemetry !== void 0) if (typeof raw.telemetry === "boolean") telemetry = raw.telemetry;
|
|
3236
|
+
else issues.push({
|
|
3237
|
+
path: "telemetry",
|
|
3238
|
+
message: "must be a boolean — only `false` does anything, and it opts this repository out of usage sharing"
|
|
3239
|
+
});
|
|
2837
3240
|
if (issues.length > 0) return { issues };
|
|
2838
3241
|
return {
|
|
2839
3242
|
manifest: {
|
|
2840
3243
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2841
3244
|
rebase: raw.rebase,
|
|
2842
3245
|
apps,
|
|
2843
|
-
...storage ? { storage } : {}
|
|
3246
|
+
...storage ? { storage } : {},
|
|
3247
|
+
...telemetry !== void 0 ? { telemetry } : {}
|
|
2844
3248
|
},
|
|
2845
3249
|
issues
|
|
2846
3250
|
};
|
|
@@ -2949,7 +3353,7 @@ function synthesizeManifest(projectRoot) {
|
|
|
2949
3353
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
2950
3354
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
2951
3355
|
apps.backend = backend;
|
|
2952
|
-
if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or run `rebase eject
|
|
3356
|
+
if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or move it aside and run `rebase eject`, which\n writes an entrypoint of its own and owns the image. Eject does not adopt\n this file: it refuses to replace it unless you pass --force.");
|
|
2953
3357
|
}
|
|
2954
3358
|
if (exists("frontend")) apps.web = {
|
|
2955
3359
|
type: "static",
|
|
@@ -2997,13 +3401,59 @@ function loadManifest(projectRoot) {
|
|
|
2997
3401
|
filePath
|
|
2998
3402
|
};
|
|
2999
3403
|
}
|
|
3000
|
-
/**
|
|
3404
|
+
/** The keys `writeManifest` knows how to write. Everything else is carried through. */
|
|
3405
|
+
var MODELLED_MANIFEST_KEYS = [
|
|
3406
|
+
"$schema",
|
|
3407
|
+
"rebase",
|
|
3408
|
+
"apps",
|
|
3409
|
+
"storage",
|
|
3410
|
+
"telemetry"
|
|
3411
|
+
];
|
|
3412
|
+
/**
|
|
3413
|
+
* What is on disk right now, or `{}` — this is a *rewrite*, so the file that is
|
|
3414
|
+
* about to be replaced is the only record of the keys the caller did not model.
|
|
3415
|
+
* Unparseable is treated as absent: `loadManifest` refuses malformed JSON long
|
|
3416
|
+
* before anything gets here, and a writer is not the place to fail on it.
|
|
3417
|
+
*/
|
|
3418
|
+
function readManifestObject(filePath) {
|
|
3419
|
+
try {
|
|
3420
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
3421
|
+
return isRecord(parsed) ? parsed : {};
|
|
3422
|
+
} catch {
|
|
3423
|
+
return {};
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Write a manifest, with a trailing newline so it plays well with other tools.
|
|
3428
|
+
*
|
|
3429
|
+
* **Every key on disk survives.** This used to emit exactly `$schema`, `rebase`
|
|
3430
|
+
* and `apps`, so a rewrite deleted the rest of the file — and two commands with
|
|
3431
|
+
* no visible relationship to either key rewrite it: `rebase eject` and
|
|
3432
|
+
* `rebase apps init --force`. A repository that had committed
|
|
3433
|
+
* `"telemetry": false` lost its opt-out, and a multi-bucket project lost its
|
|
3434
|
+
* whole `storage` block, in a commit whose stated change was `runtime: custom`.
|
|
3435
|
+
*
|
|
3436
|
+
* So: the caller's manifest wins for what it models, the file supplies the rest.
|
|
3437
|
+
* `storage` and `telemetry` fall back to the file because the callers that
|
|
3438
|
+
* synthesize a manifest (`apps init --force`) cannot know them — they are
|
|
3439
|
+
* authored, not inferred — and unknown top-level keys are copied verbatim
|
|
3440
|
+
* rather than listed, since a hand-listed set loses the next key too.
|
|
3441
|
+
*/
|
|
3001
3442
|
function writeManifest(projectRoot, manifest) {
|
|
3002
3443
|
const filePath = manifestPath(projectRoot);
|
|
3444
|
+
const existing = readManifestObject(filePath);
|
|
3445
|
+
const carried = {};
|
|
3446
|
+
for (const [key, value] of Object.entries(existing)) if (!MODELLED_MANIFEST_KEYS.includes(key)) carried[key] = value;
|
|
3447
|
+
const schema = manifest.$schema ?? (typeof existing.$schema === "string" ? existing.$schema : void 0) ?? "https://rebase.pro/schemas/rebase.json";
|
|
3448
|
+
const storage = manifest.storage ?? (isRecord(existing.storage) ? existing.storage : void 0);
|
|
3449
|
+
const telemetry = manifest.telemetry ?? (typeof existing.telemetry === "boolean" ? existing.telemetry : void 0);
|
|
3003
3450
|
const ordered = {
|
|
3004
|
-
$schema:
|
|
3451
|
+
$schema: schema,
|
|
3005
3452
|
rebase: manifest.rebase,
|
|
3006
|
-
apps: manifest.apps
|
|
3453
|
+
apps: manifest.apps,
|
|
3454
|
+
...storage ? { storage } : {},
|
|
3455
|
+
...telemetry !== void 0 ? { telemetry } : {},
|
|
3456
|
+
...carried
|
|
3007
3457
|
};
|
|
3008
3458
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
3009
3459
|
return filePath;
|
|
@@ -3074,6 +3524,106 @@ function resolveBackendPaths(app, projectRoot) {
|
|
|
3074
3524
|
};
|
|
3075
3525
|
}
|
|
3076
3526
|
//#endregion
|
|
3527
|
+
//#region src/utils/collection-drift.ts
|
|
3528
|
+
/**
|
|
3529
|
+
* Which edits under `config/collections` can put the *SQL* schema out of date.
|
|
3530
|
+
*
|
|
3531
|
+
* `rebase dev` watches that directory and, on any change, either tells the
|
|
3532
|
+
* developer to run `rebase schema generate` / `rebase db push` or runs them.
|
|
3533
|
+
* The watcher is recursive and knows nothing about what it is watching, so it
|
|
3534
|
+
* said that about every file under the directory — including a
|
|
3535
|
+
* `collections/firestore/exercises.ts`, whose documents live in Firestore and
|
|
3536
|
+
* for which there is no Drizzle schema to regenerate and no database to push
|
|
3537
|
+
* to. Advice that is wrong on every edit is advice a developer learns to
|
|
3538
|
+
* ignore, which is worse than none: the same box is the only warning for the
|
|
3539
|
+
* Postgres collection next to it, where it is real.
|
|
3540
|
+
*
|
|
3541
|
+
* Two independent reasons a change cannot affect the SQL schema, both checked
|
|
3542
|
+
* here:
|
|
3543
|
+
*
|
|
3544
|
+
* 1. **The loader would never read the file.** `loadCollectionsFromDirectory`
|
|
3545
|
+
* reads the top level of the collections directory only — no recursion, no
|
|
3546
|
+
* `index`, no tests, no declarations. A file it does not read cannot change
|
|
3547
|
+
* what it returns, and the watcher must not claim otherwise.
|
|
3548
|
+
* 2. **Every collection in it is served by another engine.** A Firestore or
|
|
3549
|
+
* MongoDB collection has no table, no migration and no policies.
|
|
3550
|
+
*
|
|
3551
|
+
* The engine check reads the source text rather than importing the module: the
|
|
3552
|
+
* CLI runs as plain Node and cannot evaluate a project's TypeScript, and a
|
|
3553
|
+
* watcher must answer in the time between two keystrokes. Anything it cannot
|
|
3554
|
+
* read confidently counts as SQL-affecting — a spurious warning is a nuisance,
|
|
3555
|
+
* a suppressed one hides real drift.
|
|
3556
|
+
*/
|
|
3557
|
+
/**
|
|
3558
|
+
* Would `loadCollectionsFromDirectory` load this file?
|
|
3559
|
+
*
|
|
3560
|
+
* Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
|
|
3561
|
+
* scan. `relativePath` is relative to the collections directory, as `fs.watch`
|
|
3562
|
+
* reports it.
|
|
3563
|
+
*/
|
|
3564
|
+
function isLoadedCollectionFile(relativePath) {
|
|
3565
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
3566
|
+
if (normalized.includes("/")) return false;
|
|
3567
|
+
const file = normalized;
|
|
3568
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) return false;
|
|
3569
|
+
if (file.startsWith(".")) return false;
|
|
3570
|
+
if (file.includes(".test.")) return false;
|
|
3571
|
+
if (file.endsWith(".d.ts")) return false;
|
|
3572
|
+
if (file === "index.ts" || file === "index.js") return false;
|
|
3573
|
+
return true;
|
|
3574
|
+
}
|
|
3575
|
+
var ENGINE_LITERAL = /\bengine\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3576
|
+
var DATA_SOURCE_LITERAL = /\bdataSource\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3577
|
+
/**
|
|
3578
|
+
* Drop comments, so a `// engine: "firestore"` in a docblock cannot silence a
|
|
3579
|
+
* warning about the Postgres collection the file actually declares.
|
|
3580
|
+
*
|
|
3581
|
+
* Deliberately naive — it does not understand strings, so a `//` inside one
|
|
3582
|
+
* (a URL in a default value) eats the rest of that line. That only ever removes
|
|
3583
|
+
* text, and removing an engine literal makes this file *more* likely to warn,
|
|
3584
|
+
* which is the side to be wrong on.
|
|
3585
|
+
*/
|
|
3586
|
+
function stripComments(source) {
|
|
3587
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
3588
|
+
}
|
|
3589
|
+
function literals(source, pattern) {
|
|
3590
|
+
const found = [];
|
|
3591
|
+
for (const match of source.matchAll(pattern)) found.push(match[1]);
|
|
3592
|
+
return found;
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Does this collection source declare anything a SQL toolchain would own?
|
|
3596
|
+
*
|
|
3597
|
+
* The fallback order matches `resolveDataSource`: an explicit `engine` wins,
|
|
3598
|
+
* and a collection that only names a `dataSource` is resolved as if the key
|
|
3599
|
+
* were the engine — which is what that function does when no registry is
|
|
3600
|
+
* available, and the CLI has none. An engine nobody recognises counts as
|
|
3601
|
+
* relational, for the reason `isRelationalCollection` gives.
|
|
3602
|
+
*
|
|
3603
|
+
* A file declaring several collections is SQL-affecting if *any* of them is.
|
|
3604
|
+
*/
|
|
3605
|
+
function declaresRelationalCollection(rawSource) {
|
|
3606
|
+
const source = stripComments(rawSource);
|
|
3607
|
+
const engines = literals(source, ENGINE_LITERAL);
|
|
3608
|
+
const declared = engines.length > 0 ? engines : literals(source, DATA_SOURCE_LITERAL).filter((key) => key !== DEFAULT_DATA_SOURCE_KEY);
|
|
3609
|
+
if (declared.length === 0) return true;
|
|
3610
|
+
return declared.some((engine) => getDataSourceCapabilities(engine).supportsRelations);
|
|
3611
|
+
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Can this edit have changed the generated SQL schema?
|
|
3614
|
+
*
|
|
3615
|
+
* Answers `true` when it cannot tell — an unreadable file is a reason to warn,
|
|
3616
|
+
* not a reason to go quiet.
|
|
3617
|
+
*/
|
|
3618
|
+
function affectsSqlSchema(collectionsDir, relativePath) {
|
|
3619
|
+
if (!isLoadedCollectionFile(relativePath)) return false;
|
|
3620
|
+
try {
|
|
3621
|
+
return declaresRelationalCollection(fs.readFileSync(path.join(collectionsDir, relativePath), "utf8"));
|
|
3622
|
+
} catch {
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
//#endregion
|
|
3077
3627
|
//#region src/commands/dev.ts
|
|
3078
3628
|
/**
|
|
3079
3629
|
* CLI command: rebase dev
|
|
@@ -3167,38 +3717,67 @@ function getProjectPort(projectRoot) {
|
|
|
3167
3717
|
* 3. Previously used port from .rebase-dev-port (port affinity across restarts)
|
|
3168
3718
|
* 4. Deterministic hash from project path (unique per project)
|
|
3169
3719
|
*/
|
|
3720
|
+
/**
|
|
3721
|
+
* A TCP port, or `undefined` for anything that is not one.
|
|
3722
|
+
*
|
|
3723
|
+
* One predicate for both sources below. The port file was already checked for
|
|
3724
|
+
* range, and `PORT` — the source a human or a platform actually sets — was not,
|
|
3725
|
+
* so `PORT=oops` reached `parseInt` and was returned as `NaN`: the dev server
|
|
3726
|
+
* then bound to whatever the OS handed out and the CLI printed a URL for a port
|
|
3727
|
+
* nothing was listening on.
|
|
3728
|
+
*/
|
|
3729
|
+
function parsePort(raw) {
|
|
3730
|
+
if (raw === void 0) return void 0;
|
|
3731
|
+
const port = Number(raw.trim());
|
|
3732
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return void 0;
|
|
3733
|
+
return port;
|
|
3734
|
+
}
|
|
3170
3735
|
function resolveStartPort(projectRoot, explicitPort) {
|
|
3171
3736
|
if (explicitPort) return explicitPort;
|
|
3172
|
-
if (process.env.PORT)
|
|
3737
|
+
if (process.env.PORT) {
|
|
3738
|
+
const fromEnv = parsePort(process.env.PORT);
|
|
3739
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
3740
|
+
console.warn(chalk.yellow(` ⚠ Ignoring PORT="${process.env.PORT}" — not a port between 1 and 65535.`));
|
|
3741
|
+
}
|
|
3173
3742
|
try {
|
|
3174
3743
|
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
3175
3744
|
if (fs.existsSync(portFile)) {
|
|
3176
|
-
const saved =
|
|
3177
|
-
if (saved
|
|
3745
|
+
const saved = parsePort(fs.readFileSync(portFile, "utf-8"));
|
|
3746
|
+
if (saved !== void 0) return saved;
|
|
3178
3747
|
}
|
|
3179
3748
|
} catch {}
|
|
3180
3749
|
return getProjectPort(projectRoot);
|
|
3181
3750
|
}
|
|
3751
|
+
/**
|
|
3752
|
+
* The flags `rebase dev` takes.
|
|
3753
|
+
*
|
|
3754
|
+
* Exported so `dev.test.ts` can assert that every short alias the help
|
|
3755
|
+
* advertises is declared here: the help said `--port, -p` while the spec has
|
|
3756
|
+
* only ever declared `-P`, so `rebase dev -p 4000` typed straight off the help
|
|
3757
|
+
* page passed `4000` as a positional and started on the default port.
|
|
3758
|
+
*/
|
|
3759
|
+
var DEV_FLAGS = {
|
|
3760
|
+
"--backend-only": Boolean,
|
|
3761
|
+
"--frontend-only": Boolean,
|
|
3762
|
+
"--port": Number,
|
|
3763
|
+
"--generate": Boolean,
|
|
3764
|
+
"-b": "--backend-only",
|
|
3765
|
+
"-f": "--frontend-only",
|
|
3766
|
+
"-P": "--port",
|
|
3767
|
+
"-g": "--generate"
|
|
3768
|
+
};
|
|
3182
3769
|
async function devCommand(rawArgs) {
|
|
3183
|
-
|
|
3184
|
-
"--backend-only": Boolean,
|
|
3185
|
-
"--frontend-only": Boolean,
|
|
3186
|
-
"--port": Number,
|
|
3187
|
-
"--generate": Boolean,
|
|
3188
|
-
"--help": Boolean,
|
|
3189
|
-
"-b": "--backend-only",
|
|
3190
|
-
"-f": "--frontend-only",
|
|
3191
|
-
"-p": "--port",
|
|
3192
|
-
"-g": "--generate",
|
|
3193
|
-
"-h": "--help"
|
|
3194
|
-
}, {
|
|
3195
|
-
argv: rawArgs.slice(3),
|
|
3196
|
-
permissive: true
|
|
3197
|
-
});
|
|
3198
|
-
if (args["--help"]) {
|
|
3770
|
+
if (wantsHelp(rawArgs)) {
|
|
3199
3771
|
printDevHelp();
|
|
3200
3772
|
return;
|
|
3201
3773
|
}
|
|
3774
|
+
const { flags: args } = parseCommandArgs({
|
|
3775
|
+
spec: DEV_FLAGS,
|
|
3776
|
+
rawArgs,
|
|
3777
|
+
commandWords: 1,
|
|
3778
|
+
command: "dev",
|
|
3779
|
+
maxPositionals: 0
|
|
3780
|
+
});
|
|
3202
3781
|
const projectRoot = requireProjectRoot();
|
|
3203
3782
|
recordEvent("cli.dev", {
|
|
3204
3783
|
backend_only: Boolean(args["--backend-only"]),
|
|
@@ -3345,6 +3924,20 @@ async function devCommand(rawArgs) {
|
|
|
3345
3924
|
} catch {}
|
|
3346
3925
|
/** Whether the frontend has been launched (we only launch it once). */
|
|
3347
3926
|
let frontendLaunched = false;
|
|
3927
|
+
try {
|
|
3928
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3929
|
+
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3930
|
+
if (pluginCli) await execa(tsxBin, [
|
|
3931
|
+
pluginCli,
|
|
3932
|
+
"schema",
|
|
3933
|
+
"stale",
|
|
3934
|
+
"--fix"
|
|
3935
|
+
], {
|
|
3936
|
+
cwd: backendDir,
|
|
3937
|
+
stdio: "inherit",
|
|
3938
|
+
env
|
|
3939
|
+
});
|
|
3940
|
+
} catch {}
|
|
3348
3941
|
if (shouldGenerate) {
|
|
3349
3942
|
console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
|
|
3350
3943
|
try {
|
|
@@ -3372,14 +3965,18 @@ async function devCommand(rawArgs) {
|
|
|
3372
3965
|
const collectionsDir = path.join(projectRoot, "config", "collections");
|
|
3373
3966
|
if (fs.existsSync(collectionsDir)) {
|
|
3374
3967
|
let watchDebounce = null;
|
|
3968
|
+
let sqlSchemaAffected = false;
|
|
3375
3969
|
fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
|
|
3376
3970
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
3971
|
+
sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
|
|
3377
3972
|
if (watchDebounce) clearTimeout(watchDebounce);
|
|
3378
3973
|
watchDebounce = setTimeout(async () => {
|
|
3379
|
-
|
|
3974
|
+
const regenerateSchema = sqlSchemaAffected;
|
|
3975
|
+
sqlSchemaAffected = false;
|
|
3976
|
+
console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating ${regenerateSchema ? "schema & SDK" : "SDK"}...`));
|
|
3380
3977
|
try {
|
|
3381
3978
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3382
|
-
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3979
|
+
const pluginCli = regenerateSchema && activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3383
3980
|
if (pluginCli) await execa(tsxBin, [
|
|
3384
3981
|
pluginCli,
|
|
3385
3982
|
"schema",
|
|
@@ -3395,7 +3992,7 @@ async function devCommand(rawArgs) {
|
|
|
3395
3992
|
stdio: "inherit",
|
|
3396
3993
|
env
|
|
3397
3994
|
});
|
|
3398
|
-
console.log(chalk.green(
|
|
3995
|
+
console.log(chalk.green(` ✓ ${regenerateSchema ? "Schema & SDK" : "SDK"} regenerated successfully. Hono will reload.`));
|
|
3399
3996
|
} catch (err) {
|
|
3400
3997
|
console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
|
|
3401
3998
|
}
|
|
@@ -3420,12 +4017,14 @@ async function devCommand(rawArgs) {
|
|
|
3420
4017
|
let driftDebounce = null;
|
|
3421
4018
|
fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
|
|
3422
4019
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
4020
|
+
if (!affectsSqlSchema(collectionsDir, filename)) return;
|
|
3423
4021
|
if (driftDebounce) clearTimeout(driftDebounce);
|
|
3424
4022
|
driftDebounce = setTimeout(() => {
|
|
4023
|
+
const shown = filename.length > 31 ? `…${filename.slice(-30)}` : filename.padEnd(31);
|
|
3425
4024
|
console.log([
|
|
3426
4025
|
"",
|
|
3427
4026
|
chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
|
|
3428
|
-
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(
|
|
4027
|
+
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(shown) + chalk.yellow("│"),
|
|
3429
4028
|
chalk.yellow(" │ │"),
|
|
3430
4029
|
chalk.yellow(" │ Your schema may be out of sync. Run: │"),
|
|
3431
4030
|
chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
|
|
@@ -3520,7 +4119,7 @@ ${chalk.green.bold("Usage")}
|
|
|
3520
4119
|
${chalk.green.bold("Options")}
|
|
3521
4120
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
3522
4121
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
3523
|
-
${chalk.blue("--port, -
|
|
4122
|
+
${chalk.blue("--port, -P")} Backend port (default: auto-detected per project)
|
|
3524
4123
|
${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
|
|
3525
4124
|
|
|
3526
4125
|
${chalk.green.bold("Description")}
|
|
@@ -4245,10 +4844,11 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
4245
4844
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
4246
4845
|
* sitting in the repository looking exactly like the server.
|
|
4247
4846
|
*
|
|
4248
|
-
* A project that means to
|
|
4249
|
-
* writes
|
|
4847
|
+
* A project that means to own its server process runs `rebase eject`, which
|
|
4848
|
+
* writes an entrypoint, a Dockerfile and a compose file together and flips the
|
|
4250
4849
|
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
4251
|
-
* implying the file is a mistake
|
|
4850
|
+
* implying the file is a mistake — but eject writes *its* entrypoint, so the
|
|
4851
|
+
* warning must not read as "eject will keep what you wrote here".
|
|
4252
4852
|
*/
|
|
4253
4853
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
4254
4854
|
const found = [path.join("backend", "src", "index.ts"), path.join(path.dirname(functionsDir), "src", "index.ts")].find((candidate) => fs.existsSync(path.join(projectRoot, candidate)));
|
|
@@ -4282,7 +4882,9 @@ async function buildBundle(options) {
|
|
|
4282
4882
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
4283
4883
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
4284
4884
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
4285
|
-
console.log(chalk.dim(" or run `rebase eject
|
|
4885
|
+
console.log(chalk.dim(" or run `rebase eject`, which writes an entrypoint of its own and owns"));
|
|
4886
|
+
console.log(chalk.dim(" the image — it does not adopt this file, and will not replace it"));
|
|
4887
|
+
console.log(chalk.dim(" without --force."));
|
|
4286
4888
|
}
|
|
4287
4889
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
4288
4890
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -4651,6 +5253,61 @@ function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
|
4651
5253
|
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
4652
5254
|
}
|
|
4653
5255
|
/**
|
|
5256
|
+
* The environment every static app is built with, wherever that build is driven from.
|
|
5257
|
+
*
|
|
5258
|
+
* Shared because there are two drivers — `foldFrontendIntoBundle` here, and
|
|
5259
|
+
* `buildAssetApp` in `build.ts` for a standalone `type: "static"` app — and they
|
|
5260
|
+
* had already drifted: the path variables were duplicated into both, so a fix
|
|
5261
|
+
* applied to one shipped a bundle built the old way from the other. One function
|
|
5262
|
+
* makes that impossible rather than merely unlikely.
|
|
5263
|
+
*
|
|
5264
|
+
* ## REBASE_APP_*
|
|
5265
|
+
*
|
|
5266
|
+
* The declared path is a build-time input, not only a serving concern: Vite
|
|
5267
|
+
* reads `base` from REBASE_APP_BASE, and the trailing slash is that field's
|
|
5268
|
+
* convention. See `assertBuiltForPath`.
|
|
5269
|
+
*
|
|
5270
|
+
* ## NODE_ENV
|
|
5271
|
+
*
|
|
5272
|
+
* A built app is a production artifact by construction, so build it as one. Not
|
|
5273
|
+
* a formality: the scaffold's `.env` carries `NODE_ENV=development` for the dev
|
|
5274
|
+
* backend, and Vite's `loadEnv` promotes a `NODE_ENV` found in an env file into
|
|
5275
|
+
* the build unless the environment already sets one. So `rebase build` and
|
|
5276
|
+
* `rebase cloud deploy` shipped a *development* bundle — `import.meta.env.DEV
|
|
5277
|
+
* === true`, development React, dev-only branches live — from commands whose
|
|
5278
|
+
* whole purpose is to produce something deployable. Setting it here is what
|
|
5279
|
+
* closes it: Vite consults the env file's NODE_ENV only when `process.env`
|
|
5280
|
+
* has none.
|
|
5281
|
+
*
|
|
5282
|
+
* ## VITE_API_URL
|
|
5283
|
+
*
|
|
5284
|
+
* An app served by the backend it talks to has its API on its own origin by
|
|
5285
|
+
* construction, so a baked-in absolute URL can only be wrong. It was: that same
|
|
5286
|
+
* `.env` carries `VITE_API_URL=http://localhost:3001` and
|
|
5287
|
+
* `frontend/vite.config.ts` reads the project root via `envDir: ".."`, so a
|
|
5288
|
+
* stock deploy shipped a site whose every request went to whoever ran the build
|
|
5289
|
+
* — passing every server-side health check on the way out. Blanking it here
|
|
5290
|
+
* fixes the bundle even for a project whose `.env` predates the `init` fix, or
|
|
5291
|
+
* was written by hand. Empty is the right value rather than a missing one: the
|
|
5292
|
+
* client falls back to `window.location.origin`, which keeps working when a
|
|
5293
|
+
* custom domain is added.
|
|
5294
|
+
*
|
|
5295
|
+
* Vite prioritises `process.env.VITE_*` over `.env` files, so an explicit
|
|
5296
|
+
* `VITE_API_URL=https://api.example.com rebase cloud deploy` still wins — the
|
|
5297
|
+
* cross-origin escape hatch stays open, it just has to be deliberate. Nothing on
|
|
5298
|
+
* this path loads the project `.env` into `process.env`, so a value inherited
|
|
5299
|
+
* here really was set by the caller.
|
|
5300
|
+
*/
|
|
5301
|
+
function staticBuildEnv(appPath, appName) {
|
|
5302
|
+
return {
|
|
5303
|
+
REBASE_APP_PATH: appPath,
|
|
5304
|
+
REBASE_APP_BASE: appPath === "/" ? "/" : `${appPath}/`,
|
|
5305
|
+
REBASE_APP_NAME: appName,
|
|
5306
|
+
NODE_ENV: "production",
|
|
5307
|
+
VITE_API_URL: process.env.VITE_API_URL ?? ""
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
5310
|
+
/**
|
|
4654
5311
|
* Build the project's static apps and fold them into the backend bundle.
|
|
4655
5312
|
*
|
|
4656
5313
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
@@ -4669,11 +5326,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
4669
5326
|
cwd: projectRoot,
|
|
4670
5327
|
stdio: "inherit",
|
|
4671
5328
|
shell: true,
|
|
4672
|
-
env:
|
|
4673
|
-
REBASE_APP_PATH: app.path,
|
|
4674
|
-
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4675
|
-
REBASE_APP_NAME: app.name
|
|
4676
|
-
}
|
|
5329
|
+
env: staticBuildEnv(app.path, app.name)
|
|
4677
5330
|
});
|
|
4678
5331
|
const assetsDir = path.join(projectRoot, app.output);
|
|
4679
5332
|
if (!fs.existsSync(assetsDir)) throw new Error(`"${app.name}" declared output "${app.output}" does not exist after building — the bundle would ship without a frontend.`);
|
|
@@ -4731,23 +5384,24 @@ ${chalk.bold("Examples")}
|
|
|
4731
5384
|
`.trim());
|
|
4732
5385
|
}
|
|
4733
5386
|
async function buildCommand(rawArgs = []) {
|
|
4734
|
-
|
|
4735
|
-
"--out": String,
|
|
4736
|
-
"--skip-type-check": Boolean,
|
|
4737
|
-
"--skip-schema": Boolean,
|
|
4738
|
-
"--no-static": Boolean,
|
|
4739
|
-
"--skip-static-build": Boolean,
|
|
4740
|
-
"--legacy": Boolean,
|
|
4741
|
-
"--help": Boolean,
|
|
4742
|
-
"-h": "--help"
|
|
4743
|
-
}, {
|
|
4744
|
-
argv: rawArgs.slice(3),
|
|
4745
|
-
permissive: true
|
|
4746
|
-
});
|
|
4747
|
-
if (args["--help"]) {
|
|
5387
|
+
if (wantsHelp(rawArgs)) {
|
|
4748
5388
|
printHelp$5();
|
|
4749
5389
|
return;
|
|
4750
5390
|
}
|
|
5391
|
+
const { flags: args, positionals: requested } = parseCommandArgs({
|
|
5392
|
+
spec: {
|
|
5393
|
+
"--output": String,
|
|
5394
|
+
"--out": "--output",
|
|
5395
|
+
"--skip-type-check": Boolean,
|
|
5396
|
+
"--skip-schema": Boolean,
|
|
5397
|
+
"--no-static": Boolean,
|
|
5398
|
+
"--skip-static-build": Boolean,
|
|
5399
|
+
"--legacy": Boolean
|
|
5400
|
+
},
|
|
5401
|
+
rawArgs,
|
|
5402
|
+
commandWords: 1,
|
|
5403
|
+
command: "build"
|
|
5404
|
+
});
|
|
4751
5405
|
const projectRoot = requireProjectRoot();
|
|
4752
5406
|
if (args["--legacy"]) {
|
|
4753
5407
|
await runWorkspaceBuilds(projectRoot);
|
|
@@ -4765,7 +5419,6 @@ async function buildCommand(rawArgs = []) {
|
|
|
4765
5419
|
throw err;
|
|
4766
5420
|
}
|
|
4767
5421
|
const { manifest, source } = loaded;
|
|
4768
|
-
const requested = args._.filter((a) => !a.startsWith("-"));
|
|
4769
5422
|
let targets = buildableApps(manifest);
|
|
4770
5423
|
if (requested.length > 0) {
|
|
4771
5424
|
const known = new Set(targets.map((t) => t.name));
|
|
@@ -4800,7 +5453,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4800
5453
|
projectRoot,
|
|
4801
5454
|
appName: name,
|
|
4802
5455
|
app,
|
|
4803
|
-
outDir: args["--
|
|
5456
|
+
outDir: args["--output"],
|
|
4804
5457
|
runtimeRange: manifest.rebase,
|
|
4805
5458
|
storage: manifest.storage,
|
|
4806
5459
|
skipTypeCheck: args["--skip-type-check"],
|
|
@@ -4837,7 +5490,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4837
5490
|
});
|
|
4838
5491
|
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
4839
5492
|
}
|
|
4840
|
-
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--
|
|
5493
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--output"]);
|
|
4841
5494
|
console.log("");
|
|
4842
5495
|
}
|
|
4843
5496
|
console.log(chalk.green("✓ Build complete."));
|
|
@@ -4862,11 +5515,7 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
4862
5515
|
cwd: projectRoot,
|
|
4863
5516
|
stdio: "inherit",
|
|
4864
5517
|
shell: true,
|
|
4865
|
-
env:
|
|
4866
|
-
REBASE_APP_PATH: basePath,
|
|
4867
|
-
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
4868
|
-
REBASE_APP_NAME: name
|
|
4869
|
-
}
|
|
5518
|
+
env: staticBuildEnv(basePath, name)
|
|
4870
5519
|
});
|
|
4871
5520
|
} catch {
|
|
4872
5521
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -4948,6 +5597,56 @@ function findCliRoot(from) {
|
|
|
4948
5597
|
}
|
|
4949
5598
|
return null;
|
|
4950
5599
|
}
|
|
5600
|
+
/** The block names the payload may switch on. A typo has to be an error. */
|
|
5601
|
+
var SHAPE_FLAGS = ["collections", "frontend"];
|
|
5602
|
+
/**
|
|
5603
|
+
* Render one payload file for this project.
|
|
5604
|
+
*
|
|
5605
|
+
* The payload is one set of files rather than one per flavour, because the
|
|
5606
|
+
* flavours differ by about a dozen lines and two copies of a 230-line
|
|
5607
|
+
* entrypoint are how copies drift — the payload had already drifted from
|
|
5608
|
+
* `app/backend/src/index.ts` over `cronsDir`, which silently stopped every cron
|
|
5609
|
+
* job in an ejected project. So both branches live in the file, marked with
|
|
5610
|
+
* lines that are comments in TypeScript, YAML and Dockerfiles alike:
|
|
5611
|
+
*
|
|
5612
|
+
* // {{#collections}}
|
|
5613
|
+
* import { tables } from "./schema.generated.js";
|
|
5614
|
+
* // {{/collections}}
|
|
5615
|
+
* // {{^collections}}
|
|
5616
|
+
* // No schema module: this project introspects the database.
|
|
5617
|
+
* // {{/collections}}
|
|
5618
|
+
*
|
|
5619
|
+
* `{{#name}}` keeps its block when the flag is on, `{{^name}}` when it is off,
|
|
5620
|
+
* and the marker lines themselves never reach the user. The template stays
|
|
5621
|
+
* valid TypeScript with every marker line removed, which is the flavour a
|
|
5622
|
+
* typechecker would see.
|
|
5623
|
+
*/
|
|
5624
|
+
function renderPayload(contents, shape, projectName) {
|
|
5625
|
+
const flags = shape;
|
|
5626
|
+
const out = [];
|
|
5627
|
+
let open = null;
|
|
5628
|
+
for (const line of contents.split("\n")) {
|
|
5629
|
+
const marker = /^\s*(?:\/\/|#)\s*\{\{([#^/])([A-Za-z]+)\}\}\s*$/.exec(line);
|
|
5630
|
+
if (!marker) {
|
|
5631
|
+
if (!open || open.keep) out.push(line);
|
|
5632
|
+
continue;
|
|
5633
|
+
}
|
|
5634
|
+
const [, kind, name] = marker;
|
|
5635
|
+
if (kind === "/") {
|
|
5636
|
+
if (!open || open.name !== name) throw new Error(`Eject template: {{/${name}}} does not close an open block.`);
|
|
5637
|
+
open = null;
|
|
5638
|
+
continue;
|
|
5639
|
+
}
|
|
5640
|
+
if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
|
|
5641
|
+
if (!SHAPE_FLAGS.includes(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
|
|
5642
|
+
open = {
|
|
5643
|
+
name,
|
|
5644
|
+
keep: kind === "#" ? flags[name] === true : flags[name] !== true
|
|
5645
|
+
};
|
|
5646
|
+
}
|
|
5647
|
+
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
5648
|
+
return out.join("\n").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
5649
|
+
}
|
|
4951
5650
|
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
4952
5651
|
var PAYLOAD = [
|
|
4953
5652
|
{
|
|
@@ -4998,25 +5697,30 @@ ${chalk.bold("Usage")}
|
|
|
4998
5697
|
|
|
4999
5698
|
${chalk.bold("Options")}
|
|
5000
5699
|
--dry-run List what would change, and change nothing
|
|
5700
|
+
--force Replace an existing backend/src/index.ts or
|
|
5701
|
+
env.ts, keeping the current file as <name>.bak
|
|
5001
5702
|
-h, --help Show this help
|
|
5002
5703
|
`.trim());
|
|
5003
5704
|
}
|
|
5004
5705
|
async function ejectCommand(rawArgs = []) {
|
|
5005
|
-
|
|
5006
|
-
"--dry-run": Boolean,
|
|
5007
|
-
"--help": Boolean,
|
|
5008
|
-
"-h": "--help"
|
|
5009
|
-
}, {
|
|
5010
|
-
argv: rawArgs.slice(2),
|
|
5011
|
-
permissive: true
|
|
5012
|
-
});
|
|
5013
|
-
if (args["--help"]) {
|
|
5706
|
+
if (wantsHelp(rawArgs)) {
|
|
5014
5707
|
printHelp$4();
|
|
5015
5708
|
return;
|
|
5016
5709
|
}
|
|
5710
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
5711
|
+
spec: {
|
|
5712
|
+
"--dry-run": Boolean,
|
|
5713
|
+
"--force": Boolean
|
|
5714
|
+
},
|
|
5715
|
+
rawArgs,
|
|
5716
|
+
commandWords: 1,
|
|
5717
|
+
command: "eject",
|
|
5718
|
+
maxPositionals: 1
|
|
5719
|
+
});
|
|
5017
5720
|
const projectRoot = requireProjectRoot();
|
|
5018
5721
|
const dryRun = Boolean(args["--dry-run"]);
|
|
5019
|
-
const
|
|
5722
|
+
const force = Boolean(args["--force"]);
|
|
5723
|
+
const requested = positionals[0];
|
|
5020
5724
|
let loaded;
|
|
5021
5725
|
try {
|
|
5022
5726
|
loaded = loadManifest(projectRoot);
|
|
@@ -5065,7 +5769,12 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5065
5769
|
process.exit(1);
|
|
5066
5770
|
}
|
|
5067
5771
|
const payloadDir = path.join(cliRoot, "templates", "eject");
|
|
5772
|
+
const shape = {
|
|
5773
|
+
collections: resolveBackendPaths(app, projectRoot).hasCollections,
|
|
5774
|
+
frontend: fs.existsSync(path.join(projectRoot, "frontend"))
|
|
5775
|
+
};
|
|
5068
5776
|
const planned = [];
|
|
5777
|
+
const blocked = [];
|
|
5069
5778
|
for (const file of PAYLOAD) {
|
|
5070
5779
|
const source = path.join(payloadDir, file.from);
|
|
5071
5780
|
if (!fs.existsSync(source)) {
|
|
@@ -5073,26 +5782,43 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5073
5782
|
process.exit(1);
|
|
5074
5783
|
}
|
|
5075
5784
|
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
5785
|
+
if (exists && file.overwrite && !force) blocked.push(file.to);
|
|
5076
5786
|
planned.push({
|
|
5077
5787
|
to: file.to,
|
|
5078
|
-
action: exists
|
|
5788
|
+
action: !exists ? "write" : file.overwrite ? "overwrite" : "keep"
|
|
5079
5789
|
});
|
|
5080
5790
|
}
|
|
5791
|
+
if (blocked.length > 0) {
|
|
5792
|
+
console.error(chalk.red("✗ Ejecting would replace a file this project already has:"));
|
|
5793
|
+
for (const item of blocked) console.error(chalk.red(` ${item}`));
|
|
5794
|
+
console.error(chalk.dim(" Eject writes its own entrypoint — it does not adopt yours."));
|
|
5795
|
+
console.error(chalk.dim(" Move the file aside, or re-run with --force, which keeps the current"));
|
|
5796
|
+
console.error(chalk.dim(" contents as <name>.bak."));
|
|
5797
|
+
process.exit(1);
|
|
5798
|
+
}
|
|
5081
5799
|
if (dryRun) {
|
|
5082
5800
|
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
5083
5801
|
console.log("");
|
|
5084
|
-
for (const item of planned)
|
|
5085
|
-
console.log(` ${chalk.
|
|
5802
|
+
for (const item of planned) if (item.action === "write") console.log(` ${chalk.green("write")} ${item.to}`);
|
|
5803
|
+
else if (item.action === "overwrite") console.log(` ${chalk.yellow("overwrite")} ${item.to} ${chalk.dim(`(kept as ${item.to}.bak)`)}`);
|
|
5804
|
+
else console.log(` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
|
|
5805
|
+
console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
|
|
5806
|
+
if (fs.existsSync(path.join(projectRoot, "backend", "package.json"))) console.log(` ${chalk.green("write")} backend/package.json ${chalk.dim("(main, dev and start scripts)")}`);
|
|
5086
5807
|
console.log("");
|
|
5087
5808
|
console.log(chalk.dim("Nothing was changed."));
|
|
5088
5809
|
return;
|
|
5089
5810
|
}
|
|
5090
5811
|
const projectName = projectNameOf(projectRoot);
|
|
5812
|
+
const backups = [];
|
|
5091
5813
|
for (const [index, file] of PAYLOAD.entries()) {
|
|
5092
5814
|
if (planned[index].action === "keep") continue;
|
|
5093
5815
|
const destination = path.join(projectRoot, file.to);
|
|
5816
|
+
if (planned[index].action === "overwrite") {
|
|
5817
|
+
fs.copyFileSync(destination, `${destination}.bak`);
|
|
5818
|
+
backups.push(`${file.to}.bak`);
|
|
5819
|
+
}
|
|
5094
5820
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
5095
|
-
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8")
|
|
5821
|
+
const contents = renderPayload(fs.readFileSync(path.join(payloadDir, file.from), "utf8"), shape, projectName);
|
|
5096
5822
|
fs.writeFileSync(destination, contents, "utf8");
|
|
5097
5823
|
}
|
|
5098
5824
|
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
@@ -5112,9 +5838,14 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5112
5838
|
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
5113
5839
|
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
5114
5840
|
console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
|
|
5841
|
+
for (const backup of backups) console.log(` ${chalk.cyan(backup.padEnd(26))} what was there before`);
|
|
5115
5842
|
console.log("");
|
|
5116
5843
|
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
5117
5844
|
console.log(chalk.yellow(" upgrades no longer reach this project."));
|
|
5845
|
+
if (!shape.collections) {
|
|
5846
|
+
console.log(chalk.yellow(" This project declares no collections, so the entrypoint derives them"));
|
|
5847
|
+
console.log(chalk.yellow(" from the live database, as the managed runtime did."));
|
|
5848
|
+
}
|
|
5118
5849
|
console.log("");
|
|
5119
5850
|
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
5120
5851
|
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
@@ -5171,19 +5902,20 @@ Build first with ${chalk.cyan("rebase build")}.
|
|
|
5171
5902
|
`.trim());
|
|
5172
5903
|
}
|
|
5173
5904
|
async function startCommand(rawArgs = []) {
|
|
5174
|
-
|
|
5175
|
-
"--bundle": String,
|
|
5176
|
-
"--legacy": Boolean,
|
|
5177
|
-
"--help": Boolean,
|
|
5178
|
-
"-h": "--help"
|
|
5179
|
-
}, {
|
|
5180
|
-
argv: rawArgs.slice(3),
|
|
5181
|
-
permissive: true
|
|
5182
|
-
});
|
|
5183
|
-
if (args["--help"]) {
|
|
5905
|
+
if (wantsHelp(rawArgs)) {
|
|
5184
5906
|
printHelp$3();
|
|
5185
5907
|
return;
|
|
5186
5908
|
}
|
|
5909
|
+
const { flags: args } = parseCommandArgs({
|
|
5910
|
+
spec: {
|
|
5911
|
+
"--bundle": String,
|
|
5912
|
+
"--legacy": Boolean
|
|
5913
|
+
},
|
|
5914
|
+
rawArgs,
|
|
5915
|
+
commandWords: 1,
|
|
5916
|
+
command: "start",
|
|
5917
|
+
maxPositionals: 0
|
|
5918
|
+
});
|
|
5187
5919
|
const projectRoot = requireProjectRoot();
|
|
5188
5920
|
const envFile = findEnvFile(projectRoot);
|
|
5189
5921
|
const env = { ...process.env };
|
|
@@ -5198,7 +5930,10 @@ async function startCommand(rawArgs = []) {
|
|
|
5198
5930
|
}
|
|
5199
5931
|
ensureBundleDependencies(projectRoot, bundleDir);
|
|
5200
5932
|
console.log(`${chalk.bold("Rebase")} — starting runtime from ${chalk.cyan(path.relative(projectRoot, bundleDir))}/\n`);
|
|
5201
|
-
if (envFile && fs.existsSync(envFile)) (await import("dotenv")).config({
|
|
5933
|
+
if (envFile && fs.existsSync(envFile)) (await import("dotenv")).config({
|
|
5934
|
+
path: envFile,
|
|
5935
|
+
quiet: true
|
|
5936
|
+
});
|
|
5202
5937
|
process.env.REBASE_BUNDLE = bundleDir;
|
|
5203
5938
|
try {
|
|
5204
5939
|
const { runFromBundle } = await import("@rebasepro/server");
|
|
@@ -5286,8 +6021,50 @@ async function startWorkspaceBackend(projectRoot, env) {
|
|
|
5286
6021
|
* Subcommands:
|
|
5287
6022
|
* reset-password — Reset a user's password
|
|
5288
6023
|
*/
|
|
6024
|
+
/**
|
|
6025
|
+
* Pick the user with exactly this email out of a search response.
|
|
6026
|
+
*
|
|
6027
|
+
* `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display
|
|
6028
|
+
* name**, ordered by role count descending. This used to take row `[0]` and
|
|
6029
|
+
* reset it, then print the email it had been *given* as confirmation — so two
|
|
6030
|
+
* ordinary situations ended in a successful-looking reset of somebody else's
|
|
6031
|
+
* account:
|
|
6032
|
+
*
|
|
6033
|
+
* - a substring collision: `bob@example.com` also matches
|
|
6034
|
+
* `robert.bob@example.com`;
|
|
6035
|
+
* - a display name, which is user-controlled and accepted up to 255
|
|
6036
|
+
* characters with no constraint on its content, containing an address
|
|
6037
|
+
* belonging to someone else.
|
|
6038
|
+
*
|
|
6039
|
+
* The ordering makes it worse rather than better — `array_length(roles) DESC
|
|
6040
|
+
* NULLS LAST` puts the most privileged match first, so the account most likely
|
|
6041
|
+
* to be reset by mistake is an admin's.
|
|
6042
|
+
*
|
|
6043
|
+
* Returns `undefined` when nothing matched exactly, which the caller reports
|
|
6044
|
+
* rather than falling through to a guess. The direct-database fallback below
|
|
6045
|
+
* has always matched with `eq(usersTable.email, email)`; this is the same
|
|
6046
|
+
* definition, so the command no longer resets different accounts depending on
|
|
6047
|
+
* whether the backend happened to be running.
|
|
6048
|
+
*/
|
|
6049
|
+
function selectUserForEmail(payload, email) {
|
|
6050
|
+
const wanted = email.trim().toLowerCase();
|
|
6051
|
+
if (!wanted) return void 0;
|
|
6052
|
+
const rows = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.users) ? payload.users : [];
|
|
6053
|
+
for (const row of rows) {
|
|
6054
|
+
if (!row || typeof row !== "object") continue;
|
|
6055
|
+
const record = row;
|
|
6056
|
+
const rowEmail = typeof record.email === "string" ? record.email.trim().toLowerCase() : void 0;
|
|
6057
|
+
if (!rowEmail || rowEmail !== wanted) continue;
|
|
6058
|
+
const id = typeof record.id === "string" ? record.id : typeof record.uid === "string" ? record.uid : void 0;
|
|
6059
|
+
if (!id) continue;
|
|
6060
|
+
return {
|
|
6061
|
+
id,
|
|
6062
|
+
email: record.email
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
5289
6066
|
async function authCommand(subcommand, rawArgs) {
|
|
5290
|
-
if (!subcommand || subcommand === "--help") {
|
|
6067
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5291
6068
|
printAuthHelp();
|
|
5292
6069
|
return;
|
|
5293
6070
|
}
|
|
@@ -5302,32 +6079,58 @@ async function authCommand(subcommand, rawArgs) {
|
|
|
5302
6079
|
process.exit(1);
|
|
5303
6080
|
}
|
|
5304
6081
|
}
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
6082
|
+
/**
|
|
6083
|
+
* The flags `rebase auth reset-password` takes.
|
|
6084
|
+
*
|
|
6085
|
+
* `-p` was advertised in this command's own help and never declared here, so
|
|
6086
|
+
* `arg` — running permissively — pushed it into the positionals and the value
|
|
6087
|
+
* *after* it shifted out of reach: anyone following the help set the account's
|
|
6088
|
+
* password to the two-character string `-p`. Declared now, and `auth.test.ts`
|
|
6089
|
+
* asserts that the help and this spec list the same aliases.
|
|
6090
|
+
*/
|
|
6091
|
+
var RESET_PASSWORD_FLAGS = {
|
|
6092
|
+
"--email": String,
|
|
6093
|
+
"--password": String,
|
|
6094
|
+
"-e": "--email",
|
|
6095
|
+
"-p": "--password"
|
|
6096
|
+
};
|
|
6097
|
+
/**
|
|
6098
|
+
* Which account, and which password, this invocation names.
|
|
6099
|
+
*
|
|
6100
|
+
* Both may still be absent — the caller reports a missing email — but neither
|
|
6101
|
+
* can be a flag. `parseCommandArgs` parses the whole line strictly, so an
|
|
6102
|
+
* undeclared flag is an error rather than a positional. That is what stops
|
|
6103
|
+
* `rebase auth reset-password bob@example.com --debug` from setting Bob's
|
|
6104
|
+
* password to `--debug`, which is the flag the CLI itself prints after every
|
|
6105
|
+
* failure as the thing to re-run with.
|
|
6106
|
+
*
|
|
6107
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6108
|
+
*/
|
|
6109
|
+
function resolveResetPasswordArgs(rawArgs) {
|
|
6110
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6111
|
+
spec: RESET_PASSWORD_FLAGS,
|
|
6112
|
+
rawArgs,
|
|
6113
|
+
commandWords: 2,
|
|
6114
|
+
command: "auth reset-password",
|
|
6115
|
+
maxPositionals: 2
|
|
6116
|
+
});
|
|
6117
|
+
return {
|
|
6118
|
+
email: flags["--email"] || positionals[0],
|
|
6119
|
+
password: flags["--password"] || positionals[1]
|
|
6120
|
+
};
|
|
6121
|
+
}
|
|
6122
|
+
async function resetPassword(rawArgs) {
|
|
6123
|
+
const { email, password: newPassword } = resolveResetPasswordArgs(rawArgs);
|
|
6124
|
+
if (!email) {
|
|
6125
|
+
console.error(chalk.red("✗ Email is required."));
|
|
5319
6126
|
console.log("");
|
|
5320
6127
|
console.log(chalk.gray(" Usage: rebase auth reset-password <email> [new-password]"));
|
|
5321
6128
|
console.log(chalk.gray(" rebase auth reset-password --email user@example.com --password NewPass123!"));
|
|
5322
6129
|
process.exit(1);
|
|
5323
6130
|
}
|
|
5324
6131
|
const projectRoot = requireProjectRoot();
|
|
5325
|
-
let envServiceKey;
|
|
5326
6132
|
const envFile = findEnvFile(projectRoot);
|
|
5327
|
-
|
|
5328
|
-
const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
|
|
5329
|
-
if (match && match[1]) envServiceKey = match[1];
|
|
5330
|
-
} catch {}
|
|
6133
|
+
const envServiceKey = readEnvFile(projectRoot).REBASE_SERVICE_KEY;
|
|
5331
6134
|
let baseUrl = process.env.REBASE_BASE_URL;
|
|
5332
6135
|
let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
|
|
5333
6136
|
const statePath = path.join(projectRoot, ".rebase", "state.json");
|
|
@@ -5347,7 +6150,7 @@ async function resetPassword(rawArgs) {
|
|
|
5347
6150
|
try {
|
|
5348
6151
|
const finalPass = newPassword || "NewPassword123!";
|
|
5349
6152
|
const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
5350
|
-
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=
|
|
6153
|
+
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;
|
|
5351
6154
|
const searchRes = await fetch(searchUrl, { headers: {
|
|
5352
6155
|
"Authorization": `Bearer ${serviceKey}`,
|
|
5353
6156
|
"Accept": "application/json"
|
|
@@ -5355,18 +6158,9 @@ async function resetPassword(rawArgs) {
|
|
|
5355
6158
|
if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
|
|
5356
6159
|
const searchData = await searchRes.json();
|
|
5357
6160
|
if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
|
|
5358
|
-
|
|
5359
|
-
if (
|
|
5360
|
-
|
|
5361
|
-
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
5362
|
-
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
5363
|
-
} else if ("users" in searchData && Array.isArray(searchData.users)) {
|
|
5364
|
-
const firstUser = searchData.users[0];
|
|
5365
|
-
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
5366
|
-
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
5367
|
-
}
|
|
5368
|
-
if (!userId) throw new Error(`User not found with email: ${email}`);
|
|
5369
|
-
const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
|
|
6161
|
+
const matched = selectUserForEmail(searchData, email);
|
|
6162
|
+
if (!matched) throw new Error(`No user has the email ${email}.`);
|
|
6163
|
+
const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;
|
|
5370
6164
|
const resetRes = await fetch(resetUrl, {
|
|
5371
6165
|
method: "POST",
|
|
5372
6166
|
headers: {
|
|
@@ -5383,7 +6177,7 @@ async function resetPassword(rawArgs) {
|
|
|
5383
6177
|
console.log("API reset successful.");
|
|
5384
6178
|
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
|
|
5385
6179
|
console.log("");
|
|
5386
|
-
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
6180
|
+
console.log(` ${chalk.gray("Email:")} ${matched.email}`);
|
|
5387
6181
|
console.log(` ${chalk.gray("Password:")} ${finalPass}`);
|
|
5388
6182
|
console.log("");
|
|
5389
6183
|
return;
|
|
@@ -5413,7 +6207,7 @@ import * as dotenv from "dotenv";
|
|
|
5413
6207
|
import path from "path";
|
|
5414
6208
|
import fs from "fs";
|
|
5415
6209
|
|
|
5416
|
-
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
|
|
6210
|
+
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });
|
|
5417
6211
|
|
|
5418
6212
|
const email = process.env.REBASE_RESET_EMAIL!;
|
|
5419
6213
|
const newPassword = process.env.REBASE_RESET_PASSWORD!;
|
|
@@ -5451,10 +6245,12 @@ async function resetPassword() {
|
|
|
5451
6245
|
if (result.length > 0) {
|
|
5452
6246
|
console.log("✅ Password reset for: " + result[0].email);
|
|
5453
6247
|
${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
5454
|
-
|
|
5455
|
-
console.log("✗ User not found: " + email);
|
|
6248
|
+
process.exit(0);
|
|
5456
6249
|
}
|
|
5457
|
-
|
|
6250
|
+
// Nothing was updated, so nothing was reset. Exiting 0 here reported
|
|
6251
|
+
// success for a no-op, which is what a script would have believed.
|
|
6252
|
+
console.error("✗ User not found: " + email);
|
|
6253
|
+
process.exit(1);
|
|
5458
6254
|
}
|
|
5459
6255
|
|
|
5460
6256
|
resetPassword().catch(console.error);
|
|
@@ -5472,11 +6268,20 @@ resetPassword().catch(console.error);
|
|
|
5472
6268
|
stdio: "inherit",
|
|
5473
6269
|
env
|
|
5474
6270
|
});
|
|
6271
|
+
const cleanup = () => {
|
|
6272
|
+
try {
|
|
6273
|
+
fs.unlinkSync(tmpScriptPath);
|
|
6274
|
+
} catch {}
|
|
6275
|
+
};
|
|
5475
6276
|
return new Promise((resolve) => {
|
|
6277
|
+
child.on("error", (err) => {
|
|
6278
|
+
cleanup();
|
|
6279
|
+
console.error(chalk.red("✗ Could not run the reset script."));
|
|
6280
|
+
console.error(chalk.gray(` ${err.message}`));
|
|
6281
|
+
process.exit(1);
|
|
6282
|
+
});
|
|
5476
6283
|
child.on("close", (code) => {
|
|
5477
|
-
|
|
5478
|
-
fs.unlinkSync(tmpScriptPath);
|
|
5479
|
-
} catch {}
|
|
6284
|
+
cleanup();
|
|
5480
6285
|
if (code !== 0) process.exit(code ?? 1);
|
|
5481
6286
|
resolve();
|
|
5482
6287
|
});
|
|
@@ -5514,7 +6319,34 @@ ${chalk.green.bold("Examples")}
|
|
|
5514
6319
|
* Detects three-way schema drift between collection definitions,
|
|
5515
6320
|
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
5516
6321
|
*/
|
|
6322
|
+
/**
|
|
6323
|
+
* `--help` is answered before the project guard, not after.
|
|
6324
|
+
*
|
|
6325
|
+
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
6326
|
+
* body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
|
|
6327
|
+
* project answered "✗ Could not find a Rebase project root." Asking a command
|
|
6328
|
+
* what it does is the one question that cannot require being somewhere
|
|
6329
|
+
* particular to ask.
|
|
6330
|
+
*/
|
|
6331
|
+
function printDoctorHelp() {
|
|
6332
|
+
console.log(`
|
|
6333
|
+
${chalk.bold("rebase doctor")} — Detect drift between collections, schema and database
|
|
6334
|
+
|
|
6335
|
+
${chalk.green.bold("Usage")}
|
|
6336
|
+
rebase doctor
|
|
6337
|
+
|
|
6338
|
+
Compares the collections you declare, the generated Drizzle schema, and the
|
|
6339
|
+
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
6340
|
+
|
|
6341
|
+
Run from inside a Rebase project — it reads the project's collections and
|
|
6342
|
+
connects to its database.
|
|
6343
|
+
`);
|
|
6344
|
+
}
|
|
5517
6345
|
async function doctorCommand(rawArgs) {
|
|
6346
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6347
|
+
printDoctorHelp();
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
5518
6350
|
const projectRoot = requireProjectRoot();
|
|
5519
6351
|
const backendDir = requireBackendDir(projectRoot);
|
|
5520
6352
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
@@ -5555,12 +6387,21 @@ async function doctorCommand(rawArgs) {
|
|
|
5555
6387
|
//#endregion
|
|
5556
6388
|
//#region src/commands/skills.ts
|
|
5557
6389
|
var require = createRequire(import.meta.url);
|
|
5558
|
-
/**
|
|
6390
|
+
/**
|
|
6391
|
+
* Supported agent environments and their target directories.
|
|
6392
|
+
*
|
|
6393
|
+
* `flatLayout` says where the installed rule file sits relative to the skill's
|
|
6394
|
+
* own assets. A subdirectory layout writes `<skill>/SKILL.md`, so a link the
|
|
6395
|
+
* skill spells `references/x.md` resolves as written; a flat layout writes
|
|
6396
|
+
* `<skill>.md` one level up, so those links have to be re-pointed at the
|
|
6397
|
+
* per-skill asset directory. See `rewriteAssetLinks`.
|
|
6398
|
+
*/
|
|
5559
6399
|
var AGENTS = {
|
|
5560
6400
|
cursor: {
|
|
5561
6401
|
label: "Cursor",
|
|
5562
6402
|
detectDir: ".cursor",
|
|
5563
6403
|
targetDir: ".cursor/rules",
|
|
6404
|
+
flatLayout: true,
|
|
5564
6405
|
/** Cursor uses .mdc files (Markdown with Context). */
|
|
5565
6406
|
transformFile: (skillName, content) => ({
|
|
5566
6407
|
fileName: `${skillName}.mdc`,
|
|
@@ -5571,6 +6412,7 @@ var AGENTS = {
|
|
|
5571
6412
|
label: "Claude Code",
|
|
5572
6413
|
detectDir: ".claude",
|
|
5573
6414
|
targetDir: ".claude/skills",
|
|
6415
|
+
flatLayout: false,
|
|
5574
6416
|
/** Claude Code uses the standard SKILL.md format in subdirectories. */
|
|
5575
6417
|
transformFile: (skillName, content) => ({
|
|
5576
6418
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5581,6 +6423,7 @@ var AGENTS = {
|
|
|
5581
6423
|
label: "Windsurf",
|
|
5582
6424
|
detectDir: ".windsurf",
|
|
5583
6425
|
targetDir: ".windsurf/rules",
|
|
6426
|
+
flatLayout: true,
|
|
5584
6427
|
/** Windsurf uses plain .md files. */
|
|
5585
6428
|
transformFile: (skillName, content) => ({
|
|
5586
6429
|
fileName: `${skillName}.md`,
|
|
@@ -5591,6 +6434,7 @@ var AGENTS = {
|
|
|
5591
6434
|
label: "Gemini CLI / Antigravity",
|
|
5592
6435
|
detectDir: ".agents",
|
|
5593
6436
|
targetDir: ".agents/skills",
|
|
6437
|
+
flatLayout: false,
|
|
5594
6438
|
/** Gemini uses the standard SKILL.md format in subdirectories. */
|
|
5595
6439
|
transformFile: (skillName, content) => ({
|
|
5596
6440
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5609,21 +6453,65 @@ function getSkillsSourceDir() {
|
|
|
5609
6453
|
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
5610
6454
|
return skillsDir;
|
|
5611
6455
|
}
|
|
5612
|
-
/**
|
|
6456
|
+
/**
|
|
6457
|
+
* Everything a skill ships alongside its SKILL.md — the `references/` tree the
|
|
6458
|
+
* Agent Skills format uses for progressive disclosure.
|
|
6459
|
+
*
|
|
6460
|
+
* These used to be dropped on install, because the installer read exactly
|
|
6461
|
+
* `<skill>/SKILL.md` and nothing else. That left `rebase-design-language`
|
|
6462
|
+
* telling the agent three separate times to read `references/view-patterns.md`
|
|
6463
|
+
* — 379 lines of view skeletons — in a project where the file had never
|
|
6464
|
+
* landed, and the instruction it carries is "extend an existing pattern; do not
|
|
6465
|
+
* invent a layout".
|
|
6466
|
+
*/
|
|
6467
|
+
function loadSkillAssets(skillDir) {
|
|
6468
|
+
const found = [];
|
|
6469
|
+
const walk = (dir, prefix) => {
|
|
6470
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6471
|
+
if (entry.name.startsWith(".")) continue;
|
|
6472
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
6473
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
|
|
6474
|
+
else if (rel !== "SKILL.md") found.push(rel);
|
|
6475
|
+
}
|
|
6476
|
+
};
|
|
6477
|
+
walk(skillDir, "");
|
|
6478
|
+
return found.sort();
|
|
6479
|
+
}
|
|
6480
|
+
/** Read all skill directories and return their names, content and assets. */
|
|
5613
6481
|
function loadSkills(skillsDir) {
|
|
5614
6482
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
5615
6483
|
const skills = [];
|
|
5616
6484
|
for (const entry of entries) {
|
|
5617
6485
|
if (!entry.isDirectory()) continue;
|
|
5618
|
-
const
|
|
6486
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
6487
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
5619
6488
|
if (!fs.existsSync(skillMdPath)) continue;
|
|
5620
6489
|
skills.push({
|
|
5621
6490
|
name: entry.name,
|
|
5622
|
-
|
|
6491
|
+
dir: skillDir,
|
|
6492
|
+
content: fs.readFileSync(skillMdPath, "utf-8"),
|
|
6493
|
+
assets: loadSkillAssets(skillDir)
|
|
5623
6494
|
});
|
|
5624
6495
|
}
|
|
5625
6496
|
return skills;
|
|
5626
6497
|
}
|
|
6498
|
+
/**
|
|
6499
|
+
* Re-point a skill's own asset links at the per-skill subdirectory, for the
|
|
6500
|
+
* agents whose rule file does not live in it.
|
|
6501
|
+
*
|
|
6502
|
+
* Only paths that name a file the skill actually ships are rewritten, and only
|
|
6503
|
+
* where they start a path segment — so prose that happens to contain the same
|
|
6504
|
+
* words is left alone.
|
|
6505
|
+
*/
|
|
6506
|
+
function rewriteAssetLinks(content, assets, skillName) {
|
|
6507
|
+
let out = content;
|
|
6508
|
+
for (const asset of assets) {
|
|
6509
|
+
const posix = asset.split(path.sep).join("/");
|
|
6510
|
+
const escaped = posix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6511
|
+
out = out.replace(new RegExp(`(?<![\\w/.-])${escaped}`, "g"), `${skillName}/${posix}`);
|
|
6512
|
+
}
|
|
6513
|
+
return out;
|
|
6514
|
+
}
|
|
5627
6515
|
/** Detect which agent environments already exist in the project. */
|
|
5628
6516
|
function detectAgents(projectDir) {
|
|
5629
6517
|
const detected = [];
|
|
@@ -5636,16 +6524,31 @@ function installForAgent(agentKey, skills, projectDir) {
|
|
|
5636
6524
|
const targetBase = path.join(projectDir, agent.targetDir);
|
|
5637
6525
|
fs.mkdirSync(targetBase, { recursive: true });
|
|
5638
6526
|
let count = 0;
|
|
6527
|
+
let assetCount = 0;
|
|
5639
6528
|
for (const skill of skills) {
|
|
5640
|
-
const
|
|
6529
|
+
const body = agent.flatLayout ? rewriteAssetLinks(skill.content, skill.assets, skill.name) : skill.content;
|
|
6530
|
+
const { fileName, content } = agent.transformFile(skill.name, body);
|
|
5641
6531
|
const targetPath = path.join(targetBase, fileName);
|
|
5642
6532
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
5643
6533
|
fs.writeFileSync(targetPath, content, "utf-8");
|
|
5644
6534
|
count++;
|
|
6535
|
+
for (const asset of skill.assets) {
|
|
6536
|
+
const assetTarget = path.join(targetBase, skill.name, asset);
|
|
6537
|
+
fs.mkdirSync(path.dirname(assetTarget), { recursive: true });
|
|
6538
|
+
fs.copyFileSync(path.join(skill.dir, asset), assetTarget);
|
|
6539
|
+
assetCount++;
|
|
6540
|
+
}
|
|
5645
6541
|
}
|
|
5646
|
-
return
|
|
6542
|
+
return {
|
|
6543
|
+
skills: count,
|
|
6544
|
+
assets: assetCount
|
|
6545
|
+
};
|
|
5647
6546
|
}
|
|
5648
6547
|
async function skillsCommand(subcommand, rawArgs) {
|
|
6548
|
+
if (wantsHelp(rawArgs)) {
|
|
6549
|
+
printSkillsHelp();
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
5649
6552
|
switch (subcommand) {
|
|
5650
6553
|
case "install":
|
|
5651
6554
|
await skillsInstall(rawArgs);
|
|
@@ -5663,7 +6566,8 @@ async function skillsCommand(subcommand, rawArgs) {
|
|
|
5663
6566
|
}
|
|
5664
6567
|
/**
|
|
5665
6568
|
* Agents named explicitly on the command line, e.g. `--agent claude --agent cursor`
|
|
5666
|
-
* (also accepts a comma-separated list). Returns null when none were
|
|
6569
|
+
* (also accepts a comma-separated list, and `all`). Returns null when none were
|
|
6570
|
+
* given.
|
|
5667
6571
|
*/
|
|
5668
6572
|
function parseAgentFlags(rawArgs) {
|
|
5669
6573
|
const requested = [];
|
|
@@ -5673,6 +6577,7 @@ function parseAgentFlags(rawArgs) {
|
|
|
5673
6577
|
if (value && !value.startsWith("-")) requested.push(...value.split(",").map((v) => v.trim()).filter(Boolean));
|
|
5674
6578
|
}
|
|
5675
6579
|
if (requested.length === 0) return null;
|
|
6580
|
+
if (requested.includes("all")) return Object.keys(AGENTS);
|
|
5676
6581
|
const valid = Object.keys(AGENTS);
|
|
5677
6582
|
const unknown = requested.filter((a) => !valid.includes(a));
|
|
5678
6583
|
if (unknown.length > 0) {
|
|
@@ -5700,6 +6605,7 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5700
6605
|
if (!process.stdin.isTTY) {
|
|
5701
6606
|
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
5702
6607
|
console.error(chalk.yellow(` Name the agents explicitly, e.g. rebase skills install --agent ${Object.keys(AGENTS)[0]}`));
|
|
6608
|
+
console.error(chalk.yellow(" Or install for every supported agent: rebase skills install --agent all"));
|
|
5703
6609
|
console.error(chalk.gray(` Available: ${Object.keys(AGENTS).join(", ")}`));
|
|
5704
6610
|
process.exit(1);
|
|
5705
6611
|
}
|
|
@@ -5725,9 +6631,10 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5725
6631
|
console.log("");
|
|
5726
6632
|
for (const agentKey of agents) {
|
|
5727
6633
|
const agent = AGENTS[agentKey];
|
|
5728
|
-
const count = installForAgent(agentKey, skills, projectDir);
|
|
6634
|
+
const { skills: count, assets } = installForAgent(agentKey, skills, projectDir);
|
|
5729
6635
|
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
5730
|
-
|
|
6636
|
+
const withAssets = assets > 0 ? ` (+ ${assets} reference file${assets === 1 ? "" : "s"})` : "";
|
|
6637
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed${withAssets} to ${chalk.gray(shown)}`);
|
|
5731
6638
|
}
|
|
5732
6639
|
console.log("");
|
|
5733
6640
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -5747,13 +6654,16 @@ ${chalk.green.bold("Subcommands")}
|
|
|
5747
6654
|
|
|
5748
6655
|
${chalk.green.bold("Options")}
|
|
5749
6656
|
${chalk.blue("--agent, -a")} Agent(s) to install for, skipping detection and the prompt.
|
|
5750
|
-
Repeat the flag or pass a comma-separated list.
|
|
5751
|
-
|
|
6657
|
+
Repeat the flag or pass a comma-separated list, or ${chalk.bold("all")}.
|
|
6658
|
+
Required without a TTY: a scaffolded project carries a marker
|
|
6659
|
+
file for every agent, so detection cannot pick one for you.
|
|
6660
|
+
Available: ${Object.keys(AGENTS).join(", ")}, all
|
|
5752
6661
|
|
|
5753
6662
|
${chalk.green.bold("Examples")}
|
|
5754
6663
|
${chalk.cyan("rebase skills install")}
|
|
5755
6664
|
${chalk.cyan("rebase skills install --agent claude")}
|
|
5756
6665
|
${chalk.cyan("rebase skills install --agent claude,cursor")}
|
|
6666
|
+
${chalk.cyan("rebase skills install --agent all")} ${chalk.gray("# scripted / CI")}
|
|
5757
6667
|
`);
|
|
5758
6668
|
}
|
|
5759
6669
|
//#endregion
|
|
@@ -5766,25 +6676,13 @@ ${chalk.green.bold("Examples")}
|
|
|
5766
6676
|
* create — Create a new API key
|
|
5767
6677
|
* revoke — Revoke an existing API key
|
|
5768
6678
|
*/
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5777
|
-
const idx = trimmed.indexOf("=");
|
|
5778
|
-
if (idx > 0) {
|
|
5779
|
-
const key = trimmed.slice(0, idx).trim();
|
|
5780
|
-
let value = trimmed.slice(idx + 1).trim();
|
|
5781
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
5782
|
-
env[key] = value;
|
|
5783
|
-
}
|
|
5784
|
-
}
|
|
5785
|
-
}
|
|
5786
|
-
return env;
|
|
5787
|
-
}
|
|
6679
|
+
/**
|
|
6680
|
+
* Was a hand-rolled `indexOf("=")` loop. It keyed `export KEY=value` as
|
|
6681
|
+
* `export KEY` and carried a trailing `# comment` into the value — so a key
|
|
6682
|
+
* that was present read as absent, or reached an `Authorization` header with a
|
|
6683
|
+
* comment attached and came back 401. See `readEnvFile`.
|
|
6684
|
+
*/
|
|
6685
|
+
var loadEnv = readEnvFile;
|
|
5788
6686
|
function resolveBaseUrl(env, projectRoot) {
|
|
5789
6687
|
if (env.REBASE_BASE_URL) return env.REBASE_BASE_URL;
|
|
5790
6688
|
if (projectRoot) try {
|
|
@@ -5797,7 +6695,7 @@ function resolveBaseUrl(env, projectRoot) {
|
|
|
5797
6695
|
return `http://localhost:${env.PORT || env.REBASE_PORT || "3001"}`;
|
|
5798
6696
|
}
|
|
5799
6697
|
async function apiKeysCommand(subcommand, rawArgs) {
|
|
5800
|
-
if (!subcommand || subcommand === "--help") {
|
|
6698
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5801
6699
|
printApiKeysHelp();
|
|
5802
6700
|
return;
|
|
5803
6701
|
}
|
|
@@ -5859,20 +6757,43 @@ async function listKeys(_rawArgs) {
|
|
|
5859
6757
|
process.exit(1);
|
|
5860
6758
|
}
|
|
5861
6759
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6760
|
+
/** The flags `rebase api-keys create` takes. */
|
|
6761
|
+
var CREATE_KEY_FLAGS = {
|
|
6762
|
+
"--name": String,
|
|
6763
|
+
"--permissions": String,
|
|
6764
|
+
"--full-access": Boolean,
|
|
6765
|
+
"--admin": Boolean,
|
|
6766
|
+
"--rate-limit": Number,
|
|
6767
|
+
"--expires": String,
|
|
6768
|
+
"-n": "--name"
|
|
6769
|
+
};
|
|
6770
|
+
/**
|
|
6771
|
+
* What this invocation asks to be created.
|
|
6772
|
+
*
|
|
6773
|
+
* The name may be given either way — `--name "My Key"` or as the single
|
|
6774
|
+
* positional — and under the old permissive parse an undeclared flag became
|
|
6775
|
+
* that positional: `rebase api-keys create --debug --full-access` created a
|
|
6776
|
+
* key called `--debug` with read/write/delete on every collection, and
|
|
6777
|
+
* `--debug` is what the CLI prints after every failure as the thing to re-run
|
|
6778
|
+
* with. Strict parsing makes the flag an error instead of a name.
|
|
6779
|
+
*
|
|
6780
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6781
|
+
*/
|
|
6782
|
+
function resolveCreateKeyArgs(rawArgs) {
|
|
6783
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6784
|
+
spec: CREATE_KEY_FLAGS,
|
|
6785
|
+
rawArgs,
|
|
6786
|
+
commandWords: 2,
|
|
6787
|
+
command: "api-keys create",
|
|
6788
|
+
maxPositionals: 1
|
|
5874
6789
|
});
|
|
5875
|
-
|
|
6790
|
+
return {
|
|
6791
|
+
flags,
|
|
6792
|
+
name: flags["--name"] || positionals[0]
|
|
6793
|
+
};
|
|
6794
|
+
}
|
|
6795
|
+
async function createKey(rawArgs) {
|
|
6796
|
+
const { flags: args, name } = resolveCreateKeyArgs(rawArgs);
|
|
5876
6797
|
const permissionsRaw = args["--permissions"];
|
|
5877
6798
|
if (!name) {
|
|
5878
6799
|
console.error(chalk.red("✗ Name is required."));
|
|
@@ -5973,12 +6894,30 @@ async function createKey(rawArgs) {
|
|
|
5973
6894
|
process.exit(1);
|
|
5974
6895
|
}
|
|
5975
6896
|
}
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
6897
|
+
/** The flags `rebase api-keys revoke` takes. */
|
|
6898
|
+
var REVOKE_KEY_FLAGS = { "--id": String };
|
|
6899
|
+
/**
|
|
6900
|
+
* Which key this invocation names.
|
|
6901
|
+
*
|
|
6902
|
+
* The id is a positional, so the permissive parse handed one straight to the
|
|
6903
|
+
* DELETE: `rebase api-keys revoke --foo` sent
|
|
6904
|
+
* `DELETE /api/admin/api-keys/--foo`, and `rebase --debug api-keys revoke <id>`
|
|
6905
|
+
* shifted the words along and revoked the key named `revoke`.
|
|
6906
|
+
*
|
|
6907
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6908
|
+
*/
|
|
6909
|
+
function resolveRevokeKeyArgs(rawArgs) {
|
|
6910
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6911
|
+
spec: REVOKE_KEY_FLAGS,
|
|
6912
|
+
rawArgs,
|
|
6913
|
+
commandWords: 2,
|
|
6914
|
+
command: "api-keys revoke",
|
|
6915
|
+
maxPositionals: 1
|
|
5980
6916
|
});
|
|
5981
|
-
|
|
6917
|
+
return { id: flags["--id"] || positionals[0] };
|
|
6918
|
+
}
|
|
6919
|
+
async function revokeKey(rawArgs) {
|
|
6920
|
+
const { id } = resolveRevokeKeyArgs(rawArgs);
|
|
5982
6921
|
if (!id) {
|
|
5983
6922
|
console.error(chalk.red("✗ Key ID is required."));
|
|
5984
6923
|
console.log("");
|
|
@@ -6055,7 +6994,12 @@ ${chalk.green.bold("Examples")}
|
|
|
6055
6994
|
* a documentation comment that quietly fell out of date two releases ago.
|
|
6056
6995
|
*/
|
|
6057
6996
|
async function telemetryCommand(rawArgs) {
|
|
6058
|
-
|
|
6997
|
+
const subcommand = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0];
|
|
6998
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6999
|
+
printHelp$2();
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
switch (subcommand) {
|
|
6059
7003
|
case "status":
|
|
6060
7004
|
case void 0:
|
|
6061
7005
|
printStatus();
|
|
@@ -6154,16 +7098,16 @@ async function loginCommand(rawArgs) {
|
|
|
6154
7098
|
const args = arg({
|
|
6155
7099
|
"--email": String,
|
|
6156
7100
|
"--password": String,
|
|
6157
|
-
"-e": "--email"
|
|
6158
|
-
"-p": "--password"
|
|
7101
|
+
"-e": "--email"
|
|
6159
7102
|
}, {
|
|
6160
7103
|
argv: rawArgs.slice(3),
|
|
6161
7104
|
permissive: true
|
|
6162
7105
|
});
|
|
6163
7106
|
const url = resolveCloudUrl(rawArgs);
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
7107
|
+
noteBlank();
|
|
7108
|
+
note(`Signing in to ${chalk.cyan(url)}`);
|
|
7109
|
+
noteBlank();
|
|
7110
|
+
if (!args["--email"] || !args["--password"]) requireInteractive("credentials", "--email and --password");
|
|
6167
7111
|
const prompts = [];
|
|
6168
7112
|
if (!args["--email"]) prompts.push({
|
|
6169
7113
|
type: "input",
|
|
@@ -6189,14 +7133,21 @@ async function loginCommand(rawArgs) {
|
|
|
6189
7133
|
if (orgs.data.length === 1 && !getContextOrg(url)) setContextOrg(url, String(orgs.data[0].id));
|
|
6190
7134
|
} catch {}
|
|
6191
7135
|
success(`Logged in as ${chalk.bold(user.email ?? email)}`);
|
|
6192
|
-
|
|
6193
|
-
[
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
7136
|
+
emit(() => {
|
|
7137
|
+
keyValues([
|
|
7138
|
+
["Host", url],
|
|
7139
|
+
["User", user.email ?? void 0],
|
|
7140
|
+
["Active org", getContextOrg(url)]
|
|
7141
|
+
]);
|
|
7142
|
+
console.log("");
|
|
7143
|
+
}, {
|
|
7144
|
+
success: true,
|
|
7145
|
+
host: url,
|
|
7146
|
+
user: user.email ?? null,
|
|
7147
|
+
activeOrg: getContextOrg(url) ?? null
|
|
7148
|
+
});
|
|
6198
7149
|
} catch (e) {
|
|
6199
|
-
if (e?.status === 401) fail("Invalid email or password.");
|
|
7150
|
+
if (e?.status === 401) fail("Invalid email or password.", void 0, "invalid_credentials");
|
|
6200
7151
|
reportError(e, "Login failed");
|
|
6201
7152
|
}
|
|
6202
7153
|
}
|
|
@@ -6204,34 +7155,60 @@ async function logoutCommand(rawArgs) {
|
|
|
6204
7155
|
const url = resolveCloudUrl(rawArgs);
|
|
6205
7156
|
const client = createCloudClient(url);
|
|
6206
7157
|
if (!client.auth.getSession()) {
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
7158
|
+
emit(() => {
|
|
7159
|
+
console.log("");
|
|
7160
|
+
console.log(chalk.gray(` Not logged in to ${url}.`));
|
|
7161
|
+
console.log("");
|
|
7162
|
+
}, {
|
|
7163
|
+
success: true,
|
|
7164
|
+
host: url,
|
|
7165
|
+
wasLoggedIn: false
|
|
7166
|
+
});
|
|
6210
7167
|
return;
|
|
6211
7168
|
}
|
|
6212
7169
|
try {
|
|
6213
7170
|
await client.auth.signOut();
|
|
6214
7171
|
} catch {}
|
|
6215
7172
|
success(`Logged out of ${url}`);
|
|
7173
|
+
emit(() => {}, {
|
|
7174
|
+
success: true,
|
|
7175
|
+
host: url,
|
|
7176
|
+
wasLoggedIn: true
|
|
7177
|
+
});
|
|
6216
7178
|
}
|
|
6217
7179
|
async function whoamiCommand(rawArgs) {
|
|
6218
7180
|
const { client, url } = await requireClient(rawArgs);
|
|
6219
7181
|
try {
|
|
6220
7182
|
const user = await client.auth.getUser();
|
|
6221
|
-
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
|
|
7183
|
+
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
|
|
6222
7184
|
const link = readLink();
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
[
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
7185
|
+
emit(() => {
|
|
7186
|
+
console.log("");
|
|
7187
|
+
console.log(chalk.bold(" 🔐 Rebase Cloud session"));
|
|
7188
|
+
console.log("");
|
|
7189
|
+
keyValues([
|
|
7190
|
+
["Host", url],
|
|
7191
|
+
["User", user.email ?? void 0],
|
|
7192
|
+
["User ID", user.uid],
|
|
7193
|
+
["Roles", user.roles?.length ? user.roles.join(", ") : void 0],
|
|
7194
|
+
["Active org", getContextOrg(url)],
|
|
7195
|
+
["Linked project", link ? `${link.projectName ?? ""} (${link.projectId})`.trim() : void 0]
|
|
7196
|
+
]);
|
|
7197
|
+
console.log("");
|
|
7198
|
+
}, {
|
|
7199
|
+
host: url,
|
|
7200
|
+
user: {
|
|
7201
|
+
email: user.email ?? null,
|
|
7202
|
+
uid: user.uid,
|
|
7203
|
+
roles: user.roles ?? []
|
|
7204
|
+
},
|
|
7205
|
+
activeOrg: getContextOrg(url) ?? null,
|
|
7206
|
+
linkedProject: link ? {
|
|
7207
|
+
id: link.projectId,
|
|
7208
|
+
name: link.projectName ?? null,
|
|
7209
|
+
slug: link.slug ?? null
|
|
7210
|
+
} : null
|
|
7211
|
+
});
|
|
6235
7212
|
} catch (e) {
|
|
6236
7213
|
reportError(e, "Failed to fetch session");
|
|
6237
7214
|
}
|
|
@@ -6261,10 +7238,10 @@ async function linkDirect(target, rawArgs) {
|
|
|
6261
7238
|
try {
|
|
6262
7239
|
base = new URL(target);
|
|
6263
7240
|
} catch {
|
|
6264
|
-
fail(`"${target}" is not a valid URL
|
|
7241
|
+
fail(`"${target}" is not a valid URL.`, void 0, "invalid_url");
|
|
6265
7242
|
return;
|
|
6266
7243
|
}
|
|
6267
|
-
if (base.protocol !== "http:" && base.protocol !== "https:") fail("A project URL must be http or https.");
|
|
7244
|
+
if (base.protocol !== "http:" && base.protocol !== "https:") fail("A project URL must be http or https.", void 0, "invalid_url");
|
|
6268
7245
|
const apiUrl = base.toString().replace(/\/+$/, "");
|
|
6269
7246
|
const probe = `${apiUrl}/api/meta/schema-version`;
|
|
6270
7247
|
let reachable = false;
|
|
@@ -6276,11 +7253,7 @@ async function linkDirect(target, rawArgs) {
|
|
|
6276
7253
|
} catch (err) {
|
|
6277
7254
|
detail = err instanceof Error ? err.message : String(err);
|
|
6278
7255
|
}
|
|
6279
|
-
if (!reachable) {
|
|
6280
|
-
console.log(chalk.yellow(`⚠ Could not reach ${probe}${detail ? ` (${detail})` : ""}.`));
|
|
6281
|
-
console.log(chalk.dim(" Linking anyway — the server may not be running yet."));
|
|
6282
|
-
console.log(chalk.dim(" It must be a Rebase backend of version 0.11 or newer."));
|
|
6283
|
-
}
|
|
7256
|
+
if (!reachable) warn(`Could not reach ${probe}${detail ? ` (${detail})` : ""}.`, "Linking anyway — the server may not be running yet. It must be a Rebase backend of version 0.11 or newer.");
|
|
6284
7257
|
writeLink({
|
|
6285
7258
|
url: apiUrl,
|
|
6286
7259
|
projectId: "",
|
|
@@ -6289,9 +7262,18 @@ async function linkDirect(target, rawArgs) {
|
|
|
6289
7262
|
projectName: base.host
|
|
6290
7263
|
});
|
|
6291
7264
|
success(`Linked to ${apiUrl}`);
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
7265
|
+
emit(() => {
|
|
7266
|
+
note(chalk.dim(`Written to ${projectLinkPath()}`));
|
|
7267
|
+
noteBlank();
|
|
7268
|
+
note(`Next: ${chalk.cyan("rebase generate-sdk --from link")}`, "");
|
|
7269
|
+
}, {
|
|
7270
|
+
success: true,
|
|
7271
|
+
mode: "direct",
|
|
7272
|
+
apiUrl,
|
|
7273
|
+
reachable,
|
|
7274
|
+
projectName: base.host,
|
|
7275
|
+
linkPath: projectLinkPath()
|
|
7276
|
+
});
|
|
6295
7277
|
}
|
|
6296
7278
|
async function linkCommand(rawArgs) {
|
|
6297
7279
|
const args = arg({
|
|
@@ -6312,14 +7294,15 @@ async function linkCommand(rawArgs) {
|
|
|
6312
7294
|
if (args["--project"]) {
|
|
6313
7295
|
const projectId = await resolveProjectRef(args["--project"], client);
|
|
6314
7296
|
project = await client.data.collection("projects").findById(projectId);
|
|
6315
|
-
if (!project) fail(`Project ${args["--project"]} not found
|
|
7297
|
+
if (!project) fail(`Project ${args["--project"]} not found.`, void 0, "project_not_found");
|
|
6316
7298
|
} else {
|
|
7299
|
+
requireInteractive("a project to link", "--project <slug>");
|
|
6317
7300
|
const org = getContextOrg(url);
|
|
6318
7301
|
const projects = (await client.data.collection("projects").find({
|
|
6319
7302
|
where: org ? { organization: ["==", org] } : void 0,
|
|
6320
7303
|
limit: 100
|
|
6321
7304
|
})).data;
|
|
6322
|
-
if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}
|
|
7305
|
+
if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`, "no_projects");
|
|
6323
7306
|
const { picked } = await inquirer.prompt([{
|
|
6324
7307
|
type: "select",
|
|
6325
7308
|
name: "picked",
|
|
@@ -6331,39 +7314,63 @@ async function linkCommand(rawArgs) {
|
|
|
6331
7314
|
}]);
|
|
6332
7315
|
project = picked;
|
|
6333
7316
|
}
|
|
6334
|
-
if (!project) fail("No project selected.");
|
|
7317
|
+
if (!project) fail("No project selected.", void 0, "no_project");
|
|
7318
|
+
const orgId = project.organization !== void 0 ? String(project.organization) : void 0;
|
|
6335
7319
|
writeLink({
|
|
6336
7320
|
url,
|
|
6337
7321
|
projectId: String(project.id),
|
|
6338
7322
|
slug: project.subdomain,
|
|
6339
7323
|
projectName: project.name,
|
|
6340
|
-
orgId
|
|
7324
|
+
orgId
|
|
6341
7325
|
});
|
|
6342
7326
|
success(`Linked to ${chalk.bold(project.name ?? project.subdomain ?? "")}`);
|
|
6343
|
-
|
|
6344
|
-
|
|
7327
|
+
emit(() => {
|
|
7328
|
+
note(chalk.gray(`Wrote ${projectLinkPath()}`));
|
|
7329
|
+
noteBlank();
|
|
7330
|
+
}, {
|
|
7331
|
+
success: true,
|
|
7332
|
+
mode: "cloud",
|
|
7333
|
+
host: url,
|
|
7334
|
+
projectId: String(project.id),
|
|
7335
|
+
slug: project.subdomain ?? null,
|
|
7336
|
+
projectName: project.name ?? null,
|
|
7337
|
+
org: orgId ?? null,
|
|
7338
|
+
linkPath: projectLinkPath()
|
|
7339
|
+
});
|
|
6345
7340
|
} catch (e) {
|
|
6346
7341
|
reportError(e, "Failed to link project");
|
|
6347
7342
|
}
|
|
6348
7343
|
}
|
|
6349
7344
|
function unlinkCommand() {
|
|
6350
7345
|
if (!readLink()) {
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
7346
|
+
emit(() => {
|
|
7347
|
+
console.log("");
|
|
7348
|
+
console.log(chalk.gray(" This directory is not linked to a cloud project."));
|
|
7349
|
+
console.log("");
|
|
7350
|
+
}, {
|
|
7351
|
+
success: true,
|
|
7352
|
+
unlinked: false,
|
|
7353
|
+
linkPath: projectLinkPath()
|
|
7354
|
+
});
|
|
6354
7355
|
return;
|
|
6355
7356
|
}
|
|
6356
7357
|
removeLink();
|
|
6357
7358
|
success("Unlinked from cloud project");
|
|
7359
|
+
emit(() => {}, {
|
|
7360
|
+
success: true,
|
|
7361
|
+
unlinked: true,
|
|
7362
|
+
linkPath: projectLinkPath()
|
|
7363
|
+
});
|
|
6358
7364
|
}
|
|
6359
7365
|
async function selectOrgCommand(rawArgs) {
|
|
6360
7366
|
const target = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
6361
7367
|
const { client, url } = await requireClient(rawArgs);
|
|
6362
7368
|
try {
|
|
6363
7369
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
6364
|
-
if (orgs.length === 0) fail("You are not a member of any organization.");
|
|
7370
|
+
if (orgs.length === 0) fail("You are not a member of any organization.", void 0, "no_orgs");
|
|
6365
7371
|
let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
|
|
6366
7372
|
if (!chosen && !target) {
|
|
7373
|
+
requireInteractive("an organization", "rebase cloud use <org-id|slug>");
|
|
6367
7374
|
const { picked } = await inquirer.prompt([{
|
|
6368
7375
|
type: "select",
|
|
6369
7376
|
name: "picked",
|
|
@@ -6375,9 +7382,18 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6375
7382
|
}]);
|
|
6376
7383
|
chosen = picked;
|
|
6377
7384
|
}
|
|
6378
|
-
if (!chosen) fail(`Organization "${target}" not found
|
|
7385
|
+
if (!chosen) fail(`Organization "${target}" not found.`, void 0, "org_not_found");
|
|
6379
7386
|
setContextOrg(url, String(chosen.id));
|
|
6380
7387
|
success(`Active organization set to ${chalk.bold(chosen.name ?? chosen.id)}`);
|
|
7388
|
+
emit(() => {}, {
|
|
7389
|
+
success: true,
|
|
7390
|
+
host: url,
|
|
7391
|
+
org: {
|
|
7392
|
+
id: String(chosen.id),
|
|
7393
|
+
name: chosen.name ?? null,
|
|
7394
|
+
slug: chosen.slug ?? null
|
|
7395
|
+
}
|
|
7396
|
+
});
|
|
6381
7397
|
} catch (e) {
|
|
6382
7398
|
reportError(e, "Failed to set organization");
|
|
6383
7399
|
}
|
|
@@ -6386,7 +7402,12 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6386
7402
|
function openCommand(rawArgs) {
|
|
6387
7403
|
const url = resolveCloudUrl(rawArgs);
|
|
6388
7404
|
const link = readLink();
|
|
6389
|
-
|
|
7405
|
+
const target = link ? `${url}/projects/${link.projectId}` : url;
|
|
7406
|
+
openUrl(target);
|
|
7407
|
+
emit(() => {}, {
|
|
7408
|
+
url: target,
|
|
7409
|
+
projectId: link?.projectId ?? null
|
|
7410
|
+
});
|
|
6390
7411
|
}
|
|
6391
7412
|
//#endregion
|
|
6392
7413
|
//#region src/commands/cloud/projects.ts
|
|
@@ -6402,21 +7423,34 @@ async function listProjects(rawArgs) {
|
|
|
6402
7423
|
orderBy: ["name", "asc"],
|
|
6403
7424
|
limit: 100
|
|
6404
7425
|
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
6405
|
-
console.log("");
|
|
6406
|
-
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
6407
|
-
console.log("");
|
|
6408
|
-
if (projects.length === 0) {
|
|
6409
|
-
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
6410
|
-
console.log("");
|
|
6411
|
-
return;
|
|
6412
|
-
}
|
|
6413
7426
|
const linkedId = readLink()?.projectId;
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
console.log(
|
|
6417
|
-
console.log(
|
|
6418
|
-
|
|
6419
|
-
|
|
7427
|
+
emit(() => {
|
|
7428
|
+
console.log("");
|
|
7429
|
+
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
7430
|
+
console.log("");
|
|
7431
|
+
if (projects.length === 0) {
|
|
7432
|
+
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
7433
|
+
console.log("");
|
|
7434
|
+
return;
|
|
7435
|
+
}
|
|
7436
|
+
for (const p of projects) {
|
|
7437
|
+
const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
|
|
7438
|
+
console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
|
|
7439
|
+
console.log(` ${chalk.gray(projectHost(p, baseDomain) ?? "—")}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
|
|
7440
|
+
}
|
|
7441
|
+
console.log("");
|
|
7442
|
+
}, {
|
|
7443
|
+
org: org ?? null,
|
|
7444
|
+
projects: projects.map((p) => ({
|
|
7445
|
+
id: String(p.id),
|
|
7446
|
+
name: p.name ?? null,
|
|
7447
|
+
slug: p.subdomain ?? null,
|
|
7448
|
+
host: projectHost(p, baseDomain) ?? null,
|
|
7449
|
+
status: p.status ?? null,
|
|
7450
|
+
provider: p.provider ?? null,
|
|
7451
|
+
linked: String(p.id) === linkedId
|
|
7452
|
+
}))
|
|
7453
|
+
});
|
|
6420
7454
|
} catch (e) {
|
|
6421
7455
|
reportError(e, "Failed to list projects");
|
|
6422
7456
|
}
|
|
@@ -6483,28 +7517,33 @@ function chooseRequestedTarget(requested, targets) {
|
|
|
6483
7517
|
}
|
|
6484
7518
|
async function resolveRequestedTarget(client, url, requested) {
|
|
6485
7519
|
const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
|
|
6486
|
-
if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway
|
|
7520
|
+
if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`, "no_deploy_targets");
|
|
6487
7521
|
return chosen;
|
|
6488
7522
|
}
|
|
7523
|
+
/** The flags `rebase cloud projects create` takes. */
|
|
7524
|
+
var CREATE_PROJECT_FLAGS = {
|
|
7525
|
+
"--name": String,
|
|
7526
|
+
"--subdomain": String,
|
|
7527
|
+
"--repo": String,
|
|
7528
|
+
"--branch": String,
|
|
7529
|
+
"--provider": String,
|
|
7530
|
+
"--region": String,
|
|
7531
|
+
"--vm-size": String,
|
|
7532
|
+
"--org": String,
|
|
7533
|
+
"--link": Boolean,
|
|
7534
|
+
"-n": "--name"
|
|
7535
|
+
};
|
|
6489
7536
|
async function createProject(rawArgs) {
|
|
6490
|
-
const args =
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
"
|
|
6495
|
-
|
|
6496
|
-
"--region": String,
|
|
6497
|
-
"--vm-size": String,
|
|
6498
|
-
"--org": String,
|
|
6499
|
-
"--link": Boolean,
|
|
6500
|
-
"-n": "--name"
|
|
6501
|
-
}, {
|
|
6502
|
-
argv: rawArgs.slice(4),
|
|
6503
|
-
permissive: true
|
|
7537
|
+
const { flags: args } = parseCloudArgs({
|
|
7538
|
+
spec: CREATE_PROJECT_FLAGS,
|
|
7539
|
+
rawArgs,
|
|
7540
|
+
commandWords: 3,
|
|
7541
|
+
command: "cloud projects create",
|
|
7542
|
+
maxPositionals: 0
|
|
6504
7543
|
});
|
|
6505
7544
|
const { client, url } = await requireClient(rawArgs);
|
|
6506
7545
|
const org = args["--org"] || getContextOrg(url);
|
|
6507
|
-
if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}
|
|
7546
|
+
if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`, "no_org");
|
|
6508
7547
|
const prompts = [];
|
|
6509
7548
|
if (!args["--name"]) prompts.push({
|
|
6510
7549
|
type: "input",
|
|
@@ -6526,14 +7565,14 @@ async function createProject(rawArgs) {
|
|
|
6526
7565
|
const defaults = providerDefaults(provider);
|
|
6527
7566
|
const region = (args["--region"] || target.region || defaults.region).trim();
|
|
6528
7567
|
const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
|
|
6529
|
-
if (!name || !subdomain) fail("Name and subdomain are required.");
|
|
7568
|
+
if (!name || !subdomain) fail("Name and subdomain are required.", `Pass ${chalk.bold("--name <name>")} and ${chalk.bold("--subdomain <slug>")}.`, "input_required");
|
|
6530
7569
|
try {
|
|
6531
7570
|
const check = await client.functions.invoke("check-subdomain", { subdomain });
|
|
6532
|
-
if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}
|
|
7571
|
+
if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_unavailable");
|
|
6533
7572
|
} catch {}
|
|
6534
7573
|
try {
|
|
6535
7574
|
const user = await client.auth.getUser();
|
|
6536
|
-
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.");
|
|
7575
|
+
if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
|
|
6537
7576
|
const created = await client.data.collection("projects").create({
|
|
6538
7577
|
name,
|
|
6539
7578
|
subdomain,
|
|
@@ -6546,87 +7585,147 @@ async function createProject(rawArgs) {
|
|
|
6546
7585
|
createdById: user.uid,
|
|
6547
7586
|
status: "provisioning"
|
|
6548
7587
|
});
|
|
7588
|
+
const host = projectHost(created, await fetchTenantBaseDomain(client, url));
|
|
7589
|
+
const linked = Boolean(args["--link"]);
|
|
7590
|
+
if (linked) writeLink({
|
|
7591
|
+
url,
|
|
7592
|
+
projectId: String(created.id),
|
|
7593
|
+
slug: created.subdomain,
|
|
7594
|
+
projectName: name,
|
|
7595
|
+
orgId: String(org)
|
|
7596
|
+
});
|
|
6549
7597
|
success(`Created project ${chalk.bold(name)}`);
|
|
6550
|
-
|
|
6551
|
-
[
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
7598
|
+
emit(() => {
|
|
7599
|
+
keyValues([
|
|
7600
|
+
["Slug", String(created.subdomain ?? "")],
|
|
7601
|
+
["URL", host],
|
|
7602
|
+
["Provider", provider],
|
|
7603
|
+
["Branch", gitBranch]
|
|
7604
|
+
]);
|
|
7605
|
+
if (linked) note(chalk.gray("Linked this directory to the new project."));
|
|
7606
|
+
noteBlank();
|
|
7607
|
+
note(chalk.gray(`Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.subdomain ?? created.id}`)}`));
|
|
7608
|
+
noteBlank();
|
|
7609
|
+
}, {
|
|
7610
|
+
success: true,
|
|
7611
|
+
id: String(created.id),
|
|
7612
|
+
name,
|
|
7613
|
+
slug: created.subdomain ?? null,
|
|
7614
|
+
host: host ?? null,
|
|
7615
|
+
provider,
|
|
7616
|
+
region,
|
|
7617
|
+
vmSize,
|
|
7618
|
+
branch: gitBranch,
|
|
7619
|
+
org: String(org),
|
|
7620
|
+
linked
|
|
7621
|
+
});
|
|
6569
7622
|
} catch (e) {
|
|
6570
7623
|
reportError(e, "Failed to create project");
|
|
6571
7624
|
}
|
|
6572
7625
|
}
|
|
7626
|
+
/**
|
|
7627
|
+
* Which project `projects info` / `projects delete` acts on.
|
|
7628
|
+
*
|
|
7629
|
+
* The id is optional — omitted, it falls back to `--project` or the link file —
|
|
7630
|
+
* and the dispatcher used to read it off `positionals()`, which skips only
|
|
7631
|
+
* LEADING `-` tokens and declares only the global cloud flags. So an undeclared
|
|
7632
|
+
* flag written after the action became the id: `rebase cloud projects delete
|
|
7633
|
+
* --force` looked up a project named "--force" and reported it missing, rather
|
|
7634
|
+
* than saying there is no such flag. Benign next to the deletes and writes the
|
|
7635
|
+
* rest of this family aimed at the wrong resource, but the same mistake, and
|
|
7636
|
+
* `positionals()` has no spec with which to do better — the handler's own
|
|
7637
|
+
* module does.
|
|
7638
|
+
*
|
|
7639
|
+
* Exported so its tests drive the real parser rather than a copy of it.
|
|
7640
|
+
*/
|
|
7641
|
+
function resolveProjectArg(rawArgs, action) {
|
|
7642
|
+
const { positionals } = parseCloudArgs({
|
|
7643
|
+
spec: {},
|
|
7644
|
+
rawArgs,
|
|
7645
|
+
commandWords: 3,
|
|
7646
|
+
command: `cloud projects ${action}`,
|
|
7647
|
+
maxPositionals: 1
|
|
7648
|
+
});
|
|
7649
|
+
return positionals[0] || requireProjectRef(rawArgs);
|
|
7650
|
+
}
|
|
6573
7651
|
async function projectInfo(rawArgs, projectRef) {
|
|
6574
7652
|
const { client, url } = await requireClient(rawArgs);
|
|
6575
7653
|
try {
|
|
6576
7654
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
6577
7655
|
const p = await client.data.collection("projects").findById(projectId);
|
|
6578
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
7656
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
6579
7657
|
const [db, lastDeploy, baseDomain] = await Promise.all([
|
|
6580
7658
|
firstRow(client, "databases", projectId),
|
|
6581
7659
|
latestDeployment(client, projectId),
|
|
6582
7660
|
fetchTenantBaseDomain(client, url)
|
|
6583
7661
|
]);
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
[
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
7662
|
+
emit(() => {
|
|
7663
|
+
console.log("");
|
|
7664
|
+
console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
|
|
7665
|
+
console.log("");
|
|
7666
|
+
keyValues([
|
|
7667
|
+
["Subdomain", projectHost(p, baseDomain)],
|
|
7668
|
+
["Custom domain", p.customDomain],
|
|
7669
|
+
["Repository", p.gitRepoUrl],
|
|
7670
|
+
["Branch", p.gitBranch],
|
|
7671
|
+
["Provider", p.provider],
|
|
7672
|
+
["Region", p.region],
|
|
7673
|
+
["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
|
|
7674
|
+
["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
|
|
7675
|
+
["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
|
|
7676
|
+
]);
|
|
7677
|
+
console.log("");
|
|
7678
|
+
}, {
|
|
7679
|
+
id: String(p.id),
|
|
7680
|
+
name: p.name ?? null,
|
|
7681
|
+
slug: p.subdomain ?? null,
|
|
7682
|
+
host: projectHost(p, baseDomain) ?? null,
|
|
7683
|
+
customDomain: p.customDomain ?? null,
|
|
7684
|
+
repository: p.gitRepoUrl ?? null,
|
|
7685
|
+
branch: p.gitBranch ?? null,
|
|
7686
|
+
provider: p.provider ?? null,
|
|
7687
|
+
region: p.region ?? null,
|
|
7688
|
+
status: p.status ?? null,
|
|
7689
|
+
org: p.organization !== void 0 ? String(p.organization) : null,
|
|
7690
|
+
database: db ? {
|
|
7691
|
+
type: db.type ?? null,
|
|
7692
|
+
connectionStatus: db.connectionStatus ?? null
|
|
7693
|
+
} : null,
|
|
7694
|
+
lastDeploy: lastDeploy ? {
|
|
7695
|
+
id: String(lastDeploy.id),
|
|
7696
|
+
status: lastDeploy.status ?? null,
|
|
7697
|
+
createdAt: lastDeploy.createdAt ?? null
|
|
7698
|
+
} : null
|
|
7699
|
+
});
|
|
6599
7700
|
} catch (e) {
|
|
6600
7701
|
reportError(e, "Failed to load project");
|
|
6601
7702
|
}
|
|
6602
7703
|
}
|
|
6603
7704
|
async function deleteProject(rawArgs, projectRef) {
|
|
6604
|
-
const args =
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
7705
|
+
const { flags: args } = parseCloudArgs({
|
|
7706
|
+
spec: {},
|
|
7707
|
+
rawArgs,
|
|
7708
|
+
commandWords: 3,
|
|
7709
|
+
command: "cloud projects delete",
|
|
7710
|
+
maxPositionals: 1
|
|
6610
7711
|
});
|
|
6611
7712
|
const { client } = await requireClient(rawArgs);
|
|
6612
7713
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
6613
7714
|
const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
|
|
6614
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
default: false,
|
|
6620
|
-
message: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
6621
|
-
}]);
|
|
6622
|
-
if (!confirmed) {
|
|
6623
|
-
console.log(chalk.gray(" Aborted."));
|
|
6624
|
-
return;
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
7715
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
7716
|
+
await confirmDestructive({
|
|
7717
|
+
yes: Boolean(args["--yes"]),
|
|
7718
|
+
prompt: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
7719
|
+
});
|
|
6627
7720
|
try {
|
|
6628
7721
|
await client.data.collection("projects").delete(projectId);
|
|
6629
7722
|
success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
|
|
7723
|
+
emit(() => {}, {
|
|
7724
|
+
success: true,
|
|
7725
|
+
id: projectId,
|
|
7726
|
+
name: p.name ?? null,
|
|
7727
|
+
slug: p.subdomain ?? null
|
|
7728
|
+
});
|
|
6630
7729
|
} catch (e) {
|
|
6631
7730
|
reportError(e, "Failed to delete project");
|
|
6632
7731
|
}
|
|
@@ -7400,7 +8499,7 @@ async function orgsCommand(subcommand, rawArgs) {
|
|
|
7400
8499
|
case "--help":
|
|
7401
8500
|
printOrgsHelp();
|
|
7402
8501
|
break;
|
|
7403
|
-
default: fail(`Unknown orgs command: ${subcommand}`);
|
|
8502
|
+
default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
|
|
7404
8503
|
}
|
|
7405
8504
|
}
|
|
7406
8505
|
async function listOrgs(rawArgs) {
|
|
@@ -7408,21 +8507,31 @@ async function listOrgs(rawArgs) {
|
|
|
7408
8507
|
try {
|
|
7409
8508
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
7410
8509
|
const active = getContextOrg(url);
|
|
7411
|
-
|
|
7412
|
-
console.log(chalk.bold(" 🏢 Organizations"));
|
|
7413
|
-
console.log("");
|
|
7414
|
-
if (orgs.length === 0) {
|
|
7415
|
-
console.log(chalk.gray(" You are not a member of any organization."));
|
|
8510
|
+
emit(() => {
|
|
7416
8511
|
console.log("");
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
8512
|
+
console.log(chalk.bold(" 🏢 Organizations"));
|
|
8513
|
+
console.log("");
|
|
8514
|
+
if (orgs.length === 0) {
|
|
8515
|
+
console.log(chalk.gray(" You are not a member of any organization."));
|
|
8516
|
+
console.log("");
|
|
8517
|
+
return;
|
|
8518
|
+
}
|
|
8519
|
+
for (const o of orgs) {
|
|
8520
|
+
const marker = String(o.id) === active ? chalk.green(" ●") : " ";
|
|
8521
|
+
console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
|
|
8522
|
+
}
|
|
8523
|
+
console.log("");
|
|
8524
|
+
note(chalk.gray("● = active organization. Switch with `rebase cloud use <id>`."));
|
|
8525
|
+
console.log("");
|
|
8526
|
+
}, {
|
|
8527
|
+
activeOrg: active ?? null,
|
|
8528
|
+
organizations: orgs.map((o) => ({
|
|
8529
|
+
id: String(o.id),
|
|
8530
|
+
name: o.name ?? null,
|
|
8531
|
+
slug: o.slug ?? null,
|
|
8532
|
+
active: String(o.id) === active
|
|
8533
|
+
}))
|
|
8534
|
+
});
|
|
7426
8535
|
} catch (e) {
|
|
7427
8536
|
reportError(e, "Failed to list organizations");
|
|
7428
8537
|
}
|
|
@@ -7438,14 +8547,17 @@ async function createOrg(rawArgs) {
|
|
|
7438
8547
|
});
|
|
7439
8548
|
const { client, url } = await requireClient(rawArgs);
|
|
7440
8549
|
const prompts = [];
|
|
7441
|
-
if (!args["--name"])
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
8550
|
+
if (!args["--name"]) {
|
|
8551
|
+
requireInteractive("an organization name", "--name <name>");
|
|
8552
|
+
prompts.push({
|
|
8553
|
+
type: "input",
|
|
8554
|
+
name: "name",
|
|
8555
|
+
message: "Organization name:"
|
|
8556
|
+
});
|
|
8557
|
+
}
|
|
7446
8558
|
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
7447
8559
|
const name = (args["--name"] || answers.name || "").trim();
|
|
7448
|
-
if (!name) fail("Organization name is required.");
|
|
8560
|
+
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
7449
8561
|
const slug = (args["--slug"] || slugify(name)).trim();
|
|
7450
8562
|
try {
|
|
7451
8563
|
const created = await client.data.collection("organizations").create({
|
|
@@ -7455,6 +8567,13 @@ async function createOrg(rawArgs) {
|
|
|
7455
8567
|
});
|
|
7456
8568
|
setContextOrg(url, String(created.id));
|
|
7457
8569
|
success(`Created organization ${chalk.bold(name)} and set it active`);
|
|
8570
|
+
emit(() => {}, {
|
|
8571
|
+
success: true,
|
|
8572
|
+
id: String(created.id),
|
|
8573
|
+
name,
|
|
8574
|
+
slug,
|
|
8575
|
+
setActive: true
|
|
8576
|
+
});
|
|
7458
8577
|
} catch (e) {
|
|
7459
8578
|
reportError(e, "Failed to create organization");
|
|
7460
8579
|
}
|
|
@@ -7462,22 +8581,31 @@ async function createOrg(rawArgs) {
|
|
|
7462
8581
|
async function listMembers(rawArgs) {
|
|
7463
8582
|
const { client, url } = await requireClient(rawArgs);
|
|
7464
8583
|
const org = getContextOrg(url);
|
|
7465
|
-
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
8584
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
7466
8585
|
try {
|
|
7467
8586
|
const members = (await client.data.collection("organization-members").find({
|
|
7468
8587
|
where: { organization: ["==", org] },
|
|
7469
8588
|
limit: 200
|
|
7470
8589
|
})).data;
|
|
7471
|
-
|
|
7472
|
-
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
7473
|
-
console.log("");
|
|
7474
|
-
if (members.length === 0) {
|
|
7475
|
-
console.log(chalk.gray(" No members found."));
|
|
8590
|
+
emit(() => {
|
|
7476
8591
|
console.log("");
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
8592
|
+
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
8593
|
+
console.log("");
|
|
8594
|
+
if (members.length === 0) {
|
|
8595
|
+
console.log(chalk.gray(" No members found."));
|
|
8596
|
+
console.log("");
|
|
8597
|
+
return;
|
|
8598
|
+
}
|
|
8599
|
+
for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
|
|
8600
|
+
console.log("");
|
|
8601
|
+
}, {
|
|
8602
|
+
org,
|
|
8603
|
+
members: members.map((m) => ({
|
|
8604
|
+
id: String(m.id),
|
|
8605
|
+
userId: m.userId ?? null,
|
|
8606
|
+
role: m.role ?? null
|
|
8607
|
+
}))
|
|
8608
|
+
});
|
|
7481
8609
|
} catch (e) {
|
|
7482
8610
|
reportError(e, "Failed to list members");
|
|
7483
8611
|
}
|
|
@@ -7486,7 +8614,12 @@ function slugify(s) {
|
|
|
7486
8614
|
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7487
8615
|
}
|
|
7488
8616
|
function printOrgsHelp() {
|
|
7489
|
-
|
|
8617
|
+
emitHelp("orgs", [
|
|
8618
|
+
"list",
|
|
8619
|
+
"create",
|
|
8620
|
+
"members"
|
|
8621
|
+
], () => {
|
|
8622
|
+
console.log(`
|
|
7490
8623
|
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
7491
8624
|
|
|
7492
8625
|
${chalk.green.bold("Commands")}
|
|
@@ -7494,6 +8627,7 @@ ${chalk.green.bold("Commands")}
|
|
|
7494
8627
|
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
7495
8628
|
${chalk.blue.bold("members")} List members of the active organization
|
|
7496
8629
|
`);
|
|
8630
|
+
});
|
|
7497
8631
|
}
|
|
7498
8632
|
//#endregion
|
|
7499
8633
|
//#region src/commands/cloud/databases.ts
|
|
@@ -7529,7 +8663,7 @@ async function dbCommand$1(subcommand, rawArgs) {
|
|
|
7529
8663
|
case "--help":
|
|
7530
8664
|
printDbHelp();
|
|
7531
8665
|
break;
|
|
7532
|
-
default: fail(`Unknown db command: ${subcommand}`);
|
|
8666
|
+
default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
|
|
7533
8667
|
}
|
|
7534
8668
|
}
|
|
7535
8669
|
async function listDatabases(rawArgs) {
|
|
@@ -7541,19 +8675,30 @@ async function listDatabases(rawArgs) {
|
|
|
7541
8675
|
where: { project: ["==", projectId] },
|
|
7542
8676
|
limit: 50
|
|
7543
8677
|
})).data;
|
|
7544
|
-
|
|
7545
|
-
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
7546
|
-
console.log("");
|
|
7547
|
-
if (dbs.length === 0) {
|
|
7548
|
-
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
8678
|
+
emit(() => {
|
|
7549
8679
|
console.log("");
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
8680
|
+
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
8681
|
+
console.log("");
|
|
8682
|
+
if (dbs.length === 0) {
|
|
8683
|
+
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
8684
|
+
console.log("");
|
|
8685
|
+
return;
|
|
8686
|
+
}
|
|
8687
|
+
for (const d of dbs) {
|
|
8688
|
+
console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
|
|
8689
|
+
keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
|
|
8690
|
+
}
|
|
8691
|
+
console.log("");
|
|
8692
|
+
}, {
|
|
8693
|
+
projectId,
|
|
8694
|
+
databases: dbs.map((d) => ({
|
|
8695
|
+
id: String(d.id),
|
|
8696
|
+
type: d.type ?? null,
|
|
8697
|
+
connectionStatus: d.connectionStatus ?? null,
|
|
8698
|
+
useSshTunnel: Boolean(d.useSshTunnel),
|
|
8699
|
+
pitrEnabled: Boolean(d.pitrEnabled)
|
|
8700
|
+
}))
|
|
8701
|
+
});
|
|
7557
8702
|
} catch (e) {
|
|
7558
8703
|
reportError(e, "Failed to list databases");
|
|
7559
8704
|
}
|
|
@@ -7573,6 +8718,7 @@ async function createDatabase(rawArgs) {
|
|
|
7573
8718
|
const projectRef = displayProjectRef(rawArgs);
|
|
7574
8719
|
let type = args["--type"];
|
|
7575
8720
|
if (!type) {
|
|
8721
|
+
requireInteractive("a database type", "--type <managed|byodb>");
|
|
7576
8722
|
const { picked } = await inquirer.prompt([{
|
|
7577
8723
|
type: "select",
|
|
7578
8724
|
name: "picked",
|
|
@@ -7589,13 +8735,14 @@ async function createDatabase(rawArgs) {
|
|
|
7589
8735
|
}
|
|
7590
8736
|
let connectionString = args["--connection-string"];
|
|
7591
8737
|
if (type === "byodb" && !connectionString) {
|
|
8738
|
+
requireInteractive("a connection string", "--connection-string <url>");
|
|
7592
8739
|
const { cs } = await inquirer.prompt([{
|
|
7593
8740
|
type: "input",
|
|
7594
8741
|
name: "cs",
|
|
7595
8742
|
message: "PostgreSQL connection string:"
|
|
7596
8743
|
}]);
|
|
7597
8744
|
connectionString = cs?.trim();
|
|
7598
|
-
if (!connectionString) fail("A connection string is required for bring-your-own databases.");
|
|
8745
|
+
if (!connectionString) fail("A connection string is required for bring-your-own databases.", "Pass `--connection-string <url>`.", "input_required");
|
|
7599
8746
|
}
|
|
7600
8747
|
try {
|
|
7601
8748
|
const created = await client.data.collection("databases").create({
|
|
@@ -7605,11 +8752,19 @@ async function createDatabase(rawArgs) {
|
|
|
7605
8752
|
connectionStatus: "untested"
|
|
7606
8753
|
});
|
|
7607
8754
|
success(`Attached ${type} database to project ${projectRef}`);
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
8755
|
+
emit(() => {
|
|
8756
|
+
keyValues([["ID", String(created.id)]]);
|
|
8757
|
+
if (type === "byodb") {
|
|
8758
|
+
note(chalk.gray("Verify it with `rebase cloud db test`."));
|
|
8759
|
+
noteBlank();
|
|
8760
|
+
}
|
|
8761
|
+
}, {
|
|
8762
|
+
success: true,
|
|
8763
|
+
id: String(created.id),
|
|
8764
|
+
projectId,
|
|
8765
|
+
type,
|
|
8766
|
+
connectionStatus: "untested"
|
|
8767
|
+
});
|
|
7613
8768
|
} catch (e) {
|
|
7614
8769
|
reportError(e, "Failed to attach database");
|
|
7615
8770
|
}
|
|
@@ -7617,15 +8772,19 @@ async function createDatabase(rawArgs) {
|
|
|
7617
8772
|
async function testDatabase(rawArgs) {
|
|
7618
8773
|
const { client } = await requireClient(rawArgs);
|
|
7619
8774
|
const projectId = await requireProject(rawArgs, client);
|
|
7620
|
-
displayProjectRef(rawArgs);
|
|
7621
|
-
|
|
7622
|
-
|
|
8775
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
8776
|
+
noteBlank();
|
|
8777
|
+
note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
|
|
7623
8778
|
try {
|
|
7624
8779
|
const res = await client.functions.invoke("db-test", { projectId });
|
|
7625
|
-
console.
|
|
7626
|
-
if (res.
|
|
7627
|
-
|
|
7628
|
-
|
|
8780
|
+
if (res.logs) console.error(`\n${res.logs}`);
|
|
8781
|
+
if (!res.success) fail("Database connection failed.", "The connection log above (stderr) has the reason.", "db_connection_failed");
|
|
8782
|
+
success("Database connection succeeded");
|
|
8783
|
+
emit(() => {}, {
|
|
8784
|
+
success: true,
|
|
8785
|
+
projectId,
|
|
8786
|
+
logs: res.logs ?? null
|
|
8787
|
+
});
|
|
7629
8788
|
} catch (e) {
|
|
7630
8789
|
reportError(e, "Failed to test database");
|
|
7631
8790
|
}
|
|
@@ -7701,18 +8860,39 @@ async function dbInfo(rawArgs) {
|
|
|
7701
8860
|
reportError(e, "Failed to load database info");
|
|
7702
8861
|
}
|
|
7703
8862
|
}
|
|
8863
|
+
/**
|
|
8864
|
+
* `db backup [action] [filename]`, resolved in one strict parse.
|
|
8865
|
+
*
|
|
8866
|
+
* Both halves were reachable by the old operand filter, and both are
|
|
8867
|
+
* destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
|
|
8868
|
+
* ACTION (falling through to a list, so the flag silently changed what ran),
|
|
8869
|
+
* and `db backup restore -p acme` read it as the FILENAME — a restore staged
|
|
8870
|
+
* over the live database, named after the project slug. An undeclared flag was
|
|
8871
|
+
* dropped instead of refused, which is the same failure one step quieter: `db
|
|
8872
|
+
* backup --dry-run` ran a list, having silently discarded the flag that was
|
|
8873
|
+
* supposed to change what it did.
|
|
8874
|
+
*
|
|
8875
|
+
* Exported so its tests drive the real parser.
|
|
8876
|
+
*/
|
|
8877
|
+
function resolveBackupArgs(rawArgs) {
|
|
8878
|
+
const { flags, positionals } = parseCloudArgs({
|
|
8879
|
+
spec: { "--yes": Boolean },
|
|
8880
|
+
rawArgs,
|
|
8881
|
+
commandWords: 3,
|
|
8882
|
+
command: "cloud db backup",
|
|
8883
|
+
maxPositionals: 2
|
|
8884
|
+
});
|
|
8885
|
+
return {
|
|
8886
|
+
flags,
|
|
8887
|
+
action: positionals[0] || "list",
|
|
8888
|
+
filename: positionals[1]
|
|
8889
|
+
};
|
|
8890
|
+
}
|
|
7704
8891
|
async function backupCommand(rawArgs) {
|
|
7705
|
-
const action
|
|
8892
|
+
const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
|
|
7706
8893
|
const { client } = await requireClient(rawArgs);
|
|
7707
8894
|
const projectId = await requireProject(rawArgs, client);
|
|
7708
8895
|
const projectRef = displayProjectRef(rawArgs);
|
|
7709
|
-
const args = arg({
|
|
7710
|
-
"--yes": Boolean,
|
|
7711
|
-
"-y": "--yes"
|
|
7712
|
-
}, {
|
|
7713
|
-
argv: rawArgs.slice(2),
|
|
7714
|
-
permissive: true
|
|
7715
|
-
});
|
|
7716
8896
|
try {
|
|
7717
8897
|
if (action === "create") {
|
|
7718
8898
|
const res = await client.functions.invoke("backup", {
|
|
@@ -7727,7 +8907,7 @@ async function backupCommand(rawArgs) {
|
|
|
7727
8907
|
return;
|
|
7728
8908
|
}
|
|
7729
8909
|
if (action === "restore") {
|
|
7730
|
-
const filename =
|
|
8910
|
+
const filename = backupFile;
|
|
7731
8911
|
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
7732
8912
|
await confirmDestructive({
|
|
7733
8913
|
yes: Boolean(args["--yes"]),
|
|
@@ -7765,7 +8945,7 @@ async function backupCommand(rawArgs) {
|
|
|
7765
8945
|
return;
|
|
7766
8946
|
}
|
|
7767
8947
|
if (action === "download") {
|
|
7768
|
-
const filename =
|
|
8948
|
+
const filename = backupFile;
|
|
7769
8949
|
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
7770
8950
|
const res = await client.functions.invoke("backup", void 0, {
|
|
7771
8951
|
method: "GET",
|
|
@@ -7823,17 +9003,17 @@ async function backupCommand(rawArgs) {
|
|
|
7823
9003
|
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
7824
9004
|
*/
|
|
7825
9005
|
async function pitrCommand(rawArgs) {
|
|
7826
|
-
const args =
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
9006
|
+
const { flags: args, positionals } = parseCloudArgs({
|
|
9007
|
+
spec: {
|
|
9008
|
+
"--target": String,
|
|
9009
|
+
"--yes": Boolean
|
|
9010
|
+
},
|
|
9011
|
+
rawArgs,
|
|
9012
|
+
commandWords: 3,
|
|
9013
|
+
command: "cloud db pitr",
|
|
9014
|
+
maxPositionals: 1
|
|
7835
9015
|
});
|
|
7836
|
-
const action =
|
|
9016
|
+
const action = positionals[0] || "status";
|
|
7837
9017
|
const { client } = await requireClient(rawArgs);
|
|
7838
9018
|
const projectId = await requireProject(rawArgs, client);
|
|
7839
9019
|
const projectRef = displayProjectRef(rawArgs);
|
|
@@ -7905,7 +9085,15 @@ async function pitrCommand(rawArgs) {
|
|
|
7905
9085
|
}
|
|
7906
9086
|
}
|
|
7907
9087
|
function printDbHelp() {
|
|
7908
|
-
|
|
9088
|
+
emitHelp("db", [
|
|
9089
|
+
"list",
|
|
9090
|
+
"create",
|
|
9091
|
+
"info",
|
|
9092
|
+
"test",
|
|
9093
|
+
"backup",
|
|
9094
|
+
"pitr"
|
|
9095
|
+
], () => {
|
|
9096
|
+
console.log(`
|
|
7909
9097
|
${chalk.bold("rebase cloud db")} — Database & backups
|
|
7910
9098
|
|
|
7911
9099
|
${chalk.green.bold("Commands")}
|
|
@@ -7930,6 +9118,7 @@ ${chalk.green.bold("Options")}
|
|
|
7930
9118
|
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
7931
9119
|
${chalk.blue("--json")} Machine-readable output
|
|
7932
9120
|
`);
|
|
9121
|
+
});
|
|
7933
9122
|
}
|
|
7934
9123
|
//#endregion
|
|
7935
9124
|
//#region src/commands/cloud/env.ts
|
|
@@ -7966,7 +9155,7 @@ async function envCommand(action, rawArgs) {
|
|
|
7966
9155
|
case "unset":
|
|
7967
9156
|
case "delete":
|
|
7968
9157
|
case "rm":
|
|
7969
|
-
await unsetEnv(rawArgs);
|
|
9158
|
+
await unsetEnv(rawArgs, action);
|
|
7970
9159
|
break;
|
|
7971
9160
|
case "reveal":
|
|
7972
9161
|
await revealEnv(rawArgs);
|
|
@@ -8066,20 +9255,43 @@ var BUILD_TIME_ENV_PREFIXES = [
|
|
|
8066
9255
|
function buildTimeEnvPrefix(key) {
|
|
8067
9256
|
return BUILD_TIME_ENV_PREFIXES.find((prefix) => key.toUpperCase().startsWith(prefix));
|
|
8068
9257
|
}
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
9258
|
+
/** The flags `rebase cloud env set` takes, on top of the global cloud ones. */
|
|
9259
|
+
var ENV_SET_FLAGS = {
|
|
9260
|
+
"--secret": Boolean,
|
|
9261
|
+
"--force": Boolean
|
|
9262
|
+
};
|
|
9263
|
+
/**
|
|
9264
|
+
* What `env set` was asked to store.
|
|
9265
|
+
*
|
|
9266
|
+
* The sharp one in this family: the old operand filter left `--project`'s value
|
|
9267
|
+
* in the operand list, so `rebase cloud env set KEY -p acme` parsed as the
|
|
9268
|
+
* `KEY VALUE` form and stored the project slug as KEY's value — a write that
|
|
9269
|
+
* succeeds, reports success, and is wrong. Strict parsing consumes `-p` with
|
|
9270
|
+
* its value, leaving `["KEY"]` and the documented empty value.
|
|
9271
|
+
*
|
|
9272
|
+
* A value beginning with `-` must use the `KEY=-v` form; the bare `KEY -v` form
|
|
9273
|
+
* is refused rather than guessed at, as everywhere else strict parsing is used.
|
|
9274
|
+
*
|
|
9275
|
+
* Exported so its tests drive the real parser rather than a copy of it.
|
|
9276
|
+
*/
|
|
9277
|
+
function resolveEnvSetArgs(rawArgs) {
|
|
9278
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9279
|
+
spec: ENV_SET_FLAGS,
|
|
9280
|
+
rawArgs,
|
|
9281
|
+
commandWords: 3,
|
|
9282
|
+
command: "cloud env set",
|
|
9283
|
+
maxPositionals: 2
|
|
8078
9284
|
});
|
|
9285
|
+
return {
|
|
9286
|
+
flags,
|
|
9287
|
+
assignment: parseEnvAssignment(positionals)
|
|
9288
|
+
};
|
|
9289
|
+
}
|
|
9290
|
+
async function setEnv(rawArgs) {
|
|
9291
|
+
const { flags: args, assignment: parsed } = resolveEnvSetArgs(rawArgs);
|
|
8079
9292
|
const { client } = await requireClient(rawArgs);
|
|
8080
9293
|
const projectId = await requireProject(rawArgs, client);
|
|
8081
9294
|
displayProjectRef(rawArgs);
|
|
8082
|
-
const parsed = parseEnvAssignment(cloudPositionals(rawArgs).slice(2));
|
|
8083
9295
|
if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
|
|
8084
9296
|
const buildTimePrefix = buildTimeEnvPrefix(parsed.key);
|
|
8085
9297
|
if (buildTimePrefix && !args["--force"]) fail(`${parsed.key} is read by your bundler at BUILD time, and project variables are applied at rollout — after the image is built. Setting it here would not reach the bundle.`, `Put ${buildTimePrefix}* variables in the source you deploy (a committed .env, or your build config), then \`rebase cloud deploy\`. Pass --force if your build genuinely reads this at run time.`, "build_time_variable");
|
|
@@ -8105,12 +9317,32 @@ async function setEnv(rawArgs) {
|
|
|
8105
9317
|
reportError(e, "Failed to set environment variable");
|
|
8106
9318
|
}
|
|
8107
9319
|
}
|
|
8108
|
-
|
|
9320
|
+
/**
|
|
9321
|
+
* The variable `env unset` / `env reveal` names.
|
|
9322
|
+
*
|
|
9323
|
+
* `unset` is a delete, and the operand filter aimed it at the wrong variable:
|
|
9324
|
+
* `rebase cloud env unset -p acme` removed a variable called "acme" from the
|
|
9325
|
+
* linked project instead of reporting a missing KEY, and `env unset -p acme
|
|
9326
|
+
* KEY` removed "acme" instead of KEY. Both read `--project`'s value as the
|
|
9327
|
+
* operand — a plain word in the right position that no flag filter can catch.
|
|
9328
|
+
*
|
|
9329
|
+
* `action` is the word the caller used (`unset`, `rm`, `delete`, `reveal`); the
|
|
9330
|
+
* count of command words is the same for all of them.
|
|
9331
|
+
*/
|
|
9332
|
+
function resolveEnvKeyArg(rawArgs, action) {
|
|
9333
|
+
return parseCloudArgs({
|
|
9334
|
+
spec: {},
|
|
9335
|
+
rawArgs,
|
|
9336
|
+
commandWords: 3,
|
|
9337
|
+
command: `cloud env ${action}`,
|
|
9338
|
+
maxPositionals: 1
|
|
9339
|
+
}).positionals[0];
|
|
9340
|
+
}
|
|
9341
|
+
async function unsetEnv(rawArgs, action) {
|
|
9342
|
+
const key = resolveEnvKeyArg(rawArgs, action);
|
|
9343
|
+
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8109
9344
|
const { client } = await requireClient(rawArgs);
|
|
8110
9345
|
const projectId = await requireProject(rawArgs, client);
|
|
8111
|
-
displayProjectRef(rawArgs);
|
|
8112
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8113
|
-
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8114
9346
|
try {
|
|
8115
9347
|
emit(() => {
|
|
8116
9348
|
success(`Removed ${chalk.bold(key)}`);
|
|
@@ -8129,11 +9361,11 @@ async function unsetEnv(rawArgs) {
|
|
|
8129
9361
|
}
|
|
8130
9362
|
}
|
|
8131
9363
|
async function revealEnv(rawArgs) {
|
|
9364
|
+
const key = resolveEnvKeyArg(rawArgs, "reveal");
|
|
9365
|
+
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8132
9366
|
const { client } = await requireClient(rawArgs);
|
|
8133
9367
|
const projectId = await requireProject(rawArgs, client);
|
|
8134
9368
|
const projectRef = displayProjectRef(rawArgs);
|
|
8135
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8136
|
-
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8137
9369
|
let list;
|
|
8138
9370
|
try {
|
|
8139
9371
|
list = await fetchEnvVars(client, projectId);
|
|
@@ -8161,20 +9393,20 @@ async function revealEnv(rawArgs) {
|
|
|
8161
9393
|
}
|
|
8162
9394
|
}
|
|
8163
9395
|
async function pullEnv(rawArgs) {
|
|
8164
|
-
const args =
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
9396
|
+
const { flags: args } = parseCloudArgs({
|
|
9397
|
+
spec: {
|
|
9398
|
+
"--output": String,
|
|
9399
|
+
"--out": "--output"
|
|
9400
|
+
},
|
|
9401
|
+
rawArgs,
|
|
9402
|
+
commandWords: 3,
|
|
9403
|
+
command: "cloud env pull",
|
|
9404
|
+
maxPositionals: 0
|
|
8173
9405
|
});
|
|
8174
9406
|
const { client } = await requireClient(rawArgs);
|
|
8175
9407
|
const projectId = await requireProject(rawArgs, client);
|
|
8176
9408
|
displayProjectRef(rawArgs);
|
|
8177
|
-
const outPath = path.resolve(args["--
|
|
9409
|
+
const outPath = path.resolve(args["--output"] || ".env");
|
|
8178
9410
|
try {
|
|
8179
9411
|
const list = await fetchEnvVars(client, projectId);
|
|
8180
9412
|
if (fs.existsSync(outPath)) await confirmDestructive({
|
|
@@ -8223,11 +9455,14 @@ async function pullEnv(rawArgs) {
|
|
|
8223
9455
|
}
|
|
8224
9456
|
}
|
|
8225
9457
|
function printEnvHelp() {
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
9458
|
+
emitHelp("env", [
|
|
9459
|
+
"list",
|
|
9460
|
+
"set",
|
|
9461
|
+
"unset",
|
|
9462
|
+
"reveal",
|
|
9463
|
+
"pull"
|
|
9464
|
+
], () => {
|
|
9465
|
+
console.log(`
|
|
8231
9466
|
${chalk.bold("rebase cloud env")} — Environment variables
|
|
8232
9467
|
|
|
8233
9468
|
${chalk.green.bold("Commands")}
|
|
@@ -8247,18 +9482,7 @@ ${chalk.gray("Values are encrypted at rest (AES-256-GCM) and only decrypted at d
|
|
|
8247
9482
|
${chalk.gray("VITE_* / NEXT_PUBLIC_* / PUBLIC_* / REACT_APP_* are read by your bundler at BUILD time;")}
|
|
8248
9483
|
${chalk.gray("these are applied at rollout, after the image is built, so they never reach the bundle.")}
|
|
8249
9484
|
`);
|
|
8250
|
-
}
|
|
8251
|
-
function printEnvHelpJson() {
|
|
8252
|
-
process.stdout.write(JSON.stringify({
|
|
8253
|
-
command: "env",
|
|
8254
|
-
actions: [
|
|
8255
|
-
"list",
|
|
8256
|
-
"set",
|
|
8257
|
-
"unset",
|
|
8258
|
-
"reveal",
|
|
8259
|
-
"pull"
|
|
8260
|
-
]
|
|
8261
|
-
}) + "\n");
|
|
9485
|
+
});
|
|
8262
9486
|
}
|
|
8263
9487
|
//#endregion
|
|
8264
9488
|
//#region src/commands/cloud/domains.ts
|
|
@@ -8346,12 +9570,28 @@ async function listDomains(rawArgs) {
|
|
|
8346
9570
|
reportError(e, "Failed to load custom domain");
|
|
8347
9571
|
}
|
|
8348
9572
|
}
|
|
9573
|
+
/**
|
|
9574
|
+
* The domain `domains add` was asked to register.
|
|
9575
|
+
*
|
|
9576
|
+
* Exported so its tests drive the real parser. Under the old operand filter
|
|
9577
|
+
* `rebase cloud domains add -p acme` registered a domain called "acme" — the
|
|
9578
|
+
* project slug, read out of `--project`'s own value — and a registered domain
|
|
9579
|
+
* is a project-record write, not a no-op.
|
|
9580
|
+
*/
|
|
9581
|
+
function resolveDomainArg(rawArgs) {
|
|
9582
|
+
return parseCloudArgs({
|
|
9583
|
+
spec: {},
|
|
9584
|
+
rawArgs,
|
|
9585
|
+
commandWords: 3,
|
|
9586
|
+
command: "cloud domains add",
|
|
9587
|
+
maxPositionals: 1
|
|
9588
|
+
}).positionals[0];
|
|
9589
|
+
}
|
|
8349
9590
|
async function addDomain(rawArgs) {
|
|
9591
|
+
const domain = resolveDomainArg(rawArgs);
|
|
9592
|
+
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8350
9593
|
const { client } = await requireClient(rawArgs);
|
|
8351
9594
|
const projectId = await requireProject(rawArgs, client);
|
|
8352
|
-
displayProjectRef(rawArgs);
|
|
8353
|
-
const domain = cloudPositionals(rawArgs).slice(2)[0];
|
|
8354
|
-
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8355
9595
|
try {
|
|
8356
9596
|
await client.data.collection("projects").update(projectId, { customDomain: domain });
|
|
8357
9597
|
const setup = await fetchDomainSetup(client, projectId);
|
|
@@ -8406,14 +9646,12 @@ async function verifyDomains(rawArgs) {
|
|
|
8406
9646
|
}
|
|
8407
9647
|
}
|
|
8408
9648
|
async function removeDomain(rawArgs) {
|
|
8409
|
-
const args =
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
argv: rawArgs.slice(2),
|
|
8416
|
-
permissive: true
|
|
9649
|
+
const { flags: args } = parseCloudArgs({
|
|
9650
|
+
spec: {},
|
|
9651
|
+
rawArgs,
|
|
9652
|
+
commandWords: 3,
|
|
9653
|
+
command: "cloud domains remove",
|
|
9654
|
+
maxPositionals: 0
|
|
8417
9655
|
});
|
|
8418
9656
|
const { client } = await requireClient(rawArgs);
|
|
8419
9657
|
const projectId = await requireProject(rawArgs, client);
|
|
@@ -8433,7 +9671,13 @@ async function removeDomain(rawArgs) {
|
|
|
8433
9671
|
}
|
|
8434
9672
|
}
|
|
8435
9673
|
function printDomainsHelp() {
|
|
8436
|
-
|
|
9674
|
+
emitHelp("domains", [
|
|
9675
|
+
"list",
|
|
9676
|
+
"add",
|
|
9677
|
+
"verify",
|
|
9678
|
+
"remove"
|
|
9679
|
+
], () => {
|
|
9680
|
+
console.log(`
|
|
8437
9681
|
${chalk.bold("rebase cloud domains")} — Custom domain
|
|
8438
9682
|
|
|
8439
9683
|
${chalk.green.bold("Commands")}
|
|
@@ -8446,6 +9690,7 @@ ${chalk.green.bold("Options")}
|
|
|
8446
9690
|
${chalk.blue("--json")} Machine-readable output
|
|
8447
9691
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8448
9692
|
`);
|
|
9693
|
+
});
|
|
8449
9694
|
}
|
|
8450
9695
|
//#endregion
|
|
8451
9696
|
//#region src/commands/cloud/extensions.ts
|
|
@@ -8523,22 +9768,33 @@ async function listExtensions(rawArgs) {
|
|
|
8523
9768
|
} catch (e) {
|
|
8524
9769
|
reportError(e, "Failed to list extensions");
|
|
8525
9770
|
}
|
|
8526
|
-
}
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
9771
|
+
}
|
|
9772
|
+
/**
|
|
9773
|
+
* The extension `enable`/`disable` names, plus the flags that gate it.
|
|
9774
|
+
*
|
|
9775
|
+
* Under the old operand filter `rebase cloud extensions enable -p acme` read
|
|
9776
|
+
* `--project`'s value as the extension name and asked the server to install one
|
|
9777
|
+
* called "acme"; `extensions disable -p acme vector` dropped "acme" rather than
|
|
9778
|
+
* vector. Strict parsing consumes the flag with its value.
|
|
9779
|
+
*/
|
|
9780
|
+
function resolveExtensionArgs(rawArgs, action) {
|
|
9781
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9782
|
+
spec: {},
|
|
9783
|
+
rawArgs,
|
|
9784
|
+
commandWords: 3,
|
|
9785
|
+
command: `cloud extensions ${action}`,
|
|
9786
|
+
maxPositionals: 1
|
|
8536
9787
|
});
|
|
9788
|
+
return {
|
|
9789
|
+
flags,
|
|
9790
|
+
name: positionals[0]
|
|
9791
|
+
};
|
|
9792
|
+
}
|
|
9793
|
+
async function enableExtension(rawArgs) {
|
|
9794
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "enable");
|
|
9795
|
+
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8537
9796
|
const { client } = await requireClient(rawArgs);
|
|
8538
9797
|
const projectId = await requireProject(rawArgs, client);
|
|
8539
|
-
displayProjectRef(rawArgs);
|
|
8540
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8541
|
-
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8542
9798
|
const name = resolveExtensionAlias(raw);
|
|
8543
9799
|
try {
|
|
8544
9800
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8575,20 +9831,10 @@ async function enableExtension(rawArgs) {
|
|
|
8575
9831
|
}
|
|
8576
9832
|
}
|
|
8577
9833
|
async function disableExtension(rawArgs) {
|
|
8578
|
-
const args =
|
|
8579
|
-
|
|
8580
|
-
"-y": "--yes",
|
|
8581
|
-
"--project": String,
|
|
8582
|
-
"-p": "--project"
|
|
8583
|
-
}, {
|
|
8584
|
-
argv: rawArgs.slice(2),
|
|
8585
|
-
permissive: true
|
|
8586
|
-
});
|
|
9834
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "disable");
|
|
9835
|
+
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8587
9836
|
const { client } = await requireClient(rawArgs);
|
|
8588
9837
|
const projectId = await requireProject(rawArgs, client);
|
|
8589
|
-
displayProjectRef(rawArgs);
|
|
8590
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8591
|
-
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8592
9838
|
const name = resolveExtensionAlias(raw);
|
|
8593
9839
|
try {
|
|
8594
9840
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8614,7 +9860,12 @@ async function disableExtension(rawArgs) {
|
|
|
8614
9860
|
}
|
|
8615
9861
|
}
|
|
8616
9862
|
function printExtensionsHelp() {
|
|
8617
|
-
|
|
9863
|
+
emitHelp("extensions", [
|
|
9864
|
+
"list",
|
|
9865
|
+
"enable",
|
|
9866
|
+
"disable"
|
|
9867
|
+
], () => {
|
|
9868
|
+
console.log(`
|
|
8618
9869
|
${chalk.bold("rebase cloud extensions")} — Postgres extensions
|
|
8619
9870
|
|
|
8620
9871
|
${chalk.green.bold("Commands")}
|
|
@@ -8627,6 +9878,7 @@ ${chalk.green.bold("Options")}
|
|
|
8627
9878
|
${chalk.blue("--json")} Machine-readable output
|
|
8628
9879
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8629
9880
|
`);
|
|
9881
|
+
});
|
|
8630
9882
|
}
|
|
8631
9883
|
//#endregion
|
|
8632
9884
|
//#region src/commands/cloud/settings.ts
|
|
@@ -8738,7 +9990,8 @@ async function setSettings(rawArgs) {
|
|
|
8738
9990
|
}
|
|
8739
9991
|
}
|
|
8740
9992
|
function printSettingsHelp() {
|
|
8741
|
-
|
|
9993
|
+
emitHelp("settings", ["show", "set"], () => {
|
|
9994
|
+
console.log(`
|
|
8742
9995
|
${chalk.bold("rebase cloud settings")} — Project configuration
|
|
8743
9996
|
|
|
8744
9997
|
${chalk.green.bold("Commands")}
|
|
@@ -8755,6 +10008,7 @@ ${chalk.green.bold("Options")}
|
|
|
8755
10008
|
${chalk.blue("--json")} Machine-readable output
|
|
8756
10009
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8757
10010
|
`);
|
|
10011
|
+
});
|
|
8758
10012
|
}
|
|
8759
10013
|
//#endregion
|
|
8760
10014
|
//#region src/commands/cloud/deployments.ts
|
|
@@ -8865,14 +10119,15 @@ function parseDeploymentsLimit(raw) {
|
|
|
8865
10119
|
return raw;
|
|
8866
10120
|
}
|
|
8867
10121
|
async function deploymentsListCommand(rawArgs) {
|
|
8868
|
-
const args =
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
10122
|
+
const { flags: args } = parseCloudArgs({
|
|
10123
|
+
spec: {
|
|
10124
|
+
"--limit": Number,
|
|
10125
|
+
"--all": Boolean
|
|
10126
|
+
},
|
|
10127
|
+
rawArgs,
|
|
10128
|
+
commandWords: 2,
|
|
10129
|
+
command: "cloud deployments list",
|
|
10130
|
+
maxPositionals: 1
|
|
8876
10131
|
});
|
|
8877
10132
|
const limit = args["--all"] ? MAX_DEPLOYMENTS_LIMIT : parseDeploymentsLimit(args["--limit"]);
|
|
8878
10133
|
const { client } = await requireClient(rawArgs);
|
|
@@ -8913,20 +10168,34 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
8913
10168
|
reportError(e, "Failed to list deployments");
|
|
8914
10169
|
}
|
|
8915
10170
|
}
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
10171
|
+
/**
|
|
10172
|
+
* The deployment id `rollback`/`cancel` was given, if any.
|
|
10173
|
+
*
|
|
10174
|
+
* Both take an optional id, which is what made the old operand filter so easy
|
|
10175
|
+
* to trip: `rebase cloud rollback -p acme` — the documented way to act on an
|
|
10176
|
+
* unlinked project — read `--project`'s value as the id and refused with
|
|
10177
|
+
* "Deployment acme not found", and `cancel -p acme` sent "acme" to the server
|
|
10178
|
+
* as the deployment to cancel. Strict parsing consumes the flag with its value,
|
|
10179
|
+
* so an id given as a flag value is never mistaken for an argument.
|
|
10180
|
+
*/
|
|
10181
|
+
function resolveDeploymentIdArg(rawArgs, command) {
|
|
10182
|
+
const { flags, positionals } = parseCloudArgs({
|
|
10183
|
+
spec: {},
|
|
10184
|
+
rawArgs,
|
|
10185
|
+
commandWords: 2,
|
|
10186
|
+
command,
|
|
10187
|
+
maxPositionals: 1
|
|
8925
10188
|
});
|
|
10189
|
+
return {
|
|
10190
|
+
flags,
|
|
10191
|
+
id: positionals[0]
|
|
10192
|
+
};
|
|
10193
|
+
}
|
|
10194
|
+
async function rollbackCommand(rawArgs) {
|
|
10195
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud rollback");
|
|
8926
10196
|
const { client } = await requireClient(rawArgs);
|
|
8927
10197
|
const projectId = await requireProject(rawArgs, client);
|
|
8928
10198
|
const projectRef = displayProjectRef(rawArgs);
|
|
8929
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
8930
10199
|
let rows;
|
|
8931
10200
|
try {
|
|
8932
10201
|
rows = await fetchDeployments(client, projectId);
|
|
@@ -8974,19 +10243,10 @@ async function rollbackCommand(rawArgs) {
|
|
|
8974
10243
|
}
|
|
8975
10244
|
}
|
|
8976
10245
|
async function cancelCommand(rawArgs) {
|
|
8977
|
-
const args =
|
|
8978
|
-
"--yes": Boolean,
|
|
8979
|
-
"-y": "--yes",
|
|
8980
|
-
"--project": String,
|
|
8981
|
-
"-p": "--project"
|
|
8982
|
-
}, {
|
|
8983
|
-
argv: rawArgs.slice(2),
|
|
8984
|
-
permissive: true
|
|
8985
|
-
});
|
|
10246
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud cancel");
|
|
8986
10247
|
const { client } = await requireClient(rawArgs);
|
|
8987
10248
|
const projectId = await requireProject(rawArgs, client);
|
|
8988
10249
|
const projectRef = displayProjectRef(rawArgs);
|
|
8989
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
8990
10250
|
await confirmDestructive({
|
|
8991
10251
|
yes: Boolean(args["--yes"]),
|
|
8992
10252
|
prompt: `Cancel the in-flight build for project ${projectRef}?`
|
|
@@ -9768,7 +11028,16 @@ async function debugCommand(action, rawArgs) {
|
|
|
9768
11028
|
}
|
|
9769
11029
|
}
|
|
9770
11030
|
function printDebugHelp() {
|
|
9771
|
-
|
|
11031
|
+
emitHelp("debug", [
|
|
11032
|
+
"health",
|
|
11033
|
+
"logs",
|
|
11034
|
+
"errors",
|
|
11035
|
+
"requests",
|
|
11036
|
+
"boot",
|
|
11037
|
+
"pod",
|
|
11038
|
+
"db"
|
|
11039
|
+
], () => {
|
|
11040
|
+
console.log(`
|
|
9772
11041
|
${chalk.bold("rebase cloud debug")} — Find out why a deployed project is misbehaving
|
|
9773
11042
|
|
|
9774
11043
|
${chalk.green.bold("Usage")}
|
|
@@ -9799,6 +11068,7 @@ ${chalk.green.bold("Options")}
|
|
|
9799
11068
|
${chalk.gray("Everything here is read-only. `health` exits non-zero when a check fails,")}
|
|
9800
11069
|
${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase cloud restart`.")}
|
|
9801
11070
|
`);
|
|
11071
|
+
});
|
|
9802
11072
|
}
|
|
9803
11073
|
//#endregion
|
|
9804
11074
|
//#region src/commands/cloud/resources.ts
|
|
@@ -9938,37 +11208,73 @@ async function metricsCommand(rawArgs) {
|
|
|
9938
11208
|
method: "GET",
|
|
9939
11209
|
path: projectId
|
|
9940
11210
|
});
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
[
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
11211
|
+
emit(() => {
|
|
11212
|
+
console.log("");
|
|
11213
|
+
console.log(chalk.bold(` 📊 Metrics — project ${displayProjectRef(rawArgs)}`));
|
|
11214
|
+
console.log("");
|
|
11215
|
+
keyValues([
|
|
11216
|
+
["Status", m.status ? colorStatus(m.status === "running" ? "active" : m.status) : void 0],
|
|
11217
|
+
["CPU", m.cpu],
|
|
11218
|
+
["Memory", m.memory ? `${m.memory}${m.memoryPercent ? ` (${m.memoryPercent})` : ""}` : void 0],
|
|
11219
|
+
["Disk", m.disk]
|
|
11220
|
+
]);
|
|
11221
|
+
console.log("");
|
|
11222
|
+
}, {
|
|
11223
|
+
projectId,
|
|
11224
|
+
status: m.status ?? null,
|
|
11225
|
+
cpu: m.cpu ?? null,
|
|
11226
|
+
memory: m.memory ?? null,
|
|
11227
|
+
memoryPercent: m.memoryPercent ?? null,
|
|
11228
|
+
disk: m.disk ?? null
|
|
11229
|
+
});
|
|
9951
11230
|
} catch (e) {
|
|
9952
11231
|
reportError(e, "Failed to fetch metrics");
|
|
9953
11232
|
}
|
|
9954
11233
|
}
|
|
11234
|
+
/**
|
|
11235
|
+
* The webhook `webhooks delete` names.
|
|
11236
|
+
*
|
|
11237
|
+
* The worst instance of the operand-filter bug in this family, because the
|
|
11238
|
+
* argument is consumed by a DELETE and the wrong value looks entirely
|
|
11239
|
+
* plausible: `rebase cloud webhooks delete --project acme 42` filtered out
|
|
11240
|
+
* `--project` and kept "acme", so the id it deleted was the project slug rather
|
|
11241
|
+
* than the 42 the caller wrote. Strict parsing consumes the flag with its
|
|
11242
|
+
* value, leaving `["42"]`.
|
|
11243
|
+
*
|
|
11244
|
+
* Exported so its tests drive the real parser.
|
|
11245
|
+
*/
|
|
11246
|
+
function resolveWebhookIdArg(rawArgs) {
|
|
11247
|
+
return parseCloudArgs({
|
|
11248
|
+
spec: {},
|
|
11249
|
+
rawArgs,
|
|
11250
|
+
commandWords: 3,
|
|
11251
|
+
command: "cloud webhooks delete",
|
|
11252
|
+
maxPositionals: 1
|
|
11253
|
+
}).positionals[0];
|
|
11254
|
+
}
|
|
9955
11255
|
async function webhooksCommand(subcommand, rawArgs) {
|
|
11256
|
+
const create = subcommand === "create" ? parseCloudArgs({
|
|
11257
|
+
spec: {
|
|
11258
|
+
"--name": String,
|
|
11259
|
+
"--table": String,
|
|
11260
|
+
"--url": String,
|
|
11261
|
+
"--events": String
|
|
11262
|
+
},
|
|
11263
|
+
rawArgs,
|
|
11264
|
+
commandWords: 3,
|
|
11265
|
+
command: "cloud webhooks create",
|
|
11266
|
+
maxPositionals: 0
|
|
11267
|
+
}).flags : void 0;
|
|
11268
|
+
const deleteId = subcommand === "delete" ? resolveWebhookIdArg(rawArgs) : void 0;
|
|
11269
|
+
if (subcommand === "delete" && !deleteId) fail("Usage: rebase cloud webhooks delete <id>", void 0, "usage");
|
|
9956
11270
|
const { client } = await requireClient(rawArgs);
|
|
9957
11271
|
const projectId = await requireProject(rawArgs, client);
|
|
9958
11272
|
try {
|
|
9959
11273
|
if (subcommand === "create") {
|
|
9960
|
-
const args =
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
"--events": String
|
|
9965
|
-
}, {
|
|
9966
|
-
argv: rawArgs.slice(4),
|
|
9967
|
-
permissive: true
|
|
9968
|
-
});
|
|
9969
|
-
const name = args["--name"] || fail("--name is required.");
|
|
9970
|
-
const table = args["--table"] || fail("--table is required.");
|
|
9971
|
-
const url = args["--url"] || fail("--url (endpoint) is required.");
|
|
11274
|
+
const args = create;
|
|
11275
|
+
const name = args["--name"] || fail("--name is required.", void 0, "usage");
|
|
11276
|
+
const table = args["--table"] || fail("--table is required.", void 0, "usage");
|
|
11277
|
+
const url = args["--url"] || fail("--url (endpoint) is required.", void 0, "usage");
|
|
9972
11278
|
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
9973
11279
|
const created = await client.data.collection("webhooks").create({
|
|
9974
11280
|
project: projectId,
|
|
@@ -9979,33 +11285,58 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
9979
11285
|
enabled: true
|
|
9980
11286
|
});
|
|
9981
11287
|
success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
|
|
11288
|
+
emit(() => {}, {
|
|
11289
|
+
success: true,
|
|
11290
|
+
id: String(created.id),
|
|
11291
|
+
projectId,
|
|
11292
|
+
name,
|
|
11293
|
+
table,
|
|
11294
|
+
url,
|
|
11295
|
+
events,
|
|
11296
|
+
enabled: true
|
|
11297
|
+
});
|
|
9982
11298
|
return;
|
|
9983
11299
|
}
|
|
9984
11300
|
if (subcommand === "delete") {
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
11301
|
+
await client.data.collection("webhooks").delete(deleteId);
|
|
11302
|
+
success(`Deleted webhook ${deleteId}`);
|
|
11303
|
+
emit(() => {}, {
|
|
11304
|
+
success: true,
|
|
11305
|
+
id: deleteId,
|
|
11306
|
+
projectId
|
|
11307
|
+
});
|
|
9989
11308
|
return;
|
|
9990
11309
|
}
|
|
9991
11310
|
const hooks = (await client.data.collection("webhooks").find({
|
|
9992
11311
|
where: { project: ["==", projectId] },
|
|
9993
11312
|
limit: 100
|
|
9994
11313
|
})).data;
|
|
9995
|
-
|
|
9996
|
-
console.log(chalk.bold(` 🔗 Webhooks — project ${displayProjectRef(rawArgs)}`));
|
|
9997
|
-
console.log("");
|
|
9998
|
-
if (hooks.length === 0) {
|
|
9999
|
-
console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
|
|
11314
|
+
emit(() => {
|
|
10000
11315
|
console.log("");
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
11316
|
+
console.log(chalk.bold(` 🔗 Webhooks — project ${displayProjectRef(rawArgs)}`));
|
|
11317
|
+
console.log("");
|
|
11318
|
+
if (hooks.length === 0) {
|
|
11319
|
+
console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
|
|
11320
|
+
console.log("");
|
|
11321
|
+
return;
|
|
11322
|
+
}
|
|
11323
|
+
for (const h of hooks) {
|
|
11324
|
+
const state = h.enabled ? chalk.green("enabled") : chalk.gray("disabled");
|
|
11325
|
+
console.log(` ${chalk.bold(h.name ?? "(unnamed)")} ${chalk.gray(`[${h.id}]`)} ${state}`);
|
|
11326
|
+
console.log(` ${chalk.gray(`${h.table ?? "?"} → ${h.url ?? "?"} (${(h.events ?? []).join(", ")})`)}`);
|
|
11327
|
+
}
|
|
11328
|
+
console.log("");
|
|
11329
|
+
}, {
|
|
11330
|
+
projectId,
|
|
11331
|
+
webhooks: hooks.map((h) => ({
|
|
11332
|
+
id: String(h.id),
|
|
11333
|
+
name: h.name ?? null,
|
|
11334
|
+
table: h.table ?? null,
|
|
11335
|
+
url: h.url ?? null,
|
|
11336
|
+
events: h.events ?? [],
|
|
11337
|
+
enabled: h.enabled ?? null
|
|
11338
|
+
}))
|
|
11339
|
+
});
|
|
10009
11340
|
} catch (e) {
|
|
10010
11341
|
reportError(e, "Webhook operation failed");
|
|
10011
11342
|
}
|
|
@@ -10021,78 +11352,116 @@ async function storageCommand(action, rawArgs) {
|
|
|
10021
11352
|
where: { project: ["==", projectId] },
|
|
10022
11353
|
limit: 50
|
|
10023
11354
|
})).data;
|
|
10024
|
-
|
|
10025
|
-
console.log(chalk.bold(` 🪣 Storage — project ${displayProjectRef(rawArgs)}`));
|
|
10026
|
-
console.log("");
|
|
10027
|
-
if (stores.length === 0) {
|
|
10028
|
-
console.log(chalk.gray(" No storage buckets attached."));
|
|
11355
|
+
emit(() => {
|
|
10029
11356
|
console.log("");
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
11357
|
+
console.log(chalk.bold(` 🪣 Storage — project ${displayProjectRef(rawArgs)}`));
|
|
11358
|
+
console.log("");
|
|
11359
|
+
if (stores.length === 0) {
|
|
11360
|
+
console.log(chalk.gray(" No storage buckets attached."));
|
|
11361
|
+
console.log("");
|
|
11362
|
+
return;
|
|
11363
|
+
}
|
|
11364
|
+
for (const s of stores) {
|
|
11365
|
+
console.log(` ${chalk.bold(s.bucketName ?? s.type ?? "bucket")} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
|
|
11366
|
+
keyValues([["Provider", s.provider], ["Type", s.type]]);
|
|
11367
|
+
}
|
|
11368
|
+
console.log("");
|
|
11369
|
+
}, {
|
|
11370
|
+
projectId,
|
|
11371
|
+
stores: stores.map((s) => ({
|
|
11372
|
+
id: String(s.id),
|
|
11373
|
+
bucketName: s.bucketName ?? null,
|
|
11374
|
+
type: s.type ?? null,
|
|
11375
|
+
provider: s.provider ?? null,
|
|
11376
|
+
status: s.status ?? null
|
|
11377
|
+
}))
|
|
11378
|
+
});
|
|
10037
11379
|
} catch (e) {
|
|
10038
11380
|
reportError(e, "Failed to list storage");
|
|
10039
11381
|
}
|
|
10040
11382
|
}
|
|
10041
11383
|
function printStorageHelp() {
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
11384
|
+
emitHelp("storage", [
|
|
11385
|
+
"list",
|
|
11386
|
+
"create",
|
|
11387
|
+
"attach"
|
|
11388
|
+
], () => {
|
|
11389
|
+
console.log("");
|
|
11390
|
+
console.log(chalk.bold(" rebase cloud storage"));
|
|
11391
|
+
console.log("");
|
|
11392
|
+
console.log(" " + chalk.blue.bold("storage") + " List this project's storage");
|
|
11393
|
+
console.log(" " + chalk.blue.bold("storage create") + " Provision platform-managed storage");
|
|
11394
|
+
console.log(" " + chalk.blue.bold("storage attach") + " Attach your own S3-compatible bucket");
|
|
11395
|
+
console.log("");
|
|
11396
|
+
console.log(chalk.gray(" attach options:"));
|
|
11397
|
+
console.log(chalk.gray(" --bucket <name> Bucket name (required)"));
|
|
11398
|
+
console.log(chalk.gray(" --access-key-id <id> Access key ID (required)"));
|
|
11399
|
+
console.log(chalk.gray(" --secret-access-key <s> Secret access key (required)"));
|
|
11400
|
+
console.log(chalk.gray(" --endpoint <url> S3 endpoint; omit for AWS"));
|
|
11401
|
+
console.log(chalk.gray(" --region <region> Region"));
|
|
11402
|
+
console.log(chalk.gray(" --force-path-style Required by MinIO and some gateways"));
|
|
11403
|
+
console.log("");
|
|
11404
|
+
console.log(chalk.gray(" Without either, file storage stays off: uploads are refused with"));
|
|
11405
|
+
console.log(chalk.gray(" 501 STORAGE_NOT_CONFIGURED rather than written to a container"));
|
|
11406
|
+
console.log(chalk.gray(" filesystem that is erased on the next restart."));
|
|
11407
|
+
console.log("");
|
|
11408
|
+
});
|
|
10061
11409
|
}
|
|
10062
11410
|
async function storageCreateCommand(rawArgs) {
|
|
11411
|
+
parseCloudArgs({
|
|
11412
|
+
spec: {},
|
|
11413
|
+
rawArgs,
|
|
11414
|
+
commandWords: 3,
|
|
11415
|
+
command: "cloud storage create",
|
|
11416
|
+
maxPositionals: 0
|
|
11417
|
+
});
|
|
10063
11418
|
const { client } = await requireClient(rawArgs);
|
|
10064
11419
|
const projectId = await requireProject(rawArgs, client);
|
|
10065
11420
|
try {
|
|
10066
|
-
|
|
10067
|
-
|
|
11421
|
+
noteBlank();
|
|
11422
|
+
note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
|
|
10068
11423
|
const res = await client.functions.invoke(`storage-provision/${encodeURIComponent(projectId)}`, void 0, { method: "POST" });
|
|
10069
11424
|
const info = res.data ?? res.data;
|
|
10070
11425
|
success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
|
|
10071
|
-
|
|
10072
|
-
[
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
11426
|
+
emit(() => {
|
|
11427
|
+
keyValues([
|
|
11428
|
+
["Bucket", info.bucketName],
|
|
11429
|
+
["Region", info.region],
|
|
11430
|
+
["Endpoint", info.endpoint],
|
|
11431
|
+
["Access key", info.accessKeyId]
|
|
11432
|
+
]);
|
|
11433
|
+
noteBlank();
|
|
11434
|
+
note(chalk.gray("The secret key is stored encrypted and injected at deploy time; it is not displayed."));
|
|
11435
|
+
note(chalk.gray("Redeploy for the tenant to pick it up: ") + chalk.bold("rebase cloud deploy"));
|
|
11436
|
+
noteBlank();
|
|
11437
|
+
}, {
|
|
11438
|
+
success: true,
|
|
11439
|
+
projectId,
|
|
11440
|
+
bucketName: info.bucketName,
|
|
11441
|
+
region: info.region,
|
|
11442
|
+
endpoint: info.endpoint,
|
|
11443
|
+
accessKeyId: info.accessKeyId,
|
|
11444
|
+
secretAccessKey: null,
|
|
11445
|
+
pendingRedeploy: true
|
|
11446
|
+
});
|
|
10081
11447
|
} catch (e) {
|
|
10082
11448
|
reportError(e, "Failed to provision managed storage");
|
|
10083
11449
|
}
|
|
10084
11450
|
}
|
|
10085
11451
|
async function storageAttachCommand(rawArgs) {
|
|
10086
|
-
const parsed =
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
11452
|
+
const { flags: parsed } = parseCloudArgs({
|
|
11453
|
+
spec: {
|
|
11454
|
+
"--bucket": String,
|
|
11455
|
+
"--access-key-id": String,
|
|
11456
|
+
"--secret-access-key": String,
|
|
11457
|
+
"--endpoint": String,
|
|
11458
|
+
"--region": String,
|
|
11459
|
+
"--force-path-style": Boolean
|
|
11460
|
+
},
|
|
11461
|
+
rawArgs,
|
|
11462
|
+
commandWords: 3,
|
|
11463
|
+
command: "cloud storage attach",
|
|
11464
|
+
maxPositionals: 0
|
|
10096
11465
|
});
|
|
10097
11466
|
const bucket = parsed["--bucket"];
|
|
10098
11467
|
const accessKeyId = parsed["--access-key-id"];
|
|
@@ -10102,7 +11471,7 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10102
11471
|
!accessKeyId && "--access-key-id",
|
|
10103
11472
|
!secretAccessKey && "--secret-access-key"
|
|
10104
11473
|
].filter(Boolean);
|
|
10105
|
-
if (missing.length > 0) fail(`Missing ${missing.join(", ")}.`, "A bucket without credentials cannot be used, and would be stored as though it could. Run `rebase cloud storage --help` for the full list.");
|
|
11474
|
+
if (missing.length > 0) fail(`Missing ${missing.join(", ")}.`, "A bucket without credentials cannot be used, and would be stored as though it could. Run `rebase cloud storage --help` for the full list.", "usage");
|
|
10106
11475
|
const { client } = await requireClient(rawArgs);
|
|
10107
11476
|
const projectId = await requireProject(rawArgs, client);
|
|
10108
11477
|
try {
|
|
@@ -10125,17 +11494,29 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10125
11494
|
row.region = parsed["--region"];
|
|
10126
11495
|
}
|
|
10127
11496
|
if (parsed["--force-path-style"]) row.s3ForcePathStyle = true;
|
|
11497
|
+
const replaced = Boolean(existing?.id);
|
|
10128
11498
|
if (existing?.id) await client.data.collection("storages").update(String(existing.id), row);
|
|
10129
11499
|
else await client.data.collection("storages").create(row);
|
|
10130
11500
|
success(`Storage attached to ${displayProjectRef(rawArgs)}.`);
|
|
10131
|
-
|
|
10132
|
-
[
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
11501
|
+
emit(() => {
|
|
11502
|
+
keyValues([
|
|
11503
|
+
["Bucket", bucket],
|
|
11504
|
+
["Endpoint", parsed["--endpoint"] ?? "AWS S3"],
|
|
11505
|
+
["Region", parsed["--region"] ?? "(default)"]
|
|
11506
|
+
]);
|
|
11507
|
+
noteBlank();
|
|
11508
|
+
note(chalk.gray("Redeploy for the tenant to pick it up: ") + chalk.bold("rebase cloud deploy"));
|
|
11509
|
+
noteBlank();
|
|
11510
|
+
}, {
|
|
11511
|
+
success: true,
|
|
11512
|
+
projectId,
|
|
11513
|
+
bucket,
|
|
11514
|
+
endpoint: parsed["--endpoint"] ?? null,
|
|
11515
|
+
region: parsed["--region"] ?? null,
|
|
11516
|
+
forcePathStyle: Boolean(parsed["--force-path-style"]),
|
|
11517
|
+
replaced,
|
|
11518
|
+
pendingRedeploy: true
|
|
11519
|
+
});
|
|
10139
11520
|
} catch (e) {
|
|
10140
11521
|
reportError(e, "Failed to attach storage");
|
|
10141
11522
|
}
|
|
@@ -10144,40 +11525,55 @@ async function clustersCommand(rawArgs) {
|
|
|
10144
11525
|
const { client } = await requireClient(rawArgs);
|
|
10145
11526
|
try {
|
|
10146
11527
|
const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
|
|
10147
|
-
|
|
10148
|
-
console.log(chalk.bold(" ☸ Clusters"));
|
|
10149
|
-
console.log("");
|
|
10150
|
-
if (clusters.length === 0) {
|
|
10151
|
-
console.log(chalk.gray(" No clusters registered."));
|
|
11528
|
+
emit(() => {
|
|
10152
11529
|
console.log("");
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
11530
|
+
console.log(chalk.bold(" ☸ Clusters"));
|
|
11531
|
+
console.log("");
|
|
11532
|
+
if (clusters.length === 0) {
|
|
11533
|
+
console.log(chalk.gray(" No clusters registered."));
|
|
11534
|
+
console.log("");
|
|
11535
|
+
return;
|
|
11536
|
+
}
|
|
11537
|
+
for (const c of clusters) {
|
|
11538
|
+
console.log(` ${chalk.bold(c.name ?? "(unnamed)")} ${chalk.gray(`[${c.id}]`)} ${colorStatus(c.status)}`);
|
|
11539
|
+
keyValues([["Provider", c.provider], ["Region", c.region]]);
|
|
11540
|
+
}
|
|
11541
|
+
console.log("");
|
|
11542
|
+
}, { clusters: clusters.map((c) => ({
|
|
11543
|
+
id: String(c.id),
|
|
11544
|
+
name: c.name ?? null,
|
|
11545
|
+
provider: c.provider ?? null,
|
|
11546
|
+
region: c.region ?? null,
|
|
11547
|
+
status: c.status ?? null
|
|
11548
|
+
})) });
|
|
10160
11549
|
} catch (e) {
|
|
10161
11550
|
reportError(e, "Failed to list clusters");
|
|
10162
11551
|
}
|
|
10163
11552
|
}
|
|
10164
11553
|
async function billingCommand(rawArgs) {
|
|
11554
|
+
const action = parseCloudArgs({
|
|
11555
|
+
spec: {},
|
|
11556
|
+
rawArgs,
|
|
11557
|
+
commandWords: 2,
|
|
11558
|
+
command: "cloud billing",
|
|
11559
|
+
maxPositionals: 1
|
|
11560
|
+
}).positionals[0];
|
|
10165
11561
|
const { client, url } = await requireClient(rawArgs);
|
|
10166
11562
|
const org = getContextOrg(url);
|
|
10167
|
-
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
10168
11563
|
if (action === "setup") {
|
|
10169
|
-
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
11564
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
10170
11565
|
try {
|
|
10171
11566
|
const res = await client.functions.invoke("stripe-billing", { organizationId: org }, { path: "setup-session" });
|
|
10172
|
-
if (!res.url) fail("Could not start billing setup.");
|
|
11567
|
+
if (!res.url) fail("Could not start billing setup.", void 0, "billing_setup_failed");
|
|
10173
11568
|
openUrl(res.url, "Add a payment method in your browser:");
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
}
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
11569
|
+
emit(() => {
|
|
11570
|
+
note(chalk.gray(res.simulated ? "(dev mode — Stripe not configured; complete setup from the console)" : "Once you've added a card, `rebase cloud deploy` runs without further prompts."));
|
|
11571
|
+
noteBlank();
|
|
11572
|
+
}, {
|
|
11573
|
+
url: res.url,
|
|
11574
|
+
org,
|
|
11575
|
+
simulated: Boolean(res.simulated)
|
|
11576
|
+
});
|
|
10181
11577
|
} catch (e) {
|
|
10182
11578
|
reportError(e, "Failed to start billing setup");
|
|
10183
11579
|
}
|
|
@@ -10187,24 +11583,36 @@ async function billingCommand(rawArgs) {
|
|
|
10187
11583
|
const projectId = await requireProject(rawArgs, client);
|
|
10188
11584
|
try {
|
|
10189
11585
|
const res = await client.functions.invoke("stripe-billing", { projectId }, { path: "session" });
|
|
10190
|
-
if (!res.url) fail("Billing session could not be created.");
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
11586
|
+
if (!res.url) fail("Billing session could not be created.", void 0, "checkout_failed");
|
|
11587
|
+
emit(() => {
|
|
11588
|
+
console.log("");
|
|
11589
|
+
console.log(" Complete checkout in your browser:");
|
|
11590
|
+
console.log(` ${chalk.cyan(res.url)}`);
|
|
11591
|
+
console.log("");
|
|
11592
|
+
}, {
|
|
11593
|
+
url: res.url,
|
|
11594
|
+
projectId
|
|
11595
|
+
});
|
|
10195
11596
|
} catch (e) {
|
|
10196
11597
|
reportError(e, "Failed to start checkout");
|
|
10197
11598
|
}
|
|
10198
11599
|
return;
|
|
10199
11600
|
}
|
|
10200
|
-
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
11601
|
+
if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
|
|
10201
11602
|
try {
|
|
10202
11603
|
const orgRow = await client.data.collection("organizations").findById(org);
|
|
10203
11604
|
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
10204
11605
|
if (!billingId) {
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
11606
|
+
emit(() => {
|
|
11607
|
+
console.log("");
|
|
11608
|
+
console.log(chalk.gray(` Organization ${org} has no billing account yet.`));
|
|
11609
|
+
console.log("");
|
|
11610
|
+
}, {
|
|
11611
|
+
org,
|
|
11612
|
+
account: null,
|
|
11613
|
+
plan: null,
|
|
11614
|
+
paymentMethod: null
|
|
11615
|
+
});
|
|
10208
11616
|
return;
|
|
10209
11617
|
}
|
|
10210
11618
|
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
@@ -10237,17 +11645,34 @@ async function billingCommand(rawArgs) {
|
|
|
10237
11645
|
} catch {}
|
|
10238
11646
|
}
|
|
10239
11647
|
} catch {}
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
[
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
11648
|
+
emit(() => {
|
|
11649
|
+
console.log("");
|
|
11650
|
+
console.log(chalk.bold(` 💳 Billing — org ${org}`));
|
|
11651
|
+
console.log("");
|
|
11652
|
+
keyValues([
|
|
11653
|
+
["Account", acct ? String(acct.id) : void 0],
|
|
11654
|
+
["Email", acct?.billingEmail],
|
|
11655
|
+
["Status", acct?.status ? colorStatus(acct.status) : void 0],
|
|
11656
|
+
["Plan", plan],
|
|
11657
|
+
["Payment method", card.hasPaymentMethod ? `${card.brand ?? "card"} •••• ${card.last4 ?? "????"}${card.expMonth ? ` (exp ${card.expMonth}/${card.expYear})` : ""}` : chalk.yellow("none — run `rebase cloud billing setup`")]
|
|
11658
|
+
]);
|
|
11659
|
+
console.log("");
|
|
11660
|
+
}, {
|
|
11661
|
+
org,
|
|
11662
|
+
account: acct ? {
|
|
11663
|
+
id: String(acct.id),
|
|
11664
|
+
billingEmail: acct.billingEmail ?? null,
|
|
11665
|
+
status: acct.status ?? null
|
|
11666
|
+
} : null,
|
|
11667
|
+
plan: plan ?? null,
|
|
11668
|
+
paymentMethod: {
|
|
11669
|
+
hasPaymentMethod: Boolean(card.hasPaymentMethod),
|
|
11670
|
+
brand: card.brand ?? null,
|
|
11671
|
+
last4: card.last4 ?? null,
|
|
11672
|
+
expMonth: card.expMonth ?? null,
|
|
11673
|
+
expYear: card.expYear ?? null
|
|
11674
|
+
}
|
|
11675
|
+
});
|
|
10251
11676
|
} catch (e) {
|
|
10252
11677
|
reportError(e, "Failed to load billing");
|
|
10253
11678
|
}
|
|
@@ -10293,15 +11718,43 @@ function positionals(rawArgs) {
|
|
|
10293
11718
|
while (i < rest.length && rest[i].startsWith("-")) i++;
|
|
10294
11719
|
return rest.slice(i);
|
|
10295
11720
|
}
|
|
11721
|
+
/**
|
|
11722
|
+
* The help page for each group, keyed by every alias the dispatch below accepts.
|
|
11723
|
+
*
|
|
11724
|
+
* Aliases are listed explicitly rather than normalised first, so a group that
|
|
11725
|
+
* gains one and forgets it here degrades to the index page — wrong, but a page.
|
|
11726
|
+
* `cloud-help.test.ts` asserts the two stay in step.
|
|
11727
|
+
*
|
|
11728
|
+
* A group absent from this map has no page of its own; the index lists it.
|
|
11729
|
+
*/
|
|
11730
|
+
var GROUP_HELP = {
|
|
11731
|
+
env: printEnvHelp,
|
|
11732
|
+
domains: printDomainsHelp,
|
|
11733
|
+
domain: printDomainsHelp,
|
|
11734
|
+
extensions: printExtensionsHelp,
|
|
11735
|
+
extension: printExtensionsHelp,
|
|
11736
|
+
settings: printSettingsHelp,
|
|
11737
|
+
orgs: printOrgsHelp,
|
|
11738
|
+
org: printOrgsHelp,
|
|
11739
|
+
db: printDbHelp,
|
|
11740
|
+
database: printDbHelp,
|
|
11741
|
+
debug: printDebugHelp,
|
|
11742
|
+
storage: printStorageHelp
|
|
11743
|
+
};
|
|
10296
11744
|
async function cloudCommand(subcommand, rawArgs) {
|
|
10297
11745
|
initOutputMode(rawArgs);
|
|
10298
11746
|
const pos = positionals(rawArgs);
|
|
10299
11747
|
const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
|
|
11748
|
+
const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
10300
11749
|
const action = pos[1];
|
|
10301
|
-
if (!group
|
|
11750
|
+
if (!group) {
|
|
10302
11751
|
printCloudHelp();
|
|
10303
11752
|
return;
|
|
10304
11753
|
}
|
|
11754
|
+
if (wantsHelp) {
|
|
11755
|
+
(GROUP_HELP[group] ?? printCloudHelp)();
|
|
11756
|
+
return;
|
|
11757
|
+
}
|
|
10305
11758
|
switch (group) {
|
|
10306
11759
|
case "login":
|
|
10307
11760
|
await loginCommand(rawArgs);
|
|
@@ -10392,11 +11845,7 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
10392
11845
|
case "billing":
|
|
10393
11846
|
await billingCommand(rawArgs);
|
|
10394
11847
|
break;
|
|
10395
|
-
default:
|
|
10396
|
-
console.error(chalk.red(`Unknown cloud command: ${group}`));
|
|
10397
|
-
console.log("");
|
|
10398
|
-
printCloudHelp();
|
|
10399
|
-
process.exit(1);
|
|
11848
|
+
default: fail(`Unknown cloud command: ${group}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10400
11849
|
}
|
|
10401
11850
|
}
|
|
10402
11851
|
async function projectsGroup(action, rawArgs) {
|
|
@@ -10409,17 +11858,15 @@ async function projectsGroup(action, rawArgs) {
|
|
|
10409
11858
|
await createProject(rawArgs);
|
|
10410
11859
|
break;
|
|
10411
11860
|
case "info":
|
|
10412
|
-
await projectInfo(rawArgs,
|
|
11861
|
+
await projectInfo(rawArgs, resolveProjectArg(rawArgs, "info"));
|
|
10413
11862
|
break;
|
|
10414
11863
|
case "delete":
|
|
10415
|
-
await deleteProject(rawArgs,
|
|
11864
|
+
await deleteProject(rawArgs, resolveProjectArg(rawArgs, "delete"));
|
|
10416
11865
|
break;
|
|
10417
11866
|
case "--help":
|
|
10418
11867
|
printCloudHelp();
|
|
10419
11868
|
break;
|
|
10420
|
-
default:
|
|
10421
|
-
console.error(chalk.red(`Unknown projects command: ${action}`));
|
|
10422
|
-
process.exit(1);
|
|
11869
|
+
default: fail(`Unknown projects command: ${action}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10423
11870
|
}
|
|
10424
11871
|
}
|
|
10425
11872
|
async function deploymentsGroup(action, rawArgs) {
|
|
@@ -10431,13 +11878,51 @@ async function deploymentsGroup(action, rawArgs) {
|
|
|
10431
11878
|
case "--help":
|
|
10432
11879
|
printCloudHelp();
|
|
10433
11880
|
break;
|
|
10434
|
-
default:
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
11881
|
+
default: fail(`Unknown deployments command: ${action}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
/**
|
|
11885
|
+
* Every group `cloudCommand` dispatches, canonical name first.
|
|
11886
|
+
*
|
|
11887
|
+
* This is the index page's JSON form, and the list an agent discovers the
|
|
11888
|
+
* family from. `cloud-help.test.ts` holds it to the dispatch switch, so a group
|
|
11889
|
+
* added there without being added here is a test failure rather than a
|
|
11890
|
+
* command that exists but cannot be found.
|
|
11891
|
+
*/
|
|
11892
|
+
var CLOUD_GROUPS = [
|
|
11893
|
+
"login",
|
|
11894
|
+
"logout",
|
|
11895
|
+
"whoami",
|
|
11896
|
+
"link",
|
|
11897
|
+
"unlink",
|
|
11898
|
+
"use",
|
|
11899
|
+
"open",
|
|
11900
|
+
"projects",
|
|
11901
|
+
"deploy",
|
|
11902
|
+
"logs",
|
|
11903
|
+
"deployments",
|
|
11904
|
+
"rollback",
|
|
11905
|
+
"cancel",
|
|
11906
|
+
"start",
|
|
11907
|
+
"stop",
|
|
11908
|
+
"restart",
|
|
11909
|
+
"status",
|
|
11910
|
+
"metrics",
|
|
11911
|
+
"debug",
|
|
11912
|
+
"env",
|
|
11913
|
+
"domains",
|
|
11914
|
+
"extensions",
|
|
11915
|
+
"settings",
|
|
11916
|
+
"orgs",
|
|
11917
|
+
"db",
|
|
11918
|
+
"webhooks",
|
|
11919
|
+
"storage",
|
|
11920
|
+
"clusters",
|
|
11921
|
+
"billing"
|
|
11922
|
+
];
|
|
10439
11923
|
function printCloudHelp() {
|
|
10440
|
-
|
|
11924
|
+
emitHelp("cloud", CLOUD_GROUPS, () => {
|
|
11925
|
+
console.log(`
|
|
10441
11926
|
${chalk.bold("rebase cloud")} — Manage your apps on Rebase Cloud
|
|
10442
11927
|
|
|
10443
11928
|
${chalk.green.bold("Usage")}
|
|
@@ -10502,6 +11987,7 @@ ${chalk.green.bold("Global options")}
|
|
|
10502
11987
|
${chalk.gray("Most commands act on the linked project (.rebase/cloud.json) unless --project is given.")}
|
|
10503
11988
|
${chalk.gray("Docs: https://rebase.pro/docs")}
|
|
10504
11989
|
`);
|
|
11990
|
+
});
|
|
10505
11991
|
}
|
|
10506
11992
|
//#endregion
|
|
10507
11993
|
//#region src/commands/apps.ts
|
|
@@ -10534,19 +12020,20 @@ ${chalk.bold("Options")}
|
|
|
10534
12020
|
`.trim());
|
|
10535
12021
|
}
|
|
10536
12022
|
async function appsCommand(subcommand, rawArgs = []) {
|
|
10537
|
-
|
|
10538
|
-
"--json": Boolean,
|
|
10539
|
-
"--force": Boolean,
|
|
10540
|
-
"--help": Boolean,
|
|
10541
|
-
"-h": "--help"
|
|
10542
|
-
}, {
|
|
10543
|
-
argv: rawArgs.slice(3),
|
|
10544
|
-
permissive: true
|
|
10545
|
-
});
|
|
10546
|
-
if (args["--help"] || !subcommand || subcommand === "--help") {
|
|
12023
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
10547
12024
|
printHelp$1();
|
|
10548
12025
|
return;
|
|
10549
12026
|
}
|
|
12027
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
12028
|
+
spec: {
|
|
12029
|
+
"--json": Boolean,
|
|
12030
|
+
"--force": Boolean
|
|
12031
|
+
},
|
|
12032
|
+
rawArgs,
|
|
12033
|
+
commandWords: 1,
|
|
12034
|
+
command: "apps",
|
|
12035
|
+
maxPositionals: 2
|
|
12036
|
+
});
|
|
10550
12037
|
switch (subcommand) {
|
|
10551
12038
|
case "list":
|
|
10552
12039
|
await listApps(Boolean(args["--json"]));
|
|
@@ -10555,7 +12042,7 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
10555
12042
|
await initManifest(Boolean(args["--force"]));
|
|
10556
12043
|
break;
|
|
10557
12044
|
case "config":
|
|
10558
|
-
await printAppConfig(
|
|
12045
|
+
await printAppConfig(positionals[1], Boolean(args["--json"]));
|
|
10559
12046
|
break;
|
|
10560
12047
|
default:
|
|
10561
12048
|
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
@@ -10704,7 +12191,25 @@ function getVersion() {
|
|
|
10704
12191
|
} catch {}
|
|
10705
12192
|
return "unknown";
|
|
10706
12193
|
}
|
|
12194
|
+
/**
|
|
12195
|
+
* Silence dotenv's own banner, for this process and everything it spawns.
|
|
12196
|
+
*
|
|
12197
|
+
* dotenv 17 prints `injected env (13) from .env // tip: ◈ encrypted .env
|
|
12198
|
+
* [www.dotenvx.com]` on every `config()` — a third-party advertisement that
|
|
12199
|
+
* appeared in `rebase dev`, `rebase build` and, because `rebase start` loads
|
|
12200
|
+
* the same way, in production server logs. `DOTENV_CONFIG_QUIET` is dotenv's
|
|
12201
|
+
* documented switch (`lib/main.js` reads it before `options.quiet`), and
|
|
12202
|
+
* setting it in `process.env` also reaches the backend, Vite and Atlas child
|
|
12203
|
+
* processes, which inherit it. Errors are unaffected — `quiet` gates the
|
|
12204
|
+
* success banner only.
|
|
12205
|
+
*
|
|
12206
|
+
* Not forced: an explicit `DOTENV_CONFIG_QUIET=false` still turns it back on.
|
|
12207
|
+
*/
|
|
12208
|
+
function silenceDotenvBanner() {
|
|
12209
|
+
if (process.env.DOTENV_CONFIG_QUIET === void 0) process.env.DOTENV_CONFIG_QUIET = "true";
|
|
12210
|
+
}
|
|
10707
12211
|
async function entry(args) {
|
|
12212
|
+
silenceDotenvBanner();
|
|
10708
12213
|
const parsedArgs = arg({
|
|
10709
12214
|
"--version": Boolean,
|
|
10710
12215
|
"--help": Boolean,
|
|
@@ -10741,7 +12246,7 @@ async function entry(args) {
|
|
|
10741
12246
|
printHelp();
|
|
10742
12247
|
return;
|
|
10743
12248
|
}
|
|
10744
|
-
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
12249
|
+
const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
|
|
10745
12250
|
switch (command) {
|
|
10746
12251
|
case "init":
|
|
10747
12252
|
await createRebaseApp(args);
|
|
@@ -10860,9 +12365,11 @@ ${chalk.green.bold("API Keys")}
|
|
|
10860
12365
|
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10861
12366
|
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10862
12367
|
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
10863
|
-
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
10864
12368
|
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
10865
12369
|
|
|
12370
|
+
${chalk.green.bold("Usage sharing")}
|
|
12371
|
+
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
12372
|
+
|
|
10866
12373
|
${chalk.green.bold("Rebase Cloud")}
|
|
10867
12374
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
10868
12375
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
@@ -10893,6 +12400,6 @@ function telemetryNotice() {
|
|
|
10893
12400
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
10894
12401
|
}
|
|
10895
12402
|
//#endregion
|
|
10896
|
-
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveStartPort, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
12403
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_FLAGS, DEV_PORT_FILENAME, INIT_FLAGS, MANIFEST_FILENAME, ManifestError, RESET_PASSWORD_FLAGS, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, isPortAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, readEnvFile, renderPayload, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveResetPasswordArgs, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
10897
12404
|
|
|
10898
12405
|
//# sourceMappingURL=index.es.js.map
|