@rebasepro/cli 0.10.0 → 0.10.1-canary.18115ba

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.
@@ -100,6 +100,16 @@ export declare function initOutputMode(rawArgs: string[]): boolean;
100
100
  export declare function isJsonMode(): boolean;
101
101
  /** Force the mode (tests only — production latches it via `initOutputMode`). */
102
102
  export declare function setJsonModeForTest(value: boolean): void;
103
+ /**
104
+ * Write one JSON value to stdout, followed by a newline.
105
+ *
106
+ * Indented, because the overwhelmingly common reader is a person or an agent
107
+ * looking at a terminal — JSON mode is entered automatically whenever stdout is
108
+ * not a TTY, so `rebase cloud deployments list` piped anywhere at all produced
109
+ * a project's entire deployment history as one unwrapped line. `JSON.parse`
110
+ * does not care about the whitespace; everything else does.
111
+ */
112
+ export declare function printJson(value: unknown): void;
103
113
  /**
104
114
  * The one output primitive every new command uses: in JSON mode emit `json`
105
115
  * (and nothing else); otherwise run `human`. Keeping the two behind a single
@@ -18,6 +18,10 @@ export interface DeploymentRow {
18
18
  triggered_by_user_id?: string;
19
19
  gitCommitHash?: string;
20
20
  gitCommitMessage?: string;
21
+ deployMessage?: string;
22
+ deploy_message?: string;
23
+ frameworkVersion?: string;
24
+ framework_version?: string;
21
25
  }
22
26
  /**
23
27
  * The backend's rule EXACTLY: a rollback is honoured only for a successful
@@ -33,6 +37,18 @@ export declare function triggerInfo(dep: DeploymentRow): {
33
37
  };
34
38
  /** Shape one deployment row into the stable JSON view the CLI publishes. */
35
39
  export declare function deploymentView(dep: DeploymentRow): Record<string, unknown>;
40
+ /**
41
+ * Rows shown when `--limit` is not given.
42
+ *
43
+ * History is unbounded and grows one row per deploy, so "all of it" is the
44
+ * wrong default in both directions: a wall of near-identical lines in a
45
+ * terminal, and — since JSON mode is entered automatically for any non-TTY
46
+ * stdout — a project's entire history dumped at anything that pipes the
47
+ * command. Recent deploys are what the question is almost always about.
48
+ */
49
+ export declare const DEFAULT_DEPLOYMENTS_LIMIT = 20;
50
+ /** `--limit N`, bounded. A garbage value is a refusal, never a silent default. */
51
+ export declare function parseDeploymentsLimit(raw: number | undefined): number;
36
52
  export declare function deploymentsListCommand(rawArgs: string[]): Promise<void>;
37
53
  export declare function rollbackCommand(rawArgs: string[]): Promise<void>;
38
54
  export declare function cancelCommand(rawArgs: string[]): Promise<void>;
@@ -4,3 +4,5 @@ export declare function parseEnvAssignment(operands: string[]): {
4
4
  key: string;
5
5
  value: string;
6
6
  } | null;
7
+ /** The prefix that makes `key` a build-time variable, or undefined. */
8
+ export declare function buildTimeEnvPrefix(key: string): string | undefined;
@@ -1,6 +1,44 @@
1
+ /** The control plane's verdict on what a tenant's uploads actually do. */
2
+ interface StorageState {
3
+ effective?: {
4
+ kind?: string;
5
+ summary?: string;
6
+ storageType?: string;
7
+ missing?: string[];
8
+ };
9
+ source?: string;
10
+ configured?: string;
11
+ overridden?: boolean;
12
+ }
13
+ /**
14
+ * One line describing this project's storage — or `undefined` when the control
15
+ * plane could not be asked, which prints as a blank rather than a guess.
16
+ *
17
+ * `status` used to render the `storages` row and nothing else, so a project
18
+ * whose bucket is configured through its own `STORAGE_TYPE`/`S3_*` variables —
19
+ * the supported path, and the one `mergeStorageEnv` deliberately lets WIN over
20
+ * the row — was reported as `Storage: none` while its pod logged `Initialized
21
+ * storage backends count: 1` against a live bucket. Storage is the thing an app
22
+ * refuses to boot without, so that false negative sends someone off to
23
+ * provision a bucket they already have. The row is not the answer; the tenant's
24
+ * resolved environment is, and the control plane computes it with the same two
25
+ * functions the build log uses.
26
+ */
27
+ export declare function describeStorageState(state: StorageState | undefined): string | undefined;
28
+ /**
29
+ * One line describing the database.
30
+ *
31
+ * `connectionStatus` is written `"untested"` at creation and only ever changed
32
+ * by `rebase cloud db test`, so `managed (untested)` was reporting the absence
33
+ * of a manual test as though it were the database's condition — on a project
34
+ * that had just deployed against it. A never-tested database says only its
35
+ * type; the verdict appears once there is one.
36
+ */
37
+ export declare function describeDatabaseState(db: Record<string, unknown> | undefined): string | undefined;
1
38
  export declare function statusCommand(rawArgs: string[]): Promise<void>;
