@indiekitai/pg-dash 0.3.2 → 0.3.4
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/README.md +88 -11
- package/README.zh-CN.md +87 -10
- package/dist/cli.js +318 -19
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +249 -9
- package/dist/mcp.js.map +1 -1
- package/dist/ui/assets/index-D5LMag3w.css +1 -0
- package/dist/ui/assets/index-RQDs_hnz.js +33 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BI4_c1SD.js +0 -33
- package/dist/ui/assets/index-F2MaHZFy.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -661,6 +661,28 @@ SELECT pg_reload_conf();`,
|
|
|
661
661
|
const ignoredSet = new Set(ignoredIds);
|
|
662
662
|
const activeIssues = issues.filter((i) => !ignoredSet.has(i.id));
|
|
663
663
|
const ignoredCount = issues.length - activeIssues.length;
|
|
664
|
+
const batchFixes = [];
|
|
665
|
+
const groups = /* @__PURE__ */ new Map();
|
|
666
|
+
for (const issue of activeIssues) {
|
|
667
|
+
const prefix = issue.id.replace(/-[^-]+$/, "");
|
|
668
|
+
if (!groups.has(prefix)) groups.set(prefix, []);
|
|
669
|
+
groups.get(prefix).push(issue);
|
|
670
|
+
}
|
|
671
|
+
const BATCH_TITLES = {
|
|
672
|
+
"schema-fk-no-idx": "Create all missing FK indexes",
|
|
673
|
+
"schema-unused-idx": "Drop all unused indexes",
|
|
674
|
+
"schema-no-pk": "Fix all tables missing primary keys",
|
|
675
|
+
"maint-vacuum": "VACUUM all overdue tables",
|
|
676
|
+
"maint-analyze": "ANALYZE all tables missing statistics",
|
|
677
|
+
"perf-bloated-idx": "REINDEX all bloated indexes",
|
|
678
|
+
"perf-bloat": "VACUUM FULL all bloated tables"
|
|
679
|
+
};
|
|
680
|
+
for (const [prefix, group] of groups) {
|
|
681
|
+
if (group.length <= 1) continue;
|
|
682
|
+
const title = BATCH_TITLES[prefix] || `Fix all ${group.length} ${prefix} issues`;
|
|
683
|
+
const sql = group.map((i) => i.fix.split("\n").filter((l) => !l.trim().startsWith("--")).join("\n").trim()).filter(Boolean).join(";\n") + ";";
|
|
684
|
+
batchFixes.push({ type: prefix, title: `${title} (${group.length})`, count: group.length, sql });
|
|
685
|
+
}
|
|
664
686
|
const score = computeAdvisorScore(activeIssues);
|
|
665
687
|
return {
|
|
666
688
|
score,
|
|
@@ -668,7 +690,8 @@ SELECT pg_reload_conf();`,
|
|
|
668
690
|
issues: activeIssues,
|
|
669
691
|
breakdown: computeBreakdown(activeIssues),
|
|
670
692
|
skipped,
|
|
671
|
-
ignoredCount
|
|
693
|
+
ignoredCount,
|
|
694
|
+
batchFixes
|
|
672
695
|
};
|
|
673
696
|
} finally {
|
|
674
697
|
client.release();
|
|
@@ -731,6 +754,54 @@ var init_advisor = __esm({
|
|
|
731
754
|
}
|
|
732
755
|
});
|
|
733
756
|
|
|
757
|
+
// src/server/snapshot.ts
|
|
758
|
+
var snapshot_exports = {};
|
|
759
|
+
__export(snapshot_exports, {
|
|
760
|
+
diffSnapshots: () => diffSnapshots2,
|
|
761
|
+
loadSnapshot: () => loadSnapshot,
|
|
762
|
+
saveSnapshot: () => saveSnapshot
|
|
763
|
+
});
|
|
764
|
+
import fs4 from "fs";
|
|
765
|
+
import path4 from "path";
|
|
766
|
+
function saveSnapshot(dataDir, result) {
|
|
767
|
+
fs4.mkdirSync(dataDir, { recursive: true });
|
|
768
|
+
const snapshot = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), result };
|
|
769
|
+
fs4.writeFileSync(path4.join(dataDir, SNAPSHOT_FILE), JSON.stringify(snapshot, null, 2));
|
|
770
|
+
}
|
|
771
|
+
function loadSnapshot(dataDir) {
|
|
772
|
+
const filePath = path4.join(dataDir, SNAPSHOT_FILE);
|
|
773
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
774
|
+
try {
|
|
775
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function diffSnapshots2(prev, current) {
|
|
781
|
+
const prevIds = new Set(prev.issues.map((i) => i.id));
|
|
782
|
+
const currIds = new Set(current.issues.map((i) => i.id));
|
|
783
|
+
const newIssues = current.issues.filter((i) => !prevIds.has(i.id));
|
|
784
|
+
const resolvedIssues = prev.issues.filter((i) => !currIds.has(i.id));
|
|
785
|
+
const unchanged = current.issues.filter((i) => prevIds.has(i.id));
|
|
786
|
+
return {
|
|
787
|
+
scoreDelta: current.score - prev.score,
|
|
788
|
+
previousScore: prev.score,
|
|
789
|
+
currentScore: current.score,
|
|
790
|
+
previousGrade: prev.grade,
|
|
791
|
+
currentGrade: current.grade,
|
|
792
|
+
newIssues,
|
|
793
|
+
resolvedIssues,
|
|
794
|
+
unchanged
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
var SNAPSHOT_FILE;
|
|
798
|
+
var init_snapshot = __esm({
|
|
799
|
+
"src/server/snapshot.ts"() {
|
|
800
|
+
"use strict";
|
|
801
|
+
SNAPSHOT_FILE = "last-check.json";
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
734
805
|
// src/cli.ts
|
|
735
806
|
import { parseArgs } from "util";
|
|
736
807
|
|
|
@@ -943,6 +1014,7 @@ var Collector = class extends EventEmitter {
|
|
|
943
1014
|
pruneTimer = null;
|
|
944
1015
|
prev = null;
|
|
945
1016
|
lastSnapshot = {};
|
|
1017
|
+
collectCount = 0;
|
|
946
1018
|
start() {
|
|
947
1019
|
this.collect().catch((err) => console.error("[collector] Initial collection failed:", err));
|
|
948
1020
|
this.timer = setInterval(() => {
|
|
@@ -1045,6 +1117,28 @@ var Collector = class extends EventEmitter {
|
|
|
1045
1117
|
console.error("[collector] Error collecting metrics:", err.message);
|
|
1046
1118
|
return snapshot;
|
|
1047
1119
|
}
|
|
1120
|
+
this.collectCount++;
|
|
1121
|
+
if (this.collectCount % 10 === 0) {
|
|
1122
|
+
try {
|
|
1123
|
+
const client = await this.pool.connect();
|
|
1124
|
+
try {
|
|
1125
|
+
const tableRes = await client.query(`
|
|
1126
|
+
SELECT schemaname, relname,
|
|
1127
|
+
pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(relname)) as total_size
|
|
1128
|
+
FROM pg_stat_user_tables
|
|
1129
|
+
ORDER BY pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(relname)) DESC
|
|
1130
|
+
LIMIT 20
|
|
1131
|
+
`);
|
|
1132
|
+
for (const row of tableRes.rows) {
|
|
1133
|
+
this.store.insert(`table_size:${row.schemaname}.${row.relname}`, parseInt(row.total_size), now);
|
|
1134
|
+
}
|
|
1135
|
+
} finally {
|
|
1136
|
+
client.release();
|
|
1137
|
+
}
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
console.error("[collector] Error collecting table sizes:", err.message);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1048
1142
|
const points = Object.entries(snapshot).map(([metric, value]) => ({
|
|
1049
1143
|
timestamp: now,
|
|
1050
1144
|
metric,
|
|
@@ -1896,7 +1990,12 @@ function registerActivityRoutes(app, pool) {
|
|
|
1896
1990
|
|
|
1897
1991
|
// src/server/routes/advisor.ts
|
|
1898
1992
|
init_advisor();
|
|
1899
|
-
|
|
1993
|
+
var RANGE_MAP2 = {
|
|
1994
|
+
"24h": 24 * 60 * 60 * 1e3,
|
|
1995
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
1996
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
1997
|
+
};
|
|
1998
|
+
function registerAdvisorRoutes(app, pool, longQueryThreshold, store) {
|
|
1900
1999
|
app.get("/api/advisor", async (c) => {
|
|
1901
2000
|
try {
|
|
1902
2001
|
return c.json(await getAdvisorReport(pool, longQueryThreshold));
|
|
@@ -1931,6 +2030,18 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold) {
|
|
|
1931
2030
|
return c.json({ error: err.message }, 500);
|
|
1932
2031
|
}
|
|
1933
2032
|
});
|
|
2033
|
+
app.get("/api/advisor/history", (c) => {
|
|
2034
|
+
if (!store) return c.json([]);
|
|
2035
|
+
try {
|
|
2036
|
+
const range = c.req.query("range") || "24h";
|
|
2037
|
+
const rangeMs = RANGE_MAP2[range] || RANGE_MAP2["24h"];
|
|
2038
|
+
const now = Date.now();
|
|
2039
|
+
const data = store.query("health_score", now - rangeMs, now);
|
|
2040
|
+
return c.json(data);
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
return c.json({ error: err.message }, 500);
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
1934
2045
|
app.post("/api/fix", async (c) => {
|
|
1935
2046
|
try {
|
|
1936
2047
|
const body = await c.req.json();
|
|
@@ -2214,7 +2325,7 @@ var DiskPredictor = class {
|
|
|
2214
2325
|
};
|
|
2215
2326
|
|
|
2216
2327
|
// src/server/routes/disk.ts
|
|
2217
|
-
var
|
|
2328
|
+
var RANGE_MAP3 = {
|
|
2218
2329
|
"24h": 24 * 60 * 60 * 1e3,
|
|
2219
2330
|
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
2220
2331
|
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
@@ -2274,10 +2385,22 @@ function registerDiskRoutes(app, pool, store) {
|
|
|
2274
2385
|
return c.json({ error: err.message }, 500);
|
|
2275
2386
|
}
|
|
2276
2387
|
});
|
|
2388
|
+
app.get("/api/disk/table-history/:table", (c) => {
|
|
2389
|
+
try {
|
|
2390
|
+
const table = c.req.param("table");
|
|
2391
|
+
const range = c.req.query("range") || "24h";
|
|
2392
|
+
const rangeMs = RANGE_MAP3[range] || RANGE_MAP3["24h"];
|
|
2393
|
+
const now = Date.now();
|
|
2394
|
+
const data = store.query(`table_size:${table}`, now - rangeMs, now);
|
|
2395
|
+
return c.json(data);
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
return c.json({ error: err.message }, 500);
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2277
2400
|
app.get("/api/disk/history", (c) => {
|
|
2278
2401
|
try {
|
|
2279
2402
|
const range = c.req.query("range") || "24h";
|
|
2280
|
-
const rangeMs =
|
|
2403
|
+
const rangeMs = RANGE_MAP3[range] || RANGE_MAP3["24h"];
|
|
2281
2404
|
const now = Date.now();
|
|
2282
2405
|
const data = store.query("db_size_bytes", now - rangeMs, now);
|
|
2283
2406
|
return c.json(data);
|
|
@@ -2476,7 +2599,7 @@ var QueryStatsStore = class {
|
|
|
2476
2599
|
};
|
|
2477
2600
|
|
|
2478
2601
|
// src/server/routes/query-stats.ts
|
|
2479
|
-
var
|
|
2602
|
+
var RANGE_MAP4 = {
|
|
2480
2603
|
"1h": 60 * 60 * 1e3,
|
|
2481
2604
|
"6h": 6 * 60 * 60 * 1e3,
|
|
2482
2605
|
"24h": 24 * 60 * 60 * 1e3,
|
|
@@ -2488,7 +2611,7 @@ function registerQueryStatsRoutes(app, store) {
|
|
|
2488
2611
|
const range = c.req.query("range") || "1h";
|
|
2489
2612
|
const orderBy = c.req.query("orderBy") || "total_time";
|
|
2490
2613
|
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
2491
|
-
const rangeMs =
|
|
2614
|
+
const rangeMs = RANGE_MAP4[range] || RANGE_MAP4["1h"];
|
|
2492
2615
|
const now = Date.now();
|
|
2493
2616
|
const data = store.getTopQueries(now - rangeMs, now, orderBy, limit);
|
|
2494
2617
|
return c.json(data);
|
|
@@ -2500,7 +2623,7 @@ function registerQueryStatsRoutes(app, store) {
|
|
|
2500
2623
|
try {
|
|
2501
2624
|
const queryid = c.req.param("queryid");
|
|
2502
2625
|
const range = c.req.query("range") || "1h";
|
|
2503
|
-
const rangeMs =
|
|
2626
|
+
const rangeMs = RANGE_MAP4[range] || RANGE_MAP4["1h"];
|
|
2504
2627
|
const now = Date.now();
|
|
2505
2628
|
const data = store.getTrend(queryid, now - rangeMs, now);
|
|
2506
2629
|
return c.json(data);
|
|
@@ -2510,6 +2633,74 @@ function registerQueryStatsRoutes(app, store) {
|
|
|
2510
2633
|
});
|
|
2511
2634
|
}
|
|
2512
2635
|
|
|
2636
|
+
// src/server/routes/export.ts
|
|
2637
|
+
init_advisor();
|
|
2638
|
+
function registerExportRoutes(app, pool, longQueryThreshold) {
|
|
2639
|
+
app.get("/api/export", async (c) => {
|
|
2640
|
+
const format = c.req.query("format") || "json";
|
|
2641
|
+
try {
|
|
2642
|
+
const [overview, advisor] = await Promise.all([
|
|
2643
|
+
getOverview(pool),
|
|
2644
|
+
getAdvisorReport(pool, longQueryThreshold)
|
|
2645
|
+
]);
|
|
2646
|
+
if (format === "md") {
|
|
2647
|
+
const lines = [];
|
|
2648
|
+
lines.push(`# pg-dash Health Report`);
|
|
2649
|
+
lines.push(`
|
|
2650
|
+
Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2651
|
+
`);
|
|
2652
|
+
lines.push(`## Overview
|
|
2653
|
+
`);
|
|
2654
|
+
lines.push(`- **PostgreSQL**: ${overview.version}`);
|
|
2655
|
+
lines.push(`- **Database Size**: ${overview.dbSize}`);
|
|
2656
|
+
lines.push(`- **Connections**: ${overview.connections.active} active / ${overview.connections.idle} idle / ${overview.connections.max} max`);
|
|
2657
|
+
lines.push(`
|
|
2658
|
+
## Health Score: ${advisor.score}/100 (Grade: ${advisor.grade})
|
|
2659
|
+
`);
|
|
2660
|
+
lines.push(`### Category Breakdown
|
|
2661
|
+
`);
|
|
2662
|
+
lines.push(`| Category | Grade | Score | Issues |`);
|
|
2663
|
+
lines.push(`|----------|-------|-------|--------|`);
|
|
2664
|
+
for (const [cat, b] of Object.entries(advisor.breakdown)) {
|
|
2665
|
+
lines.push(`| ${cat} | ${b.grade} | ${b.score}/100 | ${b.count} |`);
|
|
2666
|
+
}
|
|
2667
|
+
if (advisor.issues.length > 0) {
|
|
2668
|
+
lines.push(`
|
|
2669
|
+
### Issues (${advisor.issues.length})
|
|
2670
|
+
`);
|
|
2671
|
+
for (const issue of advisor.issues) {
|
|
2672
|
+
const icon = issue.severity === "critical" ? "\u{1F534}" : issue.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2673
|
+
lines.push(`#### ${icon} [${issue.severity}] ${issue.title}
|
|
2674
|
+
`);
|
|
2675
|
+
lines.push(`${issue.description}
|
|
2676
|
+
`);
|
|
2677
|
+
lines.push(`**Impact**: ${issue.impact}
|
|
2678
|
+
`);
|
|
2679
|
+
lines.push(`**Fix**:
|
|
2680
|
+
\`\`\`sql
|
|
2681
|
+
${issue.fix}
|
|
2682
|
+
\`\`\`
|
|
2683
|
+
`);
|
|
2684
|
+
}
|
|
2685
|
+
} else {
|
|
2686
|
+
lines.push(`
|
|
2687
|
+
\u2705 No issues found!
|
|
2688
|
+
`);
|
|
2689
|
+
}
|
|
2690
|
+
const md = lines.join("\n");
|
|
2691
|
+
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
2692
|
+
c.header("Content-Disposition", `attachment; filename="pg-dash-report-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.md"`);
|
|
2693
|
+
return c.body(md);
|
|
2694
|
+
}
|
|
2695
|
+
const data = { overview, advisor, exportedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2696
|
+
c.header("Content-Disposition", `attachment; filename="pg-dash-report-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json"`);
|
|
2697
|
+
return c.json(data);
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
return c.json({ error: err.message }, 500);
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2513
2704
|
// src/server/index.ts
|
|
2514
2705
|
import Database3 from "better-sqlite3";
|
|
2515
2706
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -2610,12 +2801,13 @@ async function startServer(opts) {
|
|
|
2610
2801
|
registerOverviewRoutes(app, pool);
|
|
2611
2802
|
registerMetricsRoutes(app, store, collector);
|
|
2612
2803
|
registerActivityRoutes(app, pool);
|
|
2613
|
-
registerAdvisorRoutes(app, pool, longQueryThreshold);
|
|
2804
|
+
registerAdvisorRoutes(app, pool, longQueryThreshold, store);
|
|
2614
2805
|
registerSchemaRoutes(app, pool, schemaTracker);
|
|
2615
2806
|
registerAlertsRoutes(app, alertManager);
|
|
2616
2807
|
registerExplainRoutes(app, pool);
|
|
2617
2808
|
registerDiskRoutes(app, pool, store);
|
|
2618
2809
|
registerQueryStatsRoutes(app, queryStatsStore);
|
|
2810
|
+
registerExportRoutes(app, pool, longQueryThreshold);
|
|
2619
2811
|
const uiPath = path3.resolve(__dirname, "ui");
|
|
2620
2812
|
const MIME_TYPES = {
|
|
2621
2813
|
".html": "text/html",
|
|
@@ -2763,6 +2955,7 @@ async function startServer(opts) {
|
|
|
2763
2955
|
try {
|
|
2764
2956
|
const report = await getAdvisorReport(pool, longQueryThreshold);
|
|
2765
2957
|
alertMetrics.health_score = report.score;
|
|
2958
|
+
store.insert("health_score", report.score);
|
|
2766
2959
|
} catch (err) {
|
|
2767
2960
|
console.error("[alerts] Error checking health score:", err.message);
|
|
2768
2961
|
}
|
|
@@ -2838,8 +3031,8 @@ async function startServer(opts) {
|
|
|
2838
3031
|
}
|
|
2839
3032
|
|
|
2840
3033
|
// src/cli.ts
|
|
2841
|
-
import
|
|
2842
|
-
import
|
|
3034
|
+
import fs5 from "fs";
|
|
3035
|
+
import path5 from "path";
|
|
2843
3036
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2844
3037
|
process.on("uncaughtException", (err) => {
|
|
2845
3038
|
console.error("Uncaught exception:", err);
|
|
@@ -2873,13 +3066,15 @@ var { values, positionals } = parseArgs({
|
|
|
2873
3066
|
help: { type: "boolean", short: "h" },
|
|
2874
3067
|
version: { type: "boolean", short: "v" },
|
|
2875
3068
|
threshold: { type: "string" },
|
|
2876
|
-
format: { type: "string", short: "f" }
|
|
3069
|
+
format: { type: "string", short: "f" },
|
|
3070
|
+
ci: { type: "boolean", default: false },
|
|
3071
|
+
diff: { type: "boolean", default: false }
|
|
2877
3072
|
}
|
|
2878
3073
|
});
|
|
2879
3074
|
if (values.version) {
|
|
2880
3075
|
try {
|
|
2881
|
-
const __dirname2 =
|
|
2882
|
-
const pkg = JSON.parse(
|
|
3076
|
+
const __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
3077
|
+
const pkg = JSON.parse(fs5.readFileSync(path5.resolve(__dirname2, "../package.json"), "utf-8"));
|
|
2883
3078
|
console.log(`pg-dash v${pkg.version}`);
|
|
2884
3079
|
} catch {
|
|
2885
3080
|
console.log("pg-dash v0.1.0");
|
|
@@ -2918,7 +3113,9 @@ Options:
|
|
|
2918
3113
|
--query-stats-interval <min> Query stats snapshot interval in minutes (default: 5)
|
|
2919
3114
|
--long-query-threshold <min> Long query threshold in minutes (default: 5)
|
|
2920
3115
|
--threshold <score> Health score threshold for check command (default: 70)
|
|
2921
|
-
-f, --format <fmt> Output format: text|json (default: text)
|
|
3116
|
+
-f, --format <fmt> Output format: text|json|md (default: text)
|
|
3117
|
+
--ci Output GitHub Actions compatible annotations
|
|
3118
|
+
--diff Compare with previous run (saves to ~/.pg-dash/last-check.json)
|
|
2922
3119
|
-v, --version Show version
|
|
2923
3120
|
-h, --help Show this help
|
|
2924
3121
|
|
|
@@ -2949,18 +3146,120 @@ if (subcommand === "check") {
|
|
|
2949
3146
|
const connectionString = resolveConnectionString(1);
|
|
2950
3147
|
const threshold = parseInt(values.threshold || "70", 10);
|
|
2951
3148
|
const format = values.format || "text";
|
|
3149
|
+
const ci = values.ci || false;
|
|
3150
|
+
const useDiff = values.diff || false;
|
|
2952
3151
|
const { Pool: Pool2 } = await import("pg");
|
|
2953
3152
|
const { getAdvisorReport: getAdvisorReport2 } = await Promise.resolve().then(() => (init_advisor(), advisor_exports));
|
|
3153
|
+
const { saveSnapshot: saveSnapshot2, loadSnapshot: loadSnapshot2, diffSnapshots: diffSnapshots3 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
3154
|
+
const os4 = await import("os");
|
|
2954
3155
|
const pool = new Pool2({ connectionString });
|
|
3156
|
+
const checkDataDir = values["data-dir"] || path5.join(os4.homedir(), ".pg-dash");
|
|
2955
3157
|
try {
|
|
2956
3158
|
const lqt = parseInt(values["long-query-threshold"] || process.env.PG_DASH_LONG_QUERY_THRESHOLD || "5", 10);
|
|
2957
3159
|
const report = await getAdvisorReport2(pool, lqt);
|
|
3160
|
+
let diff = null;
|
|
3161
|
+
if (useDiff) {
|
|
3162
|
+
const prev = loadSnapshot2(checkDataDir);
|
|
3163
|
+
if (prev) {
|
|
3164
|
+
diff = diffSnapshots3(prev.result, report);
|
|
3165
|
+
}
|
|
3166
|
+
saveSnapshot2(checkDataDir, report);
|
|
3167
|
+
}
|
|
2958
3168
|
if (format === "json") {
|
|
2959
|
-
|
|
2960
|
-
|
|
3169
|
+
const output = { ...report };
|
|
3170
|
+
if (diff) output.diff = diff;
|
|
3171
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3172
|
+
} else if (format === "md" || ci && format !== "text") {
|
|
3173
|
+
console.log(`## \u{1F3E5} pg-dash Health Report
|
|
3174
|
+
`);
|
|
3175
|
+
if (diff) {
|
|
3176
|
+
const sign = diff.scoreDelta >= 0 ? "+" : "";
|
|
3177
|
+
console.log(`**Score: ${diff.previousScore} \u2192 ${report.score} (${sign}${diff.scoreDelta})**
|
|
3178
|
+
`);
|
|
3179
|
+
} else {
|
|
3180
|
+
console.log(`**Score: ${report.score}/100 (${report.grade})**
|
|
3181
|
+
`);
|
|
3182
|
+
}
|
|
3183
|
+
console.log(`| Category | Score | Grade | Issues |`);
|
|
3184
|
+
console.log(`|----------|-------|-------|--------|`);
|
|
3185
|
+
for (const [cat, b] of Object.entries(report.breakdown)) {
|
|
3186
|
+
console.log(`| ${cat} | ${b.score} | ${b.grade} | ${b.count} |`);
|
|
3187
|
+
}
|
|
3188
|
+
if (diff) {
|
|
3189
|
+
if (diff.resolvedIssues.length > 0) {
|
|
3190
|
+
console.log(`
|
|
3191
|
+
### \u2705 Resolved (${diff.resolvedIssues.length})`);
|
|
3192
|
+
for (const i of diff.resolvedIssues) console.log(`- ~~${i.title}~~`);
|
|
3193
|
+
}
|
|
3194
|
+
if (diff.newIssues.length > 0) {
|
|
3195
|
+
console.log(`
|
|
3196
|
+
### \u{1F195} New Issues (${diff.newIssues.length})`);
|
|
3197
|
+
for (const i of diff.newIssues) {
|
|
3198
|
+
const icon = i.severity === "critical" ? "\u{1F534}" : i.severity === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
3199
|
+
console.log(`- ${icon} [${i.severity}] ${i.title}`);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
if (report.issues.length > 0) {
|
|
3204
|
+
console.log(`
|
|
3205
|
+
### \u26A0\uFE0F Issues (${report.issues.length})
|
|
3206
|
+
`);
|
|
3207
|
+
for (const issue of report.issues) {
|
|
3208
|
+
const sev = issue.severity === "critical" ? "error" : issue.severity === "warning" ? "warning" : "notice";
|
|
3209
|
+
console.log(`- [${sev}] ${issue.title}`);
|
|
3210
|
+
}
|
|
3211
|
+
} else {
|
|
3212
|
+
console.log(`
|
|
3213
|
+
\u2705 No issues found!`);
|
|
3214
|
+
}
|
|
3215
|
+
if (report.batchFixes.length > 0) {
|
|
3216
|
+
console.log(`
|
|
3217
|
+
### \u{1F527} Batch Fixes
|
|
3218
|
+
`);
|
|
3219
|
+
console.log("```sql");
|
|
3220
|
+
for (const fix of report.batchFixes) {
|
|
3221
|
+
console.log(`-- ${fix.title}`);
|
|
3222
|
+
console.log(fix.sql);
|
|
3223
|
+
}
|
|
3224
|
+
console.log("```");
|
|
3225
|
+
}
|
|
3226
|
+
} else if (ci) {
|
|
3227
|
+
for (const issue of report.issues) {
|
|
3228
|
+
const level = issue.severity === "critical" ? "error" : issue.severity === "warning" ? "warning" : "notice";
|
|
3229
|
+
console.log(`::${level}::${issue.title}: ${issue.description}`);
|
|
3230
|
+
}
|
|
2961
3231
|
console.log(`
|
|
3232
|
+
Health Score: ${report.score}/100 (${report.grade})`);
|
|
3233
|
+
for (const [cat, b] of Object.entries(report.breakdown)) {
|
|
3234
|
+
console.log(` ${cat.padEnd(14)} ${b.grade} (${b.score}/100) \u2014 ${b.count} issue${b.count !== 1 ? "s" : ""}`);
|
|
3235
|
+
}
|
|
3236
|
+
if (diff) {
|
|
3237
|
+
const sign = diff.scoreDelta >= 0 ? "+" : "";
|
|
3238
|
+
console.log(`
|
|
3239
|
+
Score: ${diff.previousScore} \u2192 ${report.score} (${sign}${diff.scoreDelta})`);
|
|
3240
|
+
console.log(`Resolved: ${diff.resolvedIssues.length} issues`);
|
|
3241
|
+
console.log(`New: ${diff.newIssues.length} issues`);
|
|
3242
|
+
}
|
|
3243
|
+
} else {
|
|
3244
|
+
if (diff) {
|
|
3245
|
+
const sign = diff.scoreDelta >= 0 ? "+" : "";
|
|
3246
|
+
console.log(`
|
|
3247
|
+
Score: ${diff.previousScore} \u2192 ${report.score} (${sign}${diff.scoreDelta})
|
|
3248
|
+
`);
|
|
3249
|
+
if (diff.resolvedIssues.length > 0) {
|
|
3250
|
+
console.log(` \u2705 Resolved: ${diff.resolvedIssues.length} issues`);
|
|
3251
|
+
for (const i of diff.resolvedIssues) console.log(` - ${i.title}`);
|
|
3252
|
+
}
|
|
3253
|
+
if (diff.newIssues.length > 0) {
|
|
3254
|
+
console.log(` \u{1F195} New: ${diff.newIssues.length} issues`);
|
|
3255
|
+
for (const i of diff.newIssues) console.log(` - ${i.title}`);
|
|
3256
|
+
}
|
|
3257
|
+
console.log();
|
|
3258
|
+
} else {
|
|
3259
|
+
console.log(`
|
|
2962
3260
|
Health Score: ${report.score}/100 (Grade: ${report.grade})
|
|
2963
3261
|
`);
|
|
3262
|
+
}
|
|
2964
3263
|
for (const [cat, b] of Object.entries(report.breakdown)) {
|
|
2965
3264
|
console.log(` ${cat.padEnd(14)} ${b.grade} (${b.score}/100) \u2014 ${b.count} issue${b.count !== 1 ? "s" : ""}`);
|
|
2966
3265
|
}
|
|
@@ -2984,9 +3283,9 @@ if (subcommand === "check") {
|
|
|
2984
3283
|
}
|
|
2985
3284
|
} else if (subcommand === "schema-diff") {
|
|
2986
3285
|
const connectionString = resolveConnectionString(1);
|
|
2987
|
-
const dataDir = values["data-dir"] ||
|
|
2988
|
-
const schemaDbPath =
|
|
2989
|
-
if (!
|
|
3286
|
+
const dataDir = values["data-dir"] || path5.join((await import("os")).homedir(), ".pg-dash");
|
|
3287
|
+
const schemaDbPath = path5.join(dataDir, "schema.db");
|
|
3288
|
+
if (!fs5.existsSync(schemaDbPath)) {
|
|
2990
3289
|
console.error("No schema tracking data found. Run pg-dash server first to collect schema snapshots.");
|
|
2991
3290
|
process.exit(1);
|
|
2992
3291
|
}
|