@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/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
- function registerAdvisorRoutes(app, pool, longQueryThreshold) {
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 RANGE_MAP2 = {
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 = RANGE_MAP2[range] || RANGE_MAP2["24h"];
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 RANGE_MAP3 = {
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 = RANGE_MAP3[range] || RANGE_MAP3["1h"];
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 = RANGE_MAP3[range] || RANGE_MAP3["1h"];
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 fs4 from "fs";
2842
- import path4 from "path";
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 = path4.dirname(fileURLToPath2(import.meta.url));
2882
- const pkg = JSON.parse(fs4.readFileSync(path4.resolve(__dirname2, "../package.json"), "utf-8"));
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
- console.log(JSON.stringify(report, null, 2));
2960
- } else {
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"] || path4.join((await import("os")).homedir(), ".pg-dash");
2988
- const schemaDbPath = path4.join(dataDir, "schema.db");
2989
- if (!fs4.existsSync(schemaDbPath)) {
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
  }