@rebasepro/cli 0.13.1-canary.gef9608c → 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 +39 -0
- package/dist/commands/cloud/debug.d.ts +1 -0
- package/dist/commands/cloud/deployments.d.ts +22 -0
- package/dist/commands/cloud/domains.d.ts +10 -0
- package/dist/commands/cloud/env.d.ts +51 -0
- package/dist/commands/cloud/extensions.d.ts +21 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/projects.d.ts +29 -0
- package/dist/commands/cloud/resources.d.ts +14 -0
- package/dist/commands/cloud/settings.d.ts +1 -0
- package/dist/commands/dev.d.ts +17 -6
- package/dist/commands/eject.d.ts +42 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/skills.d.ts +81 -0
- package/dist/fold-static.d.ts +47 -0
- package/dist/index.es.js +2003 -706
- 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 +13 -5
- package/templates/overlays/baas/backend/tsconfig.json +6 -1
- package/templates/overlays/baas/config/index.ts +9 -0
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/template/.env.example +13 -4
- package/templates/template/backend/functions/hello.ts +8 -4
- package/templates/template/backend/tsconfig.json +6 -1
- package/templates/template/docker-compose.yml +4 -4
- package/templates/template/frontend/vite.config.ts +33 -2
package/dist/index.es.js
CHANGED
|
@@ -13,9 +13,9 @@ import { execSync, spawn, spawnSync } from "child_process";
|
|
|
13
13
|
import os from "os";
|
|
14
14
|
import { createRebaseClient } from "@rebasepro/client";
|
|
15
15
|
import dotenv from "dotenv";
|
|
16
|
-
import { createRequire } from "module";
|
|
17
16
|
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
18
|
-
import { generateSDK } from "@rebasepro/codegen";
|
|
17
|
+
import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
|
|
18
|
+
import { createRequire } from "module";
|
|
19
19
|
//#region src/utils/package-manager.ts
|
|
20
20
|
/**
|
|
21
21
|
* Package manager detection and command abstraction.
|
|
@@ -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], {
|
|
@@ -978,13 +1217,35 @@ function sanitize(properties) {
|
|
|
978
1217
|
}
|
|
979
1218
|
return out;
|
|
980
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* The CLI's own version, read by walking up to this package's manifest.
|
|
1222
|
+
*
|
|
1223
|
+
* The obvious `require("../../package.json")` was wrong everywhere, not only on
|
|
1224
|
+
* one install path: `vite build` bundles this module into `dist/index.es.js`, so
|
|
1225
|
+
* the specifier resolves relative to `<pkg>/dist/` and lands on
|
|
1226
|
+
* `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or
|
|
1227
|
+
* the monorepo. Every event ever sent carried `cliVersion: "unknown"`, which is
|
|
1228
|
+
* the one field that makes the rest of a payload interpretable.
|
|
1229
|
+
*
|
|
1230
|
+
* So walk up and check the manifest's `name` rather than counting directory
|
|
1231
|
+
* levels: the count differs between `src/telemetry/` and the bundled `dist/`,
|
|
1232
|
+
* and a wrong count fails silently by finding *some* package.json — the nearest
|
|
1233
|
+
* dependency's, under a hoisted layout. Matching the name cannot do that.
|
|
1234
|
+
*/
|
|
981
1235
|
function cliVersion() {
|
|
982
1236
|
try {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1237
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1238
|
+
const root = path.parse(dir).root;
|
|
1239
|
+
while (dir && dir !== root) {
|
|
1240
|
+
const manifest = path.join(dir, "package.json");
|
|
1241
|
+
if (fs.existsSync(manifest)) {
|
|
1242
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1243
|
+
if (pkg?.name === "@rebasepro/cli" && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1244
|
+
}
|
|
1245
|
+
dir = path.dirname(dir);
|
|
1246
|
+
}
|
|
1247
|
+
} catch {}
|
|
1248
|
+
return "unknown";
|
|
988
1249
|
}
|
|
989
1250
|
function buildEvent(event, properties, identity) {
|
|
990
1251
|
return {
|
|
@@ -1484,7 +1745,7 @@ ${chalk.bold("Examples")}
|
|
|
1484
1745
|
`);
|
|
1485
1746
|
}
|
|
1486
1747
|
async function createRebaseApp(rawArgs) {
|
|
1487
|
-
if (
|
|
1748
|
+
if (wantsHelp(rawArgs)) {
|
|
1488
1749
|
printInitHelp();
|
|
1489
1750
|
return;
|
|
1490
1751
|
}
|
|
@@ -1493,26 +1754,31 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
|
|
|
1493
1754
|
`);
|
|
1494
1755
|
await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
|
|
1495
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
|
+
};
|
|
1496
1773
|
async function promptForOptions(rawArgs, pm) {
|
|
1497
|
-
const args =
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
"
|
|
1502
|
-
|
|
1503
|
-
"--headless": Boolean,
|
|
1504
|
-
"--project": String,
|
|
1505
|
-
"--setup-key": String,
|
|
1506
|
-
"--yes": Boolean,
|
|
1507
|
-
"-g": "--git",
|
|
1508
|
-
"-i": "--install",
|
|
1509
|
-
"-t": "--template",
|
|
1510
|
-
"-y": "--yes"
|
|
1511
|
-
}, {
|
|
1512
|
-
argv: rawArgs.slice(3),
|
|
1513
|
-
permissive: true
|
|
1774
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
1775
|
+
spec: INIT_FLAGS,
|
|
1776
|
+
rawArgs,
|
|
1777
|
+
commandWords: 1,
|
|
1778
|
+
command: "init",
|
|
1779
|
+
maxPositionals: 1
|
|
1514
1780
|
});
|
|
1515
|
-
const nameArg =
|
|
1781
|
+
const nameArg = positionals[0];
|
|
1516
1782
|
const isNonInteractive = args["--yes"] || false;
|
|
1517
1783
|
if (nameArg) {
|
|
1518
1784
|
const resolvedName = path.basename(path.resolve(process.cwd(), nameArg));
|
|
@@ -1635,6 +1901,49 @@ async function linkScaffoldToCloud(options) {
|
|
|
1635
1901
|
console.warn(chalk.yellow(` ${linkLater}`));
|
|
1636
1902
|
}
|
|
1637
1903
|
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Make the initial commit, after everything that writes into the project has run.
|
|
1906
|
+
*
|
|
1907
|
+
* It used to happen immediately after `git init`, which is before dependency
|
|
1908
|
+
* installation and before introspection — so `init --git --install` ended on a
|
|
1909
|
+
* dirty tree whose only untracked file was `pnpm-lock.yaml`. A lockfile is
|
|
1910
|
+
* precisely the thing that should be in a project's first commit, and a brand
|
|
1911
|
+
* new scaffold whose first `git status` is dirty invites the reader to conclude
|
|
1912
|
+
* the lockfile is deliberately ignored and never commit it at all.
|
|
1913
|
+
*
|
|
1914
|
+
* Introspection has the same shape: it generates `config/collections` and
|
|
1915
|
+
* `schema.generated.ts`, which belong in the commit describing the scaffold that
|
|
1916
|
+
* produced them.
|
|
1917
|
+
*
|
|
1918
|
+
* `git init` stays where it was. Creating the repository early costs nothing and
|
|
1919
|
+
* means a failed install still leaves the user a repository to commit into.
|
|
1920
|
+
*/
|
|
1921
|
+
async function commitScaffold(targetDirectory) {
|
|
1922
|
+
try {
|
|
1923
|
+
await execa("git", ["add", "-A"], { cwd: targetDirectory });
|
|
1924
|
+
let identity = {};
|
|
1925
|
+
try {
|
|
1926
|
+
await execa("git", ["config", "user.email"], { cwd: targetDirectory });
|
|
1927
|
+
} catch {
|
|
1928
|
+
identity = {
|
|
1929
|
+
GIT_AUTHOR_NAME: "Rebase",
|
|
1930
|
+
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1931
|
+
GIT_COMMITTER_NAME: "Rebase",
|
|
1932
|
+
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
await execa("git", [
|
|
1936
|
+
"commit",
|
|
1937
|
+
"-m",
|
|
1938
|
+
"Initial commit from Rebase"
|
|
1939
|
+
], {
|
|
1940
|
+
cwd: targetDirectory,
|
|
1941
|
+
env: identity
|
|
1942
|
+
});
|
|
1943
|
+
} catch {
|
|
1944
|
+
console.warn(chalk.yellow(" Warning: Failed to create the initial commit"));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1638
1947
|
async function createProject$1(options) {
|
|
1639
1948
|
const startedAt = Date.now();
|
|
1640
1949
|
if (fs.existsSync(options.targetDirectory)) {
|
|
@@ -1674,6 +1983,7 @@ async function createProject$1(options) {
|
|
|
1674
1983
|
await applyHeadless(options.targetDirectory, options.headless);
|
|
1675
1984
|
await replacePlaceholders(options);
|
|
1676
1985
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1986
|
+
let gitInitialized = false;
|
|
1677
1987
|
if (options.git) {
|
|
1678
1988
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
1679
1989
|
try {
|
|
@@ -1685,26 +1995,7 @@ async function createProject$1(options) {
|
|
|
1685
1995
|
"refs/heads/main"
|
|
1686
1996
|
], { cwd: options.targetDirectory });
|
|
1687
1997
|
} catch {}
|
|
1688
|
-
|
|
1689
|
-
let identity = {};
|
|
1690
|
-
try {
|
|
1691
|
-
await execa("git", ["config", "user.email"], { cwd: options.targetDirectory });
|
|
1692
|
-
} catch {
|
|
1693
|
-
identity = {
|
|
1694
|
-
GIT_AUTHOR_NAME: "Rebase",
|
|
1695
|
-
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1696
|
-
GIT_COMMITTER_NAME: "Rebase",
|
|
1697
|
-
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
await execa("git", [
|
|
1701
|
-
"commit",
|
|
1702
|
-
"-m",
|
|
1703
|
-
"Initial commit from Rebase"
|
|
1704
|
-
], {
|
|
1705
|
-
cwd: options.targetDirectory,
|
|
1706
|
-
env: identity
|
|
1707
|
-
});
|
|
1998
|
+
gitInitialized = true;
|
|
1708
1999
|
} catch {
|
|
1709
2000
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1710
2001
|
}
|
|
@@ -1761,6 +2052,7 @@ async function createProject$1(options) {
|
|
|
1761
2052
|
console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
|
|
1762
2053
|
}
|
|
1763
2054
|
}
|
|
2055
|
+
if (gitInitialized) await commitScaffold(options.targetDirectory);
|
|
1764
2056
|
await linkScaffoldToCloud(options);
|
|
1765
2057
|
console.log("");
|
|
1766
2058
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
@@ -2005,18 +2297,51 @@ project directory ${path.basename(options.targetDirectory)}/ was created and is
|
|
|
2005
2297
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
2006
2298
|
}
|
|
2007
2299
|
}
|
|
2008
|
-
|
|
2300
|
+
/** `undefined` binds the wildcard address, which is a different question — see isPortAvailable. */
|
|
2301
|
+
function canBind(port, host) {
|
|
2009
2302
|
return new Promise((resolve) => {
|
|
2010
2303
|
const server = net.createServer();
|
|
2011
|
-
server.once("error", () => {
|
|
2012
|
-
resolve(
|
|
2304
|
+
server.once("error", (err) => {
|
|
2305
|
+
resolve(err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
2013
2306
|
});
|
|
2014
2307
|
server.once("listening", () => {
|
|
2015
2308
|
server.close(() => resolve(true));
|
|
2016
2309
|
});
|
|
2017
|
-
server.listen(port);
|
|
2310
|
+
if (host === void 0) server.listen(port);
|
|
2311
|
+
else server.listen(port, host);
|
|
2018
2312
|
});
|
|
2019
2313
|
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Whether `port` is free — on the wildcard address *and* on both loopback addresses.
|
|
2316
|
+
*
|
|
2317
|
+
* All three, because on macOS/BSD a successful bind does not mean the port is
|
|
2318
|
+
* unused. Sockets carry `SO_REUSEADDR` (Node sets it), under which a wildcard
|
|
2319
|
+
* bind and a specific-address bind on the same port do not conflict — in either
|
|
2320
|
+
* direction. So each probe alone has a blind spot, and they are different ones:
|
|
2321
|
+
*
|
|
2322
|
+
* - **Wildcard only** (what this used to do) misses a server bound to
|
|
2323
|
+
* `127.0.0.1` and `[::1]` — a Homebrew or Postgres.app install, i.e. most
|
|
2324
|
+
* developer machines. 5432 was reported free while it was already serving
|
|
2325
|
+
* another project's database. Docker then published `*:5432` for the same
|
|
2326
|
+
* reason and the container started cleanly, with no "port already allocated"
|
|
2327
|
+
* error anywhere to hint at the collision. `DATABASE_URL` pointed at
|
|
2328
|
+
* `localhost:5432`, `localhost` resolves to `::1` first, and every command
|
|
2329
|
+
* reported success while reading and writing the *pre-existing* database — a
|
|
2330
|
+
* `db push` would have created tables, roles and RLS policies inside it.
|
|
2331
|
+
*
|
|
2332
|
+
* - **Loopback only** misses the opposite case: Docker Desktop publishes a
|
|
2333
|
+
* container's port on `*`, and a specific-address bind succeeds right past it.
|
|
2334
|
+
* That port is free to probe and unusable to publish, so `docker compose up -d
|
|
2335
|
+
* db` fails on "Bind for 0.0.0.0:PORT failed: port is already allocated" —
|
|
2336
|
+
* loudly, but only after the project has been generated around the bad port.
|
|
2337
|
+
*
|
|
2338
|
+
* Requiring all three costs three sockets and leaves neither gap. The wildcard
|
|
2339
|
+
* bind is the one the container itself has to make; the loopback binds are the
|
|
2340
|
+
* addresses `DATABASE_URL` will actually name.
|
|
2341
|
+
*/
|
|
2342
|
+
async function isPortAvailable(port) {
|
|
2343
|
+
return await canBind(port) && await canBind(port, "127.0.0.1") && await canBind(port, "::1");
|
|
2344
|
+
}
|
|
2020
2345
|
async function findAvailablePort(startPort) {
|
|
2021
2346
|
let port = startPort;
|
|
2022
2347
|
while (!await isPortAvailable(port)) port++;
|
|
@@ -2073,6 +2398,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2073
2398
|
const envPath = path.join(targetDirectory, ".env");
|
|
2074
2399
|
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
2075
2400
|
fs.copyFileSync(envExamplePath, envPath);
|
|
2401
|
+
fs.chmodSync(envPath, 384);
|
|
2076
2402
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
2077
2403
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
2078
2404
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
@@ -2086,6 +2412,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2086
2412
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
2087
2413
|
const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
|
|
2088
2414
|
envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
|
|
2415
|
+
envContent = envContent.replace(/^#?\s*VITE_API_URL=.*$/m, "VITE_API_URL=");
|
|
2089
2416
|
const { tag: runtimeVersion, note } = resolveRuntimeImageTag(readCliVersion());
|
|
2090
2417
|
const pinned = `${note ? `${note}\n` : ""}REBASE_VERSION=${runtimeVersion}`;
|
|
2091
2418
|
envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, pinned) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\n${pinned}\n`;
|
|
@@ -2096,7 +2423,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2096
2423
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2097
2424
|
} else {
|
|
2098
2425
|
const dbPort = await findAvailablePort(5432);
|
|
2099
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://
|
|
2426
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2100
2427
|
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
2101
2428
|
if (fs.existsSync(dockerComposePath)) {
|
|
2102
2429
|
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
@@ -2104,7 +2431,11 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2104
2431
|
fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
|
|
2105
2432
|
}
|
|
2106
2433
|
}
|
|
2107
|
-
fs.writeFileSync(envPath, envContent,
|
|
2434
|
+
fs.writeFileSync(envPath, envContent, {
|
|
2435
|
+
encoding: "utf-8",
|
|
2436
|
+
mode: 384
|
|
2437
|
+
});
|
|
2438
|
+
fs.chmodSync(envPath, 384);
|
|
2108
2439
|
}
|
|
2109
2440
|
}
|
|
2110
2441
|
//#endregion
|
|
@@ -2374,7 +2705,17 @@ async function generateSdkCommand(args) {
|
|
|
2374
2705
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
2375
2706
|
console.log("");
|
|
2376
2707
|
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
2377
|
-
|
|
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
|
+
}
|
|
2378
2719
|
const schemaVersion = remoteSchemaVersion ?? computeSchemaVersion(collections);
|
|
2379
2720
|
files.push({
|
|
2380
2721
|
path: "schema.meta.ts",
|
|
@@ -2386,7 +2727,6 @@ async function generateSdkCommand(args) {
|
|
|
2386
2727
|
// curl -s <api-url>/api/meta/schema-version
|
|
2387
2728
|
//
|
|
2388
2729
|
export const SCHEMA_VERSION = ${JSON.stringify(schemaVersion)};
|
|
2389
|
-
export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOString())};
|
|
2390
2730
|
`
|
|
2391
2731
|
});
|
|
2392
2732
|
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
@@ -2408,8 +2748,9 @@ export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOS
|
|
|
2408
2748
|
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
2409
2749
|
console.log(chalk.gray(" });"));
|
|
2410
2750
|
console.log("");
|
|
2411
|
-
|
|
2412
|
-
|
|
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();`));
|
|
2413
2754
|
console.log("");
|
|
2414
2755
|
}
|
|
2415
2756
|
//#endregion
|
|
@@ -2890,13 +3231,20 @@ function validateManifest(raw) {
|
|
|
2890
3231
|
byPath.set(at, name);
|
|
2891
3232
|
}
|
|
2892
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
|
+
});
|
|
2893
3240
|
if (issues.length > 0) return { issues };
|
|
2894
3241
|
return {
|
|
2895
3242
|
manifest: {
|
|
2896
3243
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2897
3244
|
rebase: raw.rebase,
|
|
2898
3245
|
apps,
|
|
2899
|
-
...storage ? { storage } : {}
|
|
3246
|
+
...storage ? { storage } : {},
|
|
3247
|
+
...telemetry !== void 0 ? { telemetry } : {}
|
|
2900
3248
|
},
|
|
2901
3249
|
issues
|
|
2902
3250
|
};
|
|
@@ -3005,7 +3353,7 @@ function synthesizeManifest(projectRoot) {
|
|
|
3005
3353
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
3006
3354
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
3007
3355
|
apps.backend = backend;
|
|
3008
|
-
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.");
|
|
3009
3357
|
}
|
|
3010
3358
|
if (exists("frontend")) apps.web = {
|
|
3011
3359
|
type: "static",
|
|
@@ -3053,13 +3401,59 @@ function loadManifest(projectRoot) {
|
|
|
3053
3401
|
filePath
|
|
3054
3402
|
};
|
|
3055
3403
|
}
|
|
3056
|
-
/**
|
|
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
|
+
*/
|
|
3057
3442
|
function writeManifest(projectRoot, manifest) {
|
|
3058
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);
|
|
3059
3450
|
const ordered = {
|
|
3060
|
-
$schema:
|
|
3451
|
+
$schema: schema,
|
|
3061
3452
|
rebase: manifest.rebase,
|
|
3062
|
-
apps: manifest.apps
|
|
3453
|
+
apps: manifest.apps,
|
|
3454
|
+
...storage ? { storage } : {},
|
|
3455
|
+
...telemetry !== void 0 ? { telemetry } : {},
|
|
3456
|
+
...carried
|
|
3063
3457
|
};
|
|
3064
3458
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
3065
3459
|
return filePath;
|
|
@@ -3323,38 +3717,67 @@ function getProjectPort(projectRoot) {
|
|
|
3323
3717
|
* 3. Previously used port from .rebase-dev-port (port affinity across restarts)
|
|
3324
3718
|
* 4. Deterministic hash from project path (unique per project)
|
|
3325
3719
|
*/
|
|
3720
|
+
/**
|
|
3721
|
+
* A TCP port, or `undefined` for anything that is not one.
|
|
3722
|
+
*
|
|
3723
|
+
* One predicate for both sources below. The port file was already checked for
|
|
3724
|
+
* range, and `PORT` — the source a human or a platform actually sets — was not,
|
|
3725
|
+
* so `PORT=oops` reached `parseInt` and was returned as `NaN`: the dev server
|
|
3726
|
+
* then bound to whatever the OS handed out and the CLI printed a URL for a port
|
|
3727
|
+
* nothing was listening on.
|
|
3728
|
+
*/
|
|
3729
|
+
function parsePort(raw) {
|
|
3730
|
+
if (raw === void 0) return void 0;
|
|
3731
|
+
const port = Number(raw.trim());
|
|
3732
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return void 0;
|
|
3733
|
+
return port;
|
|
3734
|
+
}
|
|
3326
3735
|
function resolveStartPort(projectRoot, explicitPort) {
|
|
3327
3736
|
if (explicitPort) return explicitPort;
|
|
3328
|
-
if (process.env.PORT)
|
|
3737
|
+
if (process.env.PORT) {
|
|
3738
|
+
const fromEnv = parsePort(process.env.PORT);
|
|
3739
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
3740
|
+
console.warn(chalk.yellow(` ⚠ Ignoring PORT="${process.env.PORT}" — not a port between 1 and 65535.`));
|
|
3741
|
+
}
|
|
3329
3742
|
try {
|
|
3330
3743
|
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
3331
3744
|
if (fs.existsSync(portFile)) {
|
|
3332
|
-
const saved =
|
|
3333
|
-
if (saved
|
|
3745
|
+
const saved = parsePort(fs.readFileSync(portFile, "utf-8"));
|
|
3746
|
+
if (saved !== void 0) return saved;
|
|
3334
3747
|
}
|
|
3335
3748
|
} catch {}
|
|
3336
3749
|
return getProjectPort(projectRoot);
|
|
3337
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
|
+
};
|
|
3338
3769
|
async function devCommand(rawArgs) {
|
|
3339
|
-
|
|
3340
|
-
"--backend-only": Boolean,
|
|
3341
|
-
"--frontend-only": Boolean,
|
|
3342
|
-
"--port": Number,
|
|
3343
|
-
"--generate": Boolean,
|
|
3344
|
-
"--help": Boolean,
|
|
3345
|
-
"-b": "--backend-only",
|
|
3346
|
-
"-f": "--frontend-only",
|
|
3347
|
-
"-p": "--port",
|
|
3348
|
-
"-g": "--generate",
|
|
3349
|
-
"-h": "--help"
|
|
3350
|
-
}, {
|
|
3351
|
-
argv: rawArgs.slice(3),
|
|
3352
|
-
permissive: true
|
|
3353
|
-
});
|
|
3354
|
-
if (args["--help"]) {
|
|
3770
|
+
if (wantsHelp(rawArgs)) {
|
|
3355
3771
|
printDevHelp();
|
|
3356
3772
|
return;
|
|
3357
3773
|
}
|
|
3774
|
+
const { flags: args } = parseCommandArgs({
|
|
3775
|
+
spec: DEV_FLAGS,
|
|
3776
|
+
rawArgs,
|
|
3777
|
+
commandWords: 1,
|
|
3778
|
+
command: "dev",
|
|
3779
|
+
maxPositionals: 0
|
|
3780
|
+
});
|
|
3358
3781
|
const projectRoot = requireProjectRoot();
|
|
3359
3782
|
recordEvent("cli.dev", {
|
|
3360
3783
|
backend_only: Boolean(args["--backend-only"]),
|
|
@@ -3696,7 +4119,7 @@ ${chalk.green.bold("Usage")}
|
|
|
3696
4119
|
${chalk.green.bold("Options")}
|
|
3697
4120
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
3698
4121
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
3699
|
-
${chalk.blue("--port, -
|
|
4122
|
+
${chalk.blue("--port, -P")} Backend port (default: auto-detected per project)
|
|
3700
4123
|
${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
|
|
3701
4124
|
|
|
3702
4125
|
${chalk.green.bold("Description")}
|
|
@@ -4421,10 +4844,11 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
4421
4844
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
4422
4845
|
* sitting in the repository looking exactly like the server.
|
|
4423
4846
|
*
|
|
4424
|
-
* A project that means to
|
|
4425
|
-
* 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
|
|
4426
4849
|
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
4427
|
-
* 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".
|
|
4428
4852
|
*/
|
|
4429
4853
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
4430
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)));
|
|
@@ -4458,7 +4882,9 @@ async function buildBundle(options) {
|
|
|
4458
4882
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
4459
4883
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
4460
4884
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
4461
|
-
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."));
|
|
4462
4888
|
}
|
|
4463
4889
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
4464
4890
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -4827,6 +5253,61 @@ function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
|
4827
5253
|
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
4828
5254
|
}
|
|
4829
5255
|
/**
|
|
5256
|
+
* The environment every static app is built with, wherever that build is driven from.
|
|
5257
|
+
*
|
|
5258
|
+
* Shared because there are two drivers — `foldFrontendIntoBundle` here, and
|
|
5259
|
+
* `buildAssetApp` in `build.ts` for a standalone `type: "static"` app — and they
|
|
5260
|
+
* had already drifted: the path variables were duplicated into both, so a fix
|
|
5261
|
+
* applied to one shipped a bundle built the old way from the other. One function
|
|
5262
|
+
* makes that impossible rather than merely unlikely.
|
|
5263
|
+
*
|
|
5264
|
+
* ## REBASE_APP_*
|
|
5265
|
+
*
|
|
5266
|
+
* The declared path is a build-time input, not only a serving concern: Vite
|
|
5267
|
+
* reads `base` from REBASE_APP_BASE, and the trailing slash is that field's
|
|
5268
|
+
* convention. See `assertBuiltForPath`.
|
|
5269
|
+
*
|
|
5270
|
+
* ## NODE_ENV
|
|
5271
|
+
*
|
|
5272
|
+
* A built app is a production artifact by construction, so build it as one. Not
|
|
5273
|
+
* a formality: the scaffold's `.env` carries `NODE_ENV=development` for the dev
|
|
5274
|
+
* backend, and Vite's `loadEnv` promotes a `NODE_ENV` found in an env file into
|
|
5275
|
+
* the build unless the environment already sets one. So `rebase build` and
|
|
5276
|
+
* `rebase cloud deploy` shipped a *development* bundle — `import.meta.env.DEV
|
|
5277
|
+
* === true`, development React, dev-only branches live — from commands whose
|
|
5278
|
+
* whole purpose is to produce something deployable. Setting it here is what
|
|
5279
|
+
* closes it: Vite consults the env file's NODE_ENV only when `process.env`
|
|
5280
|
+
* has none.
|
|
5281
|
+
*
|
|
5282
|
+
* ## VITE_API_URL
|
|
5283
|
+
*
|
|
5284
|
+
* An app served by the backend it talks to has its API on its own origin by
|
|
5285
|
+
* construction, so a baked-in absolute URL can only be wrong. It was: that same
|
|
5286
|
+
* `.env` carries `VITE_API_URL=http://localhost:3001` and
|
|
5287
|
+
* `frontend/vite.config.ts` reads the project root via `envDir: ".."`, so a
|
|
5288
|
+
* stock deploy shipped a site whose every request went to whoever ran the build
|
|
5289
|
+
* — passing every server-side health check on the way out. Blanking it here
|
|
5290
|
+
* fixes the bundle even for a project whose `.env` predates the `init` fix, or
|
|
5291
|
+
* was written by hand. Empty is the right value rather than a missing one: the
|
|
5292
|
+
* client falls back to `window.location.origin`, which keeps working when a
|
|
5293
|
+
* custom domain is added.
|
|
5294
|
+
*
|
|
5295
|
+
* Vite prioritises `process.env.VITE_*` over `.env` files, so an explicit
|
|
5296
|
+
* `VITE_API_URL=https://api.example.com rebase cloud deploy` still wins — the
|
|
5297
|
+
* cross-origin escape hatch stays open, it just has to be deliberate. Nothing on
|
|
5298
|
+
* this path loads the project `.env` into `process.env`, so a value inherited
|
|
5299
|
+
* here really was set by the caller.
|
|
5300
|
+
*/
|
|
5301
|
+
function staticBuildEnv(appPath, appName) {
|
|
5302
|
+
return {
|
|
5303
|
+
REBASE_APP_PATH: appPath,
|
|
5304
|
+
REBASE_APP_BASE: appPath === "/" ? "/" : `${appPath}/`,
|
|
5305
|
+
REBASE_APP_NAME: appName,
|
|
5306
|
+
NODE_ENV: "production",
|
|
5307
|
+
VITE_API_URL: process.env.VITE_API_URL ?? ""
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
5310
|
+
/**
|
|
4830
5311
|
* Build the project's static apps and fold them into the backend bundle.
|
|
4831
5312
|
*
|
|
4832
5313
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
@@ -4845,11 +5326,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
4845
5326
|
cwd: projectRoot,
|
|
4846
5327
|
stdio: "inherit",
|
|
4847
5328
|
shell: true,
|
|
4848
|
-
env:
|
|
4849
|
-
REBASE_APP_PATH: app.path,
|
|
4850
|
-
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4851
|
-
REBASE_APP_NAME: app.name
|
|
4852
|
-
}
|
|
5329
|
+
env: staticBuildEnv(app.path, app.name)
|
|
4853
5330
|
});
|
|
4854
5331
|
const assetsDir = path.join(projectRoot, app.output);
|
|
4855
5332
|
if (!fs.existsSync(assetsDir)) throw new Error(`"${app.name}" declared output "${app.output}" does not exist after building — the bundle would ship without a frontend.`);
|
|
@@ -4907,23 +5384,24 @@ ${chalk.bold("Examples")}
|
|
|
4907
5384
|
`.trim());
|
|
4908
5385
|
}
|
|
4909
5386
|
async function buildCommand(rawArgs = []) {
|
|
4910
|
-
|
|
4911
|
-
"--out": String,
|
|
4912
|
-
"--skip-type-check": Boolean,
|
|
4913
|
-
"--skip-schema": Boolean,
|
|
4914
|
-
"--no-static": Boolean,
|
|
4915
|
-
"--skip-static-build": Boolean,
|
|
4916
|
-
"--legacy": Boolean,
|
|
4917
|
-
"--help": Boolean,
|
|
4918
|
-
"-h": "--help"
|
|
4919
|
-
}, {
|
|
4920
|
-
argv: rawArgs.slice(3),
|
|
4921
|
-
permissive: true
|
|
4922
|
-
});
|
|
4923
|
-
if (args["--help"]) {
|
|
5387
|
+
if (wantsHelp(rawArgs)) {
|
|
4924
5388
|
printHelp$5();
|
|
4925
5389
|
return;
|
|
4926
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
|
+
});
|
|
4927
5405
|
const projectRoot = requireProjectRoot();
|
|
4928
5406
|
if (args["--legacy"]) {
|
|
4929
5407
|
await runWorkspaceBuilds(projectRoot);
|
|
@@ -4941,7 +5419,6 @@ async function buildCommand(rawArgs = []) {
|
|
|
4941
5419
|
throw err;
|
|
4942
5420
|
}
|
|
4943
5421
|
const { manifest, source } = loaded;
|
|
4944
|
-
const requested = args._.filter((a) => !a.startsWith("-"));
|
|
4945
5422
|
let targets = buildableApps(manifest);
|
|
4946
5423
|
if (requested.length > 0) {
|
|
4947
5424
|
const known = new Set(targets.map((t) => t.name));
|
|
@@ -4976,7 +5453,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4976
5453
|
projectRoot,
|
|
4977
5454
|
appName: name,
|
|
4978
5455
|
app,
|
|
4979
|
-
outDir: args["--
|
|
5456
|
+
outDir: args["--output"],
|
|
4980
5457
|
runtimeRange: manifest.rebase,
|
|
4981
5458
|
storage: manifest.storage,
|
|
4982
5459
|
skipTypeCheck: args["--skip-type-check"],
|
|
@@ -5013,7 +5490,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
5013
5490
|
});
|
|
5014
5491
|
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
5015
5492
|
}
|
|
5016
|
-
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--
|
|
5493
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--output"]);
|
|
5017
5494
|
console.log("");
|
|
5018
5495
|
}
|
|
5019
5496
|
console.log(chalk.green("✓ Build complete."));
|
|
@@ -5038,11 +5515,7 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
5038
5515
|
cwd: projectRoot,
|
|
5039
5516
|
stdio: "inherit",
|
|
5040
5517
|
shell: true,
|
|
5041
|
-
env:
|
|
5042
|
-
REBASE_APP_PATH: basePath,
|
|
5043
|
-
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
5044
|
-
REBASE_APP_NAME: name
|
|
5045
|
-
}
|
|
5518
|
+
env: staticBuildEnv(basePath, name)
|
|
5046
5519
|
});
|
|
5047
5520
|
} catch {
|
|
5048
5521
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -5124,6 +5597,56 @@ function findCliRoot(from) {
|
|
|
5124
5597
|
}
|
|
5125
5598
|
return null;
|
|
5126
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
|
+
}
|
|
5127
5650
|
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
5128
5651
|
var PAYLOAD = [
|
|
5129
5652
|
{
|
|
@@ -5174,25 +5697,30 @@ ${chalk.bold("Usage")}
|
|
|
5174
5697
|
|
|
5175
5698
|
${chalk.bold("Options")}
|
|
5176
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
|
|
5177
5702
|
-h, --help Show this help
|
|
5178
5703
|
`.trim());
|
|
5179
5704
|
}
|
|
5180
5705
|
async function ejectCommand(rawArgs = []) {
|
|
5181
|
-
|
|
5182
|
-
"--dry-run": Boolean,
|
|
5183
|
-
"--help": Boolean,
|
|
5184
|
-
"-h": "--help"
|
|
5185
|
-
}, {
|
|
5186
|
-
argv: rawArgs.slice(2),
|
|
5187
|
-
permissive: true
|
|
5188
|
-
});
|
|
5189
|
-
if (args["--help"]) {
|
|
5706
|
+
if (wantsHelp(rawArgs)) {
|
|
5190
5707
|
printHelp$4();
|
|
5191
5708
|
return;
|
|
5192
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
|
+
});
|
|
5193
5720
|
const projectRoot = requireProjectRoot();
|
|
5194
5721
|
const dryRun = Boolean(args["--dry-run"]);
|
|
5195
|
-
const
|
|
5722
|
+
const force = Boolean(args["--force"]);
|
|
5723
|
+
const requested = positionals[0];
|
|
5196
5724
|
let loaded;
|
|
5197
5725
|
try {
|
|
5198
5726
|
loaded = loadManifest(projectRoot);
|
|
@@ -5241,7 +5769,12 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5241
5769
|
process.exit(1);
|
|
5242
5770
|
}
|
|
5243
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
|
+
};
|
|
5244
5776
|
const planned = [];
|
|
5777
|
+
const blocked = [];
|
|
5245
5778
|
for (const file of PAYLOAD) {
|
|
5246
5779
|
const source = path.join(payloadDir, file.from);
|
|
5247
5780
|
if (!fs.existsSync(source)) {
|
|
@@ -5249,26 +5782,43 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5249
5782
|
process.exit(1);
|
|
5250
5783
|
}
|
|
5251
5784
|
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
5785
|
+
if (exists && file.overwrite && !force) blocked.push(file.to);
|
|
5252
5786
|
planned.push({
|
|
5253
5787
|
to: file.to,
|
|
5254
|
-
action: exists
|
|
5788
|
+
action: !exists ? "write" : file.overwrite ? "overwrite" : "keep"
|
|
5255
5789
|
});
|
|
5256
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
|
+
}
|
|
5257
5799
|
if (dryRun) {
|
|
5258
5800
|
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
5259
5801
|
console.log("");
|
|
5260
|
-
for (const item of planned)
|
|
5261
|
-
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)")}`);
|
|
5262
5807
|
console.log("");
|
|
5263
5808
|
console.log(chalk.dim("Nothing was changed."));
|
|
5264
5809
|
return;
|
|
5265
5810
|
}
|
|
5266
5811
|
const projectName = projectNameOf(projectRoot);
|
|
5812
|
+
const backups = [];
|
|
5267
5813
|
for (const [index, file] of PAYLOAD.entries()) {
|
|
5268
5814
|
if (planned[index].action === "keep") continue;
|
|
5269
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
|
+
}
|
|
5270
5820
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
5271
|
-
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8")
|
|
5821
|
+
const contents = renderPayload(fs.readFileSync(path.join(payloadDir, file.from), "utf8"), shape, projectName);
|
|
5272
5822
|
fs.writeFileSync(destination, contents, "utf8");
|
|
5273
5823
|
}
|
|
5274
5824
|
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
@@ -5288,9 +5838,14 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5288
5838
|
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
5289
5839
|
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
5290
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`);
|
|
5291
5842
|
console.log("");
|
|
5292
5843
|
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
5293
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
|
+
}
|
|
5294
5849
|
console.log("");
|
|
5295
5850
|
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
5296
5851
|
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
@@ -5347,19 +5902,20 @@ Build first with ${chalk.cyan("rebase build")}.
|
|
|
5347
5902
|
`.trim());
|
|
5348
5903
|
}
|
|
5349
5904
|
async function startCommand(rawArgs = []) {
|
|
5350
|
-
|
|
5351
|
-
"--bundle": String,
|
|
5352
|
-
"--legacy": Boolean,
|
|
5353
|
-
"--help": Boolean,
|
|
5354
|
-
"-h": "--help"
|
|
5355
|
-
}, {
|
|
5356
|
-
argv: rawArgs.slice(3),
|
|
5357
|
-
permissive: true
|
|
5358
|
-
});
|
|
5359
|
-
if (args["--help"]) {
|
|
5905
|
+
if (wantsHelp(rawArgs)) {
|
|
5360
5906
|
printHelp$3();
|
|
5361
5907
|
return;
|
|
5362
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
|
+
});
|
|
5363
5919
|
const projectRoot = requireProjectRoot();
|
|
5364
5920
|
const envFile = findEnvFile(projectRoot);
|
|
5365
5921
|
const env = { ...process.env };
|
|
@@ -5374,7 +5930,10 @@ async function startCommand(rawArgs = []) {
|
|
|
5374
5930
|
}
|
|
5375
5931
|
ensureBundleDependencies(projectRoot, bundleDir);
|
|
5376
5932
|
console.log(`${chalk.bold("Rebase")} — starting runtime from ${chalk.cyan(path.relative(projectRoot, bundleDir))}/\n`);
|
|
5377
|
-
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
|
+
});
|
|
5378
5937
|
process.env.REBASE_BUNDLE = bundleDir;
|
|
5379
5938
|
try {
|
|
5380
5939
|
const { runFromBundle } = await import("@rebasepro/server");
|
|
@@ -5505,7 +6064,7 @@ function selectUserForEmail(payload, email) {
|
|
|
5505
6064
|
}
|
|
5506
6065
|
}
|
|
5507
6066
|
async function authCommand(subcommand, rawArgs) {
|
|
5508
|
-
if (!subcommand || subcommand === "--help") {
|
|
6067
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5509
6068
|
printAuthHelp();
|
|
5510
6069
|
return;
|
|
5511
6070
|
}
|
|
@@ -5520,18 +6079,48 @@ async function authCommand(subcommand, rawArgs) {
|
|
|
5520
6079
|
process.exit(1);
|
|
5521
6080
|
}
|
|
5522
6081
|
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
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
|
|
5532
6116
|
});
|
|
5533
|
-
|
|
5534
|
-
|
|
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);
|
|
5535
6124
|
if (!email) {
|
|
5536
6125
|
console.error(chalk.red("✗ Email is required."));
|
|
5537
6126
|
console.log("");
|
|
@@ -5618,7 +6207,7 @@ import * as dotenv from "dotenv";
|
|
|
5618
6207
|
import path from "path";
|
|
5619
6208
|
import fs from "fs";
|
|
5620
6209
|
|
|
5621
|
-
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH });
|
|
6210
|
+
dotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });
|
|
5622
6211
|
|
|
5623
6212
|
const email = process.env.REBASE_RESET_EMAIL!;
|
|
5624
6213
|
const newPassword = process.env.REBASE_RESET_PASSWORD!;
|
|
@@ -5730,7 +6319,34 @@ ${chalk.green.bold("Examples")}
|
|
|
5730
6319
|
* Detects three-way schema drift between collection definitions,
|
|
5731
6320
|
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
5732
6321
|
*/
|
|
6322
|
+
/**
|
|
6323
|
+
* `--help` is answered before the project guard, not after.
|
|
6324
|
+
*
|
|
6325
|
+
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
6326
|
+
* body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
|
|
6327
|
+
* project answered "✗ Could not find a Rebase project root." Asking a command
|
|
6328
|
+
* what it does is the one question that cannot require being somewhere
|
|
6329
|
+
* particular to ask.
|
|
6330
|
+
*/
|
|
6331
|
+
function printDoctorHelp() {
|
|
6332
|
+
console.log(`
|
|
6333
|
+
${chalk.bold("rebase doctor")} — Detect drift between collections, schema and database
|
|
6334
|
+
|
|
6335
|
+
${chalk.green.bold("Usage")}
|
|
6336
|
+
rebase doctor
|
|
6337
|
+
|
|
6338
|
+
Compares the collections you declare, the generated Drizzle schema, and the
|
|
6339
|
+
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
6340
|
+
|
|
6341
|
+
Run from inside a Rebase project — it reads the project's collections and
|
|
6342
|
+
connects to its database.
|
|
6343
|
+
`);
|
|
6344
|
+
}
|
|
5733
6345
|
async function doctorCommand(rawArgs) {
|
|
6346
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6347
|
+
printDoctorHelp();
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
5734
6350
|
const projectRoot = requireProjectRoot();
|
|
5735
6351
|
const backendDir = requireBackendDir(projectRoot);
|
|
5736
6352
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
@@ -5771,12 +6387,21 @@ async function doctorCommand(rawArgs) {
|
|
|
5771
6387
|
//#endregion
|
|
5772
6388
|
//#region src/commands/skills.ts
|
|
5773
6389
|
var require = createRequire(import.meta.url);
|
|
5774
|
-
/**
|
|
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
|
+
*/
|
|
5775
6399
|
var AGENTS = {
|
|
5776
6400
|
cursor: {
|
|
5777
6401
|
label: "Cursor",
|
|
5778
6402
|
detectDir: ".cursor",
|
|
5779
6403
|
targetDir: ".cursor/rules",
|
|
6404
|
+
flatLayout: true,
|
|
5780
6405
|
/** Cursor uses .mdc files (Markdown with Context). */
|
|
5781
6406
|
transformFile: (skillName, content) => ({
|
|
5782
6407
|
fileName: `${skillName}.mdc`,
|
|
@@ -5787,6 +6412,7 @@ var AGENTS = {
|
|
|
5787
6412
|
label: "Claude Code",
|
|
5788
6413
|
detectDir: ".claude",
|
|
5789
6414
|
targetDir: ".claude/skills",
|
|
6415
|
+
flatLayout: false,
|
|
5790
6416
|
/** Claude Code uses the standard SKILL.md format in subdirectories. */
|
|
5791
6417
|
transformFile: (skillName, content) => ({
|
|
5792
6418
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5797,6 +6423,7 @@ var AGENTS = {
|
|
|
5797
6423
|
label: "Windsurf",
|
|
5798
6424
|
detectDir: ".windsurf",
|
|
5799
6425
|
targetDir: ".windsurf/rules",
|
|
6426
|
+
flatLayout: true,
|
|
5800
6427
|
/** Windsurf uses plain .md files. */
|
|
5801
6428
|
transformFile: (skillName, content) => ({
|
|
5802
6429
|
fileName: `${skillName}.md`,
|
|
@@ -5807,6 +6434,7 @@ var AGENTS = {
|
|
|
5807
6434
|
label: "Gemini CLI / Antigravity",
|
|
5808
6435
|
detectDir: ".agents",
|
|
5809
6436
|
targetDir: ".agents/skills",
|
|
6437
|
+
flatLayout: false,
|
|
5810
6438
|
/** Gemini uses the standard SKILL.md format in subdirectories. */
|
|
5811
6439
|
transformFile: (skillName, content) => ({
|
|
5812
6440
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5825,21 +6453,65 @@ function getSkillsSourceDir() {
|
|
|
5825
6453
|
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
5826
6454
|
return skillsDir;
|
|
5827
6455
|
}
|
|
5828
|
-
/**
|
|
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. */
|
|
5829
6481
|
function loadSkills(skillsDir) {
|
|
5830
6482
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
5831
6483
|
const skills = [];
|
|
5832
6484
|
for (const entry of entries) {
|
|
5833
6485
|
if (!entry.isDirectory()) continue;
|
|
5834
|
-
const
|
|
6486
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
6487
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
5835
6488
|
if (!fs.existsSync(skillMdPath)) continue;
|
|
5836
6489
|
skills.push({
|
|
5837
6490
|
name: entry.name,
|
|
5838
|
-
|
|
6491
|
+
dir: skillDir,
|
|
6492
|
+
content: fs.readFileSync(skillMdPath, "utf-8"),
|
|
6493
|
+
assets: loadSkillAssets(skillDir)
|
|
5839
6494
|
});
|
|
5840
6495
|
}
|
|
5841
6496
|
return skills;
|
|
5842
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
|
+
}
|
|
5843
6515
|
/** Detect which agent environments already exist in the project. */
|
|
5844
6516
|
function detectAgents(projectDir) {
|
|
5845
6517
|
const detected = [];
|
|
@@ -5852,16 +6524,31 @@ function installForAgent(agentKey, skills, projectDir) {
|
|
|
5852
6524
|
const targetBase = path.join(projectDir, agent.targetDir);
|
|
5853
6525
|
fs.mkdirSync(targetBase, { recursive: true });
|
|
5854
6526
|
let count = 0;
|
|
6527
|
+
let assetCount = 0;
|
|
5855
6528
|
for (const skill of skills) {
|
|
5856
|
-
const
|
|
6529
|
+
const body = agent.flatLayout ? rewriteAssetLinks(skill.content, skill.assets, skill.name) : skill.content;
|
|
6530
|
+
const { fileName, content } = agent.transformFile(skill.name, body);
|
|
5857
6531
|
const targetPath = path.join(targetBase, fileName);
|
|
5858
6532
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
5859
6533
|
fs.writeFileSync(targetPath, content, "utf-8");
|
|
5860
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
|
+
}
|
|
5861
6541
|
}
|
|
5862
|
-
return
|
|
6542
|
+
return {
|
|
6543
|
+
skills: count,
|
|
6544
|
+
assets: assetCount
|
|
6545
|
+
};
|
|
5863
6546
|
}
|
|
5864
6547
|
async function skillsCommand(subcommand, rawArgs) {
|
|
6548
|
+
if (wantsHelp(rawArgs)) {
|
|
6549
|
+
printSkillsHelp();
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
5865
6552
|
switch (subcommand) {
|
|
5866
6553
|
case "install":
|
|
5867
6554
|
await skillsInstall(rawArgs);
|
|
@@ -5944,9 +6631,10 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5944
6631
|
console.log("");
|
|
5945
6632
|
for (const agentKey of agents) {
|
|
5946
6633
|
const agent = AGENTS[agentKey];
|
|
5947
|
-
const count = installForAgent(agentKey, skills, projectDir);
|
|
6634
|
+
const { skills: count, assets } = installForAgent(agentKey, skills, projectDir);
|
|
5948
6635
|
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
5949
|
-
|
|
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)}`);
|
|
5950
6638
|
}
|
|
5951
6639
|
console.log("");
|
|
5952
6640
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -6007,7 +6695,7 @@ function resolveBaseUrl(env, projectRoot) {
|
|
|
6007
6695
|
return `http://localhost:${env.PORT || env.REBASE_PORT || "3001"}`;
|
|
6008
6696
|
}
|
|
6009
6697
|
async function apiKeysCommand(subcommand, rawArgs) {
|
|
6010
|
-
if (!subcommand || subcommand === "--help") {
|
|
6698
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
6011
6699
|
printApiKeysHelp();
|
|
6012
6700
|
return;
|
|
6013
6701
|
}
|
|
@@ -6069,20 +6757,43 @@ async function listKeys(_rawArgs) {
|
|
|
6069
6757
|
process.exit(1);
|
|
6070
6758
|
}
|
|
6071
6759
|
}
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
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
|
|
6084
6789
|
});
|
|
6085
|
-
|
|
6790
|
+
return {
|
|
6791
|
+
flags,
|
|
6792
|
+
name: flags["--name"] || positionals[0]
|
|
6793
|
+
};
|
|
6794
|
+
}
|
|
6795
|
+
async function createKey(rawArgs) {
|
|
6796
|
+
const { flags: args, name } = resolveCreateKeyArgs(rawArgs);
|
|
6086
6797
|
const permissionsRaw = args["--permissions"];
|
|
6087
6798
|
if (!name) {
|
|
6088
6799
|
console.error(chalk.red("✗ Name is required."));
|
|
@@ -6183,12 +6894,30 @@ async function createKey(rawArgs) {
|
|
|
6183
6894
|
process.exit(1);
|
|
6184
6895
|
}
|
|
6185
6896
|
}
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
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
|
|
6190
6916
|
});
|
|
6191
|
-
|
|
6917
|
+
return { id: flags["--id"] || positionals[0] };
|
|
6918
|
+
}
|
|
6919
|
+
async function revokeKey(rawArgs) {
|
|
6920
|
+
const { id } = resolveRevokeKeyArgs(rawArgs);
|
|
6192
6921
|
if (!id) {
|
|
6193
6922
|
console.error(chalk.red("✗ Key ID is required."));
|
|
6194
6923
|
console.log("");
|
|
@@ -6265,7 +6994,12 @@ ${chalk.green.bold("Examples")}
|
|
|
6265
6994
|
* a documentation comment that quietly fell out of date two releases ago.
|
|
6266
6995
|
*/
|
|
6267
6996
|
async function telemetryCommand(rawArgs) {
|
|
6268
|
-
|
|
6997
|
+
const subcommand = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0];
|
|
6998
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6999
|
+
printHelp$2();
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
switch (subcommand) {
|
|
6269
7003
|
case "status":
|
|
6270
7004
|
case void 0:
|
|
6271
7005
|
printStatus();
|
|
@@ -6364,16 +7098,16 @@ async function loginCommand(rawArgs) {
|
|
|
6364
7098
|
const args = arg({
|
|
6365
7099
|
"--email": String,
|
|
6366
7100
|
"--password": String,
|
|
6367
|
-
"-e": "--email"
|
|
6368
|
-
"-p": "--password"
|
|
7101
|
+
"-e": "--email"
|
|
6369
7102
|
}, {
|
|
6370
7103
|
argv: rawArgs.slice(3),
|
|
6371
7104
|
permissive: true
|
|
6372
7105
|
});
|
|
6373
7106
|
const url = resolveCloudUrl(rawArgs);
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
7107
|
+
noteBlank();
|
|
7108
|
+
note(`Signing in to ${chalk.cyan(url)}`);
|
|
7109
|
+
noteBlank();
|
|
7110
|
+
if (!args["--email"] || !args["--password"]) requireInteractive("credentials", "--email and --password");
|
|
6377
7111
|
const prompts = [];
|
|
6378
7112
|
if (!args["--email"]) prompts.push({
|
|
6379
7113
|
type: "input",
|
|
@@ -6399,14 +7133,21 @@ async function loginCommand(rawArgs) {
|
|
|
6399
7133
|
if (orgs.data.length === 1 && !getContextOrg(url)) setContextOrg(url, String(orgs.data[0].id));
|
|
6400
7134
|
} catch {}
|
|
6401
7135
|
success(`Logged in as ${chalk.bold(user.email ?? email)}`);
|
|
6402
|
-
|
|
6403
|
-
[
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
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
|
+
});
|
|
6408
7149
|
} catch (e) {
|
|
6409
|
-
if (e?.status === 401) fail("Invalid email or password.");
|
|
7150
|
+
if (e?.status === 401) fail("Invalid email or password.", void 0, "invalid_credentials");
|
|
6410
7151
|
reportError(e, "Login failed");
|
|
6411
7152
|
}
|
|
6412
7153
|
}
|
|
@@ -6414,34 +7155,60 @@ async function logoutCommand(rawArgs) {
|
|
|
6414
7155
|
const url = resolveCloudUrl(rawArgs);
|
|
6415
7156
|
const client = createCloudClient(url);
|
|
6416
7157
|
if (!client.auth.getSession()) {
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
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
|
+
});
|
|
6420
7167
|
return;
|
|
6421
7168
|
}
|
|
6422
7169
|
try {
|
|
6423
7170
|
await client.auth.signOut();
|
|
6424
7171
|
} catch {}
|
|
6425
7172
|
success(`Logged out of ${url}`);
|
|
7173
|
+
emit(() => {}, {
|
|
7174
|
+
success: true,
|
|
7175
|
+
host: url,
|
|
7176
|
+
wasLoggedIn: true
|
|
7177
|
+
});
|
|
6426
7178
|
}
|
|
6427
7179
|
async function whoamiCommand(rawArgs) {
|
|
6428
7180
|
const { client, url } = await requireClient(rawArgs);
|
|
6429
7181
|
try {
|
|
6430
7182
|
const user = await client.auth.getUser();
|
|
6431
|
-
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");
|
|
6432
7184
|
const link = readLink();
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
[
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
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
|
+
});
|
|
6445
7212
|
} catch (e) {
|
|
6446
7213
|
reportError(e, "Failed to fetch session");
|
|
6447
7214
|
}
|
|
@@ -6471,10 +7238,10 @@ async function linkDirect(target, rawArgs) {
|
|
|
6471
7238
|
try {
|
|
6472
7239
|
base = new URL(target);
|
|
6473
7240
|
} catch {
|
|
6474
|
-
fail(`"${target}" is not a valid URL
|
|
7241
|
+
fail(`"${target}" is not a valid URL.`, void 0, "invalid_url");
|
|
6475
7242
|
return;
|
|
6476
7243
|
}
|
|
6477
|
-
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");
|
|
6478
7245
|
const apiUrl = base.toString().replace(/\/+$/, "");
|
|
6479
7246
|
const probe = `${apiUrl}/api/meta/schema-version`;
|
|
6480
7247
|
let reachable = false;
|
|
@@ -6486,11 +7253,7 @@ async function linkDirect(target, rawArgs) {
|
|
|
6486
7253
|
} catch (err) {
|
|
6487
7254
|
detail = err instanceof Error ? err.message : String(err);
|
|
6488
7255
|
}
|
|
6489
|
-
if (!reachable) {
|
|
6490
|
-
console.log(chalk.yellow(`⚠ Could not reach ${probe}${detail ? ` (${detail})` : ""}.`));
|
|
6491
|
-
console.log(chalk.dim(" Linking anyway — the server may not be running yet."));
|
|
6492
|
-
console.log(chalk.dim(" It must be a Rebase backend of version 0.11 or newer."));
|
|
6493
|
-
}
|
|
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.");
|
|
6494
7257
|
writeLink({
|
|
6495
7258
|
url: apiUrl,
|
|
6496
7259
|
projectId: "",
|
|
@@ -6499,9 +7262,18 @@ async function linkDirect(target, rawArgs) {
|
|
|
6499
7262
|
projectName: base.host
|
|
6500
7263
|
});
|
|
6501
7264
|
success(`Linked to ${apiUrl}`);
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
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
|
+
});
|
|
6505
7277
|
}
|
|
6506
7278
|
async function linkCommand(rawArgs) {
|
|
6507
7279
|
const args = arg({
|
|
@@ -6522,14 +7294,15 @@ async function linkCommand(rawArgs) {
|
|
|
6522
7294
|
if (args["--project"]) {
|
|
6523
7295
|
const projectId = await resolveProjectRef(args["--project"], client);
|
|
6524
7296
|
project = await client.data.collection("projects").findById(projectId);
|
|
6525
|
-
if (!project) fail(`Project ${args["--project"]} not found
|
|
7297
|
+
if (!project) fail(`Project ${args["--project"]} not found.`, void 0, "project_not_found");
|
|
6526
7298
|
} else {
|
|
7299
|
+
requireInteractive("a project to link", "--project <slug>");
|
|
6527
7300
|
const org = getContextOrg(url);
|
|
6528
7301
|
const projects = (await client.data.collection("projects").find({
|
|
6529
7302
|
where: org ? { organization: ["==", org] } : void 0,
|
|
6530
7303
|
limit: 100
|
|
6531
7304
|
})).data;
|
|
6532
|
-
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");
|
|
6533
7306
|
const { picked } = await inquirer.prompt([{
|
|
6534
7307
|
type: "select",
|
|
6535
7308
|
name: "picked",
|
|
@@ -6541,39 +7314,63 @@ async function linkCommand(rawArgs) {
|
|
|
6541
7314
|
}]);
|
|
6542
7315
|
project = picked;
|
|
6543
7316
|
}
|
|
6544
|
-
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;
|
|
6545
7319
|
writeLink({
|
|
6546
7320
|
url,
|
|
6547
7321
|
projectId: String(project.id),
|
|
6548
7322
|
slug: project.subdomain,
|
|
6549
7323
|
projectName: project.name,
|
|
6550
|
-
orgId
|
|
7324
|
+
orgId
|
|
6551
7325
|
});
|
|
6552
7326
|
success(`Linked to ${chalk.bold(project.name ?? project.subdomain ?? "")}`);
|
|
6553
|
-
|
|
6554
|
-
|
|
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
|
+
});
|
|
6555
7340
|
} catch (e) {
|
|
6556
7341
|
reportError(e, "Failed to link project");
|
|
6557
7342
|
}
|
|
6558
7343
|
}
|
|
6559
7344
|
function unlinkCommand() {
|
|
6560
7345
|
if (!readLink()) {
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
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
|
+
});
|
|
6564
7355
|
return;
|
|
6565
7356
|
}
|
|
6566
7357
|
removeLink();
|
|
6567
7358
|
success("Unlinked from cloud project");
|
|
7359
|
+
emit(() => {}, {
|
|
7360
|
+
success: true,
|
|
7361
|
+
unlinked: true,
|
|
7362
|
+
linkPath: projectLinkPath()
|
|
7363
|
+
});
|
|
6568
7364
|
}
|
|
6569
7365
|
async function selectOrgCommand(rawArgs) {
|
|
6570
7366
|
const target = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
6571
7367
|
const { client, url } = await requireClient(rawArgs);
|
|
6572
7368
|
try {
|
|
6573
7369
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
6574
|
-
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");
|
|
6575
7371
|
let chosen = target ? orgs.find((o) => String(o.id) === target || o.slug === target) : void 0;
|
|
6576
7372
|
if (!chosen && !target) {
|
|
7373
|
+
requireInteractive("an organization", "rebase cloud use <org-id|slug>");
|
|
6577
7374
|
const { picked } = await inquirer.prompt([{
|
|
6578
7375
|
type: "select",
|
|
6579
7376
|
name: "picked",
|
|
@@ -6585,9 +7382,18 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6585
7382
|
}]);
|
|
6586
7383
|
chosen = picked;
|
|
6587
7384
|
}
|
|
6588
|
-
if (!chosen) fail(`Organization "${target}" not found
|
|
7385
|
+
if (!chosen) fail(`Organization "${target}" not found.`, void 0, "org_not_found");
|
|
6589
7386
|
setContextOrg(url, String(chosen.id));
|
|
6590
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
|
+
});
|
|
6591
7397
|
} catch (e) {
|
|
6592
7398
|
reportError(e, "Failed to set organization");
|
|
6593
7399
|
}
|
|
@@ -6596,7 +7402,12 @@ async function selectOrgCommand(rawArgs) {
|
|
|
6596
7402
|
function openCommand(rawArgs) {
|
|
6597
7403
|
const url = resolveCloudUrl(rawArgs);
|
|
6598
7404
|
const link = readLink();
|
|
6599
|
-
|
|
7405
|
+
const target = link ? `${url}/projects/${link.projectId}` : url;
|
|
7406
|
+
openUrl(target);
|
|
7407
|
+
emit(() => {}, {
|
|
7408
|
+
url: target,
|
|
7409
|
+
projectId: link?.projectId ?? null
|
|
7410
|
+
});
|
|
6600
7411
|
}
|
|
6601
7412
|
//#endregion
|
|
6602
7413
|
//#region src/commands/cloud/projects.ts
|
|
@@ -6612,21 +7423,34 @@ async function listProjects(rawArgs) {
|
|
|
6612
7423
|
orderBy: ["name", "asc"],
|
|
6613
7424
|
limit: 100
|
|
6614
7425
|
}).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
|
|
6615
|
-
console.log("");
|
|
6616
|
-
console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
|
|
6617
|
-
console.log("");
|
|
6618
|
-
if (projects.length === 0) {
|
|
6619
|
-
console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
|
|
6620
|
-
console.log("");
|
|
6621
|
-
return;
|
|
6622
|
-
}
|
|
6623
7426
|
const linkedId = readLink()?.projectId;
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
console.log(
|
|
6627
|
-
console.log(
|
|
6628
|
-
|
|
6629
|
-
|
|
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
|
+
});
|
|
6630
7454
|
} catch (e) {
|
|
6631
7455
|
reportError(e, "Failed to list projects");
|
|
6632
7456
|
}
|
|
@@ -6693,28 +7517,33 @@ function chooseRequestedTarget(requested, targets) {
|
|
|
6693
7517
|
}
|
|
6694
7518
|
async function resolveRequestedTarget(client, url, requested) {
|
|
6695
7519
|
const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
|
|
6696
|
-
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");
|
|
6697
7521
|
return chosen;
|
|
6698
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
|
+
};
|
|
6699
7536
|
async function createProject(rawArgs) {
|
|
6700
|
-
const args =
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
"
|
|
6705
|
-
|
|
6706
|
-
"--region": String,
|
|
6707
|
-
"--vm-size": String,
|
|
6708
|
-
"--org": String,
|
|
6709
|
-
"--link": Boolean,
|
|
6710
|
-
"-n": "--name"
|
|
6711
|
-
}, {
|
|
6712
|
-
argv: rawArgs.slice(4),
|
|
6713
|
-
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
|
|
6714
7543
|
});
|
|
6715
7544
|
const { client, url } = await requireClient(rawArgs);
|
|
6716
7545
|
const org = args["--org"] || getContextOrg(url);
|
|
6717
|
-
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");
|
|
6718
7547
|
const prompts = [];
|
|
6719
7548
|
if (!args["--name"]) prompts.push({
|
|
6720
7549
|
type: "input",
|
|
@@ -6736,14 +7565,14 @@ async function createProject(rawArgs) {
|
|
|
6736
7565
|
const defaults = providerDefaults(provider);
|
|
6737
7566
|
const region = (args["--region"] || target.region || defaults.region).trim();
|
|
6738
7567
|
const vmSize = (args["--vm-size"] || defaults.vmSize).trim();
|
|
6739
|
-
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");
|
|
6740
7569
|
try {
|
|
6741
7570
|
const check = await client.functions.invoke("check-subdomain", { subdomain });
|
|
6742
|
-
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");
|
|
6743
7572
|
} catch {}
|
|
6744
7573
|
try {
|
|
6745
7574
|
const user = await client.auth.getUser();
|
|
6746
|
-
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");
|
|
6747
7576
|
const created = await client.data.collection("projects").create({
|
|
6748
7577
|
name,
|
|
6749
7578
|
subdomain,
|
|
@@ -6756,87 +7585,147 @@ async function createProject(rawArgs) {
|
|
|
6756
7585
|
createdById: user.uid,
|
|
6757
7586
|
status: "provisioning"
|
|
6758
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
|
+
});
|
|
6759
7597
|
success(`Created project ${chalk.bold(name)}`);
|
|
6760
|
-
|
|
6761
|
-
[
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
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
|
+
});
|
|
6779
7622
|
} catch (e) {
|
|
6780
7623
|
reportError(e, "Failed to create project");
|
|
6781
7624
|
}
|
|
6782
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
|
+
}
|
|
6783
7651
|
async function projectInfo(rawArgs, projectRef) {
|
|
6784
7652
|
const { client, url } = await requireClient(rawArgs);
|
|
6785
7653
|
try {
|
|
6786
7654
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
6787
7655
|
const p = await client.data.collection("projects").findById(projectId);
|
|
6788
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
7656
|
+
if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
|
|
6789
7657
|
const [db, lastDeploy, baseDomain] = await Promise.all([
|
|
6790
7658
|
firstRow(client, "databases", projectId),
|
|
6791
7659
|
latestDeployment(client, projectId),
|
|
6792
7660
|
fetchTenantBaseDomain(client, url)
|
|
6793
7661
|
]);
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
[
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
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
|
+
});
|
|
6809
7700
|
} catch (e) {
|
|
6810
7701
|
reportError(e, "Failed to load project");
|
|
6811
7702
|
}
|
|
6812
7703
|
}
|
|
6813
7704
|
async function deleteProject(rawArgs, projectRef) {
|
|
6814
|
-
const args =
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
7705
|
+
const { flags: args } = parseCloudArgs({
|
|
7706
|
+
spec: {},
|
|
7707
|
+
rawArgs,
|
|
7708
|
+
commandWords: 3,
|
|
7709
|
+
command: "cloud projects delete",
|
|
7710
|
+
maxPositionals: 1
|
|
6820
7711
|
});
|
|
6821
7712
|
const { client } = await requireClient(rawArgs);
|
|
6822
7713
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
6823
7714
|
const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
|
|
6824
|
-
if (!p) fail(`Project ${projectRef} not found
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
default: false,
|
|
6830
|
-
message: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
|
|
6831
|
-
}]);
|
|
6832
|
-
if (!confirmed) {
|
|
6833
|
-
console.log(chalk.gray(" Aborted."));
|
|
6834
|
-
return;
|
|
6835
|
-
}
|
|
6836
|
-
}
|
|
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
|
+
});
|
|
6837
7720
|
try {
|
|
6838
7721
|
await client.data.collection("projects").delete(projectId);
|
|
6839
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
|
+
});
|
|
6840
7729
|
} catch (e) {
|
|
6841
7730
|
reportError(e, "Failed to delete project");
|
|
6842
7731
|
}
|
|
@@ -7610,7 +8499,7 @@ async function orgsCommand(subcommand, rawArgs) {
|
|
|
7610
8499
|
case "--help":
|
|
7611
8500
|
printOrgsHelp();
|
|
7612
8501
|
break;
|
|
7613
|
-
default: fail(`Unknown orgs command: ${subcommand}`);
|
|
8502
|
+
default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
|
|
7614
8503
|
}
|
|
7615
8504
|
}
|
|
7616
8505
|
async function listOrgs(rawArgs) {
|
|
@@ -7618,21 +8507,31 @@ async function listOrgs(rawArgs) {
|
|
|
7618
8507
|
try {
|
|
7619
8508
|
const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
|
|
7620
8509
|
const active = getContextOrg(url);
|
|
7621
|
-
|
|
7622
|
-
console.log(chalk.bold(" 🏢 Organizations"));
|
|
7623
|
-
console.log("");
|
|
7624
|
-
if (orgs.length === 0) {
|
|
7625
|
-
console.log(chalk.gray(" You are not a member of any organization."));
|
|
8510
|
+
emit(() => {
|
|
7626
8511
|
console.log("");
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
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
|
+
});
|
|
7636
8535
|
} catch (e) {
|
|
7637
8536
|
reportError(e, "Failed to list organizations");
|
|
7638
8537
|
}
|
|
@@ -7648,14 +8547,17 @@ async function createOrg(rawArgs) {
|
|
|
7648
8547
|
});
|
|
7649
8548
|
const { client, url } = await requireClient(rawArgs);
|
|
7650
8549
|
const prompts = [];
|
|
7651
|
-
if (!args["--name"])
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
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
|
+
}
|
|
7656
8558
|
const answers = prompts.length ? await inquirer.prompt(prompts) : {};
|
|
7657
8559
|
const name = (args["--name"] || answers.name || "").trim();
|
|
7658
|
-
if (!name) fail("Organization name is required.");
|
|
8560
|
+
if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
|
|
7659
8561
|
const slug = (args["--slug"] || slugify(name)).trim();
|
|
7660
8562
|
try {
|
|
7661
8563
|
const created = await client.data.collection("organizations").create({
|
|
@@ -7665,6 +8567,13 @@ async function createOrg(rawArgs) {
|
|
|
7665
8567
|
});
|
|
7666
8568
|
setContextOrg(url, String(created.id));
|
|
7667
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
|
+
});
|
|
7668
8577
|
} catch (e) {
|
|
7669
8578
|
reportError(e, "Failed to create organization");
|
|
7670
8579
|
}
|
|
@@ -7672,22 +8581,31 @@ async function createOrg(rawArgs) {
|
|
|
7672
8581
|
async function listMembers(rawArgs) {
|
|
7673
8582
|
const { client, url } = await requireClient(rawArgs);
|
|
7674
8583
|
const org = getContextOrg(url);
|
|
7675
|
-
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");
|
|
7676
8585
|
try {
|
|
7677
8586
|
const members = (await client.data.collection("organization-members").find({
|
|
7678
8587
|
where: { organization: ["==", org] },
|
|
7679
8588
|
limit: 200
|
|
7680
8589
|
})).data;
|
|
7681
|
-
|
|
7682
|
-
console.log(chalk.bold(` 👥 Members — org ${org}`));
|
|
7683
|
-
console.log("");
|
|
7684
|
-
if (members.length === 0) {
|
|
7685
|
-
console.log(chalk.gray(" No members found."));
|
|
8590
|
+
emit(() => {
|
|
7686
8591
|
console.log("");
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
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
|
+
});
|
|
7691
8609
|
} catch (e) {
|
|
7692
8610
|
reportError(e, "Failed to list members");
|
|
7693
8611
|
}
|
|
@@ -7696,7 +8614,12 @@ function slugify(s) {
|
|
|
7696
8614
|
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7697
8615
|
}
|
|
7698
8616
|
function printOrgsHelp() {
|
|
7699
|
-
|
|
8617
|
+
emitHelp("orgs", [
|
|
8618
|
+
"list",
|
|
8619
|
+
"create",
|
|
8620
|
+
"members"
|
|
8621
|
+
], () => {
|
|
8622
|
+
console.log(`
|
|
7700
8623
|
${chalk.bold("rebase cloud orgs")} — Manage organizations
|
|
7701
8624
|
|
|
7702
8625
|
${chalk.green.bold("Commands")}
|
|
@@ -7704,6 +8627,7 @@ ${chalk.green.bold("Commands")}
|
|
|
7704
8627
|
${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
|
|
7705
8628
|
${chalk.blue.bold("members")} List members of the active organization
|
|
7706
8629
|
`);
|
|
8630
|
+
});
|
|
7707
8631
|
}
|
|
7708
8632
|
//#endregion
|
|
7709
8633
|
//#region src/commands/cloud/databases.ts
|
|
@@ -7739,7 +8663,7 @@ async function dbCommand$1(subcommand, rawArgs) {
|
|
|
7739
8663
|
case "--help":
|
|
7740
8664
|
printDbHelp();
|
|
7741
8665
|
break;
|
|
7742
|
-
default: fail(`Unknown db command: ${subcommand}`);
|
|
8666
|
+
default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
|
|
7743
8667
|
}
|
|
7744
8668
|
}
|
|
7745
8669
|
async function listDatabases(rawArgs) {
|
|
@@ -7751,19 +8675,30 @@ async function listDatabases(rawArgs) {
|
|
|
7751
8675
|
where: { project: ["==", projectId] },
|
|
7752
8676
|
limit: 50
|
|
7753
8677
|
})).data;
|
|
7754
|
-
|
|
7755
|
-
console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
|
|
7756
|
-
console.log("");
|
|
7757
|
-
if (dbs.length === 0) {
|
|
7758
|
-
console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
|
|
8678
|
+
emit(() => {
|
|
7759
8679
|
console.log("");
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
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
|
+
});
|
|
7767
8702
|
} catch (e) {
|
|
7768
8703
|
reportError(e, "Failed to list databases");
|
|
7769
8704
|
}
|
|
@@ -7783,6 +8718,7 @@ async function createDatabase(rawArgs) {
|
|
|
7783
8718
|
const projectRef = displayProjectRef(rawArgs);
|
|
7784
8719
|
let type = args["--type"];
|
|
7785
8720
|
if (!type) {
|
|
8721
|
+
requireInteractive("a database type", "--type <managed|byodb>");
|
|
7786
8722
|
const { picked } = await inquirer.prompt([{
|
|
7787
8723
|
type: "select",
|
|
7788
8724
|
name: "picked",
|
|
@@ -7799,13 +8735,14 @@ async function createDatabase(rawArgs) {
|
|
|
7799
8735
|
}
|
|
7800
8736
|
let connectionString = args["--connection-string"];
|
|
7801
8737
|
if (type === "byodb" && !connectionString) {
|
|
8738
|
+
requireInteractive("a connection string", "--connection-string <url>");
|
|
7802
8739
|
const { cs } = await inquirer.prompt([{
|
|
7803
8740
|
type: "input",
|
|
7804
8741
|
name: "cs",
|
|
7805
8742
|
message: "PostgreSQL connection string:"
|
|
7806
8743
|
}]);
|
|
7807
8744
|
connectionString = cs?.trim();
|
|
7808
|
-
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");
|
|
7809
8746
|
}
|
|
7810
8747
|
try {
|
|
7811
8748
|
const created = await client.data.collection("databases").create({
|
|
@@ -7815,11 +8752,19 @@ async function createDatabase(rawArgs) {
|
|
|
7815
8752
|
connectionStatus: "untested"
|
|
7816
8753
|
});
|
|
7817
8754
|
success(`Attached ${type} database to project ${projectRef}`);
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
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
|
+
});
|
|
7823
8768
|
} catch (e) {
|
|
7824
8769
|
reportError(e, "Failed to attach database");
|
|
7825
8770
|
}
|
|
@@ -7827,15 +8772,19 @@ async function createDatabase(rawArgs) {
|
|
|
7827
8772
|
async function testDatabase(rawArgs) {
|
|
7828
8773
|
const { client } = await requireClient(rawArgs);
|
|
7829
8774
|
const projectId = await requireProject(rawArgs, client);
|
|
7830
|
-
displayProjectRef(rawArgs);
|
|
7831
|
-
|
|
7832
|
-
|
|
8775
|
+
const projectRef = displayProjectRef(rawArgs);
|
|
8776
|
+
noteBlank();
|
|
8777
|
+
note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
|
|
7833
8778
|
try {
|
|
7834
8779
|
const res = await client.functions.invoke("db-test", { projectId });
|
|
7835
|
-
console.
|
|
7836
|
-
if (res.
|
|
7837
|
-
|
|
7838
|
-
|
|
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
|
+
});
|
|
7839
8788
|
} catch (e) {
|
|
7840
8789
|
reportError(e, "Failed to test database");
|
|
7841
8790
|
}
|
|
@@ -7911,18 +8860,39 @@ async function dbInfo(rawArgs) {
|
|
|
7911
8860
|
reportError(e, "Failed to load database info");
|
|
7912
8861
|
}
|
|
7913
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
|
+
}
|
|
7914
8891
|
async function backupCommand(rawArgs) {
|
|
7915
|
-
const action
|
|
8892
|
+
const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
|
|
7916
8893
|
const { client } = await requireClient(rawArgs);
|
|
7917
8894
|
const projectId = await requireProject(rawArgs, client);
|
|
7918
8895
|
const projectRef = displayProjectRef(rawArgs);
|
|
7919
|
-
const args = arg({
|
|
7920
|
-
"--yes": Boolean,
|
|
7921
|
-
"-y": "--yes"
|
|
7922
|
-
}, {
|
|
7923
|
-
argv: rawArgs.slice(2),
|
|
7924
|
-
permissive: true
|
|
7925
|
-
});
|
|
7926
8896
|
try {
|
|
7927
8897
|
if (action === "create") {
|
|
7928
8898
|
const res = await client.functions.invoke("backup", {
|
|
@@ -7937,7 +8907,7 @@ async function backupCommand(rawArgs) {
|
|
|
7937
8907
|
return;
|
|
7938
8908
|
}
|
|
7939
8909
|
if (action === "restore") {
|
|
7940
|
-
const filename =
|
|
8910
|
+
const filename = backupFile;
|
|
7941
8911
|
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
7942
8912
|
await confirmDestructive({
|
|
7943
8913
|
yes: Boolean(args["--yes"]),
|
|
@@ -7975,7 +8945,7 @@ async function backupCommand(rawArgs) {
|
|
|
7975
8945
|
return;
|
|
7976
8946
|
}
|
|
7977
8947
|
if (action === "download") {
|
|
7978
|
-
const filename =
|
|
8948
|
+
const filename = backupFile;
|
|
7979
8949
|
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
7980
8950
|
const res = await client.functions.invoke("backup", void 0, {
|
|
7981
8951
|
method: "GET",
|
|
@@ -8033,17 +9003,17 @@ async function backupCommand(rawArgs) {
|
|
|
8033
9003
|
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
8034
9004
|
*/
|
|
8035
9005
|
async function pitrCommand(rawArgs) {
|
|
8036
|
-
const args =
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
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
|
|
8045
9015
|
});
|
|
8046
|
-
const action =
|
|
9016
|
+
const action = positionals[0] || "status";
|
|
8047
9017
|
const { client } = await requireClient(rawArgs);
|
|
8048
9018
|
const projectId = await requireProject(rawArgs, client);
|
|
8049
9019
|
const projectRef = displayProjectRef(rawArgs);
|
|
@@ -8115,7 +9085,15 @@ async function pitrCommand(rawArgs) {
|
|
|
8115
9085
|
}
|
|
8116
9086
|
}
|
|
8117
9087
|
function printDbHelp() {
|
|
8118
|
-
|
|
9088
|
+
emitHelp("db", [
|
|
9089
|
+
"list",
|
|
9090
|
+
"create",
|
|
9091
|
+
"info",
|
|
9092
|
+
"test",
|
|
9093
|
+
"backup",
|
|
9094
|
+
"pitr"
|
|
9095
|
+
], () => {
|
|
9096
|
+
console.log(`
|
|
8119
9097
|
${chalk.bold("rebase cloud db")} — Database & backups
|
|
8120
9098
|
|
|
8121
9099
|
${chalk.green.bold("Commands")}
|
|
@@ -8140,6 +9118,7 @@ ${chalk.green.bold("Options")}
|
|
|
8140
9118
|
${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
|
|
8141
9119
|
${chalk.blue("--json")} Machine-readable output
|
|
8142
9120
|
`);
|
|
9121
|
+
});
|
|
8143
9122
|
}
|
|
8144
9123
|
//#endregion
|
|
8145
9124
|
//#region src/commands/cloud/env.ts
|
|
@@ -8176,7 +9155,7 @@ async function envCommand(action, rawArgs) {
|
|
|
8176
9155
|
case "unset":
|
|
8177
9156
|
case "delete":
|
|
8178
9157
|
case "rm":
|
|
8179
|
-
await unsetEnv(rawArgs);
|
|
9158
|
+
await unsetEnv(rawArgs, action);
|
|
8180
9159
|
break;
|
|
8181
9160
|
case "reveal":
|
|
8182
9161
|
await revealEnv(rawArgs);
|
|
@@ -8276,20 +9255,43 @@ var BUILD_TIME_ENV_PREFIXES = [
|
|
|
8276
9255
|
function buildTimeEnvPrefix(key) {
|
|
8277
9256
|
return BUILD_TIME_ENV_PREFIXES.find((prefix) => key.toUpperCase().startsWith(prefix));
|
|
8278
9257
|
}
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
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
|
|
8288
9284
|
});
|
|
9285
|
+
return {
|
|
9286
|
+
flags,
|
|
9287
|
+
assignment: parseEnvAssignment(positionals)
|
|
9288
|
+
};
|
|
9289
|
+
}
|
|
9290
|
+
async function setEnv(rawArgs) {
|
|
9291
|
+
const { flags: args, assignment: parsed } = resolveEnvSetArgs(rawArgs);
|
|
8289
9292
|
const { client } = await requireClient(rawArgs);
|
|
8290
9293
|
const projectId = await requireProject(rawArgs, client);
|
|
8291
9294
|
displayProjectRef(rawArgs);
|
|
8292
|
-
const parsed = parseEnvAssignment(cloudPositionals(rawArgs).slice(2));
|
|
8293
9295
|
if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
|
|
8294
9296
|
const buildTimePrefix = buildTimeEnvPrefix(parsed.key);
|
|
8295
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");
|
|
@@ -8315,12 +9317,32 @@ async function setEnv(rawArgs) {
|
|
|
8315
9317
|
reportError(e, "Failed to set environment variable");
|
|
8316
9318
|
}
|
|
8317
9319
|
}
|
|
8318
|
-
|
|
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");
|
|
8319
9344
|
const { client } = await requireClient(rawArgs);
|
|
8320
9345
|
const projectId = await requireProject(rawArgs, client);
|
|
8321
|
-
displayProjectRef(rawArgs);
|
|
8322
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8323
|
-
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8324
9346
|
try {
|
|
8325
9347
|
emit(() => {
|
|
8326
9348
|
success(`Removed ${chalk.bold(key)}`);
|
|
@@ -8339,11 +9361,11 @@ async function unsetEnv(rawArgs) {
|
|
|
8339
9361
|
}
|
|
8340
9362
|
}
|
|
8341
9363
|
async function revealEnv(rawArgs) {
|
|
9364
|
+
const key = resolveEnvKeyArg(rawArgs, "reveal");
|
|
9365
|
+
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8342
9366
|
const { client } = await requireClient(rawArgs);
|
|
8343
9367
|
const projectId = await requireProject(rawArgs, client);
|
|
8344
9368
|
const projectRef = displayProjectRef(rawArgs);
|
|
8345
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8346
|
-
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8347
9369
|
let list;
|
|
8348
9370
|
try {
|
|
8349
9371
|
list = await fetchEnvVars(client, projectId);
|
|
@@ -8371,20 +9393,20 @@ async function revealEnv(rawArgs) {
|
|
|
8371
9393
|
}
|
|
8372
9394
|
}
|
|
8373
9395
|
async function pullEnv(rawArgs) {
|
|
8374
|
-
const args =
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
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
|
|
8383
9405
|
});
|
|
8384
9406
|
const { client } = await requireClient(rawArgs);
|
|
8385
9407
|
const projectId = await requireProject(rawArgs, client);
|
|
8386
9408
|
displayProjectRef(rawArgs);
|
|
8387
|
-
const outPath = path.resolve(args["--
|
|
9409
|
+
const outPath = path.resolve(args["--output"] || ".env");
|
|
8388
9410
|
try {
|
|
8389
9411
|
const list = await fetchEnvVars(client, projectId);
|
|
8390
9412
|
if (fs.existsSync(outPath)) await confirmDestructive({
|
|
@@ -8433,11 +9455,14 @@ async function pullEnv(rawArgs) {
|
|
|
8433
9455
|
}
|
|
8434
9456
|
}
|
|
8435
9457
|
function printEnvHelp() {
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
9458
|
+
emitHelp("env", [
|
|
9459
|
+
"list",
|
|
9460
|
+
"set",
|
|
9461
|
+
"unset",
|
|
9462
|
+
"reveal",
|
|
9463
|
+
"pull"
|
|
9464
|
+
], () => {
|
|
9465
|
+
console.log(`
|
|
8441
9466
|
${chalk.bold("rebase cloud env")} — Environment variables
|
|
8442
9467
|
|
|
8443
9468
|
${chalk.green.bold("Commands")}
|
|
@@ -8457,18 +9482,7 @@ ${chalk.gray("Values are encrypted at rest (AES-256-GCM) and only decrypted at d
|
|
|
8457
9482
|
${chalk.gray("VITE_* / NEXT_PUBLIC_* / PUBLIC_* / REACT_APP_* are read by your bundler at BUILD time;")}
|
|
8458
9483
|
${chalk.gray("these are applied at rollout, after the image is built, so they never reach the bundle.")}
|
|
8459
9484
|
`);
|
|
8460
|
-
}
|
|
8461
|
-
function printEnvHelpJson() {
|
|
8462
|
-
process.stdout.write(JSON.stringify({
|
|
8463
|
-
command: "env",
|
|
8464
|
-
actions: [
|
|
8465
|
-
"list",
|
|
8466
|
-
"set",
|
|
8467
|
-
"unset",
|
|
8468
|
-
"reveal",
|
|
8469
|
-
"pull"
|
|
8470
|
-
]
|
|
8471
|
-
}) + "\n");
|
|
9485
|
+
});
|
|
8472
9486
|
}
|
|
8473
9487
|
//#endregion
|
|
8474
9488
|
//#region src/commands/cloud/domains.ts
|
|
@@ -8556,12 +9570,28 @@ async function listDomains(rawArgs) {
|
|
|
8556
9570
|
reportError(e, "Failed to load custom domain");
|
|
8557
9571
|
}
|
|
8558
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
|
+
}
|
|
8559
9590
|
async function addDomain(rawArgs) {
|
|
9591
|
+
const domain = resolveDomainArg(rawArgs);
|
|
9592
|
+
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8560
9593
|
const { client } = await requireClient(rawArgs);
|
|
8561
9594
|
const projectId = await requireProject(rawArgs, client);
|
|
8562
|
-
displayProjectRef(rawArgs);
|
|
8563
|
-
const domain = cloudPositionals(rawArgs).slice(2)[0];
|
|
8564
|
-
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8565
9595
|
try {
|
|
8566
9596
|
await client.data.collection("projects").update(projectId, { customDomain: domain });
|
|
8567
9597
|
const setup = await fetchDomainSetup(client, projectId);
|
|
@@ -8616,14 +9646,12 @@ async function verifyDomains(rawArgs) {
|
|
|
8616
9646
|
}
|
|
8617
9647
|
}
|
|
8618
9648
|
async function removeDomain(rawArgs) {
|
|
8619
|
-
const args =
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
argv: rawArgs.slice(2),
|
|
8626
|
-
permissive: true
|
|
9649
|
+
const { flags: args } = parseCloudArgs({
|
|
9650
|
+
spec: {},
|
|
9651
|
+
rawArgs,
|
|
9652
|
+
commandWords: 3,
|
|
9653
|
+
command: "cloud domains remove",
|
|
9654
|
+
maxPositionals: 0
|
|
8627
9655
|
});
|
|
8628
9656
|
const { client } = await requireClient(rawArgs);
|
|
8629
9657
|
const projectId = await requireProject(rawArgs, client);
|
|
@@ -8643,7 +9671,13 @@ async function removeDomain(rawArgs) {
|
|
|
8643
9671
|
}
|
|
8644
9672
|
}
|
|
8645
9673
|
function printDomainsHelp() {
|
|
8646
|
-
|
|
9674
|
+
emitHelp("domains", [
|
|
9675
|
+
"list",
|
|
9676
|
+
"add",
|
|
9677
|
+
"verify",
|
|
9678
|
+
"remove"
|
|
9679
|
+
], () => {
|
|
9680
|
+
console.log(`
|
|
8647
9681
|
${chalk.bold("rebase cloud domains")} — Custom domain
|
|
8648
9682
|
|
|
8649
9683
|
${chalk.green.bold("Commands")}
|
|
@@ -8656,6 +9690,7 @@ ${chalk.green.bold("Options")}
|
|
|
8656
9690
|
${chalk.blue("--json")} Machine-readable output
|
|
8657
9691
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8658
9692
|
`);
|
|
9693
|
+
});
|
|
8659
9694
|
}
|
|
8660
9695
|
//#endregion
|
|
8661
9696
|
//#region src/commands/cloud/extensions.ts
|
|
@@ -8733,22 +9768,33 @@ async function listExtensions(rawArgs) {
|
|
|
8733
9768
|
} catch (e) {
|
|
8734
9769
|
reportError(e, "Failed to list extensions");
|
|
8735
9770
|
}
|
|
8736
|
-
}
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
9771
|
+
}
|
|
9772
|
+
/**
|
|
9773
|
+
* The extension `enable`/`disable` names, plus the flags that gate it.
|
|
9774
|
+
*
|
|
9775
|
+
* Under the old operand filter `rebase cloud extensions enable -p acme` read
|
|
9776
|
+
* `--project`'s value as the extension name and asked the server to install one
|
|
9777
|
+
* called "acme"; `extensions disable -p acme vector` dropped "acme" rather than
|
|
9778
|
+
* vector. Strict parsing consumes the flag with its value.
|
|
9779
|
+
*/
|
|
9780
|
+
function resolveExtensionArgs(rawArgs, action) {
|
|
9781
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9782
|
+
spec: {},
|
|
9783
|
+
rawArgs,
|
|
9784
|
+
commandWords: 3,
|
|
9785
|
+
command: `cloud extensions ${action}`,
|
|
9786
|
+
maxPositionals: 1
|
|
8746
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");
|
|
8747
9796
|
const { client } = await requireClient(rawArgs);
|
|
8748
9797
|
const projectId = await requireProject(rawArgs, client);
|
|
8749
|
-
displayProjectRef(rawArgs);
|
|
8750
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8751
|
-
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8752
9798
|
const name = resolveExtensionAlias(raw);
|
|
8753
9799
|
try {
|
|
8754
9800
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8785,20 +9831,10 @@ async function enableExtension(rawArgs) {
|
|
|
8785
9831
|
}
|
|
8786
9832
|
}
|
|
8787
9833
|
async function disableExtension(rawArgs) {
|
|
8788
|
-
const args =
|
|
8789
|
-
|
|
8790
|
-
"-y": "--yes",
|
|
8791
|
-
"--project": String,
|
|
8792
|
-
"-p": "--project"
|
|
8793
|
-
}, {
|
|
8794
|
-
argv: rawArgs.slice(2),
|
|
8795
|
-
permissive: true
|
|
8796
|
-
});
|
|
9834
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "disable");
|
|
9835
|
+
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8797
9836
|
const { client } = await requireClient(rawArgs);
|
|
8798
9837
|
const projectId = await requireProject(rawArgs, client);
|
|
8799
|
-
displayProjectRef(rawArgs);
|
|
8800
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8801
|
-
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8802
9838
|
const name = resolveExtensionAlias(raw);
|
|
8803
9839
|
try {
|
|
8804
9840
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8824,7 +9860,12 @@ async function disableExtension(rawArgs) {
|
|
|
8824
9860
|
}
|
|
8825
9861
|
}
|
|
8826
9862
|
function printExtensionsHelp() {
|
|
8827
|
-
|
|
9863
|
+
emitHelp("extensions", [
|
|
9864
|
+
"list",
|
|
9865
|
+
"enable",
|
|
9866
|
+
"disable"
|
|
9867
|
+
], () => {
|
|
9868
|
+
console.log(`
|
|
8828
9869
|
${chalk.bold("rebase cloud extensions")} — Postgres extensions
|
|
8829
9870
|
|
|
8830
9871
|
${chalk.green.bold("Commands")}
|
|
@@ -8837,6 +9878,7 @@ ${chalk.green.bold("Options")}
|
|
|
8837
9878
|
${chalk.blue("--json")} Machine-readable output
|
|
8838
9879
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8839
9880
|
`);
|
|
9881
|
+
});
|
|
8840
9882
|
}
|
|
8841
9883
|
//#endregion
|
|
8842
9884
|
//#region src/commands/cloud/settings.ts
|
|
@@ -8948,7 +9990,8 @@ async function setSettings(rawArgs) {
|
|
|
8948
9990
|
}
|
|
8949
9991
|
}
|
|
8950
9992
|
function printSettingsHelp() {
|
|
8951
|
-
|
|
9993
|
+
emitHelp("settings", ["show", "set"], () => {
|
|
9994
|
+
console.log(`
|
|
8952
9995
|
${chalk.bold("rebase cloud settings")} — Project configuration
|
|
8953
9996
|
|
|
8954
9997
|
${chalk.green.bold("Commands")}
|
|
@@ -8965,6 +10008,7 @@ ${chalk.green.bold("Options")}
|
|
|
8965
10008
|
${chalk.blue("--json")} Machine-readable output
|
|
8966
10009
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
8967
10010
|
`);
|
|
10011
|
+
});
|
|
8968
10012
|
}
|
|
8969
10013
|
//#endregion
|
|
8970
10014
|
//#region src/commands/cloud/deployments.ts
|
|
@@ -9075,14 +10119,15 @@ function parseDeploymentsLimit(raw) {
|
|
|
9075
10119
|
return raw;
|
|
9076
10120
|
}
|
|
9077
10121
|
async function deploymentsListCommand(rawArgs) {
|
|
9078
|
-
const args =
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
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
|
|
9086
10131
|
});
|
|
9087
10132
|
const limit = args["--all"] ? MAX_DEPLOYMENTS_LIMIT : parseDeploymentsLimit(args["--limit"]);
|
|
9088
10133
|
const { client } = await requireClient(rawArgs);
|
|
@@ -9123,20 +10168,34 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
9123
10168
|
reportError(e, "Failed to list deployments");
|
|
9124
10169
|
}
|
|
9125
10170
|
}
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
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
|
|
9135
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");
|
|
9136
10196
|
const { client } = await requireClient(rawArgs);
|
|
9137
10197
|
const projectId = await requireProject(rawArgs, client);
|
|
9138
10198
|
const projectRef = displayProjectRef(rawArgs);
|
|
9139
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
9140
10199
|
let rows;
|
|
9141
10200
|
try {
|
|
9142
10201
|
rows = await fetchDeployments(client, projectId);
|
|
@@ -9184,19 +10243,10 @@ async function rollbackCommand(rawArgs) {
|
|
|
9184
10243
|
}
|
|
9185
10244
|
}
|
|
9186
10245
|
async function cancelCommand(rawArgs) {
|
|
9187
|
-
const args =
|
|
9188
|
-
"--yes": Boolean,
|
|
9189
|
-
"-y": "--yes",
|
|
9190
|
-
"--project": String,
|
|
9191
|
-
"-p": "--project"
|
|
9192
|
-
}, {
|
|
9193
|
-
argv: rawArgs.slice(2),
|
|
9194
|
-
permissive: true
|
|
9195
|
-
});
|
|
10246
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud cancel");
|
|
9196
10247
|
const { client } = await requireClient(rawArgs);
|
|
9197
10248
|
const projectId = await requireProject(rawArgs, client);
|
|
9198
10249
|
const projectRef = displayProjectRef(rawArgs);
|
|
9199
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
9200
10250
|
await confirmDestructive({
|
|
9201
10251
|
yes: Boolean(args["--yes"]),
|
|
9202
10252
|
prompt: `Cancel the in-flight build for project ${projectRef}?`
|
|
@@ -9978,7 +11028,16 @@ async function debugCommand(action, rawArgs) {
|
|
|
9978
11028
|
}
|
|
9979
11029
|
}
|
|
9980
11030
|
function printDebugHelp() {
|
|
9981
|
-
|
|
11031
|
+
emitHelp("debug", [
|
|
11032
|
+
"health",
|
|
11033
|
+
"logs",
|
|
11034
|
+
"errors",
|
|
11035
|
+
"requests",
|
|
11036
|
+
"boot",
|
|
11037
|
+
"pod",
|
|
11038
|
+
"db"
|
|
11039
|
+
], () => {
|
|
11040
|
+
console.log(`
|
|
9982
11041
|
${chalk.bold("rebase cloud debug")} — Find out why a deployed project is misbehaving
|
|
9983
11042
|
|
|
9984
11043
|
${chalk.green.bold("Usage")}
|
|
@@ -10009,6 +11068,7 @@ ${chalk.green.bold("Options")}
|
|
|
10009
11068
|
${chalk.gray("Everything here is read-only. `health` exits non-zero when a check fails,")}
|
|
10010
11069
|
${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase cloud restart`.")}
|
|
10011
11070
|
`);
|
|
11071
|
+
});
|
|
10012
11072
|
}
|
|
10013
11073
|
//#endregion
|
|
10014
11074
|
//#region src/commands/cloud/resources.ts
|
|
@@ -10148,37 +11208,73 @@ async function metricsCommand(rawArgs) {
|
|
|
10148
11208
|
method: "GET",
|
|
10149
11209
|
path: projectId
|
|
10150
11210
|
});
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
[
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
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
|
+
});
|
|
10161
11230
|
} catch (e) {
|
|
10162
11231
|
reportError(e, "Failed to fetch metrics");
|
|
10163
11232
|
}
|
|
10164
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
|
+
}
|
|
10165
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");
|
|
10166
11270
|
const { client } = await requireClient(rawArgs);
|
|
10167
11271
|
const projectId = await requireProject(rawArgs, client);
|
|
10168
11272
|
try {
|
|
10169
11273
|
if (subcommand === "create") {
|
|
10170
|
-
const args =
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
"--events": String
|
|
10175
|
-
}, {
|
|
10176
|
-
argv: rawArgs.slice(4),
|
|
10177
|
-
permissive: true
|
|
10178
|
-
});
|
|
10179
|
-
const name = args["--name"] || fail("--name is required.");
|
|
10180
|
-
const table = args["--table"] || fail("--table is required.");
|
|
10181
|
-
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");
|
|
10182
11278
|
const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
|
|
10183
11279
|
const created = await client.data.collection("webhooks").create({
|
|
10184
11280
|
project: projectId,
|
|
@@ -10189,33 +11285,58 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
10189
11285
|
enabled: true
|
|
10190
11286
|
});
|
|
10191
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
|
+
});
|
|
10192
11298
|
return;
|
|
10193
11299
|
}
|
|
10194
11300
|
if (subcommand === "delete") {
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
11301
|
+
await client.data.collection("webhooks").delete(deleteId);
|
|
11302
|
+
success(`Deleted webhook ${deleteId}`);
|
|
11303
|
+
emit(() => {}, {
|
|
11304
|
+
success: true,
|
|
11305
|
+
id: deleteId,
|
|
11306
|
+
projectId
|
|
11307
|
+
});
|
|
10199
11308
|
return;
|
|
10200
11309
|
}
|
|
10201
11310
|
const hooks = (await client.data.collection("webhooks").find({
|
|
10202
11311
|
where: { project: ["==", projectId] },
|
|
10203
11312
|
limit: 100
|
|
10204
11313
|
})).data;
|
|
10205
|
-
|
|
10206
|
-
console.log(chalk.bold(` 🔗 Webhooks — project ${displayProjectRef(rawArgs)}`));
|
|
10207
|
-
console.log("");
|
|
10208
|
-
if (hooks.length === 0) {
|
|
10209
|
-
console.log(chalk.gray(" No webhooks. Add one with `rebase cloud webhooks create`."));
|
|
11314
|
+
emit(() => {
|
|
10210
11315
|
console.log("");
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
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
|
+
});
|
|
10219
11340
|
} catch (e) {
|
|
10220
11341
|
reportError(e, "Webhook operation failed");
|
|
10221
11342
|
}
|
|
@@ -10231,78 +11352,116 @@ async function storageCommand(action, rawArgs) {
|
|
|
10231
11352
|
where: { project: ["==", projectId] },
|
|
10232
11353
|
limit: 50
|
|
10233
11354
|
})).data;
|
|
10234
|
-
|
|
10235
|
-
console.log(chalk.bold(` 🪣 Storage — project ${displayProjectRef(rawArgs)}`));
|
|
10236
|
-
console.log("");
|
|
10237
|
-
if (stores.length === 0) {
|
|
10238
|
-
console.log(chalk.gray(" No storage buckets attached."));
|
|
11355
|
+
emit(() => {
|
|
10239
11356
|
console.log("");
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
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
|
+
});
|
|
10247
11379
|
} catch (e) {
|
|
10248
11380
|
reportError(e, "Failed to list storage");
|
|
10249
11381
|
}
|
|
10250
11382
|
}
|
|
10251
11383
|
function printStorageHelp() {
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
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
|
+
});
|
|
10271
11409
|
}
|
|
10272
11410
|
async function storageCreateCommand(rawArgs) {
|
|
11411
|
+
parseCloudArgs({
|
|
11412
|
+
spec: {},
|
|
11413
|
+
rawArgs,
|
|
11414
|
+
commandWords: 3,
|
|
11415
|
+
command: "cloud storage create",
|
|
11416
|
+
maxPositionals: 0
|
|
11417
|
+
});
|
|
10273
11418
|
const { client } = await requireClient(rawArgs);
|
|
10274
11419
|
const projectId = await requireProject(rawArgs, client);
|
|
10275
11420
|
try {
|
|
10276
|
-
|
|
10277
|
-
|
|
11421
|
+
noteBlank();
|
|
11422
|
+
note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
|
|
10278
11423
|
const res = await client.functions.invoke(`storage-provision/${encodeURIComponent(projectId)}`, void 0, { method: "POST" });
|
|
10279
11424
|
const info = res.data ?? res.data;
|
|
10280
11425
|
success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
|
|
10281
|
-
|
|
10282
|
-
[
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
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
|
+
});
|
|
10291
11447
|
} catch (e) {
|
|
10292
11448
|
reportError(e, "Failed to provision managed storage");
|
|
10293
11449
|
}
|
|
10294
11450
|
}
|
|
10295
11451
|
async function storageAttachCommand(rawArgs) {
|
|
10296
|
-
const parsed =
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
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
|
|
10306
11465
|
});
|
|
10307
11466
|
const bucket = parsed["--bucket"];
|
|
10308
11467
|
const accessKeyId = parsed["--access-key-id"];
|
|
@@ -10312,7 +11471,7 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10312
11471
|
!accessKeyId && "--access-key-id",
|
|
10313
11472
|
!secretAccessKey && "--secret-access-key"
|
|
10314
11473
|
].filter(Boolean);
|
|
10315
|
-
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");
|
|
10316
11475
|
const { client } = await requireClient(rawArgs);
|
|
10317
11476
|
const projectId = await requireProject(rawArgs, client);
|
|
10318
11477
|
try {
|
|
@@ -10335,17 +11494,29 @@ async function storageAttachCommand(rawArgs) {
|
|
|
10335
11494
|
row.region = parsed["--region"];
|
|
10336
11495
|
}
|
|
10337
11496
|
if (parsed["--force-path-style"]) row.s3ForcePathStyle = true;
|
|
11497
|
+
const replaced = Boolean(existing?.id);
|
|
10338
11498
|
if (existing?.id) await client.data.collection("storages").update(String(existing.id), row);
|
|
10339
11499
|
else await client.data.collection("storages").create(row);
|
|
10340
11500
|
success(`Storage attached to ${displayProjectRef(rawArgs)}.`);
|
|
10341
|
-
|
|
10342
|
-
[
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
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
|
+
});
|
|
10349
11520
|
} catch (e) {
|
|
10350
11521
|
reportError(e, "Failed to attach storage");
|
|
10351
11522
|
}
|
|
@@ -10354,40 +11525,55 @@ async function clustersCommand(rawArgs) {
|
|
|
10354
11525
|
const { client } = await requireClient(rawArgs);
|
|
10355
11526
|
try {
|
|
10356
11527
|
const clusters = (await client.data.collection("clusters").find({ limit: 100 })).data;
|
|
10357
|
-
|
|
10358
|
-
console.log(chalk.bold(" ☸ Clusters"));
|
|
10359
|
-
console.log("");
|
|
10360
|
-
if (clusters.length === 0) {
|
|
10361
|
-
console.log(chalk.gray(" No clusters registered."));
|
|
11528
|
+
emit(() => {
|
|
10362
11529
|
console.log("");
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
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
|
+
})) });
|
|
10370
11549
|
} catch (e) {
|
|
10371
11550
|
reportError(e, "Failed to list clusters");
|
|
10372
11551
|
}
|
|
10373
11552
|
}
|
|
10374
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];
|
|
10375
11561
|
const { client, url } = await requireClient(rawArgs);
|
|
10376
11562
|
const org = getContextOrg(url);
|
|
10377
|
-
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
10378
11563
|
if (action === "setup") {
|
|
10379
|
-
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");
|
|
10380
11565
|
try {
|
|
10381
11566
|
const res = await client.functions.invoke("stripe-billing", { organizationId: org }, { path: "setup-session" });
|
|
10382
|
-
if (!res.url) fail("Could not start billing setup.");
|
|
11567
|
+
if (!res.url) fail("Could not start billing setup.", void 0, "billing_setup_failed");
|
|
10383
11568
|
openUrl(res.url, "Add a payment method in your browser:");
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
}
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
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
|
+
});
|
|
10391
11577
|
} catch (e) {
|
|
10392
11578
|
reportError(e, "Failed to start billing setup");
|
|
10393
11579
|
}
|
|
@@ -10397,24 +11583,36 @@ async function billingCommand(rawArgs) {
|
|
|
10397
11583
|
const projectId = await requireProject(rawArgs, client);
|
|
10398
11584
|
try {
|
|
10399
11585
|
const res = await client.functions.invoke("stripe-billing", { projectId }, { path: "session" });
|
|
10400
|
-
if (!res.url) fail("Billing session could not be created.");
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
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
|
+
});
|
|
10405
11596
|
} catch (e) {
|
|
10406
11597
|
reportError(e, "Failed to start checkout");
|
|
10407
11598
|
}
|
|
10408
11599
|
return;
|
|
10409
11600
|
}
|
|
10410
|
-
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");
|
|
10411
11602
|
try {
|
|
10412
11603
|
const orgRow = await client.data.collection("organizations").findById(org);
|
|
10413
11604
|
const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
|
|
10414
11605
|
if (!billingId) {
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
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
|
+
});
|
|
10418
11616
|
return;
|
|
10419
11617
|
}
|
|
10420
11618
|
const acct = await client.data.collection("billing-accounts").findById(billingId);
|
|
@@ -10447,17 +11645,34 @@ async function billingCommand(rawArgs) {
|
|
|
10447
11645
|
} catch {}
|
|
10448
11646
|
}
|
|
10449
11647
|
} catch {}
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
[
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
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
|
+
});
|
|
10461
11676
|
} catch (e) {
|
|
10462
11677
|
reportError(e, "Failed to load billing");
|
|
10463
11678
|
}
|
|
@@ -10503,15 +11718,43 @@ function positionals(rawArgs) {
|
|
|
10503
11718
|
while (i < rest.length && rest[i].startsWith("-")) i++;
|
|
10504
11719
|
return rest.slice(i);
|
|
10505
11720
|
}
|
|
11721
|
+
/**
|
|
11722
|
+
* The help page for each group, keyed by every alias the dispatch below accepts.
|
|
11723
|
+
*
|
|
11724
|
+
* Aliases are listed explicitly rather than normalised first, so a group that
|
|
11725
|
+
* gains one and forgets it here degrades to the index page — wrong, but a page.
|
|
11726
|
+
* `cloud-help.test.ts` asserts the two stay in step.
|
|
11727
|
+
*
|
|
11728
|
+
* A group absent from this map has no page of its own; the index lists it.
|
|
11729
|
+
*/
|
|
11730
|
+
var GROUP_HELP = {
|
|
11731
|
+
env: printEnvHelp,
|
|
11732
|
+
domains: printDomainsHelp,
|
|
11733
|
+
domain: printDomainsHelp,
|
|
11734
|
+
extensions: printExtensionsHelp,
|
|
11735
|
+
extension: printExtensionsHelp,
|
|
11736
|
+
settings: printSettingsHelp,
|
|
11737
|
+
orgs: printOrgsHelp,
|
|
11738
|
+
org: printOrgsHelp,
|
|
11739
|
+
db: printDbHelp,
|
|
11740
|
+
database: printDbHelp,
|
|
11741
|
+
debug: printDebugHelp,
|
|
11742
|
+
storage: printStorageHelp
|
|
11743
|
+
};
|
|
10506
11744
|
async function cloudCommand(subcommand, rawArgs) {
|
|
10507
11745
|
initOutputMode(rawArgs);
|
|
10508
11746
|
const pos = positionals(rawArgs);
|
|
10509
11747
|
const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
|
|
11748
|
+
const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
10510
11749
|
const action = pos[1];
|
|
10511
|
-
if (!group
|
|
11750
|
+
if (!group) {
|
|
10512
11751
|
printCloudHelp();
|
|
10513
11752
|
return;
|
|
10514
11753
|
}
|
|
11754
|
+
if (wantsHelp) {
|
|
11755
|
+
(GROUP_HELP[group] ?? printCloudHelp)();
|
|
11756
|
+
return;
|
|
11757
|
+
}
|
|
10515
11758
|
switch (group) {
|
|
10516
11759
|
case "login":
|
|
10517
11760
|
await loginCommand(rawArgs);
|
|
@@ -10602,11 +11845,7 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
10602
11845
|
case "billing":
|
|
10603
11846
|
await billingCommand(rawArgs);
|
|
10604
11847
|
break;
|
|
10605
|
-
default:
|
|
10606
|
-
console.error(chalk.red(`Unknown cloud command: ${group}`));
|
|
10607
|
-
console.log("");
|
|
10608
|
-
printCloudHelp();
|
|
10609
|
-
process.exit(1);
|
|
11848
|
+
default: fail(`Unknown cloud command: ${group}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10610
11849
|
}
|
|
10611
11850
|
}
|
|
10612
11851
|
async function projectsGroup(action, rawArgs) {
|
|
@@ -10619,17 +11858,15 @@ async function projectsGroup(action, rawArgs) {
|
|
|
10619
11858
|
await createProject(rawArgs);
|
|
10620
11859
|
break;
|
|
10621
11860
|
case "info":
|
|
10622
|
-
await projectInfo(rawArgs,
|
|
11861
|
+
await projectInfo(rawArgs, resolveProjectArg(rawArgs, "info"));
|
|
10623
11862
|
break;
|
|
10624
11863
|
case "delete":
|
|
10625
|
-
await deleteProject(rawArgs,
|
|
11864
|
+
await deleteProject(rawArgs, resolveProjectArg(rawArgs, "delete"));
|
|
10626
11865
|
break;
|
|
10627
11866
|
case "--help":
|
|
10628
11867
|
printCloudHelp();
|
|
10629
11868
|
break;
|
|
10630
|
-
default:
|
|
10631
|
-
console.error(chalk.red(`Unknown projects command: ${action}`));
|
|
10632
|
-
process.exit(1);
|
|
11869
|
+
default: fail(`Unknown projects command: ${action}`, "Run `rebase cloud --help`.", "unknown_command");
|
|
10633
11870
|
}
|
|
10634
11871
|
}
|
|
10635
11872
|
async function deploymentsGroup(action, rawArgs) {
|
|
@@ -10641,13 +11878,51 @@ async function deploymentsGroup(action, rawArgs) {
|
|
|
10641
11878
|
case "--help":
|
|
10642
11879
|
printCloudHelp();
|
|
10643
11880
|
break;
|
|
10644
|
-
default:
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
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
|
+
];
|
|
10649
11923
|
function printCloudHelp() {
|
|
10650
|
-
|
|
11924
|
+
emitHelp("cloud", CLOUD_GROUPS, () => {
|
|
11925
|
+
console.log(`
|
|
10651
11926
|
${chalk.bold("rebase cloud")} — Manage your apps on Rebase Cloud
|
|
10652
11927
|
|
|
10653
11928
|
${chalk.green.bold("Usage")}
|
|
@@ -10712,6 +11987,7 @@ ${chalk.green.bold("Global options")}
|
|
|
10712
11987
|
${chalk.gray("Most commands act on the linked project (.rebase/cloud.json) unless --project is given.")}
|
|
10713
11988
|
${chalk.gray("Docs: https://rebase.pro/docs")}
|
|
10714
11989
|
`);
|
|
11990
|
+
});
|
|
10715
11991
|
}
|
|
10716
11992
|
//#endregion
|
|
10717
11993
|
//#region src/commands/apps.ts
|
|
@@ -10744,19 +12020,20 @@ ${chalk.bold("Options")}
|
|
|
10744
12020
|
`.trim());
|
|
10745
12021
|
}
|
|
10746
12022
|
async function appsCommand(subcommand, rawArgs = []) {
|
|
10747
|
-
|
|
10748
|
-
"--json": Boolean,
|
|
10749
|
-
"--force": Boolean,
|
|
10750
|
-
"--help": Boolean,
|
|
10751
|
-
"-h": "--help"
|
|
10752
|
-
}, {
|
|
10753
|
-
argv: rawArgs.slice(3),
|
|
10754
|
-
permissive: true
|
|
10755
|
-
});
|
|
10756
|
-
if (args["--help"] || !subcommand || subcommand === "--help") {
|
|
12023
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
10757
12024
|
printHelp$1();
|
|
10758
12025
|
return;
|
|
10759
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
|
+
});
|
|
10760
12037
|
switch (subcommand) {
|
|
10761
12038
|
case "list":
|
|
10762
12039
|
await listApps(Boolean(args["--json"]));
|
|
@@ -10765,7 +12042,7 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
10765
12042
|
await initManifest(Boolean(args["--force"]));
|
|
10766
12043
|
break;
|
|
10767
12044
|
case "config":
|
|
10768
|
-
await printAppConfig(
|
|
12045
|
+
await printAppConfig(positionals[1], Boolean(args["--json"]));
|
|
10769
12046
|
break;
|
|
10770
12047
|
default:
|
|
10771
12048
|
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
@@ -10914,7 +12191,25 @@ function getVersion() {
|
|
|
10914
12191
|
} catch {}
|
|
10915
12192
|
return "unknown";
|
|
10916
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
|
+
}
|
|
10917
12211
|
async function entry(args) {
|
|
12212
|
+
silenceDotenvBanner();
|
|
10918
12213
|
const parsedArgs = arg({
|
|
10919
12214
|
"--version": Boolean,
|
|
10920
12215
|
"--help": Boolean,
|
|
@@ -10951,7 +12246,7 @@ async function entry(args) {
|
|
|
10951
12246
|
printHelp();
|
|
10952
12247
|
return;
|
|
10953
12248
|
}
|
|
10954
|
-
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
12249
|
+
const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
|
|
10955
12250
|
switch (command) {
|
|
10956
12251
|
case "init":
|
|
10957
12252
|
await createRebaseApp(args);
|
|
@@ -11070,9 +12365,11 @@ ${chalk.green.bold("API Keys")}
|
|
|
11070
12365
|
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
11071
12366
|
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
11072
12367
|
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
11073
|
-
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
11074
12368
|
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
11075
12369
|
|
|
12370
|
+
${chalk.green.bold("Usage sharing")}
|
|
12371
|
+
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
12372
|
+
|
|
11076
12373
|
${chalk.green.bold("Rebase Cloud")}
|
|
11077
12374
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
11078
12375
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
@@ -11103,6 +12400,6 @@ function telemetryNotice() {
|
|
|
11103
12400
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
11104
12401
|
}
|
|
11105
12402
|
//#endregion
|
|
11106
|
-
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, 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 };
|
|
11107
12404
|
|
|
11108
12405
|
//# sourceMappingURL=index.es.js.map
|