@indiekitai/pg-dash 0.4.1 → 0.4.3

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 CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  **The AI-native PostgreSQL health checker.** One command to audit your database, 23 MCP tools for AI-assisted optimization, CI integration for automated checks.
6
6
 
7
+ 📖 **[Read the full writeup on Dev.to](https://dev.to/fan_yang_670d82db29664c9e/i-built-a-free-postgresql-health-checker-with-23-mcp-tools-and-ci-integration-2abc)**
8
+
7
9
  Not another monitoring dashboard — pg-dash is built to fit into your **AI coding workflow**:
8
10
 
9
11
  ```
@@ -165,6 +167,13 @@ pg-dash --host localhost --user postgres --db mydb --port 3480
165
167
 
166
168
  Opens your browser at `http://localhost:3480` with the full dashboard.
167
169
 
170
+ ## Documentation
171
+
172
+ - [Real-world example](docs/real-world-example.md) — pg-dash running against a production database
173
+ - [Migration safety guide](docs/migration-safety.md) — catching lock risks before they hit production
174
+ - [MCP setup guide](docs/mcp-setup.md) — connecting to Claude Desktop and Cursor
175
+ - [CI integration guide](docs/ci-integration.md) — automated checks in GitHub Actions
176
+
168
177
  ## CLI Options
169
178
 
170
179
  ```
package/README.zh-CN.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  **AI 原生的 PostgreSQL 健康检查工具。** 一条命令审计数据库,23 个 MCP 工具让 AI 帮你优化,CI 集成自动检查。
6
6
 
7
+ 📖 **[在 Dev.to 阅读完整介绍](https://dev.to/fan_yang_670d82db29664c9e/i-built-a-free-postgresql-health-checker-with-23-mcp-tools-and-ci-integration-2abc)**
8
+
7
9
  不是又一个监控面板 —— pg-dash 是为 **AI 编程工作流** 设计的:
8
10
 
9
11
  ```
package/dist/cli.js CHANGED
@@ -970,7 +970,7 @@ var init_schema = __esm({
970
970
  });
971
971
 
972
972
  // src/server/schema-diff.ts
973
- function diffSnapshots(oldSnap, newSnap) {
973
+ function diffSchemaSnapshots(oldSnap, newSnap) {
974
974
  const changes = [];
975
975
  const oldTableMap = new Map(oldSnap.tables.map((t) => [`${t.schema}.${t.name}`, t]));
976
976
  const newTableMap = new Map(newSnap.tables.map((t) => [`${t.schema}.${t.name}`, t]));
@@ -1105,12 +1105,13 @@ async function buildLiveSnapshot(pool) {
1105
1105
  enums: enums.map((e) => ({ name: e.name, schema: e.schema, values: e.values }))
1106
1106
  };
1107
1107
  }
1108
- var SchemaTracker;
1108
+ var SNAPSHOT_RETENTION, SchemaTracker;
1109
1109
  var init_schema_tracker = __esm({
1110
1110
  "src/server/schema-tracker.ts"() {
1111
1111
  "use strict";
1112
1112
  init_schema();
1113
1113
  init_schema_diff();
1114
+ SNAPSHOT_RETENTION = 50;
1114
1115
  SchemaTracker = class {
1115
1116
  db;
1116
1117
  pool;
@@ -1147,11 +1148,19 @@ var init_schema_tracker = __esm({
1147
1148
  const json = JSON.stringify(snapshot);
1148
1149
  const info = this.db.prepare("INSERT INTO schema_snapshots (timestamp, snapshot) VALUES (?, ?)").run(now, json);
1149
1150
  const snapshotId = Number(info.lastInsertRowid);
1151
+ this.db.prepare(`
1152
+ DELETE FROM schema_snapshots
1153
+ WHERE id NOT IN (
1154
+ SELECT id FROM schema_snapshots
1155
+ ORDER BY timestamp DESC
1156
+ LIMIT ?
1157
+ )
1158
+ `).run(SNAPSHOT_RETENTION);
1150
1159
  const prev = this.db.prepare("SELECT snapshot FROM schema_snapshots WHERE id < ? ORDER BY id DESC LIMIT 1").get(snapshotId);
1151
1160
  let changes = [];
1152
1161
  if (prev) {
1153
1162
  const oldSnap = JSON.parse(prev.snapshot);
1154
- changes = diffSnapshots(oldSnap, snapshot);
1163
+ changes = diffSchemaSnapshots(oldSnap, snapshot);
1155
1164
  if (changes.length > 0) {
1156
1165
  const insert = this.db.prepare("INSERT INTO schema_changes (snapshot_id, timestamp, change_type, object_type, table_name, detail) VALUES (?, ?, ?, ?, ?, ?)");
1157
1166
  const tx = this.db.transaction((chs) => {
@@ -1198,7 +1207,7 @@ var init_schema_tracker = __esm({
1198
1207
  const from = this.db.prepare("SELECT snapshot FROM schema_snapshots WHERE id = ?").get(fromId);
1199
1208
  const to = this.db.prepare("SELECT snapshot FROM schema_snapshots WHERE id = ?").get(toId);
1200
1209
  if (!from || !to) return null;
1201
- return diffSnapshots(JSON.parse(from.snapshot), JSON.parse(to.snapshot));
1210
+ return diffSchemaSnapshots(JSON.parse(from.snapshot), JSON.parse(to.snapshot));
1202
1211
  }
1203
1212
  };
1204
1213
  }
@@ -1207,7 +1216,7 @@ var init_schema_tracker = __esm({
1207
1216
  // src/server/snapshot.ts
1208
1217
  var snapshot_exports = {};
1209
1218
  __export(snapshot_exports, {
1210
- diffSnapshots: () => diffSnapshots2,
1219
+ diffSnapshots: () => diffSnapshots,
1211
1220
  loadSnapshot: () => loadSnapshot,
1212
1221
  saveSnapshot: () => saveSnapshot
1213
1222
  });
@@ -1229,7 +1238,7 @@ function loadSnapshot(snapshotPath) {
1229
1238
  return null;
1230
1239
  }
1231
1240
  }
1232
- function diffSnapshots2(prev, current) {
1241
+ function diffSnapshots(prev, current) {
1233
1242
  const prevNormIds = new Set(prev.issues.map((i) => normalizeIssueId(i.id)));
1234
1243
  const currNormIds = new Set(current.issues.map((i) => normalizeIssueId(i.id)));
1235
1244
  const newIssues = current.issues.filter((i) => !prevNormIds.has(normalizeIssueId(i.id)));
@@ -1739,7 +1748,7 @@ async function diffEnvironments(sourceConn, targetConn, options) {
1739
1748
  const constraintDiffs = [];
1740
1749
  const enumDiffs = [];
1741
1750
  if (sourceSnap && targetSnap) {
1742
- const snapChanges = diffSnapshots(sourceSnap, targetSnap);
1751
+ const snapChanges = diffSchemaSnapshots(sourceSnap, targetSnap);
1743
1752
  for (const c of snapChanges) {
1744
1753
  if (c.object_type === "constraint") {
1745
1754
  constraintDiffs.push({
@@ -2417,9 +2426,14 @@ var SEVERITY_COLORS = {
2417
2426
  info: { hex: "#3498db", decimal: 3447003, emoji: "\u{1F535}" }
2418
2427
  };
2419
2428
  function detectWebhookType(url) {
2420
- if (url.includes("hooks.slack.com")) return "slack";
2421
- if (url.includes("discord.com/api/webhooks") || url.includes("discordapp.com")) return "discord";
2422
- return "generic";
2429
+ try {
2430
+ const { hostname } = new URL(url);
2431
+ if (hostname.endsWith("hooks.slack.com")) return "slack";
2432
+ if (hostname.endsWith("discord.com") || hostname.endsWith("discordapp.com")) return "discord";
2433
+ return "unknown";
2434
+ } catch {
2435
+ return "unknown";
2436
+ }
2423
2437
  }
2424
2438
  function formatSlackMessage(alert, rule) {
2425
2439
  const colors = SEVERITY_COLORS[rule.severity] || SEVERITY_COLORS.info;
@@ -3895,40 +3909,32 @@ async function startServer(opts) {
3895
3909
  if (Object.keys(snapshot).length > 0) {
3896
3910
  try {
3897
3911
  const alertMetrics = {};
3898
- if (snapshot.connections_total !== void 0) {
3899
- const client = await pool.connect();
3900
- try {
3901
- const r = await client.query("SELECT setting::int AS max FROM pg_settings WHERE name = 'max_connections'");
3912
+ const alertClient = await pool.connect();
3913
+ try {
3914
+ if (snapshot.connections_total !== void 0) {
3915
+ const r = await alertClient.query("SELECT setting::int AS max FROM pg_settings WHERE name = 'max_connections'");
3902
3916
  const max = r.rows[0]?.max || 100;
3903
3917
  alertMetrics.connection_util = snapshot.connections_total / max * 100;
3904
- } finally {
3905
- client.release();
3906
- }
3907
- }
3908
- if (snapshot.cache_hit_ratio !== void 0) {
3909
- alertMetrics.cache_hit_pct = snapshot.cache_hit_ratio * 100;
3910
- }
3911
- try {
3912
- const client = await pool.connect();
3913
- try {
3914
- const r = await client.query(`SELECT count(*)::int AS c FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > $1 * interval '1 minute' AND pid != pg_backend_pid()`, [longQueryThreshold]);
3915
- alertMetrics.long_query_count = r.rows[0]?.c || 0;
3916
- } finally {
3917
- client.release();
3918
3918
  }
3919
- } catch (err) {
3920
- console.error("[alerts] Error checking long queries:", err.message);
3921
- }
3922
- try {
3923
- const client = await pool.connect();
3924
- try {
3925
- const r = await client.query(`SELECT count(*)::int AS c FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > $1 * interval '1 minute'`, [longQueryThreshold]);
3926
- alertMetrics.idle_in_tx_count = r.rows[0]?.c || 0;
3927
- } finally {
3928
- client.release();
3919
+ if (snapshot.cache_hit_ratio !== void 0) {
3920
+ alertMetrics.cache_hit_pct = snapshot.cache_hit_ratio * 100;
3929
3921
  }
3922
+ const [longQueriesResult, idleInTxResult] = await Promise.all([
3923
+ alertClient.query(
3924
+ `SELECT count(*)::int AS c FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > $1 * interval '1 minute' AND pid != pg_backend_pid()`,
3925
+ [longQueryThreshold]
3926
+ ),
3927
+ alertClient.query(
3928
+ `SELECT count(*)::int AS c FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > $1 * interval '1 minute'`,
3929
+ [longQueryThreshold]
3930
+ )
3931
+ ]);
3932
+ alertMetrics.long_query_count = longQueriesResult.rows[0]?.c || 0;
3933
+ alertMetrics.idle_in_tx_count = idleInTxResult.rows[0]?.c || 0;
3930
3934
  } catch (err) {
3931
- console.error("[alerts] Error checking idle-in-tx:", err.message);
3935
+ console.error("[alerts] Error collecting alert metrics:", err.message);
3936
+ } finally {
3937
+ alertClient.release();
3932
3938
  }
3933
3939
  collectCycleCount++;
3934
3940
  if (collectCycleCount % 10 === 0) {
@@ -4114,7 +4120,12 @@ Environment variables:
4114
4120
  `);
4115
4121
  process.exit(0);
4116
4122
  }
4123
+ var KNOWN_SUBCOMMANDS = ["check", "check-migration", "schema-diff", "diff-env"];
4117
4124
  var subcommand = positionals[0];
4125
+ function isValidConnectionString(s) {
4126
+ return s.startsWith("postgresql://") || s.startsWith("postgres://") || s.includes("@") || // user@host shorthand
4127
+ s.includes("=");
4128
+ }
4118
4129
  function resolveConnectionString(startIdx = 0) {
4119
4130
  let connStr = positionals[startIdx];
4120
4131
  if (!connStr) {
@@ -4130,6 +4141,16 @@ function resolveConnectionString(startIdx = 0) {
4130
4141
  process.exit(1);
4131
4142
  }
4132
4143
  }
4144
+ if (!isValidConnectionString(connStr)) {
4145
+ console.error(
4146
+ `Error: "${connStr}" doesn't look like a valid connection string.
4147
+ Expected: postgresql://user:pass@host:5432/db
4148
+
4149
+ Known subcommands: ${KNOWN_SUBCOMMANDS.join(", ")}
4150
+ Run pg-dash --help for usage.`
4151
+ );
4152
+ process.exit(1);
4153
+ }
4133
4154
  return connStr;
4134
4155
  }
4135
4156
  if (subcommand === "check") {
@@ -4140,7 +4161,7 @@ if (subcommand === "check") {
4140
4161
  const useDiff = values.diff || false;
4141
4162
  const { Pool: Pool3 } = await import("pg");
4142
4163
  const { getAdvisorReport: getAdvisorReport2 } = await Promise.resolve().then(() => (init_advisor(), advisor_exports));
4143
- const { saveSnapshot: saveSnapshot2, loadSnapshot: loadSnapshot2, diffSnapshots: diffSnapshots3 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
4164
+ const { saveSnapshot: saveSnapshot2, loadSnapshot: loadSnapshot2, diffSnapshots: diffSnapshots2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
4144
4165
  const os4 = await import("os");
4145
4166
  const pool = new Pool3({ connectionString, connectionTimeoutMillis: 1e4 });
4146
4167
  const checkDataDir = values["data-dir"] || path5.join(os4.homedir(), ".pg-dash");
@@ -4152,7 +4173,7 @@ if (subcommand === "check") {
4152
4173
  if (useDiff) {
4153
4174
  const prev = loadSnapshot2(snapshotPath);
4154
4175
  if (prev) {
4155
- diff = diffSnapshots3(prev.result, report);
4176
+ diff = diffSnapshots2(prev.result, report);
4156
4177
  }
4157
4178
  saveSnapshot2(snapshotPath, report);
4158
4179
  }
@@ -4457,6 +4478,15 @@ Migration check: ${filePath}`);
4457
4478
  process.exit(1);
4458
4479
  }
4459
4480
  } else {
4481
+ if (subcommand && !isValidConnectionString(subcommand) && KNOWN_SUBCOMMANDS.indexOf(subcommand) === -1) {
4482
+ console.error(
4483
+ `Error: Unknown subcommand "${subcommand}".
4484
+
4485
+ Known subcommands: ${KNOWN_SUBCOMMANDS.join(", ")}
4486
+ Run pg-dash --help for usage.`
4487
+ );
4488
+ process.exit(1);
4489
+ }
4460
4490
  const connectionString = resolveConnectionString(0);
4461
4491
  const port = parseInt(values.port, 10);
4462
4492
  const bind = values.bind || process.env.PG_DASH_BIND || "127.0.0.1";