2
39
  export declare function metricsCommand(rawArgs: string[]): Promise<void>;
3
40
  export declare function webhooksCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
4
41
  export declare function storageCommand(action: string | undefined, rawArgs: string[]): Promise<void>;
5
42
  export declare function clustersCommand(rawArgs: string[]): Promise<void>;
6
43
  export declare function billingCommand(rawArgs: string[]): Promise<void>;
44
+ export {};
package/dist/index.es.js CHANGED
@@ -23,21 +23,61 @@ import { createRequire } from "module";
23
23
  * the rest of the CLI never has to hardcode a specific PM.
24
24
  */
25
25
  /**
26
+ * How long to wait for `pnpm --version` before giving up on the probe.
27
+ *
28
+ * `pnpm --version` is a cold Node start, and on a machine that is busy — a
29
+ * parallel install, a full test run — it routinely takes seconds. Measured at
30
+ * 630ms, 990ms and 4293ms on three consecutive runs of one developer laptop
31
+ * under load, so the previous 3s budget was inside the normal spread rather
32
+ * than safely outside it.
33
+ */
34
+ var PNPM_PROBE_TIMEOUT_MS = 5e3;
35
+ /** Memoised result of the probe. pnpm cannot appear or vanish mid-process. */
36
+ var cachedPnpmAvailable;
37
+ /**
38
+ * Decide availability from a `spawnSync` outcome.
39
+ *
40
+ * Split out from the spawn itself so the decision is testable without starting
41
+ * a process — which is what made the old test load-sensitive and occasionally
42
+ * red for reasons that had nothing to do with the code under test.
43
+ *
44
+ * The three outcomes are distinguishable, and the old code conflated two of
45
+ * them by asking only `status === 0`:
46
+ *
47
+ * not installed status null, signal null, error.code ENOENT
48
+ * timed out status null, signal SIGTERM, error.code ETIMEDOUT
49
+ * broken install status non-zero, no error
50
+ */
51
+ function pnpmAvailabilityFromProbe(res) {
52
+ const code = res.error?.code;
53
+ if (code === "ENOENT") return false;
54
+ if (code === "ETIMEDOUT" || res.signal) return true;
55
+ if (res.error) return false;
56
+ return res.status === 0;
57
+ }
58
+ /**
26
59
  * Whether pnpm is runnable on this machine.
27
60
  *
28
61
  * Used to decide whether a fresh project can be scaffolded with pnpm. Kept
29
- * cheap and non-interactive (short timeout, output discarded) so it never
30
- * hangs detection if a corepack shim misbehaves.
62
+ * cheap and non-interactive (bounded timeout, output discarded) so it never
63
+ * hangs detection if a corepack shim misbehaves, and memoised so that repeated
64
+ * detection in one CLI run costs one process rather than one per call.
31
65
  */
32
66
  function isPnpmAvailable() {
67
+ if (cachedPnpmAvailable !== void 0) return cachedPnpmAvailable;
33
68
  try {
34
- return spawnSync("pnpm", ["--version"], {
69
+ cachedPnpmAvailable = pnpmAvailabilityFromProbe(spawnSync("pnpm", ["--version"], {
35
70
  stdio: "ignore",
36
- timeout: 3e3
37
- }).status === 0;
71
+ timeout: PNPM_PROBE_TIMEOUT_MS
72
+ }));
38
73
  } catch {
39
- return false;
74
+ cachedPnpmAvailable = false;
40
75
  }
76
+ return cachedPnpmAvailable;
77
+ }
78
+ /** Forget the memoised probe. For tests; nothing in a CLI run needs it. */
79
+ function resetPnpmAvailabilityCache() {
80
+ cachedPnpmAvailable = void 0;
41
81
  }
