@carllee1983/dbcli 1.23.1 → 1.25.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.25.0] - 2026-05-29 - Data-Layer Verification
11
+
12
+ ### Added
13
+
14
+ - **`dbcli snapshot <query>` — 結果指紋。** 將任一查詢結果轉成確定性、黑名單安全的 `ResultSnapshot`(`rowCount` + 每欄聚合:null/distinct 計數、min/max/sum、順序無關的 checksum)。預設落檔至 `.dbcli/snapshots/snap-<timestamp>.json`,亦支援 `--out`、`--stdout`、`--rows`(連同遮罩後的列一併存檔)、`--format`、`--no-limit`。
15
+ - **`dbcli assert <query>` — 行內不變量檢查。** 三種模式:`--expect`(`rows > 0`、`value == 5000`、`col:email not null`、`col:id unique`、`col:amount between 0 and 100`、`col:age >= 18`)、`--vs <query> --compare rows|value`(跨查詢對帳)、`--against <snapshot> --tolerance <pct>`(對既有快照基準比對)。預設失敗時 `exit 1`,可用 `--no-fail` 僅報告不改變 exit code。
16
+ - 兩個指令均沿用既有 adapter / QueryExecutor / blacklist / audit 堆疊,黑名單欄位由 QueryExecutor 在源頭遮罩,指紋天生安全。目前支援 SQL 引擎(PostgreSQL / MySQL / MariaDB)。
17
+
18
+ ## [1.24.0] - 2026-05-29 - Antigravity CLI Skill Target
19
+
20
+ ### Added
21
+
22
+ - **`dbcli skill --install antigravity` 新增 Antigravity CLI 安裝目標。** Antigravity CLI 是 Google Gemini CLI 的後繼者;skill 會寫入 CLI 範疇的全域路徑 `~/.gemini/antigravity-cli/skills/dbcli/SKILL.md`(同目錄附帶 `reference.md`)。`SUPPORTED_PLATFORMS` 一併納入 `antigravity`,故 `dbcli upgrade` 的 skill 過期檢查也會涵蓋此平台。
23
+
24
+ ### Changed
25
+
26
+ - `gemini`(Gemini CLI)安裝目標暫予保留,但已標示為即將淘汰,建議改用 `antigravity`。README(en/zh-TW)、`assets/SKILL.md`、`assets/SKILL.zh-TW.md`、`assets/reference.md` 與 `docs/user` 的平台清單同步更新。
27
+
10
28
  ## [1.23.1] - 2026-05-29 - Skill Docs Sync
11
29
 
12
30
  ### Changed
package/README.md CHANGED
@@ -630,7 +630,8 @@ Generate or install AI agent skill documentation.
630
630
  dbcli skill # Output skill to stdout
631
631
  dbcli skill --output SKILL.md # Write to file
632
632
  dbcli skill --install claude # Install to Claude Code config
633
- dbcli skill --install gemini # Install to Gemini CLI
633
+ dbcli skill --install gemini # Install to Gemini CLI (being phased out)
634
+ dbcli skill --install antigravity # Install to Antigravity CLI (Gemini CLI's successor)
634
635
  dbcli skill --install copilot # Install to GitHub Copilot
635
636
  dbcli skill --install cursor # Install to Cursor IDE
636
637
  ```
@@ -654,6 +655,7 @@ dbcli skill
654
655
  # Install for all platforms
655
656
  dbcli skill --install claude && \
656
657
  dbcli skill --install gemini && \
658
+ dbcli skill --install antigravity && \
657
659
  dbcli skill --install copilot && \
658
660
  dbcli skill --install cursor
659
661
  ```
@@ -766,6 +768,50 @@ dbcli diff --against ./schema-before.json --format table
766
768
 
767
769
  ---
768
770
 
771
+ #### `dbcli snapshot`
772
+
773
+ Capture a **result fingerprint** of a query (distinct from `diff`, which snapshots *schema*): row count plus per-column aggregates (null/distinct counts, min/max/sum) and an order-independent checksum. Blacklisted columns are masked at the source, so the snapshot is safe to store. Use it as a baseline for `dbcli assert --against`. SQL engines only.
774
+
775
+ **Usage:**
776
+ ```bash
777
+ dbcli snapshot "SELECT * FROM orders WHERE created_at >= '2026-05-01'" # → .dbcli/snapshots/snap-<timestamp>.json
778
+ dbcli snapshot @analytics/daily-revenue --out base.json
779
+ dbcli snapshot "SELECT status, count(*) FROM orders GROUP BY status" --stdout
780
+ ```
781
+
782
+ **Options:**
783
+ - `--out <path>` — Output path (default: `.dbcli/snapshots/snap-<timestamp>.json`)
784
+ - `--rows` — Also store the full (blacklist-masked) rows
785
+ - `--stdout` — Print snapshot JSON to stdout instead of writing a file
786
+ - `--format json|table` — Output format for `--stdout` (default: `json`)
787
+ - `--no-limit` — Disable the automatic query-only LIMIT
788
+
789
+ ---
790
+
791
+ #### `dbcli assert`
792
+
793
+ Assert an **invariant** on a query result. Exits `1` on failure (composes in scripts/CI) unless `--no-fail`. SQL engines only.
794
+
795
+ **Usage:**
796
+ ```bash
797
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 0"
798
+ dbcli assert "SELECT * FROM orders WHERE total < 0" --expect "rows == 0"
799
+ dbcli assert "SELECT email FROM users" --expect "col:email not null"
800
+ dbcli assert "SELECT sum(amount) FROM ledger_a" --vs "SELECT sum(amount) FROM ledger_b" --compare value
801
+ dbcli assert "SELECT * FROM orders" --against base.json --tolerance 0.01
802
+ ```
803
+
804
+ **Options:**
805
+ - `--expect <condition>` — `rows > 0`, `value == 5000`, `col:email not null`, `col:id unique`, `col:amount between 0 and 100`, `col:age >= 18`
806
+ - `--vs <query>` — Reconcile against a second query
807
+ - `--compare rows|value` — Comparison mode for `--vs` (default: `value`)
808
+ - `--against <path>` — Compare the current result fingerprint to a saved snapshot
809
+ - `--tolerance <pct>` — Allowed relative drift for `--against` (e.g. `0.01`; default: `0` = exact checksum match)
810
+ - `--no-fail` — Always exit 0; report pass/fail in output only
811
+ - `--format json|table` — Output format (default: `json`)
812
+
813
+ ---
814
+
769
815
  #### `dbcli status`
770
816
 
771
817
  Show non-sensitive configuration summary (permission level, DB system, blacklist counts, config metadata version). Does not print connection credentials — intended for AI agents.
package/README.zh-TW.md CHANGED
@@ -528,7 +528,8 @@ dbcli export "SELECT * FROM users WHERE active=true" --format json | jq '.data |
528
528
  dbcli skill # 輸出至 stdout
529
529
  dbcli skill --output SKILL.md # 寫入檔案
530
530
  dbcli skill --install claude # 安裝至 Claude Code 設定
531
- dbcli skill --install gemini # 安裝至 Gemini CLI
531
+ dbcli skill --install gemini # 安裝至 Gemini CLI(即將淘汰)
532
+ dbcli skill --install antigravity # 安裝至 Antigravity CLI(Gemini CLI 後繼者)
532
533
  dbcli skill --install copilot # 安裝至 GitHub Copilot
533
534
  dbcli skill --install cursor # 安裝至 Cursor IDE
534
535
  ```
@@ -552,6 +553,7 @@ dbcli skill
552
553
  # 為多平台安裝
553
554
  dbcli skill --install claude && \
554
555
  dbcli skill --install gemini && \
556
+ dbcli skill --install antigravity && \
555
557
  dbcli skill --install copilot && \
556
558
  dbcli skill --install cursor
557
559
  ```
@@ -665,6 +667,50 @@ dbcli diff --against ./schema-before.json --format table
665
667
 
666
668
  ---
667
669
 
