postgresai 0.14.0-dev.85 → 0.14.0-dev.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +171 -12
- package/dist/bin/postgres-ai.js +386 -23
- package/dist/sql/03.permissions.sql +41 -2
- package/dist/sql/sql/03.permissions.sql +41 -2
- package/lib/checkup-api.ts +47 -1
- package/lib/checkup-summary.ts +283 -0
- package/lib/init.ts +31 -10
- package/package.json +1 -1
- package/sql/03.permissions.sql +41 -2
- package/test/checkup.integration.test.ts +27 -0
- package/test/checkup.test.ts +580 -1
- package/test/init.test.ts +338 -1
package/bin/postgres-ai.ts
CHANGED
|
@@ -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 →
|
|
355
|
+
out("View in Console: console.postgres.ai → Checkup → checkup reports");
|
|
355
356
|
out("");
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1713
|
-
//
|
|
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
|
-
|
|
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
|
|
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)) {
|