42
82
  /**
43
83
  * Detect the package manager for a Rebase project.
@@ -636,9 +676,17 @@ var ANSI_RE = /\[[0-9;]*m/g;
636
676
  function stripAnsi(s) {
637
677
  return s.replace(ANSI_RE, "");
638
678
  }
639
- /** Write one JSON value to stdout, followed by a newline. */
679
+ /**
680
+ * Write one JSON value to stdout, followed by a newline.
681
+ *
682
+ * Indented, because the overwhelmingly common reader is a person or an agent
683
+ * looking at a terminal — JSON mode is entered automatically whenever stdout is
684
+ * not a TTY, so `rebase cloud deployments list` piped anywhere at all produced
685
+ * a project's entire deployment history as one unwrapped line. `JSON.parse`
686
+ * does not care about the whitespace; everything else does.
687
+ */
640
688
  function printJson(value) {
641
- process.stdout.write(JSON.stringify(value) + "\n");
689
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
642
690
  }
643
691
  /**
644
692
  * The one output primitive every new command uses: in JSON mode emit `json`
@@ -3421,6 +3469,35 @@ async function createSourceTarball(sourceDir) {
3421
3469
  }
3422
3470
  return tarPath;
3423
3471
  }
3472
+ /**
3473
+ * The `@rebasepro/*` version this source directory actually resolves.
3474
+ *
3475
+ * Recorded on the deployment so a row in Deployment History says which
3476
+ * framework build shipped. Nothing else on the platform knows: an app that
3477
+ * links the framework locally pins it at package time, and a silent bump is
3478
+ * invisible afterwards — it has already cost one debugging session.
3479
+ *
3480
+ * `@rebasepro/server` first, because that is what the deployed backend runs;
3481
+ * `@rebasepro/client` is the fallback for a frontend-only bundle. Resolution is
3482
+ * a plain walk up from the source directory rather than `require.resolve`,
3483
+ * which would answer for the CLI's own install tree instead of the app's.
3484
+ *
3485
+ * Best effort by construction: a version that cannot be read is simply not
3486
+ * recorded. Nothing about a deploy should fail over a bookkeeping string.
3487
+ */
3488
+ function resolveFrameworkVersion(sourceDir) {
3489
+ let dir = path.resolve(sourceDir);
3490
+ for (;;) {
3491
+ for (const pkg of ["@rebasepro/server", "@rebasepro/client"]) try {
3492
+ const manifest = path.join(dir, "node_modules", ...pkg.split("/"), "package.json");
3493
+ const version = JSON.parse(fs.readFileSync(manifest, "utf8")).version;
3494
+ if (typeof version === "string" && version.trim() !== "") return version.trim();
3495
+ } catch {}
3496
+ const parent = path.dirname(dir);
3497
+ if (parent === dir) return void 0;
3498
+ dir = parent;
3499
+ }
3500
+ }
3424
3501
  /** Upload a build-context tarball; returns the opaque `source` ref for deploy. */
