postgresai 0.14.0-dev.53 → 0.14.0-dev.55

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.
@@ -7,6 +7,7 @@ import * as yaml from "js-yaml";
7
7
  import * as fs from "fs";
8
8
  import * as path from "path";
9
9
  import * as os from "os";
10
+ import * as crypto from "node:crypto";
10
11
  import { Client } from "pg";
11
12
  import { startMcpServer } from "../lib/mcp-server";
12
13
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
@@ -17,6 +18,8 @@ import * as authServer from "../lib/auth-server";
17
18
  import { maskSecret } from "../lib/util";
18
19
  import { createInterface } from "readline";
19
20
  import * as childProcess from "child_process";
21
+ import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
22
+ import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
20
23
 
21
24
  // Singleton readline interface for stdin prompts
22
25
  let rl: ReturnType<typeof createInterface> | null = null;
@@ -109,6 +112,255 @@ async function question(prompt: string): Promise<string> {
109
112
  });
110
113
  }
111
114
 
115
+ function expandHomePath(p: string): string {
116
+ const s = (p || "").trim();
117
+ if (!s) return s;
118
+ if (s === "~") return os.homedir();
119
+ if (s.startsWith("~/") || s.startsWith("~\\")) {
120
+ return path.join(os.homedir(), s.slice(2));
121
+ }
122
+ return s;
123
+ }
124
+
125
+ function createTtySpinner(
126
+ enabled: boolean,
127
+ initialText: string
128
+ ): { update: (text: string) => void; stop: (finalText?: string) => void } {
129
+ if (!enabled) {
130
+ return {
131
+ update: () => {},
132
+ stop: () => {},
133
+ };
134
+ }
135
+
136
+ const frames = ["|", "/", "-", "\\"];
137
+ const startTs = Date.now();
138
+ let text = initialText;
139
+ let frameIdx = 0;
140
+ let stopped = false;
141
+
142
+ const render = (): void => {
143
+ if (stopped) return;
144
+ const elapsedSec = ((Date.now() - startTs) / 1000).toFixed(1);
145
+ const frame = frames[frameIdx % frames.length] ?? frames[0] ?? "⠿";
146
+ frameIdx += 1;
147
+ process.stdout.write(`\r\x1b[2K${frame} ${text} (${elapsedSec}s)`);
148
+ };
149
+
150
+ const timer = setInterval(render, 120);
151
+ render(); // immediate feedback
152
+
153
+ return {
154
+ update: (t: string) => {
155
+ text = t;
156
+ render();
157
+ },
158
+ stop: (finalText?: string) => {
159
+ if (stopped) return;
160
+ // Set flag first so any queued render() calls exit early.
161
+ // JavaScript is single-threaded, so this is safe: queued callbacks
162
+ // run after stop() returns and will see stopped=true immediately.
163
+ stopped = true;
164
+ clearInterval(timer);
165
+ process.stdout.write("\r\x1b[2K");
166
+ if (finalText && finalText.trim()) {
167
+ process.stdout.write(finalText);
168
+ }
169
+ process.stdout.write("\n");
170
+ },
171
+ };
172
+ }
173
+
174
+ // ============================================================================
175
+ // Checkup command helpers
176
+ // ============================================================================
177
+
178
+ interface CheckupOptions {
179
+ checkId: string;
180
+ nodeName: string;
181
+ output?: string;
182
+ upload?: boolean;
183
+ project?: string;
184
+ json?: boolean;
185
+ }
186
+
187
+ interface UploadConfig {
188
+ apiKey: string;
189
+ apiBaseUrl: string;
190
+ project: string;
191
+ }
192
+
193
+ interface UploadSummary {
194
+ project: string;
195
+ reportId: number;
196
+ uploaded: Array<{ checkId: string; filename: string; chunkId: number }>;
197
+ }
198
+
199
+ /**
200
+ * Prepare and validate output directory for checkup reports.
201
+ * @returns Output path if valid, null if should exit with error
202
+ */
203
+ function prepareOutputDirectory(outputOpt: string | undefined): string | null | undefined {
204
+ if (!outputOpt) return undefined;
205
+
206
+ const outputDir = expandHomePath(outputOpt);
207
+ const outputPath = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
208
+
209
+ if (!fs.existsSync(outputPath)) {
210
+ try {
211
+ fs.mkdirSync(outputPath, { recursive: true });
212
+ } catch (e) {
213
+ const errAny = e as any;
214
+ const code = typeof errAny?.code === "string" ? errAny.code : "";
215
+ const msg = errAny instanceof Error ? errAny.message : String(errAny);
216
+ if (code === "EACCES" || code === "EPERM" || code === "ENOENT") {
217
+ console.error(`Error: Failed to create output directory: ${outputPath}`);
218
+ console.error(`Reason: ${msg}`);
219
+ console.error("Tip: choose a writable path, e.g. --output ./reports or --output ~/reports");
220
+ return null; // Signal to exit
221
+ }
222
+ throw e;
223
+ }
224
+ }
225
+ return outputPath;
226
+ }
227
+
228
+ /**
229
+ * Prepare upload configuration for checkup reports.
230
+ * @returns Upload config if valid, null if should exit, undefined if upload not needed
231
+ */
232
+ function prepareUploadConfig(
233
+ opts: CheckupOptions,
234
+ rootOpts: CliOptions,
235
+ shouldUpload: boolean,
236
+ uploadExplicitlyRequested: boolean
237
+ ): { config: UploadConfig; projectWasGenerated: boolean } | null | undefined {
238
+ if (!shouldUpload) return undefined;
239
+
240
+ const { apiKey } = getConfig(rootOpts);
241
+ if (!apiKey) {
242
+ if (uploadExplicitlyRequested) {
243
+ console.error("Error: API key is required for upload");
244
+ console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
245
+ return null; // Signal to exit
246
+ }
247
+ return undefined; // Skip upload silently
248
+ }
249
+
250
+ const cfg = config.readConfig();
251
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
252
+ let project = ((opts.project || cfg.defaultProject) || "").trim();
253
+ let projectWasGenerated = false;
254
+
255
+ if (!project) {
256
+ project = `project_${crypto.randomBytes(6).toString("hex")}`;
257
+ projectWasGenerated = true;
258
+ try {
259
+ config.writeConfig({ defaultProject: project });
260
+ } catch (e) {
261
+ const message = e instanceof Error ? e.message : String(e);
262
+ console.error(`Warning: Failed to save generated default project: ${message}`);
263
+ }
264
+ }
265
+
266
+ return {
267
+ config: { apiKey, apiBaseUrl, project },
268
+ projectWasGenerated,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Upload checkup reports to PostgresAI API.
274
+ */
275
+ async function uploadCheckupReports(
276
+ uploadCfg: UploadConfig,
277
+ reports: Record<string, any>,
278
+ spinner: ReturnType<typeof createTtySpinner>,
279
+ logUpload: (msg: string) => void
280
+ ): Promise<UploadSummary> {
281
+ spinner.update("Creating remote checkup report");
282
+ const created = await withRetry(
283
+ () => createCheckupReport({
284
+ apiKey: uploadCfg.apiKey,
285
+ apiBaseUrl: uploadCfg.apiBaseUrl,
286
+ project: uploadCfg.project,
287
+ }),
288
+ { maxAttempts: 3 },
289
+ (attempt, err, delayMs) => {
290
+ const errMsg = err instanceof Error ? err.message : String(err);
291
+ logUpload(`[Retry ${attempt}/3] createCheckupReport failed: ${errMsg}, retrying in ${delayMs}ms...`);
292
+ }
293
+ );
294
+
295
+ const reportId = created.reportId;
296
+ logUpload(`Created remote checkup report: ${reportId}`);
297
+
298
+ const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
299
+ for (const [checkId, report] of Object.entries(reports)) {
300
+ spinner.update(`Uploading ${checkId}.json`);
301
+ const jsonText = JSON.stringify(report, null, 2);
302
+ const r = await withRetry(
303
+ () => uploadCheckupReportJson({
304
+ apiKey: uploadCfg.apiKey,
305
+ apiBaseUrl: uploadCfg.apiBaseUrl,
306
+ reportId,
307
+ filename: `${checkId}.json`,
308
+ checkId,
309
+ jsonText,
310
+ }),
311
+ { maxAttempts: 3 },
312
+ (attempt, err, delayMs) => {
313
+ const errMsg = err instanceof Error ? err.message : String(err);
314
+ logUpload(`[Retry ${attempt}/3] Upload ${checkId}.json failed: ${errMsg}, retrying in ${delayMs}ms...`);
315
+ }
316
+ );
317
+ uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
318
+ }
319
+ logUpload("Upload completed");
320
+
321
+ return { project: uploadCfg.project, reportId, uploaded };
322
+ }
323
+
324
+ /**
325
+ * Write checkup reports to files.
326
+ */
327
+ function writeReportFiles(reports: Record<string, any>, outputPath: string): void {
328
+ for (const [checkId, report] of Object.entries(reports)) {
329
+ const filePath = path.join(outputPath, `${checkId}.json`);
330
+ fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
331
+ console.log(`✓ ${checkId}: ${filePath}`);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Print upload summary to console.
337
+ */
338
+ function printUploadSummary(
339
+ summary: UploadSummary,
340
+ projectWasGenerated: boolean,
341
+ useStderr: boolean
342
+ ): void {
343
+ const out = useStderr ? console.error : console.log;
344
+ out("\nCheckup report uploaded");
345
+ out("======================\n");
346
+ if (projectWasGenerated) {
347
+ out(`Project: ${summary.project} (generated and saved as default)`);
348
+ } else {
349
+ out(`Project: ${summary.project}`);
350
+ }
351
+ out(`Report ID: ${summary.reportId}`);
352
+ out("View in Console: console.postgres.ai → Support → checkup reports");
353
+ out("");
354
+ out("Files:");
355
+ for (const item of summary.uploaded) {
356
+ out(`- ${item.checkId}: ${item.filename}`);
357
+ }
358
+ }
359
+
360
+ // ============================================================================
361
+ // CLI configuration
362
+ // ============================================================================
363
+
112
364
  /**
113
365
  * CLI configuration options
114
366
  */
@@ -286,6 +538,20 @@ program
286
538
  "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
287
539
  );
288
540
 
541
+ program
542
+ .command("set-default-project <project>")
543
+ .description("store default project for checkup uploads")
544
+ .action(async (project: string) => {
545
+ const value = (project || "").trim();
546
+ if (!value) {
547
+ console.error("Error: project is required");
548
+ process.exitCode = 1;
549
+ return;
550
+ }
551
+ config.writeConfig({ defaultProject: value });
552
+ console.log(`Default project saved: ${value}`);
553
+ });
554
+
289
555
  program
290
556
  .command("prepare-db [conn]")
291
557
  .description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
@@ -613,6 +879,143 @@ program
613
879
  }
614
880
  });
