@rebasepro/cli 0.9.1-canary.ff338b5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js
CHANGED
|
@@ -875,7 +875,54 @@ function buildInitQuestions(params) {
|
|
|
875
875
|
});
|
|
876
876
|
return questions;
|
|
877
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* The `cd` a user must type to enter the new project.
|
|
880
|
+
*
|
|
881
|
+
* Not the project's basename: `init apps/my-app` has to say `cd apps/my-app`,
|
|
882
|
+
* and `init .` returns "" because they are already in the project.
|
|
883
|
+
*/
|
|
884
|
+
function formatCdTarget(cwd, targetDirectory) {
|
|
885
|
+
return path.relative(cwd, targetDirectory);
|
|
886
|
+
}
|
|
887
|
+
/** Help for `rebase init` — the flags were previously only discoverable by
|
|
888
|
+
* triggering the non-TTY error. */
|
|
889
|
+
function printInitHelp() {
|
|
890
|
+
console.log(`
|
|
891
|
+
${chalk.bold("rebase init")} — Create a new Rebase project
|
|
892
|
+
|
|
893
|
+
${chalk.bold("Usage")}
|
|
894
|
+
rebase init ${chalk.blue("[name]")} [options]
|
|
895
|
+
|
|
896
|
+
${chalk.gray("The name may be a nested path (apps/my-app) or \".\" for the current directory.")}
|
|
897
|
+
${chalk.gray("Defaults to \"my-rebase-app\" when omitted with --yes.")}
|
|
898
|
+
|
|
899
|
+
${chalk.bold("Options")}
|
|
900
|
+
${chalk.blue("-t, --template")} ${chalk.gray("<preset>")} blog | ecommerce | blank ${chalk.gray("(default: blog)")}
|
|
901
|
+
${chalk.blue("-f, --flavor")} ${chalk.gray("<flavor>")} cms | baas ${chalk.gray("(default: cms)")}
|
|
902
|
+
${chalk.blue("-y, --yes")} Accept defaults, never prompt ${chalk.gray("(required for CI / non-TTY)")}
|
|
903
|
+
${chalk.blue("-i, --install")} Install dependencies after scaffolding
|
|
904
|
+
${chalk.blue("-g, --git")} Initialize a git repository and make an initial commit
|
|
905
|
+
${chalk.blue("--database-url")} ${chalk.gray("<url>")} Use an existing database instead of the generated one
|
|
906
|
+
${chalk.blue("--introspect")} Generate collections from that database ${chalk.gray("(implies --template blank; needs --install)")}
|
|
907
|
+
${chalk.blue("--project")} ${chalk.gray("<slug>")} Link the scaffold to a Rebase Cloud project
|
|
908
|
+
${chalk.blue("--setup-key")} ${chalk.gray("<key>")} One-time key authenticating the cloud link ${chalk.gray("(use with --project)")}
|
|
909
|
+
|
|
910
|
+
${chalk.bold("Flavors")}
|
|
911
|
+
${chalk.blue("cms")} BaaS + admin UI, driven by collections you define ${chalk.gray("(like Payload/Directus)")}
|
|
912
|
+
${chalk.blue("baas")} Headless API over your database — no collections, no UI ${chalk.gray("(like Supabase)")}
|
|
913
|
+
${chalk.gray("--template has no effect on this flavor.")}
|
|
914
|
+
|
|
915
|
+
${chalk.bold("Examples")}
|
|
916
|
+
${chalk.gray("$")} rebase init my-shop --template ecommerce --install
|
|
917
|
+
${chalk.gray("$")} rebase init my-api --flavor baas --yes
|
|
918
|
+
${chalk.gray("$")} rebase init . --yes --git
|
|
919
|
+
`);
|
|
920
|
+
}
|
|
878
921
|
async function createRebaseApp(rawArgs) {
|
|
922
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
923
|
+
printInitHelp();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
879
926
|
console.log(`
|
|
880
927
|
${chalk.bold("Rebase")} — Create a new project 🚀
|
|
881
928
|
`);
|
|
@@ -935,6 +982,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
935
982
|
databaseUrl: args["--database-url"] || void 0,
|
|
936
983
|
introspect: args["--introspect"] || false,
|
|
937
984
|
preset: templateArg || "blog",
|
|
985
|
+
explicitPreset: !!templateArg,
|
|
938
986
|
flavor: flavorArg || "cms",
|
|
939
987
|
pm,
|
|
940
988
|
pmCommands,
|
|
@@ -972,6 +1020,7 @@ async function promptForOptions(rawArgs, pm) {
|
|
|
972
1020
|
databaseUrl: answers.databaseUrl?.trim() || void 0,
|
|
973
1021
|
introspect: answers.introspect || false,
|
|
974
1022
|
preset: templateArg || answers.preset || "blog",
|
|
1023
|
+
explicitPreset: !!templateArg,
|
|
975
1024
|
flavor: flavorArg || answers.flavor || "cms",
|
|
976
1025
|
pm,
|
|
977
1026
|
pmCommands,
|
|
@@ -1056,6 +1105,7 @@ async function createProject$1(options) {
|
|
|
1056
1105
|
const shipped = path.join(options.targetDirectory, from);
|
|
1057
1106
|
if (fs.existsSync(shipped)) fs.renameSync(shipped, path.join(options.targetDirectory, to));
|
|
1058
1107
|
}
|
|
1108
|
+
if (options.flavor === "baas" && options.explicitPreset) console.log(chalk.yellow(` Ignoring --template ${options.preset}: the baas flavor has no collections.`));
|
|
1059
1109
|
if (options.flavor !== "baas") {
|
|
1060
1110
|
if (options.introspect && options.preset !== "blank") console.log(chalk.gray(" Using the blank template: collections will come from your database."));
|
|
1061
1111
|
await applyPreset(options.targetDirectory, options.introspect ? "blank" : options.preset);
|
|
@@ -1067,6 +1117,33 @@ async function createProject$1(options) {
|
|
|
1067
1117
|
console.log(chalk.gray(" Initializing git repository..."));
|
|
1068
1118
|
try {
|
|
1069
1119
|
await execa("git", ["init"], { cwd: options.targetDirectory });
|
|
1120
|
+
try {
|
|
1121
|
+
await execa("git", [
|
|
1122
|
+
"symbolic-ref",
|
|
1123
|
+
"HEAD",
|
|
1124
|
+
"refs/heads/main"
|
|
1125
|
+
], { cwd: options.targetDirectory });
|
|
1126
|
+
} catch {}
|
|
1127
|
+
await execa("git", ["add", "-A"], { cwd: options.targetDirectory });
|
|
1128
|
+
let identity = {};
|
|
1129
|
+
try {
|
|
1130
|
+
await execa("git", ["config", "user.email"], { cwd: options.targetDirectory });
|
|
1131
|
+
} catch {
|
|
1132
|
+
identity = {
|
|
1133
|
+
GIT_AUTHOR_NAME: "Rebase",
|
|
1134
|
+
GIT_AUTHOR_EMAIL: "noreply@rebase.pro",
|
|
1135
|
+
GIT_COMMITTER_NAME: "Rebase",
|
|
1136
|
+
GIT_COMMITTER_EMAIL: "noreply@rebase.pro"
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
await execa("git", [
|
|
1140
|
+
"commit",
|
|
1141
|
+
"-m",
|
|
1142
|
+
"Initial commit from Rebase"
|
|
1143
|
+
], {
|
|
1144
|
+
cwd: options.targetDirectory,
|
|
1145
|
+
env: identity
|
|
1146
|
+
});
|
|
1070
1147
|
} catch {
|
|
1071
1148
|
console.warn(chalk.yellow(" Warning: Failed to initialize git repository"));
|
|
1072
1149
|
}
|
|
@@ -1097,6 +1174,7 @@ async function createProject$1(options) {
|
|
|
1097
1174
|
console.warn(chalk.yellow(` Warning: Failed to install dependencies. You may need to run \`${installCmd.join(" ")}\` manually.`));
|
|
1098
1175
|
}
|
|
1099
1176
|
}
|
|
1177
|
+
let introspected = false;
|
|
1100
1178
|
if (options.introspect) {
|
|
1101
1179
|
console.log("");
|
|
1102
1180
|
if (options.installDeps) {
|
|
@@ -1112,6 +1190,7 @@ async function createProject$1(options) {
|
|
|
1112
1190
|
stdio: "inherit"
|
|
1113
1191
|
});
|
|
1114
1192
|
console.log(chalk.green(" Database successfully introspected!"));
|
|
1193
|
+
introspected = true;
|
|
1115
1194
|
} catch {
|
|
1116
1195
|
console.warn(chalk.yellow(" Warning: Failed to introspect database automatically."));
|
|
1117
1196
|
console.warn(chalk.yellow(` You can run \`${execCmd.join(" ")}\` then \`${generateCmd.join(" ")}\` manually after setup.`));
|
|
@@ -1130,13 +1209,21 @@ async function createProject$1(options) {
|
|
|
1130
1209
|
const runDev = pmCommands.run("dev");
|
|
1131
1210
|
const runDbPush = pmCommands.run("db:push");
|
|
1132
1211
|
const isBaas = options.flavor === "baas";
|
|
1133
|
-
|
|
1212
|
+
const cdTarget = formatCdTarget(process.cwd(), options.targetDirectory);
|
|
1213
|
+
if (cdTarget) console.log(` ${chalk.cyan("cd")} ${cdTarget}`);
|
|
1134
1214
|
if (!options.installDeps) console.log(` ${chalk.cyan(installCmd.join(" "))}`);
|
|
1135
1215
|
console.log("");
|
|
1136
|
-
if (options.databaseUrl) if (
|
|
1216
|
+
if (options.databaseUrl) if (introspected) {
|
|
1137
1217
|
console.log(chalk.gray(" # Database has been introspected & collections generated!"));
|
|
1138
1218
|
console.log(chalk.gray(" # Start the development server (frontend + backend):"));
|
|
1139
1219
|
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
1220
|
+
} else if (options.introspect) {
|
|
1221
|
+
console.log(chalk.gray(" # Introspection did not run — finish it with:"));
|
|
1222
|
+
console.log(` ${chalk.cyan(execCmd.join(" "))}`);
|
|
1223
|
+
console.log(` ${chalk.cyan(generateCmd.join(" "))}`);
|
|
1224
|
+
console.log("");
|
|
1225
|
+
console.log(chalk.gray(" # Then start the development server:"));
|
|
1226
|
+
console.log(` ${chalk.cyan(runDev.join(" "))}`);
|
|
1140
1227
|
} else {
|
|
1141
1228
|
console.log(chalk.gray(" # Your custom database is configured in .env."));
|
|
1142
1229
|
console.log(chalk.gray(" # If the database is empty, push the Rebase schema to initialize it:"));
|
|
@@ -1365,7 +1452,7 @@ async function configureEnvFile(targetDirectory, databaseUrl) {
|
|
|
1365
1452
|
envContent = envContent.replace(/^#\s*REBASE_SERVICE_KEY=.*$/m, `REBASE_SERVICE_KEY=${serviceKey}`);
|
|
1366
1453
|
if (databaseUrl) {
|
|
1367
1454
|
if (/[\r\n]/.test(databaseUrl)) throw new Error("Invalid DATABASE_URL: multiline values are not allowed.");
|
|
1368
|
-
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`);
|
|
1455
|
+
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}\nDATABASE_PASSWORD=${dbPassword}`);
|
|
1369
1456
|
} else {
|
|
1370
1457
|
const dbPort = await findAvailablePort(5432);
|
|
1371
1458
|
envContent = envContent.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=postgresql://rebase:${dbPassword}@localhost:${dbPort}/rebase?options=-c%20search_path=public&sslmode=disable\nDATABASE_PASSWORD=${dbPassword}`);
|
|
@@ -4871,7 +4958,7 @@ function deploymentDurationMs(dep) {
|
|
|
4871
4958
|
const ms = b - a;
|
|
4872
4959
|
return ms >= 0 ? ms : null;
|
|
4873
4960
|
}
|
|
4874
|
-
function formatDuration(ms) {
|
|
4961
|
+
function formatDuration$1(ms) {
|
|
4875
4962
|
const totalSec = Math.max(0, Math.round(ms / 1e3));
|
|
4876
4963
|
if (totalSec < 60) return `${totalSec}s`;
|
|
4877
4964
|
const m = Math.floor(totalSec / 60);
|
|
@@ -4944,7 +5031,7 @@ async function deploymentsListCommand(rawArgs) {
|
|
|
4944
5031
|
return;
|
|
4945
5032
|
}
|
|
4946
5033
|
for (const v of views) {
|
|
4947
|
-
const dur = v.durationMs !== null ? formatDuration(v.durationMs) : chalk.gray("running");
|
|
5034
|
+
const dur = v.durationMs !== null ? formatDuration$1(v.durationMs) : chalk.gray("running");
|
|
4948
5035
|
const trig = v.trigger.source;
|
|
4949
5036
|
const roll = v.rollbackable ? chalk.green(" ↺ rollbackable") : "";
|
|
4950
5037
|
console.log(` ${chalk.gray(`[${v.id}]`)} ${colorStatus(v.status)} ${chalk.gray(String(v.createdAt ?? "—"))} ${dur} ${chalk.gray(trig)}${roll}`);
|
|
@@ -5115,6 +5202,737 @@ async function powerCommand(action, rawArgs) {
|
|
|
5115
5202
|
}
|
|
5116
5203
|
}
|
|
5117
5204
|
//#endregion
|
|
5205
|
+
//#region src/commands/cloud/debug.ts
|
|
5206
|
+
/**
|
|
5207
|
+
* `rebase cloud debug <subcommand>` — one entry point for "why is my deployed
|
|
5208
|
+
* app not behaving".
|
|
5209
|
+
*
|
|
5210
|
+
* ## Why this exists
|
|
5211
|
+
*
|
|
5212
|
+
* The useful signals for a deployed project live in four different places — the
|
|
5213
|
+
* control plane, the workload's pods, the tenant database, and the public URL —
|
|
5214
|
+
* and each is normally reached with a different tool and a different set of
|
|
5215
|
+
* flags. Getting to a log should not be a research task. This started life as a
|
|
5216
|
+
* hand-rolled `prod-debug.sh` for a single project, with the namespace and the
|
|
5217
|
+
* URL hardcoded at the top; it earned its place twice in one week, so it is
|
|
5218
|
+
* generalised here to any project the CLI can already resolve.
|
|
5219
|
+
*
|
|
5220
|
+
* ## Read-only by default
|
|
5221
|
+
*
|
|
5222
|
+
* Every subcommand here only reads. The two things the original script could do
|
|
5223
|
+
* that mutate are deliberately NOT reproduced as-is:
|
|
5224
|
+
*
|
|
5225
|
+
* - restarting the workload lives at `rebase cloud restart`, which already
|
|
5226
|
+
* gates downtime behind `--yes`; duplicating it here would give the same
|
|
5227
|
+
* destructive act a second, ungated spelling.
|
|
5228
|
+
* - `debug db` prints the port-forward recipe and the connection's *shape*.
|
|
5229
|
+
* It opens no session and prints no password — `rebase cloud db info
|
|
5230
|
+
* --reveal` is the explicit, auditable way to get one.
|
|
5231
|
+
*
|
|
5232
|
+
* ## The probes are the point
|
|
5233
|
+
*
|
|
5234
|
+
* `debug health` is the highest-value piece and the reason the script existed.
|
|
5235
|
+
* A bare status code is not a diagnosis: a 404 from a functions route means
|
|
5236
|
+
* something completely different from a 404 at the root, and a 200 on an
|
|
5237
|
+
* unauthenticated read is a finding rather than a success. So every probe ships
|
|
5238
|
+
* with the interpretation of what it got, not just the number — see
|
|
5239
|
+
* {@link PROBES}.
|
|
5240
|
+
*/
|
|
5241
|
+
/** A response never arrived: DNS, TLS, ingress, or the pod. */
|
|
5242
|
+
var NO_RESPONSE = {
|
|
5243
|
+
verdict: "fail",
|
|
5244
|
+
meaning: "nothing answered — the hostname does not resolve, the ingress has no route, or no pod is running"
|
|
5245
|
+
};
|
|
5246
|
+
function serverError(what) {
|
|
5247
|
+
return {
|
|
5248
|
+
verdict: "fail",
|
|
5249
|
+
meaning: `the server is running but ${what}`
|
|
5250
|
+
};
|
|
5251
|
+
}
|
|
5252
|
+
/**
|
|
5253
|
+
* The probe set, in the order a failure cascades: if `health` is down, nothing
|
|
5254
|
+
* below it is meaningful, so it is checked first and reported first.
|
|
5255
|
+
*/
|
|
5256
|
+
var PROBES = [
|
|
5257
|
+
{
|
|
5258
|
+
id: "health",
|
|
5259
|
+
label: "health",
|
|
5260
|
+
method: "GET",
|
|
5261
|
+
path: () => "/health",
|
|
5262
|
+
healthy: "200 — the backend is up",
|
|
5263
|
+
interpret: (status) => {
|
|
5264
|
+
if (status === null) return NO_RESPONSE;
|
|
5265
|
+
if (status === 200) return {
|
|
5266
|
+
verdict: "ok",
|
|
5267
|
+
meaning: "the backend is up and serving"
|
|
5268
|
+
};
|
|
5269
|
+
if (status === 404) return {
|
|
5270
|
+
verdict: "fail",
|
|
5271
|
+
meaning: "something answered but it is not a Rebase backend — the ingress is routing this host elsewhere"
|
|
5272
|
+
};
|
|
5273
|
+
if (status >= 500) return serverError("its health endpoint is failing");
|
|
5274
|
+
return {
|
|
5275
|
+
verdict: "unknown",
|
|
5276
|
+
meaning: "unexpected for a health endpoint"
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
},
|
|
5280
|
+
{
|
|
5281
|
+
id: "spa",
|
|
5282
|
+
label: "spa",
|
|
5283
|
+
method: "GET",
|
|
5284
|
+
path: () => "/",
|
|
5285
|
+
healthy: "200 — the frontend is being served",
|
|
5286
|
+
interpret: (status) => {
|
|
5287
|
+
if (status === null) return NO_RESPONSE;
|
|
5288
|
+
if (status === 200) return {
|
|
5289
|
+
verdict: "ok",
|
|
5290
|
+
meaning: "the frontend bundle is being served"
|
|
5291
|
+
};
|
|
5292
|
+
if (status === 404) return {
|
|
5293
|
+
verdict: "warn",
|
|
5294
|
+
meaning: "no frontend at the root — expected for a backend-only project, otherwise the SPA assets were not bundled into the image"
|
|
5295
|
+
};
|
|
5296
|
+
if (status >= 500) return serverError("the root route throws");
|
|
5297
|
+
return {
|
|
5298
|
+
verdict: "unknown",
|
|
5299
|
+
meaning: "unexpected at the site root"
|
|
5300
|
+
};
|
|
5301
|
+
}
|
|
5302
|
+
},
|
|
5303
|
+
{
|
|
5304
|
+
id: "auth",
|
|
5305
|
+
label: "auth",
|
|
5306
|
+
method: "POST",
|
|
5307
|
+
path: () => "/api/auth/login",
|
|
5308
|
+
body: {},
|
|
5309
|
+
healthy: "400 — auth is mounted and rejects an empty body",
|
|
5310
|
+
interpret: (status) => {
|
|
5311
|
+
if (status === null) return NO_RESPONSE;
|
|
5312
|
+
if (status === 400 || status === 422) return {
|
|
5313
|
+
verdict: "ok",
|
|
5314
|
+
meaning: "auth is mounted and rejected the empty body, as it should"
|
|
5315
|
+
};
|
|
5316
|
+
if (status === 401 || status === 403) return {
|
|
5317
|
+
verdict: "ok",
|
|
5318
|
+
meaning: "auth is mounted and refused the credentials"
|
|
5319
|
+
};
|
|
5320
|
+
if (status === 404) return {
|
|
5321
|
+
verdict: "fail",
|
|
5322
|
+
meaning: "the auth routes are NOT mounted — this project cannot sign anyone in"
|
|
5323
|
+
};
|
|
5324
|
+
if (status === 200) return {
|
|
5325
|
+
verdict: "fail",
|
|
5326
|
+
meaning: "an EMPTY login body was accepted — a login with no credentials must never succeed"
|
|
5327
|
+
};
|
|
5328
|
+
if (status >= 500) return serverError("the login route throws — often a missing or unmigrated auth table");
|
|
5329
|
+
return {
|
|
5330
|
+
verdict: "unknown",
|
|
5331
|
+
meaning: "unexpected for a login route"
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
},
|
|
5335
|
+
{
|
|
5336
|
+
id: "unauthRead",
|
|
5337
|
+
label: "unauth read",
|
|
5338
|
+
method: "GET",
|
|
5339
|
+
path: (t) => `/api/data/${encodeURIComponent(t.collection)}`,
|
|
5340
|
+
healthy: "401 — reads require authentication",
|
|
5341
|
+
interpret: (status) => {
|
|
5342
|
+
if (status === null) return NO_RESPONSE;
|
|
5343
|
+
if (status === 401 || status === 403) return {
|
|
5344
|
+
verdict: "ok",
|
|
5345
|
+
meaning: "unauthenticated reads are refused — row-level security is enforced"
|
|
5346
|
+
};
|
|
5347
|
+
if (status === 200) return {
|
|
5348
|
+
verdict: "warn",
|
|
5349
|
+
meaning: "this collection is readable with NO authentication — correct only if it is deliberately public"
|
|
5350
|
+
};
|
|
5351
|
+
if (status === 404) return {
|
|
5352
|
+
verdict: "warn",
|
|
5353
|
+
meaning: "no such collection on this deployment — check the name, or pass --collection"
|
|
5354
|
+
};
|
|
5355
|
+
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");
|
|
5356
|
+
return {
|
|
5357
|
+
verdict: "unknown",
|
|
5358
|
+
meaning: "unexpected for a data read"
|
|
5359
|
+
};
|
|
5360
|
+
}
|
|
5361
|
+
},
|
|
5362
|
+
{
|
|
5363
|
+
id: "functions",
|
|
5364
|
+
label: "functions",
|
|
5365
|
+
method: "GET",
|
|
5366
|
+
/**
|
|
5367
|
+
* The router's own listing endpoint, NOT a function's path.
|
|
5368
|
+
*
|
|
5369
|
+
* This matters, and it is the one place the original script got a wrong
|
|
5370
|
+
* answer. A function is a Hono sub-app mounted at `/<name>`, and it
|
|
5371
|
+
* usually defines only sub-routes (`/get`, `/list`) — so
|
|
5372
|
+
* `/api/functions/<name>` 404s **even when everything is mounted and
|
|
5373
|
+
* healthy**. Probing there cannot separate "the router is missing" from
|
|
5374
|
+
* "that function defines no root route", and reporting the first is how
|
|
5375
|
+
* you send someone to debug a deployment that was fine.
|
|
5376
|
+
*
|
|
5377
|
+
* `GET /api/functions` is unambiguous: the router registers a listing
|
|
5378
|
+
* route at its own root (see `createFunctionRoutes`), so a 200 proves
|
|
5379
|
+
* the mount *and* names every function that loaded.
|
|
5380
|
+
*/
|
|
5381
|
+
path: () => "/api/functions",
|
|
5382
|
+
healthy: "200 — the functions router is mounted and lists its functions",
|
|
5383
|
+
interpret: (status) => {
|
|
5384
|
+
if (status === null) return NO_RESPONSE;
|
|
5385
|
+
if (status === 200) return {
|
|
5386
|
+
verdict: "ok",
|
|
5387
|
+
meaning: "the functions router is mounted"
|
|
5388
|
+
};
|
|
5389
|
+
if (status === 401 || status === 403) return {
|
|
5390
|
+
verdict: "ok",
|
|
5391
|
+
meaning: "the functions router is mounted (its listing requires auth)"
|
|
5392
|
+
};
|
|
5393
|
+
if (status === 404) return {
|
|
5394
|
+
verdict: "fail",
|
|
5395
|
+
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"
|
|
5396
|
+
};
|
|
5397
|
+
if (status >= 500) return serverError("the functions router throws");
|
|
5398
|
+
return {
|
|
5399
|
+
verdict: "unknown",
|
|
5400
|
+
meaning: "unexpected for the functions listing"
|
|
5401
|
+
};
|
|
5402
|
+
},
|
|
5403
|
+
needsBody: true,
|
|
5404
|
+
refine: (body, t) => {
|
|
5405
|
+
const names = functionNames(body);
|
|
5406
|
+
if (!names) return null;
|
|
5407
|
+
if (t.fn && !names.includes(t.fn)) return {
|
|
5408
|
+
verdict: "fail",
|
|
5409
|
+
meaning: `the router is mounted but no function is named "${t.fn}" — it loaded ${names.length}: ${names.join(", ")}`
|
|
5410
|
+
};
|
|
5411
|
+
const found = t.fn ? `, including ${t.fn}` : "";
|
|
5412
|
+
return {
|
|
5413
|
+
verdict: "ok",
|
|
5414
|
+
meaning: `the functions router is mounted and loaded ${names.length} function${names.length === 1 ? "" : "s"}${found}`
|
|
5415
|
+
};
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
];
|
|
5419
|
+
/** The function names out of a listing body, or null when it is not one. */
|
|
5420
|
+
function functionNames(body) {
|
|
5421
|
+
const list = body?.functions;
|
|
5422
|
+
if (!Array.isArray(list)) return null;
|
|
5423
|
+
const names = list.map((f) => f?.name).filter((n) => typeof n === "string");
|
|
5424
|
+
return names.length === list.length ? names : null;
|
|
5425
|
+
}
|
|
5426
|
+
/** Milliseconds before a probe is treated as unanswered. */
|
|
5427
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
5428
|
+
/**
|
|
5429
|
+
* Ceiling on a probe body we will parse. The only body read is the function
|
|
5430
|
+
* listing; anything larger is a page we have no use for, and a debug command
|
|
5431
|
+
* must not be the thing that runs a machine out of memory.
|
|
5432
|
+
*/
|
|
5433
|
+
var MAX_PROBE_BODY_BYTES = 256 * 1024;
|
|
5434
|
+
/**
|
|
5435
|
+
* Run one probe. A transport failure is a `null` status, never a thrown error:
|
|
5436
|
+
* "nothing answered" is a diagnosis in its own right and the other probes still
|
|
5437
|
+
* need to run.
|
|
5438
|
+
*/
|
|
5439
|
+
async function runProbe(origin, spec, targets) {
|
|
5440
|
+
const url = `${origin}${spec.path(targets)}`;
|
|
5441
|
+
const started = Date.now();
|
|
5442
|
+
let status = null;
|
|
5443
|
+
let body;
|
|
5444
|
+
try {
|
|
5445
|
+
const res = await fetch(url, {
|
|
5446
|
+
method: spec.method,
|
|
5447
|
+
headers: spec.body ? { "Content-Type": "application/json" } : void 0,
|
|
5448
|
+
body: spec.body ? JSON.stringify(spec.body) : void 0,
|
|
5449
|
+
redirect: "manual",
|
|
5450
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
5451
|
+
});
|
|
5452
|
+
status = res.status;
|
|
5453
|
+
if (spec.needsBody && res.ok) {
|
|
5454
|
+
const text = await res.text();
|
|
5455
|
+
if (text.length <= MAX_PROBE_BODY_BYTES) try {
|
|
5456
|
+
body = JSON.parse(text);
|
|
5457
|
+
} catch {}
|
|
5458
|
+
}
|
|
5459
|
+
} catch {
|
|
5460
|
+
status = null;
|
|
5461
|
+
}
|
|
5462
|
+
const statusReading = spec.interpret(status);
|
|
5463
|
+
const reading = status !== null && spec.refine?.(body, targets) || statusReading;
|
|
5464
|
+
return {
|
|
5465
|
+
statusOk: statusReading.verdict === "ok",
|
|
5466
|
+
id: spec.id,
|
|
5467
|
+
label: spec.label,
|
|
5468
|
+
method: spec.method,
|
|
5469
|
+
url,
|
|
5470
|
+
status,
|
|
5471
|
+
ms: Date.now() - started,
|
|
5472
|
+
verdict: reading.verdict,
|
|
5473
|
+
meaning: reading.meaning,
|
|
5474
|
+
healthy: spec.healthy
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
/** The worst verdict across probes — what the command's exit code keys off. */
|
|
5478
|
+
function overallVerdict(results) {
|
|
5479
|
+
if (results.some((r) => r.verdict === "fail")) return "fail";
|
|
5480
|
+
if (results.some((r) => r.verdict === "unknown")) return "unknown";
|
|
5481
|
+
if (results.some((r) => r.verdict === "warn")) return "warn";
|
|
5482
|
+
return "ok";
|
|
5483
|
+
}
|
|
5484
|
+
function verdictMark(v) {
|
|
5485
|
+
switch (v) {
|
|
5486
|
+
case "ok": return chalk.green("✓");
|
|
5487
|
+
case "warn": return chalk.yellow("!");
|
|
5488
|
+
case "fail": return chalk.red("✗");
|
|
5489
|
+
default: return chalk.gray("?");
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
var LOG_PREFIX_RE = /^(?:(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+)?(?:\[([^\]]+)\]\s+)?([\s\S]*)$/;
|
|
5493
|
+
function parseLogLine(line) {
|
|
5494
|
+
const m = LOG_PREFIX_RE.exec(line);
|
|
5495
|
+
if (!m) return {
|
|
5496
|
+
ts: null,
|
|
5497
|
+
pod: null,
|
|
5498
|
+
text: line
|
|
5499
|
+
};
|
|
5500
|
+
return {
|
|
5501
|
+
ts: m[1] ?? null,
|
|
5502
|
+
pod: m[2] ?? null,
|
|
5503
|
+
text: m[3] ?? ""
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
/**
|
|
5507
|
+
* Lines an operator scanning for a fault wants to see.
|
|
5508
|
+
*
|
|
5509
|
+
* Matches the structured `severity` field first, then the shapes that show up
|
|
5510
|
+
* in unstructured output. `refus`/`denied` are in the list because a permission
|
|
5511
|
+
* failure often logs at info level and is exactly what one is hunting for.
|
|
5512
|
+
*/
|
|
5513
|
+
function isErrorLine(text) {
|
|
5514
|
+
return /"(?:severity|level)":\s*"(?:ERROR|WARN(?:ING)?|error|warn)"|\bError:|\bERR!|refus|denied|EACCES|ECONNREFUSED/i.test(text);
|
|
5515
|
+
}
|
|
5516
|
+
/**
|
|
5517
|
+
* Pull an HTTP request record out of a log line, or null when it is not one.
|
|
5518
|
+
*
|
|
5519
|
+
* Only structured request lines are recognised. Guessing at prose would produce
|
|
5520
|
+
* a table with invented columns, which is worse than a short one.
|
|
5521
|
+
*/
|
|
5522
|
+
function parseRequestLine(text) {
|
|
5523
|
+
const start = text.indexOf("{");
|
|
5524
|
+
if (start === -1) return null;
|
|
5525
|
+
let parsed;
|
|
5526
|
+
try {
|
|
5527
|
+
parsed = JSON.parse(text.slice(start));
|
|
5528
|
+
} catch {
|
|
5529
|
+
return null;
|
|
5530
|
+
}
|
|
5531
|
+
if (parsed.message !== "request" && parsed.msg !== "request") return null;
|
|
5532
|
+
const num = (v) => {
|
|
5533
|
+
const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
|
|
5534
|
+
return Number.isFinite(n) ? n : null;
|
|
5535
|
+
};
|
|
5536
|
+
return {
|
|
5537
|
+
status: num(parsed.status),
|
|
5538
|
+
method: typeof parsed.method === "string" ? parsed.method : "",
|
|
5539
|
+
path: typeof parsed.path === "string" ? parsed.path : "",
|
|
5540
|
+
latencyMs: num(parsed.latencyMs ?? parsed.durationMs)
|
|
5541
|
+
};
|
|
5542
|
+
}
|
|
5543
|
+
/**
|
|
5544
|
+
* Startup lines: what the server *decided* it was going to do. Which storage
|
|
5545
|
+
* backend it bound, which functions it loaded, whether auth tables were found.
|
|
5546
|
+
* This is the fastest way to tell a misconfiguration from a runtime fault.
|
|
5547
|
+
*/
|
|
5548
|
+
function isBootLine(text) {
|
|
5549
|
+
return /storage|Loaded function|Mounted|Auth tables|Server running|listening|Refusing|migrat/i.test(text);
|
|
5550
|
+
}
|
|
5551
|
+
/** Render a number of seconds back as the compact duration a user would type. */
|
|
5552
|
+
function formatDuration(seconds) {
|
|
5553
|
+
if (seconds % 86400 === 0 && seconds >= 86400) return `${seconds / 86400}d`;
|
|
5554
|
+
if (seconds % 3600 === 0 && seconds >= 3600) return `${seconds / 3600}h`;
|
|
5555
|
+
if (seconds % 60 === 0 && seconds >= 60) return `${seconds / 60}m`;
|
|
5556
|
+
return `${seconds}s`;
|
|
5557
|
+
}
|
|
5558
|
+
/**
|
|
5559
|
+
* Parse a duration like `15m`, `2h`, `90s`, `1d` (or a bare number of seconds)
|
|
5560
|
+
* into seconds. Returns null when it is not a duration.
|
|
5561
|
+
*/
|
|
5562
|
+
function parseSince(input) {
|
|
5563
|
+
if (!input) return null;
|
|
5564
|
+
const m = /^(\d+(?:\.\d+)?)\s*([smhd])?$/i.exec(input.trim());
|
|
5565
|
+
if (!m) return null;
|
|
5566
|
+
const value = parseFloat(m[1]);
|
|
5567
|
+
const unit = (m[2] || "s").toLowerCase();
|
|
5568
|
+
return Math.round(value * (unit === "s" ? 1 : unit === "m" ? 60 : unit === "h" ? 3600 : 86400));
|
|
5569
|
+
}
|
|
5570
|
+
async function fetchRuntimeLogs(client, projectId, opts) {
|
|
5571
|
+
const params = new URLSearchParams();
|
|
5572
|
+
if (opts.sinceSeconds !== void 0) params.set("sinceSeconds", String(opts.sinceSeconds));
|
|
5573
|
+
if (opts.tailLines !== void 0) params.set("tailLines", String(opts.tailLines));
|
|
5574
|
+
if (opts.previous) params.set("previous", "true");
|
|
5575
|
+
params.set("timestamps", "true");
|
|
5576
|
+
const qs = params.toString();
|
|
5577
|
+
return client.functions.invoke("runtime-logs", void 0, {
|
|
5578
|
+
method: "GET",
|
|
5579
|
+
path: `${projectId}${qs ? `?${qs}` : ""}`
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
/** Print the per-pod states runtime-logs reports, including its hints. */
|
|
5583
|
+
function printPodStates(res) {
|
|
5584
|
+
if (res.state === "no_pods") {
|
|
5585
|
+
console.log(chalk.yellow(` ${res.message ?? "No pods are running for this project."}`));
|
|
5586
|
+
console.log("");
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
for (const p of res.pods ?? []) {
|
|
5590
|
+
if (p.state === "ok") continue;
|
|
5591
|
+
console.log(` ${chalk.yellow(p.pod)} ${chalk.gray(`(${p.state})`)}`);
|
|
5592
|
+
if (p.message) console.log(chalk.gray(` ${p.message}`));
|
|
5593
|
+
if (p.hint) console.log(chalk.cyan(` → ${p.hint}`));
|
|
5594
|
+
}
|
|
5595
|
+
if (res.truncated) console.log(chalk.gray(" (output truncated — narrow the window with --since)"));
|
|
5596
|
+
}
|
|
5597
|
+
/** Resolve the public origin a project is served at. */
|
|
5598
|
+
async function resolveOrigin(rawArgs, client, url, projectId) {
|
|
5599
|
+
const parsed = arg({ "--host": String }, {
|
|
5600
|
+
argv: rawArgs.slice(3),
|
|
5601
|
+
permissive: true
|
|
5602
|
+
});
|
|
5603
|
+
if (parsed["--host"]) {
|
|
5604
|
+
const h = parsed["--host"].trim().replace(/\/+$/, "");
|
|
5605
|
+
return /^https?:\/\//.test(h) ? h : `https://${h}`;
|
|
5606
|
+
}
|
|
5607
|
+
const [project, baseDomain] = await Promise.all([client.data.collection("projects").findById(projectId), fetchTenantBaseDomain(client, url)]);
|
|
5608
|
+
if (!project) fail(`Project ${displayProjectRef(rawArgs)} not found.`);
|
|
5609
|
+
const host = projectHost(project, baseDomain);
|
|
5610
|
+
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.");
|
|
5611
|
+
return `https://${host}`;
|
|
5612
|
+
}
|
|
5613
|
+
async function healthCommand(rawArgs) {
|
|
5614
|
+
const parsed = arg({
|
|
5615
|
+
"--collection": String,
|
|
5616
|
+
"--function": String
|
|
5617
|
+
}, {
|
|
5618
|
+
argv: rawArgs.slice(3),
|
|
5619
|
+
permissive: true
|
|
5620
|
+
});
|
|
5621
|
+
const targets = {
|
|
5622
|
+
collection: parsed["--collection"] || "users",
|
|
5623
|
+
fn: parsed["--function"]
|
|
5624
|
+
};
|
|
5625
|
+
const { client, url } = await requireClient(rawArgs);
|
|
5626
|
+
const origin = await resolveOrigin(rawArgs, client, url, await requireProject(rawArgs, client));
|
|
5627
|
+
const results = [];
|
|
5628
|
+
for (const spec of PROBES) results.push(await runProbe(origin, spec, targets));
|
|
5629
|
+
const overall = overallVerdict(results);
|
|
5630
|
+
emit(() => {
|
|
5631
|
+
console.log("");
|
|
5632
|
+
console.log(chalk.bold(` 🩺 Health — ${displayProjectRef(rawArgs)}`) + chalk.gray(` ${origin}`));
|
|
5633
|
+
console.log("");
|
|
5634
|
+
const width = Math.max(...results.map((r) => r.label.length));
|
|
5635
|
+
for (const r of results) {
|
|
5636
|
+
const code = r.status === null ? chalk.red("---") : String(r.status);
|
|
5637
|
+
console.log(` ${verdictMark(r.verdict)} ${chalk.bold(r.label.padEnd(width))} ${code.padStart(3)} ${chalk.gray(`${r.ms}ms`)}`);
|
|
5638
|
+
console.log(` ${chalk.gray(r.meaning)}`);
|
|
5639
|
+
}
|
|
5640
|
+
console.log("");
|
|
5641
|
+
if (overall === "ok") console.log(chalk.green(" Everything reachable and wired as expected."));
|
|
5642
|
+
else {
|
|
5643
|
+
const bad = results.filter((r) => r.verdict === "fail" || r.verdict === "warn");
|
|
5644
|
+
console.log(chalk.gray(` ${bad.length} of ${results.length} check${bad.length === 1 ? "" : "s"} need attention.`));
|
|
5645
|
+
const wrongStatus = bad.filter((r) => !r.statusOk);
|
|
5646
|
+
if (wrongStatus.length > 0) {
|
|
5647
|
+
console.log(chalk.gray(" A healthy deployment answers:"));
|
|
5648
|
+
for (const r of wrongStatus) console.log(chalk.gray(` ${r.label.padEnd(width)} ${r.healthy}`));
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
console.log("");
|
|
5652
|
+
}, {
|
|
5653
|
+
origin,
|
|
5654
|
+
overall,
|
|
5655
|
+
probes: results
|
|
5656
|
+
});
|
|
5657
|
+
if (overall === "fail") process.exit(1);
|
|
5658
|
+
}
|
|
5659
|
+
async function logView(rawArgs, view) {
|
|
5660
|
+
const parsed = arg({
|
|
5661
|
+
"--since": String,
|
|
5662
|
+
"--tail": Number,
|
|
5663
|
+
"--previous": Boolean
|
|
5664
|
+
}, {
|
|
5665
|
+
argv: rawArgs.slice(3),
|
|
5666
|
+
permissive: true
|
|
5667
|
+
});
|
|
5668
|
+
const sinceArg = parsed["--since"];
|
|
5669
|
+
if (sinceArg !== void 0 && parseSince(sinceArg) === null) fail(`--since must be a duration like 15m, 2h or 90s; received "${sinceArg}".`);
|
|
5670
|
+
const sinceSeconds = parseSince(sinceArg) ?? view.defaultSinceSeconds;
|
|
5671
|
+
const { client } = await requireClient(rawArgs);
|
|
5672
|
+
const projectId = await requireProject(rawArgs, client);
|
|
5673
|
+
let res;
|
|
5674
|
+
try {
|
|
5675
|
+
res = await fetchRuntimeLogs(client, projectId, {
|
|
5676
|
+
sinceSeconds,
|
|
5677
|
+
tailLines: parsed["--tail"] ?? 500,
|
|
5678
|
+
previous: Boolean(parsed["--previous"])
|
|
5679
|
+
});
|
|
5680
|
+
} catch (e) {
|
|
5681
|
+
reportError(e, "Failed to fetch runtime logs");
|
|
5682
|
+
}
|
|
5683
|
+
const parsedLines = (res.logs ?? "").split("\n").filter((l) => l !== "").map(parseLogLine);
|
|
5684
|
+
const kept = view.filter ? parsedLines.filter((l) => view.filter(l.text)) : parsedLines;
|
|
5685
|
+
const shown = kept.slice(-view.limit);
|
|
5686
|
+
emit(() => {
|
|
5687
|
+
console.log("");
|
|
5688
|
+
console.log(chalk.bold(` ${view.title} — ${displayProjectRef(rawArgs)}`) + chalk.gray(` last ${formatDuration(sinceSeconds)}`));
|
|
5689
|
+
console.log("");
|
|
5690
|
+
printPodStates(res);
|
|
5691
|
+
if (shown.length === 0) {
|
|
5692
|
+
console.log(chalk.gray(" (nothing matched in this window)"));
|
|
5693
|
+
console.log("");
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
for (const l of shown) console.log(` ${l.pod ? chalk.gray(`[${l.pod}] `) : ""}${l.text}`);
|
|
5697
|
+
console.log("");
|
|
5698
|
+
if (kept.length > shown.length) {
|
|
5699
|
+
console.log(chalk.gray(` (showing the last ${shown.length} of ${kept.length} matching lines)`));
|
|
5700
|
+
console.log("");
|
|
5701
|
+
}
|
|
5702
|
+
}, {
|
|
5703
|
+
sinceSeconds,
|
|
5704
|
+
state: res.state ?? null,
|
|
5705
|
+
pods: res.pods ?? [],
|
|
5706
|
+
truncated: Boolean(res.truncated),
|
|
5707
|
+
matched: kept.length,
|
|
5708
|
+
lines: shown
|
|
5709
|
+
});
|
|
5710
|
+
}
|
|
5711
|
+
async function requestsCommand(rawArgs) {
|
|
5712
|
+
const parsed = arg({
|
|
5713
|
+
"--since": String,
|
|
5714
|
+
"--tail": Number
|
|
5715
|
+
}, {
|
|
5716
|
+
argv: rawArgs.slice(3),
|
|
5717
|
+
permissive: true
|
|
5718
|
+
});
|
|
5719
|
+
const sinceArg = parsed["--since"];
|
|
5720
|
+
if (sinceArg !== void 0 && parseSince(sinceArg) === null) fail(`--since must be a duration like 15m, 2h or 90s; received "${sinceArg}".`);
|
|
5721
|
+
const sinceSeconds = parseSince(sinceArg) ?? 900;
|
|
5722
|
+
const { client } = await requireClient(rawArgs);
|
|
5723
|
+
const projectId = await requireProject(rawArgs, client);
|
|
5724
|
+
let res;
|
|
5725
|
+
try {
|
|
5726
|
+
res = await fetchRuntimeLogs(client, projectId, {
|
|
5727
|
+
sinceSeconds,
|
|
5728
|
+
tailLines: parsed["--tail"] ?? 1e3
|
|
5729
|
+
});
|
|
5730
|
+
} catch (e) {
|
|
5731
|
+
reportError(e, "Failed to fetch runtime logs");
|
|
5732
|
+
}
|
|
5733
|
+
const shown = (res.logs ?? "").split("\n").filter((l) => l !== "").map((l) => parseRequestLine(parseLogLine(l).text)).filter((e) => e !== null).slice(-40);
|
|
5734
|
+
emit(() => {
|
|
5735
|
+
console.log("");
|
|
5736
|
+
console.log(chalk.bold(` 🌐 Requests — ${displayProjectRef(rawArgs)}`) + chalk.gray(` last ${formatDuration(sinceSeconds)}`));
|
|
5737
|
+
console.log("");
|
|
5738
|
+
printPodStates(res);
|
|
5739
|
+
if (shown.length === 0) {
|
|
5740
|
+
console.log(chalk.gray(" No structured request lines in this window."));
|
|
5741
|
+
console.log(chalk.gray(" (this view needs the server's request logging; try `debug logs`)"));
|
|
5742
|
+
console.log("");
|
|
5743
|
+
return;
|
|
5744
|
+
}
|
|
5745
|
+
for (const e of shown) {
|
|
5746
|
+
const status = e.status ?? 0;
|
|
5747
|
+
const color = status >= 500 ? chalk.red : status >= 400 ? chalk.yellow : chalk.green;
|
|
5748
|
+
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`)}`);
|
|
5749
|
+
}
|
|
5750
|
+
console.log("");
|
|
5751
|
+
}, {
|
|
5752
|
+
sinceSeconds,
|
|
5753
|
+
requests: shown
|
|
5754
|
+
});
|
|
5755
|
+
}
|
|
5756
|
+
async function podCommand(rawArgs) {
|
|
5757
|
+
const { client } = await requireClient(rawArgs);
|
|
5758
|
+
const projectId = await requireProject(rawArgs, client);
|
|
5759
|
+
let m;
|
|
5760
|
+
try {
|
|
5761
|
+
m = await client.functions.invoke("metrics", void 0, {
|
|
5762
|
+
method: "GET",
|
|
5763
|
+
path: projectId
|
|
5764
|
+
});
|
|
5765
|
+
} catch (e) {
|
|
5766
|
+
reportError(e, "Failed to read workload placement");
|
|
5767
|
+
}
|
|
5768
|
+
const p = m.placement ?? {};
|
|
5769
|
+
const replicas = p.replicas ?? {};
|
|
5770
|
+
emit(() => {
|
|
5771
|
+
console.log("");
|
|
5772
|
+
console.log(chalk.bold(` ☸ Workload — ${displayProjectRef(rawArgs)}`));
|
|
5773
|
+
console.log("");
|
|
5774
|
+
keyValues([
|
|
5775
|
+
["Status", m.status ? colorStatus(m.status === "running" ? "active" : m.status) : void 0],
|
|
5776
|
+
["Replicas", replicas.desired === void 0 ? void 0 : `${replicas.available ?? 0} / ${replicas.desired} available`],
|
|
5777
|
+
["Namespace", p.namespace],
|
|
5778
|
+
["Cluster", p.cluster],
|
|
5779
|
+
["Region", [p.provider, p.region].filter(Boolean).join(" · ") || void 0],
|
|
5780
|
+
["Host", p.host],
|
|
5781
|
+
["Image", p.image],
|
|
5782
|
+
["CPU", m.cpu ?? void 0],
|
|
5783
|
+
["Memory", m.memory ?? void 0]
|
|
5784
|
+
]);
|
|
5785
|
+
console.log("");
|
|
5786
|
+
if ((replicas.available ?? 0) === 0 && (replicas.desired ?? 0) > 0) {
|
|
5787
|
+
console.log(chalk.yellow(" No replica is available — the pod is not passing its readiness check."));
|
|
5788
|
+
console.log(chalk.gray(" See why with: ") + chalk.bold("rebase cloud debug logs --previous"));
|
|
5789
|
+
console.log("");
|
|
5790
|
+
}
|
|
5791
|
+
}, {
|
|
5792
|
+
status: m.status ?? null,
|
|
5793
|
+
placement: p
|
|
5794
|
+
});
|
|
5795
|
+
}
|
|
5796
|
+
async function dbDebugCommand(rawArgs) {
|
|
5797
|
+
const { client } = await requireClient(rawArgs);
|
|
5798
|
+
const projectId = await requireProject(rawArgs, client);
|
|
5799
|
+
let info;
|
|
5800
|
+
try {
|
|
5801
|
+
info = await client.functions.invoke("db-info", void 0, {
|
|
5802
|
+
method: "GET",
|
|
5803
|
+
path: projectId
|
|
5804
|
+
});
|
|
5805
|
+
} catch (e) {
|
|
5806
|
+
reportError(e, "Failed to read database connection info");
|
|
5807
|
+
}
|
|
5808
|
+
const pf = info.portForward;
|
|
5809
|
+
const forwardCmd = pf ? `kubectl port-forward -n ${pf.namespace} svc/${pf.service} ${pf.localPort}:${pf.remotePort}` : null;
|
|
5810
|
+
const psqlCmd = pf && info.username && info.database ? `psql -h 127.0.0.1 -p ${pf.localPort} -U ${info.username} -d ${info.database}` : null;
|
|
5811
|
+
emit(() => {
|
|
5812
|
+
console.log("");
|
|
5813
|
+
console.log(chalk.bold(` 🐘 Database — ${displayProjectRef(rawArgs)}`));
|
|
5814
|
+
console.log("");
|
|
5815
|
+
if (info.unavailableReason) {
|
|
5816
|
+
console.log(chalk.yellow(` ${info.unavailableReason}`));
|
|
5817
|
+
console.log("");
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
keyValues([
|
|
5821
|
+
["Type", info.type],
|
|
5822
|
+
["Host", info.host],
|
|
5823
|
+
["Port", info.port],
|
|
5824
|
+
["Database", info.database],
|
|
5825
|
+
["Username", info.username],
|
|
5826
|
+
["Password", info.passwordAvailable ? chalk.gray("stored — not shown here") : chalk.yellow("none stored")]
|
|
5827
|
+
]);
|
|
5828
|
+
console.log("");
|
|
5829
|
+
if (forwardCmd) {
|
|
5830
|
+
console.log(chalk.gray(" A managed database is only reachable inside its cluster. To connect:"));
|
|
5831
|
+
console.log("");
|
|
5832
|
+
console.log(` ${forwardCmd}`);
|
|
5833
|
+
if (psqlCmd) console.log(` ${psqlCmd}`);
|
|
5834
|
+
console.log("");
|
|
5835
|
+
if (info.passwordAvailable) {
|
|
5836
|
+
console.log(chalk.gray(" Get the password with: ") + chalk.bold("rebase cloud db info --reveal"));
|
|
5837
|
+
console.log("");
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
}, {
|
|
5841
|
+
type: info.type ?? null,
|
|
5842
|
+
host: info.host ?? null,
|
|
5843
|
+
port: info.port ?? null,
|
|
5844
|
+
database: info.database ?? null,
|
|
5845
|
+
username: info.username ?? null,
|
|
5846
|
+
passwordAvailable: Boolean(info.passwordAvailable),
|
|
5847
|
+
portForwardCommand: forwardCmd,
|
|
5848
|
+
psqlCommand: psqlCmd
|
|
5849
|
+
});
|
|
5850
|
+
}
|
|
5851
|
+
async function debugCommand(action, rawArgs) {
|
|
5852
|
+
switch (action) {
|
|
5853
|
+
case void 0:
|
|
5854
|
+
case "health":
|
|
5855
|
+
await healthCommand(rawArgs);
|
|
5856
|
+
break;
|
|
5857
|
+
case "logs":
|
|
5858
|
+
await logView(rawArgs, {
|
|
5859
|
+
defaultSinceSeconds: 900,
|
|
5860
|
+
limit: 200,
|
|
5861
|
+
title: "📄 Logs"
|
|
5862
|
+
});
|
|
5863
|
+
break;
|
|
5864
|
+
case "errors":
|
|
5865
|
+
await logView(rawArgs, {
|
|
5866
|
+
filter: isErrorLine,
|
|
5867
|
+
defaultSinceSeconds: 3600,
|
|
5868
|
+
limit: 40,
|
|
5869
|
+
title: "🔥 Errors"
|
|
5870
|
+
});
|
|
5871
|
+
break;
|
|
5872
|
+
case "boot":
|
|
5873
|
+
await logView(rawArgs, {
|
|
5874
|
+
filter: isBootLine,
|
|
5875
|
+
defaultSinceSeconds: 168 * 3600,
|
|
5876
|
+
limit: 25,
|
|
5877
|
+
title: "🚀 Boot"
|
|
5878
|
+
});
|
|
5879
|
+
break;
|
|
5880
|
+
case "requests":
|
|
5881
|
+
await requestsCommand(rawArgs);
|
|
5882
|
+
break;
|
|
5883
|
+
case "pod":
|
|
5884
|
+
case "workload":
|
|
5885
|
+
await podCommand(rawArgs);
|
|
5886
|
+
break;
|
|
5887
|
+
case "db":
|
|
5888
|
+
await dbDebugCommand(rawArgs);
|
|
5889
|
+
break;
|
|
5890
|
+
case "help":
|
|
5891
|
+
case "--help":
|
|
5892
|
+
printDebugHelp();
|
|
5893
|
+
break;
|
|
5894
|
+
default:
|
|
5895
|
+
if (isJsonMode()) fail(`Unknown debug command: ${action}`, void 0, "unknown_command");
|
|
5896
|
+
console.error(chalk.red(`Unknown debug command: ${action}`));
|
|
5897
|
+
console.log("");
|
|
5898
|
+
printDebugHelp();
|
|
5899
|
+
process.exit(1);
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
function printDebugHelp() {
|
|
5903
|
+
console.log(`
|
|
5904
|
+
${chalk.bold("rebase cloud debug")} — Find out why a deployed project is misbehaving
|
|
5905
|
+
|
|
5906
|
+
${chalk.green.bold("Usage")}
|
|
5907
|
+
rebase cloud debug ${chalk.blue("<subcommand>")} [options]
|
|
5908
|
+
|
|
5909
|
+
${chalk.green.bold("End-to-end")}
|
|
5910
|
+
${chalk.blue.bold("health")} Probe the live URL and explain every status code ${chalk.gray("(default)")}
|
|
5911
|
+
|
|
5912
|
+
${chalk.green.bold("Runtime")}
|
|
5913
|
+
${chalk.blue.bold("logs")} ${chalk.gray("[--since 15m]")} Recent application logs
|
|
5914
|
+
${chalk.blue.bold("errors")} ${chalk.gray("[--since 1h]")} Error and warning lines only
|
|
5915
|
+
${chalk.blue.bold("requests")} ${chalk.gray("[--since 15m]")} HTTP requests the server logged ${chalk.gray("(status, path, latency)")}
|
|
5916
|
+
${chalk.blue.bold("boot")} What the server decided at startup ${chalk.gray("(storage, functions, auth)")}
|
|
5917
|
+
${chalk.blue.bold("pod")} Replicas, image, namespace, cluster placement
|
|
5918
|
+
|
|
5919
|
+
${chalk.green.bold("Data")}
|
|
5920
|
+
${chalk.blue.bold("db")} Connection shape + the port-forward recipe ${chalk.gray("(no password)")}
|
|
5921
|
+
|
|
5922
|
+
${chalk.green.bold("Options")}
|
|
5923
|
+
${chalk.blue("--since <dur>")} Lookback window: ${chalk.gray("90s, 15m, 2h, 1d")}
|
|
5924
|
+
${chalk.blue("--tail <n>")} Lines to read per pod ${chalk.gray("(default 500)")}
|
|
5925
|
+
${chalk.blue("--previous")} Read the CRASHED container instance ${chalk.gray("(where the reason lives)")}
|
|
5926
|
+
${chalk.blue("--host <hostname>")} Probe this address instead of the project's own
|
|
5927
|
+
${chalk.blue("--collection <name>")} Collection for the unauth-read probe ${chalk.gray("(default: users)")}
|
|
5928
|
+
${chalk.blue("--function <name>")} Also assert this function loaded ${chalk.gray("(checked against the listing)")}
|
|
5929
|
+
${chalk.blue("--project, -p <slug>")} Operate on a project without linking
|
|
5930
|
+
|
|
5931
|
+
${chalk.gray("Everything here is read-only. `health` exits non-zero when a check fails,")}
|
|
5932
|
+
${chalk.gray("so it works in a deploy script. To restart a workload, use `rebase cloud restart`.")}
|
|
5933
|
+
`);
|
|
5934
|
+
}
|
|
5935
|
+
//#endregion
|
|
5118
5936
|
//#region src/commands/cloud/resources.ts
|
|
5119
5937
|
/**
|
|
5120
5938
|
* `rebase cloud` resource subcommands: status, metrics, webhooks, storage,
|
|
@@ -5547,6 +6365,9 @@ async function cloudCommand(subcommand, rawArgs) {
|
|
|
5547
6365
|
case "metrics":
|
|
5548
6366
|
await metricsCommand(rawArgs);
|
|
5549
6367
|
break;
|
|
6368
|
+
case "debug":
|
|
6369
|
+
await debugCommand(action, rawArgs);
|
|
6370
|
+
break;
|
|
5550
6371
|
case "env":
|
|
5551
6372
|
await envCommand(action, rawArgs);
|
|
5552
6373
|
break;
|
|
@@ -5658,6 +6479,7 @@ ${chalk.green.bold("Deploy & observe")}
|
|
|
5658
6479
|
${chalk.blue.bold("start|stop|restart")} ${chalk.gray("[-y]")} Power ops ${chalk.gray("(stop/restart need -y)")}
|
|
5659
6480
|
${chalk.blue.bold("status")} One-glance project status
|
|
5660
6481
|
${chalk.blue.bold("metrics")} Live CPU / memory / disk
|
|
6482
|
+
${chalk.blue.bold("debug")} ${chalk.gray("[health|logs|…]")} Diagnose a misbehaving deployment ${chalk.gray("(read-only)")}
|
|
5661
6483
|
|
|
5662
6484
|
${chalk.green.bold("Config")}
|
|
5663
6485
|
${chalk.blue.bold("env list|set|unset|reveal|pull")}
|
|
@@ -5719,6 +6541,7 @@ async function entry(args) {
|
|
|
5719
6541
|
const command = parsedArgs._[0];
|
|
5720
6542
|
const subcommand = parsedArgs._[1];
|
|
5721
6543
|
if (!command || parsedArgs["--help"] && ![
|
|
6544
|
+
"init",
|
|
5722
6545
|
"schema",
|
|
5723
6546
|
"db",
|
|
5724
6547
|
"dev",
|
|
@@ -5849,6 +6672,6 @@ ${chalk.gray("Documentation: https://rebase.pro/docs")}
|
|
|
5849
6672
|
`);
|
|
5850
6673
|
}
|
|
5851
6674
|
//#endregion
|
|
5852
|
-
export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateProjectName, validateTsxInstallation };
|
|
6675
|
+
export { authCommand, buildCommand, buildInitQuestions, cloudCommand, configureEnvFile, createRebaseApp, dbCommand, detectPackageManager, devCommand, doctorCommand, entry, findBackendDir, findEnvFile, findFrontendDir, findProjectRoot, formatCdTarget, generateSdkCommand, getActiveBackendPlugin, getPMCommands, isPnpmAvailable, printInitHelp, requireBackendDir, requireProjectRoot, resolveLocalBin, resolvePluginCliScript, resolveTsx, schemaCommand, startCommand, validateProjectName, validateTsxInstallation };
|
|
5853
6676
|
|
|
5854
6677
|
//# sourceMappingURL=index.es.js.map
|