postgresai 0.14.0-beta.14 → 0.14.0-beta.16

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.
@@ -20,7 +20,9 @@ import { maskSecret } from "../lib/util";
20
20
  import { createInterface } from "readline";
21
21
  import * as childProcess from "child_process";
22
22
  import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
23
- import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
23
+ import { getCheckupEntry } from "../lib/checkup-dictionary";
24
+ import { createCheckupReport, uploadCheckupReportJson, convertCheckupReportJsonToMarkdown, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
25
+ import { generateCheckSummary } from "../lib/checkup-summary";
24
26
 
25
27
  // Singleton readline interface for stdin prompts
26
28
  let rl: ReturnType<typeof createInterface> | null = null;
@@ -183,6 +185,7 @@ interface CheckupOptions {
183
185
  upload?: boolean;
184
186
  project?: string;
185
187
  json?: boolean;
188
+ markdown?: boolean;
186
189
  }
187
190
 
188
191
  interface UploadConfig {
@@ -294,7 +297,6 @@ async function uploadCheckupReports(
294
297
  );
295
298
 
296
299
  const reportId = created.reportId;
297
- logUpload(`Created remote checkup report: ${reportId}`);
298
300
 
299
301
  const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
300
302
  for (const [checkId, report] of Object.entries(reports)) {
@@ -317,7 +319,6 @@ async function uploadCheckupReports(
317
319
  );
318
320
  uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
319
321
  }
320
- logUpload("Upload completed");
321
322
 
322
323
  return { project: uploadCfg.project, reportId, uploaded };
323
324
  }
@@ -339,7 +340,8 @@ function writeReportFiles(reports: Record<string, any>, outputPath: string): voi
339
340
  function printUploadSummary(
340
341
  summary: UploadSummary,
341
342
  projectWasGenerated: boolean,
342
- useStderr: boolean
343
+ useStderr: boolean,
344
+ reports: Record<string, any>
343
345
  ): void {
344
346
  const out = useStderr ? console.error : console.log;
345
347
  out("\nCheckup report uploaded");
@@ -350,11 +352,38 @@ function printUploadSummary(
350
352
  out(`Project: ${summary.project}`);
351
353
  }
352
354
  out(`Report ID: ${summary.reportId}`);
353
- out("View in Console: console.postgres.ai → Support → checkup reports");
355
+ out("View in Console: console.postgres.ai → Checkup → checkup reports");
354
356
  out("");
355
- out("Files:");
357
+
358
+ // Show check summaries (filter out generic info messages)
359
+ const summaries = [];
360
+ let skippedCount = 0;
361
+
356
362
  for (const item of summary.uploaded) {
357
- out(`- ${item.checkId}: ${item.filename}`);
363
+ const report = reports[item.checkId];
364
+ if (report) {
365
+ const { status, message } = generateCheckSummary(item.checkId, report);
366
+ const title = report.checkTitle || item.checkId;
367
+
368
+ // Show if: warning/ok status, or info with concrete data (contains numbers or version info)
369
+ const isSignificant = status !== 'info' || /\d/.test(message) || message.includes('PostgreSQL') || message.includes('Version');
370
+
371
+ if (isSignificant) {
372
+ summaries.push({ checkId: item.checkId, title, status, message });
373
+ } else {
374
+ skippedCount++;
375
+ }
376
+ }
377
+ }
378
+
379
+ // Print significant checks
380
+ for (const { checkId, title, message } of summaries) {
381
+ out(` ${checkId} (${title}): ${message}`);
382
+ }
383
+
384
+ // Show count of other checks
385
+ if (skippedCount > 0) {
386
+ out(` ${skippedCount} other check${skippedCount > 1 ? 's' : ''} completed`);
358
387
  }
359
388
  }
360
389
 
@@ -757,7 +786,7 @@ program
757
786
 
758
787
  const supabaseClient = new SupabaseClient(supabaseConfig);
759
788
 
760
- // Fetch database URL for JSON output (non-blocking, best-effort)
789
+ // Fetch database URL for JSON output (best-effort, errors return null)
761
790
  let databaseUrl: string | null = null;
762
791
  if (jsonOutput) {
763
792
  databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
@@ -1642,17 +1671,19 @@ program
1642
1671
  });
