@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.
Files changed (36) hide show
  1. package/dist/bundle.d.ts +4 -3
  2. package/dist/commands/api-keys.d.ts +51 -0
  3. package/dist/commands/auth.d.ts +62 -0
  4. package/dist/commands/cloud/context.d.ts +56 -7
  5. package/dist/commands/cloud/databases.d.ts +39 -0
  6. package/dist/commands/cloud/debug.d.ts +1 -0
  7. package/dist/commands/cloud/deployments.d.ts +22 -0
  8. package/dist/commands/cloud/domains.d.ts +10 -0
  9. package/dist/commands/cloud/env.d.ts +51 -0
  10. package/dist/commands/cloud/extensions.d.ts +21 -0
  11. package/dist/commands/cloud/orgs.d.ts +1 -0
  12. package/dist/commands/cloud/projects.d.ts +29 -0
  13. package/dist/commands/cloud/resources.d.ts +14 -0
  14. package/dist/commands/cloud/settings.d.ts +1 -0
  15. package/dist/commands/dev.d.ts +17 -6
  16. package/dist/commands/eject.d.ts +42 -0
  17. package/dist/commands/init.d.ts +67 -0
  18. package/dist/commands/skills.d.ts +81 -0
  19. package/dist/fold-static.d.ts +47 -0
  20. package/dist/index.es.js +1440 -415
  21. package/dist/index.es.js.map +1 -1
  22. package/dist/manifest.d.ts +16 -1
  23. package/dist/telemetry/payload.d.ts +1 -1
  24. package/dist/utils/args.d.ts +76 -0
  25. package/dist/utils/collection-drift.d.ts +27 -0
  26. package/dist/utils/project.d.ts +20 -0
  27. package/package.json +7 -7
  28. package/templates/eject/Dockerfile +29 -4
  29. package/templates/eject/backend/src/index.ts +49 -5
  30. package/templates/eject/docker-compose.custom.yml +13 -5
  31. package/templates/overlays/baas/backend/tsconfig.json +6 -1
  32. package/templates/template/.env.example +13 -4
  33. package/templates/template/backend/functions/hello.ts +8 -4
  34. package/templates/template/backend/tsconfig.json +6 -1
  35. package/templates/template/config/package.json +1 -0
  36. 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
