@rebasepro/cli 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -18,14 +18,15 @@ import { execa, execaCommandSync } from "execa";
18
18
  import { cp } from "fs/promises";
19
19
  import { fileURLToPath, pathToFileURL } from "url";
20
20
  import crypto from "crypto";
21
- import { spawn, spawnSync } from "child_process";
21
+ import { execFileSync, spawn, spawnSync } from "child_process";
22
22
  import os from "os";
23
23
  import { createRebaseClient } from "@rebasepro/client";
24
24
  import dotenv from "dotenv";
25
- import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, DEFAULT_RESOURCE_KEY, RUNTIME_CONTRACT_VERSION, buildResourceGraph, computeSchemaVersion, declareFunction, declaredQueueConsumers, declaredResources, declaredSubscriptions, deserializeCollections, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getDataSourceCapabilities, isResourceHandle, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceToDataSource } from "@rebasepro/types";
25
+ import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, DEFAULT_RESOURCE_KEY, DEFAULT_STORAGE_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, buildResourceGraph, computeSchemaVersion, declareFunction, declaredQueueConsumers, declaredResources, declaredSubscriptions, deserializeCollections, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getDataSourceCapabilities, isResourceHandle, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceToDataSource } from "@rebasepro/types";
26
26
  import { CodegenError, generateSDK, toSafeIdentifier } from "@rebasepro/codegen";
27
27
  import { createRequire } from "module";
28
28
  import { randomBytes as randomBytes$1 } from "node:crypto";
29
+ import net$1 from "node:net";
29
30
  //#region src/utils/version.ts
30
31
  /**
31
32
  * This CLI's own version, and the User-Agent built from it.
@@ -337,6 +338,27 @@ function credentialsPath() {
337
338
  return path.join(os.homedir(), ".rebase", "credentials.json");
338
339
  }
339
340
  /** Project-local link file: <project>/.rebase/cloud.json */
341
+ /**
342
+ * The billing account an organization row points at, whichever key it arrived under.
343
+ *
344
+ * REST serves every column under its own property name, so the relation comes
345
+ * back as `billingAccountId`. Both readers in this CLI checked
346
+ * `billing_account_id` and `billingAccount` — neither is what arrives — so
347
+ * `rebase cloud billing` answered "no billing account" for every organization,
348
+ * and the deploy pre-check never saw an internal plan. The other two spellings
349
+ * are still accepted: older servers and fixtures send them.
350
+ */
351
+ function billingAccountIdOf(org) {
352
+ if (typeof org !== "object" || org === null) return void 0;
353
+ for (const key of [
354
+ "billingAccountId",
355
+ "billing_account_id",
356
+ "billingAccount"
357
+ ]) {
358
+ const value = org[key];
359
+ if (typeof value === "string" || typeof value === "number") return value;
360
+ }
361
+ }
340
362
  function projectLinkPath(cwd = process.cwd()) {
341
363
  const root = findProjectRoot(cwd) || cwd;
342
364
  return path.join(root, ".rebase", "cloud.json");
@@ -1175,6 +1197,45 @@ function openUrl(target, label = "Opening") {
1175
1197
  child.unref();
1176
1198
  } catch {}
1177
1199
  }
1200
+ /**
1201
+ * Read the control plane's rows as a subcommand's row shape.
1202
+ *
1203
+ * `client.data.collection(...)` is typed against the *generated* schema of the
1204
+ * project the SDK is pointed at, and the control-plane collections this CLI
1205
+ * reads are not in one — so every row arrives as an open
1206
+ * `Record<string, unknown>`. A declared `interface` gets no implicit index
1207
+ * signature, so it does not overlap that, and a direct `as` is refused: which
1208
+ * is how ten call sites came to write `as unknown as XRow[]`, an assertion
1209
+ * about a wire payload with nothing checking it in either direction.
1210
+ *
1211
+ * There is exactly one invariant to check and this checks it. Everything else
1212
+ * the shapes declare is optional and already read as such, so there is nothing
1213
+ * further to verify — but a row with no usable `id` is not a row any of these
1214
+ * commands can act on, and passing it through was how a listing came to print
1215
+ * `[undefined]` and a lookup came to compare against the string `"undefined"`.
1216
+ */
1217
+ function cloudRows(rows) {
1218
+ return (rows ?? []).filter((row) => typeof row?.id === "string" || typeof row?.id === "number");
1219
+ }
1220
+ /** {@link cloudRows} for an endpoint that returns a single row. */
1221
+ function cloudRow(row) {
1222
+ return cloudRows(row ? [row] : [])[0];
1223
+ }
1224
+ /**
1225
+ * {@link cloudRows} for a write that must have produced a row.
1226
+ *
1227
+ * `create()` returning something with no usable `id` means the control plane
1228
+ * accepted the write and then described it in a way this CLI cannot act on.
1229
+ * The callers all go straight on to use that id — `setContextOrg(url,
1230
+ * String(created.id))` — so asserting the shape, which is what stood here,
1231
+ * turned a control-plane fault into an organization whose active id is the
1232
+ * seven-letter string `"undefined"`, stored in the user's config file.
1233
+ */
1234
+ function requireCloudRow(row, what) {
1235
+ const parsed = cloudRow(row);
1236
+ if (!parsed) throw new Error(`The control plane accepted the ${what} but returned no id for it.`);
1237
+ return parsed;
1238
+ }
1178
1239
  /** Duration in coarse bands — enough to see "slow", not enough to fingerprint. */
