@rebasepro/cli 0.13.1-canary.gf57a27e → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.d.ts +4 -3
- package/dist/commands/api-keys.d.ts +51 -0
- package/dist/commands/auth.d.ts +31 -0
- package/dist/commands/cloud/context.d.ts +143 -10
- package/dist/commands/cloud/databases.d.ts +38 -0
- package/dist/commands/cloud/deployments.d.ts +22 -0
- package/dist/commands/cloud/domains.d.ts +9 -0
- package/dist/commands/cloud/env.d.ts +50 -0
- package/dist/commands/cloud/extensions.d.ts +20 -0
- package/dist/commands/cloud/projects.d.ts +29 -0
- package/dist/commands/cloud/resources.d.ts +13 -0
- package/dist/commands/dev.d.ts +18 -0
- package/dist/commands/eject.d.ts +42 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/skills.d.ts +81 -0
- package/dist/index.es.js +1741 -654
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +16 -1
- package/dist/utils/args.d.ts +76 -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 +9 -1
- 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/backend/functions/hello.ts +8 -4
- package/templates/template/backend/tsconfig.json +6 -1
- package/templates/template/frontend/vite.config.ts +33 -2
package/dist/index.es.js
CHANGED
|
@@ -14,7 +14,7 @@ import os from "os";
|
|
|
14
14
|
import { createRebaseClient } from "@rebasepro/client";
|
|
15
15
|
import dotenv from "dotenv";
|
|
16
16
|
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
|
-
import { generateSDK } from "@rebasepro/codegen";
|
|
17
|
+
import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
|
|
18
18
|
import { createRequire } from "module";
|
|
19
19
|
//#region src/utils/package-manager.ts
|
|
20
20
|
/**
|
|
@@ -191,6 +191,90 @@ function getPMCommands(pm) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
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
|
|
194
278
|
//#region src/utils/project.ts
|
|
195
279
|
/**
|
|
196
280
|
* Project discovery utilities for the Rebase CLI.
|
|
@@ -526,7 +610,7 @@ function normalizeUrl(url) {
|
|
|
526
610
|
function createCloudClient(url) {
|
|
527
611
|
return createRebaseClient({
|
|
528
612
|
baseUrl: url,
|
|
529
|
-
|
|
613
|
+
realtime: false,
|
|
530
614
|
auth: {
|
|
531
615
|
storage: createFileAuthStorage(url),
|
|
532
616
|
persistSession: true,
|
|
@@ -545,11 +629,11 @@ async function requireClient(rawArgs) {
|
|
|
545
629
|
const url = resolveCloudUrl(rawArgs);
|
|
546
630
|
const client = createCloudClient(url);
|
|
547
631
|
const session = client.auth.getSession();
|
|
548
|
-
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");
|
|
549
633
|
if (session.expiresAt <= Date.now() + EXPIRY_BUFFER_MS) try {
|
|
550
634
|
await client.auth.refreshSession();
|
|
551
635
|
} catch {
|
|
552
|
-
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");
|
|
553
637
|
}
|
|
554
638
|
return {
|
|
555
639
|
client,
|
|
@@ -701,7 +785,7 @@ function requireProjectRef(rawArgs) {
|
|
|
701
785
|
if (parsed["--project"]) return parsed["--project"];
|
|
702
786
|
const link = readLink();
|
|
703
787
|
if (link?.projectId) return link.projectId;
|
|
704
|
-
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");
|
|
705
789
|
}
|
|
706
790
|
/**
|
|
707
791
|
* Resolve a project reference — slug or UUID — to the internal id the API
|
|
@@ -720,7 +804,7 @@ async function lookupProjectId(ref, client) {
|
|
|
720
804
|
/** Like `lookupProjectId`, but exits with guidance when the ref matches nothing. */
|
|
721
805
|
async function resolveProjectRef(ref, client) {
|
|
722
806
|
const id = await lookupProjectId(ref, client);
|
|
723
|
-
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");
|
|
724
808
|
return id;
|
|
725
809
|
}
|
|
726
810
|
/** `requireProjectRef` + `resolveProjectRef` in one step. */
|
|
@@ -789,6 +873,28 @@ function emit(human, json) {
|
|
|
789
873
|
else human();
|
|
790
874
|
}
|
|
791
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
|
+
/**
|
|
792
898
|
* Print a warning (+ optional hint) — in every output mode, always to stderr.
|
|
793
899
|
*
|
|
794
900
|
* `emit` is for a command's *result*, and JSON mode legitimately replaces the
|
|
@@ -817,12 +923,27 @@ function warn(message, hint) {
|
|
|
817
923
|
console.error(chalk.yellow(` ⚠ ${message}`));
|
|
818
924
|
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
819
925
|
}
|
|
820
|
-
/**
|
|
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
|
+
*/
|
|
821
942
|
function fail(message, hint, code) {
|
|
822
943
|
if (JSON_MODE) {
|
|
823
944
|
printJson({ error: {
|
|
824
945
|
message: stripAnsi(message),
|
|
825
|
-
code: code ??
|
|
946
|
+
code: code ?? "error",
|
|
826
947
|
hint: hint ? stripAnsi(hint) : void 0
|
|
827
948
|
} });
|
|
828
949
|
process.exit(1);
|
|
@@ -851,26 +972,139 @@ async function confirmDestructive(opts) {
|
|
|
851
972
|
message: opts.prompt
|
|
852
973
|
}]);
|
|
853
974
|
if (!confirmed) {
|
|
854
|
-
console.
|
|
975
|
+
console.error(chalk.gray(" Aborted."));
|
|
855
976
|
process.exit(0);
|
|
856
977
|
}
|
|
857
978
|
}
|
|
858
979
|
/**
|
|
859
|
-
*
|
|
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 …".
|
|
1063
|
+
*
|
|
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.
|
|
860
1069
|
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
* `--yes` as the deployment id. Operand extraction must see operands only, so
|
|
864
|
-
* anything starting with `-` is dropped — the same filter the db backup handler
|
|
865
|
-
* has always used.
|
|
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.
|
|
866
1072
|
*/
|
|
867
|
-
function cloudPositionals(rawArgs) {
|
|
868
|
-
return rawArgs.slice(3).filter((a) => !a.startsWith("-"));
|
|
869
|
-
}
|
|
870
1073
|
function success(message) {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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("");
|
|
874
1108
|
}
|
|
875
1109
|
/** Colorize a deployment / resource status token. */
|
|
876
1110
|
function colorStatus(status) {
|
|
@@ -908,7 +1142,7 @@ function reportError(e, context) {
|
|
|
908
1142
|
if (JSON_MODE) {
|
|
909
1143
|
printJson({ error: {
|
|
910
1144
|
message: err?.message ? stripAnsi(err.message) : String(e),
|
|
911
|
-
code: err?.code ??
|
|
1145
|
+
code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
|
|
912
1146
|
status: err?.status ?? null,
|
|
913
1147
|
context
|
|
914
1148
|
} });
|
|
@@ -917,13 +1151,18 @@ function reportError(e, context) {
|
|
|
917
1151
|
fail(`${context}${err?.status ? ` (${err.status})` : ""}: ${err?.message ?? String(e)}`);
|
|
918
1152
|
}
|
|
919
1153
|
/**
|
|
920
|
-
* Open a URL in the user's default browser (best effort). Always
|
|
921
|
-
* 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.
|
|
922
1161
|
*/
|
|
923
1162
|
function openUrl(target, label = "Opening") {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1163
|
+
noteBlank();
|
|
1164
|
+
note(`${label} ${chalk.cyan(target)}`);
|
|
1165
|
+
noteBlank();
|
|
927
1166
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
928
1167
|
try {
|
|
929
1168
|
const child = spawn(opener, [target], {
|
|
@@ -1506,7 +1745,7 @@ ${chalk.bold("Examples")}
|
|
|
1506
1745
|
`);
|
|
1507
1746
|
}
|
|
1508
1747
|
async function createRebaseApp(rawArgs) {
|
|
1509
|
-
if (
|
|
1748
|
+
if (wantsHelp(rawArgs)) {
|
|
1510
1749
|
printInitHelp();
|
|
1511
1750
|
return;
|
|
1512
1751
|
}
|
|
@@ -1515,26 +1754,31 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
|
|
|
1515
1754
|
`);
|
|
1516
1755
|
await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
|
|
1517
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
|
+
};
|
|
1518
1773
|
async function promptForOptions(rawArgs, pm) {
|
|
1519
|
-
const args =
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
"
|
|
1524
|
-
|
|
1525
|
-
"--headless": Boolean,
|
|
1526
|
-
"--project": String,
|
|
1527
|
-
"--setup-key": String,
|
|
1528
|
-
"--yes": Boolean,
|
|
1529
|
-
"-g": "--git",
|
|
1530
|
-
"-i": "--install",
|
|
1531
|
-
"-t": "--template",
|
|
1532
|
-
"-y": "--yes"
|
|
1533
|
-
}, {
|
|
1534
|
-
argv: rawArgs.slice(3),
|
|
1535
|
-
permissive: true
|
|
1774
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
1775
|
+
spec: INIT_FLAGS,
|
|
1776
|
+
rawArgs,
|
|
1777
|
+
commandWords: 1,
|
|
1778
|
+
command: "init",
|
|
1779
|
+
maxPositionals: 1
|
|
1536
1780
|
});
|
|
1537
|
-
const nameArg =
|
|
1781
|
+
const nameArg = positionals[0];
|
|
1538
1782
|
const isNonInteractive = args["--yes"] || false;
|
|
1539
1783
|
if (nameArg) {
|
|
1540
1784
|
const resolvedName = path.basename(path.resolve(process.cwd(), nameArg));
|
|
@@ -2154,6 +2398,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2154
2398
|
const envPath = path.join(targetDirectory, ".env");
|
|
2155
2399
|
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
2156
2400
|
fs.copyFileSync(envExamplePath, envPath);
|
|
2401
|
+
fs.chmodSync(envPath, 384);
|
|
2157
2402
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
2158
2403
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
2159
2404
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
@@ -2186,7 +2431,11 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2186
2431
|
fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
|
|
2187
2432
|
}
|
|
2188
2433
|
}
|
|
2189
|
-
fs.writeFileSync(envPath, envContent,
|
|
2434
|
+
fs.writeFileSync(envPath, envContent, {
|
|
2435
|
+
encoding: "utf-8",
|
|
2436
|
+
mode: 384
|
|
2437
|
+
});
|
|
2438
|
+
fs.chmodSync(envPath, 384);
|
|
2190
2439
|
}
|
|
2191
2440
|
}
|
|
2192
2441
|
//#endregion
|
|
@@ -2456,7 +2705,17 @@ async function generateSdkCommand(args) {
|
|
|
2456
2705
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
2457
2706
|
console.log("");
|
|
2458
2707
|
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
2459
|
-
|
|
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
|
+
}
|
|
2460
2719
|
const schemaVersion = remoteSchemaVersion ?? computeSchemaVersion(collections);
|
|
2461
2720
|
files.push({
|
|
2462
2721
|
path: "schema.meta.ts",
|
|
@@ -2468,7 +2727,6 @@ async function generateSdkCommand(args) {
|
|
|
2468
2727
|
// curl -s <api-url>/api/meta/schema-version
|
|
2469
2728
|
//
|
|
2470
2729
|
export const SCHEMA_VERSION = ${JSON.stringify(schemaVersion)};
|
|
2471
|
-
export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOString())};
|
|
2472
2730
|
`
|
|
2473
2731
|
});
|
|
2474
2732
|
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
@@ -2490,8 +2748,9 @@ export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOS
|
|
|
2490
2748
|
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
2491
2749
|
console.log(chalk.gray(" });"));
|
|
2492
2750
|
console.log("");
|
|
2493
|
-
|
|
2494
|
-
|
|
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();`));
|
|
2495
2754
|
console.log("");
|
|
2496
2755
|
}
|
|
2497
2756
|
//#endregion
|
|
@@ -2972,13 +3231,20 @@ function validateManifest(raw) {
|
|
|
2972
3231
|
byPath.set(at, name);
|
|
2973
3232
|
}
|
|
2974
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
|
+
});
|
|
2975
3240
|
if (issues.length > 0) return { issues };
|
|
2976
3241
|
return {
|
|
2977
3242
|
manifest: {
|
|
2978
3243
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2979
3244
|
rebase: raw.rebase,
|
|
2980
3245
|
apps,
|
|
2981
|
-
...storage ? { storage } : {}
|
|
3246
|
+
...storage ? { storage } : {},
|
|
3247
|
+
...telemetry !== void 0 ? { telemetry } : {}
|
|
2982
3248
|
},
|
|
2983
3249
|
issues
|
|
2984
3250
|
};
|
|
@@ -3087,7 +3353,7 @@ function synthesizeManifest(projectRoot) {
|
|
|
3087
3353
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
3088
3354
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
3089
3355
|
apps.backend = backend;
|
|
3090
|
-
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.");
|
|
3091
3357
|
}
|
|
3092
3358
|
if (exists("frontend")) apps.web = {
|
|
3093
3359
|
type: "static",
|
|
@@ -3135,13 +3401,59 @@ function loadManifest(projectRoot) {
|
|
|
3135
3401
|
filePath
|
|
3136
3402
|
};
|
|
3137
3403
|
}
|
|
3138
|
-
/**
|
|
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
|
+
*/
|
|
3139
3442
|
function writeManifest(projectRoot, manifest) {
|
|
3140
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);
|
|
3141
3450
|
const ordered = {
|
|
3142
|
-
$schema:
|
|
3451
|
+
$schema: schema,
|
|
3143
3452
|
rebase: manifest.rebase,
|
|
3144
|
-
apps: manifest.apps
|
|
3453
|
+
apps: manifest.apps,
|
|
3454
|
+
...storage ? { storage } : {},
|
|
3455
|
+
...telemetry !== void 0 ? { telemetry } : {},
|
|
3456
|
+
...carried
|
|
3145
3457
|
};
|
|
3146
3458
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
3147
3459
|
return filePath;
|
|
@@ -3436,26 +3748,36 @@ function resolveStartPort(projectRoot, explicitPort) {
|
|
|
3436
3748
|
} catch {}
|
|
3437
3749
|
return getProjectPort(projectRoot);
|
|
3438
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
|
+
};
|
|
3439
3769
|
async function devCommand(rawArgs) {
|
|
3440
|
-
|
|
3441
|
-
"--backend-only": Boolean,
|
|
3442
|
-
"--frontend-only": Boolean,
|
|
3443
|
-
"--port": Number,
|
|
3444
|
-
"--generate": Boolean,
|
|
3445
|
-
"--help": Boolean,
|
|
3446
|
-
"-b": "--backend-only",
|
|
3447
|
-
"-f": "--frontend-only",
|
|
3448
|
-
"-P": "--port",
|
|
3449
|
-
"-g": "--generate",
|
|
3450
|
-
"-h": "--help"
|
|
3451
|
-
}, {
|
|
3452
|
-
argv: rawArgs.slice(3),
|
|
3453
|
-
permissive: true
|
|
3454
|
-
});
|
|
3455
|
-
if (args["--help"]) {
|
|
3770
|
+
if (wantsHelp(rawArgs)) {
|
|
3456
3771
|
printDevHelp();
|
|
3457
3772
|
return;
|
|
3458
3773
|
}
|
|
3774
|
+
const { flags: args } = parseCommandArgs({
|
|
3775
|
+
spec: DEV_FLAGS,
|
|
3776
|
+
rawArgs,
|
|
3777
|
+
commandWords: 1,
|
|
3778
|
+
command: "dev",
|
|
3779
|
+
maxPositionals: 0
|
|
3780
|
+
});
|
|
3459
3781
|
const projectRoot = requireProjectRoot();
|
|
3460
3782
|
recordEvent("cli.dev", {
|
|
3461
3783
|
backend_only: Boolean(args["--backend-only"]),
|
|
@@ -3797,7 +4119,7 @@ ${chalk.green.bold("Usage")}
|
|
|
3797
4119
|
${chalk.green.bold("Options")}
|
|
3798
4120
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
3799
4121
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
3800
|
-
${chalk.blue("--port, -
|
|
4122
|
+
${chalk.blue("--port, -P")} Backend port (default: auto-detected per project)
|
|
3801
4123
|
${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
|
|
3802
4124
|
|
|
3803
4125
|
${chalk.green.bold("Description")}
|
|
@@ -4522,10 +4844,11 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
4522
4844
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
4523
4845
|
* sitting in the repository looking exactly like the server.
|
|
4524
4846
|
*
|
|
4525
|
-
* A project that means to
|
|
4526
|
-
* 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
|
|
4527
4849
|
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
4528
|
-
* 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".
|
|
4529
4852
|
*/
|
|
4530
4853
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
4531
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)));
|
|
@@ -4559,7 +4882,9 @@ async function buildBundle(options) {
|
|
|
4559
4882
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
4560
4883
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
4561
4884
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
4562
|
-
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."));
|
|
4563
4888
|
}
|
|
4564
4889
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
4565
4890
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -5059,24 +5384,24 @@ ${chalk.bold("Examples")}
|
|
|
5059
5384
|
`.trim());
|
|
5060
5385
|
}
|
|
5061
5386
|
async function buildCommand(rawArgs = []) {
|
|
5062
|
-
|
|
5063
|
-
"--output": String,
|
|
5064
|
-
"--out": "--output",
|
|
5065
|
-
"--skip-type-check": Boolean,
|
|
5066
|
-
"--skip-schema": Boolean,
|
|
5067
|
-
"--no-static": Boolean,
|
|
5068
|
-
"--skip-static-build": Boolean,
|
|
5069
|
-
"--legacy": Boolean,
|
|
5070
|
-
"--help": Boolean,
|
|
5071
|
-
"-h": "--help"
|
|
5072
|
-
}, {
|
|
5073
|
-
argv: rawArgs.slice(3),
|
|
5074
|
-
permissive: true
|
|
5075
|
-
});
|
|
5076
|
-
if (args["--help"]) {
|
|
5387
|
+
if (wantsHelp(rawArgs)) {
|
|
5077
5388
|
printHelp$5();
|
|
5078
5389
|
return;
|
|
5079
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
|
+
});
|
|
5080
5405
|
const projectRoot = requireProjectRoot();
|
|
5081
5406
|
if (args["--legacy"]) {
|
|
5082
5407
|
await runWorkspaceBuilds(projectRoot);
|
|
@@ -5094,7 +5419,6 @@ async function buildCommand(rawArgs = []) {
|
|
|
5094
5419
|
throw err;
|
|
5095
5420
|
}
|
|
5096
5421
|
const { manifest, source } = loaded;
|
|
5097
|
-
const requested = args._.filter((a) => !a.startsWith("-"));
|
|
5098
5422
|
let targets = buildableApps(manifest);
|
|
5099
5423
|
if (requested.length > 0) {
|
|
5100
5424
|
const known = new Set(targets.map((t) => t.name));
|
|
@@ -5273,6 +5597,56 @@ function findCliRoot(from) {
|
|
|
5273
5597
|
}
|
|
5274
5598
|
return null;
|
|
5275
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
|
+
}
|
|
5276
5650
|
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
5277
5651
|
var PAYLOAD = [
|
|
5278
5652
|
{
|
|
@@ -5323,25 +5697,30 @@ ${chalk.bold("Usage")}
|
|
|
5323
5697
|
|
|
5324
5698
|
${chalk.bold("Options")}
|
|
5325
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
|
|
5326
5702
|
-h, --help Show this help
|
|
5327
5703
|
`.trim());
|
|
5328
5704
|
}
|
|
5329
5705
|
async function ejectCommand(rawArgs = []) {
|
|
5330
|
-
|
|
5331
|
-
"--dry-run": Boolean,
|
|
5332
|
-
"--help": Boolean,
|
|
5333
|
-
"-h": "--help"
|
|
5334
|
-
}, {
|
|
5335
|
-
argv: rawArgs.slice(2),
|
|
5336
|
-
permissive: true
|
|
5337
|
-
});
|
|
5338
|
-
if (args["--help"]) {
|
|
5706
|
+
if (wantsHelp(rawArgs)) {
|
|
5339
5707
|
printHelp$4();
|
|
5340
5708
|
return;
|
|
5341
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
|
+
});
|
|
5342
5720
|
const projectRoot = requireProjectRoot();
|
|
5343
5721
|
const dryRun = Boolean(args["--dry-run"]);
|
|
5344
|
-
const
|
|
5722
|
+
const force = Boolean(args["--force"]);
|
|
5723
|
+
const requested = positionals[0];
|
|
5345
5724
|
let loaded;
|
|
5346
5725
|
try {
|
|
5347
5726
|
loaded = loadManifest(projectRoot);
|
|
@@ -5390,7 +5769,12 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5390
5769
|
process.exit(1);
|
|
5391
5770
|
}
|
|
5392
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
|
+
};
|
|
5393
5776
|
const planned = [];
|
|
5777
|
+
const blocked = [];
|
|
5394
5778
|
for (const file of PAYLOAD) {
|
|
5395
5779
|
const source = path.join(payloadDir, file.from);
|
|
5396
5780
|
if (!fs.existsSync(source)) {
|
|
@@ -5398,26 +5782,43 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5398
5782
|
process.exit(1);
|
|
5399
5783
|
}
|
|
5400
5784
|
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
5785
|
+
if (exists && file.overwrite && !force) blocked.push(file.to);
|
|
5401
5786
|
planned.push({
|
|
5402
5787
|
to: file.to,
|
|
5403
|
-
action: exists
|
|
5788
|
+
action: !exists ? "write" : file.overwrite ? "overwrite" : "keep"
|
|
5404
5789
|
});
|
|
5405
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
|
+
}
|
|
5406
5799
|
if (dryRun) {
|
|
5407
5800
|
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
5408
5801
|
console.log("");
|
|
5409
|
-
for (const item of planned)
|
|
5410
|
-
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)")}`);
|
|
5411
5807
|
console.log("");
|
|
5412
5808
|
console.log(chalk.dim("Nothing was changed."));
|
|
5413
5809
|
return;
|
|
5414
5810
|
}
|
|
5415
5811
|
const projectName = projectNameOf(projectRoot);
|
|
5812
|
+
const backups = [];
|
|
5416
5813
|
for (const [index, file] of PAYLOAD.entries()) {
|
|
5417
5814
|
if (planned[index].action === "keep") continue;
|
|
5418
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
|
+
}
|
|
5419
5820
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
5420
|
-
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8")
|
|
5821
|
+
const contents = renderPayload(fs.readFileSync(path.join(payloadDir, file.from), "utf8"), shape, projectName);
|
|
5421
5822
|
fs.writeFileSync(destination, contents, "utf8");
|
|
5422
5823
|
}
|
|
5423
5824
|
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
@@ -5437,9 +5838,14 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5437
5838
|
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
5438
5839
|
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
5439
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`);
|
|
5440
5842
|
console.log("");
|
|
5441
5843
|
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
5442
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
|
+
}
|
|
5443
5849
|
console.log("");
|
|
5444
5850
|
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
5445
5851
|
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
@@ -5496,19 +5902,20 @@ Build first with ${chalk.cyan("rebase build")}.
|
|
|
5496
5902
|
`.trim());
|
|
5497
5903
|
}
|
|
5498
5904
|
async function startCommand(rawArgs = []) {
|
|
5499
|
-
|
|
5500
|
-
"--bundle": String,
|
|
5501
|
-
"--legacy": Boolean,
|
|
5502
|
-
"--help": Boolean,
|
|
5503
|
-
"-h": "--help"
|
|
5504
|
-
}, {
|
|
5505
|
-
argv: rawArgs.slice(3),
|
|
5506
|
-
permissive: true
|
|
5507
|
-
});
|
|
5508
|
-
if (args["--help"]) {
|
|
5905
|
+
if (wantsHelp(rawArgs)) {
|
|
5509
5906
|
printHelp$3();
|
|
5510
5907
|
return;
|
|
5511
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
|
+
});
|
|
5512
5919
|
const projectRoot = requireProjectRoot();
|
|
5513
5920
|
const envFile = findEnvFile(projectRoot);
|
|
5514
5921
|
const env = { ...process.env };
|
|
@@ -5523,7 +5930,10 @@ async function startCommand(rawArgs = []) {
|
|
|
5523
5930
|
}
|
|
5524
5931
|
ensureBundleDependencies(projectRoot, bundleDir);
|
|
5525
5932
|
console.log(`${chalk.bold("Rebase")} — starting runtime from ${chalk.cyan(path.relative(projectRoot, bundleDir))}/\n`);
|
|
5526
|
-
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
|
+
});
|
|
5527
5937
|
process.env.REBASE_BUNDLE = bundleDir;
|
|
5528
5938
|
try {
|
|
5529
5939
|
const { runFromBundle } = await import("@rebasepro/server");
|
|
@@ -5654,7 +6064,7 @@ function selectUserForEmail(payload, email) {
|
|
|
5654
6064
|
}
|
|
5655
6065
|
}
|
|
5656
6066
|
async function authCommand(subcommand, rawArgs) {
|
|
5657
|
-
if (!subcommand || subcommand === "--help") {
|
|
6067
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5658
6068
|
printAuthHelp();
|
|
5659
6069
|
return;
|
|
5660
6070
|
}
|
|
@@ -5669,17 +6079,48 @@ async function authCommand(subcommand, rawArgs) {
|
|
|
5669
6079
|
process.exit(1);
|
|
5670
6080
|
}
|
|
5671
6081
|
}
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
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
|
|
5680
6116
|
});
|
|
5681
|
-
|
|
5682
|
-
|
|
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);
|
|
5683
6124
|
if (!email) {
|
|
5684
6125
|
console.error(chalk.red("✗ Email is required."));
|
|
5685
6126
|
console.log("");
|
|
@@ -5766,7 +6207,7 @@ import * as dotenv from "dotenv";
|
|
|
5766
6207
|
import path from "path";
|
|
5767
6208
|
import fs from "fs";
|
|
5768
6209
|
|
|
5769
|
-
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
|
|
6210
|
+
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });
|
|
5770
6211
|
|
|
5771
6212
|
const email = process.env.REBASE_RESET_EMAIL!;
|
|
5772
6213
|
const newPassword = process.env.REBASE_RESET_PASSWORD!;
|
|
@@ -5946,12 +6387,21 @@ async function doctorCommand(rawArgs) {
|
|
|
5946
6387
|
//#endregion
|
|
5947
6388
|
//#region src/commands/skills.ts
|
|
5948
6389
|
var require = createRequire(import.meta.url);
|
|
5949
|
-
/**
|
|
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
|
+
*/
|
|
5950
6399
|
var AGENTS = {
|
|
5951
6400
|
cursor: {
|
|
5952
6401
|
label: "Cursor",
|
|
5953
6402
|
detectDir: ".cursor",
|
|
5954
6403
|
targetDir: ".cursor/rules",
|
|
6404
|
+
flatLayout: true,
|
|
5955
6405
|
/** Cursor uses .mdc files (Markdown with Context). */
|
|
5956
6406
|
transformFile: (skillName, content) => ({
|
|
5957
6407
|
fileName: `${skillName}.mdc`,
|
|
@@ -5962,6 +6412,7 @@ var AGENTS = {
|
|
|
5962
6412
|
label: "Claude Code",
|
|
5963
6413
|
detectDir: ".claude",
|
|
5964
6414
|
targetDir: ".claude/skills",
|
|
6415
|
+
flatLayout: false,
|
|
5965
6416
|
/** Claude Code uses the standard SKILL.md format in subdirectories. */
|
|
5966
6417
|
transformFile: (skillName, content) => ({
|
|
5967
6418
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5972,6 +6423,7 @@ var AGENTS = {
|
|
|
5972
6423
|
label: "Windsurf",
|
|
5973
6424
|
detectDir: ".windsurf",
|
|
5974
6425
|
targetDir: ".windsurf/rules",
|
|
6426
|
+
flatLayout: true,
|
|
5975
6427
|
/** Windsurf uses plain .md files. */
|
|
5976
6428
|
transformFile: (skillName, content) => ({
|
|
5977
6429
|
fileName: `${skillName}.md`,
|
|
@@ -5982,6 +6434,7 @@ var AGENTS = {
|
|
|
5982
6434
|
label: "Gemini CLI / Antigravity",
|
|
5983
6435
|
detectDir: ".agents",
|
|
5984
6436
|
targetDir: ".agents/skills",
|
|
6437
|
+
flatLayout: false,
|
|
5985
6438
|
/** Gemini uses the standard SKILL.md format in subdirectories. */
|
|
5986
6439
|
transformFile: (skillName, content) => ({
|
|
5987
6440
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -6000,21 +6453,65 @@ function getSkillsSourceDir() {
|
|
|
6000
6453
|
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
6001
6454
|
return skillsDir;
|
|
6002
6455
|
}
|
|
6003
|
-
/**
|
|
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. */
|
|
6004
6481
|
function loadSkills(skillsDir) {
|
|
6005
6482
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
6006
6483
|
const skills = [];
|
|
6007
6484
|
for (const entry of entries) {
|
|
6008
6485
|
if (!entry.isDirectory()) continue;
|
|
6009
|
-
const
|
|
6486
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
6487
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
6010
6488
|
if (!fs.existsSync(skillMdPath)) continue;
|
|
6011
6489
|
skills.push({
|
|
6012
6490
|
name: entry.name,
|
|
6013
|
-
|
|
6491
|
+
dir: skillDir,
|
|
6492
|
+
content: fs.readFileSync(skillMdPath, "utf-8"),
|
|
6493
|
+
assets: loadSkillAssets(skillDir)
|
|
6014
6494
|
});
|
|
6015
6495
|
}
|
|
6016
6496
|
return skills;
|
|
6017
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
|
+
}
|
|
6018
6515
|
/** Detect which agent environments already exist in the project. */
|
|
6019
6516
|
function detectAgents(projectDir) {
|
|
6020
6517
|
const detected = [];
|
|
@@ -6027,16 +6524,31 @@ function installForAgent(agentKey, skills, projectDir) {
|
|
|
6027
6524
|
const targetBase = path.join(projectDir, agent.targetDir);
|
|
6028
6525
|
fs.mkdirSync(targetBase, { recursive: true });
|
|
6029
6526
|
let count = 0;
|
|
6527
|
+
let assetCount = 0;
|
|
6030
6528
|
for (const skill of skills) {
|
|
6031
|
-
const
|
|
6529
|
+
const body = agent.flatLayout ? rewriteAssetLinks(skill.content, skill.assets, skill.name) : skill.content;
|
|
6530
|
+
const { fileName, content } = agent.transformFile(skill.name, body);
|
|
6032
6531
|
const targetPath = path.join(targetBase, fileName);
|
|
6033
6532
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
6034
6533
|
fs.writeFileSync(targetPath, content, "utf-8");
|
|
6035
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
|
+
}
|
|
6036
6541
|
}
|
|
6037
|
-
return
|
|
6542
|
+
return {
|
|
6543
|
+
skills: count,
|
|
6544
|
+
assets: assetCount
|
|
6545
|
+
};
|
|
6038
6546
|
}
|
|
6039
6547
|
async function skillsCommand(subcommand, rawArgs) {
|
|
6548
|
+
if (wantsHelp(rawArgs)) {
|
|
6549
|
+
printSkillsHelp();
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
6040
6552
|
switch (subcommand) {
|
|
6041
6553
|
case "install":
|
|
6042
6554
|
await skillsInstall(rawArgs);
|
|
@@ -6119,9 +6631,10 @@ async function skillsInstall(rawArgs = []) {
|
|
|
6119
6631
|
console.log("");
|
|
6120
6632
|
for (const agentKey of agents) {
|
|
6121
6633
|
const agent = AGENTS[agentKey];
|
|
6122
|
-
const count = installForAgent(agentKey, skills, projectDir);
|
|
6634
|
+
const { skills: count, assets } = installForAgent(agentKey, skills, projectDir);
|
|
6123
6635
|
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
6124
|
-
|
|
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)}`);
|
|
6125
6638
|
}
|
|
6126
6639
|
console.log("");
|
|
6127
6640
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -6182,7 +6695,7 @@ function resolveBaseUrl(env, projectRoot) {
|
|
|
6182
6695
|
return `http://localhost:${env.PORT || env.REBASE_PORT || "3001"}`;
|
|
6183
6696
|
}
|
|
6184
6697
|
async function apiKeysCommand(subcommand, rawArgs) {
|
|
6185
|
-
if (!subcommand || subcommand === "--help") {
|
|
6698
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
6186
6699
|
printApiKeysHelp();
|
|
6187
6700
|
return;
|
|
6188
6701
|
}
|
|
@@ -6244,20 +6757,43 @@ async function listKeys(_rawArgs) {
|
|
|
6244
6757
|
process.exit(1);
|
|
6245
6758
|
}
|
|
6246
6759
|
}
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
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
|
|
6259
6789
|
});
|
|
6260
|
-
|
|
6790
|
+
return {
|
|
6791
|
+
flags,
|
|
6792
|
+
name: flags["--name"] || positionals[0]
|
|
6793
|
+
};
|
|
6794
|
+
}
|
|
6795
|
+
async function createKey(rawArgs) {
|
|
6796
|
+
const { flags: args, name } = resolveCreateKeyArgs(rawArgs);
|
|
6261
6797
|
const permissionsRaw = args["--permissions"];
|
|
6262
6798
|
if (!name) {
|
|
6263
6799
|
console.error(chalk.red("✗ Name is required."));
|
|
@@ -6358,12 +6894,30 @@ async function createKey(rawArgs) {
|
|
|
6358
6894
|
process.exit(1);
|
|
6359
6895
|
}
|
|
6360
6896
|
}
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
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
|
|
6365
6916
|
});
|
|
6366
|
-
|
|
6917
|
+
return { id: flags["--id"] || positionals[0] };
|
|
6918
|
+
}
|
|
6919
|
+
async function revokeKey(rawArgs) {
|
|
6920
|
+
const { id } = resolveRevokeKeyArgs(rawArgs);
|
|
6367
6921
|
if (!id) {
|
|
6368
6922
|
console.error(chalk.red("✗ Key ID is required."));
|
|
6369
6923
|
console.log("");
|
|
@@ -6550,9 +7104,10 @@ async function loginCommand(rawArgs) {
|
|
|
6550
7104
|
permissive: true
|
|
6551
7105
|
});
|
|
6552
7106
|
const url = resolveCloudUrl(rawArgs);
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
7107
|
+
noteBlank();
|
|
7108
|
+
note(`Signing in to ${chalk.cyan(url)}`);
|
|
7109
|
+
noteBlank();
|
|
7110
|
+
if (!args["--email"] || !args["--password"]) requireInteractive("credentials", "--email and --password");
|
|
6556
7111
|
const prompts = [];
|
|
6557
7112
|
if (!args["--email"]) prompts.push({
|
|
6558
7113
|
type: "input",
|
|
@@ -6578,14 +7133,21 @@ async function loginCommand(rawArgs) {
|
|
|
6578
7133
|
if (orgs.data.length === 1 && !getContextOrg(url)) setContextOrg(url, String(orgs.data[0].id));
|
|
6579
7134
|
} catch {}
|
|
6580
7135
|
success(`Logged in as ${chalk.bold(user.email ?? email)}`);
|
|
6581
|
-
|
|
6582
|
-
[
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
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
|
+
});
|
|
6587
7149
|
} catch (e) {
|
|
6588
|
-
if (e?.status === 401) fail("Invalid email or password.");
|
|
7150
|
+
if (e?.status === 401) fail("Invalid email or password.", void 0, "invalid_credentials");
|
|
6589
7151
|
reportError(e, "Login failed");
|
|
6590
7152
|
}
|
|
6591
7153
|
}
|
|
@@ -6593,34 +7155,60 @@ async function logoutCommand(rawArgs) {
|
|
|
6593
7155
|
const url = resolveCloudUrl(rawArgs);
|
|
6594
7156
|
const client = createCloudClient(url);
|
|
6595
7157
|
if (!client.auth.getSession()) {
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
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
|
+
});
|
|
6599
7167
|
return;
|
|
6600
7168
|
}
|
|
6601
7169
|
try {
|
|
6602
7170
|
await client.auth.signOut();
|
|
6603
7171
|
} catch {}
|
|
6604
7172
|
success(`Logged out of ${url}`);
|
|
7173
|
+
emit(() => {}, {
|
|
7174
|
+
success: true,
|
|
7175
|
+
host: url,
|
|
7176
|
+
wasLoggedIn: true
|
|
7177
|
+
});
|
|
6605
7178
|
}
|
|
6606
7179
|
async function whoamiCommand(rawArgs) {
|
|
6607
7180
|
const { client, url } = await requireClient(rawArgs);
|
|
6608
7181
|
try {
|
|
6609
7182
|
const user = await client.auth.getUser();
|
|
6610
|
-
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");
|
|
6611
7184
|
const link = readLink();
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
[
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
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
|
+
});
|
|
6624
7212
|
} catch (e) {
|
|
6625
7213
|
reportError(e, "Failed to fetch session");
|
|
6626
7214
|
}
|
|
@@ -6650,10 +7238,10 @@ async function linkDirect(target, rawArgs) {
|
|
|
6650
7238
|
try {
|
|
6651
7239
|
base = new URL(target);
|
|
6652
7240
|
} catch {
|
|
6653
|
-
fail(`"${target}" is not a valid URL
|
|
7241
|
+
fail(`"${target}" is not a valid URL.`, void 0, "invalid_url");
|
|
6654
7242
|
return;
|
|
6655
7243
|
}
|
|
6656
|
-
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");
|
|
6657
7245
|
const apiUrl = base.toString().replace(/\/+$/, "");
|
|
6658
7246
|
const probe = `${apiUrl}/api/meta/schema-version`;
|
|
6659
7247
|
let reachable = false;
|
|
@@ -6665,11 +7253,7 @@ async function linkDirect(target, rawArgs) {
|
|
|
6665
7253
|
} catch (err) {
|
|
6666
7254
|
detail = err instanceof Error ? err.message : String(err);
|
|
6667
7255
|
}
|
|
6668
|
-
if (!reachable) {
|
|
6669
|
-
console.log(chalk.yellow(`⚠ Could not reach ${probe}${detail ? ` (${detail})` : ""}.`));
|
|
6670
|
-
console.log(chalk.dim(" Linking anyway — the server may not be running yet."));
|
|
6671
|
-
console.log(chalk.dim(" It must be a Rebase backend of version 0.11 or newer."));
|
|
6672
|
-
}
|
|
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.");
|
|
6673
7257
|
writeLink({
|
|
6674
7258
|
url: apiUrl,
|
|
6675
7259
|
projectId: "",
|
|
@@ -6678,9 +7262,18 @@ async function linkDirect(target, rawArgs) {
|
|
|
6678
7262
|
projectName: base.host
|
|
6679
7263
|
});
|
|
6680
7264
|
success(`Linked to ${apiUrl}`);
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
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
|
+
});
|
|
6684
7277
|
}
|
|
6685
7278
|
async function linkCommand(rawArgs) {
|
|
6686
7279
|
const args = arg({
|
|
@@ -6701,14 +7294,15 @@ async function linkCommand(rawArgs) {
|
|
|
6701
7294
|
if (args["--project"]) {
|
|
6702
7295
|
const projectId = await resolveProjectRef(args["--project"], client);
|
|
6703
7296
|
project = await client.data.collection("projects").findById(projectId);
|
|
6704
|
-
if (!project) fail(`Project ${args["--project"]} not found
|
|
7297
|
+
if (!project) fail(`Project ${args["--project"]} not found.`, void 0, "project_not_found");
|
|
6705
7298
|
} else {
|
|
7299
|
+
requireInteractive("a project to link", "--project <slug>");
|
|
6706
7300
|
const org = getContextOrg(url);
|
|
6707
7301
|
const projects = (await client.data.collection("projects").find({
|
|
6708
7302
|
where: org ? { organization: ["==", org] } : void 0,
|
|
6709
7303
|
limit: 100
|
|
6710
7304
|
})).data;
|
|
6711
|
-
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");
|
|
6712
7306
|
const { picked } = await inquirer.prompt([{
|
|
6713
7307
|
type: "select",
|
|
6714
7308
|
name: "picked",
|
|
@@ -6720,39 +7314,63 @@ async function linkCommand(rawArgs) {
|
|
|
6720
7314
|
}]);
|
|
6721
7315
|
project = picked;
|
|
6722
7316
|
}
|
|
6723
|
-
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;
|
|
6724
7319
|
writeLink({
|
|
6725
7320
|
url,
|
|
6726
7321
|
projectId: String(project.id),
|
|
6727
7322
|
slug: project.subdomain,
|
|
6728
7323
|
projectName: project.name,
|
|
6729
|
-
orgId
|
|
7324
|
+
orgId
|
|
6730
7325
|
});
|
|
6731
7326
|
success(`Linked to ${chalk.bold(project.name ?? project.subdomain ?? "")}`);
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
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
|
+
});
|
|
7340
|
+
} catch (e) {
|
|
6735
7341
|
reportError(e, "Failed to link project");
|
|
6736
7342
|
}
|
|
6737
7343
|
}
|
|
6738
7344
|
function unlinkCommand() {
|
|
6739
7345
|
if (!readLink()) {
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
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
|
+
});
|
|
6743
7355
|
return;
|
|
6744
7356
|
}
|
|
6745
7357
|
removeLink();
|
|
6746
7358
|
success("Unlinked from cloud project");
|
|
7359
|
+
emit(() => {}, {
|
|
7360
|
+
success: true,
|
|
7361
|
+
unlinked: true,
|
|
7362
|
+
linkPath: projectLinkPath()
|
|
7363
|
+
});
|
|
6747
7364
|
}
|
|
6748
7365
|
async function selectOrgCommand(rawArgs) {
|
|
6749
7366
|
const target = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
6750
7367
|
const { client, url } = await requireClient(rawArgs);
|
|
6751
7368
|
try {
|
|
6752
7369
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
6753
|
-
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");
|
|
6754
7371
|
let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
|
|
6755
7372
|
if (!chosen && !target) {
|
|
7373
|
+
requireInteractive("an organization", "rebase cloud use <org-id|slug>");
|
|
6756
7374
|
const { picked } = await inquirer.prompt([{
|
|
6757
7375
|
type: "select",
|
|
6758
7376
|
name: "picked",
|
|
@@ -6764,9 +7382,18 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6764
7382
|
}]);
|
|
6765
7383
|
chosen = picked;
|
|
6766
7384
|
}
|
|
6767
|
-
if (!chosen) fail(`Organization "${target}" not found
|
|
7385
|
+
if (!chosen) fail(`Organization "${target}" not found.`, void 0, "org_not_found");
|
|
6768
7386
|
setContextOrg(url, String(chosen.id));
|
|
6769
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
|
+
});
|
|
6770
7397
|
} catch (e) {
|
|
6771
7398
|
reportError(e, "Failed to set organization");
|
|
6772
7399
|
}
|
|
@@ -6775,7 +7402,12 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6775
7402
|
function openCommand(rawArgs) {
|
|
6776
7403
|
const url = resolveCloudUrl(rawArgs);
|
|
6777
7404
|
const link = readLink();
|
|
6778
|
-
|
|
7405
|
+
const target = link ? `${url}/projects/${link.projectId}` : url;
|
|
7406
|
+
openUrl(target);
|
|
7407
|
+
emit(() => {}, {
|
|
7408
|
+
url: target,
|
|
7409
|
+
projectId: link?.projectId ?? null
|
|
7410
|
+
});
|
|
6779
7411
|
}
|
|
6780
7412
|
//#endregion
|
|
6781
7413
|
//#region src/commands/cloud/projects.ts
|
|
@@ -6791,21 +7423,34 @@ async function listProjects(rawArgs) {
|
|
|
6791
7423
|
orderBy: ["name", "asc"],
|
|
6792
7424
|
limit: 100
|
|
6793
7425
|
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
6794
|
-
console.log("");
|
|
6795
|
-
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
6796
|
-
console.log("");
|
|
6797
|
-
if (projects.length === 0) {
|
|
6798
|
-
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
6799
|
-
console.log("");
|
|
6800
|
-
return;
|
|
6801
|
-
}
|
|
6802
7426
|
const linkedId = readLink()?.projectId;
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
console.log(
|
|
6806
|
-
console.log(
|
|
6807
|
-
|
|
6808
|
-
|
|
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
|
+
});
|
|
6809
7454
|
} catch (e) {
|
|
6810
7455
|
reportError(e, "Failed to list projects");
|
|
6811
7456
|
}
|
|
@@ -6872,28 +7517,33 @@ function chooseRequestedTarget(requested, targets) {
|
|
|
6872
7517
|
}
|
|
6873
7518
|
async function resolveRequestedTarget(client, url, requested) {
|
|
6874
7519
|
const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
|
|
6875
|
-
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");
|
|
6876
7521
|
return chosen;
|
|
6877
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
|
+
};
|
|
6878
7536
|
async function createProject(rawArgs) {
|
|
6879
|
-
const args =
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
"
|
|
6884
|
-
|
|
6885
|
-
"--region": String,
|
|
6886
|
-
"--vm-size": String,
|
|
6887
|
-
"--org": String,
|
|
6888
|
-
"--link": Boolean,
|
|
6889
|
-
"-n": "--name"
|
|
6890
|
-
}, {
|
|
6891
|
-
argv: rawArgs.slice(4),
|
|
6892
|
-
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
|
|
6893
7543
|
});
|
|
6894
7544
|
const { client, url } = await requireClient(rawArgs);
|
|
6895
7545
|
const org = args["--org"] || getContextOrg(url);
|
|
6896
|
-
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");
|
|
6897
7547
|
const prompts = [];
|
|
6898
7548
|
if (!args["--name"]) prompts.push({
|
|
6899
7549
|
type: "input",
|
|
@@ -6915,14 +7565,14 @@ async function createProject(rawArgs) {
|
|
|
6915
7565
|
const defaults = providerDefaults(provider);
|
|
6916
7566
|
const region = (args["--region"] || target.region || defaults.region).trim();
|
|
6917
7567
|
const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
|
|
6918
|
-
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");
|
|
6919
7569
|
try {
|
|
6920
7570
|
const check = await client.functions.invoke("check-subdomain", { subdomain });
|
|
6921
|
-
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");
|
|
6922
7572
|
} catch {}
|
|
6923
7573
|
try {
|
|
6924
7574
|
const user = await client.auth.getUser();
|
|
6925
|
-
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");
|
|
6926
7576
|
const created = await client.data.collection("projects").create({
|
|
6927
7577
|
name,
|
|
6928
7578
|
subdomain,
|
|
@@ -6935,87 +7585,147 @@ async function createProject(rawArgs) {
|
|
|
6935
7585
|
createdById: user.uid,
|
|
6936
7586
|
status: "provisioning"
|
|
6937
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
|
+
});
|
|
6938
7597
|
success(`Created project ${chalk.bold(name)}`);
|
|
6939
|
-
|
|
6940
|
-
[
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
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
|
+
});
|
|
6958
7622
|
} catch (e) {
|
|
6959
7623
|
reportError(e, "Failed to create project");
|
|
6960
7624
|
}
|
|
6961
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
|
+
}
|
|
6962
7651
|
async function projectInfo(rawArgs, projectRef) {
|
|
6963
7652
|
const { client, url } = await requireClient(rawArgs);
|
|
6964
7653
|
try {
|
|
6965
7654
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
6966
7655
|
const p = await client.data.collection("projects").findById(projectId);
|
|
6967
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
7656
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
6968
7657
|
const [db, lastDeploy, baseDomain] = await Promise.all([
|
|
6969
7658
|
firstRow(client, "databases", projectId),
|
|
6970
7659
|
latestDeployment(client, projectId),
|
|
6971
7660
|
fetchTenantBaseDomain(client, url)
|
|
6972
7661
|
]);
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
[
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
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
|
+
});
|
|
6988
7700
|
} catch (e) {
|
|
6989
7701
|
reportError(e, "Failed to load project");
|
|
6990
7702
|
}
|
|
6991
7703
|
}
|
|
6992
7704
|
async function deleteProject(rawArgs, projectRef) {
|
|
6993
|
-
const args =
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
7705
|
+
const { flags: args } = parseCloudArgs({
|
|
7706
|
+
spec: {},
|
|
7707
|
+
rawArgs,
|
|
7708
|
+
commandWords: 3,
|
|
7709
|
+
command: "cloud projects delete",
|
|
7710
|
+
maxPositionals: 1
|
|
6999
7711
|
});
|
|
7000
7712
|
const { client } = await requireClient(rawArgs);
|
|
7001
7713
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
7002
7714
|
const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
|
|
7003
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
default: false,
|
|
7009
|
-
message: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
7010
|
-
}]);
|
|
7011
|
-
if (!confirmed) {
|
|
7012
|
-
console.log(chalk.gray(" Aborted."));
|
|
7013
|
-
return;
|
|
7014
|
-
}
|
|
7015
|
-
}
|
|
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
|
+
});
|
|
7016
7720
|
try {
|
|
7017
7721
|
await client.data.collection("projects").delete(projectId);
|
|
7018
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
|
+
});
|
|
7019
7729
|
} catch (e) {
|
|
7020
7730
|
reportError(e, "Failed to delete project");
|
|
7021
7731
|
}
|
|
@@ -7789,7 +8499,7 @@ async function orgsCommand(subcommand, rawArgs) {
|
|
|
7789
8499
|
case "--help":
|
|
7790
8500
|
printOrgsHelp();
|
|
7791
8501
|
break;
|
|
7792
|
-
default: fail(`Unknown orgs command: ${subcommand}`);
|
|
8502
|
+
default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
|
|
7793
8503
|
}
|
|
7794
8504
|
}
|
|
7795
8505
|
async function listOrgs(rawArgs) {
|
|
@@ -7797,21 +8507,31 @@ async function listOrgs(rawArgs) {
|
|
|
7797
8507
|
try {
|
|
7798
8508
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
7799
8509
|
const active = getContextOrg(url);
|
|
7800
|
-
|
|
7801
|
-
console.log(chalk.bold(" 🏢 Organizations"));
|
|
7802
|
-
console.log("");
|
|
7803
|
-
if (orgs.length === 0) {
|
|
7804
|
-
console.log(chalk.gray(" You are not a member of any organization."));
|
|
8510
|
+
emit(() => {
|
|
7805
8511
|
console.log("");
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
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
|
+
});
|
|
7815
8535
|
} catch (e) {
|
|
7816
8536
|
reportError(e, "Failed to list organizations");
|
|
7817
8537
|
}
|
|
@@ -7827,14 +8547,17 @@ async function createOrg(rawArgs) {
|
|
|
7827
8547
|
});
|
|
7828
8548
|
const { client, url } = await requireClient(rawArgs);
|
|
7829
8549
|
const prompts = [];
|
|
7830
|
-
if (!args["--name"])
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
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
|
+
}
|
|
7835
8558
|
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
7836
8559
|
const name = (args["--name"] || answers.name || "").trim();
|
|
7837
|
-
if (!name) fail("Organization name is required.");
|
|
8560
|
+
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
7838
8561
|
const slug = (args["--slug"] || slugify(name)).trim();
|
|
7839
8562
|
try {
|
|
7840
8563
|
const created = await client.data.collection("organizations").create({
|
|
@@ -7844,6 +8567,13 @@ async function createOrg(rawArgs) {
|
|
|
7844
8567
|
});
|
|
7845
8568
|
setContextOrg(url, String(created.id));
|
|
7846
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
|
+
});
|
|
7847
8577
|
} catch (e) {
|
|
7848
8578
|
reportError(e, "Failed to create organization");
|
|
7849
8579
|
}
|
|
@@ -7851,22 +8581,31 @@ async function createOrg(rawArgs) {
|
|
|
7851
8581
|
async function listMembers(rawArgs) {
|
|
7852
8582
|
const { client, url } = await requireClient(rawArgs);
|
|
7853
8583
|
const org = getContextOrg(url);
|
|
7854
|
-
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");
|
|
7855
8585
|
try {
|
|
7856
8586
|
const members = (await client.data.collection("organization-members").find({
|
|
7857
8587
|
where: { organization: ["==", org] },
|
|
7858
8588
|
limit: 200
|
|
7859
8589
|
})).data;
|
|
7860
|
-
|
|
7861
|
-
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
7862
|
-
console.log("");
|
|
7863
|
-
if (members.length === 0) {
|
|
7864
|
-
console.log(chalk.gray(" No members found."));
|
|
8590
|
+
emit(() => {
|
|
7865
8591
|
console.log("");
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
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
|
+
});
|
|
7870
8609
|
} catch (e) {
|
|
7871
8610
|
reportError(e, "Failed to list members");
|
|
7872
8611
|
}
|
|
@@ -7875,7 +8614,12 @@ function slugify(s) {
|
|
|
7875
8614
|
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7876
8615
|
}
|
|
7877
8616
|
function printOrgsHelp() {
|
|
7878
|
-
|
|
8617
|
+
emitHelp("orgs", [
|
|
8618
|
+
"list",
|
|
8619
|
+
"create",
|
|
8620
|
+
"members"
|
|
8621
|
+
], () => {
|
|
8622
|
+
console.log(`
|
|
7879
8623
|
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
7880
8624
|
|
|
7881
8625
|
${chalk.green.bold("Commands")}
|
|
@@ -7883,6 +8627,7 @@ ${chalk.green.bold("Commands")}
|
|
|
7883
8627
|
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
7884
8628
|
${chalk.blue.bold("members")} List members of the active organization
|
|
7885
8629
|
`);
|
|
8630
|
+
});
|
|
7886
8631
|
}
|
|
7887
8632
|
//#endregion
|
|
7888
8633
|
//#region src/commands/cloud/databases.ts
|
|
@@ -7918,7 +8663,7 @@ async function dbCommand$1(subcommand, rawArgs) {
|
|
|
7918
8663
|
case "--help":
|
|
7919
8664
|
printDbHelp();
|
|
7920
8665
|
break;
|
|
7921
|
-
default: fail(`Unknown db command: ${subcommand}`);
|
|
8666
|
+
default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
|
|
7922
8667
|
}
|
|
7923
8668
|
}
|
|
7924
8669
|
async function listDatabases(rawArgs) {
|
|
@@ -7930,19 +8675,30 @@ async function listDatabases(rawArgs) {
|
|
|
7930
8675
|
where: { project: ["==", projectId] },
|
|
7931
8676
|
limit: 50
|
|
7932
8677
|
})).data;
|
|
7933
|
-
|
|
7934
|
-
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
7935
|
-
console.log("");
|
|
7936
|
-
if (dbs.length === 0) {
|
|
7937
|
-
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
8678
|
+
emit(() => {
|
|
7938
8679
|
console.log("");
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
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
|
+
});
|
|
7946
8702
|
} catch (e) {
|
|
7947
8703
|
reportError(e, "Failed to list databases");
|
|
7948
8704
|
}
|
|
@@ -7962,6 +8718,7 @@ async function createDatabase(rawArgs) {
|
|
|
7962
8718
|
const projectRef = displayProjectRef(rawArgs);
|
|
7963
8719
|
let type = args["--type"];
|
|
7964
8720
|
if (!type) {
|
|
8721
|
+
requireInteractive("a database type", "--type <managed|byodb>");
|
|
7965
8722
|
const { picked } = await inquirer.prompt([{
|
|
7966
8723
|
type: "select",
|
|
7967
8724
|
name: "picked",
|
|
@@ -7978,13 +8735,14 @@ async function createDatabase(rawArgs) {
|
|
|
7978
8735
|
}
|
|
7979
8736
|
let connectionString = args["--connection-string"];
|
|
7980
8737
|
if (type === "byodb" && !connectionString) {
|
|
8738
|
+
requireInteractive("a connection string", "--connection-string <url>");
|
|
7981
8739
|
const { cs } = await inquirer.prompt([{
|
|
7982
8740
|
type: "input",
|
|
7983
8741
|
name: "cs",
|
|
7984
8742
|
message: "PostgreSQL connection string:"
|
|
7985
8743
|
}]);
|
|
7986
8744
|
connectionString = cs?.trim();
|
|
7987
|
-
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");
|
|
7988
8746
|
}
|
|
7989
8747
|
try {
|
|
7990
8748
|
const created = await client.data.collection("databases").create({
|
|
@@ -7994,11 +8752,19 @@ async function createDatabase(rawArgs) {
|
|
|
7994
8752
|
connectionStatus: "untested"
|
|
7995
8753
|
});
|
|
7996
8754
|
success(`Attached ${type} database to project ${projectRef}`);
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
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
|
+
});
|
|
8002
8768
|
} catch (e) {
|
|
8003
8769
|
reportError(e, "Failed to attach database");
|
|
8004
8770
|
}
|
|
@@ -8006,15 +8772,19 @@ async function createDatabase(rawArgs) {
|
|
|
8006
8772
|
async function testDatabase(rawArgs) {
|
|
8007
8773
|
const { client } = await requireClient(rawArgs);
|
|
8008
8774
|
const projectId = await requireProject(rawArgs, client);
|
|
8009
|
-
displayProjectRef(rawArgs);
|
|
8010
|
-
|
|
8011
|
-
|
|
8775
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
8776
|
+
noteBlank();
|
|
8777
|
+
note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
|
|
8012
8778
|
try {
|
|
8013
8779
|
const res = await client.functions.invoke("db-test", { projectId });
|
|
8014
|
-
console.
|
|
8015
|
-
if (res.
|
|
8016
|
-
|
|
8017
|
-
|
|
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
|
+
});
|
|
8018
8788
|
} catch (e) {
|
|
8019
8789
|
reportError(e, "Failed to test database");
|
|
8020
8790
|
}
|
|
@@ -8090,18 +8860,39 @@ async function dbInfo(rawArgs) {
|
|
|
8090
8860
|
reportError(e, "Failed to load database info");
|
|
8091
8861
|
}
|
|
8092
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
|
+
}
|
|
8093
8891
|
async function backupCommand(rawArgs) {
|
|
8094
|
-
const action
|
|
8892
|
+
const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
|
|
8095
8893
|
const { client } = await requireClient(rawArgs);
|
|
8096
8894
|
const projectId = await requireProject(rawArgs, client);
|
|
8097
8895
|
const projectRef = displayProjectRef(rawArgs);
|
|
8098
|
-
const args = arg({
|
|
8099
|
-
"--yes": Boolean,
|
|
8100
|
-
"-y": "--yes"
|
|
8101
|
-
}, {
|
|
8102
|
-
argv: rawArgs.slice(2),
|
|
8103
|
-
permissive: true
|
|
8104
|
-
});
|
|
8105
8896
|
try {
|
|
8106
8897
|
if (action === "create") {
|
|
8107
8898
|
const res = await client.functions.invoke("backup", {
|
|
@@ -8116,7 +8907,7 @@ async function backupCommand(rawArgs) {
|
|
|
8116
8907
|
return;
|
|
8117
8908
|
}
|
|
8118
8909
|
if (action === "restore") {
|
|
8119
|
-
const filename =
|
|
8910
|
+
const filename = backupFile;
|
|
8120
8911
|
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
8121
8912
|
await confirmDestructive({
|
|
8122
8913
|
yes: Boolean(args["--yes"]),
|
|
@@ -8154,7 +8945,7 @@ async function backupCommand(rawArgs) {
|
|
|
8154
8945
|
return;
|
|
8155
8946
|
}
|
|
8156
8947
|
if (action === "download") {
|
|
8157
|
-
const filename =
|
|
8948
|
+
const filename = backupFile;
|
|
8158
8949
|
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
8159
8950
|
const res = await client.functions.invoke("backup", void 0, {
|
|
8160
8951
|
method: "GET",
|
|
@@ -8212,17 +9003,17 @@ async function backupCommand(rawArgs) {
|
|
|
8212
9003
|
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
8213
9004
|
*/
|
|
8214
9005
|
async function pitrCommand(rawArgs) {
|
|
8215
|
-
const args =
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
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
|
|
8224
9015
|
});
|
|
8225
|
-
const action =
|
|
9016
|
+
const action = positionals[0] || "status";
|
|
8226
9017
|
const { client } = await requireClient(rawArgs);
|
|
8227
9018
|
const projectId = await requireProject(rawArgs, client);
|
|
8228
9019
|
const projectRef = displayProjectRef(rawArgs);
|
|
@@ -8294,7 +9085,15 @@ async function pitrCommand(rawArgs) {
|
|
|
8294
9085
|
}
|
|
8295
9086
|
}
|
|
8296
9087
|
function printDbHelp() {
|
|
8297
|
-
|
|
9088
|
+
emitHelp("db", [
|
|
9089
|
+
"list",
|
|
9090
|
+
"create",
|
|
9091
|
+
"info",
|
|
9092
|
+
"test",
|
|
9093
|
+
"backup",
|
|
9094
|
+
"pitr"
|
|
9095
|
+
], () => {
|
|
9096
|
+
console.log(`
|
|
8298
9097
|
${chalk.bold("rebase cloud db")} — Database & backups
|
|
8299
9098
|
|
|
8300
9099
|
${chalk.green.bold("Commands")}
|
|
@@ -8319,6 +9118,7 @@ ${chalk.green.bold("Options")}
|
|
|
8319
9118
|
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
8320
9119
|
${chalk.blue("--json")} Machine-readable output
|
|
8321
9120
|
`);
|
|
9121
|
+
});
|
|
8322
9122
|
}
|
|
8323
9123
|
//#endregion
|
|
8324
9124
|
//#region src/commands/cloud/env.ts
|
|
@@ -8355,7 +9155,7 @@ async function envCommand(action, rawArgs) {
|
|
|
8355
9155
|
case "unset":
|
|
8356
9156
|
case "delete":
|
|
8357
9157
|
case "rm":
|
|
8358
|
-
await unsetEnv(rawArgs);
|
|
9158
|
+
await unsetEnv(rawArgs, action);
|
|
8359
9159
|
break;
|
|
8360
9160
|
case "reveal":
|
|
8361
9161
|
await revealEnv(rawArgs);
|
|
@@ -8455,20 +9255,43 @@ var BUILD_TIME_ENV_PREFIXES = [
|
|
|
8455
9255
|
function buildTimeEnvPrefix(key) {
|
|
8456
9256
|
return BUILD_TIME_ENV_PREFIXES.find((prefix) => key.toUpperCase().startsWith(prefix));
|
|
8457
9257
|
}
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
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
|
|
8467
9284
|
});
|
|
9285
|
+
return {
|
|
9286
|
+
flags,
|
|
9287
|
+
assignment: parseEnvAssignment(positionals)
|
|
9288
|
+
};
|
|
9289
|
+
}
|
|
9290
|
+
async function setEnv(rawArgs) {
|
|
9291
|
+
const { flags: args, assignment: parsed } = resolveEnvSetArgs(rawArgs);
|
|
8468
9292
|
const { client } = await requireClient(rawArgs);
|
|
8469
9293
|
const projectId = await requireProject(rawArgs, client);
|
|
8470
9294
|
displayProjectRef(rawArgs);
|
|
8471
|
-
const parsed = parseEnvAssignment(cloudPositionals(rawArgs).slice(2));
|
|
8472
9295
|
if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
|
|
8473
9296
|
const buildTimePrefix = buildTimeEnvPrefix(parsed.key);
|
|
8474
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");
|
|
@@ -8494,12 +9317,32 @@ async function setEnv(rawArgs) {
|
|
|
8494
9317
|
reportError(e, "Failed to set environment variable");
|
|
8495
9318
|
}
|
|
8496
9319
|
}
|
|
8497
|
-
|
|
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");
|
|
8498
9344
|
const { client } = await requireClient(rawArgs);
|
|
8499
9345
|
const projectId = await requireProject(rawArgs, client);
|
|
8500
|
-
displayProjectRef(rawArgs);
|
|
8501
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8502
|
-
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8503
9346
|
try {
|
|
8504
9347
|
emit(() => {
|
|
8505
9348
|
success(`Removed ${chalk.bold(key)}`);
|
|
@@ -8518,11 +9361,11 @@ async function unsetEnv(rawArgs) {
|
|
|
8518
9361
|
}
|
|
8519
9362
|
}
|
|
8520
9363
|
async function revealEnv(rawArgs) {
|
|
9364
|
+
const key = resolveEnvKeyArg(rawArgs, "reveal");
|
|
9365
|
+
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8521
9366
|
const { client } = await requireClient(rawArgs);
|
|
8522
9367
|
const projectId = await requireProject(rawArgs, client);
|
|
8523
9368
|
const projectRef = displayProjectRef(rawArgs);
|
|
8524
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8525
|
-
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8526
9369
|
let list;
|
|
8527
9370
|
try {
|
|
8528
9371
|
list = await fetchEnvVars(client, projectId);
|
|
@@ -8550,16 +9393,15 @@ async function revealEnv(rawArgs) {
|
|
|
8550
9393
|
}
|
|
8551
9394
|
}
|
|
8552
9395
|
async function pullEnv(rawArgs) {
|
|
8553
|
-
const args =
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
permissive: true
|
|
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
|
|
8563
9405
|
});
|
|
8564
9406
|
const { client } = await requireClient(rawArgs);
|
|
8565
9407
|
const projectId = await requireProject(rawArgs, client);
|
|
@@ -8613,11 +9455,14 @@ async function pullEnv(rawArgs) {
|
|
|
8613
9455
|
}
|
|
8614
9456
|
}
|
|
8615
9457
|
function printEnvHelp() {
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
9458
|
+
emitHelp("env", [
|
|
9459
|
+
"list",
|
|
9460
|
+
"set",
|
|
9461
|
+
"unset",
|
|
9462
|
+
"reveal",
|
|
9463
|
+
"pull"
|
|
9464
|
+
], () => {
|
|
9465
|
+
console.log(`
|
|
8621
9466
|
${chalk.bold("rebase cloud env")} — Environment variables
|
|
8622
9467
|
|
|
8623
9468
|
${chalk.green.bold("Commands")}
|
|
@@ -8637,18 +9482,7 @@ ${chalk.gray("Values are encrypted at rest (AES-256-GCM) and only decrypted at d
|
|
|
8637
9482
|
${chalk.gray("VITE_* / NEXT_PUBLIC_* / PUBLIC_* / REACT_APP_* are read by your bundler at BUILD time;")}
|
|
8638
9483
|
${chalk.gray("these are applied at rollout, after the image is built, so they never reach the bundle.")}
|
|
8639
9484
|
`);
|
|
8640
|
-
}
|
|
8641
|
-
function printEnvHelpJson() {
|
|
8642
|
-
process.stdout.write(JSON.stringify({
|
|
8643
|
-
command: "env",
|
|
8644
|
-
actions: [
|
|
8645
|
-
"list",
|
|
8646
|
-
"set",
|
|
8647
|
-
"unset",
|
|
8648
|
-
"reveal",
|
|
8649
|
-
"pull"
|
|
8650
|
-
]
|
|
8651
|
-
}) + "\n");
|
|
9485
|
+
});
|
|
8652
9486
|
}
|
|
8653
9487
|
//#endregion
|
|
8654
9488
|
//#region src/commands/cloud/domains.ts
|
|
@@ -8736,12 +9570,28 @@ async function listDomains(rawArgs) {
|
|
|
8736
9570
|
reportError(e, "Failed to load custom domain");
|
|
8737
9571
|
}
|
|
8738
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
|
+
}
|
|
8739
9590
|
async function addDomain(rawArgs) {
|
|
9591
|
+
const domain = resolveDomainArg(rawArgs);
|
|
9592
|
+
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8740
9593
|
const { client } = await requireClient(rawArgs);
|
|
8741
9594
|
const projectId = await requireProject(rawArgs, client);
|
|
8742
|
-
displayProjectRef(rawArgs);
|
|
8743
|
-
const domain = cloudPositionals(rawArgs).slice(2)[0];
|
|
8744
|
-
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8745
9595
|
try {
|
|
8746
9596
|
await client.data.collection("projects").update(projectId, { customDomain: domain });
|
|
8747
9597
|
const setup = await fetchDomainSetup(client, projectId);
|
|
@@ -8796,14 +9646,12 @@ async function verifyDomains(rawArgs) {
|
|
|
8796
9646
|
}
|
|
8797
9647
|
}
|
|
8798
9648
|
async function removeDomain(rawArgs) {
|
|
8799
|
-
const args =
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
argv: rawArgs.slice(2),
|
|
8806
|
-
permissive: true
|
|
9649
|
+
const { flags: args } = parseCloudArgs({
|
|
9650
|
+
spec: {},
|
|
9651
|
+
rawArgs,
|
|
9652
|
+
commandWords: 3,
|
|
9653
|
+
command: "cloud domains remove",
|
|
9654
|
+
maxPositionals: 0
|
|
8807
9655
|
});
|
|
8808
9656
|
const { client } = await requireClient(rawArgs);
|
|
8809
9657
|
const projectId = await requireProject(rawArgs, client);
|
|
@@ -8823,7 +9671,13 @@ async function removeDomain(rawArgs) {
|
|
|
8823
9671
|
}
|
|
8824
9672
|
}
|
|
8825
9673
|
function printDomainsHelp() {
|
|
8826
|
-
|
|
9674
|
+
emitHelp("domains", [
|
|
9675
|
+
"list",
|
|
9676
|
+
"add",
|
|
9677
|
+
"verify",
|
|
9678
|
+
"remove"
|
|
9679
|
+
], () => {
|
|
9680
|
+
console.log(`
|
|
8827
9681
|
${chalk.bold("rebase cloud domains")} — Custom domain
|
|
8828
9682
|
|
|
8829
9683
|
${chalk.green.bold("Commands")}
|
|
@@ -8836,6 +9690,7 @@ ${chalk.green.bold("Options")}
|
|
|
8836
9690
|
${chalk.blue("--json")} Machine-readable output
|
|
8837
9691
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8838
9692
|
`);
|
|
9693
|
+
});
|
|
8839
9694
|
}
|
|
8840
9695
|
//#endregion
|
|
8841
9696
|
//#region src/commands/cloud/extensions.ts
|
|
@@ -8914,21 +9769,32 @@ async function listExtensions(rawArgs) {
|
|
|
8914
9769
|
reportError(e, "Failed to list extensions");
|
|
8915
9770
|
}
|
|
8916
9771
|
}
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
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
|
|
8926
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");
|
|
8927
9796
|
const { client } = await requireClient(rawArgs);
|
|
8928
9797
|
const projectId = await requireProject(rawArgs, client);
|
|
8929
|
-
displayProjectRef(rawArgs);
|
|
8930
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8931
|
-
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8932
9798
|
const name = resolveExtensionAlias(raw);
|
|
8933
9799
|
try {
|
|
8934
9800
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8965,20 +9831,10 @@ async function enableExtension(rawArgs) {
|
|
|
8965
9831
|
}
|
|
8966
9832
|
}
|
|
8967
9833
|
async function disableExtension(rawArgs) {
|
|
8968
|
-
const args =
|
|
8969
|
-
|
|
8970
|
-
"-y": "--yes",
|
|
8971
|
-
"--project": String,
|
|
8972
|
-
"-p": "--project"
|
|
8973
|
-
}, {
|
|
8974
|
-
argv: rawArgs.slice(2),
|
|
8975
|
-
permissive: true
|
|
8976
|
-
});
|
|
9834
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "disable");
|
|
9835
|
+
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8977
9836
|
const { client } = await requireClient(rawArgs);
|
|
8978
9837
|
const projectId = await requireProject(rawArgs, client);
|
|
8979
|
-
displayProjectRef(rawArgs);
|
|
8980
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8981
|
-
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8982
9838
|
const name = resolveExtensionAlias(raw);
|
|
8983
9839
|
try {
|
|
8984
9840
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -9004,7 +9860,12 @@ async function disableExtension(rawArgs) {
|
|
|
9004
9860
|
}
|
|
9005
9861
|
}
|
|
9006
9862
|
function printExtensionsHelp() {
|
|
9007
|
-
|
|
9863
|
+
emitHelp("extensions", [
|
|
9864
|
+
"list",
|
|
9865
|
+
"enable",
|
|
9866
|
+
"disable"
|
|
9867
|
+
], () => {
|
|
9868
|
+
console.log(`
|
|
9008
9869
|
${chalk.bold("rebase cloud extensions")} — Postgres extensions
|
|
9009
9870
|
|
|
9010
9871
|
${chalk.green.bold("Commands")}
|
|
@@ -9017,6 +9878,7 @@ ${chalk.green.bold("Options")}
|
|
|
9017
9878
|
${chalk.blue("--json")} Machine-readable output
|
|
9018
9879
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
9019
9880
|
`);
|
|
9881
|
+
});
|
|
9020
9882
|
}
|
|
9021
9883
|
//#endregion
|
|
9022
9884
|
//#region src/commands/cloud/settings.ts
|
|
@@ -9128,7 +9990,8 @@ async function setSettings(rawArgs) {
|
|
|
9128
9990
|
}
|
|
9129
9991
|
}
|
|
9130
9992
|
function printSettingsHelp() {
|
|
9131
|
-
|
|
9993
|
+
emitHelp("settings", ["show", "set"], () => {
|
|
9994
|
+
console.log(`
|
|
9132
9995
|
${chalk.bold("rebase cloud settings")} — Project configuration
|
|
9133
9996
|
|
|
9134
9997
|
${chalk.green.bold("Commands")}
|
|
@@ -9145,6 +10008,7 @@ ${chalk.green.bold("Options")}
|
|
|
9145
10008
|
${chalk.blue("--json")} Machine-readable output
|
|
9146
10009
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
9147
10010
|
`);
|
|
10011
|
+
});
|
|
9148
10012
|
}
|
|
9149
10013
|
//#endregion
|
|
9150
10014
|
//#region src/commands/cloud/deployments.ts
|
|
@@ -9255,14 +10119,15 @@ function parseDeploymentsLimit(raw) {
|
|
|
9255
10119
|
return raw;
|
|
9256
10120
|
}
|
|
9257
10121
|
async function deploymentsListCommand(rawArgs) {
|
|
9258
|
-
const args =
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
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
|
|
9266
10131
|
});
|
|
9267
10132
|
const limit = args["--all"] ? MAX_DEPLOYMENTS_LIMIT : parseDeploymentsLimit(args["--limit"]);
|
|
9268
10133
|
const { client } = await requireClient(rawArgs);
|
|
@@ -9303,20 +10168,34 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
9303
10168
|
reportError(e, "Failed to list deployments");
|
|
9304
10169
|
}
|
|
9305
10170
|
}
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
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
|
|
9315
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");
|
|
9316
10196
|
const { client } = await requireClient(rawArgs);
|
|
9317
10197
|
const projectId = await requireProject(rawArgs, client);
|
|
9318
10198
|
const projectRef = displayProjectRef(rawArgs);
|
|
9319
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
9320
10199
|
let rows;
|
|
9321
10200
|
try {
|
|
9322
10201
|
rows = await fetchDeployments(client, projectId);
|
|
@@ -9364,19 +10243,10 @@ async function rollbackCommand(rawArgs) {
|
|
|
9364
10243
|
}
|
|
9365
10244
|
}
|
|
9366
10245
|
async function cancelCommand(rawArgs) {
|
|
9367
|
-
const args =
|
|
9368
|
-
"--yes": Boolean,
|
|
9369
|
-
"-y": "--yes",
|
|
9370
|
-
"--project": String,
|
|
9371
|
-
"-p": "--project"
|
|
9372
|
-
}, {
|
|
9373
|
-
argv: rawArgs.slice(2),
|
|
9374
|
-
permissive: true
|
|
9375
|
-
});
|
|
10246
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud cancel");
|
|
9376
10247
|
const { client } = await requireClient(rawArgs);
|
|
9377
10248
|
const projectId = await requireProject(rawArgs, client);
|
|
9378
10249
|
const projectRef = displayProjectRef(rawArgs);
|
|
9379
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
9380
10250
|
await confirmDestructive({
|
|
9381
10251
|
yes: Boolean(args["--yes"]),
|
|
9382
10252
|
prompt: `Cancel the in-flight build for project ${projectRef}?`
|
|
@@ -10158,7 +11028,16 @@ async function debugCommand(action, rawArgs) {
|
|
|
10158
11028
|
}
|
|
10159
11029
|
}
|
|
10160
11030
|
function printDebugHelp() {
|
|
10161
|
-
|
|
11031
|
+
emitHelp("debug", [
|
|
11032
|
+
"health",
|
|
11033
|
+
"logs",
|
|
11034
|
+
"errors",
|
|
11035
|
+
"requests",
|
|
11036
|
+
"boot",
|
|
11037
|
+
"pod",
|
|
11038
|
+
"db"
|
|
11039
|
+
], () => {
|
|
11040
|
+
console.log(`
|
|
10162
11041
|
${chalk.bold("rebase cloud debug")} — Find out why a deployed project is misbehaving
|
|
10163
11042
|
|
|
10164
11043
|
${chalk.green.bold("Usage")}
|
|
@@ -10189,6 +11068,7 @@ ${chalk.green.bold("Options")}
|
|
|
10189
11068
|
${chalk.gray("Everything here is read-only. `health` exits non-zero when a check fails,")}
|
|
10190
11069
|
${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase cloud restart`.")}
|
|
10191
11070
|
`);
|
|
11071
|
+
});
|
|
10192
11072
|
}
|
|
10193
11073
|
//#endregion
|
|
10194
11074
|
//#region src/commands/cloud/resources.ts
|
|
@@ -10328,37 +11208,73 @@ async function metricsCommand(rawArgs) {
|
|
|
10328
11208
|
method: "GET",
|
|
10329
11209
|
path: projectId
|
|
10330
11210
|
});
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
[
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
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
|
+
});
|
|
10341
11230
|
} catch (e) {
|
|
10342
11231
|
reportError(e, "Failed to fetch metrics");
|
|
10343
11232
|
}
|
|
10344
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
|
+
}
|
|
10345
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");
|
|
10346
11270
|
const { client } = await requireClient(rawArgs);
|
|
10347
11271
|
const projectId = await requireProject(rawArgs, client);
|
|
10348
11272
|
try {
|
|
10349
11273
|
if (subcommand === "create") {
|
|
10350
|
-
const args =
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
"--events": String
|
|
10355
|
-
}, {
|
|
10356
|
-
argv: rawArgs.slice(4),
|
|
10357
|
-
permissive: true
|
|
10358
|
-
});
|
|
10359
|
-
const name = args["--name"] || fail("--name is required.");
|
|
10360
|
-
const table = args["--table"] || fail("--table is required.");
|
|
10361
|
-
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");
|
|
10362
11278
|
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
10363
11279
|
const created = await client.data.collection("webhooks").create({
|
|
10364
11280
|
project: projectId,
|
|
@@ -10369,33 +11285,58 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
10369
11285
|
enabled: true
|
|
10370
11286
|
});
|
|
10371
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
|
+
});
|
|
10372
11298
|
return;
|
|
10373
11299
|
}
|
|
10374
11300
|
if (subcommand === "delete") {
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
11301
|
+
await client.data.collection("webhooks").delete(deleteId);
|
|
11302
|
+
success(`Deleted webhook ${deleteId}`);
|
|
11303
|
+
emit(() => {}, {
|
|
11304
|
+
success: true,
|
|
11305
|
+
id: deleteId,
|
|
11306
|
+
projectId
|
|
11307
|
+
});
|
|
10379
11308
|
return;
|
|
10380
11309
|
}
|
|
10381
11310
|
const hooks = (await client.data.collection("webhooks").find({
|
|
10382
11311
|
where: { project: ["==", projectId] },
|
|
10383
11312
|
limit: 100
|
|
10384
11313
|
})).data;
|
|
10385
|
-
|
|
10386
|
-
console.log(chalk.bold(` 🔗 Webhooks — project ${displayProjectRef(rawArgs)}`));
|
|
10387
|
-
console.log("");
|
|
10388
|
-
if (hooks.length === 0) {
|
|
10389
|
-
console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
|
|
11314
|
+
emit(() => {
|
|
10390
11315
|
console.log("");
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
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
|
+
});
|
|
10399
11340
|
} catch (e) {
|
|
10400
11341
|
reportError(e, "Webhook operation failed");
|
|
10401
11342
|
}
|
|
@@ -10411,78 +11352,116 @@ async function storageCommand(action, rawArgs) {
|
|
|
10411
11352
|
where: { project: ["==", projectId] },
|
|
10412
11353
|
limit: 50
|
|
10413
11354
|
})).data;
|
|
10414
|
-
|
|
10415
|
-
console.log(chalk.bold(` 🪣 Storage — project ${displayProjectRef(rawArgs)}`));
|
|
10416
|
-
console.log("");
|
|
10417
|
-
if (stores.length === 0) {
|
|
10418
|
-
console.log(chalk.gray(" No storage buckets attached."));
|
|
11355
|
+
emit(() => {
|
|
10419
11356
|
console.log("");
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
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
|
+
});
|
|
10427
11379
|
} catch (e) {
|
|
10428
11380
|
reportError(e, "Failed to list storage");
|
|
10429
11381
|
}
|
|
10430
11382
|
}
|
|
10431
11383
|
function printStorageHelp() {
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
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
|
+
});
|
|
10451
11409
|
}
|
|
10452
11410
|
async function storageCreateCommand(rawArgs) {
|
|
11411
|
+
parseCloudArgs({
|
|
11412
|
+
spec: {},
|
|
11413
|
+
rawArgs,
|
|
11414
|
+
commandWords: 3,
|
|
11415
|
+
command: "cloud storage create",
|
|
11416
|
+
maxPositionals: 0
|
|
11417
|
+
});
|
|
10453
11418
|
const { client } = await requireClient(rawArgs);
|
|
10454
11419
|
const projectId = await requireProject(rawArgs, client);
|
|
10455
11420
|
try {
|
|
10456
|
-
|
|
10457
|
-
|
|
11421
|
+
noteBlank();
|
|
11422
|
+
note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
|
|
10458
11423
|
const res = await client.functions.invoke(`storage-provision/${encodeURIComponent(projectId)}`, void 0, { method: "POST" });
|
|
10459
11424
|
const info = res.data ?? res.data;
|
|
10460
11425
|
success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
|
|
10461
|
-
|
|
10462
|
-
[
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
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
|
+
});
|
|
10471
11447
|
} catch (e) {
|
|
10472
11448
|
reportError(e, "Failed to provision managed storage");
|
|
10473
11449
|
}
|
|
10474
11450
|
}
|
|
10475
11451
|
async function storageAttachCommand(rawArgs) {
|
|
10476
|
-
const parsed =
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
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
|
|
10486
11465
|
});
|
|
10487
11466
|
const bucket = parsed["--bucket"];
|
|
10488
11467
|
const accessKeyId = parsed["--access-key-id"];
|
|
@@ -10492,7 +11471,7 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10492
11471
|
!accessKeyId && "--access-key-id",
|
|
10493
11472
|
!secretAccessKey && "--secret-access-key"
|
|
10494
11473
|
].filter(Boolean);
|
|
10495
|
-
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");
|
|
10496
11475
|
const { client } = await requireClient(rawArgs);
|
|
10497
11476
|
const projectId = await requireProject(rawArgs, client);
|
|
10498
11477
|
try {
|
|
@@ -10515,17 +11494,29 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10515
11494
|
row.region = parsed["--region"];
|
|
10516
11495
|
}
|
|
10517
11496
|
if (parsed["--force-path-style"]) row.s3ForcePathStyle = true;
|
|
11497
|
+
const replaced = Boolean(existing?.id);
|
|
10518
11498
|
if (existing?.id) await client.data.collection("storages").update(String(existing.id), row);
|
|
10519
11499
|
else await client.data.collection("storages").create(row);
|
|
10520
11500
|
success(`Storage attached to ${displayProjectRef(rawArgs)}.`);
|
|
10521
|
-
|
|
10522
|
-
[
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
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
|
+
});
|
|
10529
11520
|
} catch (e) {
|
|
10530
11521
|
reportError(e, "Failed to attach storage");
|
|
10531
11522
|
}
|
|
@@ -10534,40 +11525,55 @@ async function clustersCommand(rawArgs) {
|
|
|
10534
11525
|
const { client } = await requireClient(rawArgs);
|
|
10535
11526
|
try {
|
|
10536
11527
|
const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
|
|
10537
|
-
|
|
10538
|
-
console.log(chalk.bold(" ☸ Clusters"));
|
|
10539
|
-
console.log("");
|
|
10540
|
-
if (clusters.length === 0) {
|
|
10541
|
-
console.log(chalk.gray(" No clusters registered."));
|
|
11528
|
+
emit(() => {
|
|
10542
11529
|
console.log("");
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
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
|
+
})) });
|
|
10550
11549
|
} catch (e) {
|
|
10551
11550
|
reportError(e, "Failed to list clusters");
|
|
10552
11551
|
}
|
|
10553
11552
|
}
|
|
10554
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];
|
|
10555
11561
|
const { client, url } = await requireClient(rawArgs);
|
|
10556
11562
|
const org = getContextOrg(url);
|
|
10557
|
-
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
10558
11563
|
if (action === "setup") {
|
|
10559
|
-
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");
|
|
10560
11565
|
try {
|
|
10561
11566
|
const res = await client.functions.invoke("stripe-billing", { organizationId: org }, { path: "setup-session" });
|
|
10562
|
-
if (!res.url) fail("Could not start billing setup.");
|
|
11567
|
+
if (!res.url) fail("Could not start billing setup.", void 0, "billing_setup_failed");
|
|
10563
11568
|
openUrl(res.url, "Add a payment method in your browser:");
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
}
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
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
|
+
});
|
|
10571
11577
|
} catch (e) {
|
|
10572
11578
|
reportError(e, "Failed to start billing setup");
|
|
10573
11579
|
}
|
|
@@ -10577,24 +11583,36 @@ async function billingCommand(rawArgs) {
|
|
|
10577
11583
|
const projectId = await requireProject(rawArgs, client);
|
|
10578
11584
|
try {
|
|
10579
11585
|
const res = await client.functions.invoke("stripe-billing", { projectId }, { path: "session" });
|
|
10580
|
-
if (!res.url) fail("Billing session could not be created.");
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
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
|
+
});
|
|
10585
11596
|
} catch (e) {
|
|
10586
11597
|
reportError(e, "Failed to start checkout");
|
|
10587
11598
|
}
|
|
10588
11599
|
return;
|
|
10589
11600
|
}
|
|
10590
|
-
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");
|
|
10591
11602
|
try {
|
|
10592
11603
|
const orgRow = await client.data.collection("organizations").findById(org);
|
|
10593
11604
|
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
10594
11605
|
if (!billingId) {
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
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
|
+
});
|
|
10598
11616
|
return;
|
|
10599
11617
|
}
|
|
10600
11618
|
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
@@ -10627,17 +11645,34 @@ async function billingCommand(rawArgs) {
|
|
|
10627
11645
|
} catch {}
|
|
10628
11646
|
}
|
|
10629
11647
|
} catch {}
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
[
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
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
|
+
});
|
|
10641
11676
|
} catch (e) {
|
|
10642
11677
|
reportError(e, "Failed to load billing");
|
|
10643
11678
|
}
|
|
@@ -10810,11 +11845,7 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
10810
11845
|
case "billing":
|
|
10811
11846
|
await billingCommand(rawArgs);
|
|
10812
11847
|
break;
|
|
10813
|
-
default:
|
|
10814
|
-
console.error(chalk.red(`Unknown cloud command: ${group}`));
|
|
10815
|
-
console.log("");
|
|
10816
|
-
printCloudHelp();
|
|
10817
|
-
process.exit(1);
|
|
11848
|
+
default: fail(`Unknown cloud command: ${group}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10818
11849
|
}
|
|
10819
11850
|
}
|
|
10820
11851
|
async function projectsGroup(action, rawArgs) {
|
|
@@ -10827,17 +11858,15 @@ async function projectsGroup(action, rawArgs) {
|
|
|
10827
11858
|
await createProject(rawArgs);
|
|
10828
11859
|
break;
|
|
10829
11860
|
case "info":
|
|
10830
|
-
await projectInfo(rawArgs,
|
|
11861
|
+
await projectInfo(rawArgs, resolveProjectArg(rawArgs, "info"));
|
|
10831
11862
|
break;
|
|
10832
11863
|
case "delete":
|
|
10833
|
-
await deleteProject(rawArgs,
|
|
11864
|
+
await deleteProject(rawArgs, resolveProjectArg(rawArgs, "delete"));
|
|
10834
11865
|
break;
|
|
10835
11866
|
case "--help":
|
|
10836
11867
|
printCloudHelp();
|
|
10837
11868
|
break;
|
|
10838
|
-
default:
|
|
10839
|
-
console.error(chalk.red(`Unknown projects command: ${action}`));
|
|
10840
|
-
process.exit(1);
|
|
11869
|
+
default: fail(`Unknown projects command: ${action}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10841
11870
|
}
|
|
10842
11871
|
}
|
|
10843
11872
|
async function deploymentsGroup(action, rawArgs) {
|
|
@@ -10849,13 +11878,51 @@ async function deploymentsGroup(action, rawArgs) {
|
|
|
10849
11878
|
case "--help":
|
|
10850
11879
|
printCloudHelp();
|
|
10851
11880
|
break;
|
|
10852
|
-
default:
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
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
|
+
];
|
|
10857
11923
|
function printCloudHelp() {
|
|
10858
|
-
|
|
11924
|
+
emitHelp("cloud", CLOUD_GROUPS, () => {
|
|
11925
|
+
console.log(`
|
|
10859
11926
|
${chalk.bold("rebase cloud")} — Manage your apps on Rebase Cloud
|
|
10860
11927
|
|
|
10861
11928
|
${chalk.green.bold("Usage")}
|
|
@@ -10920,6 +11987,7 @@ ${chalk.green.bold("Global options")}
|
|
|
10920
11987
|
${chalk.gray("Most commands act on the linked project (.rebase/cloud.json) unless --project is given.")}
|
|
10921
11988
|
${chalk.gray("Docs: https://rebase.pro/docs")}
|
|
10922
11989
|
`);
|
|
11990
|
+
});
|
|
10923
11991
|
}
|
|
10924
11992
|
//#endregion
|
|
10925
11993
|
//#region src/commands/apps.ts
|
|
@@ -10952,19 +12020,20 @@ ${chalk.bold("Options")}
|
|
|
10952
12020
|
`.trim());
|
|
10953
12021
|
}
|
|
10954
12022
|
async function appsCommand(subcommand, rawArgs = []) {
|
|
10955
|
-
|
|
10956
|
-
"--json": Boolean,
|
|
10957
|
-
"--force": Boolean,
|
|
10958
|
-
"--help": Boolean,
|
|
10959
|
-
"-h": "--help"
|
|
10960
|
-
}, {
|
|
10961
|
-
argv: rawArgs.slice(3),
|
|
10962
|
-
permissive: true
|
|
10963
|
-
});
|
|
10964
|
-
if (args["--help"] || !subcommand || subcommand === "--help") {
|
|
12023
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
10965
12024
|
printHelp$1();
|
|
10966
12025
|
return;
|
|
10967
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
|
+
});
|
|
10968
12037
|
switch (subcommand) {
|
|
10969
12038
|
case "list":
|
|
10970
12039
|
await listApps(Boolean(args["--json"]));
|
|
@@ -10973,7 +12042,7 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
10973
12042
|
await initManifest(Boolean(args["--force"]));
|
|
10974
12043
|
break;
|
|
10975
12044
|
case "config":
|
|
10976
|
-
await printAppConfig(
|
|
12045
|
+
await printAppConfig(positionals[1], Boolean(args["--json"]));
|
|
10977
12046
|
break;
|
|
10978
12047
|
default:
|
|
10979
12048
|
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
@@ -11122,7 +12191,25 @@ function getVersion() {
|
|
|
11122
12191
|
} catch {}
|
|
11123
12192
|
return "unknown";
|
|
11124
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
|
+
}
|
|
11125
12211
|
async function entry(args) {
|
|
12212
|
+
silenceDotenvBanner();
|
|
11126
12213
|
const parsedArgs = arg({
|
|
11127
12214
|
"--version": Boolean,
|
|
11128
12215
|
"--help": Boolean,
|
|
@@ -11313,6 +12400,6 @@ function telemetryNotice() {
|
|
|
11313
12400
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
11314
12401
|
}
|
|
11315
12402
|
//#endregion
|
|
11316
|
-
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, isPortAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, readEnvFile, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, 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 };
|
|
11317
12404
|
|
|
11318
12405
|
//# sourceMappingURL=index.es.js.map
|