postgresai 0.14.0 → 0.15.0-dev.2
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 +336 -10
- package/dist/bin/postgres-ai.js +1118 -17
- package/lib/checkup.ts +226 -0
- package/lib/config.ts +4 -0
- package/lib/mcp-server.ts +90 -0
- package/lib/metrics-loader.ts +3 -0
- package/lib/reports.ts +373 -0
- package/package.json +1 -1
- package/scripts/embed-metrics.ts +3 -0
- package/scripts/generate-release-notes.ts +283 -48
- package/test/checkup.test.ts +1 -1
- package/test/mcp-server.test.ts +447 -0
- package/test/monitoring.test.ts +261 -0
- package/test/reports.cli.test.ts +709 -0
- package/test/reports.test.ts +977 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as crypto from "node:crypto";
|
|
|
11
11
|
import { Client } from "pg";
|
|
12
12
|
import { startMcpServer } from "../lib/mcp-server";
|
|
13
13
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
|
|
14
|
+
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, renderMarkdownForTerminal, parseFlexibleDate } from "../lib/reports";
|
|
14
15
|
import { resolveBaseUrls } from "../lib/util";
|
|
15
16
|
import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
|
|
16
17
|
import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
|
|
@@ -24,6 +25,18 @@ import { getCheckupEntry } from "../lib/checkup-dictionary";
|
|
|
24
25
|
import { createCheckupReport, uploadCheckupReportJson, convertCheckupReportJsonToMarkdown, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
|
|
25
26
|
import { generateCheckSummary } from "../lib/checkup-summary";
|
|
26
27
|
|
|
28
|
+
// Node.js version check - require Node 18+
|
|
29
|
+
// Node 14 reached EOL in April 2023, Node 16 in September 2023.
|
|
30
|
+
// Node 18+ is required for native ESM, modern crypto APIs, and security updates.
|
|
31
|
+
const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
32
|
+
if (nodeVersion < 18) {
|
|
33
|
+
console.error(`\x1b[31mError: postgresai requires Node 18 or higher.\x1b[0m`);
|
|
34
|
+
console.error(`You are running Node.js ${process.versions.node}.`);
|
|
35
|
+
console.error(`Please upgrade to Node.js 20 LTS or Node.js 22 for security updates.`);
|
|
36
|
+
console.error(`\nDownload: https://nodejs.org/`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
// Singleton readline interface for stdin prompts
|
|
28
41
|
let rl: ReturnType<typeof createInterface> | null = null;
|
|
29
42
|
function getReadline() {
|
|
@@ -2061,6 +2074,85 @@ function checkRunningContainers(): { running: boolean; containers: string[] } {
|
|
|
2061
2074
|
}
|
|
2062
2075
|
}
|
|
2063
2076
|
|
|
2077
|
+
/**
|
|
2078
|
+
* Register monitoring instance with the API (non-blocking).
|
|
2079
|
+
* Returns immediately, logs result in background.
|
|
2080
|
+
*/
|
|
2081
|
+
function registerMonitoringInstance(
|
|
2082
|
+
apiKey: string,
|
|
2083
|
+
projectName: string,
|
|
2084
|
+
opts?: { apiBaseUrl?: string; debug?: boolean }
|
|
2085
|
+
): void {
|
|
2086
|
+
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
2087
|
+
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
2088
|
+
const debug = opts?.debug;
|
|
2089
|
+
|
|
2090
|
+
if (debug) {
|
|
2091
|
+
console.log(`\nDebug: Registering monitoring instance...`);
|
|
2092
|
+
console.log(`Debug: POST ${url}`);
|
|
2093
|
+
console.log(`Debug: project_name=${projectName}`);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Fire and forget - don't block the main flow
|
|
2097
|
+
fetch(url, {
|
|
2098
|
+
method: "POST",
|
|
2099
|
+
headers: {
|
|
2100
|
+
"Content-Type": "application/json",
|
|
2101
|
+
},
|
|
2102
|
+
body: JSON.stringify({
|
|
2103
|
+
api_token: apiKey,
|
|
2104
|
+
project_name: projectName,
|
|
2105
|
+
}),
|
|
2106
|
+
})
|
|
2107
|
+
.then(async (res) => {
|
|
2108
|
+
const body = await res.text().catch(() => "");
|
|
2109
|
+
if (!res.ok) {
|
|
2110
|
+
if (debug) {
|
|
2111
|
+
console.log(`Debug: Monitoring registration failed: HTTP ${res.status}`);
|
|
2112
|
+
console.log(`Debug: Response: ${body}`);
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (debug) {
|
|
2117
|
+
console.log(`Debug: Monitoring registration response: ${body}`);
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
.catch((err) => {
|
|
2121
|
+
if (debug) {
|
|
2122
|
+
console.log(`Debug: Monitoring registration error: ${err.message}`);
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* Update .pgwatch-config file with key=value pairs.
|
|
2129
|
+
* Preserves existing values not being updated.
|
|
2130
|
+
*/
|
|
2131
|
+
function updatePgwatchConfig(configPath: string, updates: Record<string, string>): void {
|
|
2132
|
+
let lines: string[] = [];
|
|
2133
|
+
|
|
2134
|
+
// Read existing config if it exists
|
|
2135
|
+
if (fs.existsSync(configPath)) {
|
|
2136
|
+
const stats = fs.statSync(configPath);
|
|
2137
|
+
if (!stats.isDirectory()) {
|
|
2138
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
2139
|
+
lines = content.split(/\r?\n/).filter(l => l.trim() !== "");
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Update or add each key
|
|
2144
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
2145
|
+
const existingIndex = lines.findIndex(l => l.startsWith(key + "="));
|
|
2146
|
+
if (existingIndex >= 0) {
|
|
2147
|
+
lines[existingIndex] = `${key}=${value}`;
|
|
2148
|
+
} else {
|
|
2149
|
+
lines.push(`${key}=${value}`);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
fs.writeFileSync(configPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2064
2156
|
/**
|
|
2065
2157
|
* Run docker compose command
|
|
2066
2158
|
*/
|
|
@@ -2145,12 +2237,13 @@ mon
|
|
|
2145
2237
|
.option("--api-key <key>", "Postgres AI API key for automated report uploads")
|
|
2146
2238
|
.option("--db-url <url>", "PostgreSQL connection URL to monitor")
|
|
2147
2239
|
.option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)")
|
|
2240
|
+
.option("--project <name>", "Docker Compose project name (default: postgres_ai)")
|
|
2148
2241
|
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
2149
|
-
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; yes: boolean }) => {
|
|
2242
|
+
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; project?: string; yes: boolean }) => {
|
|
2150
2243
|
// Get apiKey from global program options (--api-key is defined globally)
|
|
2151
2244
|
// This is needed because Commander.js routes --api-key to the global option, not the subcommand's option
|
|
2152
2245
|
const globalOpts = program.opts<CliOptions>();
|
|
2153
|
-
|
|
2246
|
+
let apiKey = opts.apiKey || globalOpts.apiKey;
|
|
2154
2247
|
|
|
2155
2248
|
console.log("\n=================================");
|
|
2156
2249
|
console.log(" PostgresAI monitoring local install");
|
|
@@ -2161,6 +2254,13 @@ mon
|
|
|
2161
2254
|
const { projectDir } = await resolveOrInitPaths();
|
|
2162
2255
|
console.log(`Project directory: ${projectDir}\n`);
|
|
2163
2256
|
|
|
2257
|
+
// Save project name to .pgwatch-config if provided (used by reporter container)
|
|
2258
|
+
if (opts.project) {
|
|
2259
|
+
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
2260
|
+
updatePgwatchConfig(cfgPath, { project_name: opts.project });
|
|
2261
|
+
console.log(`Using project name: ${opts.project}\n`);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2164
2264
|
// Update .env with custom tag if provided
|
|
2165
2265
|
const envFile = path.resolve(projectDir, ".env");
|
|
2166
2266
|
|
|
@@ -2230,10 +2330,7 @@ mon
|
|
|
2230
2330
|
console.log("Using API key provided via --api-key parameter");
|
|
2231
2331
|
config.writeConfig({ apiKey });
|
|
2232
2332
|
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
2233
|
-
|
|
2234
|
-
encoding: "utf8",
|
|
2235
|
-
mode: 0o600
|
|
2236
|
-
});
|
|
2333
|
+
updatePgwatchConfig(path.resolve(projectDir, ".pgwatch-config"), { api_key: apiKey });
|
|
2237
2334
|
console.log("✓ API key saved\n");
|
|
2238
2335
|
} else if (opts.yes) {
|
|
2239
2336
|
// Auto-yes mode without API key - skip API key setup
|
|
@@ -2252,10 +2349,8 @@ mon
|
|
|
2252
2349
|
if (trimmedKey) {
|
|
2253
2350
|
config.writeConfig({ apiKey: trimmedKey });
|
|
2254
2351
|
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
mode: 0o600
|
|
2258
|
-
});
|
|
2352
|
+
updatePgwatchConfig(path.resolve(projectDir, ".pgwatch-config"), { api_key: trimmedKey });
|
|
2353
|
+
apiKey = trimmedKey; // Update for later use in registerMonitoringInstance
|
|
2259
2354
|
console.log("✓ API key saved\n");
|
|
2260
2355
|
break;
|
|
2261
2356
|
}
|
|
@@ -2442,6 +2537,15 @@ mon
|
|
|
2442
2537
|
}
|
|
2443
2538
|
console.log("✓ Services started\n");
|
|
2444
2539
|
|
|
2540
|
+
// Register monitoring instance with API (non-blocking, only if API key is configured)
|
|
2541
|
+
if (apiKey && !opts.demo) {
|
|
2542
|
+
const projectName = opts.project || "postgres-ai-monitoring";
|
|
2543
|
+
registerMonitoringInstance(apiKey, projectName, {
|
|
2544
|
+
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
2545
|
+
debug: !!process.env.DEBUG,
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2445
2549
|
// Final summary
|
|
2446
2550
|
console.log("=================================");
|
|
2447
2551
|
console.log(" Local install completed!");
|
|
@@ -4087,6 +4191,228 @@ issues
|
|
|
4087
4191
|
}
|
|
4088
4192
|
});
|
|
4089
4193
|
|
|
4194
|
+
// Reports management
|
|
4195
|
+
const reports = program.command("reports").description("checkup reports management");
|
|
4196
|
+
|
|
4197
|
+
reports
|
|
4198
|
+
.command("list")
|
|
4199
|
+
.description("list checkup reports")
|
|
4200
|
+
.option("--project-id <id>", "filter by project id", (v: string) => parseInt(v, 10))
|
|
4201
|
+
.option("--status <status>", "filter by status (e.g., completed)")
|
|
4202
|
+
.option("--limit <n>", "max number of reports to return (default: 20)", (v: string) => parseInt(v, 10))
|
|
4203
|
+
.option("--before <date>", "show reports created before this date (YYYY-MM-DD, DD.MM.YYYY, etc.)")
|
|
4204
|
+
.option("--all", "fetch all reports (paginated automatically)")
|
|
4205
|
+
.option("--debug", "enable debug output")
|
|
4206
|
+
.option("--json", "output raw JSON")
|
|
4207
|
+
.action(async (opts: { projectId?: number; status?: string; limit?: number; before?: string; all?: boolean; debug?: boolean; json?: boolean }) => {
|
|
4208
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching reports...");
|
|
4209
|
+
try {
|
|
4210
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4211
|
+
const cfg = config.readConfig();
|
|
4212
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4213
|
+
if (!apiKey) {
|
|
4214
|
+
spinner.stop();
|
|
4215
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4216
|
+
process.exitCode = 1;
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
if (opts.all && opts.before) {
|
|
4220
|
+
spinner.stop();
|
|
4221
|
+
console.error("--all and --before cannot be used together");
|
|
4222
|
+
process.exitCode = 1;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4226
|
+
|
|
4227
|
+
let result;
|
|
4228
|
+
if (opts.all) {
|
|
4229
|
+
result = await fetchAllReports({
|
|
4230
|
+
apiKey,
|
|
4231
|
+
apiBaseUrl,
|
|
4232
|
+
projectId: opts.projectId,
|
|
4233
|
+
status: opts.status,
|
|
4234
|
+
limit: opts.limit,
|
|
4235
|
+
debug: !!opts.debug,
|
|
4236
|
+
});
|
|
4237
|
+
} else {
|
|
4238
|
+
result = await fetchReports({
|
|
4239
|
+
apiKey,
|
|
4240
|
+
apiBaseUrl,
|
|
4241
|
+
projectId: opts.projectId,
|
|
4242
|
+
status: opts.status,
|
|
4243
|
+
limit: opts.limit,
|
|
4244
|
+
beforeDate: opts.before ? parseFlexibleDate(opts.before) : undefined,
|
|
4245
|
+
debug: !!opts.debug,
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
spinner.stop();
|
|
4249
|
+
printResult(result, opts.json);
|
|
4250
|
+
} catch (err) {
|
|
4251
|
+
spinner.stop();
|
|
4252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4253
|
+
console.error(message);
|
|
4254
|
+
process.exitCode = 1;
|
|
4255
|
+
}
|
|
4256
|
+
});
|
|
4257
|
+
|
|
4258
|
+
reports
|
|
4259
|
+
.command("files [reportId]")
|
|
4260
|
+
.description("list files of a checkup report (metadata only, no content)")
|
|
4261
|
+
.option("--type <type>", "filter by file type: json, md")
|
|
4262
|
+
.option("--check-id <id>", "filter by check ID (e.g., H002)")
|
|
4263
|
+
.option("--debug", "enable debug output")
|
|
4264
|
+
.option("--json", "output raw JSON")
|
|
4265
|
+
.action(async (reportId: string | undefined, opts: { type?: "json" | "md"; checkId?: string; debug?: boolean; json?: boolean }) => {
|
|
4266
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching report files...");
|
|
4267
|
+
try {
|
|
4268
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4269
|
+
const cfg = config.readConfig();
|
|
4270
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4271
|
+
if (!apiKey) {
|
|
4272
|
+
spinner.stop();
|
|
4273
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4274
|
+
process.exitCode = 1;
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
let numericId: number | undefined;
|
|
4278
|
+
if (reportId !== undefined) {
|
|
4279
|
+
numericId = parseInt(reportId, 10);
|
|
4280
|
+
if (isNaN(numericId)) {
|
|
4281
|
+
spinner.stop();
|
|
4282
|
+
console.error("reportId must be a number");
|
|
4283
|
+
process.exitCode = 1;
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
if (numericId === undefined && !opts.checkId) {
|
|
4288
|
+
spinner.stop();
|
|
4289
|
+
console.error("Either reportId or --check-id is required");
|
|
4290
|
+
process.exitCode = 1;
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4294
|
+
|
|
4295
|
+
const result = await fetchReportFiles({
|
|
4296
|
+
apiKey,
|
|
4297
|
+
apiBaseUrl,
|
|
4298
|
+
reportId: numericId,
|
|
4299
|
+
type: opts.type,
|
|
4300
|
+
checkId: opts.checkId,
|
|
4301
|
+
debug: !!opts.debug,
|
|
4302
|
+
});
|
|
4303
|
+
spinner.stop();
|
|
4304
|
+
printResult(result, opts.json);
|
|
4305
|
+
} catch (err) {
|
|
4306
|
+
spinner.stop();
|
|
4307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4308
|
+
console.error(message);
|
|
4309
|
+
process.exitCode = 1;
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
|
|
4313
|
+
reports
|
|
4314
|
+
.command("data [reportId]")
|
|
4315
|
+
.description("get checkup report file data (includes content)")
|
|
4316
|
+
.option("--type <type>", "filter by file type: json, md")
|
|
4317
|
+
.option("--check-id <id>", "filter by check ID (e.g., H002)")
|
|
4318
|
+
.option("--formatted", "render markdown with ANSI styling (experimental)")
|
|
4319
|
+
.option("-o, --output <dir>", "save files to directory (uses original filenames)")
|
|
4320
|
+
.option("--debug", "enable debug output")
|
|
4321
|
+
.option("--json", "output raw JSON")
|
|
4322
|
+
.action(async (reportId: string | undefined, opts: { type?: "json" | "md"; checkId?: string; formatted?: boolean; output?: string; debug?: boolean; json?: boolean }) => {
|
|
4323
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching report data...");
|
|
4324
|
+
try {
|
|
4325
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4326
|
+
const cfg = config.readConfig();
|
|
4327
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4328
|
+
if (!apiKey) {
|
|
4329
|
+
spinner.stop();
|
|
4330
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4331
|
+
process.exitCode = 1;
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
let numericId: number | undefined;
|
|
4335
|
+
if (reportId !== undefined) {
|
|
4336
|
+
numericId = parseInt(reportId, 10);
|
|
4337
|
+
if (isNaN(numericId)) {
|
|
4338
|
+
spinner.stop();
|
|
4339
|
+
console.error("reportId must be a number");
|
|
4340
|
+
process.exitCode = 1;
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
if (numericId === undefined && !opts.checkId) {
|
|
4345
|
+
spinner.stop();
|
|
4346
|
+
console.error("Either reportId or --check-id is required");
|
|
4347
|
+
process.exitCode = 1;
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
4350
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4351
|
+
|
|
4352
|
+
// Default to "md" for terminal output (human-readable); --json and --output get all types
|
|
4353
|
+
const effectiveType = opts.type ?? (!opts.json && !opts.output ? "md" as const : undefined);
|
|
4354
|
+
const result = await fetchReportFileData({
|
|
4355
|
+
apiKey,
|
|
4356
|
+
apiBaseUrl,
|
|
4357
|
+
reportId: numericId,
|
|
4358
|
+
type: effectiveType,
|
|
4359
|
+
checkId: opts.checkId,
|
|
4360
|
+
debug: !!opts.debug,
|
|
4361
|
+
});
|
|
4362
|
+
spinner.stop();
|
|
4363
|
+
|
|
4364
|
+
if (opts.output) {
|
|
4365
|
+
const dir = path.resolve(opts.output);
|
|
4366
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4367
|
+
for (const f of result) {
|
|
4368
|
+
const safeName = path.basename(f.filename);
|
|
4369
|
+
const filePath = path.join(dir, safeName);
|
|
4370
|
+
const content = f.type === "json"
|
|
4371
|
+
? JSON.stringify(tryParseJson(f.data), null, 2)
|
|
4372
|
+
: f.data;
|
|
4373
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
4374
|
+
console.log(filePath);
|
|
4375
|
+
}
|
|
4376
|
+
} else if (opts.json) {
|
|
4377
|
+
const processed = result.map((f) => ({
|
|
4378
|
+
...f,
|
|
4379
|
+
data: f.type === "json" ? tryParseJson(f.data) : f.data,
|
|
4380
|
+
}));
|
|
4381
|
+
printResult(processed, true);
|
|
4382
|
+
} else if (opts.formatted && process.stdout.isTTY) {
|
|
4383
|
+
for (const f of result) {
|
|
4384
|
+
if (result.length > 1) {
|
|
4385
|
+
console.log(`\x1b[1m--- ${f.filename} (${f.check_id}, ${f.type}) ---\x1b[0m`);
|
|
4386
|
+
}
|
|
4387
|
+
if (f.type === "md") {
|
|
4388
|
+
console.log(renderMarkdownForTerminal(f.data));
|
|
4389
|
+
} else if (f.type === "json") {
|
|
4390
|
+
const parsed = tryParseJson(f.data);
|
|
4391
|
+
console.log(typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2));
|
|
4392
|
+
} else {
|
|
4393
|
+
console.log(f.data);
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
} else {
|
|
4397
|
+
for (const f of result) {
|
|
4398
|
+
if (result.length > 1) {
|
|
4399
|
+
console.log(`--- ${f.filename} (${f.check_id}, ${f.type}) ---`);
|
|
4400
|
+
}
|
|
4401
|
+
console.log(f.data);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
} catch (err) {
|
|
4405
|
+
spinner.stop();
|
|
4406
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4407
|
+
console.error(message);
|
|
4408
|
+
process.exitCode = 1;
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
function tryParseJson(s: string): unknown {
|
|
4413
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4090
4416
|
// MCP server
|
|
4091
4417
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
4092
4418
|
|