615
881
 
882
+ program
883
+ .command("checkup [conn]")
884
+ .description("generate health check reports directly from PostgreSQL (express mode)")
885
+ .option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
886
+ .option("--node-name <name>", "node name for reports", "node-01")
887
+ .option("--output <path>", "output directory for JSON files")
888
+ .option("--[no-]upload", "upload JSON results to PostgresAI (default: enabled; requires API key)", undefined)
889
+ .option(
890
+ "--project <project>",
891
+ "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
892
+ )
893
+ .option("--json", "output JSON to stdout (implies --no-upload)")
894
+ .addHelpText(
895
+ "after",
896
+ [
897
+ "",
898
+ "Available checks:",
899
+ ...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
900
+ "",
901
+ "Examples:",
902
+ " postgresai checkup postgresql://user:pass@host:5432/db",
903
+ " postgresai checkup postgresql://user:pass@host:5432/db --check-id A003",
904
+ " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
905
+ " postgresai checkup postgresql://user:pass@host:5432/db --project my_project",
906
+ " postgresai set-default-project my_project",
907
+ " postgresai checkup postgresql://user:pass@host:5432/db",
908
+ " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
909
+ ].join("\n")
910
+ )
911
+ .action(async (conn: string | undefined, opts: CheckupOptions, cmd: Command) => {
912
+ if (!conn) {
913
+ cmd.outputHelp();
914
+ process.exitCode = 1;
915
+ return;
916
+ }
917
+
918
+ const shouldPrintJson = !!opts.json;
919
+ const uploadExplicitlyRequested = opts.upload === true;
920
+ const uploadExplicitlyDisabled = opts.upload === false || shouldPrintJson;
921
+ let shouldUpload = !uploadExplicitlyDisabled;
922
+
923
+ // Preflight: validate/create output directory BEFORE connecting / running checks.
924
+ const outputPath = prepareOutputDirectory(opts.output);
925
+ if (outputPath === null) {
926
+ process.exitCode = 1;
927
+ return;
928
+ }
929
+
930
+ // Preflight: validate upload flags/credentials BEFORE connecting / running checks.
931
+ const rootOpts = program.opts() as CliOptions;
932
+ const uploadResult = prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyRequested);
933
+ if (uploadResult === null) {
934
+ process.exitCode = 1;
935
+ return;
936
+ }
937
+ const uploadCfg = uploadResult?.config;
938
+ const projectWasGenerated = uploadResult?.projectWasGenerated ?? false;
939
+ shouldUpload = !!uploadCfg;
940
+
941
+ // Connect and run checks
942
+ const adminConn = resolveAdminConnection({
943
+ conn,
944
+ envPassword: process.env.PGPASSWORD,
945
+ });
946
+ let client: Client | undefined;
947
+ const spinnerEnabled = !!process.stdout.isTTY && shouldUpload;
948
+ const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
949
+
950
+ try {
951
+ spinner.update("Connecting to Postgres");
952
+ const connResult = await connectWithSslFallback(Client, adminConn);
953
+ client = connResult.client as Client;
954
+
955
+ // Generate reports
956
+ let reports: Record<string, any>;
957
+ if (opts.checkId === "ALL") {
958
+ reports = await generateAllReports(client, opts.nodeName, (p) => {
959
+ spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
960
+ });
961
+ } else {
962
+ const checkId = opts.checkId.toUpperCase();
963
+ const generator = REPORT_GENERATORS[checkId];
964
+ if (!generator) {
965
+ spinner.stop();
966
+ console.error(`Unknown check ID: ${opts.checkId}`);
967
+ console.error(`Available: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
968
+ process.exitCode = 1;
969
+ return;
970
+ }
971
+ spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
972
+ reports = { [checkId]: await generator(client, opts.nodeName) };
973
+ }
974
+
975
+ // Upload to PostgresAI API (if configured)
976
+ let uploadSummary: UploadSummary | undefined;
977
+ if (uploadCfg) {
978
+ const logUpload = (msg: string): void => {
979
+ (shouldPrintJson ? console.error : console.log)(msg);
980
+ };
981
+ uploadSummary = await uploadCheckupReports(uploadCfg, reports, spinner, logUpload);
982
+ }
983
+
984
+ spinner.stop();
985
+
986
+ // Write to files (if output path specified)
987
+ if (outputPath) {
988
+ writeReportFiles(reports, outputPath);
989
+ }
990
+
991
+ // Print upload summary
992
+ if (uploadSummary) {
993
+ printUploadSummary(uploadSummary, projectWasGenerated, shouldPrintJson);
994
+ }
995
+
996
+ // Output JSON to stdout
997
+ if (shouldPrintJson || (!shouldUpload && !opts.output)) {
998
+ console.log(JSON.stringify(reports, null, 2));
999
+ }
1000
+ } catch (error) {
1001
+ if (error instanceof RpcError) {
1002
+ for (const line of formatRpcErrorForDisplay(error)) {
1003
+ console.error(line);
1004
+ }
1005
+ } else {
1006
+ const message = error instanceof Error ? error.message : String(error);
1007
+ console.error(`Error: ${message}`);
1008
+ }
1009
+ process.exitCode = 1;
1010
+ } finally {
1011
+ // Always stop spinner to prevent interval leak (idempotent - safe to call multiple times)
1012
+ spinner.stop();
1013
+ if (client) {
1014
+ await client.end();
1015
+ }
1016
+ }
1017
+ });
1018
+
616
1019
  /**
617
1020
  * Stub function for not implemented commands
618
1021
  */
@@ -782,17 +1185,33 @@ mon
782
1185
 
783
1186
  // Update .env with custom tag if provided
784
1187
  const envFile = path.resolve(projectDir, ".env");
785
- const imageTag = opts.tag || pkg.version;
786
1188
 
787
- // Build .env content
788
- const envLines: string[] = [`PGAI_TAG=${imageTag}`];
789
- // Preserve GF_SECURITY_ADMIN_PASSWORD if it exists
1189
+ // Build .env content, preserving important existing values
1190
+ // Read existing .env first to preserve CI/custom settings
1191
+ let existingTag: string | null = null;
1192
+ let existingRegistry: string | null = null;
1193
+ let existingPassword: string | null = null;
1194
+
790
1195
  if (fs.existsSync(envFile)) {
791
1196
  const existingEnv = fs.readFileSync(envFile, "utf8");
1197
+ // Extract existing values
1198
+ const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
1199
+ if (tagMatch) existingTag = tagMatch[1].trim();
1200
+ const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
1201
+ if (registryMatch) existingRegistry = registryMatch[1].trim();
792
1202
  const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
793
- if (pwdMatch) {
794
- envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${pwdMatch[1]}`);
795
- }
1203
+ if (pwdMatch) existingPassword = pwdMatch[1].trim();
1204
+ }
1205
+
1206
+ // Priority: CLI --tag flag > existing .env > package version
1207
+ const imageTag = opts.tag || existingTag || pkg.version;
1208
+
1209
+ const envLines: string[] = [`PGAI_TAG=${imageTag}`];
1210
+ if (existingRegistry) {
1211
+ envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
1212
+ }
1213
+ if (existingPassword) {
1214
+ envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
796
1215
  }
797
1216
  fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
798
1217
 
@@ -1596,8 +2015,21 @@ auth
1596
2015
  return;
1597
2016
  }
1598
2017
 
2018
+ // Read existing config to check for defaultProject before updating
2019
+ const existingConfig = config.readConfig();
2020
+ const existingProject = existingConfig.defaultProject;
2021
+
1599
2022
  config.writeConfig({ apiKey: trimmedKey });
2023
+ // When API key is set directly, only clear orgId (org selection may differ).
2024
+ // Preserve defaultProject to avoid orphaning historical reports.
2025
+ // If the new key lacks access to the project, upload will fail with a clear error.
2026
+ config.deleteConfigKeys(["orgId"]);
2027
+
1600
2028
  console.log(`API key saved to ${config.getConfigPath()}`);
2029
+ if (existingProject) {
2030
+ console.log(`Note: Your default project "${existingProject}" has been preserved.`);
2031
+ console.log(` If this key belongs to a different account, use --project to specify a new one.`);
2032
+ }
1601
2033
  return;
1602
2034
  }
