@rebasepro/cli 0.11.1-canary.gfd39654 → 0.12.1-canary.g181d0fe

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 (40) hide show
  1. package/bin/rebase.js +21 -0
  2. package/dist/bundle.d.ts +24 -7
  3. package/dist/commands/eject.d.ts +1 -0
  4. package/dist/commands/init.d.ts +19 -15
  5. package/dist/fold-static.d.ts +41 -15
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +907 -252
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/manifest.d.ts +27 -8
  10. package/package.json +11 -11
  11. package/runtime/dev-server.mjs +0 -1
  12. package/templates/{template/backend → eject}/Dockerfile +16 -4
  13. package/templates/{template → eject}/backend/src/env.ts +0 -1
  14. package/templates/{template → eject}/backend/src/index.ts +41 -27
  15. package/templates/eject/docker-compose.custom.yml +71 -0
  16. package/templates/overlays/baas/backend/package.json +3 -6
  17. package/templates/overlays/baas/backend/tsconfig.json +8 -2
  18. package/templates/overlays/baas/config/index.ts +15 -0
  19. package/templates/overlays/baas/config/package.json +28 -0
  20. package/templates/overlays/baas/package.json +3 -3
  21. package/templates/overlays/baas/pnpm-workspace.yaml +1 -0
  22. package/templates/overlays/baas/rebase.json +2 -6
  23. package/templates/template/.env.example +15 -0
  24. package/templates/template/README.md +56 -22
  25. package/templates/template/ai-instructions.md +1 -0
  26. package/templates/template/backend/functions/hello.ts +45 -14
  27. package/templates/template/backend/package.json +3 -6
  28. package/templates/template/backend/tsconfig.json +23 -2
  29. package/templates/template/config/tsconfig.json +16 -1
  30. package/templates/template/docker-compose.yml +62 -38
  31. package/templates/template/frontend/package.json +3 -3
  32. package/templates/template/frontend/src/App.tsx +2 -1
  33. package/templates/template/frontend/src/main.tsx +10 -2
  34. package/templates/template/frontend/vite.config.ts +5 -1
  35. package/templates/template/package.json +1 -2
  36. package/templates/template/rebase.json +5 -8
  37. package/templates/overlays/baas/backend/src/index.ts +0 -216
  38. package/templates/template/frontend/Dockerfile +0 -52
  39. package/templates/template/frontend/nginx.conf +0 -40
  40. /package/templates/overlays/baas/{backend/src → config}/storage.ts +0 -0
package/dist/index.es.js CHANGED
@@ -12,7 +12,7 @@ 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 { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections } from "@rebasepro/types";
15
+ import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
16
16
  import { generateSDK } from "@rebasepro/codegen";
17
17
  import { createRequire } from "module";
18
18
  //#region src/utils/package-manager.ts
