@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/cli-runtime.mjs
CHANGED
|
@@ -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
|
+
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
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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,
|
|
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
|
|
19369
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|