@carllee1983/dbcli 0.3.2-beta → 0.4.0-beta
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/assets/SKILL.md +327 -0
- package/dist/cli.mjs +167 -95
- package/package.json +2 -1
package/assets/SKILL.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dbcli
|
|
3
|
+
description: Database CLI for AI agents with permission-based access control. Use to query, inspect schemas, insert/update/delete data, export results, and manage sensitive data blacklists. Supports MySQL, PostgreSQL, MariaDB. Trigger when working with databases, running SQL, exploring table structures, or protecting sensitive columns/tables from AI access.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dbcli
|
|
7
|
+
|
|
8
|
+
Database CLI for AI agents with permission-based access control.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dbcli init # Initialize .dbcli config (parses .env automatically)
|
|
14
|
+
dbcli schema # Scan all tables and save to .dbcli
|
|
15
|
+
dbcli query "SELECT * FROM users" # Execute SQL
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
### init
|
|
21
|
+
|
|
22
|
+
Initialize `.dbcli` configuration file. Typically run manually by the developer — avoid running on behalf of the user unless explicitly requested.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
dbcli init
|
|
26
|
+
dbcli init --system mysql --host localhost --port 3306 --user root --name mydb
|
|
27
|
+
dbcli init --use-env-refs # Store env var references instead of values
|
|
28
|
+
dbcli init --no-interactive --force # Non-interactive, skip overwrite confirmation
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Key options:** `--system <postgresql|mysql|mariadb>`, `--permission <query-only|read-write|data-admin|admin>`, `--use-env-refs`, `--skip-test`, `--no-interactive`, `--force`
|
|
32
|
+
|
|
33
|
+
### list
|
|
34
|
+
|
|
35
|
+
List all tables.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dbcli list
|
|
39
|
+
dbcli list --format json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Permission:** query-only+
|
|
43
|
+
|
|
44
|
+
### schema
|
|
45
|
+
|
|
46
|
+
Display table schema or scan entire database.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
dbcli schema # Scan all tables, save to .dbcli
|
|
50
|
+
dbcli schema users # Show single table schema
|
|
51
|
+
dbcli schema users --format json
|
|
52
|
+
dbcli schema --refresh # Detect and apply schema changes
|
|
53
|
+
dbcli schema --reset # Clear all schema data and re-fetch
|
|
54
|
+
dbcli schema --reset --force # Skip confirmation
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Options:** `--format <table|json>`, `--refresh`, `--reset`, `--force`
|
|
58
|
+
**Permission:** query-only+
|
|
59
|
+
|
|
60
|
+
### query
|
|
61
|
+
|
|
62
|
+
Execute SQL query.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dbcli query "SELECT * FROM users LIMIT 10"
|
|
66
|
+
dbcli query "SELECT id, email FROM users" --format json
|
|
67
|
+
dbcli query "SELECT * FROM logs" --no-limit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Options:** `--format <table|json|csv>`, `--limit <number>`, `--no-limit`
|
|
71
|
+
**Permission:** query-only+
|
|
72
|
+
|
|
73
|
+
### insert
|
|
74
|
+
|
|
75
|
+
Insert data into a table.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dbcli insert users --data '{"name":"Alice","email":"alice@example.com"}'
|
|
79
|
+
dbcli insert users --data '{"name":"Alice"}' --dry-run
|
|
80
|
+
dbcli insert users --data '{"name":"Alice"}' --force
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Options:** `--data <json>`, `--dry-run`, `--force`
|
|
84
|
+
**Permission:** read-write+
|
|
85
|
+
|
|
86
|
+
### update
|
|
87
|
+
|
|
88
|
+
Update existing data.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
dbcli update users --where "id=1" --set '{"name":"Bob"}'
|
|
92
|
+
dbcli update users --where "id=1" --set '{"name":"Bob"}' --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Options:** `--where <condition>` (required), `--set <json>` (required), `--dry-run`, `--force`
|
|
96
|
+
**Permission:** read-write+
|
|
97
|
+
|
|
98
|
+
### delete
|
|
99
|
+
|
|
100
|
+
Delete data from a table.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
dbcli delete users --where "id=1"
|
|
104
|
+
dbcli delete users --where "id=1" --dry-run
|
|
105
|
+
dbcli delete users --where "id=1" --force
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Options:** `--where <condition>` (required), `--dry-run`, `--force`
|
|
109
|
+
**Permission:** data-admin+
|
|
110
|
+
|
|
111
|
+
### export
|
|
112
|
+
|
|
113
|
+
Export query results to file or stdout.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
dbcli export "SELECT * FROM users" --format csv --output users.csv
|
|
117
|
+
dbcli export "SELECT * FROM users" --format json | jq '.[]'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Options:** `--format <json|csv>` (required), `--output <path>`
|
|
121
|
+
**Permission:** query-only+
|
|
122
|
+
|
|
123
|
+
### blacklist
|
|
124
|
+
|
|
125
|
+
Manage sensitive data blacklist to prevent AI access to restricted tables/columns.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
dbcli blacklist list # Show current blacklist
|
|
129
|
+
dbcli blacklist table add payments # Block entire table
|
|
130
|
+
dbcli blacklist table remove payments # Unblock table
|
|
131
|
+
dbcli blacklist column add users.password # Block specific column
|
|
132
|
+
dbcli blacklist column remove users.password
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Subcommands:** `list`, `table add <name>`, `table remove <name>`, `column add <table.column>`, `column remove <table.column>`
|
|
136
|
+
|
|
137
|
+
### check
|
|
138
|
+
|
|
139
|
+
Run data health checks on tables.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
dbcli check users # Check single table
|
|
143
|
+
dbcli check users --format json # JSON output (default)
|
|
144
|
+
dbcli check --all # Check all tables (huge tables auto-skipped)
|
|
145
|
+
dbcli check --all --include-large # Include huge tables
|
|
146
|
+
dbcli check orders --checks nulls,orphans # Specific checks only
|
|
147
|
+
dbcli check orders --sample 10000 # Sample size for large tables
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Checks:** `nulls`, `duplicates`, `orphans`, `emptyStrings`, `rowCount`, `size`
|
|
151
|
+
**Options:** `--all`, `--include-large`, `--checks <types>`, `--sample <number>`, `--format <json|table>`
|
|
152
|
+
**Permission:** query-only+
|
|
153
|
+
|
|
154
|
+
### diff
|
|
155
|
+
|
|
156
|
+
Compare schema snapshots to detect changes.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
dbcli diff --snapshot before.json # Save current schema snapshot
|
|
160
|
+
dbcli diff --against before.json # Compare current vs snapshot
|
|
161
|
+
dbcli diff --against before.json --format json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Options:** `--snapshot <path>`, `--against <path>`, `--format <json|table>`
|
|
165
|
+
**Permission:** query-only+
|
|
166
|
+
|
|
167
|
+
### status
|
|
168
|
+
|
|
169
|
+
Show current configuration status (safe for AI agents, no credentials exposed).
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
dbcli status # JSON output (default)
|
|
173
|
+
dbcli status --format text # Human-readable text output
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Output:** `permission`, `system`, `blacklist` summary, `version`
|
|
177
|
+
**Permission:** query-only+
|
|
178
|
+
|
|
179
|
+
## Permission Levels
|
|
180
|
+
|
|
181
|
+
| Level | Allowed Operations |
|
|
182
|
+
|-------|-------------------|
|
|
183
|
+
| query-only | SELECT, list, schema, export |
|
|
184
|
+
| read-write | query-only + INSERT, UPDATE |
|
|
185
|
+
| data-admin | read-write + DELETE (full DML, no DDL) |
|
|
186
|
+
| admin | data-admin + DROP, ALTER, CREATE, TRUNCATE |
|
|
187
|
+
|
|
188
|
+
Set via `dbcli init --permission <level>` or in `.dbcli` config.
|
|
189
|
+
|
|
190
|
+
## Global Options
|
|
191
|
+
|
|
192
|
+
All commands support `--config <path>` to specify a custom config file (default: `.dbcli`).
|
|
193
|
+
|
|
194
|
+
## AI Agent Workflow
|
|
195
|
+
|
|
196
|
+
**Before any database operation, follow this sequence:**
|
|
197
|
+
|
|
198
|
+
1. `dbcli status` — Check current permission level and system info (safe — no credentials exposed)
|
|
199
|
+
2. `dbcli blacklist list` — Confirm sensitive data is protected
|
|
200
|
+
3. `dbcli schema <table> --format json` — Verify actual column names
|
|
201
|
+
4. Then execute `query` / `insert` / `update` / `export` / `delete` according to your permission level
|
|
202
|
+
|
|
203
|
+
**Never guess column names.** Naming conventions vary across projects (e.g. `frozen_balance` vs `freeze`, `amount` vs `balance_variable`). Always confirm with `schema` first.
|
|
204
|
+
|
|
205
|
+
## Debugging Workflow
|
|
206
|
+
|
|
207
|
+
When investigating a bug related to database state:
|
|
208
|
+
|
|
209
|
+
1. `dbcli schema <table> --format json` — Confirm actual columns and types
|
|
210
|
+
2. `dbcli check <table> --format json` — Quick health scan (nulls, orphans, duplicates)
|
|
211
|
+
3. `dbcli query "SELECT * FROM <table> WHERE <condition>" --format json` — Inspect the specific record
|
|
212
|
+
4. Follow foreign keys from schema to trace related tables
|
|
213
|
+
5. Repeat step 3 for each related table to verify referential integrity
|
|
214
|
+
|
|
215
|
+
**Key principle:** Let the data tell the story. Don't hypothesize before seeing actual state.
|
|
216
|
+
|
|
217
|
+
## Write Verification Workflow
|
|
218
|
+
|
|
219
|
+
After any INSERT or UPDATE:
|
|
220
|
+
|
|
221
|
+
1. `dbcli insert <table> --data '...' --dry-run` — Preview SQL first
|
|
222
|
+
2. Execute the actual insert/update (remove --dry-run)
|
|
223
|
+
3. `dbcli query "SELECT * FROM <table> WHERE <condition>" --format json` — Read back the written record
|
|
224
|
+
4. Compare the returned data against the intended values
|
|
225
|
+
5. If mismatch, check for triggers, default values, or blacklisted columns that may alter the result
|
|
226
|
+
|
|
227
|
+
## Migration Safety Workflow
|
|
228
|
+
|
|
229
|
+
Before and after running database migrations:
|
|
230
|
+
|
|
231
|
+
1. `dbcli diff --snapshot before.json` — Capture current schema
|
|
232
|
+
2. Run the migration
|
|
233
|
+
3. `dbcli diff --against before.json --format json` — Compare changes
|
|
234
|
+
4. Verify: added/removed/modified columns match the migration intent
|
|
235
|
+
5. `dbcli check <affected-tables> --format json` — Ensure no orphaned data from column drops or FK changes
|
|
236
|
+
|
|
237
|
+
## Health Check Workflow
|
|
238
|
+
|
|
239
|
+
Periodic or on-demand database health scan:
|
|
240
|
+
|
|
241
|
+
1. `dbcli check --all --format json` — Scan all tables (huge tables auto-skipped)
|
|
242
|
+
2. Review the summary: focus on orphans (broken FKs) and unexpected nulls
|
|
243
|
+
3. For any flagged issues, drill down with `dbcli query` to inspect specific records
|
|
244
|
+
4. Use `estimatedRowCount` and `sizeCategory` from schema to gauge table growth
|
|
245
|
+
|
|
246
|
+
## Code Generation from Schema
|
|
247
|
+
|
|
248
|
+
When setting up a new project or migrating frameworks (e.g., Laravel to Bun + Drizzle):
|
|
249
|
+
|
|
250
|
+
1. `dbcli schema --format json` — Export full database schema with FK, indexes, defaults, enums
|
|
251
|
+
2. Use the JSON output to generate ORM schema definitions (Drizzle, Prisma, TypeORM, etc.)
|
|
252
|
+
3. For each table, map:
|
|
253
|
+
- `primaryKey` + `autoIncrement` to ORM primary key decorator
|
|
254
|
+
- `foreignKey` to relation/reference definitions
|
|
255
|
+
- `indexes` to index declarations
|
|
256
|
+
- `enumValues` to TypeScript enums or union types
|
|
257
|
+
- `nullable` + `defaultValue` to column options
|
|
258
|
+
- `comment` to JSDoc or schema comments
|
|
259
|
+
4. `dbcli check --all --format json` — Verify data health before trusting existing data
|
|
260
|
+
5. After ORM setup, run a test query through the new ORM and compare results with `dbcli query` to validate correctness
|
|
261
|
+
|
|
262
|
+
## Logic Verification Workflow
|
|
263
|
+
|
|
264
|
+
Validate that application logic produces correct database state:
|
|
265
|
+
|
|
266
|
+
1. `dbcli query "SELECT * FROM <table> WHERE <condition>" --format json` — Capture state BEFORE
|
|
267
|
+
2. Execute the application logic (API call, script, etc.)
|
|
268
|
+
3. `dbcli query "SELECT * FROM <table> WHERE <condition>" --format json` — Capture state AFTER
|
|
269
|
+
4. Compare before/after:
|
|
270
|
+
- Were the expected rows created/updated/deleted?
|
|
271
|
+
- Are computed values correct (totals, balances, counters)?
|
|
272
|
+
- Did related tables update consistently?
|
|
273
|
+
5. For complex transactions, check ALL affected tables
|
|
274
|
+
6. `dbcli check <affected-tables> --format json` — Ensure no orphaned data post-operation
|
|
275
|
+
|
|
276
|
+
**When to use:** Unit tests mock the DB and may miss real constraint violations, triggers, and default values. dbcli verifies actual DB state — catches what mocks hide. Best practice: unit tests for logic, dbcli for integration truth.
|
|
277
|
+
|
|
278
|
+
## Natural Language Operations Workflow
|
|
279
|
+
|
|
280
|
+
When the user describes a database operation in plain language:
|
|
281
|
+
|
|
282
|
+
1. **Parse intent** — Identify the operation type:
|
|
283
|
+
- "查今天的訂單" → query (SELECT)
|
|
284
|
+
- "幫我新增一筆記事" → insert (INSERT)
|
|
285
|
+
- "把這筆訂單改成已出貨" → update (UPDATE)
|
|
286
|
+
|
|
287
|
+
2. **Resolve context** — Use schema to map natural language to actual columns:
|
|
288
|
+
- `dbcli schema <table> --format json` — Get real column names
|
|
289
|
+
- "今天的訂單" → `WHERE created_at >= CURDATE()` (verify column name from schema)
|
|
290
|
+
- "已出貨" → check status column's enum values or existing data patterns
|
|
291
|
+
|
|
292
|
+
3. **Infer missing fields** — Use schema defaults and context:
|
|
293
|
+
- `defaultValue` from schema → skip fields with sensible defaults
|
|
294
|
+
- `autoIncrement` → don't include primary key in INSERT
|
|
295
|
+
- `nullable: false` without default → MUST ask user for this value
|
|
296
|
+
|
|
297
|
+
4. **Safety gate**:
|
|
298
|
+
- `dbcli blacklist list` — Ensure no blacklisted columns in the operation
|
|
299
|
+
- Check `sizeCategory` — if querying a huge table without filter, warn and suggest conditions
|
|
300
|
+
- For writes: ALWAYS use `--dry-run` first, show the SQL, then confirm
|
|
301
|
+
|
|
302
|
+
5. **Execute and verify**:
|
|
303
|
+
- Run the operation
|
|
304
|
+
- For INSERT/UPDATE: read back with `dbcli query` to confirm
|
|
305
|
+
- Report result in natural language back to user
|
|
306
|
+
|
|
307
|
+
**Key principle:** Never guess column names or values. Always schema-first, dry-run-first.
|
|
308
|
+
|
|
309
|
+
## Notes
|
|
310
|
+
|
|
311
|
+
- **Use `--format json`**: More reliable for AI parsing than table format
|
|
312
|
+
- **Use `--dry-run` before writes**: Preview generated SQL before executing
|
|
313
|
+
- **auto-limit**: Query-only mode appends `LIMIT 1000` automatically. Use `--no-limit` for `information_schema` queries or statements incompatible with LIMIT
|
|
314
|
+
- **Blacklist scope**: Blacklisted tables/columns are automatically filtered from query results
|
|
315
|
+
|
|
316
|
+
## Data Volume Protection
|
|
317
|
+
|
|
318
|
+
Schema output includes `estimatedRowCount` and `sizeCategory` for each table:
|
|
319
|
+
|
|
320
|
+
| Category | Rows | Behavior |
|
|
321
|
+
|----------|------|----------|
|
|
322
|
+
| small | < 10K | No restrictions |
|
|
323
|
+
| medium | 10K - 100K | Suggest adding LIMIT/WHERE |
|
|
324
|
+
| large | 100K - 1M | Warning displayed |
|
|
325
|
+
| huge | > 1M | Full-table SELECT blocked without WHERE/LIMIT — use `--no-limit` to override |
|
|
326
|
+
|
|
327
|
+
**Always check `sizeCategory` before querying.** For `large`/`huge` tables, add WHERE conditions or reasonable LIMIT.
|
package/dist/cli.mjs
CHANGED
|
@@ -42758,7 +42758,7 @@ var {
|
|
|
42758
42758
|
// package.json
|
|
42759
42759
|
var package_default = {
|
|
42760
42760
|
name: "@carllee1983/dbcli",
|
|
42761
|
-
version: "0.
|
|
42761
|
+
version: "0.4.0-beta",
|
|
42762
42762
|
description: "Database CLI for AI agents",
|
|
42763
42763
|
type: "module",
|
|
42764
42764
|
publishConfig: {
|
|
@@ -42773,6 +42773,7 @@ var package_default = {
|
|
|
42773
42773
|
},
|
|
42774
42774
|
files: [
|
|
42775
42775
|
"dist/",
|
|
42776
|
+
"assets/",
|
|
42776
42777
|
"README.md",
|
|
42777
42778
|
"CHANGELOG.md",
|
|
42778
42779
|
"LICENSE"
|
|
@@ -42823,7 +42824,7 @@ var messages_default = {
|
|
|
42823
42824
|
prompt_user: "Database user: ",
|
|
42824
42825
|
prompt_password: "Database password: ",
|
|
42825
42826
|
prompt_name: "Database name: ",
|
|
42826
|
-
prompt_permission: "Permission level (query-only, read-write, admin): ",
|
|
42827
|
+
prompt_permission: "Permission level (query-only, read-write, data-admin, admin): ",
|
|
42827
42828
|
env_parse_note: "Note: Unable to parse .env configuration, using interactive prompts",
|
|
42828
42829
|
connection_testing: "Testing database connection...",
|
|
42829
42830
|
connection_success: "\u2713 Database connection successful",
|
|
@@ -42892,7 +42893,7 @@ var messages_default = {
|
|
|
42892
42893
|
description: "Delete data from a table",
|
|
42893
42894
|
prompt_where: "Enter WHERE clause (e.g., id=1): ",
|
|
42894
42895
|
confirm: "Delete rows from {table}? (y/n): ",
|
|
42895
|
-
admin_only: "Delete command requires admin permission"
|
|
42896
|
+
admin_only: "Delete command requires data-admin or admin permission"
|
|
42896
42897
|
},
|
|
42897
42898
|
export: {
|
|
42898
42899
|
description: "Export query results",
|
|
@@ -42932,7 +42933,7 @@ var messages_default2 = {
|
|
|
42932
42933
|
prompt_user: "\u8CC7\u6599\u5EAB\u4F7F\u7528\u8005\uFF1A",
|
|
42933
42934
|
prompt_password: "\u8CC7\u6599\u5EAB\u5BC6\u78BC\uFF1A",
|
|
42934
42935
|
prompt_name: "\u8CC7\u6599\u5EAB\u540D\u7A31\uFF1A",
|
|
42935
|
-
prompt_permission: "\u6B0A\u9650\u7B49\u7D1A\uFF08query-only, read-write, admin\uFF09\uFF1A",
|
|
42936
|
+
prompt_permission: "\u6B0A\u9650\u7B49\u7D1A\uFF08query-only, read-write, data-admin, admin\uFF09\uFF1A",
|
|
42936
42937
|
env_parse_note: "\u63D0\u793A\uFF1A\u7121\u6CD5\u89E3\u6790 .env \u914D\u7F6E\uFF0C\u4F7F\u7528\u4E92\u52D5\u5F0F\u63D0\u793A",
|
|
42937
42938
|
connection_testing: "\u6B63\u5728\u6E2C\u8A66\u8CC7\u6599\u5EAB\u9023\u63A5...",
|
|
42938
42939
|
connection_success: "\u2713 \u8CC7\u6599\u5EAB\u9023\u63A5\u6210\u529F",
|
|
@@ -43001,7 +43002,7 @@ var messages_default2 = {
|
|
|
43001
43002
|
description: "\u522A\u9664\u8868\u683C\u4E2D\u7684\u8CC7\u6599",
|
|
43002
43003
|
prompt_where: "\u8F38\u5165 WHERE \u5B50\u53E5 (\u4F8B\u5982\uFF1Aid=1)\uFF1A",
|
|
43003
43004
|
confirm: "\u522A\u9664 {table} \u4E2D\u7684\u5217\uFF1F (y/n)\uFF1A",
|
|
43004
|
-
admin_only: "\u522A\u9664\u547D\u4EE4\u9700\u8981\
|
|
43005
|
+
admin_only: "\u522A\u9664\u547D\u4EE4\u9700\u8981 data-admin \u6216 admin \u6B0A\u9650"
|
|
43005
43006
|
},
|
|
43006
43007
|
export: {
|
|
43007
43008
|
description: "\u532F\u51FA\u67E5\u8A62\u7D50\u679C",
|
|
@@ -43156,7 +43157,7 @@ function parseConnectionUrl(url) {
|
|
|
43156
43157
|
} else if (protocol === "mariadb") {
|
|
43157
43158
|
system = "mariadb";
|
|
43158
43159
|
} else {
|
|
43159
|
-
throw new Error(
|
|
43160
|
+
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
43160
43161
|
}
|
|
43161
43162
|
const host = parsed.hostname || "localhost";
|
|
43162
43163
|
const port = parsed.port !== "" ? parseInt(parsed.port, 10) : getDefaultsForSystem(system).port || 5432;
|
|
@@ -43165,7 +43166,7 @@ function parseConnectionUrl(url) {
|
|
|
43165
43166
|
const database = parsed.pathname.slice(1);
|
|
43166
43167
|
return { system, host, port, user, password, database };
|
|
43167
43168
|
} catch (error) {
|
|
43168
|
-
throw new EnvParseError(
|
|
43169
|
+
throw new EnvParseError(`Failed to parse DATABASE_URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
43169
43170
|
}
|
|
43170
43171
|
}
|
|
43171
43172
|
function parseEnvDatabase(env) {
|
|
@@ -43177,15 +43178,15 @@ function parseEnvDatabase(env) {
|
|
|
43177
43178
|
const user = env.DB_USER;
|
|
43178
43179
|
const database = env.DB_NAME;
|
|
43179
43180
|
if (!user) {
|
|
43180
|
-
throw new EnvParseError("
|
|
43181
|
+
throw new EnvParseError("DB_USER is required when using component format");
|
|
43181
43182
|
}
|
|
43182
43183
|
if (!database) {
|
|
43183
|
-
throw new EnvParseError("
|
|
43184
|
+
throw new EnvParseError("DB_NAME is required when using component format");
|
|
43184
43185
|
}
|
|
43185
43186
|
const defaults = getDefaultsForSystem(system);
|
|
43186
43187
|
const port = env.DB_PORT ? parseInt(env.DB_PORT, 10) : defaults.port || 5432;
|
|
43187
43188
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
43188
|
-
throw new EnvParseError(`DB_PORT
|
|
43189
|
+
throw new EnvParseError(`DB_PORT must be between 1 and 65535, got: ${env.DB_PORT}`);
|
|
43189
43190
|
}
|
|
43190
43191
|
return {
|
|
43191
43192
|
system,
|
|
@@ -47192,7 +47193,7 @@ var ConnectionConfigSchema = exports_external.object({
|
|
|
47192
47193
|
password: exports_external.union([exports_external.string(), EnvRefSchema]).default(""),
|
|
47193
47194
|
database: StringOrEnvRef
|
|
47194
47195
|
});
|
|
47195
|
-
var PermissionSchema = exports_external.enum(["query-only", "read-write", "admin"]).default("query-only");
|
|
47196
|
+
var PermissionSchema = exports_external.enum(["query-only", "read-write", "data-admin", "admin"]).default("query-only");
|
|
47196
47197
|
var MetadataSchema = exports_external.object({
|
|
47197
47198
|
createdAt: exports_external.string().datetime().optional(),
|
|
47198
47199
|
version: exports_external.string().default("1.0")
|
|
@@ -47237,14 +47238,14 @@ function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
|
47237
47238
|
if (!strict) {
|
|
47238
47239
|
return config;
|
|
47239
47240
|
}
|
|
47240
|
-
throw new ConfigError(
|
|
47241
|
-
` +
|
|
47242
|
-
` +
|
|
47241
|
+
throw new ConfigError(`Environment variable not defined: ${envKey}
|
|
47242
|
+
` + `Please set ${envKey} in .env or your environment.
|
|
47243
|
+
` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
|
|
47243
47244
|
}
|
|
47244
47245
|
if (parentKey === "port") {
|
|
47245
47246
|
const portNum = parseInt(value, 10);
|
|
47246
47247
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
47247
|
-
throw new ConfigError(`${envKey}
|
|
47248
|
+
throw new ConfigError(`${envKey} must be a valid port number (1-65535), got: ${value}`);
|
|
47248
47249
|
}
|
|
47249
47250
|
return portNum;
|
|
47250
47251
|
}
|
|
@@ -47307,9 +47308,9 @@ var configModule = {
|
|
|
47307
47308
|
return { ...DEFAULT_CONFIG };
|
|
47308
47309
|
} catch (error) {
|
|
47309
47310
|
if (error instanceof Error && error.message.includes("JSON")) {
|
|
47310
|
-
throw new ConfigError(
|
|
47311
|
+
throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
|
|
47311
47312
|
}
|
|
47312
|
-
throw new ConfigError(
|
|
47313
|
+
throw new ConfigError(`Failed to read .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
47313
47314
|
}
|
|
47314
47315
|
},
|
|
47315
47316
|
validate(raw) {
|
|
@@ -47317,7 +47318,7 @@ var configModule = {
|
|
|
47317
47318
|
return DbcliConfigSchema.parse(raw);
|
|
47318
47319
|
} catch (error) {
|
|
47319
47320
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47320
|
-
throw new ConfigError(
|
|
47321
|
+
throw new ConfigError(`Invalid .dbcli config structure: ${errorMessage}`);
|
|
47321
47322
|
}
|
|
47322
47323
|
},
|
|
47323
47324
|
merge(existing, updates) {
|
|
@@ -47367,8 +47368,7 @@ var configModule = {
|
|
|
47367
47368
|
await Bun.file(configPath).write(configJson);
|
|
47368
47369
|
if (password) {
|
|
47369
47370
|
const envPath = join(path, ".env.local");
|
|
47370
|
-
const envContent = `#
|
|
47371
|
-
# Database Credentials - DO NOT commit to git
|
|
47371
|
+
const envContent = `# Database Credentials - DO NOT commit to git
|
|
47372
47372
|
|
|
47373
47373
|
DBCLI_PASSWORD=${password}
|
|
47374
47374
|
`;
|
|
@@ -47383,7 +47383,7 @@ DBCLI_PASSWORD=${password}
|
|
|
47383
47383
|
if (error instanceof ConfigError) {
|
|
47384
47384
|
throw error;
|
|
47385
47385
|
}
|
|
47386
|
-
throw new ConfigError(
|
|
47386
|
+
throw new ConfigError(`Failed to write .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
47387
47387
|
}
|
|
47388
47388
|
}
|
|
47389
47389
|
};
|
|
@@ -47498,37 +47498,37 @@ function mapError(error, system, options) {
|
|
|
47498
47498
|
const errMsg = String(err?.message || String(error));
|
|
47499
47499
|
const errCode = String(err?.code || "");
|
|
47500
47500
|
if (errCode === "ECONNREFUSED" || errMsg.includes("refused")) {
|
|
47501
|
-
return new ConnectionError("ECONNREFUSED",
|
|
47502
|
-
|
|
47503
|
-
|
|
47504
|
-
|
|
47501
|
+
return new ConnectionError("ECONNREFUSED", `Cannot connect to ${options.host}:${options.port} \u2014 server is not running or not listening on this port`, [
|
|
47502
|
+
`Check that the ${system} service is running: ${system === "postgresql" ? "systemctl status postgresql" : "systemctl status mysql"}`,
|
|
47503
|
+
`Verify the port is correct: ${system === "postgresql" ? "default 5432" : "default 3306"}`,
|
|
47504
|
+
`Check that ${options.host} is reachable: ping ${options.host} or telnet ${options.host} ${options.port}`
|
|
47505
47505
|
]);
|
|
47506
47506
|
}
|
|
47507
47507
|
if (errCode === "ETIMEDOUT" || errMsg.includes("timeout") || errMsg.includes("timed out")) {
|
|
47508
|
-
return new ConnectionError("ETIMEDOUT",
|
|
47509
|
-
|
|
47510
|
-
|
|
47511
|
-
|
|
47508
|
+
return new ConnectionError("ETIMEDOUT", `Connection timed out (${options.timeout || 5000}ms) \u2014 may be blocked by a firewall or slow network`, [
|
|
47509
|
+
`Check firewall rules: ${system === "postgresql" ? "allow TCP 5432" : "allow TCP 3306"}`,
|
|
47510
|
+
`Increase timeout: edit .dbcli and add "timeout": 15000`,
|
|
47511
|
+
`Verify network connectivity: ping ${options.host} -c 3`
|
|
47512
47512
|
]);
|
|
47513
47513
|
}
|
|
47514
47514
|
if (errMsg.includes("authentication") || errMsg.includes("auth") || errMsg.includes("password") || errMsg.includes("access denied") || errMsg.includes("FATAL")) {
|
|
47515
|
-
return new ConnectionError("AUTH_FAILED",
|
|
47516
|
-
|
|
47517
|
-
|
|
47518
|
-
|
|
47515
|
+
return new ConnectionError("AUTH_FAILED", `Authentication failed \u2014 check your username or password`, [
|
|
47516
|
+
`Verify credentials: ${system === "postgresql" ? `psql -U ${options.user} -h ${options.host}` : `mysql -u ${options.user} -h ${options.host}`}`,
|
|
47517
|
+
`Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)`,
|
|
47518
|
+
`Re-run dbcli init to update credentials`
|
|
47519
47519
|
]);
|
|
47520
47520
|
}
|
|
47521
47521
|
if (errCode === "ENOTFOUND" || errMsg.includes("not found") || errMsg.includes("getaddrinfo")) {
|
|
47522
|
-
return new ConnectionError("ENOTFOUND",
|
|
47523
|
-
|
|
47524
|
-
|
|
47525
|
-
|
|
47522
|
+
return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
|
|
47523
|
+
`Check the hostname spelling: ${options.host}`,
|
|
47524
|
+
`Verify DNS resolution: nslookup ${options.host}`,
|
|
47525
|
+
`If using localhost, try 127.0.0.1 (IPv4 vs IPv6 issue)`
|
|
47526
47526
|
]);
|
|
47527
47527
|
}
|
|
47528
|
-
return new ConnectionError("UNKNOWN",
|
|
47529
|
-
|
|
47530
|
-
|
|
47531
|
-
|
|
47528
|
+
return new ConnectionError("UNKNOWN", `Connection failed: ${errMsg}`, [
|
|
47529
|
+
`Check connection parameters: host=${options.host}, port=${options.port}, user=${options.user}`,
|
|
47530
|
+
`View server logs: ${system === "postgresql" ? "postgresql.log" : "mysql.log"}`,
|
|
47531
|
+
`Try connecting directly with the ${system === "postgresql" ? "psql" : "mysql"} command-line tool`
|
|
47532
47532
|
]);
|
|
47533
47533
|
}
|
|
47534
47534
|
|
|
@@ -47554,7 +47554,7 @@ class PostgreSQLAdapter {
|
|
|
47554
47554
|
constructor(options) {
|
|
47555
47555
|
this.options = options;
|
|
47556
47556
|
if (options.port < 1 || options.port > 65535) {
|
|
47557
|
-
throw new Error(
|
|
47557
|
+
throw new Error(`Invalid port number: ${options.port}`);
|
|
47558
47558
|
}
|
|
47559
47559
|
}
|
|
47560
47560
|
async connect() {
|
|
@@ -47587,7 +47587,7 @@ class PostgreSQLAdapter {
|
|
|
47587
47587
|
}
|
|
47588
47588
|
async testConnection() {
|
|
47589
47589
|
if (!this.pool) {
|
|
47590
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47590
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47591
47591
|
}
|
|
47592
47592
|
try {
|
|
47593
47593
|
const result = await this.execute("SELECT 1 as count");
|
|
@@ -47598,7 +47598,7 @@ class PostgreSQLAdapter {
|
|
|
47598
47598
|
}
|
|
47599
47599
|
async execute(sql, params) {
|
|
47600
47600
|
if (!this.pool) {
|
|
47601
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47601
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47602
47602
|
}
|
|
47603
47603
|
try {
|
|
47604
47604
|
const result = params ? await this.pool.query(sql, params) : await this.pool.query(sql);
|
|
@@ -47609,7 +47609,7 @@ class PostgreSQLAdapter {
|
|
|
47609
47609
|
}
|
|
47610
47610
|
async listTables() {
|
|
47611
47611
|
if (!this.pool) {
|
|
47612
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47612
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47613
47613
|
}
|
|
47614
47614
|
try {
|
|
47615
47615
|
const query = `
|
|
@@ -47621,7 +47621,9 @@ class PostgreSQLAdapter {
|
|
|
47621
47621
|
WHEN 'v' THEN 'view'
|
|
47622
47622
|
WHEN 'm' THEN 'view'
|
|
47623
47623
|
ELSE 'table'
|
|
47624
|
-
END as table_type
|
|
47624
|
+
END as table_type,
|
|
47625
|
+
(SELECT COUNT(*) FROM information_schema.columns ic
|
|
47626
|
+
WHERE ic.table_schema = 'public' AND ic.table_name = c.relname)::int as column_count
|
|
47625
47627
|
FROM pg_class c
|
|
47626
47628
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
47627
47629
|
WHERE n.nspname = 'public'
|
|
@@ -47632,6 +47634,7 @@ class PostgreSQLAdapter {
|
|
|
47632
47634
|
return results.map((row) => ({
|
|
47633
47635
|
name: row.table_name,
|
|
47634
47636
|
columns: [],
|
|
47637
|
+
columnCount: row.column_count,
|
|
47635
47638
|
rowCount: Math.max(0, row.estimated_rows || 0),
|
|
47636
47639
|
engine: "PostgreSQL",
|
|
47637
47640
|
estimatedRowCount: Math.max(0, row.estimated_rows || 0),
|
|
@@ -47643,7 +47646,7 @@ class PostgreSQLAdapter {
|
|
|
47643
47646
|
}
|
|
47644
47647
|
async getTableSchema(tableName) {
|
|
47645
47648
|
if (!this.pool) {
|
|
47646
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47649
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47647
47650
|
}
|
|
47648
47651
|
try {
|
|
47649
47652
|
const columnQuery = `
|
|
@@ -47801,7 +47804,7 @@ class MySQLAdapter {
|
|
|
47801
47804
|
this.options = options;
|
|
47802
47805
|
this.system = options.system === "mariadb" ? "mariadb" : "mysql";
|
|
47803
47806
|
if (options.port < 1 || options.port > 65535) {
|
|
47804
|
-
throw new Error(
|
|
47807
|
+
throw new Error(`Invalid port number: ${options.port}`);
|
|
47805
47808
|
}
|
|
47806
47809
|
}
|
|
47807
47810
|
async connect() {
|
|
@@ -47828,7 +47831,7 @@ class MySQLAdapter {
|
|
|
47828
47831
|
}
|
|
47829
47832
|
async testConnection() {
|
|
47830
47833
|
if (!this.db) {
|
|
47831
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47834
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47832
47835
|
}
|
|
47833
47836
|
try {
|
|
47834
47837
|
const result = await this.execute("SELECT 1 as count");
|
|
@@ -47839,7 +47842,7 @@ class MySQLAdapter {
|
|
|
47839
47842
|
}
|
|
47840
47843
|
async execute(sql, params) {
|
|
47841
47844
|
if (!this.db) {
|
|
47842
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47845
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47843
47846
|
}
|
|
47844
47847
|
try {
|
|
47845
47848
|
const [rows] = params ? await this.db.execute(sql, params) : await this.db.execute(sql);
|
|
@@ -47850,7 +47853,7 @@ class MySQLAdapter {
|
|
|
47850
47853
|
}
|
|
47851
47854
|
async listTables() {
|
|
47852
47855
|
if (!this.db) {
|
|
47853
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47856
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47854
47857
|
}
|
|
47855
47858
|
try {
|
|
47856
47859
|
const query = `
|
|
@@ -47870,7 +47873,8 @@ class MySQLAdapter {
|
|
|
47870
47873
|
const results = await this.execute(query);
|
|
47871
47874
|
return results.map((row) => ({
|
|
47872
47875
|
name: row.table_name,
|
|
47873
|
-
columns:
|
|
47876
|
+
columns: [],
|
|
47877
|
+
columnCount: row.column_count,
|
|
47874
47878
|
rowCount: row.row_count || 0,
|
|
47875
47879
|
engine: row.engine,
|
|
47876
47880
|
estimatedRowCount: row.row_count || 0,
|
|
@@ -47882,7 +47886,7 @@ class MySQLAdapter {
|
|
|
47882
47886
|
}
|
|
47883
47887
|
async getTableSchema(tableName) {
|
|
47884
47888
|
if (!this.db) {
|
|
47885
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47889
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47886
47890
|
}
|
|
47887
47891
|
try {
|
|
47888
47892
|
const columnQuery = `
|
|
@@ -47993,12 +47997,12 @@ class AdapterFactory {
|
|
|
47993
47997
|
case "mariadb":
|
|
47994
47998
|
return new MySQLAdapter(options);
|
|
47995
47999
|
default:
|
|
47996
|
-
throw new Error(
|
|
48000
|
+
throw new Error(`Unsupported database system: ${options.system}`);
|
|
47997
48001
|
}
|
|
47998
48002
|
}
|
|
47999
48003
|
}
|
|
48000
48004
|
// src/commands/init.ts
|
|
48001
|
-
var initCommand = new Command("init").description("Initialize dbcli configuration with .env parsing and interactive prompts").option("--host <host>", "Database host").option("--port <port>", "Database port").option("--user <user>", "Database user").option("--password <password>", "Database password").option("--name <name>", "Database name").option("--system <system>", "Database system (postgresql, mysql, mariadb)").option("--permission <permission>", "Permission level (query-only, read-write, admin)", "query-only").option("--use-env-refs", "Generate config with environment variable references (for .env)", false).option("--env-host <var>", "Environment variable name for database host (when using --use-env-refs)").option("--env-port <var>", "Environment variable name for database port (when using --use-env-refs)").option("--env-user <var>", "Environment variable name for database user (when using --use-env-refs)").option("--env-password <var>", "Environment variable name for database password (when using --use-env-refs)").option("--env-database <var>", "Environment variable name for database name (when using --use-env-refs)").option("--skip-test", "Skip database connection test").option("--no-interactive", "Non-interactive mode (requires all values via flags)").option("--force", "Skip overwrite confirmation if .dbcli exists").action(async (options) => {
|
|
48005
|
+
var initCommand = new Command("init").description("Initialize dbcli configuration with .env parsing and interactive prompts").option("--host <host>", "Database host").option("--port <port>", "Database port").option("--user <user>", "Database user").option("--password <password>", "Database password").option("--name <name>", "Database name").option("--system <system>", "Database system (postgresql, mysql, mariadb)").option("--permission <permission>", "Permission level (query-only, read-write, data-admin, admin)", "query-only").option("--use-env-refs", "Generate config with environment variable references (for .env)", false).option("--env-host <var>", "Environment variable name for database host (when using --use-env-refs)").option("--env-port <var>", "Environment variable name for database port (when using --use-env-refs)").option("--env-user <var>", "Environment variable name for database user (when using --use-env-refs)").option("--env-password <var>", "Environment variable name for database password (when using --use-env-refs)").option("--env-database <var>", "Environment variable name for database name (when using --use-env-refs)").option("--skip-test", "Skip database connection test").option("--no-interactive", "Non-interactive mode (requires all values via flags)").option("--force", "Skip overwrite confirmation if .dbcli exists").action(async (options) => {
|
|
48002
48006
|
try {
|
|
48003
48007
|
await initCommandHandler(options);
|
|
48004
48008
|
} catch (error) {
|
|
@@ -48061,7 +48065,7 @@ async function initCommandHandler(options) {
|
|
|
48061
48065
|
"admin"
|
|
48062
48066
|
]);
|
|
48063
48067
|
}
|
|
48064
|
-
if (!["query-only", "read-write", "admin"].includes(permission2)) {
|
|
48068
|
+
if (!["query-only", "read-write", "data-admin", "admin"].includes(permission2)) {
|
|
48065
48069
|
throw new Error(`Invalid permission level: ${permission2}`);
|
|
48066
48070
|
}
|
|
48067
48071
|
const newConfig2 = configModule.merge(existingConfig, {
|
|
@@ -48107,10 +48111,11 @@ async function initCommandHandler(options) {
|
|
|
48107
48111
|
permission = await promptUser.select(t("init.prompt_permission"), [
|
|
48108
48112
|
"query-only",
|
|
48109
48113
|
"read-write",
|
|
48114
|
+
"data-admin",
|
|
48110
48115
|
"admin"
|
|
48111
48116
|
]);
|
|
48112
48117
|
}
|
|
48113
|
-
if (!["query-only", "read-write", "admin"].includes(permission)) {
|
|
48118
|
+
if (!["query-only", "read-write", "data-admin", "admin"].includes(permission)) {
|
|
48114
48119
|
throw new Error(`Invalid permission level: ${permission}`);
|
|
48115
48120
|
}
|
|
48116
48121
|
configForWrite = connection;
|
|
@@ -48238,7 +48243,7 @@ class TableListFormatter {
|
|
|
48238
48243
|
tables.forEach((t2) => {
|
|
48239
48244
|
table.push([
|
|
48240
48245
|
t2.name,
|
|
48241
|
-
t2.columns.length.toString(),
|
|
48246
|
+
(t2.columnCount ?? t2.columns.length).toString(),
|
|
48242
48247
|
(t2.rowCount ?? "?").toString(),
|
|
48243
48248
|
t2.engine || "N/A"
|
|
48244
48249
|
]);
|
|
@@ -48248,14 +48253,6 @@ class TableListFormatter {
|
|
|
48248
48253
|
}
|
|
48249
48254
|
// src/formatters/json-formatter.ts
|
|
48250
48255
|
init_size_category();
|
|
48251
|
-
|
|
48252
|
-
class JSONFormatter {
|
|
48253
|
-
format(data, options) {
|
|
48254
|
-
const spacing = options?.compact ? undefined : 2;
|
|
48255
|
-
return JSON.stringify(data, null, spacing);
|
|
48256
|
-
}
|
|
48257
|
-
}
|
|
48258
|
-
|
|
48259
48256
|
class TableSchemaJSONFormatter {
|
|
48260
48257
|
format(table, options) {
|
|
48261
48258
|
const spacing = options?.compact ? undefined : 2;
|
|
@@ -48413,8 +48410,15 @@ async function listAction(options) {
|
|
|
48413
48410
|
return;
|
|
48414
48411
|
}
|
|
48415
48412
|
if (options.format === "json") {
|
|
48416
|
-
const
|
|
48417
|
-
|
|
48413
|
+
const listOutput = tables.map((t2) => ({
|
|
48414
|
+
name: t2.name,
|
|
48415
|
+
columnCount: t2.columnCount ?? t2.columns.length,
|
|
48416
|
+
rowCount: t2.rowCount ?? 0,
|
|
48417
|
+
engine: t2.engine,
|
|
48418
|
+
estimatedRowCount: t2.estimatedRowCount ?? t2.rowCount ?? 0,
|
|
48419
|
+
tableType: t2.tableType ?? "table"
|
|
48420
|
+
}));
|
|
48421
|
+
console.log(JSON.stringify(listOutput, null, 2));
|
|
48418
48422
|
} else {
|
|
48419
48423
|
const formatter = new TableListFormatter;
|
|
48420
48424
|
console.log(formatter.format(tables));
|
|
@@ -48709,9 +48713,9 @@ async function handleFullDatabaseScan(adapter, config, options) {
|
|
|
48709
48713
|
};
|
|
48710
48714
|
await configModule.write(options.config, updatedConfig);
|
|
48711
48715
|
console.log(`
|
|
48712
|
-
\u2705
|
|
48713
|
-
console.log(` ${tables.length}
|
|
48714
|
-
console.log(`
|
|
48716
|
+
\u2705 Schema updated in .dbcli`);
|
|
48717
|
+
console.log(` ${tables.length} tables with full column details and relationships`);
|
|
48718
|
+
console.log(` Timestamp: ${updatedConfig.metadata.schemaLastUpdated}`);
|
|
48715
48719
|
}
|
|
48716
48720
|
|
|
48717
48721
|
// src/core/permission-guard.ts
|
|
@@ -48934,6 +48938,29 @@ function checkPermission(sql, permission) {
|
|
|
48934
48938
|
classification
|
|
48935
48939
|
};
|
|
48936
48940
|
}
|
|
48941
|
+
if (permission === "data-admin") {
|
|
48942
|
+
const allowedTypes = [
|
|
48943
|
+
"SELECT",
|
|
48944
|
+
"INSERT",
|
|
48945
|
+
"UPDATE",
|
|
48946
|
+
"DELETE",
|
|
48947
|
+
"SHOW",
|
|
48948
|
+
"DESCRIBE",
|
|
48949
|
+
"EXPLAIN"
|
|
48950
|
+
];
|
|
48951
|
+
if (allowedTypes.includes(classification.type)) {
|
|
48952
|
+
return {
|
|
48953
|
+
allowed: true,
|
|
48954
|
+
reason: `${classification.type} operation allowed in data-admin mode`,
|
|
48955
|
+
classification
|
|
48956
|
+
};
|
|
48957
|
+
}
|
|
48958
|
+
return {
|
|
48959
|
+
allowed: false,
|
|
48960
|
+
reason: `${classification.type} operation requires admin permission`,
|
|
48961
|
+
classification
|
|
48962
|
+
};
|
|
48963
|
+
}
|
|
48937
48964
|
if (permission === "read-write") {
|
|
48938
48965
|
const allowedTypes = [
|
|
48939
48966
|
"SELECT",
|
|
@@ -48952,7 +48979,7 @@ function checkPermission(sql, permission) {
|
|
|
48952
48979
|
}
|
|
48953
48980
|
return {
|
|
48954
48981
|
allowed: false,
|
|
48955
|
-
reason: `${classification.type} operation requires admin permission`,
|
|
48982
|
+
reason: `${classification.type} operation requires data-admin or admin permission`,
|
|
48956
48983
|
classification
|
|
48957
48984
|
};
|
|
48958
48985
|
}
|
|
@@ -49438,7 +49465,7 @@ class DataExecutor {
|
|
|
49438
49465
|
const dataKeys = Object.keys(data);
|
|
49439
49466
|
for (const key2 of dataKeys) {
|
|
49440
49467
|
if (!columnNames.includes(key2)) {
|
|
49441
|
-
throw new Error(
|
|
49468
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}". Valid columns: ${columnNames.join(", ")}`);
|
|
49442
49469
|
}
|
|
49443
49470
|
}
|
|
49444
49471
|
const columns = dataKeys;
|
|
@@ -49471,12 +49498,12 @@ class DataExecutor {
|
|
|
49471
49498
|
}
|
|
49472
49499
|
if (!options?.force) {
|
|
49473
49500
|
console.log(`
|
|
49474
|
-
|
|
49501
|
+
Generated SQL:`);
|
|
49475
49502
|
console.log(` ${sql}`);
|
|
49476
49503
|
console.log(`
|
|
49477
|
-
|
|
49504
|
+
Parameters:`);
|
|
49478
49505
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49479
|
-
const confirmed = await promptUser.confirm("
|
|
49506
|
+
const confirmed = await promptUser.confirm("Proceed with this operation?");
|
|
49480
49507
|
if (!confirmed) {
|
|
49481
49508
|
return {
|
|
49482
49509
|
status: "success",
|
|
@@ -49507,7 +49534,7 @@ class DataExecutor {
|
|
|
49507
49534
|
operation: "insert",
|
|
49508
49535
|
rows_affected: 0,
|
|
49509
49536
|
timestamp,
|
|
49510
|
-
error: "
|
|
49537
|
+
error: "Permission denied: Query-only mode only allows SELECT. Use Read-Write or Admin mode to execute INSERT."
|
|
49511
49538
|
};
|
|
49512
49539
|
}
|
|
49513
49540
|
return {
|
|
@@ -49515,7 +49542,7 @@ class DataExecutor {
|
|
|
49515
49542
|
operation: "insert",
|
|
49516
49543
|
rows_affected: 0,
|
|
49517
49544
|
timestamp,
|
|
49518
|
-
error: `INSERT
|
|
49545
|
+
error: `INSERT failed: ${errorMessage}`
|
|
49519
49546
|
};
|
|
49520
49547
|
}
|
|
49521
49548
|
}
|
|
@@ -49538,12 +49565,12 @@ class DataExecutor {
|
|
|
49538
49565
|
}
|
|
49539
49566
|
if (!options?.force) {
|
|
49540
49567
|
console.log(`
|
|
49541
|
-
|
|
49568
|
+
Generated SQL:`);
|
|
49542
49569
|
console.log(` ${sql}`);
|
|
49543
49570
|
console.log(`
|
|
49544
|
-
|
|
49571
|
+
Parameters:`);
|
|
49545
49572
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49546
|
-
const confirmed = await promptUser.confirm("
|
|
49573
|
+
const confirmed = await promptUser.confirm("Proceed with this operation?");
|
|
49547
49574
|
if (!confirmed) {
|
|
49548
49575
|
return {
|
|
49549
49576
|
status: "success",
|
|
@@ -49574,7 +49601,7 @@ class DataExecutor {
|
|
|
49574
49601
|
operation: "update",
|
|
49575
49602
|
rows_affected: 0,
|
|
49576
49603
|
timestamp,
|
|
49577
|
-
error: "
|
|
49604
|
+
error: "Permission denied: Query-only mode only allows SELECT. Use Read-Write or Admin mode to execute UPDATE."
|
|
49578
49605
|
};
|
|
49579
49606
|
}
|
|
49580
49607
|
return {
|
|
@@ -49582,7 +49609,7 @@ class DataExecutor {
|
|
|
49582
49609
|
operation: "update",
|
|
49583
49610
|
rows_affected: 0,
|
|
49584
49611
|
timestamp,
|
|
49585
|
-
error: `UPDATE
|
|
49612
|
+
error: `UPDATE failed: ${errorMessage}`
|
|
49586
49613
|
};
|
|
49587
49614
|
}
|
|
49588
49615
|
}
|
|
@@ -49592,13 +49619,13 @@ class DataExecutor {
|
|
|
49592
49619
|
if (this.blacklistValidator) {
|
|
49593
49620
|
this.blacklistValidator.checkTableBlacklist("DELETE", tableName, []);
|
|
49594
49621
|
}
|
|
49595
|
-
if (this.permission !== "admin") {
|
|
49622
|
+
if (this.permission !== "data-admin" && this.permission !== "admin") {
|
|
49596
49623
|
return {
|
|
49597
49624
|
status: "error",
|
|
49598
49625
|
operation: "delete",
|
|
49599
49626
|
rows_affected: 0,
|
|
49600
49627
|
timestamp,
|
|
49601
|
-
error: "
|
|
49628
|
+
error: "Permission denied: DELETE operation requires Data-Admin or Admin permission."
|
|
49602
49629
|
};
|
|
49603
49630
|
}
|
|
49604
49631
|
const { sql, params } = this.buildDeleteSql(tableName, where, schema);
|
|
@@ -49613,14 +49640,14 @@ class DataExecutor {
|
|
|
49613
49640
|
}
|
|
49614
49641
|
if (!options?.force) {
|
|
49615
49642
|
console.log(`
|
|
49616
|
-
\u26A0\uFE0F
|
|
49643
|
+
\u26A0\uFE0F Warning: DELETE operation is destructive and cannot be undone!`);
|
|
49617
49644
|
console.log(`
|
|
49618
|
-
|
|
49645
|
+
Generated SQL:`);
|
|
49619
49646
|
console.log(` ${sql}`);
|
|
49620
49647
|
console.log(`
|
|
49621
|
-
|
|
49648
|
+
Parameters:`);
|
|
49622
49649
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49623
|
-
const confirmed = await promptUser.confirm("
|
|
49650
|
+
const confirmed = await promptUser.confirm("Are you sure you want to execute this DELETE operation? This cannot be undone.");
|
|
49624
49651
|
if (!confirmed) {
|
|
49625
49652
|
return {
|
|
49626
49653
|
status: "success",
|
|
@@ -49650,7 +49677,7 @@ class DataExecutor {
|
|
|
49650
49677
|
operation: "delete",
|
|
49651
49678
|
rows_affected: 0,
|
|
49652
49679
|
timestamp,
|
|
49653
|
-
error: `DELETE
|
|
49680
|
+
error: `DELETE failed: ${errorMessage}`
|
|
49654
49681
|
};
|
|
49655
49682
|
}
|
|
49656
49683
|
}
|
|
@@ -49666,7 +49693,7 @@ class DataExecutor {
|
|
|
49666
49693
|
const columnNames = schema.columns.map((col) => col.name);
|
|
49667
49694
|
for (const key2 of [...dataKeys, ...whereKeys]) {
|
|
49668
49695
|
if (!columnNames.includes(key2)) {
|
|
49669
|
-
throw new Error(
|
|
49696
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}"`);
|
|
49670
49697
|
}
|
|
49671
49698
|
}
|
|
49672
49699
|
const systemType = this.getSystemType();
|
|
@@ -49689,7 +49716,7 @@ class DataExecutor {
|
|
|
49689
49716
|
const columnNames = schema.columns.map((col) => col.name);
|
|
49690
49717
|
for (const key2 of whereKeys) {
|
|
49691
49718
|
if (!columnNames.includes(key2)) {
|
|
49692
|
-
throw new Error(
|
|
49719
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}"`);
|
|
49693
49720
|
}
|
|
49694
49721
|
}
|
|
49695
49722
|
const systemType = this.getSystemType();
|
|
@@ -49983,7 +50010,7 @@ async function deleteCommand(table, options) {
|
|
|
49983
50010
|
if (!config.connection) {
|
|
49984
50011
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
49985
50012
|
}
|
|
49986
|
-
if (config.permission !== "admin") {
|
|
50013
|
+
if (config.permission !== "data-admin" && config.permission !== "admin") {
|
|
49987
50014
|
throw new PermissionError(t("delete.admin_only"), { type: "DELETE", isDangerous: true, keywords: ["DELETE"], isComposite: false, confidence: "HIGH" }, config.permission);
|
|
49988
50015
|
}
|
|
49989
50016
|
const adapter = AdapterFactory.createAdapter(config.connection);
|
|
@@ -50025,7 +50052,7 @@ async function deleteCommand(table, options) {
|
|
|
50025
50052
|
process.exit(1);
|
|
50026
50053
|
}
|
|
50027
50054
|
if (error instanceof PermissionError) {
|
|
50028
|
-
console.error(t_vars("errors.permission_denied", { required: "admin" }));
|
|
50055
|
+
console.error(t_vars("errors.permission_denied", { required: "data-admin" }));
|
|
50029
50056
|
console.error(` Operation: ${error.classification.type}`);
|
|
50030
50057
|
console.error(` Message: ${error.message}`);
|
|
50031
50058
|
process.exit(1);
|
|
@@ -50099,7 +50126,17 @@ async function exportCommand(sql, options) {
|
|
|
50099
50126
|
// src/commands/skill.ts
|
|
50100
50127
|
import * as path from "path";
|
|
50101
50128
|
import { homedir } from "os";
|
|
50102
|
-
|
|
50129
|
+
function findPackageRoot() {
|
|
50130
|
+
let dir = import.meta.dir;
|
|
50131
|
+
for (let i = 0;i < 5; i++) {
|
|
50132
|
+
if (Bun.file(path.join(dir, "package.json")).size > 0) {
|
|
50133
|
+
return dir;
|
|
50134
|
+
}
|
|
50135
|
+
dir = path.dirname(dir);
|
|
50136
|
+
}
|
|
50137
|
+
return path.resolve(import.meta.dir, "../..");
|
|
50138
|
+
}
|
|
50139
|
+
var SKILL_SOURCE_PATH = path.join(findPackageRoot(), "assets", "SKILL.md");
|
|
50103
50140
|
async function skillCommand(_program, options) {
|
|
50104
50141
|
try {
|
|
50105
50142
|
const skillFile = Bun.file(SKILL_SOURCE_PATH);
|
|
@@ -50753,6 +50790,40 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
|
|
|
50753
50790
|
}
|
|
50754
50791
|
}
|
|
50755
50792
|
|
|
50793
|
+
// src/commands/status.ts
|
|
50794
|
+
var statusCommand = new Command("status").description("Show current configuration status (safe for AI agents, no credentials exposed)").option("--format <type>", "Output format: text, json", "json").action(async (options) => {
|
|
50795
|
+
try {
|
|
50796
|
+
const config = await configModule.read(".dbcli");
|
|
50797
|
+
if (!config.connection) {
|
|
50798
|
+
console.error('No configuration found. Run "dbcli init" first.');
|
|
50799
|
+
process.exit(1);
|
|
50800
|
+
}
|
|
50801
|
+
const blacklistTables = config.blacklist?.tables ?? [];
|
|
50802
|
+
const blacklistColumns = config.blacklist?.columns ?? {};
|
|
50803
|
+
const columnCount = Object.values(blacklistColumns).reduce((sum, cols) => sum + cols.length, 0);
|
|
50804
|
+
const status = {
|
|
50805
|
+
permission: config.permission,
|
|
50806
|
+
system: config.connection.system,
|
|
50807
|
+
blacklist: {
|
|
50808
|
+
tables: blacklistTables.length,
|
|
50809
|
+
columns: columnCount
|
|
50810
|
+
},
|
|
50811
|
+
version: config.metadata?.version ?? "unknown"
|
|
50812
|
+
};
|
|
50813
|
+
if (options.format === "text") {
|
|
50814
|
+
console.log(`Permission: ${status.permission}`);
|
|
50815
|
+
console.log(`System: ${status.system}`);
|
|
50816
|
+
console.log(`Blacklist: ${status.blacklist.tables} table(s), ${status.blacklist.columns} column(s)`);
|
|
50817
|
+
console.log(`Version: ${status.version}`);
|
|
50818
|
+
} else {
|
|
50819
|
+
console.log(JSON.stringify(status, null, 2));
|
|
50820
|
+
}
|
|
50821
|
+
} catch (error) {
|
|
50822
|
+
console.error(`Error: ${error.message}`);
|
|
50823
|
+
process.exit(1);
|
|
50824
|
+
}
|
|
50825
|
+
});
|
|
50826
|
+
|
|
50756
50827
|
// src/cli.ts
|
|
50757
50828
|
var program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--config <path>", "Path to .dbcli config file", ".dbcli");
|
|
50758
50829
|
program2.addCommand(initCommand);
|
|
@@ -50813,6 +50884,7 @@ program2.command("skill").description(t("skill.description")).option("--install
|
|
|
50813
50884
|
program2.addCommand(blacklistCommand);
|
|
50814
50885
|
program2.addCommand(checkCommand);
|
|
50815
50886
|
program2.addCommand(diffCommand);
|
|
50887
|
+
program2.addCommand(statusCommand);
|
|
50816
50888
|
if (!process.argv.slice(2).length) {
|
|
50817
50889
|
program2.outputHelp();
|
|
50818
50890
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carllee1983/dbcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta",
|
|
4
4
|
"description": "Database CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist/",
|
|
18
|
+
"assets/",
|
|
18
19
|
"README.md",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"LICENSE"
|