3425
3502
  async function uploadSource(url, token, projectId, tarPath) {
3426
3503
  const bytes = fs.readFileSync(tarPath);
@@ -3446,7 +3523,9 @@ async function uploadSource(url, token, projectId, tarPath) {
3446
3523
  async function deployCommand(rawArgs, projectRef) {
3447
3524
  const args = arg({
3448
3525
  "--no-follow": Boolean,
3449
- "--source": String
3526
+ "--source": String,
3527
+ "--message": String,
3528
+ "-m": "--message"
3450
3529
  }, {
3451
3530
  argv: rawArgs.slice(2),
3452
3531
  permissive: true
@@ -3466,32 +3545,87 @@ async function deployCommand(rawArgs, projectRef) {
3466
3545
  }
3467
3546
  console.log("");
3468
3547
  console.log(` 🚀 Triggering deployment for project ${chalk.bold(projectRef)}${source ? " from uploaded source" : ""}...`);
3469
- let deploymentId;
3548
+ const body = { projectId };
3549
+ if (source) body.source = source;
3550
+ if (args["--message"]) body.message = args["--message"];
3551
+ body.client = "cli";
3552
+ const frameworkVersion = resolveFrameworkVersion(args["--source"] ?? process.cwd());
3553
+ if (frameworkVersion) body.frameworkVersion = frameworkVersion;
3554
+ let triggered;
3470
3555
  try {
3471
- const res = await client.functions.invoke("deploy", source ? {
3472
- projectId,
3473
- source
3474
- } : { projectId });
3556
+ const res = await client.functions.invoke("deploy", body);
3475
3557
  if (!res?.deployment?.id) fail("Control plane did not return a deployment id.");
3476
- deploymentId = String(res.deployment.id);
3558
+ triggered = {
3559
+ deploymentId: String(res.deployment.id),
3560
+ deduplicated: res.deduplicated === true
3561
+ };
3477
3562
  } catch (e) {
3478
- const err = e;
3479
- if (err?.status === 409) fail("A deployment is already in progress for this project.");
3480
- if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.");
3481
- reportError(e, "Failed to trigger deployment");
3563
+ triggered = resolveTriggerFailure(e);
3482
3564
  }
3483
- console.log(chalk.gray(` Deployment ${deploymentId} created.`));
3565
+ const { deploymentId, deduplicated } = triggered;
3566
+ if (!isJsonMode()) console.log(chalk.gray(deduplicated ? ` Deployment ${deploymentId} is already running — following it.` : ` Deployment ${deploymentId} created.${frameworkVersion ? ` (@rebasepro/* ${frameworkVersion})` : ""}`));
3484
3567
  if (args["--no-follow"]) {
3485
- console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
3486
- console.log("");
3568
+ emit(() => {
3569
+ console.log(chalk.gray(" Not following logs (--no-follow). Check status with `rebase cloud logs`."));
3570
+ console.log("");
3571
+ }, {
3572
+ deploymentId,
3573
+ deduplicated,
3574
+ frameworkVersion: frameworkVersion ?? null,
3575
+ following: false
3576
+ });
3487
3577
  return;
3488
3578
  }
3489
- console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
3490
- console.log("");
3491
- await streamBuildLogs(client, deploymentId);
3579
+ if (!isJsonMode()) {
3580
+ console.log(chalk.gray(" Streaming build logs (Ctrl-C to stop watching — the build keeps running):"));
3581
+ console.log("");
3582
+ }
3583
+ const status = await streamBuildLogs(client, deploymentId, { quiet: isJsonMode() });
3584
+ emit(() => {}, {
3585
+ deploymentId,
3586
+ deduplicated,
3587
+ frameworkVersion: frameworkVersion ?? null,
3588
+ following: true,
3589
+ status
3590
+ });
3591
+ }
3592
+ /**
3593
+ * Turn a failed trigger into either a deployment to follow, or an exit.
3594
+ *
3595
+ * The 409 is the interesting one. A deploy trigger can reach the control plane
3596
+ * twice without anybody asking twice — the SDK transport replays a request once
3597
+ * after refreshing an expired token, and any lost response has the same effect
3598
+ * — so "a deployment is already in progress" was routinely describing the
3599
+ * deployment this very command had just created. With no id in the message the
3600
+ * only available reading was "someone else is deploying, back off", and the
3601
+ * build stream was lost either way.
3602
+ *
3603
+ * So: if the control plane says the blocking deployment is ours, we attach to
3604
+ * it. If it is not ours, we still name it, because "which one, since when, from
3605
+ * where" is the difference between an actionable refusal and a dead end.
3606
+ */
3607
+ function resolveTriggerFailure(e) {
3608
+ const err = e;
3609
+ if (err?.status === 409) {
3610
+ const blocking = err.details?.deployment;
3611
+ if (blocking?.id && blocking.mine) return {
3612
+ deploymentId: String(blocking.id),
3613
+ deduplicated: true
3614
+ };
3615
+ 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");
3616
+ }
3617
+ 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");
3618
+ reportError(e, "Failed to trigger deployment");
3492
3619
  }
3493
- /** Poll a deployment record and print new log output as it arrives. */
3494
- async function streamBuildLogs(client, deploymentId) {
3620
+ /**
3621
+ * Poll a deployment record and print new log output as it arrives. Returns the
3622
+ * terminal status; a non-success still exits non-zero, as it always has.
3623
+ *
3624
+ * `quiet` follows without printing — JSON mode, where the log stream would
3625
+ * corrupt the one object the caller is parsing.
3626
+ */
3627
+ async function streamBuildLogs(client, deploymentId, opts = {}) {
3628
+ const quiet = opts.quiet === true;
3495
3629
  let printed = 0;
3496
3630
  const started = Date.now();
3497
3631
  for (;;) {
@@ -3501,26 +3635,37 @@ async function streamBuildLogs(client, deploymentId) {
3501
3635
  } catch (e) {
3502
3636
  reportError(e, "Failed to read deployment status");
3503
3637
  }
3504
- if (!dep) fail(`Deployment ${deploymentId} disappeared.`);
3638
+ if (!dep) fail(`Deployment ${deploymentId} disappeared.`, void 0, "not_found");
3505
3639
  const logs = dep.logs ?? "";
3506
- if (logs.length > printed) {
3507
- process.stdout.write(logs.slice(printed));
3508
- printed = logs.length;
3509
- }
3640
+ if (!quiet && logs.length > printed) process.stdout.write(logs.slice(printed));
3641
+ printed = logs.length;
3510
3642
  if (dep.status && dep.status !== "deploying") {
3511
- console.log("");
3512
- if (dep.status === "success") console.log(chalk.bold.green(" ✓ Deployment succeeded"));
3513
- else {
3643
+ if (dep.status !== "success") {
3644
+ if (quiet) {
3645
+ printJson({ error: {
3646
+ message: `Deployment ${deploymentId} ${dep.status}.`,
3647
+ code: "deploy_failed",
3648
+ status: null,
3649
+ deploymentId,
3650
+ logs
3651
+ } });
3652
+ process.exit(1);
3653
+ }
3654
+ console.log("");
3514
3655
  console.log(chalk.bold.red(` ✗ Deployment ${dep.status}`));
3515
3656
  console.log("");
3516
3657
  process.exit(1);
3517
3658
  }
3518
- console.log("");
3519
- return;
3659
+ if (!quiet) {
3660
+ console.log("");
3661
+ console.log(chalk.bold.green(" ✓ Deployment succeeded"));
3662
+ console.log("");
3663
+ }
3664
+ return dep.status;
3520
3665
  }
3521
3666
  if (Date.now() - started > POLL_TIMEOUT_MS) {
3522
- console.log("");
3523
- fail("Timed out waiting for the build to finish.", "The deployment may still be running — check `rebase cloud logs`.");
3667
+ if (!quiet) console.log("");
3668
+ fail("Timed out waiting for the build to finish.", "The deployment may still be running — check `rebase cloud logs`.", "timeout");
3524
3669
  }
3525
3670
  await sleep(POLL_INTERVAL_MS);
3526
3671
  }
@@ -4233,9 +4378,35 @@ function parseEnvAssignment(operands) {
4233
4378
  value: operands[1] ?? ""
4234
4379
  };
4235
4380
  }
4381
+ /**
4382
+ * Prefixes whose variables are read by a BUNDLER at build time, not by the
4383
+ * process at run time.
4384
+ *
4385
+ * These are the ones this command cannot deliver. A project's environment is
4386
+ * applied at rollout — after Kaniko has already built the image — so a
4387
+ * `VITE_API_URL` set here is present in the running container and absent from
4388
+ * the JavaScript that was compiled minutes earlier. Nothing fails: the variable
4389
+ * exists, the deploy succeeds, and the bundle carries `undefined` where the
4390
+ * value should be. The bug then presents in the browser as missing
4391
+ * configuration, which is several steps away from the cause.
4392
+ *
4393
+ * `import.meta.env` inlining is Vite's; `NEXT_PUBLIC_`/`PUBLIC_`/`REACT_APP_`
4394
+ * are the same contract in Next, Astro/SvelteKit and CRA.
4395
+ */
4396
+ var BUILD_TIME_ENV_PREFIXES = [
4397
+ "VITE_",
4398
+ "NEXT_PUBLIC_",
4399
+ "PUBLIC_",
4400
+ "REACT_APP_"
4401
+ ];
4402
+ /** The prefix that makes `key` a build-time variable, or undefined. */
4403
+ function buildTimeEnvPrefix(key) {
4404
+ return BUILD_TIME_ENV_PREFIXES.find((prefix) => key.toUpperCase().startsWith(prefix));
4405
+ }
4236
4406
  async function setEnv(rawArgs) {
4237
4407
  const args = arg({
4238
4408
  "--secret": Boolean,
4409
+ "--force": Boolean,
4239
4410
  "--project": String,
4240
4411
  "-p": "--project"
4241
4412
  }, {
@@ -4247,6 +4418,8 @@ async function setEnv(rawArgs) {
4247
4418
  displayProjectRef(rawArgs);
4248
4419
  const parsed = parseEnvAssignment(cloudPositionals(rawArgs).slice(2));
4249
4420
  if (!parsed || !parsed.key) fail("Usage: rebase cloud env set KEY=VALUE [--secret]", void 0, "usage");
4421
+ const buildTimePrefix = buildTimeEnvPrefix(parsed.key);
4422
+ if (buildTimePrefix && !args["--force"]) fail(`${parsed.key} is read by your bundler at BUILD time, and project variables are applied at rollout — after the image is built. Setting it here would not reach the bundle.`, `Put ${buildTimePrefix}* variables in the source you deploy (a committed .env, or your build config), then \`rebase cloud deploy\`. Pass --force if your build genuinely reads this at run time.`, "build_time_variable");
4250
4423
  const body = {
4251
4424
  key: parsed.key,
4252
4425
  value: parsed.value
@@ -4403,10 +4576,13 @@ ${chalk.green.bold("Commands")}
4403
4576
 
4404
4577
  ${chalk.green.bold("Options")}
4405
4578
  ${chalk.blue("--secret")} Mark a variable write-only ${chalk.gray("(set)")}
4579
+ ${chalk.blue("--force")} Set a build-time key anyway ${chalk.gray("(set)")}
4406
4580
  ${chalk.blue("--json")} Machine-readable output
4407
4581
  ${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
4408
4582
 
4409
4583
  ${chalk.gray("Values are encrypted at rest (AES-256-GCM) and only decrypted at deploy time.")}
4584
+ ${chalk.gray("VITE_* / NEXT_PUBLIC_* / PUBLIC_* / REACT_APP_* are read by your bundler at BUILD time;")}
4585
+ ${chalk.gray("these are applied at rollout, after the image is built, so they never reach the bundle.")}
4410
4586
  `);
4411
4587
  }
4412
4588
  function printEnvHelpJson() {
@@ -5002,6 +5178,8 @@ function deploymentView(dep) {
5002
5178
  isRollback: str(dep, "rollbackOf", "rollback_of") !== null,
5003
5179
  rollbackable: isRollbackable(dep),
5004
5180
  trigger: triggerInfo(dep),
5181
+ message: str(dep, "deployMessage", "deploy_message"),
5182
+ frameworkVersion: str(dep, "frameworkVersion", "framework_version"),
5005
5183
  commit: {
5006
5184
  hash: str(dep, "gitCommitHash", "gitCommitHash"),
5007
5185
  message: str(dep, "gitCommitMessage", "gitCommitMessage")
@@ -5015,12 +5193,31 @@ async function fetchDeployments(client, projectId, limit = 100) {
5015
5193
  limit
5016
5194
  })).data;
5017
5195
  }
5196
+ /** Hard ceiling on `--limit`, matching the backend's own page size. */
5197
+ var MAX_DEPLOYMENTS_LIMIT = 100;
5198
+ /** `--limit N`, bounded. A garbage value is a refusal, never a silent default. */
5199
+ function parseDeploymentsLimit(raw) {
5200
+ if (raw === void 0) return 20;
5201
+ if (!Number.isInteger(raw) || raw < 1 || raw > MAX_DEPLOYMENTS_LIMIT) fail(`--limit must be a whole number between 1 and ${MAX_DEPLOYMENTS_LIMIT}.`, void 0, "usage");
5202
+ return raw;
5203
+ }
5018
5204
  async function deploymentsListCommand(rawArgs) {
5205
+ const args = arg({
5206
+ "--limit": Number,
5207
+ "--all": Boolean,
5208
+ "--project": String,
5209
+ "-p": "--project"
5210
+ }, {
5211
+ argv: rawArgs.slice(2),
5212
+ permissive: true
5213
+ });
5214
+ const limit = args["--all"] ? MAX_DEPLOYMENTS_LIMIT : parseDeploymentsLimit(args["--limit"]);
5019
5215
  const { client } = await requireClient(rawArgs);
5020
5216
  const projectId = await requireProject(rawArgs, client);
5021
5217
  const projectRef = displayProjectRef(rawArgs);
5022
5218
  try {
5023
- const views = (await fetchDeployments(client, projectId)).map(deploymentView);
5219
+ const views = (await fetchDeployments(client, projectId, limit)).map(deploymentView);
5220
+ const truncated = views.length === limit;
5024
5221
  emit(() => {
5025
5222
  console.log("");
5026
5223
  console.log(chalk.bold(` 🚀 Deployments — project ${projectRef}`));
@@ -5035,10 +5232,18 @@ async function deploymentsListCommand(rawArgs) {
5035
5232
  const trig = v.trigger.source;
5036
5233
  const roll = v.rollbackable ? chalk.green(" ↺ rollbackable") : "";
5037
5234
  console.log(` ${chalk.gray(`[${v.id}]`)} ${colorStatus(v.status)} ${chalk.gray(String(v.createdAt ?? "—"))} ${dur} ${chalk.gray(trig)}${roll}`);
5235
+ const label = [v.message, v.frameworkVersion ? `@rebasepro/* ${v.frameworkVersion}` : null].filter(Boolean).join(" · ");
5236
+ if (label) console.log(` ${chalk.gray(label)}`);
5237
+ }
5238
+ if (truncated) {
5239
+ console.log("");
5240
+ console.log(chalk.gray(` Showing the ${limit} most recent. Use \`--limit N\` or \`--all\` for more.`));
5038
5241
  }
5039
5242
  console.log("");
5040
5243
  }, {
5041
5244
  projectId,
5245
+ limit,
5246
+ truncated,
5042
5247
  deployments: views
5043
5248
  });
5044
5249
  } catch (e) {
@@ -5938,29 +6143,95 @@ ${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase
5938
6143
  * `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
5939
6144
  * clusters, billing.
5940
6145
  */
6146
+ /**
6147
+ * One line describing this project's storage — or `undefined` when the control
6148
+ * plane could not be asked, which prints as a blank rather than a guess.
6149
+ *
6150
+ * `status` used to render the `storages` row and nothing else, so a project
6151
+ * whose bucket is configured through its own `STORAGE_TYPE`/`S3_*` variables —
6152
+ * the supported path, and the one `mergeStorageEnv` deliberately lets WIN over
6153
+ * the row — was reported as `Storage: none` while its pod logged `Initialized
6154
+ * storage backends count: 1` against a live bucket. Storage is the thing an app
6155
+ * refuses to boot without, so that false negative sends someone off to
6156
+ * provision a bucket they already have. The row is not the answer; the tenant's
6157
+ * resolved environment is, and the control plane computes it with the same two
6158
+ * functions the build log uses.
6159
+ */
6160
+ function describeStorageState(state) {
6161
+ const verdict = state?.effective;
6162
+ if (!verdict?.kind) return void 0;
6163
+ const via = state?.overridden ? chalk.gray(" · from env vars") : "";
6164
+ switch (verdict.kind) {
6165
+ case "durable": return `${chalk.green("durable")}${verdict.summary ? ` · ${verdict.summary}` : ""}${via}`;
6166
+ case "ephemeral": return `${chalk.yellow("ephemeral")} ${chalk.gray("· uploads are lost on restart")}`;
6167
+ case "incomplete": return `${chalk.red("incomplete")} ${chalk.gray(`· missing ${(verdict.missing ?? []).join(", ")}`)}`;
6168
+ case "unrecognized": return `${chalk.red("unrecognized")} ${chalk.gray(`· STORAGE_TYPE=${verdict.storageType ?? "?"}`)}`;
6169
+ default: return;
6170
+ }
6171
+ }
6172
+ /**
6173
+ * One line describing the database.
6174
+ *
6175
+ * `connectionStatus` is written `"untested"` at creation and only ever changed
6176
+ * by `rebase cloud db test`, so `managed (untested)` was reporting the absence
6177
+ * of a manual test as though it were the database's condition — on a project
6178
+ * that had just deployed against it. A never-tested database says only its
6179
+ * type; the verdict appears once there is one.
6180
+ */
6181
+ function describeDatabaseState(db) {
6182
+ if (!db) return void 0;
6183
+ const type = typeof db.type === "string" ? db.type : "database";
6184
+ const connection = db.connectionStatus;
6185
+ if (connection === "connected" || connection === "failed") return `${type} (${colorStatus(connection)})`;
6186
+ return `${type} ${chalk.gray("· not tested (`rebase cloud db test`)")}`;
6187
+ }
5941
6188
  async function statusCommand(rawArgs) {
5942
6189
  const { client, url } = await requireClient(rawArgs);
5943
6190
  const projectId = await requireProject(rawArgs, client);
5944
6191
  try {
5945
6192
  const project = await client.data.collection("projects").findById(projectId);
5946
- if (!project) fail(`Project ${displayProjectRef(rawArgs)} not found.`);
6193
+ if (!project) fail(`Project ${displayProjectRef(rawArgs)} not found.`, void 0, "not_found");
5947
6194
  const [db, storage, deploy, baseDomain] = await Promise.all([
5948
6195
  firstRow(client, "databases", projectId),
5949
- firstRow(client, "storages", projectId),
6196
+ client.functions.invoke("storage-provision", void 0, {
6197
+ method: "GET",
6198
+ path: projectId
6199
+ }).catch(() => void 0),
5950
6200
  latestDeployment(client, projectId),
5951
6201
  fetchTenantBaseDomain(client, url)
5952
6202
  ]);
5953
- console.log("");
5954
- console.log(` ${chalk.bold(project.name ?? project.subdomain ?? "")} ${chalk.gray(`[${project.subdomain ?? displayProjectRef(rawArgs)}]`)} ${colorStatus(project.status)}`);
5955
- console.log("");
5956
- keyValues([
5957
- ["URL", projectHost(project, baseDomain)],
5958
- ["Branch", project.gitBranch],
5959
- ["Last deploy", deploy ? `${colorStatus(deploy.status)} · ${fmtDate(deploy.createdAt)}` : "never"],
5960
- ["Database", db ? `${db.type} (${colorStatus(db.connectionStatus)})` : "none"],
5961
- ["Storage", storage ? `${storage.type} (${colorStatus(storage.status)})` : "none"]
5962
- ]);
5963
- console.log("");
6203
+ const storageLine = describeStorageState(storage);
6204
+ const databaseLine = describeDatabaseState(db);
6205
+ emit(() => {
6206
+ console.log("");
6207
+ console.log(` ${chalk.bold(project.name ?? project.subdomain ?? "")} ${chalk.gray(`[${project.subdomain ?? displayProjectRef(rawArgs)}]`)} ${colorStatus(project.status)}`);
6208
+ console.log("");
6209
+ keyValues([
6210
+ ["URL", projectHost(project, baseDomain)],
6211
+ ["Branch", project.gitBranch],
6212
+ ["Last deploy", deploy ? `${colorStatus(deploy.status)} · ${fmtDate(deploy.createdAt)}` : "never"],
6213
+ ["Database", databaseLine],
6214
+ ["Storage", storageLine]
6215
+ ]);
6216
+ console.log("");
6217
+ }, {
6218
+ projectId: String(project.id),
6219
+ name: project.name ?? null,
6220
+ subdomain: project.subdomain ?? null,
6221
+ status: project.status ?? null,
6222
+ url: projectHost(project, baseDomain) ?? null,
6223
+ branch: project.gitBranch ?? null,
6224
+ lastDeploy: deploy ? {
6225
+ id: String(deploy.id),
6226
+ status: deploy.status ?? null,
6227
+ createdAt: deploy.createdAt ?? null
6228
+ } : null,
6229
+ database: db ? {
6230
+ type: db.type ?? null,
6231
+ connectionStatus: db.connectionStatus ?? null
6232
+ } : null,
6233
+ storage: storage ?? null
6234
+ });
5964
6235
  } catch (e) {
5965
6236
  reportError(e, "Failed to load status");
5966
6237
  }
@@ -6471,9 +6742,9 @@ ${chalk.green.bold("Projects")}
6471
6742
  ${chalk.blue.bold("projects delete")} ${chalk.gray("[id]")} Delete a project
6472
6743
 
6473
6744
  ${chalk.green.bold("Deploy & observe")}
6474
- ${chalk.blue.bold("deploy")} ${chalk.gray("[--source .]")} Deploy the linked project + stream build logs
6745
+ ${chalk.blue.bold("deploy")} ${chalk.gray("[--source .] [-m msg]")} Deploy the linked project + stream build logs
6475
6746
  ${chalk.blue.bold("logs")} ${chalk.gray("[--runtime] [-f]")} Show build (or runtime) logs
6476
- ${chalk.blue.bold("deployments list")} Deployment history ${chalk.gray("(status, duration, trigger)")}
6747
+ ${chalk.blue.bold("deployments list")} ${chalk.gray("[--limit N|--all]")} Deployment history ${chalk.gray("(status, duration, trigger)")}
6477
6748
  ${chalk.blue.bold("rollback")} ${chalk.gray("[id] [-y]")} Roll back to a successful deploy
6478
6749
  ${chalk.blue.bold("cancel")} ${chalk.gray("[-y]")} Cancel the in-flight build
6479
6750
  ${chalk.blue.bold("start|stop|restart")} ${chalk.gray("[-y]")} Power ops ${chalk.gray("(stop/restart need -y)")}
@@ -6672,6 +6943,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
6672
6943
  `);
6673
6944
  }
6674
6945
  //#endregion
6675
- export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, printInitHelp, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateProjectName, validateTsxInstallation };
6946
+ export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, pnpmAvailabilityFromProbe, printInitHelp, requireBackendDir, requireProjectRoot, resetPnpmAvailabilityCache, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateProjectName, validateTsxInstallation };
6676
6947
 
6677
6948
  //# sourceMappingURL=index.es.js.map