@rebasepro/cli 0.9.1-canary.f0ac103 → 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/bin/rebase.js +67 -1
- package/dist/commands/cloud/context.d.ts +0 -6
- package/dist/commands/cloud/debug.d.ts +117 -0
- package/dist/commands/cloud/deployments.d.ts +0 -1
- package/dist/commands/cloud/power.d.ts +0 -1
- package/dist/commands/cloud/resources.d.ts +1 -1
- package/dist/commands/skills.d.ts +1 -1
- package/dist/index.es.js +924 -22
- package/dist/index.es.js.map +1 -1
- package/package.json +7 -7
- package/templates/template/config/collections/users.ts +2 -0
package/dist/index.es.js
CHANGED
|
@@ -135,7 +135,8 @@ function getPMCommands(pm) {
|
|
|
135
135
|
runWorkspace: (workspace, script) => [
|
|
136
136
|
"pnpm",
|
|
137
137
|
"--filter",
|
|
138
|
-
workspace
|
|
138
|
+
`./${workspace}`,
|
|
139
|
+
"run",
|
|
139
140
|
script
|
|
140
141
|
],
|
|
141
142
|
dlx: (pkg, args) => [
|
|
@@ -942,6 +943,13 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
942
943
|
cloudUrl: resolveCloudUrl(rawArgs)
|
|
943
944
|
};
|
|
944
945
|
}
|
|
946
|
+
if (!process.stdin.isTTY) {
|
|
947
|
+
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
948
|
+
console.error(chalk.yellow(" Re-run with --yes to accept defaults, passing any choices as flags, e.g.:"));
|
|
949
|
+
console.error(chalk.yellow(` rebase init ${nameArg || "my-app"} --yes --template blog --flavor cms`));
|
|
950
|
+
console.error(chalk.gray(" Options: --template <blog|ecommerce|blank> --flavor <cms|baas> --database-url <url> --install --git"));
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
945
953
|
const questions = buildInitQuestions({
|
|
946
954
|
nameArg,
|
|
947
955
|
templateArg,
|
|
@@ -2469,22 +2477,43 @@ function installForAgent(agentKey, skills, projectDir) {
|
|
|
2469
2477
|
}
|
|
2470
2478
|
return count;
|
|
2471
2479
|
}
|
|
2472
|
-
async function skillsCommand(subcommand,
|
|
2480
|
+
async function skillsCommand(subcommand, rawArgs) {
|
|
2473
2481
|
switch (subcommand) {
|
|
2474
2482
|
case "install":
|
|
2475
|
-
await skillsInstall();
|
|
2483
|
+
await skillsInstall(rawArgs);
|
|
2476
2484
|
break;
|
|
2477
2485
|
case "--help":
|
|
2478
2486
|
case void 0:
|
|
2479
2487
|
printSkillsHelp();
|
|
2480
2488
|
break;
|
|
2481
2489
|
default:
|
|
2482
|
-
console.
|
|
2490
|
+
console.error(chalk.red(`Unknown skills subcommand: ${subcommand}`));
|
|
2483
2491
|
console.log("");
|
|
2484
2492
|
printSkillsHelp();
|
|
2493
|
+
process.exit(1);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Agents named explicitly on the command line, e.g. `--agent claude --agent cursor`
|
|
2498
|
+
* (also accepts a comma-separated list). Returns null when none were given.
|
|
2499
|
+
*/
|
|
2500
|
+
function parseAgentFlags(rawArgs) {
|
|
2501
|
+
const requested = [];
|
|
2502
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
2503
|
+
if (rawArgs[i] !== "--agent" && rawArgs[i] !== "-a") continue;
|
|
2504
|
+
const value = rawArgs[i + 1];
|
|
2505
|
+
if (value && !value.startsWith("-")) requested.push(...value.split(",").map((v) => v.trim()).filter(Boolean));
|
|
2506
|
+
}
|
|
2507
|
+
if (requested.length === 0) return null;
|
|
2508
|
+
const valid = Object.keys(AGENTS);
|
|
2509
|
+
const unknown = requested.filter((a) => !valid.includes(a));
|
|
2510
|
+
if (unknown.length > 0) {
|
|
2511
|
+
console.error(chalk.red(`Unknown agent(s): ${unknown.join(", ")}. Available: ${valid.join(", ")}`));
|
|
2512
|
+
process.exit(1);
|
|
2485
2513
|
}
|
|
2514
|
+
return requested;
|
|
2486
2515
|
}
|
|
2487
|
-
async function skillsInstall() {
|
|
2516
|
+
async function skillsInstall(rawArgs = []) {
|
|
2488
2517
|
const projectDir = process.cwd();
|
|
2489
2518
|
let skillsDir;
|
|
2490
2519
|
try {
|
|
@@ -2498,8 +2527,14 @@ async function skillsInstall() {
|
|
|
2498
2527
|
console.error(`${chalk.red.bold("ERROR")} No skills found in ${skillsDir}`);
|
|
2499
2528
|
process.exit(1);
|
|
2500
2529
|
}
|
|
2501
|
-
let agents = detectAgents(projectDir);
|
|
2530
|
+
let agents = parseAgentFlags(rawArgs) ?? detectAgents(projectDir);
|
|
2502
2531
|
if (agents.length === 0) {
|
|
2532
|
+
if (!process.stdin.isTTY) {
|
|
2533
|
+
console.error(chalk.red("Cannot prompt: this is a non-interactive terminal (no TTY)."));
|
|
2534
|
+
console.error(chalk.yellow(` Name the agents explicitly, e.g. rebase skills install --agent ${Object.keys(AGENTS)[0]}`));
|
|
2535
|
+
console.error(chalk.gray(` Available: ${Object.keys(AGENTS).join(", ")}`));
|
|
2536
|
+
process.exit(1);
|
|
2537
|
+
}
|
|
2503
2538
|
const choices = Object.entries(AGENTS).map(([key, agent]) => ({
|
|
2504
2539
|
name: agent.label,
|
|
2505
2540
|
value: key,
|
|
@@ -2541,8 +2576,15 @@ ${chalk.green.bold("Subcommands")}
|
|
|
2541
2576
|
${chalk.blue.bold("install")} Install Rebase agent skills for your AI coding assistant
|
|
2542
2577
|
Supports: Cursor, Claude Code, Windsurf, Gemini CLI, Antigravity
|
|
2543
2578
|
|
|
2579
|
+
${chalk.green.bold("Options")}
|
|
2580
|
+
${chalk.blue("--agent, -a")} Agent(s) to install for, skipping detection and the prompt.
|
|
2581
|
+
Repeat the flag or pass a comma-separated list.
|
|
2582
|
+
Available: ${Object.keys(AGENTS).join(", ")}
|
|
2583
|
+
|
|
2544
2584
|
${chalk.green.bold("Examples")}
|
|
2545
2585
|
${chalk.cyan("rebase skills install")}
|
|
2586
|
+
${chalk.cyan("rebase skills install --agent claude")}
|
|
2587
|
+
${chalk.cyan("rebase skills install --agent claude,cursor")}
|
|
2546
2588
|
`);
|
|
2547
2589
|
}
|
|
2548
2590
|
//#endregion
|
|
@@ -2574,9 +2616,16 @@ function loadEnv(projectRoot) {
|
|
|
2574
2616
|
}
|
|
2575
2617
|
return env;
|
|
2576
2618
|
}
|
|
2577
|
-
function resolveBaseUrl(env) {
|
|
2578
|
-
|
|
2579
|
-
|
|
2619
|
+
function resolveBaseUrl(env, projectRoot) {
|
|
2620
|
+
if (env.REBASE_BASE_URL) return env.REBASE_BASE_URL;
|
|
2621
|
+
if (projectRoot) try {
|
|
2622
|
+
const urlFile = path.join(projectRoot, ".rebase-dev-url");
|
|
2623
|
+
if (fs.existsSync(urlFile)) {
|
|
2624
|
+
const devUrl = fs.readFileSync(urlFile, "utf-8").trim();
|
|
2625
|
+
if (devUrl) return devUrl;
|
|
2626
|
+
}
|
|
2627
|
+
} catch {}
|
|
2628
|
+
return `http://localhost:${env.PORT || env.REBASE_PORT || "3001"}`;
|
|
2580
2629
|
}
|
|
2581
2630
|
async function apiKeysCommand(subcommand, rawArgs) {
|
|
2582
2631
|
if (!subcommand || subcommand === "--help") {
|
|
@@ -2601,8 +2650,9 @@ async function apiKeysCommand(subcommand, rawArgs) {
|
|
|
2601
2650
|
}
|
|
2602
2651
|
}
|
|
2603
2652
|
async function listKeys(_rawArgs) {
|
|
2604
|
-
const
|
|
2605
|
-
const
|
|
2653
|
+
const projectRoot = requireProjectRoot();
|
|
2654
|
+
const env = loadEnv(projectRoot);
|
|
2655
|
+
const baseUrl = resolveBaseUrl(env, projectRoot);
|
|
2606
2656
|
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2607
2657
|
if (!serviceKey) {
|
|
2608
2658
|
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
@@ -2707,8 +2757,9 @@ async function createKey(rawArgs) {
|
|
|
2707
2757
|
expires_at = parsed.toISOString();
|
|
2708
2758
|
}
|
|
2709
2759
|
}
|
|
2710
|
-
const
|
|
2711
|
-
const
|
|
2760
|
+
const projectRoot = requireProjectRoot();
|
|
2761
|
+
const env = loadEnv(projectRoot);
|
|
2762
|
+
const baseUrl = resolveBaseUrl(env, projectRoot);
|
|
2712
2763
|
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2713
2764
|
if (!serviceKey) {
|
|
2714
2765
|
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
@@ -2765,8 +2816,9 @@ async function revokeKey(rawArgs) {
|
|
|
2765
2816
|
console.log(chalk.gray(" Usage: rebase api-keys revoke <key-id>"));
|
|
2766
2817
|
process.exit(1);
|
|
2767
2818
|
}
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2819
|
+
const projectRoot = requireProjectRoot();
|
|
2820
|
+
const env = loadEnv(projectRoot);
|
|
2821
|
+
const baseUrl = resolveBaseUrl(env, projectRoot);
|
|
2770
2822
|
const serviceKey = env.SERVICE_KEY || env.REBASE_SERVICE_KEY;
|
|
2771
2823
|
if (!serviceKey) {
|
|
2772
2824
|
console.error(chalk.red("✗ SERVICE_KEY not found in .env — required for admin operations."));
|
|
@@ -3235,13 +3287,18 @@ function fmtDate(value) {
|
|
|
3235
3287
|
*/
|
|
3236
3288
|
var POLL_INTERVAL_MS = 1500;
|
|
3237
3289
|
var POLL_TIMEOUT_MS = 900 * 1e3;
|
|
3290
|
+
var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
3238
3291
|
function sleep(ms) {
|
|
3239
3292
|
return new Promise((r) => setTimeout(r, ms));
|
|
3240
3293
|
}
|
|
3241
|
-
function run(cmd, cmdArgs, cwd) {
|
|
3294
|
+
function run(cmd, cmdArgs, cwd, env) {
|
|
3242
3295
|
return new Promise((resolve, reject) => {
|
|
3243
3296
|
const child = spawn(cmd, cmdArgs, {
|
|
3244
3297
|
cwd,
|
|
3298
|
+
env: env ? {
|
|
3299
|
+
...process.env,
|
|
3300
|
+
...env
|
|
3301
|
+
} : void 0,
|
|
3245
3302
|
stdio: [
|
|
3246
3303
|
"ignore",
|
|
3247
3304
|
"ignore",
|
|
@@ -3271,7 +3328,7 @@ async function createSourceTarball(sourceDir) {
|
|
|
3271
3328
|
for (const ignore of [".gitignore", ".rebaseignore"]) if (fs.existsSync(path.join(dir, ignore))) tarArgs.push(`--exclude-from=${ignore}`);
|
|
3272
3329
|
tarArgs.push(".");
|
|
3273
3330
|
try {
|
|
3274
|
-
await run("tar", tarArgs, dir);
|
|
3331
|
+
await run("tar", tarArgs, dir, { COPYFILE_DISABLE: "1" });
|
|
3275
3332
|
} catch (e) {
|
|
3276
3333
|
fail(`Failed to package source: ${e instanceof Error ? e.message : String(e)}`);
|
|
3277
3334
|
}
|
|
@@ -3281,6 +3338,7 @@ async function createSourceTarball(sourceDir) {
|
|
|
3281
3338
|
async function uploadSource(url, token, projectId, tarPath) {
|
|
3282
3339
|
const bytes = fs.readFileSync(tarPath);
|
|
3283
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.");
|
|
3284
3342
|
console.log(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
3285
3343
|
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
3286
3344
|
method: "POST",
|
|
@@ -4260,6 +4318,8 @@ ${chalk.green.bold("Options")}
|
|
|
4260
4318
|
${chalk.blue("--secret")} Mark a variable write-only ${chalk.gray("(set)")}
|
|
4261
4319
|
${chalk.blue("--json")} Machine-readable output
|
|
4262
4320
|
${chalk.blue("--project, -p")} Project slug ${chalk.gray("(defaults to the linked project)")}
|
|
4321
|
+
|
|
4322
|
+
${chalk.gray("Values are encrypted at rest (AES-256-GCM) and only decrypted at deploy time.")}
|
|
4263
4323
|
`);
|
|
4264
4324
|
}
|
|
4265
4325
|
function printEnvHelpJson() {
|
|
@@ -4811,7 +4871,7 @@ function deploymentDurationMs(dep) {
|
|
|
4811
4871
|
const ms = b - a;
|
|
4812
4872
|
return ms >= 0 ? ms : null;
|
|
4813
4873
|
}
|
|
4814
|
-
function formatDuration(ms) {
|
|
4874
|
+
function formatDuration$1(ms) {
|
|
4815
4875
|
const totalSec = Math.max(0, Math.round(ms / 1e3));
|
|
4816
4876
|
if (totalSec < 60) return `${totalSec}s`;
|
|
4817
4877
|
const m = Math.floor(totalSec / 60);
|
|
@@ -4884,7 +4944,7 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
4884
4944
|
return;
|
|
4885
4945
|
}
|
|
4886
4946
|
for (const v of views) {
|
|
4887
|
-
const dur = v.durationMs !== null ? formatDuration(v.durationMs) : chalk.gray("running");
|
|
4947
|
+
const dur = v.durationMs !== null ? formatDuration$1(v.durationMs) : chalk.gray("running");
|
|
4888
4948
|
const trig = v.trigger.source;
|
|
4889
4949
|
const roll = v.rollbackable ? chalk.green(" ↺ rollbackable") : "";
|
|
4890
4950
|
console.log(` ${chalk.gray(`[${v.id}]`)} ${colorStatus(v.status)} ${chalk.gray(String(v.createdAt ?? "—"))} ${dur} ${chalk.gray(trig)}${roll}`);
|
|
@@ -5055,6 +5115,737 @@ async function powerCommand(action, rawArgs) {
|
|
|
5055
5115
|
}
|
|
5056
5116
|
}
|
|
5057
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
|
|
5058
5849
|
//#region src/commands/cloud/resources.ts
|
|
5059
5850
|
/**
|
|
5060
5851
|
* `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
|
|
@@ -5167,7 +5958,10 @@ async function webhooksCommand(subcommand, rawArgs) {
|
|
|
5167
5958
|
reportError(e, "Webhook operation failed");
|
|
5168
5959
|
}
|
|
5169
5960
|
}
|
|
5170
|
-
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();
|
|
5171
5965
|
const { client } = await requireClient(rawArgs);
|
|
5172
5966
|
const projectId = await requireProject(rawArgs, client);
|
|
5173
5967
|
try {
|
|
@@ -5192,6 +5986,107 @@ async function storageCommand(rawArgs) {
|
|
|
5192
5986
|
reportError(e, "Failed to list storage");
|
|
5193
5987
|
}
|
|
5194
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
|
+
}
|
|
5195
6090
|
async function clustersCommand(rawArgs) {
|
|
5196
6091
|
const { client } = await requireClient(rawArgs);
|
|
5197
6092
|
try {
|
|
@@ -5383,6 +6278,9 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
5383
6278
|
case "metrics":
|
|
5384
6279
|
await metricsCommand(rawArgs);
|
|
5385
6280
|
break;
|
|
6281
|
+
case "debug":
|
|
6282
|
+
await debugCommand(action, rawArgs);
|
|
6283
|
+
break;
|
|
5386
6284
|
case "env":
|
|
5387
6285
|
await envCommand(action, rawArgs);
|
|
5388
6286
|
break;
|
|
@@ -5409,7 +6307,7 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
5409
6307
|
await webhooksCommand(action, rawArgs);
|
|
5410
6308
|
break;
|
|
5411
6309
|
case "storage":
|
|
5412
|
-
await storageCommand(rawArgs);
|
|
6310
|
+
await storageCommand(action, rawArgs);
|
|
5413
6311
|
break;
|
|
5414
6312
|
case "clusters":
|
|
5415
6313
|
await clustersCommand(rawArgs);
|
|
@@ -5494,6 +6392,7 @@ ${chalk.green.bold("Deploy & observe")}
|
|
|
5494
6392
|
${chalk.blue.bold("start|stop|restart")} ${chalk.gray("[-y]")} Power ops ${chalk.gray("(stop/restart need -y)")}
|
|
5495
6393
|
${chalk.blue.bold("status")} One-glance project status
|
|
5496
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)")}
|
|
5497
6396
|
|
|
5498
6397
|
${chalk.green.bold("Config")}
|
|
5499
6398
|
${chalk.blue.bold("env list|set|unset|reveal|pull")}
|
|
@@ -5512,6 +6411,8 @@ ${chalk.green.bold("Databases")}
|
|
|
5512
6411
|
${chalk.green.bold("Other resources")}
|
|
5513
6412
|
${chalk.blue.bold("webhooks list|create|delete")}
|
|
5514
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
|
|
5515
6416
|
${chalk.blue.bold("clusters")} List compute clusters
|
|
5516
6417
|
${chalk.blue.bold("billing setup")} Attach a card to the org ${chalk.gray("(one-time, opens browser)")}
|
|
5517
6418
|
${chalk.blue.bold("billing")} Show billing account + card on file
|
|
@@ -5620,9 +6521,10 @@ async function entry(args) {
|
|
|
5620
6521
|
await cloudCommand(effectiveSubcommand, args);
|
|
5621
6522
|
break;
|
|
5622
6523
|
default:
|
|
5623
|
-
console.
|
|
6524
|
+
console.error(chalk.red(`Unknown command: ${command}`));
|
|
5624
6525
|
console.log("");
|
|
5625
6526
|
printHelp();
|
|
6527
|
+
process.exit(1);
|
|
5626
6528
|
}
|
|
5627
6529
|
}
|
|
5628
6530
|
function printHelp() {
|