@carllee1983/dbcli 0.2.0-beta → 0.3.1-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/README.md CHANGED
@@ -36,14 +36,20 @@ All messages, help text, error messages, and command output respond to the langu
36
36
  #### Global Installation (Recommended)
37
37
 
38
38
  ```bash
39
- npm install -g dbcli
39
+ npm install -g @carllee1983/dbcli
40
40
  ```
41
41
 
42
42
  #### Zero-Install (No Installation Needed)
43
43
 
44
44
  ```bash
45
- npx dbcli init
46
- npx dbcli query "SELECT * FROM users"
45
+ npx @carllee1983/dbcli init
46
+ npx @carllee1983/dbcli query "SELECT * FROM users"
47
+ ```
48
+
49
+ #### Update
50
+
51
+ ```bash
52
+ npm update -g @carllee1983/dbcli
47
53
  ```
48
54
 
49
55
  #### Development Installation
package/README.zh-TW.md CHANGED
@@ -17,13 +17,23 @@ dbcli 是一個統一的資料庫 CLI 工具,能讓 AI 代理(Claude Code、
17
17
  使用 Bun 安裝:
18
18
 
19
19
  ```bash
20
- bun add -D dbcli
20
+ bun add -D @carllee1983/dbcli
21
21
  ```
22
22
 
23
23
  或使用 npm:
24
24
 
25
25
  ```bash
26
- npm install --save-dev dbcli
26
+ npm install --save-dev @carllee1983/dbcli
27
+ ```
28
+
29
+ ### 更新
30
+
31
+ ```bash
32
+ # Bun
33
+ bun update @carllee1983/dbcli
34
+
35
+ # npm
36
+ npm update --save-dev @carllee1983/dbcli
27
37
  ```
28
38
 
29
39
  ### 初始化
package/dist/cli.mjs CHANGED
@@ -42686,6 +42686,60 @@ var require_table = __commonJS((exports, module) => {
42686
42686
  module.exports = Table;
42687
42687
  });
42688
42688
 
42689
+ // src/core/size-category.ts
42690
+ var exports_size_category = {};
42691
+ __export(exports_size_category, {
42692
+ getSizeCategory: () => getSizeCategory
42693
+ });
42694
+ function getSizeCategory(estimatedRowCount) {
42695
+ const count = estimatedRowCount ?? 0;
42696
+ for (const [threshold, category] of THRESHOLDS) {
42697
+ if (count >= threshold)
42698
+ return category;
42699
+ }
42700
+ return "small";
42701
+ }
42702
+ var THRESHOLDS;
42703
+ var init_size_category = __esm(() => {
42704
+ THRESHOLDS = [
42705
+ [1e6, "huge"],
42706
+ [1e5, "large"],
42707
+ [1e4, "medium"]
42708
+ ];
42709
+ });
42710
+
42711
+ // src/commands/query-size-guard.ts
42712
+ var exports_query_size_guard = {};
42713
+ __export(exports_query_size_guard, {
42714
+ shouldBlockQuery: () => shouldBlockQuery
42715
+ });
42716
+ function shouldBlockQuery(sql, tableSizeInfo) {
42717
+ if (!tableSizeInfo) {
42718
+ return { blocked: false };
42719
+ }
42720
+ const category = getSizeCategory(tableSizeInfo.estimatedRowCount);
42721
+ if (category !== "huge") {
42722
+ return { blocked: false, sizeCategory: category };
42723
+ }
42724
+ const normalizedSql = sql.trim().toUpperCase();
42725
+ if (!normalizedSql.startsWith("SELECT")) {
42726
+ return { blocked: false, sizeCategory: category };
42727
+ }
42728
+ const hasWhere = /\bWHERE\b/i.test(sql);
42729
+ const hasLimit = /\bLIMIT\b/i.test(sql);
42730
+ if (hasWhere || hasLimit) {
42731
+ return { blocked: false, sizeCategory: category };
42732
+ }
42733
+ return {
42734
+ blocked: true,
42735
+ sizeCategory: category,
42736
+ reason: `Table has ~${tableSizeInfo.estimatedRowCount.toLocaleString()} rows (huge). Add WHERE or LIMIT clause, or use --no-limit to override.`
42737
+ };
42738
+ }
42739
+ var init_query_size_guard = __esm(() => {
42740
+ init_size_category();
42741
+ });
42742
+
42689
42743
  // node_modules/commander/esm.mjs
42690
42744
  var import__ = __toESM(require_commander(), 1);
42691
42745
  var {
@@ -42704,7 +42758,7 @@ var {
42704
42758
  // package.json
42705
42759
  var package_default = {
42706
42760
  name: "@carllee1983/dbcli",
42707
- version: "0.2.0-beta",
42761
+ version: "0.3.1-beta",
42708
42762
  description: "Database CLI for AI agents",
42709
42763
  type: "module",
42710
42764
  publishConfig: {
@@ -47358,18 +47412,28 @@ class PostgreSQLAdapter {
47358
47412
  try {
47359
47413
  const query = `
47360
47414
  SELECT
47361
- schemaname,
47362
- relname as table_name,
47363
- n_live_tup as row_count
47364
- FROM pg_stat_user_tables
47365
- ORDER BY relname
47415
+ c.relname as table_name,
47416
+ c.reltuples::bigint as estimated_rows,
47417
+ CASE c.relkind
47418
+ WHEN 'r' THEN 'table'
47419
+ WHEN 'v' THEN 'view'
47420
+ WHEN 'm' THEN 'view'
47421
+ ELSE 'table'
47422
+ END as table_type
47423
+ FROM pg_class c
47424
+ JOIN pg_namespace n ON n.oid = c.relnamespace
47425
+ WHERE n.nspname = 'public'
47426
+ AND c.relkind IN ('r', 'v', 'm')
47427
+ ORDER BY c.relname
47366
47428
  `;
47367
47429
  const results = await this.execute(query);
47368
47430
  return results.map((row) => ({
47369
47431
  name: row.table_name,
47370
47432
  columns: [],
47371
- rowCount: row.row_count || 0,
47372
- engine: "PostgreSQL"
47433
+ rowCount: Math.max(0, row.estimated_rows || 0),
47434
+ engine: "PostgreSQL",
47435
+ estimatedRowCount: Math.max(0, row.estimated_rows || 0),
47436
+ tableType: row.table_type
47373
47437
  }));
47374
47438
  } catch (error) {
47375
47439
  throw mapError(error, "postgresql", this.options);
@@ -47391,19 +47455,40 @@ class PostgreSQLAdapter {
47391
47455
  SELECT 1 FROM information_schema.table_constraints tc
47392
47456
  JOIN information_schema.key_column_usage kcu
47393
47457
  ON tc.constraint_name = kcu.constraint_name
47394
- WHERE tc.table_name = t.table_name
47458
+ WHERE tc.table_name = $1
47459
+ AND tc.table_schema = 'public'
47395
47460
  AND tc.constraint_type = 'PRIMARY KEY'
47396
47461
  AND kcu.column_name = c.column_name
47397
47462
  )
47398
- ) as is_primary_key
47463
+ ) as is_primary_key,
47464
+ (c.column_default LIKE 'nextval%') as auto_increment,
47465
+ pgd.description as comment
47399
47466
  FROM information_schema.columns c
47400
- JOIN information_schema.tables t
47401
- ON c.table_name = t.table_name
47467
+ LEFT JOIN pg_catalog.pg_statio_all_tables st
47468
+ ON st.schemaname = c.table_schema AND st.relname = c.table_name
47469
+ LEFT JOIN pg_catalog.pg_description pgd
47470
+ ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
47402
47471
  WHERE c.table_name = $1
47403
- AND t.table_schema = 'public'
47472
+ AND c.table_schema = 'public'
47404
47473
  ORDER BY c.ordinal_position
47405
47474
  `;
47406
47475
  const columns = await this.execute(columnQuery, [tableName]);
47476
+ const enumQuery = `
47477
+ SELECT
47478
+ c.column_name as name,
47479
+ array_agg(e.enumlabel ORDER BY e.enumsortorder) as enum_values
47480
+ FROM information_schema.columns c
47481
+ JOIN pg_type t ON t.typname = c.udt_name
47482
+ JOIN pg_enum e ON e.enumtypid = t.oid
47483
+ WHERE c.table_name = $1
47484
+ AND c.table_schema = 'public'
47485
+ GROUP BY c.column_name
47486
+ `;
47487
+ const enumResults = await this.execute(enumQuery, [tableName]);
47488
+ const enumMap = new Map;
47489
+ for (const row of enumResults) {
47490
+ enumMap.set(row.name, Array.isArray(row.enum_values) ? row.enum_values : []);
47491
+ }
47407
47492
  const fkQuery = `
47408
47493
  SELECT
47409
47494
  tc.constraint_name as name,
@@ -47425,6 +47510,28 @@ class PostgreSQLAdapter {
47425
47510
  fkMap.set(fk.columns[0], { table: fk.ref_table, column: fk.ref_columns[0] });
47426
47511
  }
47427
47512
  }
47513
+ const indexQuery = `
47514
+ SELECT
47515
+ i.relname as name,
47516
+ array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
47517
+ ix.indisunique as is_unique
47518
+ FROM pg_index ix
47519
+ JOIN pg_class t ON t.oid = ix.indrelid
47520
+ JOIN pg_class i ON i.oid = ix.indexrelid
47521
+ JOIN pg_namespace n ON n.oid = t.relnamespace
47522
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
47523
+ WHERE t.relname = $1
47524
+ AND n.nspname = 'public'
47525
+ AND NOT ix.indisprimary
47526
+ GROUP BY i.relname, ix.indisunique
47527
+ `;
47528
+ const indexResults = await this.execute(indexQuery, [tableName]);
47529
+ const estimateQuery = `
47530
+ SELECT reltuples::bigint as estimated_rows
47531
+ FROM pg_class
47532
+ WHERE relname = $1
47533
+ `;
47534
+ const estimateResults = await this.execute(estimateQuery, [tableName]);
47428
47535
  const pkQuery = `
47429
47536
  SELECT array_agg(a.attname) as columns
47430
47537
  FROM (
@@ -47452,12 +47559,21 @@ class PostgreSQLAdapter {
47452
47559
  nullable: col.nullable,
47453
47560
  default: col.default_value || undefined,
47454
47561
  primaryKey: col.is_primary_key,
47455
- foreignKey: fkMap.get(col.name)
47562
+ foreignKey: fkMap.get(col.name),
47563
+ autoIncrement: col.auto_increment,
47564
+ comment: col.comment || null,
47565
+ enumValues: enumMap.get(col.name)
47456
47566
  })),
47457
47567
  rowCount: countResult[0]?.count || 0,
47458
47568
  engine: "PostgreSQL",
47459
47569
  primaryKey: primaryKeyArray,
47460
- foreignKeys: safeForeignKeys
47570
+ foreignKeys: safeForeignKeys,
47571
+ indexes: indexResults.map((idx) => ({
47572
+ name: idx.name,
47573
+ columns: Array.isArray(idx.columns) ? idx.columns : [],
47574
+ unique: idx.is_unique
47575
+ })),
47576
+ estimatedRowCount: Math.max(0, estimateResults[0]?.estimated_rows || 0)
47461
47577
  };
47462
47578
  return schema;
47463
47579
  } catch (error) {
@@ -47468,6 +47584,13 @@ class PostgreSQLAdapter {
47468
47584
 
47469
47585
  // src/adapters/mysql-adapter.ts
47470
47586
  var import_promise = __toESM(require_promise(), 1);
47587
+ function parseEnumValues(columnType) {
47588
+ const match = columnType.match(/^enum\((.+)\)$/i);
47589
+ if (!match)
47590
+ return;
47591
+ return match[1].split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
47592
+ }
47593
+
47471
47594
  class MySQLAdapter {
47472
47595
  db = null;
47473
47596
  options;
@@ -47533,12 +47656,13 @@ class MySQLAdapter {
47533
47656
  t.TABLE_NAME as table_name,
47534
47657
  t.TABLE_ROWS as row_count,
47535
47658
  t.ENGINE as engine,
47659
+ t.TABLE_TYPE as table_type,
47536
47660
  COUNT(c.COLUMN_NAME) as column_count
47537
47661
  FROM information_schema.TABLES t
47538
47662
  LEFT JOIN information_schema.COLUMNS c
47539
47663
  ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
47540
47664
  WHERE t.TABLE_SCHEMA = DATABASE()
47541
- GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE
47665
+ GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE, t.TABLE_TYPE
47542
47666
  ORDER BY t.TABLE_NAME
47543
47667
  `;
47544
47668
  const results = await this.execute(query);
@@ -47546,7 +47670,9 @@ class MySQLAdapter {
47546
47670
  name: row.table_name,
47547
47671
  columns: Array(row.column_count).fill(null),
47548
47672
  rowCount: row.row_count || 0,
47549
- engine: row.engine
47673
+ engine: row.engine,
47674
+ estimatedRowCount: row.row_count || 0,
47675
+ tableType: row.table_type === "VIEW" ? "view" : "table"
47550
47676
  }));
47551
47677
  } catch (error) {
47552
47678
  throw mapError(error, this.system, this.options);
@@ -47563,7 +47689,9 @@ class MySQLAdapter {
47563
47689
  COLUMN_TYPE as type,
47564
47690
  IS_NULLABLE = 'YES' as nullable,
47565
47691
  COLUMN_DEFAULT as default_value,
47566
- COLUMN_KEY = 'PRI' as is_primary_key
47692
+ COLUMN_KEY = 'PRI' as is_primary_key,
47693
+ EXTRA LIKE '%auto_increment%' as auto_increment,
47694
+ COLUMN_COMMENT as comment
47567
47695
  FROM information_schema.COLUMNS
47568
47696
  WHERE TABLE_SCHEMA = DATABASE()
47569
47697
  AND TABLE_NAME = ?
@@ -47592,6 +47720,18 @@ class MySQLAdapter {
47592
47720
  }
47593
47721
  }
47594
47722
  const primaryKeyColumns = columns.filter((col) => col.is_primary_key).map((col) => col.name);
47723
+ const indexQuery = `
47724
+ SELECT
47725
+ INDEX_NAME as name,
47726
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
47727
+ NOT NON_UNIQUE as is_unique
47728
+ FROM information_schema.STATISTICS
47729
+ WHERE TABLE_SCHEMA = DATABASE()
47730
+ AND TABLE_NAME = ?
47731
+ AND INDEX_NAME != 'PRIMARY'
47732
+ GROUP BY INDEX_NAME, NON_UNIQUE
47733
+ `;
47734
+ const indexResults = await this.execute(indexQuery, [tableName]);
47595
47735
  const countResult = await this.execute(`SELECT COUNT(*) as count FROM \`${tableName}\``);
47596
47736
  const tableQuery = `
47597
47737
  SELECT ENGINE as engine
@@ -47599,6 +47739,12 @@ class MySQLAdapter {
47599
47739
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
47600
47740
  `;
47601
47741
  const tableResults = await this.execute(tableQuery, [tableName]);
47742
+ const estimateQuery = `
47743
+ SELECT TABLE_ROWS as estimated_rows
47744
+ FROM information_schema.TABLES
47745
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
47746
+ `;
47747
+ const estimateResults = await this.execute(estimateQuery, [tableName]);
47602
47748
  const schema = {
47603
47749
  name: tableName,
47604
47750
  columns: columns.map((col) => ({
@@ -47607,7 +47753,10 @@ class MySQLAdapter {
47607
47753
  nullable: col.nullable,
47608
47754
  default: col.default_value || undefined,
47609
47755
  primaryKey: col.is_primary_key,
47610
- foreignKey: fkMap.get(col.name)
47756
+ foreignKey: fkMap.get(col.name),
47757
+ autoIncrement: col.auto_increment,
47758
+ comment: col.comment || null,
47759
+ enumValues: parseEnumValues(col.type)
47611
47760
  })),
47612
47761
  rowCount: countResult[0]?.count || 0,
47613
47762
  engine: tableResults[0]?.engine || "MySQL",
@@ -47617,7 +47766,13 @@ class MySQLAdapter {
47617
47766
  columns: fk.columns.split(",").map((c) => c.trim()),
47618
47767
  refTable: fk.ref_table,
47619
47768
  refColumns: fk.ref_columns.split(",").map((c) => c.trim())
47620
- }))
47769
+ })),
47770
+ indexes: indexResults.map((idx) => ({
47771
+ name: idx.name,
47772
+ columns: idx.columns.split(",").map((c) => c.trim()),
47773
+ unique: idx.is_unique
47774
+ })),
47775
+ estimatedRowCount: estimateResults[0]?.estimated_rows || 0
47621
47776
  };
