postgresai 0.15.0-dev.1 → 0.15.0-dev.3
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 +228 -4
- package/dist/bin/postgres-ai.js +704 -4
- package/lib/mcp-server.ts +90 -0
- package/lib/reports.ts +373 -0
- package/package.json +1 -1
- package/test/checkup.test.ts +28 -0
- package/test/mcp-server.test.ts +390 -0
- package/test/reports.cli.test.ts +793 -0
- package/test/reports.test.ts +977 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { Command } from "commander";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
4
|
import pkg from "../package.json";
|
|
5
5
|
import * as config from "../lib/config";
|
|
6
6
|
import * as yaml from "js-yaml";
|
|
@@ -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";
|
|
@@ -342,7 +343,8 @@ function writeReportFiles(reports: Record<string, any>, outputPath: string): voi
|
|
|
342
343
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
343
344
|
const filePath = path.join(outputPath, `${checkId}.json`);
|
|
344
345
|
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
345
|
-
|
|
346
|
+
const title = report.checkTitle || checkId;
|
|
347
|
+
console.log(`✓ ${checkId} ${title}: ${filePath}`);
|
|
346
348
|
}
|
|
347
349
|
}
|
|
348
350
|
|
|
@@ -1924,8 +1926,8 @@ program
|
|
|
1924
1926
|
}
|
|
1925
1927
|
}
|
|
1926
1928
|
|
|
1927
|
-
// Output JSON to stdout
|
|
1928
|
-
if (shouldPrintJson) {
|
|
1929
|
+
// Output JSON to stdout (unless --output is specified, in which case files are written instead)
|
|
1930
|
+
if (shouldPrintJson && !outputPath) {
|
|
1929
1931
|
console.log(JSON.stringify(reports, null, 2));
|
|
1930
1932
|
}
|
|
1931
1933
|
|
|
@@ -4190,6 +4192,228 @@ issues
|
|
|
4190
4192
|
}
|
|
4191
4193
|
});
|
|
4192
4194
|
|
|
4195
|
+
// Reports management
|
|
4196
|
+
const reports = program.command("reports").description("checkup reports management");
|
|
4197
|
+
|
|
4198
|
+
reports
|
|
4199
|
+
.command("list")
|
|
4200
|
+
.description("list checkup reports")
|
|
4201
|
+
.option("--project-id <id>", "filter by project id", (v: string) => parseInt(v, 10))
|
|
4202
|
+
.addOption(new Option("--status <status>", "filter by status (e.g., completed)").hideHelp())
|
|
4203
|
+
.option("--limit <n>", "max number of reports to return (default: 20, max: 100)", (v: string) => { const n = parseInt(v, 10); return Number.isNaN(n) ? 20 : Math.max(1, Math.min(n, 100)); })
|
|
4204
|
+
.option("--before <date>", "show reports created before this date (YYYY-MM-DD, DD.MM.YYYY, etc.)")
|
|
4205
|
+
.option("--all", "fetch all reports (paginated automatically)")
|
|
4206
|
+
.addOption(new Option("--debug", "enable debug output").hideHelp())
|
|
4207
|
+
.option("--json", "output raw JSON")
|
|
4208
|
+
.action(async (opts: { projectId?: number; status?: string; limit?: number; before?: string; all?: boolean; debug?: boolean; json?: boolean }) => {
|
|
4209
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching reports...");
|
|
4210
|
+
try {
|
|
4211
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4212
|
+
const cfg = config.readConfig();
|
|
4213
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4214
|
+
if (!apiKey) {
|
|
4215
|
+
spinner.stop();
|
|
4216
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4217
|
+
process.exitCode = 1;
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (opts.all && opts.before) {
|
|
4221
|
+
spinner.stop();
|
|
4222
|
+
console.error("--all and --before cannot be used together");
|
|
4223
|
+
process.exitCode = 1;
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4227
|
+
|
|
4228
|
+
let result;
|
|
4229
|
+
if (opts.all) {
|
|
4230
|
+
result = await fetchAllReports({
|
|
4231
|
+
apiKey,
|
|
4232
|
+
apiBaseUrl,
|
|
4233
|
+
projectId: opts.projectId,
|
|
4234
|
+
status: opts.status,
|
|
4235
|
+
limit: opts.limit,
|
|
4236
|
+
debug: !!opts.debug,
|
|
4237
|
+
});
|
|
4238
|
+
} else {
|
|
4239
|
+
result = await fetchReports({
|
|
4240
|
+
apiKey,
|
|
4241
|
+
apiBaseUrl,
|
|
4242
|
+
projectId: opts.projectId,
|
|
4243
|
+
status: opts.status,
|
|
4244
|
+
limit: opts.limit,
|
|
4245
|
+
beforeDate: opts.before ? parseFlexibleDate(opts.before) : undefined,
|
|
4246
|
+
debug: !!opts.debug,
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
spinner.stop();
|
|
4250
|
+
printResult(result, opts.json);
|
|
4251
|
+
} catch (err) {
|
|
4252
|
+
spinner.stop();
|
|
4253
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4254
|
+
console.error(message);
|
|
4255
|
+
process.exitCode = 1;
|
|
4256
|
+
}
|
|
4257
|
+
});
|
|
4258
|
+
|
|
4259
|
+
reports
|
|
4260
|
+
.command("files [reportId]")
|
|
4261
|
+
.description("list files of a checkup report (metadata only, no content)")
|
|
4262
|
+
.option("--type <type>", "filter by file type: json, md")
|
|
4263
|
+
.option("--check-id <id>", "filter by check ID (e.g., H002)")
|
|
4264
|
+
.addOption(new Option("--debug", "enable debug output").hideHelp())
|
|
4265
|
+
.option("--json", "output raw JSON")
|
|
4266
|
+
.action(async (reportId: string | undefined, opts: { type?: "json" | "md"; checkId?: string; debug?: boolean; json?: boolean }) => {
|
|
4267
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching report files...");
|
|
4268
|
+
try {
|
|
4269
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4270
|
+
const cfg = config.readConfig();
|
|
4271
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4272
|
+
if (!apiKey) {
|
|
4273
|
+
spinner.stop();
|
|
4274
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4275
|
+
process.exitCode = 1;
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
let numericId: number | undefined;
|
|
4279
|
+
if (reportId !== undefined) {
|
|
4280
|
+
numericId = parseInt(reportId, 10);
|
|
4281
|
+
if (isNaN(numericId)) {
|
|
4282
|
+
spinner.stop();
|
|
4283
|
+
console.error("reportId must be a number");
|
|
4284
|
+
process.exitCode = 1;
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
if (numericId === undefined && !opts.checkId) {
|
|
4289
|
+
spinner.stop();
|
|
4290
|
+
console.error("Either reportId or --check-id is required");
|
|
4291
|
+
process.exitCode = 1;
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4295
|
+
|
|
4296
|
+
const result = await fetchReportFiles({
|
|
4297
|
+
apiKey,
|
|
4298
|
+
apiBaseUrl,
|
|
4299
|
+
reportId: numericId,
|
|
4300
|
+
type: opts.type,
|
|
4301
|
+
checkId: opts.checkId,
|
|
4302
|
+
debug: !!opts.debug,
|
|
4303
|
+
});
|
|
4304
|
+
spinner.stop();
|
|
4305
|
+
printResult(result, opts.json);
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
spinner.stop();
|
|
4308
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4309
|
+
console.error(message);
|
|
4310
|
+
process.exitCode = 1;
|
|
4311
|
+
}
|
|
4312
|
+
});
|
|
4313
|
+
|
|
4314
|
+
reports
|
|
4315
|
+
.command("data [reportId]")
|
|
4316
|
+
.description("get checkup report file data (includes content)")
|
|
4317
|
+
.option("--type <type>", "filter by file type: json, md")
|
|
4318
|
+
.option("--check-id <id>", "filter by check ID (e.g., H002)")
|
|
4319
|
+
.option("--formatted", "render markdown with ANSI styling (experimental)")
|
|
4320
|
+
.option("-o, --output <dir>", "save files to directory (uses original filenames)")
|
|
4321
|
+
.addOption(new Option("--debug", "enable debug output").hideHelp())
|
|
4322
|
+
.option("--json", "output raw JSON")
|
|
4323
|
+
.action(async (reportId: string | undefined, opts: { type?: "json" | "md"; checkId?: string; formatted?: boolean; output?: string; debug?: boolean; json?: boolean }) => {
|
|
4324
|
+
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching report data...");
|
|
4325
|
+
try {
|
|
4326
|
+
const rootOpts = program.opts<CliOptions>();
|
|
4327
|
+
const cfg = config.readConfig();
|
|
4328
|
+
const { apiKey } = getConfig(rootOpts);
|
|
4329
|
+
if (!apiKey) {
|
|
4330
|
+
spinner.stop();
|
|
4331
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
4332
|
+
process.exitCode = 1;
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
let numericId: number | undefined;
|
|
4336
|
+
if (reportId !== undefined) {
|
|
4337
|
+
numericId = parseInt(reportId, 10);
|
|
4338
|
+
if (isNaN(numericId)) {
|
|
4339
|
+
spinner.stop();
|
|
4340
|
+
console.error("reportId must be a number");
|
|
4341
|
+
process.exitCode = 1;
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
if (numericId === undefined && !opts.checkId) {
|
|
4346
|
+
spinner.stop();
|
|
4347
|
+
console.error("Either reportId or --check-id is required");
|
|
4348
|
+
process.exitCode = 1;
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
4352
|
+
|
|
4353
|
+
// Default to "md" for terminal output (human-readable); --json and --output get all types
|
|
4354
|
+
const effectiveType = opts.type ?? (!opts.json && !opts.output ? "md" as const : undefined);
|
|
4355
|
+
const result = await fetchReportFileData({
|
|
4356
|
+
apiKey,
|
|
4357
|
+
apiBaseUrl,
|
|
4358
|
+
reportId: numericId,
|
|
4359
|
+
type: effectiveType,
|
|
4360
|
+
checkId: opts.checkId,
|
|
4361
|
+
debug: !!opts.debug,
|
|
4362
|
+
});
|
|
4363
|
+
spinner.stop();
|
|
4364
|
+
|
|
4365
|
+
if (opts.output) {
|
|
4366
|
+
const dir = path.resolve(opts.output);
|
|
4367
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4368
|
+
for (const f of result) {
|
|
4369
|
+
const safeName = path.basename(f.filename);
|
|
4370
|
+
const filePath = path.join(dir, safeName);
|
|
4371
|
+
const content = f.type === "json"
|
|
4372
|
+
? JSON.stringify(tryParseJson(f.data), null, 2)
|
|
4373
|
+
: f.data;
|
|
4374
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
4375
|
+
console.log(filePath);
|
|
4376
|
+
}
|
|
4377
|
+
} else if (opts.json) {
|
|
4378
|
+
const processed = result.map((f) => ({
|
|
4379
|
+
...f,
|
|
4380
|
+
data: f.type === "json" ? tryParseJson(f.data) : f.data,
|
|
4381
|
+
}));
|
|
4382
|
+
printResult(processed, true);
|
|
4383
|
+
} else if (opts.formatted && process.stdout.isTTY) {
|
|
4384
|
+
for (const f of result) {
|
|
4385
|
+
if (result.length > 1) {
|
|
4386
|
+
console.log(`\x1b[1m--- ${f.filename} (${f.check_id}, ${f.type}) ---\x1b[0m`);
|
|
4387
|
+
}
|
|
4388
|
+
if (f.type === "md") {
|
|
4389
|
+
console.log(renderMarkdownForTerminal(f.data));
|
|
4390
|
+
} else if (f.type === "json") {
|
|
4391
|
+
const parsed = tryParseJson(f.data);
|
|
4392
|
+
console.log(typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2));
|
|
4393
|
+
} else {
|
|
4394
|
+
console.log(f.data);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
} else {
|
|
4398
|
+
for (const f of result) {
|
|
4399
|
+
if (result.length > 1) {
|
|
4400
|
+
console.log(`--- ${f.filename} (${f.check_id}, ${f.type}) ---`);
|
|
4401
|
+
}
|
|
4402
|
+
console.log(f.data);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
} catch (err) {
|
|
4406
|
+
spinner.stop();
|
|
4407
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4408
|
+
console.error(message);
|
|
4409
|
+
process.exitCode = 1;
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
function tryParseJson(s: string): unknown {
|
|
4414
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4193
4417
|
// MCP server
|
|
4194
4418
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
4195
4419
|
|