postgresai 0.16.0-rc.4 → 0.16.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.
Files changed (85) hide show
  1. package/README.md +154 -0
  2. package/dist/bin/postgres-ai.js +2911 -255
  3. package/package.json +12 -3
  4. package/schemas/A002.schema.json +63 -0
  5. package/schemas/A003.schema.json +73 -0
  6. package/schemas/A004.schema.json +81 -0
  7. package/schemas/A007.schema.json +71 -0
  8. package/schemas/A013.schema.json +61 -0
  9. package/schemas/D001.schema.json +71 -0
  10. package/schemas/D004.schema.json +136 -0
  11. package/schemas/F001.schema.json +73 -0
  12. package/schemas/F002.schema.json +108 -0
  13. package/schemas/F003.schema.json +138 -0
  14. package/schemas/F004.schema.json +125 -0
  15. package/schemas/F005.schema.json +131 -0
  16. package/schemas/F009.schema.json +155 -0
  17. package/schemas/G001.schema.json +135 -0
  18. package/schemas/G003.schema.json +90 -0
  19. package/schemas/H001.schema.json +141 -0
  20. package/schemas/H002.schema.json +129 -0
  21. package/schemas/H004.schema.json +128 -0
  22. package/schemas/I001.schema.json +149 -0
  23. package/schemas/K001.schema.json +161 -0
  24. package/schemas/K003.schema.json +163 -0
  25. package/schemas/K004.schema.json +110 -0
  26. package/schemas/K005.schema.json +110 -0
  27. package/schemas/K006.schema.json +110 -0
  28. package/schemas/K007.schema.json +110 -0
  29. package/schemas/K008.schema.json +110 -0
  30. package/schemas/M001.schema.json +119 -0
  31. package/schemas/M002.schema.json +110 -0
  32. package/schemas/M003.schema.json +128 -0
  33. package/schemas/N001.schema.json +161 -0
  34. package/schemas/query.schema.json +62 -0
  35. package/CHANGELOG.md +0 -11
  36. package/bin/postgres-ai.ts +0 -5578
  37. package/bun.lock +0 -258
  38. package/bunfig.toml +0 -20
  39. package/lib/aas-onboard.ts +0 -251
  40. package/lib/auth-server.ts +0 -285
  41. package/lib/checkup-api.ts +0 -526
  42. package/lib/checkup-dictionary.ts +0 -103
  43. package/lib/checkup-summary.ts +0 -338
  44. package/lib/checkup.ts +0 -2261
  45. package/lib/config.ts +0 -171
  46. package/lib/init.ts +0 -1152
  47. package/lib/instances.ts +0 -245
  48. package/lib/issues.ts +0 -1060
  49. package/lib/mcp-server.ts +0 -667
  50. package/lib/metrics-loader.ts +0 -134
  51. package/lib/pkce.ts +0 -79
  52. package/lib/reports.ts +0 -373
  53. package/lib/storage.ts +0 -367
  54. package/lib/supabase.ts +0 -826
  55. package/lib/util.ts +0 -134
  56. package/packages/postgres-ai/README.md +0 -26
  57. package/packages/postgres-ai/bin/postgres-ai.js +0 -27
  58. package/packages/postgres-ai/package.json +0 -27
  59. package/scripts/embed-checkup-dictionary.ts +0 -115
  60. package/scripts/embed-metrics.ts +0 -160
  61. package/scripts/generate-release-notes.ts +0 -668
  62. package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
  63. package/test/aas-onboard.test.ts +0 -301
  64. package/test/auth.test.ts +0 -287
  65. package/test/checkup.integration.test.ts +0 -413
  66. package/test/checkup.test.ts +0 -3626
  67. package/test/compose-cmd.test.ts +0 -120
  68. package/test/config-consistency.test.ts +0 -352
  69. package/test/init.integration.test.ts +0 -438
  70. package/test/init.test.ts +0 -1816
  71. package/test/issues.cli.test.ts +0 -1162
  72. package/test/issues.test.ts +0 -456
  73. package/test/mcp-server.test.ts +0 -2530
  74. package/test/monitoring.test.ts +0 -746
  75. package/test/permission-check-sql.test.ts +0 -116
  76. package/test/reports.cli.test.ts +0 -793
  77. package/test/reports.test.ts +0 -977
  78. package/test/schema-validation.test.ts +0 -231
  79. package/test/storage.test.ts +0 -935
  80. package/test/supabase.test.ts +0 -709
  81. package/test/targets-add-config.test.ts +0 -28
  82. package/test/test-utils.ts +0 -190
  83. package/test/upgrade.test.ts +0 -1056
  84. package/test/util.test.ts +0 -44
  85. package/tsconfig.json +0 -20
