@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.
@@ -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.3.2-beta",
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\u7BA1\u7406\u54E1\u6B0A\u9650"
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(`\u4E0D\u652F\u63F4\u7684\u5354\u8B70: ${protocol}`);
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(`\u7121\u6CD5\u89E3\u6790 DATABASE_URL: ${error instanceof Error ? error.message : String(error)}`);
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("\u4F7F\u7528\u5143\u4EF6\u683C\u5F0F\u6642 DB_USER \u70BA\u5FC5\u9700");
43181
+ throw new EnvParseError("DB_USER is required when using component format");
43181
43182
  }
43182
43183
  if (!database) {
43183
- throw new EnvParseError("\u4F7F\u7528\u5143\u4EF6\u683C\u5F0F\u6642 DB_NAME \u70BA\u5FC5\u9700");
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 \u5FC5\u9808\u5728 1 \u5230 65535 \u4E4B\u9593\uFF0C\u5F97\u5230: ${env.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(`\u74B0\u5883\u8B8A\u91CF\u672A\u5B9A\u7FA9: ${envKey}
47241
- ` + `\u8ACB\u5728 .env \u6216\u74B0\u5883\u8B8A\u91CF\u4E2D\u8A2D\u7F6E ${envKey}\u3002
47242
- ` + `\u63D0\u793A: \u6AA2\u67E5 .env \u6587\u4EF6\u6216\u57F7\u884C 'export ${envKey}=<\u503C>'`);
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} \u5FC5\u9808\u662F\u6709\u6548\u7684\u7AEF\u53E3\u865F (1-65535)\uFF0C\u5F97\u5230: ${value}`);
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(`\u7121\u6CD5\u89E3\u6790 .dbcli \u6587\u4EF6\uFF1A${error.message}`);
47311
+ throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
47311
47312
  }
47312
- throw new ConfigError(`\u7121\u6CD5\u8B80\u53D6 .dbcli \u914D\u7F6E: ${error instanceof Error ? error.message : String(error)}`);
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(`\u7121\u6548\u7684 .dbcli \u914D\u7F6E\u7D50\u69CB: ${errorMessage}`);
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 = `# \u8CC7\u6599\u5EAB\u654F\u611F\u4FE1\u606F - \u8ACB\u52FF\u63D0\u4EA4\u5230 git
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(`\u7121\u6CD5\u5BEB\u5165 .dbcli \u914D\u7F6E: ${error instanceof Error ? error.message : String(error)}`);
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", `\u7121\u6CD5\u9023\u63A5\u81F3 ${options.host}:${options.port} \u2014 \u4F3A\u670D\u5668\u672A\u904B\u884C\u6216\u672A\u76E3\u807D\u8A72\u57E0\u865F`, [
47502
- `\u78BA\u8A8D ${system} \u670D\u52D9\u5DF2\u555F\u52D5: ${system === "postgresql" ? "systemctl status postgresql" : "systemctl status mysql"}`,
47503
- `\u78BA\u8A8D\u57E0\u865F\u6B63\u78BA: ${system === "postgresql" ? "\u9810\u8A2D 5432" : "\u9810\u8A2D 3306"}`,
47504
- `\u6AA2\u67E5 ${options.host} \u662F\u5426\u53EF\u9054: ping ${options.host} \u6216 telnet ${options.host} ${options.port}`
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", `\u9023\u63A5\u8D85\u6642 (${options.timeout || 5000}ms) \u2014 \u53EF\u80FD\u662F\u9632\u706B\u7246\u963B\u64CB\u6216\u7DB2\u8DEF\u5EF6\u9072`, [
47509
- `\u6AA2\u67E5\u9632\u706B\u7246: ${system === "postgresql" ? "\u5141\u8A31 TCP 5432" : "\u5141\u8A31 TCP 3306"}`,
47510
- `\u589E\u52A0\u8D85\u6642\u6642\u9593: \u7DE8\u8F2F .dbcli \u4E26\u65B0\u589E "timeout": 15000`,
47511
- `\u78BA\u8A8D\u7DB2\u8DEF\u9023\u63A5: ping ${options.host} -c 3`
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", `\u8A8D\u8B49\u5931\u6557 \u2014 \u6AA2\u67E5\u4F7F\u7528\u8005\u540D\u7A31\u6216\u5BC6\u78BC`, [
47516
- `\u9A57\u8B49\u8A8D\u8B49: ${system === "postgresql" ? `psql -U ${options.user} -h ${options.host}` : `mysql -u ${options.user} -h ${options.host}`}`,
47517
- `\u6AA2\u67E5 pg_hba.conf (PostgreSQL) \u6216 user privileges (MySQL)`,
47518
- `\u91CD\u65B0\u57F7\u884C dbcli init \u4EE5\u66F4\u65B0\u8A8D\u8B49`
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", `\u627E\u4E0D\u5230\u4E3B\u6A5F: ${options.host}`, [
47523
- `\u6AA2\u67E5\u4E3B\u6A5F\u540D\u62FC\u5BEB: ${options.host}`,
47524
- `\u78BA\u8A8D DNS \u53EF\u89E3\u6790: nslookup ${options.host}`,
47525
- `\u82E5\u4F7F\u7528 localhost\uFF0C\u8A66\u8A66 127.0.0.1 (IPv4 vs IPv6 \u554F\u984C)`
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", `\u9023\u63A5\u5931\u6557: ${errMsg}`, [
47529
- `\u6AA2\u67E5\u9023\u63A5\u53C3\u6578: host=${options.host}, port=${options.port}, user=${options.user}`,
47530
- `\u67E5\u770B\u4F3A\u670D\u5668\u65E5\u8A8C: ${system === "postgresql" ? "postgresql.log" : "mysql.log"}`,
47531
- `\u5617\u8A66\u76F4\u63A5\u7528 ${system === "postgresql" ? "psql" : "mysql"} \u547D\u4EE4\u884C\u5DE5\u5177\u6E2C\u8A66`
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(`\u7121\u6548\u7684\u57E0\u865F: ${options.port}`);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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(`\u7121\u6548\u7684\u57E0\u865F: ${options.port}`);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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: Array(row.column_count).fill(null),
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", "\u8CC7\u6599\u5EAB\u9023\u63A5\u672A\u5EFA\u7ACB", ["\u547C\u53EB connect() \u4EE5\u5EFA\u7ACB\u9023\u63A5"]);
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(`\u4E0D\u652F\u6301\u7684\u8CC7\u6599\u5EAB\u7CFB\u7D71: ${options.system}`);
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 formatter = new JSONFormatter;
48417
- console.log(formatter.format(tables, { compact: false }));
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 \u67B6\u69CB\u5DF2\u5728 .dbcli \u4E2D\u66F4\u65B0`);
48713
- console.log(` ${tables.length} \u500B\u8868\u683C\u53CA\u5B8C\u6574\u5217\u8A73\u60C5\u548C\u95DC\u4FC2`);
48714
- console.log(` \u6642\u9593\u6233: ${updatedConfig.metadata.schemaLastUpdated}`);
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(`\u8CC7\u6599\u8868 "${tableName}" \u4E2D\u627E\u4E0D\u5230\u6B04\u4F4D "${key2}"\u3002\u6709\u6548\u7684\u6B04\u4F4D: ${columnNames.join(", ")}`);
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
- \u751F\u6210\u7684 SQL:`);
49501
+ Generated SQL:`);
49475
49502
  console.log(` ${sql}`);
49476
49503
  console.log(`
