@rebasepro/cli 0.17.1 → 0.17.2-canary.ga94217a

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
@@ -1,5 +1,5 @@
1
1
  import { f as __exportAll } from "./state-c0CJ6Kwb.js";
2
- import { t as MANAGED_LIMITATIONS } from "./constraints-BK1_4vci.js";
2
+ import { t as MANAGED_LIMITATIONS } from "./constraints-DKGbNfUW.js";
3
3
  import { n as ensureManagedDatabase } from "./daemon-Bdl4lrdt.js";
4
4
  import chalk from "chalk";
5
5
  import arg from "arg";
@@ -493,6 +493,135 @@ function requireBackendDir(projectRoot) {
493
493
  return backendDir;
494
494
  }
495
495
  //#endregion
496
+ //#region src/commands/cloud/errors.ts
497
+ /**
498
+ * The service accounts a Kubernetes 403 can name, and what each means.
499
+ *
500
+ * `system:serviceaccount:` is the platform's own identity — the control plane's
501
+ * pod, or a tenant's operator. A project cannot grant it anything, because the
502
+ * grant lives in a cluster the project does not own. `system:anonymous` is the
503
+ * same fact with the credential missing entirely.
504
+ */
505
+ var PLATFORM_PRINCIPAL_RE = /system:(?:serviceaccount:|anonymous|node:)/;
506
+ /**
507
+ * A Kubernetes `Status` object embedded anywhere in a message.
508
+ *
509
+ * Scanned for rather than parsed off the front: the control plane wraps it
510
+ * ("Failed to create the tenant namespace: …"), the client library appends
511
+ * headers after it, and both halves are worth keeping out of the summary.
512
+ * Balanced-brace scanning rather than a regex, because `details.causes` nests
513
+ * and a lazy `\{.*?\}` truncates the object at the first inner brace — which
514
+ * parses to nothing and silently falls through to the raw message.
515
+ */
516
+ function extractKubernetesStatus(text) {
517
+ for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
518
+ let depth = 0;
519
+ let inString = false;
520
+ let escaped = false;
521
+ for (let i = start; i < text.length; i++) {
522
+ const ch = text[i];
523
+ if (escaped) {
524
+ escaped = false;
525
+ continue;
526
+ }
527
+ if (ch === "\\") {
528
+ escaped = true;
529
+ continue;
530
+ }
531
+ if (ch === "\"") {
532
+ inString = !inString;
533
+ continue;
534
+ }
535
+ if (inString) continue;
536
+ if (ch === "{") depth++;
537
+ else if (ch === "}") {
538
+ depth--;
539
+ if (depth !== 0) continue;
540
+ const candidate = text.slice(start, i + 1);
541
+ let parsed;
542
+ try {
543
+ parsed = JSON.parse(candidate);
544
+ } catch {
545
+ break;
546
+ }
547
+ const obj = parsed;
548
+ if (obj?.kind === "Status" || obj?.status === "Failure" && typeof obj.code === "number") return {
549
+ message: typeof obj.message === "string" ? obj.message : void 0,
550
+ reason: typeof obj.reason === "string" ? obj.reason : void 0,
551
+ code: typeof obj.code === "number" ? obj.code : void 0,
552
+ details: obj.details ?? void 0
553
+ };
554
+ break;
555
+ }
556
+ }
557
+ }
558
+ }
559
+ /**
560
+ * Strip the transport noise a Kubernetes client appends to its own message.
561
+ *
562
+ * Only ever applied to the fallback path — when no `Status` was found, the
563
+ * message is all there is, and cutting it at the first header keeps the
564
+ * sentence while dropping the `audit-id` and the flowschema uid nobody outside
565
+ * the cluster can use.
566
+ */
567
+ function trimTransportNoise(message) {
568
+ const cut = message.search(/\s*(?:headers:|HTTP request failed|audit-id|x-kubernetes-pf-|\{"kind":"Status")/i);
569
+ return (cut > 0 ? message.slice(0, cut) : message).trim().replace(/[\s:,-]+$/, "");
570
+ }
571
+ /** The longest a summary may be before it stops being one. */
572
+ var MAX_SUMMARY = 300;
573
+ /**
574
+ * One actionable line (plus a hint) from whatever the control plane returned.
575
+ *
576
+ * `status` is the HTTP status of the control-plane call itself, which is a
577
+ * different number from the `code` inside an embedded Kubernetes `Status` — the
578
+ * control plane routinely answers 500 while the cluster answered 403, and it is
579
+ * the inner one that says what happened.
580
+ */
581
+ function summarizeError(error, context) {
582
+ const err = error;
583
+ const raw = err?.message ?? String(error);
584
+ const k8s = extractKubernetesStatus(raw);
585
+ if (k8s) {
586
+ const inner = k8s.message?.trim() ?? "";
587
+ if ((k8s.code === 403 || k8s.reason === "Forbidden") && PLATFORM_PRINCIPAL_RE.test(inner)) return {
588
+ message: `${context}: the platform's own cluster credentials were refused by Kubernetes.`,
589
+ hint: `${truncate(inner)}\n This is a platform-side permission, not something your project can grant. Nothing in your code, collections or deploy flags will change it — report it with the message above rather than retrying or changing the project.`,
590
+ code: "platform_permission_denied",
591
+ platform: true,
592
+ raw
593
+ };
594
+ return {
595
+ message: `${context}: ${truncate(inner || k8s.reason || "the cluster refused the request")}`,
596
+ hint: k8s.code ? `Kubernetes answered ${k8s.code}${k8s.reason ? ` (${k8s.reason})` : ""}.` : void 0,
597
+ code: k8s.reason ? `k8s_${k8s.reason.toLowerCase()}` : "k8s_error",
598
+ platform: false,
599
+ raw
600
+ };
601
+ }
602
+ const trimmed = trimTransportNoise(raw);
603
+ return {
604
+ message: `${context}${err?.status ? ` (${err.status})` : ""}: ${truncate(trimmed || raw)}`,
605
+ code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
606
+ platform: false,
607
+ raw
608
+ };
609
+ }
610
+ function truncate(text) {
611
+ const flat = text.replace(/\s+/g, " ").trim();
612
+ return flat.length > MAX_SUMMARY ? `${flat.slice(0, MAX_SUMMARY - 1)}…` : flat;
613
+ }
614
+ /**
615
+ * Whether the caller asked for the untouched body.
616
+ *
617
+ * `--debug` is already what `bin/rebase.js` prints after every failure as the
618
+ * thing to add, so the raw payload hangs off the flag people are told to reach
619
+ * for rather than off one invented here.
620
+ */
621
+ function wantsRawError(argv = process.argv) {
622
+ return argv.includes("--debug") || process.env.REBASE_DEBUG === "1";
623
+ }
624
+ //#endregion
496
625
  //#region src/commands/cloud/context.ts
497
626
  /**
498
627
  * Shared foundation for the `rebase cloud` command family.
@@ -1065,6 +1194,24 @@ function parseCloudArgs(opts) {
1065
1194
  }
1066
1195
  }
1067
1196
  /**
1197
+ * `--timeout <seconds>` as milliseconds, or `fallbackMs` when it was not given.
1198
+ *
1199
+ * One function rather than one per command, because two commands take this flag
1200
+ * and a second copy is where the two would come to disagree about what
1201
+ * `--timeout 0` means.
1202
+ *
1203
+ * A value this cannot read is a refusal, not a fall back to the default. The
1204
+ * whole reason a caller passes a timeout is that it has a deadline of its own;
1205
+ * quietly substituting a different one is how a fifteen-minute wait turns up
1206
+ * inside a five-minute CI step, having been asked for `--timeout 30s`.
1207
+ */
1208
+ function resolveTimeoutMs(value, opts) {
1209
+ if (value === void 0) return opts.fallbackMs;
1210
+ const seconds = Number(value);
1211
+ if (!Number.isFinite(seconds) || seconds <= 0) fail(`--timeout takes a number of seconds (got "${value}").`, `Run \`rebase ${opts.command} --help\`.`, "usage");
1212
+ return seconds * 1e3;
1213
+ }
1214
+ /**
1068
1215
  * Announce an outcome — "Logged in as …", "Deleted project …".
1069
1216
  *
1070
1217
  * On **stderr**, in both modes. It reads like a result and is not one: the
@@ -1142,19 +1289,29 @@ function keyValues(rows) {
1142
1289
  /**
1143
1290
  * Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
1144
1291
  * a `.status` and `.message`; anything else falls back to its string form.
1292
+ *
1293
+ * The message is summarised rather than printed — see `summarizeError`. What
1294
+ * arrives here is routinely a whole Kubernetes `Status` object with the request
1295
+ * headers appended, and the one sentence worth reading is inside it. The
1296
+ * untouched body is still available, behind `--debug`, on stderr where it
1297
+ * cannot corrupt the JSON value on stdout.
1145
1298
  */
1146
1299
  function reportError(e, context) {
1147
1300
  const err = e;
1301
+ const summary = summarizeError(e, context);
1302
+ if (wantsRawError()) process.stderr.write(`\n${summary.raw}\n\n`);
1148
1303
  if (JSON_MODE) {
1149
1304
  printJson({ error: {
1150
- message: err?.message ? stripAnsi(err.message) : String(e),
1151
- code: err?.code ?? (err?.status ? `http_${err.status}` : "request_failed"),
1305
+ message: stripAnsi(summary.message),
1306
+ code: summary.code,
1152
1307
  status: err?.status ?? null,
1308
+ hint: summary.hint ? stripAnsi(summary.hint) : void 0,
1309
+ platform: summary.platform,
1153
1310
  context
1154
1311
  } });
1155
1312
  process.exit(1);
1156
1313
  }
1157
- fail(`${context}${err?.status ? ` (${err.status})` : ""}: ${err?.message ?? String(e)}`);
1314
+ fail(summary.message, summary.hint, summary.code);
1158
1315
  }
1159
1316
  /**
1160
1317
  * Open a URL in the user's default browser (best effort). Always announces the
@@ -2972,8 +3129,8 @@ async function runDriverDbCommand(rawArgs, options = {}) {
2972
3129
  });
2973
3130
  }
2974
3131
  async function dbCommand(subcommand, rawArgs) {
2975
- if (!subcommand || subcommand === "--help") {
2976
- printDbHelp$1();
3132
+ if (!subcommand || subcommand === "--help" || rawArgs.includes("--help") || rawArgs.includes("-h")) {
3133
+ printDbHelp$1(subcommand === "--help" ? void 0 : subcommand);
2977
3134
  return;
2978
3135
  }
2979
3136
  const projectRoot = requireProjectRoot();
@@ -3135,7 +3292,76 @@ async function pullIntoLocal(projectRoot, rawArgs) {
3135
3292
  fs.rmSync(dumpFile, { force: true });
3136
3293
  }
3137
3294
  }
3138
- function printDbHelp$1() {
3295
+ /**
3296
+ * What each `rebase db <action>` does and takes.
3297
+ *
3298
+ * Kept here rather than delegated to the driver because the driver is reached
3299
+ * by *running* it — which is exactly what `--help` must not do. The page is
3300
+ * deliberately short: the authority on a flag is the command's own spec, and
3301
+ * `check-doc-commands.mjs` holds every command written in this repository's
3302
+ * markdown to it. What a reader needs from here is which subcommands exist and
3303
+ * what the destructive ones want before they will run.
3304
+ */
3305
+ var DB_ACTION_HELP = {
3306
+ push: {
3307
+ usage: "rebase db push [--collections <dir>] [--allow-destructive] [--yes]",
3308
+ summary: "Apply the schema straight to the database. Development only — it does not write a migration.",
3309
+ notes: ["A change that would drop data needs --allow-destructive."]
3310
+ },
3311
+ generate: {
3312
+ usage: "rebase db generate [--collections <dir>]",
3313
+ summary: "Generate the Drizzle schema, the Postgres DDL and a SQL migration file from the collections."
3314
+ },
3315
+ migrate: {
3316
+ usage: "rebase db migrate",
3317
+ summary: "Run the pending migration files against the database."
3318
+ },
3319
+ branch: {
3320
+ usage: "rebase db branch <create|list|delete|info> [name]",
3321
+ summary: "Database branching."
3322
+ },
3323
+ backup: {
3324
+ usage: "rebase db backup [--out <path|s3://…>]",
3325
+ summary: "Create a pg_dump backup. --out is resolved against the directory you are standing in."
3326
+ },
3327
+ backups: {
3328
+ usage: "rebase db backups",
3329
+ summary: "List stored backups."
3330
+ },
3331
+ restore: {
3332
+ usage: "rebase db restore <dump> [--target-db <name>] [--create-db] --yes",
3333
+ summary: "Restore a backup with pg_restore.",
3334
+ notes: ["Destructive, and refuses to run without --yes."]
3335
+ },
3336
+ pull: {
3337
+ usage: "rebase db pull --from <url> [--schema <name>] [--anonymize] [--yes]",
3338
+ summary: "Copy another database into local development. One-directional by design — it can never push."
3339
+ },
3340
+ stop: {
3341
+ usage: "rebase db stop",
3342
+ summary: "Stop the managed development database. Data is kept."
3343
+ },
3344
+ reset: {
3345
+ usage: "rebase db reset [--yes]",
3346
+ summary: "Delete the managed development database and start over.",
3347
+ notes: ["Destructive, and the data exists only on this machine."]
3348
+ }
3349
+ };
3350
+ function printDbHelp$1(action) {
3351
+ const entry = action ? DB_ACTION_HELP[action] : void 0;
3352
+ if (entry) {
3353
+ console.log(`
3354
+ ${chalk.bold(`rebase db ${action}`)}
3355
+
3356
+ ${entry.summary}
3357
+
3358
+ ${chalk.green.bold("Usage")}
3359
+ ${chalk.blue(entry.usage)}
3360
+ ${entry.notes?.length ? `\n${chalk.green.bold("Notes")}\n${entry.notes.map((n) => ` ${chalk.gray(`• ${n}`)}`).join("\n")}\n` : ""}
3361
+ ${chalk.gray("Run `rebase db --help` for every subcommand.")}
3362
+ `);
3363
+ return;
3364
+ }
3139
3365
  console.log(`
3140
3366
  ${chalk.bold("rebase db")} — Database management commands
3141
3367
 
@@ -9122,6 +9348,39 @@ function describeDatabaseState(db) {
9122
9348
  if (connection === "connected" || connection === "failed") return `${type} (${colorStatus(connection)})`;
9123
9349
  return `${type} ${chalk.gray("· not tested (`rebase cloud db test`)")}`;
9124
9350
  }
9351
+ /** Deployment states that mean the platform is still doing something. */
9352
+ var IN_FLIGHT = /* @__PURE__ */ new Set([
9353
+ "deploying",
9354
+ "building",
9355
+ "pending",
9356
+ "queued"
9357
+ ]);
9358
+ function resolveBlockedState(input) {
9359
+ if (!input.database) return {
9360
+ blockedOn: "no_database",
9361
+ nextAction: "rebase cloud db create --type managed"
9362
+ };
9363
+ if (input.lastDeploy && IN_FLIGHT.has(String(input.lastDeploy.status))) return {
9364
+ blockedOn: null,
9365
+ nextAction: null
9366
+ };
9367
+ if (!input.lastDeploy) return {
9368
+ blockedOn: "never_deployed",
9369
+ nextAction: "rebase cloud deploy"
9370
+ };
9371
+ if (input.lastDeploy.status && input.lastDeploy.status !== "success") return {
9372
+ blockedOn: "last_deploy_failed",
9373
+ nextAction: "rebase cloud logs"
9374
+ };
9375
+ if (input.database.connectionStatus === "failed") return {
9376
+ blockedOn: "database_unreachable",
9377
+ nextAction: "rebase cloud db test"
9378
+ };
9379
+ return {
9380
+ blockedOn: null,
9381
+ nextAction: null
9382
+ };
9383
+ }
9125
9384
  /**
9126
9385
  * One line describing what engine is serving this project.
9127
9386
  *
@@ -9161,6 +9420,11 @@ async function statusCommand(rawArgs) {
9161
9420
  ]);
9162
9421
  const storageLine = describeStorageState(storage);
9163
9422
  const databaseLine = describeDatabaseState(db);
9423
+ const blocked = resolveBlockedState({
9424
+ projectStatus: project.status,
9425
+ database: db,
9426
+ lastDeploy: deploy
9427
+ });
9164
9428
  emit(() => {
9165
9429
  console.log("");
9166
9430
  console.log(` ${chalk.bold(project.name ?? project.subdomain ?? "")} ${chalk.gray(`[${project.subdomain ?? displayProjectRef(rawArgs)}]`)} ${colorStatus(project.status)}`);
@@ -9173,12 +9437,19 @@ async function statusCommand(rawArgs) {
9173
9437
  ["Database", databaseLine],
9174
9438
  ["Storage", storageLine]
9175
9439
  ]);
9440
+ if (blocked.blockedOn) {
9441
+ console.log("");
9442
+ console.log(` ${chalk.yellow("Waiting on you")} ${chalk.gray(`— ${blocked.blockedOn}`)}`);
9443
+ console.log(` ${chalk.bold(blocked.nextAction ?? "")}`);
9444
+ }
9176
9445
  console.log("");
9177
9446
  }, {
9178
9447
  projectId: String(project.id),
9179
9448
  name: project.name ?? null,
9180
9449
  subdomain: project.subdomain ?? null,
9181
9450
  status: project.status ?? null,
9451
+ blockedOn: blocked.blockedOn,
9452
+ nextAction: blocked.nextAction,
9182
9453
  url: projectHost(project, baseDomain) ?? null,
9183
9454
  branch: project.gitBranch ?? null,
9184
9455
  lastDeploy: deploy ? {
@@ -9589,16 +9860,48 @@ async function clustersCommand(action, rawArgs) {
9589
9860
  * answered by a customer's first deploy failing halfway through provisioning,
9590
9861
  * with an error they cannot act on and half a tenant already created.
9591
9862
  */
9863
+ /**
9864
+ * Which cluster `clusters verify` was asked about, and whether `--baseline`
9865
+ * was given.
9866
+ *
9867
+ * Resolved against a real spec, not scanned out of `rawArgs` by hand — and this
9868
+ * is not a tidy-up. `rawArgs` is the whole `process.argv`, so the old scan
9869
+ * ("the first token that is not `--…` and is neither `clusters` nor `verify`")
9870
+ * matched `argv[0]`, the **node binary path**. Every `rebase cloud clusters
9871
+ * verify <id>` therefore asked the control plane about a cluster called
9872
+ * `/usr/local/bin/node`, and came back 404.
9873
+ *
9874
+ * So the one diagnostic that reports `permissions.allowed` /
9875
+ * `permissions.denied` was unreachable, and its 404 read as "this command is
9876
+ * not deployed yet" rather than "the id never left this machine intact". It is
9877
+ * the command that names a missing `cronjobs.batch` grant in a single call
9878
+ * instead of a twenty-minute A/B against a live project.
9879
+ *
9880
+ * Same failure as `cloud deploy` reading `_[0]` as `"cloud"`, and the same fix:
9881
+ * one parser, exported so its test drives the real thing rather than a copy.
9882
+ */
9883
+ function resolveClusterVerifyArgs(rawArgs) {
9884
+ const { flags, positionals } = parseCloudArgs({
9885
+ spec: { "--baseline": Boolean },
9886
+ rawArgs,
9887
+ commandWords: 2,
9888
+ command: "cloud clusters verify",
9889
+ maxPositionals: 2
9890
+ });
9891
+ return {
9892
+ id: positionals[1],
9893
+ baseline: flags["--baseline"] === true
9894
+ };
9895
+ }
9592
9896
  async function clustersVerifyCommand(rawArgs) {
9593
- const { client } = await requireClient(rawArgs);
9594
- const id = rawArgs.find((a) => !a.startsWith("--") && a !== "clusters" && a !== "verify");
9897
+ const { id, baseline: withBaseline } = resolveClusterVerifyArgs(rawArgs);
9595
9898
  if (!id) fail("Usage: rebase cloud clusters verify <cluster-id> [--baseline]", void 0, "bad_request");
9596
- const withBaseline = rawArgs.includes("--baseline");
9899
+ const { client } = await requireClient(rawArgs);
9597
9900
  let report;
9598
9901
  try {
9599
9902
  report = await client.functions.invoke("cluster-baseline", void 0, {
9600
9903
  method: "GET",
9601
- path: `verify/${id}${withBaseline ? "?baseline=1" : ""}`
9904
+ path: `verify/${encodeURIComponent(id)}${withBaseline ? "?baseline=1" : ""}`
9602
9905
  });
9603
9906
  } catch (error) {
9604
9907
  reportError(error, "Could not verify the cluster");
@@ -10012,1834 +10315,2039 @@ function buildDialPatch(rawArgs, opts) {
10012
10315
  return { patch };
10013
10316
  }
10014
10317
  //#endregion
10015
- //#region src/commands/cloud/projects.ts
10318
+ //#region src/commands/cloud/databases.ts
10016
10319
  /**
10017
- * `rebase cloud projects` — list / create / info / delete.
10320
+ * `rebase cloud db` — database + backup management for a project.
10321
+ *
10322
+ * db list List databases attached to the project
10323
+ * db create Attach a managed or bring-your-own database
10324
+ * db test Test connectivity to the project's database
10325
+ * db backup list|create|restore
10018
10326
  */
10019
- async function listProjects(rawArgs) {
10020
- const { client, url } = await requireClient(rawArgs);
10021
- const org = getContextOrg(url);
10327
+ async function dbCommand$1(subcommand, rawArgs) {
10328
+ switch (subcommand) {
10329
+ case "list":
10330
+ case void 0:
10331
+ await listDatabases(rawArgs);
10332
+ break;
10333
+ case "create":
10334
+ await createDatabase(rawArgs);
10335
+ break;
10336
+ case "info":
10337
+ await dbInfo(rawArgs);
10338
+ break;
10339
+ case "test":
10340
+ await testDatabase(rawArgs);
10341
+ break;
10342
+ case "backup":
10343
+ await backupCommand(rawArgs);
10344
+ break;
10345
+ case "pitr":
10346
+ await pitrCommand(rawArgs);
10347
+ break;
10348
+ case "--help":
10349
+ printDbHelp();
10350
+ break;
10351
+ default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
10352
+ }
10353
+ }
10354
+ async function listDatabases(rawArgs) {
10355
+ const { client } = await requireClient(rawArgs);
10356
+ const projectId = await requireProject(rawArgs, client);
10357
+ const projectRef = displayProjectRef(rawArgs);
10022
10358
  try {
10023
- const [projects, baseDomain] = await Promise.all([client.data.collection("projects").find({
10024
- where: org ? { organization: ["==", org] } : void 0,
10025
- orderBy: ["name", "asc"],
10026
- limit: 100
10027
- }).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
10028
- const linkedId = readLink()?.projectId;
10359
+ const dbs = (await client.data.collection("databases").find({
10360
+ where: { project: ["==", projectId] },
10361
+ limit: 50
10362
+ })).data;
10029
10363
  emit(() => {
10030
10364
  console.log("");
10031
- console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
10365
+ console.log(chalk.bold(` 🗄 Databases project ${projectRef}`));
10032
10366
  console.log("");
10033
- if (projects.length === 0) {
10034
- console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
10367
+ if (dbs.length === 0) {
10368
+ console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
10035
10369
  console.log("");
10036
10370
  return;
10037
10371
  }
10038
- for (const p of projects) {
10039
- const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
10040
- console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
10041
- console.log(` ${chalk.gray(projectHost(p, baseDomain) ?? "—")}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
10372
+ for (const d of dbs) {
10373
+ console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
10374
+ keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
10042
10375
  }
10043
10376
  console.log("");
10044
10377
  }, {
10045
- org: org ?? null,
10046
- projects: projects.map((p) => ({
10047
- id: String(p.id),
10048
- name: p.name ?? null,
10049
- slug: p.subdomain ?? null,
10050
- host: projectHost(p, baseDomain) ?? null,
10051
- status: p.status ?? null,
10052
- provider: p.provider ?? null,
10053
- linked: String(p.id) === linkedId
10378
+ projectId,
10379
+ databases: dbs.map((d) => ({
10380
+ id: String(d.id),
10381
+ type: d.type ?? null,
10382
+ connectionStatus: d.connectionStatus ?? null,
10383
+ useSshTunnel: Boolean(d.useSshTunnel),
10384
+ pitrEnabled: Boolean(d.pitrEnabled)
10054
10385
  }))
10055
10386
  });
10056
10387
  } catch (e) {
10057
- reportError(e, "Failed to list projects");
10388
+ reportError(e, "Failed to list databases");
10058
10389
  }
10059
10390
  }
10060
10391
  /**
10061
- * Default region per provider. Required on create; the rest is dialled.
10392
+ * The database already attached to a project, if any.
10062
10393
  *
10063
- * There used to be a `vmSize` here too `e2-small`, `cx21` — naming a machine
10064
- * on a price list this platform does not buy from. The column was dropped on
10065
- * 2026-08-20 and this went on sending it, which a control plane can only ignore
10066
- * or refuse. What a project reserves is a set of dials now, and a project that
10067
- * sets none takes the platform default.
10394
+ * `limit: 1` deliberately mirrors what the control plane's own `db-test`,
10395
+ * `db-info` and `backup` do asking the same question the same way is the
10396
+ * point, since the answer decides which row a deploy will actually use.
10068
10397
  */
10069
- function providerDefaults(provider) {
10070
- switch (provider) {
10071
- case "gcp": return { region: "europe-west1" };
10072
- case "aws": return { region: "us-east-1" };
10073
- default: return { region: "nbg1" };
10074
- }
10398
+ async function firstAttachedDatabase(client, projectId) {
10399
+ return (await client.data.collection("databases").find({
10400
+ where: { project: ["==", projectId] },
10401
+ limit: 1
10402
+ })).data[0];
10075
10403
  }
10076
10404
  /**
10077
- * Where this project says it runs.
10078
- *
10079
- * `provider`/`region` are a *request*: no code downstream reads them to pick a
10080
- * deploy target — that comes from the project's cluster record or the ambient
10081
- * in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
10082
- * never contradicted by a failure; it just sits in the record. The CLI used to
10083
- * default to `hetzner`/`nbg1` unconditionally, which is how projects running on
10084
- * our GKE cluster came to describe themselves as Hetzner in the console — and
10085
- * `provider` also decides which substrate's rules a project's dials are clamped
10086
- * to — Autopilot's 250m floor and 1:1-6.5:1 band do not apply on Hetzner or EKS
10087
- * — so a wrong value here resizes pods, it is not a cosmetic slip.
10088
- *
10089
- * The control plane already publishes the infrastructure that actually exists,
10090
- * and the console's create wizard reads it. Ask the same question here.
10091
- *
10092
- * Exported for tests: the decision is pure, so it can be pinned without a
10093
- * control plane. The fetching and the exit live in `resolveRequestedTarget`.
10405
+ * Attach a database row to a project.
10094
10406
  *
10095
- * @param requested `--provider`, if the caller named one. An explicit flag wins:
10096
- * it is the caller stating intent, and `deploy` corrects the record anyway.
10097
- * @param targets What the control plane says exists, or `undefined` when it
10098
- * cannot say — an older deployment with no `platform-config`, or a failed
10099
- * request. That is different from an empty list, which is a control plane
10100
- * stating it has no infrastructure at all.
10101
- * @returns the target to record, or `null` when the control plane answered that
10102
- * there is none.
10407
+ * Extracted so `rebase cloud projects create` can do it in the same breath as
10408
+ * creating the project see `--db` there. Two call sites, one insert, so the
10409
+ * shape of the row cannot drift between "attached at creation" and "attached
10410
+ * afterwards".
10103
10411
  */
10104
- function chooseRequestedTarget(requested, targets) {
10105
- if (requested) return {
10106
- provider: requested,
10107
- region: void 0
10108
- };
10109
- if (!targets) return {
10110
- provider: "hetzner",
10111
- region: void 0
10112
- };
10113
- if (targets.length === 0) return null;
10114
- const [target] = targets;
10115
- return {
10116
- provider: target.provider,
10117
- region: target.region?.trim() || void 0
10118
- };
10119
- }
10120
- async function resolveRequestedTarget(client, url, requested) {
10121
- const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
10122
- if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`, "no_deploy_targets");
10123
- return chosen;
10412
+ async function attachDatabaseRow(client, input) {
10413
+ return await client.data.collection("databases").create({
10414
+ project: input.projectId,
10415
+ type: input.type,
10416
+ connectionString: input.type === "byodb" ? input.connectionString : void 0,
10417
+ connectionStatus: "untested"
10418
+ });
10124
10419
  }
10125
- /** The flags `rebase cloud projects create` takes. */
10126
- var CREATE_PROJECT_FLAGS = {
10127
- "--name": String,
10128
- "--subdomain": String,
10129
- "--repo": String,
10130
- "--branch": String,
10131
- "--provider": String,
10132
- "--region": String,
10133
- "--org": String,
10134
- "--link": Boolean,
10135
- "-n": "--name",
10136
- "--cpu": String,
10137
- "--memory": String,
10138
- "--replicas": String,
10139
- "--spot": String,
10140
- "--scale-to-zero": String,
10141
- "--db-mode": String,
10142
- "--db-instances": String,
10143
- "--db-cpu": String,
10144
- "--db-memory": String,
10145
- "--storage": String
10420
+ /** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
10421
+ var CREATE_DATABASE_FLAGS = {
10422
+ "--type": String,
10423
+ "--connection-string": String,
10424
+ "--wait": Boolean,
10425
+ "--timeout": String,
10426
+ "--project": String,
10427
+ "-p": "--project"
10146
10428
  };
10147
- async function createProject(rawArgs) {
10148
- const { flags: args } = parseCloudArgs({
10149
- spec: CREATE_PROJECT_FLAGS,
10150
- rawArgs,
10151
- commandWords: 3,
10152
- command: "cloud projects create",
10153
- maxPositionals: 0
10154
- });
10155
- const { client, url } = await requireClient(rawArgs);
10156
- const org = args["--org"] || getContextOrg(url);
10157
- if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`, "no_org");
10158
- const prompts = [];
10159
- if (!args["--name"]) prompts.push({
10160
- type: "input",
10161
- name: "name",
10162
- message: "Project name:"
10163
- });
10164
- if (!args["--subdomain"]) prompts.push({
10165
- type: "input",
10166
- name: "subdomain",
10167
- message: "Subdomain:"
10429
+ async function createDatabase(rawArgs) {
10430
+ const args = arg(CREATE_DATABASE_FLAGS, {
10431
+ argv: rawArgs.slice(4),
10432
+ permissive: true
10168
10433
  });
10169
- const a = prompts.length && process.stdin.isTTY ? await inquirer.prompt(prompts) : {};
10170
- const name = (args["--name"] || a.name || "").trim();
10171
- const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
10172
- const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
10173
- const gitBranch = (args["--branch"] || a.branch || "main").trim();
10174
- const target = await resolveRequestedTarget(client, url, (args["--provider"] || a.provider)?.trim() || void 0);
10175
- const provider = target.provider;
10176
- const defaults = providerDefaults(provider);
10177
- const region = (args["--region"] || target.region || defaults.region).trim();
10178
- const dials = buildDialPatch(rawArgs, { requireOne: false });
10179
- if (dials.error) fail(dials.error, void 0, "bad_request");
10180
- if (!name || !subdomain) fail("Name and subdomain are required.", `Pass ${chalk.bold("--name <name>")} and ${chalk.bold("--subdomain <slug>")}.`, "input_required");
10181
- try {
10182
- const check = await client.functions.invoke("check-subdomain", { subdomain });
10183
- if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_unavailable");
10184
- } catch {}
10434
+ const { client } = await requireClient(rawArgs);
10435
+ const projectId = await requireProject(rawArgs, client);
10436
+ const projectRef = displayProjectRef(rawArgs);
10437
+ const existing = await firstAttachedDatabase(client, projectId);
10438
+ if (existing) fail(`Project ${projectRef} already has a ${existing.type ?? "database"} attached (${existing.id}).`, "A project has exactly one database. Remove that one first, or run `rebase cloud db info` to see what it points at.", "database_exists");
10439
+ let type = args["--type"];
10440
+ if (!type) {
10441
+ requireInteractive("a database type", "--type <managed|byodb>");
10442
+ const { picked } = await inquirer.prompt([{
10443
+ type: "select",
10444
+ name: "picked",
10445
+ message: "Database type:",
10446
+ choices: [{
10447
+ name: "SaaS Managed (provisioned for you)",
10448
+ value: "managed"
10449
+ }, {
10450
+ name: "Bring Your Own DB (external PostgreSQL)",
10451
+ value: "byodb"
10452
+ }]
10453
+ }]);
10454
+ type = picked;
10455
+ }
10456
+ let connectionString = args["--connection-string"];
10457
+ if (type === "byodb" && !connectionString) {
10458
+ requireInteractive("a connection string", "--connection-string <url>");
10459
+ const { cs } = await inquirer.prompt([{
10460
+ type: "input",
10461
+ name: "cs",
10462
+ message: "PostgreSQL connection string:"
10463
+ }]);
10464
+ connectionString = cs?.trim();
10465
+ if (!connectionString) fail("A connection string is required for bring-your-own databases.", "Pass `--connection-string <url>`.", "input_required");
10466
+ }
10467
+ let created;
10185
10468
  try {
10186
- const user = await client.auth.getUser();
10187
- if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
10188
- const created = await client.data.collection("projects").create({
10189
- name,
10190
- subdomain,
10191
- gitRepoUrl,
10192
- gitBranch,
10193
- provider,
10194
- region,
10195
- ...dials.patch,
10196
- organization: org,
10197
- createdById: user.uid,
10198
- status: "provisioning"
10199
- });
10200
- const host = projectHost(created, await fetchTenantBaseDomain(client, url));
10201
- const linked = Boolean(args["--link"]);
10202
- if (linked) writeLink({
10203
- url,
10204
- projectId: String(created.id),
10205
- slug: created.subdomain,
10206
- projectName: name,
10207
- orgId: String(org)
10208
- });
10209
- success(`Created project ${chalk.bold(name)}`);
10210
- emit(() => {
10211
- keyValues([
10212
- ["Slug", String(created.subdomain ?? "")],
10213
- ["URL", host],
10214
- ["Provider", provider],
10215
- ["Branch", gitBranch]
10216
- ]);
10217
- if (linked) note(chalk.gray("Linked this directory to the new project."));
10218
- noteBlank();
10219
- note(chalk.gray(`Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.subdomain ?? created.id}`)}`));
10220
- noteBlank();
10221
- }, {
10222
- success: true,
10223
- id: String(created.id),
10224
- name,
10225
- slug: created.subdomain ?? null,
10226
- host: host ?? null,
10227
- provider,
10228
- region,
10229
- dials: dials.patch,
10230
- branch: gitBranch,
10231
- org: String(org),
10232
- linked
10469
+ created = await attachDatabaseRow(client, {
10470
+ projectId,
10471
+ type,
10472
+ connectionString
10233
10473
  });
10234
10474
  } catch (e) {
10235
- reportError(e, "Failed to create project");
10475
+ reportError(e, "Failed to attach database");
10236
10476
  }
10477
+ const waited = args["--wait"] === true ? await waitForDatabase(client, {
10478
+ projectId,
10479
+ type,
10480
+ timeoutMs: resolveTimeoutMs(args["--timeout"], {
10481
+ fallbackMs: DEFAULT_WAIT_MS,
10482
+ command: "cloud db create"
10483
+ })
10484
+ }) : void 0;
10485
+ success(`Attached ${type} database to project ${projectRef}`);
10486
+ emit(() => {
10487
+ keyValues([["ID", String(created.id)]]);
10488
+ if (waited?.note) note(chalk.gray(waited.note));
10489
+ else if (type === "byodb") note(chalk.gray("Verify it with `rebase cloud db test`."));
10490
+ else note(chalk.gray("It is created at your first deploy — run `rebase cloud deploy` next."));
10491
+ noteBlank();
10492
+ }, {
10493
+ success: true,
10494
+ id: String(created.id),
10495
+ projectId,
10496
+ type,
10497
+ connectionStatus: waited?.connectionStatus ?? "untested",
10498
+ waited: waited ? waited.waited : false,
10499
+ materializedAt: type === "managed" ? "first_deploy" : "now"
10500
+ });
10237
10501
  }
10502
+ /** How long `db create --wait` waits by default, and `--timeout` overrides. */
10503
+ var DEFAULT_WAIT_MS = 300 * 1e3;
10504
+ var WAIT_POLL_MS = 3e3;
10238
10505
  /**
10239
- * Which project `projects info` / `projects delete` acts on.
10240
- *
10241
- * The id is optional — omitted, it falls back to `--project` or the link file —
10242
- * and the dispatcher used to read it off `positionals()`, which skips only
10243
- * LEADING `-` tokens and declares only the global cloud flags. So an undeclared
10244
- * flag written after the action became the id: `rebase cloud projects delete
10245
- * --force` looked up a project named "--force" and reported it missing, rather
10246
- * than saying there is no such flag. Benign next to the deletes and writes the
10247
- * rest of this family aimed at the wrong resource, but the same mistake, and
10248
- * `positionals()` has no spec with which to do better — the handler's own
10249
- * module does.
10506
+ * Wait for an attached database to become usable, where "usable" means
10507
+ * something.
10250
10508
  *
10251
- * Exported so its tests drive the real parser rather than a copy of it.
10509
+ * Returns `waited: false` for the managed case, and says why the caller then
10510
+ * knows the state it is looking at is final rather than early.
10252
10511
  */
10253
- function resolveProjectArg(rawArgs, action) {
10254
- const { positionals } = parseCloudArgs({
10255
- spec: {},
10256
- rawArgs,
10257
- commandWords: 3,
10258
- command: `cloud projects ${action}`,
10259
- maxPositionals: 1
10260
- });
10261
- return positionals[0] || requireProjectRef(rawArgs);
10512
+ async function waitForDatabase(client, opts) {
10513
+ if (opts.type !== "byodb") return {
10514
+ waited: false,
10515
+ connectionStatus: "untested",
10516
+ note: "Nothing to wait for: a managed database is created at the project's first deploy. Run `rebase cloud deploy` next; `rebase cloud db test` only answers after that."
10517
+ };
10518
+ const started = Date.now();
10519
+ for (;;) {
10520
+ try {
10521
+ if ((await client.functions.invoke("db-test", { projectId: opts.projectId })).success) return {
10522
+ waited: true,
10523
+ connectionStatus: "connected"
10524
+ };
10525
+ } catch {}
10526
+ if (Date.now() - started > opts.timeoutMs) fail(`The database did not become reachable within ${Math.round(opts.timeoutMs / 1e3)}s.`, "Run `rebase cloud db test` for the connection log.", "timeout");
10527
+ await new Promise((resolve) => setTimeout(resolve, opts.pollMs ?? WAIT_POLL_MS));
10528
+ }
10529
+ }
10530
+ async function testDatabase(rawArgs) {
10531
+ const { client } = await requireClient(rawArgs);
10532
+ const projectId = await requireProject(rawArgs, client);
10533
+ const projectRef = displayProjectRef(rawArgs);
10534
+ noteBlank();
10535
+ note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
10536
+ try {
10537
+ const res = await client.functions.invoke("db-test", { projectId });
10538
+ if (res.logs) console.error(`\n${res.logs}`);
10539
+ if (!res.success) fail("Database connection failed.", "The connection log above (stderr) has the reason.", "db_connection_failed");
10540
+ success("Database connection succeeded");
10541
+ emit(() => {}, {
10542
+ success: true,
10543
+ projectId,
10544
+ logs: res.logs ?? null
10545
+ });
10546
+ } catch (e) {
10547
+ reportError(e, "Failed to test database");
10548
+ }
10262
10549
  }
10263
10550
  /**
10264
- * A project's database capacity, or null.
10551
+ * `rebase cloud db info [--reveal]` — where a project's database actually lives.
10265
10552
  *
10266
- * Swallows every failure on purpose. This decorates `status`; a control plane
10267
- * that predates the `capacity` function 404s here, and an older CLI talking to a
10268
- * newer one must still print the project. Losing the capacity line is a missing
10269
- * nicety failing the whole command over it would be the bug.
10553
+ * The password is NEVER in the default output; `--reveal` fetches it through the
10554
+ * separate reveal call, and it appears in JSON only when `--reveal` is given.
10555
+ * Any field the server could not resolve comes back `null` and is rendered as
10556
+ * unavailable, never a placeholder.
10270
10557
  */
10271
- async function fetchCapacity(client, projectId) {
10558
+ async function dbInfo(rawArgs) {
10559
+ const args = arg({
10560
+ "--reveal": Boolean,
10561
+ "--project": String,
10562
+ "-p": "--project"
10563
+ }, {
10564
+ argv: rawArgs.slice(2),
10565
+ permissive: true
10566
+ });
10567
+ const { client } = await requireClient(rawArgs);
10568
+ const projectId = await requireProject(rawArgs, client);
10569
+ const projectRef = displayProjectRef(rawArgs);
10272
10570
  try {
10273
- return (await client.functions.invoke("capacity", void 0, {
10571
+ const info = await client.functions.invoke("db-info", void 0, {
10274
10572
  method: "GET",
10275
10573
  path: projectId
10276
- }))?.database ?? null;
10277
- } catch {
10278
- return null;
10279
- }
10280
- }
10281
- async function projectInfo(rawArgs, projectRef) {
10282
- const { client, url } = await requireClient(rawArgs);
10283
- try {
10284
- const projectId = await resolveProjectRef(projectRef, client);
10285
- const p = await client.data.collection("projects").findById(projectId);
10286
- if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
10287
- const [db, lastDeploy, baseDomain, capacity] = await Promise.all([
10288
- firstRow(client, "databases", projectId),
10289
- latestDeployment(client, projectId),
10290
- fetchTenantBaseDomain(client, url),
10291
- fetchCapacity(client, projectId)
10292
- ]);
10574
+ });
10575
+ let password;
10576
+ let connectionString;
10577
+ if (args["--reveal"]) {
10578
+ if (!info.passwordAvailable) fail("No password is available to reveal for this database.", info.unavailableReason ?? void 0, "password_unavailable");
10579
+ const revealed = await client.functions.invoke("db-info", { projectId }, { path: "reveal" });
10580
+ password = revealed.password;
10581
+ connectionString = revealed.connectionString;
10582
+ }
10293
10583
  emit(() => {
10294
10584
  console.log("");
10295
- console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
10585
+ console.log(chalk.bold(` 🗄 Database project ${projectRef}`) + chalk.gray(` (${info.type})`));
10296
10586
  console.log("");
10297
10587
  keyValues([
10298
- ["Subdomain", projectHost(p, baseDomain)],
10299
- ["Custom domain", p.customDomain],
10300
- ["Repository", p.gitRepoUrl],
10301
- ["Branch", p.gitBranch],
10302
- ["Provider", p.provider],
10303
- ["Region", p.region],
10304
- ["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
10305
- ["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
10306
- ["Storage", capacity && capacity.limitMb > 0 ? `${capacity.usedMb} MB / ${capacity.limitMb} MB${capacity.usedFraction !== null ? ` (${(capacity.usedFraction * 100).toFixed(0)}%)` : ""}` : void 0],
10307
- ["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
10588
+ ["Host", info.host],
10589
+ ["Port", info.port],
10590
+ ["Database", info.database],
10591
+ ["Username", info.username],
10592
+ ["Password", info.passwordAvailable ? password ?? chalk.gray("hidden — pass --reveal") : chalk.gray("unavailable")],
10593
+ ["Connection", connectionString]
10308
10594
  ]);
10309
- if (capacity && capacity.state !== "ok") {
10595
+ if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
10596
+ if (info.portForward) {
10597
+ const pf = info.portForward;
10310
10598
  console.log("");
10311
- console.log(capacity.state === "locked" ? ` ${chalk.red.bold("✗ Database locked — over its storage limit")}` : ` ${chalk.yellow.bold("⚠ Database approaching its storage limit")}`);
10312
- console.log(` ${chalk.gray(capacity.detail)}`);
10599
+ console.log(chalk.gray(` Port-forward: kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
10313
10600
  }
10314
10601
  console.log("");
10315
10602
  }, {
10316
- id: String(p.id),
10317
- name: p.name ?? null,
10318
- slug: p.subdomain ?? null,
10319
- host: projectHost(p, baseDomain) ?? null,
10320
- customDomain: p.customDomain ?? null,
10321
- repository: p.gitRepoUrl ?? null,
10322
- branch: p.gitBranch ?? null,
10323
- provider: p.provider ?? null,
10324
- region: p.region ?? null,
10325
- status: p.status ?? null,
10326
- org: p.organization !== void 0 ? String(p.organization) : null,
10327
- database: db ? {
10328
- type: db.type ?? null,
10329
- connectionStatus: db.connectionStatus ?? null,
10330
- capacity: capacity ?? null
10331
- } : null,
10332
- lastDeploy: lastDeploy ? {
10333
- id: String(lastDeploy.id),
10334
- status: lastDeploy.status ?? null,
10335
- createdAt: lastDeploy.createdAt ?? null
10336
- } : null
10603
+ projectId,
10604
+ type: info.type,
10605
+ host: info.host,
10606
+ port: info.port,
10607
+ database: info.database,
10608
+ username: info.username,
10609
+ passwordAvailable: info.passwordAvailable,
10610
+ portForward: info.portForward,
10611
+ unavailableReason: info.unavailableReason,
10612
+ ...args["--reveal"] ? {
10613
+ password,
10614
+ connectionString
10615
+ } : {}
10337
10616
  });
10338
10617
  } catch (e) {
10339
- reportError(e, "Failed to load project");
10618
+ reportError(e, "Failed to load database info");
10340
10619
  }
10341
10620
  }
10342
- async function deleteProject(rawArgs, projectRef) {
10343
- const { flags: args } = parseCloudArgs({
10344
- spec: {},
10621
+ /**
10622
+ * `db backup [action] [filename]`, resolved in one strict parse.
10623
+ *
10624
+ * Both halves were reachable by the old operand filter, and both are
10625
+ * destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
10626
+ * ACTION (falling through to a list, so the flag silently changed what ran),
10627
+ * and `db backup restore -p acme` read it as the FILENAME — a restore staged
10628
+ * over the live database, named after the project slug. An undeclared flag was
10629
+ * dropped instead of refused, which is the same failure one step quieter: `db
10630
+ * backup --dry-run` ran a list, having silently discarded the flag that was
10631
+ * supposed to change what it did.
10632
+ *
10633
+ * Exported so its tests drive the real parser.
10634
+ */
10635
+ function resolveBackupArgs(rawArgs) {
10636
+ const { flags, positionals } = parseCloudArgs({
10637
+ spec: { "--yes": Boolean },
10345
10638
  rawArgs,
10346
10639
  commandWords: 3,
10347
- command: "cloud projects delete",
10348
- maxPositionals: 1
10640
+ command: "cloud db backup",
10641
+ maxPositionals: 2
10349
10642
  });
10643
+ return {
10644
+ flags,
10645
+ action: positionals[0] || "list",
10646
+ filename: positionals[1]
10647
+ };
10648
+ }
10649
+ async function backupCommand(rawArgs) {
10650
+ const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
10350
10651
  const { client } = await requireClient(rawArgs);
10351
- const projectId = await resolveProjectRef(projectRef, client);
10352
- const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
10353
- if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
10354
- await confirmDestructive({
10355
- yes: Boolean(args["--yes"]),
10356
- prompt: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
10357
- });
10652
+ const projectId = await requireProject(rawArgs, client);
10653
+ const projectRef = displayProjectRef(rawArgs);
10358
10654
  try {
10359
- await client.data.collection("projects").delete(projectId);
10360
- success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
10361
- emit(() => {}, {
10362
- success: true,
10363
- id: projectId,
10364
- name: p.name ?? null,
10365
- slug: p.subdomain ?? null
10655
+ if (action === "create") {
10656
+ const res = await client.functions.invoke("backup", {
10657
+ projectId,
10658
+ type: "manual"
10659
+ }, { path: "create" });
10660
+ if (!res.success) fail(res.error || "Backup failed.");
10661
+ emit(() => success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`), {
10662
+ success: true,
10663
+ backup: res.backup ?? null
10664
+ });
10665
+ return;
10666
+ }
10667
+ if (action === "restore") {
10668
+ const filename = backupFile;
10669
+ if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
10670
+ await confirmDestructive({
10671
+ yes: Boolean(args["--yes"]),
10672
+ prompt: `Restore "${filename}" over the current database for project ${projectRef}?`
10673
+ });
10674
+ const res = await client.functions.invoke("backup", {
10675
+ projectId,
10676
+ filename
10677
+ }, { path: "restore" });
10678
+ if (!res.success) fail(res.error || "Restore failed.");
10679
+ emit(() => success(res.message || "Restore complete"), {
10680
+ success: true,
10681
+ message: res.message ?? null
10682
+ });
10683
+ return;
10684
+ }
10685
+ if (action === "status") {
10686
+ const res = await client.functions.invoke("backup", void 0, {
10687
+ method: "GET",
10688
+ path: `backup-status/${projectId}`
10689
+ });
10690
+ emit(() => {
10691
+ console.log("");
10692
+ console.log(chalk.bold(` 💾 Automated backups — project ${projectRef}`));
10693
+ console.log("");
10694
+ keyValues([
10695
+ ["Enabled", res.enabled ? chalk.green("yes") : chalk.yellow("no")],
10696
+ ["Reason", String(res.reason ?? "")],
10697
+ ["Database type", String(res.databaseType ?? "")],
10698
+ ["Last backup", res.lastSuccessfulBackup ?? void 0],
10699
+ ["Recovery window", res.recoveryWindow ? `${res.recoveryWindow.from} → ${res.recoveryWindow.to}` : void 0]
10700
+ ]);
10701
+ console.log("");
10702
+ }, res);
10703
+ return;
10704
+ }
10705
+ if (action === "download") {
10706
+ const filename = backupFile;
10707
+ if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
10708
+ const res = await client.functions.invoke("backup", void 0, {
10709
+ method: "GET",
10710
+ path: `download/${projectId}/${encodeURIComponent(filename)}`
10711
+ });
10712
+ emit(() => {
10713
+ console.log("");
10714
+ console.log(chalk.bold(` ${res.name}`) + chalk.gray(` ${(res.size / 1024 / 1024).toFixed(1)} MB`));
10715
+ console.log(` ${chalk.cyan(res.url)}`);
10716
+ console.log("");
10717
+ console.log(chalk.gray(" Short-lived signed URL — fetch it with curl/wget."));
10718
+ console.log("");
10719
+ }, {
10720
+ name: res.name,
10721
+ size: res.size,
10722
+ url: res.url
10723
+ });
10724
+ return;
10725
+ }
10726
+ const res = await client.functions.invoke("backup", void 0, {
10727
+ method: "GET",
10728
+ path: `list/${projectId}`
10729
+ });
10730
+ emit(() => {
10731
+ console.log("");
10732
+ console.log(chalk.bold(` 💾 Backups — project ${projectRef}`));
10733
+ console.log("");
10734
+ if (!res.backups?.length) {
10735
+ console.log(chalk.gray(" No backups yet. Create one with `rebase cloud db backup create`."));
10736
+ console.log("");
10737
+ return;
10738
+ }
10739
+ for (const b of res.backups) {
10740
+ const size = b.size !== void 0 ? `${(b.size / 1024 / 1024).toFixed(1)} MB` : "";
10741
+ console.log(` ${chalk.bold(b.filename)} ${chalk.gray(`${b.type ?? ""} ${size}`.trim())}`);
10742
+ }
10743
+ console.log("");
10744
+ }, {
10745
+ projectId,
10746
+ backups: res.backups ?? []
10366
10747
  });
10367
10748
  } catch (e) {
10368
- reportError(e, "Failed to delete project");
10749
+ reportError(e, "Backup operation failed");
10369
10750
  }
10370
10751
  }
10371
- async function firstRow(client, collection, projectId) {
10372
- return (await client.data.collection(collection).find({
10373
- where: { project: ["==", projectId] },
10374
- limit: 1
10375
- })).data[0];
10376
- }
10377
- async function latestDeployment(client, projectId) {
10378
- return (await client.data.collection("deployments").find({
10379
- where: { project: ["==", projectId] },
10380
- orderBy: ["createdAt", "desc"],
10381
- limit: 1
10382
- })).data[0];
10383
- }
10384
- function fmtDate(value) {
10385
- if (!value) return "—";
10386
- const d = new Date(value);
10387
- return isNaN(d.getTime()) ? value : d.toLocaleString();
10388
- }
10389
- //#endregion
10390
- //#region src/commands/cloud/bundle-deploy.ts
10391
10752
  /**
10392
- * Deploying a project as a managed **bundle** rather than a source build.
10393
- *
10394
- * `rebase cloud deploy --bundle` builds the bundle, tars it, uploads it to the
10395
- * control plane's bundle endpoint, and triggers a deploy carrying the bundle id
10396
- * and its generated manifest. The control plane resolves a runtime from the
10397
- * manifest's range and runs the platform image with this bundle — the managed
10398
- * path. A project not in managed mode, or one whose bundle fails intake, is told
10399
- * so by the control plane; this side just packages and hands it over.
10753
+ * `rebase cloud db pitr <status|restore|cutover|discard>`.
10400
10754
  *
10401
- * The pieces here are separated from the network calls so they can be tested: the
10402
- * manifest read, the tar packaging, and the request body assembly are pure enough
10403
- * to check without a control plane.
10755
+ * A PITR restore is STAGED, not applied: `restore` creates a recovered copy of
10756
+ * the database beside the live one the application is NOT repointed and the
10757
+ * original is left running and unchanged. `cutover` is the separate, explicit
10758
+ * step that repoints the app at the recovered copy (and restarts it). `discard`
10759
+ * removes a staged copy; the server refuses to discard a copy that has been cut
10760
+ * over to (it is now the live database). Every mutating step requires `--yes` in
10761
+ * non-interactive use, and the CLI surfaces these staged semantics honestly.
10404
10762
  */
10405
- /** Read and shallow-validate a built bundle's manifest. */
10406
- function readBundleManifest(bundleDir) {
10407
- const manifestPath = path.join(bundleDir, "manifest.json");
10408
- if (!fs.existsSync(manifestPath)) throw new Error(`No manifest.json in ${bundleDir}. Run \`rebase build\` first.`);
10409
- let manifest;
10763
+ async function pitrCommand(rawArgs) {
10764
+ const { flags: args, positionals } = parseCloudArgs({
10765
+ spec: {
10766
+ "--target": String,
10767
+ "--yes": Boolean
10768
+ },
10769
+ rawArgs,
10770
+ commandWords: 3,
10771
+ command: "cloud db pitr",
10772
+ maxPositionals: 1
10773
+ });
10774
+ const action = positionals[0] || "status";
10775
+ const { client } = await requireClient(rawArgs);
10776
+ const projectId = await requireProject(rawArgs, client);
10777
+ const projectRef = displayProjectRef(rawArgs);
10410
10778
  try {
10411
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
10412
- } catch (err) {
10413
- throw new Error(`${manifestPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
10414
- }
10415
- if (typeof manifest.bundleFormat !== "number" || !manifest.runtime?.range) throw new Error(`${manifestPath} is not a valid bundle manifest.`);
10416
- return manifest;
10417
- }
10418
- /**
10419
- * Tar a built bundle into a gzipped archive.
10420
- *
10421
- * `node_modules` is excluded on purpose: the bundle ships a `package.json`, and
10422
- * the managed runtime installs the declared dependencies at boot. Uploading an
10423
- * installed `node_modules` would bloat the archive and could carry a
10424
- * platform-specific build that will not run on the runtime image.
10425
- */
10426
- function packBundle(bundleDir, outPath) {
10427
- return new Promise((resolve, reject) => {
10428
- const child = spawn("tar", [
10429
- "-czf",
10430
- outPath,
10431
- "--no-xattrs",
10432
- "--exclude",
10433
- "node_modules",
10434
- "-C",
10435
- bundleDir,
10436
- "."
10437
- ], {
10438
- stdio: "inherit",
10439
- env: {
10440
- ...process.env,
10441
- COPYFILE_DISABLE: "1"
10442
- }
10443
- });
10444
- child.on("error", reject);
10445
- child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`tar exited ${code}`)));
10779
+ if (action === "status") {
10780
+ const res = await client.functions.invoke("backup", void 0, {
10781
+ method: "GET",
10782
+ path: `pitr-status/${projectId}`
10783
+ });
10784
+ emit(() => {
10785
+ console.log("");
10786
+ console.log(chalk.bold(` ⏱ Point-in-time recovery — project ${projectRef}`));
10787
+ console.log("");
10788
+ keyValues([
10789
+ ["Available", res.available ? chalk.green("yes") : chalk.yellow("no")],
10790
+ ["First recoverable", res.firstRecoverabilityPoint ?? void 0],
10791
+ ["Last backup", res.lastSuccessfulBackup ?? void 0],
10792
+ ["Message", res.message ?? void 0]
10793
+ ]);
10794
+ console.log("");
10795
+ }, res);
10796
+ return;
10797
+ }
10798
+ if (action === "restore") {
10799
+ const target = args["--target"];
10800
+ if (!target) fail("Usage: rebase cloud db pitr restore --target <ISO timestamp>", void 0, "usage");
10801
+ await confirmDestructive({
10802
+ yes: Boolean(args["--yes"]),
10803
+ prompt: `Stage a point-in-time recovery of project ${projectRef} at ${target}? (stages a copy; does not repoint your app)`
10804
+ });
10805
+ const res = await client.functions.invoke("backup", {
10806
+ projectId,
10807
+ targetTime: target,
10808
+ acknowledgeNoCutover: true
10809
+ }, { path: "pitr-restore" });
10810
+ emit(() => {
10811
+ console.log("");
10812
+ console.log(chalk.yellow(` ⏳ ${String(res.message ?? "Recovery staged.")}`));
10813
+ console.log(chalk.gray(" Watch progress with `rebase cloud db pitr status`, then `rebase cloud db pitr cutover --yes`."));
10814
+ console.log("");
10815
+ }, res);
10816
+ return;
10817
+ }
10818
+ if (action === "cutover") {
10819
+ await confirmDestructive({
10820
+ yes: Boolean(args["--yes"]),
10821
+ prompt: `Cut project ${projectRef} over to the staged recovery? This repoints and restarts your application.`
10822
+ });
10823
+ const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-cutover" });
10824
+ emit(() => {
10825
+ console.log("");
10826
+ console.log(String(res.message ?? "Cutover requested."));
10827
+ console.log("");
10828
+ }, res);
10829
+ return;
10830
+ }
10831
+ if (action === "discard") {
10832
+ await confirmDestructive({
10833
+ yes: Boolean(args["--yes"]),
10834
+ prompt: `Discard the staged recovery for project ${projectRef}? This deletes the staged copy and its storage.`
10835
+ });
10836
+ const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-discard" });
10837
+ emit(() => success(String(res.message ?? "Staged restore discarded.")), res);
10838
+ return;
10839
+ }
10840
+ fail(`Unknown pitr command: ${action}`, "Try status | restore | cutover | discard.", "usage");
10841
+ } catch (e) {
10842
+ reportError(e, "PITR operation failed");
10843
+ }
10844
+ }
10845
+ function printDbHelp() {
10846
+ emitHelp("db", [
10847
+ "list",
10848
+ "create",
10849
+ "info",
10850
+ "test",
10851
+ "backup",
10852
+ "pitr"
10853
+ ], () => {
10854
+ console.log(`
10855
+ ${chalk.bold("rebase cloud db")} — Database & backups
10856
+
10857
+ ${chalk.green.bold("Commands")}
10858
+ ${chalk.blue.bold("list")} List databases attached to the project
10859
+ ${chalk.blue.bold("create")} Attach a managed or bring-your-own database
10860
+ ${chalk.blue.bold("info")} ${chalk.gray("[--reveal]")} Connection details ${chalk.gray("(password only with --reveal)")}
10861
+ ${chalk.blue.bold("test")} Test database connectivity
10862
+ ${chalk.blue.bold("backup list")} List backups
10863
+ ${chalk.blue.bold("backup create")} Create a manual backup
10864
+ ${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
10865
+ ${chalk.blue.bold("backup status")} Automated-backup health
10866
+ ${chalk.blue.bold("backup download")} ${chalk.gray("<file>")} Signed URL for a backup
10867
+ ${chalk.blue.bold("pitr status")} Point-in-time recovery window
10868
+ ${chalk.blue.bold("pitr restore")} ${chalk.gray("--target <ISO>")} Stage a recovery ${chalk.gray("(does not repoint)")}
10869
+ ${chalk.blue.bold("pitr cutover")} ${chalk.gray("-y")} Repoint the app at the staged recovery
10870
+ ${chalk.blue.bold("pitr discard")} ${chalk.gray("-y")} Delete a staged recovery
10871
+
10872
+ ${chalk.green.bold("Options")}
10873
+ ${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
10874
+ ${chalk.blue("--reveal")} Include the DB password ${chalk.gray("(info)")}
10875
+ ${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
10876
+ ${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
10877
+ ${chalk.blue("--json")} Machine-readable output
10878
+ `);
10446
10879
  });
10447
10880
  }
10881
+ //#endregion
10882
+ //#region src/commands/cloud/projects.ts
10448
10883
  /**
10449
- * Assemble the deploy-trigger body for a bundle deploy.
10450
- *
10451
- * The manifest travels with the trigger so the control plane can validate intake
10452
- * without unpacking the uploaded archive first — a rejection (native deps, no
10453
- * matching runtime) is then a fast, cheap answer.
10884
+ * `rebase cloud projects` list / create / info / delete.
10454
10885
  */
10455
- function bundleDeployBody(input) {
10456
- return {
10457
- projectId: input.projectId,
10458
- bundleId: input.bundleId,
10459
- bundleManifest: input.manifest,
10460
- app: input.app ?? input.manifest.app ?? "backend",
10461
- client: "cli",
10462
- frameworkVersion: input.manifest.runtime?.builtAgainst,
10463
- ...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
10464
- ...input.message ? { message: input.message } : {}
10465
- };
10886
+ async function listProjects(rawArgs) {
10887
+ const { client, url } = await requireClient(rawArgs);
10888
+ const org = getContextOrg(url);
10889
+ try {
10890
+ const [projects, baseDomain] = await Promise.all([client.data.collection("projects").find({
10891
+ where: org ? { organization: ["==", org] } : void 0,
10892
+ orderBy: ["name", "asc"],
10893
+ limit: 100
10894
+ }).then((res) => res.data), fetchTenantBaseDomain(client, url)]);
10895
+ const linkedId = readLink()?.projectId;
10896
+ emit(() => {
10897
+ console.log("");
10898
+ console.log(chalk.bold(" 📦 Projects") + (org ? chalk.gray(` (org ${org})`) : ""));
10899
+ console.log("");
10900
+ if (projects.length === 0) {
10901
+ console.log(chalk.gray(" No projects yet. Create one with `rebase cloud projects create`."));
10902
+ console.log("");
10903
+ return;
10904
+ }
10905
+ for (const p of projects) {
10906
+ const marker = String(p.id) === linkedId ? chalk.green(" ●") : " ";
10907
+ console.log(`${marker}${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
10908
+ console.log(` ${chalk.gray(projectHost(p, baseDomain) ?? "—")}${p.provider ? chalk.gray(` · ${p.provider}`) : ""}`);
10909
+ }
10910
+ console.log("");
10911
+ }, {
10912
+ org: org ?? null,
10913
+ projects: projects.map((p) => ({
10914
+ id: String(p.id),
10915
+ name: p.name ?? null,
10916
+ slug: p.subdomain ?? null,
10917
+ host: projectHost(p, baseDomain) ?? null,
10918
+ status: p.status ?? null,
10919
+ provider: p.provider ?? null,
10920
+ linked: String(p.id) === linkedId
10921
+ }))
10922
+ });
10923
+ } catch (e) {
10924
+ reportError(e, "Failed to list projects");
10925
+ }
10466
10926
  }
10467
10927
  /**
10468
- * The apps a project manifest declares.
10928
+ * Default region per provider. Required on create; the rest is dialled.
10469
10929
  *
10470
- * A deploy only ever ships ONE app's bundle, so the trigger alone could never
10471
- * tell the platform that the repository also contains a web frontend and an
10472
- * admin panel and the Apps page, whose whole job is to show the set, listed a
10473
- * single entry called "backend". Sending the declared set fixes that without
10474
- * pretending the others are deployed: the platform registers them, and their
10475
- * status says what is actually true.
10930
+ * There used to be a `vmSize` here too `e2-small`, `cx21` naming a machine
10931
+ * on a price list this platform does not buy from. The column was dropped on
10932
+ * 2026-08-20 and this went on sending it, which a control plane can only ignore
10933
+ * or refuse. What a project reserves is a set of dials now, and a project that
10934
+ * sets none takes the platform default.
10476
10935
  */
10477
- function declaredAppsFrom(manifest) {
10478
- const apps = manifest?.apps;
10479
- if (!apps || typeof apps !== "object") return [];
10480
- return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
10481
- name,
10482
- type: value?.type === "backend" ? "backend" : "static"
10483
- }));
10484
- }
10485
- /** Upload a bundle archive; returns the control-plane bundle id. */
10486
- async function uploadBundle(url, token, projectId, tarPath) {
10487
- const bytes = fs.readFileSync(tarPath);
10488
- const res = await fetch(`${url}/api/functions/deploy/bundle/upload?projectId=${encodeURIComponent(projectId)}`, {
10489
- method: "POST",
10490
- headers: {
10491
- Authorization: `Bearer ${token}`,
10492
- "Content-Type": "application/gzip"
10493
- },
10494
- body: bytes
10495
- });
10496
- if (!res.ok) {
10497
- const body = await res.text().catch(() => "");
10498
- throw new Error(`Bundle upload failed (${res.status}): ${body || res.statusText}`);
10936
+ function providerDefaults(provider) {
10937
+ switch (provider) {
10938
+ case "gcp": return { region: "europe-west1" };
10939
+ case "aws": return { region: "us-east-1" };
10940
+ default: return { region: "nbg1" };
10499
10941
  }
10500
- const data = await res.json();
10501
- if (!data.bundleId) throw new Error("Bundle upload endpoint did not return a bundle id.");
10502
- return data.bundleId;
10503
10942
  }
10504
- //#endregion
10505
- //#region src/commands/cloud/deploy.ts
10506
10943
  /**
10507
- * `rebase cloud deploy` and `rebase cloud logs`.
10944
+ * Where this project says it runs.
10508
10945
  *
10509
- * `deploy` triggers the control-plane `deploy` function, then tails the build
10510
- * logs from the deployment record until it succeeds or fails. `logs` shows the
10511
- * latest build log, or runtime logs with `--runtime`.
10946
+ * `provider`/`region` are a *request*: no code downstream reads them to pick a
10947
+ * deploy target that comes from the project's cluster record or the ambient
10948
+ * in-cluster context (saas/backend/src/k8s/resolve.ts). So a wrong value here is
10949
+ * never contradicted by a failure; it just sits in the record. The CLI used to
10950
+ * default to `hetzner`/`nbg1` unconditionally, which is how projects running on
10951
+ * our GKE cluster came to describe themselves as Hetzner in the console — and
10952
+ * `provider` also decides which substrate's rules a project's dials are clamped
10953
+ * to — Autopilot's 250m floor and 1:1-6.5:1 band do not apply on Hetzner or EKS
10954
+ * — so a wrong value here resizes pods, it is not a cosmetic slip.
10512
10955
  *
10513
- * There are three deploys behind the one verb, and which one runs depends on the
10514
- * flags: `--bundle` builds and uploads a managed bundle, `--source .` uploads
10515
- * this directory as a build context, and the bare form uploads nothing and asks
10516
- * the control plane to rebuild what it already holds. That last one is the
10517
- * dangerous one see `planBareDeploy`.
10956
+ * The control plane already publishes the infrastructure that actually exists,
10957
+ * and the console's create wizard reads it. Ask the same question here.
10958
+ *
10959
+ * Exported for tests: the decision is pure, so it can be pinned without a
10960
+ * control plane. The fetching and the exit live in `resolveRequestedTarget`.
10961
+ *
10962
+ * @param requested `--provider`, if the caller named one. An explicit flag wins:
10963
+ * it is the caller stating intent, and `deploy` corrects the record anyway.
10964
+ * @param targets What the control plane says exists, or `undefined` when it
10965
+ * cannot say — an older deployment with no `platform-config`, or a failed
10966
+ * request. That is different from an empty list, which is a control plane
10967
+ * stating it has no infrastructure at all.
10968
+ * @returns the target to record, or `null` when the control plane answered that
10969
+ * there is none.
10518
10970
  */
10519
- var POLL_INTERVAL_MS = 1500;
10520
- var POLL_TIMEOUT_MS = 900 * 1e3;
10521
- var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
10522
- function sleep(ms) {
10523
- return new Promise((r) => setTimeout(r, ms));
10524
- }
10525
- function run(cmd, cmdArgs, cwd, env) {
10526
- return new Promise((resolve, reject) => {
10527
- const child = spawn(cmd, cmdArgs, {
10528
- cwd,
10529
- env: env ? {
10530
- ...process.env,
10531
- ...env
10532
- } : void 0,
10533
- stdio: [
10534
- "ignore",
10535
- "ignore",
10536
- "pipe"
10537
- ]
10538
- });
10539
- let stderr = "";
10540
- child.stderr.on("data", (d) => stderr += d.toString());
10541
- child.on("error", reject);
10542
- child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
10971
+ function chooseRequestedTarget(requested, targets) {
10972
+ if (requested) return {
10973
+ provider: requested,
10974
+ region: void 0
10975
+ };
10976
+ if (!targets) return {
10977
+ provider: "hetzner",
10978
+ region: void 0
10979
+ };
10980
+ if (targets.length === 0) return null;
10981
+ const [target] = targets;
10982
+ return {
10983
+ provider: target.provider,
10984
+ region: target.region?.trim() || void 0
10985
+ };
10986
+ }
10987
+ async function resolveRequestedTarget(client, url, requested) {
10988
+ const chosen = chooseRequestedTarget(requested, await fetchDeployTargets(client, url));
10989
+ if (!chosen) fail("This control plane has no deploy targets configured.", `Register a cluster, or pass ${chalk.bold("--provider")} and ${chalk.bold("--region")} to record one anyway.`, "no_deploy_targets");
10990
+ return chosen;
10991
+ }
10992
+ /** The flags `rebase cloud projects create` takes. */
10993
+ var CREATE_PROJECT_FLAGS = {
10994
+ "--name": String,
10995
+ "--subdomain": String,
10996
+ "--repo": String,
10997
+ "--branch": String,
10998
+ "--provider": String,
10999
+ "--region": String,
11000
+ "--org": String,
11001
+ "--link": Boolean,
11002
+ /**
11003
+ * Which database the new project gets — `managed` (the default), `byodb`,
11004
+ * or `none`.
11005
+ *
11006
+ * A default rather than a prompt, and `managed` rather than `none`, because
11007
+ * the state this removes is not a missing convenience: a project with no
11008
+ * database is written `status: "provisioning"` and can never deploy, and
11009
+ * nothing in that word says a second command is owed. Attaching one here
11010
+ * means the two-command sequence that every project needs is one command,
11011
+ * and `--db none` is there for the case that genuinely wants to decide later.
11012
+ *
11013
+ * Distinct from `--db-mode`/`--db-cpu` next to it, which are resource dials
11014
+ * on a database that exists. This is whether there is one.
11015
+ */
11016
+ "--db": String,
11017
+ /** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
11018
+ "--connection-string": String,
11019
+ "-n": "--name",
11020
+ "--cpu": String,
11021
+ "--memory": String,
11022
+ "--replicas": String,
11023
+ "--spot": String,
11024
+ "--scale-to-zero": String,
11025
+ "--db-mode": String,
11026
+ "--db-instances": String,
11027
+ "--db-cpu": String,
11028
+ "--db-memory": String,
11029
+ "--storage": String
11030
+ };
11031
+ async function createProject(rawArgs) {
11032
+ const { flags: args } = parseCloudArgs({
11033
+ spec: CREATE_PROJECT_FLAGS,
11034
+ rawArgs,
11035
+ commandWords: 3,
11036
+ command: "cloud projects create",
11037
+ maxPositionals: 0
11038
+ });
11039
+ const { client, url } = await requireClient(rawArgs);
11040
+ const org = args["--org"] || getContextOrg(url);
11041
+ if (!org) fail("No organization selected.", `Pass ${chalk.bold("--org <id>")} or run ${chalk.bold("rebase cloud use")}.`, "no_org");
11042
+ const prompts = [];
11043
+ if (!args["--name"]) prompts.push({
11044
+ type: "input",
11045
+ name: "name",
11046
+ message: "Project name:"
10543
11047
  });
11048
+ if (!args["--subdomain"]) prompts.push({
11049
+ type: "input",
11050
+ name: "subdomain",
11051
+ message: "Subdomain:"
11052
+ });
11053
+ const a = prompts.length && process.stdin.isTTY ? await inquirer.prompt(prompts) : {};
11054
+ const name = (args["--name"] || a.name || "").trim();
11055
+ const subdomain = (args["--subdomain"] || a.subdomain || "").trim().toLowerCase();
11056
+ const gitRepoUrl = (args["--repo"] || a.repo || "").trim();
11057
+ const gitBranch = (args["--branch"] || a.branch || "main").trim();
11058
+ const target = await resolveRequestedTarget(client, url, (args["--provider"] || a.provider)?.trim() || void 0);
11059
+ const provider = target.provider;
11060
+ const defaults = providerDefaults(provider);
11061
+ const region = (args["--region"] || target.region || defaults.region).trim();
11062
+ const dials = buildDialPatch(rawArgs, { requireOne: false });
11063
+ if (dials.error) fail(dials.error, void 0, "bad_request");
11064
+ const dbChoice = (args["--db"] ?? "managed").trim().toLowerCase();
11065
+ if (![
11066
+ "managed",
11067
+ "byodb",
11068
+ "none"
11069
+ ].includes(dbChoice)) fail(`--db must be managed, byodb or none (got "${args["--db"]}").`, void 0, "bad_request");
11070
+ if (dbChoice === "byodb" && !args["--connection-string"]) fail("--db byodb needs the database to point at.", `Pass ${chalk.bold("--connection-string <url>")}.`, "input_required");
11071
+ if (!name || !subdomain) fail("Name and subdomain are required.", `Pass ${chalk.bold("--name <name>")} and ${chalk.bold("--subdomain <slug>")}.`, "input_required");
11072
+ try {
11073
+ const check = await client.functions.invoke("check-subdomain", { subdomain });
11074
+ if (!check.available) fail(`Subdomain "${subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_unavailable");
11075
+ } catch {}
11076
+ try {
11077
+ const user = await client.auth.getUser();
11078
+ if (!user) fail("Session is no longer valid.", "Run `rebase cloud login` again.", "session_invalid");
11079
+ const created = await client.data.collection("projects").create({
11080
+ name,
11081
+ subdomain,
11082
+ gitRepoUrl,
11083
+ gitBranch,
11084
+ provider,
11085
+ region,
11086
+ ...dials.patch,
11087
+ organization: org,
11088
+ createdById: user.uid,
11089
+ status: "provisioning"
11090
+ });
11091
+ const host = projectHost(created, await fetchTenantBaseDomain(client, url));
11092
+ const linked = Boolean(args["--link"]);
11093
+ if (linked) writeLink({
11094
+ url,
11095
+ projectId: String(created.id),
11096
+ slug: created.subdomain,
11097
+ projectName: name,
11098
+ orgId: String(org)
11099
+ });
11100
+ const database = await attachRequestedDatabase(client, {
11101
+ projectId: String(created.id),
11102
+ choice: dbChoice,
11103
+ connectionString: args["--connection-string"],
11104
+ projectRef: created.subdomain ?? String(created.id)
11105
+ });
11106
+ if (database.warning) warn(database.warning[0], database.warning[1]);
11107
+ success(`Created project ${chalk.bold(name)}`);
11108
+ emit(() => {
11109
+ keyValues([
11110
+ ["Slug", String(created.subdomain ?? "")],
11111
+ ["URL", host],
11112
+ ["Provider", provider],
11113
+ ["Branch", gitBranch],
11114
+ ["Database", database.line]
11115
+ ]);
11116
+ if (linked) note(chalk.gray("Linked this directory to the new project."));
11117
+ noteBlank();
11118
+ for (const line of database.notes) note(chalk.gray(line));
11119
+ note(chalk.gray(`Deploy it with: ${chalk.bold(`rebase cloud deploy --project ${created.subdomain ?? created.id}`)}`));
11120
+ noteBlank();
11121
+ }, {
11122
+ success: true,
11123
+ id: String(created.id),
11124
+ name,
11125
+ slug: created.subdomain ?? null,
11126
+ host: host ?? null,
11127
+ provider,
11128
+ region,
11129
+ dials: dials.patch,
11130
+ branch: gitBranch,
11131
+ org: String(org),
11132
+ linked,
11133
+ database: database.payload
11134
+ });
11135
+ } catch (e) {
11136
+ reportError(e, "Failed to create project");
11137
+ }
10544
11138
  }
10545
11139
  /**
10546
- * Package `sourceDir` into a gzipped tarball, honoring `.gitignore`/`.rebaseignore`
10547
- * and always excluding `.git` and `node_modules`. Returns the temp archive path.
11140
+ * Attach the database `--db` asked for, and describe what happened.
11141
+ *
11142
+ * Never throws. The project already exists by the time this runs, so a failure
11143
+ * here is a *partial* success and has to be reported as one — with the exact
11144
+ * command that finishes the job. Turning it into an exception would report the
11145
+ * whole `projects create` as failed and leave a real project behind, which is
11146
+ * the worst of both readings.
10548
11147
  */
10549
- async function createSourceTarball(sourceDir) {
10550
- const dir = path.resolve(sourceDir);
10551
- if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
10552
- const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
10553
- const tarArgs = [
10554
- "-czf",
10555
- tarPath,
10556
- "--exclude=.git",
10557
- "--exclude=node_modules"
10558
- ];
10559
- for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
10560
- tarArgs.push(".");
11148
+ async function attachRequestedDatabase(client, input) {
11149
+ if (input.choice === "none") return {
11150
+ line: chalk.yellow("none"),
11151
+ notes: ["No database attached (--db none). This project cannot deploy until one is:", " rebase cloud db create --type managed"],
11152
+ payload: {
11153
+ attached: false,
11154
+ type: null,
11155
+ reason: "requested_none"
11156
+ }
11157
+ };
10561
11158
  try {
10562
- await run("tar", tarArgs, dir, { COPYFILE_DISABLE: "1" });
11159
+ const row = await attachDatabaseRow(client, {
11160
+ projectId: input.projectId,
11161
+ type: input.choice,
11162
+ connectionString: input.connectionString
11163
+ });
11164
+ return {
11165
+ line: input.choice === "managed" ? `managed ${chalk.gray("· created at the first deploy")}` : `byodb ${chalk.gray("· not tested (`rebase cloud db test`)")}`,
11166
+ notes: [],
11167
+ payload: {
11168
+ attached: true,
11169
+ id: String(row.id),
11170
+ type: input.choice,
11171
+ materializedAt: input.choice === "managed" ? "first_deploy" : "now"
11172
+ }
11173
+ };
10563
11174
  } catch (e) {
10564
- fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
11175
+ const message = e instanceof Error ? e.message : String(e);
11176
+ return {
11177
+ line: chalk.red("not attached"),
11178
+ notes: [],
11179
+ warning: [`The project was created, but attaching its ${input.choice} database failed: ${message}`, `Finish it with: rebase cloud db create --type ${input.choice} --project ${input.projectRef}`],
11180
+ payload: {
11181
+ attached: false,
11182
+ type: input.choice,
11183
+ reason: "attach_failed",
11184
+ error: message
11185
+ }
11186
+ };
10565
11187
  }
10566
- return tarPath;
10567
11188
  }
10568
11189
  /**
10569
- * The `@rebasepro/*` version this source directory actually resolves.
10570
- *
10571
- * Recorded on the deployment so a row in Deployment History says which
10572
- * framework build shipped. Nothing else on the platform knows: an app that
10573
- * links the framework locally pins it at package time, and a silent bump is
10574
- * invisible afterwards — it has already cost one debugging session.
11190
+ * Which project `projects info` / `projects delete` acts on.
10575
11191
  *
10576
- * `@rebasepro/server` first, because that is what the deployed backend runs;
10577
- * `@rebasepro/client` is the fallback for a frontend-only bundle. Resolution is
10578
- * a plain walk up from the source directory rather than `require.resolve`,
10579
- * which would answer for the CLI's own install tree instead of the app's.
11192
+ * The id is optional — omitted, it falls back to `--project` or the link file
11193
+ * and the dispatcher used to read it off `positionals()`, which skips only
11194
+ * LEADING `-` tokens and declares only the global cloud flags. So an undeclared
11195
+ * flag written after the action became the id: `rebase cloud projects delete
11196
+ * --force` looked up a project named "--force" and reported it missing, rather
11197
+ * than saying there is no such flag. Benign next to the deletes and writes the
11198
+ * rest of this family aimed at the wrong resource, but the same mistake, and
11199
+ * `positionals()` has no spec with which to do better — the handler's own
11200
+ * module does.
10580
11201
  *
10581
- * Best effort by construction: a version that cannot be read is simply not
10582
- * recorded. Nothing about a deploy should fail over a bookkeeping string.
11202
+ * Exported so its tests drive the real parser rather than a copy of it.
10583
11203
  */
10584
- function resolveFrameworkVersion(sourceDir) {
10585
- let dir = path.resolve(sourceDir);
10586
- for (;;) {
10587
- for (const pkg of ["@rebasepro/server", "@rebasepro/client"]) try {
10588
- const manifest = path.join(dir, "node_modules", ...pkg.split("/"), "package.json");
10589
- const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version;
10590
- if (typeof version === "string" && version.trim() !== "") return version.trim();
10591
- } catch {}
10592
- const parent = path.dirname(dir);
10593
- if (parent === dir) return void 0;
10594
- dir = parent;
10595
- }
11204
+ function resolveProjectArg(rawArgs, action) {
11205
+ const { positionals } = parseCloudArgs({
11206
+ spec: {},
11207
+ rawArgs,
11208
+ commandWords: 3,
11209
+ command: `cloud projects ${action}`,
11210
+ maxPositionals: 1
11211
+ });
11212
+ return positionals[0] || requireProjectRef(rawArgs);
10596
11213
  }
10597
11214
  /**
10598
- * A progress line for a human — dropped entirely in JSON mode.
11215
+ * A project's database capacity, or null.
10599
11216
  *
10600
- * Progress is not a result. In JSON mode stdout carries the one result value
10601
- * and nothing else, so every unguarded `console.log` on a deploy path was a
10602
- * line printed in front of the JSON, breaking the parser meant to read it.
10603
- * Warnings are the other half of this rule and go the other way: they are
10604
- * `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
11217
+ * Swallows every failure on purpose. This decorates `status`; a control plane
11218
+ * that predates the `capacity` function 404s here, and an older CLI talking to a
11219
+ * newer one must still print the project. Losing the capacity line is a missing
11220
+ * nicety failing the whole command over it would be the bug.
10605
11221
  */
10606
- function progress(line) {
10607
- if (!isJsonMode()) console.log(line);
11222
+ async function fetchCapacity(client, projectId) {
11223
+ try {
11224
+ return (await client.functions.invoke("capacity", void 0, {
11225
+ method: "GET",
11226
+ path: projectId
11227
+ }))?.database ?? null;
11228
+ } catch {
11229
+ return null;
11230
+ }
10608
11231
  }
10609
- /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
10610
- async function uploadSource(url, token, projectId, tarPath) {
10611
- const bytes = fs.readFileSync(tarPath);
10612
- const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
10613
- if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
10614
- progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
10615
- const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
10616
- method: "POST",
10617
- headers: {
10618
- Authorization: `Bearer ${token}`,
10619
- "Content-Type": "application/gzip"
10620
- },
10621
- body: bytes
10622
- });
10623
- if (!res.ok) {
10624
- const body = await res.text().catch(() => "");
10625
- fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
10626
- }
10627
- const data = await res.json();
10628
- if (!data.source) fail("Upload endpoint did not return a source reference.");
10629
- return data.source;
10630
- }
10631
- /**
10632
- * Build, upload and deploy a project as a managed bundle.
10633
- *
10634
- * Builds the backend app into `dist-bundle` (unless one is pointed at with
10635
- * `--bundle-dir`), packs it without `node_modules`, uploads it, and triggers a
10636
- * deploy carrying the manifest so the control plane can validate intake fast.
10637
- */
10638
- async function deployBundle(opts) {
10639
- const projectRoot = requireProjectRoot();
10640
- let bundleDir = opts.bundleDir ? path.resolve(process.cwd(), opts.bundleDir) : path.join(projectRoot, "dist-bundle");
10641
- if (!opts.bundleDir) {
10642
- const loaded = loadManifest(projectRoot);
10643
- let target;
10644
- try {
10645
- target = selectDeployApp(loaded.manifest, opts.appName);
10646
- } catch (err) {
10647
- fail(err instanceof Error ? err.message : String(err));
10648
- return;
10649
- }
10650
- if (target.app.type === "static") {
10651
- progress(chalk.gray(` Building static app "${target.name}"...`));
10652
- const staticDir = await buildAssetApp(projectRoot, target.name, target.app, loaded.manifest.rebase);
10653
- if (!staticDir) {
10654
- fail(`App "${target.name}" produced no bundle.`, "A static app needs both a `build` command and an `output` directory in rebase.json.");
10655
- return;
10656
- }
10657
- bundleDir = staticDir;
10658
- await uploadAndTrigger({
10659
- ...opts,
10660
- bundleDir,
10661
- appName: target.name
10662
- });
10663
- return;
10664
- }
10665
- progress(chalk.gray(" Building bundle..."));
10666
- const backend = {
10667
- name: target.name,
10668
- app: target.app
10669
- };
10670
- const { graph: resourceGraph, issues: resourceIssues } = await deriveResourceGraph({ configDir: path.join(projectRoot, resolveBackendPaths(backend.app, projectRoot).config) });
10671
- if (resourceIssues.length > 0) throw new Error(`${resourceIssues.length} problem(s) in the declared resources:\n` + resourceIssues.map((i) => ` ${i.path} ${i.message}`).join("\n"));
10672
- bundleDir = (await buildBundle({
10673
- projectRoot,
10674
- appName: backend.name,
10675
- app: backend.app,
10676
- runtimeRange: loaded.manifest.rebase,
10677
- resources: resourceGraph,
10678
- skipTypeCheck: opts.skipTypeCheck,
10679
- log: (m) => progress(chalk.gray(m))
10680
- })).outDir;
10681
- try {
10682
- const folded = await foldFrontendIntoBundle({
10683
- projectRoot,
10684
- manifest: loaded.manifest,
10685
- bundleDir,
10686
- log: (m) => progress(m)
10687
- });
10688
- for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
10689
- } catch (err) {
10690
- fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
10691
- }
10692
- }
10693
- await uploadAndTrigger({
10694
- ...opts,
10695
- bundleDir
10696
- });
10697
- }
10698
- /**
10699
- * Pack a built bundle, upload it, and trigger the deploy.
10700
- *
10701
- * Shared by every managed deploy, whichever kind of app produced the bundle: a
10702
- * backend and a static app differ in what is built and in the `kind` their
10703
- * manifest carries, and in nothing after that. Keeping one tail is what makes
10704
- * that true rather than nearly true — the folding step was once missing from
10705
- * one of two callers producing the same artifact, and the deploy shipped a
10706
- * bundle with no site in it.
10707
- */
10708
- async function uploadAndTrigger(opts) {
10709
- const { client, url, projectId, projectRef, bundleDir } = opts;
10710
- const manifest = readBundleManifest(bundleDir);
10711
- if (manifest.hooks?.native) {
10712
- const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
10713
- fail(`This bundle depends on native modules${names ? ` (${names})` : ""}, which the managed runtime cannot run.`, "Remove the native dependency, or deploy on the custom runtime.");
10714
- }
10715
- const tarPath = path.join(os.tmpdir(), `rebase-bundle-${Date.now()}.tar.gz`);
10716
- const token = client.auth.getSession()?.accessToken;
10717
- if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
10718
- let bundleId;
11232
+ async function projectInfo(rawArgs, projectRef) {
11233
+ const { client, url } = await requireClient(rawArgs);
10719
11234
  try {
10720
- await packBundle(bundleDir, tarPath);
10721
- const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
10722
- progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
10723
- bundleId = await uploadBundle(url, token, projectId, tarPath);
11235
+ const projectId = await resolveProjectRef(projectRef, client);
11236
+ const p = await client.data.collection("projects").findById(projectId);
11237
+ if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
11238
+ const [db, lastDeploy, baseDomain, capacity] = await Promise.all([
11239
+ firstRow(client, "databases", projectId),
11240
+ latestDeployment(client, projectId),
11241
+ fetchTenantBaseDomain(client, url),
11242
+ fetchCapacity(client, projectId)
11243
+ ]);
11244
+ emit(() => {
11245
+ console.log("");
11246
+ console.log(` ${chalk.bold(p.name ?? "(unnamed)")} ${chalk.gray(`[${p.subdomain ?? p.id}]`)} ${colorStatus(p.status)}`);
11247
+ console.log("");
11248
+ keyValues([
11249
+ ["Subdomain", projectHost(p, baseDomain)],
11250
+ ["Custom domain", p.customDomain],
11251
+ ["Repository", p.gitRepoUrl],
11252
+ ["Branch", p.gitBranch],
11253
+ ["Provider", p.provider],
11254
+ ["Region", p.region],
11255
+ ["Organization", p.organization !== void 0 ? String(p.organization) : void 0],
11256
+ ["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
11257
+ ["Storage", capacity && capacity.limitMb > 0 ? `${capacity.usedMb} MB / ${capacity.limitMb} MB${capacity.usedFraction !== null ? ` (${(capacity.usedFraction * 100).toFixed(0)}%)` : ""}` : void 0],
11258
+ ["Last deploy", lastDeploy ? `${colorStatus(lastDeploy.status)} · ${fmtDate(lastDeploy.createdAt)}` : "never"]
11259
+ ]);
11260
+ if (capacity && capacity.state !== "ok") {
11261
+ console.log("");
11262
+ console.log(capacity.state === "locked" ? ` ${chalk.red.bold("✗ Database locked — over its storage limit")}` : ` ${chalk.yellow.bold("⚠ Database approaching its storage limit")}`);
11263
+ console.log(` ${chalk.gray(capacity.detail)}`);
11264
+ }
11265
+ console.log("");
11266
+ }, {
11267
+ id: String(p.id),
11268
+ name: p.name ?? null,
11269
+ slug: p.subdomain ?? null,
11270
+ host: projectHost(p, baseDomain) ?? null,
11271
+ customDomain: p.customDomain ?? null,
11272
+ repository: p.gitRepoUrl ?? null,
11273
+ branch: p.gitBranch ?? null,
11274
+ provider: p.provider ?? null,
11275
+ region: p.region ?? null,
11276
+ status: p.status ?? null,
11277
+ org: p.organization !== void 0 ? String(p.organization) : null,
11278
+ database: db ? {
11279
+ type: db.type ?? null,
11280
+ connectionStatus: db.connectionStatus ?? null,
11281
+ capacity: capacity ?? null
11282
+ } : null,
11283
+ lastDeploy: lastDeploy ? {
11284
+ id: String(lastDeploy.id),
11285
+ status: lastDeploy.status ?? null,
11286
+ createdAt: lastDeploy.createdAt ?? null
11287
+ } : null
11288
+ });
10724
11289
  } catch (e) {
10725
- fail(e instanceof Error ? e.message : String(e));
10726
- return;
10727
- } finally {
10728
- fs.rmSync(tarPath, { force: true });
10729
- }
10730
- if (!isJsonMode()) {
10731
- console.log("");
10732
- console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
11290
+ reportError(e, "Failed to load project");
10733
11291
  }
10734
- let declaredApps = [];
10735
- try {
10736
- declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
10737
- } catch {}
10738
- const body = bundleDeployBody({
10739
- projectId,
10740
- bundleId,
10741
- manifest,
10742
- app: opts.appName,
10743
- message: opts.message,
10744
- declaredApps
11292
+ }
11293
+ async function deleteProject(rawArgs, projectRef) {
11294
+ const { flags: args } = parseCloudArgs({
11295
+ spec: {},
11296
+ rawArgs,
11297
+ commandWords: 3,
11298
+ command: "cloud projects delete",
11299
+ maxPositionals: 1
11300
+ });
11301
+ const { client } = await requireClient(rawArgs);
11302
+ const projectId = await resolveProjectRef(projectRef, client);
11303
+ const p = await client.data.collection("projects").findById(projectId).catch(() => void 0);
11304
+ if (!p) fail(`Project ${projectRef} not found.`, void 0, "project_not_found");
11305
+ await confirmDestructive({
11306
+ yes: Boolean(args["--yes"]),
11307
+ prompt: `Permanently delete project "${p.name ?? projectRef}" (${p.subdomain ?? projectRef})? This tears down its deployment.`
10745
11308
  });
10746
11309
  try {
10747
- const res = await client.functions.invoke("deploy", body);
10748
- if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
10749
- if (isJsonMode()) printJson({
11310
+ await client.data.collection("projects").delete(projectId);
11311
+ success(`Deleted project ${chalk.bold(p.name ?? projectId)}`);
11312
+ emit(() => {}, {
10750
11313
  success: true,
10751
- deploymentId: String(res.deployment.id),
10752
- managed: res.managed === true
11314
+ id: projectId,
11315
+ name: p.name ?? null,
11316
+ slug: p.subdomain ?? null
10753
11317
  });
10754
- else {
10755
- console.log(chalk.green(` ✓ Managed deploy started (deployment ${res.deployment.id}).`));
10756
- console.log(chalk.gray(" Track it with `rebase cloud logs` or in the console."));
10757
- }
10758
11318
  } catch (e) {
10759
- reportError(e, "Managed deploy failed to start");
11319
+ reportError(e, "Failed to delete project");
10760
11320
  }
10761
11321
  }
10762
- function pick(row, ...keys) {
10763
- for (const key of keys) {
10764
- const raw = row?.[key];
10765
- if (typeof raw === "string" && raw.trim() !== "") return raw.trim();
10766
- }
11322
+ async function firstRow(client, collection, projectId) {
11323
+ return (await client.data.collection(collection).find({
11324
+ where: { project: ["==", projectId] },
11325
+ limit: 1
11326
+ })).data[0];
10767
11327
  }
10768
- /** Rough age of a timestamp, for "…uploaded 6d ago". Undefined if unreadable. */
10769
- function timeAgo(value, now) {
10770
- if (value === void 0) return void 0;
10771
- const then = value instanceof Date ? value.getTime() : new Date(value).getTime();
10772
- if (Number.isNaN(then)) return void 0;
10773
- const ms = now.getTime() - then;
10774
- if (ms < 0) return void 0;
10775
- const minutes = Math.floor(ms / 6e4);
10776
- if (minutes < 1) return "just now";
10777
- if (minutes < 60) return `${minutes}m ago`;
10778
- const hours = Math.floor(minutes / 60);
10779
- if (hours < 24) return `${hours}h ago`;
10780
- return `${Math.floor(hours / 24)}d ago`;
11328
+ async function latestDeployment(client, projectId) {
11329
+ return (await client.data.collection("deployments").find({
11330
+ where: { project: ["==", projectId] },
11331
+ orderBy: ["createdAt", "desc"],
11332
+ limit: 1
11333
+ })).data[0];
11334
+ }
11335
+ function fmtDate(value) {
11336
+ if (!value) return "";
11337
+ const d = new Date(value);
11338
+ return isNaN(d.getTime()) ? value : d.toLocaleString();
10781
11339
  }
11340
+ //#endregion
11341
+ //#region src/commands/cloud/bundle-deploy.ts
10782
11342
  /**
10783
- * Whether this project runs on the managed runtime.
11343
+ * Deploying a project as a managed **bundle** rather than a source build.
10784
11344
  *
10785
- * `runtimeMode` on the project row is the authority the control plane writes
10786
- * it. The bundle-id fallback covers a control plane that does not return the
10787
- * field: a successful deploy that served a bundle only happens on the managed
10788
- * path.
10789
- */
10790
- function isManagedProject(project, latest) {
10791
- if (pick(project, "runtimeMode", "runtime_mode") === "managed") return true;
10792
- return latest?.status === "success" && pick(latest, "bundleId", "bundle_id") !== void 0;
10793
- }
10794
- /** What a `deploy` with nothing attached will build, in the words to print. */
10795
- function planBareDeploy(project, latest, now) {
10796
- const projectRow = project;
10797
- const deploymentRow = latest;
10798
- const managed = isManagedProject(project, latest);
10799
- const repo = pick(projectRow, "gitRepoUrl", "git_repo_url");
10800
- if (repo) {
10801
- const branch = pick(projectRow, "gitBranch", "git_branch");
10802
- return {
10803
- managed,
10804
- source: "git",
10805
- lines: [`Building from git: ${repo}${branch ? ` (${branch})` : ""}.`]
10806
- };
10807
- }
10808
- if (pick(deploymentRow, "sourceRef", "source_ref")) {
10809
- const age = timeAgo(latest?.createdAt ?? latest?.created_at, now);
10810
- return {
10811
- managed,
10812
- source: "snapshot",
10813
- lines: [`Rebuilding the stored source archive${latest?.id !== void 0 ? ` from deployment ${latest.id}` : ""}${age ? `, uploaded ${age}` : ""}.`, "This directory is NOT uploaded — pass `--source .` to build what is on disk."]
10814
- };
11345
+ * `rebase cloud deploy --bundle` builds the bundle, tars it, uploads it to the
11346
+ * control plane's bundle endpoint, and triggers a deploy carrying the bundle id
11347
+ * and its generated manifest. The control plane resolves a runtime from the
11348
+ * manifest's range and runs the platform image with this bundle — the managed
11349
+ * path. A project not in managed mode, or one whose bundle fails intake, is told
11350
+ * so by the control plane; this side just packages and hands it over.
11351
+ *
11352
+ * The pieces here are separated from the network calls so they can be tested: the
11353
+ * manifest read, the tar packaging, and the request body assembly are pure enough
11354
+ * to check without a control plane.
11355
+ */
11356
+ /** Read and shallow-validate a built bundle's manifest. */
11357
+ function readBundleManifest(bundleDir) {
11358
+ const manifestPath = path.join(bundleDir, "manifest.json");
11359
+ if (!fs.existsSync(manifestPath)) throw new Error(`No manifest.json in ${bundleDir}. Run \`rebase build\` first.`);
11360
+ let manifest;
11361
+ try {
11362
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
11363
+ } catch (err) {
11364
+ throw new Error(`${manifestPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
10815
11365
  }
10816
- return {
10817
- managed,
10818
- source: "none",
10819
- lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
10820
- };
10821
- }
10822
- /** `code` of the warning below, and the field name it sets in the payload. */
10823
- var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
10824
- /** The one sentence that says a source build undoes `runtimeMode: managed`. */
10825
- function ejectWarning(projectRef) {
10826
- return {
10827
- code: EJECTS_MANAGED_RUNTIME,
10828
- message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
10829
- hint: "Use `rebase cloud deploy --bundle` to stay on managed."
10830
- };
11366
+ if (typeof manifest.bundleFormat !== "number" || !manifest.runtime?.range) throw new Error(`${manifestPath} is not a valid bundle manifest.`);
11367
+ return manifest;
10831
11368
  }
10832
11369
  /**
10833
- * Why a container-image deploy of a managed project is refused — or `undefined`
10834
- * to let it through.
10835
- *
10836
- * Every path below this point builds a container image, and a successful one
10837
- * sets `runtimeMode: "custom"` server-side. So the question is never "which flag
10838
- * was used" but "did the caller ask to leave the managed runtime", and only
10839
- * `--force` answers it.
11370
+ * Tar a built bundle into a gzipped archive.
10840
11371
  *
10841
- * `--source` used to be read as answering it too, on the theory that uploading a
10842
- * build context is self-evidently a deliberate eject. It is not: `--source`
10843
- * picks *which source* gets built this directory, rather than the stale
10844
- * archive the control plane is holding and the eject is a side effect of the
10845
- * answer. That is exactly how a live project got flipped to `custom` by someone
10846
- * whose actual intent was "deploy what I have here", and it is the same
10847
- * ignorance the bare form is refused for. Same ignorance, same refusal.
11372
+ * `node_modules` is excluded on purpose: the bundle ships a `package.json`, and
11373
+ * the managed runtime installs the declared dependencies at boot. Uploading an
11374
+ * installed `node_modules` would bloat the archive and could carry a
11375
+ * platform-specific build that will not run on the runtime image.
10848
11376
  */
10849
- function ejectRefusal(opts, projectRef) {
10850
- if (!opts.managed || opts.force) return void 0;
10851
- const eject = "To eject on purpose, add `--force`.";
10852
- if (opts.source) return {
10853
- message: `${projectRef} runs on the managed runtime, and \`--source\` builds a container image from this directory — which ejects it from managed. Picking a build method is not the same as asking to leave the runtime.`,
10854
- hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
10855
- code: "managed_project"
10856
- };
10857
- return {
10858
- message: `${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`,
10859
- hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
10860
- code: "managed_project"
10861
- };
11377
+ function packBundle(bundleDir, outPath) {
11378
+ return new Promise((resolve, reject) => {
11379
+ const child = spawn("tar", [
11380
+ "-czf",
11381
+ outPath,
11382
+ "--no-xattrs",
11383
+ "--exclude",
11384
+ "node_modules",
11385
+ "-C",
11386
+ bundleDir,
11387
+ "."
11388
+ ], {
11389
+ stdio: "inherit",
11390
+ env: {
11391
+ ...process.env,
11392
+ COPYFILE_DISABLE: "1"
11393
+ }
11394
+ });
11395
+ child.on("error", reject);
11396
+ child.on("close", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`tar exited ${code}`)));
11397
+ });
10862
11398
  }
10863
11399
  /**
10864
- * Which warnings a container-image deploy has earned.
10865
- *
10866
- * Pure, and separate from the printing, because the printing is what went
10867
- * wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
10868
- * the fact that a deploy ejects a managed project existed only as a side effect
10869
- * of a TTY being attached. Deciding here, emitting once at the call site, means
10870
- * the decision cannot be output-mode-dependent again.
11400
+ * Assemble the deploy-trigger body for a bundle deploy.
10871
11401
  *
10872
- * The condition is just `managed`: anything reaching this point is a container
10873
- * image build that `ejectRefusal` has already let through, and on a managed
10874
- * project that is an eject however it was spelled. A caller who passed `--force`
10875
- * knows — the warning is for the transcript and the payload, which is what
10876
- * anyone reviewing the deploy afterwards actually reads.
11402
+ * The manifest travels with the trigger so the control plane can validate intake
11403
+ * without unpacking the uploaded archive first a rejection (native deps, no
11404
+ * matching runtime) is then a fast, cheap answer.
10877
11405
  */
10878
- function deployWarnings(opts, projectRef) {
10879
- return opts.managed ? [ejectWarning(projectRef)] : [];
10880
- }
10881
- /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
10882
- function warningPayload(warnings) {
11406
+ function bundleDeployBody(input) {
10883
11407
  return {
10884
- warnings: warnings.map((w) => ({
10885
- code: w.code,
10886
- message: w.message,
10887
- hint: w.hint ?? null
10888
- })),
10889
- ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
11408
+ projectId: input.projectId,
11409
+ bundleId: input.bundleId,
11410
+ bundleManifest: input.manifest,
11411
+ app: input.app ?? input.manifest.app ?? "backend",
11412
+ client: "cli",
11413
+ frameworkVersion: input.manifest.runtime?.builtAgainst,
11414
+ ...input.declaredApps?.length ? { declaredApps: input.declaredApps } : {},
11415
+ ...input.message ? { message: input.message } : {}
10890
11416
  };
10891
11417
  }
10892
11418
  /**
10893
- * Read the two rows the preflight needs.
11419
+ * The apps a project manifest declares.
10894
11420
  *
10895
- * Best effort by construction: a preflight that cannot read is a preflight that
10896
- * says nothing, never a deploy that fails. The managed refusal rides on the same
10897
- * read, so an unreadable project falls through to the old behaviour rather than
10898
- * blocking a deploy on a lookup.
11421
+ * A deploy only ever ships ONE app's bundle, so the trigger alone could never
11422
+ * tell the platform that the repository also contains a web frontend and an
11423
+ * admin panel and the Apps page, whose whole job is to show the set, listed a
11424
+ * single entry called "backend". Sending the declared set fixes that without
11425
+ * pretending the others are deployed: the platform registers them, and their
11426
+ * status says what is actually true.
10899
11427
  */
10900
- async function readDeployContext(client, projectId) {
10901
- try {
10902
- const [project, latest] = await Promise.all([client.data.collection("projects").findById(projectId), latestDeployment(client, projectId)]);
10903
- return {
10904
- project,
10905
- latest
10906
- };
10907
- } catch {
10908
- return {};
10909
- }
11428
+ function declaredAppsFrom(manifest) {
11429
+ const apps = manifest?.apps;
11430
+ if (!apps || typeof apps !== "object") return [];
11431
+ return Object.entries(apps).filter(([name]) => name.trim().length > 0).map(([name, value]) => ({
11432
+ name,
11433
+ type: value?.type === "backend" ? "backend" : "static"
11434
+ }));
10910
11435
  }
10911
- /**
10912
- * Whether this repository's backend declares the managed runtime.
10913
- *
10914
- * Deliberately quiet: a directory that is not a Rebase project, or whose
10915
- * manifest does not parse, simply does not route this way — `rebase build` is
10916
- * where a broken manifest gets reported, and a deploy refusing on one before it
10917
- * has even said what it is doing would be the wrong place to find out.
10918
- */
10919
- function declaresManagedRuntime(appName) {
10920
- try {
10921
- const projectRoot = findProjectRoot();
10922
- if (!projectRoot) return false;
10923
- const manifest = loadManifest(projectRoot).manifest;
10924
- if (appName) {
10925
- const app = manifest.apps[appName];
10926
- if (app?.type === "static") return true;
10927
- if (app?.type === "backend") return app.runtime === "managed";
10928
- }
10929
- const backend = findBackendApp(manifest);
10930
- if (!backend) return Object.keys(manifest.apps).length > 0;
10931
- return backend.app.runtime === "managed";
10932
- } catch {
10933
- return false;
11436
+ /** Upload a bundle archive; returns the control-plane bundle id. */
11437
+ async function uploadBundle(url, token, projectId, tarPath) {
11438
+ const bytes = fs.readFileSync(tarPath);
11439
+ const res = await fetch(`${url}/api/functions/deploy/bundle/upload?projectId=${encodeURIComponent(projectId)}`, {
11440
+ method: "POST",
11441
+ headers: {
11442
+ Authorization: `Bearer ${token}`,
11443
+ "Content-Type": "application/gzip"
11444
+ },
11445
+ body: bytes
11446
+ });
11447
+ if (!res.ok) {
11448
+ const body = await res.text().catch(() => "");
11449
+ throw new Error(`Bundle upload failed (${res.status}): ${body || res.statusText}`);
10934
11450
  }
11451
+ const data = await res.json();
11452
+ if (!data.bundleId) throw new Error("Bundle upload endpoint did not return a bundle id.");
11453
+ return data.bundleId;
10935
11454
  }
11455
+ //#endregion
11456
+ //#region src/commands/cloud/deploy.ts
10936
11457
  /**
10937
- * `rebase cloud deploy [app]` — its flags, and which app of this repository the
10938
- * line named.
10939
- *
10940
- * Parsed through `parseCloudArgs` rather than `arg` directly, and the reason is
10941
- * the positional. This command used to parse `rawArgs.slice(2)` permissively
10942
- * and read `_[0]` as the app name — but `rawArgs` is the WHOLE `process.argv`,
10943
- * so `_` opens with the command words themselves. `_[0]` was therefore the
10944
- * literal string `"cloud"` on every run, which then went to `selectDeployApp`
10945
- * and came back as:
10946
- *
10947
- * This repository declares no app named "cloud". It declares: backend, web.
11458
+ * `rebase cloud deploy` and `rebase cloud logs`.
10948
11459
  *
10949
- * So the documented `rebase cloud deploy --bundle` failed on every project that
10950
- * did not happen to declare an app called `cloud`, `rebase cloud deploy web`
10951
- * could not reach `web`, and the refusal named the user's real apps — reading
10952
- * as a fault in their `rebase.json` rather than in the CLI's own parse.
11460
+ * `deploy` triggers the control-plane `deploy` function, then tails the build
11461
+ * logs from the deployment record until it succeeds or fails. `logs` shows the
11462
+ * latest build log, or runtime logs with `--runtime`.
10953
11463
  *
10954
- * `commandWords` counts from `cloud` itself, so `cloud deploy` is 2, and it is
10955
- * applied to the PARSED positionals: a flag written before the group no longer
10956
- * shifts the app name either.
11464
+ * There are three deploys behind the one verb, and which one runs depends on the
11465
+ * flags: `--bundle` builds and uploads a managed bundle, `--source .` uploads
11466
+ * this directory as a build context, and the bare form uploads nothing and asks
11467
+ * the control plane to rebuild what it already holds. That last one is the
11468
+ * dangerous one — see `planBareDeploy`.
10957
11469
  */
10958
- function resolveDeployArgs(rawArgs) {
10959
- const { flags, positionals } = parseCloudArgs({
10960
- spec: {
10961
- "--no-follow": Boolean,
10962
- "--source": String,
10963
- "--message": String,
10964
- "--bundle": Boolean,
10965
- "--bundle-dir": String,
10966
- "--skip-type-check": Boolean,
10967
- "--force": Boolean,
10968
- "-m": "--message"
10969
- },
10970
- rawArgs,
10971
- commandWords: 2,
10972
- command: "cloud deploy",
10973
- maxPositionals: 1
10974
- });
10975
- return {
10976
- flags,
10977
- appName: positionals[0]
10978
- };
11470
+ var POLL_INTERVAL_MS = 1500;
11471
+ var POLL_TIMEOUT_MS = 900 * 1e3;
11472
+ var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
11473
+ function sleep(ms) {
11474
+ return new Promise((r) => setTimeout(r, ms));
10979
11475
  }
10980
- async function deployCommand(rawArgs, projectRef) {
10981
- const { flags: args, appName } = resolveDeployArgs(rawArgs);
10982
- const { client, url } = await requireClient(rawArgs);
10983
- const projectId = await resolveProjectRef(projectRef, client);
10984
- const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime(appName);
10985
- if (args["--bundle"] || declaredManaged) {
10986
- if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
10987
- if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
10988
- await deployBundle({
10989
- client,
10990
- url,
10991
- projectId,
10992
- projectRef,
10993
- bundleDir: args["--bundle-dir"],
10994
- message: args["--message"],
10995
- appName,
10996
- skipTypeCheck: args["--skip-type-check"] === true
11476
+ function run(cmd, cmdArgs, cwd, env) {
11477
+ return new Promise((resolve, reject) => {
11478
+ const child = spawn(cmd, cmdArgs, {
11479
+ cwd,
11480
+ env: env ? {
11481
+ ...process.env,
11482
+ ...env
11483
+ } : void 0,
11484
+ stdio: [
11485
+ "ignore",
11486
+ "ignore",
11487
+ "pipe"
11488
+ ]
10997
11489
  });
10998
- return;
10999
- }
11000
- const { project, latest } = await readDeployContext(client, projectId);
11001
- const plan = planBareDeploy(project, latest, /* @__PURE__ */ new Date());
11002
- const eject = {
11003
- managed: plan.managed,
11004
- source: Boolean(args["--source"]),
11005
- force: args["--force"] === true
11006
- };
11007
- const refusal = ejectRefusal(eject, projectRef);
11008
- if (refusal) fail(refusal.message, refusal.hint, refusal.code);
11009
- const warnings = deployWarnings(eject, projectRef);
11010
- for (const w of warnings) warn(w.message, w.hint);
11011
- if (!args["--source"] && !isJsonMode()) {
11012
- console.log("");
11013
- for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
11014
- }
11015
- let source;
11016
- if (args["--source"]) {
11017
- const tarPath = await createSourceTarball(args["--source"]);
11018
- try {
11019
- const token = client.auth.getSession()?.accessToken;
11020
- if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
11021
- source = await uploadSource(url, token, projectId, tarPath);
11022
- } finally {
11023
- fs.rmSync(tarPath, { force: true });
11024
- }
11025
- }
11026
- if (!isJsonMode()) {
11027
- console.log("");
11028
- console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
11029
- }
11030
- const body = { projectId };
11031
- if (source) body.source = source;
11032
- if (args["--message"]) body.message = args["--message"];
11033
- body.client = "cli";
11034
- const frameworkVersion = resolveFrameworkVersion(args["--source"] ?? process.cwd());
11035
- if (frameworkVersion) body.frameworkVersion = frameworkVersion;
11036
- let triggered;
11490
+ let stderr = "";
11491
+ child.stderr.on("data", (d) => stderr += d.toString());
11492
+ child.on("error", reject);
11493
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || `${cmd} exited ${code}`)));
11494
+ });
11495
+ }
11496
+ /**
11497
+ * Package `sourceDir` into a gzipped tarball, honoring `.gitignore`/`.rebaseignore`
11498
+ * and always excluding `.git` and `node_modules`. Returns the temp archive path.
11499
+ */
11500
+ async function createSourceTarball(sourceDir) {
11501
+ const dir = path.resolve(sourceDir);
11502
+ if (!fs.existsSync(dir)) fail(`Source directory not found: ${dir}`);
11503
+ const tarPath = path.join(os.tmpdir(), `rebase-src-${Date.now()}.tar.gz`);
11504
+ const tarArgs = [
11505
+ "-czf",
11506
+ tarPath,
11507
+ "--exclude=.git",
11508
+ "--exclude=node_modules"
11509
+ ];
11510
+ for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
11511
+ tarArgs.push(".");
11037
11512
  try {
11038
- const res = await client.functions.invoke("deploy", body);
11039
- if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
11040
- triggered = {
11041
- deploymentId: String(res.deployment.id),
11042
- deduplicated: res.deduplicated === true
11043
- };
11513
+ await run("tar", tarArgs, dir, { COPYFILE_DISABLE: "1" });
11044
11514
  } catch (e) {
11045
- triggered = resolveTriggerFailure(e);
11046
- }
11047
- const { deploymentId, deduplicated } = triggered;
11048
- if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
11049
- if (args["--no-follow"]) {
11050
- emit(() => {
11051
- console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
11052
- console.log("");
11053
- }, {
11054
- deploymentId,
11055
- deduplicated,
11056
- frameworkVersion: frameworkVersion ?? null,
11057
- following: false,
11058
- ...warningPayload(warnings)
11059
- });
11060
- return;
11061
- }
11062
- if (!isJsonMode()) {
11063
- console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
11064
- console.log("");
11515
+ fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
11065
11516
  }
11066
- const status = await streamBuildLogs(client, deploymentId, { quiet: isJsonMode() });
11067
- emit(() => {}, {
11068
- deploymentId,
11069
- deduplicated,
11070
- frameworkVersion: frameworkVersion ?? null,
11071
- following: true,
11072
- status,
11073
- ...warningPayload(warnings)
11074
- });
11517
+ return tarPath;
11075
11518
  }
11076
11519
  /**
11077
- * Turn a failed trigger into either a deployment to follow, or an exit.
11520
+ * The `@rebasepro/*` version this source directory actually resolves.
11078
11521
  *
11079
- * The 409 is the interesting one. A deploy trigger can reach the control plane
11080
- * twice without anybody asking twice the SDK transport replays a request once
11081
- * after refreshing an expired token, and any lost response has the same effect
11082
- * so "a deployment is already in progress" was routinely describing the
11083
- * deployment this very command had just created. With no id in the message the
11084
- * only available reading was "someone else is deploying, back off", and the
11085
- * build stream was lost either way.
11522
+ * Recorded on the deployment so a row in Deployment History says which
11523
+ * framework build shipped. Nothing else on the platform knows: an app that
11524
+ * links the framework locally pins it at package time, and a silent bump is
11525
+ * invisible afterwards it has already cost one debugging session.
11086
11526
  *
11087
- * So: if the control plane says the blocking deployment is ours, we attach to
11088
- * it. If it is not ours, we still name it, because "which one, since when, from
11089
- * where" is the difference between an actionable refusal and a dead end.
11527
+ * `@rebasepro/server` first, because that is what the deployed backend runs;
11528
+ * `@rebasepro/client` is the fallback for a frontend-only bundle. Resolution is
11529
+ * a plain walk up from the source directory rather than `require.resolve`,
11530
+ * which would answer for the CLI's own install tree instead of the app's.
11531
+ *
11532
+ * Best effort by construction: a version that cannot be read is simply not
11533
+ * recorded. Nothing about a deploy should fail over a bookkeeping string.
11090
11534
  */
11091
- function resolveTriggerFailure(e) {
11092
- const err = e;
11093
- if (err?.status === 409) {
11094
- const blocking = err.details?.deployment;
11095
- if (blocking?.id && blocking.mine) return {
11096
- deploymentId: String(blocking.id),
11097
- deduplicated: true
11098
- };
11099
- fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${blocking.triggerSource && blocking.triggerSource !== "unknown" ? `, triggered from the ${blocking.triggerSource}` : ""}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Follow it with \`rebase cloud logs -f\`, or stop it with \`rebase cloud cancel ${blocking.id}\`.` : "Follow it with `rebase cloud logs -f`.", "deploy_in_progress");
11535
+ function resolveFrameworkVersion(sourceDir) {
11536
+ let dir = path.resolve(sourceDir);
11537
+ for (;;) {
11538
+ for (const pkg of ["@rebasepro/server", "@rebasepro/client"]) try {
11539
+ const manifest = path.join(dir, "node_modules", ...pkg.split("/"), "package.json");
11540
+ const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version;
11541
+ if (typeof version === "string" && version.trim() !== "") return version.trim();
11542
+ } catch {}
11543
+ const parent = path.dirname(dir);
11544
+ if (parent === dir) return void 0;
11545
+ dir = parent;
11100
11546
  }
11101
- if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
11102
- if (err?.status === 400 && err.details?.intakeCode) fail(err.message || "This bundle was refused.", err.details.hint ?? "Run `rebase cloud resources` to see what this project is given.", err.details.intakeCode);
11103
- reportError(e, "Failed to trigger deployment");
11104
11547
  }
11105
11548
  /**
11106
- * Poll a deployment record and print new log output as it arrives. Returns the
11107
- * terminal status; a non-success still exits non-zero, as it always has.
11549
+ * A progress line for a human dropped entirely in JSON mode.
11108
11550
  *
11109
- * `quiet` follows without printing JSON mode, where the log stream would
11110
- * corrupt the one object the caller is parsing.
11551
+ * Progress is not a result. In JSON mode stdout carries the one result value
11552
+ * and nothing else, so every unguarded `console.log` on a deploy path was a
11553
+ * line printed in front of the JSON, breaking the parser meant to read it.
11554
+ * Warnings are the other half of this rule and go the other way: they are
11555
+ * `warn`, which prints in every mode, to stderr. See `warn` in `context.ts`.
11111
11556
  */
11112
- async function streamBuildLogs(client, deploymentId, opts = {}) {
11113
- const quiet = opts.quiet === true;
11114
- let printed = 0;
11115
- const started = Date.now();
11116
- for (;;) {
11117
- let dep;
11557
+ function progress(line) {
11558
+ if (!isJsonMode()) console.log(line);
11559
+ }
11560
+ /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
11561
+ async function uploadSource(url, token, projectId, tarPath) {
11562
+ const bytes = fs.readFileSync(tarPath);
11563
+ const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
11564
+ if (bytes.length > MAX_SOURCE_UPLOAD_BYTES) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
11565
+ progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
11566
+ const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
11567
+ method: "POST",
11568
+ headers: {
11569
+ Authorization: `Bearer ${token}`,
11570
+ "Content-Type": "application/gzip"
11571
+ },
11572
+ body: bytes
11573
+ });
11574
+ if (!res.ok) {
11575
+ const body = await res.text().catch(() => "");
11576
+ fail(`Source upload failed (${res.status}): ${body || res.statusText}`);
11577
+ }
11578
+ const data = await res.json();
11579
+ if (!data.source) fail("Upload endpoint did not return a source reference.");
11580
+ return data.source;
11581
+ }
11582
+ /**
11583
+ * Build, upload and deploy a project as a managed bundle.
11584
+ *
11585
+ * Builds the backend app into `dist-bundle` (unless one is pointed at with
11586
+ * `--bundle-dir`), packs it without `node_modules`, uploads it, and triggers a
11587
+ * deploy carrying the manifest so the control plane can validate intake fast.
11588
+ */
11589
+ async function deployBundle(opts) {
11590
+ const projectRoot = requireProjectRoot();
11591
+ let bundleDir = opts.bundleDir ? path.resolve(process.cwd(), opts.bundleDir) : path.join(projectRoot, "dist-bundle");
11592
+ if (!opts.bundleDir) {
11593
+ const loaded = loadManifest(projectRoot);
11594
+ let target;
11118
11595
  try {
11119
- dep = await client.data.collection("deployments").findById(deploymentId);
11120
- } catch (e) {
11121
- reportError(e, "Failed to read deployment status");
11596
+ target = selectDeployApp(loaded.manifest, opts.appName);
11597
+ } catch (err) {
11598
+ fail(err instanceof Error ? err.message : String(err));
11599
+ return;
11122
11600
  }
11123
- if (!dep) fail(`Deployment ${deploymentId} disappeared.`, void 0, "not_found");
11124
- const logs = dep.logs ?? "";
11125
- if (!quiet && logs.length > printed) process.stdout.write(logs.slice(printed));
11126
- printed = logs.length;
11127
- if (dep.status && dep.status !== "deploying") {
11128
- if (dep.status !== "success") {
11129
- if (quiet) {
11130
- printJson({ error: {
11131
- message: `Deployment ${deploymentId} ${dep.status}.`,
11132
- code: "deploy_failed",
11133
- status: null,
11134
- deploymentId,
11135
- logs
11136
- } });
11137
- process.exit(1);
11138
- }
11139
- console.log("");
11140
- console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
11141
- console.log("");
11142
- process.exit(1);
11143
- }
11144
- if (!quiet) {
11145
- console.log("");
11146
- console.log(chalk.bold.green(" ✓ Deployment succeeded"));
11147
- console.log("");
11601
+ if (target.app.type === "static") {
11602
+ progress(chalk.gray(` Building static app "${target.name}"...`));
11603
+ const staticDir = await buildAssetApp(projectRoot, target.name, target.app, loaded.manifest.rebase);
11604
+ if (!staticDir) {
11605
+ fail(`App "${target.name}" produced no bundle.`, "A static app needs both a `build` command and an `output` directory in rebase.json.");
11606
+ return;
11148
11607
  }
11149
- return dep.status;
11150
- }
11151
- if (Date.now() - started > POLL_TIMEOUT_MS) {
11152
- if (!quiet) console.log("");
11153
- fail("Timed out waiting for the build to finish.", "The deployment may still be running — check `rebase cloud logs`.", "timeout");
11608
+ bundleDir = staticDir;
11609
+ await uploadAndTrigger({
11610
+ ...opts,
11611
+ bundleDir,
11612
+ appName: target.name
11613
+ });
11614
+ return;
11154
11615
  }
11155
- await sleep(POLL_INTERVAL_MS);
11156
- }
11157
- }
11158
- async function logsCommand(rawArgs, projectRef) {
11159
- const args = arg({
11160
- "--runtime": Boolean,
11161
- "--follow": Boolean,
11162
- "-f": "--follow"
11163
- }, {
11164
- argv: rawArgs.slice(2),
11165
- permissive: true
11166
- });
11167
- const { client } = await requireClient(rawArgs);
11168
- const projectId = await resolveProjectRef(projectRef, client);
11169
- if (args["--runtime"]) {
11616
+ progress(chalk.gray(" Building bundle..."));
11617
+ const backend = {
11618
+ name: target.name,
11619
+ app: target.app
11620
+ };
11621
+ const { graph: resourceGraph, issues: resourceIssues } = await deriveResourceGraph({ configDir: path.join(projectRoot, resolveBackendPaths(backend.app, projectRoot).config) });
11622
+ if (resourceIssues.length > 0) throw new Error(`${resourceIssues.length} problem(s) in the declared resources:\n` + resourceIssues.map((i) => ` ${i.path} ${i.message}`).join("\n"));
11623
+ bundleDir = (await buildBundle({
11624
+ projectRoot,
11625
+ appName: backend.name,
11626
+ app: backend.app,
11627
+ runtimeRange: loaded.manifest.rebase,
11628
+ resources: resourceGraph,
11629
+ skipTypeCheck: opts.skipTypeCheck,
11630
+ log: (m) => progress(chalk.gray(m))
11631
+ })).outDir;
11170
11632
  try {
11171
- const res = await client.functions.invoke("runtime-logs", void 0, {
11172
- method: "GET",
11173
- path: projectId
11633
+ const folded = await foldFrontendIntoBundle({
11634
+ projectRoot,
11635
+ manifest: loaded.manifest,
11636
+ bundleDir,
11637
+ log: (m) => progress(m)
11174
11638
  });
11175
- console.log("");
11176
- console.log(chalk.bold(` 📄 Runtime logs — project ${projectRef}`));
11177
- console.log("");
11178
- console.log(res.logs ?? chalk.gray(" (no logs)"));
11179
- console.log("");
11180
- } catch (e) {
11181
- reportError(e, "Failed to fetch runtime logs");
11639
+ for (const outcome of folded) progress(chalk.gray(` folded ${outcome.appName} in (${outcome.fileCount} file(s), served at ${outcome.path})`));
11640
+ } catch (err) {
11641
+ fail(err instanceof Error ? err.message : String(err), "Fix the frontend build, or pass --no-static to deploy the API alone.");
11182
11642
  }
11183
- return;
11184
11643
  }
11644
+ await uploadAndTrigger({
11645
+ ...opts,
11646
+ bundleDir
11647
+ });
11648
+ }
11649
+ /**
11650
+ * Pack a built bundle, upload it, and trigger the deploy.
11651
+ *
11652
+ * Shared by every managed deploy, whichever kind of app produced the bundle: a
11653
+ * backend and a static app differ in what is built and in the `kind` their
11654
+ * manifest carries, and in nothing after that. Keeping one tail is what makes
11655
+ * that true rather than nearly true — the folding step was once missing from
11656
+ * one of two callers producing the same artifact, and the deploy shipped a
11657
+ * bundle with no site in it.
11658
+ */
11659
+ async function uploadAndTrigger(opts) {
11660
+ const { client, url, projectId, projectRef, bundleDir } = opts;
11661
+ const manifest = readBundleManifest(bundleDir);
11662
+ if (manifest.hooks?.native) {
11663
+ const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
11664
+ fail(`This bundle depends on native modules${names ? ` (${names})` : ""}, which the managed runtime cannot run.`, "Remove the native dependency, or deploy on the custom runtime.");
11665
+ }
11666
+ const tarPath = path.join(os.tmpdir(), `rebase-bundle-${Date.now()}.tar.gz`);
11667
+ const token = client.auth.getSession()?.accessToken;
11668
+ if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
11669
+ let bundleId;
11185
11670
  try {
11186
- const dep = await latestDeployment(client, projectId);
11187
- if (!dep) {
11188
- console.log("");
11189
- console.log(chalk.gray(" No deployments yet for this project."));
11190
- console.log("");
11191
- return;
11192
- }
11193
- console.log("");
11194
- console.log(chalk.bold(` 📄 Build logs deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
11671
+ await packBundle(bundleDir, tarPath);
11672
+ const sizeMb = (fs.statSync(tarPath).size / 1024 / 1024).toFixed(1);
11673
+ progress(chalk.gray(` Uploading bundle (${sizeMb} MB)...`));
11674
+ bundleId = await uploadBundle(url, token, projectId, tarPath);
11675
+ } catch (e) {
11676
+ fail(e instanceof Error ? e.message : String(e));
11677
+ return;
11678
+ } finally {
11679
+ fs.rmSync(tarPath, { force: true });
11680
+ }
11681
+ if (!isJsonMode()) {
11195
11682
  console.log("");
11196
- if (args["--follow"] && dep.status === "deploying") await streamBuildLogs(client, String(dep.id));
11197
- else {
11198
- console.log(dep.logs ?? chalk.gray(" (no logs)"));
11199
- console.log("");
11200
- }
11683
+ console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
11684
+ }
11685
+ let declaredApps = [];
11686
+ try {
11687
+ declaredApps = declaredAppsFrom(loadManifest(process.cwd()).manifest);
11688
+ } catch {}
11689
+ const body = bundleDeployBody({
11690
+ projectId,
11691
+ bundleId,
11692
+ manifest,
11693
+ app: opts.appName,
11694
+ message: opts.message,
11695
+ declaredApps
11696
+ });
11697
+ let deploymentId;
11698
+ let managed;
11699
+ try {
11700
+ const res = await client.functions.invoke("deploy", body);
11701
+ if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
11702
+ deploymentId = String(res.deployment.id);
11703
+ managed = res.managed === true;
11201
11704
  } catch (e) {
11202
- reportError(e, "Failed to fetch build logs");
11705
+ reportError(e, "Managed deploy failed to start");
11706
+ }
11707
+ if (!opts.follow) {
11708
+ emit(() => {
11709
+ console.log(chalk.green(` ✓ Managed deploy started (deployment ${deploymentId}).`));
11710
+ console.log(chalk.gray(" Not following (--no-follow). Track it with `rebase cloud logs`."));
11711
+ }, {
11712
+ success: true,
11713
+ deploymentId,
11714
+ managed,
11715
+ following: false
11716
+ });
11717
+ return;
11203
11718
  }
11719
+ if (!isJsonMode()) {
11720
+ console.log(chalk.green(` ✓ Managed deploy started (deployment ${deploymentId}).`));
11721
+ console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
11722
+ console.log("");
11723
+ }
11724
+ const status = await streamBuildLogs(client, deploymentId, {
11725
+ quiet: isJsonMode(),
11726
+ timeoutMs: opts.timeoutMs
11727
+ });
11728
+ emit(() => {}, {
11729
+ success: true,
11730
+ deploymentId,
11731
+ managed,
11732
+ following: true,
11733
+ status
11734
+ });
11735
+ }
11736
+ function pick(row, ...keys) {
11737
+ for (const key of keys) {
11738
+ const raw = row?.[key];
11739
+ if (typeof raw === "string" && raw.trim() !== "") return raw.trim();
11740
+ }
11741
+ }
11742
+ /** Rough age of a timestamp, for "…uploaded 6d ago". Undefined if unreadable. */
11743
+ function timeAgo(value, now) {
11744
+ if (value === void 0) return void 0;
11745
+ const then = value instanceof Date ? value.getTime() : new Date(value).getTime();
11746
+ if (Number.isNaN(then)) return void 0;
11747
+ const ms = now.getTime() - then;
11748
+ if (ms < 0) return void 0;
11749
+ const minutes = Math.floor(ms / 6e4);
11750
+ if (minutes < 1) return "just now";
11751
+ if (minutes < 60) return `${minutes}m ago`;
11752
+ const hours = Math.floor(minutes / 60);
11753
+ if (hours < 24) return `${hours}h ago`;
11754
+ return `${Math.floor(hours / 24)}d ago`;
11204
11755
  }
11205
- //#endregion
11206
- //#region src/commands/cloud/orgs.ts
11207
11756
  /**
11208
- * `rebase cloud orgs` list / create / members.
11757
+ * Whether this project runs on the managed runtime.
11758
+ *
11759
+ * `runtimeMode` on the project row is the authority — the control plane writes
11760
+ * it. The bundle-id fallback covers a control plane that does not return the
11761
+ * field: a successful deploy that served a bundle only happens on the managed
11762
+ * path.
11209
11763
  */
11210
- async function orgsCommand(subcommand, rawArgs) {
11211
- switch (subcommand) {
11212
- case "list":
11213
- case void 0:
11214
- await listOrgs(rawArgs);
11215
- break;
11216
- case "create":
11217
- await createOrg(rawArgs);
11218
- break;
11219
- case "members":
11220
- await listMembers(rawArgs);
11221
- break;
11222
- case "--help":
11223
- printOrgsHelp();
11224
- break;
11225
- default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
11764
+ function isManagedProject(project, latest) {
11765
+ if (pick(project, "runtimeMode", "runtime_mode") === "managed") return true;
11766
+ return latest?.status === "success" && pick(latest, "bundleId", "bundle_id") !== void 0;
11767
+ }
11768
+ /** What a `deploy` with nothing attached will build, in the words to print. */
11769
+ function planBareDeploy(project, latest, now) {
11770
+ const projectRow = project;
11771
+ const deploymentRow = latest;
11772
+ const managed = isManagedProject(project, latest);
11773
+ const repo = pick(projectRow, "gitRepoUrl", "git_repo_url");
11774
+ if (repo) {
11775
+ const branch = pick(projectRow, "gitBranch", "git_branch");
11776
+ return {
11777
+ managed,
11778
+ source: "git",
11779
+ lines: [`Building from git: ${repo}${branch ? ` (${branch})` : ""}.`]
11780
+ };
11781
+ }
11782
+ if (pick(deploymentRow, "sourceRef", "source_ref")) {
11783
+ const age = timeAgo(latest?.createdAt ?? latest?.created_at, now);
11784
+ return {
11785
+ managed,
11786
+ source: "snapshot",
11787
+ lines: [`Rebuilding the stored source archive${latest?.id !== void 0 ? ` from deployment ${latest.id}` : ""}${age ? `, uploaded ${age}` : ""}.`, "This directory is NOT uploaded — pass `--source .` to build what is on disk."]
11788
+ };
11226
11789
  }
11790
+ return {
11791
+ managed,
11792
+ source: "none",
11793
+ lines: ["This project has no git repository configured and no stored source archive to rebuild.", "Upload this directory with `--source .`, or set a repository URL in the project settings."]
11794
+ };
11227
11795
  }
11228
- async function listOrgs(rawArgs) {
11229
- const { client, url } = await requireClient(rawArgs);
11796
+ /** `code` of the warning below, and the field name it sets in the payload. */
11797
+ var EJECTS_MANAGED_RUNTIME = "ejects_managed_runtime";
11798
+ /** The one sentence that says a source build undoes `runtimeMode: managed`. */
11799
+ function ejectWarning(projectRef) {
11800
+ return {
11801
+ code: EJECTS_MANAGED_RUNTIME,
11802
+ message: `${projectRef} runs on the managed runtime — this build ejects it to a custom container.`,
11803
+ hint: "Use `rebase cloud deploy --bundle` to stay on managed."
11804
+ };
11805
+ }
11806
+ /**
11807
+ * Why a container-image deploy of a managed project is refused — or `undefined`
11808
+ * to let it through.
11809
+ *
11810
+ * Every path below this point builds a container image, and a successful one
11811
+ * sets `runtimeMode: "custom"` server-side. So the question is never "which flag
11812
+ * was used" but "did the caller ask to leave the managed runtime", and only
11813
+ * `--force` answers it.
11814
+ *
11815
+ * `--source` used to be read as answering it too, on the theory that uploading a
11816
+ * build context is self-evidently a deliberate eject. It is not: `--source`
11817
+ * picks *which source* gets built — this directory, rather than the stale
11818
+ * archive the control plane is holding — and the eject is a side effect of the
11819
+ * answer. That is exactly how a live project got flipped to `custom` by someone
11820
+ * whose actual intent was "deploy what I have here", and it is the same
11821
+ * ignorance the bare form is refused for. Same ignorance, same refusal.
11822
+ */
11823
+ function ejectRefusal(opts, projectRef) {
11824
+ if (!opts.managed || opts.force) return void 0;
11825
+ const eject = "To eject on purpose, add `--force`.";
11826
+ if (opts.source) return {
11827
+ message: `${projectRef} runs on the managed runtime, and \`--source\` builds a container image from this directory — which ejects it from managed. Picking a build method is not the same as asking to leave the runtime.`,
11828
+ hint: `Deploy this directory to the managed runtime with \`rebase cloud deploy --bundle\`. ${eject}`,
11829
+ code: "managed_project"
11830
+ };
11831
+ return {
11832
+ message: `${projectRef} runs on the managed runtime, and a plain \`rebase cloud deploy\` builds a container image instead — ejecting it from managed, from source the control plane already holds rather than this directory.`,
11833
+ hint: `Redeploy it with \`rebase cloud deploy --bundle\`. ${eject} \`--source . --force\` builds this directory; \`--force\` alone builds what the control plane holds.`,
11834
+ code: "managed_project"
11835
+ };
11836
+ }
11837
+ /**
11838
+ * Which warnings a container-image deploy has earned.
11839
+ *
11840
+ * Pure, and separate from the printing, because the printing is what went
11841
+ * wrong: the eject warning used to be written inline behind `!isJsonMode()`, so
11842
+ * the fact that a deploy ejects a managed project existed only as a side effect
11843
+ * of a TTY being attached. Deciding here, emitting once at the call site, means
11844
+ * the decision cannot be output-mode-dependent again.
11845
+ *
11846
+ * The condition is just `managed`: anything reaching this point is a container
11847
+ * image build that `ejectRefusal` has already let through, and on a managed
11848
+ * project that is an eject however it was spelled. A caller who passed `--force`
11849
+ * knows — the warning is for the transcript and the payload, which is what
11850
+ * anyone reviewing the deploy afterwards actually reads.
11851
+ */
11852
+ function deployWarnings(opts, projectRef) {
11853
+ return opts.managed ? [ejectWarning(projectRef)] : [];
11854
+ }
11855
+ /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
11856
+ function warningPayload(warnings) {
11857
+ return {
11858
+ warnings: warnings.map((w) => ({
11859
+ code: w.code,
11860
+ message: w.message,
11861
+ hint: w.hint ?? null
11862
+ })),
11863
+ ejectsManagedRuntime: warnings.some((w) => w.code === EJECTS_MANAGED_RUNTIME)
11864
+ };
11865
+ }
11866
+ /**
11867
+ * Read the two rows the preflight needs.
11868
+ *
11869
+ * Best effort by construction: a preflight that cannot read is a preflight that
11870
+ * says nothing, never a deploy that fails. The managed refusal rides on the same
11871
+ * read, so an unreadable project falls through to the old behaviour rather than
11872
+ * blocking a deploy on a lookup.
11873
+ */
11874
+ async function readDeployContext(client, projectId) {
11230
11875
  try {
11231
- const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
11232
- const active = getContextOrg(url);
11233
- emit(() => {
11234
- console.log("");
11235
- console.log(chalk.bold(" 🏢 Organizations"));
11236
- console.log("");
11237
- if (orgs.length === 0) {
11238
- console.log(chalk.gray(" You are not a member of any organization."));
11239
- console.log("");
11240
- return;
11241
- }
11242
- for (const o of orgs) {
11243
- const marker = String(o.id) === active ? chalk.green(" ●") : " ";
11244
- console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
11245
- }
11246
- console.log("");
11247
- note(chalk.gray("● = active organization. Switch with `rebase cloud use <id>`."));
11248
- console.log("");
11249
- }, {
11250
- activeOrg: active ?? null,
11251
- organizations: orgs.map((o) => ({
11252
- id: String(o.id),
11253
- name: o.name ?? null,
11254
- slug: o.slug ?? null,
11255
- active: String(o.id) === active
11256
- }))
11257
- });
11258
- } catch (e) {
11259
- reportError(e, "Failed to list organizations");
11876
+ const [project, latest] = await Promise.all([client.data.collection("projects").findById(projectId), latestDeployment(client, projectId)]);
11877
+ return {
11878
+ project,
11879
+ latest
11880
+ };
11881
+ } catch {
11882
+ return {};
11260
11883
  }
11261
11884
  }
11262
- async function createOrg(rawArgs) {
11263
- const args = arg({
11264
- "--name": String,
11265
- "--slug": String,
11266
- "-n": "--name"
11267
- }, {
11268
- argv: rawArgs.slice(4),
11269
- permissive: true
11885
+ /**
11886
+ * Whether this repository's backend declares the managed runtime.
11887
+ *
11888
+ * Deliberately quiet: a directory that is not a Rebase project, or whose
11889
+ * manifest does not parse, simply does not route this way — `rebase build` is
11890
+ * where a broken manifest gets reported, and a deploy refusing on one before it
11891
+ * has even said what it is doing would be the wrong place to find out.
11892
+ */
11893
+ function declaresManagedRuntime(appName) {
11894
+ try {
11895
+ const projectRoot = findProjectRoot();
11896
+ if (!projectRoot) return false;
11897
+ const manifest = loadManifest(projectRoot).manifest;
11898
+ if (appName) {
11899
+ const app = manifest.apps[appName];
11900
+ if (app?.type === "static") return true;
11901
+ if (app?.type === "backend") return app.runtime === "managed";
11902
+ }
11903
+ const backend = findBackendApp(manifest);
11904
+ if (!backend) return Object.keys(manifest.apps).length > 0;
11905
+ return backend.app.runtime === "managed";
11906
+ } catch {
11907
+ return false;
11908
+ }
11909
+ }
11910
+ /**
11911
+ * Every flag `rebase cloud deploy` accepts.
11912
+ *
11913
+ * Hoisted out of the `parseCloudArgs` call so that one declaration serves three
11914
+ * readers: the parser, `action-help.ts`'s page for this command, and the test
11915
+ * that holds the two to each other. A flag added here with no line in the help
11916
+ * page is a failing test rather than a flag nobody can discover.
11917
+ */
11918
+ var DEPLOY_FLAGS = {
11919
+ "--no-follow": Boolean,
11920
+ "--wait": Boolean,
11921
+ "--timeout": String,
11922
+ "--source": String,
11923
+ "--message": String,
11924
+ "--bundle": Boolean,
11925
+ "--bundle-dir": String,
11926
+ "--skip-type-check": Boolean,
11927
+ "--force": Boolean,
11928
+ "-m": "--message"
11929
+ };
11930
+ /**
11931
+ * `rebase cloud deploy [app]` — its flags, and which app of this repository the
11932
+ * line named.
11933
+ *
11934
+ * Parsed through `parseCloudArgs` rather than `arg` directly, and the reason is
11935
+ * the positional. This command used to parse `rawArgs.slice(2)` permissively
11936
+ * and read `_[0]` as the app name — but `rawArgs` is the WHOLE `process.argv`,
11937
+ * so `_` opens with the command words themselves. `_[0]` was therefore the
11938
+ * literal string `"cloud"` on every run, which then went to `selectDeployApp`
11939
+ * and came back as:
11940
+ *
11941
+ * This repository declares no app named "cloud". It declares: backend, web.
11942
+ *
11943
+ * So the documented `rebase cloud deploy --bundle` failed on every project that
11944
+ * did not happen to declare an app called `cloud`, `rebase cloud deploy web`
11945
+ * could not reach `web`, and the refusal named the user's real apps — reading
11946
+ * as a fault in their `rebase.json` rather than in the CLI's own parse.
11947
+ *
11948
+ * `commandWords` counts from `cloud` itself, so `cloud deploy` is 2, and it is
11949
+ * applied to the PARSED positionals: a flag written before the group no longer
11950
+ * shifts the app name either.
11951
+ */
11952
+ function resolveDeployArgs(rawArgs) {
11953
+ const { flags, positionals } = parseCloudArgs({
11954
+ spec: DEPLOY_FLAGS,
11955
+ rawArgs,
11956
+ commandWords: 2,
11957
+ command: "cloud deploy",
11958
+ maxPositionals: 1
11270
11959
  });
11960
+ return {
11961
+ flags,
11962
+ appName: positionals[0]
11963
+ };
11964
+ }
11965
+ async function deployCommand(rawArgs, projectRef) {
11966
+ const { flags: args, appName } = resolveDeployArgs(rawArgs);
11967
+ if (args["--wait"] && args["--no-follow"]) fail("--wait and --no-follow ask for opposite things.", "`deploy` follows by default — pass neither, or `--no-follow` to return as soon as the build is triggered.", "usage");
11271
11968
  const { client, url } = await requireClient(rawArgs);
11272
- const prompts = [];
11273
- if (!args["--name"]) {
11274
- requireInteractive("an organization name", "--name <name>");
11275
- prompts.push({
11276
- type: "input",
11277
- name: "name",
11278
- message: "Organization name:"
11969
+ const projectId = await resolveProjectRef(projectRef, client);
11970
+ const declaredManaged = !args["--source"] && !args["--bundle"] && declaresManagedRuntime(appName);
11971
+ if (args["--bundle"] || declaredManaged) {
11972
+ if (args["--bundle"] && args["--source"]) fail("--bundle and --source cannot be combined: one is a managed bundle, the other a source build.");
11973
+ if (declaredManaged && !isJsonMode()) console.log(chalk.gray(" rebase.json declares runtime: managed — deploying a bundle."));
11974
+ await deployBundle({
11975
+ client,
11976
+ url,
11977
+ projectId,
11978
+ projectRef,
11979
+ bundleDir: args["--bundle-dir"],
11980
+ message: args["--message"],
11981
+ appName,
11982
+ skipTypeCheck: args["--skip-type-check"] === true,
11983
+ follow: args["--no-follow"] !== true,
11984
+ timeoutMs: resolveDeployTimeout(args["--timeout"])
11279
11985
  });
11986
+ return;
11280
11987
  }
11281
- const answers = prompts.length ? await inquirer.prompt(prompts) : {};
11282
- const name = (args["--name"] || answers.name || "").trim();
11283
- if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
11284
- const slug = (args["--slug"] || slugify(name)).trim();
11285
- try {
11286
- const created = await client.data.collection("organizations").create({
11287
- name,
11288
- slug,
11289
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
11290
- });
11291
- setContextOrg(url, String(created.id));
11292
- success(`Created organization ${chalk.bold(name)} and set it active`);
11293
- emit(() => {}, {
11294
- success: true,
11295
- id: String(created.id),
11296
- name,
11297
- slug,
11298
- setActive: true
11299
- });
11300
- } catch (e) {
11301
- reportError(e, "Failed to create organization");
11988
+ const { project, latest } = await readDeployContext(client, projectId);
11989
+ const plan = planBareDeploy(project, latest, /* @__PURE__ */ new Date());
11990
+ const eject = {
11991
+ managed: plan.managed,
11992
+ source: Boolean(args["--source"]),
11993
+ force: args["--force"] === true
11994
+ };
11995
+ const refusal = ejectRefusal(eject, projectRef);
11996
+ if (refusal) fail(refusal.message, refusal.hint, refusal.code);
11997
+ const warnings = deployWarnings(eject, projectRef);
11998
+ for (const w of warnings) warn(w.message, w.hint);
11999
+ if (!args["--source"] && !isJsonMode()) {
12000
+ console.log("");
12001
+ for (const line of plan.lines) console.log(chalk.gray(` ${line}`));
12002
+ }
12003
+ let source;
12004
+ if (args["--source"]) {
12005
+ const tarPath = await createSourceTarball(args["--source"]);
12006
+ try {
12007
+ const token = client.auth.getSession()?.accessToken;
12008
+ if (!token) fail("Not authenticated.", "Run `rebase cloud login`.");
12009
+ source = await uploadSource(url, token, projectId, tarPath);
12010
+ } finally {
12011
+ fs.rmSync(tarPath, { force: true });
12012
+ }
12013
+ }
12014
+ if (!isJsonMode()) {
12015
+ console.log("");
12016
+ console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
11302
12017
  }
11303
- }
11304
- async function listMembers(rawArgs) {
11305
- const { client, url } = await requireClient(rawArgs);
11306
- const org = getContextOrg(url);
11307
- if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
12018
+ const body = { projectId };
12019
+ if (source) body.source = source;
12020
+ if (args["--message"]) body.message = args["--message"];
12021
+ body.client = "cli";
12022
+ const frameworkVersion = resolveFrameworkVersion(args["--source"] ?? process.cwd());
12023
+ if (frameworkVersion) body.frameworkVersion = frameworkVersion;
12024
+ let triggered;
11308
12025
  try {
11309
- const members = (await client.data.collection("organization-members").find({
11310
- where: { organization: ["==", org] },
11311
- limit: 200
11312
- })).data;
12026
+ const res = await client.functions.invoke("deploy", body);
12027
+ if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
12028
+ triggered = {
12029
+ deploymentId: String(res.deployment.id),
12030
+ deduplicated: res.deduplicated === true
12031
+ };
12032
+ } catch (e) {
12033
+ triggered = resolveTriggerFailure(e);
12034
+ }
12035
+ const { deploymentId, deduplicated } = triggered;
12036
+ if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
12037
+ if (args["--no-follow"]) {
11313
12038
  emit(() => {
11314
- console.log("");
11315
- console.log(chalk.bold(` 👥 Members — org ${org}`));
11316
- console.log("");
11317
- if (members.length === 0) {
11318
- console.log(chalk.gray(" No members found."));
11319
- console.log("");
11320
- return;
11321
- }
11322
- for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
12039
+ console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
11323
12040
  console.log("");
11324
12041
  }, {
11325
- org,
11326
- members: members.map((m) => ({
11327
- id: String(m.id),
11328
- userId: m.userId ?? null,
11329
- role: m.role ?? null
11330
- }))
12042
+ deploymentId,
12043
+ deduplicated,
12044
+ frameworkVersion: frameworkVersion ?? null,
12045
+ following: false,
12046
+ ...warningPayload(warnings)
11331
12047
  });
11332
- } catch (e) {
11333
- reportError(e, "Failed to list members");
12048
+ return;
11334
12049
  }
12050
+ if (!isJsonMode()) {
12051
+ console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
12052
+ console.log("");
12053
+ }
12054
+ const status = await streamBuildLogs(client, deploymentId, {
12055
+ quiet: isJsonMode(),
12056
+ timeoutMs: resolveDeployTimeout(args["--timeout"])
12057
+ });
12058
+ emit(() => {}, {
12059
+ deploymentId,
12060
+ deduplicated,
12061
+ frameworkVersion: frameworkVersion ?? null,
12062
+ following: true,
12063
+ status,
12064
+ ...warningPayload(warnings)
12065
+ });
11335
12066
  }
11336
- function slugify(s) {
11337
- return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11338
- }
11339
- function printOrgsHelp() {
11340
- emitHelp("orgs", [
11341
- "list",
11342
- "create",
11343
- "members"
11344
- ], () => {
11345
- console.log(`
11346
- ${chalk.bold("rebase cloud orgs")} — Manage organizations
11347
-
11348
- ${chalk.green.bold("Commands")}
11349
- ${chalk.blue.bold("list")} List organizations you belong to
11350
- ${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
11351
- ${chalk.blue.bold("members")} List members of the active organization
11352
- `);
12067
+ /** `--timeout <seconds>` for a deploy, or the 15-minute default. */
12068
+ function resolveDeployTimeout(value) {
12069
+ return resolveTimeoutMs(value, {
12070
+ fallbackMs: POLL_TIMEOUT_MS,
12071
+ command: "cloud deploy"
11353
12072
  });
11354
12073
  }
11355
- //#endregion
11356
- //#region src/commands/cloud/databases.ts
11357
12074
  /**
11358
- * `rebase cloud db` database + backup management for a project.
12075
+ * Turn a failed trigger into either a deployment to follow, or an exit.
11359
12076
  *
11360
- * db list List databases attached to the project
11361
- * db create Attach a managed or bring-your-own database
11362
- * db test Test connectivity to the project's database
11363
- * db backup list|create|restore
12077
+ * The 409 is the interesting one. A deploy trigger can reach the control plane
12078
+ * twice without anybody asking twice — the SDK transport replays a request once
12079
+ * after refreshing an expired token, and any lost response has the same effect
12080
+ * so "a deployment is already in progress" was routinely describing the
12081
+ * deployment this very command had just created. With no id in the message the
12082
+ * only available reading was "someone else is deploying, back off", and the
12083
+ * build stream was lost either way.
12084
+ *
12085
+ * So: if the control plane says the blocking deployment is ours, we attach to
12086
+ * it. If it is not ours, we still name it, because "which one, since when, from
12087
+ * where" is the difference between an actionable refusal and a dead end.
11364
12088
  */
11365
- async function dbCommand$1(subcommand, rawArgs) {
11366
- switch (subcommand) {
11367
- case "list":
11368
- case void 0:
11369
- await listDatabases(rawArgs);
11370
- break;
11371
- case "create":
11372
- await createDatabase(rawArgs);
11373
- break;
11374
- case "info":
11375
- await dbInfo(rawArgs);
11376
- break;
11377
- case "test":
11378
- await testDatabase(rawArgs);
11379
- break;
11380
- case "backup":
11381
- await backupCommand(rawArgs);
11382
- break;
11383
- case "pitr":
11384
- await pitrCommand(rawArgs);
11385
- break;
11386
- case "--help":
11387
- printDbHelp();
11388
- break;
11389
- default: fail(`Unknown db command: ${subcommand}`, "Run `rebase cloud db --help`.", "unknown_command");
12089
+ function resolveTriggerFailure(e) {
12090
+ const err = e;
12091
+ if (err?.status === 409) {
12092
+ const blocking = err.details?.deployment;
12093
+ if (blocking?.id && blocking.mine) return {
12094
+ deploymentId: String(blocking.id),
12095
+ deduplicated: true
12096
+ };
12097
+ fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${blocking.triggerSource && blocking.triggerSource !== "unknown" ? `, triggered from the ${blocking.triggerSource}` : ""}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Follow it with \`rebase cloud logs -f\`, or stop it with \`rebase cloud cancel ${blocking.id}\`.` : "Follow it with `rebase cloud logs -f`.", "deploy_in_progress");
11390
12098
  }
12099
+ if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
12100
+ if (err?.status === 400 && err.details?.intakeCode) fail(err.message || "This bundle was refused.", err.details.hint ?? "Run `rebase cloud resources` to see what this project is given.", err.details.intakeCode);
12101
+ reportError(e, "Failed to trigger deployment");
11391
12102
  }
11392
- async function listDatabases(rawArgs) {
11393
- const { client } = await requireClient(rawArgs);
11394
- const projectId = await requireProject(rawArgs, client);
11395
- const projectRef = displayProjectRef(rawArgs);
11396
- try {
11397
- const dbs = (await client.data.collection("databases").find({
11398
- where: { project: ["==", projectId] },
11399
- limit: 50
11400
- })).data;
11401
- emit(() => {
11402
- console.log("");
11403
- console.log(chalk.bold(` 🗄 Databases project ${projectRef}`));
11404
- console.log("");
11405
- if (dbs.length === 0) {
11406
- console.log(chalk.gray(" No database attached. Add one with `rebase cloud db create`."));
12103
+ /**
12104
+ * Poll a deployment record and print new log output as it arrives. Returns the
12105
+ * terminal status; a non-success still exits non-zero, as it always has.
12106
+ *
12107
+ * `quiet` follows without printing — JSON mode, where the log stream would
12108
+ * corrupt the one object the caller is parsing.
12109
+ */
12110
+ async function streamBuildLogs(client, deploymentId, opts = {}) {
12111
+ const quiet = opts.quiet === true;
12112
+ const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
12113
+ let printed = 0;
12114
+ const started = Date.now();
12115
+ for (;;) {
12116
+ let dep;
12117
+ try {
12118
+ dep = await client.data.collection("deployments").findById(deploymentId);
12119
+ } catch (e) {
12120
+ reportError(e, "Failed to read deployment status");
12121
+ }
12122
+ if (!dep) fail(`Deployment ${deploymentId} disappeared.`, void 0, "not_found");
12123
+ const logs = dep.logs ?? "";
12124
+ if (!quiet && logs.length > printed) process.stdout.write(logs.slice(printed));
12125
+ printed = logs.length;
12126
+ if (dep.status && dep.status !== "deploying") {
12127
+ if (dep.status !== "success") {
12128
+ if (quiet) {
12129
+ printJson({ error: {
12130
+ message: `Deployment ${deploymentId} ${dep.status}.`,
12131
+ code: "deploy_failed",
12132
+ status: null,
12133
+ deploymentId,
12134
+ logs
12135
+ } });
12136
+ process.exit(1);
12137
+ }
11407
12138
  console.log("");
11408
- return;
11409
- }
11410
- for (const d of dbs) {
11411
- console.log(` ${chalk.bold(d.type ?? "unknown")} ${chalk.gray(`[${d.id}]`)} ${colorStatus(d.connectionStatus)}`);
11412
- keyValues([["SSH tunnel", d.useSshTunnel ? "yes" : void 0], ["PITR", d.pitrEnabled ? "enabled" : void 0]]);
12139
+ console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
12140
+ console.log("");
12141
+ process.exit(1);
11413
12142
  }
11414
- console.log("");
11415
- }, {
11416
- projectId,
11417
- databases: dbs.map((d) => ({
11418
- id: String(d.id),
11419
- type: d.type ?? null,
11420
- connectionStatus: d.connectionStatus ?? null,
11421
- useSshTunnel: Boolean(d.useSshTunnel),
11422
- pitrEnabled: Boolean(d.pitrEnabled)
11423
- }))
11424
- });
11425
- } catch (e) {
11426
- reportError(e, "Failed to list databases");
11427
- }
11428
- }
11429
- async function createDatabase(rawArgs) {
11430
- const args = arg({
11431
- "--type": String,
11432
- "--connection-string": String,
11433
- "--project": String,
11434
- "-p": "--project"
11435
- }, {
11436
- argv: rawArgs.slice(4),
11437
- permissive: true
11438
- });
11439
- const { client } = await requireClient(rawArgs);
11440
- const projectId = await requireProject(rawArgs, client);
11441
- const projectRef = displayProjectRef(rawArgs);
11442
- let type = args["--type"];
11443
- if (!type) {
11444
- requireInteractive("a database type", "--type <managed|byodb>");
11445
- const { picked } = await inquirer.prompt([{
11446
- type: "select",
11447
- name: "picked",
11448
- message: "Database type:",
11449
- choices: [{
11450
- name: "SaaS Managed (provisioned for you)",
11451
- value: "managed"
11452
- }, {
11453
- name: "Bring Your Own DB (external PostgreSQL)",
11454
- value: "byodb"
11455
- }]
11456
- }]);
11457
- type = picked;
11458
- }
11459
- let connectionString = args["--connection-string"];
11460
- if (type === "byodb" && !connectionString) {
11461
- requireInteractive("a connection string", "--connection-string <url>");
11462
- const { cs } = await inquirer.prompt([{
11463
- type: "input",
11464
- name: "cs",
11465
- message: "PostgreSQL connection string:"
11466
- }]);
11467
- connectionString = cs?.trim();
11468
- if (!connectionString) fail("A connection string is required for bring-your-own databases.", "Pass `--connection-string <url>`.", "input_required");
11469
- }
11470
- try {
11471
- const created = await client.data.collection("databases").create({
11472
- project: projectId,
11473
- type,
11474
- connectionString: type === "byodb" ? connectionString : void 0,
11475
- connectionStatus: "untested"
11476
- });
11477
- success(`Attached ${type} database to project ${projectRef}`);
11478
- emit(() => {
11479
- keyValues([["ID", String(created.id)]]);
11480
- if (type === "byodb") {
11481
- note(chalk.gray("Verify it with `rebase cloud db test`."));
11482
- noteBlank();
12143
+ if (!quiet) {
12144
+ console.log("");
12145
+ console.log(chalk.bold.green(" ✓ Deployment succeeded"));
12146
+ console.log("");
11483
12147
  }
11484
- }, {
11485
- success: true,
11486
- id: String(created.id),
11487
- projectId,
11488
- type,
11489
- connectionStatus: "untested"
11490
- });
11491
- } catch (e) {
11492
- reportError(e, "Failed to attach database");
11493
- }
11494
- }
11495
- async function testDatabase(rawArgs) {
11496
- const { client } = await requireClient(rawArgs);
11497
- const projectId = await requireProject(rawArgs, client);
11498
- const projectRef = displayProjectRef(rawArgs);
11499
- noteBlank();
11500
- note(`Testing database connectivity for project ${chalk.bold(projectRef)}...`);
11501
- try {
11502
- const res = await client.functions.invoke("db-test", { projectId });
11503
- if (res.logs) console.error(`\n${res.logs}`);
11504
- if (!res.success) fail("Database connection failed.", "The connection log above (stderr) has the reason.", "db_connection_failed");
11505
- success("Database connection succeeded");
11506
- emit(() => {}, {
11507
- success: true,
11508
- projectId,
11509
- logs: res.logs ?? null
11510
- });
11511
- } catch (e) {
11512
- reportError(e, "Failed to test database");
12148
+ return dep.status;
12149
+ }
12150
+ if (Date.now() - started > timeoutMs) {
12151
+ if (!quiet) console.log("");
12152
+ fail(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the build to finish.`, "The deployment may still be running — check `rebase cloud logs`.", "timeout");
12153
+ }
12154
+ await sleep(POLL_INTERVAL_MS);
11513
12155
  }
11514
12156
  }
11515
- /**
11516
- * `rebase cloud db info [--reveal]` — where a project's database actually lives.
11517
- *
11518
- * The password is NEVER in the default output; `--reveal` fetches it through the
11519
- * separate reveal call, and it appears in JSON only when `--reveal` is given.
11520
- * Any field the server could not resolve comes back `null` and is rendered as
11521
- * unavailable, never a placeholder.
11522
- */
11523
- async function dbInfo(rawArgs) {
12157
+ async function logsCommand(rawArgs, projectRef) {
11524
12158
  const args = arg({
11525
- "--reveal": Boolean,
11526
- "--project": String,
11527
- "-p": "--project"
12159
+ "--runtime": Boolean,
12160
+ "--follow": Boolean,
12161
+ "-f": "--follow"
11528
12162
  }, {
11529
12163
  argv: rawArgs.slice(2),
11530
12164
  permissive: true
11531
12165
  });
11532
12166
  const { client } = await requireClient(rawArgs);
11533
- const projectId = await requireProject(rawArgs, client);
11534
- const projectRef = displayProjectRef(rawArgs);
11535
- try {
11536
- const info = await client.functions.invoke("db-info", void 0, {
11537
- method: "GET",
11538
- path: projectId
11539
- });
11540
- let password;
11541
- let connectionString;
11542
- if (args["--reveal"]) {
11543
- if (!info.passwordAvailable) fail("No password is available to reveal for this database.", info.unavailableReason ?? void 0, "password_unavailable");
11544
- const revealed = await client.functions.invoke("db-info", { projectId }, { path: "reveal" });
11545
- password = revealed.password;
11546
- connectionString = revealed.connectionString;
12167
+ const projectId = await resolveProjectRef(projectRef, client);
12168
+ if (args["--runtime"]) {
12169
+ try {
12170
+ const res = await client.functions.invoke("runtime-logs", void 0, {
12171
+ method: "GET",
12172
+ path: projectId
12173
+ });
12174
+ console.log("");
12175
+ console.log(chalk.bold(` 📄 Runtime logs — project ${projectRef}`));
12176
+ console.log("");
12177
+ console.log(res.logs ?? chalk.gray(" (no logs)"));
12178
+ console.log("");
12179
+ } catch (e) {
12180
+ reportError(e, "Failed to fetch runtime logs");
11547
12181
  }
11548
- emit(() => {
12182
+ return;
12183
+ }
12184
+ try {
12185
+ const dep = await latestDeployment(client, projectId);
12186
+ if (!dep) {
11549
12187
  console.log("");
11550
- console.log(chalk.bold(` 🗄 Database project ${projectRef}`) + chalk.gray(` (${info.type})`));
12188
+ console.log(chalk.gray(" No deployments yet for this project."));
11551
12189
  console.log("");
11552
- keyValues([
11553
- ["Host", info.host],
11554
- ["Port", info.port],
11555
- ["Database", info.database],
11556
- ["Username", info.username],
11557
- ["Password", info.passwordAvailable ? password ?? chalk.gray("hidden pass --reveal") : chalk.gray("unavailable")],
11558
- ["Connection", connectionString]
11559
- ]);
11560
- if (info.unavailableReason) console.log(chalk.gray(` ${info.unavailableReason}`));
11561
- if (info.portForward) {
11562
- const pf = info.portForward;
11563
- console.log("");
11564
- console.log(chalk.gray(` Port-forward: kubectl -n ${pf.namespace} port-forward svc/${pf.service} ${pf.localPort}:${pf.remotePort}`));
11565
- }
12190
+ return;
12191
+ }
12192
+ console.log("");
12193
+ console.log(chalk.bold(` 📄 Build logs — deployment ${dep.id}`) + ` ${colorStatus(dep.status)}`);
12194
+ console.log("");
12195
+ if (args["--follow"] && dep.status === "deploying") await streamBuildLogs(client, String(dep.id));
12196
+ else {
12197
+ console.log(dep.logs ?? chalk.gray(" (no logs)"));
11566
12198
  console.log("");
11567
- }, {
11568
- projectId,
11569
- type: info.type,
11570
- host: info.host,
11571
- port: info.port,
11572
- database: info.database,
11573
- username: info.username,
11574
- passwordAvailable: info.passwordAvailable,
11575
- portForward: info.portForward,
11576
- unavailableReason: info.unavailableReason,
11577
- ...args["--reveal"] ? {
11578
- password,
11579
- connectionString
11580
- } : {}
11581
- });
12199
+ }
11582
12200
  } catch (e) {
11583
- reportError(e, "Failed to load database info");
12201
+ reportError(e, "Failed to fetch build logs");
11584
12202
  }
11585
12203
  }
12204
+ //#endregion
12205
+ //#region src/commands/cloud/orgs.ts
11586
12206
  /**
11587
- * `db backup [action] [filename]`, resolved in one strict parse.
11588
- *
11589
- * Both halves were reachable by the old operand filter, and both are
11590
- * destructive: `rebase cloud db backup -p acme` read `--project`'s value as the
11591
- * ACTION (falling through to a list, so the flag silently changed what ran),
11592
- * and `db backup restore -p acme` read it as the FILENAME — a restore staged
11593
- * over the live database, named after the project slug. An undeclared flag was
11594
- * dropped instead of refused, which is the same failure one step quieter: `db
11595
- * backup --dry-run` ran a list, having silently discarded the flag that was
11596
- * supposed to change what it did.
11597
- *
11598
- * Exported so its tests drive the real parser.
12207
+ * `rebase cloud orgs` list / create / members.
11599
12208
  */
11600
- function resolveBackupArgs(rawArgs) {
11601
- const { flags, positionals } = parseCloudArgs({
11602
- spec: { "--yes": Boolean },
11603
- rawArgs,
11604
- commandWords: 3,
11605
- command: "cloud db backup",
11606
- maxPositionals: 2
11607
- });
11608
- return {
11609
- flags,
11610
- action: positionals[0] || "list",
11611
- filename: positionals[1]
11612
- };
12209
+ async function orgsCommand(subcommand, rawArgs) {
12210
+ switch (subcommand) {
12211
+ case "list":
12212
+ case void 0:
12213
+ await listOrgs(rawArgs);
12214
+ break;
12215
+ case "create":
12216
+ await createOrg(rawArgs);
12217
+ break;
12218
+ case "members":
12219
+ await listMembers(rawArgs);
12220
+ break;
12221
+ case "--help":
12222
+ printOrgsHelp();
12223
+ break;
12224
+ default: fail(`Unknown orgs command: ${subcommand}`, "Run `rebase cloud orgs --help`.", "unknown_command");
12225
+ }
11613
12226
  }
11614
- async function backupCommand(rawArgs) {
11615
- const { flags: args, action, filename: backupFile } = resolveBackupArgs(rawArgs);
11616
- const { client } = await requireClient(rawArgs);
11617
- const projectId = await requireProject(rawArgs, client);
11618
- const projectRef = displayProjectRef(rawArgs);
12227
+ async function listOrgs(rawArgs) {
12228
+ const { client, url } = await requireClient(rawArgs);
11619
12229
  try {
11620
- if (action === "create") {
11621
- const res = await client.functions.invoke("backup", {
11622
- projectId,
11623
- type: "manual"
11624
- }, { path: "create" });
11625
- if (!res.success) fail(res.error || "Backup failed.");
11626
- emit(() => success(`Backup created: ${res.backup?.filename ?? "(unknown)"}`), {
11627
- success: true,
11628
- backup: res.backup ?? null
11629
- });
11630
- return;
11631
- }
11632
- if (action === "restore") {
11633
- const filename = backupFile;
11634
- if (!filename) fail("Usage: rebase cloud db backup restore <filename>", void 0, "usage");
11635
- await confirmDestructive({
11636
- yes: Boolean(args["--yes"]),
11637
- prompt: `Restore "${filename}" over the current database for project ${projectRef}?`
11638
- });
11639
- const res = await client.functions.invoke("backup", {
11640
- projectId,
11641
- filename
11642
- }, { path: "restore" });
11643
- if (!res.success) fail(res.error || "Restore failed.");
11644
- emit(() => success(res.message || "Restore complete"), {
11645
- success: true,
11646
- message: res.message ?? null
11647
- });
11648
- return;
11649
- }
11650
- if (action === "status") {
11651
- const res = await client.functions.invoke("backup", void 0, {
11652
- method: "GET",
11653
- path: `backup-status/${projectId}`
11654
- });
11655
- emit(() => {
11656
- console.log("");
11657
- console.log(chalk.bold(` 💾 Automated backups — project ${projectRef}`));
11658
- console.log("");
11659
- keyValues([
11660
- ["Enabled", res.enabled ? chalk.green("yes") : chalk.yellow("no")],
11661
- ["Reason", String(res.reason ?? "")],
11662
- ["Database type", String(res.databaseType ?? "")],
11663
- ["Last backup", res.lastSuccessfulBackup ?? void 0],
11664
- ["Recovery window", res.recoveryWindow ? `${res.recoveryWindow.from} → ${res.recoveryWindow.to}` : void 0]
11665
- ]);
11666
- console.log("");
11667
- }, res);
11668
- return;
11669
- }
11670
- if (action === "download") {
11671
- const filename = backupFile;
11672
- if (!filename) fail("Usage: rebase cloud db backup download <filename>", void 0, "usage");
11673
- const res = await client.functions.invoke("backup", void 0, {
11674
- method: "GET",
11675
- path: `download/${projectId}/${encodeURIComponent(filename)}`
11676
- });
11677
- emit(() => {
11678
- console.log("");
11679
- console.log(chalk.bold(` ${res.name}`) + chalk.gray(` ${(res.size / 1024 / 1024).toFixed(1)} MB`));
11680
- console.log(` ${chalk.cyan(res.url)}`);
11681
- console.log("");
11682
- console.log(chalk.gray(" Short-lived signed URL — fetch it with curl/wget."));
11683
- console.log("");
11684
- }, {
11685
- name: res.name,
11686
- size: res.size,
11687
- url: res.url
11688
- });
11689
- return;
11690
- }
11691
- const res = await client.functions.invoke("backup", void 0, {
11692
- method: "GET",
11693
- path: `list/${projectId}`
11694
- });
12230
+ const orgs = (await client.data.collection("organizations").find({ limit: 100 })).data;
12231
+ const active = getContextOrg(url);
11695
12232
  emit(() => {
11696
12233
  console.log("");
11697
- console.log(chalk.bold(` 💾 Backups — project ${projectRef}`));
12234
+ console.log(chalk.bold(" 🏢 Organizations"));
11698
12235
  console.log("");
11699
- if (!res.backups?.length) {
11700
- console.log(chalk.gray(" No backups yet. Create one with `rebase cloud db backup create`."));
12236
+ if (orgs.length === 0) {
12237
+ console.log(chalk.gray(" You are not a member of any organization."));
11701
12238
  console.log("");
11702
12239
  return;
11703
12240
  }
11704
- for (const b of res.backups) {
11705
- const size = b.size !== void 0 ? `${(b.size / 1024 / 1024).toFixed(1)} MB` : "";
11706
- console.log(` ${chalk.bold(b.filename)} ${chalk.gray(`${b.type ?? ""} ${size}`.trim())}`);
12241
+ for (const o of orgs) {
12242
+ const marker = String(o.id) === active ? chalk.green(" ●") : " ";
12243
+ console.log(`${marker}${chalk.bold(o.name ?? "(unnamed)")} ${chalk.gray(`[${o.id}]`)}${o.slug ? chalk.gray(` ${o.slug}`) : ""}`);
11707
12244
  }
11708
12245
  console.log("");
12246
+ note(chalk.gray("● = active organization. Switch with `rebase cloud use <id>`."));
12247
+ console.log("");
11709
12248
  }, {
11710
- projectId,
11711
- backups: res.backups ?? []
12249
+ activeOrg: active ?? null,
12250
+ organizations: orgs.map((o) => ({
12251
+ id: String(o.id),
12252
+ name: o.name ?? null,
12253
+ slug: o.slug ?? null,
12254
+ active: String(o.id) === active
12255
+ }))
11712
12256
  });
11713
12257
  } catch (e) {
11714
- reportError(e, "Backup operation failed");
12258
+ reportError(e, "Failed to list organizations");
11715
12259
  }
11716
12260
  }
11717
- /**
11718
- * `rebase cloud db pitr <status|restore|cutover|discard>`.
11719
- *
11720
- * A PITR restore is STAGED, not applied: `restore` creates a recovered copy of
11721
- * the database beside the live one — the application is NOT repointed and the
11722
- * original is left running and unchanged. `cutover` is the separate, explicit
11723
- * step that repoints the app at the recovered copy (and restarts it). `discard`
11724
- * removes a staged copy; the server refuses to discard a copy that has been cut
11725
- * over to (it is now the live database). Every mutating step requires `--yes` in
11726
- * non-interactive use, and the CLI surfaces these staged semantics honestly.
11727
- */
11728
- async function pitrCommand(rawArgs) {
11729
- const { flags: args, positionals } = parseCloudArgs({
11730
- spec: {
11731
- "--target": String,
11732
- "--yes": Boolean
11733
- },
11734
- rawArgs,
11735
- commandWords: 3,
11736
- command: "cloud db pitr",
11737
- maxPositionals: 1
12261
+ async function createOrg(rawArgs) {
12262
+ const args = arg({
12263
+ "--name": String,
12264
+ "--slug": String,
12265
+ "-n": "--name"
12266
+ }, {
12267
+ argv: rawArgs.slice(4),
12268
+ permissive: true
11738
12269
  });
11739
- const action = positionals[0] || "status";
11740
- const { client } = await requireClient(rawArgs);
11741
- const projectId = await requireProject(rawArgs, client);
11742
- const projectRef = displayProjectRef(rawArgs);
12270
+ const { client, url } = await requireClient(rawArgs);
12271
+ const prompts = [];
12272
+ if (!args["--name"]) {
12273
+ requireInteractive("an organization name", "--name <name>");
12274
+ prompts.push({
12275
+ type: "input",
12276
+ name: "name",
12277
+ message: "Organization name:"
12278
+ });
12279
+ }
12280
+ const answers = prompts.length ? await inquirer.prompt(prompts) : {};
12281
+ const name = (args["--name"] || answers.name || "").trim();
12282
+ if (!name) fail("Organization name is required.", "Pass `--name <name>`.", "input_required");
12283
+ const slug = (args["--slug"] || slugify(name)).trim();
11743
12284
  try {
11744
- if (action === "status") {
11745
- const res = await client.functions.invoke("backup", void 0, {
11746
- method: "GET",
11747
- path: `pitr-status/${projectId}`
11748
- });
11749
- emit(() => {
11750
- console.log("");
11751
- console.log(chalk.bold(` ⏱ Point-in-time recovery project ${projectRef}`));
11752
- console.log("");
11753
- keyValues([
11754
- ["Available", res.available ? chalk.green("yes") : chalk.yellow("no")],
11755
- ["First recoverable", res.firstRecoverabilityPoint ?? void 0],
11756
- ["Last backup", res.lastSuccessfulBackup ?? void 0],
11757
- ["Message", res.message ?? void 0]
11758
- ]);
11759
- console.log("");
11760
- }, res);
11761
- return;
11762
- }
11763
- if (action === "restore") {
11764
- const target = args["--target"];
11765
- if (!target) fail("Usage: rebase cloud db pitr restore --target <ISO timestamp>", void 0, "usage");
11766
- await confirmDestructive({
11767
- yes: Boolean(args["--yes"]),
11768
- prompt: `Stage a point-in-time recovery of project ${projectRef} at ${target}? (stages a copy; does not repoint your app)`
11769
- });
11770
- const res = await client.functions.invoke("backup", {
11771
- projectId,
11772
- targetTime: target,
11773
- acknowledgeNoCutover: true
11774
- }, { path: "pitr-restore" });
11775
- emit(() => {
11776
- console.log("");
11777
- console.log(chalk.yellow(` ⏳ ${String(res.message ?? "Recovery staged.")}`));
11778
- console.log(chalk.gray(" Watch progress with `rebase cloud db pitr status`, then `rebase cloud db pitr cutover --yes`."));
11779
- console.log("");
11780
- }, res);
11781
- return;
11782
- }
11783
- if (action === "cutover") {
11784
- await confirmDestructive({
11785
- yes: Boolean(args["--yes"]),
11786
- prompt: `Cut project ${projectRef} over to the staged recovery? This repoints and restarts your application.`
11787
- });
11788
- const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-cutover" });
11789
- emit(() => {
11790
- console.log("");
11791
- console.log(String(res.message ?? "Cutover requested."));
12285
+ const created = await client.data.collection("organizations").create({
12286
+ name,
12287
+ slug,
12288
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12289
+ });
12290
+ setContextOrg(url, String(created.id));
12291
+ success(`Created organization ${chalk.bold(name)} and set it active`);
12292
+ emit(() => {}, {
12293
+ success: true,
12294
+ id: String(created.id),
12295
+ name,
12296
+ slug,
12297
+ setActive: true
12298
+ });
12299
+ } catch (e) {
12300
+ reportError(e, "Failed to create organization");
12301
+ }
12302
+ }
12303
+ async function listMembers(rawArgs) {
12304
+ const { client, url } = await requireClient(rawArgs);
12305
+ const org = getContextOrg(url);
12306
+ if (!org) fail("No active organization.", "Run `rebase cloud use` first.", "no_org");
12307
+ try {
12308
+ const members = (await client.data.collection("organization-members").find({
12309
+ where: { organization: ["==", org] },
12310
+ limit: 200
12311
+ })).data;
12312
+ emit(() => {
12313
+ console.log("");
12314
+ console.log(chalk.bold(` 👥 Members — org ${org}`));
12315
+ console.log("");
12316
+ if (members.length === 0) {
12317
+ console.log(chalk.gray(" No members found."));
11792
12318
  console.log("");
11793
- }, res);
11794
- return;
11795
- }
11796
- if (action === "discard") {
11797
- await confirmDestructive({
11798
- yes: Boolean(args["--yes"]),
11799
- prompt: `Discard the staged recovery for project ${projectRef}? This deletes the staged copy and its storage.`
11800
- });
11801
- const res = await client.functions.invoke("backup", { projectId }, { path: "pitr-restore-discard" });
11802
- emit(() => success(String(res.message ?? "Staged restore discarded.")), res);
11803
- return;
11804
- }
11805
- fail(`Unknown pitr command: ${action}`, "Try status | restore | cutover | discard.", "usage");
12319
+ return;
12320
+ }
12321
+ for (const m of members) console.log(` ${chalk.bold(m.userId ?? "?")} ${colorStatus(m.role)}`);
12322
+ console.log("");
12323
+ }, {
12324
+ org,
12325
+ members: members.map((m) => ({
12326
+ id: String(m.id),
12327
+ userId: m.userId ?? null,
12328
+ role: m.role ?? null
12329
+ }))
12330
+ });
11806
12331
  } catch (e) {
11807
- reportError(e, "PITR operation failed");
12332
+ reportError(e, "Failed to list members");
11808
12333
  }
11809
12334
  }
11810
- function printDbHelp() {
11811
- emitHelp("db", [
12335
+ function slugify(s) {
12336
+ return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
12337
+ }
12338
+ function printOrgsHelp() {
12339
+ emitHelp("orgs", [
11812
12340
  "list",
11813
12341
  "create",
11814
- "info",
11815
- "test",
11816
- "backup",
11817
- "pitr"
12342
+ "members"
11818
12343
  ], () => {
11819
12344
  console.log(`
11820
- ${chalk.bold("rebase cloud db")} — Database & backups
12345
+ ${chalk.bold("rebase cloud orgs")} — Manage organizations
11821
12346
 
11822
12347
  ${chalk.green.bold("Commands")}
11823
- ${chalk.blue.bold("list")} List databases attached to the project
11824
- ${chalk.blue.bold("create")} Attach a managed or bring-your-own database
11825
- ${chalk.blue.bold("info")} ${chalk.gray("[--reveal]")} Connection details ${chalk.gray("(password only with --reveal)")}
11826
- ${chalk.blue.bold("test")} Test database connectivity
11827
- ${chalk.blue.bold("backup list")} List backups
11828
- ${chalk.blue.bold("backup create")} Create a manual backup
11829
- ${chalk.blue.bold("backup restore")} ${chalk.gray("<file>")} Restore a backup
11830
- ${chalk.blue.bold("backup status")} Automated-backup health
11831
- ${chalk.blue.bold("backup download")} ${chalk.gray("<file>")} Signed URL for a backup
11832
- ${chalk.blue.bold("pitr status")} Point-in-time recovery window
11833
- ${chalk.blue.bold("pitr restore")} ${chalk.gray("--target <ISO>")} Stage a recovery ${chalk.gray("(does not repoint)")}
11834
- ${chalk.blue.bold("pitr cutover")} ${chalk.gray("-y")} Repoint the app at the staged recovery
11835
- ${chalk.blue.bold("pitr discard")} ${chalk.gray("-y")} Delete a staged recovery
11836
-
11837
- ${chalk.green.bold("Options")}
11838
- ${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
11839
- ${chalk.blue("--reveal")} Include the DB password ${chalk.gray("(info)")}
11840
- ${chalk.blue("--type")} managed | byodb ${chalk.gray("(create)")}
11841
- ${chalk.blue("--connection-string")} External DB URL ${chalk.gray("(byodb)")}
11842
- ${chalk.blue("--json")} Machine-readable output
12348
+ ${chalk.blue.bold("list")} List organizations you belong to
12349
+ ${chalk.blue.bold("create")} Create a new organization ${chalk.gray("(--name, --slug)")}
12350
+ ${chalk.blue.bold("members")} List members of the active organization
11843
12351
  `);
11844
12352
  });
11845
12353
  }
@@ -13832,6 +14340,208 @@ ${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase
13832
14340
  });
13833
14341
  }
13834
14342
  //#endregion
14343
+ //#region src/commands/cloud/action-help.ts
14344
+ /**
14345
+ * Per-action help for `rebase cloud`.
14346
+ *
14347
+ * `--help` was already answered centrally, but only per *group*: `rebase cloud
14348
+ * projects create --help` and `rebase cloud deploy --help` both fell through to
14349
+ * the index page, which lists groups and no flags at all. So the flags that
14350
+ * decide what those commands do — `--name`, `--subdomain`, `--type`, `--bundle`
14351
+ * — had no discoverable spelling anywhere. The way they were found on a real
14352
+ * first deploy was by reading `dist/index.es.js.map`. An agent cannot do that,
14353
+ * and neither should a person.
14354
+ *
14355
+ * Two rules hold this file honest:
14356
+ *
14357
+ * 1. **The spec is the source of truth, not this page.** `action-help.test.ts`
14358
+ * pairs every entry with the `arg` spec its command actually parses and
14359
+ * asserts they agree in both directions. A flag added to a command without
14360
+ * a line here is a failing test, not a page that is quietly a year old.
14361
+ * The pairing lives in the test rather than here on purpose: importing the
14362
+ * command modules to describe them would make `--help` depend on the code
14363
+ * it is meant to be readable without.
14364
+ * 2. **It answers in the reader's language.** Like every other page in this
14365
+ * family it goes through `emitHelp`, so a piped `--help` is a structured
14366
+ * description of the command rather than sixty lines of ANSI to scrape.
14367
+ */
14368
+ /**
14369
+ * Flags every cloud command accepts, documented once.
14370
+ *
14371
+ * Excluded from the spec comparison below — they are merged in by
14372
+ * `parseCloudArgs` for every command in the family, so repeating them per entry
14373
+ * would be nine copies of the same four lines.
14374
+ */
14375
+ var GLOBAL_HELP_FLAGS = [
14376
+ ["--project, -p <slug>", "Operate on a project without linking this directory"],
14377
+ ["--json", "Machine-readable output (also when piped, or REBASE_JSON=1)"],
14378
+ ["--url <origin>", "Target a specific control plane (or REBASE_CLOUD_URL)"],
14379
+ ["--yes, -y", "Skip confirmation prompts"],
14380
+ ["--debug", "Print the untouched error body after a failure"]
14381
+ ];
14382
+ var ACTION_HELP = {
14383
+ projects: {
14384
+ command: "cloud projects",
14385
+ usage: "cloud projects <list|create|info|delete> [options]",
14386
+ summary: "List, create, inspect and delete projects in the selected organization.",
14387
+ flags: [],
14388
+ examples: [
14389
+ "rebase cloud projects list",
14390
+ "rebase cloud projects create --help",
14391
+ "rebase cloud projects info shop",
14392
+ "rebase cloud projects delete shop --yes"
14393
+ ]
14394
+ },
14395
+ clusters: {
14396
+ command: "cloud clusters",
14397
+ usage: "cloud clusters [list|add|verify] [options]",
14398
+ summary: "The compute clusters tenants run on. Platform-admin only.",
14399
+ flags: [],
14400
+ examples: [
14401
+ "rebase cloud clusters",
14402
+ "rebase cloud clusters verify <cluster-id> --baseline",
14403
+ "rebase cloud clusters add --name gke-eu --provider gcp --region europe-west1 --kubeconfig ./kubeconfig"
14404
+ ]
14405
+ },
14406
+ "projects create": {
14407
+ command: "cloud projects create",
14408
+ usage: "cloud projects create --name <name> --subdomain <slug> [options]",
14409
+ summary: "Create a project. Unless --db says otherwise a managed database is attached in the same call, because a project without one can never deploy — it sits at status \"provisioning\" indefinitely, and that word does not mean work is underway.",
14410
+ flags: [
14411
+ ["--name, -n <name>", "Display name. Required (prompted only on a terminal)"],
14412
+ ["--subdomain <slug>", "The <slug>.rebase.website host. Required, and not editable afterwards"],
14413
+ ["--db <managed|byodb|none>", "Which database to attach. Default: managed"],
14414
+ ["--connection-string <url>", "The PostgreSQL URL, for --db byodb"],
14415
+ ["--org <id>", "Organization to create it in (default: the selected one)"],
14416
+ ["--link", "Link this directory to the new project"],
14417
+ ["--repo <url>", "Git repository to build from"],
14418
+ ["--branch <name>", "Git branch. Default: main"],
14419
+ ["--provider <gcp|hetzner|…>", "Where to run it (default: the platform's target)"],
14420
+ ["--region <region>", "Region within the provider"],
14421
+ ["--cpu <n>", "vCPU per instance"],
14422
+ ["--memory <size>", "Memory per instance, e.g. 512Mi"],
14423
+ ["--replicas <n>", "Instance count"],
14424
+ ["--spot <true|false>", "Run on preemptible capacity"],
14425
+ ["--scale-to-zero <true|false>", "Stop the instances when idle"],
14426
+ ["--db-mode <mode>", "Database topology dial"],
14427
+ ["--db-instances <n>", "Database instance count"],
14428
+ ["--db-cpu <n>", "vCPU per database instance"],
14429
+ ["--db-memory <size>", "Memory per database instance"],
14430
+ ["--storage <size>", "Database volume size"]
14431
+ ],
14432
+ examples: [
14433
+ "rebase cloud projects create --name \"Shop\" --subdomain shop --link",
14434
+ "rebase cloud projects create --name Shop --subdomain shop --db none",
14435
+ "rebase cloud projects create --name Shop --subdomain shop --db byodb --connection-string \"$DATABASE_URL\""
14436
+ ],
14437
+ notes: ["The subdomain cannot be changed in passing later — a typo here is a new project.", "A managed database is created at the project's FIRST DEPLOY, not here."]
14438
+ },
14439
+ "db create": {
14440
+ command: "cloud db create",
14441
+ usage: "cloud db create --type <managed|byodb> [options]",
14442
+ summary: "Attach a database to the project. Required before the first deploy: a project with no database stays at status \"provisioning\" forever, waiting for this command. `rebase cloud projects create` now does it for you unless you passed --db none.",
14443
+ flags: [
14444
+ ["--type <managed|byodb>", "Platform-provisioned Postgres, or your own"],
14445
+ ["--connection-string <url>", "The PostgreSQL URL, for --type byodb"],
14446
+ ["--wait", "Wait for the database to answer (byodb only — see below)"],
14447
+ ["--timeout <seconds>", "Ceiling on --wait. Default: 300"]
14448
+ ],
14449
+ examples: ["rebase cloud db create --type managed", "rebase cloud db create --type byodb --connection-string \"$DATABASE_URL\" --wait"],
14450
+ notes: [
14451
+ "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.",
14452
+ "`rebase cloud db test` legitimately fails before the first deploy.",
14453
+ "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."
14454
+ ]
14455
+ },
14456
+ deploy: {
14457
+ command: "cloud deploy",
14458
+ usage: "cloud deploy [app] [options]",
14459
+ summary: "Deploy an app of the linked project and follow the build to a terminal state, exiting non-zero if it failed. With no app named, the repository's backend is deployed.",
14460
+ flags: [
14461
+ ["--wait", "Follow to a terminal state. Already the default; here to be explicit"],
14462
+ ["--timeout <seconds>", "Ceiling on the follow. Default: 900"],
14463
+ ["--no-follow", "Return as soon as the build is triggered"],
14464
+ ["--message, -m <text>", "Label the release"],
14465
+ ["--bundle", "Force a managed-bundle deploy (the default for runtime: managed)"],
14466
+ ["--bundle-dir <path>", "Deploy a bundle that is already built"],
14467
+ ["--source <path>", "Upload this directory and build a container image from it"],
14468
+ ["--skip-type-check", "Compile without type checking, as `rebase build` does"],
14469
+ ["--force", "Leave the managed runtime on purpose (ejects to a container image)"]
14470
+ ],
14471
+ examples: [
14472
+ "rebase cloud deploy",
14473
+ "rebase cloud deploy web --message \"add search\"",
14474
+ "rebase cloud deploy --timeout 300 --json"
14475
+ ],
14476
+ notes: ["A project whose rebase.json declares runtime: managed deploys a bundle without --bundle.", "--source on a managed project is refused: it would swap the project onto a container image."]
14477
+ },
14478
+ logs: {
14479
+ command: "cloud logs",
14480
+ usage: "cloud logs [--runtime] [--follow]",
14481
+ summary: "The latest build log, or the running container's log with --runtime.",
14482
+ flags: [["--runtime", "The running app's log instead of the build log"], ["--follow, -f", "Tail a build that is still running"]],
14483
+ examples: [
14484
+ "rebase cloud logs",
14485
+ "rebase cloud logs --runtime",
14486
+ "rebase cloud logs -f"
14487
+ ]
14488
+ },
14489
+ status: {
14490
+ command: "cloud status",
14491
+ usage: "cloud status",
14492
+ summary: "One-glance project status: URL, last deploy, runtime, database, storage — and blockedOn/nextAction, which say whether the platform is working or waiting for you.",
14493
+ flags: [],
14494
+ examples: ["rebase cloud status", "rebase cloud status --project shop --json"],
14495
+ notes: ["Poll `status` only while blockedOn is null. Any other value names a command, and the state will not change until you run it."]
14496
+ },
14497
+ "clusters verify": {
14498
+ command: "cloud clusters verify",
14499
+ usage: "cloud clusters verify <cluster-id> [--baseline]",
14500
+ summary: "Ask a registered cluster whether it can host tenants. Reports permissions.allowed and permissions.denied, which is what names a missing RBAC grant.",
14501
+ flags: [["--baseline", "Also check ingress-nginx, cert-manager and CloudNativePG"]],
14502
+ examples: ["rebase cloud clusters verify gke-europe-west1 --baseline"],
14503
+ notes: ["Exits non-zero when the verdict is `unusable`, so it works as a gate."]
14504
+ }
14505
+ };
14506
+ /** Print one action's page — human, or its JSON description when piped. */
14507
+ function printActionHelp(entry) {
14508
+ emitHelp(entry.command, [], () => {
14509
+ console.log("");
14510
+ console.log(`${chalk.bold(`rebase ${entry.command}`)}`);
14511
+ console.log("");
14512
+ console.log(` ${entry.summary}`);
14513
+ console.log("");
14514
+ console.log(chalk.green.bold("Usage"));
14515
+ console.log(` rebase ${chalk.blue(entry.usage.replace(/^cloud /, "cloud "))}`);
14516
+ if (entry.flags.length > 0) {
14517
+ console.log("");
14518
+ console.log(chalk.green.bold("Options"));
14519
+ for (const [flag, description] of entry.flags) console.log(` ${chalk.blue(flag.padEnd(30))} ${description}`);
14520
+ }
14521
+ console.log("");
14522
+ console.log(chalk.green.bold("Global options"));
14523
+ for (const [flag, description] of GLOBAL_HELP_FLAGS) console.log(` ${chalk.blue(flag.padEnd(30))} ${chalk.gray(description)}`);
14524
+ if (entry.notes?.length) {
14525
+ console.log("");
14526
+ console.log(chalk.green.bold("Notes"));
14527
+ for (const note of entry.notes) console.log(` ${chalk.gray(`• ${note}`)}`);
14528
+ }
14529
+ console.log("");
14530
+ console.log(chalk.green.bold("Examples"));
14531
+ for (const example of entry.examples) console.log(` ${chalk.gray(example)}`);
14532
+ console.log("");
14533
+ }, {
14534
+ usage: `rebase ${entry.usage}`,
14535
+ summary: entry.summary,
14536
+ flags: [...entry.flags, ...GLOBAL_HELP_FLAGS].map(([flag, description]) => ({
14537
+ flag,
14538
+ description
14539
+ })),
14540
+ notes: entry.notes ?? [],
14541
+ examples: entry.examples
14542
+ });
14543
+ }
14544
+ //#endregion
13835
14545
  //#region src/commands/cloud/index.ts
13836
14546
  /**
13837
14547
  * CLI command: `rebase cloud <group> [action] [options]`
@@ -13906,6 +14616,11 @@ async function cloudCommand(subcommand, rawArgs) {
13906
14616
  return;
13907
14617
  }
13908
14618
  if (wantsHelp) {
14619
+ const page = (action ? ACTION_HELP[`${group} ${action}`] : void 0) ?? ACTION_HELP[group];
14620
+ if (page) {
14621
+ printActionHelp(page);
14622
+ return;
14623
+ }
13909
14624
  (GROUP_HELP[group] ?? printCloudHelp)();
13910
14625
  return;
13911
14626
  }
@@ -14414,7 +15129,7 @@ async function entry(args) {
14414
15129
  const effectiveSubcommand = parsedArgs["--help"] && !subcommand ? "--help" : subcommand;
14415
15130
  switch (command) {
14416
15131
  case "__dev-db-daemon": {
14417
- const { parseDaemonArgs, runDaemon } = await import("./daemon-entry-Brq-S8XX.js");
15132
+ const { parseDaemonArgs, runDaemon } = await import("./daemon-entry-CmJn83zu.js");
14418
15133
  await runDaemon(parseDaemonArgs(args));
14419
15134
  return;
14420
15135
  }