670
+ #### `dbcli snapshot`
671
+
672
+ 擷取查詢的**結果指紋**(與 `diff` 不同,`diff` 快照的是 *schema*):rowCount 加上每欄聚合(null/distinct 計數、min/max/sum)與順序無關的 checksum。黑名單欄位在源頭遮罩,因此快照可安全保存。作為 `dbcli assert --against` 的基準。僅支援 SQL 引擎。
673
+
674
+ **用法:**
675
+ ```bash
676
+ dbcli snapshot "SELECT * FROM orders WHERE created_at >= '2026-05-01'" # → .dbcli/snapshots/snap-<timestamp>.json
677
+ dbcli snapshot @analytics/daily-revenue --out base.json
678
+ dbcli snapshot "SELECT status, count(*) FROM orders GROUP BY status" --stdout
679
+ ```
680
+
681
+ **選項:**
682
+ - `--out <path>` — 輸出路徑(預設:`.dbcli/snapshots/snap-<timestamp>.json`)
683
+ - `--rows` — 連同遮罩後的完整列一併存檔
684
+ - `--stdout` — 將快照 JSON 印到 stdout 而非寫檔
685
+ - `--format json|table` — `--stdout` 的輸出格式(預設:`json`)
686
+ - `--no-limit` — 停用查詢限定模式的自動 LIMIT
687
+
688
+ ---
689
+
690
+ #### `dbcli assert`
691
+
692
+ 對查詢結果驗證**不變量**。失敗時 `exit 1`(可組合進腳本 / CI),除非加上 `--no-fail`。僅支援 SQL 引擎。
693
+
694
+ **用法:**
695
+ ```bash
696
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 0"
697
+ dbcli assert "SELECT * FROM orders WHERE total < 0" --expect "rows == 0"
698
+ dbcli assert "SELECT email FROM users" --expect "col:email not null"
699
+ dbcli assert "SELECT sum(amount) FROM ledger_a" --vs "SELECT sum(amount) FROM ledger_b" --compare value
700
+ dbcli assert "SELECT * FROM orders" --against base.json --tolerance 0.01
701
+ ```
702
+
703
+ **選項:**
704
+ - `--expect <condition>` — `rows > 0`、`value == 5000`、`col:email not null`、`col:id unique`、`col:amount between 0 and 100`、`col:age >= 18`
705
+ - `--vs <query>` — 與第二個查詢對帳
706
+ - `--compare rows|value` — `--vs` 的比較模式(預設:`value`)
707
+ - `--against <path>` — 將目前結果指紋與已存快照比對
708
+ - `--tolerance <pct>` — `--against` 容許的相對漂移(例如 `0.01`;預設 `0` = 完全相符 checksum)
709
+ - `--no-fail` — 永遠 exit 0;僅在輸出中報告 pass/fail
710
+ - `--format json|table` — 輸出格式(預設:`json`)
711
+
712
+ ---
713
+
668
714
  #### `dbcli status`
669
715
 
670
716
  顯示不含連線憑證的設定摘要(權限、資料庫系統、黑名單筆數、設定中繼版本),適合提供給 AI 代理。
package/assets/SKILL.md CHANGED
@@ -250,6 +250,8 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
250
250
  | `blacklist` | n/a | `list` / `table` / `column` subcommands redact sensitive data from query results. |
251
251
  | `check` | query-only+ | SQL only (best on MySQL/MariaDB). |
252
252
  | `diff` | query-only+ | SQL only. Save/compare schema snapshots. |
253
+ | `snapshot` | query-only+ | **(v1.25)** SQL only. Capture a result fingerprint (`rowCount` + per-column null/distinct/min/max/sum + order-independent checksum). `--out` (default `.dbcli/snapshots/snap-<ts>.json`), `--rows`, `--stdout`, `--format`, `--no-limit`. Baseline for `assert --against`. |
254
+ | `assert` | query-only+ | **(v1.25)** SQL only. Verify an invariant; exit 1 on failure unless `--no-fail`. `--expect "rows>0\|value==X\|col:c not null\|unique\|between a and b\|>= n"`, `--vs <query> --compare rows\|value` (reconcile), `--against <snapshot> --tolerance <pct>`. |
253
255
  | `status` | query-only+ | Safe JSON/text summary (no credentials). |
254
256
  | `inspect` | query-only+ | Read-only context snapshot (connection, permission, blacklist, objects, snippets, context-aware `suggestedCommands`, and **(v1.23)** human-readable `hints`). `--for-agent` / `--brief` / `--no-connect` / `--require-schema-cache`. Supports `--recovery`. |
255
257
  | `report` | query-only+ | Diagnostic report (health / capacity / perf) built from `@diag/*` snippets. `--section`, `--brief`, `--for-agent`, `--no-connect`. |
@@ -260,7 +262,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
260
262
  | `completion` | n/a | bash / zsh / fish scripts. |
261
263
  | `upgrade` | n/a | Self-update from npm; 24h-cached version hints on every command. |
262
264
  | `shell` | (same as query+) | Interactive REPL. SQL engines, MongoDB, and Redis (single-line; `.no-limit on/off`). **(v1.22)** Elasticsearch opens a Kibana Dev Tools-style REPL (`<METHOD> /<path>` + optional JSON body, blank line submits). |
263
- | `skill` | n/a | Generate / install AI skill docs (`--install <claude\|gemini\|copilot\|cursor>`); `skill tasks list/show/plan` for Agent Task Packs; `skill context` for LLM prompt context payload. |
265
+ | `skill` | n/a | Generate / install AI skill docs (`--install <claude\|gemini\|antigravity\|copilot\|cursor>`); `skill tasks list/show/plan` for Agent Task Packs; `skill context` for LLM prompt context payload. |
264
266
  | `migrate` | admin | SQL only. **DDL; dry-run by default** — needs `--execute`. |
265
267
 
266
268
  `--use <name>` on any subcommand targets a v2 connection without changing the default.
@@ -209,6 +209,8 @@ dbcli init --use-env-refs \
209
209
  | `blacklist` | n/a | `list` / `table` / `column` 子指令,從查詢結果中遮蔽敏感資料。 |
210
210
  | `check` | query-only+ | 僅 SQL(在 MySQL / MariaDB 最佳)。 |
211
211
  | `diff` | query-only+ | 僅 SQL。儲存 / 比較 schema snapshot。 |
212
+ | `snapshot` | query-only+ | **(v1.25)** 僅 SQL。擷取結果指紋(`rowCount` + 每欄 null/distinct/min/max/sum + 順序無關 checksum)。`--out`(預設 `.dbcli/snapshots/snap-<ts>.json`)、`--rows`、`--stdout`、`--format`、`--no-limit`。作為 `assert --against` 的基準。 |
213
+ | `assert` | query-only+ | **(v1.25)** 僅 SQL。驗證不變量;失敗時 exit 1,除非 `--no-fail`。`--expect "rows>0\|value==X\|col:c not null\|unique\|between a and b\|>= n"`、`--vs <query> --compare rows\|value`(對帳)、`--against <snapshot> --tolerance <pct>`。 |
212
214
  | `status` | query-only+ | 安全 JSON / 文字摘要(不含憑證)。 |
213
215
  | `inspect` | query-only+ | 唯讀脈絡快照(連線、權限、blacklist、物件、snippets、建議指令)。`--for-agent` / `--no-connect` / `--require-schema-cache`。支援 `--recovery`。 |
214
216
  | `report` | query-only+ | 以 `@diag/*` snippet 組成的診斷報告(health / capacity / perf)。`--section`、`--brief`、`--for-agent`、`--no-connect`。 |
@@ -219,7 +221,7 @@ dbcli init --use-env-refs \
219
221
  | `completion` | n/a | bash / zsh / fish 腳本。 |
220
222
  | `upgrade` | n/a | 從 npm 自我更新;每個指令都帶 24h 快取的版本提示。 |
221
223
  | `shell` | (與 query 同) | 互動式 REPL。支援 SQL 引擎、MongoDB 與 Redis(單行;`.no-limit on/off`)。 |