1643
1672
 
1644
1673
  program
1645
- .command("checkup [conn]")
1674
+ .command("checkup [checkIdOrConn] [conn]")
1646
1675
  .description("generate health check reports directly from PostgreSQL (express mode)")
1647
- .option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
1676
+ .option("--check-id <id>", `specific check to run (see list below), or ALL`)
1648
1677
  .option("--node-name <name>", "node name for reports", "node-01")
1649
1678
  .option("--output <path>", "output directory for JSON files")
1650
- .option("--[no-]upload", "upload JSON results to PostgresAI (default: enabled; requires API key)", undefined)
1679
+ .option("--upload", "upload JSON results to PostgresAI (requires API key)")
1680
+ .option("--no-upload", "disable upload to PostgresAI")
1651
1681
  .option(
1652
1682
  "--project <project>",
1653
1683
  "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
1654
1684
  )
1655
- .option("--json", "output JSON to stdout (implies --no-upload)")
1685
+ .option("--json", "output JSON to stdout")
1686
+ .option("--markdown", "output markdown to stdout")
1656
1687
  .addHelpText(
1657
1688
  "after",
1658
1689
  [
@@ -1662,15 +1693,44 @@ program
1662
1693
  "",
1663
1694
  "Examples:",
1664
1695
  " postgresai checkup postgresql://user:pass@host:5432/db",
1665
- " postgresai checkup postgresql://user:pass@host:5432/db --check-id A003",
1696
+ " postgresai checkup H002 postgresql://user:pass@host:5432/db",
1697
+ " postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
1666
1698
  " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
1667
- " postgresai checkup postgresql://user:pass@host:5432/db --project my_project",
1668
- " postgresai set-default-project my_project",
1669
- " postgresai checkup postgresql://user:pass@host:5432/db",
1670
1699
  " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
1700
+ " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --markdown",
1671
1701
  ].join("\n")
1672
1702
  )
