@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.
@@ -51,7 +51,7 @@ var package_default;
51
51
  var init_package = __esm(() => {
52
52
  package_default = {
53
53
  name: "@carllee1983/dbcli",
54
- version: "1.54.0",
54
+ version: "1.55.0",
55
55
  description: "Database CLI for AI agents",
56
56
  type: "module",
57
57
  publishConfig: {
@@ -93,11 +93,11 @@ var init_package = __esm(() => {
93
93
  "blacklist"
94
94
  ],
95
95
  engines: {
96
- node: ">=18.0.0",
97
96
  bun: ">=1.3.3"
98
97
  },
99
98
  files: [
100
99
  "dist/",
100
+ "scripts/postinstall-check-bun.mjs",
101
101
  "!dist/.build-stamp",
102
102
  "assets/",
103
103
  "plugins/",
@@ -114,6 +114,7 @@ var init_package = __esm(() => {
114
114
  "LICENSE"
115
115
  ],
116
116
  scripts: {
117
+ postinstall: "bun scripts/postinstall-check-bun.mjs || node scripts/postinstall-check-bun.mjs || exit 0",
117
118
  dev: "bun run src/cli.ts",
118
119
  build: "bun run scripts/build.ts",
119
120
  "build:determinism": "bun run scripts/check-build-determinism.ts",
@@ -9674,10 +9675,12 @@ var init_types2 = __esm(() => {
9674
9675
  ConnectionError = class ConnectionError extends Error {
9675
9676
  code;
9676
9677
  hints;
9677
- constructor(code, message, hints) {
9678
+ limitMs;
9679
+ constructor(code, message, hints, limitMs) {
9678
9680
  super(message);
9679
9681
  this.code = code;
9680
9682
  this.hints = hints;
9683
+ this.limitMs = limitMs;
9681
9684
  this.name = "ConnectionError";
9682
9685
  Object.setPrototypeOf(this, ConnectionError.prototype);
9683
9686
  }
@@ -9836,18 +9839,28 @@ var init_capabilities = __esm(() => {
9836
9839
  });
9837
9840
 
9838
9841
  // src/adapters/error-mapper.ts
9839
- function categorize(errCode, errno, errMsg) {
9842
+ function isTransportDriverCode(code) {
9843
+ return TLS_CODE_PREFIX.test(code) || TRANSPORT_CODES[code] !== undefined;
9844
+ }
9845
+ function categorize(errCode, errno, errMsg, system) {
9840
9846
  if (errCode) {
9847
+ if (TLS_CODE_PREFIX.test(errCode))
9848
+ return "TLS_ERROR";
9841
9849
  const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
9850
+ if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
9851
+ return null;
9852
+ }
9842
9853
  if (known)
9843
9854
  return known;
9844
9855
  }
9845
- if (typeof errno === "number" && MYSQL_ERRNOS[errno])
9856
+ const isMysqlFamily = system === "mysql" || system === "mariadb";
9857
+ if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
9846
9858
  return MYSQL_ERRNOS[errno];
9859
+ }
9847
9860
  const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
9848
9861
  if (redisPrefix && REDIS_PREFIXES[redisPrefix])
9849
9862
  return REDIS_PREFIXES[redisPrefix];
9850
- if (errCode || typeof errno === "number")
9863
+ if (errCode || isMysqlFamily && typeof errno === "number")
9851
9864
  return null;
9852
9865
  for (const [pattern, category] of FALLBACK_PATTERNS) {
9853
9866
  if (pattern.test(errMsg))
@@ -9870,7 +9883,7 @@ function mapError(error, system, options) {
9870
9883
  const errMsg = String(err?.message || String(error));
9871
9884
  const errCode = String(err?.code || "");
9872
9885
  const errno = err?.errno;
9873
- switch (categorize(errCode, errno, errMsg)) {
9886
+ switch (categorize(errCode, errno, errMsg, system)) {
9874
9887
  case "ECONNREFUSED":
9875
9888
  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));
9876
9889
  case "ETIMEDOUT":
@@ -9879,6 +9892,42 @@ function mapError(error, system, options) {
9879
9892
  `Increase timeout: edit .dbcli and add "timeout": 15000`,
9880
9893
  `Verify network connectivity: ping ${options.host} -c 3`
9881
9894
  ]);
9895
+ case "CONNECTION_LOST":
9896
+ return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
9897
+ "Re-run the command: a dropped connection is often transient",
9898
+ `Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
9899
+ "If it happens on long-running work, look at the server idle/wait timeout"
9900
+ ]);
9901
+ case "SERVER_NOT_READY":
9902
+ return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
9903
+ "Retry shortly: this clears on its own once startup or recovery finishes",
9904
+ `Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
9905
+ "On a replica, this also appears while it catches up with the primary"
9906
+ ]);
9907
+ case "CONNECTION_REJECTED":
9908
+ return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
9909
+ "Check the server access rules (pg_hba.conf) for this user, database, and client address",
9910
+ "If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
9911
+ "Check whether the connection limit for this user or database is exhausted"
9912
+ ]);
9913
+ case "TOO_MANY_CONNECTIONS":
9914
+ return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
9915
+ "Wait and retry: the limit is on concurrent connections, not on you",
9916
+ 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',
9917
+ "Close idle sessions, or raise max_connections if the load is legitimate"
9918
+ ]);
9919
+ case "EHOSTUNREACH":
9920
+ return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
9921
+ "Check that you are on the right network or VPN",
9922
+ `Check routing and firewall between here and ${options.host}`,
9923
+ `Confirm the address is the one you meant: ${options.host}:${options.port}`
9924
+ ]);
9925
+ case "TLS_ERROR":
9926
+ return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
9927
+ 'Point the connection at the right CA bundle: set "caPath" in .dbcli',
9928
+ 'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
9929
+ `Confirm the certificate covers the host you connected to: ${options.host}`
9930
+ ]);
9882
9931
  case "ENOTFOUND":
9883
9932
  return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
9884
9933
  `Check the hostname spelling: ${options.host}`,
@@ -9909,6 +9958,14 @@ function mapError(error, system, options) {
9909
9958
  "Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
9910
9959
  ]);
9911
9960
  }
9961
+ case "STATEMENT_TIMEOUT": {
9962
+ const limitMs = options.statementTimeout ?? options.timeout;
9963
+ return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
9964
+ 'Inspect the query plan: dbcli explain "<sql>"',
9965
+ "Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
9966
+ "Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
9967
+ ], limitMs);
9968
+ }
9912
9969
  case "SQL_SYNTAX_ERROR":