222
- | `skill` | n/a | 產出 / 安裝 AI skill 文件(`--install <claude\|gemini\|copilot\|cursor>`);`skill tasks list/show/plan` 提供 Agent Task Packs;`skill context` 提供 LLM 提示詞脈絡載荷。 |
224
+ | `skill` | n/a | 產出 / 安裝 AI skill 文件(`--install <claude\|gemini\|antigravity\|copilot\|cursor>`);`skill tasks list/show/plan` 提供 Agent Task Packs;`skill context` 提供 LLM 提示詞脈絡載荷。 |
223
225
  | `migrate` | admin | 僅 SQL。**DDL;預設 dry-run** — 需 `--execute` 才會真的執行。 |
224
226
 
225
227
  任何子指令上的 `--use <name>` 都會把目標切到對應的 v2 連線,但不改變預設值。
@@ -572,6 +572,50 @@ dbcli diff --against before.json --format json
572
572
  **Options:** `--snapshot <path>`, `--against <path>`, `--format <json|table>`
573
573
  **Permission:** query-only+
574
574
 
575
+ ### snapshot
576
+
577
+ Capture a **result fingerprint** of a query (not schema): `rowCount` plus per-column
578
+ aggregates (null/distinct counts, min/max/sum, an order-independent checksum) and a
579
+ top-level `resultChecksum`. Blacklisted columns are masked at the source by QueryExecutor,
580
+ so the fingerprint is safe to store and share. Use it as a baseline for `assert --against`.
581
+
582
+ ```bash
583
+ dbcli snapshot "SELECT * FROM orders WHERE created_at >= '2026-05-01'" # → .dbcli/snapshots/snap-<timestamp>.json
584
+ dbcli snapshot @analytics/daily-revenue --out base.json # saved query → explicit path
585
+ dbcli snapshot "SELECT status, count(*) FROM orders GROUP BY status" --stdout
586
+ dbcli snapshot "SELECT * FROM orders" --rows --out full.json # also store masked rows
587
+ ```
588
+
589
+ **Options:** `--out <path>` (default `.dbcli/snapshots/snap-<timestamp>.json`), `--rows`, `--stdout`, `--format <json|table>`, `--no-limit`
590
+ **Engines:** SQL only (PostgreSQL / MySQL / MariaDB)
591
+ **Permission:** query-only+
592
+
593
+ ### assert
594
+
595
+ Assert an **invariant** on a query result. Exits `1` on failure (so it composes in
596
+ scripts / CI) unless `--no-fail` is given. Three modes (combinable):
597
+
598
+ - `--expect <condition>` — inline check against the result:
599
+ - `rows > 0` / `rows == 1` … (row count vs operators `> >= < <= == !=`)
600
+ - `value == 5000` / `value == "done"` (single-cell result; project to one column)
601
+ - `col:email not null` · `col:id unique` · `col:amount between 0 and 100` · `col:age >= 18`
602
+ - `--vs <query> --compare rows|value` — reconcile against a second query (cross-check totals/counts).
603
+ - `--against <snapshot> --tolerance <pct>` — compare the current result fingerprint to a saved snapshot. `tolerance 0` requires an exact (order-independent) checksum match; `tolerance 0.01` allows ±1% drift on rowCount and each numeric column sum.
604
+
605
+ ```bash
606
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 0"
607
+ dbcli assert "SELECT * FROM orders WHERE total < 0" --expect "rows == 0" # no negative totals
608
+ dbcli assert "SELECT email FROM users" --expect "col:email not null"
609
+ dbcli assert "SELECT sum(amount) FROM ledger_a" --vs "SELECT sum(amount) FROM ledger_b" --compare value
610
+ dbcli assert "SELECT * FROM orders" --against base.json --tolerance 0.01
611
+ dbcli assert "SELECT count(*) FROM orders" --expect "value > 100" --no-fail # report only, exit 0
612
+ ```
613
+
614
+ **Options:** `--expect <condition>`, `--vs <query>`, `--compare <rows|value>` (default `value`), `--against <path>`, `--tolerance <pct>` (default `0`), `--no-fail`, `--format <json|table>`
615
+ **Output:** `AssertVerdict` = `{ pass, checks: [{ name, expected, actual, pass }] }`
616
+ **Engines:** SQL only (PostgreSQL / MySQL / MariaDB)
617
+ **Permission:** query-only+
618
+
575
619
  ### status
576
620
 
577
621
  Show current configuration status (safe for AI agents, no credentials exposed).
@@ -1138,26 +1182,28 @@ dbcli migrate drop-enum status --execute --force
1138
1182
 
1139
1183
  ### skill
1140
1184
 
1141
- Emit `SKILL.md` (and the companion `reference.md`) to stdout, a file, or one of
1142
- four AI-agent platform directories. The skill is the source of truth that lets
1143
- Claude Code / Gemini / Copilot / Cursor know how to drive dbcli safely.
1185
+ Emit `SKILL.md` (and the companion `reference.md`) to stdout, a file, or an
1186
+ AI-agent platform directory. The skill is the source of truth that lets
1187
+ Claude Code / Gemini / Antigravity / Copilot / Cursor know how to drive dbcli safely.
1144
1188
 
1145
1189
  ```bash
1146
1190
  dbcli skill # print SKILL.md to stdout
1147
1191
  dbcli skill --output ./SKILL.md # write to a file (no platform install)
1148
1192
  dbcli skill --install claude # install to ~/.claude/skills/dbcli/
1149
- dbcli skill --install gemini # install to ~/.gemini/skills/dbcli/
1193
+ dbcli skill --install gemini # install to ~/.gemini/skills/dbcli/ (being phased out)
1194
+ dbcli skill --install antigravity # install to ~/.gemini/antigravity-cli/skills/dbcli/
1150
1195
  dbcli skill --install copilot # install to .github/skills/dbcli/ (repo-local)
1151
1196
  dbcli skill --install cursor # install to .cursor/skills/dbcli/ (repo-local)
1152
1197
  ```
1153
1198
 
1154
1199
  **Options:**
1155
- - `--install <platform>` — `claude` | `gemini` | `copilot` | `cursor`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
1200
+ - `--install <platform>` — `claude` | `gemini` | `antigravity` | `copilot` | `cursor` | `codex` | `windsurf`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
1156
1201
  - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
1157
1202
 
1158
1203
  **Notes:**
1159
1204
  - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
1160
- - `claude` / `gemini` install paths are user-global; `copilot` / `cursor` are repo-local under `.github/` / `.cursor/`.
1205
+ - `claude` / `gemini` / `antigravity` install paths are user-global; `copilot` / `cursor` are repo-local under `.github/` / `.cursor/`.
1206
+ - `gemini` (Gemini CLI) is retained for now but is being phased out in favour of `antigravity` (Antigravity CLI), Google's successor terminal agent.
1161
1207
  - Re-running `--install` overwrites the existing skill atomically; no prompt.
1162
1208
 
1163
1209
  **Permission:** n/a.
