postgresai 0.14.0-dev.86 → 0.14.0-dev.88

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.
@@ -21,7 +21,8 @@ import { createInterface } from "readline";
21
21
  import * as childProcess from "child_process";
22
22
  import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
23
23
  import { getCheckupEntry } from "../lib/checkup-dictionary";
24
- import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
24
+ import { createCheckupReport, uploadCheckupReportJson, convertCheckupReportJsonToMarkdown, RpcError, formatRpcErrorForDisplay, withRetry } from "../lib/checkup-api";
25
+ import { generateCheckSummary } from "../lib/checkup-summary";
25
26
 
26
27
  // Singleton readline interface for stdin prompts
27
28
  let rl: ReturnType<typeof createInterface> | null = null;
@@ -184,6 +185,7 @@ interface CheckupOptions {
184
185
  upload?: boolean;
185
186
  project?: string;
186
187
  json?: boolean;
188
+ markdown?: boolean;
187
189
  }
188
190
 
189
191
  interface UploadConfig {
@@ -295,7 +297,6 @@ async function uploadCheckupReports(
295
297
  );
296
298
 
297
299
  const reportId = created.reportId;
298
- logUpload(`Created remote checkup report: ${reportId}`);
299
300
 
300
301
  const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
301
302
  for (const [checkId, report] of Object.entries(reports)) {
@@ -318,7 +319,6 @@ async function uploadCheckupReports(
318
319
  );
319
320
  uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
320
321
  }
321
- logUpload("Upload completed");
322
322
 
323
323
  return { project: uploadCfg.project, reportId, uploaded };
324
324
  }
@@ -340,7 +340,8 @@ function writeReportFiles(reports: Record<string, any>, outputPath: string): voi
340
340
  function printUploadSummary(
341
341
  summary: UploadSummary,
342
342
  projectWasGenerated: boolean,
343
- useStderr: boolean
343
+ useStderr: boolean,
344
+ reports: Record<string, any>
344
345
  ): void {
345
346
  const out = useStderr ? console.error : console.log;
346
347
  out("\nCheckup report uploaded");
@@ -351,11 +352,38 @@ function printUploadSummary(
351
352
  out(`Project: ${summary.project}`);
352
353
  }
353
354
  out(`Report ID: ${summary.reportId}`);
354
- out("View in Console: console.postgres.ai → Support → checkup reports");
355
+ out("View in Console: console.postgres.ai → Checkup → checkup reports");
355
356
  out("");
356
- out("Files:");
357
+
358
+ // Show check summaries (filter out generic info messages)
359
+ const summaries = [];
360
+ let skippedCount = 0;
361
+
357
362
  for (const item of summary.uploaded) {
358
- 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`);
359
387
  }
360
388
  }
361
389
 
@@ -1655,6 +1683,7 @@ program
1655
1683
  "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
1656
1684
  )
1657
1685
  .option("--json", "output JSON to stdout")
1686
+ .option("--markdown", "output markdown to stdout")
1658
1687
  .addHelpText(
1659
1688
  "after",
1660
1689
  [
@@ -1668,6 +1697,7 @@ program
1668
1697
  " postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
1669
1698
  " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
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
1703
  .action(async (checkIdOrConn: string | undefined, connArg: string | undefined, opts: CheckupOptions, cmd: Command) => {
@@ -1708,9 +1738,17 @@ program
1708
1738
  }
1709
1739
 
1710
1740
  const shouldPrintJson = !!opts.json;
1741
+ const shouldConvertMarkdown = !!opts.markdown;
1711
1742
  const uploadExplicitlyRequested = opts.upload === true;
1712
- // Note: --json and --upload/--no-upload are independent flags.
1713
- // Use --no-upload to explicitly disable upload when using --json.
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.
1714
1752
  const uploadExplicitlyDisabled = opts.upload === false;
1715
1753
  let shouldUpload = !uploadExplicitlyDisabled;
1716
1754
 
@@ -1738,7 +1776,8 @@ program
1738
1776
  envPassword: process.env.PGPASSWORD,
1739
1777
  });
1740
1778
  let client: Client | undefined;
1741
- 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;
1742
1781
  const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
1743
1782
 
1744
1783
  try {
@@ -1790,13 +1829,133 @@ program
1790
1829
 
1791
1830
  // Print upload summary
1792
1831
  if (uploadSummary) {
1793
- 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
+ }
1794
1913
  }
1795
1914
 
1796
1915
  // Output JSON to stdout
1797
- if (shouldPrintJson || (!shouldUpload && !opts.output)) {
1916
+ if (shouldPrintJson) {
1798
1917
  console.log(JSON.stringify(reports, null, 2));
1799
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
+ }
1800
1959
  } catch (error) {
1801
1960
  if (error instanceof RpcError) {
1802
1961
  for (const line of formatRpcErrorForDisplay(error)) {
@@ -1956,10 +2115,10 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
1956
2115
  }
1957
2116
  }
1958
2117
 
1959
- // On macOS, node-exporter can't mount host root filesystem - skip it
2118
+ // On macOS, self-node-exporter can't mount host root filesystem - skip it
1960
2119
  const finalArgs = [...args];
1961
2120
  if (process.platform === "darwin" && args.includes("up")) {
1962
- finalArgs.push("--scale", "node-exporter=0");
2121
+ finalArgs.push("--scale", "self-node-exporter=0");
1963
2122
  }
1964
2123
 
1965
2124
  return new Promise<number>((resolve) => {
@@ -2337,15 +2496,15 @@ mon
2337
2496
  // Known container names for cleanup
2338
2497
  const MONITORING_CONTAINERS = [
2339
2498
  "postgres-ai-config-init",
2340
- "node-exporter",
2341
- "cadvisor",
2499
+ "self-node-exporter",
2500
+ "self-cadvisor",
2342
2501
  "grafana-with-datasources",
2343
2502
  "sink-postgres",
2344
2503
  "sink-prometheus",
2345
2504
  "target-db",
2346
2505
  "pgwatch-postgres",
2347
2506
  "pgwatch-prometheus",
2348
- "postgres-exporter-sink",
2507
+ "self-postgres-exporter",
2349
2508
  "flask-pgss-api",
2350
2509
  "sources-generator",
2351
2510
  "postgres-reports",