postgresai 0.14.0-dev.86 → 0.14.0-dev.87
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/postgres-ai.ts +171 -12
- package/dist/bin/postgres-ai.js +367 -15
- package/lib/checkup-api.ts +47 -1
- package/lib/checkup-summary.ts +283 -0
- package/package.json +1 -1
- package/test/checkup.integration.test.ts +27 -0
- package/test/checkup.test.ts +580 -1
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-dev.
|
|
13067
|
+
version: "0.14.0-dev.87",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -15889,7 +15889,7 @@ var Result = import_lib.default.Result;
|
|
|
15889
15889
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15890
15890
|
var defaults = import_lib.default.defaults;
|
|
15891
15891
|
// package.json
|
|
15892
|
-
var version = "0.14.0-dev.
|
|
15892
|
+
var version = "0.14.0-dev.87";
|
|
15893
15893
|
var package_default2 = {
|
|
15894
15894
|
name: "postgresai",
|
|
15895
15895
|
version,
|
|
@@ -28093,8 +28093,6 @@ function unwrapRpcResponse(parsed) {
|
|
|
28093
28093
|
var HTTP_TIMEOUT_MS = 30000;
|
|
28094
28094
|
async function postRpc(params) {
|
|
28095
28095
|
const { apiKey, apiBaseUrl, rpcName, bodyObj, timeoutMs = HTTP_TIMEOUT_MS } = params;
|
|
28096
|
-
if (!apiKey)
|
|
28097
|
-
throw new Error("API key is required");
|
|
28098
28096
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
28099
28097
|
const url = new URL3(`${base}/rpc/${rpcName}`);
|
|
28100
28098
|
const body = JSON.stringify(bodyObj);
|
|
@@ -28227,6 +28225,249 @@ async function uploadCheckupReportJson(params) {
|
|
|
28227
28225
|
}
|
|
28228
28226
|
return { reportChunkId: chunkId };
|
|
28229
28227
|
}
|
|
28228
|
+
async function convertCheckupReportJsonToMarkdown(params) {
|
|
28229
|
+
const { apiKey, apiBaseUrl, checkId, jsonPayload, reportType } = params;
|
|
28230
|
+
const bodyObj = {
|
|
28231
|
+
check_id: checkId,
|
|
28232
|
+
json_payload: jsonPayload,
|
|
28233
|
+
access_token: apiKey
|
|
28234
|
+
};
|
|
28235
|
+
if (reportType) {
|
|
28236
|
+
bodyObj.report_type = reportType;
|
|
28237
|
+
}
|
|
28238
|
+
const resp = await postRpc({
|
|
28239
|
+
apiKey,
|
|
28240
|
+
apiBaseUrl,
|
|
28241
|
+
rpcName: "checkup_report_json_to_markdown",
|
|
28242
|
+
bodyObj
|
|
28243
|
+
});
|
|
28244
|
+
return resp;
|
|
28245
|
+
}
|
|
28246
|
+
|
|
28247
|
+
// lib/checkup-summary.ts
|
|
28248
|
+
function generateCheckSummary(checkId, report) {
|
|
28249
|
+
const nodeIds = Object.keys(report.results || {});
|
|
28250
|
+
if (nodeIds.length === 0) {
|
|
28251
|
+
return { status: "info", message: "No data" };
|
|
28252
|
+
}
|
|
28253
|
+
const nodeData = report.results[nodeIds[0]];
|
|
28254
|
+
switch (checkId) {
|
|
28255
|
+
case "H001":
|
|
28256
|
+
return summarizeH001(nodeData);
|
|
28257
|
+
case "H002":
|
|
28258
|
+
return summarizeH002(nodeData);
|
|
28259
|
+
case "H004":
|
|
28260
|
+
return summarizeH004(nodeData);
|
|
28261
|
+
case "A002":
|
|
28262
|
+
return summarizeA002(nodeData);
|
|
28263
|
+
case "A013":
|
|
28264
|
+
return summarizeA013(nodeData);
|
|
28265
|
+
case "A003":
|
|
28266
|
+
return summarizeA003(nodeData);
|
|
28267
|
+
case "A004":
|
|
28268
|
+
return summarizeA004(nodeData);
|
|
28269
|
+
case "A007":
|
|
28270
|
+
return summarizeA007(nodeData);
|
|
28271
|
+
case "D001":
|
|
28272
|
+
return summarizeD001(nodeData);
|
|
28273
|
+
case "D004":
|
|
28274
|
+
return summarizeD004(nodeData);
|
|
28275
|
+
case "F001":
|
|
28276
|
+
return summarizeF001(nodeData);
|
|
28277
|
+
case "G001":
|
|
28278
|
+
return summarizeG001(nodeData);
|
|
28279
|
+
case "G003":
|
|
28280
|
+
return summarizeG003(nodeData);
|
|
28281
|
+
default:
|
|
28282
|
+
return { status: "info", message: "Check completed" };
|
|
28283
|
+
}
|
|
28284
|
+
}
|
|
28285
|
+
function summarizeA003(nodeData) {
|
|
28286
|
+
const data = nodeData?.data || {};
|
|
28287
|
+
const settingsCount = Object.keys(data).length;
|
|
28288
|
+
if (settingsCount === 0) {
|
|
28289
|
+
return { status: "info", message: "No settings found" };
|
|
28290
|
+
}
|
|
28291
|
+
return {
|
|
28292
|
+
status: "info",
|
|
28293
|
+
message: `${settingsCount} setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28294
|
+
};
|
|
28295
|
+
}
|
|
28296
|
+
function summarizeA004(nodeData) {
|
|
28297
|
+
const data = nodeData?.data;
|
|
28298
|
+
if (!data) {
|
|
28299
|
+
return { status: "info", message: "Cluster information collected" };
|
|
28300
|
+
}
|
|
28301
|
+
const dbCount = Object.keys(data.database_sizes || {}).length;
|
|
28302
|
+
if (dbCount > 0) {
|
|
28303
|
+
return { status: "info", message: `${dbCount} database${dbCount > 1 ? "s" : ""} analyzed` };
|
|
28304
|
+
}
|
|
28305
|
+
return { status: "info", message: "Cluster information collected" };
|
|
28306
|
+
}
|
|
28307
|
+
function summarizeA007(nodeData) {
|
|
28308
|
+
const data = nodeData?.data || {};
|
|
28309
|
+
const alteredCount = Object.keys(data).length;
|
|
28310
|
+
if (alteredCount === 0) {
|
|
28311
|
+
return { status: "ok", message: "No altered settings" };
|
|
28312
|
+
}
|
|
28313
|
+
return {
|
|
28314
|
+
status: "info",
|
|
28315
|
+
message: `${alteredCount} setting${alteredCount > 1 ? "s" : ""} altered from defaults`
|
|
28316
|
+
};
|
|
28317
|
+
}
|
|
28318
|
+
function formatBytes2(bytes) {
|
|
28319
|
+
if (bytes === 0)
|
|
28320
|
+
return "0 B";
|
|
28321
|
+
if (bytes < 1024)
|
|
28322
|
+
return `${bytes} B`;
|
|
28323
|
+
if (bytes < 1024 * 1024)
|
|
28324
|
+
return `${(bytes / 1024).toFixed(0)} KiB`;
|
|
28325
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
28326
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
|
|
28327
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
28328
|
+
}
|
|
28329
|
+
function summarizeH001(nodeData) {
|
|
28330
|
+
const data = nodeData?.data || {};
|
|
28331
|
+
let totalCount = 0;
|
|
28332
|
+
let totalSize = 0;
|
|
28333
|
+
for (const dbData of Object.values(data)) {
|
|
28334
|
+
const dbEntry = dbData;
|
|
28335
|
+
totalCount += dbEntry.total_count || 0;
|
|
28336
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
28337
|
+
}
|
|
28338
|
+
if (totalCount === 0) {
|
|
28339
|
+
return { status: "ok", message: "No invalid indexes" };
|
|
28340
|
+
}
|
|
28341
|
+
return {
|
|
28342
|
+
status: "warning",
|
|
28343
|
+
message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
|
|
28344
|
+
};
|
|
28345
|
+
}
|
|
28346
|
+
function summarizeH002(nodeData) {
|
|
28347
|
+
const data = nodeData?.data || {};
|
|
28348
|
+
let totalCount = 0;
|
|
28349
|
+
let totalSize = 0;
|
|
28350
|
+
for (const dbData of Object.values(data)) {
|
|
28351
|
+
const dbEntry = dbData;
|
|
28352
|
+
totalCount += dbEntry.total_count || 0;
|
|
28353
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
28354
|
+
}
|
|
28355
|
+
if (totalCount === 0) {
|
|
28356
|
+
return { status: "ok", message: "All indexes utilized" };
|
|
28357
|
+
}
|
|
28358
|
+
return {
|
|
28359
|
+
status: "warning",
|
|
28360
|
+
message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
|
|
28361
|
+
};
|
|
28362
|
+
}
|
|
28363
|
+
function summarizeH004(nodeData) {
|
|
28364
|
+
const data = nodeData?.data || {};
|
|
28365
|
+
let totalCount = 0;
|
|
28366
|
+
let totalSize = 0;
|
|
28367
|
+
for (const dbData of Object.values(data)) {
|
|
28368
|
+
const dbEntry = dbData;
|
|
28369
|
+
totalCount += dbEntry.total_count || 0;
|
|
28370
|
+
totalSize += dbEntry.total_size_bytes || 0;
|
|
28371
|
+
}
|
|
28372
|
+
if (totalCount === 0) {
|
|
28373
|
+
return { status: "ok", message: "No redundant indexes" };
|
|
28374
|
+
}
|
|
28375
|
+
return {
|
|
28376
|
+
status: "warning",
|
|
28377
|
+
message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
|
|
28378
|
+
};
|
|
28379
|
+
}
|
|
28380
|
+
function summarizeA002(nodeData) {
|
|
28381
|
+
const ver = nodeData?.data?.version;
|
|
28382
|
+
if (!ver) {
|
|
28383
|
+
return { status: "info", message: "Version checked" };
|
|
28384
|
+
}
|
|
28385
|
+
const major = parseInt(ver.server_major_ver, 10);
|
|
28386
|
+
if (major >= 17) {
|
|
28387
|
+
return { status: "ok", message: `PostgreSQL ${major}` };
|
|
28388
|
+
}
|
|
28389
|
+
if (major >= 15) {
|
|
28390
|
+
return { status: "info", message: `PostgreSQL ${major}` };
|
|
28391
|
+
}
|
|
28392
|
+
return {
|
|
28393
|
+
status: "warning",
|
|
28394
|
+
message: `PostgreSQL ${major} (consider upgrading)`
|
|
28395
|
+
};
|
|
28396
|
+
}
|
|
28397
|
+
function summarizeA013(nodeData) {
|
|
28398
|
+
const ver = nodeData?.data?.version;
|
|
28399
|
+
if (!ver) {
|
|
28400
|
+
return { status: "info", message: "Minor version checked" };
|
|
28401
|
+
}
|
|
28402
|
+
const current = ver.version || "";
|
|
28403
|
+
return {
|
|
28404
|
+
status: "info",
|
|
28405
|
+
message: `Version ${current}`
|
|
28406
|
+
};
|
|
28407
|
+
}
|
|
28408
|
+
function summarizeD001(nodeData) {
|
|
28409
|
+
const data = nodeData?.data || {};
|
|
28410
|
+
const settingsCount = Object.keys(data).length;
|
|
28411
|
+
if (settingsCount === 0) {
|
|
28412
|
+
return { status: "info", message: "No logging settings found" };
|
|
28413
|
+
}
|
|
28414
|
+
return {
|
|
28415
|
+
status: "info",
|
|
28416
|
+
message: `${settingsCount} logging setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28417
|
+
};
|
|
28418
|
+
}
|
|
28419
|
+
function summarizeD004(nodeData) {
|
|
28420
|
+
const data = nodeData?.data || {};
|
|
28421
|
+
const settingsCount = Object.keys(data).length;
|
|
28422
|
+
if (settingsCount === 0) {
|
|
28423
|
+
return { status: "info", message: "No pg_stat_statements settings found" };
|
|
28424
|
+
}
|
|
28425
|
+
return {
|
|
28426
|
+
status: "info",
|
|
28427
|
+
message: `${settingsCount} pg_stat_statements setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28428
|
+
};
|
|
28429
|
+
}
|
|
28430
|
+
function summarizeF001(nodeData) {
|
|
28431
|
+
const data = nodeData?.data || {};
|
|
28432
|
+
const settingsCount = Object.keys(data).length;
|
|
28433
|
+
if (settingsCount === 0) {
|
|
28434
|
+
return { status: "info", message: "No autovacuum settings found" };
|
|
28435
|
+
}
|
|
28436
|
+
return {
|
|
28437
|
+
status: "info",
|
|
28438
|
+
message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28439
|
+
};
|
|
28440
|
+
}
|
|
28441
|
+
function summarizeG001(nodeData) {
|
|
28442
|
+
const data = nodeData?.data || {};
|
|
28443
|
+
const settingsCount = Object.keys(data).length;
|
|
28444
|
+
if (settingsCount === 0) {
|
|
28445
|
+
return { status: "info", message: "No memory settings found" };
|
|
28446
|
+
}
|
|
28447
|
+
return {
|
|
28448
|
+
status: "info",
|
|
28449
|
+
message: `${settingsCount} memory setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28450
|
+
};
|
|
28451
|
+
}
|
|
28452
|
+
function summarizeG003(nodeData) {
|
|
28453
|
+
const data = nodeData?.data || {};
|
|
28454
|
+
const settings = data.settings || {};
|
|
28455
|
+
const deadlockStats = data.deadlock_stats;
|
|
28456
|
+
const settingsCount = Object.keys(settings).length;
|
|
28457
|
+
if (deadlockStats && typeof deadlockStats.deadlocks === "number" && deadlockStats.deadlocks > 0) {
|
|
28458
|
+
return {
|
|
28459
|
+
status: "warning",
|
|
28460
|
+
message: `${deadlockStats.deadlocks} deadlock${deadlockStats.deadlocks > 1 ? "s" : ""} detected`
|
|
28461
|
+
};
|
|
28462
|
+
}
|
|
28463
|
+
if (settingsCount === 0) {
|
|
28464
|
+
return { status: "info", message: "No timeout/lock settings found" };
|
|
28465
|
+
}
|
|
28466
|
+
return {
|
|
28467
|
+
status: "info",
|
|
28468
|
+
message: `${settingsCount} timeout/lock setting${settingsCount > 1 ? "s" : ""} collected`
|
|
28469
|
+
};
|
|
28470
|
+
}
|
|
28230
28471
|
|
|
28231
28472
|
// bin/postgres-ai.ts
|
|
28232
28473
|
var rl = null;
|
|
@@ -28427,7 +28668,6 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
28427
28668
|
logUpload(`[Retry ${attempt}/3] createCheckupReport failed: ${errMsg}, retrying in ${delayMs}ms...`);
|
|
28428
28669
|
});
|
|
28429
28670
|
const reportId = created.reportId;
|
|
28430
|
-
logUpload(`Created remote checkup report: ${reportId}`);
|
|
28431
28671
|
const uploaded = [];
|
|
28432
28672
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
28433
28673
|
spinner.update(`Uploading ${checkId}.json`);
|
|
@@ -28445,7 +28685,6 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
28445
28685
|
});
|
|
28446
28686
|
uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
|
|
28447
28687
|
}
|
|
28448
|
-
logUpload("Upload completed");
|
|
28449
28688
|
return { project: uploadCfg.project, reportId, uploaded };
|
|
28450
28689
|
}
|
|
28451
28690
|
function writeReportFiles(reports, outputPath) {
|
|
@@ -28455,7 +28694,7 @@ function writeReportFiles(reports, outputPath) {
|
|
|
28455
28694
|
console.log(`\u2713 ${checkId}: ${filePath}`);
|
|
28456
28695
|
}
|
|
28457
28696
|
}
|
|
28458
|
-
function printUploadSummary(summary, projectWasGenerated, useStderr) {
|
|
28697
|
+
function printUploadSummary(summary, projectWasGenerated, useStderr, reports) {
|
|
28459
28698
|
const out = useStderr ? console.error : console.log;
|
|
28460
28699
|
out(`
|
|
28461
28700
|
Checkup report uploaded`);
|
|
@@ -28467,11 +28706,28 @@ Checkup report uploaded`);
|
|
|
28467
28706
|
out(`Project: ${summary.project}`);
|
|
28468
28707
|
}
|
|
28469
28708
|
out(`Report ID: ${summary.reportId}`);
|
|
28470
|
-
out("View in Console: console.postgres.ai \u2192
|
|
28709
|
+
out("View in Console: console.postgres.ai \u2192 Checkup \u2192 checkup reports");
|
|
28471
28710
|
out("");
|
|
28472
|
-
|
|
28711
|
+
const summaries = [];
|
|
28712
|
+
let skippedCount = 0;
|
|
28473
28713
|
for (const item of summary.uploaded) {
|
|
28474
|
-
|
|
28714
|
+
const report = reports[item.checkId];
|
|
28715
|
+
if (report) {
|
|
28716
|
+
const { status, message } = generateCheckSummary(item.checkId, report);
|
|
28717
|
+
const title = report.checkTitle || item.checkId;
|
|
28718
|
+
const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
|
|
28719
|
+
if (isSignificant) {
|
|
28720
|
+
summaries.push({ checkId: item.checkId, title, status, message });
|
|
28721
|
+
} else {
|
|
28722
|
+
skippedCount++;
|
|
28723
|
+
}
|
|
28724
|
+
}
|
|
28725
|
+
}
|
|
28726
|
+
for (const { checkId, title, message } of summaries) {
|
|
28727
|
+
out(` ${checkId} (${title}): ${message}`);
|
|
28728
|
+
}
|
|
28729
|
+
if (skippedCount > 0) {
|
|
28730
|
+
out(` ${skippedCount} other check${skippedCount > 1 ? "s" : ""} completed`);
|
|
28475
28731
|
}
|
|
28476
28732
|
}
|
|
28477
28733
|
function getDefaultMonitoringProjectDir() {
|
|
@@ -29498,7 +29754,7 @@ program2.command("unprepare-db [conn]").description("remove monitoring setup: dr
|
|
|
29498
29754
|
closeReadline();
|
|
29499
29755
|
}
|
|
29500
29756
|
});
|
|
29501
|
-
program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").addHelpText("after", [
|
|
29757
|
+
program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").option("--markdown", "output markdown to stdout").addHelpText("after", [
|
|
29502
29758
|
"",
|
|
29503
29759
|
"Available checks:",
|
|
29504
29760
|
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
|
|
@@ -29508,7 +29764,8 @@ program2.command("checkup [checkIdOrConn] [conn]").description("generate health
|
|
|
29508
29764
|
" postgresai checkup H002 postgresql://user:pass@host:5432/db",
|
|
29509
29765
|
" postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
|
|
29510
29766
|
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
29511
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json"
|
|
29767
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
|
|
29768
|
+
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --markdown"
|
|
29512
29769
|
].join(`
|
|
29513
29770
|
`)).action(async (checkIdOrConn, connArg, opts, cmd) => {
|
|
29514
29771
|
const checkIdPattern = /^[A-Z]\d{3}$/i;
|
|
@@ -29540,7 +29797,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
29540
29797
|
return;
|
|
29541
29798
|
}
|
|
29542
29799
|
const shouldPrintJson = !!opts.json;
|
|
29800
|
+
const shouldConvertMarkdown = !!opts.markdown;
|
|
29543
29801
|
const uploadExplicitlyRequested = opts.upload === true;
|
|
29802
|
+
if (shouldPrintJson && shouldConvertMarkdown) {
|
|
29803
|
+
console.error("Error: --json and --markdown are mutually exclusive");
|
|
29804
|
+
process.exitCode = 1;
|
|
29805
|
+
return;
|
|
29806
|
+
}
|
|
29544
29807
|
const uploadExplicitlyDisabled = opts.upload === false;
|
|
29545
29808
|
let shouldUpload = !uploadExplicitlyDisabled;
|
|
29546
29809
|
const outputPath = prepareOutputDirectory(opts.output);
|
|
@@ -29562,7 +29825,7 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
29562
29825
|
envPassword: process.env.PGPASSWORD
|
|
29563
29826
|
});
|
|
29564
29827
|
let client;
|
|
29565
|
-
const spinnerEnabled = !!process.stdout.isTTY &&
|
|
29828
|
+
const spinnerEnabled = !!process.stdout.isTTY && !shouldPrintJson;
|
|
29566
29829
|
const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
|
|
29567
29830
|
try {
|
|
29568
29831
|
spinner.update("Connecting to Postgres");
|
|
@@ -29603,11 +29866,100 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
29603
29866
|
writeReportFiles(reports, outputPath);
|
|
29604
29867
|
}
|
|
29605
29868
|
if (uploadSummary) {
|
|
29606
|
-
printUploadSummary(uploadSummary, projectWasGenerated, shouldPrintJson);
|
|
29869
|
+
printUploadSummary(uploadSummary, projectWasGenerated, shouldPrintJson || shouldConvertMarkdown, reports);
|
|
29607
29870
|
}
|
|
29608
|
-
if (
|
|
29871
|
+
if (shouldConvertMarkdown) {
|
|
29872
|
+
let apiKey;
|
|
29873
|
+
let apiBaseUrl;
|
|
29874
|
+
try {
|
|
29875
|
+
const configResult = getConfig(rootOpts);
|
|
29876
|
+
apiKey = configResult.apiKey;
|
|
29877
|
+
const cfg = readConfig();
|
|
29878
|
+
apiBaseUrl = resolveBaseUrls2(rootOpts, cfg).apiBaseUrl;
|
|
29879
|
+
} catch (error2) {
|
|
29880
|
+
spinner.stop();
|
|
29881
|
+
console.error("Error: Failed to read configuration for markdown conversion");
|
|
29882
|
+
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
29883
|
+
process.exitCode = 1;
|
|
29884
|
+
return;
|
|
29885
|
+
}
|
|
29886
|
+
const markdownResults = [];
|
|
29887
|
+
for (const [checkId2, report] of Object.entries(reports)) {
|
|
29888
|
+
try {
|
|
29889
|
+
spinner.update(`Converting ${checkId2} to markdown`);
|
|
29890
|
+
const markdownResult = await convertCheckupReportJsonToMarkdown({
|
|
29891
|
+
apiKey,
|
|
29892
|
+
apiBaseUrl,
|
|
29893
|
+
checkId: checkId2,
|
|
29894
|
+
jsonPayload: report,
|
|
29895
|
+
reportType: checkId2
|
|
29896
|
+
});
|
|
29897
|
+
const markdown = markdownResult?.reports?.[0]?.markdown || markdownResult?.markdown;
|
|
29898
|
+
markdownResults.push({
|
|
29899
|
+
checkId: checkId2,
|
|
29900
|
+
markdown
|
|
29901
|
+
});
|
|
29902
|
+
} catch (error2) {
|
|
29903
|
+
markdownResults.push({
|
|
29904
|
+
checkId: checkId2,
|
|
29905
|
+
error: error2 instanceof Error ? error2 : new Error(String(error2))
|
|
29906
|
+
});
|
|
29907
|
+
}
|
|
29908
|
+
}
|
|
29909
|
+
spinner.stop();
|
|
29910
|
+
for (const result of markdownResults) {
|
|
29911
|
+
if (result.error) {
|
|
29912
|
+
if (result.error instanceof RpcError) {
|
|
29913
|
+
console.error(`Error converting ${result.checkId} to markdown:`);
|
|
29914
|
+
for (const line of formatRpcErrorForDisplay(result.error)) {
|
|
29915
|
+
console.error(line);
|
|
29916
|
+
}
|
|
29917
|
+
} else {
|
|
29918
|
+
console.error(`Error converting ${result.checkId} to markdown: ${result.error.message}`);
|
|
29919
|
+
}
|
|
29920
|
+
} else if (result.markdown) {
|
|
29921
|
+
console.log(result.markdown);
|
|
29922
|
+
if (!result.markdown.endsWith(`
|
|
29923
|
+
`)) {
|
|
29924
|
+
console.log();
|
|
29925
|
+
}
|
|
29926
|
+
} else {
|
|
29927
|
+
console.error(`Warning: No markdown content returned for ${result.checkId}`);
|
|
29928
|
+
}
|
|
29929
|
+
}
|
|
29930
|
+
}
|
|
29931
|
+
if (shouldPrintJson) {
|
|
29609
29932
|
console.log(JSON.stringify(reports, null, 2));
|
|
29610
29933
|
}
|
|
29934
|
+
const hadOutput = shouldPrintJson || shouldConvertMarkdown || outputPath || uploadSummary;
|
|
29935
|
+
if (!hadOutput) {
|
|
29936
|
+
const checkCount = Object.keys(reports).length;
|
|
29937
|
+
console.log(`Checkup completed: ${checkCount} check${checkCount > 1 ? "s" : ""}
|
|
29938
|
+
`);
|
|
29939
|
+
const summaries = [];
|
|
29940
|
+
let skippedCount = 0;
|
|
29941
|
+
for (const [checkId2, report] of Object.entries(reports)) {
|
|
29942
|
+
const { status, message } = generateCheckSummary(checkId2, report);
|
|
29943
|
+
const title = report.checkTitle || checkId2;
|
|
29944
|
+
const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
|
|
29945
|
+
if (isSignificant) {
|
|
29946
|
+
summaries.push({ checkId: checkId2, title, status, message });
|
|
29947
|
+
} else {
|
|
29948
|
+
skippedCount++;
|
|
29949
|
+
}
|
|
29950
|
+
}
|
|
29951
|
+
for (const { checkId: checkId2, title, message } of summaries) {
|
|
29952
|
+
console.log(` ${checkId2} (${title}): ${message}`);
|
|
29953
|
+
}
|
|
29954
|
+
if (skippedCount > 0) {
|
|
29955
|
+
console.log(` ${skippedCount} other check${skippedCount > 1 ? "s" : ""} completed`);
|
|
29956
|
+
}
|
|
29957
|
+
console.log(`
|
|
29958
|
+
For details:`);
|
|
29959
|
+
console.log(" --json Output JSON");
|
|
29960
|
+
console.log(" --markdown Output markdown");
|
|
29961
|
+
console.log(" --output <dir> Save to directory");
|
|
29962
|
+
}
|
|
29611
29963
|
} catch (error2) {
|
|
29612
29964
|
if (error2 instanceof RpcError) {
|
|
29613
29965
|
for (const line of formatRpcErrorForDisplay(error2)) {
|
package/lib/checkup-api.ts
CHANGED
|
@@ -186,7 +186,11 @@ async function postRpc<T>(params: {
|
|
|
186
186
|
timeoutMs?: number;
|
|
187
187
|
}): Promise<T> {
|
|
188
188
|
const { apiKey, apiBaseUrl, rpcName, bodyObj, timeoutMs = HTTP_TIMEOUT_MS } = params;
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
// NOTE: API key validation removed intentionally to allow markdown conversion without auth.
|
|
191
|
+
// When apiKey is empty, API returns partial markdown (observations only, no full reports).
|
|
192
|
+
// API will return 401/403 for endpoints that require authentication.
|
|
193
|
+
|
|
190
194
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
191
195
|
const url = new URL(`${base}/rpc/${rpcName}`);
|
|
192
196
|
const body = JSON.stringify(bodyObj);
|
|
@@ -384,3 +388,45 @@ export async function uploadCheckupReportJson(params: {
|
|
|
384
388
|
}
|
|
385
389
|
return { reportChunkId: chunkId };
|
|
386
390
|
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Convert a checkup report JSON to markdown format using the PostgresAI API.
|
|
394
|
+
* This calls the v1.checkup_report_json_to_markdown RPC function.
|
|
395
|
+
*
|
|
396
|
+
* @param params - Configuration for the conversion
|
|
397
|
+
* @param params.apiKey - PostgresAI API access token
|
|
398
|
+
* @param params.apiBaseUrl - Base URL of the PostgresAI API
|
|
399
|
+
* @param params.checkId - Check identifier (e.g., "H001", "A003")
|
|
400
|
+
* @param params.jsonPayload - The JSON data from the check report
|
|
401
|
+
* @param params.reportType - Optional report type parameter
|
|
402
|
+
* @returns Promise resolving to the markdown content as JSON
|
|
403
|
+
* @throws {RpcError} On API failures (4xx/5xx responses)
|
|
404
|
+
* @throws {Error} On network errors or unexpected response format
|
|
405
|
+
*/
|
|
406
|
+
export async function convertCheckupReportJsonToMarkdown(params: {
|
|
407
|
+
apiKey: string;
|
|
408
|
+
apiBaseUrl: string;
|
|
409
|
+
checkId: string;
|
|
410
|
+
jsonPayload: any;
|
|
411
|
+
reportType?: string;
|
|
412
|
+
}): Promise<any> {
|
|
413
|
+
const { apiKey, apiBaseUrl, checkId, jsonPayload, reportType } = params;
|
|
414
|
+
const bodyObj: Record<string, unknown> = {
|
|
415
|
+
check_id: checkId,
|
|
416
|
+
json_payload: jsonPayload,
|
|
417
|
+
access_token: apiKey,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
if (reportType) {
|
|
421
|
+
bodyObj.report_type = reportType;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const resp = await postRpc<any>({
|
|
425
|
+
apiKey,
|
|
426
|
+
apiBaseUrl,
|
|
427
|
+
rpcName: "checkup_report_json_to_markdown",
|
|
428
|
+
bodyObj,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return resp;
|
|
432
|
+
}
|