@rebasepro/cli 0.9.1-canary.f2f61da → 0.9.1-canary.faee831

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
@@ -3287,13 +3287,18 @@ function fmtDate(value) {
3287
3287
  */
3288
3288
  var POLL_INTERVAL_MS = 1500;
3289
3289
  var POLL_TIMEOUT_MS = 900 * 1e3;
3290
+ var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
3290
3291
  function sleep(ms) {
3291
3292
  return new Promise((r) => setTimeout(r, ms));
3292
3293
  }
3293
- function run(cmd, cmdArgs, cwd) {
3294
+ function run(cmd, cmdArgs, cwd, env) {
3294
3295
  return new Promise((resolve, reject) => {
3295
3296
  const child = spawn(cmd, cmdArgs, {
3296
3297
  cwd,
3298
+ env: env ? {
3299
+ ...process.env,
3300
+ ...env
3301
+ } : void 0,
3297
3302
  stdio: [
3298
3303
  "ignore",
3299
3304
  "ignore",
@@ -3323,7 +3328,7 @@ async function createSourceTarball(sourceDir) {
3323
3328
  for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
3324
3329
  tarArgs.push(".");
3325
3330
  try {
3326
- await run("tar", tarArgs, dir);
3331
+ await run("tar", tarArgs, dir, { COPYFILE_DISABLE: "1" });
3327
3332
  } catch (e) {
3328
3333
  fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
3329
3334
  }
@@ -3333,6 +3338,7 @@ async function createSourceTarball(sourceDir) {
3333
3338
  async function uploadSource(url, token, projectId, tarPath) {
3334
3339
  const bytes = fs.readFileSync(tarPath);
3335
3340
  const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
3341
+ 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.");
3336
3342
  console.log(chalk.gray(` Uploading source (${sizeMb} MB)...`));
3337
3343
  const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
3338
3344
  method: "POST",
@@ -4865,7 +4871,7 @@ function deploymentDurationMs(dep) {
4865
4871
  const ms = b - a;
4866
4872
  return ms >= 0 ? ms : null;
4867
4873
  }
4868
- function formatDuration(ms) {
4874
+ function formatDuration$1(ms) {
4869
4875
  const totalSec = Math.max(0, Math.round(ms / 1e3));
4870
4876
  if (totalSec < 60) return `${totalSec}s`;
4871
4877
  const m = Math.floor(totalSec / 60);
@@ -4938,7 +4944,7 @@ async function deploymentsListCommand(rawArgs) {
4938
4944
  return;
4939
4945
  }
4940
4946
  for (const v of views) {
4941
- const dur = v.durationMs !== null ? formatDuration(v.durationMs) : chalk.gray("running");
4947
+ const dur = v.durationMs !== null ? formatDuration$1(v.durationMs) : chalk.gray("running");
4942
4948
  const trig = v.trigger.source;
4943
4949
  const roll = v.rollbackable ? chalk.green(" ↺ rollbackable") : "";
4944
4950
  console.log(` ${chalk.gray(`[${v.id}]`)} ${colorStatus(v.status)} ${chalk.gray(String(v.createdAt ?? "—"))} ${dur} ${chalk.gray(trig)}${roll}`);
@@ -5109,6 +5115,737 @@ async function powerCommand(action, rawArgs) {
5109
5115
  }
5110
5116
  }
5111
5117
  //#endregion
5118
+ //#region src/commands/cloud/debug.ts
5119
+ /**
5120
+ * `rebase cloud debug <subcommand>` — one entry point for "why is my deployed
5121
+ * app not behaving".
5122
+ *
5123
+ * ## Why this exists
5124
+ *
5125
+ * The useful signals for a deployed project live in four different places — the
5126
+ * control plane, the workload's pods, the tenant database, and the public URL —
5127
+ * and each is normally reached with a different tool and a different set of
5128
+ * flags. Getting to a log should not be a research task. This started life as a
5129
+ * hand-rolled `prod-debug.sh` for a single project, with the namespace and the
5130
+ * URL hardcoded at the top; it earned its place twice in one week, so it is
5131
+ * generalised here to any project the CLI can already resolve.
5132
+ *
5133
+ * ## Read-only by default
5134
+ *
5135
+ * Every subcommand here only reads. The two things the original script could do
5136
+ * that mutate are deliberately NOT reproduced as-is:
5137
+ *
5138
+ * - restarting the workload lives at `rebase cloud restart`, which already
5139
+ * gates downtime behind `--yes`; duplicating it here would give the same
5140
+ * destructive act a second, ungated spelling.
5141
+ * - `debug db` prints the port-forward recipe and the connection's *shape*.
5142
+ * It opens no session and prints no password — `rebase cloud db info
5143
+ * --reveal` is the explicit, auditable way to get one.
5144
+ *
5145
+ * ## The probes are the point
5146
+ *
5147
+ * `debug health` is the highest-value piece and the reason the script existed.
5148
+ * A bare status code is not a diagnosis: a 404 from a functions route means
5149
+ * something completely different from a 404 at the root, and a 200 on an
5150
+ * unauthenticated read is a finding rather than a success. So every probe ships
5151
+ * with the interpretation of what it got, not just the number — see
5152
+ * {@link PROBES}.
5153
+ */
5154
+ /** A response never arrived: DNS, TLS, ingress, or the pod. */
5155
+ var NO_RESPONSE = {
5156
+ verdict: "fail",
5157
+ meaning: "nothing answered — the hostname does not resolve, the ingress has no route, or no pod is running"
5158
+ };
5159
+ function serverError(what) {
5160
+ return {
5161
+ verdict: "fail",
5162
+ meaning: `the server is running but ${what}`
5163
+ };
5164
+ }
5165
+ /**
5166
+ * The probe set, in the order a failure cascades: if `health` is down, nothing
5167
+ * below it is meaningful, so it is checked first and reported first.
5168
+ */
5169
+ var PROBES = [
5170
+ {
5171
+ id: "health",
5172
+ label: "health",
5173
+ method: "GET",
5174
+ path: () => "/health",
5175
+ healthy: "200 — the backend is up",
5176
+ interpret: (status) => {
5177
+ if (status === null) return NO_RESPONSE;
5178
+ if (status === 200) return {
5179
+ verdict: "ok",
5180
+ meaning: "the backend is up and serving"
5181
+ };
5182
+ if (status === 404) return {
5183
+ verdict: "fail",
5184
+ meaning: "something answered but it is not a Rebase backend — the ingress is routing this host elsewhere"
5185
+ };
5186
+ if (status >= 500) return serverError("its health endpoint is failing");
5187
+ return {
5188
+ verdict: "unknown",
5189
+ meaning: "unexpected for a health endpoint"
5190
+ };
5191
+ }
5192
+ },
5193
+ {
5194
+ id: "spa",
5195
+ label: "spa",
5196
+ method: "GET",
5197
+ path: () => "/",
5198
+ healthy: "200 — the frontend is being served",
5199
+ interpret: (status) => {
5200
+ if (status === null) return NO_RESPONSE;
5201
+ if (status === 200) return {
5202
+ verdict: "ok",
5203
+ meaning: "the frontend bundle is being served"
5204
+ };
5205
+ if (status === 404) return {
5206
+ verdict: "warn",
5207
+ meaning: "no frontend at the root — expected for a backend-only project, otherwise the SPA assets were not bundled into the image"
5208
+ };
5209
+ if (status >= 500) return serverError("the root route throws");
5210
+ return {
5211
+ verdict: "unknown",
5212
+ meaning: "unexpected at the site root"
5213
+ };
5214
+ }
5215
+ },
5216
+ {
5217
+ id: "auth",
5218
+ label: "auth",
5219
+ method: "POST",
5220
+ path: () => "/api/auth/login",
5221
+ body: {},
5222
+ healthy: "400 — auth is mounted and rejects an empty body",
5223
+ interpret: (status) => {
5224
+ if (status === null) return NO_RESPONSE;
5225
+ if (status === 400 || status === 422) return {
5226
+ verdict: "ok",
5227
+ meaning: "auth is mounted and rejected the empty body, as it should"
5228
+ };
5229
+ if (status === 401 || status === 403) return {
5230
+ verdict: "ok",
5231
+ meaning: "auth is mounted and refused the credentials"
5232
+ };
5233
+ if (status === 404) return {
5234
+ verdict: "fail",
5235
+ meaning: "the auth routes are NOT mounted — this project cannot sign anyone in"
5236
+ };
5237
+ if (status === 200) return {
5238
+ verdict: "fail",
5239
+ meaning: "an EMPTY login body was accepted — a login with no credentials must never succeed"
5240
+ };
5241
+ if (status >= 500) return serverError("the login route throws — often a missing or unmigrated auth table");
5242
+ return {
5243
+ verdict: "unknown",
5244
+ meaning: "unexpected for a login route"
5245
+ };
5246
+ }
5247
+ },
5248
+ {
5249
+ id: "unauthRead",
5250
+ label: "unauth read",
5251
+ method: "GET",
5252
+ path: (t) => `/api/data/${encodeURIComponent(t.collection)}`,
5253
+ healthy: "401 — reads require authentication",
5254
+ interpret: (status) => {
5255
+ if (status === null) return NO_RESPONSE;
5256
+ if (status === 401 || status === 403) return {
5257
+ verdict: "ok",
5258
+ meaning: "unauthenticated reads are refused — row-level security is enforced"
5259
+ };
5260
+ if (status === 200) return {
5261
+ verdict: "warn",
5262
+ meaning: "this collection is readable with NO authentication — correct only if it is deliberately public"
5263
+ };
5264
+ if (status === 404) return {
5265
+ verdict: "warn",
5266
+ meaning: "no such collection on this deployment — check the name, or pass --collection"
5267
+ };
5268
+ if (status >= 500) return serverError("the read reached the database and failed — most often an RLS policy naming a column or table that is not there");
5269
+ return {
5270
+ verdict: "unknown",
5271
+ meaning: "unexpected for a data read"
5272
+ };
5273
+ }
5274
+ },
5275
+ {
5276
+ id: "functions",
5277
+ label: "functions",
5278
+ method: "GET",
5279
+ /**
5280
+ * The router's own listing endpoint, NOT a function's path.
5281
+ *
5282
+ * This matters, and it is the one place the original script got a wrong
5283
+ * answer. A function is a Hono sub-app mounted at `/<name>`, and it
5284
+ * usually defines only sub-routes (`/get`, `/list`) — so
5285
+ * `/api/functions/<name>` 404s **even when everything is mounted and
5286
+ * healthy**. Probing there cannot separate "the router is missing" from
5287
+ * "that function defines no root route", and reporting the first is how
5288
+ * you send someone to debug a deployment that was fine.
5289
+ *
5290
+ * `GET /api/functions` is unambiguous: the router registers a listing
5291
+ * route at its own root (see `createFunctionRoutes`), so a 200 proves
5292
+ * the mount *and* names every function that loaded.
5293
+ */
5294
+ path: () => "/api/functions",
5295
+ healthy: "200 — the functions router is mounted and lists its functions",
5296
+ interpret: (status) => {
5297
+ if (status === null) return NO_RESPONSE;
5298
+ if (status === 200) return {
5299
+ verdict: "ok",
5300
+ meaning: "the functions router is mounted"
5301
+ };
5302
+ if (status === 401 || status === 403) return {
5303
+ verdict: "ok",
5304
+ meaning: "the functions router is mounted (its listing requires auth)"
5305
+ };
5306
+ if (status === 404) return {
5307
+ verdict: "fail",
5308
+ meaning: "the functions router did not mount — no functions directory was found at build time, or it held no functions, so every function on this project is unreachable"
5309
+ };
5310
+ if (status >= 500) return serverError("the functions router throws");
5311
+ return {
5312
+ verdict: "unknown",
5313
+ meaning: "unexpected for the functions listing"
5314
+ };
5315
+ },
5316
+ needsBody: true,
5317
+ refine: (body, t) => {
5318
+ const names = functionNames(body);
5319
+ if (!names) return null;
5320
+ if (t.fn && !names.includes(t.fn)) return {
5321
+ verdict: "fail",
5322
+ meaning: `the router is mounted but no function is named "${t.fn}" — it loaded ${names.length}: ${names.join(", ")}`
5323
+ };
5324
+ const found = t.fn ? `, including ${t.fn}` : "";
5325
+ return {
5326
+ verdict: "ok",
5327
+ meaning: `the functions router is mounted and loaded ${names.length} function${names.length === 1 ? "" : "s"}${found}`
5328
+ };
5329
+ }
5330
+ }
5331
+ ];
5332
+ /** The function names out of a listing body, or null when it is not one. */
5333
+ function functionNames(body) {
5334
+ const list = body?.functions;
5335
+ if (!Array.isArray(list)) return null;
5336
+ const names = list.map((f) => f?.name).filter((n) => typeof n === "string");
5337
+ return names.length === list.length ? names : null;
5338
+ }
5339
+ /** Milliseconds before a probe is treated as unanswered. */
5340
+ var PROBE_TIMEOUT_MS = 1e4;
5341
+ /**
5342
+ * Ceiling on a probe body we will parse. The only body read is the function
5343
+ * listing; anything larger is a page we have no use for, and a debug command
5344
+ * must not be the thing that runs a machine out of memory.
5345
+ */
5346
+ var MAX_PROBE_BODY_BYTES = 256 * 1024;
5347
+ /**
5348
+ * Run one probe. A transport failure is a `null` status, never a thrown error:
5349
+ * "nothing answered" is a diagnosis in its own right and the other probes still
5350
+ * need to run.
5351
+ */
5352
+ async function runProbe(origin, spec, targets) {
5353
+ const url = `${origin}${spec.path(targets)}`;
5354
+ const started = Date.now();
5355
+ let status = null;
5356
+ let body;
5357
+ try {
5358
+ const res = await fetch(url, {
5359
+ method: spec.method,
5360
+ headers: spec.body ? { "Content-Type": "application/json" } : void 0,
5361
+ body: spec.body ? JSON.stringify(spec.body) : void 0,
5362
+ redirect: "manual",
5363
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
5364
+ });
5365
+ status = res.status;
5366
+ if (spec.needsBody && res.ok) {
5367
+ const text = await res.text();
5368
+ if (text.length <= MAX_PROBE_BODY_BYTES) try {
5369
+ body = JSON.parse(text);
5370
+ } catch {}
5371
+ }
5372
+ } catch {
5373
+ status = null;
5374
+ }
5375
+ const statusReading = spec.interpret(status);
5376
+ const reading = status !== null && spec.refine?.(body, targets) || statusReading;
5377
+ return {
5378
+ statusOk: statusReading.verdict === "ok",
5379
+ id: spec.id,
5380
+ label: spec.label,
5381
+ method: spec.method,
5382
+ url,
5383
+ status,
5384
+ ms: Date.now() - started,
5385
+ verdict: reading.verdict,
5386
+ meaning: reading.meaning,
5387
+ healthy: spec.healthy
5388
+ };
5389
+ }
5390
+ /** The worst verdict across probes — what the command's exit code keys off. */
5391
+ function overallVerdict(results) {
5392
+ if (results.some((r) => r.verdict === "fail")) return "fail";
5393
+ if (results.some((r) => r.verdict === "unknown")) return "unknown";
5394
+ if (results.some((r) => r.verdict === "warn")) return "warn";
5395
+ return "ok";
5396
+ }
5397
+ function verdictMark(v) {
5398
+ switch (v) {
5399
+ case "ok": return chalk.green("✓");
5400
+ case "warn": return chalk.yellow("!");
5401
+ case "fail": return chalk.red("✗");
5402
+ default: return chalk.gray("?");
5403
+ }
5404
+ }
5405
+ var LOG_PREFIX_RE = /^(?:(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+)?(?:\[([^\]]+)\]\s+)?([\s\S]*)$/;
5406
+ function parseLogLine(line) {
5407
+ const m = LOG_PREFIX_RE.exec(line);
5408
+ if (!m) return {
5409
+ ts: null,
5410
+ pod: null,
5411
+ text: line
5412
+ };
5413
+ return {
5414
+ ts: m[1] ?? null,
5415
+ pod: m[2] ?? null,
5416
+ text: m[3] ?? ""
5417
+ };
5418
+ }
5419
+ /**
5420
+ * Lines an operator scanning for a fault wants to see.
5421
+ *
5422
+ * Matches the structured `severity` field first, then the shapes that show up
5423
+ * in unstructured output. `refus`/`denied` are in the list because a permission
5424
+ * failure often logs at info level and is exactly what one is hunting for.
5425
+ */
5426
+ function isErrorLine(text) {
5427
+ return /"(?:severity|level)":\s*"(?:ERROR|WARN(?:ING)?|error|warn)"|\bError:|\bERR!|refus|denied|EACCES|ECONNREFUSED/i.test(text);
5428
+ }
5429
+ /**
5430
+ * Pull an HTTP request record out of a log line, or null when it is not one.
5431
+ *
5432
+ * Only structured request lines are recognised. Guessing at prose would produce
5433
+ * a table with invented columns, which is worse than a short one.
5434
+ */
5435
+ function parseRequestLine(text) {
5436
+ const start = text.indexOf("{");
5437
+ if (start === -1) return null;
5438
+ let parsed;
5439
+ try {
5440
+ parsed = JSON.parse(text.slice(start));
5441
+ } catch {
5442
+ return null;
5443
+ }
5444
+ if (parsed.message !== "request" && parsed.msg !== "request") return null;
5445
+ const num = (v) => {
5446
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
5447
+ return Number.isFinite(n) ? n : null;
5448
+ };
5449
+ return {
5450
+ status: num(parsed.status),
5451
+ method: typeof parsed.method === "string" ? parsed.method : "",
5452
+ path: typeof parsed.path === "string" ? parsed.path : "",
5453
+ latencyMs: num(parsed.latencyMs ?? parsed.durationMs)
5454
+ };
5455
+ }
5456
+ /**
5457
+ * Startup lines: what the server *decided* it was going to do. Which storage
5458
+ * backend it bound, which functions it loaded, whether auth tables were found.
5459
+ * This is the fastest way to tell a misconfiguration from a runtime fault.
5460
+ */
5461
+ function isBootLine(text) {
5462
+ return /storage|Loaded function|Mounted|Auth tables|Server running|listening|Refusing|migrat/i.test(text);
5463
+ }
5464
+ /** Render a number of seconds back as the compact duration a user would type. */
5465
+ function formatDuration(seconds) {
5466
+ if (seconds % 86400 === 0 && seconds >= 86400) return `${seconds / 86400}d`;
5467
+ if (seconds % 3600 === 0 && seconds >= 3600) return `${seconds / 3600}h`;
5468
+ if (seconds % 60 === 0 && seconds >= 60) return `${seconds / 60}m`;
5469
+ return `${seconds}s`;
5470
+ }
5471
+ /**
5472
+ * Parse a duration like `15m`, `2h`, `90s`, `1d` (or a bare number of seconds)
5473
+ * into seconds. Returns null when it is not a duration.
5474
+ */
5475
+ function parseSince(input) {
5476
+ if (!input) return null;
5477
+ const m = /^(\d+(?:\.\d+)?)\s*([smhd])?$/i.exec(input.trim());
5478
+ if (!m) return null;
5479
+ const value = parseFloat(m[1]);
5480
+ const unit = (m[2] || "s").toLowerCase();
5481
+ return Math.round(value * (unit === "s" ? 1 : unit === "m" ? 60 : unit === "h" ? 3600 : 86400));
5482
+ }
5483
+ async function fetchRuntimeLogs(client, projectId, opts) {
5484
+ const params = new URLSearchParams();
5485
+ if (opts.sinceSeconds !== void 0) params.set("sinceSeconds", String(opts.sinceSeconds));
5486
+ if (opts.tailLines !== void 0) params.set("tailLines", String(opts.tailLines));
5487
+ if (opts.previous) params.set("previous", "true");
5488
+ params.set("timestamps", "true");
5489
+ const qs = params.toString();
5490
+ return client.functions.invoke("runtime-logs", void 0, {
5491
+ method: "GET",
5492
+ path: `${projectId}${qs ? `?${qs}` : ""}`
5493
+ });
5494
+ }
5495
+ /** Print the per-pod states runtime-logs reports, including its hints. */
5496
+ function printPodStates(res) {
5497
+ if (res.state === "no_pods") {
5498
+ console.log(chalk.yellow(` ${res.message ?? "No pods are running for this project."}`));
5499
+ console.log("");
5500
+ return;
5501
+ }
5502
+ for (const p of res.pods ?? []) {
5503
+ if (p.state === "ok") continue;
5504
+ console.log(` ${chalk.yellow(p.pod)} ${chalk.gray(`(${p.state})`)}`);
5505
+ if (p.message) console.log(chalk.gray(` ${p.message}`));
5506
+ if (p.hint) console.log(chalk.cyan(` → ${p.hint}`));
5507
+ }
5508
+ if (res.truncated) console.log(chalk.gray(" (output truncated — narrow the window with --since)"));
5509
+ }
5510
+ /** Resolve the public origin a project is served at. */
5511
+ async function resolveOrigin(rawArgs, client, url, projectId) {
5512
+ const parsed = arg({ "--host": String }, {
5513
+ argv: rawArgs.slice(3),
5514
+ permissive: true
5515
+ });
5516
+ if (parsed["--host"]) {
5517
+ const h = parsed["--host"].trim().replace(/\/+$/, "");
5518
+ return /^https?:\/\//.test(h) ? h : `https://${h}`;
5519
+ }
5520
+ const [project, baseDomain] = await Promise.all([client.data.collection("projects").findById(projectId), fetchTenantBaseDomain(client, url)]);
5521
+ if (!project) fail(`Project ${displayProjectRef(rawArgs)} not found.`);
5522
+ const host = projectHost(project, baseDomain);
5523
+ if (!host) fail("Could not determine the public URL for this project.", "It may never have been deployed. Pass --host <hostname> to probe an address directly.");
5524
+ return `https://${host}`;
5525
+ }
5526
+ async function healthCommand(rawArgs) {
5527
+ const parsed = arg({
5528
+ "--collection": String,
5529
+ "--function": String
5530
+ }, {
5531
+ argv: rawArgs.slice(3),
5532
+ permissive: true
5533
+ });
5534
+ const targets = {
5535
+ collection: parsed["--collection"] || "users",
5536
+ fn: parsed["--function"]
5537
+ };
5538
+ const { client, url } = await requireClient(rawArgs);
5539
+ const origin = await resolveOrigin(rawArgs, client, url, await requireProject(rawArgs, client));
5540
+ const results = [];
5541
+ for (const spec of PROBES) results.push(await runProbe(origin, spec, targets));
5542
+ const overall = overallVerdict(results);
5543
+ emit(() => {
5544
+ console.log("");
5545
+ console.log(chalk.bold(` 🩺 Health — ${displayProjectRef(rawArgs)}`) + chalk.gray(` ${origin}`));
5546
+ console.log("");
5547
+ const width = Math.max(...results.map((r) => r.label.length));
5548
+ for (const r of results) {
5549
+ const code = r.status === null ? chalk.red("---") : String(r.status);
5550
+ console.log(` ${verdictMark(r.verdict)} ${chalk.bold(r.label.padEnd(width))} ${code.padStart(3)} ${chalk.gray(`${r.ms}ms`)}`);
5551
+ console.log(` ${chalk.gray(r.meaning)}`);
5552
+ }
5553
+ console.log("");
5554
+ if (overall === "ok") console.log(chalk.green(" Everything reachable and wired as expected."));
5555
+ else {
5556
+ const bad = results.filter((r) => r.verdict === "fail" || r.verdict === "warn");
5557
+ console.log(chalk.gray(` ${bad.length} of ${results.length} check${bad.length === 1 ? "" : "s"} need attention.`));
5558
+ const wrongStatus = bad.filter((r) => !r.statusOk);
5559
+ if (wrongStatus.length > 0) {
5560
+ console.log(chalk.gray(" A healthy deployment answers:"));
5561
+ for (const r of wrongStatus) console.log(chalk.gray(` ${r.label.padEnd(width)} ${r.healthy}`));
5562
+ }
5563
+ }
5564
+ console.log("");
5565
+ }, {
5566
+ origin,
5567
+ overall,
5568
+ probes: results
5569
+ });
5570
+ if (overall === "fail") process.exit(1);
5571
+ }
5572
+ async function logView(rawArgs, view) {
5573
+ const parsed = arg({
5574
+ "--since": String,
5575
+ "--tail": Number,
5576
+ "--previous": Boolean
5577
+ }, {
5578
+ argv: rawArgs.slice(3),
5579
+ permissive: true
5580
+ });
5581
+ const sinceArg = parsed["--since"];
5582
+ if (sinceArg !== void 0 && parseSince(sinceArg) === null) fail(`--since must be a duration like 15m, 2h or 90s; received "${sinceArg}".`);
5583
+ const sinceSeconds = parseSince(sinceArg) ?? view.defaultSinceSeconds;
5584
+ const { client } = await requireClient(rawArgs);
5585
+ const projectId = await requireProject(rawArgs, client);
5586
+ let res;
5587
+ try {
5588
+ res = await fetchRuntimeLogs(client, projectId, {
5589
+ sinceSeconds,
5590
+ tailLines: parsed["--tail"] ?? 500,
5591
+ previous: Boolean(parsed["--previous"])
5592
+ });
5593
+ } catch (e) {
5594
+ reportError(e, "Failed to fetch runtime logs");
5595
+ }
5596
+ const parsedLines = (res.logs ?? "").split("\n").filter((l) => l !== "").map(parseLogLine);
5597
+ const kept = view.filter ? parsedLines.filter((l) => view.filter(l.text)) : parsedLines;
5598
+ const shown = kept.slice(-view.limit);
5599
+ emit(() => {
5600
+ console.log("");
5601
+ console.log(chalk.bold(` ${view.title} — ${displayProjectRef(rawArgs)}`) + chalk.gray(` last ${formatDuration(sinceSeconds)}`));
5602
+ console.log("");
5603
+ printPodStates(res);
5604
+ if (shown.length === 0) {
5605
+ console.log(chalk.gray(" (nothing matched in this window)"));
5606
+ console.log("");
5607
+ return;
5608
+ }
5609
+ for (const l of shown) console.log(` ${l.pod ? chalk.gray(`[${l.pod}] `) : ""}${l.text}`);
5610
+ console.log("");
5611
+ if (kept.length > shown.length) {
5612
+ console.log(chalk.gray(` (showing the last ${shown.length} of ${kept.length} matching lines)`));
5613
+ console.log("");
5614
+ }
5615
+ }, {
5616
+ sinceSeconds,
5617
+ state: res.state ?? null,
5618
+ pods: res.pods ?? [],
5619
+ truncated: Boolean(res.truncated),
5620
+ matched: kept.length,
5621
+ lines: shown
5622
+ });
5623
+ }
5624
+ async function requestsCommand(rawArgs) {
5625
+ const parsed = arg({
5626
+ "--since": String,
5627
+ "--tail": Number
5628
+ }, {
5629
+ argv: rawArgs.slice(3),
5630
+ permissive: true
5631
+ });
5632
+ const sinceArg = parsed["--since"];
5633
+ if (sinceArg !== void 0 && parseSince(sinceArg) === null) fail(`--since must be a duration like 15m, 2h or 90s; received "${sinceArg}".`);
5634
+ const sinceSeconds = parseSince(sinceArg) ?? 900;
5635
+ const { client } = await requireClient(rawArgs);
5636
+ const projectId = await requireProject(rawArgs, client);
5637
+ let res;
5638
+ try {
5639
+ res = await fetchRuntimeLogs(client, projectId, {
5640
+ sinceSeconds,
5641
+ tailLines: parsed["--tail"] ?? 1e3
5642
+ });
5643
+ } catch (e) {
5644
+ reportError(e, "Failed to fetch runtime logs");
5645
+ }
5646
+ const shown = (res.logs ?? "").split("\n").filter((l) => l !== "").map((l) => parseRequestLine(parseLogLine(l).text)).filter((e) => e !== null).slice(-40);
5647
+ emit(() => {
5648
+ console.log("");
5649
+ console.log(chalk.bold(` 🌐 Requests — ${displayProjectRef(rawArgs)}`) + chalk.gray(` last ${formatDuration(sinceSeconds)}`));
5650
+ console.log("");
5651
+ printPodStates(res);
5652
+ if (shown.length === 0) {
5653
+ console.log(chalk.gray(" No structured request lines in this window."));
5654
+ console.log(chalk.gray(" (this view needs the server's request logging; try `debug logs`)"));
5655
+ console.log("");
5656
+ return;
5657
+ }
5658
+ for (const e of shown) {
5659
+ const status = e.status ?? 0;
5660
+ const color = status >= 500 ? chalk.red : status >= 400 ? chalk.yellow : chalk.green;
5661
+ console.log(` ${color(String(e.status ?? "---").padStart(3))} ${e.method.padEnd(6)} ${e.path.slice(0, 70).padEnd(70)} ${chalk.gray(e.latencyMs === null ? "" : `${e.latencyMs}ms`)}`);
5662
+ }
5663
+ console.log("");
5664
+ }, {
5665
+ sinceSeconds,
5666
+ requests: shown
5667
+ });
5668
+ }
5669
+ async function podCommand(rawArgs) {
5670
+ const { client } = await requireClient(rawArgs);
5671
+ const projectId = await requireProject(rawArgs, client);
5672
+ let m;
5673
+ try {
5674
+ m = await client.functions.invoke("metrics", void 0, {
5675
+ method: "GET",
5676
+ path: projectId
5677
+ });
5678
+ } catch (e) {
5679
+ reportError(e, "Failed to read workload placement");
5680
+ }
5681
+ const p = m.placement ?? {};
5682
+ const replicas = p.replicas ?? {};
5683
+ emit(() => {
5684
+ console.log("");
5685
+ console.log(chalk.bold(` ☸ Workload — ${displayProjectRef(rawArgs)}`));
5686
+ console.log("");
5687
+ keyValues([
5688
+ ["Status", m.status ? colorStatus(m.status === "running" ? "active" : m.status) : void 0],
5689
+ ["Replicas", replicas.desired === void 0 ? void 0 : `${replicas.available ?? 0} / ${replicas.desired} available`],
5690
+ ["Namespace", p.namespace],
5691
+ ["Cluster", p.cluster],
5692
+ ["Region", [p.provider, p.region].filter(Boolean).join(" · ") || void 0],
5693
+ ["Host", p.host],
5694
+ ["Image", p.image],
5695
+ ["CPU", m.cpu ?? void 0],
5696
+ ["Memory", m.memory ?? void 0]
5697
+ ]);
5698
+ console.log("");
5699
+ if ((replicas.available ?? 0) === 0 && (replicas.desired ?? 0) > 0) {
5700
+ console.log(chalk.yellow(" No replica is available — the pod is not passing its readiness check."));
5701
+ console.log(chalk.gray(" See why with: ") + chalk.bold("rebase cloud debug logs --previous"));
5702
+ console.log("");
5703
+ }
5704
+ }, {
5705
+ status: m.status ?? null,
5706
+ placement: p
5707
+ });
5708
+ }
5709
+ async function dbDebugCommand(rawArgs) {
5710
+ const { client } = await requireClient(rawArgs);
5711
+ const projectId = await requireProject(rawArgs, client);
5712
+ let info;
5713
+ try {
5714
+ info = await client.functions.invoke("db-info", void 0, {
5715
+ method: "GET",
5716
+ path: projectId
5717
+ });
5718
+ } catch (e) {
5719
+ reportError(e, "Failed to read database connection info");
5720
+ }
5721
+ const pf = info.portForward;
5722
+ const forwardCmd = pf ? `kubectl port-forward -n ${pf.namespace} svc/${pf.service} ${pf.localPort}:${pf.remotePort}` : null;
5723
+ const psqlCmd = pf && info.username && info.database ? `psql -h 127.0.0.1 -p ${pf.localPort} -U ${info.username} -d ${info.database}` : null;
5724
+ emit(() => {
5725
+ console.log("");
5726
+ console.log(chalk.bold(` 🐘 Database — ${displayProjectRef(rawArgs)}`));
5727
+ console.log("");
5728
+ if (info.unavailableReason) {
5729
+ console.log(chalk.yellow(` ${info.unavailableReason}`));
5730
+ console.log("");
5731
+ return;
5732
+ }
5733
+ keyValues([
5734
+ ["Type", info.type],
5735
+ ["Host", info.host],
5736
+ ["Port", info.port],
5737
+ ["Database", info.database],
5738
+ ["Username", info.username],
5739
+ ["Password", info.passwordAvailable ? chalk.gray("stored — not shown here") : chalk.yellow("none stored")]
5740
+ ]);
5741
+ console.log("");
5742
+ if (forwardCmd) {
5743
+ console.log(chalk.gray(" A managed database is only reachable inside its cluster. To connect:"));
5744
+ console.log("");
5745
+ console.log(` ${forwardCmd}`);
5746
+ if (psqlCmd) console.log(` ${psqlCmd}`);
5747
+ console.log("");
5748
+ if (info.passwordAvailable) {
5749
+ console.log(chalk.gray(" Get the password with: ") + chalk.bold("rebase cloud db info --reveal"));
5750
+ console.log("");
5751
+ }
5752
+ }
5753
+ }, {
5754
+ type: info.type ?? null,
5755
+ host: info.host ?? null,
5756
+ port: info.port ?? null,
5757
+ database: info.database ?? null,
5758
+ username: info.username ?? null,
5759
+ passwordAvailable: Boolean(info.passwordAvailable),
5760
+ portForwardCommand: forwardCmd,
5761
+ psqlCommand: psqlCmd
5762
+ });
5763
+ }
5764
+ async function debugCommand(action, rawArgs) {
5765
+ switch (action) {
5766
+ case void 0:
5767
+ case "health":
5768
+ await healthCommand(rawArgs);
5769
+ break;
5770
+ case "logs":
5771
+ await logView(rawArgs, {
5772
+ defaultSinceSeconds: 900,
5773
+ limit: 200,
5774
+ title: "📄 Logs"
5775
+ });
5776
+ break;
5777
+ case "errors":
5778
+ await logView(rawArgs, {
5779
+ filter: isErrorLine,
5780
+ defaultSinceSeconds: 3600,
5781
+ limit: 40,
5782
+ title: "🔥 Errors"
5783
+ });
5784
+ break;
5785
+ case "boot":
5786
+ await logView(rawArgs, {
5787
+ filter: isBootLine,
5788
+ defaultSinceSeconds: 168 * 3600,
5789
+ limit: 25,
5790
+ title: "🚀 Boot"
5791
+ });
5792
+ break;
5793
+ case "requests":
5794
+ await requestsCommand(rawArgs);
5795
+ break;
5796
+ case "pod":
5797
+ case "workload":
5798
+ await podCommand(rawArgs);
5799
+ break;
5800
+ case "db":
5801
+ await dbDebugCommand(rawArgs);
5802
+ break;
5803
+ case "help":
5804
+ case "--help":
5805
+ printDebugHelp();
5806
+ break;
5807
+ default:
5808
+ if (isJsonMode()) fail(`Unknown debug command: ${action}`, void 0, "unknown_command");
5809
+ console.error(chalk.red(`Unknown debug command: ${action}`));
5810
+ console.log("");
5811
+ printDebugHelp();
5812
+ process.exit(1);
5813
+ }
5814
+ }
5815
+ function printDebugHelp() {
5816
+ console.log(`
5817
+ ${chalk.bold("rebase cloud debug")} — Find out why a deployed project is misbehaving
5818
+
5819
+ ${chalk.green.bold("Usage")}
5820
+ rebase cloud debug ${chalk.blue("<subcommand>")} [options]
5821
+
5822
+ ${chalk.green.bold("End-to-end")}
5823
+ ${chalk.blue.bold("health")} Probe the live URL and explain every status code ${chalk.gray("(default)")}
5824
+
5825
+ ${chalk.green.bold("Runtime")}
5826
+ ${chalk.blue.bold("logs")} ${chalk.gray("[--since 15m]")} Recent application logs
5827
+ ${chalk.blue.bold("errors")} ${chalk.gray("[--since 1h]")} Error and warning lines only
5828
+ ${chalk.blue.bold("requests")} ${chalk.gray("[--since 15m]")} HTTP requests the server logged ${chalk.gray("(status, path, latency)")}
5829
+ ${chalk.blue.bold("boot")} What the server decided at startup ${chalk.gray("(storage, functions, auth)")}
5830
+ ${chalk.blue.bold("pod")} Replicas, image, namespace, cluster placement
5831
+
5832
+ ${chalk.green.bold("Data")}
5833
+ ${chalk.blue.bold("db")} Connection shape + the port-forward recipe ${chalk.gray("(no password)")}
5834
+
5835
+ ${chalk.green.bold("Options")}
5836
+ ${chalk.blue("--since <dur>")} Lookback window: ${chalk.gray("90s, 15m, 2h, 1d")}
5837
+ ${chalk.blue("--tail <n>")} Lines to read per pod ${chalk.gray("(default 500)")}
5838
+ ${chalk.blue("--previous")} Read the CRASHED container instance ${chalk.gray("(where the reason lives)")}
5839
+ ${chalk.blue("--host <hostname>")} Probe this address instead of the project's own
5840
+ ${chalk.blue("--collection <name>")} Collection for the unauth-read probe ${chalk.gray("(default: users)")}
5841
+ ${chalk.blue("--function <name>")} Also assert this function loaded ${chalk.gray("(checked against the listing)")}
5842
+ ${chalk.blue("--project, -p <slug>")} Operate on a project without linking
5843
+
5844
+ ${chalk.gray("Everything here is read-only. `health` exits non-zero when a check fails,")}
5845
+ ${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase cloud restart`.")}
5846
+ `);
5847
+ }
5848
+ //#endregion
5112
5849
  //#region src/commands/cloud/resources.ts
5113
5850
  /**
5114
5851
  * `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
@@ -5221,7 +5958,10 @@ async function webhooksCommand(subcommand, rawArgs) {
5221
5958
  reportError(e, "Webhook operation failed");
5222
5959
  }
5223
5960
  }
5224
- async function storageCommand(rawArgs) {
5961
+ async function storageCommand(action, rawArgs) {
5962
+ if (action === "create") return storageCreateCommand(rawArgs);
5963
+ if (action === "attach") return storageAttachCommand(rawArgs);
5964
+ if (action === "help") return printStorageHelp();
5225
5965
  const { client } = await requireClient(rawArgs);
5226
5966
  const projectId = await requireProject(rawArgs, client);
5227
5967
  try {
@@ -5246,6 +5986,107 @@ async function storageCommand(rawArgs) {
5246
5986
  reportError(e, "Failed to list storage");
5247
5987
  }
5248
5988
  }
5989
+ function printStorageHelp() {
5990
+ console.log("");
5991
+ console.log(chalk.bold(" rebase cloud storage"));
5992
+ console.log("");
5993
+ console.log(" " + chalk.blue.bold("storage") + " List this project's storage");
5994
+ console.log(" " + chalk.blue.bold("storage create") + " Provision platform-managed storage");
5995
+ console.log(" " + chalk.blue.bold("storage attach") + " Attach your own S3-compatible bucket");
5996
+ console.log("");
5997
+ console.log(chalk.gray(" attach options:"));
5998
+ console.log(chalk.gray(" --bucket <name> Bucket name (required)"));
5999
+ console.log(chalk.gray(" --access-key-id <id> Access key ID (required)"));
6000
+ console.log(chalk.gray(" --secret-access-key <s> Secret access key (required)"));
6001
+ console.log(chalk.gray(" --endpoint <url> S3 endpoint; omit for AWS"));
6002
+ console.log(chalk.gray(" --region <region> Region"));
6003
+ console.log(chalk.gray(" --force-path-style Required by MinIO and some gateways"));
6004
+ console.log("");
6005
+ console.log(chalk.gray(" Without either, a tenant falls back to the container filesystem and"));
6006
+ console.log(chalk.gray(" loses uploaded files on its next restart."));
6007
+ console.log("");
6008
+ }
6009
+ async function storageCreateCommand(rawArgs) {
6010
+ const { client } = await requireClient(rawArgs);
6011
+ const projectId = await requireProject(rawArgs, client);
6012
+ try {
6013
+ console.log("");
6014
+ console.log(chalk.gray(" Provisioning managed storage — this creates a bucket and its credentials..."));
6015
+ const res = await client.functions.invoke(`storage-provision/${encodeURIComponent(projectId)}`, void 0, { method: "POST" });
6016
+ const info = res.data ?? res.data;
6017
+ success(`Managed storage provisioned for ${displayProjectRef(rawArgs)}.`);
6018
+ keyValues([
6019
+ ["Bucket", info.bucketName],
6020
+ ["Region", info.region],
6021
+ ["Endpoint", info.endpoint],
6022
+ ["Access key", info.accessKeyId]
6023
+ ]);
6024
+ console.log("");
6025
+ console.log(chalk.gray(" The secret key is stored encrypted and injected at deploy time; it is not displayed."));
6026
+ console.log(chalk.gray(" Redeploy for the tenant to pick it up: ") + chalk.bold("rebase cloud deploy"));
6027
+ console.log("");
6028
+ } catch (e) {
6029
+ reportError(e, "Failed to provision managed storage");
6030
+ }
6031
+ }
6032
+ async function storageAttachCommand(rawArgs) {
6033
+ const parsed = arg({
6034
+ "--bucket": String,
6035
+ "--access-key-id": String,
6036
+ "--secret-access-key": String,
6037
+ "--endpoint": String,
6038
+ "--region": String,
6039
+ "--force-path-style": Boolean
6040
+ }, {
6041
+ argv: rawArgs.slice(3),
6042
+ permissive: true
6043
+ });
6044
+ const bucket = parsed["--bucket"];
6045
+ const accessKeyId = parsed["--access-key-id"];
6046
+ const secretAccessKey = parsed["--secret-access-key"];
6047
+ const missing = [
6048
+ !bucket && "--bucket",
6049
+ !accessKeyId && "--access-key-id",
6050
+ !secretAccessKey && "--secret-access-key"
6051
+ ].filter(Boolean);
6052
+ if (missing.length > 0) fail(`Missing ${missing.join(", ")}.`, "A bucket without credentials cannot be used, and would be stored as though it could. Run `rebase cloud storage --help` for the full list.");
6053
+ const { client } = await requireClient(rawArgs);
6054
+ const projectId = await requireProject(rawArgs, client);
6055
+ try {
6056
+ const existing = (await client.data.collection("storages").find({
6057
+ where: { project: ["==", projectId] },
6058
+ limit: 1
6059
+ })).data[0];
6060
+ const row = {
6061
+ project: projectId,
6062
+ type: "byos",
6063
+ status: "active",
6064
+ s3Bucket: bucket,
6065
+ s3AccessKeyId: accessKeyId,
6066
+ s3SecretAccessKey: secretAccessKey,
6067
+ bucketName: bucket
6068
+ };
6069
+ if (parsed["--endpoint"]) row.s3Endpoint = parsed["--endpoint"];
6070
+ if (parsed["--region"]) {
6071
+ row.s3Region = parsed["--region"];
6072
+ row.region = parsed["--region"];
6073
+ }
6074
+ if (parsed["--force-path-style"]) row.s3ForcePathStyle = true;
6075
+ if (existing?.id) await client.data.collection("storages").update(String(existing.id), row);
6076
+ else await client.data.collection("storages").create(row);
6077
+ success(`Storage attached to ${displayProjectRef(rawArgs)}.`);
6078
+ keyValues([
6079
+ ["Bucket", bucket],
6080
+ ["Endpoint", parsed["--endpoint"] ?? "AWS S3"],
6081
+ ["Region", parsed["--region"] ?? "(default)"]
6082
+ ]);
6083
+ console.log("");
6084
+ console.log(chalk.gray(" Redeploy for the tenant to pick it up: ") + chalk.bold("rebase cloud deploy"));
6085
+ console.log("");
6086
+ } catch (e) {
6087
+ reportError(e, "Failed to attach storage");
6088
+ }
6089
+ }
5249
6090
  async function clustersCommand(rawArgs) {
5250
6091
  const { client } = await requireClient(rawArgs);
5251
6092
  try {
@@ -5437,6 +6278,9 @@ async function cloudCommand(subcommand, rawArgs) {
5437
6278
  case "metrics":
5438
6279
  await metricsCommand(rawArgs);
5439
6280
  break;
6281
+ case "debug":
6282
+ await debugCommand(action, rawArgs);
6283
+ break;
5440
6284
  case "env":
5441
6285
  await envCommand(action, rawArgs);
5442
6286
  break;
@@ -5463,7 +6307,7 @@ async function cloudCommand(subcommand, rawArgs) {
5463
6307
  await webhooksCommand(action, rawArgs);
5464
6308
  break;
5465
6309
  case "storage":
5466
- await storageCommand(rawArgs);
6310
+ await storageCommand(action, rawArgs);
5467
6311
  break;
5468
6312
  case "clusters":
5469
6313
  await clustersCommand(rawArgs);
@@ -5548,6 +6392,7 @@ ${chalk.green.bold("Deploy & observe")}
5548
6392
  ${chalk.blue.bold("start|stop|restart")} ${chalk.gray("[-y]")} Power ops ${chalk.gray("(stop/restart need -y)")}
5549
6393
  ${chalk.blue.bold("status")} One-glance project status
5550
6394
  ${chalk.blue.bold("metrics")} Live CPU / memory / disk
6395
+ ${chalk.blue.bold("debug")} ${chalk.gray("[health|logs|…]")} Diagnose a misbehaving deployment ${chalk.gray("(read-only)")}
5551
6396
 
5552
6397
  ${chalk.green.bold("Config")}
5553
6398
  ${chalk.blue.bold("env list|set|unset|reveal|pull")}
@@ -5566,6 +6411,8 @@ ${chalk.green.bold("Databases")}
5566
6411
  ${chalk.green.bold("Other resources")}
5567
6412
  ${chalk.blue.bold("webhooks list|create|delete")}
5568
6413
  ${chalk.blue.bold("storage")} List storage buckets
6414
+ ${chalk.blue.bold("storage create")} Provision platform-managed storage
6415
+ ${chalk.blue.bold("storage attach")} Attach your own S3-compatible bucket
5569
6416
  ${chalk.blue.bold("clusters")} List compute clusters
5570
6417
  ${chalk.blue.bold("billing setup")} Attach a card to the org ${chalk.gray("(one-time, opens browser)")}
5571
6418
  ${chalk.blue.bold("billing")} Show billing account + card on file