@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 +18 -0
- package/README.md +47 -1
- package/README.zh-TW.md +47 -1
- package/assets/SKILL.md +3 -1
- package/assets/SKILL.zh-TW.md +3 -1
- package/assets/reference.md +52 -6
- package/dist/cli.mjs +488 -40
- package/package.json +1 -1
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.
|
package/assets/SKILL.zh-TW.md
CHANGED
|
@@ -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 連線,但不改變預設值。
|
package/assets/reference.md
CHANGED
|
@@ -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
|
|
1142
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
90811
|
-
const auditFile =
|
|
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: ${
|
|
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,
|
|
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: ${
|
|
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,
|
|
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: ${
|
|
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,
|
|
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
|
|
91684
|
+
import { join as join28 } from "path";
|
|
91239
91685
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
91240
|
-
function
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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,
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
92336
|
+
return join29(home, ".bashrc");
|
|
91891
92337
|
case "zsh":
|
|
91892
|
-
return
|
|
92338
|
+
return join29(home, ".zshrc");
|
|
91893
92339
|
case "fish":
|
|
91894
|
-
return
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
}
|