9913
9970
  return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
9914
9971
  "Check your SQL syntax near the position reported above",
@@ -9929,7 +9986,7 @@ function mapError(error, system, options) {
9929
9986
  `Try connecting directly with the ${SYSTEM_FACTS[system].client} command-line tool`
9930
9987
  ]);
9931
9988
  }
9932
- var TRANSPORT_CODES, MYSQL_CODES, MYSQL_ERRNOS, SQLSTATES, REDIS_PREFIXES, FALLBACK_PATTERNS, SYSTEM_FACTS;
9989
+ var TRANSPORT_CODES, TLS_CODE_PREFIX, MYSQL_CODES, MYSQL_ERRNOS, SQLSTATES, REDIS_PREFIXES, FALLBACK_PATTERNS, PG_CANCEL_BY_TIMEOUT, SYSTEM_FACTS;
9933
9990
  var init_error_mapper = __esm(() => {
9934
9991
  init_types2();
9935
9992
  TRANSPORT_CODES = {
@@ -9938,9 +9995,16 @@ var init_error_mapper = __esm(() => {
9938
9995
  ESOCKETTIMEDOUT: "ETIMEDOUT",
9939
9996
  ENOTFOUND: "ENOTFOUND",
9940
9997
  EAI_AGAIN: "ENOTFOUND",
9941
- PROTOCOL_CONNECTION_LOST: "ECONNREFUSED",
9942
- PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT"
9943
- };
9998
+ PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
9999
+ PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
10000
+ ECONNRESET: "CONNECTION_LOST",
10001
+ EPIPE: "CONNECTION_LOST",
10002
+ ECONNABORTED: "CONNECTION_LOST",
10003
+ EHOSTUNREACH: "EHOSTUNREACH",
10004
+ ENETUNREACH: "EHOSTUNREACH",
10005
+ ENETDOWN: "EHOSTUNREACH"
10006
+ };
10007
+ TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
9944
10008
  MYSQL_CODES = {
9945
10009
  ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
9946
10010
  ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
@@ -9948,7 +10012,11 @@ var init_error_mapper = __esm(() => {
9948
10012
  ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
9949
10013
  ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
9950
10014
  ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
9951
- ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED"
10015
+ ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
10016
+ ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
10017
+ ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
10018
+ ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
10019
+ ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
9952
10020
  };
9953
10021
  MYSQL_ERRNOS = {
9954
10022
  1045: "AUTH_FAILED",
@@ -9957,14 +10025,30 @@ var init_error_mapper = __esm(() => {
9957
10025
  1064: "SQL_SYNTAX_ERROR",
9958
10026
  1146: "TABLE_NOT_FOUND",
9959
10027
  1251: "AUTH_FAILED",
9960
- 1698: "AUTH_FAILED"
10028
+ 1698: "AUTH_FAILED",
10029
+ 1040: "TOO_MANY_CONNECTIONS",
10030
+ 1053: "CONNECTION_LOST",
10031
+ 2006: "CONNECTION_LOST",
10032
+ 2013: "CONNECTION_LOST",
10033
+ 1969: "STATEMENT_TIMEOUT",
10034
+ 3024: "STATEMENT_TIMEOUT"
9961
10035
  };
9962
10036
  SQLSTATES = {
9963
10037
  "28000": "AUTH_FAILED",
9964
10038
  "28P01": "AUTH_FAILED",
9965
10039
  "42601": "SQL_SYNTAX_ERROR",
9966
10040
  "42P01": "TABLE_NOT_FOUND",
9967
- "42703": "COLUMN_NOT_FOUND"
10041
+ "42703": "COLUMN_NOT_FOUND",
10042
+ "57014": "STATEMENT_TIMEOUT",
10043
+ "08000": "CONNECTION_LOST",
10044
+ "08003": "CONNECTION_LOST",
10045
+ "08006": "CONNECTION_LOST",
10046
+ "08001": "ECONNREFUSED",
10047
+ "08004": "CONNECTION_REJECTED",
10048
+ "53300": "TOO_MANY_CONNECTIONS",
10049
+ "57P01": "CONNECTION_LOST",
10050
+ "57P02": "CONNECTION_LOST",
10051
+ "57P03": "SERVER_NOT_READY"
9968
10052
  };
9969
10053
  REDIS_PREFIXES = {
9970
10054
  NOAUTH: "AUTH_FAILED",
@@ -9976,6 +10060,10 @@ var init_error_mapper = __esm(() => {
9976
10060
  [/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
9977
10061
  [/timeout exceeded/i, "ETIMEDOUT"],
9978
10062
  [/timed out/i, "ETIMEDOUT"],
10063
+ [/connection terminated/i, "CONNECTION_LOST"],
10064
+ [/server closed the connection/i, "CONNECTION_LOST"],
10065
+ [/server has gone away/i, "CONNECTION_LOST"],
10066
+ [/lost connection to \S+ server/i, "CONNECTION_LOST"],
9979
10067
  [/getaddrinfo/i, "ENOTFOUND"],
9980
10068
  [/authentication failed/i, "AUTH_FAILED"],
9981
10069
  [/access denied for user/i, "AUTH_FAILED"],
@@ -9983,6 +10071,7 @@ var init_error_mapper = __esm(() => {
9983
10071
  [/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
9984
10072
  [/password supplied/i, "AUTH_FAILED"]
9985
10073
  ];
10074
+ PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
9986
10075
  SYSTEM_FACTS = {
9987
10076
  postgresql: {
9988
10077
  serviceCheck: "systemctl status postgresql",
@@ -17956,7 +18045,7 @@ async function mapWithConcurrency(items, limit, worker) {
17956
18045
  }
17957
18046
 
17958
18047
  // src/core/recovery/types.ts
17959
- var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, MAX_BRANCH_STEPS = 6, MAX_BRANCH_COUNT = 8, SchemaCacheMissingError, RECOVERY_CODE_METADATA;
18048
+ var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, MAX_BRANCH_STEPS = 6, MAX_BRANCH_COUNT = 8, STATEMENT_TIMEOUT_CODE = "STATEMENT_TIMEOUT", SchemaCacheMissingError, RECOVERY_CODE_METADATA;
17960
18049
  var init_types4 = __esm(() => {
17961
18050
  RECOVERY_CODES = [
17962
18051
  "CONFIG_MISSING",
@@ -18081,6 +18170,64 @@ function dryRunStepForWrite(ctx, quotedTable, placeholders) {
18081
18170
  };
18082
18171
  return draft;
18083
18172
  }
18173
+ function statementTimeoutSteps() {
18174
+ return [
18175
+ {
18176
+ command: 'dbcli lint "<sql>"',
18177
+ rationale: "Static anti-pattern read of the statement; needs no connection, so it cannot time out in turn.",
18178
+ risk: "readonly",
18179
+ expects: "Findings with rewrite drafts, or an empty list when nothing is flagged.",
18180
+ placeholders: ["<sql>"]
18181
+ },
18182
+ {
18183
+ command: 'dbcli explain "<sql>"',
18184
+ rationale: "Read the query plan to find the scan or join that exceeded the statement limit.",
18185
+ risk: "readonly",
18186
+ expects: "Annotated query plan; look for sequential scans and unindexed joins.",
18187
+ placeholders: ["<sql>"]
18188
+ },
18189
+ {
18190
+ command: 'dbcli --statement-timeout <ms> query "<sql>"',
18191
+ rationale: "Re-run with an explicit ceiling once the cost is understood; 0 removes the limit entirely.",
18192
+ risk: "readonly",
18193
+ expects: "Query result, or the same timeout if <ms> is still below what the plan costs.",
18194
+ placeholders: ["<ms>", "<sql>"]
18195
+ }
18196
+ ];
18197
+ }
18198
+ function tlsErrorSteps() {
18199
+ return [
18200
+ {
18201
+ command: "dbcli status --format json",
18202
+ rationale: "Read back the active connection without a live probe to confirm which host and TLS settings are in force.",
18203
+ risk: "readonly",
18204
+ expects: "JSON status with system / permission; no credentials."
18205
+ },
18206
+ {
18207
+ command: "dbcli doctor --format json",
18208
+ rationale: "Doctor reports the handshake failure verbatim, which names the certificate problem (expired, self-signed, altname mismatch).",
18209
+ risk: "readonly",
18210
+ expects: "JSON report whose connection check fails with the TLS error text."
18211
+ }
18212
+ ];
18213
+ }
18214
+ function connectionsExhaustedSteps(ctx) {
18215
+ const inspectSql = ctx.system === "postgresql" ? "SELECT count(*) FROM pg_stat_activity" : 'SHOW STATUS LIKE "Threads_connected"';
18216
+ return [
18217
+ {
18218
+ command: `dbcli query ${shellQuote(inspectSql)} --format json`,
18219
+ rationale: "Count the connections currently held so the limit can be compared against real usage; needs one free slot, so it may have to wait.",
18220
+ risk: "readonly",
18221
+ expects: "A single row with the current connection count."
18222
+ },
18223
+ {
18224
+ command: "dbcli doctor --format json",
18225
+ rationale: "Once a slot frees up, confirm the connection itself is healthy \u2014 the config was never the problem.",
18226
+ risk: "readonly",
18227
+ expects: "JSON report with the connection check passing."
18228
+ }
18229
+ ];
18230
+ }
18084
18231
  function stepsForCode(code, ctx) {
18085
18232
  const drafts = draftsForCode(code, ctx);
18086
18233
  return drafts.slice(0, MAX_RECOVERY_STEPS).map((d, i) => ({ ...d, order: i + 1 }));
@@ -18106,6 +18253,13 @@ function draftsForCode(code, ctx) {
18106
18253
  case "CONN_REFUSED":
18107
18254
  case "CONN_TIMEOUT":
18108
18255
  case "CONN_UNKNOWN": {
18256
+ if (code === "CONN_TIMEOUT" && ctx.connectionCode === STATEMENT_TIMEOUT_CODE) {
18257
+ return statementTimeoutSteps();
18258
+ }
18259
+ if (ctx.connectionCode === "TLS_ERROR")
18260
+ return tlsErrorSteps();
18261
+ if (ctx.connectionCode === "TOO_MANY_CONNECTIONS")
18262
+ return connectionsExhaustedSteps(ctx);
18109
18263
  const out = [
18110
18264
  {
18111
18265
  command: "dbcli doctor --format json",
@@ -18329,7 +18483,9 @@ function draftsForCode(code, ctx) {
18329
18483
  }
18330
18484
  }
18331
18485
  var MAX_RECOVERY_STEPS = 6;
18332
- var init_recovery_steps = () => {};
18486
+ var init_recovery_steps = __esm(() => {
18487
+ init_types4();
18488
+ });
18333
18489
 
18334
18490
  // src/utils/sql-lexical.ts
18335
18491
  function dollarQuoteDelimiterAt(sql, index) {
@@ -19055,7 +19211,9 @@ var init_types5 = __esm(() => {
19055
19211
  });
19056
19212
 
19057
19213
  // src/core/recovery/verify-steps.ts
19058
- function verifyForCode(code, _ctx) {
19214
+ function verifyForCode(code, ctx) {
19215
+ if (ctx.connectionCode === STATEMENT_TIMEOUT_CODE)
19216
+ return null;
19059
19217
  const command = VERIFY_COMMAND_BY_CODE[code];
19060
19218
  if (!command)
19061
19219
  return null;
@@ -19069,6 +19227,7 @@ function verifyForCode(code, _ctx) {
19069
19227
  }
19070
19228
  var VERIFY_COMMAND_BY_CODE, VERIFY_RATIONALE_BY_CODE, VERIFY_EXPECTS_BY_CODE;
19071
19229
  var init_verify_steps = __esm(() => {
19230
+ init_types4();
19072
19231
  VERIFY_COMMAND_BY_CODE = {
19073
19232
  CONFIG_MISSING: "dbcli inspect --no-connect --format json",
19074
19233
  CONN_REFUSED: "dbcli doctor --format json",
@@ -19326,7 +19485,7 @@ function classifyError(error, ctx) {
19326
19485
  const ctxWithDetails = applyDetailsToContext(ctx, recoveryError);
19327
19486
  const recovery = stepsForCode(recoveryError.code, ctxWithDetails);
19328
19487
  const verify = verifyForCode(recoveryError.code, ctxWithDetails);
19329
- const branchExtras = recoveryError.category === "connection" ? buildConnectionBranches(ctxWithDetails) : null;
19488
+ const branchExtras = recoveryError.category === "connection" && ctxWithDetails.connectionCode !== STATEMENT_TIMEOUT_CODE ? buildConnectionBranches(ctxWithDetails) : null;
19330
19489
  return {
19331
19490
  schemaVersion: RECOVERY_SCHEMA_VERSION,
19332
19491
  generatedAt: new Date().toISOString(),
@@ -19365,8 +19524,19 @@ function errorToRecoveryError(error, ctx) {
19365
19524
  return baseError("UNKNOWN");
19366
19525
  }
19367
19526
  function classifyConnection(err) {
19368
- const code = err.code === "ECONNREFUSED" ? "CONN_REFUSED" : err.code === "ETIMEDOUT" ? "CONN_TIMEOUT" : err.code === "AUTH_FAILED" ? "CONN_AUTH_FAILED" : err.code === "ENOTFOUND" ? "CONN_HOST_NOT_FOUND" : "CONN_UNKNOWN";
19369
- return baseError(code, { connectionCode: err.code });
19527
+ const code = RECOVERY_CODE_BY_CONNECTION_CODE[err.code];
19528
+ const base = baseError(code, { connectionCode: err.code });
19529
+ const override = MESSAGE_BY_CONNECTION_CODE[err.code];
19530
+ if (override)
19531
+ return { ...base, message: override };
19532
+ if (err.code === STATEMENT_TIMEOUT_CODE) {
19533
+ const ceiling = err.limitMs !== undefined ? ` The ceiling in force was ${err.limitMs}ms.` : "";
19534
+ return {
19535
+ ...base,
19536
+ message: `The server canceled the statement for exceeding the statement timeout.${ceiling}`
19537
+ };
19538
+ }
19539
+ return base;
19370
19540
  }
19371
19541
  function classifyBlacklist(err) {
19372
19542
  if (/touches blacklisted columns:/i.test(err.message)) {
@@ -19417,9 +19587,11 @@ function applyDetailsToContext(ctx, err) {
19417
19587
  ...ctx,
19418
19588
  table: err.details?.table ?? ctx.table,
19419
19589
  snippet: err.details?.snippet ?? ctx.snippet,
19420
- hint: err.details?.paramName ?? ctx.hint
19590
+ hint: err.details?.paramName ?? ctx.hint,
19591
+ connectionCode: err.details?.connectionCode ?? ctx.connectionCode
19421
19592
  };
19422
19593
  }
19594
+ var RECOVERY_CODE_BY_CONNECTION_CODE, MESSAGE_BY_CONNECTION_CODE;
19423
19595
  var init_classify = __esm(() => {
19424
19596
  init_types2();
19425
19597
  init_permission_guard();
@@ -19429,6 +19601,31 @@ var init_classify = __esm(() => {
19429
19601
  init_verify_steps();
19430
19602
  init_connection_branches();
19431
19603
  init_types4();
19604
+ RECOVERY_CODE_BY_CONNECTION_CODE = {
19605
+ ECONNREFUSED: "CONN_REFUSED",
19606
+ CONNECTION_LOST: "CONN_REFUSED",
19607
+ TOO_MANY_CONNECTIONS: "CONN_REFUSED",
19608
+ SERVER_NOT_READY: "CONN_REFUSED",
19609
+ CONNECTION_REJECTED: "CONN_REFUSED",
19610
+ ETIMEDOUT: "CONN_TIMEOUT",
19611
+ STATEMENT_TIMEOUT: "CONN_TIMEOUT",
19612
+ AUTH_FAILED: "CONN_AUTH_FAILED",
19613
+ TLS_ERROR: "CONN_UNKNOWN",
19614
+ ENOTFOUND: "CONN_HOST_NOT_FOUND",
19615
+ EHOSTUNREACH: "CONN_HOST_NOT_FOUND",
19616
+ SQL_SYNTAX_ERROR: "CONN_UNKNOWN",
19617
+ TABLE_NOT_FOUND: "CONN_UNKNOWN",
19618
+ COLUMN_NOT_FOUND: "CONN_UNKNOWN",
19619
+ UNKNOWN: "CONN_UNKNOWN"
19620
+ };
19621
+ MESSAGE_BY_CONNECTION_CODE = {
19622
+ EHOSTUNREACH: "The host name resolved, but the host or network is unreachable (routing or VPN).",
19623
+ TOO_MANY_CONNECTIONS: "The server has no connection slots left; the limit is on concurrent connections, not on this caller.",
19624
+ CONNECTION_LOST: "The connection was established and then dropped mid-session.",
19625
+ TLS_ERROR: "The TLS handshake failed (certificate or trust chain).",
19626
+ SERVER_NOT_READY: "The server is starting up or recovering and is not accepting connections yet.",
19627
+ CONNECTION_REJECTED: "The server answered and rejected the connection attempt (access rules, pooler, or a per-user limit)."
19628
+ };
19432
19629
  });
19433
19630
 
19434
19631
  // src/core/recovery/render-json.ts
@@ -20579,6 +20776,7 @@ __export(exports_recovery, {
20579
20776
  __resetExecutorForTests: () => __resetExecutorForTests,
20580
20777
  SchemaCacheMissingError: () => SchemaCacheMissingError,
20581
20778
  STEP_RESULT_SUMMARY_FIELD_CAP: () => STEP_RESULT_SUMMARY_FIELD_CAP,
20779
+ STATEMENT_TIMEOUT_CODE: () => STATEMENT_TIMEOUT_CODE,
20582
20780
  RECOVERY_SCHEMA_VERSION: () => RECOVERY_SCHEMA_VERSION,
20583
20781
  RECOVERY_CODE_METADATA: () => RECOVERY_CODE_METADATA,
20584
20782
  RECOVERY_CODES: () => RECOVERY_CODES,
@@ -24498,6 +24696,45 @@ var init_saved_queries = __esm(() => {
24498
24696
  init_snippet_paths();
24499
24697
  });
24500
24698
 
24699
+ // src/utils/connection-error-message.ts
24700
+ function isTransportFailure(error) {
24701
+ if (error instanceof ConnectionError)
24702
+ return IS_TRANSPORT_FAILURE[error.code];
24703
+ const raw = error?.code;
24704
+ return typeof raw === "string" && isTransportDriverCode(raw);
24705
+ }
24706
+ function presentConnectionError(error) {
24707
+ const key = IS_TRANSPORT_FAILURE[error.code] ? "errors.connection_failed" : "errors.message";
24708
+ return {
24709
+ message: t_vars(key, { message: error.message }),
24710
+ code: error.code,
24711
+ hints: error.hints
24712
+ };
24713
+ }
24714
+ var IS_TRANSPORT_FAILURE;
24715
+ var init_connection_error_message = __esm(() => {
24716
+ init_types2();
24717
+ init_error_mapper();
24718
+ init_message_loader();
24719
+ IS_TRANSPORT_FAILURE = {
24720
+ ECONNREFUSED: true,
24721
+ ETIMEDOUT: true,
24722
+ AUTH_FAILED: true,
24723
+ ENOTFOUND: true,
24724
+ EHOSTUNREACH: true,
24725
+ CONNECTION_LOST: true,
24726
+ TOO_MANY_CONNECTIONS: true,
24727
+ TLS_ERROR: true,
24728
+ SERVER_NOT_READY: true,
24729
+ CONNECTION_REJECTED: true,
24730
+ STATEMENT_TIMEOUT: false,
24731
+ SQL_SYNTAX_ERROR: false,
24732
+ TABLE_NOT_FOUND: false,
24733
+ COLUMN_NOT_FOUND: false,
24734
+ UNKNOWN: false
24735
+ };
24736
+ });
24737
+
24501
24738
  // src/commands/q-mongo.ts
24502
24739
  var exports_q_mongo = {};
24503
24740
  __export(exports_q_mongo, {
@@ -24834,7 +25071,7 @@ async function handleQError(error, snippetName, options, config) {
24834
25071
  process.exit(1);
24835
25072
  }
24836
25073
  if (error instanceof ConnectionError) {
24837
- printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
25074
+ printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
24838
25075
  process.exit(1);
24839
25076
  }
24840
25077
  printLocalizedCliError(t_vars("errors.message", { message: error.message }), error);
@@ -24952,6 +25189,7 @@ var init_q = __esm(() => {
24952
25189
  init_colors();
24953
25190
  init_applied_limit();
24954
25191
  init_cli_error();
25192
+ init_connection_error_message();
24955
25193
  init_strategies();
24956
25194
  init_slow_query_advisory();
24957
25195
  });
@@ -26197,7 +26435,7 @@ async function insertCommand(table, options, command) {
26197
26435
  process.exit(1);
26198
26436
  }
26199
26437
  if (error instanceof ConnectionError) {
26200
- printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
26438
+ printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
26201
26439
  process.exit(1);
26202
26440
  }
26203
26441
  const output = {
@@ -26213,6 +26451,7 @@ async function insertCommand(table, options, command) {
26213
26451
  var init_insert = __esm(() => {
26214
26452
  init_message_loader();
26215
26453
  init_cli_error();
26454
+ init_connection_error_message();
26216
26455
  init_adapters();
26217
26456
  init_data_executor();
26218
26457
  init_config();
@@ -26504,7 +26743,7 @@ async function updateCommand(table, options, command) {
26504
26743
  process.exit(1);
26505
26744
  }
26506
26745
  if (error instanceof ConnectionError) {
26507
- printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
26746
+ printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
26508
26747
  process.exit(1);
26509
26748
  }
26510
26749
  const output = {
@@ -26520,6 +26759,7 @@ async function updateCommand(table, options, command) {
26520
26759
  var init_update = __esm(() => {
26521
26760
  init_message_loader();
26522
26761
  init_cli_error();
26762
+ init_connection_error_message();
26523
26763
  init_adapters();
26524
26764
  init_data_executor();
26525
26765
  init_config();
@@ -26795,7 +27035,7 @@ async function deleteCommand(table, options, command) {
26795
27035
  process.exit(1);
26796
27036
  }
26797
27037
  if (error instanceof ConnectionError) {
26798
- printLocalizedCliError(t_vars("errors.connection_failed", { message: error.message }), error);
27038
+ printLocalizedCliError(formatCliError(presentConnectionError(error)), error);
26799
27039
  process.exit(1);
26800
27040
  }
26801
27041
  const output = {
@@ -26811,6 +27051,7 @@ async function deleteCommand(table, options, command) {
26811
27051
  var init_delete = __esm(() => {
26812
27052
  init_message_loader();
26813
27053
  init_cli_error();
27054
+ init_connection_error_message();
26814
27055
  init_adapters();
26815
27056
  init_data_executor();
26816
27057
  init_config();
@@ -43842,13 +44083,12 @@ class ReplEngine {
43842
44083
  return null;
43843
44084
  }
43844
44085
  isConnectionError(error) {
43845
- const e = error;
43846
- const msg = (e.message ?? "").toLowerCase();
43847
- return e.code === "ECONNREFUSED" || e.code === "ECONNRESET" || e.code === "ETIMEDOUT" || msg.includes("connection") || msg.includes("terminated") || msg.includes("socket");
44086
+ return isTransportFailure(error);
43848
44087
  }
43849
44088
  }
43850
44089
  var import_picocolors3;
43851
44090
  var init_repl_engine = __esm(() => {
44091
+ init_connection_error_message();
43852
44092
  init_input_classifier();
43853
44093
  init_meta_commands();
43854
44094
  init_history_manager();
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@carllee1983/dbcli",
6
- version: "1.54.0",
6
+ version: "1.55.0",
7
7
  description: "Database CLI for AI agents",
8
8
  type: "module",
9
9
  publishConfig: {
@@ -45,11 +45,11 @@ var package_default = {
45
45
  "blacklist"
46
46
  ],
47
47
  engines: {
48
- node: ">=18.0.0",
49
48
  bun: ">=1.3.3"
50
49
  },
51
50
  files: [
52
51
  "dist/",
52
+ "scripts/postinstall-check-bun.mjs",
53
53
  "!dist/.build-stamp",
54
54
  "assets/",
55
55
  "plugins/",
@@ -66,6 +66,7 @@ var package_default = {
66
66
  "LICENSE"
67
67
  ],
68
68
  scripts: {
69
+ postinstall: "bun scripts/postinstall-check-bun.mjs || node scripts/postinstall-check-bun.mjs || exit 0",
69
70
  dev: "bun run src/cli.ts",
70
71
  build: "bun run scripts/build.ts",
71
72
  "build:determinism": "bun run scripts/check-build-determinism.ts",
package/dist/core.d.ts CHANGED
@@ -129,21 +129,31 @@ export interface TableSchema {
129
129
  /** Type of table (table or view) */
130
130
  tableType?: "table" | "view";
131
131
  }
132
- /**
133
- * Connection error with categorized error code and troubleshooting hints
134
- */
132
+ type ConnectionErrorCode = "ECONNREFUSED" | "ETIMEDOUT" | "AUTH_FAILED" | "ENOTFOUND" | "EHOSTUNREACH" | "CONNECTION_LOST" | "TOO_MANY_CONNECTIONS" | "TLS_ERROR" | "SERVER_NOT_READY" | "CONNECTION_REJECTED" | "SQL_SYNTAX_ERROR" | "STATEMENT_TIMEOUT" | "TABLE_NOT_FOUND" | "COLUMN_NOT_FOUND" | "UNKNOWN";
135
133
  export declare class ConnectionError extends Error {
136
134
  /** Error category code */
137
- code: "ECONNREFUSED" | "ETIMEDOUT" | "AUTH_FAILED" | "ENOTFOUND" | "SQL_SYNTAX_ERROR" | "TABLE_NOT_FOUND" | "COLUMN_NOT_FOUND" | "UNKNOWN";
135
+ code: ConnectionErrorCode;
138
136
  /** Array of actionable troubleshooting hints */
139
137
  hints: string[];
138
+ /**
139
+ * The ceiling that was in force, in milliseconds. Set on STATEMENT_TIMEOUT so
140
+ * consumers can state it without parsing `message`; the recovery envelope needs
141
+ * it to tell an agent what `--statement-timeout <ms>` it was already up against.
142
+ */
143
+ limitMs?: number | undefined;
140
144
  constructor(
141
145
  /** Error category code */
142
- code: "ECONNREFUSED" | "ETIMEDOUT" | "AUTH_FAILED" | "ENOTFOUND" | "SQL_SYNTAX_ERROR" | "TABLE_NOT_FOUND" | "COLUMN_NOT_FOUND" | "UNKNOWN",
146
+ code: ConnectionErrorCode,
143
147
  /** User-friendly error message */
144
148
  message: string,
145
149
  /** Array of actionable troubleshooting hints */
146
- hints: string[]);
150
+ hints: string[],
151
+ /**
152
+ * The ceiling that was in force, in milliseconds. Set on STATEMENT_TIMEOUT so
153
+ * consumers can state it without parsing `message`; the recovery envelope needs
154
+ * it to tell an agent what `--statement-timeout <ms>` it was already up against.
155
+ */
156
+ limitMs?: number | undefined);
147
157
  }
148
158
  /**
149
159
  * Result of a database query or command execution