@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 +9 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli.js +71 -41
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
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
|
|
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: () =>
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
const r = await
|
|
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
|
-
|
|
3920
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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";
|