47622
47777
  return schema;
47623
47778
  } catch (error) {
@@ -47890,6 +48045,8 @@ class TableListFormatter {
47890
48045
  }
47891
48046
  }
47892
48047
  // src/formatters/json-formatter.ts
48048
+ init_size_category();
48049
+
47893
48050
  class JSONFormatter {
47894
48051
  format(data, options) {
47895
48052
  const spacing = options?.compact ? undefined : 2;
@@ -47900,11 +48057,16 @@ class JSONFormatter {
47900
48057
  class TableSchemaJSONFormatter {
47901
48058
  format(table, options) {
47902
48059
  const spacing = options?.compact ? undefined : 2;
48060
+ const rowCount = table.estimatedRowCount ?? table.rowCount;
47903
48061
  const output = {
47904
48062
  name: table.name,
48063
+ estimatedRowCount: rowCount ?? null,
48064
+ sizeCategory: rowCount !== undefined && rowCount !== null ? getSizeCategory(rowCount) : null,
48065
+ tableType: table.tableType ?? "table",
47905
48066
  columns: table.columns,
47906
48067
  primaryKey: table.primaryKey || [],
47907
48068
  foreignKeys: table.foreignKeys || [],
48069
+ indexes: table.indexes || [],
47908
48070
  metadata: {
47909
48071
  rowCount: table.rowCount,
47910
48072
  engine: table.engine
@@ -48055,8 +48217,11 @@ async function listAction(options) {
48055
48217
  const formatter = new TableListFormatter;
48056
48218
  console.log(formatter.format(tables));
48057
48219
  }
48220
+ const tableCount = tables.filter((t2) => t2.tableType !== "view").length;
48221
+ const viewCount = tables.filter((t2) => t2.tableType === "view").length;
48222
+ const viewSuffix = viewCount > 0 ? ` (${viewCount} views)` : "";
48058
48223
  console.log(`
48059
- \u2713 Found ${tables.length} tables`);
48224
+ \u2713 Found ${tableCount} tables${viewSuffix}`);
48060
48225
  } finally {
48061
48226
  await adapter.disconnect();
48062
48227
  }
@@ -48197,6 +48362,19 @@ Row count: ~${schema.rowCount.toLocaleString()}`);
48197
48362
  if (schema.engine) {
48198
48363
  console.log(`Engine: ${schema.engine}`);
48199
48364
  }
48365
+ if (schema.estimatedRowCount !== undefined) {
48366
+ const { getSizeCategory: getSizeCategory2 } = await Promise.resolve().then(() => (init_size_category(), exports_size_category));
48367
+ const category = getSizeCategory2(schema.estimatedRowCount);
48368
+ console.log(`Estimated rows: ~${schema.estimatedRowCount.toLocaleString()} (${category})`);
48369
+ }
48370
+ if (schema.indexes && schema.indexes.length > 0) {
48371
+ console.log(`
48372
+ Indexes:`);
48373
+ schema.indexes.forEach((idx) => {
48374
+ const uniqueTag = idx.unique ? " [UNIQUE]" : "";
48375
+ console.log(` ${idx.name}: (${idx.columns.join(", ")})${uniqueTag}`);
48376
+ });
48377
+ }
48200
48378
  }
48201
48379
  }
48202
48380
  async function handleSchemaRefresh(adapter, config, options) {
@@ -48260,7 +48438,10 @@ async function handleSchemaReset(adapter, config, options) {
48260
48438
  rowCount: fullSchema.rowCount,
48261
48439
  engine: fullSchema.engine,
48262
48440
  primaryKey: fullSchema.primaryKey || [],
48263
- foreignKeys: fullSchema.foreignKeys || []
48441
+ foreignKeys: fullSchema.foreignKeys || [],
48442
+ indexes: fullSchema.indexes || [],
48443
+ estimatedRowCount: fullSchema.estimatedRowCount || 0,
48444
+ tableType: fullSchema.tableType || "table"
48264
48445
  };
48265
48446
  processed++;
48266
48447
  if (processed % 10 === 0 || processed === tables.length) {
@@ -48299,7 +48480,10 @@ async function handleFullDatabaseScan(adapter, config, options) {
48299
48480
  rowCount: fullSchema.rowCount,
48300
48481
  engine: fullSchema.engine,
48301
48482
  primaryKey: fullSchema.primaryKey || [],
48302
- foreignKeys: fullSchema.foreignKeys || []
48483
+ foreignKeys: fullSchema.foreignKeys || [],
48484
+ indexes: fullSchema.indexes || [],
48485
+ estimatedRowCount: fullSchema.estimatedRowCount || 0,
48486
+ tableType: fullSchema.tableType || "table"
48303
48487
  };
48304
48488
  processed++;
48305
48489
  if (processed % 10 === 0 || processed === tables.length) {
@@ -48980,6 +49164,18 @@ async function queryCommand(sql, options) {
48980
49164
  if (!config.connection) {
48981
49165
  throw new Error('Run "dbcli init" first');
48982
49166
  }
49167
+ const mainTable = extractMainTable(sql);
49168
+ if (mainTable && config.schema && !options.noLimit) {
49169
+ const tableSchema = config.schema[mainTable];
49170
+ if (tableSchema) {
49171
+ const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
49172
+ const guard = shouldBlockQuery2(sql, tableSchema);
49173
+ if (guard.blocked) {
49174
+ console.error(`\u26A0 ${guard.reason}`);
49175
+ process.exit(1);
49176
+ }
49177
+ }
49178
+ }
48983
49179
  const adapter = AdapterFactory.createAdapter(config.connection);
48984
49180
  await adapter.connect();
48985
49181
  try {
@@ -49018,6 +49214,10 @@ async function queryCommand(sql, options) {
49018
49214
  process.exit(1);
49019
49215
  }
49020
49216
  }
49217
+ function extractMainTable(sql) {
49218
+ const match = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
49219
+ return match ? match[1] : null;
49220
+ }
49021
49221
 
49022
49222
  // src/core/data-executor.ts
49023
49223
  class DataExecutor {
@@ -49697,152 +49897,14 @@ async function exportCommand(sql, options) {
49697
49897
  // src/commands/skill.ts
49698
49898
  import * as path from "path";
49699
49899
  import { homedir } from "os";
49700
-
49701
- // src/core/skill-generator.ts
49702
- class SkillGenerator {
49703
- options;
49704
- constructor(options) {
49705
- this.options = options;
49706
- }
49707
- generateSkillMarkdown() {
49708
- const commands = this.collectCommands();
49709
- const filtered = this.filterByPermission(commands);
49710
- return this.renderSkillMarkdown(filtered);
49711
- }
49712
- collectCommands() {
49713
- const commands = [];
49714
- this.options.program.commands.forEach((cmd) => {
49715
- const skillCmd = {
49716
- name: cmd.name(),
49717
- description: cmd.description() || "No description",
49718
- args: cmd.args || [],
49719
- options: cmd.options.map((opt) => ({
49720
- flag: opt.flags,
49721
- description: opt.description || "No description",
49722
- required: opt.required ?? false
49723
- })),
49724
- permissionLevel: this.detectPermissionLevel(cmd.name()),
49725
- examples: this.generateExamples(cmd.name())
49726
- };
49727
- commands.push(skillCmd);
49728
- });
49729
- return commands;
49730
- }
49731
- detectPermissionLevel(commandName) {
49732
- if (commandName === "delete")
49733
- return "admin";
49734
- if (["insert", "update"].includes(commandName))
49735
- return "read-write";
49736
- return "query-only";
49737
- }
49738
- filterByPermission(commands) {
49739
- const permLevel = this.options.permissionLevel;
49740
- return commands.filter((cmd) => {
49741
- if (permLevel === "query-only") {
49742
- return !["insert", "update", "delete"].includes(cmd.name);
49743
- }
49744
- if (permLevel === "read-write") {
49745
- return cmd.name !== "delete";
49746
- }
49747
- return true;
49748
- });
49749
- }
49750
- generateExamples(commandName) {
49751
- const examples = {
49752
- init: ["dbcli init"],
49753
- list: ["dbcli list", "dbcli list --format json"],
49754
- schema: ["dbcli schema users", "dbcli schema users --format json"],
49755
- query: [
49756
- 'dbcli query "SELECT * FROM users LIMIT 10"',
49757
- 'dbcli query "SELECT id, email FROM users" --format json'
49758
- ],
49759
- insert: [`dbcli insert users --data '{"name":"Alice","email":"alice@example.com"}'`],
49760
- update: [`dbcli update users --where "id=1" --set '{"name":"Bob"}'`],
49761
- delete: ['dbcli delete users --where "id=1" --force'],
49762
- export: [
49763
- 'dbcli export "SELECT * FROM users" --format csv --output users.csv',
49764
- `dbcli export "SELECT * FROM users" --format json | jq '.[]'`
49765
- ]
49766
- };
49767
- return examples[commandName] || [`dbcli ${commandName}`];
49768
- }
49769
- renderSkillMarkdown(commands) {
49770
- const frontmatter = `---
49771
- name: dbcli
49772
- description: Database CLI for AI agents. Use to query, modify, and manage database schemas with permission-based access control.
49773
- user-invocable: true
49774
- allowed-tools: Bash(dbcli *)
49775
- ---`;
49776
- const header = `# dbcli Skill Documentation
49777
-
49778
- Database CLI for AI agents with permission-based access control.
49779
-
49780
- ## Commands`;
49781
- const commandDocs = commands.map((cmd) => this.renderCommandSection(cmd)).join(`
49782
-
49783
- `);
49784
- const footer = `## Permission Levels
49785
-
49786
- dbcli enforces permission-based access control:
49787
-
49788
- - **Query-only**: Execute SELECT queries, list tables, view schemas, export data
49789
- - **Read-Write**: Query-only + INSERT and UPDATE operations
49790
- - **Admin**: Read-Write + DELETE operations
49791
-
49792
- Your current permission level is set in \`.dbcli\` config.
49793
-
49794
- ## Tips for AI Agents
49795
-
49796
- 1. **Schema introspection first**: Start with \`dbcli schema <table>\` to understand structure
49797
- 2. **Test with --dry-run**: Use \`--dry-run\` to preview SQL before executing
49798
- 3. **Use --format json**: AI parsing of JSON is more reliable than tables
49799
- 4. **Check permission level**: Review \`.dbcli\` to understand what operations are allowed`;
49800
- return `${frontmatter}
49801
-
49802
- ${header}
49803
-
49804
- ${commandDocs}
49805
-
49806
- ${footer}`;
49807
- }
49808
- renderCommandSection(cmd) {
49809
- const argsStr = cmd.args.length > 0 ? " " + cmd.args.join(" ") : "";
49810
- const optionsStr = cmd.options.length > 0 ? cmd.options.map((opt) => `- \`${opt.flag}\`: ${opt.description}`).join(`
49811
- `) : "(No options)";
49812
- const examplesStr = cmd.examples.map((ex) => `\`\`\`bash
49813
- ${ex}
49814
- \`\`\``).join(`
49815
-
49816
- `);
49817
- return `### ${cmd.name}
49818
-
49819
- ${cmd.description}
49820
-
49821
- **Usage:** \`dbcli ${cmd.name}${argsStr}\`
49822
-
49823
- **Options:**
49824
- ${optionsStr}
49825
-
49826
- **Permission required:** ${cmd.permissionLevel} or higher
49827
-
49828
- **Example:**
49829
- ${examplesStr}`;
49830
- }
49831
- }
49832
-
49833
- // src/commands/skill.ts
49834
- async function skillCommand(program2, options) {
49900
+ var SKILL_SOURCE_PATH = path.resolve(import.meta.dir, "../../assets/SKILL.md");
49901
+ async function skillCommand(_program, options) {
49835
49902
  try {
49836
- const config = await configModule.read(".dbcli");
49837
- if (!config.connection) {
49838
- throw new Error('Run "dbcli init" to initialize project');
49903
+ const skillFile = Bun.file(SKILL_SOURCE_PATH);
49904
+ if (!await skillFile.exists()) {
49905
+ throw new Error(`Skill source not found: ${SKILL_SOURCE_PATH}`);
49839
49906
  }
49840
- const skillGen = new SkillGenerator({
49841
- program: program2,
49842
- config,
49843
- permissionLevel: config.permission
49844
- });
49845
- const skillMarkdown = skillGen.generateSkillMarkdown();
49907
+ const skillMarkdown = await skillFile.text();
49846
49908
  if (options.output) {
49847
49909
  await Bun.file(options.output).write(skillMarkdown);
49848
49910
  console.error(`Skill written to ${options.output}`);
@@ -50067,6 +50129,428 @@ columnCmd.command("remove <table.column>").description("Remove column from black
50067
50129
  }
50068
50130
  });
50069
50131
 
50132
+ // src/core/health-checker.ts
50133
+ init_size_category();
50134
+
50135
+ class HealthChecker {
50136
+ adapter;
50137
+ constructor(adapter) {
50138
+ this.adapter = adapter;
50139
+ }
50140
+ async check(schema, options = {}) {
50141
+ const checks = options.checks || ["nulls", "duplicates", "orphans", "emptyStrings", "rowCount"];
50142
+ const sample2 = options.sample || 1e4;
50143
+ const blacklisted = options.blacklistedColumns || new Set;
50144
+ const visibleColumns = schema.columns.filter((c) => !blacklisted.has(`${schema.name}.${c.name}`));
50145
+ const countResult = await this.adapter.execute(`SELECT COUNT(*) as count FROM \`${schema.name}\``);
50146
+ const rowCount = countResult[0]?.count || 0;
50147
+ const nulls = checks.includes("nulls") ? await this.checkNulls(schema.name, visibleColumns, rowCount, sample2) : [];
50148
+ const orphans = checks.includes("orphans") ? await this.checkOrphans(schema.name, visibleColumns) : [];
50149
+ const duplicates = checks.includes("duplicates") ? await this.checkDuplicates(schema.name, schema.indexes || []) : [];
50150
+ const emptyStrings = checks.includes("emptyStrings") ? await this.checkEmptyStrings(schema.name, visibleColumns) : [];
50151
+ const issues = orphans.length + duplicates.length;
50152
+ const warnings = nulls.filter((n) => n.nullPercent > 50).length + emptyStrings.length;
50153
+ const clean = Math.max(0, checks.length - (issues > 0 ? 1 : 0) - (warnings > 0 ? 1 : 0));
50154
+ return {
50155
+ table: schema.name,
50156
+ rowCount,
50157
+ sizeCategory: getSizeCategory(schema.estimatedRowCount),
50158
+ checks: { nulls, orphans, duplicates, emptyStrings },
50159
+ summary: { issues, warnings, clean },
50160
+ skippedColumns: blacklisted.size > 0 ? Array.from(blacklisted).filter((c) => c.startsWith(`${schema.name}.`)) : undefined
50161
+ };
50162
+ }
50163
+ async checkNulls(tableName, columns, totalRows, sample2) {
50164
+ if (totalRows === 0)
50165
+ return [];
50166
+ const nullableColumns = columns.filter((c) => c.nullable);
50167
+ const results = [];
50168
+ for (const col of nullableColumns) {
50169
+ try {
50170
+ const useSample = totalRows > sample2;
50171
+ const sql = useSample ? `SELECT COUNT(*) - COUNT(\`${col.name}\`) as null_count FROM (SELECT \`${col.name}\` FROM \`${tableName}\` LIMIT ${sample2}) sub` : `SELECT COUNT(*) - COUNT(\`${col.name}\`) as null_count FROM \`${tableName}\``;
50172
+ const result = await this.adapter.execute(sql);
50173
+ const nullCount = result[0]?.null_count || 0;
50174
+ if (nullCount > 0) {
50175
+ const sampleSize = Math.min(totalRows, sample2);
50176
+ results.push({
50177
+ column: col.name,
50178
+ nullCount,
50179
+ nullPercent: Number((nullCount / sampleSize * 100).toFixed(1))
50180
+ });
50181
+ }
50182
+ } catch {}
50183
+ }
50184
+ return results;
50185
+ }
50186
+ async checkOrphans(tableName, columns) {
50187
+ const fkColumns = columns.filter((c) => c.foreignKey);
50188
+ const results = [];
50189
+ for (const col of fkColumns) {
50190
+ if (!col.foreignKey)
50191
+ continue;
50192
+ try {
50193
+ const sql = `
50194
+ SELECT COUNT(*) as orphan_count
50195
+ FROM \`${tableName}\` child
50196
+ LEFT JOIN \`${col.foreignKey.table}\` parent
50197
+ ON child.\`${col.name}\` = parent.\`${col.foreignKey.column}\`
50198
+ WHERE child.\`${col.name}\` IS NOT NULL
50199
+ AND parent.\`${col.foreignKey.column}\` IS NULL
50200
+ `;
50201
+ const result = await this.adapter.execute(sql);
50202
+ const orphanCount = result[0]?.orphan_count || 0;
50203
+ if (orphanCount > 0) {
50204
+ results.push({
50205
+ column: col.name,
50206
+ references: `${col.foreignKey.table}.${col.foreignKey.column}`,
50207
+ orphanCount
50208
+ });
50209
+ }
50210
+ } catch {}
50211
+ }
50212
+ return results;
50213
+ }
50214
+ async checkDuplicates(tableName, indexes) {
50215
+ const uniqueIndexes = indexes.filter((idx) => idx.unique);
50216
+ const results = [];
50217
+ for (const idx of uniqueIndexes) {
50218
+ try {
50219
+ const colList = idx.columns.map((c) => `\`${c}\``).join(", ");
50220
+ const sql = `
50221
+ SELECT COUNT(*) as dup_count FROM (
50222
+ SELECT ${colList}
50223
+ FROM \`${tableName}\`
50224
+ GROUP BY ${colList}
50225
+ HAVING COUNT(*) > 1
50226
+ ) dups
50227
+ `;
50228
+ const result = await this.adapter.execute(sql);
50229
+ const dupCount = result[0]?.dup_count || 0;
50230
+ if (dupCount > 0) {
50231
+ results.push({
50232
+ columns: idx.columns,
50233
+ indexName: idx.name,
50234
+ duplicateCount: dupCount
50235
+ });
50236
+ }
50237
+ } catch {}
50238
+ }
50239
+ return results;
50240
+ }
50241
+ async checkEmptyStrings(tableName, columns) {
50242
+ const stringColumns = columns.filter((c) => /varchar|text|char/i.test(c.type));
50243
+ const results = [];
50244
+ for (const col of stringColumns) {
50245
+ try {
50246
+ const sql = `SELECT COUNT(*) as empty_count FROM \`${tableName}\` WHERE \`${col.name}\` = ''`;
50247
+ const result = await this.adapter.execute(sql);
50248
+ const count = result[0]?.empty_count || 0;
50249
+ if (count > 0) {
50250
+ results.push({ column: col.name, count });
50251
+ }
50252
+ } catch {}
50253
+ }
50254
+ return results;
50255
+ }
50256
+ }
50257
+
50258
+ // src/commands/check.ts
50259
+ init_size_category();
50260
+ var checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(checkAction);
50261
+ async function checkAction(table, options) {
50262
+ try {
50263
+ const config = await configModule.read(options.config);
50264
+ if (!config.connection) {
50265
+ console.error("Database not configured. Run: dbcli init");
50266
+ process.exit(1);
50267
+ }
50268
+ const adapter = AdapterFactory.createAdapter(config.connection);
50269
+ await adapter.connect();
50270
+ try {
50271
+ const checker = new HealthChecker(adapter);
50272
+ const blacklistManager = new BlacklistManager(config);
50273
+ const blacklistedColumns = getBlacklistedColumnSet(blacklistManager);
50274
+ const blacklistedTables = getBlacklistedTableSet(blacklistManager);
50275
+ const checkTypes = options.checks ? options.checks.split(",") : undefined;
50276
+ const sampleSize = parseInt(options.sample, 10) || 1e4;
50277
+ if (table) {
50278
+ if (blacklistedTables.has(table.toLowerCase())) {
50279
+ console.error(`Table "${table}" is blacklisted`);
50280
+ process.exit(1);
50281
+ }
50282
+ const schema = await adapter.getTableSchema(table);
50283
+ const report = await checker.check(schema, {
50284
+ checks: checkTypes,
50285
+ sample: sampleSize,
50286
+ blacklistedColumns
50287
+ });
50288
+ outputReport(report, options.format);
50289
+ } else if (options.all) {
50290
+ const tables = await adapter.listTables();
50291
+ const reports = [];
50292
+ const skipped = [];
50293
+ for (const t7 of tables) {
50294
+ if (blacklistedTables.has(t7.name.toLowerCase())) {
50295
+ skipped.push(`${t7.name} (blacklisted)`);
50296
+ continue;
50297
+ }
50298
+ if (t7.tableType === "view") {
50299
+ skipped.push(`${t7.name} (view)`);
50300
+ continue;
50301
+ }
50302
+ const category = getSizeCategory(t7.estimatedRowCount);
50303
+ if (category === "huge" && !options.includeLarge) {
50304
+ skipped.push(`${t7.name} (~${(t7.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
50305
+ continue;
50306
+ }
50307
+ const schema = await adapter.getTableSchema(t7.name);
50308
+ const report = await checker.check(schema, {
50309
+ checks: checkTypes,
50310
+ sample: sampleSize,
50311
+ blacklistedColumns
50312
+ });
50313
+ reports.push(report);
50314
+ }
50315
+ if (options.format === "json") {
50316
+ console.log(JSON.stringify({ reports, skipped }, null, 2));
50317
+ } else {
50318
+ for (const report of reports) {
50319
+ outputReport(report, "table");
50320
+ console.log("");
50321
+ }
50322
+ if (skipped.length > 0) {
50323
+ console.log(`Skipped: ${skipped.join(", ")}`);
50324
+ }
50325
+ }
50326
+ } else {
50327
+ console.error("Specify a table name or use --all");
50328
+ process.exit(1);
50329
+ }
50330
+ } finally {
50331
+ await adapter.disconnect();
50332
+ }
50333
+ } catch (error) {
50334
+ if (error instanceof Error) {
50335
+ console.error(error.message);
50336
+ if (error instanceof ConnectionError) {
50337
+ error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
50338
+ }
50339
+ }
50340
+ process.exit(1);
50341
+ }
50342
+ }
50343
+ function outputReport(report, format) {
50344
+ if (format === "json") {
50345
+ console.log(JSON.stringify(report, null, 2));
50346
+ } else {
50347
+ console.log(`
50348
+ Table: ${report.table} (${report.rowCount} rows, ${report.sizeCategory})`);
50349
+ console.log(` Nulls: ${report.checks.nulls.length} columns with NULLs`);
50350
+ for (const n of report.checks.nulls) {
50351
+ console.log(` ${n.column}: ${n.nullCount} nulls (${n.nullPercent}%)`);
50352
+ }
50353
+ console.log(` Orphans: ${report.checks.orphans.length} FK violations`);
50354
+ for (const o of report.checks.orphans) {
50355
+ console.log(` ${o.column} -> ${o.references}: ${o.orphanCount} orphans`);
50356
+ }
50357
+ console.log(` Duplicates: ${report.checks.duplicates.length} unique index violations`);
50358
+ for (const d of report.checks.duplicates) {
50359
+ console.log(` ${d.indexName} (${d.columns.join(",")}): ${d.duplicateCount} duplicates`);
50360
+ }
50361
+ console.log(` Empty strings: ${report.checks.emptyStrings.length} columns`);
50362
+ for (const e of report.checks.emptyStrings) {
50363
+ console.log(` ${e.column}: ${e.count} empty strings`);
50364
+ }
50365
+ console.log(` Summary: ${report.summary.issues} issues, ${report.summary.warnings} warnings, ${report.summary.clean} clean`);
50366
+ }
50367
+ }
50368
+ function getBlacklistedColumnSet(manager) {
50369
+ const state = manager.state;
50370
+ const result = new Set;
50371
+ if (state?.columns) {
50372
+ for (const [table, cols] of state.columns.entries()) {
50373
+ for (const col of cols) {
50374
+ result.add(`${table}.${col}`);
50375
+ }
50376
+ }
50377
+ }
50378
+ return result;
50379
+ }
50380
+ function getBlacklistedTableSet(manager) {
50381
+ const state = manager.state;
50382
+ return state?.tables || new Set;
50383
+ }
50384
+
50385
+ // src/commands/diff.ts
50386
+ function compareSnapshots(before, after) {
50387
+ const beforeTables = new Set(Object.keys(before.tables));
50388
+ const afterTables = new Set(Object.keys(after.tables));
50389
+ const addedTables = Array.from(afterTables).filter((t7) => !beforeTables.has(t7));
50390
+ const removedTables = Array.from(beforeTables).filter((t7) => !afterTables.has(t7));
50391
+ const addedColumns = [];
50392
+ const removedColumns = [];
50393
+ const modifiedColumns = [];
50394
+ const indexChanges = [];
50395
+ for (const tableName of addedTables) {
50396
+ for (const col of after.tables[tableName].columns) {
50397
+ addedColumns.push({ table: tableName, column: col.name, type: col.type, nullable: col.nullable });
50398
+ }
50399
+ }
50400
+ for (const tableName of removedTables) {
50401
+ for (const col of before.tables[tableName].columns) {
50402
+ removedColumns.push({ table: tableName, column: col.name, type: col.type });
50403
+ }
50404
+ }
50405
+ const commonTables = Array.from(afterTables).filter((t7) => beforeTables.has(t7));
50406
+ for (const tableName of commonTables) {
50407
+ const beforeTable = before.tables[tableName];
50408
+ const afterTable = after.tables[tableName];
50409
+ const beforeColMap = new Map(beforeTable.columns.map((c) => [c.name, c]));
50410
+ const afterColMap = new Map(afterTable.columns.map((c) => [c.name, c]));
50411
+ for (const [name, col] of afterColMap) {
50412
+ if (!beforeColMap.has(name)) {
50413
+ addedColumns.push({ table: tableName, column: name, type: col.type, nullable: col.nullable });
50414
+ }
50415
+ }
50416
+ for (const [name, col] of beforeColMap) {
50417
+ if (!afterColMap.has(name)) {
50418
+ removedColumns.push({ table: tableName, column: name, type: col.type });
50419
+ }
50420
+ }
50421
+ for (const [name, afterCol] of afterColMap) {
50422
+ const beforeCol = beforeColMap.get(name);
50423
+ if (!beforeCol)
50424
+ continue;
50425
+ if (beforeCol.type.toLowerCase() !== afterCol.type.toLowerCase() || beforeCol.nullable !== afterCol.nullable) {
50426
+ modifiedColumns.push({
50427
+ table: tableName,
50428
+ column: name,
50429
+ before: { type: beforeCol.type, nullable: beforeCol.nullable },
50430
+ after: { type: afterCol.type, nullable: afterCol.nullable }
50431
+ });
50432
+ }
50433
+ }
50434
+ const beforeIndexes = new Map((beforeTable.indexes || []).map((i) => [i.name, i]));
50435
+ const afterIndexes = new Map((afterTable.indexes || []).map((i) => [i.name, i]));
50436
+ for (const name of afterIndexes.keys()) {
50437
+ if (!beforeIndexes.has(name)) {
50438
+ indexChanges.push({ table: tableName, name, change: "added" });
50439
+ }
50440
+ }
50441
+ for (const name of beforeIndexes.keys()) {
50442
+ if (!afterIndexes.has(name)) {
50443
+ indexChanges.push({ table: tableName, name, change: "removed" });
50444
+ }
50445
+ }
50446
+ }
50447
+ return {
50448
+ added: { tables: addedTables, columns: addedColumns },
50449
+ removed: { tables: removedTables, columns: removedColumns },
50450
+ modified: { columns: modifiedColumns, indexes: indexChanges },
50451
+ summary: {
50452
+ added: addedTables.length + addedColumns.length,
50453
+ removed: removedTables.length + removedColumns.length,
50454
+ modified: modifiedColumns.length + indexChanges.length
50455
+ }
50456
+ };
50457
+ }
50458
+ var diffCommand = new Command().name("diff").description("Compare schema snapshots to detect changes").option("--snapshot <path>", "Save current schema snapshot to file").option("--against <path>", "Compare current schema against a snapshot file").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(diffAction);
50459
+ async function diffAction(options) {
50460
+ try {
50461
+ if (!options.snapshot && !options.against) {
50462
+ console.error("Specify --snapshot <path> to save, or --against <path> to compare");
50463
+ process.exit(1);
50464
+ }
50465
+ const config = await configModule.read(options.config);
50466
+ if (!config.connection) {
50467
+ console.error("Database not configured. Run: dbcli init");
50468
+ process.exit(1);
50469
+ }
50470
+ const adapter = AdapterFactory.createAdapter(config.connection);
50471
+ await adapter.connect();
50472
+ try {
50473
+ const tables = await adapter.listTables();
50474
+ const currentSnapshot = {
50475
+ tables: {},
50476
+ createdAt: new Date().toISOString()
50477
+ };
50478
+ for (const t7 of tables) {
50479
+ if (t7.tableType === "view")
50480
+ continue;
50481
+ const schema = await adapter.getTableSchema(t7.name);
50482
+ currentSnapshot.tables[t7.name] = {
50483
+ name: schema.name,
50484
+ columns: schema.columns,
50485
+ indexes: schema.indexes || []
50486
+ };
50487
+ }
50488
+ if (options.snapshot) {
50489
+ await Bun.write(options.snapshot, JSON.stringify(currentSnapshot, null, 2));
50490
+ console.error(`Snapshot saved to ${options.snapshot} (${Object.keys(currentSnapshot.tables).length} tables)`);
50491
+ return;
50492
+ }
50493
+ if (options.against) {
50494
+ const beforeFile = Bun.file(options.against);
50495
+ if (!await beforeFile.exists()) {
50496
+ console.error(`Snapshot file not found: ${options.against}`);
50497
+ process.exit(1);
50498
+ }
50499
+ const beforeSnapshot = JSON.parse(await beforeFile.text());
50500
+ const result = compareSnapshots(beforeSnapshot, currentSnapshot);
50501
+ if (options.format === "json") {
50502
+ console.log(JSON.stringify(result, null, 2));
50503
+ } else {
50504
+ console.log(`
50505
+ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
50506
+ if (result.added.tables.length > 0)
50507
+ console.log(`
50508
+ Added tables: ${result.added.tables.join(", ")}`);
50509
+ if (result.removed.tables.length > 0)
50510
+ console.log(`
50511
+ Removed tables: ${result.removed.tables.join(", ")}`);
50512
+ if (result.added.columns.length > 0) {
50513
+ console.log(`
50514
+ Added columns:`);
50515
+ for (const c of result.added.columns)
50516
+ console.log(` ${c.table}.${c.column} (${c.type})`);
50517
+ }
50518
+ if (result.removed.columns.length > 0) {
50519
+ console.log(`
50520
+ Removed columns:`);
50521
+ for (const c of result.removed.columns)
50522
+ console.log(` ${c.table}.${c.column} (${c.type})`);
50523
+ }
50524
+ if (result.modified.columns.length > 0) {
50525
+ console.log(`
50526
+ Modified columns:`);
50527
+ for (const c of result.modified.columns)
50528
+ console.log(` ${c.table}.${c.column}: ${c.before.type} -> ${c.after.type}`);
50529
+ }
50530
+ if (result.modified.indexes.length > 0) {
50531
+ console.log(`
50532
+ Index changes:`);
50533
+ for (const i of result.modified.indexes)
50534
+ console.log(` ${i.table}.${i.name}: ${i.change}`);
50535
+ }
50536
+ console.log(`
50537
+ Summary: +${result.summary.added} -${result.summary.removed} ~${result.summary.modified}`);
50538
+ }
50539
+ }
50540
+ } finally {
50541
+ await adapter.disconnect();
50542
+ }
50543
+ } catch (error) {
50544
+ if (error instanceof Error) {
50545
+ console.error(error.message);
50546
+ if (error instanceof ConnectionError) {
50547
+ error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
50548
+ }
50549
+ }
50550
+ process.exit(1);
50551
+ }
50552
+ }
50553
+
50070
50554
  // src/cli.ts
50071
50555
  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");
50072
50556
  program2.addCommand(initCommand);
@@ -50125,6 +50609,8 @@ program2.command("skill").description(t("skill.description")).option("--install
50125
50609
  }
50126
50610
  });
50127
50611
  program2.addCommand(blacklistCommand);
50612
+ program2.addCommand(checkCommand);
50613
+ program2.addCommand(diffCommand);
50128
50614
  if (!process.argv.slice(2).length) {
50129
50615
  program2.outputHelp();
50130
50616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "0.2.0-beta",
3
+ "version": "0.3.1-beta",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {