@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/.cursor/skills/dbcli/reference.md +15 -0
- package/.github/skills/dbcli/reference.md +15 -0
- package/CHANGELOG.md +20 -0
- package/README.dev.md +10 -7
- package/README.md +34 -20
- package/README.zh-TW.md +31 -20
- package/assets/reference.md +15 -0
- package/dist/cli-runtime.mjs +268 -28
- package/dist/cli.mjs +3 -2
- package/dist/core.d.ts +16 -6
- package/dist/core.mjs +95 -10
- package/package.json +3 -2
- package/plugins/dbcli-agent/INSTALL.md +5 -6
- package/plugins/dbcli-agent/scripts/install-dbcli.sh +6 -6
- package/plugins/dbcli-agent/skills/dbcli/reference.md +15 -0
- package/scripts/postinstall-check-bun.mjs +49 -0
- package/skills/dbcli/reference.md +15 -0
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
echo "
|
|
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.
|