@carllee1983/dbcli 1.54.0 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.mjs CHANGED
@@ -1091,10 +1091,12 @@ var init_schema_loader = __esm(() => {
1091
1091
  class ConnectionError extends Error {
1092
1092
  code;
1093
1093
  hints;
1094
- constructor(code, message, hints) {
1094
+ limitMs;
1095
+ constructor(code, message, hints, limitMs) {
1095
1096
  super(message);
1096
1097
  this.code = code;
1097
1098
  this.hints = hints;
1099
+ this.limitMs = limitMs;
1098
1100
  this.name = "ConnectionError";
1099
1101
  Object.setPrototypeOf(this, ConnectionError.prototype);
1100
1102
  }
@@ -1250,9 +1252,16 @@ var TRANSPORT_CODES = {
1250
1252
  ESOCKETTIMEDOUT: "ETIMEDOUT",
1251
1253
  ENOTFOUND: "ENOTFOUND",
1252
1254
  EAI_AGAIN: "ENOTFOUND",
1253
- PROTOCOL_CONNECTION_LOST: "ECONNREFUSED",
1254
- PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT"
1255
+ PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
1256
+ PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
1257
+ ECONNRESET: "CONNECTION_LOST",
1258
+ EPIPE: "CONNECTION_LOST",
1259
+ ECONNABORTED: "CONNECTION_LOST",
1260
+ EHOSTUNREACH: "EHOSTUNREACH",
1261
+ ENETUNREACH: "EHOSTUNREACH",
1262
+ ENETDOWN: "EHOSTUNREACH"
1255
1263
  };
1264
+ var TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
1256
1265
  var MYSQL_CODES = {
1257
1266
  ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
1258
1267
  ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
@@ -1260,7 +1269,11 @@ var MYSQL_CODES = {
1260
1269
  ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
1261
1270
  ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
1262
1271
  ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
1263
- ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED"
1272
+ ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
1273
+ ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
1274
+ ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
1275
+ ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
1276
+ ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
1264
1277
  };
1265
1278
  var MYSQL_ERRNOS = {
1266
1279
  1045: "AUTH_FAILED",
@@ -1269,14 +1282,30 @@ var MYSQL_ERRNOS = {
1269
1282
  1064: "SQL_SYNTAX_ERROR",
1270
1283
  1146: "TABLE_NOT_FOUND",
1271
1284
  1251: "AUTH_FAILED",
1272
- 1698: "AUTH_FAILED"
1285
+ 1698: "AUTH_FAILED",
1286
+ 1040: "TOO_MANY_CONNECTIONS",
1287
+ 1053: "CONNECTION_LOST",
1288
+ 2006: "CONNECTION_LOST",
1289
+ 2013: "CONNECTION_LOST",
1290
+ 1969: "STATEMENT_TIMEOUT",
1291
+ 3024: "STATEMENT_TIMEOUT"
1273
1292
  };
1274
1293
  var SQLSTATES = {
1275
1294
  "28000": "AUTH_FAILED",
1276
1295
  "28P01": "AUTH_FAILED",
1277
1296
  "42601": "SQL_SYNTAX_ERROR",
1278
1297
  "42P01": "TABLE_NOT_FOUND",
1279
- "42703": "COLUMN_NOT_FOUND"
1298
+ "42703": "COLUMN_NOT_FOUND",
1299
+ "57014": "STATEMENT_TIMEOUT",
1300
+ "08000": "CONNECTION_LOST",
1301
+ "08003": "CONNECTION_LOST",
1302
+ "08006": "CONNECTION_LOST",
1303
+ "08001": "ECONNREFUSED",
1304
+ "08004": "CONNECTION_REJECTED",
1305
+ "53300": "TOO_MANY_CONNECTIONS",
1306
+ "57P01": "CONNECTION_LOST",
1307
+ "57P02": "CONNECTION_LOST",
1308
+ "57P03": "SERVER_NOT_READY"
1280
1309
  };
1281
1310
  var REDIS_PREFIXES = {
1282
1311
  NOAUTH: "AUTH_FAILED",
@@ -1288,6 +1317,10 @@ var FALLBACK_PATTERNS = [
1288
1317
  [/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
1289
1318
  [/timeout exceeded/i, "ETIMEDOUT"],
1290
1319
  [/timed out/i, "ETIMEDOUT"],
1320
+ [/connection terminated/i, "CONNECTION_LOST"],
1321
+ [/server closed the connection/i, "CONNECTION_LOST"],
1322
+ [/server has gone away/i, "CONNECTION_LOST"],
1323
+ [/lost connection to \S+ server/i, "CONNECTION_LOST"],
1291
1324
  [/getaddrinfo/i, "ENOTFOUND"],
1292
1325
  [/authentication failed/i, "AUTH_FAILED"],
1293
1326
  [/access denied for user/i, "AUTH_FAILED"],
@@ -1295,18 +1328,26 @@ var FALLBACK_PATTERNS = [
1295
1328
  [/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
1296
1329
  [/password supplied/i, "AUTH_FAILED"]
1297
1330
  ];
1298
- function categorize(errCode, errno, errMsg) {
1331
+ var PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
1332
+ function categorize(errCode, errno, errMsg, system) {
1299
1333
  if (errCode) {
1334
+ if (TLS_CODE_PREFIX.test(errCode))
1335
+ return "TLS_ERROR";
1300
1336
  const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
1337
+ if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
1338
+ return null;
1339
+ }
1301
1340
  if (known)
1302
1341
  return known;
1303
1342
  }
1304
- if (typeof errno === "number" && MYSQL_ERRNOS[errno])
1343
+ const isMysqlFamily = system === "mysql" || system === "mariadb";
1344
+ if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
1305
1345
  return MYSQL_ERRNOS[errno];
1346
+ }
1306
1347
  const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
1307
1348
  if (redisPrefix && REDIS_PREFIXES[redisPrefix])
1308
1349
  return REDIS_PREFIXES[redisPrefix];
1309
- if (errCode || typeof errno === "number")
1350
+ if (errCode || isMysqlFamily && typeof errno === "number")
1310
1351
  return null;
1311
1352
  for (const [pattern, category] of FALLBACK_PATTERNS) {
1312
1353
  if (pattern.test(errMsg))
@@ -1359,7 +1400,7 @@ function mapError(error, system, options) {
1359
1400
  const errMsg = String(err?.message || String(error));
1360
1401
  const errCode = String(err?.code || "");
1361
1402
  const errno = err?.errno;
1362
- switch (categorize(errCode, errno, errMsg)) {
1403
+ switch (categorize(errCode, errno, errMsg, system)) {
1363
1404
  case "ECONNREFUSED":
1364
1405
  return new ConnectionError("ECONNREFUSED", `Cannot connect to ${options.host}:${options.port} \u2014 server is not running or not listening on this port`, serviceHints(system, options));
1365
1406
  case "ETIMEDOUT":
@@ -1368,6 +1409,42 @@ function mapError(error, system, options) {
1368
1409
  `Increase timeout: edit .dbcli and add "timeout": 15000`,
1369
1410
  `Verify network connectivity: ping ${options.host} -c 3`
1370
1411
  ]);
1412
+ case "CONNECTION_LOST":
1413
+ return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
1414
+ "Re-run the command: a dropped connection is often transient",
1415
+ `Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
1416
+ "If it happens on long-running work, look at the server idle/wait timeout"
1417
+ ]);
1418
+ case "SERVER_NOT_READY":
1419
+ return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
1420
+ "Retry shortly: this clears on its own once startup or recovery finishes",
1421
+ `Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
1422
+ "On a replica, this also appears while it catches up with the primary"
1423
+ ]);
1424
+ case "CONNECTION_REJECTED":
1425
+ return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
1426
+ "Check the server access rules (pg_hba.conf) for this user, database, and client address",
1427
+ "If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
1428
+ "Check whether the connection limit for this user or database is exhausted"
1429
+ ]);
1430
+ case "TOO_MANY_CONNECTIONS":
1431
+ return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
1432
+ "Wait and retry: the limit is on concurrent connections, not on you",
1433
+ system === "postgresql" ? "Inspect usage: SELECT count(*) FROM pg_stat_activity \u2014 compare against max_connections" : 'Inspect usage: SHOW STATUS LIKE "Threads_connected" \u2014 compare against max_connections',
1434
+ "Close idle sessions, or raise max_connections if the load is legitimate"
1435
+ ]);
1436
+ case "EHOSTUNREACH":
1437
+ return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
1438
+ "Check that you are on the right network or VPN",
1439
+ `Check routing and firewall between here and ${options.host}`,
1440
+ `Confirm the address is the one you meant: ${options.host}:${options.port}`
1441
+ ]);
1442
+ case "TLS_ERROR":
1443
+ return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
1444
+ 'Point the connection at the right CA bundle: set "caPath" in .dbcli',
1445
+ 'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
1446
+ `Confirm the certificate covers the host you connected to: ${options.host}`
1447
+ ]);
1371
1448
  case "ENOTFOUND":
1372
1449
  return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
1373
1450
  `Check the hostname spelling: ${options.host}`,
@@ -1398,6 +1475,14 @@ function mapError(error, system, options) {
1398
1475
  "Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
1399
1476
  ]);
1400
1477
  }
1478
+ case "STATEMENT_TIMEOUT": {
1479
+ const limitMs = options.statementTimeout ?? options.timeout;
1480
+ return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
1481
+ 'Inspect the query plan: dbcli explain "<sql>"',
1482
+ "Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
1483
+ "Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
1484
+ ], limitMs);
1485
+ }
1401
1486
  case "SQL_SYNTAX_ERROR":