- * Positional tokens after `rebase cloud` `[group, action, arg1, …]`.
831
- *
832
- * Deliberately NOT `arg({}, { permissive: true })._`: in permissive mode `arg`
833
- * pushes UNKNOWN FLAGS onto `_` too, so `rollback --yes --json` would report
834
- * `--yes` as the deployment id. Operand extraction must see operands only, so
835
- * anything starting with `-` is dropped the same filter the db backup handler
836
- * has always used.
837
- */
838
- function cloudPositionals(rawArgs) {
839
- return rawArgs.slice(3).filter((a) => !a.startsWith("-"));
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
- const pkg = createRequire(import.meta.url)("../../package.json");
955
- return typeof pkg?.version === "string" ? pkg.version : "unknown";
956
- } catch {
957
- return "unknown";
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 (rawArgs.includes("--help") || rawArgs.includes("-h")) {
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 = arg({
1469
- "--git": Boolean,
1470
- "--install": Boolean,
1471
- "--database-url": String,
1472
- "--introspect": Boolean,
1473
- "--template": String,
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 = args._[0];
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
- await execa("git", ["add", "-A"], { cwd: options.targetDirectory });
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
- async function isPortAvailable(port) {
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(false);
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
- const runtimeVersion = readCliVersion();
2035
- envContent = /^#?\s*REBASE_VERSION=.*$/m.test(envContent) ? envContent.replace(/^#?\s*REBASE_VERSION=.*$/m, `REBASE_VERSION=${runtimeVersion}`) : `${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.\nREBASE_VERSION=${runtimeVersion}\n`;
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://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
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, "utf-8");
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
- const files = generateSDK(collections);
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
- console.log(chalk.gray(` const { data } = await rebase.data.collection('${exampleSlug}').find();`));
2356
- if (isIdentifierLike(exampleSlug)) console.log(chalk.gray(` // …or in property style: rebase.data.${exampleSlug}.find()`));
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.schema_generate", { subcommand: subcommand ?? "none" }, { projectRoot });
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.db_push", { subcommand: subcommand ?? "none" }, { projectRoot });
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` to make it the entrypoint.");
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
- /** Write a manifest, with a trailing newline so it plays well with other tools. */
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: manifest.$schema ?? "https://rebase.pro/schemas/rebase.json",
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) return parseInt(process.env.PORT, 10);
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 = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
3177
- if (saved > 0 && saved < 65536) return 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
- const args = arg({
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
- console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
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(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
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(filename.padEnd(31)) + chalk.yellow("│"),
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, -p")} Backend port (default: auto-detected per project)
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 keep its own entrypoint runs `rebase eject`, which
4249
- * writes the entrypoint, a Dockerfile and a compose file together and flips the
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` to make this file the entrypoint and own the image."));
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
- const args = arg({
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["--out"],
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["--out"]);
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
- const args = arg({
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 requested = args._.slice(1).find((value) => !value.startsWith("-"));
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 && !file.overwrite ? "keep" : "write"
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) console.log(item.action === "write" ? ` ${chalk.green("write")} ${item.to}` : ` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
5085
- console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
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").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
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
- const args = arg({
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
- async function resetPassword(rawArgs) {
5306
- const args = arg({
5307
- "--email": String,
5308
- "--password": String,
5309
- "-e": "--email",
5310
- "-p": "--password"
5311
- }, {
5312
- argv: rawArgs.slice(4),
5313
- permissive: true
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
- const email = args["--email"] || args._[0];
5316
- const newPassword = args["--password"] || args._[1];
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
- if (envFile && fs.existsSync(envFile)) try {
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=1`;
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
- let userId;
5359
- if (Array.isArray(searchData)) {
5360
- const firstUser = searchData[0];
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
- } else {
5455
- console.log("✗ User not found: " + email);
6143
+ process.exit(0);
5456
6144
  }
5457
- process.exit(0);
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
- try {
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
- /** Supported agent environments and their target directories. */
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
- /** Read all skill directories and return their names + content. */
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 skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
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
- content: fs.readFileSync(skillMdPath, "utf-8")
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 { fileName, content } = agent.transformFile(skill.name, skill.content);
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 count;
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 given.
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
- console.log(` ${chalk.green("✓")} ${chalk.bold(agent.label)} ${count} skills installed to ${chalk.gray(shown)}`);
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
- Available: ${Object.keys(AGENTS).join(", ")}
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
- function loadEnv(projectRoot) {
5770
- const envFile = findEnvFile(projectRoot);
5771
- const env = {};
5772
- if (envFile && fs.existsSync(envFile)) {
5773
- const content = fs.readFileSync(envFile, "utf-8");
5774
- for (const line of content.split("\n")) {
5775
- const trimmed = line.trim();
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
- async function createKey(rawArgs) {
5863
- const args = arg({
5864
- "--name": String,
5865
- "--permissions": String,
5866
- "--full-access": Boolean,
5867
- "--admin": Boolean,
5868
- "--rate-limit": Number,
5869
- "--expires": String,
5870
- "-n": "--name"
5871
- }, {
5872
- argv: rawArgs.slice(4),
5873
- permissive: true
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
- const name = args["--name"] || args._[0];
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
- async function revokeKey(rawArgs) {
5977
- const args = arg({ "--id": String }, {
5978
- argv: rawArgs.slice(4),
5979
- permissive: true
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
- const id = args["--id"] || args._[0];
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
- switch (rawArgs.slice(3).filter((a) => !a.startsWith("-"))[0]) {
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 = arg({
6491
- "--name": String,
6492
- "--subdomain": String,
6493
- "--repo": String,
6494
- "--branch": String,
6495
- "--provider": String,
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 = arg({
6605
- "--yes": Boolean,
6606
- "-y": "--yes"
6607
- }, {
6608
- argv: rawArgs.slice(2),
6609
- permissive: true
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 = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2] || "list";
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 = cloudPositionals(rawArgs).slice(3)[0];
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 = cloudPositionals(rawArgs).slice(3)[0];
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 = arg({
7827
- "--target": String,
7828
- "--yes": Boolean,
7829
- "-y": "--yes",
7830
- "--project": String,
7831
- "-p": "--project"
7832
- }, {
7833
- argv: rawArgs.slice(2),
7834
- permissive: true
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 = cloudPositionals(rawArgs).slice(2)[0] || "status";
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
- async function setEnv(rawArgs) {
8070
- const args = arg({
8071
- "--secret": Boolean,
8072
- "--force": Boolean,
8073
- "--project": String,
8074
- "-p": "--project"
8075
- }, {
8076
- argv: rawArgs.slice(2),
8077
- permissive: true
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
- async function unsetEnv(rawArgs) {
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 = arg({
8165
- "--out": String,
8166
- "--yes": Boolean,
8167
- "-y": "--yes",
8168
- "--project": String,
8169
- "-p": "--project"
8170
- }, {
8171
- argv: rawArgs.slice(2),
8172
- permissive: true
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["--out"] || ".env");
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 = arg({
8410
- "--yes": Boolean,
8411
- "-y": "--yes",
8412
- "--project": String,
8413
- "-p": "--project"
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
- async function enableExtension(rawArgs) {
8528
- const args = arg({
8529
- "--yes": Boolean,
8530
- "-y": "--yes",
8531
- "--project": String,
8532
- "-p": "--project"
8533
- }, {
8534
- argv: rawArgs.slice(2),
8535
- permissive: true
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 = arg({
8579
- "--yes": Boolean,
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 = arg({
8869
- "--limit": Number,
8870
- "--all": Boolean,
8871
- "--project": String,
8872
- "-p": "--project"
8873
- }, {
8874
- argv: rawArgs.slice(2),
8875
- permissive: true
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
- async function rollbackCommand(rawArgs) {
8917
- const args = arg({
8918
- "--yes": Boolean,
8919
- "-y": "--yes",
8920
- "--project": String,
8921
- "-p": "--project"
8922
- }, {
8923
- argv: rawArgs.slice(2),
8924
- permissive: true
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 = arg({
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 = arg({
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
- const id = rawArgs.slice(3).filter((a) => !a.startsWith("-"))[2];
9986
- if (!id) fail("Usage: rebase cloud webhooks delete <id>");
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 = arg({
10087
- "--bucket": String,
10088
- "--access-key-id": String,
10089
- "--secret-access-key": String,
10090
- "--endpoint": String,
10091
- "--region": String,
10092
- "--force-path-style": Boolean
10093
- }, {
10094
- argv: rawArgs.slice(3),
10095
- permissive: true
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 || subcommand === "--help") {
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, positionals(rawArgs)[2] || requireProjectRef(rawArgs));
11434
+ await projectInfo(rawArgs, resolveProjectArg(rawArgs, "info"));
10413
11435
  break;
10414
11436
  case "delete":
10415
- await deleteProject(rawArgs, positionals(rawArgs)[2] || requireProjectRef(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
- const args = arg({
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(args._[1], Boolean(args["--json"]));
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