@@ -13323,43 +13323,92 @@ var require_dist2 = __commonJS((exports, module) => {
13323
13323
  var exports_util2 = {};
13324
13324
  __export(exports_util2, {
13325
13325
  resolveBaseUrls: () => resolveBaseUrls2,
13326
+ requestTimeoutSignal: () => requestTimeoutSignal2,
13327
+ redactTextSecrets: () => redactTextSecrets2,
13328
+ redactSecretsForLog: () => redactSecretsForLog2,
13329
+ redactSecrets: () => redactSecrets2,
13326
13330
  normalizeBaseUrl: () => normalizeBaseUrl2,
13327
13331
  maskSecret: () => maskSecret2,
13328
- formatHttpError: () => formatHttpError2
13332
+ isRetryableHttpStatus: () => isRetryableHttpStatus2,
13333
+ isFetchTimeout: () => isFetchTimeout2,
13334
+ formatHttpError: () => formatHttpError2,
13335
+ describeFetchError: () => describeFetchError2,
13336
+ HttpStatusError: () => HttpStatusError2,
13337
+ HttpRequestTimeoutError: () => HttpRequestTimeoutError2,
13338
+ DEFAULT_HTTP_REQUEST_TIMEOUT_MS: () => DEFAULT_HTTP_REQUEST_TIMEOUT_MS2
13329
13339
  });
13330
13340
  function isHtmlContent2(text) {
13331
13341
  const trimmed = text.trim();
13332
13342
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
13333
13343
  }
13334
- function formatHttpError2(operation, status, responseBody) {
13335
- const statusMessage = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13336
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
13344
+ function formatHttpError2(operation, status, responseBody, statusText) {
13345
+ const generic = HTTP_STATUS_MESSAGES2[status] || "Request failed";
13337
13346
  const remediation = status === 401 ? `
13338
13347
  ${AUTH_REMEDIATION_HINT2}` : "";
13339
- if (responseBody) {
13340
- if (isHtmlContent2(responseBody)) {
13341
- return errMsg + remediation;
13342
- }
13348
+ let bodyMessage;
13349
+ let bodyDetails;
13350
+ let bodyCode;
13351
+ let bodyHint;
13352
+ if (responseBody && !isHtmlContent2(responseBody)) {
13343
13353
  try {
13344
13354
  const errObj = JSON.parse(responseBody);
13345
- const message = errObj.message || errObj.error || errObj.detail;
13346
- if (message && typeof message === "string") {
13347
- errMsg += `
13348
- ${message}`;
13349
- } else {
13350
- errMsg += `
13351
- ${JSON.stringify(errObj, null, 2)}`;
13355
+ const message = errObj.message ?? errObj.error;
13356
+ if (typeof message === "string" && message.trim().length > 0) {
13357
+ bodyMessage = redactTextSecrets2(message.trim());
13358
+ }
13359
+ const details = errObj.details ?? errObj.detail;
13360
+ if (typeof details === "string" && details.trim().length > 0) {
13361
+ bodyDetails = redactTextSecrets2(details.trim());
13362
+ }
13363
+ if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
13364
+ bodyCode = errObj.code.trim();
13365
+ }
13366
+ if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
13367
+ bodyHint = redactTextSecrets2(errObj.hint.trim());
13368
+ }
13369
+ if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
13370
+ bodyDetails = redactSecretsForLog2(JSON.stringify(errObj));
13352
13371
  }
13353
13372
  } catch {
13354
13373
  const trimmed = responseBody.trim();
13355
13374
  if (trimmed.length > 0 && trimmed.length < 500) {
13356
- errMsg += `
13357
- ${trimmed}`;
13375
+ bodyDetails = redactTextSecrets2(trimmed);
13358
13376
  }
13359
13377
  }
13360
13378
  }
13379
+ const trimmedReason = statusText?.trim();
13380
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES2[status] && trimmedReason !== generic ? trimmedReason : undefined;
13381
+ const safeReasonPhrase = reasonPhrase ? redactTextSecrets2(reasonPhrase) : undefined;
13382
+ const headline = bodyMessage ?? safeReasonPhrase ?? generic;
13383
+ const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
13384
+ let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
13385
+ if (bodyDetails && bodyDetails !== headline) {
13386
+ errMsg += `
13387
+ ${bodyDetails}`;
13388
+ }
13389
+ if (bodyHint) {
13390
+ errMsg += `
13391
+ Hint: ${bodyHint}`;
13392
+ }
13361
13393
  return errMsg + remediation;
13362
13394
  }
13395
+ function describeFetchError2(operation, url, err) {
13396
+ const cause = err?.cause;
13397
+ const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
13398
+ return `${operation}: could not reach ${url} (${detail})`;
13399
+ }
13400
+ function isRetryableHttpStatus2(status) {
13401
+ return status >= 500 || status === 429;
13402
+ }
13403
+ function requestTimeoutSignal2(timeoutMs) {
13404
+ const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS2;
13405
+ const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS2, Math.floor(requested)));
13406
+ return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
13407
+ }
13408
+ function isFetchTimeout2(err) {
13409
+ const name = err?.name;
13410
+ return name === "AbortError" || name === "TimeoutError";
13411
+ }
13363
13412
  function maskSecret2(secret) {
13364
13413
  if (!secret)
13365
13414
  return "";
@@ -13369,6 +13418,35 @@ function maskSecret2(secret) {
13369
13418
  return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
13370
13419
  return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
13371
13420
  }
13421
+ function isSensitiveLogKey2(key) {
13422
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
13423
+ return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
13424
+ }
13425
+ function redactSecrets2(value) {
13426
+ if (Array.isArray(value)) {
13427
+ return value.map(redactSecrets2);
13428
+ }
13429
+ if (value && typeof value === "object") {
13430
+ const out = {};
13431
+ for (const [key, child] of Object.entries(value)) {
13432
+ out[key] = isSensitiveLogKey2(key) && child != null ? "[REDACTED]" : redactSecrets2(child);
13433
+ }
13434
+ return out;
13435
+ }
13436
+ return value;
13437
+ }
13438
+ function redactSecretsForLog2(text) {
13439
+ let parsed;
13440
+ try {
13441
+ parsed = JSON.parse(text);
13442
+ } catch {
13443
+ return redactTextSecrets2(text);
13444
+ }
13445
+ return JSON.stringify(redactSecrets2(parsed));
13446
+ }
13447
+ function redactTextSecrets2(text) {
13448
+ return text.replace(TEXT_URL_USERINFO2, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR2, "$1[REDACTED]");
13449
+ }
13372
13450
  function normalizeBaseUrl2(value) {
13373
13451
  const trimmed = (value || "").replace(/\/$/, "");
13374
13452
  try {
@@ -13391,7 +13469,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
13391
13469
  storageBaseUrl: normalizeBaseUrl2(storageCandidate)
13392
13470
  };
13393
13471
  }
13394
- var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
13472
+ var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.", STANDARD_REASON_PHRASES2, HttpStatusError2, DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, HttpRequestTimeoutError2, TEXT_URL_USERINFO2, TEXT_SENSITIVE_PAIR2;
13395
13473
  var init_util = __esm(() => {
13396
13474
  HTTP_STATUS_MESSAGES2 = {
13397
13475
  400: "Bad Request",
@@ -13405,6 +13483,36 @@ var init_util = __esm(() => {
13405
13483
  503: "Service Unavailable - server temporarily unavailable",
13406
13484
  504: "Gateway Timeout - server temporarily unavailable"
13407
13485
  };
13486
+ STANDARD_REASON_PHRASES2 = {
13487
+ 400: "Bad Request",
13488
+ 401: "Unauthorized",
13489
+ 403: "Forbidden",
13490
+ 404: "Not Found",
13491
+ 408: "Request Timeout",
13492
+ 409: "Conflict",
13493
+ 413: "Payload Too Large",
13494
+ 429: "Too Many Requests",
13495
+ 500: "Internal Server Error",
13496
+ 502: "Bad Gateway",
13497
+ 503: "Service Unavailable",
13498
+ 504: "Gateway Timeout"
13499
+ };
13500
+ HttpStatusError2 = class HttpStatusError2 extends Error {
13501
+ status;
13502
+ constructor(message, status) {
13503
+ super(message);
13504
+ this.name = "HttpStatusError";
13505
+ this.status = status;
13506
+ }
13507
+ };
13508
+ HttpRequestTimeoutError2 = class HttpRequestTimeoutError2 extends Error {
13509
+ constructor(operation, timeoutMs) {
13510
+ super(`${operation}: request timed out after ${timeoutMs}ms`);
13511
+ this.name = "HttpRequestTimeoutError";
13512
+ }
13513
+ };
13514
+ TEXT_URL_USERINFO2 = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
13515
+ TEXT_SENSITIVE_PAIR2 = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
13408
13516
  });
13409
13517
 
13410
13518
  // node_modules/commander/esm.mjs
@@ -13425,7 +13533,7 @@ var {
13425
13533
  // package.json
13426
13534
  var package_default = {
13427
13535
  name: "postgresai",
13428
- version: "0.16.0-rc.4",
13536
+ version: "0.16.0",
13429
13537
  description: "postgres_ai CLI",
13430
13538
  license: "Apache-2.0",
13431
13539
  private: false,
@@ -13443,8 +13551,15 @@ var package_default = {
13443
13551
  },
13444
13552
  exports: {
13445
13553
  ".": "./dist/bin/postgres-ai.js",
13446
- "./cli": "./dist/bin/postgres-ai.js"
13554
+ "./cli": "./dist/bin/postgres-ai.js",
13555
+ "./schemas/*.schema.json": "./schemas/*.schema.json"
13447
13556
  },
13557
+ files: [
13558
+ "dist/",
13559
+ "instances.demo.yml",
13560
+ "schemas/",
13561
+ "sql/"
13562
+ ],
13448
13563
  type: "module",
13449
13564
  engines: {
13450
13565
  node: ">=18"
@@ -13453,7 +13568,9 @@ var package_default = {
13453
13568
  "embed-metrics": "bun run scripts/embed-metrics.ts",
13454
13569
  "embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
13455
13570
  "embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
13456
- build: `bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
13571
+ "sync-schemas": "node scripts/sync-schemas.mjs",
13572
+ build: `bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
13573
+ prepack: "npm run sync-schemas",
13457
13574
  prepublishOnly: "npm run build",
13458
13575
  start: "bun ./bin/postgres-ai.ts --help",
13459
13576
  "start:node": "node ./dist/bin/postgres-ai.js --help",
@@ -13504,7 +13621,8 @@ function readConfig() {
13504
13621
  storageBaseUrl: null,
13505
13622
  orgId: null,
13506
13623
  defaultProject: null,
13507
- projectName: null
13624
+ projectName: null,
13625
+ feedbackTipLastShownAt: null
13508
13626
  };
13509
13627
  const userConfigPath = getConfigPath();
13510
13628
  if (fs.existsSync(userConfigPath)) {
@@ -13517,6 +13635,7 @@ function readConfig() {
13517
13635
  config.orgId = parsed.orgId ?? null;
13518
13636
  config.defaultProject = parsed.defaultProject ?? null;
13519
13637
  config.projectName = parsed.projectName ?? null;
13638
+ config.feedbackTipLastShownAt = parsed.feedbackTipLastShownAt ?? null;
13520
13639
  return config;
13521
13640
  } catch (err) {
13522
13641
  const message = err instanceof Error ? err.message : String(err);
@@ -16256,7 +16375,7 @@ var Result = import_lib.default.Result;
16256
16375
  var TypeOverrides = import_lib.default.TypeOverrides;
16257
16376
  var defaults = import_lib.default.defaults;
16258
16377
  // package.json
16259
- var version = "0.16.0-rc.4";
16378
+ var version = "0.16.0";
16260
16379
  var package_default2 = {
16261
16380
  name: "postgresai",
16262
16381
  version,
@@ -16277,8 +16396,15 @@ var package_default2 = {
16277
16396
  },
16278
16397
  exports: {
16279
16398
  ".": "./dist/bin/postgres-ai.js",
16280
- "./cli": "./dist/bin/postgres-ai.js"
16399
+ "./cli": "./dist/bin/postgres-ai.js",
16400
+ "./schemas/*.schema.json": "./schemas/*.schema.json"
16281
16401
  },
16402
+ files: [
16403
+ "dist/",
16404
+ "instances.demo.yml",
16405
+ "schemas/",
16406
+ "sql/"
16407
+ ],
16282
16408
  type: "module",
16283
16409
  engines: {
16284
16410
  node: ">=18"
@@ -16287,7 +16413,9 @@ var package_default2 = {
16287
16413
  "embed-metrics": "bun run scripts/embed-metrics.ts",
16288
16414
  "embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
16289
16415
  "embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
16290
- build: `bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
16416
+ "sync-schemas": "node scripts/sync-schemas.mjs",
16417
+ build: `bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e "const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml`,
16418
+ prepack: "npm run sync-schemas",
16291
16419
  prepublishOnly: "npm run build",
16292
16420
  start: "bun ./bin/postgres-ai.ts --help",
16293
16421
  "start:node": "node ./dist/bin/postgres-ai.js --help",
@@ -16338,7 +16466,8 @@ function readConfig2() {
16338
16466
  storageBaseUrl: null,
16339
16467
  orgId: null,
16340
16468
  defaultProject: null,
16341
- projectName: null
16469
+ projectName: null,
16470
+ feedbackTipLastShownAt: null
16342
16471
  };
16343
16472
  const userConfigPath = getConfigPath2();
16344
16473
  if (fs2.existsSync(userConfigPath)) {
@@ -16351,6 +16480,7 @@ function readConfig2() {
16351
16480
  config.orgId = parsed.orgId ?? null;
16352
16481
  config.defaultProject = parsed.defaultProject ?? null;
16353
16482
  config.projectName = parsed.projectName ?? null;
16483
+ config.feedbackTipLastShownAt = parsed.feedbackTipLastShownAt ?? null;
16354
16484
  return config;
16355
16485
  } catch (err) {
16356
16486
  const message = err instanceof Error ? err.message : String(err);
@@ -16379,6 +16509,28 @@ function readConfig2() {
16379
16509
  }
16380
16510
  return config;
16381
16511
  }
16512
+ function writeConfig2(config) {
16513
+ const configDir = getConfigDir2();
16514
+ const configPath = getConfigPath2();
16515
+ if (!fs2.existsSync(configDir)) {
16516
+ fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
16517
+ }
16518
+ let existingConfig = {};
16519
+ if (fs2.existsSync(configPath)) {
16520
+ try {
16521
+ const content = fs2.readFileSync(configPath, "utf8");
16522
+ existingConfig = JSON.parse(content);
16523
+ } catch (err) {}
16524
+ }
16525
+ const mergedConfig = {
16526
+ ...existingConfig,
16527
+ ...config
16528
+ };
16529
+ fs2.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2) + `
16530
+ `, {
16531
+ mode: 384
16532
+ });
16533
+ }
16382
16534
 
16383
16535
  // lib/util.ts
16384
16536
  var HTTP_STATUS_MESSAGES = {
@@ -16398,35 +16550,105 @@ function isHtmlContent(text) {
16398
16550
  return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
16399
16551
  }
16400
16552
  var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
16401
- function formatHttpError(operation, status, responseBody) {
16402
- const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
16403
- let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
16553
+ var STANDARD_REASON_PHRASES = {
16554
+ 400: "Bad Request",
16555
+ 401: "Unauthorized",
16556
+ 403: "Forbidden",
16557
+ 404: "Not Found",
16558
+ 408: "Request Timeout",
16559
+ 409: "Conflict",
16560
+ 413: "Payload Too Large",
16561
+ 429: "Too Many Requests",
16562
+ 500: "Internal Server Error",
16563
+ 502: "Bad Gateway",
16564
+ 503: "Service Unavailable",
16565
+ 504: "Gateway Timeout"
16566
+ };
16567
+ function formatHttpError(operation, status, responseBody, statusText) {
16568
+ const generic = HTTP_STATUS_MESSAGES[status] || "Request failed";
16404
16569
  const remediation = status === 401 ? `
16405
16570
  ${AUTH_REMEDIATION_HINT}` : "";
16406
- if (responseBody) {
16407
- if (isHtmlContent(responseBody)) {
16408
- return errMsg + remediation;
16409
- }
16571
+ let bodyMessage;
16572
+ let bodyDetails;
16573
+ let bodyCode;
16574
+ let bodyHint;
16575
+ if (responseBody && !isHtmlContent(responseBody)) {
16410
16576
  try {
16411
16577
  const errObj = JSON.parse(responseBody);
16412
- const message = errObj.message || errObj.error || errObj.detail;
16413
- if (message && typeof message === "string") {
16414
- errMsg += `
16415
- ${message}`;
16416
- } else {
16417
- errMsg += `
16418
- ${JSON.stringify(errObj, null, 2)}`;
16578
+ const message = errObj.message ?? errObj.error;
16579
+ if (typeof message === "string" && message.trim().length > 0) {
16580
+ bodyMessage = redactTextSecrets(message.trim());
16581
+ }
16582
+ const details = errObj.details ?? errObj.detail;
16583
+ if (typeof details === "string" && details.trim().length > 0) {
16584
+ bodyDetails = redactTextSecrets(details.trim());
16585
+ }
16586
+ if (typeof errObj.code === "string" && errObj.code.trim().length > 0) {
16587
+ bodyCode = errObj.code.trim();
16588
+ }
16589
+ if (typeof errObj.hint === "string" && errObj.hint.trim().length > 0) {
16590
+ bodyHint = redactTextSecrets(errObj.hint.trim());
16591
+ }
16592
+ if (bodyMessage === undefined && bodyDetails === undefined && bodyCode === undefined && bodyHint === undefined) {
16593
+ bodyDetails = redactSecretsForLog(JSON.stringify(errObj));
16419
16594
  }
16420
16595
  } catch {
16421
16596
  const trimmed = responseBody.trim();
16422
16597
  if (trimmed.length > 0 && trimmed.length < 500) {
16423
- errMsg += `
16424
- ${trimmed}`;
16598
+ bodyDetails = redactTextSecrets(trimmed);
16425
16599
  }
16426
16600
  }
16427
16601
  }
16602
+ const trimmedReason = statusText?.trim();
16603
+ const reasonPhrase = trimmedReason && trimmedReason !== STANDARD_REASON_PHRASES[status] && trimmedReason !== generic ? trimmedReason : undefined;
16604
+ const safeReasonPhrase = reasonPhrase ? redactTextSecrets(reasonPhrase) : undefined;
16605
+ const headline = bodyMessage ?? safeReasonPhrase ?? generic;
16606
+ const codeSuffix = bodyCode && !headline.includes(bodyCode) ? ` (${bodyCode})` : "";
16607
+ let errMsg = `${operation}: HTTP ${status} - ${headline}${codeSuffix}`;
16608
+ if (bodyDetails && bodyDetails !== headline) {
16609
+ errMsg += `
16610
+ ${bodyDetails}`;
16611
+ }
16612
+ if (bodyHint) {
16613
+ errMsg += `
16614
+ Hint: ${bodyHint}`;
16615
+ }
16428
16616
  return errMsg + remediation;
16429
16617
  }
16618
+ function describeFetchError(operation, url, err) {
16619
+ const cause = err?.cause;
16620
+ const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
16621
+ return `${operation}: could not reach ${url} (${detail})`;
16622
+ }
16623
+
16624
+ class HttpStatusError extends Error {
16625
+ status;
16626
+ constructor(message, status) {
16627
+ super(message);
16628
+ this.name = "HttpStatusError";
16629
+ this.status = status;
16630
+ }
16631
+ }
16632
+ function isRetryableHttpStatus(status) {
16633
+ return status >= 500 || status === 429;
16634
+ }
16635
+ var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
16636
+
16637
+ class HttpRequestTimeoutError extends Error {
16638
+ constructor(operation, timeoutMs) {
16639
+ super(`${operation}: request timed out after ${timeoutMs}ms`);
16640
+ this.name = "HttpRequestTimeoutError";
16641
+ }
16642
+ }
16643
+ function requestTimeoutSignal(timeoutMs) {
16644
+ const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS;
16645
+ const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, Math.floor(requested)));
16646
+ return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
16647
+ }
16648
+ function isFetchTimeout(err) {
16649
+ const name = err?.name;
16650
+ return name === "AbortError" || name === "TimeoutError";
16651
+ }
16430
16652
  function maskSecret(secret) {
16431
16653
  if (!secret)
16432
16654
  return "";
@@ -16436,6 +16658,37 @@ function maskSecret(secret) {
16436
16658
  return `${secret.slice(0, 4)}${"*".repeat(secret.length - 8)}${secret.slice(-4)}`;
16437
16659
  return `${secret.slice(0, Math.min(12, secret.length - 8))}${"*".repeat(Math.max(4, secret.length - 16))}${secret.slice(-4)}`;
16438
16660
  }
16661
+ function isSensitiveLogKey(key) {
16662
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
16663
+ return /^(?:password|passwd|db_(?:pass|password)|conn_?str|secret|token|api_key|private_key|access_key|access_token|refresh_token|auth|auth_key|auth_token|authorization|credentials?|dsn)$/.test(normalized);
16664
+ }
16665
+ function redactSecrets(value) {
16666
+ if (Array.isArray(value)) {
16667
+ return value.map(redactSecrets);
16668
+ }
16669
+ if (value && typeof value === "object") {
16670
+ const out = {};
16671
+ for (const [key, child] of Object.entries(value)) {
16672
+ out[key] = isSensitiveLogKey(key) && child != null ? "[REDACTED]" : redactSecrets(child);
16673
+ }
16674
+ return out;
16675
+ }
16676
+ return value;
16677
+ }
16678
+ function redactSecretsForLog(text) {
16679
+ let parsed;
16680
+ try {
16681
+ parsed = JSON.parse(text);
16682
+ } catch {
16683
+ return redactTextSecrets(text);
16684
+ }
16685
+ return JSON.stringify(redactSecrets(parsed));
16686
+ }
16687
+ var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
16688
+ var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
16689
+ function redactTextSecrets(text) {
16690
+ return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
16691
+ }
16439
16692
  function normalizeBaseUrl(value) {
16440
16693
  const trimmed = (value || "").replace(/\/$/, "");
16441
16694
  try {
@@ -25801,117 +26054,846 @@ function renderMarkdownForTerminal(md) {
25801
26054
  `);
25802
26055
  }
25803
26056
 
25804
- // bin/postgres-ai.ts
25805
- init_util();
25806
-
25807
- // lib/instances.ts
25808
- import * as fs4 from "fs";
25809
-
25810
- // node_modules/js-yaml/dist/js-yaml.mjs
25811
- /*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
25812
- function isNothing2(subject) {
25813
- return typeof subject === "undefined" || subject === null;
25814
- }
25815
- function isObject3(subject) {
25816
- return typeof subject === "object" && subject !== null;
26057
+ // lib/joe.ts
26058
+ var DEFAULT_BUDGET_MS = 25000;
26059
+ var DEFAULT_POLL_INTERVAL_MS = 800;
26060
+ async function callRpc(params) {
26061
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26062
+ if (!apiKey) {
26063
+ throw new Error("API key is required");
26064
+ }
26065
+ const base = normalizeBaseUrl(apiBaseUrl);
26066
+ const url = new URL(`${base}/rpc/${fn}`);
26067
+ const payload = JSON.stringify(body);
26068
+ const headers = {
26069
+ "access-token": apiKey,
26070
+ "Content-Type": "application/json",
26071
+ Connection: "close"
26072
+ };
26073
+ if (debug) {
26074
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26075
+ console.error(`Debug: POST URL: ${url.toString()}`);
26076
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26077
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26078
+ }
26079
+ let response;
26080
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26081
+ try {
26082
+ response = await fetch(url.toString(), {
26083
+ method: "POST",
26084
+ headers,
26085
+ body: payload,
26086
+ signal: requestTimeout.signal
26087
+ });
26088
+ } catch (err) {
26089
+ if (isFetchTimeout(err)) {
26090
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26091
+ }
26092
+ throw new Error(describeFetchError(operation, base, err));
26093
+ }
26094
+ const text = await response.text();
26095
+ if (debug) {
26096
+ console.error(`Debug: Response status: ${response.status}`);
26097
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26098
+ }
26099
+ if (!response.ok) {
26100
+ throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26101
+ }
26102
+ try {
26103
+ return JSON.parse(text);
26104
+ } catch {
26105
+ throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26106
+ }
25817
26107
  }
25818
- function toArray2(sequence) {
25819
- if (Array.isArray(sequence))
25820
- return sequence;
25821
- else if (isNothing2(sequence))
25822
- return [];
25823
- return [sequence];
26108
+ async function startCommand(params) {
26109
+ const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
26110
+ if (!String(command ?? "").trim()) {
26111
+ throw new Error("command text is required");
26112
+ }
26113
+ const commandId = await callRpc({
26114
+ apiKey,
26115
+ apiBaseUrl,
26116
+ fn: "joe_command_run",
26117
+ body: { instance_id: instanceId, command },
26118
+ operation: "Failed to run Joe command",
26119
+ debug,
26120
+ timeoutMs
26121
+ });
26122
+ if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
26123
+ throw new Error(`Failed to run Joe command: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(commandId))}`);
26124
+ }
26125
+ return commandId;
25824
26126
  }
25825
- function extend4(target, source) {
25826
- var index, length, key, sourceKeys;
25827
- if (source) {
25828
- sourceKeys = Object.keys(source);
25829
- for (index = 0, length = sourceKeys.length;index < length; index += 1) {
25830
- key = sourceKeys[index];
25831
- target[key] = source[key];
25832
- }
26127
+ async function getCommandOutput(params) {
26128
+ const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
26129
+ if (!commandId) {
26130
+ throw new Error("commandId is required");
25833
26131
  }
25834
- return target;
26132
+ return callRpc({
26133
+ apiKey,
26134
+ apiBaseUrl,
26135
+ fn: "joe_command_output",
26136
+ body: { command_id: commandId },
26137
+ operation: "Failed to fetch command output",
26138
+ debug,
26139
+ timeoutMs
26140
+ });
25835
26141
  }
25836
- function repeat2(string4, count) {
25837
- var result = "", cycle;
25838
- for (cycle = 0;cycle < count; cycle += 1) {
25839
- result += string4;
26142
+ function preserveIntegerId(value) {
26143
+ if (typeof value === "number")
26144
+ return value;
26145
+ const parsed = Number(value);
26146
+ return Number.isSafeInteger(parsed) ? parsed : value;
26147
+ }
26148
+ function normalizeProjectRow(row) {
26149
+ return {
26150
+ project_id: preserveIntegerId(row.project_id ?? 0),
26151
+ alias: row.alias ?? null,
26152
+ name: row.name ?? null,
26153
+ joe_ready: Boolean(row.joe_ready ?? false),
26154
+ tunnel: Boolean(row.tunnel ?? false),
26155
+ instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
26156
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
26157
+ };
26158
+ }
26159
+ async function listProjects(params) {
26160
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
26161
+ const body = {};
26162
+ if (typeof orgId === "number") {
26163
+ body.org_id = orgId;
25840
26164
  }
25841
- return result;
26165
+ const rows = await callRpc({
26166
+ apiKey,
26167
+ apiBaseUrl,
26168
+ fn: "projects_list",
26169
+ body,
26170
+ operation: "Failed to list projects",
26171
+ debug
26172
+ });
26173
+ if (!Array.isArray(rows)) {
26174
+ return [];
26175
+ }
26176
+ return rows.map(normalizeProjectRow);
25842
26177
  }
25843
- function isNegativeZero2(number4) {
25844
- return number4 === 0 && Number.NEGATIVE_INFINITY === 1 / number4;
26178
+ function isNumericProjectRef(ref) {
26179
+ return /^[0-9]+$/.test(ref.trim());
25845
26180
  }
25846
- var isNothing_12 = isNothing2;
25847
- var isObject_12 = isObject3;
25848
- var toArray_12 = toArray2;
25849
- var repeat_12 = repeat2;
25850
- var isNegativeZero_12 = isNegativeZero2;
25851
- var extend_12 = extend4;
25852
- var common2 = {
25853
- isNothing: isNothing_12,
25854
- isObject: isObject_12,
25855
- toArray: toArray_12,
25856
- repeat: repeat_12,
25857
- isNegativeZero: isNegativeZero_12,
25858
- extend: extend_12
25859
- };
25860
- function formatError3(exception2, compact) {
25861
- var where = "", message = exception2.reason || "(unknown reason)";
25862
- if (!exception2.mark)
25863
- return message;
25864
- if (exception2.mark.name) {
25865
- where += 'in "' + exception2.mark.name + '" ';
26181
+ async function resolveJoeInstanceId(params) {
26182
+ const ref = String(params.project ?? "").trim();
26183
+ if (!ref) {
26184
+ throw new Error("project is required (--project <id|alias>)");
25866
26185
  }
25867
- where += "(" + (exception2.mark.line + 1) + ":" + (exception2.mark.column + 1) + ")";
25868
- if (!compact && exception2.mark.snippet) {
25869
- where += `
25870
-
25871
- ` + exception2.mark.snippet;
26186
+ const projects = await listProjects({
26187
+ apiKey: params.apiKey,
26188
+ apiBaseUrl: params.apiBaseUrl,
26189
+ orgId: params.orgId,
26190
+ debug: params.debug
26191
+ });
26192
+ const needle = ref.toLowerCase();
26193
+ const match = isNumericProjectRef(ref) ? projects.find((p) => String(p.project_id) === String(preserveIntegerId(ref))) : projects.find((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
26194
+ if (!match) {
26195
+ throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
26196
+ }
26197
+ if (match.instance_id == null) {
26198
+ throw new Error(`Project '${ref}' has no Joe instance. Run 'pgai projects' to see which projects have Joe ready.`);
26199
+ }
26200
+ return match.instance_id;
26201
+ }
26202
+ var DESCRIBE_VARIANTS = [
26203
+ "\\d",
26204
+ "\\d+",
26205
+ "\\dt",
26206
+ "\\dt+",
26207
+ "\\di",
26208
+ "\\di+",
26209
+ "\\l",
26210
+ "\\l+",
26211
+ "\\dv",
26212
+ "\\dv+",
26213
+ "\\dm",
26214
+ "\\dm+"
26215
+ ];
26216
+ function buildJoeCommandText(command, input = {}) {
26217
+ const arg = String(input.arg ?? "").trim();
26218
+ switch (command) {
26219
+ case "plan":
26220
+ case "explain":
26221
+ case "exec":
26222
+ case "hypo": {
26223
+ if (!arg) {
26224
+ throw new Error(`${command} requires an argument`);
26225
+ }
26226
+ return `${command} ${arg}`;
26227
+ }
26228
+ case "activity":
26229
+ case "reset":
26230
+ return command;
26231
+ case "terminate": {
26232
+ if (!/^[1-9][0-9]*$/.test(arg)) {
26233
+ throw new Error("pid must be a positive integer");
26234
+ }
26235
+ return `terminate ${arg}`;
26236
+ }
26237
+ case "describe": {
26238
+ if (!arg) {
26239
+ throw new Error("describe requires an object name");
26240
+ }
26241
+ const variant = String(input.variant ?? "\\d").trim();
26242
+ if (!DESCRIBE_VARIANTS.includes(variant)) {
26243
+ throw new Error(`Unsupported describe variant '${variant}'. Supported: ${DESCRIBE_VARIANTS.join(" ")}`);
26244
+ }
26245
+ return `${variant} ${arg}`;
26246
+ }
26247
+ }
26248
+ }
26249
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
26250
+ async function runCommand(params) {
26251
+ const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
26252
+ const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : DEFAULT_BUDGET_MS;
26253
+ const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
26254
+ const now = params.now ?? Date.now;
26255
+ const sleep = params.sleep ?? defaultSleep;
26256
+ const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
26257
+ const deadline = now() + budgetMs;
26258
+ let status = "pending";
26259
+ const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
26260
+ if (now() >= deadline) {
26261
+ return { commandId, status, output: null, budgetExpired: true };
26262
+ }
26263
+ for (;; ) {
26264
+ let output;
26265
+ try {
26266
+ output = await getCommandOutput({
26267
+ apiKey,
26268
+ apiBaseUrl,
26269
+ commandId,
26270
+ debug,
26271
+ timeoutMs: remainingRequestMs()
26272
+ });
26273
+ } catch (err) {
26274
+ if (err instanceof HttpRequestTimeoutError) {
26275
+ return { commandId, status, output: null, budgetExpired: true };
26276
+ }
26277
+ if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
26278
+ if (now() >= deadline) {
26279
+ return { commandId, status, output: null, budgetExpired: true };
26280
+ }
26281
+ await sleep(pollIntervalMs);
26282
+ continue;
26283
+ }
26284
+ throw err;
26285
+ }
26286
+ status = output.status;
26287
+ if (status === "ok" || status === "error") {
26288
+ return { commandId, status, output, budgetExpired: false };
26289
+ }
26290
+ if (now() >= deadline) {
26291
+ return { commandId, status, output: null, budgetExpired: true };
26292
+ }
26293
+ await sleep(pollIntervalMs);
25872
26294
  }
25873
- return message + " " + where;
25874
26295
  }
25875
- function YAMLException$12(reason, mark) {
25876
- Error.call(this);
25877
- this.name = "YAMLException";
25878
- this.reason = reason;
25879
- this.mark = mark;
25880
- this.message = formatError3(this, false);
25881
- if (Error.captureStackTrace) {
25882
- Error.captureStackTrace(this, this.constructor);
26296
+ async function executeJoeCommand(params) {
26297
+ const commandText = buildJoeCommandText(params.command, params.input);
26298
+ let instanceId;
26299
+ const directRef = String(params.instanceId ?? "").trim();
26300
+ if (directRef) {
26301
+ if (!/^[0-9]+$/.test(directRef)) {
26302
+ throw new Error("instanceId must be a numeric Joe instance id");
26303
+ }
26304
+ instanceId = directRef;
26305
+ } else if (String(params.project ?? "").trim()) {
26306
+ instanceId = await resolveJoeInstanceId({
26307
+ apiKey: params.apiKey,
26308
+ apiBaseUrl: params.apiBaseUrl,
26309
+ project: String(params.project),
26310
+ orgId: params.orgId,
26311
+ debug: params.debug
26312
+ });
25883
26313
  } else {
25884
- this.stack = new Error().stack || "";
25885
- }
26314
+ throw new Error("either instanceId or project is required");
26315
+ }
26316
+ const outcome = await runCommand({
26317
+ apiKey: params.apiKey,
26318
+ apiBaseUrl: params.apiBaseUrl,
26319
+ instanceId,
26320
+ command: commandText,
26321
+ budgetMs: params.budgetMs,
26322
+ pollIntervalMs: params.pollIntervalMs,
26323
+ debug: params.debug,
26324
+ now: params.now,
26325
+ sleep: params.sleep
26326
+ });
26327
+ return { ...outcome, command: params.command, instanceId, commandText };
25886
26328
  }
25887
- YAMLException$12.prototype = Object.create(Error.prototype);
25888
- YAMLException$12.prototype.constructor = YAMLException$12;
25889
- YAMLException$12.prototype.toString = function toString2(compact) {
25890
- return this.name + ": " + formatError3(this, compact);
25891
- };
25892
- var exception2 = YAMLException$12;
25893
- function getLine2(buffer, lineStart, lineEnd, position, maxLineLength) {
25894
- var head = "";
25895
- var tail = "";
25896
- var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
25897
- if (position - lineStart > maxHalfLength) {
25898
- head = " ... ";
25899
- lineStart = position - maxHalfLength + head.length;
26329
+ function clientSidePlanFlags(planJson) {
26330
+ const flags = [];
26331
+ const walk = (node) => {
26332
+ if (!node || typeof node !== "object") {
26333
+ return;
26334
+ }
26335
+ const nodeType = node["Node Type"];
26336
+ if (nodeType === "Seq Scan") {
26337
+ const rel = node["Relation Name"];
26338
+ flags.push(`client-side: Seq Scan${rel ? ` on ${rel}` : ""} — no index serves this predicate; consider adding one.`);
26339
+ }
26340
+ if (Array.isArray(node.Plans)) {
26341
+ for (const child of node.Plans) {
26342
+ walk(child);
26343
+ }
26344
+ }
26345
+ };
26346
+ if (Array.isArray(planJson)) {
26347
+ for (const entry of planJson) {
26348
+ if (entry && typeof entry === "object") {
26349
+ walk(entry.Plan ?? entry);
26350
+ }
26351
+ }
26352
+ return flags;
25900
26353
  }
25901
- if (lineEnd - position > maxHalfLength) {
25902
- tail = " ...";
25903
- lineEnd = position + maxHalfLength - tail.length;
26354
+ if (planJson && typeof planJson === "object") {
26355
+ const root = planJson;
26356
+ walk(root.Plan ?? planJson);
25904
26357
  }
25905
- return {
25906
- str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
25907
- pos: position - lineStart + head.length
25908
- };
26358
+ return flags;
25909
26359
  }
25910
- function padStart2(string4, max) {
25911
- return common2.repeat(" ", max - string4.length) + string4;
26360
+ function formatJoeOutput(output) {
26361
+ const lines = [];
26362
+ const section = (value, label) => {
26363
+ if (value == null || value.trim() === "")
26364
+ return;
26365
+ if (lines.length > 0)
26366
+ lines.push("");
26367
+ if (label)
26368
+ lines.push(`${label}:`);
26369
+ lines.push(value);
26370
+ };
26371
+ section(output.response);
26372
+ section(output.plan_text, "plan");
26373
+ const flags = clientSidePlanFlags(output.plan_json).map((flag) => `⚑ ${flag}`);
26374
+ if (flags.length > 0) {
26375
+ lines.push(...flags);
26376
+ }
26377
+ section(output.plan_execution_text, "execution plan (EXPLAIN ANALYZE)");
26378
+ section(output.stats, "stats");
26379
+ section(output.recommendations, "recommendations");
26380
+ if (output.queryid) {
26381
+ if (lines.length > 0)
26382
+ lines.push("");
26383
+ lines.push(`(queryid ${output.queryid})`);
26384
+ }
26385
+ return lines.join(`
26386
+ `);
25912
26387
  }
25913
- function makeSnippet2(mark, options) {
25914
- options = Object.create(options || null);
26388
+ function formatProjectsTable(projects) {
26389
+ const header = ["PROJECT_ID", "ALIAS", "PROJECT", "JOE", "TUNNEL"];
26390
+ const rows = projects.map((p) => [
26391
+ String(p.project_id),
26392
+ p.alias ?? "-",
26393
+ p.name ?? "-",
26394
+ p.joe_ready ? "ready" : "no",
26395
+ p.tunnel ? "yes" : "no"
26396
+ ]);
26397
+ const widths = header.map((h, i2) => Math.max(h.length, ...rows.map((r) => r[i2].length), 0));
26398
+ const pad = (cells) => cells.map((c, i2) => c.padEnd(i2 === cells.length - 1 ? 0 : widths[i2])).join(" ").trimEnd();
26399
+ return [pad(header), ...rows.map(pad)].join(`
26400
+ `);
26401
+ }
26402
+
26403
+ // lib/joe.ts
26404
+ async function callRpc2(params) {
26405
+ const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26406
+ if (!apiKey) {
26407
+ throw new Error("API key is required");
26408
+ }
26409
+ const base = normalizeBaseUrl(apiBaseUrl);
26410
+ const url = new URL(`${base}/rpc/${fn}`);
26411
+ const payload = JSON.stringify(body);
26412
+ const headers = {
26413
+ "access-token": apiKey,
26414
+ "Content-Type": "application/json",
26415
+ Connection: "close"
26416
+ };
26417
+ if (debug) {
26418
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26419
+ console.error(`Debug: POST URL: ${url.toString()}`);
26420
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26421
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26422
+ }
26423
+ let response;
26424
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26425
+ try {
26426
+ response = await fetch(url.toString(), {
26427
+ method: "POST",
26428
+ headers,
26429
+ body: payload,
26430
+ signal: requestTimeout.signal
26431
+ });
26432
+ } catch (err) {
26433
+ if (isFetchTimeout(err)) {
26434
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26435
+ }
26436
+ throw new Error(describeFetchError(operation, base, err));
26437
+ }
26438
+ const text = await response.text();
26439
+ if (debug) {
26440
+ console.error(`Debug: Response status: ${response.status}`);
26441
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26442
+ }
26443
+ if (!response.ok) {
26444
+ throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26445
+ }
26446
+ try {
26447
+ return JSON.parse(text);
26448
+ } catch {
26449
+ throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26450
+ }
26451
+ }
26452
+ function preserveIntegerId2(value) {
26453
+ if (typeof value === "number")
26454
+ return value;
26455
+ const parsed = Number(value);
26456
+ return Number.isSafeInteger(parsed) ? parsed : value;
26457
+ }
26458
+ function normalizeProjectRow2(row) {
26459
+ return {
26460
+ project_id: preserveIntegerId2(row.project_id ?? 0),
26461
+ alias: row.alias ?? null,
26462
+ name: row.name ?? null,
26463
+ joe_ready: Boolean(row.joe_ready ?? false),
26464
+ tunnel: Boolean(row.tunnel ?? false),
26465
+ instance_id: row.instance_id == null ? null : preserveIntegerId2(row.instance_id),
26466
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id)
26467
+ };
26468
+ }
26469
+ async function listProjects2(params) {
26470
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
26471
+ const body = {};
26472
+ if (typeof orgId === "number") {
26473
+ body.org_id = orgId;
26474
+ }
26475
+ const rows = await callRpc2({
26476
+ apiKey,
26477
+ apiBaseUrl,
26478
+ fn: "projects_list",
26479
+ body,
26480
+ operation: "Failed to list projects",
26481
+ debug
26482
+ });
26483
+ if (!Array.isArray(rows)) {
26484
+ return [];
26485
+ }
26486
+ return rows.map(normalizeProjectRow2);
26487
+ }
26488
+ function isNumericProjectRef2(ref) {
26489
+ return /^[0-9]+$/.test(ref.trim());
26490
+ }
26491
+
26492
+ // lib/dblab.ts
26493
+ async function resolveDblabInstanceId(params) {
26494
+ const { apiKey, apiBaseUrl, orgId, debug } = params;
26495
+ if (!apiKey) {
26496
+ throw new Error("API key is required");
26497
+ }
26498
+ const ref = String(params.project ?? "").trim();
26499
+ if (!ref) {
26500
+ throw new Error("project is required (--project <id|alias>)");
26501
+ }
26502
+ let projects;
26503
+ try {
26504
+ projects = await listProjects2({ apiKey, apiBaseUrl, orgId, debug });
26505
+ } catch (err) {
26506
+ const message = err instanceof Error ? err.message : String(err);
26507
+ throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
26508
+ }
26509
+ const numeric = isNumericProjectRef2(ref);
26510
+ const needle = ref.toLowerCase();
26511
+ const match = projects.find((p) => {
26512
+ if (numeric) {
26513
+ return String(p.project_id) === ref;
26514
+ }
26515
+ return p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle;
26516
+ });
26517
+ if (!match) {
26518
+ throw new Error(`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`);
26519
+ }
26520
+ if (match.dblab_instance_id == null) {
26521
+ throw new Error(`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`);
26522
+ }
26523
+ return String(match.dblab_instance_id);
26524
+ }
26525
+ async function callDblabApi(params) {
26526
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
26527
+ if (!apiKey) {
26528
+ throw new Error("API key is required");
26529
+ }
26530
+ if (!instanceId) {
26531
+ throw new Error("instanceId is required");
26532
+ }
26533
+ const base = normalizeBaseUrl(apiBaseUrl);
26534
+ const url = new URL(`${base}/rpc/dblab_api_call`);
26535
+ const bodyObj = {
26536
+ instance_id: instanceId,
26537
+ action,
26538
+ method
26539
+ };
26540
+ if (data !== undefined) {
26541
+ bodyObj.data = data;
26542
+ }
26543
+ const body = JSON.stringify(bodyObj);
26544
+ const headers = {
26545
+ "access-token": apiKey,
26546
+ "Content-Type": "application/json",
26547
+ Connection: "close"
26548
+ };
26549
+ if (debug) {
26550
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26551
+ console.error(`Debug: POST URL: ${url.toString()}`);
26552
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26553
+ console.error(`Debug: Request body: ${redactSecretsForLog(body)}`);
26554
+ }
26555
+ let response;
26556
+ const requestTimeout = requestTimeoutSignal();
26557
+ try {
26558
+ response = await fetch(url.toString(), {
26559
+ method: "POST",
26560
+ headers,
26561
+ body,
26562
+ signal: requestTimeout.signal
26563
+ });
26564
+ } catch (err) {
26565
+ if (isFetchTimeout(err)) {
26566
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26567
+ }
26568
+ throw new Error(describeFetchError(operation, base, err));
26569
+ }
26570
+ const text = await response.text();
26571
+ if (debug) {
26572
+ console.error(`Debug: Response status: ${response.status}`);
26573
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26574
+ }
26575
+ if (!response.ok) {
26576
+ throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26577
+ }
26578
+ if (text.trim() === "") {
26579
+ return null;
26580
+ }
26581
+ try {
26582
+ return JSON.parse(text);
26583
+ } catch {
26584
+ throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26585
+ }
26586
+ }
26587
+ async function createClone(params) {
26588
+ const { apiKey, apiBaseUrl, instanceId, cloneId, branch, snapshotId, dbUser, dbPassword, isProtected, debug } = params;
26589
+ const data = { protected: Boolean(isProtected) };
26590
+ if (cloneId)
26591
+ data.id = cloneId;
26592
+ if (branch)
26593
+ data.branch = branch;
26594
+ if (snapshotId)
26595
+ data.snapshot = { id: snapshotId };
26596
+ if (dbUser && dbPassword)
26597
+ data.db = { username: dbUser, password: dbPassword };
26598
+ return callDblabApi({
26599
+ apiKey,
26600
+ apiBaseUrl,
26601
+ instanceId,
26602
+ action: "/clone",
26603
+ method: "post",
26604
+ data,
26605
+ operation: "Failed to create clone",
26606
+ debug
26607
+ });
26608
+ }
26609
+ async function listClones(params) {
26610
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
26611
+ return callDblabApi({
26612
+ apiKey,
26613
+ apiBaseUrl,
26614
+ instanceId,
26615
+ action: "/clones",
26616
+ method: "get",
26617
+ operation: "Failed to list clones",
26618
+ debug
26619
+ });
26620
+ }
26621
+ async function getClone(params) {
26622
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
26623
+ if (!cloneId)
26624
+ throw new Error("cloneId is required");
26625
+ return callDblabApi({
26626
+ apiKey,
26627
+ apiBaseUrl,
26628
+ instanceId,
26629
+ action: `/clone/${encodeURIComponent(cloneId)}`,
26630
+ method: "get",
26631
+ operation: "Failed to get clone",
26632
+ debug
26633
+ });
26634
+ }
26635
+ async function resetClone(params) {
26636
+ const { apiKey, apiBaseUrl, instanceId, cloneId, snapshotId, latest, debug } = params;
26637
+ if (!cloneId)
26638
+ throw new Error("cloneId is required");
26639
+ const data = { latest: latest ?? !snapshotId };
26640
+ if (snapshotId)
26641
+ data.snapshotID = snapshotId;
26642
+ return callDblabApi({
26643
+ apiKey,
26644
+ apiBaseUrl,
26645
+ instanceId,
26646
+ action: `/clone/${encodeURIComponent(cloneId)}/reset`,
26647
+ method: "post",
26648
+ data,
26649
+ operation: "Failed to reset clone",
26650
+ debug
26651
+ });
26652
+ }
26653
+ async function destroyClone(params) {
26654
+ const { apiKey, apiBaseUrl, instanceId, cloneId, debug } = params;
26655
+ if (!cloneId)
26656
+ throw new Error("cloneId is required");
26657
+ return callDblabApi({
26658
+ apiKey,
26659
+ apiBaseUrl,
26660
+ instanceId,
26661
+ action: `/clone/${encodeURIComponent(cloneId)}`,
26662
+ method: "delete",
26663
+ operation: "Failed to destroy clone",
26664
+ debug
26665
+ });
26666
+ }
26667
+ async function listBranches(params) {
26668
+ const { apiKey, apiBaseUrl, instanceId, debug } = params;
26669
+ return callDblabApi({
26670
+ apiKey,
26671
+ apiBaseUrl,
26672
+ instanceId,
26673
+ action: "/branches",
26674
+ method: "get",
26675
+ operation: "Failed to list branches",
26676
+ debug
26677
+ });
26678
+ }
26679
+ async function createBranch(params) {
26680
+ const { apiKey, apiBaseUrl, instanceId, branchName, baseBranch, snapshotId, debug } = params;
26681
+ if (!branchName)
26682
+ throw new Error("branchName is required");
26683
+ const data = { branchName };
26684
+ if (baseBranch)
26685
+ data.baseBranch = baseBranch;
26686
+ if (snapshotId)
26687
+ data.snapshotID = snapshotId;
26688
+ return callDblabApi({
26689
+ apiKey,
26690
+ apiBaseUrl,
26691
+ instanceId,
26692
+ action: "/branch",
26693
+ method: "post",
26694
+ data,
26695
+ operation: "Failed to create branch",
26696
+ debug
26697
+ });
26698
+ }
26699
+ async function deleteBranch(params) {
26700
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
26701
+ if (!branchName)
26702
+ throw new Error("branchName is required");
26703
+ return callDblabApi({
26704
+ apiKey,
26705
+ apiBaseUrl,
26706
+ instanceId,
26707
+ action: `/branch/${encodeURIComponent(branchName)}`,
26708
+ method: "delete",
26709
+ operation: "Failed to delete branch",
26710
+ debug
26711
+ });
26712
+ }
26713
+ async function branchLog(params) {
26714
+ const { apiKey, apiBaseUrl, instanceId, branchName, debug } = params;
26715
+ if (!branchName)
26716
+ throw new Error("branchName is required");
26717
+ return callDblabApi({
26718
+ apiKey,
26719
+ apiBaseUrl,
26720
+ instanceId,
26721
+ action: `/branch/${encodeURIComponent(branchName)}/log`,
26722
+ method: "get",
26723
+ operation: "Failed to fetch branch log",
26724
+ debug
26725
+ });
26726
+ }
26727
+ async function listSnapshots(params) {
26728
+ const { apiKey, apiBaseUrl, instanceId, branchName, dataset, debug } = params;
26729
+ const qs = new URLSearchParams;
26730
+ const branch = branchName?.trim();
26731
+ if (branch)
26732
+ qs.append("branch", branch);
26733
+ if (dataset)
26734
+ qs.append("dataset", dataset);
26735
+ const action = `/snapshots${qs.toString() ? `?${qs.toString()}` : ""}`;
26736
+ return callDblabApi({
26737
+ apiKey,
26738
+ apiBaseUrl,
26739
+ instanceId,
26740
+ action,
26741
+ method: "get",
26742
+ operation: "Failed to list snapshots",
26743
+ debug
26744
+ });
26745
+ }
26746
+ async function createSnapshot(params) {
26747
+ const { apiKey, apiBaseUrl, instanceId, cloneId, message, debug } = params;
26748
+ if (!cloneId)
26749
+ throw new Error("cloneId is required");
26750
+ const data = { cloneID: cloneId };
26751
+ if (message)
26752
+ data.message = message;
26753
+ return callDblabApi({
26754
+ apiKey,
26755
+ apiBaseUrl,
26756
+ instanceId,
26757
+ action: "/branch/snapshot",
26758
+ method: "post",
26759
+ data,
26760
+ operation: "Failed to create snapshot",
26761
+ debug
26762
+ });
26763
+ }
26764
+ async function destroySnapshot(params) {
26765
+ const { apiKey, apiBaseUrl, instanceId, snapshotId, force, debug } = params;
26766
+ if (!snapshotId)
26767
+ throw new Error("snapshotId is required");
26768
+ if (!/^[a-zA-Z0-9_.@/:-]+$/.test(snapshotId)) {
26769
+ throw new Error("snapshotId contains invalid characters");
26770
+ }
26771
+ if (snapshotId.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
26772
+ throw new Error("snapshotId contains an invalid path segment");
26773
+ }
26774
+ const action = `/snapshot/${snapshotId}?force=${Boolean(force)}`;
26775
+ return callDblabApi({
26776
+ apiKey,
26777
+ apiBaseUrl,
26778
+ instanceId,
26779
+ action,
26780
+ method: "delete",
26781
+ operation: "Failed to destroy snapshot",
26782
+ debug
26783
+ });
26784
+ }
26785
+
26786
+ // bin/postgres-ai.ts
26787
+ init_util();
26788
+
26789
+ // lib/instances.ts
26790
+ import * as fs4 from "fs";
26791
+
26792
+ // node_modules/js-yaml/dist/js-yaml.mjs
26793
+ /*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
26794
+ function isNothing2(subject) {
26795
+ return typeof subject === "undefined" || subject === null;
26796
+ }
26797
+ function isObject3(subject) {
26798
+ return typeof subject === "object" && subject !== null;
26799
+ }
26800
+ function toArray2(sequence) {
26801
+ if (Array.isArray(sequence))
26802
+ return sequence;
26803
+ else if (isNothing2(sequence))
26804
+ return [];
26805
+ return [sequence];
26806
+ }
26807
+ function extend4(target, source) {
26808
+ var index, length, key, sourceKeys;
26809
+ if (source) {
26810
+ sourceKeys = Object.keys(source);
26811
+ for (index = 0, length = sourceKeys.length;index < length; index += 1) {
26812
+ key = sourceKeys[index];
26813
+ target[key] = source[key];
26814
+ }
26815
+ }
26816
+ return target;
26817
+ }
26818
+ function repeat2(string4, count) {
26819
+ var result = "", cycle;
26820
+ for (cycle = 0;cycle < count; cycle += 1) {
26821
+ result += string4;
26822
+ }
26823
+ return result;
26824
+ }
26825
+ function isNegativeZero2(number4) {
26826
+ return number4 === 0 && Number.NEGATIVE_INFINITY === 1 / number4;
26827
+ }
26828
+ var isNothing_12 = isNothing2;
26829
+ var isObject_12 = isObject3;
26830
+ var toArray_12 = toArray2;
26831
+ var repeat_12 = repeat2;
26832
+ var isNegativeZero_12 = isNegativeZero2;
26833
+ var extend_12 = extend4;
26834
+ var common2 = {
26835
+ isNothing: isNothing_12,
26836
+ isObject: isObject_12,
26837
+ toArray: toArray_12,
26838
+ repeat: repeat_12,
26839
+ isNegativeZero: isNegativeZero_12,
26840
+ extend: extend_12
26841
+ };
26842
+ function formatError3(exception2, compact) {
26843
+ var where = "", message = exception2.reason || "(unknown reason)";
26844
+ if (!exception2.mark)
26845
+ return message;
26846
+ if (exception2.mark.name) {
26847
+ where += 'in "' + exception2.mark.name + '" ';
26848
+ }
26849
+ where += "(" + (exception2.mark.line + 1) + ":" + (exception2.mark.column + 1) + ")";
26850
+ if (!compact && exception2.mark.snippet) {
26851
+ where += `
26852
+
26853
+ ` + exception2.mark.snippet;
26854
+ }
26855
+ return message + " " + where;
26856
+ }
26857
+ function YAMLException$12(reason, mark) {
26858
+ Error.call(this);
26859
+ this.name = "YAMLException";
26860
+ this.reason = reason;
26861
+ this.mark = mark;
26862
+ this.message = formatError3(this, false);
26863
+ if (Error.captureStackTrace) {
26864
+ Error.captureStackTrace(this, this.constructor);
26865
+ } else {
26866
+ this.stack = new Error().stack || "";
26867
+ }
26868
+ }
26869
+ YAMLException$12.prototype = Object.create(Error.prototype);
26870
+ YAMLException$12.prototype.constructor = YAMLException$12;
26871
+ YAMLException$12.prototype.toString = function toString2(compact) {
26872
+ return this.name + ": " + formatError3(this, compact);
26873
+ };
26874
+ var exception2 = YAMLException$12;
26875
+ function getLine2(buffer, lineStart, lineEnd, position, maxLineLength) {
26876
+ var head = "";
26877
+ var tail = "";
26878
+ var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
26879
+ if (position - lineStart > maxHalfLength) {
26880
+ head = " ... ";
26881
+ lineStart = position - maxHalfLength + head.length;
26882
+ }
26883
+ if (lineEnd - position > maxHalfLength) {
26884
+ tail = " ...";
26885
+ lineEnd = position + maxHalfLength - tail.length;
26886
+ }
26887
+ return {
26888
+ str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
26889
+ pos: position - lineStart + head.length
26890
+ };
26891
+ }
26892
+ function padStart2(string4, max) {
26893
+ return common2.repeat(" ", max - string4.length) + string4;
26894
+ }
26895
+ function makeSnippet2(mark, options) {
26896
+ options = Object.create(options || null);
25915
26897
  if (!mark.buffer)
25916
26898
  return null;
25917
26899
  if (!options.maxLength)
@@ -28595,6 +29577,12 @@ async function registerAasCollection(apiKey, instanceId, opts) {
28595
29577
  try {
28596
29578
  if (!apiKey || !instanceId)
28597
29579
  return { ok: false, reason: "missing api key or instance id" };
29580
+ if (!(opts.vcpus > 0)) {
29581
+ return {
29582
+ ok: false,
29583
+ reason: "vcpus is unknown (0 or omitted); AAS collection stays off until a real value is set — re-run with --vcpus <n>"
29584
+ };
29585
+ }
28598
29586
  const labels = resolveAasLabels(opts.instancesPath);
28599
29587
  if (!labels)
28600
29588
  return { ok: false, reason: "could not determine a single (cluster, node_name) target" };
@@ -30281,16 +31269,93 @@ function createCallbackServer(port = 0, expectedState = null, timeoutMs = 300000
30281
31269
  }
30282
31270
  resolveReady(actualPort);
30283
31271
  });
30284
- return {
30285
- server: { stop: stopServer },
30286
- promise: promise2,
30287
- ready,
30288
- getPort: () => actualPort
30289
- };
31272
+ return {
31273
+ server: { stop: stopServer },
31274
+ promise: promise2,
31275
+ ready,
31276
+ getPort: () => actualPort
31277
+ };
31278
+ }
31279
+
31280
+ // bin/postgres-ai.ts
31281
+ init_util();
31282
+
31283
+ // lib/feedback.ts
31284
+ var FEEDBACK_URL = "https://gitlab.com/postgres-ai/postgresai/-/work_items/300";
31285
+ var FEEDBACK_SUPPRESS_ENV = "PGAI_NO_FEEDBACK_TIP";
31286
+ var FEEDBACK_TIP_MIN_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
31287
+ function feedbackTipSuppressed(suppress) {
31288
+ if (suppress === undefined)
31289
+ return false;
31290
+ const value = suppress.trim().toLowerCase();
31291
+ return value !== "" && value !== "0" && value !== "false" && value !== "no";
31292
+ }
31293
+ function feedbackTipAllowed(ctx) {
31294
+ if (ctx.json)
31295
+ return false;
31296
+ if (!ctx.isTTY)
31297
+ return false;
31298
+ if (feedbackTipSuppressed(ctx.suppress))
31299
+ return false;
31300
+ return true;
31301
+ }
31302
+ function isFeedbackTipDue(lastShownMs, nowMs) {
31303
+ if (lastShownMs === null || lastShownMs === undefined || !Number.isFinite(lastShownMs)) {
31304
+ return true;
31305
+ }
31306
+ return nowMs - lastShownMs >= FEEDBACK_TIP_MIN_INTERVAL_MS;
31307
+ }
31308
+ function feedbackTipLine() {
31309
+ return `\x1B[2m\uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL} (silence: ${FEEDBACK_SUPPRESS_ENV}=1)\x1B[0m`;
31310
+ }
31311
+ function readLastShown() {
31312
+ const value = readConfig2().feedbackTipLastShownAt;
31313
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
31314
+ }
31315
+ function persistLastShown(nowMs) {
31316
+ writeConfig2({ feedbackTipLastShownAt: nowMs });
31317
+ }
31318
+ function maybeEmitFeedbackTip(ctx, io = {}) {
31319
+ if (!feedbackTipAllowed(ctx))
31320
+ return false;
31321
+ const now = (io.now ?? Date.now)();
31322
+ const read = io.readLastShown ?? readLastShown;
31323
+ const persist = io.persistLastShown ?? persistLastShown;
31324
+ const write = io.write ?? ((line) => {
31325
+ process.stderr.write(line);
31326
+ });
31327
+ let lastShown = null;
31328
+ try {
31329
+ lastShown = read();
31330
+ } catch {
31331
+ lastShown = null;
31332
+ }
31333
+ if (!isFeedbackTipDue(lastShown, now))
31334
+ return false;
31335
+ try {
31336
+ persist(now);
31337
+ } catch {}
31338
+ write(feedbackTipLine() + `
31339
+ `);
31340
+ return true;
31341
+ }
31342
+ function feedbackJson() {
31343
+ return JSON.stringify({ url: FEEDBACK_URL }, null, 2);
31344
+ }
31345
+ function feedbackMessage() {
31346
+ return [
31347
+ "\uD83D\uDCA1 Help shape the PostgresAI CLI",
31348
+ "",
31349
+ "We read every idea and bug report. Share yours here:",
31350
+ "",
31351
+ ` ${FEEDBACK_URL}`,
31352
+ "",
31353
+ `Tip: set ${FEEDBACK_SUPPRESS_ENV}=1 to silence the occasional reminder.`
31354
+ ].join(`
31355
+ `);
30290
31356
  }
30291
31357
 
30292
31358
  // bin/postgres-ai.ts
30293
- init_util();
30294
31359
  import { createInterface } from "readline";
30295
31360
  import * as childProcess from "child_process";
30296
31361
 
@@ -30426,7 +31491,9 @@ where
30426
31491
  blk_read_time,
30427
31492
  blk_write_time,
30428
31493
  extract(epoch from (now() - pg_postmaster_start_time()))::int8 as postmaster_uptime_s,
30429
- extract(epoch from (now() - pg_backup_start_time()))::int8 as backup_duration_s,
31494
+ case when to_regproc('aurora_version') is null
31495
+ then extract(epoch from (now() - pg_backup_start_time()))::int8
31496
+ end as backup_duration_s,
30430
31497
  checksum_failures,
30431
31498
  extract(epoch from (now() - checksum_last_failure))::int8 as checksum_last_failure_s,
30432
31499
  case when pg_is_in_recovery() then 1 else 0 end as in_recovery_int,
@@ -30464,7 +31531,9 @@ where
30464
31531
  blk_read_time,
30465
31532
  blk_write_time,
30466
31533
  extract(epoch from (now() - pg_postmaster_start_time()))::int8 as postmaster_uptime_s,
30467
- extract(epoch from (now() - pg_backup_start_time()))::int8 as backup_duration_s,
31534
+ case when to_regproc('aurora_version') is null
31535
+ then extract(epoch from (now() - pg_backup_start_time()))::int8
31536
+ end as backup_duration_s,
30468
31537
  checksum_failures,
30469
31538
  extract(epoch from (now() - checksum_last_failure))::int8 as checksum_last_failure_s,
30470
31539
  case when pg_is_in_recovery() then 1 else 0 end as in_recovery_int,
@@ -31029,6 +32098,246 @@ where datname = current_database()
31029
32098
  gauges: ["stats_reset_epoch", "seconds_since_reset"],
31030
32099
  statement_timeout_seconds: 15
31031
32100
  },
32101
+ pg_database_wraparound: {
32102
+ sqls: {
32103
+ 11: `select /* pgwatch_generated */
32104
+ (extract(epoch from now()) * 1e9)::int8 as epoch_ns,
32105
+ datname as tag_datname,
32106
+ age(d.datfrozenxid) as age_datfrozenxid,
32107
+ mxid_age(d.datminmxid) as age_datminmxid
32108
+ from
32109
+ pg_catalog.pg_database d
32110
+ where
32111
+ d.datallowconn
32112
+ `
32113
+ },
32114
+ gauges: ["age_datfrozenxid", "age_datminmxid"],
32115
+ statement_timeout_seconds: 15
32116
+ },
32117
+ pg_settings_wraparound: {
32118
+ description: "Snapshots wraparound-related pg_settings values so dashboards can plot threshold lines (soft autovacuum_freeze_max_age, failsafe age) that reflect actual cluster configuration instead of compiled defaults. pg_settings is cluster-global, so this metric is is_instance_level — pgwatch scrapes it once per cluster instead of once per database.",
32119
+ sqls: {
32120
+ 11: `select /* pgwatch_generated */
32121
+ (extract(epoch from now()) * 1e9)::int8 as epoch_ns,
32122
+ (select setting::int8 from pg_settings where name = 'autovacuum_freeze_max_age') as autovacuum_freeze_max_age,
32123
+ (select setting::int8 from pg_settings where name = 'vacuum_freeze_min_age') as vacuum_freeze_min_age,
32124
+ (select setting::int8 from pg_settings where name = 'vacuum_freeze_table_age') as vacuum_freeze_table_age,
32125
+ (select setting::int8 from pg_settings where name = 'autovacuum_multixact_freeze_max_age') as autovacuum_multixact_freeze_max_age,
32126
+ (select setting::int8 from pg_settings where name = 'vacuum_multixact_freeze_min_age') as vacuum_multixact_freeze_min_age,
32127
+ (select setting::int8 from pg_settings where name = 'vacuum_multixact_freeze_table_age') as vacuum_multixact_freeze_table_age,
32128
+ coalesce((select setting::int8 from pg_settings where name = 'vacuum_failsafe_age'), 0) as vacuum_failsafe_age,
32129
+ coalesce((select setting::int8 from pg_settings where name = 'vacuum_multixact_failsafe_age'), 0) as vacuum_multixact_failsafe_age`
32130
+ },
32131
+ gauges: ["*"],
32132
+ statement_timeout_seconds: 15
32133
+ },
32134
+ pg_table_wraparound: {
32135
+ description: "Top relations at risk of transaction ID or MultiXact wraparound. Ranking reads pg_class only and is independently bounded to 50 relations for each age. Exact relation sizes are evaluated only after the bounded candidate set has been selected. Per-table freeze age overrides are exposed so F002 can evaluate the effective emergency threshold.",
32136
+ sqls: {
32137
+ 11: `with candidates as ( /* pgwatch_generated */
32138
+ select
32139
+ c.oid,
32140
+ n.nspname,
32141
+ c.relname,
32142
+ age(c.relfrozenxid)::int8 as xid_age,
32143
+ mxid_age(c.relminmxid)::int8 as multixact_age,
32144
+ coalesce(
32145
+ -- Follow-up: toast.* overrides live on the parent heap's
32146
+ -- reloptions, so relkind 't' currently uses the global value.
32147
+ (select split_part(option, '=', 2)::int8
32148
+ from unnest(coalesce(c.reloptions, '{}'::text[])) option
32149
+ where split_part(option, '=', 1) = 'autovacuum_freeze_max_age'),
32150
+ current_setting('autovacuum_freeze_max_age')::int8
32151
+ ) as effective_freeze_max_age,
32152
+ coalesce(
32153
+ (select split_part(option, '=', 2)::int8
32154
+ from unnest(coalesce(c.reloptions, '{}'::text[])) option
32155
+ where split_part(option, '=', 1) = 'autovacuum_multixact_freeze_max_age'),
32156
+ current_setting('autovacuum_multixact_freeze_max_age')::int8
32157
+ ) as effective_multixact_freeze_max_age
32158
+ from pg_catalog.pg_class c
32159
+ join pg_catalog.pg_namespace n on n.oid = c.relnamespace
32160
+ where c.relkind in ('r', 'm', 't')
32161
+ and c.relpersistence <> 't'
32162
+ and n.nspname <> 'information_schema'
32163
+ ),
32164
+ ranked as (
32165
+ (select *, 'xid'::text as ranked_by
32166
+ from candidates
32167
+ order by xid_age desc, nspname, relname
32168
+ limit 50)
32169
+ union all
32170
+ (select *, 'multixact'::text as ranked_by
32171
+ from candidates
32172
+ order by multixact_age desc, nspname, relname
32173
+ limit 50)
32174
+ ),
32175
+ selected as (
32176
+ select
32177
+ oid, nspname, relname, xid_age, multixact_age,
32178
+ effective_freeze_max_age, effective_multixact_freeze_max_age,
32179
+ string_agg(ranked_by, ',' order by ranked_by) as ranked_by
32180
+ from ranked
32181
+ group by oid, nspname, relname, xid_age, multixact_age,
32182
+ effective_freeze_max_age, effective_multixact_freeze_max_age
32183
+ )
32184
+ select
32185
+ (extract(epoch from now()) * 1e9)::int8 as epoch_ns,
32186
+ current_database() as tag_datname,
32187
+ s.nspname as tag_schema_name,
32188
+ s.relname as tag_table_name,
32189
+ s.ranked_by as tag_ranked_by,
32190
+ s.xid_age,
32191
+ s.multixact_age,
32192
+ s.effective_freeze_max_age,
32193
+ s.effective_multixact_freeze_max_age,
32194
+ pg_catalog.pg_total_relation_size(s.oid)::int8 as table_size_bytes
32195
+ from selected s`
32196
+ },
32197
+ gauges: ["xid_age", "multixact_age", "effective_freeze_max_age", "effective_multixact_freeze_max_age", "table_size_bytes"],
32198
+ statement_timeout_seconds: 15
32199
+ },
32200
+ multixact_size: {
32201
+ sqls: {
32202
+ 11: `with env as ( /* pgwatch_generated */
32203
+ select
32204
+ exists (
32205
+ select
32206
+ from pg_proc p
32207
+ join pg_namespace n on n.oid = p.pronamespace
32208
+ where p.proname = 'pg_ls_multixactdir' and n.nspname = 'rds_tools'
32209
+ ) as has_rds_fn,
32210
+ exists (
32211
+ select
32212
+ from pg_proc p
32213
+ join pg_namespace n on n.oid = p.pronamespace
32214
+ where p.proname = 'aurora_stat_file' and n.nspname = 'pg_catalog'
32215
+ ) as has_aurora_fn,
32216
+ exists (select from pg_proc where proname = 'pg_ls_dir') as has_pg_ls_dir_func,
32217
+ exists (select from pg_proc where proname = 'pg_stat_file') as has_pg_stat_file_func
32218
+ ),
32219
+ can_local as (
32220
+ -- to_regprocedure returns NULL instead of erroring when the signature
32221
+ -- does not exist, so this stays safe even on databases where
32222
+ -- pg_stat_file has been dropped or has a different signature.
32223
+ -- coalesce guards against NULL propagating through the AND chain.
32224
+ -- The execute-privilege check prevents "permission denied" on
32225
+ -- managed databases (Supabase etc.) where pg_stat_file is
32226
+ -- restricted to superusers (#142).
32227
+ select (has_pg_ls_dir_func and has_pg_stat_file_func
32228
+ and coalesce(
32229
+ has_function_privilege(
32230
+ to_regprocedure('pg_stat_file(text,boolean)'), 'execute'),
32231
+ false)
32232
+ ) as ok from env
32233
+ ),
32234
+ -- Use query_to_xml to safely execute Aurora-specific multixact query.
32235
+ -- Aurora uses aurora_stat_file() function instead of rds_tools.pg_ls_multixactdir().
32236
+ aurora_probe_xml as (
32237
+ select query_to_xml($q$
32238
+ with files as (
32239
+ select filename, allocated_bytes, used_bytes
32240
+ from aurora_stat_file()
32241
+ where filename like 'pg_multixact/%'
32242
+ ),
32243
+ members as (
32244
+ select sum(used_bytes)::bigint as sz from files where filename like 'pg_multixact/members%'
32245
+ ),
32246
+ offsets as (
32247
+ select sum(used_bytes)::bigint as sz from files where filename like 'pg_multixact/offsets%'
32248
+ ),
32249
+ has_rows as (
32250
+ select exists(select 1 from files) as any_rows
32251
+ )
32252
+ select
32253
+ case when (select any_rows from has_rows) then coalesce((select sz from members), 0) end as members_bytes,
32254
+ case when (select any_rows from has_rows) then coalesce((select sz from offsets), 0) end as offsets_bytes,
32255
+ case when (select any_rows from has_rows) then 0 else 1 end as status_code
32256
+ $q$, true, true, '') as x
32257
+ where (select has_aurora_fn from env)
32258
+ ),
32259
+ -- Use query_to_xml to safely execute RDS-specific multixact directory listing query.
32260
+ -- The XML wrapper allows the query to fail gracefully if rds_tools.pg_ls_multixactdir()
32261
+ -- is unavailable or returns errors, preventing the entire metric from failing.
32262
+ rds_probe_xml as (
32263
+ select query_to_xml($q$
32264
+ with files as (
32265
+ select name, size
32266
+ from rds_tools.pg_ls_multixactdir()
32267
+ ),
32268
+ members as (
32269
+ select sum(size)::bigint as sz from files where name like 'pg_multixact/members%'
32270
+ ),
32271
+ offsets as (
32272
+ select sum(size)::bigint as sz from files where name like 'pg_multixact/offsets%'
32273
+ ),
32274
+ has_rows as (
32275
+ select exists(select 1 from files where name like 'pg_multixact/%') as any_rows
32276
+ )
32277
+ select
32278
+ case when (select any_rows from has_rows) then coalesce((select sz from members), 0) end as members_bytes,
32279
+ case when (select any_rows from has_rows) then coalesce((select sz from offsets), 0) end as offsets_bytes,
32280
+ case when (select any_rows from has_rows) then 0 else 1 end as status_code
32281
+ $q$, true, true, '') as x
32282
+ where (select has_rds_fn from env) and not (select has_aurora_fn from env)
32283
+ ),
32284
+ -- Use query_to_xml to safely execute standard Postgres multixact directory listing query.
32285
+ -- The XML wrapper allows the query to fail gracefully if pg_stat_file() or pg_ls_dir()
32286
+ -- are unavailable or return permission errors, preventing the entire metric from failing.
32287
+ local_probe_xml as (
32288
+ select query_to_xml($q$
32289
+ with dirs as (
32290
+ select
32291
+ (pg_stat_file('pg_multixact/members', true)).isdir as has_members,
32292
+ (pg_stat_file('pg_multixact/offsets', true)).isdir as has_offsets
32293
+ ),
32294
+ flags as (
32295
+ select ((select has_members from dirs) or (select has_offsets from dirs)) as has_any
32296
+ ),
32297
+ members as (
32298
+ select sum((pg_stat_file(format('pg_multixact/members/%s', d), true)).size)::bigint as sz
32299
+ from pg_ls_dir('pg_multixact/members') as d(d)
32300
+ where (select has_members from dirs)
32301
+ ),
32302
+ offsets as (
32303
+ select sum((pg_stat_file(format('pg_multixact/offsets/%s', d), true)).size)::bigint as sz
32304
+ from pg_ls_dir('pg_multixact/offsets') as d(d)
32305
+ where (select has_offsets from dirs)
32306
+ )
32307
+ select
32308
+ case when (select has_any from flags) then coalesce((select sz from members), 0) end as members_bytes,
32309
+ case when (select has_any from flags) then coalesce((select sz from offsets), 0) end as offsets_bytes,
32310
+ case when (select has_any from flags) then 0 else 1 end as status_code
32311
+ $q$, true, true, '') as x
32312
+ where not (select has_rds_fn from env) and not (select has_aurora_fn from env) and (select ok from can_local)
32313
+ ),
32314
+ picked as (
32315
+ select * from aurora_probe_xml
32316
+ union all
32317
+ select * from rds_probe_xml
32318
+ union all
32319
+ select * from local_probe_xml
32320
+ limit 1
32321
+ ),
32322
+ parsed as (
32323
+ select
32324
+ (xpath('//members_bytes/text()', x))[1]::text::bigint as members_bytes,
32325
+ (xpath('//offsets_bytes/text()', x))[1]::text::bigint as offsets_bytes,
32326
+ (xpath('//status_code/text()', x))[1]::text::int as status_code
32327
+ from picked
32328
+ )
32329
+ select * from parsed
32330
+ union all
32331
+ select
32332
+ null::bigint as members_bytes,
32333
+ null::bigint as offsets_bytes,
32334
+ 2::int as status_code
32335
+ where not exists (select 1 from parsed);
32336
+ `
32337
+ },
32338
+ gauges: ["members_bytes", "offsets_bytes", "status_code"],
32339
+ statement_timeout_seconds: 15
32340
+ },
31032
32341
  pg_dead_tuples: {
31033
32342
  description: "Per-table dead-tuple accumulation and per-table autovacuum overrides for the F003 express check (\"Autovacuum: dead tuples\"). Reads the live counters in pg_stat_user_tables joined to pg_class for reloptions, so dead tuples that have NEVER been vacuumed are visible — unlike the statistical bloat estimators (F004/F005), which completely miss a table where autovacuum is disabled and dead tuples pile up unvacuumed. Returns relations that either carry dead tuples or have autovacuum disabled per-table, bounded to the top 100 by n_dead_tup so pathological schemas can't blow up the report. autovacuum_disabled recognises every boolean spelling Postgres accepts and stores verbatim in reloptions (false/off/no/0 and their unique prefixes; confirmed against PG13 and PG16 that `alter table .. set (autovacuum_enabled = false|off|0)` stores the literal text, so matching only 'autovacuum_enabled=off' misses the common '=false' spelling). Timestamps are exported as epoch seconds (0 = never) because Prometheus gauges must be numeric. The AccessExclusiveLock guard mirrors table_stats: pg_table_size() takes a lock and would block behind a fully locked relation. Compatible with all supported Postgres versions.",
31034
32343
  sqls: {
@@ -31361,6 +32670,200 @@ having count(*) > 0
31361
32670
  gauges: ["real_size_mib", "table_size_mib", "extra_size", "extra_pct", "fillfactor", "bloat_size", "bloat_pct", "is_na", "reltuples"],
31362
32671
  statement_timeout_seconds: 15
31363
32672
  },
32673
+ xmin_horizon_snapshot: {
32674
+ description: "Express-checkup single snapshot of xmin horizon ages and the oldest blocker for each holder class. Returns one row with component JSON, privilege/privacy metadata, and a pg_flight_recorder availability hint. Primary-only analysis is enforced in SQL; replicas return an informational skip row.",
32675
+ sqls: {
32676
+ 12: `with context as materialized (
32677
+ select
32678
+ pg_is_in_recovery() as is_in_recovery,
32679
+ case when pg_is_in_recovery() then null
32680
+ else txid_snapshot_xmin(txid_current_snapshot())::int8 end as snapshot_xmin,
32681
+ current_setting('autovacuum_freeze_max_age')::int8 as autovacuum_freeze_max_age,
32682
+ coalesce(
32683
+ (select rolsuper from pg_roles where rolname = current_user), false
32684
+ ) or coalesce(
32685
+ (select pg_has_role(current_user, oid, 'member') from pg_roles where rolname = 'pg_monitor'), false
32686
+ ) or coalesce(
32687
+ (select pg_has_role(current_user, oid, 'member') from pg_roles where rolname = 'pg_read_all_stats'), false
32688
+ )
32689
+ as has_full_visibility,
32690
+ lower(coalesce(current_setting('postgresai.capture_query_preview', true), 'on'))
32691
+ not in ('', 'off', 'false', '0') as query_preview_enabled,
32692
+ to_regnamespace('pgfr_analyze') is not null as pg_flight_recorder_detected
32693
+ ),
32694
+ activity_rows as materialized (
32695
+ select
32696
+ greatest(age(a.backend_xmin), age(a.backend_xid))::int8 as age_tx,
32697
+ jsonb_strip_nulls(jsonb_build_object(
32698
+ 'pid', a.pid,
32699
+ 'database', a.datname,
32700
+ 'user', a.usename,
32701
+ 'application_name', a.application_name,
32702
+ 'state', a.state,
32703
+ 'backend_type', to_jsonb(a)->>'backend_type',
32704
+ 'queryid', to_jsonb(a)->>'query_id',
32705
+ 'xact_age_seconds', extract(epoch from now() - a.xact_start)::int8,
32706
+ 'state_age_seconds', extract(epoch from now() - a.state_change)::int8,
32707
+ 'query_age_seconds', extract(epoch from now() - a.query_start)::int8,
32708
+ 'query_preview', case
32709
+ when c.has_full_visibility and c.query_preview_enabled
32710
+ then left(regexp_replace(a.query, E'[\\\\r\\\\n\\\\t]+', ' ', 'g'), 500)
32711
+ else null
32712
+ end
32713
+ )) as detail
32714
+ from pg_stat_activity a
32715
+ cross join context c
32716
+ where not c.is_in_recovery
32717
+ and a.pid <> pg_backend_pid()
32718
+ and (a.backend_xmin is not null or a.backend_xid is not null)
32719
+ and coalesce(to_jsonb(a)->>'leader_pid', '') = ''
32720
+ and coalesce(to_jsonb(a)->>'backend_type', 'client backend')
32721
+ in ('client backend', 'autovacuum worker')
32722
+ ),
32723
+ activity as (
32724
+ select
32725
+ coalesce(max(age_tx), 0)::int8 as age_tx,
32726
+ count(*)::int8 as count,
32727
+ (array_agg(detail order by age_tx desc, (detail->>'pid')::int asc))[1] as top_blocker
32728
+ from activity_rows
32729
+ ),
32730
+ slot_rows as materialized (
32731
+ select
32732
+ age(s.xmin)::int8 as age_tx,
32733
+ age(s.catalog_xmin)::int8 as catalog_age_tx,
32734
+ jsonb_strip_nulls(jsonb_build_object(
32735
+ 'slot_name', s.slot_name,
32736
+ 'slot_type', s.slot_type,
32737
+ 'plugin', s.plugin,
32738
+ 'database', s.database,
32739
+ 'active', s.active,
32740
+ 'active_pid', s.active_pid,
32741
+ 'status', case
32742
+ when coalesce(to_jsonb(s)->>'invalidation_reason', '') <> '' then 'invalidated'
32743
+ when coalesce(to_jsonb(s)->>'conflicting', 'false') = 'true' then 'conflicting'
32744
+ when not s.active and coalesce(to_jsonb(s)->>'inactive_since', '') <> '' then 'inactive'
32745
+ when not s.active then 'unused'
32746
+ else 'active'
32747
+ end,
32748
+ 'inactive_since', to_jsonb(s)->>'inactive_since',
32749
+ 'wal_status', to_jsonb(s)->>'wal_status',
32750
+ 'conflicting', (to_jsonb(s)->>'conflicting')::boolean,
32751
+ 'invalidation_reason', to_jsonb(s)->>'invalidation_reason'
32752
+ )) as detail
32753
+ from pg_replication_slots s
32754
+ cross join context c
32755
+ where not c.is_in_recovery
32756
+ and (s.xmin is not null or s.catalog_xmin is not null)
32757
+ ),
32758
+ slots as (
32759
+ select
32760
+ coalesce(max(age_tx), 0)::int8 as age_tx,
32761
+ count(*) filter (where age_tx is not null)::int8 as count,
32762
+ (array_agg(detail order by age_tx desc nulls last, detail->>'slot_name')
32763
+ filter (where age_tx is not null))[1] as top_blocker
32764
+ from slot_rows
32765
+ ),
32766
+ slots_catalog as (
32767
+ select
32768
+ coalesce(max(catalog_age_tx), 0)::int8 as age_tx,
32769
+ count(*) filter (where catalog_age_tx is not null)::int8 as count,
32770
+ (array_agg(detail order by catalog_age_tx desc nulls last, detail->>'slot_name')
32771
+ filter (where catalog_age_tx is not null))[1] as top_blocker
32772
+ from slot_rows
32773
+ ),
32774
+ replication_rows as materialized (
32775
+ select
32776
+ age(r.backend_xmin)::int8 as age_tx,
32777
+ jsonb_strip_nulls(jsonb_build_object(
32778
+ 'pid', r.pid,
32779
+ 'user', r.usename,
32780
+ 'application_name', r.application_name,
32781
+ 'client_addr', r.client_addr,
32782
+ 'state', r.state,
32783
+ 'sync_state', r.sync_state
32784
+ )) as detail
32785
+ from pg_stat_replication r
32786
+ cross join context c
32787
+ where not c.is_in_recovery and r.backend_xmin is not null
32788
+ ),
32789
+ replication as (
32790
+ select
32791
+ coalesce(max(age_tx), 0)::int8 as age_tx,
32792
+ count(*)::int8 as count,
32793
+ (array_agg(detail order by age_tx desc, (detail->>'pid')::int asc))[1] as top_blocker
32794
+ from replication_rows
32795
+ ),
32796
+ prepared_rows as materialized (
32797
+ select
32798
+ age(p.transaction)::int8 as age_tx,
32799
+ jsonb_strip_nulls(jsonb_build_object(
32800
+ 'gid', p.gid,
32801
+ 'owner', p.owner,
32802
+ 'database', p.database,
32803
+ 'prepared_at', p.prepared,
32804
+ 'prepared_age_seconds', extract(epoch from now() - p.prepared)::int8
32805
+ )) as detail
32806
+ from pg_prepared_xacts p
32807
+ cross join context c
32808
+ where not c.is_in_recovery
32809
+ ),
32810
+ prepared as (
32811
+ select
32812
+ coalesce(max(age_tx), 0)::int8 as age_tx,
32813
+ count(*)::int8 as count,
32814
+ (array_agg(detail order by age_tx desc, detail->>'gid' asc))[1] as top_blocker
32815
+ from prepared_rows
32816
+ )
32817
+ select /* pgwatch_generated xmin_horizon_snapshot */
32818
+ (extract(epoch from now()) * 1e9)::int8 as epoch_ns,
32819
+ current_database() as tag_datname,
32820
+ c.is_in_recovery,
32821
+ case when c.is_in_recovery then 'replica: xmin horizon analysis is primary-only' end as skip_reason,
32822
+ c.snapshot_xmin,
32823
+ c.autovacuum_freeze_max_age,
32824
+ c.has_full_visibility,
32825
+ c.query_preview_enabled,
32826
+ c.pg_flight_recorder_detected,
32827
+ case when c.is_in_recovery then 0 else greatest(a.age_tx, s.age_tx, r.age_tx, p.age_tx) end
32828
+ as data_horizon_age_tx,
32829
+ case when c.is_in_recovery then 0 else sc.age_tx end as catalog_horizon_age_tx,
32830
+ jsonb_build_object(
32831
+ 'pg_stat_activity', jsonb_build_object(
32832
+ 'age_tx', case when c.is_in_recovery then 0 else a.age_tx end,
32833
+ 'count', case when c.is_in_recovery then 0 else a.count end,
32834
+ 'top_blocker', case when c.is_in_recovery then null else a.top_blocker end
32835
+ ),
32836
+ 'pg_replication_slots', jsonb_build_object(
32837
+ 'age_tx', case when c.is_in_recovery then 0 else s.age_tx end,
32838
+ 'count', case when c.is_in_recovery then 0 else s.count end,
32839
+ 'top_blocker', case when c.is_in_recovery then null else s.top_blocker end
32840
+ ),
32841
+ 'pg_replication_slots_catalog', jsonb_build_object(
32842
+ 'age_tx', case when c.is_in_recovery then 0 else sc.age_tx end,
32843
+ 'count', case when c.is_in_recovery then 0 else sc.count end,
32844
+ 'top_blocker', case when c.is_in_recovery then null else sc.top_blocker end
32845
+ ),
32846
+ 'pg_stat_replication', jsonb_build_object(
32847
+ 'age_tx', case when c.is_in_recovery then 0 else r.age_tx end,
32848
+ 'count', case when c.is_in_recovery then 0 else r.count end,
32849
+ 'top_blocker', case when c.is_in_recovery then null else r.top_blocker end
32850
+ ),
32851
+ 'pg_prepared_xacts', jsonb_build_object(
32852
+ 'age_tx', case when c.is_in_recovery then 0 else p.age_tx end,
32853
+ 'count', case when c.is_in_recovery then 0 else p.count end,
32854
+ 'top_blocker', case when c.is_in_recovery then null else p.top_blocker end
32855
+ )
32856
+ ) as components
32857
+ from context c
32858
+ cross join activity a
32859
+ cross join slots s
32860
+ cross join slots_catalog sc
32861
+ cross join replication r
32862
+ cross join prepared p`
32863
+ },
32864
+ gauges: ["data_horizon_age_tx", "catalog_horizon_age_tx"],
32865
+ statement_timeout_seconds: 15
32866
+ },
31364
32867
  pg_stat_io: {
31365
32868
  description: "Collects I/O statistics from the PostgreSQL `pg_stat_io` view (available in PostgreSQL 16+). Provides insights into read and write operations by backend type, including the number of operations, MiB transferred (divided by 1048576), and time spent on I/O. This metric is essential for monitoring I/O performance and identifying potential bottlenecks in disk operations. The data is aggregated by backend type with a total row included via ROLLUP.",
31366
32869
  sqls: {
@@ -31439,9 +32942,14 @@ var METRIC_NAMES = {
31439
32942
  H001: "pg_invalid_indexes",
31440
32943
  H002: "unused_indexes",
31441
32944
  H004: "redundant_indexes",
32945
+ F002Database: "pg_database_wraparound",
32946
+ F002Settings: "pg_settings_wraparound",
32947
+ F002Tables: "pg_table_wraparound",
32948
+ F002MultixactSize: "multixact_size",
31442
32949
  F003: "pg_dead_tuples",
31443
32950
  F004: "pg_table_bloat",
31444
32951
  F005: "pg_btree_bloat",
32952
+ F009: "xmin_horizon_snapshot",
31445
32953
  settings: "settings",
31446
32954
  dbStats: "db_stats",
31447
32955
  dbSize: "db_size",
@@ -32014,26 +33522,377 @@ var CHECKUP_DICTIONARY_DATA = [
32014
33522
  sort_order: null,
32015
33523
  is_system_report: false
32016
33524
  }
32017
- ];
32018
-
32019
- // lib/checkup-dictionary.ts
32020
- var dictionaryByCode = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
32021
- function buildCheckInfoMap() {
32022
- const result = {};
32023
- for (const entry of CHECKUP_DICTIONARY_DATA) {
32024
- result[entry.code] = entry.title;
33525
+ ];
33526
+
33527
+ // lib/checkup-dictionary.ts
33528
+ var dictionaryByCode = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
33529
+ function buildCheckInfoMap() {
33530
+ const result = {};
33531
+ for (const entry of CHECKUP_DICTIONARY_DATA) {
33532
+ result[entry.code] = entry.title;
33533
+ }
33534
+ return result;
33535
+ }
33536
+
33537
+ // lib/checkup-summary.ts
33538
+ function generateCheckSummary(checkId, report) {
33539
+ const nodeIds = Object.keys(report.results || {});
33540
+ if (nodeIds.length === 0) {
33541
+ return { status: "info", message: "No data" };
33542
+ }
33543
+ const nodeData = report.results[nodeIds[0]];
33544
+ switch (checkId) {
33545
+ case "H001":
33546
+ return summarizeH001(nodeData);
33547
+ case "H002":
33548
+ return summarizeH002(nodeData);
33549
+ case "H004":
33550
+ return summarizeH004(nodeData);
33551
+ case "A002":
33552
+ return summarizeA002(nodeData);
33553
+ case "A013":
33554
+ return summarizeA013(nodeData);
33555
+ case "A003":
33556
+ return summarizeA003(nodeData);
33557
+ case "A004":
33558
+ return summarizeA004(nodeData);
33559
+ case "A007":
33560
+ return summarizeA007(nodeData);
33561
+ case "D001":
33562
+ return summarizeD001(nodeData);
33563
+ case "D004":
33564
+ return summarizeD004(nodeData);
33565
+ case "F001":
33566
+ return summarizeF001(nodeData);
33567
+ case "F002":
33568
+ return summarizeF002(nodeData);
33569
+ case "F003":
33570
+ return summarizeF003(nodeData);
33571
+ case "F004":
33572
+ return summarizeBloat(nodeData, "table");
33573
+ case "F005":
33574
+ return summarizeBloat(nodeData, "index");
33575
+ case "F009":
33576
+ return summarizeF009(nodeData);
33577
+ case "G001":
33578
+ return summarizeG001(nodeData);
33579
+ case "G003":
33580
+ return summarizeG003(nodeData);
33581
+ default:
33582
+ return { status: "info", message: "Check completed" };
33583
+ }
33584
+ }
33585
+ function summarizeA003(nodeData) {
33586
+ const data = nodeData?.data || {};
33587
+ const settingsCount = Object.keys(data).length;
33588
+ if (settingsCount === 0) {
33589
+ return { status: "info", message: "No settings found" };
33590
+ }
33591
+ return {
33592
+ status: "info",
33593
+ message: `${settingsCount} setting${settingsCount > 1 ? "s" : ""} collected`
33594
+ };
33595
+ }
33596
+ function summarizeA004(nodeData) {
33597
+ const data = nodeData?.data;
33598
+ if (!data) {
33599
+ return { status: "info", message: "Cluster information collected" };
33600
+ }
33601
+ const dbCount = Object.keys(data.database_sizes || {}).length;
33602
+ if (dbCount > 0) {
33603
+ return { status: "info", message: `${dbCount} database${dbCount > 1 ? "s" : ""} analyzed` };
33604
+ }
33605
+ return { status: "info", message: "Cluster information collected" };
33606
+ }
33607
+ function summarizeA007(nodeData) {
33608
+ const data = nodeData?.data || {};
33609
+ const alteredCount = Object.keys(data).length;
33610
+ if (alteredCount === 0) {
33611
+ return { status: "ok", message: "No altered settings" };
33612
+ }
33613
+ return {
33614
+ status: "info",
33615
+ message: `${alteredCount} setting${alteredCount > 1 ? "s" : ""} altered from defaults`
33616
+ };
33617
+ }
33618
+ function formatBytes(bytes) {
33619
+ if (bytes === 0)
33620
+ return "0 B";
33621
+ if (bytes < 1024)
33622
+ return `${bytes} B`;
33623
+ if (bytes < 1024 * 1024)
33624
+ return `${(bytes / 1024).toFixed(0)} KiB`;
33625
+ if (bytes < 1024 * 1024 * 1024)
33626
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
33627
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
33628
+ }
33629
+ function summarizeH001(nodeData) {
33630
+ const data = nodeData?.data || {};
33631
+ let totalCount = 0;
33632
+ let totalSize = 0;
33633
+ for (const dbData of Object.values(data)) {
33634
+ const dbEntry = dbData;
33635
+ totalCount += dbEntry.total_count || 0;
33636
+ totalSize += dbEntry.total_size_bytes || 0;
33637
+ }
33638
+ if (totalCount === 0) {
33639
+ return { status: "ok", message: "No invalid indexes" };
33640
+ }
33641
+ return {
33642
+ status: "warning",
33643
+ message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
33644
+ };
33645
+ }
33646
+ function summarizeH002(nodeData) {
33647
+ const data = nodeData?.data || {};
33648
+ let totalCount = 0;
33649
+ let totalSize = 0;
33650
+ for (const dbData of Object.values(data)) {
33651
+ const dbEntry = dbData;
33652
+ totalCount += dbEntry.total_count || 0;
33653
+ totalSize += dbEntry.total_size_bytes || 0;
33654
+ }
33655
+ if (totalCount === 0) {
33656
+ return { status: "ok", message: "All indexes utilized" };
33657
+ }
33658
+ return {
33659
+ status: "warning",
33660
+ message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
33661
+ };
33662
+ }
33663
+ function summarizeH004(nodeData) {
33664
+ const data = nodeData?.data || {};
33665
+ let totalCount = 0;
33666
+ let totalSize = 0;
33667
+ for (const dbData of Object.values(data)) {
33668
+ const dbEntry = dbData;
33669
+ totalCount += dbEntry.total_count || 0;
33670
+ totalSize += dbEntry.total_size_bytes || 0;
33671
+ }
33672
+ if (totalCount === 0) {
33673
+ return { status: "ok", message: "No redundant indexes" };
33674
+ }
33675
+ return {
33676
+ status: "warning",
33677
+ message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes(totalSize)})`
33678
+ };
33679
+ }
33680
+ function summarizeA002(nodeData) {
33681
+ const ver = nodeData?.data?.version;
33682
+ if (!ver) {
33683
+ return { status: "info", message: "Version checked" };
32025
33684
  }
32026
- return result;
33685
+ const major = parseInt(ver.server_major_ver, 10);
33686
+ if (major >= 17) {
33687
+ return { status: "ok", message: `PostgreSQL ${major}` };
33688
+ }
33689
+ if (major >= 15) {
33690
+ return { status: "info", message: `PostgreSQL ${major}` };
33691
+ }
33692
+ return {
33693
+ status: "warning",
33694
+ message: `PostgreSQL ${major} (consider upgrading)`
33695
+ };
33696
+ }
33697
+ function summarizeA013(nodeData) {
33698
+ const ver = nodeData?.data?.version;
33699
+ if (!ver) {
33700
+ return { status: "info", message: "Minor version checked" };
33701
+ }
33702
+ const current = ver.version || "";
33703
+ return {
33704
+ status: "info",
33705
+ message: `Version ${current}`
33706
+ };
33707
+ }
33708
+ function summarizeD001(nodeData) {
33709
+ const data = nodeData?.data || {};
33710
+ const settingsCount = Object.keys(data).length;
33711
+ if (settingsCount === 0) {
33712
+ return { status: "info", message: "No logging settings found" };
33713
+ }
33714
+ return {
33715
+ status: "info",
33716
+ message: `${settingsCount} logging setting${settingsCount > 1 ? "s" : ""} collected`
33717
+ };
33718
+ }
33719
+ function summarizeD004(nodeData) {
33720
+ const data = nodeData?.data || {};
33721
+ const settingsCount = Object.keys(data).length;
33722
+ if (settingsCount === 0) {
33723
+ return { status: "info", message: "No pg_stat_statements settings found" };
33724
+ }
33725
+ return {
33726
+ status: "info",
33727
+ message: `${settingsCount} pg_stat_statements setting${settingsCount > 1 ? "s" : ""} collected`
33728
+ };
33729
+ }
33730
+ function summarizeF001(nodeData) {
33731
+ const data = nodeData?.data || {};
33732
+ const settingsCount = Object.keys(data).length;
33733
+ if (settingsCount === 0) {
33734
+ return { status: "info", message: "No autovacuum settings found" };
33735
+ }
33736
+ return {
33737
+ status: "info",
33738
+ message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
33739
+ };
33740
+ }
33741
+ function summarizeF002(nodeData) {
33742
+ if (nodeData?.data?.settings_available === false) {
33743
+ return { status: "info", message: "Wraparound settings unavailable" };
33744
+ }
33745
+ const severity = nodeData?.data?.severity;
33746
+ const databases = nodeData?.data?.databases?.length || 0;
33747
+ const tables = nodeData?.data?.tables || [];
33748
+ const offenders = tables.filter((table) => table?.xid?.severity !== "info" || table?.multixact?.severity !== "info").length;
33749
+ if (!severity)
33750
+ return { status: "info", message: "No wraparound data" };
33751
+ if (severity === "info") {
33752
+ return { status: "ok", message: `${databases} database${databases === 1 ? "" : "s"} below wraparound thresholds` };
33753
+ }
33754
+ return {
33755
+ status: "warning",
33756
+ message: `${severity.toUpperCase()} wraparound risk${offenders ? `; ${offenders} table${offenders === 1 ? "" : "s"} affected` : ""}`
33757
+ };
33758
+ }
33759
+ function summarizeF003(nodeData) {
33760
+ const data = nodeData?.data || {};
33761
+ let flaggedCount = 0;
33762
+ let disabledCount = 0;
33763
+ for (const dbData of Object.values(data)) {
33764
+ const dbEntry = dbData;
33765
+ flaggedCount += dbEntry.flagged_count || 0;
33766
+ disabledCount += dbEntry.autovacuum_disabled_flagged_count || 0;
33767
+ }
33768
+ if (flaggedCount === 0 && disabledCount === 0) {
33769
+ return { status: "ok", message: "No significant dead tuple accumulation" };
33770
+ }
33771
+ const parts = [];
33772
+ if (flaggedCount > 0) {
33773
+ parts.push(`${flaggedCount} table${flaggedCount > 1 ? "s" : ""} with excessive dead tuples`);
33774
+ }
33775
+ if (disabledCount > 0) {
33776
+ parts.push(`${disabledCount} table${disabledCount > 1 ? "s" : ""} with autovacuum disabled`);
33777
+ }
33778
+ return { status: "warning", message: parts.join(", ") };
33779
+ }
33780
+ function summarizeF009(nodeData) {
33781
+ const data = nodeData?.data || {};
33782
+ if (data.skipped) {
33783
+ return { status: "info", message: data.skip_reason || "Primary-only check skipped on replica" };
33784
+ }
33785
+ const severity = String(data.severity || "OK");
33786
+ const age = Math.max(Number(data.data_horizon_age_tx || 0), Number(data.catalog_horizon_age_tx || 0));
33787
+ if (severity === "OK") {
33788
+ return { status: "ok", message: `Xmin horizons healthy (oldest holder: ${age} transactions)` };
33789
+ }
33790
+ const holder = data.dominant_holder?.source ? `; dominant holder: ${data.dominant_holder.source}` : "";
33791
+ const activityAgeSeconds = Number(data.components?.pg_stat_activity?.top_blocker?.xact_age_seconds || 0);
33792
+ if (severity === "NOTICE" && data.data_horizon_severity === "OK" && data.catalog_horizon_severity === "OK" && activityAgeSeconds > 0) {
33793
+ return {
33794
+ status: "info",
33795
+ message: `NOTICE: transaction open for ${activityAgeSeconds} seconds${holder}`
33796
+ };
33797
+ }
33798
+ return {
33799
+ status: severity === "NOTICE" ? "info" : "warning",
33800
+ message: `${severity}: xmin horizon age ${age} transactions${holder}`
33801
+ };
33802
+ }
33803
+ function summarizeBloat(nodeData, kind) {
33804
+ const data = nodeData?.data || {};
33805
+ let totalCount = 0;
33806
+ for (const dbData of Object.values(data)) {
33807
+ const dbEntry = dbData;
33808
+ if (dbEntry?.status?.ok === false) {
33809
+ const reason = String(dbEntry.status.reason || "query_error").replaceAll("_", " ");
33810
+ return { status: "warning", message: `Bloat estimate degraded: ${reason}` };
33811
+ }
33812
+ totalCount += dbEntry?.total_count || 0;
33813
+ }
33814
+ if (totalCount === 0) {
33815
+ return { status: "ok", message: `No bloated ${kind}${kind === "index" ? "es" : "s"} found` };
33816
+ }
33817
+ return {
33818
+ status: "warning",
33819
+ message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
33820
+ };
33821
+ }
33822
+ function summarizeG001(nodeData) {
33823
+ const data = nodeData?.data || {};
33824
+ const settingsCount = Object.keys(data).length;
33825
+ if (settingsCount === 0) {
33826
+ return { status: "info", message: "No memory settings found" };
33827
+ }
33828
+ return {
33829
+ status: "info",
33830
+ message: `${settingsCount} memory setting${settingsCount > 1 ? "s" : ""} collected`
33831
+ };
33832
+ }
33833
+ function summarizeG003(nodeData) {
33834
+ const data = nodeData?.data || {};
33835
+ const settings = data.settings || {};
33836
+ const deadlockStats = data.deadlock_stats;
33837
+ const settingsCount = Object.keys(settings).length;
33838
+ if (deadlockStats && typeof deadlockStats.deadlocks === "number" && deadlockStats.deadlocks > 0) {
33839
+ return {
33840
+ status: "warning",
33841
+ message: `${deadlockStats.deadlocks} deadlock${deadlockStats.deadlocks > 1 ? "s" : ""} detected`
33842
+ };
33843
+ }
33844
+ if (settingsCount === 0) {
33845
+ return { status: "info", message: "No timeout/lock settings found" };
33846
+ }
33847
+ return {
33848
+ status: "info",
33849
+ message: `${settingsCount} timeout/lock setting${settingsCount > 1 ? "s" : ""} collected`
33850
+ };
32027
33851
  }
32028
33852
 
32029
33853
  // lib/checkup.ts
32030
33854
  var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
33855
+ var CONTRACT_VERSION = "1.0.0";
32031
33856
  var SECONDS_PER_DAY = 86400;
32032
33857
  var SECONDS_PER_HOUR = 3600;
32033
33858
  var SECONDS_PER_MINUTE = 60;
32034
33859
  function toBool(val) {
32035
33860
  return val === true || val === 1 || val === "t" || val === "true";
32036
33861
  }
33862
+ var F002_WRAPAROUND_LIMIT = 2147483648;
33863
+ var F002_CRITICAL_AGE = 1e9;
33864
+ var F002_FAILSAFE_HIGH_PCT = 80;
33865
+ var severityRank = {
33866
+ info: 0,
33867
+ warning: 1,
33868
+ high: 2,
33869
+ critical: 3
33870
+ };
33871
+ function evaluateWraparoundRisk(age, emergencyAge, failsafeAge) {
33872
+ let severity = "info";
33873
+ if (emergencyAge <= 0) {
33874
+ severity = "info";
33875
+ } else if (age >= F002_CRITICAL_AGE) {
33876
+ severity = "critical";
33877
+ } else if (age >= 2 * emergencyAge || failsafeAge !== null && age >= failsafeAge * (F002_FAILSAFE_HIGH_PCT / 100)) {
33878
+ severity = "high";
33879
+ } else if (age >= emergencyAge) {
33880
+ severity = "warning";
33881
+ }
33882
+ const pct = (value, limit) => limit > 0 ? Math.round(value / limit * 1e4) / 100 : 0;
33883
+ return {
33884
+ age,
33885
+ emergency_age: emergencyAge,
33886
+ failsafe_age: failsafeAge,
33887
+ pct_towards_wraparound: pct(age, F002_WRAPAROUND_LIMIT),
33888
+ pct_towards_emergency: pct(age, emergencyAge),
33889
+ pct_towards_failsafe: failsafeAge === null ? null : pct(age, failsafeAge),
33890
+ severity
33891
+ };
33892
+ }
33893
+ function maxSeverity(...severities) {
33894
+ return severities.reduce((max, value) => severityRank[value] > severityRank[max] ? value : max, "info");
33895
+ }
32037
33896
  var F003_DEAD_TUPLES_MIN = 1e5;
32038
33897
  var F003_DEAD_PCT_MIN = 20;
32039
33898
  var F003_AUTOVACUUM_DISABLED_MIN_ROWS = 1e4;
@@ -32053,11 +33912,11 @@ function parseVersionNum(versionNum) {
32053
33912
  return { major: "", minor: "" };
32054
33913
  }
32055
33914
  }
32056
- function formatBytes(bytes) {
33915
+ function formatBytes2(bytes) {
32057
33916
  if (bytes === 0)
32058
33917
  return "0 B";
32059
33918
  if (bytes < 0)
32060
- return `-${formatBytes(-bytes)}`;
33919
+ return `-${formatBytes2(-bytes)}`;
32061
33920
  if (!Number.isFinite(bytes))
32062
33921
  return `${bytes} B`;
32063
33922
  const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
@@ -32069,7 +33928,7 @@ function formatSettingPrettyValue(settingNormalized, unitNormalized, rawValue) {
32069
33928
  return rawValue;
32070
33929
  }
32071
33930
  if (unitNormalized === "bytes") {
32072
- return formatBytes(settingNormalized);
33931
+ return formatBytes2(settingNormalized);
32073
33932
  }
32074
33933
  if (unitNormalized === "seconds") {
32075
33934
  const MS_PER_SECOND = 1000;
@@ -32243,7 +34102,7 @@ async function getClusterInfo(client, pgMajorVersion = 16) {
32243
34102
  };
32244
34103
  const tempBytes = parseInt(stats.temp_bytes || "0", 10);
32245
34104
  info.temp_bytes_written = {
32246
- value: formatBytes(tempBytes),
34105
+ value: formatBytes2(tempBytes),
32247
34106
  unit: "bytes",
32248
34107
  description: "Total temporary file bytes written"
32249
34108
  };
@@ -32309,7 +34168,7 @@ async function getInvalidIndexes(client, pgMajorVersion = 16) {
32309
34168
  index_name: String(transformed.index_name || ""),
32310
34169
  relation_name: String(transformed.relation_name || ""),
32311
34170
  index_size_bytes: indexSizeBytes,
32312
- index_size_pretty: formatBytes(indexSizeBytes),
34171
+ index_size_pretty: formatBytes2(indexSizeBytes),
32313
34172
  index_definition: String(transformed.index_definition || ""),
32314
34173
  supports_fk: toBool(transformed.supports_fk),
32315
34174
  is_pk: toBool(transformed.is_pk),
@@ -32338,7 +34197,7 @@ async function getUnusedIndexes(client, pgMajorVersion = 16) {
32338
34197
  index_size_bytes: indexSizeBytes,
32339
34198
  idx_is_btree: toBool(transformed.idx_is_btree),
32340
34199
  supports_fk: toBool(transformed.supports_fk),
32341
- index_size_pretty: formatBytes(indexSizeBytes)
34200
+ index_size_pretty: formatBytes2(indexSizeBytes)
32342
34201
  };
32343
34202
  });
32344
34203
  }
@@ -32411,7 +34270,7 @@ async function getRedundantIndexes(client, pgMajorVersion = 16) {
32411
34270
  index_name: String(item.index_name ?? ""),
32412
34271
  index_definition: String(item.index_definition ?? ""),
32413
34272
  index_size_bytes: sizeBytes,
32414
- index_size_pretty: formatBytes(sizeBytes)
34273
+ index_size_pretty: formatBytes2(sizeBytes)
32415
34274
  };
32416
34275
  });
32417
34276
  }
@@ -32433,8 +34292,8 @@ async function getRedundantIndexes(client, pgMajorVersion = 16) {
32433
34292
  index_usage: parseInt(String(transformed.index_usage || 0), 10),
32434
34293
  supports_fk: toBool(transformed.supports_fk),
32435
34294
  index_definition: String(transformed.index_definition || ""),
32436
- index_size_pretty: formatBytes(indexSizeBytes),
32437
- table_size_pretty: formatBytes(tableSizeBytes),
34295
+ index_size_pretty: formatBytes2(indexSizeBytes),
34296
+ table_size_pretty: formatBytes2(tableSizeBytes),
32438
34297
  redundant_to: redundantTo
32439
34298
  };
32440
34299
  if (parseError) {
@@ -32469,12 +34328,103 @@ async function getDeadTuples(client, pgMajorVersion = 16) {
32469
34328
  vacuum_count: parseInt(String(t.vacuum_count || 0), 10),
32470
34329
  autovacuum_disabled: autovacuumDisabled,
32471
34330
  table_size_bytes: tableSizeBytes,
32472
- table_size_pretty: formatBytes(tableSizeBytes),
34331
+ table_size_pretty: formatBytes2(tableSizeBytes),
32473
34332
  exceeds_dead_tuple_thresholds: nDead >= F003_DEAD_TUPLES_MIN && deadPct >= F003_DEAD_PCT_MIN,
32474
34333
  autovacuum_disabled_flagged: autovacuumDisabled && nLive + nDead >= F003_AUTOVACUUM_DISABLED_MIN_ROWS
32475
34334
  };
32476
34335
  });
32477
34336
  }
34337
+ async function getWraparoundData(client, pgMajorVersion = 16) {
34338
+ const [settingsResult, databaseResult, tableResult, multixactSizeResult] = await Promise.all([
34339
+ client.query(getMetricSql(METRIC_NAMES.F002Settings, pgMajorVersion)),
34340
+ client.query(getMetricSql(METRIC_NAMES.F002Database, pgMajorVersion)),
34341
+ client.query(getMetricSql(METRIC_NAMES.F002Tables, pgMajorVersion)),
34342
+ client.query(getMetricSql(METRIC_NAMES.F002MultixactSize, pgMajorVersion))
34343
+ ]);
34344
+ const numberValue = (value) => parseInt(String(value ?? 0), 10) || 0;
34345
+ const rawSettings = settingsResult.rows[0] || {};
34346
+ const failsafe3 = pgMajorVersion >= 14 ? numberValue(rawSettings.vacuum_failsafe_age) : 0;
34347
+ const multixactFailsafe = pgMajorVersion >= 14 ? numberValue(rawSettings.vacuum_multixact_failsafe_age) : 0;
34348
+ const settings = {
34349
+ autovacuum_freeze_max_age: numberValue(rawSettings.autovacuum_freeze_max_age),
34350
+ vacuum_freeze_min_age: numberValue(rawSettings.vacuum_freeze_min_age),
34351
+ vacuum_freeze_table_age: numberValue(rawSettings.vacuum_freeze_table_age),
34352
+ autovacuum_multixact_freeze_max_age: numberValue(rawSettings.autovacuum_multixact_freeze_max_age),
34353
+ vacuum_multixact_freeze_min_age: numberValue(rawSettings.vacuum_multixact_freeze_min_age),
34354
+ vacuum_multixact_freeze_table_age: numberValue(rawSettings.vacuum_multixact_freeze_table_age),
34355
+ vacuum_failsafe_age: failsafe3 > 0 ? failsafe3 : null,
34356
+ vacuum_multixact_failsafe_age: multixactFailsafe > 0 ? multixactFailsafe : null
34357
+ };
34358
+ const settings_available = settings.autovacuum_freeze_max_age > 0 && settings.autovacuum_multixact_freeze_max_age > 0;
34359
+ const databases = databaseResult.rows.map((row) => {
34360
+ const xidAge = numberValue(row.age_datfrozenxid);
34361
+ const multixactAge = numberValue(row.age_datminmxid);
34362
+ return {
34363
+ database_name: String(row.tag_datname || ""),
34364
+ xid: evaluateWraparoundRisk(xidAge, settings.autovacuum_freeze_max_age, settings.vacuum_failsafe_age),
34365
+ multixact: evaluateWraparoundRisk(multixactAge, settings.autovacuum_multixact_freeze_max_age, settings.vacuum_multixact_failsafe_age)
34366
+ };
34367
+ }).sort((a, b) => Math.max(b.xid.age, b.multixact.age) - Math.max(a.xid.age, a.multixact.age));
34368
+ const tables = tableResult.rows.map((row) => {
34369
+ const xidAge = numberValue(row.xid_age);
34370
+ const multixactAge = numberValue(row.multixact_age);
34371
+ const tableSizeBytes = numberValue(row.table_size_bytes);
34372
+ return {
34373
+ database_name: String(row.tag_datname || ""),
34374
+ schema_name: String(row.tag_schema_name || ""),
34375
+ table_name: String(row.tag_table_name || ""),
34376
+ ranked_by: String(row.tag_ranked_by || "").split(",").filter(Boolean),
34377
+ table_size_bytes: tableSizeBytes,
34378
+ table_size_pretty: formatBytes2(tableSizeBytes),
34379
+ xid: evaluateWraparoundRisk(xidAge, numberValue(row.effective_freeze_max_age) || settings.autovacuum_freeze_max_age, settings.vacuum_failsafe_age),
34380
+ multixact: evaluateWraparoundRisk(multixactAge, numberValue(row.effective_multixact_freeze_max_age) || settings.autovacuum_multixact_freeze_max_age, settings.vacuum_multixact_failsafe_age)
34381
+ };
34382
+ }).sort((a, b) => Math.max(b.xid.age, b.multixact.age) - Math.max(a.xid.age, a.multixact.age) || a.database_name.localeCompare(b.database_name) || a.schema_name.localeCompare(b.schema_name) || a.table_name.localeCompare(b.table_name));
34383
+ const rawMultixactSize = multixactSizeResult.rows[0] || {};
34384
+ const multixactBytes = rawMultixactSize.members_bytes === null || rawMultixactSize.members_bytes === undefined || rawMultixactSize.offsets_bytes === null || rawMultixactSize.offsets_bytes === undefined ? null : numberValue(rawMultixactSize.members_bytes) + numberValue(rawMultixactSize.offsets_bytes);
34385
+ const multixact_size = {
34386
+ bytes: multixactBytes,
34387
+ size_pretty: multixactBytes === null ? null : formatBytes2(multixactBytes),
34388
+ status_code: numberValue(rawMultixactSize.status_code)
34389
+ };
34390
+ return { settings, databases, tables, multixact_size, settings_available };
34391
+ }
34392
+ function buildWraparoundConclusions(databases, tables, settingsAvailable = true) {
34393
+ if (!settingsAvailable) {
34394
+ return {
34395
+ severity: "info",
34396
+ conclusions: ["Wraparound settings are unavailable; severity could not be evaluated."],
34397
+ recommendations: ["Verify pg_settings_wraparound collection and rerun F002 before assessing wraparound risk."]
34398
+ };
34399
+ }
34400
+ const offenders = [
34401
+ ...databases.flatMap((db) => [
34402
+ { name: `database "${db.database_name}"`, kind: "transaction ID", risk: db.xid },
34403
+ { name: `database "${db.database_name}"`, kind: "MultiXact", risk: db.multixact }
34404
+ ]),
34405
+ ...tables.flatMap((table) => [
34406
+ { name: `table "${table.database_name}"."${table.schema_name}"."${table.table_name}"`, kind: "transaction ID", risk: table.xid },
34407
+ { name: `table "${table.database_name}"."${table.schema_name}"."${table.table_name}"`, kind: "MultiXact", risk: table.multixact }
34408
+ ])
34409
+ ].filter((item) => item.risk.severity !== "info").sort((a, b) => severityRank[b.risk.severity] - severityRank[a.risk.severity] || b.risk.age - a.risk.age);
34410
+ const severity = maxSeverity(...offenders.map((item) => item.risk.severity));
34411
+ if (offenders.length === 0) {
34412
+ return {
34413
+ severity,
34414
+ conclusions: ["Transaction ID and MultiXact ages are below their emergency vacuum thresholds."],
34415
+ recommendations: []
34416
+ };
34417
+ }
34418
+ const conclusions = offenders.slice(0, 10).map((item) => `${item.name} has ${item.kind} age ${item.risk.age.toLocaleString("en-US")} ` + `(${item.risk.pct_towards_emergency}% of its emergency vacuum threshold; ${item.risk.severity}).`);
34419
+ const recommendations = [
34420
+ "Check pg_stat_activity for 'autovacuum: %to prevent wraparound%' workers and inspect pg_stat_progress_vacuum for progress.",
34421
+ "Check xmin-horizon holders (long transactions, stale replication slots, and prepared transactions) and the F003 autovacuum queue analysis; high age is usually a symptom of vacuum starvation or a blocked horizon."
34422
+ ];
34423
+ if (severity === "high" || severity === "critical") {
34424
+ recommendations.push("Run VACUUM (FREEZE, VERBOSE) on the highest-age offender tables after confirming operational impact. Do not raise freeze_max_age merely to silence the check; that reduces the remaining safety margin.");
34425
+ }
34426
+ return { severity, conclusions, recommendations };
34427
+ }
32478
34428
  function buildDeadTuplesConclusions(tables) {
32479
34429
  const conclusions = [];
32480
34430
  const recommendations = [];
@@ -32498,6 +34448,7 @@ function buildDeadTuplesConclusions(tables) {
32498
34448
  function createBaseReport(checkId, checkTitle, nodeName) {
32499
34449
  const buildTs = resolveBuildTs();
32500
34450
  return {
34451
+ contract_version: CONTRACT_VERSION,
32501
34452
  version: version || null,
32502
34453
  build_ts: buildTs,
32503
34454
  generation_mode: "express",
@@ -32572,9 +34523,9 @@ async function generateIndexReport(client, nodeName, checkId, checkTitle, indexF
32572
34523
  [indexFieldName]: indexes,
32573
34524
  total_count: totalCount,
32574
34525
  total_size_bytes: totalSizeBytes,
32575
- total_size_pretty: formatBytes(totalSizeBytes),
34526
+ total_size_pretty: formatBytes2(totalSizeBytes),
32576
34527
  database_size_bytes: dbSizeBytes,
32577
- database_size_pretty: formatBytes(dbSizeBytes)
34528
+ database_size_pretty: formatBytes2(dbSizeBytes)
32578
34529
  };
32579
34530
  if (extraFields) {
32580
34531
  Object.assign(dbEntry, await extraFields(client, pgMajorVersion));
@@ -32758,6 +34709,33 @@ async function generateF001(client, nodeName) {
32758
34709
  };
32759
34710
  return report;
32760
34711
  }
34712
+ async function generateF002(client, nodeName) {
34713
+ const report = createBaseReport("F002", "Autovacuum: transaction ID and MultiXact wraparound", nodeName);
34714
+ const postgresVersion = await getPostgresVersion(client);
34715
+ const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
34716
+ const { settings, databases, tables, multixact_size, settings_available } = await getWraparoundData(client, pgMajorVersion);
34717
+ const { severity, conclusions, recommendations } = buildWraparoundConclusions(databases, tables, settings_available);
34718
+ report.results[nodeName] = {
34719
+ data: {
34720
+ settings,
34721
+ databases,
34722
+ tables,
34723
+ multixact_size,
34724
+ settings_available,
34725
+ severity,
34726
+ thresholds: {
34727
+ wraparound_limit: F002_WRAPAROUND_LIMIT,
34728
+ critical_age: F002_CRITICAL_AGE,
34729
+ failsafe_high_pct: F002_FAILSAFE_HIGH_PCT,
34730
+ table_limit_per_age: 50
34731
+ },
34732
+ conclusions,
34733
+ recommendations
34734
+ },
34735
+ postgres_version: postgresVersion
34736
+ };
34737
+ return report;
34738
+ }
32761
34739
  async function generateF003(client, nodeName) {
32762
34740
  const report = createBaseReport("F003", "Autovacuum: dead tuples", nodeName);
32763
34741
  const postgresVersion = await getPostgresVersion(client);
@@ -32784,7 +34762,7 @@ async function generateF003(client, nodeName) {
32784
34762
  conclusions,
32785
34763
  recommendations,
32786
34764
  database_size_bytes: dbSizeBytes,
32787
- database_size_pretty: formatBytes(dbSizeBytes)
34765
+ database_size_pretty: formatBytes2(dbSizeBytes)
32788
34766
  };
32789
34767
  report.results[nodeName] = {
32790
34768
  data: { [dbName]: dbEntry },
@@ -32792,6 +34770,185 @@ async function generateF003(client, nodeName) {
32792
34770
  };
32793
34771
  return report;
32794
34772
  }
34773
+ var F009_NOTICE_FRACTION = 0.1;
34774
+ var F009_WARNING_FRACTION = 0.5;
34775
+ var F009_CRITICAL_FRACTION = 0.8;
34776
+ var F009_ACTIVITY_NOTICE_SECONDS = SECONDS_PER_HOUR;
34777
+ var F009_SEVERITY_RANK = {
34778
+ OK: 0,
34779
+ NOTICE: 1,
34780
+ WARNING: 2,
34781
+ CRITICAL: 3
34782
+ };
34783
+ function numberValue(value) {
34784
+ const parsed = Number(value ?? 0);
34785
+ return Number.isFinite(parsed) ? parsed : 0;
34786
+ }
34787
+ function parseJsonObject(value) {
34788
+ if (value && typeof value === "object")
34789
+ return value;
34790
+ if (typeof value === "string") {
34791
+ try {
34792
+ const parsed = JSON.parse(value);
34793
+ if (parsed && typeof parsed === "object")
34794
+ return parsed;
34795
+ } catch {}
34796
+ }
34797
+ return {};
34798
+ }
34799
+ function normalizeF009Components(value) {
34800
+ const raw = parseJsonObject(value);
34801
+ const component = (name) => {
34802
+ const item = parseJsonObject(raw[name]);
34803
+ const detail = item.top_blocker;
34804
+ return {
34805
+ age_tx: numberValue(item.age_tx),
34806
+ count: numberValue(item.count),
34807
+ top_blocker: detail && typeof detail === "object" ? detail : null
34808
+ };
34809
+ };
34810
+ return {
34811
+ pg_stat_activity: component("pg_stat_activity"),
34812
+ pg_replication_slots: component("pg_replication_slots"),
34813
+ pg_replication_slots_catalog: component("pg_replication_slots_catalog"),
34814
+ pg_stat_replication: component("pg_stat_replication"),
34815
+ pg_prepared_xacts: component("pg_prepared_xacts")
34816
+ };
34817
+ }
34818
+ function getF009Severity(ageTx, freezeMaxAge) {
34819
+ if (freezeMaxAge <= 0)
34820
+ return "OK";
34821
+ const fraction = ageTx / freezeMaxAge;
34822
+ if (fraction >= F009_CRITICAL_FRACTION)
34823
+ return "CRITICAL";
34824
+ if (fraction >= F009_WARNING_FRACTION)
34825
+ return "WARNING";
34826
+ if (fraction >= F009_NOTICE_FRACTION)
34827
+ return "NOTICE";
34828
+ return "OK";
34829
+ }
34830
+ function selectF009DominantHolder(components) {
34831
+ const candidates = [];
34832
+ const add = (source, component, horizonType, priority) => {
34833
+ const item = components[component];
34834
+ if (item.count > 0 && item.top_blocker) {
34835
+ candidates.push({ source, component, horizon_type: horizonType, age_tx: item.age_tx, detail: item.top_blocker, priority });
34836
+ }
34837
+ };
34838
+ add("slot", "pg_replication_slots", "data", 4);
34839
+ add("slot", "pg_replication_slots_catalog", "catalog", 4);
34840
+ add("prepared", "pg_prepared_xacts", "data", 3);
34841
+ add("activity", "pg_stat_activity", "data", 2);
34842
+ add("replication", "pg_stat_replication", "data", 1);
34843
+ candidates.sort((a, b) => b.age_tx - a.age_tx || b.priority - a.priority || a.component.localeCompare(b.component));
34844
+ if (candidates.length === 0)
34845
+ return null;
34846
+ const { priority: _priority, ...dominant } = candidates[0];
34847
+ return dominant;
34848
+ }
34849
+ function sqlLiteral(value) {
34850
+ return String(value ?? "").replace(/'/g, "''");
34851
+ }
34852
+ function buildF009Analysis(components, dominantHolder, dataSeverity, catalogSeverity, options) {
34853
+ const conclusions = [];
34854
+ const recommendations = [];
34855
+ if (options.skipped) {
34856
+ return { conclusions: ["INFO: xmin horizon analysis is primary-only; this replica was skipped."], recommendations };
34857
+ }
34858
+ if (options.limitedVisibility) {
34859
+ conclusions.push("NOTICE: the check ran with limited visibility; sessions owned by other users may hide query text or xmin details. Grant pg_monitor or pg_read_all_stats for complete RCA context.");
34860
+ }
34861
+ if (options.historyDetected) {
34862
+ conclusions.push("INFO: pg_flight_recorder detected — historical xmin timeline is available via pgfr_analyze.xmin_horizon_history().");
34863
+ }
34864
+ const dataAge = Math.max(components.pg_stat_activity.age_tx, components.pg_replication_slots.age_tx, components.pg_stat_replication.age_tx, components.pg_prepared_xacts.age_tx);
34865
+ const catalogAge = components.pg_replication_slots_catalog.age_tx;
34866
+ if (dataSeverity !== "OK")
34867
+ conclusions.push(`${dataSeverity}: data xmin horizon age is ${dataAge} transactions.`);
34868
+ if (catalogSeverity !== "OK")
34869
+ conclusions.push(`${catalogSeverity}: catalog xmin horizon age is ${catalogAge} transactions.`);
34870
+ const activity = components.pg_stat_activity.top_blocker;
34871
+ const xactAgeSeconds = numberValue(activity?.xact_age_seconds);
34872
+ if (activity && xactAgeSeconds >= F009_ACTIVITY_NOTICE_SECONDS) {
34873
+ conclusions.push(`NOTICE: activity PID ${activity.pid} has held a transaction open for ${xactAgeSeconds} seconds.`);
34874
+ }
34875
+ if (!dominantHolder) {
34876
+ if (conclusions.length === 0)
34877
+ conclusions.push("OK: no xmin horizon blocker is currently visible.");
34878
+ return { conclusions, recommendations };
34879
+ }
34880
+ const d = dominantHolder.detail;
34881
+ conclusions.unshift(`${dataSeverity === "OK" && catalogSeverity === "OK" ? "INFO" : "CAUSE"}: dominant xmin holder is ${dominantHolder.source} (${dominantHolder.age_tx} transactions, ${dominantHolder.horizon_type} horizon).`);
34882
+ if (dominantHolder.source === "activity") {
34883
+ const pid = numberValue(d.pid);
34884
+ const backendType = String(d.backend_type || "").toLowerCase();
34885
+ const state = String(d.state || "").toLowerCase();
34886
+ if (backendType.includes("autovacuum")) {
34887
+ recommendations.push(`Inspect pg_stat_progress_vacuum for autovacuum worker PID ${pid}; do not kill it until its progress and table size are understood.`);
34888
+ } else if (state.includes("idle in transaction")) {
34889
+ recommendations.push(`Terminate the idle-in-transaction holder with SELECT pg_terminate_backend(${pid}); pg_cancel_backend() is a no-op while it is idle. Prevent recurrence with idle_in_transaction_session_timeout.`);
34890
+ } else {
34891
+ recommendations.push(`Cancel the active statement first with SELECT pg_cancel_backend(${pid}); if backend_xmin reappears, the query is inside an explicit BEGIN and SELECT pg_terminate_backend(${pid}) may be required. Prevent recurrence with statement_timeout and, on PostgreSQL 17+, transaction_timeout.`);
34892
+ }
34893
+ } else if (dominantHolder.source === "slot") {
34894
+ const slotName = sqlLiteral(d.slot_name);
34895
+ recommendations.push(`Confirm subscriber state for replication slot '${slotName}' before intervening. Then advance it with pg_replication_slot_advance() or, only if obsolete, SELECT pg_drop_replication_slot('${slotName}'). Slot status=${d.status || "unknown"}, wal_status=${d.wal_status || "unknown"}.`);
34896
+ } else if (dominantHolder.source === "replication") {
34897
+ recommendations.push(`On standby '${d.application_name || "unknown"}', find and cancel the long query responsible for hot_standby_feedback. Do not disable hot_standby_feedback as a first response; that trades xmin holdback for recovery-conflict cancellations.`);
34898
+ } else {
34899
+ const gid = sqlLiteral(d.gid);
34900
+ recommendations.push(`After confirming the transaction outcome, resolve the prepared transaction with ROLLBACK PREPARED '${gid}'. Owner=${d.owner || "unknown"}, database=${d.database || "unknown"}, prepared_at=${d.prepared_at || "unknown"}.`);
34901
+ }
34902
+ return { conclusions, recommendations };
34903
+ }
34904
+ async function generateF009(client, nodeName) {
34905
+ const report = createBaseReport("F009", "Xmin horizon and blockers", nodeName);
34906
+ const postgresVersion = await getPostgresVersion(client);
34907
+ const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
34908
+ const result = await client.query(getMetricSql(METRIC_NAMES.F009, pgMajorVersion));
34909
+ const row = result.rows[0] || {};
34910
+ const components = normalizeF009Components(row.components);
34911
+ const skipped = toBool(row.is_in_recovery);
34912
+ const freezeMaxAge = numberValue(row.autovacuum_freeze_max_age);
34913
+ const dataAge = numberValue(row.data_horizon_age_tx);
34914
+ const catalogAge = numberValue(row.catalog_horizon_age_tx);
34915
+ const dataSeverity = skipped ? "OK" : getF009Severity(dataAge, freezeMaxAge);
34916
+ const catalogSeverity = skipped ? "OK" : getF009Severity(catalogAge, freezeMaxAge);
34917
+ const activityAgeSeconds = numberValue(components.pg_stat_activity.top_blocker?.xact_age_seconds);
34918
+ let severity = F009_SEVERITY_RANK[dataSeverity] >= F009_SEVERITY_RANK[catalogSeverity] ? dataSeverity : catalogSeverity;
34919
+ if (severity === "OK" && activityAgeSeconds >= F009_ACTIVITY_NOTICE_SECONDS)
34920
+ severity = "NOTICE";
34921
+ const dominantHolder = skipped ? null : selectF009DominantHolder(components);
34922
+ const limitedVisibility = !toBool(row.has_full_visibility);
34923
+ const historyDetected = toBool(row.pg_flight_recorder_detected);
34924
+ const { conclusions, recommendations } = buildF009Analysis(components, dominantHolder, dataSeverity, catalogSeverity, { limitedVisibility, historyDetected, skipped });
34925
+ const data = {
34926
+ skipped,
34927
+ skip_reason: row.skip_reason || null,
34928
+ snapshot_xmin: row.snapshot_xmin === null || row.snapshot_xmin === undefined ? null : numberValue(row.snapshot_xmin),
34929
+ data_horizon_age_tx: dataAge,
34930
+ catalog_horizon_age_tx: catalogAge,
34931
+ autovacuum_freeze_max_age: freezeMaxAge,
34932
+ severity,
34933
+ data_horizon_severity: dataSeverity,
34934
+ catalog_horizon_severity: catalogSeverity,
34935
+ visibility: limitedVisibility ? "limited" : "full",
34936
+ query_preview_captured: toBool(row.query_preview_enabled) && !limitedVisibility,
34937
+ components,
34938
+ dominant_holder: dominantHolder,
34939
+ conclusions,
34940
+ recommendations
34941
+ };
34942
+ if (historyDetected) {
34943
+ data.history = {
34944
+ available: true,
34945
+ source: "pg_flight_recorder",
34946
+ message: "Historical xmin timeline is available via pgfr_analyze.xmin_horizon_history()."
34947
+ };
34948
+ }
34949
+ report.results[nodeName] = { data, postgres_version: postgresVersion };
34950
+ return report;
34951
+ }
32795
34952
  function bloatErrorStatus(err) {
32796
34953
  const error2 = err instanceof Error ? err.message : String(err);
32797
34954
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code || "") : "";
@@ -32893,9 +35050,9 @@ async function generateF004(client, nodeName) {
32893
35050
  fillfactor: parseInt(String(t.fillfactor || 100), 10),
32894
35051
  last_vacuum: vacuumInfo.last_vacuum,
32895
35052
  last_vacuum_epoch: vacuumInfo.last_vacuum_epoch,
32896
- real_size_pretty: formatBytes(realSizeBytes),
32897
- extra_size_pretty: formatBytes(extraSize),
32898
- bloat_size_pretty: formatBytes(bloatSize)
35053
+ real_size_pretty: formatBytes2(realSizeBytes),
35054
+ extra_size_pretty: formatBytes2(extraSize),
35055
+ bloat_size_pretty: formatBytes2(bloatSize)
32899
35056
  };
32900
35057
  });
32901
35058
  } catch (err) {
@@ -32914,9 +35071,9 @@ async function generateF004(client, nodeName) {
32914
35071
  bloated_tables: bloatedTables,
32915
35072
  total_count: totalCount,
32916
35073
  total_bloat_size_bytes: totalBloatSizeBytes,
32917
- total_bloat_size_pretty: formatBytes(totalBloatSizeBytes),
35074
+ total_bloat_size_pretty: formatBytes2(totalBloatSizeBytes),
32918
35075
  database_size_bytes: dbSizeBytes,
32919
- database_size_pretty: formatBytes(dbSizeBytes)
35076
+ database_size_pretty: formatBytes2(dbSizeBytes)
32920
35077
  };
32921
35078
  report.results[nodeName] = {
32922
35079
  data: { [dbName]: dbEntry },
@@ -32976,10 +35133,10 @@ async function generateF005(client, nodeName) {
32976
35133
  fillfactor: parseInt(String(t.fillfactor || 90), 10),
32977
35134
  last_vacuum: vacuumInfo.last_vacuum,
32978
35135
  last_vacuum_epoch: vacuumInfo.last_vacuum_epoch,
32979
- real_size_pretty: formatBytes(realSizeBytes),
32980
- table_size_pretty: formatBytes(tableSizeBytes),
32981
- extra_size_pretty: formatBytes(extraSize),
32982
- bloat_size_pretty: formatBytes(bloatSize)
35136
+ real_size_pretty: formatBytes2(realSizeBytes),
35137
+ table_size_pretty: formatBytes2(tableSizeBytes),
35138
+ extra_size_pretty: formatBytes2(extraSize),
35139
+ bloat_size_pretty: formatBytes2(bloatSize)
32983
35140
  };
32984
35141
  });
32985
35142
  } catch (err) {
@@ -32998,9 +35155,9 @@ async function generateF005(client, nodeName) {
32998
35155
  bloated_indexes: bloatedIndexes,
32999
35156
  total_count: totalCount,
33000
35157
  total_bloat_size_bytes: totalBloatSizeBytes,
33001
- total_bloat_size_pretty: formatBytes(totalBloatSizeBytes),
35158
+ total_bloat_size_pretty: formatBytes2(totalBloatSizeBytes),
33002
35159
  database_size_bytes: dbSizeBytes,
33003
- database_size_pretty: formatBytes(dbSizeBytes)
35160
+ database_size_pretty: formatBytes2(dbSizeBytes)
33004
35161
  };
33005
35162
  report.results[nodeName] = {
33006
35163
  data: { [dbName]: dbEntry },
@@ -33059,19 +35216,19 @@ async function generateG001(client, nodeName) {
33059
35216
  const maxWorkMemUsage = workMemBytes * maxConnections;
33060
35217
  memoryUsage = {
33061
35218
  shared_buffers_bytes: sharedBuffersBytes,
33062
- shared_buffers_pretty: formatBytes(sharedBuffersBytes),
35219
+ shared_buffers_pretty: formatBytes2(sharedBuffersBytes),
33063
35220
  wal_buffers_bytes: walBuffersBytes,
33064
- wal_buffers_pretty: formatBytes(walBuffersBytes),
35221
+ wal_buffers_pretty: formatBytes2(walBuffersBytes),
33065
35222
  shared_memory_total_bytes: sharedMemoryTotal,
33066
- shared_memory_total_pretty: formatBytes(sharedMemoryTotal),
35223
+ shared_memory_total_pretty: formatBytes2(sharedMemoryTotal),
33067
35224
  work_mem_per_connection_bytes: workMemBytes,
33068
- work_mem_per_connection_pretty: formatBytes(workMemBytes),
35225
+ work_mem_per_connection_pretty: formatBytes2(workMemBytes),
33069
35226
  max_work_mem_usage_bytes: maxWorkMemUsage,
33070
- max_work_mem_usage_pretty: formatBytes(maxWorkMemUsage),
35227
+ max_work_mem_usage_pretty: formatBytes2(maxWorkMemUsage),
33071
35228
  maintenance_work_mem_bytes: maintenanceWorkMemBytes,
33072
- maintenance_work_mem_pretty: formatBytes(maintenanceWorkMemBytes),
35229
+ maintenance_work_mem_pretty: formatBytes2(maintenanceWorkMemBytes),
33073
35230
  effective_cache_size_bytes: effectiveCacheSizeBytes,
33074
- effective_cache_size_pretty: formatBytes(effectiveCacheSizeBytes)
35231
+ effective_cache_size_pretty: formatBytes2(effectiveCacheSizeBytes)
33075
35232
  };
33076
35233
  }
33077
35234
  } catch (err) {
@@ -33292,9 +35449,11 @@ var REPORT_GENERATORS = {
33292
35449
  D001: generateD001,
33293
35450
  D004: generateD004,
33294
35451
  F001: generateF001,
35452
+ F002: generateF002,
33295
35453
  F003: generateF003,
33296
35454
  F004: generateF004,
33297
35455
  F005: generateF005,
35456
+ F009: generateF009,
33298
35457
  G001: generateG001,
33299
35458
  G003: generateG003,
33300
35459
  H001: generateH001,
@@ -33311,23 +35470,43 @@ var CHECK_INFO = (() => {
33311
35470
  }
33312
35471
  return filtered;
33313
35472
  })();
33314
- async function generateAllReports(client, nodeName = "node-01", onProgress) {
35473
+ async function generateAllReports(client, nodeName = "node-01", onProgress, onCheckError) {
33315
35474
  const reports = {};
35475
+ const failures = [];
33316
35476
  const entries = Object.entries(REPORT_GENERATORS);
33317
35477
  const total = entries.length;
33318
35478
  let index = 0;
33319
35479
  for (const [checkId, generator] of entries) {
33320
35480
  index += 1;
35481
+ const checkTitle = CHECK_INFO[checkId] || checkId;
33321
35482
  onProgress?.({
33322
35483
  checkId,
33323
- checkTitle: CHECK_INFO[checkId] || checkId,
35484
+ checkTitle,
33324
35485
  index,
33325
35486
  total
33326
35487
  });
33327
- reports[checkId] = await generator(client, nodeName);
35488
+ try {
35489
+ reports[checkId] = withCheckSummary(await generator(client, nodeName));
35490
+ } catch (err) {
35491
+ const failure = {
35492
+ checkId,
35493
+ checkTitle,
35494
+ error: err instanceof Error ? err : new Error(String(err))
35495
+ };
35496
+ failures.push(failure);
35497
+ onCheckError?.(failure);
35498
+ }
35499
+ }
35500
+ if (total > 0 && Object.keys(reports).length === 0) {
35501
+ const first = failures[0];
35502
+ throw new Error(`All ${total} checks failed — first error (${first.checkId}): ${first.error.message}`);
33328
35503
  }
33329
35504
  return reports;
33330
35505
  }
35506
+ function withCheckSummary(report) {
35507
+ report.summary = generateCheckSummary(report.checkId, report);
35508
+ return report;
35509
+ }
33331
35510
 
33332
35511
  // lib/checkup-dictionary.ts
33333
35512
  var dictionaryByCode2 = new Map(CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry]));
@@ -33483,6 +35662,7 @@ async function postRpc(params) {
33483
35662
  clearTimeout(timeoutId);
33484
35663
  timeoutId = null;
33485
35664
  }
35665
+ res.setEncoding("utf8");
33486
35666
  let data = "";
33487
35667
  res.on("data", (chunk) => data += chunk);
33488
35668
  res.on("end", () => {
@@ -33559,9 +35739,15 @@ async function verifyApiKey(params) {
33559
35739
  signal: controller.signal
33560
35740
  });
33561
35741
  await response.text().catch(() => "");
33562
- if (response.status === 401 || response.status === 403) {
35742
+ if (response.status === 401) {
33563
35743
  return { status: "invalid", statusCode: response.status };
33564
35744
  }
35745
+ if (response.status === 403) {
35746
+ return {
35747
+ status: "unknown",
35748
+ detail: "HTTP 403 from GET /checkup_reports — the token may still be allowed to upload (different permission surface)"
35749
+ };
35750
+ }
33565
35751
  if (response.ok) {
33566
35752
  return { status: "valid" };
33567
35753
  }
@@ -33636,7 +35822,7 @@ async function convertCheckupReportJsonToMarkdown(params) {
33636
35822
  }
33637
35823
 
33638
35824
  // lib/checkup-summary.ts
33639
- function generateCheckSummary(checkId, report) {
35825
+ function generateCheckSummary2(checkId, report) {
33640
35826
  const nodeIds = Object.keys(report.results || {});
33641
35827
  if (nodeIds.length === 0) {
33642
35828
  return { status: "info", message: "No data" };
@@ -33644,42 +35830,46 @@ function generateCheckSummary(checkId, report) {
33644
35830
  const nodeData = report.results[nodeIds[0]];
33645
35831
  switch (checkId) {
33646
35832
  case "H001":
33647
- return summarizeH001(nodeData);
35833
+ return summarizeH0012(nodeData);
33648
35834
  case "H002":
33649
- return summarizeH002(nodeData);
35835
+ return summarizeH0022(nodeData);
33650
35836
  case "H004":
33651
- return summarizeH004(nodeData);
35837
+ return summarizeH0042(nodeData);
33652
35838
  case "A002":
33653
- return summarizeA002(nodeData);
35839
+ return summarizeA0022(nodeData);
33654
35840
  case "A013":
33655
- return summarizeA013(nodeData);
35841
+ return summarizeA0132(nodeData);
33656
35842
  case "A003":
33657
- return summarizeA003(nodeData);
35843
+ return summarizeA0032(nodeData);
33658
35844
  case "A004":
33659
- return summarizeA004(nodeData);
35845
+ return summarizeA0042(nodeData);
33660
35846
  case "A007":
33661
- return summarizeA007(nodeData);
35847
+ return summarizeA0072(nodeData);
33662
35848
  case "D001":
33663
- return summarizeD001(nodeData);
35849
+ return summarizeD0012(nodeData);
33664
35850
  case "D004":
33665
- return summarizeD004(nodeData);
35851
+ return summarizeD0042(nodeData);
33666
35852
  case "F001":
33667
- return summarizeF001(nodeData);
35853
+ return summarizeF0012(nodeData);
35854
+ case "F002":
35855
+ return summarizeF0022(nodeData);
33668
35856
  case "F003":
33669
- return summarizeF003(nodeData);
35857
+ return summarizeF0032(nodeData);
33670
35858
  case "F004":
33671
- return summarizeBloat(nodeData, "table");
35859
+ return summarizeBloat2(nodeData, "table");
33672
35860
  case "F005":
33673
- return summarizeBloat(nodeData, "index");
35861
+ return summarizeBloat2(nodeData, "index");
35862
+ case "F009":
35863
+ return summarizeF0092(nodeData);
33674
35864
  case "G001":
33675
- return summarizeG001(nodeData);
35865
+ return summarizeG0012(nodeData);
33676
35866
  case "G003":
33677
- return summarizeG003(nodeData);
35867
+ return summarizeG0032(nodeData);
33678
35868
  default:
33679
35869
  return { status: "info", message: "Check completed" };
33680
35870
  }
33681
35871
  }
33682
- function summarizeA003(nodeData) {
35872
+ function summarizeA0032(nodeData) {
33683
35873
  const data = nodeData?.data || {};
33684
35874
  const settingsCount = Object.keys(data).length;
33685
35875
  if (settingsCount === 0) {
@@ -33690,7 +35880,7 @@ function summarizeA003(nodeData) {
33690
35880
  message: `${settingsCount} setting${settingsCount > 1 ? "s" : ""} collected`
33691
35881
  };
33692
35882
  }
33693
- function summarizeA004(nodeData) {
35883
+ function summarizeA0042(nodeData) {
33694
35884
  const data = nodeData?.data;
33695
35885
  if (!data) {
33696
35886
  return { status: "info", message: "Cluster information collected" };
@@ -33701,7 +35891,7 @@ function summarizeA004(nodeData) {
33701
35891
  }
33702
35892
  return { status: "info", message: "Cluster information collected" };
33703
35893
  }
33704
- function summarizeA007(nodeData) {
35894
+ function summarizeA0072(nodeData) {
33705
35895
  const data = nodeData?.data || {};
33706
35896
  const alteredCount = Object.keys(data).length;
33707
35897
  if (alteredCount === 0) {
@@ -33712,7 +35902,7 @@ function summarizeA007(nodeData) {
33712
35902
  message: `${alteredCount} setting${alteredCount > 1 ? "s" : ""} altered from defaults`
33713
35903
  };
33714
35904
  }
33715
- function formatBytes2(bytes) {
35905
+ function formatBytes3(bytes) {
33716
35906
  if (bytes === 0)
33717
35907
  return "0 B";
33718
35908
  if (bytes < 1024)
@@ -33723,7 +35913,7 @@ function formatBytes2(bytes) {
33723
35913
  return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
33724
35914
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
33725
35915
  }
33726
- function summarizeH001(nodeData) {
35916
+ function summarizeH0012(nodeData) {
33727
35917
  const data = nodeData?.data || {};
33728
35918
  let totalCount = 0;
33729
35919
  let totalSize = 0;
@@ -33737,10 +35927,10 @@ function summarizeH001(nodeData) {
33737
35927
  }
33738
35928
  return {
33739
35929
  status: "warning",
33740
- message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
35930
+ message: `Found ${totalCount} invalid index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
33741
35931
  };
33742
35932
  }
33743
- function summarizeH002(nodeData) {
35933
+ function summarizeH0022(nodeData) {
33744
35934
  const data = nodeData?.data || {};
33745
35935
  let totalCount = 0;
33746
35936
  let totalSize = 0;
@@ -33754,10 +35944,10 @@ function summarizeH002(nodeData) {
33754
35944
  }
33755
35945
  return {
33756
35946
  status: "warning",
33757
- message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
35947
+ message: `Found ${totalCount} unused index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
33758
35948
  };
33759
35949
  }
33760
- function summarizeH004(nodeData) {
35950
+ function summarizeH0042(nodeData) {
33761
35951
  const data = nodeData?.data || {};
33762
35952
  let totalCount = 0;
33763
35953
  let totalSize = 0;
@@ -33771,10 +35961,10 @@ function summarizeH004(nodeData) {
33771
35961
  }
33772
35962
  return {
33773
35963
  status: "warning",
33774
- message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes2(totalSize)})`
35964
+ message: `Found ${totalCount} redundant index${totalCount > 1 ? "es" : ""} (${formatBytes3(totalSize)})`
33775
35965
  };
33776
35966
  }
33777
- function summarizeA002(nodeData) {
35967
+ function summarizeA0022(nodeData) {
33778
35968
  const ver = nodeData?.data?.version;
33779
35969
  if (!ver) {
33780
35970
  return { status: "info", message: "Version checked" };
@@ -33791,7 +35981,7 @@ function summarizeA002(nodeData) {
33791
35981
  message: `PostgreSQL ${major} (consider upgrading)`
33792
35982
  };
33793
35983
  }
33794
- function summarizeA013(nodeData) {
35984
+ function summarizeA0132(nodeData) {
33795
35985
  const ver = nodeData?.data?.version;
33796
35986
  if (!ver) {
33797
35987
  return { status: "info", message: "Minor version checked" };
@@ -33802,7 +35992,7 @@ function summarizeA013(nodeData) {
33802
35992
  message: `Version ${current}`
33803
35993
  };
33804
35994
  }
33805
- function summarizeD001(nodeData) {
35995
+ function summarizeD0012(nodeData) {
33806
35996
  const data = nodeData?.data || {};
33807
35997
  const settingsCount = Object.keys(data).length;
33808
35998
  if (settingsCount === 0) {
@@ -33813,7 +36003,7 @@ function summarizeD001(nodeData) {
33813
36003
  message: `${settingsCount} logging setting${settingsCount > 1 ? "s" : ""} collected`
33814
36004
  };
33815
36005
  }
33816
- function summarizeD004(nodeData) {
36006
+ function summarizeD0042(nodeData) {
33817
36007
  const data = nodeData?.data || {};
33818
36008
  const settingsCount = Object.keys(data).length;
33819
36009
  if (settingsCount === 0) {
@@ -33824,7 +36014,7 @@ function summarizeD004(nodeData) {
33824
36014
  message: `${settingsCount} pg_stat_statements setting${settingsCount > 1 ? "s" : ""} collected`
33825
36015
  };
33826
36016
  }
33827
- function summarizeF001(nodeData) {
36017
+ function summarizeF0012(nodeData) {
33828
36018
  const data = nodeData?.data || {};
33829
36019
  const settingsCount = Object.keys(data).length;
33830
36020
  if (settingsCount === 0) {
@@ -33835,7 +36025,25 @@ function summarizeF001(nodeData) {
33835
36025
  message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
33836
36026
  };
33837
36027
  }
33838
- function summarizeF003(nodeData) {
36028
+ function summarizeF0022(nodeData) {
36029
+ if (nodeData?.data?.settings_available === false) {
36030
+ return { status: "info", message: "Wraparound settings unavailable" };
36031
+ }
36032
+ const severity = nodeData?.data?.severity;
36033
+ const databases = nodeData?.data?.databases?.length || 0;
36034
+ const tables = nodeData?.data?.tables || [];
36035
+ const offenders = tables.filter((table) => table?.xid?.severity !== "info" || table?.multixact?.severity !== "info").length;
36036
+ if (!severity)
36037
+ return { status: "info", message: "No wraparound data" };
36038
+ if (severity === "info") {
36039
+ return { status: "ok", message: `${databases} database${databases === 1 ? "" : "s"} below wraparound thresholds` };
36040
+ }
36041
+ return {
36042
+ status: "warning",
36043
+ message: `${severity.toUpperCase()} wraparound risk${offenders ? `; ${offenders} table${offenders === 1 ? "" : "s"} affected` : ""}`
36044
+ };
36045
+ }
36046
+ function summarizeF0032(nodeData) {
33839
36047
  const data = nodeData?.data || {};
33840
36048
  let flaggedCount = 0;
33841
36049
  let disabledCount = 0;
@@ -33856,7 +36064,30 @@ function summarizeF003(nodeData) {
33856
36064
  }
33857
36065
  return { status: "warning", message: parts.join(", ") };
33858
36066
  }
33859
- function summarizeBloat(nodeData, kind) {
36067
+ function summarizeF0092(nodeData) {
36068
+ const data = nodeData?.data || {};
36069
+ if (data.skipped) {
36070
+ return { status: "info", message: data.skip_reason || "Primary-only check skipped on replica" };
36071
+ }
36072
+ const severity = String(data.severity || "OK");
36073
+ const age = Math.max(Number(data.data_horizon_age_tx || 0), Number(data.catalog_horizon_age_tx || 0));
36074
+ if (severity === "OK") {
36075
+ return { status: "ok", message: `Xmin horizons healthy (oldest holder: ${age} transactions)` };
36076
+ }
36077
+ const holder = data.dominant_holder?.source ? `; dominant holder: ${data.dominant_holder.source}` : "";
36078
+ const activityAgeSeconds = Number(data.components?.pg_stat_activity?.top_blocker?.xact_age_seconds || 0);
36079
+ if (severity === "NOTICE" && data.data_horizon_severity === "OK" && data.catalog_horizon_severity === "OK" && activityAgeSeconds > 0) {
36080
+ return {
36081
+ status: "info",
36082
+ message: `NOTICE: transaction open for ${activityAgeSeconds} seconds${holder}`
36083
+ };
36084
+ }
36085
+ return {
36086
+ status: severity === "NOTICE" ? "info" : "warning",
36087
+ message: `${severity}: xmin horizon age ${age} transactions${holder}`
36088
+ };
36089
+ }
36090
+ function summarizeBloat2(nodeData, kind) {
33860
36091
  const data = nodeData?.data || {};
33861
36092
  let totalCount = 0;
33862
36093
  for (const dbData of Object.values(data)) {
@@ -33875,7 +36106,7 @@ function summarizeBloat(nodeData, kind) {
33875
36106
  message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
33876
36107
  };
33877
36108
  }
33878
- function summarizeG001(nodeData) {
36109
+ function summarizeG0012(nodeData) {
33879
36110
  const data = nodeData?.data || {};
33880
36111
  const settingsCount = Object.keys(data).length;
33881
36112
  if (settingsCount === 0) {
@@ -33886,7 +36117,7 @@ function summarizeG001(nodeData) {
33886
36117
  message: `${settingsCount} memory setting${settingsCount > 1 ? "s" : ""} collected`
33887
36118
  };
33888
36119
  }
33889
- function summarizeG003(nodeData) {
36120
+ function summarizeG0032(nodeData) {
33890
36121
  const data = nodeData?.data || {};
33891
36122
  const settings = data.settings || {};
33892
36123
  const deadlockStats = data.deadlock_stats;
@@ -34318,7 +36549,7 @@ Checkup report uploaded`);
34318
36549
  for (const item of summary.uploaded) {
34319
36550
  const report = reports[item.checkId];
34320
36551
  if (report) {
34321
- const { status, message } = generateCheckSummary(item.checkId, report);
36552
+ const { status, message } = generateCheckSummary2(item.checkId, report);
34322
36553
  const title = report.checkTitle || item.checkId;
34323
36554
  const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
34324
36555
  if (isSignificant) {
@@ -34354,6 +36585,14 @@ async function downloadText(url) {
34354
36585
  clearTimeout(timeout);
34355
36586
  }
34356
36587
  }
36588
+ function composeRefCandidates() {
36589
+ return [
36590
+ process.env.PGAI_PROJECT_REF,
36591
+ package_default.version,
36592
+ `v${package_default.version}`,
36593
+ "main"
36594
+ ].filter((v) => Boolean(v && v.trim()));
36595
+ }
34357
36596
  async function ensureDefaultMonitoringProject() {
34358
36597
  const projectDir = getDefaultMonitoringProjectDir();
34359
36598
  const composeFile = path7.resolve(projectDir, "docker-compose.yml");
@@ -34362,12 +36601,7 @@ async function ensureDefaultMonitoringProject() {
34362
36601
  fs9.mkdirSync(projectDir, { recursive: true, mode: 448 });
34363
36602
  }
34364
36603
  if (!fs9.existsSync(composeFile)) {
34365
- const refs = [
34366
- process.env.PGAI_PROJECT_REF,
34367
- package_default.version,
34368
- `v${package_default.version}`,
34369
- "main"
34370
- ].filter((v) => Boolean(v && v.trim()));
36604
+ const refs = composeRefCandidates();
34371
36605
  let lastErr;
34372
36606
  for (const ref of refs) {
34373
36607
  const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
@@ -34459,11 +36693,7 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
34459
36693
  const composeFile = path7.resolve(projectDir, "docker-compose.yml");
34460
36694
  if (!fs9.existsSync(composeFile))
34461
36695
  return false;
34462
- const refs = [
34463
- process.env.PGAI_PROJECT_REF,
34464
- package_default.version,
34465
- `v${package_default.version}`
34466
- ].filter((v) => Boolean(v && v.trim()));
36696
+ const refs = composeRefCandidates();
34467
36697
  const fetched = await fetchTargetCompose(refs);
34468
36698
  if (!fetched || !isValidComposeYaml(fetched)) {
34469
36699
  console.error(`\u26A0 Could not refresh docker-compose.yml to ${package_default.version} (no valid compose was retrieved).`);
@@ -34522,6 +36752,36 @@ function printResult(result, json3) {
34522
36752
  }
34523
36753
  var program2 = new Command;
34524
36754
  program2.name("postgres-ai").description("PostgresAI CLI").version(package_default.version).option("--api-key <key>", "API key (overrides PGAI_API_KEY)").option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)").option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)").option("--storage-base-url <url>", "Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)");
36755
+ program2.showHelpAfterError();
36756
+ program2.addHelpText("after", () => `
36757
+ \uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL}
36758
+ `);
36759
+ program2.hook("postAction", (_thisCommand, actionCommand) => {
36760
+ const name = actionCommand.name();
36761
+ if (name === "feedback" || name === "help")
36762
+ return;
36763
+ if (process.exitCode && process.exitCode !== 0)
36764
+ return;
36765
+ const opts = actionCommand.opts();
36766
+ maybeEmitFeedbackTip({
36767
+ isTTY: !!process.stdout.isTTY,
36768
+ json: !!opts.json,
36769
+ suppress: process.env[FEEDBACK_SUPPRESS_ENV]
36770
+ });
36771
+ });
36772
+ program2.command("feedback").description("share ideas or feedback about the CLI").option("--open", "open the feedback page in your browser", false).option("--json", "output the feedback URL as JSON (machine-readable)", false).action(async (opts) => {
36773
+ if (opts.json) {
36774
+ console.log(feedbackJson());
36775
+ return;
36776
+ }
36777
+ console.log(feedbackMessage());
36778
+ if (opts.open) {
36779
+ const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
36780
+ try {
36781
+ spawn2(openCommand, [FEEDBACK_URL], { detached: true, stdio: "ignore" }).unref();
36782
+ } catch {}
36783
+ }
36784
+ });
34525
36785
  program2.command("set-default-project <project>").description("store default project for checkup uploads").action(async (project) => {
34526
36786
  const value = (project || "").trim();
34527
36787
  if (!value) {
@@ -35579,9 +37839,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
35579
37839
  return;
35580
37840
  }
35581
37841
  let reports;
37842
+ const failedChecks = [];
35582
37843
  if (checkId === "ALL") {
35583
37844
  reports = await generateAllReports(client, opts.nodeName, (p) => {
35584
37845
  spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
37846
+ }, (failure) => {
37847
+ failedChecks.push(failure);
37848
+ console.error(`Warning: check ${failure.checkId} (${failure.checkTitle}) failed: ${failure.error.message} \u2014 continuing with remaining checks`);
35585
37849
  });
35586
37850
  } else {
35587
37851
  const generator = REPORT_GENERATORS[checkId];
@@ -35599,7 +37863,7 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
35599
37863
  return;
35600
37864
  }
35601
37865
  spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
35602
- reports = { [checkId]: await generator(client, opts.nodeName) };
37866
+ reports = { [checkId]: withCheckSummary(await generator(client, opts.nodeName)) };
35603
37867
  }
35604
37868
  let uploadSummary;
35605
37869
  if (uploadCfg) {
@@ -35686,7 +37950,7 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
35686
37950
  const summaries = [];
35687
37951
  let skippedCount = 0;
35688
37952
  for (const [checkId2, report] of Object.entries(reports)) {
35689
- const { status, message } = generateCheckSummary(checkId2, report);
37953
+ const { status, message } = generateCheckSummary2(checkId2, report);
35690
37954
  const title = report.checkTitle || checkId2;
35691
37955
  const isSignificant = status !== "info" || /\d/.test(message) || message.includes("PostgreSQL") || message.includes("Version");
35692
37956
  if (isSignificant) {
@@ -35709,6 +37973,11 @@ For details:`);
35709
37973
  }
35710
37974
  console.log(" --output <dir> Save to directory");
35711
37975
  }
37976
+ if (failedChecks.length > 0) {
37977
+ const ids = failedChecks.map((f) => f.checkId).join(", ");
37978
+ console.error(`Warning: ${failedChecks.length} of ${failedChecks.length + Object.keys(reports).length} checks failed (${ids}); results are partial`);
37979
+ process.exitCode = 1;
37980
+ }
35712
37981
  } catch (error2) {
35713
37982
  if (error2 instanceof RpcError) {
35714
37983
  for (const line of formatRpcErrorForDisplay(error2)) {
@@ -35971,7 +38240,7 @@ async function runCompose(args, grafanaPassword) {
35971
38240
  child.on("close", (code) => resolve8(code || 0));
35972
38241
  });
35973
38242
  }
35974
- program2.command("help", { isDefault: true }).description("show help").action(() => {
38243
+ program2.command("help").description("show help").action(() => {
35975
38244
  program2.outputHelp();
35976
38245
  });
35977
38246
  var mon = program2.command("mon").description("monitoring services management");
@@ -36372,19 +38641,24 @@ Searched: ${demoCandidates.join(", ")}
36372
38641
  } else {
36373
38642
  console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
36374
38643
  }
36375
- const aas = await registerAasCollection(apiKey, instanceId, {
36376
- grafanaPassword,
36377
- instancesPath,
36378
- vcpus: parseVcpus(opts.vcpus ?? process.env.PGAI_VCPUS),
36379
- apiBaseUrl: globalOpts.apiBaseUrl,
36380
- debug: !!process.env.DEBUG
36381
- });
36382
- if (aas.ok) {
36383
- console.log(`\u2713 AAS auto-collection registered
38644
+ if (reg === null) {
38645
+ console.error(`\u26A0 Skipping AAS auto-collection arming because instance adoption failed; it can be enabled later by re-running 'postgresai mon local-install'
36384
38646
  `);
36385
38647
  } else {
36386
- console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
38648
+ const aas = await registerAasCollection(apiKey, instanceId, {
38649
+ grafanaPassword,
38650
+ instancesPath,
38651
+ vcpus: parseVcpus(opts.vcpus ?? process.env.PGAI_VCPUS),
38652
+ apiBaseUrl: globalOpts.apiBaseUrl,
38653
+ debug: !!process.env.DEBUG
38654
+ });
38655
+ if (aas.ok) {
38656
+ console.log(`\u2713 AAS auto-collection registered
38657
+ `);
38658
+ } else {
38659
+ console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
36387
38660
  `);
38661
+ }
36388
38662
  }
36389
38663
  } else if (plan.kind === "error-missing-project") {
36390
38664
  console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
@@ -37014,7 +39288,9 @@ Opening browser for authentication...`);
37014
39288
  ${authUrl}
37015
39289
  `);
37016
39290
  const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
37017
- spawn2(openCommand, [authUrl], { detached: true, stdio: "ignore" }).unref();
39291
+ const browserProcess = spawn2(openCommand, [authUrl], { detached: true, stdio: "ignore" });
39292
+ browserProcess.on("error", () => {});
39293
+ browserProcess.unref();
37018
39294
  console.log("Waiting for authorization...");
37019
39295
  console.log(`(Press Ctrl+C to cancel)
37020
39296
  `);
@@ -37073,10 +39349,22 @@ Please verify the --api-base-url parameter.`);
37073
39349
  const result = JSON.parse(exchangeBody);
37074
39350
  const apiToken = result.api_token || result?.[0]?.result?.api_token;
37075
39351
  const orgId = result.org_id || result?.[0]?.result?.org_id;
39352
+ const tokenValid = typeof apiToken === "string" && apiToken.length > 0;
39353
+ const orgIdValid = orgId !== undefined && orgId !== null && orgId !== "";
39354
+ if (!tokenValid || !orgIdValid) {
39355
+ const missingField = !tokenValid ? "an API token" : "an organization ID";
39356
+ console.error(`Authentication failed: server response did not include ${missingField}.`);
39357
+ console.error("Your existing configuration has been left unchanged.");
39358
+ if (opts.debug) {
39359
+ console.error(`Debug: exchange response body: ${exchangeBody}`);
39360
+ }
39361
+ process.exit(1);
39362
+ return;
39363
+ }
37076
39364
  const existingConfig = readConfig();
37077
39365
  const existingOrgId = existingConfig.orgId;
37078
39366
  const existingProject = existingConfig.defaultProject;
37079
- const orgChanged = existingOrgId && existingOrgId !== orgId;
39367
+ const orgChanged = existingOrgId != null && existingOrgId !== orgId;
37080
39368
  writeConfig({
37081
39369
  apiKey: apiToken,
37082
39370
  baseUrl: apiBaseUrl,
@@ -38030,6 +40318,369 @@ function tryParseJson(s) {
38030
40318
  return s;
38031
40319
  }
38032
40320
  }
40321
+ function printJoeOutcome(outcome, json3, budgetMs) {
40322
+ const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
40323
+ const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
40324
+ if (outcome.budgetExpired) {
40325
+ if (json3) {
40326
+ console.log(JSON.stringify({
40327
+ command_id: outcome.commandId,
40328
+ status: outcome.status,
40329
+ budget_expired: true,
40330
+ resume: `pgai joe result ${outcome.commandId}`
40331
+ }, null, 2));
40332
+ } else {
40333
+ console.log(`started ${outcome.commandId} \xB7 ${outcome.status} \xB7 budget ${budgetSeconds}s reached \u2014 resume: pgai joe result ${outcome.commandId}`);
40334
+ }
40335
+ return;
40336
+ }
40337
+ const output = outcome.output;
40338
+ if (outcome.status === "ok") {
40339
+ if (!output) {
40340
+ console.error(`command ${outcome.commandId} ok but output is empty`);
40341
+ process.exitCode = 1;
40342
+ return;
40343
+ }
40344
+ if (json3)
40345
+ console.log(JSON.stringify(output, null, 2));
40346
+ else {
40347
+ console.log(`command ${outcome.commandId} \xB7 ok`);
40348
+ const body = formatJoeOutput(output);
40349
+ if (body)
40350
+ console.log(body);
40351
+ }
40352
+ return;
40353
+ }
40354
+ if (json3 && output)
40355
+ console.log(JSON.stringify(output, null, 2));
40356
+ console.error(`command ${outcome.commandId} error: ${output?.error ?? "command failed"}`);
40357
+ process.exitCode = 1;
40358
+ }
40359
+ async function runJoeCli(command, arg, opts) {
40360
+ try {
40361
+ const rootOpts = program2.opts();
40362
+ const cfg = readConfig();
40363
+ const { apiKey } = getConfig(rootOpts);
40364
+ if (!apiKey) {
40365
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
40366
+ process.exitCode = 1;
40367
+ return;
40368
+ }
40369
+ const projectRef = (opts.project ?? cfg.defaultProject ?? "").toString().trim();
40370
+ const instanceRef = (opts.instanceId ?? "").toString().trim();
40371
+ if (!projectRef && !instanceRef) {
40372
+ console.error("Specify --project <id|alias> or --instance-id <id>.");
40373
+ process.exitCode = 1;
40374
+ return;
40375
+ }
40376
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40377
+ if (typeof opts.budget === "number" && (!Number.isFinite(opts.budget) || opts.budget < 0)) {
40378
+ throw new Error("--budget must be a non-negative number of seconds");
40379
+ }
40380
+ const budgetMs = typeof opts.budget === "number" ? opts.budget * 1000 : undefined;
40381
+ const outcome = await executeJoeCommand({
40382
+ apiKey,
40383
+ apiBaseUrl,
40384
+ command,
40385
+ project: projectRef || undefined,
40386
+ instanceId: instanceRef || undefined,
40387
+ input: { arg, variant: opts.variant ?? null },
40388
+ orgId: cfg.orgId ?? undefined,
40389
+ budgetMs,
40390
+ debug: !!opts.debug
40391
+ });
40392
+ printJoeOutcome(outcome, !!opts.json, budgetMs);
40393
+ } catch (err) {
40394
+ const message = err instanceof Error ? err.message : String(err);
40395
+ console.error(message);
40396
+ process.exitCode = 1;
40397
+ }
40398
+ }
40399
+ function withJoeOptions(cmd) {
40400
+ return cmd.option("--instance-id <id>", "target the Joe instance id directly (skips --project resolution); required unless --project is given").option("--project <id|alias>", "target project by numeric id OR alias/name; required unless --instance-id is given").option("--budget <seconds>", "one-shot poll budget in seconds (default 25)", (v) => parseFloat(v)).option("--debug", "enable debug output").option("--json", "output raw JSON");
40401
+ }
40402
+ var joe = program2.command("joe").description("Joe \u2014 plan/EXPLAIN/exec queries on ephemeral DBLab clones. " + "All subcommands except 'result' require --project <id|alias> or --instance-id <id>.");
40403
+ withJoeOptions(joe.command("plan <sql>").description("plan a query (EXPLAIN, plan-only \u2014 no execution; the fast/safe default)")).action(async (sql, opts) => {
40404
+ await runJoeCli("plan", sql, opts);
40405
+ });
40406
+ withJoeOptions(joe.command("explain <sql>").description("EXPLAIN + EXPLAIN ANALYZE a query (EXECUTES on the ephemeral clone)")).action(async (sql, opts) => {
40407
+ await runJoeCli("explain", sql, opts);
40408
+ });
40409
+ withJoeOptions(joe.command("exec <sql>").description("run arbitrary DDL/DML on the clone (e.g. create index, analyze)")).action(async (sql, opts) => {
40410
+ await runJoeCli("exec", sql, opts);
40411
+ });
40412
+ withJoeOptions(joe.command("hypo <args>").description("HypoPG hypothetical indexes (e.g. `hypo create index on users (email)`, `hypo desc`, `hypo reset`)")).action(async (args, opts) => {
40413
+ await runJoeCli("hypo", args, opts);
40414
+ });
40415
+ withJoeOptions(joe.command("activity").description("running-activity snapshot (pg_stat_activity) on the clone")).action(async (opts) => {
40416
+ await runJoeCli("activity", null, opts);
40417
+ });
40418
+ withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
40419
+ await runJoeCli("terminate", pid, opts);
40420
+ });
40421
+ withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
40422
+ await runJoeCli("reset", null, opts);
40423
+ });
40424
+ withJoeOptions(joe.command("describe <object>").description("\\d-family schema/relation/index metadata").option("--variant <variant>", "\\d-family variant (e.g. \\d+, \\di, \\dt)")).action(async (object4, opts) => {
40425
+ await runJoeCli("describe", object4, opts);
40426
+ });
40427
+ joe.command("result <commandId>").description("fetch a Joe command's output by id (resume a budget-expired one-shot)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (commandId, opts) => {
40428
+ try {
40429
+ const rootOpts = program2.opts();
40430
+ const cfg = readConfig();
40431
+ const { apiKey } = getConfig(rootOpts);
40432
+ if (!apiKey) {
40433
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
40434
+ process.exitCode = 1;
40435
+ return;
40436
+ }
40437
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40438
+ const output = await getCommandOutput({ apiKey, apiBaseUrl, commandId, debug: !!opts.debug });
40439
+ if (opts.json) {
40440
+ console.log(JSON.stringify(output, null, 2));
40441
+ if (output.status !== "ok") {
40442
+ process.exitCode = 1;
40443
+ }
40444
+ return;
40445
+ }
40446
+ if (output.status === "ok") {
40447
+ console.log(`command ${output.command_id} \xB7 ok`);
40448
+ const body = formatJoeOutput(output);
40449
+ if (body)
40450
+ console.log(body);
40451
+ } else if (output.status === "error") {
40452
+ console.error(`command ${output.command_id} error: ${output.error ?? "command failed"}`);
40453
+ process.exitCode = 1;
40454
+ } else {
40455
+ console.error(`command ${output.command_id} \xB7 ${output.status} \u2014 result is not ready`);
40456
+ process.exitCode = 1;
40457
+ }
40458
+ } catch (err) {
40459
+ const message = err instanceof Error ? err.message : String(err);
40460
+ console.error(message);
40461
+ process.exitCode = 1;
40462
+ }
40463
+ });
40464
+ program2.command("projects").description("list the org's projects (shows which have Joe ready) \u2014 org-level, not a Joe endpoint").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40465
+ try {
40466
+ const rootOpts = program2.opts();
40467
+ const cfg = readConfig();
40468
+ const { apiKey } = getConfig(rootOpts);
40469
+ if (!apiKey) {
40470
+ console.error("API key is required. Run 'pgai auth' first or set --api-key.");
40471
+ process.exitCode = 1;
40472
+ return;
40473
+ }
40474
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40475
+ const projects = await listProjects({
40476
+ apiKey,
40477
+ apiBaseUrl,
40478
+ orgId: cfg.orgId ?? undefined,
40479
+ debug: !!opts.debug
40480
+ });
40481
+ if (opts.json) {
40482
+ console.log(JSON.stringify(projects, null, 2));
40483
+ } else {
40484
+ console.log(formatProjectsTable(projects));
40485
+ }
40486
+ } catch (err) {
40487
+ const message = err instanceof Error ? err.message : String(err);
40488
+ console.error(message);
40489
+ process.exitCode = 1;
40490
+ }
40491
+ });
40492
+ async function resolveDblabTarget(project, debug) {
40493
+ const rootOpts = program2.opts();
40494
+ const cfg = readConfig();
40495
+ const { apiKey } = getConfig(rootOpts);
40496
+ if (!apiKey) {
40497
+ throw new Error("API key is required. Run 'pgai auth' first or set --api-key.");
40498
+ }
40499
+ const ref = (project ?? "").trim();
40500
+ if (!ref) {
40501
+ throw new Error("--project <id|alias> is required");
40502
+ }
40503
+ const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
40504
+ const orgId = cfg.orgId ?? undefined;
40505
+ const instanceId = await resolveDblabInstanceId({ apiKey, apiBaseUrl, project: ref, orgId, debug });
40506
+ return { apiKey, apiBaseUrl, orgId, instanceId };
40507
+ }
40508
+ var dblab = program2.command("dblab").description("DBLab thin-clone / branch / snapshot management (proxies the Platform DBLab API)");
40509
+ var clone2 = dblab.command("clone").description("DBLab thin-clone management (proxies the Platform DBLab API)");
40510
+ clone2.command("create").description("create a thin clone of the project's database").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "branch to clone from").option("--snapshot <id>", "snapshot id to clone from").option("--id <id>", "clone id (DBLab generates one when omitted)").option("--db-user <user>", "clone DB user (set password via PGAI_CLONE_DB_PASSWORD)").option("--protected", "protect the clone from auto-deletion").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40511
+ try {
40512
+ const dbPassword = process.env.PGAI_CLONE_DB_PASSWORD;
40513
+ if (opts.dbUser && !dbPassword || !opts.dbUser && dbPassword) {
40514
+ throw new Error("--db-user and PGAI_CLONE_DB_PASSWORD must be provided together");
40515
+ }
40516
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40517
+ const result = await createClone({
40518
+ apiKey,
40519
+ apiBaseUrl,
40520
+ instanceId,
40521
+ cloneId: opts.id,
40522
+ branch: opts.branch,
40523
+ snapshotId: opts.snapshot,
40524
+ dbUser: opts.dbUser,
40525
+ dbPassword,
40526
+ isProtected: !!opts.protected,
40527
+ debug: !!opts.debug
40528
+ });
40529
+ printResult(result, opts.json);
40530
+ } catch (err) {
40531
+ console.error(err instanceof Error ? err.message : String(err));
40532
+ process.exitCode = 1;
40533
+ }
40534
+ });
40535
+ clone2.command("list").description("list the project's thin clones").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40536
+ try {
40537
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40538
+ const result = await listClones({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
40539
+ printResult(result, opts.json);
40540
+ } catch (err) {
40541
+ console.error(err instanceof Error ? err.message : String(err));
40542
+ process.exitCode = 1;
40543
+ }
40544
+ });
40545
+ clone2.command("status <cloneId>").description("show a clone's status").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40546
+ try {
40547
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40548
+ const result = await getClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
40549
+ printResult(result, opts.json);
40550
+ } catch (err) {
40551
+ console.error(err instanceof Error ? err.message : String(err));
40552
+ process.exitCode = 1;
40553
+ }
40554
+ });
40555
+ clone2.command("reset <cloneId>").description("reset a clone to a pristine snapshot").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to reset to (default: latest)").option("--latest", "reset to the latest snapshot").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40556
+ try {
40557
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40558
+ const result = await resetClone({
40559
+ apiKey,
40560
+ apiBaseUrl,
40561
+ instanceId,
40562
+ cloneId,
40563
+ snapshotId: opts.snapshot,
40564
+ latest: opts.latest,
40565
+ debug: !!opts.debug
40566
+ });
40567
+ printResult(result ?? { reset: true, cloneId }, opts.json);
40568
+ } catch (err) {
40569
+ console.error(err instanceof Error ? err.message : String(err));
40570
+ process.exitCode = 1;
40571
+ }
40572
+ });
40573
+ clone2.command("destroy <cloneId>").description("destroy a clone (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (cloneId, opts) => {
40574
+ try {
40575
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40576
+ const result = await destroyClone({ apiKey, apiBaseUrl, instanceId, cloneId, debug: !!opts.debug });
40577
+ printResult(result ?? { destroyed: true, cloneId }, opts.json);
40578
+ } catch (err) {
40579
+ console.error(err instanceof Error ? err.message : String(err));
40580
+ process.exitCode = 1;
40581
+ }
40582
+ });
40583
+ var branch = dblab.command("branch").description("DBLab branch management (proxies the Platform DBLab API)");
40584
+ branch.command("list").description("list the project's branches").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40585
+ try {
40586
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40587
+ const result = await listBranches({ apiKey, apiBaseUrl, instanceId, debug: !!opts.debug });
40588
+ printResult(result, opts.json);
40589
+ } catch (err) {
40590
+ console.error(err instanceof Error ? err.message : String(err));
40591
+ process.exitCode = 1;
40592
+ }
40593
+ });
40594
+ branch.command("create <name>").description("create a branch").requiredOption("--project <id|alias>", "project id or alias").option("--snapshot <id>", "snapshot id to base the branch on").option("--base-branch <branch>", "parent branch to fork from").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40595
+ try {
40596
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40597
+ const result = await createBranch({
40598
+ apiKey,
40599
+ apiBaseUrl,
40600
+ instanceId,
40601
+ branchName: name,
40602
+ baseBranch: opts.baseBranch,
40603
+ snapshotId: opts.snapshot,
40604
+ debug: !!opts.debug
40605
+ });
40606
+ printResult(result, opts.json);
40607
+ } catch (err) {
40608
+ console.error(err instanceof Error ? err.message : String(err));
40609
+ process.exitCode = 1;
40610
+ }
40611
+ });
40612
+ branch.command("delete <name>").description("delete a branch (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40613
+ try {
40614
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40615
+ const result = await deleteBranch({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40616
+ printResult(result ?? { deleted: true, branch: name }, opts.json);
40617
+ } catch (err) {
40618
+ console.error(err instanceof Error ? err.message : String(err));
40619
+ process.exitCode = 1;
40620
+ }
40621
+ });
40622
+ branch.command("log <name>").description("show a branch's snapshot log").requiredOption("--project <id|alias>", "project id or alias").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (name, opts) => {
40623
+ try {
40624
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40625
+ const result = await branchLog({ apiKey, apiBaseUrl, instanceId, branchName: name, debug: !!opts.debug });
40626
+ printResult(result, opts.json);
40627
+ } catch (err) {
40628
+ console.error(err instanceof Error ? err.message : String(err));
40629
+ process.exitCode = 1;
40630
+ }
40631
+ });
40632
+ var snapshot = dblab.command("snapshot").description("DBLab snapshot management (proxies the Platform DBLab API)");
40633
+ snapshot.command("list").description("list the project's snapshots").requiredOption("--project <id|alias>", "project id or alias").option("--branch <branch>", "filter by branch").option("--dataset <dataset>", "filter by dataset").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40634
+ try {
40635
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40636
+ const result = await listSnapshots({
40637
+ apiKey,
40638
+ apiBaseUrl,
40639
+ instanceId,
40640
+ branchName: opts.branch,
40641
+ dataset: opts.dataset,
40642
+ debug: !!opts.debug
40643
+ });
40644
+ printResult(result, opts.json);
40645
+ } catch (err) {
40646
+ console.error(err instanceof Error ? err.message : String(err));
40647
+ process.exitCode = 1;
40648
+ }
40649
+ });
40650
+ snapshot.command("create").description("create a snapshot from a clone").requiredOption("--project <id|alias>", "project id or alias").requiredOption("--clone <id>", "clone id to snapshot").option("--message <message>", "snapshot message").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
40651
+ try {
40652
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40653
+ const result = await createSnapshot({
40654
+ apiKey,
40655
+ apiBaseUrl,
40656
+ instanceId,
40657
+ cloneId: opts.clone,
40658
+ message: opts.message,
40659
+ debug: !!opts.debug
40660
+ });
40661
+ printResult(result, opts.json);
40662
+ } catch (err) {
40663
+ console.error(err instanceof Error ? err.message : String(err));
40664
+ process.exitCode = 1;
40665
+ }
40666
+ });
40667
+ snapshot.command("destroy <snapshotId>").description("destroy a snapshot (requires the Admin or AllFeaturesUser role)").requiredOption("--project <id|alias>", "project id or alias").option("--force", "force-delete even when dependent clones exist").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (snapshotId, opts) => {
40668
+ try {
40669
+ const { apiKey, apiBaseUrl, instanceId } = await resolveDblabTarget(opts.project, !!opts.debug);
40670
+ const result = await destroySnapshot({
40671
+ apiKey,
40672
+ apiBaseUrl,
40673
+ instanceId,
40674
+ snapshotId,
40675
+ force: !!opts.force,
40676
+ debug: !!opts.debug
40677
+ });
40678
+ printResult(result ?? { destroyed: true, snapshot: snapshotId }, opts.json);
40679
+ } catch (err) {
40680
+ console.error(err instanceof Error ? err.message : String(err));
40681
+ process.exitCode = 1;
40682
+ }
40683
+ });
38033
40684
  var mcp = program2.command("mcp").description("MCP server integration");
38034
40685
  mcp.command("start").description("start MCP stdio server").option("--debug", "enable debug output").action(async (opts) => {
38035
40686
  const rootOpts = program2.opts();
@@ -38150,9 +40801,14 @@ mcp.command("install [client]").description("install MCP server configuration fo
38150
40801
  }
38151
40802
  });
38152
40803
  if (__require.main == __require.module) {
38153
- program2.parseAsync(process.argv).finally(() => {
40804
+ if (process.argv.length <= 2) {
40805
+ program2.outputHelp();
38154
40806
  closeReadline();
38155
- });
40807
+ } else {
40808
+ program2.parseAsync(process.argv).finally(() => {
40809
+ closeReadline();
40810
+ });
40811
+ }
38156
40812
  }
38157
40813
  export {
38158
40814
  resolveAdoptedProject,