1603
2035
 
@@ -1622,10 +2054,10 @@ auth
1622
2054
  const requestedPort = opts.port || 0; // 0 = OS assigns available port
1623
2055
  const callbackServer = authServer.createCallbackServer(requestedPort, params.state, 120000); // 2 minute timeout
1624
2056
 
1625
- // Wait a bit for server to start and get port
1626
- await new Promise(resolve => setTimeout(resolve, 100));
1627
- const actualPort = callbackServer.getPort();
1628
- const redirectUri = `http://localhost:${actualPort}/callback`;
2057
+ // Wait for server to start and get the actual port
2058
+ const actualPort = await callbackServer.ready;
2059
+ // Use 127.0.0.1 to match the server bind address (avoids IPv6 issues on some hosts)
2060
+ const redirectUri = `http://127.0.0.1:${actualPort}/callback`;
1629
2061
 
1630
2062
  console.log(`Callback server listening on port ${actualPort}`);
1631
2063
 
@@ -1686,7 +2118,8 @@ auth
1686
2118
  }
1687
2119
 
1688
2120
  // Step 3: Open browser
1689
- const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
2121
+ // Pass api_url so UI calls oauth_approve on the same backend where oauth_init created the session
2122
+ const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
1690
2123
 
1691
2124
  if (opts.debug) {
1692
2125
  console.log(`Debug: Auth URL: ${authUrl}`);
@@ -1770,15 +2203,31 @@ auth
1770
2203
  const orgId = result.org_id || result?.[0]?.result?.org_id; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
1771
2204
 
1772
2205
  // Step 6: Save token to config
2206
+ // Check if org changed to decide whether to preserve defaultProject
2207
+ const existingConfig = config.readConfig();
2208
+ const existingOrgId = existingConfig.orgId;
2209
+ const existingProject = existingConfig.defaultProject;
2210
+ const orgChanged = existingOrgId && existingOrgId !== orgId;
2211
+
1773
2212
  config.writeConfig({
1774
2213
  apiKey: apiToken,
1775
2214
  baseUrl: apiBaseUrl,
1776
2215
  orgId: orgId,
1777
2216
  });
2217
+
2218
+ // Only clear defaultProject if org actually changed
2219
+ if (orgChanged && existingProject) {
2220
+ config.deleteConfigKeys(["defaultProject"]);
2221
+ console.log(`\nNote: Organization changed (${existingOrgId} → ${orgId}).`);
2222
+ console.log(` Default project "${existingProject}" has been cleared.`);
2223
+ }
1778
2224
 
1779
2225
  console.log("\nAuthentication successful!");
1780
2226
  console.log(`API key saved to: ${config.getConfigPath()}`);
1781
2227
  console.log(`Organization ID: ${orgId}`);
2228
+ if (!orgChanged && existingProject) {
2229
+ console.log(`Default project: ${existingProject} (preserved)`);
2230
+ }
1782
2231
  console.log(`\nYou can now use the CLI without specifying an API key.`);
1783
2232
  process.exit(0);
1784
2233
  } catch (err) {
package/bun.lock CHANGED
@@ -14,6 +14,8 @@
14
14
  "@types/bun": "^1.1.14",
15
15
  "@types/js-yaml": "^4.0.9",
16
16
  "@types/pg": "^8.15.6",
17
+ "ajv": "^8.17.1",
18
+ "ajv-formats": "^3.0.1",
17
19
  "typescript": "^5.3.3",
18
20
  },
19
21
  },
@@ -129,7 +131,7 @@
129
131
 
130
132
  "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
131
133
 
132
- "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
134
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
133
135
 
134
136
  "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
135
137
 
package/bunfig.toml ADDED
@@ -0,0 +1,19 @@
1
+ # Bun configuration for postgres_ai CLI
2
+ # https://bun.sh/docs/runtime/bunfig
3
+
4
+ [test]
5
+ # Default timeout for all tests (30 seconds)
6
+ # Integration tests that connect to databases need longer timeouts
7
+ timeout = 30000
8
+
9
+ # Coverage settings - enabled by default for test runs
10
+ coverage = true
11
+ coverageDir = "coverage"
12
+
13
+ # Skip coverage for test files and node_modules
14
+ coverageSkipTestFiles = true
15
+
16
+ # Reporter format for CI integration
17
+ # - text: console output with summary table
18
+ # - lcov: standard format for coverage tools
19
+ coverageReporter = ["text", "lcov"]