@@ -355,8 +355,9 @@ function requireProjectRoot() {
355
355
  const root = findProjectRoot();
356
356
  if (!root) {
357
357
  console.error(chalk.red("✗ Could not find a Rebase project root."));
358
- console.error(chalk.gray(" Make sure you are inside a Rebase project directory"));
359
- console.error(chalk.gray(" (one with backend/, frontend/, and config/ directories)."));
358
+ console.error(chalk.gray(` Looked in this directory and every parent for a ${MANIFEST_FILENAME},`));
359
+ console.error(chalk.gray(" a package.json with a \"backend\" workspace, or a backend/ next to a config/."));
360
+ console.error(chalk.gray(" Run this from inside a project, or create one with `rebase init`."));
360
361
  process.exit(1);
361
362
  }
362
363
  return root;
@@ -829,7 +830,7 @@ function openUrl(target, label = "Opening") {
829
830
  //#region src/commands/init.ts
830
831
  var access = promisify(fs.access);
831
832
  var __filename$1 = fileURLToPath(import.meta.url);
832
- var __dirname$1 = path.dirname(__filename$1);
833
+ var __dirname$2 = path.dirname(__filename$1);
833
834
  function findParentDir(currentDir, targetName) {
834
835
  const root = path.parse(currentDir).root;
835
836
  while (currentDir && currentDir !== root) {
@@ -838,22 +839,56 @@ function findParentDir(currentDir, targetName) {
838
839
  }
839
840
  return null;
840
841
  }
841
- var cliRoot = findParentDir(__dirname$1, "cli");
842
+ var cliRoot = findParentDir(__dirname$2, "cli");
842
843
  var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
844
+ /**
845
+ * Every scaffolded file that carries a `{{PLACEHOLDER}}`.
846
+ *
847
+ * Exported because it is the single source of truth: `init.test.ts` reads it and
848
+ * asserts that every template file containing `{{` appears here. It used to be a
849
+ * local, with the test harness keeping a *second* copy — so the two drifted, and
850
+ * a test built on the copy could not observe the production list at all.
851
+ *
852
+ * The drift shipped: `docker-compose.yml` arrived with the self-host work
853
+ * (26fd5259c) and was added to neither, so every scaffolded project got a literal
854
+ * `name: {{PROJECT_NAME}}`. In YAML `{{...}}` is a map, not a string, so the
855
+ * documented `docker compose up` path failed on the file before doing anything:
856
+ *
857
+ * yaml: unmarshal errors: line 28: cannot unmarshal !!map into string
858
+ */
859
+ var TEMPLATE_PLACEHOLDER_FILES = [
860
+ "package.json",
861
+ "frontend/package.json",
862
+ "backend/package.json",
863
+ "config/package.json",
864
+ "frontend/index.html",
865
+ "pnpm-workspace.yaml",
866
+ "docker-compose.yml",
867
+ "README.md"
868
+ ];
843
869
  /** Returns an error message, or null when the name is a valid package name. */
844
870
  function validateProjectName(name) {
845
871
  if (!name.trim()) return "Project name is required";
846
872
  if (!PROJECT_NAME_RE.test(name)) return "Project name must start with a lowercase letter or number and contain only lowercase letters, numbers, hyphens, dots, or underscores";
847
873
  return null;
848
874
  }
849
- var FLAVOR_CHOICES = [{
850
- name: "BaaS + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
851
- value: "cms",
852
- short: "BaaS + admin"
875
+ /**
876
+ * Whether to scaffold the admin panel alongside the backend.
877
+ *
878
+ * A boolean, not a named pair. It was `--flavor cms|baas`, and neither word
879
+ * survived what they described: "cms" is a product category rather than a thing
880
+ * this tool builds, and "baas" was the same value that used to appear as
881
+ * `backend.mode` — which is now derived from whether collections are declared.
882
+ * What is left is one question: does this project get an admin UI?
883
+ */
884
+ var HEADLESS_CHOICES = [{
885
+ name: "Backend + admin — API plus an admin UI, driven by collections you define (like Payload/Directus)",
886
+ value: false,
887
+ short: "Backend + admin"
853
888
  }, {
854
- name: "BaaS only — headless API over your database. No collections, no UI (like Supabase)",
855
- value: "baas",
856
- short: "BaaS only"
889
+ name: "Backend only — headless API over your database. No collections, no UI (like Supabase)",
890
+ value: true,
891
+ short: "Backend only"
857
892
  }];
858
893
  var PRESET_CHOICES = [
859
894
  {
@@ -878,7 +913,7 @@ var PRESET_CHOICES = [
878
913
  * types registered by the installed version of inquirer.
879
914
  */
880
915
  function buildInitQuestions(params) {
881
- const { nameArg, templateArg, flavorArg, hasGitFlag, hasInstallFlag, pm } = params;
916
+ const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
882
917
  const questions = [];
883
918
  if (!nameArg) questions.push({
884
919
  type: "input",
@@ -887,12 +922,12 @@ function buildInitQuestions(params) {
887
922
  default: "my-rebase-app",
888
923
  validate: (input) => validateProjectName(input) ?? true
889
924
  });
890
- if (!flavorArg) questions.push({
925
+ if (headlessArg === void 0) questions.push({
891
926
  type: "select",
892
- name: "flavor",
927
+ name: "headless",
893
928
  message: "What do you want to build?",
894
- choices: FLAVOR_CHOICES,
895
- default: "cms"
929
+ choices: HEADLESS_CHOICES,
930
+ default: false
896
931
  });
897
932
  if (!templateArg) questions.push({
898
933
  type: "select",
@@ -900,7 +935,7 @@ function buildInitQuestions(params) {
900
935
  message: "Choose a starter template:",
901
936
  choices: PRESET_CHOICES,
902
937
  default: "blog",
903
- when: (answers) => (flavorArg ?? answers.flavor) !== "baas"
938
+ when: (answers) => !(headlessArg ?? answers.headless)
904
939
  });
905
940
  if (!hasGitFlag) questions.push({
906
941
  type: "confirm",
@@ -956,7 +991,7 @@ ${chalk.bold("Usage")}
956
991
 
957
992
  ${chalk.bold("Options")}
958
993
  ${chalk.blue("-t, --template")} ${chalk.gray("<preset>")} blog | ecommerce | blank ${chalk.gray("(default: blog)")}
959
- ${chalk.blue("-f, --flavor")} ${chalk.gray("<flavor>")} cms | baas ${chalk.gray("(default: cms)")}
994
+ ${chalk.blue("--headless")} Backend only — no admin panel, no collections
960
995
  ${chalk.blue("-y, --yes")} Accept defaults, never prompt ${chalk.gray("(required for CI / non-TTY)")}
961
996
  ${chalk.blue("-i, --install")} Install dependencies after scaffolding
962
997
  ${chalk.blue("-g, --git")} Initialize a git repository and make an initial commit
@@ -965,14 +1000,14 @@ ${chalk.bold("Options")}
965
1000
  ${chalk.blue("--project")} ${chalk.gray("<slug>")} Link the scaffold to a Rebase Cloud project
966
1001
  ${chalk.blue("--setup-key")} ${chalk.gray("<key>")} One-time key authenticating the cloud link ${chalk.gray("(use with --project)")}
967
1002
 
968
- ${chalk.bold("Flavors")}
969
- ${chalk.blue("cms")} BaaS + admin UI, driven by collections you define ${chalk.gray("(like Payload/Directus)")}
970
- ${chalk.blue("baas")} Headless API over your database — no collections, no UI ${chalk.gray("(like Supabase)")}
971
- ${chalk.gray("--template has no effect on this flavor.")}
1003
+ ${chalk.bold("What gets scaffolded")}
1004
+ ${chalk.gray("default")} Backend + an admin UI, driven by collections you define ${chalk.gray("(like Payload/Directus)")}
1005
+ ${chalk.blue("--headless")} Backend only, over your existing database ${chalk.gray("(like Supabase)")}
1006
+ ${chalk.gray("--template has no effect: there are no collections to seed.")}
972
1007
 
973
1008
  ${chalk.bold("Examples")}
974
1009
  ${chalk.gray("$")} rebase init my-shop --template ecommerce --install
975
- ${chalk.gray("$")} rebase init my-api --flavor baas --yes
1010
+ ${chalk.gray("$")} rebase init my-api --headless --yes
976
1011
  ${chalk.gray("$")} rebase init . --yes --git
977
1012
  `);
978
1013
  }
@@ -993,14 +1028,13 @@ async function promptForOptions(rawArgs, pm) {
993
1028
  "--database-url": String,
994
1029
  "--introspect": Boolean,
995
1030
  "--template": String,
996
- "--flavor": String,
1031
+ "--headless": Boolean,
997
1032
  "--project": String,
998
1033
  "--setup-key": String,
999
1034
  "--yes": Boolean,
1000
1035
  "-g": "--git",
1001
1036
  "-i": "--install",
1002
1037
  "-t": "--template",
1003
- "-f": "--flavor",
1004
1038
  "-y": "--yes"
1005
1039
  }, {
1006
1040
  argv: rawArgs.slice(3),
@@ -1021,11 +1055,7 @@ async function promptForOptions(rawArgs, pm) {
1021
1055
  console.error(chalk.red(`Unknown template "${templateArg}". Available: ${PRESET_CHOICES.map((p) => p.value).join(", ")}`));
1022
1056
  process.exit(1);
1023
1057
  }
1024
- const flavorArg = args["--flavor"];
1025
- if (flavorArg && !FLAVOR_CHOICES.some((f) => f.value === flavorArg)) {
1026
- console.error(chalk.red(`Unknown flavor "${flavorArg}". Available: ${FLAVOR_CHOICES.map((f) => f.value).join(", ")}`));
1027
- process.exit(1);
1028
- }
1058
+ const headlessArg = args["--headless"] === true ? true : void 0;
1029
1059
  if (isNonInteractive) {
1030
1060
  const projectName = nameArg || "my-rebase-app";
1031
1061
  const targetDirectory = path.resolve(process.cwd(), projectName);
@@ -1041,7 +1071,7 @@ async function promptForOptions(rawArgs, pm) {
1041
1071
  introspect: args["--introspect"] || false,
1042
1072
  preset: templateArg || "blog",
1043
1073
  explicitPreset: !!templateArg,
1044
- flavor: flavorArg || "cms",
1074
+ headless: headlessArg ?? false,
1045
1075
  pm,
1046
1076
  pmCommands,
1047
1077
  cloudProject: args["--project"] || void 0,
@@ -1052,14 +1082,14 @@ async function promptForOptions(rawArgs, pm) {
1052
1082
  if (!process.stdin.isTTY) {
1053
1083
  console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
1054
1084
  console.error(chalk.yellow(" Re-run with --yes to accept defaults, passing any choices as flags, e.g.:"));
1055
- console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog --flavor cms`));
1056
- console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --flavor <cms|baas> --database-url <url> --install --git"));
1085
+ console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog`));
1086
+ console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --headless --database-url <url> --install --git"));
1057
1087
  process.exit(1);
1058
1088
  }
1059
1089
  const questions = buildInitQuestions({
1060
1090
  nameArg,
1061
1091
  templateArg,
1062
- flavorArg,
1092
+ headlessArg,
1063
1093
  hasGitFlag: !!args["--git"],
1064
1094
  hasInstallFlag: !!args["--install"],
1065
1095
  pm
@@ -1079,7 +1109,7 @@ async function promptForOptions(rawArgs, pm) {
1079
1109
  introspect: answers.introspect || false,
1080
1110
  preset: templateArg || answers.preset || "blog",
1081
1111
  explicitPreset: !!templateArg,
1082
- flavor: flavorArg || answers.flavor || "cms",
1112
+ headless: headlessArg ?? Boolean(answers.headless),
1083
1113
  pm,
1084
1114
  pmCommands,
1085
1115
  cloudProject: args["--project"] || void 0,
@@ -1163,12 +1193,12 @@ async function createProject$1(options) {
1163
1193
  const shipped = path.join(options.targetDirectory, from);
1164
1194
  if (fs.existsSync(shipped)) fs.renameSync(shipped, path.join(options.targetDirectory, to));
1165
1195
  }
1166
- if (options.flavor === "baas" && options.explicitPreset) console.log(chalk.yellow(` Ignoring --template ${options.preset}: the baas flavor has no collections.`));
1167
- if (options.flavor !== "baas") {
1196
+ if (options.headless && options.explicitPreset) console.log(chalk.yellow(` Ignoring --template ${options.preset}: a headless project declares no collections.`));
1197
+ if (!options.headless) {
1168
1198
  if (options.introspect && options.preset !== "blank") console.log(chalk.gray(" Using the blank template: collections will come from your database."));
1169
1199
  await applyPreset(options.targetDirectory, options.introspect ? "blank" : options.preset);
1170
1200
  }
1171
- await applyFlavor(options.targetDirectory, options.flavor);
1201
+ await applyHeadless(options.targetDirectory, options.headless);
1172
1202
  await replacePlaceholders(options);
1173
1203
  await configureEnvFile(options.targetDirectory, options.databaseUrl);
1174
1204
  if (options.git) {
@@ -1266,7 +1296,7 @@ async function createProject$1(options) {
1266
1296
  console.log("");
1267
1297
  const runDev = pmCommands.run("dev");
1268
1298
  const runDbPush = pmCommands.run("db:push");
1269
- const isBaas = options.flavor === "baas";
1299
+ const isBaas = options.headless;
1270
1300
  const cdTarget = formatCdTarget(process.cwd(), options.targetDirectory);
1271
1301
  if (cdTarget) console.log(` ${chalk.cyan("cd")} ${cdTarget}`);
1272
1302
  if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
@@ -1336,18 +1366,31 @@ async function createProject$1(options) {
1336
1366
  * presets directory so the final project is clean.
1337
1367
  */
1338
1368
  /**
1339
- * Reduce the scaffolded project to the chosen flavor.
1369
+ * Reduce the scaffolded project to the chosen shape.
1370
+ *
1371
+ * The base template is the full triad. `--headless` drops the frontend and the
1372
+ * declared collections — there is nothing to define, since the server derives
1373
+ * its API from the database — and overlays the files that differ.
1340
1374
  *
1341
- * The base template is the full CMS triad. `baas` drops the frontend and the
1342
- * collections config entirely — there is nothing to define, since the server
1343
- * derives its API from the database — and overlays the files that differ.
1375
+ * The config *package* stays, holding only `storageAuthorize`. Storage is not
1376
+ * under row-level security, so a deployment with file storage enabled and no
1377
+ * access model serves every user's files to every signed-in user; the server
1378
+ * refuses to boot in that state. Deleting the package outright would leave a
1379
+ * headless project with nowhere to put the hook, and the scaffold's own
1380
+ * docker-compose.yml enables storage — so the first `docker compose up` would
1381
+ * crash-loop.
1344
1382
  */
1345
- async function applyFlavor(targetDirectory, flavor) {
1346
- if (flavor !== "baas") return;
1347
- for (const dir of ["frontend", "config"]) fs.rmSync(path.join(targetDirectory, dir), {
1383
+ async function applyHeadless(targetDirectory, headless) {
1384
+ if (!headless) return;
1385
+ fs.rmSync(path.join(targetDirectory, "frontend"), {
1348
1386
  recursive: true,
1349
1387
  force: true
1350
1388
  });
1389
+ fs.rmSync(path.join(targetDirectory, "config", "collections"), {
1390
+ recursive: true,
1391
+ force: true
1392
+ });
1393
+ for (const stray of ["admin.d.ts", "frontend-assets.d.ts"]) fs.rmSync(path.join(targetDirectory, "config", stray), { force: true });
1351
1394
  fs.rmSync(path.join(targetDirectory, "backend", "src", "schema.generated.ts"), { force: true });
1352
1395
  const overlayDir = path.resolve(cliRoot, "templates", "overlays", "baas");
1353
1396
  if (!fs.existsSync(overlayDir)) {
@@ -1394,15 +1437,7 @@ function cleanupPresets(presetsDir) {
1394
1437
  });
1395
1438
  }
1396
1439
  async function replacePlaceholders(options) {
1397
- const filesToProcess = [
1398
- "package.json",
1399
- "frontend/package.json",
1400
- "backend/package.json",
1401
- "config/package.json",
1402
- "frontend/index.html",
1403
- "pnpm-workspace.yaml",
1404
- "README.md"
1405
- ];
1440
+ const filesToProcess = TEMPLATE_PLACEHOLDER_FILES;
1406
1441
  const packageJsonPath = path.resolve(cliRoot, "package.json");
1407
1442
  let cliVersion = "latest";
1408
1443
  if (fs.existsSync(packageJsonPath)) cliVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).version || "latest";
@@ -1427,9 +1462,10 @@ async function replacePlaceholders(options) {
1427
1462
  versionToUse = stdout.trim();
1428
1463
  } catch {
1429
1464
  try {
1465
+ const tag = cliVersion.includes("canary") ? "canary" : "latest";
1430
1466
  const { stdout } = await execa(viewBin, [
1431
1467
  "view",
1432
- `${pkgName}@${cliVersion.includes("canary") ? "canary" : "latest"}`,
1468
+ `${pkgName}@${tag}`,
1433
1469
  "version"
1434
1470
  ]);
1435
1471
  if (!stdout.trim()) throw new Error("Not found");
@@ -1497,6 +1533,26 @@ async function findAvailablePort(startPort) {
1497
1533
  while (!await isPortAvailable(port)) port++;
1498
1534
  return port;
1499
1535
  }
1536
+ /**
1537
+ * The CLI's own version, which is the runtime image tag a scaffolded project
1538
+ * pins. They move together: the CLI, the packages and the runtime image are cut
1539
+ * from one release, so the version that installed the project is the version
1540
+ * whose runtime can boot its bundle.
1541
+ *
1542
+ * Falls back to `latest` only when the CLI cannot read its own manifest — a
1543
+ * floating tag is worse than a pinned one, but better than a compose file that
1544
+ * names a version that was never published.
1545
+ */
1546
+ function readCliVersion() {
1547
+ try {
1548
+ const manifest = path.resolve(cliRoot, "package.json");
1549
+ if (fs.existsSync(manifest)) {
1550
+ const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
1551
+ if (typeof pkg.version === "string" && pkg.version) return pkg.version;
1552
+ }
1553
+ } catch {}
1554
+ return "latest";
1555
+ }
1500
1556
  async function configureEnvFile(targetDirectory, databaseUrl) {
1501
1557
  const envExamplePath = path.join(targetDirectory, ".env.example");
1502
1558
  const envPath = path.join(targetDirectory, ".env");
@@ -1506,8 +1562,15 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
1506
1562
  const dbPassword = crypto.randomBytes(16).toString("hex");
1507
1563
  const serviceKey = crypto.randomBytes(48).toString("base64");
1508
1564
  let envContent = fs.readFileSync(envPath, "utf-8");
1565
+ envContent = envContent.replace(/^(# ║ {2})(Copy this file to \.env and fill in the values)( +║)$/m, (_match, open, old, close) => {
1566
+ const replacement = "Generated by `rebase init` — the secrets below are already set";
1567
+ const width = old.length + close.length - 1;
1568
+ return open + replacement.slice(0, width).padEnd(width, " ") + "║";
1569
+ });
1509
1570
  envContent = envContent.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET=${jwtSecret}`);
1510
1571
  envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
1572
+ const runtimeVersion = readCliVersion();
1573
+ 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`;
1511
1574
  if (databaseUrl) {
1512
1575
  if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
1513
1576
  envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}\nDATABASE_PASSWORD=${dbPassword}`);
@@ -2014,21 +2077,51 @@ var ManifestError = class extends Error {
2014
2077
  this.name = "ManifestError";
2015
2078
  }
2016
2079
  };
2017
- var APP_TYPES = [
2018
- "backend",
2019
- "static",
2020
- "admin",
2021
- "mobile",
2022
- "custom"
2023
- ];
2080
+ var APP_TYPES = ["backend", "static"];
2081
+ /**
2082
+ * App types that used to exist, and what replaced each one.
2083
+ *
2084
+ * Kept so a manifest written against the old vocabulary fails with the fix in
2085
+ * hand rather than with `must be one of: backend, static`, which tells a reader
2086
+ * what is wrong but not what to write instead.
2087
+ */
2088
+ var REMOVED_APP_TYPES = {
2089
+ admin: "the admin panel is an ordinary static app now — declare it as { \"type\": \"static\", \"root\": \"frontend\", \"output\": \"frontend/dist\", \"path\": \"/admin\" }",
2090
+ custom: "who owns the server is a property of the backend now — declare { \"type\": \"backend\", \"runtime\": \"custom\" } instead of a separate app",
2091
+ mobile: "mobile apps are no longer declared in the manifest — nothing consumed this type. Remove the entry"
2092
+ };
2024
2093
  /** Reserved because they name things in URLs and CLI output. */
2025
- var RESERVED_APP_NAMES = new Set([
2094
+ var RESERVED_APP_NAMES = /* @__PURE__ */ new Set([
2026
2095
  "api",
2027
2096
  "health",
2028
2097
  "metrics",
2029
2098
  "livez",
2030
2099
  "_rebase"
2031
2100
  ]);
2101
+ /**
2102
+ * Validate a static app's public base path.
2103
+ *
2104
+ * Normalizes to "no trailing slash, except for the root" so that mounting and
2105
+ * the `REBASE_APP_BASE` build variable have one shape to reason about.
2106
+ */
2107
+ function checkAppPath(value, fieldPath, issues) {
2108
+ if (value === void 0) return void 0;
2109
+ if (typeof value !== "string" || !value.startsWith("/") || value.includes("..")) {
2110
+ issues.push({
2111
+ path: fieldPath,
2112
+ message: "must be an absolute path like \"/admin\""
2113
+ });
2114
+ return;
2115
+ }
2116
+ if (value !== "/" && value.endsWith("/")) {
2117
+ issues.push({
2118
+ path: fieldPath,
2119
+ message: "must not end with a slash — write \"/admin\", not \"/admin/\""
2120
+ });
2121
+ return;
2122
+ }
2123
+ return value;
2124
+ }
2032
2125
  function isRecord(value) {
2033
2126
  return typeof value === "object" && value !== null && !Array.isArray(value);
2034
2127
  }
@@ -2071,6 +2164,73 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
2071
2164
  }
2072
2165
  return value;
2073
2166
  }
2167
+ /** Fields each app type understands. Anything else is a typo or a newer CLI. */
2168
+ var KNOWN_APP_FIELDS = {
2169
+ backend: [
2170
+ "type",
2171
+ "runtime",
2172
+ "config",
2173
+ "functions",
2174
+ "crons",
2175
+ "schema",
2176
+ "usersCollection",
2177
+ "dockerfile",
2178
+ "context",
2179
+ "port"
2180
+ ],
2181
+ static: [
2182
+ "type",
2183
+ "root",
2184
+ "build",
2185
+ "output",
2186
+ "path",
2187
+ "spa"
2188
+ ]
2189
+ };
2190
+ /**
2191
+ * Report a field this CLI does not recognise.
2192
+ *
2193
+ * A warning rather than an error, and the distinction matters in both
2194
+ * directions. `"pathh": "/admin"` is a typo that would otherwise be silently
2195
+ * dropped — the app builds for `/`, mounts at `/`, and the only symptom is that
2196
+ * it is not where you put it. But an unknown field is also what an *older* CLI
2197
+ * sees when it opens a manifest written for a newer one, and refusing to build
2198
+ * over a field that is simply from the future would make every manifest addition
2199
+ * a breaking change.
2200
+ *
2201
+ * So: name it, suggest the near-miss, and carry on.
2202
+ */
2203
+ function warnUnknownFields(name, raw, type) {
2204
+ const known = KNOWN_APP_FIELDS[type];
2205
+ if (!known) return;
2206
+ for (const field of Object.keys(raw)) {
2207
+ if (known.includes(field)) continue;
2208
+ const suggestion = known.find((candidate) => isNearMiss(candidate, field));
2209
+ console.warn(`⚠ rebase.json: apps.${name}.${field} is not a field this CLI knows.` + (suggestion ? ` Did you mean "${suggestion}"?` : " It is ignored — your CLI may be older than this manifest."));
2210
+ }
2211
+ }
2212
+ /** One edit apart, ignoring case: enough for a typo, tight enough to stay quiet. */
2213
+ function isNearMiss(a, b) {
2214
+ const x = a.toLowerCase();
2215
+ const y = b.toLowerCase();
2216
+ if (x === y) return true;
2217
+ if (Math.abs(x.length - y.length) > 1) return false;
2218
+ const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
2219
+ let i = 0;
2220
+ let j = 0;
2221
+ let edits = 0;
2222
+ while (i < shorter.length && j < longer.length) {
2223
+ if (shorter[i] === longer[j]) {
2224
+ i++;
2225
+ j++;
2226
+ continue;
2227
+ }
2228
+ if (++edits > 1) return false;
2229
+ if (shorter.length === longer.length) i++;
2230
+ j++;
2231
+ }
2232
+ return edits + (longer.length - j) + (shorter.length - i) <= 1;
2233
+ }
2074
2234
  function validateApp(name, raw, issues) {
2075
2235
  const base = `apps.${name}`;
2076
2236
  if (!isRecord(raw)) {
@@ -2081,6 +2241,13 @@ function validateApp(name, raw, issues) {
2081
2241
  return;
2082
2242
  }
2083
2243
  const type = raw.type;
2244
+ if (typeof type === "string" && REMOVED_APP_TYPES[type]) {
2245
+ issues.push({
2246
+ path: `${base}.type`,
2247
+ message: `"${type}" is no longer an app type — ${REMOVED_APP_TYPES[type]}`
2248
+ });
2249
+ return;
2250
+ }
2084
2251
  if (typeof type !== "string" || !APP_TYPES.includes(type)) {
2085
2252
  issues.push({
2086
2253
  path: `${base}.type`,
@@ -2088,18 +2255,39 @@ function validateApp(name, raw, issues) {
2088
2255
  });
2089
2256
  return;
2090
2257
  }
2258
+ warnUnknownFields(name, raw, type);
2091
2259
  switch (type) {
2092
- case "backend":
2260
+ case "backend": {
2093
2261
  checkRelativePath(raw.config, `${base}.config`, issues, { required: false });
2094
2262
  checkRelativePath(raw.functions, `${base}.functions`, issues, { required: false });
2095
2263
  checkRelativePath(raw.crons, `${base}.crons`, issues, { required: false });
2096
2264
  checkRelativePath(raw.schema, `${base}.schema`, issues, { required: false });
2097
2265
  checkRelativePath(raw.usersCollection, `${base}.usersCollection`, issues, { required: false });
2098
- if (raw.mode !== void 0 && raw.mode !== "cms" && raw.mode !== "baas") issues.push({
2266
+ if (raw.mode !== void 0) issues.push({
2099
2267
  path: `${base}.mode`,
2100
- message: "must be \"cms\" or \"baas\""
2268
+ message: "is no longer a field — collections come from the config directory when it exists, and are introspected from the database when it does not"
2269
+ });
2270
+ const custom = raw.runtime === "custom";
2271
+ if (raw.runtime !== "managed" && !custom) issues.push({
2272
+ path: `${base}.runtime`,
2273
+ message: "is required, and must be \"managed\" or \"custom\""
2274
+ });
2275
+ for (const field of [
2276
+ "dockerfile",
2277
+ "context",
2278
+ "port"
2279
+ ]) if (raw[field] !== void 0 && !custom) issues.push({
2280
+ path: `${base}.${field}`,
2281
+ message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
2282
+ });
2283
+ checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
2284
+ checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
2285
+ if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
2286
+ path: `${base}.port`,
2287
+ message: "must be an integer"
2101
2288
  });
2102
2289
  return raw;
2290
+ }
2103
2291
  case "static":
2104
2292
  checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
2105
2293
  checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
@@ -2111,37 +2299,7 @@ function validateApp(name, raw, issues) {
2111
2299
  path: `${base}.spa`,
2112
2300
  message: "must be a boolean"
2113
2301
  });
2114
- return raw;
2115
- case "admin": {
2116
- const mode = raw.mode ?? "hosted";
2117
- if (mode !== "hosted" && mode !== "bundled") {
2118
- issues.push({
2119
- path: `${base}.mode`,
2120
- message: "must be \"hosted\" or \"bundled\""
2121
- });
2122
- return;
2123
- }
2124
- if (mode === "bundled") {
2125
- checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
2126
- checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
2127
- }
2128
- return raw;
2129
- }
2130
- case "mobile": {
2131
- const platform = raw.platform;
2132
- if (platform !== "ios" && platform !== "android" && platform !== "other") issues.push({
2133
- path: `${base}.platform`,
2134
- message: "must be \"ios\", \"android\" or \"other\""
2135
- });
2136
- return raw;
2137
- }
2138
- case "custom":
2139
- checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
2140
- checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
2141
- if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
2142
- path: `${base}.port`,
2143
- message: "must be an integer"
2144
- });
2302
+ checkAppPath(raw.path, `${base}.path`, issues);
2145
2303
  return raw;
2146
2304
  default: return;
2147
2305
  }
@@ -2155,10 +2313,13 @@ function validateManifest(raw) {
2155
2313
  path: "",
2156
2314
  message: `${MANIFEST_FILENAME} must contain a JSON object`
2157
2315
  }] };
2158
- if (typeof raw.runtime !== "string" || raw.runtime.trim() === "") issues.push({
2159
- path: "runtime",
2160
- message: `is required, e.g. "^1"`
2161
- });
2316
+ if (typeof raw.rebase !== "string" || raw.rebase.trim() === "") {
2317
+ const message = typeof raw.runtime === "string" ? `is required, e.g. "^1" — this is the top-level "runtime" key renamed, because "runtime" now means "managed" or "custom" on the backend app` : `is required, e.g. "^1"`;
2318
+ issues.push({
2319
+ path: "rebase",
2320
+ message
2321
+ });
2322
+ }
2162
2323
  if (!isRecord(raw.apps)) {
2163
2324
  issues.push({
2164
2325
  path: "apps",
@@ -2192,55 +2353,148 @@ function validateManifest(raw) {
2192
2353
  path: "apps",
2193
2354
  message: "a project may declare at most one backend app"
2194
2355
  });
2356
+ const byPath = /* @__PURE__ */ new Map();
2357
+ for (const [name, app] of Object.entries(apps)) {
2358
+ if (app.type !== "static") continue;
2359
+ const at = app.path ?? "/";
2360
+ const owner = byPath.get(at);
2361
+ if (owner) {
2362
+ issues.push({
2363
+ path: `apps.${name}.path`,
2364
+ message: `two apps cannot serve the same path — "${owner}" is already at "${at}"`
2365
+ });
2366
+ continue;
2367
+ }
2368
+ byPath.set(at, name);
2369
+ }
2370
+ const storage = validateStorageSources(raw.storage, issues);
2195
2371
  if (issues.length > 0) return { issues };
2196
2372
  return {
2197
2373
  manifest: {
2198
2374
  $schema: typeof raw.$schema === "string" ? raw.$schema : void 0,
2199
- runtime: raw.runtime,
2200
- apps
2375
+ rebase: raw.rebase,
2376
+ apps,
2377
+ ...storage ? { storage } : {}
2201
2378
  },
2202
2379
  issues
2203
2380
  };
2204
2381
  }
2205
2382
  /**
2383
+ * Validate the `storage` block — which buckets this project uses.
2384
+ *
2385
+ * Absent means one default source, so `undefined` is a valid answer and not an
2386
+ * issue. Everything else is checked strictly, because each key here becomes the
2387
+ * suffix of a set of environment variables: a key that cannot become a variable
2388
+ * name, or two keys that become the *same* one, are failures worth catching
2389
+ * while someone is looking at the file rather than at a tenant serving one
2390
+ * bucket's files with another's credentials.
2391
+ */
2392
+ function validateStorageSources(raw, issues) {
2393
+ if (raw === void 0) return void 0;
2394
+ if (!isRecord(raw)) {
2395
+ issues.push({
2396
+ path: "storage",
2397
+ message: "must be an object keyed by storage source name"
2398
+ });
2399
+ return;
2400
+ }
2401
+ const sources = {};
2402
+ for (const [key, value] of Object.entries(raw)) {
2403
+ if (!isRecord(value)) {
2404
+ issues.push({
2405
+ path: `storage.${key}`,
2406
+ message: "must be an object"
2407
+ });
2408
+ continue;
2409
+ }
2410
+ if (typeof value.engine !== "string" || value.engine.trim() === "") {
2411
+ issues.push({
2412
+ path: `storage.${key}.engine`,
2413
+ message: "is required, e.g. \"s3\", \"gcs\" or \"local\""
2414
+ });
2415
+ continue;
2416
+ }
2417
+ if (value.transport !== void 0 && value.transport !== "server" && value.transport !== "direct") {
2418
+ issues.push({
2419
+ path: `storage.${key}.transport`,
2420
+ message: "must be \"server\" or \"direct\""
2421
+ });
2422
+ continue;
2423
+ }
2424
+ if (value.label !== void 0 && typeof value.label !== "string") {
2425
+ issues.push({
2426
+ path: `storage.${key}.label`,
2427
+ message: "must be a string"
2428
+ });
2429
+ continue;
2430
+ }
2431
+ try {
2432
+ storageEnvSuffix(key);
2433
+ } catch {
2434
+ issues.push({
2435
+ path: `storage.${key}`,
2436
+ message: "name cannot become an environment variable suffix — use a name containing at least one letter or digit"
2437
+ });
2438
+ continue;
2439
+ }
2440
+ sources[key] = {
2441
+ engine: value.engine,
2442
+ ...value.transport !== void 0 ? { transport: value.transport } : {},
2443
+ ...value.label !== void 0 ? { label: value.label } : {}
2444
+ };
2445
+ }
2446
+ const collision = findStorageSuffixCollision(Object.keys(sources));
2447
+ if (collision) issues.push({
2448
+ path: `storage.${collision.b}`,
2449
+ message: `maps to the same environment variable suffix ("${collision.suffix || "(none)"}") as "${collision.a}", so the two would read each other's configuration — rename one of them`
2450
+ });
2451
+ return Object.keys(sources).length > 0 ? sources : void 0;
2452
+ }
2453
+ /**
2206
2454
  * Infer a manifest from a directory that does not have one.
2207
2455
  *
2208
2456
  * This mirrors exactly what the template scaffolds, which is what makes adopting
2209
2457
  * the manifest a no-op for existing projects: the synthesized result is what
2210
2458
  * they would have written by hand.
2211
2459
  *
2212
- * An ejected backend — one with its own `src/index.ts` entrypoint — is reported
2213
- * as a `custom` app rather than a `backend` app. That is not a downgrade; it is
2214
- * an accurate description, and it is what keeps such a project deploying exactly
2215
- * as it does today.
2460
+ * The backend's `runtime` is inferred from whether the repository declares a
2461
+ * **Dockerfile** — the thing that actually builds an image — and from nothing
2462
+ * else. It used to be inferred from the presence of `backend/src/index.ts`,
2463
+ * which every scaffolded project had whether or not it wanted its own server, so
2464
+ * projects predating the manifest silently landed on the custom runtime and paid
2465
+ * for it (see `docs/cloud-deploy-workspace-vendoring.md`).
2216
2466
  */
2217
2467
  function synthesizeManifest(projectRoot) {
2218
2468
  const exists = (relative) => fs.existsSync(path.join(projectRoot, relative));
2219
2469
  const apps = {};
2220
2470
  const hasConfig = exists(DEFAULT_CONFIG_DIR);
2221
2471
  const hasBackend = exists("backend");
2222
- const backendEntry = exists("backend/src/index.ts");
2223
- if (hasBackend && backendEntry) apps.backend = {
2224
- type: "custom",
2225
- dockerfile: exists("backend/Dockerfile") ? "backend/Dockerfile" : void 0,
2226
- context: "."
2227
- };
2228
- else if (hasBackend || hasConfig) {
2229
- const backend = { type: "backend" };
2230
- if (!hasConfig) backend.mode = "baas";
2472
+ const dockerfile = ["Dockerfile", "backend/Dockerfile"].find(exists);
2473
+ if (hasBackend || hasConfig) {
2474
+ const backend = dockerfile ? {
2475
+ type: "backend",
2476
+ runtime: "custom",
2477
+ dockerfile,
2478
+ context: "."
2479
+ } : {
2480
+ type: "backend",
2481
+ runtime: "managed"
2482
+ };
2231
2483
  if (exists("backend/functions")) backend.functions = DEFAULT_FUNCTIONS_DIR;
2232
2484
  if (exists("backend/crons")) backend.crons = DEFAULT_CRONS_DIR;
2233
2485
  apps.backend = backend;
2486
+ 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.");
2234
2487
  }
2235
2488
  if (exists("frontend")) apps.web = {
2236
2489
  type: "static",
2237
2490
  root: "frontend",
2238
2491
  build: "npm run build --workspace frontend",
2239
2492
  output: "frontend/dist",
2493
+ path: "/",
2240
2494
  spa: true
2241
2495
  };
2242
2496
  return {
2243
- runtime: "^1",
2497
+ rebase: "^1",
2244
2498
  apps
2245
2499
  };
2246
2500
  }
@@ -2282,7 +2536,7 @@ function writeManifest(projectRoot, manifest) {
2282
2536
  const filePath = manifestPath(projectRoot);
2283
2537
  const ordered = {
2284
2538
  $schema: manifest.$schema ?? "https://rebase.pro/schemas/rebase.json",
2285
- runtime: manifest.runtime,
2539
+ rebase: manifest.rebase,
2286
2540
  apps: manifest.apps
2287
2541
  };
2288
2542
  fs.writeFileSync(filePath, `${JSON.stringify(ordered, null, 4)}\n`, "utf8");
@@ -2301,13 +2555,8 @@ function buildableApps(manifest) {
2301
2555
  name,
2302
2556
  app
2303
2557
  }));
2304
- const rank = (app) => {
2305
- if (app.type === "backend") return 0;
2306
- if (app.type === "admin") return 1;
2307
- if (app.type === "static") return 2;
2308
- return 3;
2309
- };
2310
- return entries.filter(({ app }) => app.type !== "mobile").sort((a, b) => rank(a.app) - rank(b.app));
2558
+ const rank = (app) => app.type === "backend" ? 0 : 1;
2559
+ return entries.sort((a, b) => rank(a.app) - rank(b.app));
2311
2560
  }
2312
2561
  /**
2313
2562
  * Decide whether a project can run on the managed runtime, and say why not.
@@ -2317,28 +2566,45 @@ function buildableApps(manifest) {
2317
2566
  * verdict.
2318
2567
  */
2319
2568
  function assessManagedCompatibility(manifest) {
2320
- const reasons = [];
2321
2569
  const backend = findBackendApp(manifest);
2322
- if (!backend) {
2323
- const custom = Object.entries(manifest.apps).find(([, app]) => app.type === "custom");
2324
- if (custom) reasons.push(`App "${custom[0]}" is a custom container. The managed runtime runs the platform image with your bundle, so a project that builds its own image uses the custom runtime instead.`);
2325
- else reasons.push("No backend app is declared in this repository. Only the repository that declares the backend selects the runtime.");
2326
- }
2327
- for (const [name, app] of Object.entries(manifest.apps)) if (app.type === "custom") reasons.push(`App "${name}" is a custom container image.`);
2570
+ if (!backend) return {
2571
+ eligible: false,
2572
+ reasons: ["No backend app is declared in this repository. Only the repository that declares the backend selects the runtime."]
2573
+ };
2574
+ if (backend.app.runtime === "custom") return {
2575
+ eligible: false,
2576
+ reasons: [`App "${backend.name}" declares runtime: "custom", so it builds and runs its own image. The managed runtime boots the platform image with your bundle; the custom runtime deploys exactly the same way, from your Dockerfile.`]
2577
+ };
2328
2578
  return {
2329
- eligible: reasons.length === 0 && Boolean(backend),
2330
- reasons
2579
+ eligible: true,
2580
+ reasons: []
2331
2581
  };
2332
2582
  }
2333
- /** Resolve a backend app's directories against the conventions it omits. */
2334
- function resolveBackendPaths(app) {
2583
+ /**
2584
+ * Resolve a backend app's directories against the conventions it omits.
2585
+ *
2586
+ * `hasCollections` replaces the old `mode: "cms" | "baas"` field. Where the
2587
+ * collections come from was never an independent choice: either they are
2588
+ * declared in code and the bundle ships them, or they are not and the runtime
2589
+ * introspects the live database at boot. Declaring it separately only created
2590
+ * the contradictory state — code-first declared, no collections anywhere.
2591
+ *
2592
+ * `hasConfig` is deliberately a **separate** question. A headless project has no
2593
+ * `config/collections`, but it may still ship a config package — that is where
2594
+ * the `storageAuthorize` hook lives, and storage is not under row-level
2595
+ * security, so without one the server refuses to boot with storage enabled.
2596
+ * Collapsing the two would leave a headless project nowhere to put it.
2597
+ */
2598
+ function resolveBackendPaths(app, projectRoot) {
2599
+ const config = app.config ?? "config";
2335
2600
  return {
2336
- config: app.config ?? "config",
2601
+ config,
2337
2602
  functions: app.functions ?? "backend/functions",
2338
2603
  crons: app.crons ?? "backend/crons",
2339
2604
  schema: app.schema ?? "backend/src/schema.generated.ts",
2340
2605
  usersCollection: app.usersCollection ?? "collections/users",
2341
- mode: app.mode ?? "cms"
2606
+ hasConfig: fs.existsSync(path.join(projectRoot, config)),
2607
+ hasCollections: fs.existsSync(path.join(projectRoot, config, "collections"))
2342
2608
  };
2343
2609
  }
2344
2610
  //#endregion
@@ -2401,22 +2667,19 @@ function devRuntimeEnv(projectRoot) {
2401
2667
  REBASE_DEV_CONFIG: "config",
2402
2668
  REBASE_DEV_FUNCTIONS: "backend/functions",
2403
2669
  REBASE_DEV_CRONS: "backend/crons",
2404
- REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts",
2405
- REBASE_DEV_MODE: "cms"
2670
+ REBASE_DEV_SCHEMA: "backend/src/schema.generated.ts"
2406
2671
  };
2407
2672
  try {
2408
2673
  const backend = findBackendApp(loadManifest(projectRoot).manifest);
2409
2674
  if (backend) {
2410
- const paths = resolveBackendPaths(backend.app);
2675
+ const paths = resolveBackendPaths(backend.app, projectRoot);
2411
2676
  result.REBASE_DEV_CONFIG = paths.config;
2412
2677
  result.REBASE_DEV_FUNCTIONS = paths.functions;
2413
2678
  result.REBASE_DEV_CRONS = paths.crons;
2414
2679
  result.REBASE_DEV_SCHEMA = paths.schema;
2415
- result.REBASE_DEV_MODE = paths.mode;
2416
2680
  result.REBASE_DEV_APP = backend.name;
2417
2681
  }
2418
2682
  } catch {}
2419
- if (!fs.existsSync(path.join(projectRoot, result.REBASE_DEV_CONFIG))) result.REBASE_DEV_MODE = "baas";
2420
2683
  return result;
2421
2684
  }
2422
2685
  /** Well-known filename the backend writes its actual port to. */
@@ -2825,7 +3088,7 @@ ${chalk.green.bold("Description")}
2825
3088
  */
2826
3089
  var DEFAULT_BUNDLE_DIR = "dist-bundle";
2827
3090
  /** Packages whose presence means the bundle cannot run on a stock runtime image. */
2828
- var KNOWN_NATIVE_PACKAGES = new Set([
3091
+ var KNOWN_NATIVE_PACKAGES = /* @__PURE__ */ new Set([
2829
3092
  "sharp",
2830
3093
  "canvas",
2831
3094
  "bcrypt",
@@ -2841,7 +3104,7 @@ var KNOWN_NATIVE_PACKAGES = new Set([
2841
3104
  "pg-native"
2842
3105
  ]);
2843
3106
  /** Dependencies supplied by the runtime image itself, not by the bundle. */
2844
- var RUNTIME_PROVIDED = new Set([
3107
+ var RUNTIME_PROVIDED = /* @__PURE__ */ new Set([
2845
3108
  "@rebasepro/server",
2846
3109
  "@rebasepro/types",
2847
3110
  "@rebasepro/client",
@@ -3024,16 +3287,27 @@ async function writeBundleTsconfig(projectRoot, outDir, includes, skipTypeCheck)
3024
3287
  * says exactly how to proceed, while a false positive would hand back the crash
3025
3288
  * loop this exists to prevent.
3026
3289
  */
3027
- function detectStorageAuthorize(compiledConfigDir) {
3290
+ function detectStorageAuthorize(compiledConfigDir, depth = 0) {
3028
3291
  const indexPath = [
3029
3292
  ".js",
3030
3293
  ".mjs",
3031
3294
  ".ts"
3032
3295
  ].map((ext) => path.join(compiledConfigDir, `index${ext}`)).find((candidate) => fs.existsSync(candidate));
3033
3296
  if (!indexPath) return false;
3297
+ return moduleExportsStorageAuthorize(indexPath, depth);
3298
+ }
3299
+ /**
3300
+ * Whether one compiled module re-exports or defines `storageAuthorize`.
3301
+ *
3302
+ * Split out from {@link detectStorageAuthorize} so a wildcard re-export can be
3303
+ * followed. `export * from "./storage.js"` is an ordinary way to write a config
3304
+ * barrel, and treating it as "no hook" rejected deploys that were correct — with
3305
+ * a message telling the developer to add a hook they had already written.
3306
+ */
3307
+ function moduleExportsStorageAuthorize(modulePath, depth) {
3034
3308
  let source;
3035
3309
  try {
3036
- source = fs.readFileSync(indexPath, "utf8");
3310
+ source = fs.readFileSync(modulePath, "utf8");
3037
3311
  } catch {
3038
3312
  return false;
3039
3313
  }
@@ -3042,9 +3316,48 @@ function detectStorageAuthorize(compiledConfigDir) {
3042
3316
  const parts = entry.split(/\bas\b/);
3043
3317
  return parts[parts.length - 1].trim();
3044
3318
  }).includes("storageAuthorize")) return true;
3319
+ if (depth < 3) for (const clause of source.matchAll(/\bexport\s*\*\s*from\s*["']([^"']+)["']/g)) {
3320
+ const specifier = clause[1];
3321
+ if (!specifier.startsWith(".")) continue;
3322
+ const resolved = resolveRelativeModule(path.dirname(modulePath), specifier);
3323
+ if (resolved && moduleExportsStorageAuthorize(resolved, depth + 1)) return true;
3324
+ }
3045
3325
  return false;
3046
3326
  }
3047
3327
  /**
3328
+ * Resolve a relative ESM specifier to a file on disk.
3329
+ *
3330
+ * Compiled output carries explicit `.js` extensions, but the same function reads
3331
+ * TypeScript sources during a source boot, where the specifier may be
3332
+ * extensionless or point at a directory index.
3333
+ */
3334
+ function resolveRelativeModule(fromDir, specifier) {
3335
+ const base = path.resolve(fromDir, specifier);
3336
+ const candidates = [
3337
+ base,
3338
+ `${base}.js`,
3339
+ `${base}.mjs`,
3340
+ `${base}.ts`,
3341
+ path.join(base, "index.js"),
3342
+ path.join(base, "index.mjs"),
3343
+ path.join(base, "index.ts")
3344
+ ];
3345
+ for (const candidate of candidates) try {
3346
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
3347
+ } catch {
3348
+ continue;
3349
+ }
3350
+ if (/\.js$/.test(base)) {
3351
+ const asTs = base.replace(/\.js$/, ".ts");
3352
+ try {
3353
+ if (fs.existsSync(asTs) && fs.statSync(asTs).isFile()) return asTs;
3354
+ } catch {
3355
+ return null;
3356
+ }
3357
+ }
3358
+ return null;
3359
+ }
3360
+ /**
3048
3361
  * Detect native code in the dependency closure.
3049
3362
  *
3050
3363
  * Walks declared runtime dependencies breadth-first through `node_modules`,
@@ -3328,11 +3641,10 @@ async function regenerateSchema(projectRoot, configDir, options) {
3328
3641
  * deployed green, and answered 404 on every one of them, with the file still
3329
3642
  * sitting in the repository looking exactly like the server.
3330
3643
  *
3331
- * A project that means to keep its own entrypoint declares the app as
3332
- * `"type": "custom"`, which builds the repository's Dockerfile instead — which
3333
- * is what {@link synthesizeManifest} already infers for a manifest-less repo
3334
- * carrying one. The warning names that route rather than implying the file is
3335
- * a mistake.
3644
+ * A project that means to keep its own entrypoint runs `rebase eject`, which
3645
+ * writes the entrypoint, a Dockerfile and a compose file together and flips the
3646
+ * backend to `runtime: "custom"`. The warning names that route rather than
3647
+ * implying the file is a mistake.
3336
3648
  */
3337
3649
  function findUnusedServerEntry(projectRoot, functionsDir) {
3338
3650
  const found = [path.join("backend", "src", "index.ts"), path.join(path.dirname(functionsDir), "src", "index.ts")].find((candidate) => fs.existsSync(path.join(projectRoot, candidate)));
@@ -3343,22 +3655,22 @@ function findUnusedServerEntry(projectRoot, functionsDir) {
3343
3655
  */
3344
3656
  async function buildBundle(options) {
3345
3657
  const { projectRoot, app, appName } = options;
3346
- const paths = resolveBackendPaths(app);
3658
+ const paths = resolveBackendPaths(app, projectRoot);
3347
3659
  const outDir = path.resolve(projectRoot, options.outDir ?? "dist-bundle");
3348
3660
  const includes = [];
3349
3661
  const addIfExists = (relative, pattern) => {
3350
3662
  if (fs.existsSync(path.join(projectRoot, relative))) includes.push(pattern);
3351
3663
  };
3352
- if (paths.mode === "cms") addIfExists(paths.config, `${paths.config}/**/*.ts`);
3664
+ if (paths.hasConfig) addIfExists(paths.config, `${paths.config}/**/*.ts`);
3353
3665
  addIfExists(paths.functions, `${paths.functions}/**/*.ts`);
3354
3666
  addIfExists(paths.crons, `${paths.crons}/**/*.ts`);
3355
3667
  if (fs.existsSync(path.join(projectRoot, paths.schema))) includes.push(paths.schema);
3356
3668
  if (includes.length === 0) throw new Error(`Nothing to build for app "${appName}". Expected a config directory at "${paths.config}" or functions at "${paths.functions}".`);
3357
- if (paths.mode === "cms" && options.skipSchema !== true) await regenerateSchema(projectRoot, paths.config, options);
3669
+ if (paths.hasCollections && options.skipSchema !== true) await regenerateSchema(projectRoot, paths.config, options);
3358
3670
  const unusedEntry = findUnusedServerEntry(projectRoot, paths.functions);
3359
3671
  if (unusedEntry) {
3360
3672
  const parts = [
3361
- ...paths.mode === "cms" ? [`${paths.config}/`] : [],
3673
+ ...paths.hasCollections ? [`${paths.config}/`] : [],
3362
3674
  `${paths.functions}/`,
3363
3675
  "the schema"
3364
3676
  ];
@@ -3366,7 +3678,7 @@ async function buildBundle(options) {
3366
3678
  console.log(chalk.yellow(` ⚠ ${unusedEntry} is not the bundle's entry point — it is not compiled or shipped.`));
3367
3679
  console.log(chalk.dim(` The runtime boots the bundle itself and mounts ${compiled}.`));
3368
3680
  console.log(chalk.dim(` Routes defined there will not exist once deployed: move them to ${paths.functions}/,`));
3369
- console.log(chalk.dim(` or declare this app as "type": "custom" in rebase.json to keep your own entrypoint.`));
3681
+ console.log(chalk.dim(` or run \`rebase eject\` to make this file the entrypoint and own the image.`));
3370
3682
  }
3371
3683
  log(options, chalk.dim(` compiling ${includes.length} source group(s) → ${path.relative(projectRoot, outDir)}/`));
3372
3684
  cleanOutDir(projectRoot, outDir);
@@ -3391,14 +3703,17 @@ async function buildBundle(options) {
3391
3703
  const compiledConfigDir = path.join(outDir, paths.config);
3392
3704
  const compiledCollectionsDir = path.join(compiledConfigDir, "collections");
3393
3705
  let collections = [];
3394
- if (paths.mode === "cms") {
3706
+ if (paths.hasCollections) {
3395
3707
  collections = await loadSourceCollections(path.join(projectRoot, paths.config, "collections"));
3396
- if (collections.length === 0) throw new Error(`No collections were found in ${path.join(paths.config, "collections")}. A cms-mode project must define at least one collection.`);
3708
+ if (collections.length === 0) throw new Error(`No collections were found in ${path.join(paths.config, "collections")}. Define at least one collection there, or remove the directory to have the runtime introspect collections from the live database instead.`);
3397
3709
  if (!fs.existsSync(compiledCollectionsDir)) throw new Error(`Compilation produced no collections directory at ${path.relative(projectRoot, compiledCollectionsDir)}.`);
3398
3710
  }
3399
3711
  const declared = collectDeclaredDependencies(projectRoot);
3400
3712
  const nativeModules = detectNativeDependencies(projectRoot, declared);
3401
3713
  const declaresStorageAuthorize = detectStorageAuthorize(path.join(outDir, paths.config));
3714
+ const storageSources = normalizeStorageSources(options.storage, void 0);
3715
+ const collision = findStorageSuffixCollision(storageSources.map((s) => s.key));
3716
+ if (collision) throw new Error(`Storage sources "${collision.a}" and "${collision.b}" in rebase.json both map to the environment variable suffix "${collision.suffix || "(none)"}", so they would read each other's configuration. Rename one of them.`);
3402
3717
  const schemaOut = paths.schema.replace(/\.ts$/, ".js");
3403
3718
  const relative = (target) => fs.existsSync(path.join(outDir, target)) ? target : void 0;
3404
3719
  const manifest = {
@@ -3408,23 +3723,26 @@ async function buildBundle(options) {
3408
3723
  builtAgainst: resolveServerVersion(projectRoot),
3409
3724
  contract: RUNTIME_CONTRACT_VERSION
3410
3725
  },
3411
- schemaVersion: paths.mode === "baas" ? "" : computeSchemaVersion(collections),
3726
+ schemaVersion: paths.hasCollections ? computeSchemaVersion(collections) : "",
3412
3727
  app: appName,
3413
- mode: paths.mode,
3728
+ kind: "backend",
3414
3729
  entry: {
3415
- config: paths.mode === "cms" ? relative(paths.config) : void 0,
3416
- collections: paths.mode === "cms" ? relative(path.join(paths.config, "collections")) : void 0,
3730
+ config: paths.hasConfig ? relative(paths.config) : void 0,
3731
+ collections: paths.hasCollections ? relative(path.join(paths.config, "collections")) : void 0,
3417
3732
  functions: relative(paths.functions),
3418
3733
  crons: relative(paths.crons),
3419
3734
  schema: relative(schemaOut),
3420
- usersCollection: paths.mode === "cms" ? relative(path.join(paths.config, `${paths.usersCollection}.js`)) : void 0
3735
+ usersCollection: paths.hasCollections ? relative(path.join(paths.config, `${paths.usersCollection}.js`)) : void 0
3421
3736
  },
3422
3737
  collections: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)).sort(),
3423
3738
  hooks: {
3424
3739
  native: nativeModules.length > 0,
3425
3740
  nativeModules: nativeModules.length > 0 ? nativeModules : void 0
3426
3741
  },
3427
- storage: { authorize: declaresStorageAuthorize },
3742
+ storage: {
3743
+ authorize: declaresStorageAuthorize,
3744
+ ...storageSources.length > 0 ? { sources: storageSources } : {}
3745
+ },
3428
3746
  deps: { declared },
3429
3747
  build: {
3430
3748
  cli: resolveCliVersion(),
@@ -3487,11 +3805,12 @@ async function buildBundle(options) {
3487
3805
  * in one image already, that is exactly what it had.
3488
3806
  */
3489
3807
  function foldStaticIntoBundle(options) {
3490
- const { bundleDir, assetsDir } = options;
3808
+ const { bundleDir, assetsDir, appName, path: basePath, spa } = options;
3491
3809
  const manifestPath = path.join(bundleDir, "manifest.json");
3492
3810
  if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
3493
3811
  if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
3494
- const staticOut = path.join(bundleDir, "static");
3812
+ const dir = path.posix.join("static", appName);
3813
+ const staticOut = path.join(bundleDir, "static", appName);
3495
3814
  fs.rmSync(staticOut, {
3496
3815
  recursive: true,
3497
3816
  force: true
@@ -3499,21 +3818,30 @@ function foldStaticIntoBundle(options) {
3499
3818
  fs.mkdirSync(staticOut, { recursive: true });
3500
3819
  fs.cpSync(assetsDir, staticOut, { recursive: true });
3501
3820
  let fileCount = 0;
3502
- const count = (dir) => {
3503
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(dir, entry.name));
3821
+ const count = (target) => {
3822
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) if (entry.isDirectory()) count(path.join(target, entry.name));
3504
3823
  else fileCount++;
3505
3824
  };
3506
3825
  count(staticOut);
3507
3826
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
3827
+ const existing = (manifest.entry?.static ?? []).filter((entry) => entry.dir !== dir);
3508
3828
  manifest.entry = {
3509
3829
  ...manifest.entry,
3510
- static: "static"
3830
+ static: [...existing, {
3831
+ path: basePath,
3832
+ dir,
3833
+ spa
3834
+ }]
3511
3835
  };
3512
3836
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
3513
- return { fileCount };
3837
+ return {
3838
+ fileCount,
3839
+ dir
3840
+ };
3514
3841
  }
3515
3842
  function buildStaticBundle(options) {
3516
3843
  const { projectRoot, appName, assetsDir, outDir, runtimeRange } = options;
3844
+ const basePath = options.path ?? "/";
3517
3845
  cleanOutDir(projectRoot, outDir);
3518
3846
  const staticOut = path.join(outDir, "static");
3519
3847
  fs.mkdirSync(staticOut, { recursive: true });
@@ -3533,8 +3861,12 @@ function buildStaticBundle(options) {
3533
3861
  },
3534
3862
  schemaVersion: "",
3535
3863
  app: appName,
3536
- mode: "static",
3537
- entry: { static: "static" },
3864
+ kind: "static",
3865
+ entry: { static: [{
3866
+ path: basePath,
3867
+ dir: "static",
3868
+ spa: options.spa ?? true
3869
+ }] },
3538
3870
  hooks: { native: false },
3539
3871
  deps: { declared: {} },
3540
3872
  build: {
@@ -3636,7 +3968,7 @@ function resolveCliVersion() {
3636
3968
  //#endregion
3637
3969
  //#region src/fold-static.ts
3638
3970
  /**
3639
- * Folding a project's frontend into its backend bundle.
3971
+ * Folding a project's static apps into its backend bundle.
3640
3972
  *
3641
3973
  * Shared by `rebase build` and `rebase cloud deploy` deliberately. It lived in
3642
3974
  * the build *command* first, and `deploy` rebuilds the bundle itself — so a
@@ -3645,34 +3977,77 @@ function resolveCliVersion() {
3645
3977
  * folding had never been written. Two callers building the same artefact must
3646
3978
  * share the step that completes it.
3647
3979
  *
3648
- * Why fold at all: `bootFromBundle` already serves a SPA from `entry.static`
3980
+ * Why fold at all: `bootFromBundle` serves static apps from `entry.static`
3649
3981
  * behind `REBASE_SERVE_STATIC` (default on). A managed tenant runs one pod, so
3650
- * putting the built site in the bundle gives it the shape a custom container
3651
- * already had — site at `/`, API at `/api` — which is the only honest baseline
3652
- * for calling the managed runtime a drop-in replacement.
3982
+ * putting the built assets in the bundle gives it the shape a custom container
3983
+ * already had — site at `/`, admin at `/admin`, API at `/api` — which is the
3984
+ * only honest baseline for calling the managed runtime a drop-in replacement.
3985
+ *
3986
+ * **Every** static app is folded, each at its declared path. Folding used to
3987
+ * pick exactly one and refuse when it found two, which meant a project with a
3988
+ * site and an admin panel deployed with neither.
3653
3989
  */
3654
3990
  /**
3655
- * Which static app, if any, should be served by the backend.
3991
+ * Every static app in the manifest, in mount order.
3656
3992
  *
3657
- * Exactly one `static` app is folded. With several, folding would have to choose,
3658
- * and silently picking one of two websites is worse than doing nothing — so it
3659
- * declines and names what it saw. Pure, so the decision is testable without a
3660
- * filesystem.
3993
+ * Longest path first, so the `/`-rooted app is registered last — its catch-all
3994
+ * would otherwise claim its siblings' URLs. Pure, so the ordering is testable
3995
+ * without a filesystem.
3661
3996
  */
3662
- function selectFoldableApp(manifest) {
3663
- const statics = Object.entries(manifest.apps ?? {}).filter(([, app]) => app?.type === "static").map(([name, app]) => ({
3664
- name,
3665
- build: app?.build,
3666
- output: app?.output
3667
- }));
3668
- if (statics.length === 0) return {};
3669
- if (statics.length > 1) return { reason: `${statics.length} static apps (${statics.map((s) => s.name).join(", ")}) — none folded in. Pick one to serve from the backend, or host them separately.` };
3670
- const only = statics[0];
3671
- if (!only.output) return { reason: `"${only.name}" declares no output directory — not folded in.` };
3672
- return { app: only };
3997
+ function foldableApps(manifest) {
3998
+ const apps = [];
3999
+ const skipped = [];
4000
+ for (const [name, app] of Object.entries(manifest.apps ?? {})) {
4001
+ if (app?.type !== "static") continue;
4002
+ if (!app.output) {
4003
+ skipped.push({
4004
+ name,
4005
+ reason: `"${name}" declares no output directory — not folded in.`
4006
+ });
4007
+ continue;
4008
+ }
4009
+ apps.push({
4010
+ name,
4011
+ build: app.build,
4012
+ output: app.output,
4013
+ path: app.path ?? "/",
4014
+ spa: app.spa ?? true
4015
+ });
4016
+ }
4017
+ apps.sort((a, b) => b.path.length - a.path.length);
4018
+ return {
4019
+ apps,
4020
+ skipped
4021
+ };
3673
4022
  }
3674
4023
  /**
3675
- * Build the project's frontend and fold it into the backend bundle.
4024
+ * Assert a built app's assets are actually rooted at the path it is served from.
4025
+ *
4026
+ * An app mounted at `/admin` but built with Vite's default `base: "/"` emits
4027
+ * `<script src="/assets/index-a1b2.js">`. The server serves `index.html` fine
4028
+ * and 404s every asset: a blank page, no server error, nothing in the logs. It
4029
+ * is the single most expensive silent failure in this design, so it is a build
4030
+ * error rather than a runtime surprise.
4031
+ *
4032
+ * Only `<script src>` and `<link href>` are inspected — those are what a bundler
4033
+ * rewrites through `base`. Author-written anchors and canonical URLs are not
4034
+ * evidence of a misbuild.
4035
+ */
4036
+ function assertBuiltForPath(indexHtml, basePath, appName) {
4037
+ if (basePath === "/") return;
4038
+ const offenders = [];
4039
+ for (const match of indexHtml.matchAll(/<(?:script|link)\b[^>]*?\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
4040
+ const ref = match[1];
4041
+ if (!ref.startsWith("/")) continue;
4042
+ if (ref === `${basePath}` || ref.startsWith(`${basePath}/`)) continue;
4043
+ offenders.push(ref);
4044
+ }
4045
+ if (offenders.length === 0) return;
4046
+ throw new Error(`"${appName}" is declared at ${basePath} but its build emitted assets rooted at /.\n index.html references: ${offenders.slice(0, 3).join(", ")}\n The app would load a blank page. Set \`base\` from REBASE_APP_BASE in its
4047
+ build config — see docs/apps-and-runtimes.md §4.2.`);
4048
+ }
4049
+ /**
4050
+ * Build the project's static apps and fold them into the backend bundle.
3676
4051
  *
3677
4052
  * Throws rather than exiting, so the caller decides whether a missing frontend
3678
4053
  * should fail its command — a `build` may reasonably want to stop, and so should
@@ -3681,27 +4056,39 @@ function selectFoldableApp(manifest) {
3681
4056
  async function foldFrontendIntoBundle(options) {
3682
4057
  const { projectRoot, manifest, bundleDir, skipBuild } = options;
3683
4058
  const log = options.log ?? ((m) => console.log(m));
3684
- const { app, reason } = selectFoldableApp(manifest);
3685
- if (reason) {
3686
- log(chalk.yellow(` ⚠ ${reason}`));
3687
- return null;
4059
+ const { apps, skipped } = foldableApps(manifest);
4060
+ for (const { reason } of skipped) log(chalk.yellow(` ⚠ ${reason}`));
4061
+ if (apps.length === 0) return [];
4062
+ const outcomes = [];
4063
+ for (const app of apps) {
4064
+ if (app.build && !skipBuild) await execa(app.build, {
4065
+ cwd: projectRoot,
4066
+ stdio: "inherit",
4067
+ shell: true,
4068
+ env: {
4069
+ REBASE_APP_PATH: app.path,
4070
+ REBASE_APP_BASE: app.path === "/" ? "/" : `${app.path}/`,
4071
+ REBASE_APP_NAME: app.name
4072
+ }
4073
+ });
4074
+ const assetsDir = path.join(projectRoot, app.output);
4075
+ 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.`);
4076
+ const indexHtml = path.join(assetsDir, "index.html");
4077
+ if (fs.existsSync(indexHtml)) assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), app.path, app.name);
4078
+ const { fileCount } = foldStaticIntoBundle({
4079
+ bundleDir,
4080
+ assetsDir,
4081
+ appName: app.name,
4082
+ path: app.path,
4083
+ spa: app.spa
4084
+ });
4085
+ outcomes.push({
4086
+ appName: app.name,
4087
+ fileCount,
4088
+ path: app.path
4089
+ });
3688
4090
  }
3689
- if (!app) return null;
3690
- if (app.build && !skipBuild) await execa(app.build, {
3691
- cwd: projectRoot,
3692
- stdio: "inherit",
3693
- shell: true
3694
- });
3695
- const assetsDir = path.join(projectRoot, app.output);
3696
- 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.`);
3697
- const { fileCount } = foldStaticIntoBundle({
3698
- bundleDir,
3699
- assetsDir
3700
- });
3701
- return {
3702
- appName: app.name,
3703
- fileCount
3704
- };
4091
+ return outcomes;
3705
4092
  }
3706
4093
  //#endregion
3707
4094
  //#region src/commands/build.ts
@@ -3719,7 +4106,7 @@ async function foldFrontendIntoBundle(options) {
3719
4106
  * entrypoint, falls back to the previous behaviour: run every workspace's own
3720
4107
  * `build` script. Nothing that built before stops building.
3721
4108
  */
3722
- function printHelp$3() {
4109
+ function printHelp$4() {
3723
4110
  console.log(`
3724
4111
  ${chalk.bold("rebase build")} — build the apps declared in rebase.json
3725
4112
 
@@ -3754,7 +4141,7 @@ async function buildCommand(rawArgs = []) {
3754
4141
  permissive: true
3755
4142
  });
3756
4143
  if (args["--help"]) {
3757
- printHelp$3();
4144
+ printHelp$4();
3758
4145
  return;
3759
4146
  }
3760
4147
  const projectRoot = requireProjectRoot();
@@ -3798,13 +4185,20 @@ async function buildCommand(rawArgs = []) {
3798
4185
  console.log(`${chalk.bold("Rebase")} — building ${targets.length} app(s)\n`);
3799
4186
  for (const { name, app } of targets) {
3800
4187
  console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
4188
+ if (app.type === "backend" && app.runtime === "custom") {
4189
+ console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
4190
+ console.log(chalk.dim(` ${chalk.cyan(`npm run build --workspace ${name}`)} then ${chalk.cyan(`docker build -f ${app.dockerfile ?? "Dockerfile"} .`)}`));
4191
+ console.log("");
4192
+ continue;
4193
+ }
3801
4194
  if (app.type === "backend") {
3802
4195
  const result = await buildBundle({
3803
4196
  projectRoot,
3804
4197
  appName: name,
3805
4198
  app,
3806
4199
  outDir: args["--out"],
3807
- runtimeRange: manifest.runtime,
4200
+ runtimeRange: manifest.rebase,
4201
+ storage: manifest.storage,
3808
4202
  skipTypeCheck: args["--skip-type-check"],
3809
4203
  skipSchema: args["--skip-schema"]
3810
4204
  });
@@ -3827,28 +4221,24 @@ async function buildCommand(rawArgs = []) {
3827
4221
  console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
3828
4222
  process.exit(1);
3829
4223
  });
3830
- if (folded) console.log(chalk.green(` ✓ ${folded.appName} folded in`) + chalk.dim(` (${folded.fileCount} file(s) → served at /)`));
4224
+ for (const outcome of folded ?? []) console.log(chalk.green(` ✓ ${outcome.appName} folded in`) + chalk.dim(` (${outcome.fileCount} file(s) → served at ${outcome.path})`));
3831
4225
  }
3832
- } else if (app.type === "static" || app.type === "admin") await buildAssetApp(projectRoot, name, app, manifest.runtime, args["--out"]);
3833
- else if (app.type === "custom") console.log(chalk.dim(" custom container — built at deploy time from its Dockerfile"));
4226
+ } else if (app.type === "static") await buildAssetApp(projectRoot, name, app, manifest.rebase, args["--out"]);
3834
4227
  console.log("");
3835
4228
  }
3836
4229
  console.log(chalk.green("✓ Build complete."));
3837
4230
  }
3838
4231
  /**
3839
- * Build a static or bundled-admin app and package it into a static bundle.
4232
+ * Build a static app and package it into a static bundle.
3840
4233
  *
3841
4234
  * Runs the app's own build command, checks it produced the declared output, then
3842
- * packages that output into a `static`-mode bundle — the same deployable shape as
4235
+ * packages that output into a `static`-kind bundle — the same deployable shape as
3843
4236
  * a backend bundle, so a frontend or admin app deploys through the identical
3844
4237
  * path and runs on the identical image, just serving files instead of an API.
3845
4238
  */
3846
4239
  async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride) {
3847
4240
  const asset = app;
3848
- if (app.type === "admin" && app.mode !== "bundled") {
3849
- console.log(chalk.dim(" hosted admin panel — nothing to build"));
3850
- return;
3851
- }
4241
+ const basePath = asset.path ?? "/";
3852
4242
  if (!asset.build) {
3853
4243
  console.log(chalk.dim(" no build command declared — skipping"));
3854
4244
  return;
@@ -3857,7 +4247,12 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
3857
4247
  await execa(asset.build, {
3858
4248
  cwd: projectRoot,
3859
4249
  stdio: "inherit",
3860
- shell: true
4250
+ shell: true,
4251
+ env: {
4252
+ REBASE_APP_PATH: basePath,
4253
+ REBASE_APP_BASE: basePath === "/" ? "/" : `${basePath}/`,
4254
+ REBASE_APP_NAME: name
4255
+ }
3861
4256
  });
3862
4257
  } catch {
3863
4258
  console.error(chalk.red(` ✗ build command failed for "${name}"`));
@@ -3872,15 +4267,24 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
3872
4267
  console.error(chalk.red(` ✗ declared output "${asset.output}" does not exist after building`));
3873
4268
  process.exit(1);
3874
4269
  }
4270
+ const indexHtml = path.join(outputPath, "index.html");
4271
+ if (fs.existsSync(indexHtml)) try {
4272
+ assertBuiltForPath(fs.readFileSync(indexHtml, "utf8"), basePath, name);
4273
+ } catch (err) {
4274
+ console.error(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));
4275
+ process.exit(1);
4276
+ }
3875
4277
  const result = buildStaticBundle({
3876
4278
  projectRoot,
3877
4279
  appName: name,
3878
4280
  assetsDir: outputPath,
3879
4281
  outDir: outOverride ? path.resolve(process.cwd(), outOverride) : path.join(projectRoot, `dist-bundle-${name}`),
3880
- runtimeRange
4282
+ runtimeRange,
4283
+ path: basePath,
4284
+ spa: asset.spa ?? true
3881
4285
  });
3882
4286
  const rel = path.relative(projectRoot, result.outDir);
3883
- console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s))`));
4287
+ console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
3884
4288
  }
3885
4289
  /** The pre-manifest behaviour: build every workspace package. */
3886
4290
  async function runWorkspaceBuilds(projectRoot) {
@@ -3898,6 +4302,234 @@ async function runWorkspaceBuilds(projectRoot) {
3898
4302
  }
3899
4303
  }
3900
4304
  //#endregion
4305
+ //#region src/commands/eject.ts
4306
+ /**
4307
+ * CLI command: rebase eject
4308
+ *
4309
+ * The supported route from the managed runtime to a custom one.
4310
+ *
4311
+ * Without it, `runtime: "custom"` is a mode a user can only reach by
4312
+ * hand-writing an entrypoint they have never seen. The template used to solve
4313
+ * that by scaffolding `backend/src/index.ts` into every project — ~190 lines
4314
+ * configuring CORS, auth, cookies, storage and history, which the managed
4315
+ * runtime never loads. It was the most important-looking file in a new project
4316
+ * and editing it did nothing.
4317
+ *
4318
+ * So the file moved here. A managed project does not carry it; a project that
4319
+ * asks for it gets it together with the Dockerfile and the manifest change that
4320
+ * make it actually run.
4321
+ *
4322
+ * There is deliberately no `rebase uneject`. Going back is deleting two files
4323
+ * and editing one line, and a command that silently discarded a user's server
4324
+ * code would be worse than its absence.
4325
+ */
4326
+ var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
4327
+ /** Walk up to the package root, which holds `templates/`. */
4328
+ function findCliRoot(from) {
4329
+ const root = path.parse(from).root;
4330
+ let dir = from;
4331
+ while (dir && dir !== root) {
4332
+ if (fs.existsSync(path.join(dir, "templates", "eject"))) return dir;
4333
+ dir = path.dirname(dir);
4334
+ }
4335
+ return null;
4336
+ }
4337
+ /** Files the eject payload contributes, as `<source> → <destination>`. */
4338
+ var PAYLOAD = [
4339
+ {
4340
+ from: "backend/src/index.ts",
4341
+ to: "backend/src/index.ts",
4342
+ overwrite: true
4343
+ },
4344
+ {
4345
+ from: "backend/src/env.ts",
4346
+ to: "backend/src/env.ts",
4347
+ overwrite: true
4348
+ },
4349
+ {
4350
+ from: "Dockerfile",
4351
+ to: "Dockerfile",
4352
+ overwrite: false
4353
+ },
4354
+ {
4355
+ from: "docker-compose.custom.yml",
4356
+ to: "docker-compose.custom.yml",
4357
+ overwrite: false
4358
+ }
4359
+ ];
4360
+ /**
4361
+ * The project's name, for the `{{PROJECT_NAME}}` the payload carries.
4362
+ *
4363
+ * Falls back to the directory name — a compose project name is cosmetic, and a
4364
+ * missing or unreadable package.json is not a reason to refuse to eject.
4365
+ */
4366
+ function projectNameOf(projectRoot) {
4367
+ try {
4368
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
4369
+ if (typeof pkg.name === "string" && pkg.name.trim()) return pkg.name.trim();
4370
+ } catch {}
4371
+ return path.basename(projectRoot);
4372
+ }
4373
+ function printHelp$3() {
4374
+ console.log(`
4375
+ ${chalk.bold("rebase eject")} — take ownership of the server process
4376
+
4377
+ Writes the backend entrypoint and a Dockerfile into this project, and flips its
4378
+ backend to ${chalk.cyan("runtime: \"custom\"")}. From then on this repository builds its own
4379
+ image: platform runtime upgrades no longer reach it, and CORS, auth wiring,
4380
+ storage and shutdown become yours to configure.
4381
+
4382
+ ${chalk.bold("Usage")}
4383
+ rebase eject [app]
4384
+
4385
+ ${chalk.bold("Options")}
4386
+ --dry-run List what would change, and change nothing
4387
+ -h, --help Show this help
4388
+ `.trim());
4389
+ }
4390
+ async function ejectCommand(rawArgs = []) {
4391
+ const args = arg({
4392
+ "--dry-run": Boolean,
4393
+ "--help": Boolean,
4394
+ "-h": "--help"
4395
+ }, {
4396
+ argv: rawArgs.slice(2),
4397
+ permissive: true
4398
+ });
4399
+ if (args["--help"]) {
4400
+ printHelp$3();
4401
+ return;
4402
+ }
4403
+ const projectRoot = requireProjectRoot();
4404
+ const dryRun = Boolean(args["--dry-run"]);
4405
+ const requested = args._.slice(1).find((value) => !value.startsWith("-"));
4406
+ let loaded;
4407
+ try {
4408
+ loaded = loadManifest(projectRoot);
4409
+ } catch (err) {
4410
+ if (err instanceof ManifestError) {
4411
+ console.error(chalk.red(`✗ ${err.message}`));
4412
+ for (const issue of err.issues) console.error(chalk.dim(` ${issue.path}: ${issue.message}`));
4413
+ process.exit(1);
4414
+ }
4415
+ throw err;
4416
+ }
4417
+ const { manifest } = loaded;
4418
+ let appName;
4419
+ let app;
4420
+ if (requested) {
4421
+ const declared = manifest.apps[requested];
4422
+ if (!declared) {
4423
+ console.error(chalk.red(`✗ No app named "${requested}" in rebase.json.`));
4424
+ console.error(chalk.dim(` Declared: ${Object.keys(manifest.apps).join(", ") || "(none)"}`));
4425
+ process.exit(1);
4426
+ }
4427
+ if (declared.type !== "backend") {
4428
+ console.error(chalk.red(`✗ "${requested}" is a ${declared.type} app — only a backend can be ejected.`));
4429
+ process.exit(1);
4430
+ }
4431
+ appName = requested;
4432
+ app = declared;
4433
+ } else {
4434
+ const backend = findBackendApp(manifest);
4435
+ if (!backend) {
4436
+ console.error(chalk.red("✗ This repository declares no backend app."));
4437
+ console.error(chalk.dim(" Only the repository that declares the backend chooses its runtime."));
4438
+ process.exit(1);
4439
+ }
4440
+ appName = backend.name;
4441
+ app = backend.app;
4442
+ }
4443
+ if (app.runtime === "custom") {
4444
+ console.error(chalk.red(`✗ "${appName}" is already ejected — it declares runtime: "custom".`));
4445
+ console.error(chalk.dim(` Its entrypoint is ${app.dockerfile ?? "Dockerfile"} and backend/src/index.ts.`));
4446
+ process.exit(1);
4447
+ }
4448
+ const cliRoot = findCliRoot(__dirname$1);
4449
+ if (!cliRoot) {
4450
+ console.error(chalk.red("✗ Could not locate the eject templates. Reinstall @rebasepro/cli."));
4451
+ process.exit(1);
4452
+ }
4453
+ const payloadDir = path.join(cliRoot, "templates", "eject");
4454
+ const planned = [];
4455
+ for (const file of PAYLOAD) {
4456
+ const source = path.join(payloadDir, file.from);
4457
+ if (!fs.existsSync(source)) {
4458
+ console.error(chalk.red(`✗ The eject template is missing ${file.from}. Reinstall @rebasepro/cli.`));
4459
+ process.exit(1);
4460
+ }
4461
+ const exists = fs.existsSync(path.join(projectRoot, file.to));
4462
+ planned.push({
4463
+ to: file.to,
4464
+ action: exists && !file.overwrite ? "keep" : "write"
4465
+ });
4466
+ }
4467
+ if (dryRun) {
4468
+ console.log(chalk.bold(`Would eject "${appName}" to a custom runtime:`));
4469
+ console.log("");
4470
+ for (const item of planned) console.log(item.action === "write" ? ` ${chalk.green("write")} ${item.to}` : ` ${chalk.dim("keep")} ${item.to} ${chalk.dim("(already exists)")}`);
4471
+ console.log(` ${chalk.green("write")} rebase.json ${chalk.dim("(runtime: \"custom\")")}`);
4472
+ console.log("");
4473
+ console.log(chalk.dim("Nothing was changed."));
4474
+ return;
4475
+ }
4476
+ const projectName = projectNameOf(projectRoot);
4477
+ for (const [index, file] of PAYLOAD.entries()) {
4478
+ if (planned[index].action === "keep") continue;
4479
+ const destination = path.join(projectRoot, file.to);
4480
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
4481
+ const contents = fs.readFileSync(path.join(payloadDir, file.from), "utf8").replace(/\{\{PROJECT_NAME\}\}/g, projectName);
4482
+ fs.writeFileSync(destination, contents, "utf8");
4483
+ }
4484
+ const dockerfile = app.dockerfile ?? "Dockerfile";
4485
+ manifest.apps[appName] = {
4486
+ ...app,
4487
+ runtime: "custom",
4488
+ dockerfile,
4489
+ port: app.port ?? 8080
4490
+ };
4491
+ writeManifest(projectRoot, manifest);
4492
+ restoreBackendScripts(projectRoot);
4493
+ console.log("");
4494
+ console.log(chalk.green(`✓ Ejected "${appName}" to a custom runtime.`));
4495
+ console.log("");
4496
+ console.log(` ${chalk.cyan("backend/src/index.ts".padEnd(26))} your entrypoint — the runtime no longer boots the bundle`);
4497
+ console.log(` ${chalk.cyan("backend/src/env.ts".padEnd(26))} the environment it reads`);
4498
+ console.log(` ${chalk.cyan(dockerfile.padEnd(26))} your image`);
4499
+ console.log(` ${chalk.cyan("docker-compose.custom.yml".padEnd(26))} runs it`);
4500
+ console.log(` ${chalk.cyan("rebase.json".padEnd(26))} runtime: custom`);
4501
+ console.log("");
4502
+ console.log(chalk.yellow(" You now own CORS, auth wiring, storage and shutdown. Platform runtime"));
4503
+ console.log(chalk.yellow(" upgrades no longer reach this project."));
4504
+ console.log("");
4505
+ console.log(chalk.dim(` ${chalk.cyan("docker compose -f docker-compose.custom.yml up --build")}`));
4506
+ console.log(chalk.dim(" docker-compose.yml is untouched — it still runs the managed shape if you go back."));
4507
+ console.log("");
4508
+ }
4509
+ /**
4510
+ * Point the backend workspace's scripts at the entrypoint that now exists.
4511
+ *
4512
+ * A managed project's backend package deliberately declares no `main` and no
4513
+ * `start`: there is no entrypoint to name. Ejecting creates one.
4514
+ */
4515
+ function restoreBackendScripts(projectRoot) {
4516
+ const packagePath = path.join(projectRoot, "backend", "package.json");
4517
+ if (!fs.existsSync(packagePath)) return;
4518
+ let parsed;
4519
+ try {
4520
+ parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
4521
+ } catch {
4522
+ console.log(chalk.yellow(" ⚠ backend/package.json is not valid JSON — its scripts were left alone."));
4523
+ return;
4524
+ }
4525
+ const scripts = parsed.scripts ?? {};
4526
+ parsed.main ??= "src/index.ts";
4527
+ scripts.dev ??= "tsx watch --include=\"../config/**/*\" --include=\"./functions/**/*\" src/index.ts";
4528
+ scripts.start ??= "node dist/backend/src/index.js";
4529
+ parsed.scripts = scripts;
4530
+ fs.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 4)}\n`, "utf8");
4531
+ }
4532
+ //#endregion
3901
4533
  //#region src/commands/start.ts
3902
4534
  /**
3903
4535
  * CLI command: rebase start
@@ -5345,7 +5977,7 @@ function declaredAppsFrom(manifest) {
5345
5977
  if (!apps || typeof apps !== "object") return [];
5346
5978
  return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
5347
5979
  name,
5348
- type: String(value?.type ?? "custom")
5980
+ type: value?.type === "backend" ? "backend" : "static"
5349
5981
  }));
5350
5982
  }
5351
5983
  /** Upload a bundle archive; returns the control-plane bundle id. */
@@ -5502,7 +6134,8 @@ async function deployBundle(opts) {
5502
6134
  projectRoot,
5503
6135
  appName: backend.name,
5504
6136
  app: backend.app,
5505
- runtimeRange: loaded.manifest.runtime,
6137
+ runtimeRange: loaded.manifest.rebase,
6138
+ storage: loaded.manifest.storage,
5506
6139
  skipTypeCheck: opts.skipTypeCheck,
5507
6140
  log: (m) => console.log(chalk.gray(m))
5508
6141
  })).outDir;
@@ -5513,7 +6146,7 @@ async function deployBundle(opts) {
5513
6146
  bundleDir,
5514
6147
  log: (m) => console.log(m)
5515
6148
  });
5516
- if (folded) console.log(chalk.gray(` folded ${folded.appName} in (${folded.fileCount} file(s), served at /)`));
6149
+ for (const outcome of folded) console.log(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
5517
6150
  } catch (err) {
5518
6151
  fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
5519
6152
  }
@@ -5650,6 +6283,23 @@ async function readDeployContext(client, projectId) {
5650
6283
  return {};
5651
6284
  }
5652
6285
  }
6286
+ /**
6287
+ * Whether this repository's backend declares the managed runtime.
6288
+ *
6289
+ * Deliberately quiet: a directory that is not a Rebase project, or whose
6290
+ * manifest does not parse, simply does not route this way — `rebase build` is
6291
+ * where a broken manifest gets reported, and a deploy refusing on one before it
6292
+ * has even said what it is doing would be the wrong place to find out.
6293
+ */
6294
+ function declaresManagedRuntime() {
6295
+ try {
6296
+ const projectRoot = findProjectRoot();
6297
+ if (!projectRoot) return false;
6298
+ return findBackendApp(loadManifest(projectRoot).manifest)?.app.runtime === "managed";
6299
+ } catch {
6300
+ return false;
6301
+ }
6302
+ }
5653
6303
  async function deployCommand(rawArgs, projectRef) {
5654
6304
  const args = arg({
5655
6305
  "--no-follow": Boolean,
@@ -5666,8 +6316,10 @@ async function deployCommand(rawArgs, projectRef) {
5666
6316
  });
5667
6317
  const { client, url } = await requireClient(rawArgs);
5668
6318
  const projectId = await resolveProjectRef(projectRef, client);
5669
- if (args["--bundle"]) {
5670
- if (args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
6319
+ const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime();
6320
+ if (args["--bundle"] || declaredManaged) {
6321
+ if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
6322
+ if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
5671
6323
  await deployBundle({
5672
6324
  client,
5673
6325
  url,
@@ -9039,11 +9691,8 @@ async function appsCommand(subcommand, rawArgs = []) {
9039
9691
  }
9040
9692
  function describeApp(app) {
9041
9693
  switch (app.type) {
9042
- case "backend": return `config: ${app.config ?? "config"}, mode: ${app.mode ?? "cms"}`;
9043
- case "static": return `${app.root} → ${app.output}`;
9044
- case "admin": return app.mode === "bundled" ? `bundled → ${app.output ?? "?"}` : "hosted by the platform";
9045
- case "mobile": return app.platform;
9046
- case "custom": return app.dockerfile ?? "Dockerfile";
9694
+ case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
9695
+ case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}`;
9047
9696
  default: return "";
9048
9697
  }
9049
9698
  }
@@ -9053,7 +9702,7 @@ async function listApps(asJson) {
9053
9702
  if (asJson) {
9054
9703
  console.log(JSON.stringify({
9055
9704
  source: loaded.source,
9056
- runtime: loaded.manifest.runtime,
9705
+ rebase: loaded.manifest.rebase,
9057
9706
  apps: loaded.manifest.apps,
9058
9707
  managed: compatibility
9059
9708
  }, null, 2));
@@ -9063,7 +9712,7 @@ async function listApps(asJson) {
9063
9712
  console.log(chalk.dim("No rebase.json — showing the layout inferred from this project."));
9064
9713
  console.log(chalk.dim(`Run ${chalk.cyan("rebase apps init")} to write it down.\n`));
9065
9714
  }
9066
- console.log(chalk.bold(`Runtime ${loaded.manifest.runtime}`));
9715
+ console.log(chalk.bold(`Rebase ${loaded.manifest.rebase}`));
9067
9716
  console.log("");
9068
9717
  const entries = Object.entries(loaded.manifest.apps);
9069
9718
  if (entries.length === 0) {
@@ -9209,6 +9858,7 @@ async function entry(args) {
9209
9858
  "api-keys",
9210
9859
  "cloud",
9211
9860
  "apps",
9861
+ "eject",
9212
9862
  "generate-sdk"
9213
9863
  ].includes(command)) {
9214
9864
  printHelp();
@@ -9261,6 +9911,9 @@ async function entry(args) {
9261
9911
  case "apps":
9262
9912
  await appsCommand(effectiveSubcommand, args);
9263
9913
  break;
9914
+ case "eject":
9915
+ await ejectCommand(args);
9916
+ break;
9264
9917
  case "auth":
9265
9918
  await authCommand(effectiveSubcommand, args);
9266
9919
  break;
@@ -9295,6 +9948,8 @@ ${chalk.green.bold("Commands")}
9295
9948
  ${chalk.blue.bold("dev")} Start the development server
9296
9949
  ${chalk.blue.bold("build")} Build all workspace packages
9297
9950
  ${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
9951
+ ${chalk.blue.bold("apps list")} Show the apps this repository declares
9952
+ ${chalk.blue.bold("eject")} Take ownership of the server process and image
9298
9953
 
9299
9954
  ${chalk.green.bold("Schema")}
9300
9955
  ${chalk.blue.bold("schema generate")} Generate Drizzle schema from collections
@@ -9312,25 +9967,25 @@ ${chalk.green.bold("SDK")}
9312
9967
 
9313
9968
  ${chalk.green.bold("Auth")}
9314
9969
  ${chalk.blue.bold("auth reset-password")} Reset a user's password
9315
- ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
9970
+ ${chalk.blue.bold("auth")} ${chalk.gray("--help")} Show auth command help
9316
9971
 
9317
9972
  ${chalk.green.bold("Diagnostics")}
9318
9973
  ${chalk.blue.bold("doctor")} Detect schema drift between collections, schema, and DB
9319
9974
 
9320
9975
  ${chalk.green.bold("AI Agent Skills")}
9321
- ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
9976
+ ${chalk.blue.bold("skills install")} Install Rebase agent skills for your AI coding assistant
9322
9977
 
9323
9978
  ${chalk.green.bold("API Keys")}
9324
- ${chalk.blue.bold("api-keys list")} List all service API keys
9325
- ${chalk.blue.bold("api-keys create")} Create a new scoped API key
9326
- ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
9327
- ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
9979
+ ${chalk.blue.bold("api-keys list")} List all service API keys
9980
+ ${chalk.blue.bold("api-keys create")} Create a new scoped API key
9981
+ ${chalk.blue.bold("api-keys revoke")} Revoke an existing API key
9982
+ ${chalk.blue.bold("api-keys")} ${chalk.gray("--help")} Show API key command help
9328
9983
 
9329
9984
  ${chalk.green.bold("Rebase Cloud")}
9330
9985
  ${chalk.blue.bold("cloud login")} Sign in to the hosted control plane
9331
9986
  ${chalk.blue.bold("cloud link")} Link this directory to a cloud project
9332
9987
  ${chalk.blue.bold("cloud deploy")} Deploy the linked project + stream logs
9333
- ${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
9988
+ ${chalk.blue.bold("cloud")} ${chalk.gray("--help")} Show all cloud commands
9334
9989
 
9335
9990
  ${chalk.green.bold("Options")}
9336
9991
  ${chalk.blue("--version, -v")} Show version number
@@ -9340,6 +9995,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
9340
9995
  `);
9341
9996
  }
9342
9997
  //#endregion
9343
- export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, MANIFEST_FILENAME, ManifestError, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
9998
+ export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, MANIFEST_FILENAME, ManifestError, TEMPLATE_PLACEHOLDER_FILES, appsCommand, assessManagedCompatibility, authCommand, buildBundle, buildCommand, buildInitQuestions, buildStaticBundle, buildableApps, cloudCommand, collectDeclaredDependencies, configureEnvFile, createRebaseApp, dbCommand, detectNativeDependencies, detectPackageManager, detectStorageAuthorize, devCommand, doctorCommand, ejectCommand, entry, findBackendApp, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, findUnusedServerEntry, foldStaticIntoBundle, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isIdentifierLike, isPnpmAvailable, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveBackendPaths, resolveExampleBaseUrl, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, synthesizeManifest, validateManifest, validateProjectName, validateTsxInstallation, writeManifest };
9344
9999
 
9345
10000
  //# sourceMappingURL=index.es.js.map