1673
- .action(async (conn: string | undefined, opts: CheckupOptions, cmd: Command) => {
1703
+ .action(async (checkIdOrConn: string | undefined, connArg: string | undefined, opts: CheckupOptions, cmd: Command) => {
1704
+ // Support both syntaxes:
1705
+ // pgai checkup postgresql://... -> run ALL checks
1706
+ // pgai checkup H002 postgresql://... -> run specific check (positional)
1707
+ // pgai checkup --check-id H002 postgresql:// -> run specific check (option)
1708
+ const checkIdPattern = /^[A-Z]\d{3}$/i;
1709
+ let conn: string | undefined;
1710
+ let checkId: string;
1711
+
1712
+ if (!checkIdOrConn) {
1713
+ cmd.outputHelp();
1714
+ process.exitCode = 1;
1715
+ return;
1716
+ }
1717
+
1718
+ if (checkIdPattern.test(checkIdOrConn)) {
1719
+ // First arg is a check ID
1720
+ checkId = checkIdOrConn.toUpperCase();
1721
+ conn = connArg;
1722
+ if (!conn) {
1723
+ console.error(`Error: Connection string required when specifying check ID "${checkId}"`);
1724
+ console.error(`\nUsage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname\n`);
1725
+ process.exitCode = 1;
1726
+ return;
1727
+ }
1728
+ } else {
1729
+ // First arg is the connection string
1730
+ conn = checkIdOrConn;
1731
+ checkId = opts.checkId?.toUpperCase() || "ALL";
1732
+ }
1733
+
1674
1734
  if (!conn) {
1675
1735
  cmd.outputHelp();
1676
1736
  process.exitCode = 1;
@@ -1678,8 +1738,18 @@ program
1678
1738
  }
1679
1739
 
1680
1740
  const shouldPrintJson = !!opts.json;
1741
+ const shouldConvertMarkdown = !!opts.markdown;
1681
1742
  const uploadExplicitlyRequested = opts.upload === true;
1682
- const uploadExplicitlyDisabled = opts.upload === false || shouldPrintJson;
1743
+
1744
+ // Validate mutually exclusive flags
1745
+ if (shouldPrintJson && shouldConvertMarkdown) {
1746
+ console.error("Error: --json and --markdown are mutually exclusive");
1747
+ process.exitCode = 1;
1748
+ return;
1749
+ }
1750
+ // Note: --json, --markdown and --upload/--no-upload are independent flags.
1751
+ // Use --no-upload to explicitly disable upload when using --json or --markdown.
1752
+ const uploadExplicitlyDisabled = opts.upload === false;
1683
1753
  let shouldUpload = !uploadExplicitlyDisabled;
1684
1754
 
1685
1755
  // Preflight: validate/create output directory BEFORE connecting / running checks.
@@ -1706,7 +1776,8 @@ program
1706
1776
  envPassword: process.env.PGPASSWORD,
1707
1777
  });
1708
1778
  let client: Client | undefined;
1709
- const spinnerEnabled = !!process.stdout.isTTY && shouldUpload;
1779
+ // Show spinner when output is to TTY (not redirected) and not in JSON mode
1780
+ const spinnerEnabled = !!process.stdout.isTTY && !shouldPrintJson;
1710
1781
  const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
1711
1782
 
1712
1783
  try {
@@ -1716,17 +1787,23 @@ program
1716
1787
 
1717
1788
  // Generate reports
1718
1789
  let reports: Record<string, any>;
1719
- if (opts.checkId === "ALL") {
1790
+ if (checkId === "ALL") {
1720
1791
  reports = await generateAllReports(client, opts.nodeName, (p) => {
1721
1792
  spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
1722
1793
  });
1723
1794
  } else {
1724
- const checkId = opts.checkId.toUpperCase();
1725
1795
  const generator = REPORT_GENERATORS[checkId];
1726
1796
  if (!generator) {
1727
1797
  spinner.stop();
1728
- console.error(`Unknown check ID: ${opts.checkId}`);
1729
- console.error(`Available: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
1798
+ // Check if it's a valid check ID from the dictionary (just not implemented in express mode)
1799
+ const dictEntry = getCheckupEntry(checkId);
1800
+ if (dictEntry) {
1801
+ console.error(`Check ${checkId} (${dictEntry.title}) is not yet available in express mode.`);
1802
+ console.error(`Express-mode checks: ${Object.keys(CHECK_INFO).join(", ")}`);
1803
+ } else {
1804
+ console.error(`Unknown check ID: ${checkId}`);
1805
+ console.error(`See 'postgresai checkup --help' for available checks.`);
1806
+ }
1730
1807
  process.exitCode = 1;
1731
1808
  return;
1732
1809
  }
@@ -1752,13 +1829,133 @@ program
1752
1829
 
1753
1830
  // Print upload summary
1754
1831
  if (uploadSummary) {
1755
- printUploadSummary(uploadSummary, projectWasGenerated, shouldPrintJson);
1832
+ printUploadSummary(uploadSummary, projectWasGenerated, shouldPrintJson || shouldConvertMarkdown, reports);
1833
+ }
1834
+
1835
+ // Convert to markdown if requested
1836
+ if (shouldConvertMarkdown) {
1837
+ let apiKey: string;
1838
+ let apiBaseUrl: string;
1839
+
1840
+ try {
1841
+ const configResult = getConfig(rootOpts);
1842
+ apiKey = configResult.apiKey;
1843
+ const cfg = config.readConfig();
1844
+ apiBaseUrl = resolveBaseUrls(rootOpts, cfg).apiBaseUrl;
1845
+ } catch (error) {
1846
+ spinner.stop();
1847
+ console.error("Error: Failed to read configuration for markdown conversion");
1848
+ console.error(error instanceof Error ? error.message : String(error));
1849
+ process.exitCode = 1;
1850
+ return;
1851
+ }
1852
+
1853
+ // NOTE: apiKey can be empty - this is intentional. The API will return:
1854
+ // - Without API key: Partial markdown with observations only (limited functionality)
1855
+ // - With API key: Full markdown reports with all details
1856
+ // This allows users to get basic insights without requiring authentication.
1857
+
1858
+ const markdownResults: Array<{ checkId: string; markdown?: string; error?: Error }> = [];
1859
+
1860
+ for (const [checkId, report] of Object.entries(reports)) {
1861
+ try {
1862
+ spinner.update(`Converting ${checkId} to markdown`);
1863
+
1864
+ // For reports that share JSON files (e.g., A002/A013 share a002.json,
1865
+ // A003/D001/G003/F001 share a003.json), pass checkId as report_type
1866
+ // so the API can generate the correct markdown variant
1867
+ const markdownResult = await convertCheckupReportJsonToMarkdown({
1868
+ apiKey,
1869
+ apiBaseUrl,
1870
+ checkId,
1871
+ jsonPayload: report,
1872
+ reportType: checkId,
1873
+ });
1874
+
1875
+ // Extract markdown from response structure
1876
+ // API returns: { reports: [{ markdown: "...", ... }], ... }
1877
+ const markdown = markdownResult?.reports?.[0]?.markdown || markdownResult?.markdown;
1878
+
1879
+ markdownResults.push({
1880
+ checkId,
1881
+ markdown,
1882
+ });
1883
+ } catch (error) {
1884
+ markdownResults.push({
1885
+ checkId,
1886
+ error: error instanceof Error ? error : new Error(String(error)),
1887
+ });
1888
+ }
1889
+ }
1890
+
1891
+ spinner.stop();
1892
+
1893
+ // Output all markdown results
1894
+ for (const result of markdownResults) {
1895
+ if (result.error) {
1896
+ if (result.error instanceof RpcError) {
1897
+ console.error(`Error converting ${result.checkId} to markdown:`);
1898
+ for (const line of formatRpcErrorForDisplay(result.error)) {
1899
+ console.error(line);
1900
+ }
1901
+ } else {
1902
+ console.error(`Error converting ${result.checkId} to markdown: ${result.error.message}`);
1903
+ }
1904
+ } else if (result.markdown) {
1905
+ console.log(result.markdown);
1906
+ if (!result.markdown.endsWith('\n')) {
1907
+ console.log();
1908
+ }
1909
+ } else {
1910
+ console.error(`Warning: No markdown content returned for ${result.checkId}`);
1911
+ }
1912
+ }
1756
1913
  }
1757
1914
 
1758
1915
  // Output JSON to stdout
1759
- if (shouldPrintJson || (!shouldUpload && !opts.output)) {
1916
+ if (shouldPrintJson) {
1760
1917
  console.log(JSON.stringify(reports, null, 2));
1761
1918
  }
1919
+
1920
+ // If no output was produced, show summary
1921
+ const hadOutput = shouldPrintJson || shouldConvertMarkdown || outputPath || uploadSummary;
1922
+ if (!hadOutput) {
1923
+ const checkCount = Object.keys(reports).length;
1924
+ console.log(`Checkup completed: ${checkCount} check${checkCount > 1 ? 's' : ''}\n`);
1925
+
1926
+ // Collect and filter summaries
1927
+ const summaries = [];
1928
+ let skippedCount = 0;
1929
+
1930
+ for (const [checkId, report] of Object.entries(reports)) {
1931
+ const { status, message } = generateCheckSummary(checkId, report);
1932
+ const title = report.checkTitle || checkId;
1933
+
1934
+ // Show if: warning/ok status, or info with concrete data (contains numbers or version info)
1935
+ const isSignificant = status !== 'info' || /\d/.test(message) || message.includes('PostgreSQL') || message.includes('Version');
1936
+
1937
+ if (isSignificant) {
1938
+ summaries.push({ checkId, title, status, message });
1939
+ } else {
1940
+ skippedCount++;
1941
+ }
1942
+ }
1943
+
1944
+ // Print significant checks
1945
+ for (const { checkId, title, message } of summaries) {
1946
+ console.log(` ${checkId} (${title}): ${message}`);
1947
+ }
1948
+
1949
+ // Show count of other checks
1950
+ if (skippedCount > 0) {
1951
+ console.log(` ${skippedCount} other check${skippedCount > 1 ? 's' : ''} completed`);
1952
+ }
1953
+
1954
+ console.log('\nFor details:');
1955
+ console.log(' --json Output JSON');
1956
+ console.log(' --markdown Output markdown');
1957
+ console.log(' --output <dir> Save to directory');
1958
+ }
1762
1959
  } catch (error) {
1763
1960
  if (error instanceof RpcError) {
1764
1961
  for (const line of formatRpcErrorForDisplay(error)) {
@@ -1824,7 +2021,8 @@ async function resolveOrInitPaths(): Promise<PathResolution> {
1824
2021
  */
1825
2022
  function isDockerRunning(): boolean {
1826
2023
  try {
1827
- const result = spawnSync("docker", ["info"], { stdio: "pipe", timeout: 5000 });
2024
+ // Note: timeout is supported by Bun but not in @types/bun
2025
+ const result = spawnSync("docker", ["info"], { stdio: "pipe", timeout: 5000 } as Parameters<typeof spawnSync>[2]);
1828
2026
  return result.status === 0;
1829
2027
  } catch {
1830
2028
  return false;
@@ -1836,7 +2034,7 @@ function isDockerRunning(): boolean {
1836
2034
  */
1837
2035
  function getComposeCmd(): string[] | null {
1838
2036
  const tryCmd = (cmd: string, args: string[]): boolean =>
1839
- spawnSync(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
2037
+ spawnSync(cmd, args, { stdio: "ignore", timeout: 5000 } as Parameters<typeof spawnSync>[2]).status === 0;
1840
2038
  if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
1841
2039
  if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
1842
2040
  return null;
@@ -1850,7 +2048,7 @@ function checkRunningContainers(): { running: boolean; containers: string[] } {
1850
2048
  const result = spawnSync(
1851
2049
  "docker",
1852
2050
  ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"],
1853
- { stdio: "pipe", encoding: "utf8", timeout: 5000 }
2051
+ { stdio: "pipe", encoding: "utf8", timeout: 5000 } as Parameters<typeof spawnSync>[2]
1854
2052
  );
1855
2053
 
1856
2054
  if (result.status === 0 && result.stdout) {
package/bun.lock CHANGED
@@ -11,12 +11,12 @@
11
11
  "pg": "^8.16.3",
12
12
  },
13
13
  "devDependencies": {
14
- "@types/bun": "^1.1.14",
14
+ "@types/bun": "^1.3.6",
15
15
  "@types/js-yaml": "^4.0.9",
16
16
  "@types/pg": "^8.15.6",
17
17
  "ajv": "^8.17.1",
18
18
  "ajv-formats": "^3.0.1",
19
- "typescript": "^5.3.3",
19
+ "typescript": "^5.9.3",
20
20
  },
21
21
  },
22
22
  },
@@ -25,7 +25,7 @@
25
25
 
26
26
  "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
27
27
 
28
- "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
28
+ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
29
29
 
30
30
  "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
31
31
 
@@ -43,7 +43,7 @@
43
43
 
44
44
  "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
45
45
 
46
- "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
46
+ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
47
47
 
48
48
  "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
49
49