1402
1487
  return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
1403
1488
  "Check your SQL syntax near the position reported above",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.54.0",
3
+ "version": "1.55.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -42,11 +42,11 @@
42
42
  "blacklist"
43
43
  ],
44
44
  "engines": {
45
- "node": ">=18.0.0",
46
45
  "bun": ">=1.3.3"
47
46
  },
48
47
  "files": [
49
48
  "dist/",
49
+ "scripts/postinstall-check-bun.mjs",
50
50
  "!dist/.build-stamp",
51
51
  "assets/",
52
52
  "plugins/",
@@ -63,6 +63,7 @@
63
63
  "LICENSE"
64
64
  ],
65
65
  "scripts": {
66
+ "postinstall": "bun scripts/postinstall-check-bun.mjs || node scripts/postinstall-check-bun.mjs || exit 0",
66
67
  "dev": "bun run src/cli.ts",
67
68
  "build": "bun run scripts/build.ts",
68
69
  "build:determinism": "bun run scripts/check-build-determinism.ts",
@@ -197,17 +197,16 @@ For a persistent `dbcli` executable in `PATH`, run:
197
197
  plugins/dbcli-agent/scripts/install-dbcli.sh
198
198
  ```
199
199
 
200
- The installer prefers Bun:
200
+ The installer requires Bun:
201
201
 
202
202
  ```bash
203
203
  bun install -g @carllee1983/dbcli
204
204
  ```
205
205
 
206
- If Bun is unavailable but npm exists, it falls back to:
207
-
208
- ```bash
209
- npm install -g @carllee1983/dbcli
210
- ```
206
+ If Bun is not on `PATH` the script refuses and exits non-zero rather than installing
207
+ through npm: the executable starts with `#!/usr/bin/env bun`, so an npm-only machine would
208
+ end up with a `dbcli` that cannot start. Install Bun first with
209
+ `curl -fsSL https://bun.sh/install | bash`.
211
210
 
212
211
  ## Verify Installation
213
212
 
@@ -6,10 +6,10 @@ if command -v bun >/dev/null 2>&1; then
6
6
  exit 0
7
7
  fi
8
8
 
9
- if command -v npm >/dev/null 2>&1; then
10
- npm install -g @carllee1983/dbcli
11
- exit 0
12
- fi
13
-
14
- echo "Install Bun or npm first, then run: bun install -g @carllee1983/dbcli" >&2
9
+ # npm can fetch the package, but the installed executable runs under Bun
10
+ # (`#!/usr/bin/env bun`), so installing it here would leave a `dbcli` on PATH that
11
+ # cannot start. Refuse instead of half-installing.
12
+ echo "Bun is required: dbcli's executable runs under Bun, whichever package manager installs it." >&2
13
+ echo "Install Bun, then re-run this script:" >&2
14
+ echo " curl -fsSL https://bun.sh/install | bash" >&2
15
15
  exit 1
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
1660
1660
  Recovery codes (fixed in v1.15.0):
1661
1661
  - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
1662
1662
  - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
1663
+ `CONN_TIMEOUT` also covers a statement the server canceled for exceeding the statement
1664
+ timeout (PostgreSQL `57014`, MySQL `3024`, MariaDB `1969`); `details.connectionCode` is
1665
+ `STATEMENT_TIMEOUT` there instead of `ETIMEDOUT`, the plan targets the query (`lint` →
1666
+ `explain` → re-run with `--statement-timeout <ms>`) rather than `doctor`, and no
1667
+ `branches` / `branchFork` / `verify` is emitted. PostgreSQL `57014` only counts as a
1668
+ statement timeout when the server says so — a `pg_cancel_backend()` cancel keeps the
1669
+ verbatim `Database error (57014): …` form instead of claiming a ceiling.
1670
+ The same `details.connectionCode` field distinguishes the other causes that share a
1671
+ connection code: `CONNECTION_LOST` (server closed the connection mid-session — PostgreSQL
1672
+ class 08 and `57P01`/`57P02`, MySQL `1053`), `TOO_MANY_CONNECTIONS` (PostgreSQL `53300`,
1673
+ MySQL `1040` — its plan counts current connections instead of running `doctor`, because
1674
+ rewriting host/port cannot create a slot), `SERVER_NOT_READY` (`57P03`, still starting up),
1675
+ `CONNECTION_REJECTED` (`08004` — the server answered and refused), `EHOSTUNREACH` (resolved
1676
+ but unroutable) and `TLS_ERROR` (handshake failure; reported as `CONN_UNKNOWN` rather than
1677
+ `CONN_AUTH_FAILED`, whose plan re-runs `init` for credentials that are not the problem).
1663
1678
  - `PERMISSION_DENIED` — active permission level forbids the operation.
1664
1679
  - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
1665
1680
  - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Post-install runtime check.
3
+ *
4
+ * `engines` declares bun only, but npm ignores engine fields it does not know, so an
5
+ * `npm install -g @carllee1983/dbcli` on a machine without Bun succeeds and leaves a
6
+ * `dbcli` on PATH whose `#!/usr/bin/env bun` shebang cannot start. This warns at the
7
+ * moment that happens, which is closer to the user than any README paragraph.
8
+ *
9
+ * Plain Node ESM on purpose: npm runs this with node, not bun. Never fails the install —
10
+ * a warning is the whole point, and a non-zero exit here would break the package.
11
+ */
12
+
13
+ import { spawnSync } from 'node:child_process'
14
+
15
+ const BUN_INSTALL_HINT =
16
+ process.platform === 'win32'
17
+ ? 'powershell -c "irm bun.sh/install.ps1 | iex"'
18
+ : 'curl -fsSL https://bun.sh/install | bash'
19
+
20
+ function bunIsAvailable() {
21
+ if (process.versions.bun) return true
22
+ try {
23
+ const result = spawnSync('bun', ['--version'], {
24
+ encoding: 'utf8',
25
+ timeout: 10_000,
26
+ shell: process.platform === 'win32',
27
+ })
28
+ return result.status === 0
29
+ } catch {
30
+ return false
31
+ }
32
+ }
33
+
34
+ if (!bunIsAvailable()) {
35
+ console.warn(
36
+ [
37
+ '',
38
+ 'dbcli: Bun was not found on PATH.',
39
+ '',
40
+ " The package installed, but dbcli's executable runs under Bun (its shebang is",
41
+ ' `#!/usr/bin/env bun`), so `dbcli` will not start until Bun 1.3.3+ is installed:',
42
+ '',
43
+ ` ${BUN_INSTALL_HINT}`,
44
+ '',
45
+ ' The ./agent-core subpath export is importable from Node without Bun.',
46
+ '',
47
+ ].join('\n')
48
+ )
49
+ }
@@ -1660,6 +1660,21 @@ shape (`schemaVersion: 1`):
1660
1660
  Recovery codes (fixed in v1.15.0):
1661
1661
  - `CONFIG_MISSING` — no `.dbcli` config; run `dbcli init`.
1662
1662
  - `CONN_REFUSED` / `CONN_AUTH_FAILED` / `CONN_TIMEOUT` / `CONN_HOST_NOT_FOUND` / `CONN_UNKNOWN` — connection failure variants.
1663
+ `CONN_TIMEOUT` also covers a statement the server canceled for exceeding the statement
1664
+ timeout (PostgreSQL `57014`, MySQL `3024`, MariaDB `1969`); `details.connectionCode` is
1665
+ `STATEMENT_TIMEOUT` there instead of `ETIMEDOUT`, the plan targets the query (`lint` →
1666
+ `explain` → re-run with `--statement-timeout <ms>`) rather than `doctor`, and no
1667
+ `branches` / `branchFork` / `verify` is emitted. PostgreSQL `57014` only counts as a
1668
+ statement timeout when the server says so — a `pg_cancel_backend()` cancel keeps the
1669
+ verbatim `Database error (57014): …` form instead of claiming a ceiling.
1670
+ The same `details.connectionCode` field distinguishes the other causes that share a
1671
+ connection code: `CONNECTION_LOST` (server closed the connection mid-session — PostgreSQL
1672
+ class 08 and `57P01`/`57P02`, MySQL `1053`), `TOO_MANY_CONNECTIONS` (PostgreSQL `53300`,
1673
+ MySQL `1040` — its plan counts current connections instead of running `doctor`, because
1674
+ rewriting host/port cannot create a slot), `SERVER_NOT_READY` (`57P03`, still starting up),
1675
+ `CONNECTION_REJECTED` (`08004` — the server answered and refused), `EHOSTUNREACH` (resolved
1676
+ but unroutable) and `TLS_ERROR` (handshake failure; reported as `CONN_UNKNOWN` rather than
1677
+ `CONN_AUTH_FAILED`, whose plan re-runs `init` for credentials that are not the problem).
1663
1678
  - `PERMISSION_DENIED` — active permission level forbids the operation.
1664
1679
  - `BLACKLIST_TABLE` / `BLACKLIST_COLUMN_WRITE` — blacklist violations.
1665
1680
  - `SNIPPET_NOT_FOUND` / `SNIPPET_AMBIGUOUS` / `SNIPPET_PARAM_MISSING` — saved-query failures.