@carllee1983/dbcli 0.3.1-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 +389 -115
- 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"
|
|
@@ -42812,8 +42813,231 @@ var package_default = {
|
|
|
42812
42813
|
vitest: "^1.2.0"
|
|
42813
42814
|
}
|
|
42814
42815
|
};
|
|
42816
|
+
// resources/lang/en/messages.json
|
|
42817
|
+
var messages_default = {
|
|
42818
|
+
init: {
|
|
42819
|
+
description: "Initialize dbcli configuration with .env parsing and interactive prompts",
|
|
42820
|
+
welcome: "Welcome to dbcli",
|
|
42821
|
+
select_system: "Select database system:",
|
|
42822
|
+
prompt_host: "Database host: ",
|
|
42823
|
+
prompt_port: "Database port: ",
|
|
42824
|
+
prompt_user: "Database user: ",
|
|
42825
|
+
prompt_password: "Database password: ",
|
|
42826
|
+
prompt_name: "Database name: ",
|
|
42827
|
+
prompt_permission: "Permission level (query-only, read-write, data-admin, admin): ",
|
|
42828
|
+
env_parse_note: "Note: Unable to parse .env configuration, using interactive prompts",
|
|
42829
|
+
connection_testing: "Testing database connection...",
|
|
42830
|
+
connection_success: "\u2713 Database connection successful",
|
|
42831
|
+
connection_failed: "\u2717 Database connection failed",
|
|
42832
|
+
config_saved: "Configuration saved to .dbcli",
|
|
42833
|
+
config_exists_overwrite: "Configuration file .dbcli already exists. Overwrite? (y/n): ",
|
|
42834
|
+
cancelled: "Cancelled. Configuration not changed."
|
|
42835
|
+
},
|
|
42836
|
+
schema: {
|
|
42837
|
+
description: "Retrieve table structure or list all tables",
|
|
42838
|
+
fetching: "Fetching schema...",
|
|
42839
|
+
success: "Schema updated",
|
|
42840
|
+
not_found: "Table not found in database",
|
|
42841
|
+
refresh_prompt: "Schema has changes. Apply? (y/n): ",
|
|
42842
|
+
columns_header: "Columns",
|
|
42843
|
+
scanning_database: "\uD83D\uDD0D Scanning database schema...",
|
|
42844
|
+
tables_found: "\uD83D\uDCCD Found {count} table(s). Fetching schema details...",
|
|
42845
|
+
processing_tables: " Processed {processed}/{total} table(s)",
|
|
42846
|
+
schema_exists_warning: "\u26A0\uFE0F Database schema already exists in .dbcli",
|
|
42847
|
+
use_force_to_override: " Use --force to override without confirmation"
|
|
42848
|
+
},
|
|
42849
|
+
list: {
|
|
42850
|
+
description: "List all tables in the database",
|
|
42851
|
+
no_tables: "No tables found in database"
|
|
42852
|
+
},
|
|
42853
|
+
query: {
|
|
42854
|
+
description: "Execute SQL query",
|
|
42855
|
+
executing: "Executing query...",
|
|
42856
|
+
no_results: "No results returned",
|
|
42857
|
+
result_count: "Results: {count} row(s)"
|
|
42858
|
+
},
|
|
42859
|
+
errors: {
|
|
42860
|
+
message: "Error: {message}",
|
|
42861
|
+
invalid_config: "Invalid configuration: {field}",
|
|
42862
|
+
connection_failed: "Failed to connect to database: {message}",
|
|
42863
|
+
permission_denied: "Permission denied (required: {required})",
|
|
42864
|
+
table_not_found: "Table not found: {table}",
|
|
42865
|
+
unsupported_lang: "Language '{lang}' not supported, using English",
|
|
42866
|
+
invalid_json: "Invalid JSON: {message}",
|
|
42867
|
+
table_blacklisted: "Error: Table '{table}' is blacklisted for {operation} operations",
|
|
42868
|
+
table_already_blacklisted: "Error: Table '{table}' is already blacklisted",
|
|
42869
|
+
table_not_in_blacklist: "Error: Table '{table}' is not in the blacklist",
|
|
42870
|
+
column_already_blacklisted: "Error: Column '{table}.{column}' is already blacklisted",
|
|
42871
|
+
column_not_in_blacklist: "Error: Column '{table}.{column}' is not in the blacklist",
|
|
42872
|
+
invalid_table_name: "Error: Invalid table name: {table}",
|
|
42873
|
+
invalid_column_format: "Error: Invalid column format. Use 'table.column'"
|
|
42874
|
+
},
|
|
42875
|
+
success: {
|
|
42876
|
+
inserted: "Successfully inserted {count} row(s)",
|
|
42877
|
+
updated: "Successfully updated {count} row(s)",
|
|
42878
|
+
deleted: "Successfully deleted {count} row(s)",
|
|
42879
|
+
no_changes: "No changes to apply"
|
|
42880
|
+
},
|
|
42881
|
+
insert: {
|
|
42882
|
+
description: "Insert data into a table",
|
|
42883
|
+
prompt_data: "Enter JSON data to insert: ",
|
|
42884
|
+
confirm: "Insert {count} row(s) into {table}? (y/n): "
|
|
42885
|
+
},
|
|
42886
|
+
update: {
|
|
42887
|
+
description: "Update data in a table",
|
|
42888
|
+
prompt_where: "Enter WHERE clause (e.g., id=1): ",
|
|
42889
|
+
prompt_set: 'Enter JSON updates (e.g., {"status": "active"}): ',
|
|
42890
|
+
confirm: "Update rows in {table}? (y/n): "
|
|
42891
|
+
},
|
|
42892
|
+
delete: {
|
|
42893
|
+
description: "Delete data from a table",
|
|
42894
|
+
prompt_where: "Enter WHERE clause (e.g., id=1): ",
|
|
42895
|
+
confirm: "Delete rows from {table}? (y/n): ",
|
|
42896
|
+
admin_only: "Delete command requires data-admin or admin permission"
|
|
42897
|
+
},
|
|
42898
|
+
export: {
|
|
42899
|
+
description: "Export query results",
|
|
42900
|
+
formats: "Supported formats: json, csv",
|
|
42901
|
+
exported: "Exported {count} rows to {file}"
|
|
42902
|
+
},
|
|
42903
|
+
skill: {
|
|
42904
|
+
description: "Generate AI skill documentation",
|
|
42905
|
+
installed: "Skill installed to {path}"
|
|
42906
|
+
},
|
|
42907
|
+
blacklist: {
|
|
42908
|
+
description: "Manage sensitive data blacklist to prevent AI access",
|
|
42909
|
+
list_title: "Current Blacklist Configuration",
|
|
42910
|
+
tables_label: "Blacklisted tables",
|
|
42911
|
+
columns_label: "Blacklisted columns",
|
|
42912
|
+
none: "No tables or columns are currently blacklisted",
|
|
42913
|
+
table_added: "\u2713 Table '{table}' added to blacklist",
|
|
42914
|
+
table_removed: "\u2713 Table '{table}' removed from blacklist",
|
|
42915
|
+
column_added: "\u2713 Column '{table}.{column}' added to blacklist",
|
|
42916
|
+
column_removed: "\u2713 Column '{table}.{column}' removed from blacklist"
|
|
42917
|
+
},
|
|
42918
|
+
security: {
|
|
42919
|
+
columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
|
|
42920
|
+
},
|
|
42921
|
+
warnings: {
|
|
42922
|
+
blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'"
|
|
42923
|
+
}
|
|
42924
|
+
};
|
|
42925
|
+
// resources/lang/zh-TW/messages.json
|
|
42926
|
+
var messages_default2 = {
|
|
42927
|
+
init: {
|
|
42928
|
+
description: "\u521D\u59CB\u5316 dbcli \u914D\u7F6E\uFF0C\u89E3\u6790 .env \u4E26\u9032\u884C\u4E92\u52D5\u5F0F\u63D0\u793A",
|
|
42929
|
+
welcome: "\u6B61\u8FCE\u4F7F\u7528 dbcli",
|
|
42930
|
+
select_system: "\u9078\u64C7\u8CC7\u6599\u5EAB\u7CFB\u7D71\uFF1A",
|
|
42931
|
+
prompt_host: "\u8CC7\u6599\u5EAB\u4E3B\u6A5F\uFF1A",
|
|
42932
|
+
prompt_port: "\u8CC7\u6599\u5EAB\u57E0\u865F\uFF1A",
|
|
42933
|
+
prompt_user: "\u8CC7\u6599\u5EAB\u4F7F\u7528\u8005\uFF1A",
|
|
42934
|
+
prompt_password: "\u8CC7\u6599\u5EAB\u5BC6\u78BC\uFF1A",
|
|
42935
|
+
prompt_name: "\u8CC7\u6599\u5EAB\u540D\u7A31\uFF1A",
|
|
42936
|
+
prompt_permission: "\u6B0A\u9650\u7B49\u7D1A\uFF08query-only, read-write, data-admin, admin\uFF09\uFF1A",
|
|
42937
|
+
env_parse_note: "\u63D0\u793A\uFF1A\u7121\u6CD5\u89E3\u6790 .env \u914D\u7F6E\uFF0C\u4F7F\u7528\u4E92\u52D5\u5F0F\u63D0\u793A",
|
|
42938
|
+
connection_testing: "\u6B63\u5728\u6E2C\u8A66\u8CC7\u6599\u5EAB\u9023\u63A5...",
|
|
42939
|
+
connection_success: "\u2713 \u8CC7\u6599\u5EAB\u9023\u63A5\u6210\u529F",
|
|
42940
|
+
connection_failed: "\u2717 \u8CC7\u6599\u5EAB\u9023\u63A5\u5931\u6557",
|
|
42941
|
+
config_saved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 .dbcli",
|
|
42942
|
+
config_exists_overwrite: "\u914D\u7F6E\u6A94\u6848 .dbcli \u5DF2\u5B58\u5728\u3002\u662F\u5426\u8986\u84CB\uFF1F (y/n)\uFF1A",
|
|
42943
|
+
cancelled: "\u5DF2\u53D6\u6D88\u3002\u914D\u7F6E\u672A\u66F4\u6539\u3002"
|
|
42944
|
+
},
|
|
42945
|
+
schema: {
|
|
42946
|
+
description: "\u6AA2\u7D22\u8868\u683C\u7D50\u69CB\u6216\u5217\u51FA\u6240\u6709\u8868\u683C",
|
|
42947
|
+
fetching: "\u6B63\u5728\u6293\u53D6\u6A21\u5F0F...",
|
|
42948
|
+
success: "\u6A21\u5F0F\u5DF2\u66F4\u65B0",
|
|
42949
|
+
not_found: "\u8CC7\u6599\u5EAB\u4E2D\u627E\u4E0D\u5230\u8868\u683C",
|
|
42950
|
+
refresh_prompt: "\u6A21\u5F0F\u6709\u8B8A\u66F4\u3002\u662F\u5426\u5957\u7528\uFF1F (y/n)\uFF1A",
|
|
42951
|
+
columns_header: "\u6B04\u4F4D",
|
|
42952
|
+
scanning_database: "\uD83D\uDD0D \u6B63\u5728\u6383\u63CF\u8CC7\u6599\u5EAB\u67B6\u69CB...",
|
|
42953
|
+
tables_found: "\uD83D\uDCCD \u627E\u5230 {count} \u500B\u8868\u683C\u3002\u6B63\u5728\u7372\u53D6\u67B6\u69CB\u8A73\u60C5...",
|
|
42954
|
+
processing_tables: " \u8655\u7406\u4E86 {processed}/{total} \u500B\u8868\u683C",
|
|
42955
|
+
schema_exists_warning: "\u26A0\uFE0F \u8CC7\u6599\u5EAB\u67B6\u69CB\u5DF2\u5B58\u5728\u65BC .dbcli",
|
|
42956
|
+
use_force_to_override: " \u4F7F\u7528 --force \u9032\u884C\u8986\u84CB\u800C\u7121\u9700\u78BA\u8A8D"
|
|
42957
|
+
},
|
|
42958
|
+
list: {
|
|
42959
|
+
description: "\u5217\u51FA\u8CC7\u6599\u5EAB\u4E2D\u7684\u6240\u6709\u8868\u683C",
|
|
42960
|
+
no_tables: "\u8CC7\u6599\u5EAB\u4E2D\u672A\u627E\u5230\u8868\u683C"
|
|
42961
|
+
},
|
|
42962
|
+
query: {
|
|
42963
|
+
description: "\u57F7\u884C SQL \u67E5\u8A62",
|
|
42964
|
+
executing: "\u6B63\u5728\u57F7\u884C\u67E5\u8A62...",
|
|
42965
|
+
no_results: "\u672A\u8FD4\u56DE\u4EFB\u4F55\u7D50\u679C",
|
|
42966
|
+
result_count: "\u7D50\u679C\uFF1A{count} \u5217"
|
|
42967
|
+
},
|
|
42968
|
+
errors: {
|
|
42969
|
+
message: "\u932F\u8AA4\uFF1A{message}",
|
|
42970
|
+
invalid_config: "\u914D\u7F6E\u7121\u6548\uFF1A{field}",
|
|
42971
|
+
connection_failed: "\u7121\u6CD5\u9023\u63A5\u5230\u8CC7\u6599\u5EAB\uFF1A{message}",
|
|
42972
|
+
permission_denied: "\u6B0A\u9650\u88AB\u62D2\uFF08\u9700\u8981\uFF1A{required}\uFF09",
|
|
42973
|
+
table_not_found: "\u627E\u4E0D\u5230\u8868\u683C\uFF1A{table}",
|
|
42974
|
+
unsupported_lang: "\u4E0D\u652F\u63F4\u7684\u8A9E\u8A00 '{lang}'\uFF0C\u4F7F\u7528\u82F1\u6587",
|
|
42975
|
+
invalid_json: "\u7121\u6548\u7684 JSON\uFF1A{message}",
|
|
42976
|
+
table_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5DF2\u88AB\u9ED1\u540D\u55AE",
|
|
42977
|
+
table_already_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
42978
|
+
table_not_in_blacklist: "\u932F\u8AA4: \u8868\u683C '{table}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
42979
|
+
column_already_blacklisted: "\u932F\u8AA4: \u6B04\u4F4D '{table}.{column}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
42980
|
+
column_not_in_blacklist: "\u932F\u8AA4: \u6B04\u4F4D '{table}.{column}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
|
|
42981
|
+
invalid_table_name: "\u932F\u8AA4: \u7121\u6548\u7684\u8868\u683C\u540D\u7A31: {table}",
|
|
42982
|
+
invalid_column_format: "\u932F\u8AA4: \u7121\u6548\u7684\u6B04\u4F4D\u683C\u5F0F\u3002\u4F7F\u7528 'table.column'"
|
|
42983
|
+
},
|
|
42984
|
+
success: {
|
|
42985
|
+
inserted: "\u6210\u529F\u63D2\u5165 {count} \u5217",
|
|
42986
|
+
updated: "\u6210\u529F\u66F4\u65B0 {count} \u5217",
|
|
42987
|
+
deleted: "\u6210\u529F\u522A\u9664 {count} \u5217",
|
|
42988
|
+
no_changes: "\u6C92\u6709\u8B8A\u66F4\u8981\u5957\u7528"
|
|
42989
|
+
},
|
|
42990
|
+
insert: {
|
|
42991
|
+
description: "\u63D2\u5165\u8CC7\u6599\u5230\u8868\u683C",
|
|
42992
|
+
prompt_data: "\u8F38\u5165\u8981\u63D2\u5165\u7684 JSON \u8CC7\u6599\uFF1A",
|
|
42993
|
+
confirm: "\u5C07 {count} \u5217\u63D2\u5165\u5230 {table}\uFF1F (y/n)\uFF1A"
|
|
42994
|
+
},
|
|
42995
|
+
update: {
|
|
42996
|
+
description: "\u66F4\u65B0\u8868\u683C\u4E2D\u7684\u8CC7\u6599",
|
|
42997
|
+
prompt_where: "\u8F38\u5165 WHERE \u5B50\u53E5 (\u4F8B\u5982\uFF1Aid=1)\uFF1A",
|
|
42998
|
+
prompt_set: '\u8F38\u5165 JSON \u66F4\u65B0 (\u4F8B\u5982\uFF1A{"status": "active"})\uFF1A',
|
|
42999
|
+
confirm: "\u66F4\u65B0 {table} \u4E2D\u7684\u5217\uFF1F (y/n)\uFF1A"
|
|
43000
|
+
},
|
|
43001
|
+
delete: {
|
|
43002
|
+
description: "\u522A\u9664\u8868\u683C\u4E2D\u7684\u8CC7\u6599",
|
|
43003
|
+
prompt_where: "\u8F38\u5165 WHERE \u5B50\u53E5 (\u4F8B\u5982\uFF1Aid=1)\uFF1A",
|
|
43004
|
+
confirm: "\u522A\u9664 {table} \u4E2D\u7684\u5217\uFF1F (y/n)\uFF1A",
|
|
43005
|
+
admin_only: "\u522A\u9664\u547D\u4EE4\u9700\u8981 data-admin \u6216 admin \u6B0A\u9650"
|
|
43006
|
+
},
|
|
43007
|
+
export: {
|
|
43008
|
+
description: "\u532F\u51FA\u67E5\u8A62\u7D50\u679C",
|
|
43009
|
+
formats: "\u652F\u63F4\u7684\u683C\u5F0F\uFF1Ajson\u3001csv",
|
|
43010
|
+
exported: "\u5DF2\u5C07 {count} \u5217\u532F\u51FA\u81F3 {file}"
|
|
43011
|
+
},
|
|
43012
|
+
skill: {
|
|
43013
|
+
description: "\u751F\u6210 AI \u6280\u80FD\u6587\u6A94",
|
|
43014
|
+
installed: "\u6280\u80FD\u5DF2\u5B89\u88DD\u81F3 {path}"
|
|
43015
|
+
},
|
|
43016
|
+
blacklist: {
|
|
43017
|
+
description: "\u7BA1\u7406\u654F\u611F\u8CC7\u6599\u9ED1\u540D\u55AE\u4EE5\u9632\u6B62 AI \u5B58\u53D6",
|
|
43018
|
+
list_title: "\u76EE\u524D\u9ED1\u540D\u55AE\u914D\u7F6E",
|
|
43019
|
+
tables_label: "\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C",
|
|
43020
|
+
columns_label: "\u5DF2\u9ED1\u540D\u55AE\u7684\u6B04\u4F4D",
|
|
43021
|
+
none: "\u76EE\u524D\u6C92\u6709\u9ED1\u540D\u55AE\u8868\u683C\u6216\u6B04\u4F4D",
|
|
43022
|
+
table_added: "\u2713 \u8868\u683C '{table}' \u5DF2\u65B0\u589E\u81F3\u9ED1\u540D\u55AE",
|
|
43023
|
+
table_removed: "\u2713 \u8868\u683C '{table}' \u5DF2\u5F9E\u9ED1\u540D\u55AE\u79FB\u9664",
|
|
43024
|
+
column_added: "\u2713 \u6B04\u4F4D '{table}.{column}' \u5DF2\u65B0\u589E\u81F3\u9ED1\u540D\u55AE",
|
|
43025
|
+
column_removed: "\u2713 \u6B04\u4F4D '{table}.{column}' \u5DF2\u5F9E\u9ED1\u540D\u55AE\u79FB\u9664"
|
|
43026
|
+
},
|
|
43027
|
+
security: {
|
|
43028
|
+
columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
|
|
43029
|
+
},
|
|
43030
|
+
warnings: {
|
|
43031
|
+
blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A"
|
|
43032
|
+
}
|
|
43033
|
+
};
|
|
42815
43034
|
|
|
42816
43035
|
// src/i18n/message-loader.ts
|
|
43036
|
+
var BUNDLED_MESSAGES = {
|
|
43037
|
+
en: messages_default,
|
|
43038
|
+
"zh-TW": messages_default2
|
|
43039
|
+
};
|
|
43040
|
+
|
|
42817
43041
|
class MessageLoader {
|
|
42818
43042
|
static instance = null;
|
|
42819
43043
|
messages = {};
|
|
@@ -42830,31 +43054,10 @@ class MessageLoader {
|
|
|
42830
43054
|
return MessageLoader.instance;
|
|
42831
43055
|
}
|
|
42832
43056
|
loadMessages() {
|
|
42833
|
-
|
|
42834
|
-
|
|
42835
|
-
this.messages = this.loadLanguageFile(this.currentLang);
|
|
42836
|
-
}
|
|
42837
|
-
this.fallbackMessages = this.loadLanguageFile("en");
|
|
42838
|
-
} catch (error) {
|
|
42839
|
-
console.error("Failed to load messages:", error);
|
|
42840
|
-
throw new Error("Failed to initialize message loader");
|
|
42841
|
-
}
|
|
42842
|
-
}
|
|
42843
|
-
loadLanguageFile(lang) {
|
|
42844
|
-
try {
|
|
42845
|
-
const dir = import.meta.dir;
|
|
42846
|
-
const filePath = `${dir}/../../resources/lang/${lang}/messages.json`;
|
|
42847
|
-
const messages = __require(filePath);
|
|
42848
|
-
if (!messages || typeof messages !== "object") {
|
|
42849
|
-
throw new Error(`Language file contains invalid data: ${lang}`);
|
|
42850
|
-
}
|
|
42851
|
-
return messages;
|
|
42852
|
-
} catch (error) {
|
|
42853
|
-
if (error instanceof Error) {
|
|
42854
|
-
throw new Error(`Language file error (${lang}): ${error.message}`);
|
|
42855
|
-
}
|
|
42856
|
-
throw error;
|
|
43057
|
+
if (this.currentLang !== "en") {
|
|
43058
|
+
this.messages = BUNDLED_MESSAGES[this.currentLang] || {};
|
|
42857
43059
|
}
|
|
43060
|
+
this.fallbackMessages = BUNDLED_MESSAGES["en"] || {};
|
|
42858
43061
|
}
|
|
42859
43062
|
t(key) {
|
|
42860
43063
|
const parts = key.split(".");
|
|
@@ -42954,7 +43157,7 @@ function parseConnectionUrl(url) {
|
|
|
42954
43157
|
} else if (protocol === "mariadb") {
|
|
42955
43158
|
system = "mariadb";
|
|
42956
43159
|
} else {
|
|
42957
|
-
throw new Error(
|
|
43160
|
+
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
42958
43161
|
}
|
|
42959
43162
|
const host = parsed.hostname || "localhost";
|
|
42960
43163
|
const port = parsed.port !== "" ? parseInt(parsed.port, 10) : getDefaultsForSystem(system).port || 5432;
|
|
@@ -42963,7 +43166,7 @@ function parseConnectionUrl(url) {
|
|
|
42963
43166
|
const database = parsed.pathname.slice(1);
|
|
42964
43167
|
return { system, host, port, user, password, database };
|
|
42965
43168
|
} catch (error) {
|
|
42966
|
-
throw new EnvParseError(
|
|
43169
|
+
throw new EnvParseError(`Failed to parse DATABASE_URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
42967
43170
|
}
|
|
42968
43171
|
}
|
|
42969
43172
|
function parseEnvDatabase(env) {
|
|
@@ -42975,15 +43178,15 @@ function parseEnvDatabase(env) {
|
|
|
42975
43178
|
const user = env.DB_USER;
|
|
42976
43179
|
const database = env.DB_NAME;
|
|
42977
43180
|
if (!user) {
|
|
42978
|
-
throw new EnvParseError("
|
|
43181
|
+
throw new EnvParseError("DB_USER is required when using component format");
|
|
42979
43182
|
}
|
|
42980
43183
|
if (!database) {
|
|
42981
|
-
throw new EnvParseError("
|
|
43184
|
+
throw new EnvParseError("DB_NAME is required when using component format");
|
|
42982
43185
|
}
|
|
42983
43186
|
const defaults = getDefaultsForSystem(system);
|
|
42984
43187
|
const port = env.DB_PORT ? parseInt(env.DB_PORT, 10) : defaults.port || 5432;
|
|
42985
43188
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
42986
|
-
throw new EnvParseError(`DB_PORT
|
|
43189
|
+
throw new EnvParseError(`DB_PORT must be between 1 and 65535, got: ${env.DB_PORT}`);
|
|
42987
43190
|
}
|
|
42988
43191
|
return {
|
|
42989
43192
|
system,
|
|
@@ -46990,7 +47193,7 @@ var ConnectionConfigSchema = exports_external.object({
|
|
|
46990
47193
|
password: exports_external.union([exports_external.string(), EnvRefSchema]).default(""),
|
|
46991
47194
|
database: StringOrEnvRef
|
|
46992
47195
|
});
|
|
46993
|
-
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");
|
|
46994
47197
|
var MetadataSchema = exports_external.object({
|
|
46995
47198
|
createdAt: exports_external.string().datetime().optional(),
|
|
46996
47199
|
version: exports_external.string().default("1.0")
|
|
@@ -47035,14 +47238,14 @@ function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
|
47035
47238
|
if (!strict) {
|
|
47036
47239
|
return config;
|
|
47037
47240
|
}
|
|
47038
|
-
throw new ConfigError(
|
|
47039
|
-
` +
|
|
47040
|
-
` +
|
|
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>'`);
|
|
47041
47244
|
}
|
|
47042
47245
|
if (parentKey === "port") {
|
|
47043
47246
|
const portNum = parseInt(value, 10);
|
|
47044
47247
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
47045
|
-
throw new ConfigError(`${envKey}
|
|
47248
|
+
throw new ConfigError(`${envKey} must be a valid port number (1-65535), got: ${value}`);
|
|
47046
47249
|
}
|
|
47047
47250
|
return portNum;
|
|
47048
47251
|
}
|
|
@@ -47105,9 +47308,9 @@ var configModule = {
|
|
|
47105
47308
|
return { ...DEFAULT_CONFIG };
|
|
47106
47309
|
} catch (error) {
|
|
47107
47310
|
if (error instanceof Error && error.message.includes("JSON")) {
|
|
47108
|
-
throw new ConfigError(
|
|
47311
|
+
throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
|
|
47109
47312
|
}
|
|
47110
|
-
throw new ConfigError(
|
|
47313
|
+
throw new ConfigError(`Failed to read .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
47111
47314
|
}
|
|
47112
47315
|
},
|
|
47113
47316
|
validate(raw) {
|
|
@@ -47115,7 +47318,7 @@ var configModule = {
|
|
|
47115
47318
|
return DbcliConfigSchema.parse(raw);
|
|
47116
47319
|
} catch (error) {
|
|
47117
47320
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47118
|
-
throw new ConfigError(
|
|
47321
|
+
throw new ConfigError(`Invalid .dbcli config structure: ${errorMessage}`);
|
|
47119
47322
|
}
|
|
47120
47323
|
},
|
|
47121
47324
|
merge(existing, updates) {
|
|
@@ -47165,8 +47368,7 @@ var configModule = {
|
|
|
47165
47368
|
await Bun.file(configPath).write(configJson);
|
|
47166
47369
|
if (password) {
|
|
47167
47370
|
const envPath = join(path, ".env.local");
|
|
47168
|
-
const envContent = `#
|
|
47169
|
-
# Database Credentials - DO NOT commit to git
|
|
47371
|
+
const envContent = `# Database Credentials - DO NOT commit to git
|
|
47170
47372
|
|
|
47171
47373
|
DBCLI_PASSWORD=${password}
|
|
47172
47374
|
`;
|
|
@@ -47181,7 +47383,7 @@ DBCLI_PASSWORD=${password}
|
|
|
47181
47383
|
if (error instanceof ConfigError) {
|
|
47182
47384
|
throw error;
|
|
47183
47385
|
}
|
|
47184
|
-
throw new ConfigError(
|
|
47386
|
+
throw new ConfigError(`Failed to write .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
47185
47387
|
}
|
|
47186
47388
|
}
|
|
47187
47389
|
};
|
|
@@ -47296,37 +47498,37 @@ function mapError(error, system, options) {
|
|
|
47296
47498
|
const errMsg = String(err?.message || String(error));
|
|
47297
47499
|
const errCode = String(err?.code || "");
|
|
47298
47500
|
if (errCode === "ECONNREFUSED" || errMsg.includes("refused")) {
|
|
47299
|
-
return new ConnectionError("ECONNREFUSED",
|
|
47300
|
-
|
|
47301
|
-
|
|
47302
|
-
|
|
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}`
|
|
47303
47505
|
]);
|
|
47304
47506
|
}
|
|
47305
47507
|
if (errCode === "ETIMEDOUT" || errMsg.includes("timeout") || errMsg.includes("timed out")) {
|
|
47306
|
-
return new ConnectionError("ETIMEDOUT",
|
|
47307
|
-
|
|
47308
|
-
|
|
47309
|
-
|
|
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`
|
|
47310
47512
|
]);
|
|
47311
47513
|
}
|
|
47312
47514
|
if (errMsg.includes("authentication") || errMsg.includes("auth") || errMsg.includes("password") || errMsg.includes("access denied") || errMsg.includes("FATAL")) {
|
|
47313
|
-
return new ConnectionError("AUTH_FAILED",
|
|
47314
|
-
|
|
47315
|
-
|
|
47316
|
-
|
|
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`
|
|
47317
47519
|
]);
|
|
47318
47520
|
}
|
|
47319
47521
|
if (errCode === "ENOTFOUND" || errMsg.includes("not found") || errMsg.includes("getaddrinfo")) {
|
|
47320
|
-
return new ConnectionError("ENOTFOUND",
|
|
47321
|
-
|
|
47322
|
-
|
|
47323
|
-
|
|
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)`
|
|
47324
47526
|
]);
|
|
47325
47527
|
}
|
|
47326
|
-
return new ConnectionError("UNKNOWN",
|
|
47327
|
-
|
|
47328
|
-
|
|
47329
|
-
|
|
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`
|
|
47330
47532
|
]);
|
|
47331
47533
|
}
|
|
47332
47534
|
|
|
@@ -47352,7 +47554,7 @@ class PostgreSQLAdapter {
|
|
|
47352
47554
|
constructor(options) {
|
|
47353
47555
|
this.options = options;
|
|
47354
47556
|
if (options.port < 1 || options.port > 65535) {
|
|
47355
|
-
throw new Error(
|
|
47557
|
+
throw new Error(`Invalid port number: ${options.port}`);
|
|
47356
47558
|
}
|
|
47357
47559
|
}
|
|
47358
47560
|
async connect() {
|
|
@@ -47385,7 +47587,7 @@ class PostgreSQLAdapter {
|
|
|
47385
47587
|
}
|
|
47386
47588
|
async testConnection() {
|
|
47387
47589
|
if (!this.pool) {
|
|
47388
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47590
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47389
47591
|
}
|
|
47390
47592
|
try {
|
|
47391
47593
|
const result = await this.execute("SELECT 1 as count");
|
|
@@ -47396,7 +47598,7 @@ class PostgreSQLAdapter {
|
|
|
47396
47598
|
}
|
|
47397
47599
|
async execute(sql, params) {
|
|
47398
47600
|
if (!this.pool) {
|
|
47399
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47601
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47400
47602
|
}
|
|
47401
47603
|
try {
|
|
47402
47604
|
const result = params ? await this.pool.query(sql, params) : await this.pool.query(sql);
|
|
@@ -47407,7 +47609,7 @@ class PostgreSQLAdapter {
|
|
|
47407
47609
|
}
|
|
47408
47610
|
async listTables() {
|
|
47409
47611
|
if (!this.pool) {
|
|
47410
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47612
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47411
47613
|
}
|
|
47412
47614
|
try {
|
|
47413
47615
|
const query = `
|
|
@@ -47419,7 +47621,9 @@ class PostgreSQLAdapter {
|
|
|
47419
47621
|
WHEN 'v' THEN 'view'
|
|
47420
47622
|
WHEN 'm' THEN 'view'
|
|
47421
47623
|
ELSE 'table'
|
|
47422
|
-
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
|
|
47423
47627
|
FROM pg_class c
|
|
47424
47628
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
47425
47629
|
WHERE n.nspname = 'public'
|
|
@@ -47430,6 +47634,7 @@ class PostgreSQLAdapter {
|
|
|
47430
47634
|
return results.map((row) => ({
|
|
47431
47635
|
name: row.table_name,
|
|
47432
47636
|
columns: [],
|
|
47637
|
+
columnCount: row.column_count,
|
|
47433
47638
|
rowCount: Math.max(0, row.estimated_rows || 0),
|
|
47434
47639
|
engine: "PostgreSQL",
|
|
47435
47640
|
estimatedRowCount: Math.max(0, row.estimated_rows || 0),
|
|
@@ -47441,7 +47646,7 @@ class PostgreSQLAdapter {
|
|
|
47441
47646
|
}
|
|
47442
47647
|
async getTableSchema(tableName) {
|
|
47443
47648
|
if (!this.pool) {
|
|
47444
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47649
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47445
47650
|
}
|
|
47446
47651
|
try {
|
|
47447
47652
|
const columnQuery = `
|
|
@@ -47599,7 +47804,7 @@ class MySQLAdapter {
|
|
|
47599
47804
|
this.options = options;
|
|
47600
47805
|
this.system = options.system === "mariadb" ? "mariadb" : "mysql";
|
|
47601
47806
|
if (options.port < 1 || options.port > 65535) {
|
|
47602
|
-
throw new Error(
|
|
47807
|
+
throw new Error(`Invalid port number: ${options.port}`);
|
|
47603
47808
|
}
|
|
47604
47809
|
}
|
|
47605
47810
|
async connect() {
|
|
@@ -47626,7 +47831,7 @@ class MySQLAdapter {
|
|
|
47626
47831
|
}
|
|
47627
47832
|
async testConnection() {
|
|
47628
47833
|
if (!this.db) {
|
|
47629
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47834
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47630
47835
|
}
|
|
47631
47836
|
try {
|
|
47632
47837
|
const result = await this.execute("SELECT 1 as count");
|
|
@@ -47637,7 +47842,7 @@ class MySQLAdapter {
|
|
|
47637
47842
|
}
|
|
47638
47843
|
async execute(sql, params) {
|
|
47639
47844
|
if (!this.db) {
|
|
47640
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47845
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47641
47846
|
}
|
|
47642
47847
|
try {
|
|
47643
47848
|
const [rows] = params ? await this.db.execute(sql, params) : await this.db.execute(sql);
|
|
@@ -47648,7 +47853,7 @@ class MySQLAdapter {
|
|
|
47648
47853
|
}
|
|
47649
47854
|
async listTables() {
|
|
47650
47855
|
if (!this.db) {
|
|
47651
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47856
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47652
47857
|
}
|
|
47653
47858
|
try {
|
|
47654
47859
|
const query = `
|
|
@@ -47668,7 +47873,8 @@ class MySQLAdapter {
|
|
|
47668
47873
|
const results = await this.execute(query);
|
|
47669
47874
|
return results.map((row) => ({
|
|
47670
47875
|
name: row.table_name,
|
|
47671
|
-
columns:
|
|
47876
|
+
columns: [],
|
|
47877
|
+
columnCount: row.column_count,
|
|
47672
47878
|
rowCount: row.row_count || 0,
|
|
47673
47879
|
engine: row.engine,
|
|
47674
47880
|
estimatedRowCount: row.row_count || 0,
|
|
@@ -47680,7 +47886,7 @@ class MySQLAdapter {
|
|
|
47680
47886
|
}
|
|
47681
47887
|
async getTableSchema(tableName) {
|
|
47682
47888
|
if (!this.db) {
|
|
47683
|
-
throw new ConnectionError("UNKNOWN", "
|
|
47889
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", ["Call connect() to establish a connection"]);
|
|
47684
47890
|
}
|
|
47685
47891
|
try {
|
|
47686
47892
|
const columnQuery = `
|
|
@@ -47791,12 +47997,12 @@ class AdapterFactory {
|
|
|
47791
47997
|
case "mariadb":
|
|
47792
47998
|
return new MySQLAdapter(options);
|
|
47793
47999
|
default:
|
|
47794
|
-
throw new Error(
|
|
48000
|
+
throw new Error(`Unsupported database system: ${options.system}`);
|
|
47795
48001
|
}
|
|
47796
48002
|
}
|
|
47797
48003
|
}
|
|
47798
48004
|
// src/commands/init.ts
|
|
47799
|
-
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) => {
|
|
47800
48006
|
try {
|
|
47801
48007
|
await initCommandHandler(options);
|
|
47802
48008
|
} catch (error) {
|
|
@@ -47859,7 +48065,7 @@ async function initCommandHandler(options) {
|
|
|
47859
48065
|
"admin"
|
|
47860
48066
|
]);
|
|
47861
48067
|
}
|
|
47862
|
-
if (!["query-only", "read-write", "admin"].includes(permission2)) {
|
|
48068
|
+
if (!["query-only", "read-write", "data-admin", "admin"].includes(permission2)) {
|
|
47863
48069
|
throw new Error(`Invalid permission level: ${permission2}`);
|
|
47864
48070
|
}
|
|
47865
48071
|
const newConfig2 = configModule.merge(existingConfig, {
|
|
@@ -47905,10 +48111,11 @@ async function initCommandHandler(options) {
|
|
|
47905
48111
|
permission = await promptUser.select(t("init.prompt_permission"), [
|
|
47906
48112
|
"query-only",
|
|
47907
48113
|
"read-write",
|
|
48114
|
+
"data-admin",
|
|
47908
48115
|
"admin"
|
|
47909
48116
|
]);
|
|
47910
48117
|
}
|
|
47911
|
-
if (!["query-only", "read-write", "admin"].includes(permission)) {
|
|
48118
|
+
if (!["query-only", "read-write", "data-admin", "admin"].includes(permission)) {
|
|
47912
48119
|
throw new Error(`Invalid permission level: ${permission}`);
|
|
47913
48120
|
}
|
|
47914
48121
|
configForWrite = connection;
|
|
@@ -48036,7 +48243,7 @@ class TableListFormatter {
|
|
|
48036
48243
|
tables.forEach((t2) => {
|
|
48037
48244
|
table.push([
|
|
48038
48245
|
t2.name,
|
|
48039
|
-
t2.columns.length.toString(),
|
|
48246
|
+
(t2.columnCount ?? t2.columns.length).toString(),
|
|
48040
48247
|
(t2.rowCount ?? "?").toString(),
|
|
48041
48248
|
t2.engine || "N/A"
|
|
48042
48249
|
]);
|
|
@@ -48046,14 +48253,6 @@ class TableListFormatter {
|
|
|
48046
48253
|
}
|
|
48047
48254
|
// src/formatters/json-formatter.ts
|
|
48048
48255
|
init_size_category();
|
|
48049
|
-
|
|
48050
|
-
class JSONFormatter {
|
|
48051
|
-
format(data, options) {
|
|
48052
|
-
const spacing = options?.compact ? undefined : 2;
|
|
48053
|
-
return JSON.stringify(data, null, spacing);
|
|
48054
|
-
}
|
|
48055
|
-
}
|
|
48056
|
-
|
|
48057
48256
|
class TableSchemaJSONFormatter {
|
|
48058
48257
|
format(table, options) {
|
|
48059
48258
|
const spacing = options?.compact ? undefined : 2;
|
|
@@ -48211,8 +48410,15 @@ async function listAction(options) {
|
|
|
48211
48410
|
return;
|
|
48212
48411
|
}
|
|
48213
48412
|
if (options.format === "json") {
|
|
48214
|
-
const
|
|
48215
|
-
|
|
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));
|
|
48216
48422
|
} else {
|
|
48217
48423
|
const formatter = new TableListFormatter;
|
|
48218
48424
|
console.log(formatter.format(tables));
|
|
@@ -48507,9 +48713,9 @@ async function handleFullDatabaseScan(adapter, config, options) {
|
|
|
48507
48713
|
};
|
|
48508
48714
|
await configModule.write(options.config, updatedConfig);
|
|
48509
48715
|
console.log(`
|
|
48510
|
-
\u2705
|
|
48511
|
-
console.log(` ${tables.length}
|
|
48512
|
-
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}`);
|
|
48513
48719
|
}
|
|
48514
48720
|
|
|
48515
48721
|
// src/core/permission-guard.ts
|
|
@@ -48732,6 +48938,29 @@ function checkPermission(sql, permission) {
|
|
|
48732
48938
|
classification
|
|
48733
48939
|
};
|
|
48734
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
|
+
}
|
|
48735
48964
|
if (permission === "read-write") {
|
|
48736
48965
|
const allowedTypes = [
|
|
48737
48966
|
"SELECT",
|
|
@@ -48750,7 +48979,7 @@ function checkPermission(sql, permission) {
|
|
|
48750
48979
|
}
|
|
48751
48980
|
return {
|
|
48752
48981
|
allowed: false,
|
|
48753
|
-
reason: `${classification.type} operation requires admin permission`,
|
|
48982
|
+
reason: `${classification.type} operation requires data-admin or admin permission`,
|
|
48754
48983
|
classification
|
|
48755
48984
|
};
|
|
48756
48985
|
}
|
|
@@ -49236,7 +49465,7 @@ class DataExecutor {
|
|
|
49236
49465
|
const dataKeys = Object.keys(data);
|
|
49237
49466
|
for (const key2 of dataKeys) {
|
|
49238
49467
|
if (!columnNames.includes(key2)) {
|
|
49239
|
-
throw new Error(
|
|
49468
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}". Valid columns: ${columnNames.join(", ")}`);
|
|
49240
49469
|
}
|
|
49241
49470
|
}
|
|
49242
49471
|
const columns = dataKeys;
|
|
@@ -49269,12 +49498,12 @@ class DataExecutor {
|
|
|
49269
49498
|
}
|
|
49270
49499
|
if (!options?.force) {
|
|
49271
49500
|
console.log(`
|
|
49272
|
-
|
|
49501
|
+
Generated SQL:`);
|
|
49273
49502
|
console.log(` ${sql}`);
|
|
49274
49503
|
console.log(`
|
|
49275
|
-
|
|
49504
|
+
Parameters:`);
|
|
49276
49505
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49277
|
-
const confirmed = await promptUser.confirm("
|
|
49506
|
+
const confirmed = await promptUser.confirm("Proceed with this operation?");
|
|
49278
49507
|
if (!confirmed) {
|
|
49279
49508
|
return {
|
|
49280
49509
|
status: "success",
|
|
@@ -49305,7 +49534,7 @@ class DataExecutor {
|
|
|
49305
49534
|
operation: "insert",
|
|
49306
49535
|
rows_affected: 0,
|
|
49307
49536
|
timestamp,
|
|
49308
|
-
error: "
|
|
49537
|
+
error: "Permission denied: Query-only mode only allows SELECT. Use Read-Write or Admin mode to execute INSERT."
|
|
49309
49538
|
};
|
|
49310
49539
|
}
|
|
49311
49540
|
return {
|
|
@@ -49313,7 +49542,7 @@ class DataExecutor {
|
|
|
49313
49542
|
operation: "insert",
|
|
49314
49543
|
rows_affected: 0,
|
|
49315
49544
|
timestamp,
|
|
49316
|
-
error: `INSERT
|
|
49545
|
+
error: `INSERT failed: ${errorMessage}`
|
|
49317
49546
|
};
|
|
49318
49547
|
}
|
|
49319
49548
|
}
|
|
@@ -49336,12 +49565,12 @@ class DataExecutor {
|
|
|
49336
49565
|
}
|
|
49337
49566
|
if (!options?.force) {
|
|
49338
49567
|
console.log(`
|
|
49339
|
-
|
|
49568
|
+
Generated SQL:`);
|
|
49340
49569
|
console.log(` ${sql}`);
|
|
49341
49570
|
console.log(`
|
|
49342
|
-
|
|
49571
|
+
Parameters:`);
|
|
49343
49572
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49344
|
-
const confirmed = await promptUser.confirm("
|
|
49573
|
+
const confirmed = await promptUser.confirm("Proceed with this operation?");
|
|
49345
49574
|
if (!confirmed) {
|
|
49346
49575
|
return {
|
|
49347
49576
|
status: "success",
|
|
@@ -49372,7 +49601,7 @@ class DataExecutor {
|
|
|
49372
49601
|
operation: "update",
|
|
49373
49602
|
rows_affected: 0,
|
|
49374
49603
|
timestamp,
|
|
49375
|
-
error: "
|
|
49604
|
+
error: "Permission denied: Query-only mode only allows SELECT. Use Read-Write or Admin mode to execute UPDATE."
|
|
49376
49605
|
};
|
|
49377
49606
|
}
|
|
49378
49607
|
return {
|
|
@@ -49380,7 +49609,7 @@ class DataExecutor {
|
|
|
49380
49609
|
operation: "update",
|
|
49381
49610
|
rows_affected: 0,
|
|
49382
49611
|
timestamp,
|
|
49383
|
-
error: `UPDATE
|
|
49612
|
+
error: `UPDATE failed: ${errorMessage}`
|
|
49384
49613
|
};
|
|
49385
49614
|
}
|
|
49386
49615
|
}
|
|
@@ -49390,13 +49619,13 @@ class DataExecutor {
|
|
|
49390
49619
|
if (this.blacklistValidator) {
|
|
49391
49620
|
this.blacklistValidator.checkTableBlacklist("DELETE", tableName, []);
|
|
49392
49621
|
}
|
|
49393
|
-
if (this.permission !== "admin") {
|
|
49622
|
+
if (this.permission !== "data-admin" && this.permission !== "admin") {
|
|
49394
49623
|
return {
|
|
49395
49624
|
status: "error",
|
|
49396
49625
|
operation: "delete",
|
|
49397
49626
|
rows_affected: 0,
|
|
49398
49627
|
timestamp,
|
|
49399
|
-
error: "
|
|
49628
|
+
error: "Permission denied: DELETE operation requires Data-Admin or Admin permission."
|
|
49400
49629
|
};
|
|
49401
49630
|
}
|
|
49402
49631
|
const { sql, params } = this.buildDeleteSql(tableName, where, schema);
|
|
@@ -49411,14 +49640,14 @@ class DataExecutor {
|
|
|
49411
49640
|
}
|
|
49412
49641
|
if (!options?.force) {
|
|
49413
49642
|
console.log(`
|
|
49414
|
-
\u26A0\uFE0F
|
|
49643
|
+
\u26A0\uFE0F Warning: DELETE operation is destructive and cannot be undone!`);
|
|
49415
49644
|
console.log(`
|
|
49416
|
-
|
|
49645
|
+
Generated SQL:`);
|
|
49417
49646
|
console.log(` ${sql}`);
|
|
49418
49647
|
console.log(`
|
|
49419
|
-
|
|
49648
|
+
Parameters:`);
|
|
49420
49649
|
console.log(` ${JSON.stringify(params, null, 2)}`);
|
|
49421
|
-
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.");
|
|
49422
49651
|
if (!confirmed) {
|
|
49423
49652
|
return {
|
|
49424
49653
|
status: "success",
|
|
@@ -49448,7 +49677,7 @@ class DataExecutor {
|
|
|
49448
49677
|
operation: "delete",
|
|
49449
49678
|
rows_affected: 0,
|
|
49450
49679
|
timestamp,
|
|
49451
|
-
error: `DELETE
|
|
49680
|
+
error: `DELETE failed: ${errorMessage}`
|
|
49452
49681
|
};
|
|
49453
49682
|
}
|
|
49454
49683
|
}
|
|
@@ -49464,7 +49693,7 @@ class DataExecutor {
|
|
|
49464
49693
|
const columnNames = schema.columns.map((col) => col.name);
|
|
49465
49694
|
for (const key2 of [...dataKeys, ...whereKeys]) {
|
|
49466
49695
|
if (!columnNames.includes(key2)) {
|
|
49467
|
-
throw new Error(
|
|
49696
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}"`);
|
|
49468
49697
|
}
|
|
49469
49698
|
}
|
|
49470
49699
|
const systemType = this.getSystemType();
|
|
@@ -49487,7 +49716,7 @@ class DataExecutor {
|
|
|
49487
49716
|
const columnNames = schema.columns.map((col) => col.name);
|
|
49488
49717
|
for (const key2 of whereKeys) {
|
|
49489
49718
|
if (!columnNames.includes(key2)) {
|
|
49490
|
-
throw new Error(
|
|
49719
|
+
throw new Error(`Column "${key2}" not found in table "${tableName}"`);
|
|
49491
49720
|
}
|
|
49492
49721
|
}
|
|
49493
49722
|
const systemType = this.getSystemType();
|
|
@@ -49781,7 +50010,7 @@ async function deleteCommand(table, options) {
|
|
|
49781
50010
|
if (!config.connection) {
|
|
49782
50011
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
49783
50012
|
}
|
|
49784
|
-
if (config.permission !== "admin") {
|
|
50013
|
+
if (config.permission !== "data-admin" && config.permission !== "admin") {
|
|
49785
50014
|
throw new PermissionError(t("delete.admin_only"), { type: "DELETE", isDangerous: true, keywords: ["DELETE"], isComposite: false, confidence: "HIGH" }, config.permission);
|
|
49786
50015
|
}
|
|
49787
50016
|
const adapter = AdapterFactory.createAdapter(config.connection);
|
|
@@ -49823,7 +50052,7 @@ async function deleteCommand(table, options) {
|
|
|
49823
50052
|
process.exit(1);
|
|
49824
50053
|
}
|
|
49825
50054
|
if (error instanceof PermissionError) {
|
|
49826
|
-
console.error(t_vars("errors.permission_denied", { required: "admin" }));
|
|
50055
|
+
console.error(t_vars("errors.permission_denied", { required: "data-admin" }));
|
|
49827
50056
|
console.error(` Operation: ${error.classification.type}`);
|
|
49828
50057
|
console.error(` Message: ${error.message}`);
|
|
49829
50058
|
process.exit(1);
|
|
@@ -49897,7 +50126,17 @@ async function exportCommand(sql, options) {
|
|
|
49897
50126
|
// src/commands/skill.ts
|
|
49898
50127
|
import * as path from "path";
|
|
49899
50128
|
import { homedir } from "os";
|
|
49900
|
-
|
|
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");
|
|
49901
50140
|
async function skillCommand(_program, options) {
|
|
49902
50141
|
try {
|
|
49903
50142
|
const skillFile = Bun.file(SKILL_SOURCE_PATH);
|
|
@@ -50551,6 +50790,40 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
|
|
|
50551
50790
|
}
|
|
50552
50791
|
}
|
|
50553
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
|
+
|
|
50554
50827
|
// src/cli.ts
|
|
50555
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");
|
|
50556
50829
|
program2.addCommand(initCommand);
|
|
@@ -50611,6 +50884,7 @@ program2.command("skill").description(t("skill.description")).option("--install
|
|
|
50611
50884
|
program2.addCommand(blacklistCommand);
|
|
50612
50885
|
program2.addCommand(checkCommand);
|
|
50613
50886
|
program2.addCommand(diffCommand);
|
|
50887
|
+
program2.addCommand(statusCommand);
|
|
50614
50888
|
if (!process.argv.slice(2).length) {
|
|
50615
50889
|
program2.outputHelp();
|
|
50616
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"
|