@rebasepro/cli 0.13.0 → 0.13.1-canary.g06dbe5b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.d.ts +4 -3
- package/dist/commands/api-keys.d.ts +51 -0
- package/dist/commands/auth.d.ts +62 -0
- package/dist/commands/cloud/context.d.ts +56 -7
- package/dist/commands/cloud/databases.d.ts +39 -0
- package/dist/commands/cloud/debug.d.ts +1 -0
- package/dist/commands/cloud/deployments.d.ts +22 -0
- package/dist/commands/cloud/domains.d.ts +10 -0
- package/dist/commands/cloud/env.d.ts +51 -0
- package/dist/commands/cloud/extensions.d.ts +21 -0
- package/dist/commands/cloud/orgs.d.ts +1 -0
- package/dist/commands/cloud/projects.d.ts +29 -0
- package/dist/commands/cloud/resources.d.ts +14 -0
- package/dist/commands/cloud/settings.d.ts +1 -0
- package/dist/commands/dev.d.ts +17 -6
- package/dist/commands/eject.d.ts +42 -0
- package/dist/commands/init.d.ts +67 -0
- package/dist/commands/skills.d.ts +81 -0
- package/dist/fold-static.d.ts +47 -0
- package/dist/index.es.js +1440 -415
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +16 -1
- package/dist/telemetry/payload.d.ts +1 -1
- package/dist/utils/args.d.ts +76 -0
- package/dist/utils/collection-drift.d.ts +27 -0
- package/dist/utils/project.d.ts +20 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +29 -4
- package/templates/eject/backend/src/index.ts +49 -5
- package/templates/eject/docker-compose.custom.yml +13 -5
- package/templates/overlays/baas/backend/tsconfig.json +6 -1
- package/templates/template/.env.example +13 -4
- package/templates/template/backend/functions/hello.ts +8 -4
- package/templates/template/backend/tsconfig.json +6 -1
- package/templates/template/config/package.json +1 -0
- package/templates/template/docker-compose.yml +4 -4
package/dist/index.es.js
CHANGED
|
@@ -12,9 +12,10 @@ import crypto from "crypto";
|
|
|
12
12
|
import { execSync, spawn, spawnSync } from "child_process";
|
|
13
13
|
import os from "os";
|
|
14
14
|
import { createRebaseClient } from "@rebasepro/client";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
|
+
import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
|
|
15
18
|
import { createRequire } from "module";
|
|
16
|
-
import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
|
|
17
|
-
import { generateSDK } from "@rebasepro/codegen";
|
|
18
19
|
//#region src/utils/package-manager.ts
|
|
19
20
|
/**
|
|
20
21
|
* Package manager detection and command abstraction.
|
|
@@ -190,6 +191,90 @@ function getPMCommands(pm) {
|
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
//#endregion
|
|
194
|
+
//#region src/utils/args.ts
|
|
195
|
+
/**
|
|
196
|
+
* Argument parsing for the commands that take positional arguments.
|
|
197
|
+
*
|
|
198
|
+
* Every command in the small group used to parse its own line the same way:
|
|
199
|
+
* `arg(spec, { argv: rawArgs.slice(4), permissive: true })`, then read `_[0]`
|
|
200
|
+
* and `_[1]`. Both halves of that are wrong in a way that costs data.
|
|
201
|
+
*
|
|
202
|
+
* - **`permissive: true` turns an unknown flag into a positional.** `arg`
|
|
203
|
+
* pushes an undeclared flag into `_` as a bare token, so `_[1]` is whatever
|
|
204
|
+
* came second on the line, flag or not. `rebase auth reset-password
|
|
205
|
+
* bob@example.com --debug` set Bob's password to the literal `--debug` — and
|
|
206
|
+
* `--debug` is what `bin/rebase.js` prints after *every* failure as the thing
|
|
207
|
+
* to re-run with, so the single most likely next keystroke after a failed
|
|
208
|
+
* reset was the one that reset the account to a two-word string.
|
|
209
|
+
* - **`slice(4)` assumes the command words are at fixed indices.** They are
|
|
210
|
+
* not: a flag before the command shifts everything, so `rebase --debug auth
|
|
211
|
+
* reset-password bob@example.com NewPass1!` read the email as
|
|
212
|
+
* `reset-password` and the password as `bob@example.com`.
|
|
213
|
+
*
|
|
214
|
+
* So: parse the *whole* line — `rawArgs` is `process.argv` — against a spec
|
|
215
|
+
* strictly, with no permissive mode. `arg` then consumes every flag wherever it
|
|
216
|
+
* appears and rejects the ones nobody declared, which leaves `_` holding the
|
|
217
|
+
* command words followed by the real positionals, in order and at a known
|
|
218
|
+
* offset. An unrecognised flag becomes an error naming the command's help,
|
|
219
|
+
* which is the only safe answer: the alternative is guessing that it was meant
|
|
220
|
+
* as a value.
|
|
221
|
+
*
|
|
222
|
+
* `commands/cloud/index.ts` resolves its positionals against its own spec for
|
|
223
|
+
* the same reason; this is that idea for the commands whose positionals are
|
|
224
|
+
* credentials rather than resource names.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Flags accepted on top of whatever a command declares.
|
|
228
|
+
*
|
|
229
|
+
* `--debug` is read by `bin/rebase.js` off `process.argv` and never by a
|
|
230
|
+
* command, but it has to be *declared* somewhere or strict parsing rejects the
|
|
231
|
+
* exact flag the CLI tells people to add. `--help`/`-h` are answered by each
|
|
232
|
+
* command's dispatcher before any work happens.
|
|
233
|
+
*/
|
|
234
|
+
var GLOBAL_COMMAND_FLAGS = {
|
|
235
|
+
"--debug": Boolean,
|
|
236
|
+
"--help": Boolean,
|
|
237
|
+
"-h": "--help"
|
|
238
|
+
};
|
|
239
|
+
/** Did the line ask for help? Answered before dispatch, never by a handler. */
|
|
240
|
+
function wantsHelp(rawArgs) {
|
|
241
|
+
return rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a command's flags and positionals from the full `process.argv`.
|
|
245
|
+
*
|
|
246
|
+
* `commandWords` is how many words name the command itself — 2 for
|
|
247
|
+
* `auth reset-password`, 1 for `start` — and is applied to the *parsed*
|
|
248
|
+
* positionals rather than to `argv`, so a flag placed before the command no
|
|
249
|
+
* longer shifts them.
|
|
250
|
+
*
|
|
251
|
+
* `command` names the command in error messages, e.g. `auth reset-password`.
|
|
252
|
+
*
|
|
253
|
+
* Throws on an unknown flag, on a positional that looks like a flag, and on
|
|
254
|
+
* more positionals than the command takes. `bin/rebase.js` turns each into a
|
|
255
|
+
* one-line `✗ …` and exit 1.
|
|
256
|
+
*/
|
|
257
|
+
function parseCommandArgs({ spec, rawArgs, commandWords, command, maxPositionals }) {
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = arg({
|
|
261
|
+
...GLOBAL_COMMAND_FLAGS,
|
|
262
|
+
...spec
|
|
263
|
+
}, { argv: rawArgs.slice(2) });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof Error && err.code === "ARG_UNKNOWN_OPTION") throw new Error(`${err.message} — run \`rebase ${command} --help\` for the options it takes.`);
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
const positionals = parsed._.slice(commandWords);
|
|
269
|
+
for (const value of positionals) if (value.startsWith("-")) throw new Error(`\`${value}\` looks like an option, not a value — pass it with an explicit flag (\`rebase ${command} --help\`).`);
|
|
270
|
+
if (maxPositionals !== void 0 && positionals.length > maxPositionals) throw new Error(`rebase ${command} takes ${maxPositionals} argument${maxPositionals === 1 ? "" : "s"}, got ${positionals.length}: ${positionals.join(" ")}`);
|
|
271
|
+
return {
|
|
272
|
+
flags: parsed,
|
|
273
|
+
positionals,
|
|
274
|
+
help: Boolean(parsed["--help"])
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
193
278
|
//#region src/utils/project.ts
|
|
194
279
|
/**
|
|
195
280
|
* Project discovery utilities for the Rebase CLI.
|
|
@@ -289,6 +374,34 @@ function findEnvFile(projectRoot) {
|
|
|
289
374
|
return null;
|
|
290
375
|
}
|
|
291
376
|
/**
|
|
377
|
+
* Read the project's `.env` into a plain object.
|
|
378
|
+
*
|
|
379
|
+
* One reader, because there were four: `dotenv` in `start`, a hand-rolled
|
|
380
|
+
* `indexOf("=")` loop in `api-keys`, a single-key regex in `auth`, and its own
|
|
381
|
+
* splitting in `cloud env`. `dotenv` is a declared dependency of this package,
|
|
382
|
+
* so the other three existed for no reason and disagreed with the correct one
|
|
383
|
+
* on the two things people actually write in a `.env`:
|
|
384
|
+
*
|
|
385
|
+
* - `export KEY=value`, which the hand-rolled parser keyed as
|
|
386
|
+
* `export KEY` — so the command reported the key as unset while it was
|
|
387
|
+
* right there in the file;
|
|
388
|
+
* - `KEY=value # comment`, whose comment travelled into the value and then
|
|
389
|
+
* into an `Authorization` header, coming back as a 401 with nothing
|
|
390
|
+
* pointing at the cause.
|
|
391
|
+
*
|
|
392
|
+
* Returns `{}` when the project has no `.env`, so callers can treat "absent"
|
|
393
|
+
* and "empty" alike.
|
|
394
|
+
*/
|
|
395
|
+
function readEnvFile(projectRoot) {
|
|
396
|
+
const envFile = findEnvFile(projectRoot);
|
|
397
|
+
if (!envFile || !fs.existsSync(envFile)) return {};
|
|
398
|
+
try {
|
|
399
|
+
return dotenv.parse(fs.readFileSync(envFile, "utf-8"));
|
|
400
|
+
} catch {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
292
405
|
* Resolve a binary from the project's node_modules/.bin.
|
|
293
406
|
* Checks backend, root, parent monorepo root, then falls back to PATH.
|
|
294
407
|
*/
|
|
@@ -827,16 +940,69 @@ async function confirmDestructive(opts) {
|
|
|
827
940
|
}
|
|
828
941
|
}
|
|
829
942
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
943
|
+
* Resolve one cloud command's flags and ARGUMENTS from the full `process.argv`.
|
|
944
|
+
*
|
|
945
|
+
* This replaces `cloudPositionals`, which was `rawArgs.slice(3).filter(a =>
|
|
946
|
+
* !a.startsWith("-"))`. Dropping `-`-prefixed tokens looks like it solves the
|
|
947
|
+
* permissive-parse problem and does not: a flag that takes a VALUE leaves the
|
|
948
|
+
* value behind, an ordinary word in the argument position that no filter can
|
|
949
|
+
* tell from a real one. `--project` is the flag every one of these commands
|
|
950
|
+
* documents, so the failure was reachable from the help page:
|
|
951
|
+
*
|
|
952
|
+
* rebase cloud env unset -p acme → removed the variable "acme"
|
|
953
|
+
* rebase cloud env set KEY -p acme → stored the value "acme"
|
|
954
|
+
* rebase cloud domains add -p acme → registered the domain "acme"
|
|
955
|
+
* rebase cloud webhooks delete -p acme 42 → deleted webhook "acme", not 42
|
|
956
|
+
* rebase cloud cancel -p acme → cancelled deployment id "acme"
|
|
957
|
+
*
|
|
958
|
+
* The filter's other half is quieter. A flag nobody declared *is* dropped by
|
|
959
|
+
* it — but only from the operands, never from the run: nothing rejects it, so
|
|
960
|
+
* the command proceeds with the argument missing or defaulted. `db backup
|
|
961
|
+
* --dry-run` listed backups, `domains remove --dry-run` detached the domain,
|
|
962
|
+
* and `env set KEY=v --secrett` stored the value as an ordinary variable that
|
|
963
|
+
* `env reveal` will hand back. The one place an undeclared flag became the
|
|
964
|
+
* argument outright is `projects info|delete`, which resolved its id through
|
|
965
|
+
* `positionals()` instead — that skips only LEADING `-` tokens, so `projects
|
|
966
|
+
* delete --force` looked up a project named "--force".
|
|
967
|
+
*
|
|
968
|
+
* So: parse the whole line strictly, through the same `parseCommandArgs` the
|
|
969
|
+
* non-cloud commands use — `arg` then consumes each declared flag *with its
|
|
970
|
+
* value* wherever it appears, and rejects the undeclared, leaving `_` holding
|
|
971
|
+
* the command words followed by the real arguments. `commandWords` counts from
|
|
972
|
+
* `cloud` itself (`cloud env set` ⇒ 3), and is applied to the parsed
|
|
973
|
+
* positionals, so a flag written before the group shifts nothing.
|
|
974
|
+
*
|
|
975
|
+
* Two things this adds over calling `parseCommandArgs` directly, and the reason
|
|
976
|
+
* it is worth a wrapper:
|
|
977
|
+
*
|
|
978
|
+
* - `GLOBAL_CLOUD_FLAGS` is merged in. `--json`, `--yes` and `--project` may
|
|
979
|
+
* appear anywhere on a cloud line including before the group, so a strict
|
|
980
|
+
* parse that did not declare them would reject the CLI's own documented
|
|
981
|
+
* usage. (`parseCommandArgs` adds `--debug`/`--help` on top of that.)
|
|
982
|
+
* - A parse error is reported through `fail`, not thrown. A throw reaches
|
|
983
|
+
* `bin/rebase.js`, which prints `✗ …` to stderr — which is right for every
|
|
984
|
+
* other command and wrong here: `rebase cloud` is in JSON mode whenever
|
|
985
|
+
* stdout is not a TTY, i.e. always for the agents this family is built for,
|
|
986
|
+
* and it promises them exactly one JSON value. `fail` keeps that promise,
|
|
987
|
+
* with the same `usage` code the other refusals in this family use.
|
|
988
|
+
*/
|
|
989
|
+
function parseCloudArgs(opts) {
|
|
990
|
+
const spec = {
|
|
991
|
+
...GLOBAL_CLOUD_FLAGS,
|
|
992
|
+
...opts.spec
|
|
993
|
+
};
|
|
994
|
+
try {
|
|
995
|
+
const parsed = parseCommandArgs({
|
|
996
|
+
...opts,
|
|
997
|
+
spec
|
|
998
|
+
});
|
|
999
|
+
return {
|
|
1000
|
+
flags: parsed.flags,
|
|
1001
|
+
positionals: parsed.positionals
|
|
1002
|
+
};
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
fail(err instanceof Error ? err.message : String(err), void 0, "usage");
|
|
1005
|
+
}
|
|
840
1006
|
}
|
|
841
1007
|
function success(message) {
|
|
842
1008
|
console.log("");
|
|
@@ -949,13 +1115,35 @@ function sanitize(properties) {
|
|
|
949
1115
|
}
|
|
950
1116
|
return out;
|
|
951
1117
|
}
|
|
1118
|
+
/**
|
|
1119
|
+
* The CLI's own version, read by walking up to this package's manifest.
|
|
1120
|
+
*
|
|
1121
|
+
* The obvious `require("../../package.json")` was wrong everywhere, not only on
|
|
1122
|
+
* one install path: `vite build` bundles this module into `dist/index.es.js`, so
|
|
1123
|
+
* the specifier resolves relative to `<pkg>/dist/` and lands on
|
|
1124
|
+
* `<parent-of-pkg>/package.json` — a file that does not exist under npm, pnpm or
|
|
1125
|
+
* the monorepo. Every event ever sent carried `cliVersion: "unknown"`, which is
|
|
1126
|
+
* the one field that makes the rest of a payload interpretable.
|
|
1127
|
+
*
|
|
1128
|
+
* So walk up and check the manifest's `name` rather than counting directory
|
|
1129
|
+
* levels: the count differs between `src/telemetry/` and the bundled `dist/`,
|
|
1130
|
+
* and a wrong count fails silently by finding *some* package.json — the nearest
|
|
1131
|
+
* dependency's, under a hoisted layout. Matching the name cannot do that.
|
|
1132
|
+
*/
|
|
952
1133
|
function cliVersion() {
|
|
953
1134
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1135
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1136
|
+
const root = path.parse(dir).root;
|
|
1137
|
+
while (dir && dir !== root) {
|
|
1138
|
+
const manifest = path.join(dir, "package.json");
|
|
1139
|
+
if (fs.existsSync(manifest)) {
|
|
1140
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
1141
|
+
if (pkg?.name === "@rebasepro/cli" && typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
1142
|
+
}
|
|
1143
|
+
dir = path.dirname(dir);
|
|
1144
|
+
}
|
|
1145
|
+
} catch {}
|
|
1146
|
+
return "unknown";
|
|
959
1147
|
}
|
|
960
1148
|
function buildEvent(event, properties, identity) {
|
|
961
1149
|
return {
|
|
@@ -1455,7 +1643,7 @@ ${chalk.bold("Examples")}
|
|
|
1455
1643
|
`);
|
|
1456
1644
|
}
|
|
1457
1645
|
async function createRebaseApp(rawArgs) {
|
|
1458
|
-
if (
|
|
1646
|
+
if (wantsHelp(rawArgs)) {
|
|
1459
1647
|
printInitHelp();
|
|
1460
1648
|
return;
|
|
1461
1649
|
}
|
|
@@ -1464,26 +1652,31 @@ ${chalk.bold("Rebase")} — Create a new project 🚀
|
|
|
1464
1652
|
`);
|
|
1465
1653
|
await createProject$1(await promptForOptions(rawArgs, detectPackageManager()));
|
|
1466
1654
|
}
|
|
1655
|
+
/** The flags `rebase init` takes. */
|
|
1656
|
+
var INIT_FLAGS = {
|
|
1657
|
+
"--git": Boolean,
|
|
1658
|
+
"--install": Boolean,
|
|
1659
|
+
"--database-url": String,
|
|
1660
|
+
"--introspect": Boolean,
|
|
1661
|
+
"--template": String,
|
|
1662
|
+
"--headless": Boolean,
|
|
1663
|
+
"--project": String,
|
|
1664
|
+
"--setup-key": String,
|
|
1665
|
+
"--yes": Boolean,
|
|
1666
|
+
"-g": "--git",
|
|
1667
|
+
"-i": "--install",
|
|
1668
|
+
"-t": "--template",
|
|
1669
|
+
"-y": "--yes"
|
|
1670
|
+
};
|
|
1467
1671
|
async function promptForOptions(rawArgs, pm) {
|
|
1468
|
-
const args =
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
"
|
|
1473
|
-
|
|
1474
|
-
"--headless": Boolean,
|
|
1475
|
-
"--project": String,
|
|
1476
|
-
"--setup-key": String,
|
|
1477
|
-
"--yes": Boolean,
|
|
1478
|
-
"-g": "--git",
|
|
1479
|
-
"-i": "--install",
|
|
1480
|
-
"-t": "--template",
|
|
1481
|
-
"-y": "--yes"
|
|
1482
|
-
}, {
|
|
1483
|
-
argv: rawArgs.slice(3),
|
|
1484
|
-
permissive: true
|
|
1672
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
1673
|
+
spec: INIT_FLAGS,
|
|
1674
|
+
rawArgs,
|
|
1675
|
+
commandWords: 1,
|
|
1676
|
+
command: "init",
|
|
1677
|
+
maxPositionals: 1
|
|
1485
1678
|
});
|
|
1486
|
-
const nameArg =
|
|
1679
|
+
const nameArg = positionals[0];
|
|
1487
1680
|
const isNonInteractive = args["--yes"] || false;
|
|
1488
1681
|
if (nameArg) {
|
|
1489
1682
|
const resolvedName = path.basename(path.resolve(process.cwd(), nameArg));
|
|
@@ -1606,6 +1799,49 @@ async function linkScaffoldToCloud(options) {
|
|
|
1606
1799
|
console.warn(chalk.yellow(` ${linkLater}`));
|
|
1607
1800
|
}
|
|
1608
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Make the initial commit, after everything that writes into the project has run.
|
|
1804
|
+
*
|
|
1805
|
+
* It used to happen immediately after `git init`, which is before dependency
|
|
1806
|
+
* installation and before introspection — so `init --git --install` ended on a
|
|
1807
|
+
* dirty tree whose only untracked file was `pnpm-lock.yaml`. A lockfile is
|
|
1808
|
+
* precisely the thing that should be in a project's first commit, and a brand
|
|
1809
|
+
* new scaffold whose first `git status` is dirty invites the reader to conclude
|
|
1810
|
+
* the lockfile is deliberately ignored and never commit it at all.
|
|
1811
|
+
*
|
|
1812
|
+
* Introspection has the same shape: it generates `config/collections` and
|
|
1813
|
+
* `schema.generated.ts`, which belong in the commit describing the scaffold that
|
|
1814
|
+
* produced them.
|
|
1815
|
+
*
|
|
1816
|
+
* `git init` stays where it was. Creating the repository early costs nothing and
|
|
1817
|
+
* means a failed install still leaves the user a repository to commit into.
|
|
1818
|
+
*/
|
|
1819
|
+
async function commitScaffold(targetDirectory) {
|
|
1820
|
+
try {
|
|
1821
|
+
await execa("git", ["add", "-A"], { cwd: targetDirectory });
|
|
1822
|
+
let identity = {};
|
|
1823
|
+
try {
|
|
1824
|
+
await execa("git", ["config", "user.email"], { cwd: targetDirectory });
|
|
1825
|
+
} catch {
|
|
1826
|
+
identity = {
|
|
1827
|
+
GIT_AUTHOR_NAME: "Rebase",
|
|
1828
|
+
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1829
|
+
GIT_COMMITTER_NAME: "Rebase",
|
|
1830
|
+
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
await execa("git", [
|
|
1834
|
+
"commit",
|
|
1835
|
+
"-m",
|
|
1836
|
+
"Initial commit from Rebase"
|
|
1837
|
+
], {
|
|
1838
|
+
cwd: targetDirectory,
|
|
1839
|
+
env: identity
|
|
1840
|
+
});
|
|
1841
|
+
} catch {
|
|
1842
|
+
console.warn(chalk.yellow(" Warning: Failed to create the initial commit"));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1609
1845
|
async function createProject$1(options) {
|
|
1610
1846
|
const startedAt = Date.now();
|
|
1611
1847
|
if (fs.existsSync(options.targetDirectory)) {
|
|
@@ -1645,6 +1881,7 @@ async function createProject$1(options) {
|
|
|
1645
1881
|
await applyHeadless(options.targetDirectory, options.headless);
|
|
1646
1882
|
await replacePlaceholders(options);
|
|
1647
1883
|
await configureEnvFile(options.targetDirectory, options.databaseUrl);
|
|
1884
|
+
let gitInitialized = false;
|
|
1648
1885
|
if (options.git) {
|
|
1649
1886
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
1650
1887
|
try {
|
|
@@ -1656,26 +1893,7 @@ async function createProject$1(options) {
|
|
|
1656
1893
|
"refs/heads/main"
|
|
1657
1894
|
], { cwd: options.targetDirectory });
|
|
1658
1895
|
} catch {}
|
|
1659
|
-
|
|
1660
|
-
let identity = {};
|
|
1661
|
-
try {
|
|
1662
|
-
await execa("git", ["config", "user.email"], { cwd: options.targetDirectory });
|
|
1663
|
-
} catch {
|
|
1664
|
-
identity = {
|
|
1665
|
-
GIT_AUTHOR_NAME: "Rebase",
|
|
1666
|
-
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1667
|
-
GIT_COMMITTER_NAME: "Rebase",
|
|
1668
|
-
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
await execa("git", [
|
|
1672
|
-
"commit",
|
|
1673
|
-
"-m",
|
|
1674
|
-
"Initial commit from Rebase"
|
|
1675
|
-
], {
|
|
1676
|
-
cwd: options.targetDirectory,
|
|
1677
|
-
env: identity
|
|
1678
|
-
});
|
|
1896
|
+
gitInitialized = true;
|
|
1679
1897
|
} catch {
|
|
1680
1898
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1681
1899
|
}
|
|
@@ -1732,6 +1950,7 @@ async function createProject$1(options) {
|
|
|
1732
1950
|
console.warn(chalk.yellow(` Run \`${installCmd.join(" ")}\` then \`${execCmd.join(" ")}\` manually.`));
|
|
1733
1951
|
}
|
|
1734
1952
|
}
|
|
1953
|
+
if (gitInitialized) await commitScaffold(options.targetDirectory);
|
|
1735
1954
|
await linkScaffoldToCloud(options);
|
|
1736
1955
|
console.log("");
|
|
1737
1956
|
console.log(`${chalk.green.bold("✓")} Project ${chalk.bold(options.projectName)} created successfully!`);
|
|
@@ -1789,7 +2008,7 @@ async function createProject$1(options) {
|
|
|
1789
2008
|
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
1790
2009
|
}
|
|
1791
2010
|
console.log("");
|
|
1792
|
-
console.log(isBaas ? chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
2011
|
+
console.log(isBaas ? introspected ? chalk.gray("This starts a headless API (Hono + PostgreSQL) over the collections just ") + chalk.gray("generated from your database in config/collections. Edit them to change what the ") + chalk.gray("API exposes; docs are at /api/swagger.") : chalk.gray("This starts a headless API (Hono + PostgreSQL). There are no collection files: ") + chalk.gray("the API is derived from your database schema. Once it serves a table, docs are at /api/swagger.") : chalk.gray("This starts both the backend (Hono + PostgreSQL)") + chalk.gray(" and the frontend (Vite + React) concurrently."));
|
|
1793
2012
|
console.log("");
|
|
1794
2013
|
console.log(chalk.gray("Docs: https://rebase.pro/docs"));
|
|
1795
2014
|
console.log(chalk.gray("GitHub: https://github.com/rebasepro/rebase"));
|
|
@@ -1976,18 +2195,51 @@ project directory ${path.basename(options.targetDirectory)}/ was created and is
|
|
|
1976
2195
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1977
2196
|
}
|
|
1978
2197
|
}
|
|
1979
|
-
|
|
2198
|
+
/** `undefined` binds the wildcard address, which is a different question — see isPortAvailable. */
|
|
2199
|
+
function canBind(port, host) {
|
|
1980
2200
|
return new Promise((resolve) => {
|
|
1981
2201
|
const server = net.createServer();
|
|
1982
|
-
server.once("error", () => {
|
|
1983
|
-
resolve(
|
|
2202
|
+
server.once("error", (err) => {
|
|
2203
|
+
resolve(err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL");
|
|
1984
2204
|
});
|
|
1985
2205
|
server.once("listening", () => {
|
|
1986
2206
|
server.close(() => resolve(true));
|
|
1987
2207
|
});
|
|
1988
|
-
server.listen(port);
|
|
2208
|
+
if (host === void 0) server.listen(port);
|
|
2209
|
+
else server.listen(port, host);
|
|
1989
2210
|
});
|
|
1990
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Whether `port` is free — on the wildcard address *and* on both loopback addresses.
|
|
2214
|
+
*
|
|
2215
|
+
* All three, because on macOS/BSD a successful bind does not mean the port is
|
|
2216
|
+
* unused. Sockets carry `SO_REUSEADDR` (Node sets it), under which a wildcard
|
|
2217
|
+
* bind and a specific-address bind on the same port do not conflict — in either
|
|
2218
|
+
* direction. So each probe alone has a blind spot, and they are different ones:
|
|
2219
|
+
*
|
|
2220
|
+
* - **Wildcard only** (what this used to do) misses a server bound to
|
|
2221
|
+
* `127.0.0.1` and `[::1]` — a Homebrew or Postgres.app install, i.e. most
|
|
2222
|
+
* developer machines. 5432 was reported free while it was already serving
|
|
2223
|
+
* another project's database. Docker then published `*:5432` for the same
|
|
2224
|
+
* reason and the container started cleanly, with no "port already allocated"
|
|
2225
|
+
* error anywhere to hint at the collision. `DATABASE_URL` pointed at
|
|
2226
|
+
* `localhost:5432`, `localhost` resolves to `::1` first, and every command
|
|
2227
|
+
* reported success while reading and writing the *pre-existing* database — a
|
|
2228
|
+
* `db push` would have created tables, roles and RLS policies inside it.
|
|
2229
|
+
*
|
|
2230
|
+
* - **Loopback only** misses the opposite case: Docker Desktop publishes a
|
|
2231
|
+
* container's port on `*`, and a specific-address bind succeeds right past it.
|
|
2232
|
+
* That port is free to probe and unusable to publish, so `docker compose up -d
|
|
2233
|
+
* db` fails on "Bind for 0.0.0.0:PORT failed: port is already allocated" —
|
|
2234
|
+
* loudly, but only after the project has been generated around the bad port.
|
|
2235
|
+
*
|
|
2236
|
+
* Requiring all three costs three sockets and leaves neither gap. The wildcard
|
|
2237
|
+
* bind is the one the container itself has to make; the loopback binds are the
|
|
2238
|
+
* addresses `DATABASE_URL` will actually name.
|
|
2239
|
+
*/
|
|
2240
|
+
async function isPortAvailable(port) {
|
|
2241
|
+
return await canBind(port) && await canBind(port, "127.0.0.1") && await canBind(port, "::1");
|
|
2242
|
+
}
|
|
1991
2243
|
async function findAvailablePort(startPort) {
|
|
1992
2244
|
let port = startPort;
|
|
1993
2245
|
while (!await isPortAvailable(port)) port++;
|
|
@@ -2013,11 +2265,38 @@ function readCliVersion() {
|
|
|
2013
2265
|
} catch {}
|
|
2014
2266
|
return "latest";
|
|
2015
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* The runtime image tag to pin, given the version of the CLI doing the scaffolding.
|
|
2270
|
+
*
|
|
2271
|
+
* Only a stable release publishes `rebasepro/server` — a multi-arch build on
|
|
2272
|
+
* every push to main would cost minutes per commit for an image nobody pulls.
|
|
2273
|
+
* So pinning a prerelease CLI's own version writes a tag that cannot exist, and
|
|
2274
|
+
* `docker compose up` fails on `manifest unknown`, which is the same dead end
|
|
2275
|
+
* as the missing-repository bug this pinning was added to prevent.
|
|
2276
|
+
*
|
|
2277
|
+
* A prerelease therefore falls back to `latest`, which is correct rather than
|
|
2278
|
+
* merely available: a bundle's manifest declares the runtime range it needs
|
|
2279
|
+
* (`^1`), the image supplies only `@rebasepro/server`, and the framework a
|
|
2280
|
+
* bundle runs is installed from its own `deps.declared` at boot. The current
|
|
2281
|
+
* stable runtime boots a canary bundle by design.
|
|
2282
|
+
*
|
|
2283
|
+
* A floating tag is a real cost — it is what pinning exists to avoid — so say
|
|
2284
|
+
* so in the file rather than leaving a reader to discover it.
|
|
2285
|
+
*/
|
|
2286
|
+
function resolveRuntimeImageTag(cliVersion) {
|
|
2287
|
+
if (/^\d+\.\d+\.\d+-/.test(cliVersion)) return {
|
|
2288
|
+
tag: "latest",
|
|
2289
|
+
note: `# Scaffolded by a prerelease CLI (${cliVersion}), which publishes no runtime image,\n# so this floats to the newest stable runtime. Pin an exact version once you
|
|
2290
|
+
# deploy: a moving tag changes what you are running with no version changing.`
|
|
2291
|
+
};
|
|
2292
|
+
return { tag: cliVersion };
|
|
2293
|
+
}
|
|
2016
2294
|
async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
2017
2295
|
const envExamplePath = path.join(targetDirectory, ".env.example");
|
|
2018
2296
|
const envPath = path.join(targetDirectory, ".env");
|
|
2019
2297
|
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
|
|
2020
2298
|
fs.copyFileSync(envExamplePath, envPath);
|
|
2299
|
+
fs.chmodSync(envPath, 384);
|
|
2021
2300
|
const jwtSecret = crypto.randomBytes(32).toString("hex");
|
|
2022
2301
|
const dbPassword = crypto.randomBytes(16).toString("hex");
|
|
2023
2302
|
const serviceKey = crypto.randomBytes(48).toString("base64");
|
|
@@ -2031,8 +2310,10 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2031
2310
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
2032
2311
|
const composeApiPort = /^PORT=(\d+)/m.exec(envContent)?.[1] ?? "3001";
|
|
2033
2312
|
envContent = envContent.replace(/^#\s*CORS_ORIGINS=.*$/m, `CORS_ORIGINS=http://localhost:${composeApiPort}`);
|
|
2034
|
-
|
|
2035
|
-
|
|
2313
|
+
envContent = envContent.replace(/^#?\s*VITE_API_URL=.*$/m, "VITE_API_URL=");
|
|
2314
|
+
const { tag: runtimeVersion, note } = resolveRuntimeImageTag(readCliVersion());
|
|
2315
|
+
const pinned = `${note ? `${note}\n` : ""}REBASE_VERSION=${runtimeVersion}`;
|
|
2316
|
+
envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, pinned) : `${envContent.trimEnd()}\n\n# The Rebase runtime image tag docker-compose.yml pulls.\n# Change this and restart to upgrade; your project bundle is untouched.\n${pinned}\n`;
|
|
2036
2317
|
if (databaseUrl) {
|
|
2037
2318
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
2038
2319
|
const { pinSearchPath } = await import("@rebasepro/server-postgres");
|
|
@@ -2040,7 +2321,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2040
2321
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${pinnedUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2041
2322
|
} else {
|
|
2042
2323
|
const dbPort = await findAvailablePort(5432);
|
|
2043
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://
|
|
2324
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase_app:${dbPassword}@127.0.0.1:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
2044
2325
|
const dockerComposePath = path.join(targetDirectory, "docker-compose.yml");
|
|
2045
2326
|
if (fs.existsSync(dockerComposePath)) {
|
|
2046
2327
|
let dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8");
|
|
@@ -2048,7 +2329,11 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
2048
2329
|
fs.writeFileSync(dockerComposePath, dockerComposeContent, "utf-8");
|
|
2049
2330
|
}
|
|
2050
2331
|
}
|
|
2051
|
-
fs.writeFileSync(envPath, envContent,
|
|
2332
|
+
fs.writeFileSync(envPath, envContent, {
|
|
2333
|
+
encoding: "utf-8",
|
|
2334
|
+
mode: 384
|
|
2335
|
+
});
|
|
2336
|
+
fs.chmodSync(envPath, 384);
|
|
2052
2337
|
}
|
|
2053
2338
|
}
|
|
2054
2339
|
//#endregion
|
|
@@ -2318,7 +2603,17 @@ async function generateSdkCommand(args) {
|
|
|
2318
2603
|
console.log(chalk.green(` ✓ Found ${collections.length} collection(s): ${collections.map((c) => c.slug).join(", ")}`));
|
|
2319
2604
|
console.log("");
|
|
2320
2605
|
console.log(chalk.cyan(" → Generating SDK files..."));
|
|
2321
|
-
|
|
2606
|
+
let files;
|
|
2607
|
+
try {
|
|
2608
|
+
files = generateSDK(collections);
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
if (err instanceof CodegenError) {
|
|
2611
|
+
console.log("");
|
|
2612
|
+
console.log(chalk.red(` ✗ ${err.message}`));
|
|
2613
|
+
process.exit(1);
|
|
2614
|
+
}
|
|
2615
|
+
throw err;
|
|
2616
|
+
}
|
|
2322
2617
|
const schemaVersion = remoteSchemaVersion ?? computeSchemaVersion(collections);
|
|
2323
2618
|
files.push({
|
|
2324
2619
|
path: "schema.meta.ts",
|
|
@@ -2330,7 +2625,6 @@ async function generateSdkCommand(args) {
|
|
|
2330
2625
|
// curl -s <api-url>/api/meta/schema-version
|
|
2331
2626
|
//
|
|
2332
2627
|
export const SCHEMA_VERSION = ${JSON.stringify(schemaVersion)};
|
|
2333
|
-
export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOString())};
|
|
2334
2628
|
`
|
|
2335
2629
|
});
|
|
2336
2630
|
console.log(chalk.green(` ✓ Generated ${files.length} file(s)`));
|
|
@@ -2352,8 +2646,9 @@ export const GENERATED_AT = ${JSON.stringify((/* @__PURE__ */ new Date()).toISOS
|
|
|
2352
2646
|
console.log(chalk.gray(" // token: 'your-jwt-token',"));
|
|
2353
2647
|
console.log(chalk.gray(" });"));
|
|
2354
2648
|
console.log("");
|
|
2355
|
-
|
|
2356
|
-
|
|
2649
|
+
const exampleAccessor = toSafeIdentifier(exampleSlug);
|
|
2650
|
+
const exampleAccess = isIdentifierLike(exampleAccessor) ? `rebase.data.${exampleAccessor}` : `rebase.data[${JSON.stringify(exampleAccessor)}]`;
|
|
2651
|
+
console.log(chalk.gray(` const { data } = await ${exampleAccess}.find();`));
|
|
2357
2652
|
console.log("");
|
|
2358
2653
|
}
|
|
2359
2654
|
//#endregion
|
|
@@ -2367,7 +2662,7 @@ async function schemaCommand(subcommand, rawArgs) {
|
|
|
2367
2662
|
return;
|
|
2368
2663
|
}
|
|
2369
2664
|
const projectRoot = requireProjectRoot();
|
|
2370
|
-
recordEvent("cli.
|
|
2665
|
+
recordEvent("cli.schema", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2371
2666
|
const backendDir = requireBackendDir(projectRoot);
|
|
2372
2667
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2373
2668
|
if (!activePlugin) {
|
|
@@ -2436,7 +2731,7 @@ async function dbCommand(subcommand, rawArgs) {
|
|
|
2436
2731
|
return;
|
|
2437
2732
|
}
|
|
2438
2733
|
const projectRoot = requireProjectRoot();
|
|
2439
|
-
recordEvent("cli.
|
|
2734
|
+
recordEvent("cli.db", { subcommand: subcommand ?? "none" }, { projectRoot });
|
|
2440
2735
|
const backendDir = requireBackendDir(projectRoot);
|
|
2441
2736
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
2442
2737
|
if (!activePlugin) {
|
|
@@ -2834,13 +3129,20 @@ function validateManifest(raw) {
|
|
|
2834
3129
|
byPath.set(at, name);
|
|
2835
3130
|
}
|
|
2836
3131
|
const storage = validateStorageSources(raw.storage, issues);
|
|
3132
|
+
let telemetry;
|
|
3133
|
+
if (raw.telemetry !== void 0) if (typeof raw.telemetry === "boolean") telemetry = raw.telemetry;
|
|
3134
|
+
else issues.push({
|
|
3135
|
+
path: "telemetry",
|
|
3136
|
+
message: "must be a boolean — only `false` does anything, and it opts this repository out of usage sharing"
|
|
3137
|
+
});
|
|
2837
3138
|
if (issues.length > 0) return { issues };
|
|
2838
3139
|
return {
|
|
2839
3140
|
manifest: {
|
|
2840
3141
|
$schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
|
|
2841
3142
|
rebase: raw.rebase,
|
|
2842
3143
|
apps,
|
|
2843
|
-
...storage ? { storage } : {}
|
|
3144
|
+
...storage ? { storage } : {},
|
|
3145
|
+
...telemetry !== void 0 ? { telemetry } : {}
|
|
2844
3146
|
},
|
|
2845
3147
|
issues
|
|
2846
3148
|
};
|
|
@@ -2949,7 +3251,7 @@ function synthesizeManifest(projectRoot) {
|
|
|
2949
3251
|
if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
|
|
2950
3252
|
if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
|
|
2951
3253
|
apps.backend = backend;
|
|
2952
|
-
if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or run `rebase eject
|
|
3254
|
+
if (!dockerfile && exists("backend/src/index.ts")) console.warn("⚠ backend/src/index.ts exists but this project's backend is managed — it is\n never loaded. Delete it, or move it aside and run `rebase eject`, which\n writes an entrypoint of its own and owns the image. Eject does not adopt\n this file: it refuses to replace it unless you pass --force.");
|
|
2953
3255
|
}
|
|
2954
3256
|
if (exists("frontend")) apps.web = {
|
|
2955
3257
|
type: "static",
|
|
@@ -2997,13 +3299,59 @@ function loadManifest(projectRoot) {
|
|
|
2997
3299
|
filePath
|
|
2998
3300
|
};
|
|
2999
3301
|
}
|
|
3000
|
-
/**
|
|
3302
|
+
/** The keys `writeManifest` knows how to write. Everything else is carried through. */
|
|
3303
|
+
var MODELLED_MANIFEST_KEYS = [
|
|
3304
|
+
"$schema",
|
|
3305
|
+
"rebase",
|
|
3306
|
+
"apps",
|
|
3307
|
+
"storage",
|
|
3308
|
+
"telemetry"
|
|
3309
|
+
];
|
|
3310
|
+
/**
|
|
3311
|
+
* What is on disk right now, or `{}` — this is a *rewrite*, so the file that is
|
|
3312
|
+
* about to be replaced is the only record of the keys the caller did not model.
|
|
3313
|
+
* Unparseable is treated as absent: `loadManifest` refuses malformed JSON long
|
|
3314
|
+
* before anything gets here, and a writer is not the place to fail on it.
|
|
3315
|
+
*/
|
|
3316
|
+
function readManifestObject(filePath) {
|
|
3317
|
+
try {
|
|
3318
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
3319
|
+
return isRecord(parsed) ? parsed : {};
|
|
3320
|
+
} catch {
|
|
3321
|
+
return {};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Write a manifest, with a trailing newline so it plays well with other tools.
|
|
3326
|
+
*
|
|
3327
|
+
* **Every key on disk survives.** This used to emit exactly `$schema`, `rebase`
|
|
3328
|
+
* and `apps`, so a rewrite deleted the rest of the file — and two commands with
|
|
3329
|
+
* no visible relationship to either key rewrite it: `rebase eject` and
|
|
3330
|
+
* `rebase apps init --force`. A repository that had committed
|
|
3331
|
+
* `"telemetry": false` lost its opt-out, and a multi-bucket project lost its
|
|
3332
|
+
* whole `storage` block, in a commit whose stated change was `runtime: custom`.
|
|
3333
|
+
*
|
|
3334
|
+
* So: the caller's manifest wins for what it models, the file supplies the rest.
|
|
3335
|
+
* `storage` and `telemetry` fall back to the file because the callers that
|
|
3336
|
+
* synthesize a manifest (`apps init --force`) cannot know them — they are
|
|
3337
|
+
* authored, not inferred — and unknown top-level keys are copied verbatim
|
|
3338
|
+
* rather than listed, since a hand-listed set loses the next key too.
|
|
3339
|
+
*/
|
|
3001
3340
|
function writeManifest(projectRoot, manifest) {
|
|
3002
3341
|
const filePath = manifestPath(projectRoot);
|
|
3342
|
+
const existing = readManifestObject(filePath);
|
|
3343
|
+
const carried = {};
|
|
3344
|
+
for (const [key, value] of Object.entries(existing)) if (!MODELLED_MANIFEST_KEYS.includes(key)) carried[key] = value;
|
|
3345
|
+
const schema = manifest.$schema ?? (typeof existing.$schema === "string" ? existing.$schema : void 0) ?? "https://rebase.pro/schemas/rebase.json";
|
|
3346
|
+
const storage = manifest.storage ?? (isRecord(existing.storage) ? existing.storage : void 0);
|
|
3347
|
+
const telemetry = manifest.telemetry ?? (typeof existing.telemetry === "boolean" ? existing.telemetry : void 0);
|
|
3003
3348
|
const ordered = {
|
|
3004
|
-
$schema:
|
|
3349
|
+
$schema: schema,
|
|
3005
3350
|
rebase: manifest.rebase,
|
|
3006
|
-
apps: manifest.apps
|
|
3351
|
+
apps: manifest.apps,
|
|
3352
|
+
...storage ? { storage } : {},
|
|
3353
|
+
...telemetry !== void 0 ? { telemetry } : {},
|
|
3354
|
+
...carried
|
|
3007
3355
|
};
|
|
3008
3356
|
fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
|
|
3009
3357
|
return filePath;
|
|
@@ -3074,6 +3422,106 @@ function resolveBackendPaths(app, projectRoot) {
|
|
|
3074
3422
|
};
|
|
3075
3423
|
}
|
|
3076
3424
|
//#endregion
|
|
3425
|
+
//#region src/utils/collection-drift.ts
|
|
3426
|
+
/**
|
|
3427
|
+
* Which edits under `config/collections` can put the *SQL* schema out of date.
|
|
3428
|
+
*
|
|
3429
|
+
* `rebase dev` watches that directory and, on any change, either tells the
|
|
3430
|
+
* developer to run `rebase schema generate` / `rebase db push` or runs them.
|
|
3431
|
+
* The watcher is recursive and knows nothing about what it is watching, so it
|
|
3432
|
+
* said that about every file under the directory — including a
|
|
3433
|
+
* `collections/firestore/exercises.ts`, whose documents live in Firestore and
|
|
3434
|
+
* for which there is no Drizzle schema to regenerate and no database to push
|
|
3435
|
+
* to. Advice that is wrong on every edit is advice a developer learns to
|
|
3436
|
+
* ignore, which is worse than none: the same box is the only warning for the
|
|
3437
|
+
* Postgres collection next to it, where it is real.
|
|
3438
|
+
*
|
|
3439
|
+
* Two independent reasons a change cannot affect the SQL schema, both checked
|
|
3440
|
+
* here:
|
|
3441
|
+
*
|
|
3442
|
+
* 1. **The loader would never read the file.** `loadCollectionsFromDirectory`
|
|
3443
|
+
* reads the top level of the collections directory only — no recursion, no
|
|
3444
|
+
* `index`, no tests, no declarations. A file it does not read cannot change
|
|
3445
|
+
* what it returns, and the watcher must not claim otherwise.
|
|
3446
|
+
* 2. **Every collection in it is served by another engine.** A Firestore or
|
|
3447
|
+
* MongoDB collection has no table, no migration and no policies.
|
|
3448
|
+
*
|
|
3449
|
+
* The engine check reads the source text rather than importing the module: the
|
|
3450
|
+
* CLI runs as plain Node and cannot evaluate a project's TypeScript, and a
|
|
3451
|
+
* watcher must answer in the time between two keystrokes. Anything it cannot
|
|
3452
|
+
* read confidently counts as SQL-affecting — a spurious warning is a nuisance,
|
|
3453
|
+
* a suppressed one hides real drift.
|
|
3454
|
+
*/
|
|
3455
|
+
/**
|
|
3456
|
+
* Would `loadCollectionsFromDirectory` load this file?
|
|
3457
|
+
*
|
|
3458
|
+
* Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
|
|
3459
|
+
* scan. `relativePath` is relative to the collections directory, as `fs.watch`
|
|
3460
|
+
* reports it.
|
|
3461
|
+
*/
|
|
3462
|
+
function isLoadedCollectionFile(relativePath) {
|
|
3463
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
3464
|
+
if (normalized.includes("/")) return false;
|
|
3465
|
+
const file = normalized;
|
|
3466
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) return false;
|
|
3467
|
+
if (file.startsWith(".")) return false;
|
|
3468
|
+
if (file.includes(".test.")) return false;
|
|
3469
|
+
if (file.endsWith(".d.ts")) return false;
|
|
3470
|
+
if (file === "index.ts" || file === "index.js") return false;
|
|
3471
|
+
return true;
|
|
3472
|
+
}
|
|
3473
|
+
var ENGINE_LITERAL = /\bengine\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3474
|
+
var DATA_SOURCE_LITERAL = /\bdataSource\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
3475
|
+
/**
|
|
3476
|
+
* Drop comments, so a `// engine: "firestore"` in a docblock cannot silence a
|
|
3477
|
+
* warning about the Postgres collection the file actually declares.
|
|
3478
|
+
*
|
|
3479
|
+
* Deliberately naive — it does not understand strings, so a `//` inside one
|
|
3480
|
+
* (a URL in a default value) eats the rest of that line. That only ever removes
|
|
3481
|
+
* text, and removing an engine literal makes this file *more* likely to warn,
|
|
3482
|
+
* which is the side to be wrong on.
|
|
3483
|
+
*/
|
|
3484
|
+
function stripComments(source) {
|
|
3485
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
3486
|
+
}
|
|
3487
|
+
function literals(source, pattern) {
|
|
3488
|
+
const found = [];
|
|
3489
|
+
for (const match of source.matchAll(pattern)) found.push(match[1]);
|
|
3490
|
+
return found;
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Does this collection source declare anything a SQL toolchain would own?
|
|
3494
|
+
*
|
|
3495
|
+
* The fallback order matches `resolveDataSource`: an explicit `engine` wins,
|
|
3496
|
+
* and a collection that only names a `dataSource` is resolved as if the key
|
|
3497
|
+
* were the engine — which is what that function does when no registry is
|
|
3498
|
+
* available, and the CLI has none. An engine nobody recognises counts as
|
|
3499
|
+
* relational, for the reason `isRelationalCollection` gives.
|
|
3500
|
+
*
|
|
3501
|
+
* A file declaring several collections is SQL-affecting if *any* of them is.
|
|
3502
|
+
*/
|
|
3503
|
+
function declaresRelationalCollection(rawSource) {
|
|
3504
|
+
const source = stripComments(rawSource);
|
|
3505
|
+
const engines = literals(source, ENGINE_LITERAL);
|
|
3506
|
+
const declared = engines.length > 0 ? engines : literals(source, DATA_SOURCE_LITERAL).filter((key) => key !== DEFAULT_DATA_SOURCE_KEY);
|
|
3507
|
+
if (declared.length === 0) return true;
|
|
3508
|
+
return declared.some((engine) => getDataSourceCapabilities(engine).supportsRelations);
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Can this edit have changed the generated SQL schema?
|
|
3512
|
+
*
|
|
3513
|
+
* Answers `true` when it cannot tell — an unreadable file is a reason to warn,
|
|
3514
|
+
* not a reason to go quiet.
|
|
3515
|
+
*/
|
|
3516
|
+
function affectsSqlSchema(collectionsDir, relativePath) {
|
|
3517
|
+
if (!isLoadedCollectionFile(relativePath)) return false;
|
|
3518
|
+
try {
|
|
3519
|
+
return declaresRelationalCollection(fs.readFileSync(path.join(collectionsDir, relativePath), "utf8"));
|
|
3520
|
+
} catch {
|
|
3521
|
+
return true;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
//#endregion
|
|
3077
3525
|
//#region src/commands/dev.ts
|
|
3078
3526
|
/**
|
|
3079
3527
|
* CLI command: rebase dev
|
|
@@ -3167,38 +3615,67 @@ function getProjectPort(projectRoot) {
|
|
|
3167
3615
|
* 3. Previously used port from .rebase-dev-port (port affinity across restarts)
|
|
3168
3616
|
* 4. Deterministic hash from project path (unique per project)
|
|
3169
3617
|
*/
|
|
3618
|
+
/**
|
|
3619
|
+
* A TCP port, or `undefined` for anything that is not one.
|
|
3620
|
+
*
|
|
3621
|
+
* One predicate for both sources below. The port file was already checked for
|
|
3622
|
+
* range, and `PORT` — the source a human or a platform actually sets — was not,
|
|
3623
|
+
* so `PORT=oops` reached `parseInt` and was returned as `NaN`: the dev server
|
|
3624
|
+
* then bound to whatever the OS handed out and the CLI printed a URL for a port
|
|
3625
|
+
* nothing was listening on.
|
|
3626
|
+
*/
|
|
3627
|
+
function parsePort(raw) {
|
|
3628
|
+
if (raw === void 0) return void 0;
|
|
3629
|
+
const port = Number(raw.trim());
|
|
3630
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return void 0;
|
|
3631
|
+
return port;
|
|
3632
|
+
}
|
|
3170
3633
|
function resolveStartPort(projectRoot, explicitPort) {
|
|
3171
3634
|
if (explicitPort) return explicitPort;
|
|
3172
|
-
if (process.env.PORT)
|
|
3635
|
+
if (process.env.PORT) {
|
|
3636
|
+
const fromEnv = parsePort(process.env.PORT);
|
|
3637
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
3638
|
+
console.warn(chalk.yellow(` ⚠ Ignoring PORT="${process.env.PORT}" — not a port between 1 and 65535.`));
|
|
3639
|
+
}
|
|
3173
3640
|
try {
|
|
3174
3641
|
const portFile = path.join(projectRoot, DEV_PORT_FILENAME);
|
|
3175
3642
|
if (fs.existsSync(portFile)) {
|
|
3176
|
-
const saved =
|
|
3177
|
-
if (saved
|
|
3643
|
+
const saved = parsePort(fs.readFileSync(portFile, "utf-8"));
|
|
3644
|
+
if (saved !== void 0) return saved;
|
|
3178
3645
|
}
|
|
3179
3646
|
} catch {}
|
|
3180
3647
|
return getProjectPort(projectRoot);
|
|
3181
3648
|
}
|
|
3649
|
+
/**
|
|
3650
|
+
* The flags `rebase dev` takes.
|
|
3651
|
+
*
|
|
3652
|
+
* Exported so `dev.test.ts` can assert that every short alias the help
|
|
3653
|
+
* advertises is declared here: the help said `--port, -p` while the spec has
|
|
3654
|
+
* only ever declared `-P`, so `rebase dev -p 4000` typed straight off the help
|
|
3655
|
+
* page passed `4000` as a positional and started on the default port.
|
|
3656
|
+
*/
|
|
3657
|
+
var DEV_FLAGS = {
|
|
3658
|
+
"--backend-only": Boolean,
|
|
3659
|
+
"--frontend-only": Boolean,
|
|
3660
|
+
"--port": Number,
|
|
3661
|
+
"--generate": Boolean,
|
|
3662
|
+
"-b": "--backend-only",
|
|
3663
|
+
"-f": "--frontend-only",
|
|
3664
|
+
"-P": "--port",
|
|
3665
|
+
"-g": "--generate"
|
|
3666
|
+
};
|
|
3182
3667
|
async function devCommand(rawArgs) {
|
|
3183
|
-
|
|
3184
|
-
"--backend-only": Boolean,
|
|
3185
|
-
"--frontend-only": Boolean,
|
|
3186
|
-
"--port": Number,
|
|
3187
|
-
"--generate": Boolean,
|
|
3188
|
-
"--help": Boolean,
|
|
3189
|
-
"-b": "--backend-only",
|
|
3190
|
-
"-f": "--frontend-only",
|
|
3191
|
-
"-p": "--port",
|
|
3192
|
-
"-g": "--generate",
|
|
3193
|
-
"-h": "--help"
|
|
3194
|
-
}, {
|
|
3195
|
-
argv: rawArgs.slice(3),
|
|
3196
|
-
permissive: true
|
|
3197
|
-
});
|
|
3198
|
-
if (args["--help"]) {
|
|
3668
|
+
if (wantsHelp(rawArgs)) {
|
|
3199
3669
|
printDevHelp();
|
|
3200
3670
|
return;
|
|
3201
3671
|
}
|
|
3672
|
+
const { flags: args } = parseCommandArgs({
|
|
3673
|
+
spec: DEV_FLAGS,
|
|
3674
|
+
rawArgs,
|
|
3675
|
+
commandWords: 1,
|
|
3676
|
+
command: "dev",
|
|
3677
|
+
maxPositionals: 0
|
|
3678
|
+
});
|
|
3202
3679
|
const projectRoot = requireProjectRoot();
|
|
3203
3680
|
recordEvent("cli.dev", {
|
|
3204
3681
|
backend_only: Boolean(args["--backend-only"]),
|
|
@@ -3345,6 +3822,20 @@ async function devCommand(rawArgs) {
|
|
|
3345
3822
|
} catch {}
|
|
3346
3823
|
/** Whether the frontend has been launched (we only launch it once). */
|
|
3347
3824
|
let frontendLaunched = false;
|
|
3825
|
+
try {
|
|
3826
|
+
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3827
|
+
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3828
|
+
if (pluginCli) await execa(tsxBin, [
|
|
3829
|
+
pluginCli,
|
|
3830
|
+
"schema",
|
|
3831
|
+
"stale",
|
|
3832
|
+
"--fix"
|
|
3833
|
+
], {
|
|
3834
|
+
cwd: backendDir,
|
|
3835
|
+
stdio: "inherit",
|
|
3836
|
+
env
|
|
3837
|
+
});
|
|
3838
|
+
} catch {}
|
|
3348
3839
|
if (shouldGenerate) {
|
|
3349
3840
|
console.log(chalk.gray(" → Ensuring schema and SDK are generated on start..."));
|
|
3350
3841
|
try {
|
|
@@ -3372,14 +3863,18 @@ async function devCommand(rawArgs) {
|
|
|
3372
3863
|
const collectionsDir = path.join(projectRoot, "config", "collections");
|
|
3373
3864
|
if (fs.existsSync(collectionsDir)) {
|
|
3374
3865
|
let watchDebounce = null;
|
|
3866
|
+
let sqlSchemaAffected = false;
|
|
3375
3867
|
fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
|
|
3376
3868
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
3869
|
+
sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
|
|
3377
3870
|
if (watchDebounce) clearTimeout(watchDebounce);
|
|
3378
3871
|
watchDebounce = setTimeout(async () => {
|
|
3379
|
-
|
|
3872
|
+
const regenerateSchema = sqlSchemaAffected;
|
|
3873
|
+
sqlSchemaAffected = false;
|
|
3874
|
+
console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating ${regenerateSchema ? "schema & SDK" : "SDK"}...`));
|
|
3380
3875
|
try {
|
|
3381
3876
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
3382
|
-
const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3877
|
+
const pluginCli = regenerateSchema && activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
|
|
3383
3878
|
if (pluginCli) await execa(tsxBin, [
|
|
3384
3879
|
pluginCli,
|
|
3385
3880
|
"schema",
|
|
@@ -3395,7 +3890,7 @@ async function devCommand(rawArgs) {
|
|
|
3395
3890
|
stdio: "inherit",
|
|
3396
3891
|
env
|
|
3397
3892
|
});
|
|
3398
|
-
console.log(chalk.green(
|
|
3893
|
+
console.log(chalk.green(` ✓ ${regenerateSchema ? "Schema & SDK" : "SDK"} regenerated successfully. Hono will reload.`));
|
|
3399
3894
|
} catch (err) {
|
|
3400
3895
|
console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
|
|
3401
3896
|
}
|
|
@@ -3420,12 +3915,14 @@ async function devCommand(rawArgs) {
|
|
|
3420
3915
|
let driftDebounce = null;
|
|
3421
3916
|
fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
|
|
3422
3917
|
if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
|
|
3918
|
+
if (!affectsSqlSchema(collectionsDir, filename)) return;
|
|
3423
3919
|
if (driftDebounce) clearTimeout(driftDebounce);
|
|
3424
3920
|
driftDebounce = setTimeout(() => {
|
|
3921
|
+
const shown = filename.length > 31 ? `…${filename.slice(-30)}` : filename.padEnd(31);
|
|
3425
3922
|
console.log([
|
|
3426
3923
|
"",
|
|
3427
3924
|
chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
|
|
3428
|
-
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(
|
|
3925
|
+
chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(shown) + chalk.yellow("│"),
|
|
3429
3926
|
chalk.yellow(" │ │"),
|
|
3430
3927
|
chalk.yellow(" │ Your schema may be out of sync. Run: │"),
|
|
3431
3928
|
chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),
|
|
@@ -3520,7 +4017,7 @@ ${chalk.green.bold("Usage")}
|
|
|
3520
4017
|
${chalk.green.bold("Options")}
|
|
3521
4018
|
${chalk.blue("--backend-only, -b")} Only start the backend server
|
|
3522
4019
|
${chalk.blue("--frontend-only, -f")} Only start the frontend server
|
|
3523
|
-
${chalk.blue("--port, -
|
|
4020
|
+
${chalk.blue("--port, -P")} Backend port (default: auto-detected per project)
|
|
3524
4021
|
${chalk.blue("--generate, -g")} Enable automatic schema and SDK generation on startup and file changes
|
|
3525
4022
|
|
|
3526
4023
|
${chalk.green.bold("Description")}
|
|
@@ -4245,10 +4742,11 @@ async function regenerateSchema(projectRoot, configDir, options) {
|
|
|
4245
4742
|
* deployed green, and answered 404 on every one of them, with the file still
|
|
4246
4743
|
* sitting in the repository looking exactly like the server.
|
|
4247
4744
|
*
|
|
4248
|
-
* A project that means to
|
|
4249
|
-
* writes
|
|
4745
|
+
* A project that means to own its server process runs `rebase eject`, which
|
|
4746
|
+
* writes an entrypoint, a Dockerfile and a compose file together and flips the
|
|
4250
4747
|
* backend to `runtime: "custom"`. The warning names that route rather than
|
|
4251
|
-
* implying the file is a mistake
|
|
4748
|
+
* implying the file is a mistake — but eject writes *its* entrypoint, so the
|
|
4749
|
+
* warning must not read as "eject will keep what you wrote here".
|
|
4252
4750
|
*/
|
|
4253
4751
|
function findUnusedServerEntry(projectRoot, functionsDir) {
|
|
4254
4752
|
const found = [path.join("backend", "src", "index.ts"), path.join(path.dirname(functionsDir), "src", "index.ts")].find((candidate) => fs.existsSync(path.join(projectRoot, candidate)));
|
|
@@ -4282,7 +4780,9 @@ async function buildBundle(options) {
|
|
|
4282
4780
|
console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
|
|
4283
4781
|
console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
|
|
4284
4782
|
console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
|
|
4285
|
-
console.log(chalk.dim(" or run `rebase eject
|
|
4783
|
+
console.log(chalk.dim(" or run `rebase eject`, which writes an entrypoint of its own and owns"));
|
|
4784
|
+
console.log(chalk.dim(" the image — it does not adopt this file, and will not replace it"));
|
|
4785
|
+
console.log(chalk.dim(" without --force."));
|
|
4286
4786
|
}
|
|
4287
4787
|
log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
|
|
4288
4788
|
cleanOutDir(projectRoot, outDir);
|
|
@@ -4651,6 +5151,61 @@ function assertBuiltForPath(indexHtml, basePath, appName) {
|
|
|
4651
5151
|
build config — see docs/apps-and-runtimes.md §4.2.`);
|
|
4652
5152
|
}
|
|
4653
5153
|
/**
|
|
5154
|
+
* The environment every static app is built with, wherever that build is driven from.
|
|
5155
|
+
*
|
|
5156
|
+
* Shared because there are two drivers — `foldFrontendIntoBundle` here, and
|
|
5157
|
+
* `buildAssetApp` in `build.ts` for a standalone `type: "static"` app — and they
|
|
5158
|
+
* had already drifted: the path variables were duplicated into both, so a fix
|
|
5159
|
+
* applied to one shipped a bundle built the old way from the other. One function
|
|
5160
|
+
* makes that impossible rather than merely unlikely.
|
|
5161
|
+
*
|
|
5162
|
+
* ## REBASE_APP_*
|
|
5163
|
+
*
|
|
5164
|
+
* The declared path is a build-time input, not only a serving concern: Vite
|
|
5165
|
+
* reads `base` from REBASE_APP_BASE, and the trailing slash is that field's
|
|
5166
|
+
* convention. See `assertBuiltForPath`.
|
|
5167
|
+
*
|
|
5168
|
+
* ## NODE_ENV
|
|
5169
|
+
*
|
|
5170
|
+
* A built app is a production artifact by construction, so build it as one. Not
|
|
5171
|
+
* a formality: the scaffold's `.env` carries `NODE_ENV=development` for the dev
|
|
5172
|
+
* backend, and Vite's `loadEnv` promotes a `NODE_ENV` found in an env file into
|
|
5173
|
+
* the build unless the environment already sets one. So `rebase build` and
|
|
5174
|
+
* `rebase cloud deploy` shipped a *development* bundle — `import.meta.env.DEV
|
|
5175
|
+
* === true`, development React, dev-only branches live — from commands whose
|
|
5176
|
+
* whole purpose is to produce something deployable. Setting it here is what
|
|
5177
|
+
* closes it: Vite consults the env file's NODE_ENV only when `process.env`
|
|
5178
|
+
* has none.
|
|
5179
|
+
*
|
|
5180
|
+
* ## VITE_API_URL
|
|
5181
|
+
*
|
|
5182
|
+
* An app served by the backend it talks to has its API on its own origin by
|
|
5183
|
+
* construction, so a baked-in absolute URL can only be wrong. It was: that same
|
|
5184
|
+
* `.env` carries `VITE_API_URL=http://localhost:3001` and
|
|
5185
|
+
* `frontend/vite.config.ts` reads the project root via `envDir: ".."`, so a
|
|
5186
|
+
* stock deploy shipped a site whose every request went to whoever ran the build
|
|
5187
|
+
* — passing every server-side health check on the way out. Blanking it here
|
|
5188
|
+
* fixes the bundle even for a project whose `.env` predates the `init` fix, or
|
|
5189
|
+
* was written by hand. Empty is the right value rather than a missing one: the
|
|
5190
|
+
* client falls back to `window.location.origin`, which keeps working when a
|
|
5191
|
+
* custom domain is added.
|
|
5192
|
+
*
|
|
5193
|
+
* Vite prioritises `process.env.VITE_*` over `.env` files, so an explicit
|
|
5194
|
+
* `VITE_API_URL=https://api.example.com rebase cloud deploy` still wins — the
|
|
5195
|
+
* cross-origin escape hatch stays open, it just has to be deliberate. Nothing on
|
|
5196
|
+
* this path loads the project `.env` into `process.env`, so a value inherited
|
|
5197
|
+
* here really was set by the caller.
|
|
5198
|
+
*/
|
|
5199
|
+
function staticBuildEnv(appPath, appName) {
|
|
5200
|
+
return {
|
|
5201
|
+
REBASE_APP_PATH: appPath,
|
|
5202
|
+
REBASE_APP_BASE: appPath === "/" ? "/" : `${appPath}/`,
|
|
5203
|
+
REBASE_APP_NAME: appName,
|
|
5204
|
+
NODE_ENV: "production",
|
|
5205
|
+
VITE_API_URL: process.env.VITE_API_URL ?? ""
|
|
5206
|
+
};
|
|
5207
|
+
}
|
|
5208
|
+
/**
|
|
4654
5209
|
* Build the project's static apps and fold them into the backend bundle.
|
|
4655
5210
|
*
|
|
4656
5211
|
* Throws rather than exiting, so the caller decides whether a missing frontend
|
|
@@ -4669,11 +5224,7 @@ async function foldFrontendIntoBundle(options) {
|
|
|
4669
5224
|
cwd: projectRoot,
|
|
4670
5225
|
stdio: "inherit",
|
|
4671
5226
|
shell: true,
|
|
4672
|
-
env:
|
|
4673
|
-
REBASE_APP_PATH: app.path,
|
|
4674
|
-
REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
|
|
4675
|
-
REBASE_APP_NAME: app.name
|
|
4676
|
-
}
|
|
5227
|
+
env: staticBuildEnv(app.path, app.name)
|
|
4677
5228
|
});
|
|
4678
5229
|
const assetsDir = path.join(projectRoot, app.output);
|
|
4679
5230
|
if (!fs.existsSync(assetsDir)) throw new Error(`"${app.name}" declared output "${app.output}" does not exist after building — the bundle would ship without a frontend.`);
|
|
@@ -4731,23 +5282,24 @@ ${chalk.bold("Examples")}
|
|
|
4731
5282
|
`.trim());
|
|
4732
5283
|
}
|
|
4733
5284
|
async function buildCommand(rawArgs = []) {
|
|
4734
|
-
|
|
4735
|
-
"--out": String,
|
|
4736
|
-
"--skip-type-check": Boolean,
|
|
4737
|
-
"--skip-schema": Boolean,
|
|
4738
|
-
"--no-static": Boolean,
|
|
4739
|
-
"--skip-static-build": Boolean,
|
|
4740
|
-
"--legacy": Boolean,
|
|
4741
|
-
"--help": Boolean,
|
|
4742
|
-
"-h": "--help"
|
|
4743
|
-
}, {
|
|
4744
|
-
argv: rawArgs.slice(3),
|
|
4745
|
-
permissive: true
|
|
4746
|
-
});
|
|
4747
|
-
if (args["--help"]) {
|
|
5285
|
+
if (wantsHelp(rawArgs)) {
|
|
4748
5286
|
printHelp$5();
|
|
4749
5287
|
return;
|
|
4750
5288
|
}
|
|
5289
|
+
const { flags: args, positionals: requested } = parseCommandArgs({
|
|
5290
|
+
spec: {
|
|
5291
|
+
"--output": String,
|
|
5292
|
+
"--out": "--output",
|
|
5293
|
+
"--skip-type-check": Boolean,
|
|
5294
|
+
"--skip-schema": Boolean,
|
|
5295
|
+
"--no-static": Boolean,
|
|
5296
|
+
"--skip-static-build": Boolean,
|
|
5297
|
+
"--legacy": Boolean
|
|
5298
|
+
},
|
|
5299
|
+
rawArgs,
|
|
5300
|
+
commandWords: 1,
|
|
5301
|
+
command: "build"
|
|
5302
|
+
});
|
|
4751
5303
|
const projectRoot = requireProjectRoot();
|
|
4752
5304
|
if (args["--legacy"]) {
|
|
4753
5305
|
await runWorkspaceBuilds(projectRoot);
|
|
@@ -4765,7 +5317,6 @@ async function buildCommand(rawArgs = []) {
|
|
|
4765
5317
|
throw err;
|
|
4766
5318
|
}
|
|
4767
5319
|
const { manifest, source } = loaded;
|
|
4768
|
-
const requested = args._.filter((a) => !a.startsWith("-"));
|
|
4769
5320
|
let targets = buildableApps(manifest);
|
|
4770
5321
|
if (requested.length > 0) {
|
|
4771
5322
|
const known = new Set(targets.map((t) => t.name));
|
|
@@ -4800,7 +5351,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4800
5351
|
projectRoot,
|
|
4801
5352
|
appName: name,
|
|
4802
5353
|
app,
|
|
4803
|
-
outDir: args["--
|
|
5354
|
+
outDir: args["--output"],
|
|
4804
5355
|
runtimeRange: manifest.rebase,
|
|
4805
5356
|
storage: manifest.storage,
|
|
4806
5357
|
skipTypeCheck: args["--skip-type-check"],
|
|
@@ -4837,7 +5388,7 @@ async function buildCommand(rawArgs = []) {
|
|
|
4837
5388
|
});
|
|
4838
5389
|
for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
|
|
4839
5390
|
}
|
|
4840
|
-
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--
|
|
5391
|
+
} else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--output"]);
|
|
4841
5392
|
console.log("");
|
|
4842
5393
|
}
|
|
4843
5394
|
console.log(chalk.green("✓ Build complete."));
|
|
@@ -4862,11 +5413,7 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
4862
5413
|
cwd: projectRoot,
|
|
4863
5414
|
stdio: "inherit",
|
|
4864
5415
|
shell: true,
|
|
4865
|
-
env:
|
|
4866
|
-
REBASE_APP_PATH: basePath,
|
|
4867
|
-
REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
|
|
4868
|
-
REBASE_APP_NAME: name
|
|
4869
|
-
}
|
|
5416
|
+
env: staticBuildEnv(basePath, name)
|
|
4870
5417
|
});
|
|
4871
5418
|
} catch {
|
|
4872
5419
|
console.error(chalk.red(` ✗ build command failed for "${name}"`));
|
|
@@ -4948,6 +5495,56 @@ function findCliRoot(from) {
|
|
|
4948
5495
|
}
|
|
4949
5496
|
return null;
|
|
4950
5497
|
}
|
|
5498
|
+
/** The block names the payload may switch on. A typo has to be an error. */
|
|
5499
|
+
var SHAPE_FLAGS = ["collections", "frontend"];
|
|
5500
|
+
/**
|
|
5501
|
+
* Render one payload file for this project.
|
|
5502
|
+
*
|
|
5503
|
+
* The payload is one set of files rather than one per flavour, because the
|
|
5504
|
+
* flavours differ by about a dozen lines and two copies of a 230-line
|
|
5505
|
+
* entrypoint are how copies drift — the payload had already drifted from
|
|
5506
|
+
* `app/backend/src/index.ts` over `cronsDir`, which silently stopped every cron
|
|
5507
|
+
* job in an ejected project. So both branches live in the file, marked with
|
|
5508
|
+
* lines that are comments in TypeScript, YAML and Dockerfiles alike:
|
|
5509
|
+
*
|
|
5510
|
+
* // {{#collections}}
|
|
5511
|
+
* import { tables } from "./schema.generated.js";
|
|
5512
|
+
* // {{/collections}}
|
|
5513
|
+
* // {{^collections}}
|
|
5514
|
+
* // No schema module: this project introspects the database.
|
|
5515
|
+
* // {{/collections}}
|
|
5516
|
+
*
|
|
5517
|
+
* `{{#name}}` keeps its block when the flag is on, `{{^name}}` when it is off,
|
|
5518
|
+
* and the marker lines themselves never reach the user. The template stays
|
|
5519
|
+
* valid TypeScript with every marker line removed, which is the flavour a
|
|
5520
|
+
* typechecker would see.
|
|
5521
|
+
*/
|
|
5522
|
+
function renderPayload(contents, shape, projectName) {
|
|
5523
|
+
const flags = shape;
|
|
5524
|
+
const out = [];
|
|
5525
|
+
let open = null;
|
|
5526
|
+
for (const line of contents.split("\n")) {
|
|
5527
|
+
const marker = /^\s*(?:\/\/|#)\s*\{\{([#^/])([A-Za-z]+)\}\}\s*$/.exec(line);
|
|
5528
|
+
if (!marker) {
|
|
5529
|
+
if (!open || open.keep) out.push(line);
|
|
5530
|
+
continue;
|
|
5531
|
+
}
|
|
5532
|
+
const [, kind, name] = marker;
|
|
5533
|
+
if (kind === "/") {
|
|
5534
|
+
if (!open || open.name !== name) throw new Error(`Eject template: {{/${name}}} does not close an open block.`);
|
|
5535
|
+
open = null;
|
|
5536
|
+
continue;
|
|
5537
|
+
}
|
|
5538
|
+
if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
|
|
5539
|
+
if (!SHAPE_FLAGS.includes(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
|
|
5540
|
+
open = {
|
|
5541
|
+
name,
|
|
5542
|
+
keep: kind === "#" ? flags[name] === true : flags[name] !== true
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
|
|
5546
|
+
return out.join("\n").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
5547
|
+
}
|
|
4951
5548
|
/** Files the eject payload contributes, as `<source> → <destination>`. */
|
|
4952
5549
|
var PAYLOAD = [
|
|
4953
5550
|
{
|
|
@@ -4998,25 +5595,30 @@ ${chalk.bold("Usage")}
|
|
|
4998
5595
|
|
|
4999
5596
|
${chalk.bold("Options")}
|
|
5000
5597
|
--dry-run List what would change, and change nothing
|
|
5598
|
+
--force Replace an existing backend/src/index.ts or
|
|
5599
|
+
env.ts, keeping the current file as <name>.bak
|
|
5001
5600
|
-h, --help Show this help
|
|
5002
5601
|
`.trim());
|
|
5003
5602
|
}
|
|
5004
5603
|
async function ejectCommand(rawArgs = []) {
|
|
5005
|
-
|
|
5006
|
-
"--dry-run": Boolean,
|
|
5007
|
-
"--help": Boolean,
|
|
5008
|
-
"-h": "--help"
|
|
5009
|
-
}, {
|
|
5010
|
-
argv: rawArgs.slice(2),
|
|
5011
|
-
permissive: true
|
|
5012
|
-
});
|
|
5013
|
-
if (args["--help"]) {
|
|
5604
|
+
if (wantsHelp(rawArgs)) {
|
|
5014
5605
|
printHelp$4();
|
|
5015
5606
|
return;
|
|
5016
5607
|
}
|
|
5608
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
5609
|
+
spec: {
|
|
5610
|
+
"--dry-run": Boolean,
|
|
5611
|
+
"--force": Boolean
|
|
5612
|
+
},
|
|
5613
|
+
rawArgs,
|
|
5614
|
+
commandWords: 1,
|
|
5615
|
+
command: "eject",
|
|
5616
|
+
maxPositionals: 1
|
|
5617
|
+
});
|
|
5017
5618
|
const projectRoot = requireProjectRoot();
|
|
5018
5619
|
const dryRun = Boolean(args["--dry-run"]);
|
|
5019
|
-
const
|
|
5620
|
+
const force = Boolean(args["--force"]);
|
|
5621
|
+
const requested = positionals[0];
|
|
5020
5622
|
let loaded;
|
|
5021
5623
|
try {
|
|
5022
5624
|
loaded = loadManifest(projectRoot);
|
|
@@ -5065,7 +5667,12 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5065
5667
|
process.exit(1);
|
|
5066
5668
|
}
|
|
5067
5669
|
const payloadDir = path.join(cliRoot, "templates", "eject");
|
|
5670
|
+
const shape = {
|
|
5671
|
+
collections: resolveBackendPaths(app, projectRoot).hasCollections,
|
|
5672
|
+
frontend: fs.existsSync(path.join(projectRoot, "frontend"))
|
|
5673
|
+
};
|
|
5068
5674
|
const planned = [];
|
|
5675
|
+
const blocked = [];
|
|
5069
5676
|
for (const file of PAYLOAD) {
|
|
5070
5677
|
const source = path.join(payloadDir, file.from);
|
|
5071
5678
|
if (!fs.existsSync(source)) {
|
|
@@ -5073,26 +5680,43 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5073
5680
|
process.exit(1);
|
|
5074
5681
|
}
|
|
5075
5682
|
const exists = fs.existsSync(path.join(projectRoot, file.to));
|
|
5683
|
+
if (exists && file.overwrite && !force) blocked.push(file.to);
|
|
5076
5684
|
planned.push({
|
|
5077
5685
|
to: file.to,
|
|
5078
|
-
action: exists
|
|
5686
|
+
action: !exists ? "write" : file.overwrite ? "overwrite" : "keep"
|
|
5079
5687
|
});
|
|
5080
5688
|
}
|
|
5689
|
+
if (blocked.length > 0) {
|
|
5690
|
+
console.error(chalk.red("✗ Ejecting would replace a file this project already has:"));
|
|
5691
|
+
for (const item of blocked) console.error(chalk.red(` ${item}`));
|
|
5692
|
+
console.error(chalk.dim(" Eject writes its own entrypoint — it does not adopt yours."));
|
|
5693
|
+
console.error(chalk.dim(" Move the file aside, or re-run with --force, which keeps the current"));
|
|
5694
|
+
console.error(chalk.dim(" contents as <name>.bak."));
|
|
5695
|
+
process.exit(1);
|
|
5696
|
+
}
|
|
5081
5697
|
if (dryRun) {
|
|
5082
5698
|
console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
|
|
5083
5699
|
console.log("");
|
|
5084
|
-
for (const item of planned)
|
|
5085
|
-
console.log(` ${chalk.
|
|
5700
|
+
for (const item of planned) if (item.action === "write") console.log(` ${chalk.green("write")} ${item.to}`);
|
|
5701
|
+
else if (item.action === "overwrite") console.log(` ${chalk.yellow("overwrite")} ${item.to} ${chalk.dim(`(kept as ${item.to}.bak)`)}`);
|
|
5702
|
+
else console.log(` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
|
|
5703
|
+
console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
|
|
5704
|
+
if (fs.existsSync(path.join(projectRoot, "backend", "package.json"))) console.log(` ${chalk.green("write")} backend/package.json ${chalk.dim("(main, dev and start scripts)")}`);
|
|
5086
5705
|
console.log("");
|
|
5087
5706
|
console.log(chalk.dim("Nothing was changed."));
|
|
5088
5707
|
return;
|
|
5089
5708
|
}
|
|
5090
5709
|
const projectName = projectNameOf(projectRoot);
|
|
5710
|
+
const backups = [];
|
|
5091
5711
|
for (const [index, file] of PAYLOAD.entries()) {
|
|
5092
5712
|
if (planned[index].action === "keep") continue;
|
|
5093
5713
|
const destination = path.join(projectRoot, file.to);
|
|
5714
|
+
if (planned[index].action === "overwrite") {
|
|
5715
|
+
fs.copyFileSync(destination, `${destination}.bak`);
|
|
5716
|
+
backups.push(`${file.to}.bak`);
|
|
5717
|
+
}
|
|
5094
5718
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
5095
|
-
const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8")
|
|
5719
|
+
const contents = renderPayload(fs.readFileSync(path.join(payloadDir, file.from), "utf8"), shape, projectName);
|
|
5096
5720
|
fs.writeFileSync(destination, contents, "utf8");
|
|
5097
5721
|
}
|
|
5098
5722
|
const dockerfile = app.dockerfile ?? "Dockerfile";
|
|
@@ -5112,9 +5736,14 @@ async function ejectCommand(rawArgs = []) {
|
|
|
5112
5736
|
console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
|
|
5113
5737
|
console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
|
|
5114
5738
|
console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
|
|
5739
|
+
for (const backup of backups) console.log(` ${chalk.cyan(backup.padEnd(26))} what was there before`);
|
|
5115
5740
|
console.log("");
|
|
5116
5741
|
console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
|
|
5117
5742
|
console.log(chalk.yellow(" upgrades no longer reach this project."));
|
|
5743
|
+
if (!shape.collections) {
|
|
5744
|
+
console.log(chalk.yellow(" This project declares no collections, so the entrypoint derives them"));
|
|
5745
|
+
console.log(chalk.yellow(" from the live database, as the managed runtime did."));
|
|
5746
|
+
}
|
|
5118
5747
|
console.log("");
|
|
5119
5748
|
console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
|
|
5120
5749
|
console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
|
|
@@ -5171,19 +5800,20 @@ Build first with ${chalk.cyan("rebase build")}.
|
|
|
5171
5800
|
`.trim());
|
|
5172
5801
|
}
|
|
5173
5802
|
async function startCommand(rawArgs = []) {
|
|
5174
|
-
|
|
5175
|
-
"--bundle": String,
|
|
5176
|
-
"--legacy": Boolean,
|
|
5177
|
-
"--help": Boolean,
|
|
5178
|
-
"-h": "--help"
|
|
5179
|
-
}, {
|
|
5180
|
-
argv: rawArgs.slice(3),
|
|
5181
|
-
permissive: true
|
|
5182
|
-
});
|
|
5183
|
-
if (args["--help"]) {
|
|
5803
|
+
if (wantsHelp(rawArgs)) {
|
|
5184
5804
|
printHelp$3();
|
|
5185
5805
|
return;
|
|
5186
5806
|
}
|
|
5807
|
+
const { flags: args } = parseCommandArgs({
|
|
5808
|
+
spec: {
|
|
5809
|
+
"--bundle": String,
|
|
5810
|
+
"--legacy": Boolean
|
|
5811
|
+
},
|
|
5812
|
+
rawArgs,
|
|
5813
|
+
commandWords: 1,
|
|
5814
|
+
command: "start",
|
|
5815
|
+
maxPositionals: 0
|
|
5816
|
+
});
|
|
5187
5817
|
const projectRoot = requireProjectRoot();
|
|
5188
5818
|
const envFile = findEnvFile(projectRoot);
|
|
5189
5819
|
const env = { ...process.env };
|
|
@@ -5286,8 +5916,50 @@ async function startWorkspaceBackend(projectRoot, env) {
|
|
|
5286
5916
|
* Subcommands:
|
|
5287
5917
|
* reset-password — Reset a user's password
|
|
5288
5918
|
*/
|
|
5919
|
+
/**
|
|
5920
|
+
* Pick the user with exactly this email out of a search response.
|
|
5921
|
+
*
|
|
5922
|
+
* `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display
|
|
5923
|
+
* name**, ordered by role count descending. This used to take row `[0]` and
|
|
5924
|
+
* reset it, then print the email it had been *given* as confirmation — so two
|
|
5925
|
+
* ordinary situations ended in a successful-looking reset of somebody else's
|
|
5926
|
+
* account:
|
|
5927
|
+
*
|
|
5928
|
+
* - a substring collision: `bob@example.com` also matches
|
|
5929
|
+
* `robert.bob@example.com`;
|
|
5930
|
+
* - a display name, which is user-controlled and accepted up to 255
|
|
5931
|
+
* characters with no constraint on its content, containing an address
|
|
5932
|
+
* belonging to someone else.
|
|
5933
|
+
*
|
|
5934
|
+
* The ordering makes it worse rather than better — `array_length(roles) DESC
|
|
5935
|
+
* NULLS LAST` puts the most privileged match first, so the account most likely
|
|
5936
|
+
* to be reset by mistake is an admin's.
|
|
5937
|
+
*
|
|
5938
|
+
* Returns `undefined` when nothing matched exactly, which the caller reports
|
|
5939
|
+
* rather than falling through to a guess. The direct-database fallback below
|
|
5940
|
+
* has always matched with `eq(usersTable.email, email)`; this is the same
|
|
5941
|
+
* definition, so the command no longer resets different accounts depending on
|
|
5942
|
+
* whether the backend happened to be running.
|
|
5943
|
+
*/
|
|
5944
|
+
function selectUserForEmail(payload, email) {
|
|
5945
|
+
const wanted = email.trim().toLowerCase();
|
|
5946
|
+
if (!wanted) return void 0;
|
|
5947
|
+
const rows = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.users) ? payload.users : [];
|
|
5948
|
+
for (const row of rows) {
|
|
5949
|
+
if (!row || typeof row !== "object") continue;
|
|
5950
|
+
const record = row;
|
|
5951
|
+
const rowEmail = typeof record.email === "string" ? record.email.trim().toLowerCase() : void 0;
|
|
5952
|
+
if (!rowEmail || rowEmail !== wanted) continue;
|
|
5953
|
+
const id = typeof record.id === "string" ? record.id : typeof record.uid === "string" ? record.uid : void 0;
|
|
5954
|
+
if (!id) continue;
|
|
5955
|
+
return {
|
|
5956
|
+
id,
|
|
5957
|
+
email: record.email
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5289
5961
|
async function authCommand(subcommand, rawArgs) {
|
|
5290
|
-
if (!subcommand || subcommand === "--help") {
|
|
5962
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5291
5963
|
printAuthHelp();
|
|
5292
5964
|
return;
|
|
5293
5965
|
}
|
|
@@ -5302,18 +5974,48 @@ async function authCommand(subcommand, rawArgs) {
|
|
|
5302
5974
|
process.exit(1);
|
|
5303
5975
|
}
|
|
5304
5976
|
}
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5977
|
+
/**
|
|
5978
|
+
* The flags `rebase auth reset-password` takes.
|
|
5979
|
+
*
|
|
5980
|
+
* `-p` was advertised in this command's own help and never declared here, so
|
|
5981
|
+
* `arg` — running permissively — pushed it into the positionals and the value
|
|
5982
|
+
* *after* it shifted out of reach: anyone following the help set the account's
|
|
5983
|
+
* password to the two-character string `-p`. Declared now, and `auth.test.ts`
|
|
5984
|
+
* asserts that the help and this spec list the same aliases.
|
|
5985
|
+
*/
|
|
5986
|
+
var RESET_PASSWORD_FLAGS = {
|
|
5987
|
+
"--email": String,
|
|
5988
|
+
"--password": String,
|
|
5989
|
+
"-e": "--email",
|
|
5990
|
+
"-p": "--password"
|
|
5991
|
+
};
|
|
5992
|
+
/**
|
|
5993
|
+
* Which account, and which password, this invocation names.
|
|
5994
|
+
*
|
|
5995
|
+
* Both may still be absent — the caller reports a missing email — but neither
|
|
5996
|
+
* can be a flag. `parseCommandArgs` parses the whole line strictly, so an
|
|
5997
|
+
* undeclared flag is an error rather than a positional. That is what stops
|
|
5998
|
+
* `rebase auth reset-password bob@example.com --debug` from setting Bob's
|
|
5999
|
+
* password to `--debug`, which is the flag the CLI itself prints after every
|
|
6000
|
+
* failure as the thing to re-run with.
|
|
6001
|
+
*
|
|
6002
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6003
|
+
*/
|
|
6004
|
+
function resolveResetPasswordArgs(rawArgs) {
|
|
6005
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6006
|
+
spec: RESET_PASSWORD_FLAGS,
|
|
6007
|
+
rawArgs,
|
|
6008
|
+
commandWords: 2,
|
|
6009
|
+
command: "auth reset-password",
|
|
6010
|
+
maxPositionals: 2
|
|
5314
6011
|
});
|
|
5315
|
-
|
|
5316
|
-
|
|
6012
|
+
return {
|
|
6013
|
+
email: flags["--email"] || positionals[0],
|
|
6014
|
+
password: flags["--password"] || positionals[1]
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
async function resetPassword(rawArgs) {
|
|
6018
|
+
const { email, password: newPassword } = resolveResetPasswordArgs(rawArgs);
|
|
5317
6019
|
if (!email) {
|
|
5318
6020
|
console.error(chalk.red("✗ Email is required."));
|
|
5319
6021
|
console.log("");
|
|
@@ -5322,12 +6024,8 @@ async function resetPassword(rawArgs) {
|
|
|
5322
6024
|
process.exit(1);
|
|
5323
6025
|
}
|
|
5324
6026
|
const projectRoot = requireProjectRoot();
|
|
5325
|
-
let envServiceKey;
|
|
5326
6027
|
const envFile = findEnvFile(projectRoot);
|
|
5327
|
-
|
|
5328
|
-
const match = fs.readFileSync(envFile, "utf8").match(/^\s*REBASE_SERVICE_KEY\s*=\s*['"]?(.*?)['"]?\s*$/m);
|
|
5329
|
-
if (match && match[1]) envServiceKey = match[1];
|
|
5330
|
-
} catch {}
|
|
6028
|
+
const envServiceKey = readEnvFile(projectRoot).REBASE_SERVICE_KEY;
|
|
5331
6029
|
let baseUrl = process.env.REBASE_BASE_URL;
|
|
5332
6030
|
let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;
|
|
5333
6031
|
const statePath = path.join(projectRoot, ".rebase", "state.json");
|
|
@@ -5347,7 +6045,7 @@ async function resetPassword(rawArgs) {
|
|
|
5347
6045
|
try {
|
|
5348
6046
|
const finalPass = newPassword || "NewPassword123!";
|
|
5349
6047
|
const cleanBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
5350
|
-
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=
|
|
6048
|
+
const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;
|
|
5351
6049
|
const searchRes = await fetch(searchUrl, { headers: {
|
|
5352
6050
|
"Authorization": `Bearer ${serviceKey}`,
|
|
5353
6051
|
"Accept": "application/json"
|
|
@@ -5355,18 +6053,9 @@ async function resetPassword(rawArgs) {
|
|
|
5355
6053
|
if (!searchRes.ok) throw new Error(`Failed to list users: ${searchRes.statusText}`);
|
|
5356
6054
|
const searchData = await searchRes.json();
|
|
5357
6055
|
if (!searchData || typeof searchData !== "object") throw new Error("Invalid response format from user search API.");
|
|
5358
|
-
|
|
5359
|
-
if (
|
|
5360
|
-
|
|
5361
|
-
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
5362
|
-
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
5363
|
-
} else if ("users" in searchData && Array.isArray(searchData.users)) {
|
|
5364
|
-
const firstUser = searchData.users[0];
|
|
5365
|
-
if (firstUser && typeof firstUser === "object" && "id" in firstUser && typeof firstUser.id === "string") userId = firstUser.id;
|
|
5366
|
-
else if (firstUser && typeof firstUser === "object" && "uid" in firstUser && typeof firstUser.uid === "string") userId = firstUser.uid;
|
|
5367
|
-
}
|
|
5368
|
-
if (!userId) throw new Error(`User not found with email: ${email}`);
|
|
5369
|
-
const resetUrl = `${cleanBaseUrl}/api/admin/users/${userId}/reset-password`;
|
|
6056
|
+
const matched = selectUserForEmail(searchData, email);
|
|
6057
|
+
if (!matched) throw new Error(`No user has the email ${email}.`);
|
|
6058
|
+
const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;
|
|
5370
6059
|
const resetRes = await fetch(resetUrl, {
|
|
5371
6060
|
method: "POST",
|
|
5372
6061
|
headers: {
|
|
@@ -5383,7 +6072,7 @@ async function resetPassword(rawArgs) {
|
|
|
5383
6072
|
console.log("API reset successful.");
|
|
5384
6073
|
console.log(chalk.bold(" 🔑 Rebase Auth — Reset Password (via API)"));
|
|
5385
6074
|
console.log("");
|
|
5386
|
-
console.log(` ${chalk.gray("Email:")} ${email}`);
|
|
6075
|
+
console.log(` ${chalk.gray("Email:")} ${matched.email}`);
|
|
5387
6076
|
console.log(` ${chalk.gray("Password:")} ${finalPass}`);
|
|
5388
6077
|
console.log("");
|
|
5389
6078
|
return;
|
|
@@ -5451,10 +6140,12 @@ async function resetPassword() {
|
|
|
5451
6140
|
if (result.length > 0) {
|
|
5452
6141
|
console.log("✅ Password reset for: " + result[0].email);
|
|
5453
6142
|
${!newPassword ? "console.log(\" New password: \" + newPassword);" : ""}
|
|
5454
|
-
|
|
5455
|
-
console.log("✗ User not found: " + email);
|
|
6143
|
+
process.exit(0);
|
|
5456
6144
|
}
|
|
5457
|
-
|
|
6145
|
+
// Nothing was updated, so nothing was reset. Exiting 0 here reported
|
|
6146
|
+
// success for a no-op, which is what a script would have believed.
|
|
6147
|
+
console.error("✗ User not found: " + email);
|
|
6148
|
+
process.exit(1);
|
|
5458
6149
|
}
|
|
5459
6150
|
|
|
5460
6151
|
resetPassword().catch(console.error);
|
|
@@ -5472,11 +6163,20 @@ resetPassword().catch(console.error);
|
|
|
5472
6163
|
stdio: "inherit",
|
|
5473
6164
|
env
|
|
5474
6165
|
});
|
|
6166
|
+
const cleanup = () => {
|
|
6167
|
+
try {
|
|
6168
|
+
fs.unlinkSync(tmpScriptPath);
|
|
6169
|
+
} catch {}
|
|
6170
|
+
};
|
|
5475
6171
|
return new Promise((resolve) => {
|
|
6172
|
+
child.on("error", (err) => {
|
|
6173
|
+
cleanup();
|
|
6174
|
+
console.error(chalk.red("✗ Could not run the reset script."));
|
|
6175
|
+
console.error(chalk.gray(` ${err.message}`));
|
|
6176
|
+
process.exit(1);
|
|
6177
|
+
});
|
|
5476
6178
|
child.on("close", (code) => {
|
|
5477
|
-
|
|
5478
|
-
fs.unlinkSync(tmpScriptPath);
|
|
5479
|
-
} catch {}
|
|
6179
|
+
cleanup();
|
|
5480
6180
|
if (code !== 0) process.exit(code ?? 1);
|
|
5481
6181
|
resolve();
|
|
5482
6182
|
});
|
|
@@ -5514,7 +6214,34 @@ ${chalk.green.bold("Examples")}
|
|
|
5514
6214
|
* Detects three-way schema drift between collection definitions,
|
|
5515
6215
|
* the generated Drizzle schema, and the live PostgreSQL database.
|
|
5516
6216
|
*/
|
|
6217
|
+
/**
|
|
6218
|
+
* `--help` is answered before the project guard, not after.
|
|
6219
|
+
*
|
|
6220
|
+
* `doctor` declared no `--help` at all, so the flag fell through to the command
|
|
6221
|
+
* body and hit `requireProjectRoot()` — and `rebase doctor --help` outside a
|
|
6222
|
+
* project answered "✗ Could not find a Rebase project root." Asking a command
|
|
6223
|
+
* what it does is the one question that cannot require being somewhere
|
|
6224
|
+
* particular to ask.
|
|
6225
|
+
*/
|
|
6226
|
+
function printDoctorHelp() {
|
|
6227
|
+
console.log(`
|
|
6228
|
+
${chalk.bold("rebase doctor")} — Detect drift between collections, schema and database
|
|
6229
|
+
|
|
6230
|
+
${chalk.green.bold("Usage")}
|
|
6231
|
+
rebase doctor
|
|
6232
|
+
|
|
6233
|
+
Compares the collections you declare, the generated Drizzle schema, and the
|
|
6234
|
+
tables that actually exist, then reports what disagrees and how to reconcile it.
|
|
6235
|
+
|
|
6236
|
+
Run from inside a Rebase project — it reads the project's collections and
|
|
6237
|
+
connects to its database.
|
|
6238
|
+
`);
|
|
6239
|
+
}
|
|
5517
6240
|
async function doctorCommand(rawArgs) {
|
|
6241
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6242
|
+
printDoctorHelp();
|
|
6243
|
+
return;
|
|
6244
|
+
}
|
|
5518
6245
|
const projectRoot = requireProjectRoot();
|
|
5519
6246
|
const backendDir = requireBackendDir(projectRoot);
|
|
5520
6247
|
const activePlugin = getActiveBackendPlugin(backendDir);
|
|
@@ -5555,12 +6282,21 @@ async function doctorCommand(rawArgs) {
|
|
|
5555
6282
|
//#endregion
|
|
5556
6283
|
//#region src/commands/skills.ts
|
|
5557
6284
|
var require = createRequire(import.meta.url);
|
|
5558
|
-
/**
|
|
6285
|
+
/**
|
|
6286
|
+
* Supported agent environments and their target directories.
|
|
6287
|
+
*
|
|
6288
|
+
* `flatLayout` says where the installed rule file sits relative to the skill's
|
|
6289
|
+
* own assets. A subdirectory layout writes `<skill>/SKILL.md`, so a link the
|
|
6290
|
+
* skill spells `references/x.md` resolves as written; a flat layout writes
|
|
6291
|
+
* `<skill>.md` one level up, so those links have to be re-pointed at the
|
|
6292
|
+
* per-skill asset directory. See `rewriteAssetLinks`.
|
|
6293
|
+
*/
|
|
5559
6294
|
var AGENTS = {
|
|
5560
6295
|
cursor: {
|
|
5561
6296
|
label: "Cursor",
|
|
5562
6297
|
detectDir: ".cursor",
|
|
5563
6298
|
targetDir: ".cursor/rules",
|
|
6299
|
+
flatLayout: true,
|
|
5564
6300
|
/** Cursor uses .mdc files (Markdown with Context). */
|
|
5565
6301
|
transformFile: (skillName, content) => ({
|
|
5566
6302
|
fileName: `${skillName}.mdc`,
|
|
@@ -5571,6 +6307,7 @@ var AGENTS = {
|
|
|
5571
6307
|
label: "Claude Code",
|
|
5572
6308
|
detectDir: ".claude",
|
|
5573
6309
|
targetDir: ".claude/skills",
|
|
6310
|
+
flatLayout: false,
|
|
5574
6311
|
/** Claude Code uses the standard SKILL.md format in subdirectories. */
|
|
5575
6312
|
transformFile: (skillName, content) => ({
|
|
5576
6313
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5581,6 +6318,7 @@ var AGENTS = {
|
|
|
5581
6318
|
label: "Windsurf",
|
|
5582
6319
|
detectDir: ".windsurf",
|
|
5583
6320
|
targetDir: ".windsurf/rules",
|
|
6321
|
+
flatLayout: true,
|
|
5584
6322
|
/** Windsurf uses plain .md files. */
|
|
5585
6323
|
transformFile: (skillName, content) => ({
|
|
5586
6324
|
fileName: `${skillName}.md`,
|
|
@@ -5591,6 +6329,7 @@ var AGENTS = {
|
|
|
5591
6329
|
label: "Gemini CLI / Antigravity",
|
|
5592
6330
|
detectDir: ".agents",
|
|
5593
6331
|
targetDir: ".agents/skills",
|
|
6332
|
+
flatLayout: false,
|
|
5594
6333
|
/** Gemini uses the standard SKILL.md format in subdirectories. */
|
|
5595
6334
|
transformFile: (skillName, content) => ({
|
|
5596
6335
|
fileName: path.join(skillName, "SKILL.md"),
|
|
@@ -5609,21 +6348,65 @@ function getSkillsSourceDir() {
|
|
|
5609
6348
|
if (!fs.existsSync(skillsDir)) throw new Error(`Skills directory not found at ${skillsDir}. Make sure @rebasepro/agent-skills is installed.`);
|
|
5610
6349
|
return skillsDir;
|
|
5611
6350
|
}
|
|
5612
|
-
/**
|
|
6351
|
+
/**
|
|
6352
|
+
* Everything a skill ships alongside its SKILL.md — the `references/` tree the
|
|
6353
|
+
* Agent Skills format uses for progressive disclosure.
|
|
6354
|
+
*
|
|
6355
|
+
* These used to be dropped on install, because the installer read exactly
|
|
6356
|
+
* `<skill>/SKILL.md` and nothing else. That left `rebase-design-language`
|
|
6357
|
+
* telling the agent three separate times to read `references/view-patterns.md`
|
|
6358
|
+
* — 379 lines of view skeletons — in a project where the file had never
|
|
6359
|
+
* landed, and the instruction it carries is "extend an existing pattern; do not
|
|
6360
|
+
* invent a layout".
|
|
6361
|
+
*/
|
|
6362
|
+
function loadSkillAssets(skillDir) {
|
|
6363
|
+
const found = [];
|
|
6364
|
+
const walk = (dir, prefix) => {
|
|
6365
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6366
|
+
if (entry.name.startsWith(".")) continue;
|
|
6367
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
6368
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
|
|
6369
|
+
else if (rel !== "SKILL.md") found.push(rel);
|
|
6370
|
+
}
|
|
6371
|
+
};
|
|
6372
|
+
walk(skillDir, "");
|
|
6373
|
+
return found.sort();
|
|
6374
|
+
}
|
|
6375
|
+
/** Read all skill directories and return their names, content and assets. */
|
|
5613
6376
|
function loadSkills(skillsDir) {
|
|
5614
6377
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
5615
6378
|
const skills = [];
|
|
5616
6379
|
for (const entry of entries) {
|
|
5617
6380
|
if (!entry.isDirectory()) continue;
|
|
5618
|
-
const
|
|
6381
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
6382
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
5619
6383
|
if (!fs.existsSync(skillMdPath)) continue;
|
|
5620
6384
|
skills.push({
|
|
5621
6385
|
name: entry.name,
|
|
5622
|
-
|
|
6386
|
+
dir: skillDir,
|
|
6387
|
+
content: fs.readFileSync(skillMdPath, "utf-8"),
|
|
6388
|
+
assets: loadSkillAssets(skillDir)
|
|
5623
6389
|
});
|
|
5624
6390
|
}
|
|
5625
6391
|
return skills;
|
|
5626
6392
|
}
|
|
6393
|
+
/**
|
|
6394
|
+
* Re-point a skill's own asset links at the per-skill subdirectory, for the
|
|
6395
|
+
* agents whose rule file does not live in it.
|
|
6396
|
+
*
|
|
6397
|
+
* Only paths that name a file the skill actually ships are rewritten, and only
|
|
6398
|
+
* where they start a path segment — so prose that happens to contain the same
|
|
6399
|
+
* words is left alone.
|
|
6400
|
+
*/
|
|
6401
|
+
function rewriteAssetLinks(content, assets, skillName) {
|
|
6402
|
+
let out = content;
|
|
6403
|
+
for (const asset of assets) {
|
|
6404
|
+
const posix = asset.split(path.sep).join("/");
|
|
6405
|
+
const escaped = posix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6406
|
+
out = out.replace(new RegExp(`(?<![\\w/.-])${escaped}`, "g"), `${skillName}/${posix}`);
|
|
6407
|
+
}
|
|
6408
|
+
return out;
|
|
6409
|
+
}
|
|
5627
6410
|
/** Detect which agent environments already exist in the project. */
|
|
5628
6411
|
function detectAgents(projectDir) {
|
|
5629
6412
|
const detected = [];
|
|
@@ -5636,16 +6419,31 @@ function installForAgent(agentKey, skills, projectDir) {
|
|
|
5636
6419
|
const targetBase = path.join(projectDir, agent.targetDir);
|
|
5637
6420
|
fs.mkdirSync(targetBase, { recursive: true });
|
|
5638
6421
|
let count = 0;
|
|
6422
|
+
let assetCount = 0;
|
|
5639
6423
|
for (const skill of skills) {
|
|
5640
|
-
const
|
|
6424
|
+
const body = agent.flatLayout ? rewriteAssetLinks(skill.content, skill.assets, skill.name) : skill.content;
|
|
6425
|
+
const { fileName, content } = agent.transformFile(skill.name, body);
|
|
5641
6426
|
const targetPath = path.join(targetBase, fileName);
|
|
5642
6427
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
5643
6428
|
fs.writeFileSync(targetPath, content, "utf-8");
|
|
5644
6429
|
count++;
|
|
6430
|
+
for (const asset of skill.assets) {
|
|
6431
|
+
const assetTarget = path.join(targetBase, skill.name, asset);
|
|
6432
|
+
fs.mkdirSync(path.dirname(assetTarget), { recursive: true });
|
|
6433
|
+
fs.copyFileSync(path.join(skill.dir, asset), assetTarget);
|
|
6434
|
+
assetCount++;
|
|
6435
|
+
}
|
|
5645
6436
|
}
|
|
5646
|
-
return
|
|
6437
|
+
return {
|
|
6438
|
+
skills: count,
|
|
6439
|
+
assets: assetCount
|
|
6440
|
+
};
|
|
5647
6441
|
}
|
|
5648
6442
|
async function skillsCommand(subcommand, rawArgs) {
|
|
6443
|
+
if (wantsHelp(rawArgs)) {
|
|
6444
|
+
printSkillsHelp();
|
|
6445
|
+
return;
|
|
6446
|
+
}
|
|
5649
6447
|
switch (subcommand) {
|
|
5650
6448
|
case "install":
|
|
5651
6449
|
await skillsInstall(rawArgs);
|
|
@@ -5663,7 +6461,8 @@ async function skillsCommand(subcommand, rawArgs) {
|
|
|
5663
6461
|
}
|
|
5664
6462
|
/**
|
|
5665
6463
|
* Agents named explicitly on the command line, e.g. `--agent claude --agent cursor`
|
|
5666
|
-
* (also accepts a comma-separated list). Returns null when none were
|
|
6464
|
+
* (also accepts a comma-separated list, and `all`). Returns null when none were
|
|
6465
|
+
* given.
|
|
5667
6466
|
*/
|
|
5668
6467
|
function parseAgentFlags(rawArgs) {
|
|
5669
6468
|
const requested = [];
|
|
@@ -5673,6 +6472,7 @@ function parseAgentFlags(rawArgs) {
|
|
|
5673
6472
|
if (value && !value.startsWith("-")) requested.push(...value.split(",").map((v) => v.trim()).filter(Boolean));
|
|
5674
6473
|
}
|
|
5675
6474
|
if (requested.length === 0) return null;
|
|
6475
|
+
if (requested.includes("all")) return Object.keys(AGENTS);
|
|
5676
6476
|
const valid = Object.keys(AGENTS);
|
|
5677
6477
|
const unknown = requested.filter((a) => !valid.includes(a));
|
|
5678
6478
|
if (unknown.length > 0) {
|
|
@@ -5700,6 +6500,7 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5700
6500
|
if (!process.stdin.isTTY) {
|
|
5701
6501
|
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
5702
6502
|
console.error(chalk.yellow(` Name the agents explicitly, e.g. rebase skills install --agent ${Object.keys(AGENTS)[0]}`));
|
|
6503
|
+
console.error(chalk.yellow(" Or install for every supported agent: rebase skills install --agent all"));
|
|
5703
6504
|
console.error(chalk.gray(` Available: ${Object.keys(AGENTS).join(", ")}`));
|
|
5704
6505
|
process.exit(1);
|
|
5705
6506
|
}
|
|
@@ -5725,9 +6526,10 @@ async function skillsInstall(rawArgs = []) {
|
|
|
5725
6526
|
console.log("");
|
|
5726
6527
|
for (const agentKey of agents) {
|
|
5727
6528
|
const agent = AGENTS[agentKey];
|
|
5728
|
-
const count = installForAgent(agentKey, skills, projectDir);
|
|
6529
|
+
const { skills: count, assets } = installForAgent(agentKey, skills, projectDir);
|
|
5729
6530
|
const shown = path.relative(process.cwd(), path.join(projectDir, agent.targetDir)) || agent.targetDir;
|
|
5730
|
-
|
|
6531
|
+
const withAssets = assets > 0 ? ` (+ ${assets} reference file${assets === 1 ? "" : "s"})` : "";
|
|
6532
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} — ${count} skills installed${withAssets} to ${chalk.gray(shown)}`);
|
|
5731
6533
|
}
|
|
5732
6534
|
console.log("");
|
|
5733
6535
|
console.log(chalk.gray(" Skills are project-local. Commit them to share with your team."));
|
|
@@ -5747,13 +6549,16 @@ ${chalk.green.bold("Subcommands")}
|
|
|
5747
6549
|
|
|
5748
6550
|
${chalk.green.bold("Options")}
|
|
5749
6551
|
${chalk.blue("--agent, -a")} Agent(s) to install for, skipping detection and the prompt.
|
|
5750
|
-
Repeat the flag or pass a comma-separated list.
|
|
5751
|
-
|
|
6552
|
+
Repeat the flag or pass a comma-separated list, or ${chalk.bold("all")}.
|
|
6553
|
+
Required without a TTY: a scaffolded project carries a marker
|
|
6554
|
+
file for every agent, so detection cannot pick one for you.
|
|
6555
|
+
Available: ${Object.keys(AGENTS).join(", ")}, all
|
|
5752
6556
|
|
|
5753
6557
|
${chalk.green.bold("Examples")}
|
|
5754
6558
|
${chalk.cyan("rebase skills install")}
|
|
5755
6559
|
${chalk.cyan("rebase skills install --agent claude")}
|
|
5756
6560
|
${chalk.cyan("rebase skills install --agent claude,cursor")}
|
|
6561
|
+
${chalk.cyan("rebase skills install --agent all")} ${chalk.gray("# scripted / CI")}
|
|
5757
6562
|
`);
|
|
5758
6563
|
}
|
|
5759
6564
|
//#endregion
|
|
@@ -5766,25 +6571,13 @@ ${chalk.green.bold("Examples")}
|
|
|
5766
6571
|
* create — Create a new API key
|
|
5767
6572
|
* revoke — Revoke an existing API key
|
|
5768
6573
|
*/
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5777
|
-
const idx = trimmed.indexOf("=");
|
|
5778
|
-
if (idx > 0) {
|
|
5779
|
-
const key = trimmed.slice(0, idx).trim();
|
|
5780
|
-
let value = trimmed.slice(idx + 1).trim();
|
|
5781
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
5782
|
-
env[key] = value;
|
|
5783
|
-
}
|
|
5784
|
-
}
|
|
5785
|
-
}
|
|
5786
|
-
return env;
|
|
5787
|
-
}
|
|
6574
|
+
/**
|
|
6575
|
+
* Was a hand-rolled `indexOf("=")` loop. It keyed `export KEY=value` as
|
|
6576
|
+
* `export KEY` and carried a trailing `# comment` into the value — so a key
|
|
6577
|
+
* that was present read as absent, or reached an `Authorization` header with a
|
|
6578
|
+
* comment attached and came back 401. See `readEnvFile`.
|
|
6579
|
+
*/
|
|
6580
|
+
var loadEnv = readEnvFile;
|
|
5788
6581
|
function resolveBaseUrl(env, projectRoot) {
|
|
5789
6582
|
if (env.REBASE_BASE_URL) return env.REBASE_BASE_URL;
|
|
5790
6583
|
if (projectRoot) try {
|
|
@@ -5797,7 +6590,7 @@ function resolveBaseUrl(env, projectRoot) {
|
|
|
5797
6590
|
return `http://localhost:${env.PORT || env.REBASE_PORT || "3001"}`;
|
|
5798
6591
|
}
|
|
5799
6592
|
async function apiKeysCommand(subcommand, rawArgs) {
|
|
5800
|
-
if (!subcommand || subcommand === "--help") {
|
|
6593
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
5801
6594
|
printApiKeysHelp();
|
|
5802
6595
|
return;
|
|
5803
6596
|
}
|
|
@@ -5859,20 +6652,43 @@ async function listKeys(_rawArgs) {
|
|
|
5859
6652
|
process.exit(1);
|
|
5860
6653
|
}
|
|
5861
6654
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6655
|
+
/** The flags `rebase api-keys create` takes. */
|
|
6656
|
+
var CREATE_KEY_FLAGS = {
|
|
6657
|
+
"--name": String,
|
|
6658
|
+
"--permissions": String,
|
|
6659
|
+
"--full-access": Boolean,
|
|
6660
|
+
"--admin": Boolean,
|
|
6661
|
+
"--rate-limit": Number,
|
|
6662
|
+
"--expires": String,
|
|
6663
|
+
"-n": "--name"
|
|
6664
|
+
};
|
|
6665
|
+
/**
|
|
6666
|
+
* What this invocation asks to be created.
|
|
6667
|
+
*
|
|
6668
|
+
* The name may be given either way — `--name "My Key"` or as the single
|
|
6669
|
+
* positional — and under the old permissive parse an undeclared flag became
|
|
6670
|
+
* that positional: `rebase api-keys create --debug --full-access` created a
|
|
6671
|
+
* key called `--debug` with read/write/delete on every collection, and
|
|
6672
|
+
* `--debug` is what the CLI prints after every failure as the thing to re-run
|
|
6673
|
+
* with. Strict parsing makes the flag an error instead of a name.
|
|
6674
|
+
*
|
|
6675
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6676
|
+
*/
|
|
6677
|
+
function resolveCreateKeyArgs(rawArgs) {
|
|
6678
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6679
|
+
spec: CREATE_KEY_FLAGS,
|
|
6680
|
+
rawArgs,
|
|
6681
|
+
commandWords: 2,
|
|
6682
|
+
command: "api-keys create",
|
|
6683
|
+
maxPositionals: 1
|
|
5874
6684
|
});
|
|
5875
|
-
|
|
6685
|
+
return {
|
|
6686
|
+
flags,
|
|
6687
|
+
name: flags["--name"] || positionals[0]
|
|
6688
|
+
};
|
|
6689
|
+
}
|
|
6690
|
+
async function createKey(rawArgs) {
|
|
6691
|
+
const { flags: args, name } = resolveCreateKeyArgs(rawArgs);
|
|
5876
6692
|
const permissionsRaw = args["--permissions"];
|
|
5877
6693
|
if (!name) {
|
|
5878
6694
|
console.error(chalk.red("✗ Name is required."));
|
|
@@ -5973,12 +6789,30 @@ async function createKey(rawArgs) {
|
|
|
5973
6789
|
process.exit(1);
|
|
5974
6790
|
}
|
|
5975
6791
|
}
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
6792
|
+
/** The flags `rebase api-keys revoke` takes. */
|
|
6793
|
+
var REVOKE_KEY_FLAGS = { "--id": String };
|
|
6794
|
+
/**
|
|
6795
|
+
* Which key this invocation names.
|
|
6796
|
+
*
|
|
6797
|
+
* The id is a positional, so the permissive parse handed one straight to the
|
|
6798
|
+
* DELETE: `rebase api-keys revoke --foo` sent
|
|
6799
|
+
* `DELETE /api/admin/api-keys/--foo`, and `rebase --debug api-keys revoke <id>`
|
|
6800
|
+
* shifted the words along and revoked the key named `revoke`.
|
|
6801
|
+
*
|
|
6802
|
+
* Exported so its tests can drive the real parser rather than a copy of it.
|
|
6803
|
+
*/
|
|
6804
|
+
function resolveRevokeKeyArgs(rawArgs) {
|
|
6805
|
+
const { flags, positionals } = parseCommandArgs({
|
|
6806
|
+
spec: REVOKE_KEY_FLAGS,
|
|
6807
|
+
rawArgs,
|
|
6808
|
+
commandWords: 2,
|
|
6809
|
+
command: "api-keys revoke",
|
|
6810
|
+
maxPositionals: 1
|
|
5980
6811
|
});
|
|
5981
|
-
|
|
6812
|
+
return { id: flags["--id"] || positionals[0] };
|
|
6813
|
+
}
|
|
6814
|
+
async function revokeKey(rawArgs) {
|
|
6815
|
+
const { id } = resolveRevokeKeyArgs(rawArgs);
|
|
5982
6816
|
if (!id) {
|
|
5983
6817
|
console.error(chalk.red("✗ Key ID is required."));
|
|
5984
6818
|
console.log("");
|
|
@@ -6055,7 +6889,12 @@ ${chalk.green.bold("Examples")}
|
|
|
6055
6889
|
* a documentation comment that quietly fell out of date two releases ago.
|
|
6056
6890
|
*/
|
|
6057
6891
|
async function telemetryCommand(rawArgs) {
|
|
6058
|
-
|
|
6892
|
+
const subcommand = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0];
|
|
6893
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
6894
|
+
printHelp$2();
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
switch (subcommand) {
|
|
6059
6898
|
case "status":
|
|
6060
6899
|
case void 0:
|
|
6061
6900
|
printStatus();
|
|
@@ -6154,8 +6993,7 @@ async function loginCommand(rawArgs) {
|
|
|
6154
6993
|
const args = arg({
|
|
6155
6994
|
"--email": String,
|
|
6156
6995
|
"--password": String,
|
|
6157
|
-
"-e": "--email"
|
|
6158
|
-
"-p": "--password"
|
|
6996
|
+
"-e": "--email"
|
|
6159
6997
|
}, {
|
|
6160
6998
|
argv: rawArgs.slice(3),
|
|
6161
6999
|
permissive: true
|
|
@@ -6486,21 +7324,26 @@ async function resolveRequestedTarget(client, url, requested) {
|
|
|
6486
7324
|
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.`);
|
|
6487
7325
|
return chosen;
|
|
6488
7326
|
}
|
|
7327
|
+
/** The flags `rebase cloud projects create` takes. */
|
|
7328
|
+
var CREATE_PROJECT_FLAGS = {
|
|
7329
|
+
"--name": String,
|
|
7330
|
+
"--subdomain": String,
|
|
7331
|
+
"--repo": String,
|
|
7332
|
+
"--branch": String,
|
|
7333
|
+
"--provider": String,
|
|
7334
|
+
"--region": String,
|
|
7335
|
+
"--vm-size": String,
|
|
7336
|
+
"--org": String,
|
|
7337
|
+
"--link": Boolean,
|
|
7338
|
+
"-n": "--name"
|
|
7339
|
+
};
|
|
6489
7340
|
async function createProject(rawArgs) {
|
|
6490
|
-
const args =
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
"
|
|
6495
|
-
|
|
6496
|
-
"--region": String,
|
|
6497
|
-
"--vm-size": String,
|
|
6498
|
-
"--org": String,
|
|
6499
|
-
"--link": Boolean,
|
|
6500
|
-
"-n": "--name"
|
|
6501
|
-
}, {
|
|
6502
|
-
argv: rawArgs.slice(4),
|
|
6503
|
-
permissive: true
|
|
7341
|
+
const { flags: args } = parseCloudArgs({
|
|
7342
|
+
spec: CREATE_PROJECT_FLAGS,
|
|
7343
|
+
rawArgs,
|
|
7344
|
+
commandWords: 3,
|
|
7345
|
+
command: "cloud projects create",
|
|
7346
|
+
maxPositionals: 0
|
|
6504
7347
|
});
|
|
6505
7348
|
const { client, url } = await requireClient(rawArgs);
|
|
6506
7349
|
const org = args["--org"] || getContextOrg(url);
|
|
@@ -6570,6 +7413,31 @@ async function createProject(rawArgs) {
|
|
|
6570
7413
|
reportError(e, "Failed to create project");
|
|
6571
7414
|
}
|
|
6572
7415
|
}
|
|
7416
|
+
/**
|
|
7417
|
+
* Which project `projects info` / `projects delete` acts on.
|
|
7418
|
+
*
|
|
7419
|
+
* The id is optional — omitted, it falls back to `--project` or the link file —
|
|
7420
|
+
* and the dispatcher used to read it off `positionals()`, which skips only
|
|
7421
|
+
* LEADING `-` tokens and declares only the global cloud flags. So an undeclared
|
|
7422
|
+
* flag written after the action became the id: `rebase cloud projects delete
|
|
7423
|
+
* --force` looked up a project named "--force" and reported it missing, rather
|
|
7424
|
+
* than saying there is no such flag. Benign next to the deletes and writes the
|
|
7425
|
+
* rest of this family aimed at the wrong resource, but the same mistake, and
|
|
7426
|
+
* `positionals()` has no spec with which to do better — the handler's own
|
|
7427
|
+
* module does.
|
|
7428
|
+
*
|
|
7429
|
+
* Exported so its tests drive the real parser rather than a copy of it.
|
|
7430
|
+
*/
|
|
7431
|
+
function resolveProjectArg(rawArgs, action) {
|
|
7432
|
+
const { positionals } = parseCloudArgs({
|
|
7433
|
+
spec: {},
|
|
7434
|
+
rawArgs,
|
|
7435
|
+
commandWords: 3,
|
|
7436
|
+
command: `cloud projects ${action}`,
|
|
7437
|
+
maxPositionals: 1
|
|
7438
|
+
});
|
|
7439
|
+
return positionals[0] || requireProjectRef(rawArgs);
|
|
7440
|
+
}
|
|
6573
7441
|
async function projectInfo(rawArgs, projectRef) {
|
|
6574
7442
|
const { client, url } = await requireClient(rawArgs);
|
|
6575
7443
|
try {
|
|
@@ -6601,12 +7469,12 @@ async function projectInfo(rawArgs, projectRef) {
|
|
|
6601
7469
|
}
|
|
6602
7470
|
}
|
|
6603
7471
|
async function deleteProject(rawArgs, projectRef) {
|
|
6604
|
-
const args =
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
7472
|
+
const { flags: args } = parseCloudArgs({
|
|
7473
|
+
spec: {},
|
|
7474
|
+
rawArgs,
|
|
7475
|
+
commandWords: 3,
|
|
7476
|
+
command: "cloud projects delete",
|
|
7477
|
+
maxPositionals: 1
|
|
6610
7478
|
});
|
|
6611
7479
|
const { client } = await requireClient(rawArgs);
|
|
6612
7480
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
@@ -7701,18 +8569,39 @@ async function dbInfo(rawArgs) {
|
|
|
7701
8569
|
reportError(e, "Failed to load database info");
|
|
7702
8570
|
}
|
|
7703
8571
|
}
|
|
8572
|
+
/**
|
|
8573
|
+
* `db backup [action] [filename]`, resolved in one strict parse.
|
|
8574
|
+
*
|
|
8575
|
+
* Both halves were reachable by the old operand filter, and both are
|
|
8576
|
+
* destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
|
|
8577
|
+
* ACTION (falling through to a list, so the flag silently changed what ran),
|
|
8578
|
+
* and `db backup restore -p acme` read it as the FILENAME — a restore staged
|
|
8579
|
+
* over the live database, named after the project slug. An undeclared flag was
|
|
8580
|
+
* dropped instead of refused, which is the same failure one step quieter: `db
|
|
8581
|
+
* backup --dry-run` ran a list, having silently discarded the flag that was
|
|
8582
|
+
* supposed to change what it did.
|
|
8583
|
+
*
|
|
8584
|
+
* Exported so its tests drive the real parser.
|
|
8585
|
+
*/
|
|
8586
|
+
function resolveBackupArgs(rawArgs) {
|
|
8587
|
+
const { flags, positionals } = parseCloudArgs({
|
|
8588
|
+
spec: { "--yes": Boolean },
|
|
8589
|
+
rawArgs,
|
|
8590
|
+
commandWords: 3,
|
|
8591
|
+
command: "cloud db backup",
|
|
8592
|
+
maxPositionals: 2
|
|
8593
|
+
});
|
|
8594
|
+
return {
|
|
8595
|
+
flags,
|
|
8596
|
+
action: positionals[0] || "list",
|
|
8597
|
+
filename: positionals[1]
|
|
8598
|
+
};
|
|
8599
|
+
}
|
|
7704
8600
|
async function backupCommand(rawArgs) {
|
|
7705
|
-
const action
|
|
8601
|
+
const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
|
|
7706
8602
|
const { client } = await requireClient(rawArgs);
|
|
7707
8603
|
const projectId = await requireProject(rawArgs, client);
|
|
7708
8604
|
const projectRef = displayProjectRef(rawArgs);
|
|
7709
|
-
const args = arg({
|
|
7710
|
-
"--yes": Boolean,
|
|
7711
|
-
"-y": "--yes"
|
|
7712
|
-
}, {
|
|
7713
|
-
argv: rawArgs.slice(2),
|
|
7714
|
-
permissive: true
|
|
7715
|
-
});
|
|
7716
8605
|
try {
|
|
7717
8606
|
if (action === "create") {
|
|
7718
8607
|
const res = await client.functions.invoke("backup", {
|
|
@@ -7727,7 +8616,7 @@ async function backupCommand(rawArgs) {
|
|
|
7727
8616
|
return;
|
|
7728
8617
|
}
|
|
7729
8618
|
if (action === "restore") {
|
|
7730
|
-
const filename =
|
|
8619
|
+
const filename = backupFile;
|
|
7731
8620
|
if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
|
|
7732
8621
|
await confirmDestructive({
|
|
7733
8622
|
yes: Boolean(args["--yes"]),
|
|
@@ -7765,7 +8654,7 @@ async function backupCommand(rawArgs) {
|
|
|
7765
8654
|
return;
|
|
7766
8655
|
}
|
|
7767
8656
|
if (action === "download") {
|
|
7768
|
-
const filename =
|
|
8657
|
+
const filename = backupFile;
|
|
7769
8658
|
if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
|
|
7770
8659
|
const res = await client.functions.invoke("backup", void 0, {
|
|
7771
8660
|
method: "GET",
|
|
@@ -7823,17 +8712,17 @@ async function backupCommand(rawArgs) {
|
|
|
7823
8712
|
* non-interactive use, and the CLI surfaces these staged semantics honestly.
|
|
7824
8713
|
*/
|
|
7825
8714
|
async function pitrCommand(rawArgs) {
|
|
7826
|
-
const args =
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
8715
|
+
const { flags: args, positionals } = parseCloudArgs({
|
|
8716
|
+
spec: {
|
|
8717
|
+
"--target": String,
|
|
8718
|
+
"--yes": Boolean
|
|
8719
|
+
},
|
|
8720
|
+
rawArgs,
|
|
8721
|
+
commandWords: 3,
|
|
8722
|
+
command: "cloud db pitr",
|
|
8723
|
+
maxPositionals: 1
|
|
7835
8724
|
});
|
|
7836
|
-
const action =
|
|
8725
|
+
const action = positionals[0] || "status";
|
|
7837
8726
|
const { client } = await requireClient(rawArgs);
|
|
7838
8727
|
const projectId = await requireProject(rawArgs, client);
|
|
7839
8728
|
const projectRef = displayProjectRef(rawArgs);
|
|
@@ -7966,7 +8855,7 @@ async function envCommand(action, rawArgs) {
|
|
|
7966
8855
|
case "unset":
|
|
7967
8856
|
case "delete":
|
|
7968
8857
|
case "rm":
|
|
7969
|
-
await unsetEnv(rawArgs);
|
|
8858
|
+
await unsetEnv(rawArgs, action);
|
|
7970
8859
|
break;
|
|
7971
8860
|
case "reveal":
|
|
7972
8861
|
await revealEnv(rawArgs);
|
|
@@ -8066,20 +8955,43 @@ var BUILD_TIME_ENV_PREFIXES = [
|
|
|
8066
8955
|
function buildTimeEnvPrefix(key) {
|
|
8067
8956
|
return BUILD_TIME_ENV_PREFIXES.find((prefix) => key.toUpperCase().startsWith(prefix));
|
|
8068
8957
|
}
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8958
|
+
/** The flags `rebase cloud env set` takes, on top of the global cloud ones. */
|
|
8959
|
+
var ENV_SET_FLAGS = {
|
|
8960
|
+
"--secret": Boolean,
|
|
8961
|
+
"--force": Boolean
|
|
8962
|
+
};
|
|
8963
|
+
/**
|
|
8964
|
+
* What `env set` was asked to store.
|
|
8965
|
+
*
|
|
8966
|
+
* The sharp one in this family: the old operand filter left `--project`'s value
|
|
8967
|
+
* in the operand list, so `rebase cloud env set KEY -p acme` parsed as the
|
|
8968
|
+
* `KEY VALUE` form and stored the project slug as KEY's value — a write that
|
|
8969
|
+
* succeeds, reports success, and is wrong. Strict parsing consumes `-p` with
|
|
8970
|
+
* its value, leaving `["KEY"]` and the documented empty value.
|
|
8971
|
+
*
|
|
8972
|
+
* A value beginning with `-` must use the `KEY=-v` form; the bare `KEY -v` form
|
|
8973
|
+
* is refused rather than guessed at, as everywhere else strict parsing is used.
|
|
8974
|
+
*
|
|
8975
|
+
* Exported so its tests drive the real parser rather than a copy of it.
|
|
8976
|
+
*/
|
|
8977
|
+
function resolveEnvSetArgs(rawArgs) {
|
|
8978
|
+
const { flags, positionals } = parseCloudArgs({
|
|
8979
|
+
spec: ENV_SET_FLAGS,
|
|
8980
|
+
rawArgs,
|
|
8981
|
+
commandWords: 3,
|
|
8982
|
+
command: "cloud env set",
|
|
8983
|
+
maxPositionals: 2
|
|
8078
8984
|
});
|
|
8985
|
+
return {
|
|
8986
|
+
flags,
|
|
8987
|
+
assignment: parseEnvAssignment(positionals)
|
|
8988
|
+
};
|
|
8989
|
+
}
|
|
8990
|
+
async function setEnv(rawArgs) {
|
|
8991
|
+
const { flags: args, assignment: parsed } = resolveEnvSetArgs(rawArgs);
|
|
8079
8992
|
const { client } = await requireClient(rawArgs);
|
|
8080
8993
|
const projectId = await requireProject(rawArgs, client);
|
|
8081
8994
|
displayProjectRef(rawArgs);
|
|
8082
|
-
const parsed = parseEnvAssignment(cloudPositionals(rawArgs).slice(2));
|
|
8083
8995
|
if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
|
|
8084
8996
|
const buildTimePrefix = buildTimeEnvPrefix(parsed.key);
|
|
8085
8997
|
if (buildTimePrefix && !args["--force"]) fail(`${parsed.key} is read by your bundler at BUILD time, and project variables are applied at rollout — after the image is built. Setting it here would not reach the bundle.`, `Put ${buildTimePrefix}* variables in the source you deploy (a committed .env, or your build config), then \`rebase cloud deploy\`. Pass --force if your build genuinely reads this at run time.`, "build_time_variable");
|
|
@@ -8105,12 +9017,32 @@ async function setEnv(rawArgs) {
|
|
|
8105
9017
|
reportError(e, "Failed to set environment variable");
|
|
8106
9018
|
}
|
|
8107
9019
|
}
|
|
8108
|
-
|
|
9020
|
+
/**
|
|
9021
|
+
* The variable `env unset` / `env reveal` names.
|
|
9022
|
+
*
|
|
9023
|
+
* `unset` is a delete, and the operand filter aimed it at the wrong variable:
|
|
9024
|
+
* `rebase cloud env unset -p acme` removed a variable called "acme" from the
|
|
9025
|
+
* linked project instead of reporting a missing KEY, and `env unset -p acme
|
|
9026
|
+
* KEY` removed "acme" instead of KEY. Both read `--project`'s value as the
|
|
9027
|
+
* operand — a plain word in the right position that no flag filter can catch.
|
|
9028
|
+
*
|
|
9029
|
+
* `action` is the word the caller used (`unset`, `rm`, `delete`, `reveal`); the
|
|
9030
|
+
* count of command words is the same for all of them.
|
|
9031
|
+
*/
|
|
9032
|
+
function resolveEnvKeyArg(rawArgs, action) {
|
|
9033
|
+
return parseCloudArgs({
|
|
9034
|
+
spec: {},
|
|
9035
|
+
rawArgs,
|
|
9036
|
+
commandWords: 3,
|
|
9037
|
+
command: `cloud env ${action}`,
|
|
9038
|
+
maxPositionals: 1
|
|
9039
|
+
}).positionals[0];
|
|
9040
|
+
}
|
|
9041
|
+
async function unsetEnv(rawArgs, action) {
|
|
9042
|
+
const key = resolveEnvKeyArg(rawArgs, action);
|
|
9043
|
+
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8109
9044
|
const { client } = await requireClient(rawArgs);
|
|
8110
9045
|
const projectId = await requireProject(rawArgs, client);
|
|
8111
|
-
displayProjectRef(rawArgs);
|
|
8112
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8113
|
-
if (!key) fail("Usage: rebase cloud env unset KEY", void 0, "usage");
|
|
8114
9046
|
try {
|
|
8115
9047
|
emit(() => {
|
|
8116
9048
|
success(`Removed ${chalk.bold(key)}`);
|
|
@@ -8129,11 +9061,11 @@ async function unsetEnv(rawArgs) {
|
|
|
8129
9061
|
}
|
|
8130
9062
|
}
|
|
8131
9063
|
async function revealEnv(rawArgs) {
|
|
9064
|
+
const key = resolveEnvKeyArg(rawArgs, "reveal");
|
|
9065
|
+
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8132
9066
|
const { client } = await requireClient(rawArgs);
|
|
8133
9067
|
const projectId = await requireProject(rawArgs, client);
|
|
8134
9068
|
const projectRef = displayProjectRef(rawArgs);
|
|
8135
|
-
const key = cloudPositionals(rawArgs).slice(2)[0];
|
|
8136
|
-
if (!key) fail("Usage: rebase cloud env reveal KEY", void 0, "usage");
|
|
8137
9069
|
let list;
|
|
8138
9070
|
try {
|
|
8139
9071
|
list = await fetchEnvVars(client, projectId);
|
|
@@ -8161,20 +9093,20 @@ async function revealEnv(rawArgs) {
|
|
|
8161
9093
|
}
|
|
8162
9094
|
}
|
|
8163
9095
|
async function pullEnv(rawArgs) {
|
|
8164
|
-
const args =
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
9096
|
+
const { flags: args } = parseCloudArgs({
|
|
9097
|
+
spec: {
|
|
9098
|
+
"--output": String,
|
|
9099
|
+
"--out": "--output"
|
|
9100
|
+
},
|
|
9101
|
+
rawArgs,
|
|
9102
|
+
commandWords: 3,
|
|
9103
|
+
command: "cloud env pull",
|
|
9104
|
+
maxPositionals: 0
|
|
8173
9105
|
});
|
|
8174
9106
|
const { client } = await requireClient(rawArgs);
|
|
8175
9107
|
const projectId = await requireProject(rawArgs, client);
|
|
8176
9108
|
displayProjectRef(rawArgs);
|
|
8177
|
-
const outPath = path.resolve(args["--
|
|
9109
|
+
const outPath = path.resolve(args["--output"] || ".env");
|
|
8178
9110
|
try {
|
|
8179
9111
|
const list = await fetchEnvVars(client, projectId);
|
|
8180
9112
|
if (fs.existsSync(outPath)) await confirmDestructive({
|
|
@@ -8346,12 +9278,28 @@ async function listDomains(rawArgs) {
|
|
|
8346
9278
|
reportError(e, "Failed to load custom domain");
|
|
8347
9279
|
}
|
|
8348
9280
|
}
|
|
9281
|
+
/**
|
|
9282
|
+
* The domain `domains add` was asked to register.
|
|
9283
|
+
*
|
|
9284
|
+
* Exported so its tests drive the real parser. Under the old operand filter
|
|
9285
|
+
* `rebase cloud domains add -p acme` registered a domain called "acme" — the
|
|
9286
|
+
* project slug, read out of `--project`'s own value — and a registered domain
|
|
9287
|
+
* is a project-record write, not a no-op.
|
|
9288
|
+
*/
|
|
9289
|
+
function resolveDomainArg(rawArgs) {
|
|
9290
|
+
return parseCloudArgs({
|
|
9291
|
+
spec: {},
|
|
9292
|
+
rawArgs,
|
|
9293
|
+
commandWords: 3,
|
|
9294
|
+
command: "cloud domains add",
|
|
9295
|
+
maxPositionals: 1
|
|
9296
|
+
}).positionals[0];
|
|
9297
|
+
}
|
|
8349
9298
|
async function addDomain(rawArgs) {
|
|
9299
|
+
const domain = resolveDomainArg(rawArgs);
|
|
9300
|
+
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8350
9301
|
const { client } = await requireClient(rawArgs);
|
|
8351
9302
|
const projectId = await requireProject(rawArgs, client);
|
|
8352
|
-
displayProjectRef(rawArgs);
|
|
8353
|
-
const domain = cloudPositionals(rawArgs).slice(2)[0];
|
|
8354
|
-
if (!domain) fail("Usage: rebase cloud domains add <domain>", void 0, "usage");
|
|
8355
9303
|
try {
|
|
8356
9304
|
await client.data.collection("projects").update(projectId, { customDomain: domain });
|
|
8357
9305
|
const setup = await fetchDomainSetup(client, projectId);
|
|
@@ -8406,14 +9354,12 @@ async function verifyDomains(rawArgs) {
|
|
|
8406
9354
|
}
|
|
8407
9355
|
}
|
|
8408
9356
|
async function removeDomain(rawArgs) {
|
|
8409
|
-
const args =
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
argv: rawArgs.slice(2),
|
|
8416
|
-
permissive: true
|
|
9357
|
+
const { flags: args } = parseCloudArgs({
|
|
9358
|
+
spec: {},
|
|
9359
|
+
rawArgs,
|
|
9360
|
+
commandWords: 3,
|
|
9361
|
+
command: "cloud domains remove",
|
|
9362
|
+
maxPositionals: 0
|
|
8417
9363
|
});
|
|
8418
9364
|
const { client } = await requireClient(rawArgs);
|
|
8419
9365
|
const projectId = await requireProject(rawArgs, client);
|
|
@@ -8524,21 +9470,32 @@ async function listExtensions(rawArgs) {
|
|
|
8524
9470
|
reportError(e, "Failed to list extensions");
|
|
8525
9471
|
}
|
|
8526
9472
|
}
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
9473
|
+
/**
|
|
9474
|
+
* The extension `enable`/`disable` names, plus the flags that gate it.
|
|
9475
|
+
*
|
|
9476
|
+
* Under the old operand filter `rebase cloud extensions enable -p acme` read
|
|
9477
|
+
* `--project`'s value as the extension name and asked the server to install one
|
|
9478
|
+
* called "acme"; `extensions disable -p acme vector` dropped "acme" rather than
|
|
9479
|
+
* vector. Strict parsing consumes the flag with its value.
|
|
9480
|
+
*/
|
|
9481
|
+
function resolveExtensionArgs(rawArgs, action) {
|
|
9482
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9483
|
+
spec: {},
|
|
9484
|
+
rawArgs,
|
|
9485
|
+
commandWords: 3,
|
|
9486
|
+
command: `cloud extensions ${action}`,
|
|
9487
|
+
maxPositionals: 1
|
|
8536
9488
|
});
|
|
9489
|
+
return {
|
|
9490
|
+
flags,
|
|
9491
|
+
name: positionals[0]
|
|
9492
|
+
};
|
|
9493
|
+
}
|
|
9494
|
+
async function enableExtension(rawArgs) {
|
|
9495
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "enable");
|
|
9496
|
+
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8537
9497
|
const { client } = await requireClient(rawArgs);
|
|
8538
9498
|
const projectId = await requireProject(rawArgs, client);
|
|
8539
|
-
displayProjectRef(rawArgs);
|
|
8540
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8541
|
-
if (!raw) fail("Usage: rebase cloud extensions enable <name>", void 0, "usage");
|
|
8542
9499
|
const name = resolveExtensionAlias(raw);
|
|
8543
9500
|
try {
|
|
8544
9501
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8575,20 +9532,10 @@ async function enableExtension(rawArgs) {
|
|
|
8575
9532
|
}
|
|
8576
9533
|
}
|
|
8577
9534
|
async function disableExtension(rawArgs) {
|
|
8578
|
-
const args =
|
|
8579
|
-
|
|
8580
|
-
"-y": "--yes",
|
|
8581
|
-
"--project": String,
|
|
8582
|
-
"-p": "--project"
|
|
8583
|
-
}, {
|
|
8584
|
-
argv: rawArgs.slice(2),
|
|
8585
|
-
permissive: true
|
|
8586
|
-
});
|
|
9535
|
+
const { flags: args, name: raw } = resolveExtensionArgs(rawArgs, "disable");
|
|
9536
|
+
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8587
9537
|
const { client } = await requireClient(rawArgs);
|
|
8588
9538
|
const projectId = await requireProject(rawArgs, client);
|
|
8589
|
-
displayProjectRef(rawArgs);
|
|
8590
|
-
const raw = cloudPositionals(rawArgs).slice(2)[0];
|
|
8591
|
-
if (!raw) fail("Usage: rebase cloud extensions disable <name>", void 0, "usage");
|
|
8592
9539
|
const name = resolveExtensionAlias(raw);
|
|
8593
9540
|
try {
|
|
8594
9541
|
const ext = (await fetchExtensions(client, projectId)).extensions.find((e) => e.name === name);
|
|
@@ -8865,14 +9812,15 @@ function parseDeploymentsLimit(raw) {
|
|
|
8865
9812
|
return raw;
|
|
8866
9813
|
}
|
|
8867
9814
|
async function deploymentsListCommand(rawArgs) {
|
|
8868
|
-
const args =
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
9815
|
+
const { flags: args } = parseCloudArgs({
|
|
9816
|
+
spec: {
|
|
9817
|
+
"--limit": Number,
|
|
9818
|
+
"--all": Boolean
|
|
9819
|
+
},
|
|
9820
|
+
rawArgs,
|
|
9821
|
+
commandWords: 2,
|
|
9822
|
+
command: "cloud deployments list",
|
|
9823
|
+
maxPositionals: 1
|
|
8876
9824
|
});
|
|
8877
9825
|
const limit = args["--all"] ? MAX_DEPLOYMENTS_LIMIT : parseDeploymentsLimit(args["--limit"]);
|
|
8878
9826
|
const { client } = await requireClient(rawArgs);
|
|
@@ -8913,20 +9861,34 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
8913
9861
|
reportError(e, "Failed to list deployments");
|
|
8914
9862
|
}
|
|
8915
9863
|
}
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
9864
|
+
/**
|
|
9865
|
+
* The deployment id `rollback`/`cancel` was given, if any.
|
|
9866
|
+
*
|
|
9867
|
+
* Both take an optional id, which is what made the old operand filter so easy
|
|
9868
|
+
* to trip: `rebase cloud rollback -p acme` — the documented way to act on an
|
|
9869
|
+
* unlinked project — read `--project`'s value as the id and refused with
|
|
9870
|
+
* "Deployment acme not found", and `cancel -p acme` sent "acme" to the server
|
|
9871
|
+
* as the deployment to cancel. Strict parsing consumes the flag with its value,
|
|
9872
|
+
* so an id given as a flag value is never mistaken for an argument.
|
|
9873
|
+
*/
|
|
9874
|
+
function resolveDeploymentIdArg(rawArgs, command) {
|
|
9875
|
+
const { flags, positionals } = parseCloudArgs({
|
|
9876
|
+
spec: {},
|
|
9877
|
+
rawArgs,
|
|
9878
|
+
commandWords: 2,
|
|
9879
|
+
command,
|
|
9880
|
+
maxPositionals: 1
|
|
8925
9881
|
});
|
|
9882
|
+
return {
|
|
9883
|
+
flags,
|
|
9884
|
+
id: positionals[0]
|
|
9885
|
+
};
|
|
9886
|
+
}
|
|
9887
|
+
async function rollbackCommand(rawArgs) {
|
|
9888
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud rollback");
|
|
8926
9889
|
const { client } = await requireClient(rawArgs);
|
|
8927
9890
|
const projectId = await requireProject(rawArgs, client);
|
|
8928
9891
|
const projectRef = displayProjectRef(rawArgs);
|
|
8929
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
8930
9892
|
let rows;
|
|
8931
9893
|
try {
|
|
8932
9894
|
rows = await fetchDeployments(client, projectId);
|
|
@@ -8974,19 +9936,10 @@ async function rollbackCommand(rawArgs) {
|
|
|
8974
9936
|
}
|
|
8975
9937
|
}
|
|
8976
9938
|
async function cancelCommand(rawArgs) {
|
|
8977
|
-
const args =
|
|
8978
|
-
"--yes": Boolean,
|
|
8979
|
-
"-y": "--yes",
|
|
8980
|
-
"--project": String,
|
|
8981
|
-
"-p": "--project"
|
|
8982
|
-
}, {
|
|
8983
|
-
argv: rawArgs.slice(2),
|
|
8984
|
-
permissive: true
|
|
8985
|
-
});
|
|
9939
|
+
const { flags: args, id: explicitId } = resolveDeploymentIdArg(rawArgs, "cloud cancel");
|
|
8986
9940
|
const { client } = await requireClient(rawArgs);
|
|
8987
9941
|
const projectId = await requireProject(rawArgs, client);
|
|
8988
9942
|
const projectRef = displayProjectRef(rawArgs);
|
|
8989
|
-
const explicitId = cloudPositionals(rawArgs).slice(1)[0];
|
|
8990
9943
|
await confirmDestructive({
|
|
8991
9944
|
yes: Boolean(args["--yes"]),
|
|
8992
9945
|
prompt: `Cancel the in-flight build for project ${projectRef}?`
|
|
@@ -9952,20 +10905,47 @@ async function metricsCommand(rawArgs) {
|
|
|
9952
10905
|
reportError(e, "Failed to fetch metrics");
|
|
9953
10906
|
}
|
|
9954
10907
|
}
|
|
10908
|
+
/**
|
|
10909
|
+
* The webhook `webhooks delete` names.
|
|
10910
|
+
*
|
|
10911
|
+
* The worst instance of the operand-filter bug in this family, because the
|
|
10912
|
+
* argument is consumed by a DELETE and the wrong value looks entirely
|
|
10913
|
+
* plausible: `rebase cloud webhooks delete --project acme 42` filtered out
|
|
10914
|
+
* `--project` and kept "acme", so the id it deleted was the project slug rather
|
|
10915
|
+
* than the 42 the caller wrote. Strict parsing consumes the flag with its
|
|
10916
|
+
* value, leaving `["42"]`.
|
|
10917
|
+
*
|
|
10918
|
+
* Exported so its tests drive the real parser.
|
|
10919
|
+
*/
|
|
10920
|
+
function resolveWebhookIdArg(rawArgs) {
|
|
10921
|
+
return parseCloudArgs({
|
|
10922
|
+
spec: {},
|
|
10923
|
+
rawArgs,
|
|
10924
|
+
commandWords: 3,
|
|
10925
|
+
command: "cloud webhooks delete",
|
|
10926
|
+
maxPositionals: 1
|
|
10927
|
+
}).positionals[0];
|
|
10928
|
+
}
|
|
9955
10929
|
async function webhooksCommand(subcommand, rawArgs) {
|
|
10930
|
+
const create = subcommand === "create" ? parseCloudArgs({
|
|
10931
|
+
spec: {
|
|
10932
|
+
"--name": String,
|
|
10933
|
+
"--table": String,
|
|
10934
|
+
"--url": String,
|
|
10935
|
+
"--events": String
|
|
10936
|
+
},
|
|
10937
|
+
rawArgs,
|
|
10938
|
+
commandWords: 3,
|
|
10939
|
+
command: "cloud webhooks create",
|
|
10940
|
+
maxPositionals: 0
|
|
10941
|
+
}).flags : void 0;
|
|
10942
|
+
const deleteId = subcommand === "delete" ? resolveWebhookIdArg(rawArgs) : void 0;
|
|
10943
|
+
if (subcommand === "delete" && !deleteId) fail("Usage: rebase cloud webhooks delete <id>");
|
|
9956
10944
|
const { client } = await requireClient(rawArgs);
|
|
9957
10945
|
const projectId = await requireProject(rawArgs, client);
|
|
9958
10946
|
try {
|
|
9959
10947
|
if (subcommand === "create") {
|
|
9960
|
-
const args =
|
|
9961
|
-
"--name": String,
|
|
9962
|
-
"--table": String,
|
|
9963
|
-
"--url": String,
|
|
9964
|
-
"--events": String
|
|
9965
|
-
}, {
|
|
9966
|
-
argv: rawArgs.slice(4),
|
|
9967
|
-
permissive: true
|
|
9968
|
-
});
|
|
10948
|
+
const args = create;
|
|
9969
10949
|
const name = args["--name"] || fail("--name is required.");
|
|
9970
10950
|
const table = args["--table"] || fail("--table is required.");
|
|
9971
10951
|
const url = args["--url"] || fail("--url (endpoint) is required.");
|
|
@@ -9982,10 +10962,8 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
9982
10962
|
return;
|
|
9983
10963
|
}
|
|
9984
10964
|
if (subcommand === "delete") {
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
await client.data.collection("webhooks").delete(id);
|
|
9988
|
-
success(`Deleted webhook ${id}`);
|
|
10965
|
+
await client.data.collection("webhooks").delete(deleteId);
|
|
10966
|
+
success(`Deleted webhook ${deleteId}`);
|
|
9989
10967
|
return;
|
|
9990
10968
|
}
|
|
9991
10969
|
const hooks = (await client.data.collection("webhooks").find({
|
|
@@ -10060,6 +11038,13 @@ function printStorageHelp() {
|
|
|
10060
11038
|
console.log("");
|
|
10061
11039
|
}
|
|
10062
11040
|
async function storageCreateCommand(rawArgs) {
|
|
11041
|
+
parseCloudArgs({
|
|
11042
|
+
spec: {},
|
|
11043
|
+
rawArgs,
|
|
11044
|
+
commandWords: 3,
|
|
11045
|
+
command: "cloud storage create",
|
|
11046
|
+
maxPositionals: 0
|
|
11047
|
+
});
|
|
10063
11048
|
const { client } = await requireClient(rawArgs);
|
|
10064
11049
|
const projectId = await requireProject(rawArgs, client);
|
|
10065
11050
|
try {
|
|
@@ -10083,16 +11068,19 @@ async function storageCreateCommand(rawArgs) {
|
|
|
10083
11068
|
}
|
|
10084
11069
|
}
|
|
10085
11070
|
async function storageAttachCommand(rawArgs) {
|
|
10086
|
-
const parsed =
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
11071
|
+
const { flags: parsed } = parseCloudArgs({
|
|
11072
|
+
spec: {
|
|
11073
|
+
"--bucket": String,
|
|
11074
|
+
"--access-key-id": String,
|
|
11075
|
+
"--secret-access-key": String,
|
|
11076
|
+
"--endpoint": String,
|
|
11077
|
+
"--region": String,
|
|
11078
|
+
"--force-path-style": Boolean
|
|
11079
|
+
},
|
|
11080
|
+
rawArgs,
|
|
11081
|
+
commandWords: 3,
|
|
11082
|
+
command: "cloud storage attach",
|
|
11083
|
+
maxPositionals: 0
|
|
10096
11084
|
});
|
|
10097
11085
|
const bucket = parsed["--bucket"];
|
|
10098
11086
|
const accessKeyId = parsed["--access-key-id"];
|
|
@@ -10162,9 +11150,15 @@ async function clustersCommand(rawArgs) {
|
|
|
10162
11150
|
}
|
|
10163
11151
|
}
|
|
10164
11152
|
async function billingCommand(rawArgs) {
|
|
11153
|
+
const action = parseCloudArgs({
|
|
11154
|
+
spec: {},
|
|
11155
|
+
rawArgs,
|
|
11156
|
+
commandWords: 2,
|
|
11157
|
+
command: "cloud billing",
|
|
11158
|
+
maxPositionals: 1
|
|
11159
|
+
}).positionals[0];
|
|
10165
11160
|
const { client, url } = await requireClient(rawArgs);
|
|
10166
11161
|
const org = getContextOrg(url);
|
|
10167
|
-
const action = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[1];
|
|
10168
11162
|
if (action === "setup") {
|
|
10169
11163
|
if (!org) fail("No active organization.", "Run `rebase cloud use` first.");
|
|
10170
11164
|
try {
|
|
@@ -10293,15 +11287,43 @@ function positionals(rawArgs) {
|
|
|
10293
11287
|
while (i < rest.length && rest[i].startsWith("-")) i++;
|
|
10294
11288
|
return rest.slice(i);
|
|
10295
11289
|
}
|
|
11290
|
+
/**
|
|
11291
|
+
* The help page for each group, keyed by every alias the dispatch below accepts.
|
|
11292
|
+
*
|
|
11293
|
+
* Aliases are listed explicitly rather than normalised first, so a group that
|
|
11294
|
+
* gains one and forgets it here degrades to the index page — wrong, but a page.
|
|
11295
|
+
* `cloud-help.test.ts` asserts the two stay in step.
|
|
11296
|
+
*
|
|
11297
|
+
* A group absent from this map has no page of its own; the index lists it.
|
|
11298
|
+
*/
|
|
11299
|
+
var GROUP_HELP = {
|
|
11300
|
+
env: printEnvHelp,
|
|
11301
|
+
domains: printDomainsHelp,
|
|
11302
|
+
domain: printDomainsHelp,
|
|
11303
|
+
extensions: printExtensionsHelp,
|
|
11304
|
+
extension: printExtensionsHelp,
|
|
11305
|
+
settings: printSettingsHelp,
|
|
11306
|
+
orgs: printOrgsHelp,
|
|
11307
|
+
org: printOrgsHelp,
|
|
11308
|
+
db: printDbHelp,
|
|
11309
|
+
database: printDbHelp,
|
|
11310
|
+
debug: printDebugHelp,
|
|
11311
|
+
storage: printStorageHelp
|
|
11312
|
+
};
|
|
10296
11313
|
async function cloudCommand(subcommand, rawArgs) {
|
|
10297
11314
|
initOutputMode(rawArgs);
|
|
10298
11315
|
const pos = positionals(rawArgs);
|
|
10299
11316
|
const group = pos[0] ?? (subcommand !== "--help" ? subcommand : void 0);
|
|
11317
|
+
const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
|
|
10300
11318
|
const action = pos[1];
|
|
10301
|
-
if (!group
|
|
11319
|
+
if (!group) {
|
|
10302
11320
|
printCloudHelp();
|
|
10303
11321
|
return;
|
|
10304
11322
|
}
|
|
11323
|
+
if (wantsHelp) {
|
|
11324
|
+
(GROUP_HELP[group] ?? printCloudHelp)();
|
|
11325
|
+
return;
|
|
11326
|
+
}
|
|
10305
11327
|
switch (group) {
|
|
10306
11328
|
case "login":
|
|
10307
11329
|
await loginCommand(rawArgs);
|
|
@@ -10409,10 +11431,10 @@ async function projectsGroup(action, rawArgs) {
|
|
|
10409
11431
|
await createProject(rawArgs);
|
|
10410
11432
|
break;
|
|
10411
11433
|
case "info":
|
|
10412
|
-
await projectInfo(rawArgs,
|
|
11434
|
+
await projectInfo(rawArgs, resolveProjectArg(rawArgs, "info"));
|
|
10413
11435
|
break;
|
|
10414
11436
|
case "delete":
|
|
10415
|
-
await deleteProject(rawArgs,
|
|
11437
|
+
await deleteProject(rawArgs, resolveProjectArg(rawArgs, "delete"));
|
|
10416
11438
|
break;
|
|
10417
11439
|
case "--help":
|
|
10418
11440
|
printCloudHelp();
|
|
@@ -10534,19 +11556,20 @@ ${chalk.bold("Options")}
|
|
|
10534
11556
|
`.trim());
|
|
10535
11557
|
}
|
|
10536
11558
|
async function appsCommand(subcommand, rawArgs = []) {
|
|
10537
|
-
|
|
10538
|
-
"--json": Boolean,
|
|
10539
|
-
"--force": Boolean,
|
|
10540
|
-
"--help": Boolean,
|
|
10541
|
-
"-h": "--help"
|
|
10542
|
-
}, {
|
|
10543
|
-
argv: rawArgs.slice(3),
|
|
10544
|
-
permissive: true
|
|
10545
|
-
});
|
|
10546
|
-
if (args["--help"] || !subcommand || subcommand === "--help") {
|
|
11559
|
+
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
10547
11560
|
printHelp$1();
|
|
10548
11561
|
return;
|
|
10549
11562
|
}
|
|
11563
|
+
const { flags: args, positionals } = parseCommandArgs({
|
|
11564
|
+
spec: {
|
|
11565
|
+
"--json": Boolean,
|
|
11566
|
+
"--force": Boolean
|
|
11567
|
+
},
|
|
11568
|
+
rawArgs,
|
|
11569
|
+
commandWords: 1,
|
|
11570
|
+
command: "apps",
|
|
11571
|
+
maxPositionals: 2
|
|
11572
|
+
});
|
|
10550
11573
|
switch (subcommand) {
|
|
10551
11574
|
case "list":
|
|
10552
11575
|
await listApps(Boolean(args["--json"]));
|
|
@@ -10555,7 +11578,7 @@ async function appsCommand(subcommand, rawArgs = []) {
|
|
|
10555
11578
|
await initManifest(Boolean(args["--force"]));
|
|
10556
11579
|
break;
|
|
10557
11580
|
case "config":
|
|
10558
|
-
await printAppConfig(
|
|
11581
|
+
await printAppConfig(positionals[1], Boolean(args["--json"]));
|
|
10559
11582
|
break;
|
|
10560
11583
|
default:
|
|
10561
11584
|
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
@@ -10741,7 +11764,7 @@ async function entry(args) {
|
|
|
10741
11764
|
printHelp();
|
|
10742
11765
|
return;
|
|
10743
11766
|
}
|
|
10744
|
-
const effectiveSubcommand = parsedArgs["--help"] ? "--help" : subcommand;
|
|
11767
|
+
const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
|
|
10745
11768
|
switch (command) {
|
|
10746
11769
|
case "init":
|
|
10747
11770
|
await createRebaseApp(args);
|
|
@@ -10860,9 +11883,11 @@ ${chalk.green.bold("API Keys")}
|
|
|
10860
11883
|
${chalk.blue.bold("api-keys list")} List all service API keys
|
|
10861
11884
|
${chalk.blue.bold("api-keys create")} Create a new scoped API key
|
|
10862
11885
|
${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
|
|
10863
|
-
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
10864
11886
|
${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
|
|
10865
11887
|
|
|
11888
|
+
${chalk.green.bold("Usage sharing")}
|
|
11889
|
+
${chalk.blue.bold("telemetry")} Anonymous usage sharing (opt-in, off by default)
|
|
11890
|
+
|
|
10866
11891
|
${chalk.green.bold("Rebase Cloud")}
|
|
10867
11892
|
${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
|
|
10868
11893
|
${chalk.blue.bold("cloud link")} Link this directory to a cloud project
|
|
@@ -10893,6 +11918,6 @@ function telemetryNotice() {
|
|
|
10893
11918
|
return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
|
|
10894
11919
|
}
|
|
10895
11920
|
//#endregion
|
|
10896
|
-
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_PORT_FILENAME, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveStartPort, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
11921
|
+
export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, DEV_FLAGS, DEV_PORT_FILENAME, INIT_FLAGS, MANIFEST_FILENAME, ManifestError, RESET_PASSWORD_FLAGS, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectFrameworkDepDrift, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, getProjectPort, isIdentifierLike, isPnpmAvailable, isPortAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, positionals, printInitHelp, readEnvFile, renderPayload, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveCliVersion, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveResetPasswordArgs, resolveRuntimeImageTag, resolveStartPort, resolveTsx, schemaCommand, selectUserForEmail, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
|
|
10897
11922
|
|
|
10898
11923
|
//# sourceMappingURL=index.es.js.map
|