1179
1240
  function durationBucket(ms) {
1180
1241
  if (!Number.isFinite(ms) || ms < 0) return "unknown";
@@ -1184,6 +1245,24 @@ function durationBucket(ms) {
1184
1245
  return "120s+";
1185
1246
  }
1186
1247
  /**
1248
+ * An error reduced to something safe to transmit.
1249
+ *
1250
+ * The message and the stack are discarded, always. What survives is the
1251
+ * constructor name and, for the errors that carry one, a `code` — both of which
1252
+ * come from the program rather than from anything the user typed or named.
1253
+ * `EACCES` is useful and safe; "cannot write /Users/francesco/clients/acme" is
1254
+ * neither.
1255
+ */
1256
+ function errorClass(error) {
1257
+ if (error && typeof error === "object") {
1258
+ const code = error.code;
1259
+ if (typeof code === "string" && /^[A-Z][A-Z0-9_]{1,31}$/.test(code)) return code;
1260
+ const name = error.name;
1261
+ if (typeof name === "string" && /^[A-Za-z][A-Za-z0-9]{0,31}$/.test(name)) return name;
1262
+ }
1263
+ return "Unknown";
1264
+ }
1265
+ /**
1187
1266
  * Drop anything that is not a permitted value type, and clamp strings.
1188
1267
  *
1189
1268
  * The last line of defence rather than the first. Every call site is supposed
@@ -1495,33 +1574,30 @@ function renderPreview(event, properties) {
1495
1574
  * CI must behave exactly as it does today, which means not asking and not
1496
1575
  * sending.
1497
1576
  */
1498
- async function promptForConsent(event, properties, options = {}) {
1577
+ async function promptForConsent(options = {}) {
1499
1578
  if (!shouldPrompt(process.env, options)) return false;
1500
1579
  const askedBefore = suppressionReason(process.env) === "declined";
1501
1580
  try {
1502
1581
  console.log("");
1503
1582
  console.log(chalk.bold("Help improve Rebase?"));
1504
1583
  console.log("");
1505
- console.log(chalk.gray(" Rebase is self-hosted, so we have no idea what works and what does not"));
1506
- console.log(chalk.gray(" unless you tell us. Sharing is entirely optional and off by default."));
1507
- console.log("");
1508
- console.log(chalk.gray(" This is exactly what would be sent — nothing more, ever:"));
1509
- console.log("");
1510
- console.log(renderPreview(event, properties).split("\n").map((line) => chalk.gray(" " + line)).join("\n"));
1584
+ console.log(chalk.gray(" Rebase is self-hosted, so we only learn what works if you tell us."));
1585
+ console.log(chalk.gray(" Anonymous: random ids, the CLI version, your OS, and which template and"));
1586
+ console.log(chalk.gray(" package manager you used. Never project names, paths, schemas, URLs or"));
1587
+ console.log(chalk.gray(" error messages."));
1511
1588
  console.log("");
1512
- console.log(chalk.gray(" No project names, paths, schemas, URLs or error messages. Change your"));
1513
- console.log(chalk.gray(` mind any time with ${chalk.cyan("rebase telemetry disable")}.`));
1589
+ console.log(chalk.gray(` Print the exact payload with ${chalk.cyan("rebase telemetry show")}; change your mind`));
1590
+ console.log(chalk.gray(` any time with ${chalk.cyan("rebase telemetry disable")}.`));
1514
1591
  if (askedBefore) {
1515
1592
  console.log("");
1516
- console.log(chalk.gray(" You declined before, and that is still the answer unless you change it"));
1517
- console.log(chalk.gray(" here. Asked once per new project; never during ordinary work."));
1593
+ console.log(chalk.gray(" You declined before; that stands unless you change it here."));
1518
1594
  }
1519
1595
  console.log("");
1520
1596
  const { accepted } = await inquirer.prompt([{
1521
1597
  type: "confirm",
1522
1598
  name: "accepted",
1523
1599
  message: "Share anonymous usage data?",
1524
- default: false
1600
+ default: true
1525
1601
  }]);
1526
1602
  setConsent(Boolean(accepted));
1527
1603
  console.log(accepted ? chalk.green(" Thank you — sharing enabled.") : chalk.gray(" Nothing will be sent." + (options.reAskDeclined ? " You will be asked again the next time you scaffold a project." : " You will not be asked again.")));
@@ -1626,11 +1702,6 @@ var PRESET_CHOICES = [
1626
1702
  short: "Blank"
1627
1703
  }
1628
1704
  ];
1629
- /**
1630
- * Builds the interactive prompt questions for `rebase init`.
1631
- * Exported for testability — all prompt `type` values must match
1632
- * types registered by the installed version of inquirer.
1633
- */
1634
1705
  function buildInitQuestions(params) {
1635
1706
  const { nameArg, templateArg, headlessArg, hasGitFlag, hasInstallFlag, pm } = params;
1636
1707
  const questions = [];
@@ -2124,7 +2195,7 @@ async function createProject$1(options) {
2124
2195
  git: Boolean(options.git),
2125
2196
  duration: durationBucket(Date.now() - startedAt)
2126
2197
  };
2127
- if (await promptForConsent("cli.init", initProperties, { reAskDeclined: true })) await recordEvent("cli.init", initProperties, { projectRoot: options.targetDirectory });
2198
+ if (await promptForConsent({ reAskDeclined: true })) await recordEvent("cli.init", initProperties, { projectRoot: options.targetDirectory });
2128
2199
  }
2129
2200
  /**
2130
2201
  * Apply a template preset by replacing the default collection files.
@@ -4403,6 +4474,38 @@ function checkAppPath(value, fieldPath, issues) {
4403
4474
  }
4404
4475
  return value;
4405
4476
  }
4477
+ /**
4478
+ * Is `candidate` the path `parent`, or something beneath it?
4479
+ *
4480
+ * Segment-aware for the same reason `isForbiddenStaticPath` is: a plain
4481
+ * `startsWith` reads `/admin` as living under `/adm`.
4482
+ */
4483
+ function isUnderPath(candidate, parent) {
4484
+ if (parent === "/") return true;
4485
+ return candidate === parent || candidate.startsWith(`${parent}/`);
4486
+ }
4487
+ /**
4488
+ * Validate where an app says it mounts the Rebase CMS.
4489
+ *
4490
+ * The same shape as {@link checkAppPath} — it is a URL path and ends up in the
4491
+ * same places — plus one rule of its own: it has to be inside the app declaring
4492
+ * it. The app's SPA fallback is what answers that URL, so a `cms` outside its
4493
+ * `path` names an address this app will never serve. That is a link the console
4494
+ * would then offer to a 404, which is worse than the missing link it replaces.
4495
+ */
4496
+ function checkCmsPath(value, appPath, fieldPath, issues) {
4497
+ if (value === void 0) return void 0;
4498
+ const cms = checkAppPath(value, fieldPath, issues);
4499
+ if (cms === void 0) return void 0;
4500
+ if (!isUnderPath(cms, appPath)) {
4501
+ issues.push({
4502
+ path: fieldPath,
4503
+ message: `must be inside this app's path — it is at "${appPath}", so it cannot serve "${cms}". The CMS is a route of this app, not a separate deployment; if it really lives elsewhere, declare that app and put \`cms\` on it instead`
4504
+ });
4505
+ return;
4506
+ }
4507
+ return cms;
4508
+ }
4406
4509
  function isRecord(value) {
4407
4510
  return typeof value === "object" && value !== null && !Array.isArray(value);
4408
4511
  }
@@ -4412,8 +4515,25 @@ function isRecord(value) {
4412
4515
  * A manifest is committed and reviewed, so this is not a security boundary so
4413
4516
  * much as a guard against `../../` typos that would otherwise have `rebase build`
4414
4517
  * writing outside the project.
4415
- */
4416
- function checkRelativePath(value, fieldPath, issues, { required }) {
4518
+ *
4519
+ * `mayEscape` is for `context`, and only for `context`. Every other path here
4520
+ * names something Rebase *reads* — collections, functions, the generated
4521
+ * schema, the built assets to serve — and those have to be inside the project
4522
+ * or the bundle cannot carry them. A Docker build context is the one field that
4523
+ * names something Rebase never opens: it is handed to `docker build`, and in
4524
+ * any workspace repository the thing it has to name is the workspace root,
4525
+ * above the app. That is not exotic. It is what pnpm, turbo and nx layouts all
4526
+ * look like, and it is what this repository's own reference project needs — its
4527
+ * Dockerfile's first instruction copies `pnpm-lock.yaml`, which does not exist
4528
+ * beside `rebase.json` and never will.
4529
+ *
4530
+ * Refusing it did not prevent the escape; it only stopped anyone declaring it.
4531
+ * `app/rebase.json` built from the monorepo root the whole time — via
4532
+ * `infra/cloudbuild.yaml`, which says `-f app/backend/Dockerfile .` — while the
4533
+ * manifest said the context was `app/` and `rebase build` printed a command
4534
+ * that dies on its first `COPY`.
4535
+ */
4536
+ function checkRelativePath(value, fieldPath, issues, { required, mayEscape = false }) {
4417
4537
  if (value === void 0) {
4418
4538
  if (required) issues.push({
4419
4539
  path: fieldPath,
@@ -4436,7 +4556,7 @@ function checkRelativePath(value, fieldPath, issues, { required }) {
4436
4556
  return;
4437
4557
  }
4438
4558
  const normalized = path.normalize(value);
4439
- if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
4559
+ if (!mayEscape && (normalized === ".." || normalized.startsWith(`..${path.sep}`))) {
4440
4560
  issues.push({
4441
4561
  path: fieldPath,
4442
4562
  message: "must stay inside the project directory"
@@ -4465,7 +4585,8 @@ var KNOWN_APP_FIELDS = {
4465
4585
  "build",
4466
4586
  "output",
4467
4587
  "path",
4468
- "spa"
4588
+ "spa",
4589
+ "cms"
4469
4590
  ]
4470
4591
  };
4471
4592
  /**
@@ -4570,14 +4691,28 @@ function validateApp(name, raw, issues) {
4570
4691
  message: "only applies to a custom runtime — set \"runtime\": \"custom\" to build your own image"
4571
4692
  });
4572
4693
  checkRelativePath(raw.dockerfile, `${base}.dockerfile`, issues, { required: false });
4573
- checkRelativePath(raw.context, `${base}.context`, issues, { required: false });
4694
+ checkRelativePath(raw.context, `${base}.context`, issues, {
4695
+ required: false,
4696
+ mayEscape: true
4697
+ });
4574
4698
  if (raw.port !== void 0 && (typeof raw.port !== "number" || !Number.isInteger(raw.port))) issues.push({
4575
4699
  path: `${base}.port`,
4576
4700
  message: "must be an integer"
4577
4701
  });
4578
- return raw;
4702
+ return {
4703
+ type: "backend",
4704
+ runtime: custom ? "custom" : "managed",
4705
+ config: raw.config,
4706
+ functions: raw.functions,
4707
+ crons: raw.crons,
4708
+ schema: raw.schema,
4709
+ usersCollection: raw.usersCollection,
4710
+ dockerfile: raw.dockerfile,
4711
+ context: raw.context,
4712
+ port: raw.port
4713
+ };
4579
4714
  }
4580
- case "static":
4715
+ case "static": {
4581
4716
  checkRelativePath(raw.root, `${base}.root`, issues, { required: true });
4582
4717
  checkRelativePath(raw.output, `${base}.output`, issues, { required: true });
4583
4718
  if (raw.build !== void 0 && typeof raw.build !== "string") issues.push({
@@ -4588,8 +4723,18 @@ function validateApp(name, raw, issues) {
4588
4723
  path: `${base}.spa`,
4589
4724
  message: "must be a boolean"
4590
4725
  });
4591
- checkAppPath(raw.path, `${base}.path`, issues);
4592
- return raw;
4726
+ const appPath = checkAppPath(raw.path, `${base}.path`, issues);
4727
+ checkCmsPath(raw.cms, appPath ?? "/", `${base}.cms`, issues);
4728
+ return {
4729
+ type: "static",
4730
+ root: raw.root,
4731
+ build: raw.build,
4732
+ output: raw.output,
4733
+ path: appPath,
4734
+ spa: raw.spa,
4735
+ cms: raw.cms
4736
+ };
4737
+ }
4593
4738
  default: return;
4594
4739
  }
4595
4740
  }
@@ -4656,6 +4801,11 @@ function validateManifest(raw) {
4656
4801
  }
4657
4802
  byPath.set(at, name);
4658
4803
  }
4804
+ const withCms = Object.entries(apps).filter(([, app]) => app.type === "static" && typeof app.cms === "string");
4805
+ if (withCms.length > 1) for (const [name] of withCms.slice(1)) issues.push({
4806
+ path: `apps.${name}.cms`,
4807
+ message: `a project has one CMS — "${withCms[0][0]}" already declares one`
4808
+ });
4659
4809
  refuseStorageBlock(raw.storage, issues);
4660
4810
  let telemetry;
4661
4811
  if (raw.telemetry !== void 0) if (typeof raw.telemetry === "boolean") telemetry = raw.telemetry;
@@ -4838,6 +4988,30 @@ function findBackendApp(manifest) {
4838
4988
  };
4839
4989
  }
4840
4990
  /**
4991
+ * Where this project mounts the Rebase CMS, if it says.
4992
+ *
4993
+ * The CMS is a component inside a developer's own app, so its address is a
4994
+ * client-side route: no build artifact, no running server and no control plane
4995
+ * can observe it. {@link RebaseStaticAppConfig.cms} is the only place that fact
4996
+ * is ever written down, and this is the one reader of it — so that "the
4997
+ * project's CMS" means the same thing to `rebase dev`, `rebase apps list`, the
4998
+ * bundle it builds and the console that shows it.
4999
+ *
5000
+ * Returns the *serving* app alongside the path, because a caller with a base URL
5001
+ * needs to know which app answers there — and because the path alone cannot say
5002
+ * whether the CMS is the whole of an app or one route of it.
5003
+ */
5004
+ function cmsMountOf(manifest) {
5005
+ for (const [name, app] of Object.entries(manifest.apps)) {
5006
+ if (app.type !== "static" || typeof app.cms !== "string") continue;
5007
+ return {
5008
+ appName: name,
5009
+ app,
5010
+ path: app.cms
5011
+ };
5012
+ }
5013
+ }
5014
+ /**
4841
5015
  * The app a deploy targets: the one named, or the obvious one.
4842
5016
  *
4843
5017
  * A repository declares apps; a project owns them. So "which app" is a question
@@ -5634,14 +5808,21 @@ async function devCommand(rawArgs) {
5634
5808
  * a shape that has no such directory reads as a broken scaffold on the
5635
5809
  * very first run of the very command the headless quickstart names.
5636
5810
  */
5637
- const declaresStaticApp = (() => {
5811
+ const staticShape = (() => {
5638
5812
  try {
5639
5813
  const { manifest } = loadManifest(projectRoot);
5640
- return Object.values(manifest.apps ?? {}).some((app) => app.type === "static");
5814
+ return {
5815
+ declaresStaticApp: Object.values(manifest.apps ?? {}).some((app) => app.type === "static"),
5816
+ cmsPath: cmsMountOf(manifest)?.path
5817
+ };
5641
5818
  } catch {
5642
- return Boolean(frontendDir);
5819
+ return {
5820
+ declaresStaticApp: Boolean(frontendDir),
5821
+ cmsPath: void 0
5822
+ };
5643
5823
  }
5644
5824
  })();
5825
+ const declaresStaticApp = staticShape.declaresStaticApp;
5645
5826
  let frontendUrl = "";
5646
5827
  let backendUrl = "";
5647
5828
  let debounceSummary = null;
@@ -5681,6 +5862,7 @@ async function devCommand(rawArgs) {
5681
5862
  ["✦ Rebase API is ready!", ""],
5682
5863
  ["➜ API: ", api]
5683
5864
  ];
5865
+ if (declaresStaticApp && staticShape.cmsPath && staticShape.cmsPath !== "/") lines.push(["➜ CMS: ", `${stripAnsi(frontendUrl).replace(/\/$/, "")}${staticShape.cmsPath}`]);
5684
5866
  if (!declaresStaticApp) {
5685
5867
  if (swaggerPath) lines.push(["➜ Swagger: ", `${api}${swaggerPath}`]);
5686
5868
  else lines.push([" ", "(no tables served yet, so no data API and no docs)"]);
@@ -7797,7 +7979,7 @@ function vendorDependencies(options) {
7797
7979
  * in one image already, that is exactly what it had.
7798
7980
  */
7799
7981
  function foldStaticIntoBundle(options) {
7800
- const { bundleDir, assetsDir, appName, path: basePath, spa } = options;
7982
+ const { bundleDir, assetsDir, appName, path: basePath, spa, cms } = options;
7801
7983
  const manifestPath = path.join(bundleDir, "manifest.json");
7802
7984
  if (!fs.existsSync(manifestPath)) throw new Error(`No manifest at ${manifestPath} — build the backend bundle first.`);
7803
7985
  if (!fs.existsSync(assetsDir)) throw new Error(`No built assets at ${assetsDir}.`);
@@ -7822,7 +8004,9 @@ function foldStaticIntoBundle(options) {
7822
8004
  static: [...existing, {
7823
8005
  path: basePath,
7824
8006
  dir,
7825
- spa
8007
+ spa,
8008
+ name: appName,
8009
+ ...cms ? { cms } : {}
7826
8010
  }]
7827
8011
  };
7828
8012
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
@@ -7857,7 +8041,9 @@ function buildStaticBundle(options) {
7857
8041
  entry: { static: [{
7858
8042
  path: basePath,
7859
8043
  dir: "static",
7860
- spa: options.spa ?? true
8044
+ spa: options.spa ?? true,
8045
+ name: appName,
8046
+ ...options.cms ? { cms: options.cms } : {}
7861
8047
  }] },
7862
8048
  hooks: { native: false },
7863
8049
  deps: { declared: {} },
@@ -7989,7 +8175,8 @@ function foldableApps(manifest) {
7989
8175
  build: app.build,
7990
8176
  output: app.output,
7991
8177
  path: app.path ?? "/",
7992
- spa: app.spa ?? true
8178
+ spa: app.spa ?? true,
8179
+ cms: app.cms
7993
8180
  });
7994
8181
  }
7995
8182
  apps.sort((a, b) => b.path.length - a.path.length);
@@ -8109,7 +8296,8 @@ async function foldFrontendIntoBundle(options) {
8109
8296
  assetsDir,
8110
8297
  appName: app.name,
8111
8298
  path: app.path,
8112
- spa: app.spa
8299
+ spa: app.spa,
8300
+ cms: app.cms
8113
8301
  });
8114
8302
  outcomes.push({
8115
8303
  appName: app.name,
@@ -8621,6 +8809,42 @@ ${chalk.bold("Examples")}
8621
8809
  rebase build web Build only the "web" static app
8622
8810
  `.trim());
8623
8811
  }
8812
+ /**
8813
+ * The two commands that turn a custom-runtime app into an image.
8814
+ *
8815
+ * Exported for the test, because the failure this replaced was in the printed
8816
+ * text and nowhere else. It said:
8817
+ *
8818
+ * docker build -f backend/Dockerfile .
8819
+ *
8820
+ * for every custom backend, and the `.` was a guess — `context` was validated,
8821
+ * stored on the config, and read by nothing. For the reference project that
8822
+ * guess is wrong: `app/backend/Dockerfile` opens by copying `pnpm-lock.yaml`
8823
+ * and `pnpm-workspace.yaml`, which live at the monorepo root, so the command
8824
+ * `rebase build` handed you died on its first instruction. The deploy that
8825
+ * actually works, `infra/cloudbuild.yaml`, has always said
8826
+ * `-f app/backend/Dockerfile .` from the root.
8827
+ *
8828
+ * `dockerfile` is relative to `rebase.json`; `context` is too, and may point
8829
+ * above it. Docker resolves `-f` against the working directory, not the
8830
+ * context, so the path has to be re-expressed against wherever the command
8831
+ * runs — which is what the old line never did and is the whole reason it
8832
+ * could not be right for both.
8833
+ */
8834
+ function dockerBuildHint(projectRoot, name, app) {
8835
+ const dockerfile = app.dockerfile ?? "Dockerfile";
8836
+ const context = app.context ?? ".";
8837
+ const contextDir = path.resolve(projectRoot, context);
8838
+ const fromContext = path.relative(contextDir, path.resolve(projectRoot, dockerfile));
8839
+ const build = `${chalk.cyan(`npm run build --workspace ${name}`)}`;
8840
+ if (fromContext.startsWith("..")) return [
8841
+ chalk.dim(` ${build}`),
8842
+ chalk.yellow(` ⚠ ${dockerfile} is outside the build context (${context}) — nothing it COPYs is reachable.`),
8843
+ chalk.dim(` Widen "context" in rebase.json, or move the Dockerfile inside it.`)
8844
+ ];
8845
+ const docker = chalk.cyan(`docker build -f ${fromContext} .`);
8846
+ return context === "." ? [chalk.dim(` ${build} then ${docker}`)] : [chalk.dim(` ${build}`), chalk.dim(` ${chalk.cyan(`cd ${context}`)} && ${docker}`)];
8847
+ }
8624
8848
  async function buildCommand(rawArgs = []) {
8625
8849
  if (wantsHelp(rawArgs)) {
8626
8850
  printHelp$5();
@@ -8684,7 +8908,7 @@ async function buildCommand(rawArgs = []) {
8684
8908
  console.log(chalk.cyan(`▸ ${name}`) + chalk.dim(` (${app.type})`));
8685
8909
  if (app.type === "backend" && app.runtime === "custom") {
8686
8910
  console.log(chalk.dim(" custom runtime — this project builds its own image, not a bundle"));
8687
- console.log(chalk.dim(` ${chalk.cyan(`npm run build --workspace ${name}`)} then ${chalk.cyan(`docker build -f ${app.dockerfile ?? "Dockerfile"} .`)}`));
8911
+ for (const line of dockerBuildHint(projectRoot, name, app)) console.log(line);
8688
8912
  console.log("");
8689
8913
  continue;
8690
8914
  }
@@ -8821,7 +9045,8 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
8821
9045
  outDir: outOverride ? path.resolve(process.cwd(), outOverride) : path.join(projectRoot, `dist-bundle-${name}`),
8822
9046
  runtimeRange,
8823
9047
  path: basePath,
8824
- spa: asset.spa ?? true
9048
+ spa: asset.spa ?? true,
9049
+ cms: asset.cms
8825
9050
  });
8826
9051
  const rel = path.relative(projectRoot, result.outDir);
8827
9052
  console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
@@ -8875,8 +9100,19 @@ function findCliRoot(from) {
8875
9100
  }
8876
9101
  return null;
8877
9102
  }
8878
- /** The block names the payload may switch on. A typo has to be an error. */
9103
+ /**
9104
+ * The block names the payload may switch on. A typo has to be an error.
9105
+ *
9106
+ * Typed as keys of {@link ProjectShape}, which is what makes that true: `as
9107
+ * const` alone gave these two string literals and tied them to nothing, so a
9108
+ * flag renamed on the interface left this list naming a block that no longer
9109
+ * exists, and the template kept switching on it.
9110
+ */
8879
9111
  var SHAPE_FLAGS = ["collections", "frontend"];
9112
+ /** Narrow a marker name to a flag, having checked it is one. */
9113
+ function isShapeFlag(name) {
9114
+ return SHAPE_FLAGS.includes(name);
9115
+ }
8880
9116
  /**
8881
9117
  * Render one payload file for this project.
8882
9118
  *
@@ -8900,7 +9136,6 @@ var SHAPE_FLAGS = ["collections", "frontend"];
8900
9136
  * typechecker would see.
8901
9137
  */
8902
9138
  function renderPayload(contents, shape, projectName) {
8903
- const flags = shape;
8904
9139
  const out = [];
8905
9140
  let open = null;
8906
9141
  for (const line of contents.split("\n")) {
@@ -8916,10 +9151,10 @@ function renderPayload(contents, shape, projectName) {
8916
9151
  continue;
8917
9152
  }
8918
9153
  if (open) throw new Error(`Eject template: {{${kind}${name}}} inside an open ${open.name} block.`);
8919
- if (!SHAPE_FLAGS.includes(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
9154
+ if (!isShapeFlag(name)) throw new Error(`Eject template: unknown block {{${kind}${name}}}.`);
8920
9155
  open = {
8921
9156
  name,
8922
- keep: kind === "#" ? flags[name] === true : flags[name] !== true
9157
+ keep: kind === "#" ? shape[name] === true : shape[name] !== true
8923
9158
  };
8924
9159
  }
8925
9160
  if (open) throw new Error(`Eject template: {{#${open.name}}} was never closed.`);
@@ -9004,13 +9239,6 @@ async function ejectCommand(rawArgs = []) {
9004
9239
  const dryRun = Boolean(args["--dry-run"]);
9005
9240
  const force = Boolean(args["--force"]);
9006
9241
  const requested = positionals[0];
9007
- if (requested === "infra") {
9008
- console.error(chalk.red(" ✗ `rebase eject infra` has been removed."));
9009
- console.error(chalk.gray(" It wrote rebase.infra.json, which nothing ever read. Resources bind"));
9010
- console.error(chalk.gray(" from the environment on the <BASE>__<KEY> convention; declare them in"));
9011
- console.error(chalk.gray(" config/resources.ts and see `rebase resources` for the names."));
9012
- process.exit(1);
9013
- }
9014
9242
  let loaded;
9015
9243
  try {
9016
9244
  loaded = loadManifest(projectRoot);
@@ -11735,8 +11963,11 @@ function printPayload() {
11735
11963
  console.log("");
11736
11964
  console.log(` ${describeState()}`);
11737
11965
  console.log("");
11738
- console.log(chalk.gray(" Nothing is being sent, so there is no payload to show."));
11739
- console.log(chalk.gray(` Run ${chalk.cyan("rebase telemetry enable")} first if you want to inspect one.`));
11966
+ console.log(chalk.gray(" Nothing is being sent. This is what WOULD be sent if you said yes:"));
11967
+ console.log("");
11968
+ console.log(renderPreview("cli.dev", { first_run: false }).split("\n").map((l) => " " + l).join("\n"));
11969
+ console.log("");
11970
+ console.log(chalk.gray(` Start sharing with ${chalk.cyan("rebase telemetry enable")}.`));
11740
11971
  console.log("");
11741
11972
  return;
11742
11973
  }
@@ -11752,7 +11983,7 @@ function printPayload() {
11752
11983
  }
11753
11984
  function printHelp$2() {
11754
11985
  console.log(`
11755
- ${chalk.bold("rebase telemetry")} — anonymous usage sharing (opt-in, off by default)
11986
+ ${chalk.bold("rebase telemetry")} — anonymous usage sharing (asked once per project)
11756
11987
 
11757
11988
  ${chalk.bold("Commands")}
11758
11989
  ${chalk.blue("status")} Whether anything is being shared, and why ${chalk.gray("(default)")}
@@ -12039,10 +12270,10 @@ async function linkCommand(rawArgs) {
12039
12270
  } else {
12040
12271
  requireInteractive("a project to link", "--project <slug>");
12041
12272
  const org = getContextOrg(url);
12042
- const projects = (await client.data.collection("projects").find({
12273
+ const projects = cloudRows((await client.data.collection("projects").find({
12043
12274
  where: org ? { organization: ["==", org] } : void 0,
12044
12275
  limit: 100
12045
- })).data;
12276
+ })).data);
12046
12277
  if (projects.length === 0) fail("No projects found for your account.", `Create one with ${chalk.bold("rebase cloud projects create")}.`, "no_projects");
12047
12278
  const { picked } = await inquirer.prompt([{
12048
12279
  type: "select",
@@ -12506,14 +12737,14 @@ async function webhooksCommand(subcommand, rawArgs) {
12506
12737
  const table = args["--table"] || fail("--table is required.", void 0, "usage");
12507
12738
  const url = args["--endpoint"] || fail("--endpoint is required.", "Where the POST goes.", "usage");
12508
12739
  const events = (args["--events"] || "insert,update,delete").split(",").map((s) => s.trim());
12509
- const created = await client.data.collection("webhooks").create({
12740
+ const created = requireCloudRow(await client.data.collection("webhooks").create({
12510
12741
  project: projectId,
12511
12742
  name,
12512
12743
  table,
12513
12744
  url,
12514
12745
  events,
12515
12746
  enabled: true
12516
- });
12747
+ }), "webhook");
12517
12748
  success(`Created webhook ${chalk.bold(name)} [${created.id}]`);
12518
12749
  emit(() => {}, {
12519
12750
  success: true,
@@ -12606,15 +12837,22 @@ async function storageCommand(action, rawArgs) {
12606
12837
  return;
12607
12838
  }
12608
12839
  for (const s of stores) {
12609
- console.log(` ${chalk.bold(s.bucketName ?? s.type ?? "bucket")} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
12610
- keyValues([["Provider", s.provider], ["Type", s.type]]);
12840
+ const name = s.bucketName || s.s3Bucket || "(no bucket)";
12841
+ const key = s.sourceKey || DEFAULT_STORAGE_SOURCE_KEY;
12842
+ console.log(` ${chalk.bold(name)} ${chalk.gray(`[${s.id}]`)} ${colorStatus(s.status)}`);
12843
+ keyValues([
12844
+ ["Source", key],
12845
+ ["Provider", s.provider],
12846
+ ["Type", s.type]
12847
+ ]);
12611
12848
  }
12612
12849
  console.log("");
12613
12850
  }, {
12614
12851
  projectId,
12615
12852
  stores: stores.map((s) => ({
12616
12853
  id: String(s.id),
12617
- bucketName: s.bucketName ?? null,
12854
+ sourceKey: s.sourceKey || DEFAULT_STORAGE_SOURCE_KEY,
12855
+ bucketName: s.bucketName || s.s3Bucket || null,
12618
12856
  type: s.type ?? null,
12619
12857
  provider: s.provider ?? null,
12620
12858
  status: s.status ?? null
@@ -12646,7 +12884,8 @@ function printStorageHelp() {
12646
12884
  ["--secret-access-key <s>", "Secret access key. Required"],
12647
12885
  ["--endpoint <url>", "S3 endpoint. Omit for AWS"],
12648
12886
  ["--region <region>", "Region"],
12649
- ["--force-path-style", "Required by MinIO and some gateways"]
12887
+ ["--force-path-style", "Required by MinIO and some gateways"],
12888
+ ["--source <key>", "Which declared bucket. Default: the default one"]
12650
12889
  ]
12651
12890
  }
12652
12891
  ],
@@ -12671,11 +12910,10 @@ async function storageCreateCommand(rawArgs) {
12671
12910
  try {
12672
12911
  noteBlank();
12673
12912
  note(chalk.gray("Provisioning managed storage — this creates a bucket and its credentials..."));
12674
- const res = await client.functions.invoke("storage-provision", void 0, {
12913
+ const info = (await client.functions.invoke("storage-provision", void 0, {
12675
12914
  method: "POST",
12676
12915
  path: projectId
12677
- });
12678
- const info = res.data ?? res.data;
12916
+ })).data;
12679
12917
  success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
12680
12918
  emit(() => {
12681
12919
  keyValues([
@@ -12710,7 +12948,8 @@ async function storageAttachCommand(rawArgs) {
12710
12948
  "--secret-access-key": String,
12711
12949
  "--endpoint": String,
12712
12950
  "--region": String,
12713
- "--force-path-style": Boolean
12951
+ "--force-path-style": Boolean,
12952
+ "--source": String
12714
12953
  },
12715
12954
  rawArgs,
12716
12955
  commandWords: 3,
@@ -12728,13 +12967,18 @@ async function storageAttachCommand(rawArgs) {
12728
12967
  if (missing.length > 0) fail(`Missing ${missing.join(", ")}.`, "A bucket without credentials cannot be used, and would be stored as though it could. Run `rebase cloud storage --help` for the full list.", "usage");
12729
12968
  const { client } = await requireClient(rawArgs);
12730
12969
  const projectId = await requireProject(rawArgs, client);
12970
+ const sourceKey = (parsed["--source"] ?? "").trim() || DEFAULT_STORAGE_SOURCE_KEY;
12731
12971
  try {
12732
12972
  const existing = (await client.data.collection("storages").find({
12733
12973
  where: { project: ["==", projectId] },
12734
- limit: 1
12735
- })).data[0];
12974
+ limit: 50
12975
+ })).data.find((r) => {
12976
+ const key = r.sourceKey;
12977
+ return (typeof key === "string" && key ? key : DEFAULT_STORAGE_SOURCE_KEY) === sourceKey;
12978
+ });
12736
12979
  const row = {
12737
12980
  project: projectId,
12981
+ sourceKey,
12738
12982
  type: "byos",
12739
12983
  status: "active",
12740
12984
  s3Bucket: bucket,
@@ -13065,8 +13309,7 @@ async function billingCommand(rawArgs) {
13065
13309
  }
13066
13310
  if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
13067
13311
  try {
13068
- const orgRow = await client.data.collection("organizations").findById(org);
13069
- const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
13312
+ const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
13070
13313
  if (!billingId) {
13071
13314
  emit(() => {
13072
13315
  console.log("");
@@ -13154,7 +13397,6 @@ var DIAL_FLAGS = {
13154
13397
  "--replicas": "replicaCount",
13155
13398
  "--spot": "preemptible",
13156
13399
  "--scale-to-zero": "scaleToZero",
13157
- "--db-mode": "databaseMode",
13158
13400
  "--db-instances": "databaseInstances",
13159
13401
  "--db-cpu": "databaseCpu",
13160
13402
  "--db-memory": "databaseMemory",
@@ -13246,7 +13488,6 @@ async function computeCommand(action, rawArgs) {
13246
13488
  true: "scale to zero",
13247
13489
  false: "stay warm"
13248
13490
  })],
13249
- ["Database", dialLine(project.databaseMode)],
13250
13491
  ["Database instances", dialLine(project.databaseInstances)],
13251
13492
  ["Database CPU", dialLine(project.databaseCpu)],
13252
13493
  ["Database memory", dialLine(project.databaseMemory)],
@@ -13273,7 +13514,6 @@ async function computeCommand(action, rawArgs) {
13273
13514
  replicaCount: project.replicaCount ?? null,
13274
13515
  preemptible: project.preemptible ?? null,
13275
13516
  scaleToZero: project.scaleToZero ?? null,
13276
- databaseMode: project.databaseMode ?? null,
13277
13517
  databaseInstances: project.databaseInstances ?? null,
13278
13518
  databaseCpu: project.databaseCpu ?? null,
13279
13519
  databaseMemory: project.databaseMemory ?? null,
@@ -13355,6 +13595,227 @@ function buildDialPatch(rawArgs, opts) {
13355
13595
  return { patch };
13356
13596
  }
13357
13597
  //#endregion
13598
+ //#region src/commands/cloud/db-connect.ts
13599
+ /**
13600
+ * `rebase cloud db connect` — a local port that is your cloud database.
13601
+ *
13602
+ * ## What this replaces
13603
+ *
13604
+ * A managed database lives in a namespace of the platform's cluster, and its
13605
+ * address (`postgres-rw.rebase-tenant-….svc.cluster.local`) resolves to nothing
13606
+ * on a developer's machine. The console used to bridge that gap by printing
13607
+ *
13608
+ * kubectl port-forward svc/postgres-rw -n rebase-tenant-… 5432:5432
13609
+ *
13610
+ * which nobody outside the platform can run: a tenant of Rebase Cloud has no
13611
+ * kubeconfig for our cluster, and there is no product that sells one. So the
13612
+ * platform reaches into the cluster instead, and this command is the local end
13613
+ * of that reach.
13614
+ *
13615
+ * ## How it works
13616
+ *
13617
+ * A listener on 127.0.0.1. Every TCP connection it accepts opens its own
13618
+ * WebSocket to the control plane, authenticates in-band with the console session
13619
+ * this CLI already holds, and from `ready` onwards the two are a byte pipe. The
13620
+ * database still asks for a password — the tunnel is a network path, not a
13621
+ * credential — so `psql` behaves exactly as it would against a local Postgres.
13622
+ *
13623
+ * One WebSocket per connection rather than one multiplexed socket: `psql` is one
13624
+ * connection and a pool is a handful, and per-connection sockets keep the
13625
+ * framing at "these bytes are those bytes" with no stream ids to get wrong.
13626
+ *
13627
+ * Node's global `WebSocket` is used rather than `ws`, which is why the token
13628
+ * goes in the first frame instead of an `Authorization` header — the WHATWG
13629
+ * client cannot set request headers, and a header-only endpoint would be
13630
+ * unreachable from a browser too.
13631
+ */
13632
+ /** Documented in `action-help.ts`, and paired with it by `action-help.test.ts`. */
13633
+ var DB_CONNECT_FLAGS = {
13634
+ "--port": Number,
13635
+ "--reveal": Boolean
13636
+ };
13637
+ /** The default, because it is what every Postgres client assumes. */
13638
+ var DEFAULT_LOCAL_PORT = 5432;
13639
+ /** `https://app.rebase.pro` → `wss://app.rebase.pro/api/db-tunnel/p1`. */
13640
+ function tunnelUrl(cloudUrl, projectId) {
13641
+ const url = new URL(cloudUrl);
13642
+ url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
13643
+ url.pathname = `/api/db-tunnel/${encodeURIComponent(projectId)}`;
13644
+ url.search = "";
13645
+ return url.toString();
13646
+ }
13647
+ /**
13648
+ * A local DSN for the tunnel, with the password only if the caller asked.
13649
+ *
13650
+ * Built here and nowhere else. The server reports the *cluster's* URI, which is
13651
+ * the one thing that must not be printed as the way to connect — it is exactly
13652
+ * the address that does not work from here.
13653
+ */
13654
+ function localDsn(opts) {
13655
+ const user = opts.username ? encodeURIComponent(opts.username) : "postgres";
13656
+ const auth = opts.password ? `${user}:${encodeURIComponent(opts.password)}` : user;
13657
+ const database = opts.database ?? "rebase";
13658
+ return `postgresql://${auth}@127.0.0.1:${opts.port}/${database}`;
13659
+ }
13660
+ async function dbConnect(rawArgs) {
13661
+ const { flags: args } = parseCloudArgs({
13662
+ spec: DB_CONNECT_FLAGS,
13663
+ rawArgs,
13664
+ commandWords: 3,
13665
+ command: "cloud db connect",
13666
+ maxPositionals: 0
13667
+ });
13668
+ const { client, url } = await requireClient(rawArgs);
13669
+ const projectId = await requireProject(rawArgs, client);
13670
+ const projectRef = displayProjectRef(rawArgs);
13671
+ let info;
13672
+ try {
13673
+ info = await client.functions.invoke("db-info", void 0, {
13674
+ method: "GET",
13675
+ path: projectId
13676
+ });
13677
+ } catch (e) {
13678
+ return reportError(e, "Failed to load database info");
13679
+ }
13680
+ if (info.type === "byodb") fail("This project uses your own database.", "Connect to it directly — there is nothing for the platform to tunnel.", "byodb");
13681
+ if (!info.directAccess) fail("The platform could not resolve this project's database, so it cannot open a path to it.", info.unavailableReason ?? "A managed database is provisioned at the project's first deploy; before then there is nothing to connect to.", "db_unavailable");
13682
+ let password;
13683
+ if (args["--reveal"]) {
13684
+ if (!info.passwordAvailable) fail("No password is available to reveal for this database.", info.unavailableReason ?? void 0, "password_unavailable");
13685
+ try {
13686
+ password = (await client.functions.invoke("db-info", { projectId }, { path: "reveal" })).password;
13687
+ } catch (e) {
13688
+ return reportError(e, "Failed to reveal the database password");
13689
+ }
13690
+ }
13691
+ const requested = args["--port"] ?? DEFAULT_LOCAL_PORT;
13692
+ if (!Number.isInteger(requested) || requested < 0 || requested > 65535) fail(`--port must be a port number, not ${requested}.`, void 0, "bad_port");
13693
+ const endpoint = tunnelUrl(url, projectId);
13694
+ const server = net$1.createServer((socket) => {
13695
+ pipeThroughTunnel(socket, endpoint, client.auth.getSession()?.accessToken ?? "");
13696
+ });
13697
+ server.on("error", (err) => {
13698
+ if (err.code === "EADDRINUSE") fail(`Port ${requested} on 127.0.0.1 is already in use.`, "Pass --port to choose another, or stop whatever is listening there.", "port_in_use");
13699
+ fail(`Could not open a local listener: ${err.message}`, void 0, "listen_failed");
13700
+ });
13701
+ await new Promise((resolve) => {
13702
+ server.listen(requested, "127.0.0.1", () => resolve());
13703
+ });
13704
+ const address = server.address();
13705
+ const port = address && typeof address !== "string" ? address.port : requested;
13706
+ const dsn = localDsn({
13707
+ port,
13708
+ username: info.username,
13709
+ database: info.database,
13710
+ password
13711
+ });
13712
+ emit(() => {
13713
+ console.log("");
13714
+ console.log(chalk.bold(` 🔌 Tunnel open — project ${projectRef}`));
13715
+ console.log("");
13716
+ console.log(` ${chalk.green(dsn)}`);
13717
+ console.log("");
13718
+ if (!password) {
13719
+ console.log(chalk.gray(" The password is hidden. Re-run with --reveal to print it in the URL,"));
13720
+ console.log(chalk.gray(" or read it with `rebase cloud db info --reveal`."));
13721
+ console.log("");
13722
+ }
13723
+ console.log(chalk.gray(" Leave this running, and point any Postgres client at it."));
13724
+ console.log(chalk.gray(" Ctrl-C closes the tunnel."));
13725
+ console.log("");
13726
+ }, {
13727
+ projectId,
13728
+ host: "127.0.0.1",
13729
+ port,
13730
+ database: info.database,
13731
+ username: info.username,
13732
+ connectionString: dsn,
13733
+ ...password ? { password } : {}
13734
+ });
13735
+ await new Promise((resolve) => {
13736
+ process.once("SIGINT", () => {
13737
+ noteBlank();
13738
+ note(chalk.gray("Tunnel closed."));
13739
+ server.close();
13740
+ resolve();
13741
+ });
13742
+ });
13743
+ }
13744
+ /**
13745
+ * One accepted connection, carried over one WebSocket.
13746
+ *
13747
+ * The local socket is paused until the tunnel says `ready`, so a client that
13748
+ * sends its startup packet the instant it connects — which every Postgres
13749
+ * client does — cannot have those bytes arrive before there is a database to
13750
+ * send them to.
13751
+ *
13752
+ * Exported for `db-connect.pipe.test.ts`, which drives it with a real socket
13753
+ * against a real WebSocket: this is the half a developer's client actually
13754
+ * talks to, and a pipe is the one component whose bugs are silent — a dropped
13755
+ * or reordered chunk does not throw, it corrupts a Postgres message and
13756
+ * surfaces as a protocol error nowhere near here.
13757
+ */
13758
+ function pipeThroughTunnel(socket, endpoint, token) {
13759
+ socket.pause();
13760
+ socket.setNoDelay(true);
13761
+ const ws = new WebSocket(endpoint);
13762
+ ws.binaryType = "arraybuffer";
13763
+ let ready = false;
13764
+ const closeBoth = (reason) => {
13765
+ if (reason && !ready) note(chalk.red(`✗ ${reason}`));
13766
+ try {
13767
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
13768
+ } catch {}
13769
+ socket.destroy();
13770
+ };
13771
+ ws.onopen = () => {
13772
+ ws.send(JSON.stringify({
13773
+ type: "authenticate",
13774
+ token
13775
+ }));
13776
+ };
13777
+ ws.onmessage = (event) => {
13778
+ if (typeof event.data === "string") {
13779
+ let frame = null;
13780
+ try {
13781
+ frame = JSON.parse(event.data);
13782
+ } catch {
13783
+ return closeBoth(`the tunnel endpoint answered with ${event.data.slice(0, 80)}`);
13784
+ }
13785
+ if (frame.type === "ready") {
13786
+ ready = true;
13787
+ socket.resume();
13788
+ return;
13789
+ }
13790
+ const refusal = frame.type === "error" ? frame : null;
13791
+ return closeBoth(refusal?.message ?? `the tunnel was refused (${refusal?.code ?? "unknown"})`);
13792
+ }
13793
+ socket.write(Buffer.from(event.data));
13794
+ };
13795
+ ws.onerror = () => {
13796
+ closeBoth("could not reach the control plane's tunnel endpoint");
13797
+ };
13798
+ ws.onclose = () => {
13799
+ socket.destroy();
13800
+ };
13801
+ socket.on("data", (chunk) => {
13802
+ if (ws.readyState !== WebSocket.OPEN) return;
13803
+ ws.send(new Uint8Array(chunk));
13804
+ if (ws.bufferedAmount > 1024 * 1024) {
13805
+ socket.pause();
13806
+ const drain = setInterval(() => {
13807
+ if (ws.readyState !== WebSocket.OPEN) return clearInterval(drain);
13808
+ if (ws.bufferedAmount < 256 * 1024) {
13809
+ clearInterval(drain);
13810
+ socket.resume();
13811
+ }
13812
+ }, 5);
13813
+ }
13814
+ });
13815
+ socket.on("close", () => closeBoth());
13816
+ socket.on("error", () => closeBoth());
13817
+ }
13818
+ //#endregion
13358
13819
  //#region src/commands/cloud/databases.ts
13359
13820
  async function dbCommand(subcommand, rawArgs) {
13360
13821
  switch (subcommand) {
@@ -13368,6 +13829,9 @@ async function dbCommand(subcommand, rawArgs) {
13368
13829
  case "info":
13369
13830
  await dbInfo(rawArgs);
13370
13831
  break;
13832
+ case "connect":
13833
+ await dbConnect(rawArgs);
13834
+ break;
13371
13835
  case "test":
13372
13836
  await testDatabase(rawArgs);
13373
13837
  break;
@@ -13395,10 +13859,10 @@ async function listDatabases(rawArgs) {
13395
13859
  const projectId = await requireProject(rawArgs, client);
13396
13860
  const projectRef = displayProjectRef(rawArgs);
13397
13861
  try {
13398
- const dbs = (await client.data.collection("databases").find({
13862
+ const dbs = cloudRows((await client.data.collection("databases").find({
13399
13863
  where: { project: ["==", projectId] },
13400
13864
  limit: 50
13401
- })).data;
13865
+ })).data);
13402
13866
  emit(() => {
13403
13867
  console.log("");
13404
13868
  console.log(chalk.bold(` 🗄 Databases — project ${projectRef}`));
@@ -13435,10 +13899,10 @@ async function listDatabases(rawArgs) {
13435
13899
  * point, since the answer decides which row a deploy will actually use.
13436
13900
  */
13437
13901
  async function firstAttachedDatabase(client, projectId) {
13438
- return (await client.data.collection("databases").find({
13902
+ return cloudRows((await client.data.collection("databases").find({
13439
13903
  where: { project: ["==", projectId] },
13440
13904
  limit: 1
13441
- })).data[0];
13905
+ })).data)[0];
13442
13906
  }
13443
13907
  /**
13444
13908
  * Attach a database row to a project.
@@ -13449,12 +13913,12 @@ async function firstAttachedDatabase(client, projectId) {
13449
13913
  * afterwards".
13450
13914
  */
13451
13915
  async function attachDatabaseRow(client, input) {
13452
- return await client.data.collection("databases").create({
13916
+ return requireCloudRow(await client.data.collection("databases").create({
13453
13917
  project: input.projectId,
13454
13918
  type: input.type,
13455
13919
  connectionString: input.type === "byodb" ? input.connectionString : void 0,
13456
13920
  connectionStatus: "untested"
13457
- });
13921
+ }), "database");
13458
13922
  }
13459
13923
  /** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
13460
13924
  var CREATE_DATABASE_FLAGS = {
@@ -13641,10 +14105,19 @@ async function dbInfo(rawArgs) {
13641
14105
  ["Connection", connectionString]
13642
14106
  ]);
13643
14107
  if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
13644
- if (info.portForward) {
13645
- const pf = info.portForward;
14108
+ if (info.directAccess) {
14109
+ console.log("");
14110
+ console.log(chalk.gray(" The host above is inside the platform's cluster: it is the address your"));
14111
+ console.log(chalk.gray(" deployed backend uses, and it resolves to nothing from here. To reach this"));
14112
+ console.log(chalk.gray(" database from this machine, open a tunnel:"));
13646
14113
  console.log("");
13647
- console.log(chalk.gray(` Port-forward: kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
14114
+ console.log(` ${chalk.cyan("rebase cloud db connect")}`);
14115
+ const pf = info.directAccess.kubectl;
14116
+ if (pf) {
14117
+ console.log("");
14118
+ console.log(chalk.gray(" Or, on your own cluster:"));
14119
+ console.log(chalk.gray(` kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
14120
+ }
13648
14121
  }
13649
14122
  console.log("");
13650
14123
  }, {
@@ -13655,7 +14128,7 @@ async function dbInfo(rawArgs) {
13655
14128
  database: info.database,
13656
14129
  username: info.username,
13657
14130
  passwordAvailable: info.passwordAvailable,
13658
- portForward: info.portForward,
14131
+ directAccess: info.directAccess,
13659
14132
  unavailableReason: info.unavailableReason,
13660
14133
  ...args["--reveal"] ? {
13661
14134
  password,
@@ -13941,7 +14414,13 @@ function printDbHelp() {
13941
14414
  action: "info",
13942
14415
  section: "Database",
13943
14416
  description: "Connection details",
13944
- flags: [["--reveal", "Include the password. Without it, the value is masked"]]
14417
+ flags: [["--reveal", "Include the password (owner or admin). Without it, the value is masked"]]
14418
+ },
14419
+ {
14420
+ action: "connect",
14421
+ section: "Database",
14422
+ description: "Open a local port that IS the project's database",
14423
+ flags: [["--port <n>", "Local port to listen on. Default: 5432"], ["--reveal", "Print the password in the connection URL"]]
13945
14424
  },
13946
14425
  {
13947
14426
  action: "test",
@@ -14001,7 +14480,9 @@ function printDbHelp() {
14001
14480
  "A project has exactly one database: `create` refuses rather than attaching a second, because",
14002
14481
  "which of two rows a deploy uses is undefined.",
14003
14482
  "A managed database is provisioned at the project's FIRST DEPLOY, so `test` failing before then",
14004
- "is not a fault."
14483
+ "is not a fault.",
14484
+ "The host `info` reports is inside the platform's cluster — your backend's address for it, not",
14485
+ "one your laptop can resolve. `connect` is what makes it reachable from here."
14005
14486
  ]
14006
14487
  });
14007
14488
  }
@@ -14025,7 +14506,7 @@ async function listProjects(rawArgs) {
14025
14506
  where: org ? { organization: ["==", org] } : void 0,
14026
14507
  orderBy: ["name", "asc"],
14027
14508
  limit: 100
14028
- }).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
14509
+ }).then((res) => cloudRows(res.data)), fetchTenantBaseDomain(client, url)]);
14029
14510
  const linkedId = readLink()?.projectId;
14030
14511
  emit(() => {
14031
14512
  console.log("");
@@ -14144,8 +14625,8 @@ var CREATE_PROJECT_FLAGS = {
14144
14625
  * means the two-command sequence that every project needs is one command,
14145
14626
  * and `--db none` is there for the case that genuinely wants to decide later.
14146
14627
  *
14147
- * Distinct from `--db-mode`/`--db-cpu` next to it, which are resource dials
14148
- * on a database that exists. This is whether there is one.
14628
+ * Distinct from `--db-cpu`/`--db-instances` next to it, which are resource
14629
+ * dials on a database that exists. This is whether there is one.
14149
14630
  */
14150
14631
  "--db": String,
14151
14632
  /** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
@@ -14156,7 +14637,6 @@ var CREATE_PROJECT_FLAGS = {
14156
14637
  "--replicas": String,
14157
14638
  "--spot": String,
14158
14639
  "--scale-to-zero": String,
14159
- "--db-mode": String,
14160
14640
  "--db-instances": String,
14161
14641
  "--db-cpu": String,
14162
14642
  "--db-memory": String,
@@ -14210,7 +14690,7 @@ async function createProject(rawArgs) {
14210
14690
  try {
14211
14691
  const user = await client.auth.getUser();
14212
14692
  if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
14213
- const created = await client.data.collection("projects").create({
14693
+ const created = requireCloudRow(await client.data.collection("projects").create({
14214
14694
  name,
14215
14695
  subdomain,
14216
14696
  gitRepoUrl,
@@ -14221,7 +14701,7 @@ async function createProject(rawArgs) {
14221
14701
  organization: org,
14222
14702
  createdById: user.uid,
14223
14703
  status: "provisioning"
14224
- });
14704
+ }), "project");
14225
14705
  const host = projectHost(created, await fetchTenantBaseDomain(client, url));
14226
14706
  const linked = Boolean(args["--link"]);
14227
14707
  if (linked) writeLink({
@@ -14531,12 +15011,63 @@ function packBundle(bundleDir, outPath) {
14531
15011
  });
14532
15012
  }
14533
15013
  /**
14534
- * Assemble the deploy-trigger body for a bundle deploy.
15014
+ * The commit HEAD is on, read here because here is the only place it exists.
15015
+ *
15016
+ * A bundle deploy has no repository anywhere near the control plane: the CLI
15017
+ * builds a tarball and uploads it, so the three paths `deploy.ts` documents for
15018
+ * learning a commit — clone, `ls-remote`, or "there is no repo at all" — all
15019
+ * resolve to the third. Every bundle deployment therefore recorded an empty
15020
+ * hash, which is 291 of the 305 rows in production: a Deployments list where
15021
+ * almost nothing says what it shipped.
15022
+ *
15023
+ * But the CLI is standing IN the repository. `git -C <dir> log -1` answers
15024
+ * exactly, message included — the one thing even the git-build path cannot get
15025
+ * from `ls-remote`.
14535
15026
  *
14536
- * The manifest travels with the trigger so the control plane can validate intake
14537
- * without unpacking the uploaded archive first a rejection (native deps, no
14538
- * matching runtime) is then a fast, cheap answer.
15027
+ * Returns null rather than guessing, for every reason it can fail: no git, not a
15028
+ * repository, no commits yet. The server records what it is given and nothing
15029
+ * more, so null here stays `UNKNOWN_COMMIT_HASH` there.
15030
+ *
15031
+ * A dirty tree is NOT reported as a different commit. The bundle may contain
15032
+ * uncommitted work, and the honest statement about that is "built from a tree at
15033
+ * <hash>", not a fabricated identifier — the same rule the rest of this file
15034
+ * follows about inventing values.
14539
15035
  */
15036
+ function bundleCommit(cwd, run = gitIn(cwd)) {
15037
+ try {
15038
+ const hash = run([
15039
+ "rev-parse",
15040
+ "--short=7",
15041
+ "HEAD"
15042
+ ]).trim();
15043
+ if (!/^[0-9a-f]{7,40}$/.test(hash)) return null;
15044
+ return {
15045
+ hash,
15046
+ message: run([
15047
+ "log",
15048
+ "-1",
15049
+ "--pretty=%s"
15050
+ ]).trim()
15051
+ };
15052
+ } catch {
15053
+ return null;
15054
+ }
15055
+ }
15056
+ /** `git -C <cwd> …`, as a function, so `bundleCommit` is testable without a repo. */
15057
+ function gitIn(cwd) {
15058
+ return (args) => execFileSync("git", [
15059
+ "-C",
15060
+ cwd,
15061
+ ...args
15062
+ ], {
15063
+ encoding: "utf8",
15064
+ stdio: [
15065
+ "ignore",
15066
+ "pipe",
15067
+ "ignore"
15068
+ ]
15069
+ });
15070
+ }
14540
15071
  function bundleDeployBody(input) {
14541
15072
  return {
14542
15073
  projectId: input.projectId,
@@ -14546,7 +15077,11 @@ function bundleDeployBody(input) {
14546
15077
  client: "cli",
14547
15078
  frameworkVersion: input.manifest.runtime?.builtAgainst,
14548
15079
  ...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
14549
- ...input.message ? { message: input.message } : {}
15080
+ ...input.message ? { message: input.message } : {},
15081
+ ...input.commit ? {
15082
+ gitCommitHash: input.commit.hash,
15083
+ gitCommitMessage: input.commit.message
15084
+ } : {}
14550
15085
  };
14551
15086
  }
14552
15087
  /**
@@ -14813,7 +15348,8 @@ async function uploadAndTrigger(opts) {
14813
15348
  manifest,
14814
15349
  app: opts.appName,
14815
15350
  message: opts.message,
14816
- declaredApps
15351
+ declaredApps,
15352
+ commit: bundleCommit(process.cwd())
14817
15353
  });
14818
15354
  let deploymentId;
14819
15355
  let managed;
@@ -15058,8 +15594,7 @@ async function readBillingState(client, projectId) {
15058
15594
  if (!card) return unknown;
15059
15595
  let plan = null;
15060
15596
  try {
15061
- const orgRow = await client.data.collection("organizations").findById(org);
15062
- const billingId = orgRow?.billing_account_id ?? orgRow?.billingAccount;
15597
+ const billingId = billingAccountIdOf(await client.data.collection("organizations").findById(org));
15063
15598
  if (billingId != null) {
15064
15599
  const acct = await client.data.collection("billing-accounts").findById(billingId);
15065
15600
  plan = typeof acct?.plan === "string" ? acct.plan : null;
@@ -15500,7 +16035,7 @@ async function listOrgs(rawArgs) {
15500
16035
  });
15501
16036
  const { client, url } = await requireClient(rawArgs);
15502
16037
  try {
15503
- const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
16038
+ const orgs = cloudRows((await client.data.collection("organizations").find({ limit: 100 })).data);
15504
16039
  const active = getContextOrg(url);
15505
16040
  emit(() => {
15506
16041
  console.log("");
@@ -15560,11 +16095,11 @@ async function createOrg(rawArgs) {
15560
16095
  if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
15561
16096
  const slug = (args["--slug"] || slugify(name)).trim();
15562
16097
  try {
15563
- const created = await client.data.collection("organizations").create({
16098
+ const created = requireCloudRow(await client.data.collection("organizations").create({
15564
16099
  name,
15565
16100
  slug,
15566
16101
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15567
- });
16102
+ }), "organization");
15568
16103
  setContextOrg(url, String(created.id));
15569
16104
  success(`Created organization ${chalk.bold(name)} and set it active`);
15570
16105
  emit(() => {}, {
@@ -16745,11 +17280,11 @@ function cancelView(res) {
16745
17280
  };
16746
17281
  }
16747
17282
  async function fetchDeployments(client, projectId, limit = 100) {
16748
- return (await client.data.collection("deployments").find({
17283
+ return cloudRows((await client.data.collection("deployments").find({
16749
17284
  where: { project: ["==", projectId] },
16750
17285
  orderBy: ["createdAt", "desc"],
16751
17286
  limit
16752
- })).data;
17287
+ })).data);
16753
17288
  }
16754
17289
  /** Hard ceiling on `--limit`, matching the backend's own page size. */
16755
17290
  var MAX_DEPLOYMENTS_LIMIT = 100;
@@ -17594,9 +18129,10 @@ async function dbDebugCommand(rawArgs) {
17594
18129
  } catch (e) {
17595
18130
  reportError(e, "Failed to read database connection info");
17596
18131
  }
17597
- const pf = info.portForward;
17598
- const forwardCmd = pf ? `kubectl port-forward -n ${pf.namespace} svc/${pf.service} ${pf.localPort}:${pf.remotePort}` : null;
17599
- const psqlCmd = pf && info.username && info.database ? `psql -h 127.0.0.1 -p ${pf.localPort} -U ${info.username} -d ${info.database}` : null;
18132
+ const access = info.directAccess;
18133
+ const connectCmd = access ? "rebase cloud db connect" : null;
18134
+ const psqlCmd = access && info.username && info.database ? `psql -h 127.0.0.1 -p 5432 -U ${info.username} -d ${info.database}` : null;
18135
+ const kubectlCmd = access?.kubectl ? `kubectl port-forward -n ${access.kubectl.namespace} svc/${access.kubectl.service} ${access.kubectl.localPort}:${access.kubectl.remotePort}` : null;
17600
18136
  emit(() => {
17601
18137
  console.log("");
17602
18138
  console.log(chalk.bold(` 🐘 Database — ${displayProjectRef(rawArgs)}`));
@@ -17615,12 +18151,18 @@ async function dbDebugCommand(rawArgs) {
17615
18151
  ["Password", info.passwordAvailable ? chalk.gray("stored — not shown here") : chalk.yellow("none stored")]
17616
18152
  ]);
17617
18153
  console.log("");
17618
- if (forwardCmd) {
17619
- console.log(chalk.gray(" A managed database is only reachable inside its cluster. To connect:"));
18154
+ if (connectCmd) {
18155
+ console.log(chalk.gray(" That host is inside the platform's cluster and does not resolve here."));
18156
+ console.log(chalk.gray(" To reach the database from this machine:"));
17620
18157
  console.log("");
17621
- console.log(` ${forwardCmd}`);
18158
+ console.log(` ${connectCmd}`);
17622
18159
  if (psqlCmd) console.log(` ${psqlCmd}`);
17623
18160
  console.log("");
18161
+ if (kubectlCmd) {
18162
+ console.log(chalk.gray(" Or, on your own cluster:"));
18163
+ console.log(` ${kubectlCmd}`);
18164
+ console.log("");
18165
+ }
17624
18166
  if (info.passwordAvailable) {
17625
18167
  console.log(chalk.gray(" Get the password with: ") + chalk.bold("rebase cloud db info --reveal"));
17626
18168
  console.log("");
@@ -17633,7 +18175,8 @@ async function dbDebugCommand(rawArgs) {
17633
18175
  database: info.database ?? null,
17634
18176
  username: info.username ?? null,
17635
18177
  passwordAvailable: Boolean(info.passwordAvailable),
17636
- portForwardCommand: forwardCmd,
18178
+ connectCommand: connectCmd,
18179
+ kubectlCommand: kubectlCmd,
17637
18180
  psqlCommand: psqlCmd
17638
18181
  });
17639
18182
  }
@@ -17932,7 +18475,6 @@ var ACTION_HELP = {
17932
18475
  ["--replicas <n>", "Instance count"],
17933
18476
  ["--spot <true|false>", "Run on preemptible capacity"],
17934
18477
  ["--scale-to-zero <true|false>", "Stop the instances when idle"],
17935
- ["--db-mode <mode>", "Database topology dial"],
17936
18478
  ["--db-instances <n>", "Database instance count"],
17937
18479
  ["--db-cpu <n>", "vCPU per database instance"],
17938
18480
  ["--db-memory <size>", "Memory per database instance"],
@@ -17961,11 +18503,28 @@ var ACTION_HELP = {
17961
18503
  ],
17962
18504
  examples: ["rebase cloud db create --type managed", "rebase cloud db create --type byodb --connection-string \"$DATABASE_URL\" --wait"],
17963
18505
  notes: [
17964
- "A managed database is CloudNativePG in a shared in-cluster pool, and it is created at the project's first deploy. There is nothing to poll before then, so --wait says so and returns rather than looping.",
18506
+ "A managed database is a CloudNativePG cluster of the project's own, in the project's own namespace and backed up on its own schedule. It is created at the project's first deploy: there is nothing to poll before then, so --wait says so and returns rather than looping.",
17965
18507
  "`rebase cloud db test` legitimately fails before the first deploy.",
17966
18508
  "A project has exactly one database — attaching a second is refused, because the platform reads one row and it becomes undefined which it deploys against."
17967
18509
  ]
17968
18510
  },
18511
+ "db connect": {
18512
+ command: "cloud db connect",
18513
+ usage: "cloud db connect [--port <n>] [--reveal]",
18514
+ summary: "Open a local port that is the project's managed database, and hold it open until Ctrl-C. A managed database lives inside the platform's cluster, so the host `db info` reports is your backend's address for it and resolves to nothing on your machine; this is what makes it reachable from here. Point psql, TablePlus, Drizzle Studio or pg_dump at the URL it prints. The database still asks for its password — the tunnel is a network path, not a credential.",
18515
+ flags: [["--port <n>", "Local port to listen on. Default: 5432"], ["--reveal", "Print the password inside the connection URL"]],
18516
+ examples: [
18517
+ "rebase cloud db connect",
18518
+ "rebase cloud db connect --port 6543",
18519
+ "rebase cloud db connect --reveal --project shop"
18520
+ ],
18521
+ notes: [
18522
+ "Requires the organization's owner or admin role — the same gate as the console's SQL console, because it is the same capability.",
18523
+ "Piped or with --json it prints one object — host, port, database, username, connectionString — and keeps serving, so a script can read the URL and connect.",
18524
+ "Connections go through the control plane, so they count against the project's own database connection limit like any other client.",
18525
+ "Bring-your-own databases are refused: that host is already yours to reach, and there is nothing for the platform to tunnel."
18526
+ ]
18527
+ },
17969
18528
  deploy: {
17970
18529
  command: "cloud deploy",
17971
18530
  usage: "cloud deploy [app] [options]",
@@ -18067,7 +18626,7 @@ var ACTION_HELP = {
18067
18626
  "rebase cloud db backup restore base-20260831 --yes",
18068
18627
  "rebase cloud db backup download base-20260831"
18069
18628
  ],
18070
- notes: ["`restore` replaces the live database. Nothing about it is undoable from here — `--yes`, the global flag listed below, is what skips that confirmation.", "A managed database on the shared pool is backed up with the pool, not per project."]
18629
+ notes: ["`restore` replaces the live database. Nothing about it is undoable from here — `--yes`, the global flag listed below, is what skips that confirmation.", "Backups are the project's own: a base backup on a schedule, plus continuous WAL archiving for point-in-time recovery."]
18071
18630
  },
18072
18631
  "db pitr": {
18073
18632
  command: "cloud db pitr",
@@ -18142,7 +18701,7 @@ var ACTION_HELP = {
18142
18701
  },
18143
18702
  "storage attach": {
18144
18703
  command: "cloud storage attach",
18145
- usage: "cloud storage attach --bucket <name> --access-key-id <id> --secret-access-key <secret> [--endpoint <url>] [--region <region>] [--force-path-style]",
18704
+ usage: "cloud storage attach --bucket <name> --access-key-id <id> --secret-access-key <secret> [--endpoint <url>] [--region <region>] [--force-path-style] [--source <key>]",
18146
18705
  summary: "Point the project at storage you already own — S3, R2, MinIO, any S3-compatible bucket. The alternative to `storage create`, for a bucket the platform does not manage.",
18147
18706
  flags: [
18148
18707
  ["--bucket <name>", "The bucket name. Required"],
@@ -18150,9 +18709,14 @@ var ACTION_HELP = {
18150
18709
  ["--secret-access-key <secret>", "Required. Stored encrypted and never returned"],
18151
18710
  ["--endpoint <url>", "For anything that is not AWS S3 — R2, MinIO, Backblaze"],
18152
18711
  ["--region <region>", "Bucket region. Default: the provider's own default"],
18153
- ["--force-path-style", "Address as endpoint/bucket rather than bucket.endpoint. Needed by MinIO"]
18712
+ ["--force-path-style", "Address as endpoint/bucket rather than bucket.endpoint. Needed by MinIO"],
18713
+ ["--source <key>", "Which declared bucket to configure. Default: the project's default bucket"]
18714
+ ],
18715
+ examples: [
18716
+ "rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key …",
18717
+ "rebase cloud storage attach --source media --bucket media-eu --access-key-id … --secret-access-key …",
18718
+ "rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key … \\\n --endpoint https://<account>.r2.cloudflarestorage.com --region auto"
18154
18719
  ],
18155
- examples: ["rebase cloud storage attach --bucket assets --access-key-id AKIA… --secret-access-key …", "rebase cloud storage attach --bucket assets --access-key-id … --secret-access-key … \\\n --endpoint https://<account>.r2.cloudflarestorage.com --region auto"],
18156
18720
  notes: ["All three of --bucket, --access-key-id and --secret-access-key, or none: a bucket with no credentials reads as configured and fails on the first upload.", "Redeploy for the tenant to pick the credentials up."]
18157
18721
  },
18158
18722
  "webhooks create": {
@@ -18239,7 +18803,6 @@ var ACTION_HELP = {
18239
18803
  ["--replicas <n>", "Instances that always exist — the autoscaler's floor, and what is billed at rest"],
18240
18804
  ["--spot <true|false>", "Preemptible capacity: cheaper, and restarted without notice"],
18241
18805
  ["--scale-to-zero <true|false>", "Request-billed compute that stops when idle, at the cost of a cold start"],
18242
- ["--db-mode <shared|dedicated>", "Pooled cluster, or one of this project's own"],
18243
18806
  ["--db-instances <n>", "1–3. 1 is a single instance with no failover; 2 adds an automatic standby"],
18244
18807
  ["--db-cpu <n>", "Database CPU request per instance. Default: 500m"],
18245
18808
  ["--db-memory <size>", "Database memory request per instance. Default: 2Gi"],
@@ -18251,7 +18814,7 @@ var ACTION_HELP = {
18251
18814
  examples: [
18252
18815
  "rebase cloud compute set --cpu 500m --memory 2Gi",
18253
18816
  "rebase cloud compute set --replicas 2 --autoscale-max 6",
18254
- "rebase cloud compute set --db-mode dedicated --db-instances 2"
18817
+ "rebase cloud compute set --db-instances 2 --db-memory 4Gi"
18255
18818
  ],
18256
18819
  notes: [
18257
18820
  "Run `rebase cloud compute` first — it prints the current dials and the €/month this project is quoted.",
@@ -18893,8 +19456,8 @@ async function appsCommand(subcommand, rawArgs = []) {
18893
19456
  }
18894
19457
  function describeApp(app) {
18895
19458
  switch (app.type) {
18896
- case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` : `managed runtime, config: ${app.config ?? "config"}`;
18897
- case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}`;
19459
+ case "backend": return app.runtime === "custom" ? `custom runtime — ${app.dockerfile ?? "Dockerfile"}` + (app.context && app.context !== "." ? ` (context: ${app.context})` : "") : `managed runtime, config: ${app.config ?? "config"}`;
19460
+ case "static": return `${app.root} → ${app.output} @ ${app.path ?? "/"}` + (app.cms ? ` ${chalk.magenta(`CMS at ${app.cms}`)}` : "");
18898
19461
  default: return "";
18899
19462
  }
18900
19463
  }
@@ -19108,7 +19671,7 @@ async function entry(args) {
19108
19671
  await recordEvent("cli.error", {
19109
19672
  command: command ?? "none",
19110
19673
  subcommand: effectiveSubcommand ?? "none",
19111
- error_type: error instanceof Error ? error.constructor.name : "unknown",
19674
+ error_type: errorClass(error),
19112
19675
  usage: Boolean(error && typeof error === "object" && error.isUsageError)
19113
19676
  }, { projectRoot: process.cwd() });
19114
19677
  throw error;
@@ -19297,6 +19860,6 @@ function telemetryNotice() {
19297
19860
  return chalk.gray(`Usage sharing: ${sharing ? "on" : "off"} — ${chalk.cyan("rebase telemetry")} to inspect or change\n`);
19298
19861
  }
19299
19862
  //#endregion
19300
- export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, ManifestError, VENDOR_SIZE_MAX_BYTES, VENDOR_SIZE_WARN_BYTES, VENDOR_TARGET_CPU, VENDOR_TARGET_OS, assessManagedCompatibility, buildBundle, buildStaticBundle, buildableApps, collectDeclaredDependencies, commentSpans, composeBundleManifest, detectDeclaredDepConflicts, detectFrameworkDepDrift, detectNativeDependencies, detectStorageAuthorize, entry, findBackendApp, findUnusedServerEntry, foldStaticIntoBundle, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, resolveBackendPaths, selectDeployApp, synthesizeManifest, validateManifest, vendorDependencies, writeManifest };
19863
+ export { CURRENT_RUNTIME_RANGE, DEFAULT_BUNDLE_DIR, DEFAULT_CONFIG_DIR, DEFAULT_CRONS_DIR, DEFAULT_FUNCTIONS_DIR, DEFAULT_SCHEMA_FILE, ManifestError, VENDOR_SIZE_MAX_BYTES, VENDOR_SIZE_WARN_BYTES, VENDOR_TARGET_CPU, VENDOR_TARGET_OS, assessManagedCompatibility, buildBundle, buildStaticBundle, buildableApps, cmsMountOf, collectDeclaredDependencies, commentSpans, composeBundleManifest, detectDeclaredDepConflicts, detectFrameworkDepDrift, detectNativeDependencies, detectStorageAuthorize, entry, findBackendApp, findUnusedServerEntry, foldStaticIntoBundle, loadManifest, manifestExists, manifestPath, normalizeEsmSpecifiers, resolveBackendPaths, selectDeployApp, synthesizeManifest, validateManifest, vendorDependencies, writeManifest };
19301
19864
 
19302
19865
  //# sourceMappingURL=index.es.js.map