package/dist/cli.mjs CHANGED
@@ -81217,7 +81217,7 @@ var {
81217
81217
  // package.json
81218
81218
  var package_default = {
81219
81219
  name: "@carllee1983/dbcli",
81220
- version: "1.23.1",
81220
+ version: "1.25.0",
81221
81221
  description: "Database CLI for AI agents",
81222
81222
  type: "module",
81223
81223
  publishConfig: {
@@ -87648,6 +87648,7 @@ function resolveSkillSource(lang) {
87648
87648
  var SUPPORTED_PLATFORMS = [
87649
87649
  "claude",
87650
87650
  "gemini",
87651
+ "antigravity",
87651
87652
  "copilot",
87652
87653
  "cursor",
87653
87654
  "codex",
@@ -87715,6 +87716,8 @@ function getInstallPath(platform) {
87715
87716
  return path4.join(home, ".claude", "skills", "dbcli", "SKILL.md");
87716
87717
  case "gemini":
87717
87718
  return path4.join(home, ".gemini", "skills", "dbcli", "SKILL.md");
87719
+ case "antigravity":
87720
+ return path4.join(home, ".gemini", "antigravity-cli", "skills", "dbcli", "SKILL.md");
87718
87721
  case "codex":
87719
87722
  return path4.join(home, ".codex", "skills", "dbcli", "SKILL.md");
87720
87723
  case "copilot":
@@ -87755,7 +87758,7 @@ async function ensureDir(dirPath) {
87755
87758
  }
87756
87759
  }
87757
87760
  function registerSkillCommand(program2) {
87758
- const skill = program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").addOption(new Option("--lang <lang>", "Source language for SKILL content").choices(["en", "zh-TW"]).default("en")).action(async (options) => {
87761
+ const skill = program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, antigravity, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").addOption(new Option("--lang <lang>", "Source language for SKILL content").choices(["en", "zh-TW"]).default("en")).action(async (options) => {
87759
87762
  try {
87760
87763
  await skillCommand(program2, options);
87761
87764
  } catch (error) {
@@ -90355,11 +90358,454 @@ function makeSavedQueryLoader2() {
90355
90358
  };
90356
90359
  }
90357
90360
 
90361
+ // src/commands/snapshot.ts
90362
+ init_adapters();
90363
+ init_config();
90364
+ import { join as join26 } from "path";
90365
+ init_validation();
90366
+ init_blacklist_validator();
90367
+ init_engine_hints();
90368
+
90369
+ // src/core/result-snapshot/fingerprint.ts
90370
+ import { createHash as createHash2 } from "crypto";
90371
+ function sha256(input) {
90372
+ return createHash2("sha256").update(input).digest("hex");
90373
+ }
90374
+ function isNumeric(values) {
90375
+ return values.length > 0 && values.every((v) => typeof v === "number");
90376
+ }
90377
+ function buildColumn(name2, type, values) {
90378
+ const nonNull = values.filter((v) => v !== null && v !== undefined);
90379
+ const nullCount = values.length - nonNull.length;
90380
+ const asStrings = nonNull.map((v) => String(v));
90381
+ const distinctCount = new Set(asStrings).size;
90382
+ const checksum = sha256(JSON.stringify([...asStrings].sort()));
90383
+ const col = { name: name2, type, nullCount, distinctCount, checksum };
90384
+ if (isNumeric(nonNull)) {
90385
+ col.min = Math.min(...nonNull);
90386
+ col.max = Math.max(...nonNull);
90387
+ col.sum = nonNull.reduce((a, b) => a + b, 0);
90388
+ } else if (nonNull.length > 0) {
90389
+ const sorted = [...asStrings].sort();
90390
+ col.min = sorted[0];
90391
+ col.max = sorted[sorted.length - 1];
90392
+ }
90393
+ return col;
90394
+ }
90395
+ function buildFingerprint(result, opts) {
90396
+ const columns = result.columnNames.map((name2, i) => buildColumn(name2, result.columnTypes?.[i] ?? "unknown", result.rows.map((r) => r[name2])));
90397
+ for (const name2 of opts.redactedColumns ?? []) {
90398
+ if (!columns.some((c2) => c2.name === name2)) {
90399
+ columns.push({
90400
+ name: name2,
90401
+ type: "redacted",
90402
+ nullCount: 0,
90403
+ distinctCount: 0,
90404
+ checksum: "",
90405
+ redacted: true
90406
+ });
90407
+ }
90408
+ }
90409
+ const rowStrings = result.rows.map((r) => JSON.stringify(result.columnNames.map((n) => r[n]))).sort();
90410
+ const snap = {
90411
+ schemaVersion: 1,
90412
+ query: opts.query ?? "",
90413
+ engine: opts.engine ?? "postgresql",
90414
+ createdAt: opts.createdAt ?? new Date().toISOString(),
90415
+ rowCount: result.rowCount,
90416
+ resultChecksum: sha256(JSON.stringify(rowStrings)),
90417
+ columns
90418
+ };
90419
+ if (opts.includeRows)
90420
+ snap.rows = result.rows;
90421
+ return snap;
90422
+ }
90423
+ function compareAgainst(current, baseline, tolerance) {
90424
+ const checks = [];
90425
+ if (tolerance === 0) {
90426
+ checks.push({
90427
+ name: "resultChecksum",
90428
+ expected: baseline.resultChecksum,
90429
+ actual: current.resultChecksum,
90430
+ pass: current.resultChecksum === baseline.resultChecksum
90431
+ });
90432
+ return checks;
90433
+ }
90434
+ const within = (a, b) => b === 0 ? a === 0 : Math.abs(a - b) / Math.abs(b) <= tolerance;
90435
+ checks.push({
90436
+ name: "rowCount",
90437
+ expected: `${baseline.rowCount} \xB1${tolerance * 100}%`,
90438
+ actual: String(current.rowCount),
90439
+ pass: within(current.rowCount, baseline.rowCount)
90440
+ });
90441
+ for (const base of baseline.columns) {
90442
+ if (base.sum === undefined)
90443
+ continue;
90444
+ const cur = current.columns.find((c2) => c2.name === base.name);
90445
+ const curSum = cur?.sum;
90446
+ checks.push({
90447
+ name: `sum(${base.name})`,
90448
+ expected: `${base.sum} \xB1${tolerance * 100}%`,
90449
+ actual: String(curSum),
90450
+ pass: curSum !== undefined && within(curSum, base.sum)
90451
+ });
90452
+ }
90453
+ return checks;
90454
+ }
90455
+
90456
+ // src/core/result-snapshot/types.ts
90457
+ class SnapshotVersionError extends Error {
90458
+ code = "SNAPSHOT_VERSION_MISMATCH";
90459
+ constructor(message) {
90460
+ super(message);
90461
+ this.name = "SnapshotVersionError";
90462
+ }
90463
+ }
90464
+
90465
+ // src/core/result-snapshot/serializer.ts
90466
+ async function writeSnapshot(path6, snap) {
90467
+ await Bun.write(path6, JSON.stringify(snap, null, 2));
90468
+ }
90469
+ async function readSnapshot(path6) {
90470
+ const file = Bun.file(path6);
90471
+ if (!await file.exists()) {
90472
+ const err = new Error(`Snapshot file not found: ${path6}`);
90473
+ err.code = "SNAPSHOT_NOT_FOUND";
90474
+ throw err;
90475
+ }
90476
+ const parsed = JSON.parse(await file.text());
90477
+ if (parsed.schemaVersion !== 1) {
90478
+ throw new SnapshotVersionError(`Unsupported snapshot schemaVersion ${parsed.schemaVersion} (expected 1)`);
90479
+ }
90480
+ return parsed;
90481
+ }
90482
+
90483
+ // src/commands/snapshot.ts
90484
+ init_saved_queries();
90485
+ init_integration_helper();
90486
+ var ALLOWED_FORMATS12 = ["json", "table"];
90487
+ var SQL_SYSTEMS5 = ["postgresql", "mysql", "mariadb"];
90488
+ function requireSqlConnection9(connection) {
90489
+ if (!SQL_SYSTEMS5.includes(connection.system)) {
90490
+ throw new Error(`snapshot currently supports SQL engines only, got: ${connection.system}`);
90491
+ }
90492
+ return connection;
90493
+ }
90494
+ function pad(n) {
90495
+ return String(n).padStart(2, "0");
90496
+ }
90497
+ function defaultSnapshotPath() {
90498
+ const d = new Date;
90499
+ const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
90500
+ return join26(".dbcli", "snapshots", `snap-${stamp}.json`);
90501
+ }
90502
+ var snapshotCommand = new Command().name("snapshot").description("Capture a result fingerprint (rowCount + per-column aggregates) for later comparison").argument("<query>", "SQL string or @saved-query reference").option("--out <path>", "Write snapshot to this path (default: .dbcli/snapshots/snap-<timestamp>.json)").option("--rows", "Also include full (blacklist-masked) rows in the snapshot", false).option("--stdout", "Print snapshot JSON to stdout instead of writing a file", false).option("--format <format>", "Output format for --stdout: json (default) or table", "json").option("--no-limit", "Disable the automatic query-only LIMIT").action(async (query, options, command) => {
90503
+ try {
90504
+ validateFormat(options.format, ALLOWED_FORMATS12, "snapshot");
90505
+ const configPath = resolveConfigPath(command, options);
90506
+ const config = await configModule.read(configPath);
90507
+ if (!config.connection) {
90508
+ console.error("Database not configured. Run: dbcli init");
90509
+ process.exit(1);
90510
+ }
90511
+ let sql = query;
90512
+ if (query.startsWith("@")) {
90513
+ const engine = mapSystemToEngine(config.connection.system);
90514
+ const dirs = resolveSnippetDirs(process.cwd());
90515
+ const snippets = await loadSnippets(dirs);
90516
+ sql = resolveByName(snippets, query.slice(1), engine).query.sqlBody;
90517
+ }
90518
+ const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection9(config.connection));
90519
+ await adapter.connect();
90520
+ try {
90521
+ const blacklistManager = new BlacklistManager(config);
90522
+ const blacklistValidator = new BlacklistValidator(blacklistManager);
90523
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
90524
+ const result = await executor3.execute(sql, { autoLimit: options.limit !== false });
90525
+ const table = extractTableName(sql);
90526
+ const redactedColumns = table ? blacklistManager.getBlacklistedColumns(table) : [];
90527
+ const snap = buildFingerprint(result, {
90528
+ includeRows: options.rows === true,
90529
+ redactedColumns,
90530
+ query: sql,
90531
+ engine: config.connection.system
90532
+ });
90533
+ if (options.stdout === true) {
90534
+ console.log(JSON.stringify(snap, null, 2));
90535
+ } else {
90536
+ const outPath = options.out ?? defaultSnapshotPath();
90537
+ await writeSnapshot(outPath, snap);
90538
+ console.error(`Snapshot saved to ${outPath} (${snap.rowCount} rows, ${snap.columns.length} columns)`);
90539
+ }
90540
+ await writeAuditEntry(config, "snapshot", options, {
90541
+ success: true,
90542
+ sql
90543
+ });
90544
+ } finally {
90545
+ await adapter.disconnect();
90546
+ }
90547
+ } catch (error) {
90548
+ if (error instanceof Error) {
90549
+ console.error(error.message);
90550
+ if (error instanceof ConnectionError)
90551
+ error.hints.forEach((h) => console.error(` Hint: ${h}`));
90552
+ }
90553
+ process.exit(1);
90554
+ }
90555
+ });
90556
+
90557
+ // src/commands/assert.ts
90558
+ init_adapters();
90559
+ init_config();
90560
+ init_validation();
90561
+ init_blacklist_validator();
90562
+ init_saved_queries();
90563
+
90564
+ // src/core/assert/grammar.ts
90565
+ class AssertExpressionError extends Error {
90566
+ code = "ASSERT_BAD_EXPRESSION";
90567
+ constructor(input) {
90568
+ super(`Cannot parse --expect "${input}". Examples: "rows > 0", "value == 5000", ` + `"col:email not null", "col:id unique", "col:amount between 0 and 100", "col:age >= 18".`);
90569
+ this.name = "AssertExpressionError";
90570
+ }
90571
+ }
90572
+ var OP = /(>=|<=|==|!=|>|<)/;
90573
+ function parseScalar2(raw) {
90574
+ const s = raw.trim();
90575
+ if (/^".*"$/.test(s) || /^'.*'$/.test(s))
90576
+ return s.slice(1, -1);
90577
+ const n = Number(s);
90578
+ return Number.isNaN(n) ? s : n;
90579
+ }
90580
+ function parseExpect(input) {
90581
+ const s = input.trim();
90582
+ const rows = s.match(new RegExp(`^rows\\s*${OP.source}\\s*(\\d+)$`));
90583
+ if (rows)
90584
+ return { kind: "rows", op: rows[1], value: parseInt(rows[2], 10) };
90585
+ const value = s.match(new RegExp(`^value\\s*${OP.source}\\s*(.+)$`));
90586
+ if (value)
90587
+ return { kind: "value", op: value[1], value: parseScalar2(value[2]) };
90588
+ const col = s.match(/^col:(\w+)\s+(.+)$/);
90589
+ if (col) {
90590
+ const column = col[1];
90591
+ const rest = col[2].trim();
90592
+ if (/^not\s+null$/i.test(rest))
90593
+ return { kind: "col", column, pred: { type: "notNull" } };
90594
+ if (/^unique$/i.test(rest))
90595
+ return { kind: "col", column, pred: { type: "unique" } };
90596
+ const between = rest.match(/^between\s+(-?\d+(?:\.\d+)?)\s+and\s+(-?\d+(?:\.\d+)?)$/i);
90597
+ if (between) {
90598
+ return {
90599
+ kind: "col",
90600
+ column,
90601
+ pred: { type: "between", low: Number(between[1]), high: Number(between[2]) }
90602
+ };
90603
+ }
90604
+ const cmp = rest.match(new RegExp(`^${OP.source}\\s*(.+)$`));
90605
+ if (cmp)
90606
+ return {
90607
+ kind: "col",
90608
+ column,
90609
+ pred: { type: "cmp", op: cmp[1], value: parseScalar2(cmp[2]) }
90610
+ };
90611
+ }
90612
+ throw new AssertExpressionError(input);
90613
+ }
90614
+
90615
+ // src/core/assert/evaluator.ts
90616
+ class AssertShapeError extends Error {
90617
+ code = "ASSERT_SHAPE_MISMATCH";
90618
+ constructor(message) {
90619
+ super(message);
90620
+ this.name = "AssertShapeError";
90621
+ }
90622
+ }
90623
+ function compare(a, op, b) {
90624
+ switch (op) {
90625
+ case ">":
90626
+ return a > b;
90627
+ case ">=":
90628
+ return a >= b;
90629
+ case "<":
90630
+ return a < b;
90631
+ case "<=":
90632
+ return a <= b;
90633
+ case "==":
90634
+ return a === b;
90635
+ case "!=":
90636
+ return a !== b;
90637
+ }
90638
+ }
90639
+ function firstScalar(result) {
90640
+ if (result.columnNames.length !== 1) {
90641
+ throw new AssertShapeError(`value assertion needs a single-column result, got ${result.columnNames.length} columns. Project to one column.`);
90642
+ }
90643
+ if (result.rows.length === 0)
90644
+ return null;
90645
+ const v = result.rows[0][result.columnNames[0]];
90646
+ return v === null || v === undefined ? null : v;
90647
+ }
90648
+ function evaluateExpect(node, result) {
90649
+ if (node.kind === "rows") {
90650
+ const actual = result.rowCount;
90651
+ return {
90652
+ name: "rows",
90653
+ expected: `rows ${node.op} ${node.value}`,
90654
+ actual: String(actual),
90655
+ pass: compare(actual, node.op, node.value)
90656
+ };
90657
+ }
90658
+ if (node.kind === "value") {
90659
+ const actual = firstScalar(result);
90660
+ return {
90661
+ name: "value",
90662
+ expected: `value ${node.op} ${node.value}`,
90663
+ actual: String(actual),
90664
+ pass: actual !== null && compare(actual, node.op, node.value)
90665
+ };
90666
+ }
90667
+ const { column, pred } = node;
90668
+ const values = result.rows.map((r) => r[column]);
90669
+ const nonNull = values.filter((v) => v !== null && v !== undefined);
90670
+ if (pred.type === "notNull") {
90671
+ const nullCount = values.length - nonNull.length;
90672
+ return {
90673
+ name: `col:${column} not null`,
90674
+ expected: "0 nulls",
90675
+ actual: `${nullCount} nulls`,
90676
+ pass: nullCount === 0
90677
+ };
90678
+ }
90679
+ if (pred.type === "unique") {
90680
+ const distinct = new Set(nonNull.map((v) => String(v))).size;
90681
+ return {
90682
+ name: `col:${column} unique`,
90683
+ expected: `${nonNull.length} distinct`,
90684
+ actual: `${distinct} distinct`,
90685
+ pass: distinct === nonNull.length
90686
+ };
90687
+ }
90688
+ if (pred.type === "between") {
90689
+ const bad2 = nonNull.filter((v) => typeof v !== "number" || v < pred.low || v > pred.high);
90690
+ return {
90691
+ name: `col:${column} between ${pred.low} and ${pred.high}`,
90692
+ expected: "0 out of range",
90693
+ actual: `${bad2.length} out of range`,
90694
+ pass: bad2.length === 0
90695
+ };
90696
+ }
90697
+ const bad = nonNull.filter((v) => !compare(v, pred.op, pred.value));
90698
+ return {
90699
+ name: `col:${column} ${pred.op} ${pred.value}`,
90700
+ expected: "0 violations",
90701
+ actual: `${bad.length} violations`,
90702
+ pass: bad.length === 0
90703
+ };
90704
+ }
90705
+ function compareVs(a, b, mode) {
90706
+ if (mode === "rows") {
90707
+ return {
90708
+ name: "vs:rows",
90709
+ expected: String(b.rowCount),
90710
+ actual: String(a.rowCount),
90711
+ pass: a.rowCount === b.rowCount
90712
+ };
90713
+ }
90714
+ const av = firstScalar(a);
90715
+ const bv = firstScalar(b);
90716
+ return {
90717
+ name: "vs:value",
90718
+ expected: String(bv),
90719
+ actual: String(av),
90720
+ pass: av !== null && bv !== null && av === bv
90721
+ };
90722
+ }
90723
+
90724
+ // src/commands/assert.ts
90725
+ init_integration_helper();
90726
+ var ALLOWED_FORMATS13 = ["json", "table"];
90727
+ var SQL_SYSTEMS6 = ["postgresql", "mysql", "mariadb"];
90728
+ function requireSqlConnection10(connection) {
90729
+ if (!SQL_SYSTEMS6.includes(connection.system)) {
90730
+ throw new Error(`assert currently supports SQL engines only, got: ${connection.system}`);
90731
+ }
90732
+ return connection;
90733
+ }
90734
+ var assertCommand = new Command().name("assert").description("Assert an invariant on a query result (exit 1 on failure unless --no-fail)").argument("<query>", "SQL string or @saved-query reference").option("--expect <condition>", 'e.g. "rows > 0", "value == 5000", "col:email not null"').option("--vs <query>", "Second SQL/@saved query for reconciliation").option("--compare <mode>", "For --vs: rows | value (default value)", "value").option("--against <path>", "Compare current result fingerprint against a saved snapshot").option("--tolerance <pct>", "For --against: allowed relative drift, e.g. 0.01 (default 0)", (v) => parseFloat(v), 0).option("--no-fail", "Always exit 0; report pass/fail in output only").option("--format <format>", "Output format: json (default) or table", "json").action(async (query, options, command) => {
90735
+ try {
90736
+ validateFormat(options.format, ALLOWED_FORMATS13, "assert");
90737
+ if (!options.expect && !options.vs && !options.against) {
90738
+ console.error("Specify one of --expect, --vs, or --against");
90739
+ process.exit(1);
90740
+ }
90741
+ const configPath = resolveConfigPath(command, options);
90742
+ const config = await configModule.read(configPath);
90743
+ if (!config.connection) {
90744
+ console.error("Database not configured. Run: dbcli init");
90745
+ process.exit(1);
90746
+ }
90747
+ const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection10(config.connection));
90748
+ await adapter.connect();
90749
+ let verdict;
90750
+ try {
90751
+ const blacklistManager = new BlacklistManager(config);
90752
+ const blacklistValidator = new BlacklistValidator(blacklistManager);
90753
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
90754
+ const engine = mapSystemToEngine(config.connection.system);
90755
+ const dirs = resolveSnippetDirs(process.cwd());
90756
+ const snippets = await loadSnippets(dirs);
90757
+ const resolveSql = (q) => q.startsWith("@") ? resolveByName(snippets, q.slice(1), engine).query.sqlBody : q;
90758
+ const result = await executor3.execute(resolveSql(query), { autoLimit: true });
90759
+ const checks = [];
90760
+ if (options.expect)
90761
+ checks.push(evaluateExpect(parseExpect(options.expect), result));
90762
+ if (options.vs) {
90763
+ const other = await executor3.execute(resolveSql(options.vs), {
90764
+ autoLimit: true
90765
+ });
90766
+ checks.push(compareVs(result, other, options.compare ?? "value"));
90767
+ }
90768
+ if (options.against) {
90769
+ const baseline = await readSnapshot(options.against);
90770
+ const current = buildFingerprint(result, {
90771
+ query: resolveSql(query),
90772
+ engine: config.connection.system
90773
+ });
90774
+ checks.push(...compareAgainst(current, baseline, options.tolerance));
90775
+ }
90776
+ verdict = { pass: checks.every((c2) => c2.pass), checks };
90777
+ await writeAuditEntry(config, "assert", options, {
90778
+ success: verdict.pass,
90779
+ sql: query
90780
+ });
90781
+ } finally {
90782
+ await adapter.disconnect();
90783
+ }
90784
+ if (options.format === "json") {
90785
+ console.log(JSON.stringify(verdict, null, 2));
90786
+ } else {
90787
+ for (const c2 of verdict.checks) {
90788
+ console.log(`${c2.pass ? "PASS" : "FAIL"} ${c2.name} expected=${c2.expected} actual=${c2.actual}`);
90789
+ }
90790
+ console.log(`
90791
+ Verdict: ${verdict.pass ? "PASS" : "FAIL"}`);
90792
+ }
90793
+ process.exit(verdict.pass || options.fail === false ? 0 : 1);
90794
+ } catch (error) {
90795
+ if (error instanceof Error) {
90796
+ console.error(error.message);
90797
+ if (error instanceof ConnectionError)
90798
+ error.hints.forEach((h) => console.error(` Hint: ${h}`));
90799
+ }
90800
+ process.exit(1);
90801
+ }
90802
+ });
90803
+
90358
90804
  // src/commands/recovery.ts
90359
90805
  init_message_loader();
90360
90806
  init_validation();
90361
90807
  init_recovery();
90362
- var ALLOWED_FORMATS12 = ["json", "markdown"];
90808
+ var ALLOWED_FORMATS14 = ["json", "markdown"];
90363
90809
  function parseCode(value) {
90364
90810
  const normalized = value.trim();
90365
90811
  if (!RECOVERY_CODES.includes(normalized)) {
@@ -90395,7 +90841,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
90395
90841
  const forAgent = options.forAgent === true;
90396
90842
  const format = forAgent ? "json" : options.format;
90397
90843
  const brief = forAgent || options.brief === true;
90398
- validateFormat(format, ALLOWED_FORMATS12, "recovery");
90844
+ validateFormat(format, ALLOWED_FORMATS14, "recovery");
90399
90845
  if (options.list === true) {
90400
90846
  if (format === "markdown") {
90401
90847
  console.log(renderCodeList());
@@ -90572,7 +91018,7 @@ function looksLikeSavedEnvelope(x) {
90572
91018
  init_next_step();
90573
91019
  init_next_step_schema();
90574
91020
  init_validation();
90575
- var ALLOWED_FORMATS13 = ["json", "markdown"];
91021
+ var ALLOWED_FORMATS15 = ["json", "markdown"];
90576
91022
  var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
90577
91023
 
90578
91024
  class RecoverCliError extends Error {
@@ -90724,7 +91170,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
90724
91170
  }
90725
91171
  const explicitFormat = options.format;
90726
91172
  const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
90727
- validateFormat(format, ALLOWED_FORMATS13, "recover");
91173
+ validateFormat(format, ALLOWED_FORMATS15, "recover");
90728
91174
  let allowWrite = "none";
90729
91175
  if (options.next !== true) {
90730
91176
  const allowWriteRaw = options.allowWrite;
@@ -90791,14 +91237,14 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
90791
91237
 
90792
91238
  // src/commands/audit.ts
90793
91239
  import { rm as rm3, stat as stat7 } from "fs/promises";
90794
- import { join as join26 } from "path";
91240
+ import { join as join27 } from "path";
90795
91241
  init_message_loader();
90796
91242
  init_validation();
90797
91243
  init_config();
90798
91244
  init_config_binding();
90799
91245
  init_reader();
90800
91246
  init_integration_helper();
90801
- var ALLOWED_FORMATS14 = ["table", "json"];
91247
+ var ALLOWED_FORMATS16 = ["table", "json"];
90802
91248
  var DEFAULT_TAIL_N = 10;
90803
91249
  var MAX_TAIL_N = 1e4;
90804
91250
  var SHORT_ID_LEN = 8;
@@ -90807,8 +91253,8 @@ var PREFIX_MIN = 4;
90807
91253
  async function resolveAuditPaths(configPath, config) {
90808
91254
  const storagePath = await resolveConfigStoragePath(configPath);
90809
91255
  const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
90810
- const auditDir = join26(storagePath, ".dbcli", "audit");
90811
- const auditFile = join26(auditDir, `${connName}.jsonl`);
91256
+ const auditDir = join27(storagePath, ".dbcli", "audit");
91257
+ const auditFile = join27(auditDir, `${connName}.jsonl`);
90812
91258
  return { auditDir, connectionName: connName, auditFile };
90813
91259
  }
90814
91260
  function isAuditDisabled(config) {
@@ -90992,13 +91438,13 @@ async function statAuditFile(file) {
90992
91438
  }
90993
91439
  }
90994
91440
  var auditCommand = new Command("audit").description(t("audit.description"));
90995
- auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
91441
+ auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
90996
91442
  const forAgent = options.forAgent === true;
90997
91443
  const format = forAgent ? "json" : options.format;
90998
91444
  const briefSource = command.getOptionValueSource("brief");
90999
91445
  const briefExplicit = briefSource !== undefined && briefSource !== "default";
91000
91446
  const brief = briefExplicit ? options.brief === true : forAgent;
91001
- validateFormat(format, ALLOWED_FORMATS14, "audit tail");
91447
+ validateFormat(format, ALLOWED_FORMATS16, "audit tail");
91002
91448
  const n = parseTailN(options.n);
91003
91449
  const configPath = resolveConfigPath(command, options);
91004
91450
  const config = await configModule.read(configPath);
@@ -91041,7 +91487,7 @@ auditCommand.command("tail").description(t("audit.tail.description")).option("--
91041
91487
  console.log(renderTailTable(tail));
91042
91488
  }
91043
91489
  });
91044
- auditCommand.command("show [id]").description(t("audit.show.description")).option("--all", "Search across all connections", false).option("--recovery-ref <ref>", "Look up by entry.recovery_ref (exact match)").option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim metadata + redacted_query", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (id, options, command) => {
91490
+ auditCommand.command("show [id]").description(t("audit.show.description")).option("--all", "Search across all connections", false).option("--recovery-ref <ref>", "Look up by entry.recovery_ref (exact match)").option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim metadata + redacted_query", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (id, options, command) => {
91045
91491
  if (id && options.recoveryRef) {
91046
91492
  console.error(t("audit.show_mutex_violation"));
91047
91493
  process.exit(1);
@@ -91055,7 +91501,7 @@ auditCommand.command("show [id]").description(t("audit.show.description")).optio
91055
91501
  const briefSource = command.getOptionValueSource("brief");
91056
91502
  const briefExplicit = briefSource !== undefined && briefSource !== "default";
91057
91503
  const brief = briefExplicit ? options.brief === true : forAgent;
91058
- validateFormat(format, ALLOWED_FORMATS14, "audit show");
91504
+ validateFormat(format, ALLOWED_FORMATS16, "audit show");
91059
91505
  const configPath = resolveConfigPath(command, options);
91060
91506
  const config = await configModule.read(configPath);
91061
91507
  if (isAuditDisabled(config))
@@ -91197,13 +91643,13 @@ auditCommand.command("clear").description(t("audit.clear.description")).option("
91197
91643
  `);
91198
91644
  process.exit(0);
91199
91645
  });
91200
- auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
91646
+ auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
91201
91647
  const forAgent = options.forAgent === true;
91202
91648
  const format = forAgent ? "json" : options.format;
91203
91649
  const briefSource = command.getOptionValueSource("brief");
91204
91650
  const briefExplicit = briefSource !== undefined && briefSource !== "default";
91205
91651
  const brief = briefExplicit ? options.brief === true : forAgent;
91206
- validateFormat(format, ALLOWED_FORMATS14, "audit health");
91652
+ validateFormat(format, ALLOWED_FORMATS16, "audit health");
91207
91653
  const configPath = resolveConfigPath(command, options);
91208
91654
  const config = await configModule.read(configPath);
91209
91655
  const logger = await getAuditLogger(config, configPath);
@@ -91235,15 +91681,15 @@ init_config_binding();
91235
91681
  init_schema_path();
91236
91682
  init_config();
91237
91683
  init_integration_helper();
91238
- import { join as join27 } from "path";
91684
+ import { join as join28 } from "path";
91239
91685
  import { resolveSrv as resolveSrv2 } from "dns/promises";
91240
- function requireSqlConnection9(connection) {
91686
+ function requireSqlConnection11(connection) {
91241
91687
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
91242
91688
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
91243
91689
  }
91244
91690
  return connection;
91245
91691
  }
91246
- var ALLOWED_FORMATS15 = ["text", "json"];
91692
+ var ALLOWED_FORMATS17 = ["text", "json"];
91247
91693
  var SENSITIVE_PATTERNS = [
91248
91694
  "password",
91249
91695
  "passwd",
@@ -91315,7 +91761,7 @@ var runDoctorChecks = {
91315
91761
  }
91316
91762
  },
91317
91763
  async checkConfigExists(configPath, existsFn) {
91318
- const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join27(configPath, "config.json")).exists();
91764
+ const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join28(configPath, "config.json")).exists();
91319
91765
  return {
91320
91766
  group: "Configuration",
91321
91767
  label: "Config exists",
@@ -91472,7 +91918,7 @@ var runDoctorChecks = {
91472
91918
  async checkV2Config(configPath) {
91473
91919
  const results = [];
91474
91920
  const storagePath = await resolveConfigStoragePath(configPath);
91475
- const configFile = Bun.file(join27(storagePath, "config.json"));
91921
+ const configFile = Bun.file(join28(storagePath, "config.json"));
91476
91922
  if (!await configFile.exists())
91477
91923
  return results;
91478
91924
  let raw;
@@ -91512,7 +91958,7 @@ var runDoctorChecks = {
91512
91958
  }
91513
91959
  for (const [name2, conn] of Object.entries(config.connections)) {
91514
91960
  if (conn.envFile) {
91515
- const envPath = join27(storagePath, conn.envFile);
91961
+ const envPath = join28(storagePath, conn.envFile);
91516
91962
  const exists = await Bun.file(envPath).exists();
91517
91963
  results.push({
91518
91964
  group: "Configuration",
@@ -91657,7 +92103,7 @@ async function collectElasticsearchDoctorResults(config) {
91657
92103
  return results;
91658
92104
  }
91659
92105
  var doctorCommand = new Command("doctor").description("Run diagnostic checks on dbcli configuration, environment, and connection").option("--format <type>", "Output format: text, json", "text").action(async (options) => {
91660
- validateFormat(options.format, ALLOWED_FORMATS15, "doctor");
92106
+ validateFormat(options.format, ALLOWED_FORMATS17, "doctor");
91661
92107
  const logger = getLogger();
91662
92108
  const results = [];
91663
92109
  const configPath = resolveConfigPath(doctorCommand);
@@ -91703,7 +92149,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
91703
92149
  blacklistedColumns
91704
92150
  }));
91705
92151
  } else {
91706
- const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection9(config.connection));
92152
+ const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection11(config.connection));
91707
92153
  await adapter.connect();
91708
92154
  results.push({
91709
92155
  group: "Connection & Data",
@@ -91731,7 +92177,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
91731
92177
  }
91732
92178
  try {
91733
92179
  const schemaConnName = await getSchemaIsolationConnectionName(configPath);
91734
- const indexPath = join27(resolveSchemaPath(storagePath, schemaConnName), "index.json");
92180
+ const indexPath = join28(resolveSchemaPath(storagePath, schemaConnName), "index.json");
91735
92181
  const indexFile = Bun.file(indexPath);
91736
92182
  let indexParsed = null;
91737
92183
  if (await indexFile.exists()) {
@@ -91785,7 +92231,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
91785
92231
 
91786
92232
  // src/commands/completion.ts
91787
92233
  init_colors();
91788
- import { join as join28 } from "path";
92234
+ import { join as join29 } from "path";
91789
92235
  import { homedir as homedir3 } from "os";
91790
92236
  function extractCommands(program2) {
91791
92237
  return program2.commands.map((cmd) => ({
@@ -91887,11 +92333,11 @@ function getInstallPath2(shell) {
91887
92333
  const home = homedir3();
91888
92334
  switch (shell) {
91889
92335
  case "bash":
91890
- return join28(home, ".bashrc");
92336
+ return join29(home, ".bashrc");
91891
92337
  case "zsh":
91892
- return join28(home, ".zshrc");
92338
+ return join29(home, ".zshrc");
91893
92339
  case "fish":
91894
- return join28(home, ".config", "fish", "completions", "dbcli.fish");
92340
+ return join29(home, ".config", "fish", "completions", "dbcli.fish");
91895
92341
  default:
91896
92342
  throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
91897
92343
  }
@@ -91911,7 +92357,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
91911
92357
  async function installCompletion(shell, script) {
91912
92358
  const targetPath = getInstallPath2(shell);
91913
92359
  if (shell === "fish") {
91914
- const dir = join28(homedir3(), ".config", "fish", "completions");
92360
+ const dir = join29(homedir3(), ".config", "fish", "completions");
91915
92361
  await Bun.$`mkdir -p ${dir}`.quiet();
91916
92362
  await Bun.file(targetPath).write(script);
91917
92363
  console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
@@ -92144,7 +92590,7 @@ ${t("upgrade.failed")}`));
92144
92590
  init_config();
92145
92591
  init_adapters();
92146
92592
  import { createInterface as createInterface3 } from "readline";
92147
- import { join as join29 } from "path";
92593
+ import { join as join30 } from "path";
92148
92594
  import { homedir as homedir4 } from "os";
92149
92595
 
92150
92596
  // src/core/repl/types.ts
@@ -93066,13 +93512,13 @@ async function runEsShell(configPath) {
93066
93512
  }
93067
93513
 
93068
93514
  // src/commands/shell.ts
93069
- function requireSqlConnection10(connection) {
93515
+ function requireSqlConnection12(connection) {
93070
93516
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
93071
93517
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
93072
93518
  }
93073
93519
  return connection;
93074
93520
  }
93075
- var HISTORY_PATH = join29(homedir4(), ".dbcli_history");
93521
+ var HISTORY_PATH = join30(homedir4(), ".dbcli_history");
93076
93522
  var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
93077
93523
  async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
93078
93524
  const columnsByTable = {};
@@ -93109,7 +93555,7 @@ async function runShell(options, configPath) {
93109
93555
  const connectionOpts = config.connection;
93110
93556
  const mongoInner = isMongoDB ? AdapterFactory.createMongoDBAdapter(connectionOpts) : null;
93111
93557
  const redisInner = isRedis ? AdapterFactory.createRedisAdapter(connectionOpts, config.blacklist?.tables ?? [], config.redis?.mask ?? []) : null;
93112
- const adapter = isMongoDB ? new MongoShellAdapter(mongoInner) : isRedis ? new RedisShellAdapter(redisInner) : AdapterFactory.createSqlAdapter(requireSqlConnection10(connectionOpts));
93558
+ const adapter = isMongoDB ? new MongoShellAdapter(mongoInner) : isRedis ? new RedisShellAdapter(redisInner) : AdapterFactory.createSqlAdapter(requireSqlConnection12(connectionOpts));
93113
93559
  try {
93114
93560
  await adapter.connect();
93115
93561
  } catch (error) {
@@ -93836,7 +94282,7 @@ class DDLExecutor {
93836
94282
  }
93837
94283
  }
93838
94284
  // src/commands/migrate.ts
93839
- function requireSqlConnection11(connection) {
94285
+ function requireSqlConnection13(connection) {
93840
94286
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
93841
94287
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
93842
94288
  }
@@ -93860,7 +94306,7 @@ async function runDDL(operation, opts) {
93860
94306
  console.error("\u6B64\u547D\u4EE4\u76EE\u524D\u4E0D\u652F\u63F4 Elasticsearch\uFF1B\u5982\u9700\u5EFA\u7ACB\u7D22\u5F15\u6216\u8ABF\u6574 mapping\uFF0C\u8ACB\u6539\u7528\u5916\u90E8\u5DE5\u5177");
93861
94307
  process.exit(1);
93862
94308
  }
93863
- const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection11(config.connection));
94309
+ const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection13(config.connection));
93864
94310
  const isDryRun = !opts.execute;
93865
94311
  if (!isDryRun) {
93866
94312
  await adapter.connect();
@@ -94071,7 +94517,7 @@ init_config();
94071
94517
  init_errors();
94072
94518
  init_message_loader();
94073
94519
  init_config_binding();
94074
- import { join as join30 } from "path";
94520
+ import { join as join31 } from "path";
94075
94521
  async function switchDefault(configPath, name2, config) {
94076
94522
  if (!config.connections[name2]) {
94077
94523
  const available = Object.keys(config.connections).join(", ");
@@ -94094,7 +94540,7 @@ function listConnectionsForDisplay(config) {
94094
94540
  }
94095
94541
  async function ensureV2Config(configPath) {
94096
94542
  const storagePath = await resolveConfigStoragePath(configPath);
94097
- const configFile = Bun.file(join30(storagePath, "config.json"));
94543
+ const configFile = Bun.file(join31(storagePath, "config.json"));
94098
94544
  const legacyFile = Bun.file(configPath);
94099
94545
  if (!await configFile.exists() && !await legacyFile.exists()) {
94100
94546
  throw new ConfigError(t("init.config_not_found"));
@@ -94158,7 +94604,7 @@ var useCommand = new Command("use").description("Switch or display the default d
94158
94604
 
94159
94605
  // src/cli.ts
94160
94606
  init_config();
94161
- import { join as join31 } from "path";
94607
+ import { join as join32 } from "path";
94162
94608
  var _bgVersionCheckResult;
94163
94609
  function shouldSkipBackgroundChecks() {
94164
94610
  return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
@@ -94187,7 +94633,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
94187
94633
  try {
94188
94634
  let cache = null;
94189
94635
  try {
94190
- const cacheFile = Bun.file(join31(configPath, "version-check.json"));
94636
+ const cacheFile = Bun.file(join32(configPath, "version-check.json"));
94191
94637
  if (await cacheFile.exists()) {
94192
94638
  cache = await cacheFile.json();
94193
94639
  }
@@ -94307,6 +94753,8 @@ program2.addCommand(migrateCommand);
94307
94753
  program2.addCommand(useCommand);
94308
94754
  program2.addCommand(queriesCommand);
94309
94755
  program2.addCommand(explainCommand);
94756
+ program2.addCommand(snapshotCommand);
94757
+ program2.addCommand(assertCommand);
94310
94758
  if (!process.argv.slice(2).length) {
94311
94759
  program2.outputHelp();
94312
94760
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.23.1",
3
+ "version": "1.25.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {