@indiekitai/pg-dash 0.3.4 → 0.3.5

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
@@ -186,6 +186,59 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
186
186
  | `pg_dash_export` | Export full health report (JSON or Markdown) |
187
187
  | `pg_dash_diff` | Compare current health with last saved snapshot |
188
188
 
189
+ ## MCP Setup
190
+
191
+ Connect pg-dash to Claude Desktop or Cursor for AI-assisted database management.
192
+
193
+ ### Claude Desktop
194
+
195
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
196
+
197
+ ```json
198
+ {
199
+ "mcpServers": {
200
+ "pg-dash": {
201
+ "command": "npx",
202
+ "args": ["-y", "-p", "@indiekitai/pg-dash", "pg-dash-mcp", "postgresql://user:pass@host/db"]
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ ### Cursor
209
+
210
+ Add to `.cursor/mcp.json` in your project:
211
+
212
+ ```json
213
+ {
214
+ "mcpServers": {
215
+ "pg-dash": {
216
+ "command": "npx",
217
+ "args": ["-y", "-p", "@indiekitai/pg-dash", "pg-dash-mcp", "postgresql://user:pass@host/db"]
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ ### Example Conversations
224
+
225
+ Once connected, you can ask your AI assistant:
226
+
227
+ **Diagnosis:**
228
+ - "What's wrong with my database right now?"
229
+ - "Why is my `users` table slow? Check for missing indexes."
230
+ - "Show me the top 5 slowest queries this week."
231
+
232
+ **Optimization:**
233
+ - "Generate SQL to fix all missing FK indexes in one go."
234
+ - "EXPLAIN this query for me: SELECT * FROM orders WHERE user_id = 123"
235
+ - "Which tables are taking up the most space?"
236
+
237
+ **Pre-migration check:**
238
+ - "Run a health check and tell me if it's safe to deploy."
239
+ - "What changed in the schema since last week?"
240
+ - "Check if there are any idle connections blocking my migration."
241
+
189
242
  ## CI Integration
190
243
 
191
244
  ### GitHub Actions
@@ -212,17 +265,45 @@ Sample workflow (`.github/workflows/pg-check.yml`):
212
265
  name: Database Health Check
213
266
  on:
214
267
  push:
215
- paths: ['migrations/**', 'prisma/**', 'drizzle/**']
268
+ paths: ['migrations/**', 'prisma/**', 'drizzle/**', 'supabase/migrations/**']
269
+ pull_request:
270
+ paths: ['migrations/**', 'prisma/**', 'drizzle/**', 'supabase/migrations/**']
216
271
  schedule:
217
- - cron: '0 8 * * 1' # Weekly Monday 8am
272
+ - cron: '0 8 * * 1' # Weekly Monday 8am UTC
218
273
  jobs:
219
- check:
274
+ db-health:
220
275
  runs-on: ubuntu-latest
221
276
  steps:
222
277
  - uses: actions/checkout@v4
223
- - run: npx @indiekitai/pg-dash check ${{ secrets.DATABASE_URL }} --ci --diff --format md
278
+ # Cache snapshot across ephemeral runners for --diff to work
279
+ - name: Restore health snapshot
280
+ uses: actions/cache@v4
281
+ with:
282
+ path: .pg-dash-cache
283
+ key: pg-dash-snapshot-${{ github.ref }}
284
+ restore-keys: pg-dash-snapshot-
285
+ - name: Run pg-dash health check
286
+ id: pg-check
287
+ run: |
288
+ mkdir -p .pg-dash-cache
289
+ npx @indiekitai/pg-dash check ${{ secrets.DATABASE_URL }} \
290
+ --ci --diff --snapshot-path ./.pg-dash-cache/last-check.json \
291
+ --format md > pg-dash-report.md 2>&1
292
+ echo "exit_code=$?" >> $GITHUB_OUTPUT
293
+ continue-on-error: true
294
+ - name: Save health snapshot
295
+ uses: actions/cache/save@v4
296
+ if: always()
297
+ with:
298
+ path: .pg-dash-cache
299
+ key: pg-dash-snapshot-${{ github.ref }}-${{ github.run_id }}
300
+ - name: Fail if unhealthy
301
+ if: steps.pg-check.outputs.exit_code != '0'
302
+ run: exit 1
224
303
  ```
225
304
 
305
+ See [`examples/github-actions-pg-check.yml`](examples/github-actions-pg-check.yml) for a full workflow with PR comments.
306
+
226
307
  ## Health Checks
227
308
 
228
309
  pg-dash runs 46+ automated checks:
package/README.zh-CN.md CHANGED
@@ -186,6 +186,59 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
186
186
  | `pg_dash_export` | 导出完整健康报告(JSON 或 Markdown) |
187
187
  | `pg_dash_diff` | 与上次快照对比当前健康状态 |
188
188
 
189
+ ## MCP 配置
190
+
191
+ 将 pg-dash 接入 Claude Desktop 或 Cursor,实现 AI 辅助的数据库管理。
192
+
193
+ ### Claude Desktop
194
+
195
+ 在 macOS 上编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`,Windows 上编辑 `%APPDATA%\Claude\claude_desktop_config.json`:
196
+
197
+ ```json
198
+ {
199
+ "mcpServers": {
200
+ "pg-dash": {
201
+ "command": "npx",
202
+ "args": ["-y", "-p", "@indiekitai/pg-dash", "pg-dash-mcp", "postgresql://user:pass@host/db"]
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ ### Cursor
209
+
210
+ 在项目的 `.cursor/mcp.json` 中添加:
211
+
212
+ ```json
213
+ {
214
+ "mcpServers": {
215
+ "pg-dash": {
216
+ "command": "npx",
217
+ "args": ["-y", "-p", "@indiekitai/pg-dash", "pg-dash-mcp", "postgresql://user:pass@host/db"]
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ ### 示例对话
224
+
225
+ 连接后,你可以直接问 AI 助手:
226
+
227
+ **诊断问题:**
228
+ - "我的数据库现在有什么问题?"
229
+ - "为什么我的 `users` 表这么慢?检查一下缺失的索引。"
230
+ - "显示本周最慢的 5 条查询。"
231
+
232
+ **性能优化:**
233
+ - "一次性生成 SQL,修复所有缺失的外键索引。"
234
+ - "帮我分析这条查询:SELECT * FROM orders WHERE user_id = 123"
235
+ - "哪些表占用空间最多?"
236
+
237
+ **迁移前检查:**
238
+ - "跑一次健康检查,告诉我现在部署安不安全。"
239
+ - "上周以来 schema 有哪些变化?"
240
+ - "检查是否有空闲连接会阻塞我的迁移。"
241
+
189
242
  ## CI 集成
190
243
 
191
244
  ### GitHub Actions
@@ -212,17 +265,45 @@ pg-dash check postgres://... --ci --diff --format md
212
265
  name: Database Health Check
213
266
  on:
214
267
  push:
215
- paths: ['migrations/**', 'prisma/**', 'drizzle/**']
268
+ paths: ['migrations/**', 'prisma/**', 'drizzle/**', 'supabase/migrations/**']
269
+ pull_request:
270
+ paths: ['migrations/**', 'prisma/**', 'drizzle/**', 'supabase/migrations/**']
216
271
  schedule:
217
- - cron: '0 8 * * 1' # 每周一早 8 点
272
+ - cron: '0 8 * * 1' # 每周一 UTC 早 8 点
218
273
  jobs:
219
- check:
274
+ db-health:
220
275
  runs-on: ubuntu-latest
221
276
  steps:
222
277
  - uses: actions/checkout@v4
223
- - run: npx @indiekitai/pg-dash check ${{ secrets.DATABASE_URL }} --ci --diff --format md
278
+ # 缓存快照,解决 ephemeral runner 丢失 ~/.pg-dash 的问题
279
+ - name: Restore health snapshot
280
+ uses: actions/cache@v4
281
+ with:
282
+ path: .pg-dash-cache
283
+ key: pg-dash-snapshot-${{ github.ref }}
284
+ restore-keys: pg-dash-snapshot-
285
+ - name: Run pg-dash health check
286
+ id: pg-check
287
+ run: |
288
+ mkdir -p .pg-dash-cache
289
+ npx @indiekitai/pg-dash check ${{ secrets.DATABASE_URL }} \
290
+ --ci --diff --snapshot-path ./.pg-dash-cache/last-check.json \
291
+ --format md > pg-dash-report.md 2>&1
292
+ echo "exit_code=$?" >> $GITHUB_OUTPUT
293
+ continue-on-error: true
294
+ - name: Save health snapshot
295
+ uses: actions/cache/save@v4
296
+ if: always()
297
+ with:
298
+ path: .pg-dash-cache
299
+ key: pg-dash-snapshot-${{ github.ref }}-${{ github.run_id }}
300
+ - name: Fail if unhealthy
301
+ if: steps.pg-check.outputs.exit_code != '0'
302
+ run: exit 1
224
303
  ```
225
304
 
305
+ 完整工作流(包含 PR 评论)请参考 [`examples/github-actions-pg-check.yml`](examples/github-actions-pg-check.yml)。
306
+
226
307
  ## 健康检查
227
308
 
228
309
  pg-dash 运行 46+ 项自动化检查:
package/dist/cli.js CHANGED
@@ -763,26 +763,28 @@ __export(snapshot_exports, {
763
763
  });
764
764
  import fs4 from "fs";
765
765
  import path4 from "path";
766
- function saveSnapshot(dataDir, result) {
767
- fs4.mkdirSync(dataDir, { recursive: true });
766
+ function normalizeIssueId(id) {
767
+ return id.replace(/-\d+$/, "");
768
+ }
769
+ function saveSnapshot(snapshotPath, result) {
770
+ fs4.mkdirSync(path4.dirname(snapshotPath), { recursive: true });
768
771
  const snapshot = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), result };
769
- fs4.writeFileSync(path4.join(dataDir, SNAPSHOT_FILE), JSON.stringify(snapshot, null, 2));
772
+ fs4.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
770
773
  }
771
- function loadSnapshot(dataDir) {
772
- const filePath = path4.join(dataDir, SNAPSHOT_FILE);
773
- if (!fs4.existsSync(filePath)) return null;
774
+ function loadSnapshot(snapshotPath) {
775
+ if (!fs4.existsSync(snapshotPath)) return null;
774
776
  try {
775
- return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
777
+ return JSON.parse(fs4.readFileSync(snapshotPath, "utf-8"));
776
778
  } catch {
777
779
  return null;
778
780
  }
779
781
  }
780
782
  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));
783
+ const prevNormIds = new Set(prev.issues.map((i) => normalizeIssueId(i.id)));
784
+ const currNormIds = new Set(current.issues.map((i) => normalizeIssueId(i.id)));
785
+ const newIssues = current.issues.filter((i) => !prevNormIds.has(normalizeIssueId(i.id)));
786
+ const resolvedIssues = prev.issues.filter((i) => !currNormIds.has(normalizeIssueId(i.id)));
787
+ const unchanged = current.issues.filter((i) => prevNormIds.has(normalizeIssueId(i.id)));
786
788
  return {
787
789
  scoreDelta: current.score - prev.score,
788
790
  previousScore: prev.score,
@@ -794,11 +796,9 @@ function diffSnapshots2(prev, current) {
794
796
  unchanged
795
797
  };
796
798
  }
797
- var SNAPSHOT_FILE;
798
799
  var init_snapshot = __esm({
799
800
  "src/server/snapshot.ts"() {
800
801
  "use strict";
801
- SNAPSHOT_FILE = "last-check.json";
802
802
  }
803
803
  });
804
804
 
@@ -2707,7 +2707,7 @@ import { WebSocketServer, WebSocket } from "ws";
2707
2707
  import http from "http";
2708
2708
  var __dirname = path3.dirname(fileURLToPath(import.meta.url));
2709
2709
  async function startServer(opts) {
2710
- const pool = new Pool({ connectionString: opts.connectionString });
2710
+ const pool = new Pool({ connectionString: opts.connectionString, connectionTimeoutMillis: 1e4 });
2711
2711
  try {
2712
2712
  const client = await pool.connect();
2713
2713
  client.release();
@@ -3068,7 +3068,8 @@ var { values, positionals } = parseArgs({
3068
3068
  threshold: { type: "string" },
3069
3069
  format: { type: "string", short: "f" },
3070
3070
  ci: { type: "boolean", default: false },
3071
- diff: { type: "boolean", default: false }
3071
+ diff: { type: "boolean", default: false },
3072
+ "snapshot-path": { type: "string" }
3072
3073
  }
3073
3074
  });
3074
3075
  if (values.version) {
@@ -3115,7 +3116,8 @@ Options:
3115
3116
  --threshold <score> Health score threshold for check command (default: 70)
3116
3117
  -f, --format <fmt> Output format: text|json|md (default: text)
3117
3118
  --ci Output GitHub Actions compatible annotations
3118
- --diff Compare with previous run (saves to ~/.pg-dash/last-check.json)
3119
+ --diff Compare with previous run (saves snapshot for next run)
3120
+ --snapshot-path <path> Path to snapshot file for --diff (default: ~/.pg-dash/last-check.json)
3119
3121
  -v, --version Show version
3120
3122
  -h, --help Show this help
3121
3123
 
@@ -3152,18 +3154,19 @@ if (subcommand === "check") {
3152
3154
  const { getAdvisorReport: getAdvisorReport2 } = await Promise.resolve().then(() => (init_advisor(), advisor_exports));
3153
3155
  const { saveSnapshot: saveSnapshot2, loadSnapshot: loadSnapshot2, diffSnapshots: diffSnapshots3 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
3154
3156
  const os4 = await import("os");
3155
- const pool = new Pool2({ connectionString });
3157
+ const pool = new Pool2({ connectionString, connectionTimeoutMillis: 1e4 });
3156
3158
  const checkDataDir = values["data-dir"] || path5.join(os4.homedir(), ".pg-dash");
3159
+ const snapshotPath = values["snapshot-path"] || path5.join(checkDataDir, "last-check.json");
3157
3160
  try {
3158
3161
  const lqt = parseInt(values["long-query-threshold"] || process.env.PG_DASH_LONG_QUERY_THRESHOLD || "5", 10);
3159
3162
  const report = await getAdvisorReport2(pool, lqt);
3160
3163
  let diff = null;
3161
3164
  if (useDiff) {
3162
- const prev = loadSnapshot2(checkDataDir);
3165
+ const prev = loadSnapshot2(snapshotPath);
3163
3166
  if (prev) {
3164
3167
  diff = diffSnapshots3(prev.result, report);
3165
3168
  }
3166
- saveSnapshot2(checkDataDir, report);
3169
+ saveSnapshot2(snapshotPath, report);
3167
3170
  }
3168
3171
  if (format === "json") {
3169
3172
  const output = { ...report };