49477
- \u53C3\u6578:`);
49504
+ Parameters:`);
49478
49505
  console.log(` ${JSON.stringify(params, null, 2)}`);
49479
- const confirmed = await promptUser.confirm("\u662F\u5426\u57F7\u884C\u6B64\u64CD\u4F5C?");
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: "\u6B0A\u9650\u88AB\u62D2: Query-only \u6A21\u5F0F\u50C5\u5141\u8A31 SELECT\u3002\u4F7F\u7528 Read-Write \u6216 Admin \u6A21\u5F0F\u57F7\u884C INSERT\u3002"
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 \u5931\u6557: ${errorMessage}`
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
- \u751F\u6210\u7684 SQL:`);
49568
+ Generated SQL:`);
49542
49569
  console.log(` ${sql}`);
49543
49570
  console.log(`
49544
- \u53C3\u6578:`);
49571
+ Parameters:`);
49545
49572
  console.log(` ${JSON.stringify(params, null, 2)}`);
49546
- const confirmed = await promptUser.confirm("\u662F\u5426\u57F7\u884C\u6B64\u64CD\u4F5C?");
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: "\u6B0A\u9650\u88AB\u62D2: Query-only \u6A21\u5F0F\u50C5\u5141\u8A31 SELECT\u3002\u4F7F\u7528 Read-Write \u6216 Admin \u6A21\u5F0F\u57F7\u884C UPDATE\u3002"
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 \u5931\u6557: ${errorMessage}`
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: "\u6B0A\u9650\u88AB\u62D2: DELETE \u64CD\u4F5C\u9700\u8981 Admin \u6B0A\u9650\u3002"
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 \u8B66\u544A: DELETE \u64CD\u4F5C\u662F\u7834\u58DE\u6027\u7684\uFF0C\u7121\u6CD5\u64A4\u92B7\uFF01`);
49643
+ \u26A0\uFE0F Warning: DELETE operation is destructive and cannot be undone!`);
49617
49644
  console.log(`
49618
- \u751F\u6210\u7684 SQL:`);
49645
+ Generated SQL:`);
49619
49646
  console.log(` ${sql}`);
49620
49647
  console.log(`
49621
- \u53C3\u6578:`);
49648
+ Parameters:`);
49622
49649
  console.log(` ${JSON.stringify(params, null, 2)}`);
49623
- const confirmed = await promptUser.confirm("\u662F\u5426\u771F\u7684\u8981\u57F7\u884C\u6B64 DELETE \u64CD\u4F5C? \u6B64\u64CD\u4F5C\u7121\u6CD5\u64A4\u92B7\u3002");
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 \u5931\u6557: ${errorMessage}`
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(`\u8CC7\u6599\u8868 "${tableName}" \u4E2D\u627E\u4E0D\u5230\u6B04\u4F4D "${key2}"`);
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(`\u8CC7\u6599\u8868 "${tableName}" \u4E2D\u627E\u4E0D\u5230\u6B04\u4F4D "${key2}"`);
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
- var SKILL_SOURCE_PATH = path.resolve(import.meta.dir, "../../assets/SKILL.md");
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.2-beta",
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"