@carllee1983/dbcli 7.0.0 → 8.0.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/.agents/skills/story-development/SKILL.md +70 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/dbcli.mdc +65 -2
- package/.cursor/skills/dbcli/reference.md +197 -5
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +65 -2
- package/.github/skills/dbcli/reference.md +197 -5
- package/CHANGELOG.md +299 -0
- package/assets/SKILL.md +65 -2
- package/assets/SKILL.zh-TW.md +60 -2
- package/assets/integration-kit/README.md +43 -0
- package/assets/integration-kit/external-consumers.ts +9 -0
- package/assets/integration-kit/skill-author-consumer.ts +77 -0
- package/assets/integration-kit/task-pack.md +18 -0
- package/assets/reference.md +197 -5
- package/assets/tasks/README.md +6 -0
- package/assets/tasks/analyze-table-perf.md +3 -3
- package/assets/tasks/audit-permissions.md +2 -2
- package/assets/tasks/connection-health.md +2 -2
- package/assets/tasks/design-review.md +3 -3
- package/assets/tasks/diagnose-slow-query.md +3 -3
- package/assets/tasks/migration-review.md +3 -3
- package/assets/tasks/mongo-safe-backfill.md +2 -2
- package/assets/tasks/mongo-schema-drift-review.md +2 -2
- package/assets/tasks/orm-drift-review.md +3 -3
- package/assets/tasks/pr-database-review.md +3 -3
- package/assets/tasks/safe-backfill-verify.md +3 -3
- package/assets/tasks/safe-backfill.md +3 -3
- package/assets/tasks/schema-drift-review.md +3 -3
- package/assets/tasks/slow-endpoint-investigation.md +19 -6
- package/assets/ui-template.html +18 -18
- package/dist/cli-runtime.mjs +25533 -22188
- package/dist/cli.mjs +7 -4
- package/dist/core.d.ts +297 -2
- package/dist/core.mjs +2075 -3
- package/dist/ui-style.css +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -3
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +65 -2
- package/plugins/dbcli-agent/skills/dbcli/reference.md +197 -5
- package/skills/dbcli/SKILL.md +65 -2
- package/skills/dbcli/reference.md +197 -5
package/dist/core.mjs
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
2
4
|
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
3
33
|
var __returnValue = (v) => v;
|
|
4
34
|
function __exportSetter(name, newValue) {
|
|
5
35
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -16,6 +46,76 @@ var __export = (target, all) => {
|
|
|
16
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
47
|
var __require = import.meta.require;
|
|
18
48
|
|
|
49
|
+
// node_modules/picocolors/picocolors.js
|
|
50
|
+
var require_picocolors = __commonJS((exports, module) => {
|
|
51
|
+
var p = process || {};
|
|
52
|
+
var argv = p.argv || [];
|
|
53
|
+
var env = p.env || {};
|
|
54
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
55
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
56
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
57
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
58
|
+
};
|
|
59
|
+
var replaceClose = (string, close, replace, index) => {
|
|
60
|
+
let result = "", cursor = 0;
|
|
61
|
+
do {
|
|
62
|
+
result += string.substring(cursor, index) + replace;
|
|
63
|
+
cursor = index + close.length;
|
|
64
|
+
index = string.indexOf(close, cursor);
|
|
65
|
+
} while (~index);
|
|
66
|
+
return result + string.substring(cursor);
|
|
67
|
+
};
|
|
68
|
+
var createColors = (enabled = isColorSupported) => {
|
|
69
|
+
let f = enabled ? formatter : () => String;
|
|
70
|
+
return {
|
|
71
|
+
isColorSupported: enabled,
|
|
72
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
73
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
74
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
75
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
76
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
77
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
78
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
79
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
80
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
81
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
82
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
83
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
84
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
85
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
86
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
87
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
88
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
89
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
90
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
91
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
92
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
93
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
94
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
95
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
96
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
97
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
98
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
99
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
100
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
101
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
102
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
103
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
104
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
105
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
106
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
107
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
108
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
109
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
110
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
111
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
112
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
module.exports = createColors();
|
|
116
|
+
module.exports.createColors = createColors;
|
|
117
|
+
});
|
|
118
|
+
|
|
19
119
|
// node_modules/lru-cache/dist/esm/node/index.min.js
|
|
20
120
|
import { tracingChannel as j, channel as G } from "diagnostics_channel";
|
|
21
121
|
var w, W, C, D = () => w.hasSubscribers || W.hasSubscribers, I, L, P = (u, e, t2, i) => {
|
|
@@ -988,15 +1088,26 @@ var init_schema_loader = __esm(() => {
|
|
|
988
1088
|
});
|
|
989
1089
|
|
|
990
1090
|
// src/adapters/types.ts
|
|
1091
|
+
var DATABASE_SYSTEMS = Object.freeze([
|
|
1092
|
+
"postgresql",
|
|
1093
|
+
"mysql",
|
|
1094
|
+
"mariadb",
|
|
1095
|
+
"mongodb",
|
|
1096
|
+
"redis",
|
|
1097
|
+
"elasticsearch"
|
|
1098
|
+
]);
|
|
1099
|
+
|
|
991
1100
|
class ConnectionError extends Error {
|
|
992
1101
|
code;
|
|
993
1102
|
hints;
|
|
994
1103
|
limitMs;
|
|
995
|
-
|
|
1104
|
+
retrySafe;
|
|
1105
|
+
constructor(code, message, hints, limitMs, retrySafe = true) {
|
|
996
1106
|
super(message);
|
|
997
1107
|
this.code = code;
|
|
998
1108
|
this.hints = hints;
|
|
999
1109
|
this.limitMs = limitMs;
|
|
1110
|
+
this.retrySafe = retrySafe;
|
|
1000
1111
|
this.name = "ConnectionError";
|
|
1001
1112
|
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
1002
1113
|
}
|
|
@@ -1036,7 +1147,26 @@ var COMMAND_CAPABILITY_KEYS = Object.freeze([
|
|
|
1036
1147
|
"auditTail",
|
|
1037
1148
|
"auditShow",
|
|
1038
1149
|
"auditClear",
|
|
1039
|
-
"auditHealth"
|
|
1150
|
+
"auditHealth",
|
|
1151
|
+
"explain",
|
|
1152
|
+
"plan",
|
|
1153
|
+
"impactAssess",
|
|
1154
|
+
"assert",
|
|
1155
|
+
"snapshot",
|
|
1156
|
+
"verify",
|
|
1157
|
+
"semantic",
|
|
1158
|
+
"design",
|
|
1159
|
+
"proxy",
|
|
1160
|
+
"proxyAnalyze",
|
|
1161
|
+
"verification",
|
|
1162
|
+
"verificationPrune",
|
|
1163
|
+
"evidence",
|
|
1164
|
+
"contract",
|
|
1165
|
+
"recovery",
|
|
1166
|
+
"backfillArtifact",
|
|
1167
|
+
"password",
|
|
1168
|
+
"capabilityDiscover",
|
|
1169
|
+
"capabilityCheck"
|
|
1040
1170
|
]);
|
|
1041
1171
|
function cap(status, tier, note) {
|
|
1042
1172
|
return Object.freeze({ status, tier, note });
|
|
@@ -1078,8 +1208,40 @@ var SQL_BASE = {
|
|
|
1078
1208
|
inspect: cap("supported", "readonly", "Agent context snapshot is supported."),
|
|
1079
1209
|
report: cap("supported", "readonly", "Diagnostic report is supported."),
|
|
1080
1210
|
guide: cap("supported", "readonly", "Deterministic next-command planner is supported."),
|
|
1211
|
+
explain: cap("supported", "readonly", "EXPLAIN plans are read from SQL engines."),
|
|
1212
|
+
plan: cap("supported", "readonly", "Offline write-risk analysis of SQL against the schema cache."),
|
|
1213
|
+
impactAssess: cap("supported", "local-write", "Offline impact assessment of a SQL schema change; writes its report locally."),
|
|
1214
|
+
assert: cap("supported", "local-write", "Data assertions over SQL results; optionally writes a verification artifact."),
|
|
1215
|
+
snapshot: cap("supported", "local-write", "Captures a SQL result fingerprint under .dbcli/snapshots/."),
|
|
1216
|
+
verify: cap("supported", "local-write", "Scenario verification over SQL; writes a verification artifact."),
|
|
1217
|
+
semantic: cap("supported", "readonly", "Governed semantic context and drift over a SQL schema cache."),
|
|
1218
|
+
design: cap("supported", "local-write", "Schema design authoring and comparison for SQL dialects; init writes a template."),
|
|
1219
|
+
proxy: cap("supported", "interactive", "Local observing proxy for the MySQL, MariaDB and PostgreSQL wire protocols."),
|
|
1220
|
+
password: cap("supported", "interactive", "Rotates the stored connection credential; refused under agent mode."),
|
|
1221
|
+
proxyAnalyze: cap("not-applicable", "readonly", "Reads locally recorded proxy events; never contacts an engine."),
|
|
1222
|
+
verification: cap("not-applicable", "readonly", "Inspects verification artifacts under .dbcli/verification/."),
|
|
1223
|
+
verificationPrune: cap("not-applicable", "local-write", "Deletes local verification artifacts; dry-run unless --execute --force."),
|
|
1224
|
+
evidence: cap("not-applicable", "local-write", "Composes, validates and renders evidence packs from local artifacts."),
|
|
1225
|
+
contract: cap("not-applicable", "readonly", "Validates and inspects governed semantic contracts offline."),
|
|
1226
|
+
recovery: cap("not-applicable", "readonly", "Renders recovery codes and their remediation steps from a static table."),
|
|
1227
|
+
backfillArtifact: cap("not-applicable", "local-write", "Builds a backfill artifact from a source manifest and v2 connection identity."),
|
|
1228
|
+
capabilityDiscover: cap("not-applicable", "readonly", "Emits the static capability catalog; opens no connection."),
|
|
1229
|
+
capabilityCheck: cap("not-applicable", "readonly", "Evaluates required capability ids against the local configuration only."),
|
|
1081
1230
|
...ENGINE_INDEPENDENT
|
|
1082
1231
|
};
|
|
1232
|
+
var SQL_ONLY_UNSUPPORTED = {
|
|
1233
|
+
explain: cap("unsupported", "none", "src/commands/explain.ts refuses a non-SQL connection."),
|
|
1234
|
+
plan: cap("unsupported", "none", "Risk analysis is SQL-dialect based; toSqlDialect resolves no dialect here."),
|
|
1235
|
+
assert: cap("unsupported", "none", "src/commands/assert.ts refuses a non-SQL connection."),
|
|
1236
|
+
snapshot: cap("unsupported", "none", "src/commands/snapshot.ts refuses a non-SQL connection."),
|
|
1237
|
+
verify: cap("unsupported", "none", "src/commands/verify.ts refuses a non-SQL connection."),
|
|
1238
|
+
proxy: cap("unsupported", "none", "The proxy speaks the MySQL and PostgreSQL wire protocols only.")
|
|
1239
|
+
};
|
|
1240
|
+
var SQL_MODE_LIMITED = {
|
|
1241
|
+
impactAssess: cap("limited", "local-write", "Runs against an ORM artifact; --against-cache needs a SQL connection."),
|
|
1242
|
+
semantic: cap("limited", "readonly", "Governed context, search and drift work; draft validate needs a SQL connection."),
|
|
1243
|
+
design: cap("limited", "local-write", "Authoring and ORM comparison work; --against-cache needs a SQL connection.")
|
|
1244
|
+
};
|
|
1083
1245
|
var ENGINE_CAPABILITIES = Object.freeze({
|
|
1084
1246
|
postgresql: Object.freeze({
|
|
1085
1247
|
...SQL_BASE,
|
|
@@ -1089,6 +1251,10 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
1089
1251
|
mariadb: Object.freeze(SQL_BASE),
|
|
1090
1252
|
mongodb: Object.freeze({
|
|
1091
1253
|
...SQL_BASE,
|
|
1254
|
+
...SQL_ONLY_UNSUPPORTED,
|
|
1255
|
+
...SQL_MODE_LIMITED,
|
|
1256
|
+
...SQL_MODE_LIMITED,
|
|
1257
|
+
...SQL_MODE_LIMITED,
|
|
1092
1258
|
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
1093
1259
|
schemaSingle: cap("limited", "readonly", "MongoDB schema is sampled from documents."),
|
|
1094
1260
|
schemaFullScan: cap("limited", "readonly", "Full scan is sampled and document-oriented."),
|
|
@@ -1108,6 +1274,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
1108
1274
|
}),
|
|
1109
1275
|
redis: Object.freeze({
|
|
1110
1276
|
...SQL_BASE,
|
|
1277
|
+
...SQL_ONLY_UNSUPPORTED,
|
|
1111
1278
|
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
1112
1279
|
schemaSingle: cap("limited", "readonly", "Per-key synthetic schema only."),
|
|
1113
1280
|
schemaFullScan: cap("unsupported", "none", "Redis has no full schema cache scan."),
|
|
@@ -1127,6 +1294,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
1127
1294
|
}),
|
|
1128
1295
|
elasticsearch: Object.freeze({
|
|
1129
1296
|
...SQL_BASE,
|
|
1297
|
+
...SQL_ONLY_UNSUPPORTED,
|
|
1130
1298
|
lint: cap("unsupported", "none", "Static lint accepts SQL connections only."),
|
|
1131
1299
|
schemaSingle: cap("limited", "readonly", "Schema flattens index mappings."),
|
|
1132
1300
|
schemaFullScan: cap("supported", "readonly", "Full scan iterates non-system indices."),
|
|
@@ -1145,8 +1313,272 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
1145
1313
|
shell: cap("limited", "interactive", "Dedicated Kibana Dev Tools-style REPL (method/path + JSON body, blank-line submit); read-focused.")
|
|
1146
1314
|
})
|
|
1147
1315
|
});
|
|
1316
|
+
// src/adapters/error-mapper.ts
|
|
1317
|
+
var TRANSPORT_CODES = {
|
|
1318
|
+
ECONNREFUSED: "ECONNREFUSED",
|
|
1319
|
+
ETIMEDOUT: "ETIMEDOUT",
|
|
1320
|
+
ESOCKETTIMEDOUT: "ETIMEDOUT",
|
|
1321
|
+
ENOTFOUND: "ENOTFOUND",
|
|
1322
|
+
EAI_AGAIN: "ENOTFOUND",
|
|
1323
|
+
PROTOCOL_CONNECTION_LOST: "CONNECTION_LOST",
|
|
1324
|
+
PROTOCOL_SEQUENCE_TIMEOUT: "ETIMEDOUT",
|
|
1325
|
+
ECONNRESET: "CONNECTION_LOST",
|
|
1326
|
+
EPIPE: "CONNECTION_LOST",
|
|
1327
|
+
ECONNABORTED: "CONNECTION_LOST",
|
|
1328
|
+
EHOSTUNREACH: "EHOSTUNREACH",
|
|
1329
|
+
ENETUNREACH: "EHOSTUNREACH",
|
|
1330
|
+
ENETDOWN: "EHOSTUNREACH"
|
|
1331
|
+
};
|
|
1332
|
+
function isTransportDriverCode(code) {
|
|
1333
|
+
return TLS_CODE_PREFIX.test(code) || TRANSPORT_CODES[code] !== undefined;
|
|
1334
|
+
}
|
|
1335
|
+
var TLS_CODE_PREFIX = /^(CERT_|SELF_SIGNED_|DEPTH_ZERO_|UNABLE_TO_(?:GET|VERIFY)_|ERR_TLS_|ERR_SSL_)/;
|
|
1336
|
+
var MYSQL_CODES = {
|
|
1337
|
+
ER_NO_SUCH_TABLE: "TABLE_NOT_FOUND",
|
|
1338
|
+
ER_BAD_FIELD_ERROR: "COLUMN_NOT_FOUND",
|
|
1339
|
+
ER_PARSE_ERROR: "SQL_SYNTAX_ERROR",
|
|
1340
|
+
ER_ACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1341
|
+
ER_DBACCESS_DENIED_ERROR: "AUTH_FAILED",
|
|
1342
|
+
ER_NOT_SUPPORTED_AUTH_MODE: "AUTH_FAILED",
|
|
1343
|
+
ER_MUST_CHANGE_PASSWORD_LOGIN: "AUTH_FAILED",
|
|
1344
|
+
ER_QUERY_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1345
|
+
ER_STATEMENT_TIMEOUT: "STATEMENT_TIMEOUT",
|
|
1346
|
+
ER_CON_COUNT_ERROR: "TOO_MANY_CONNECTIONS",
|
|
1347
|
+
ER_SERVER_SHUTDOWN: "CONNECTION_LOST"
|
|
1348
|
+
};
|
|
1349
|
+
var MYSQL_ERRNOS = {
|
|
1350
|
+
1045: "AUTH_FAILED",
|
|
1351
|
+
1044: "AUTH_FAILED",
|
|
1352
|
+
1054: "COLUMN_NOT_FOUND",
|
|
1353
|
+
1064: "SQL_SYNTAX_ERROR",
|
|
1354
|
+
1146: "TABLE_NOT_FOUND",
|
|
1355
|
+
1251: "AUTH_FAILED",
|
|
1356
|
+
1698: "AUTH_FAILED",
|
|
1357
|
+
1040: "TOO_MANY_CONNECTIONS",
|
|
1358
|
+
1053: "CONNECTION_LOST",
|
|
1359
|
+
2006: "CONNECTION_LOST",
|
|
1360
|
+
2013: "CONNECTION_LOST",
|
|
1361
|
+
1969: "STATEMENT_TIMEOUT",
|
|
1362
|
+
3024: "STATEMENT_TIMEOUT"
|
|
1363
|
+
};
|
|
1364
|
+
var SQLSTATES = {
|
|
1365
|
+
"28000": "AUTH_FAILED",
|
|
1366
|
+
"28P01": "AUTH_FAILED",
|
|
1367
|
+
"42601": "SQL_SYNTAX_ERROR",
|
|
1368
|
+
"42P01": "TABLE_NOT_FOUND",
|
|
1369
|
+
"42703": "COLUMN_NOT_FOUND",
|
|
1370
|
+
"57014": "STATEMENT_TIMEOUT",
|
|
1371
|
+
"08000": "CONNECTION_LOST",
|
|
1372
|
+
"08003": "CONNECTION_LOST",
|
|
1373
|
+
"08006": "CONNECTION_LOST",
|
|
1374
|
+
"08001": "ECONNREFUSED",
|
|
1375
|
+
"08004": "CONNECTION_REJECTED",
|
|
1376
|
+
"53300": "TOO_MANY_CONNECTIONS",
|
|
1377
|
+
"57P01": "CONNECTION_LOST",
|
|
1378
|
+
"57P02": "CONNECTION_LOST",
|
|
1379
|
+
"57P03": "SERVER_NOT_READY"
|
|
1380
|
+
};
|
|
1381
|
+
var REDIS_PREFIXES = {
|
|
1382
|
+
NOAUTH: "AUTH_FAILED",
|
|
1383
|
+
WRONGPASS: "AUTH_FAILED"
|
|
1384
|
+
};
|
|
1385
|
+
var FALLBACK_PATTERNS = [
|
|
1386
|
+
[/connection refused/i, "ECONNREFUSED"],
|
|
1387
|
+
[/\bECONNREFUSED\b/, "ECONNREFUSED"],
|
|
1388
|
+
[/connect(?:ion)? time(?:d )?out/i, "ETIMEDOUT"],
|
|
1389
|
+
[/timeout exceeded/i, "ETIMEDOUT"],
|
|
1390
|
+
[/timed out/i, "ETIMEDOUT"],
|
|
1391
|
+
[/connection terminated/i, "CONNECTION_LOST"],
|
|
1392
|
+
[/server closed the connection/i, "CONNECTION_LOST"],
|
|
1393
|
+
[/server has gone away/i, "CONNECTION_LOST"],
|
|
1394
|
+
[/lost connection to \S+ server/i, "CONNECTION_LOST"],
|
|
1395
|
+
[/getaddrinfo/i, "ENOTFOUND"],
|
|
1396
|
+
[/authentication failed/i, "AUTH_FAILED"],
|
|
1397
|
+
[/access denied for user/i, "AUTH_FAILED"],
|
|
1398
|
+
[/no pg_hba\.conf entry/i, "AUTH_FAILED"],
|
|
1399
|
+
[/role\s+".*?"\s+does not exist/i, "AUTH_FAILED"],
|
|
1400
|
+
[/password supplied/i, "AUTH_FAILED"]
|
|
1401
|
+
];
|
|
1402
|
+
var PG_CANCEL_BY_TIMEOUT = /statement timeout/i;
|
|
1403
|
+
function categorize(errCode, errno, errMsg, system) {
|
|
1404
|
+
if (errCode) {
|
|
1405
|
+
if (TLS_CODE_PREFIX.test(errCode))
|
|
1406
|
+
return "TLS_ERROR";
|
|
1407
|
+
const known = TRANSPORT_CODES[errCode] ?? MYSQL_CODES[errCode] ?? SQLSTATES[errCode] ?? REDIS_PREFIXES[errCode];
|
|
1408
|
+
if (known === "STATEMENT_TIMEOUT" && errCode === "57014" && !PG_CANCEL_BY_TIMEOUT.test(errMsg)) {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
if (known)
|
|
1412
|
+
return known;
|
|
1413
|
+
}
|
|
1414
|
+
const isMysqlFamily = system === "mysql" || system === "mariadb";
|
|
1415
|
+
if (isMysqlFamily && typeof errno === "number" && MYSQL_ERRNOS[errno]) {
|
|
1416
|
+
return MYSQL_ERRNOS[errno];
|
|
1417
|
+
}
|
|
1418
|
+
const redisPrefix = errMsg.match(/^([A-Z]+)\s/)?.[1];
|
|
1419
|
+
if (redisPrefix && REDIS_PREFIXES[redisPrefix])
|
|
1420
|
+
return REDIS_PREFIXES[redisPrefix];
|
|
1421
|
+
if (errCode || isMysqlFamily && typeof errno === "number")
|
|
1422
|
+
return null;
|
|
1423
|
+
for (const [pattern, category] of FALLBACK_PATTERNS) {
|
|
1424
|
+
if (pattern.test(errMsg))
|
|
1425
|
+
return category;
|
|
1426
|
+
}
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
var SYSTEM_FACTS = {
|
|
1430
|
+
postgresql: {
|
|
1431
|
+
serviceCheck: "systemctl status postgresql",
|
|
1432
|
+
defaultPort: "default 5432",
|
|
1433
|
+
firewallRule: "allow TCP 5432",
|
|
1434
|
+
client: "psql",
|
|
1435
|
+
logFile: "postgresql.log"
|
|
1436
|
+
},
|
|
1437
|
+
mysql: {
|
|
1438
|
+
serviceCheck: "systemctl status mysql",
|
|
1439
|
+
defaultPort: "default 3306",
|
|
1440
|
+
firewallRule: "allow TCP 3306",
|
|
1441
|
+
client: "mysql",
|
|
1442
|
+
logFile: "mysql.log"
|
|
1443
|
+
},
|
|
1444
|
+
mariadb: {
|
|
1445
|
+
serviceCheck: "systemctl status mariadb",
|
|
1446
|
+
defaultPort: "default 3306",
|
|
1447
|
+
firewallRule: "allow TCP 3306",
|
|
1448
|
+
client: "mariadb",
|
|
1449
|
+
logFile: "mariadb.log"
|
|
1450
|
+
},
|
|
1451
|
+
redis: {
|
|
1452
|
+
serviceCheck: "redis-cli ping",
|
|
1453
|
+
defaultPort: "default 6379",
|
|
1454
|
+
firewallRule: "allow TCP 6379",
|
|
1455
|
+
client: "redis-cli",
|
|
1456
|
+
logFile: "redis.log"
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
function serviceHints(system, options) {
|
|
1460
|
+
const facts = SYSTEM_FACTS[system];
|
|
1461
|
+
return [
|
|
1462
|
+
`Check that the ${system} service is running: ${facts.serviceCheck}`,
|
|
1463
|
+
`Verify the port is correct: ${facts.defaultPort}`,
|
|
1464
|
+
`Check that ${options.host} is reachable: ping ${options.host} or telnet ${options.host} ${options.port}`
|
|
1465
|
+
];
|
|
1466
|
+
}
|
|
1467
|
+
function mapError(error, system, options) {
|
|
1468
|
+
if (error instanceof ConnectionError)
|
|
1469
|
+
return error;
|
|
1470
|
+
const err = error;
|
|
1471
|
+
const errMsg = String(err?.message || String(error));
|
|
1472
|
+
const errCode = String(err?.code || "");
|
|
1473
|
+
const errno = err?.errno;
|
|
1474
|
+
switch (categorize(errCode, errno, errMsg, system)) {
|
|
1475
|
+
case "ECONNREFUSED":
|
|
1476
|
+
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));
|
|
1477
|
+
case "ETIMEDOUT":
|
|
1478
|
+
return new ConnectionError("ETIMEDOUT", `Connection timed out (${options.timeout || 5000}ms) \u2014 may be blocked by a firewall or slow network`, [
|
|
1479
|
+
`Check firewall rules: ${SYSTEM_FACTS[system].firewallRule}`,
|
|
1480
|
+
`Increase timeout: edit .dbcli and add "timeout": 15000`,
|
|
1481
|
+
`Verify network connectivity: ping ${options.host} -c 3`
|
|
1482
|
+
]);
|
|
1483
|
+
case "CONNECTION_LOST":
|
|
1484
|
+
return new ConnectionError("CONNECTION_LOST", `Connection to ${options.host}:${options.port} was lost mid-session \u2014 the server closed it or the network dropped`, [
|
|
1485
|
+
"Re-run the command: a dropped connection is often transient",
|
|
1486
|
+
`Check whether the server restarted or is cycling: ${SYSTEM_FACTS[system].logFile}`,
|
|
1487
|
+
"If it happens on long-running work, look at the server idle/wait timeout"
|
|
1488
|
+
]);
|
|
1489
|
+
case "SERVER_NOT_READY":
|
|
1490
|
+
return new ConnectionError("SERVER_NOT_READY", `${options.host}:${options.port} is not accepting connections yet \u2014 the server is starting up or recovering`, [
|
|
1491
|
+
"Retry shortly: this clears on its own once startup or recovery finishes",
|
|
1492
|
+
`Watch progress in the server log: ${SYSTEM_FACTS[system].logFile}`,
|
|
1493
|
+
"On a replica, this also appears while it catches up with the primary"
|
|
1494
|
+
]);
|
|
1495
|
+
case "CONNECTION_REJECTED":
|
|
1496
|
+
return new ConnectionError("CONNECTION_REJECTED", `${options.host}:${options.port} answered but rejected the connection attempt`, [
|
|
1497
|
+
"Check the server access rules (pg_hba.conf) for this user, database, and client address",
|
|
1498
|
+
"If a pooler (pgbouncer / ProxySQL) sits in front, check its own rules and limits",
|
|
1499
|
+
"Check whether the connection limit for this user or database is exhausted"
|
|
1500
|
+
]);
|
|
1501
|
+
case "TOO_MANY_CONNECTIONS":
|
|
1502
|
+
return new ConnectionError("TOO_MANY_CONNECTIONS", `${options.host}:${options.port} refused the connection \u2014 the server has no connection slots left`, [
|
|
1503
|
+
"Wait and retry: the limit is on concurrent connections, not on you",
|
|
1504
|
+
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',
|
|
1505
|
+
"Close idle sessions, or raise max_connections if the load is legitimate"
|
|
1506
|
+
]);
|
|
1507
|
+
case "EHOSTUNREACH":
|
|
1508
|
+
return new ConnectionError("EHOSTUNREACH", `No route to ${options.host} \u2014 the name resolved, but the host or network is unreachable`, [
|
|
1509
|
+
"Check that you are on the right network or VPN",
|
|
1510
|
+
`Check routing and firewall between here and ${options.host}`,
|
|
1511
|
+
`Confirm the address is the one you meant: ${options.host}:${options.port}`
|
|
1512
|
+
]);
|
|
1513
|
+
case "TLS_ERROR":
|
|
1514
|
+
return new ConnectionError("TLS_ERROR", `TLS handshake failed: ${errMsg}`, [
|
|
1515
|
+
'Point the connection at the right CA bundle: set "caPath" in .dbcli',
|
|
1516
|
+
'For a self-signed certificate in a trusted network, set "rejectUnauthorized": false',
|
|
1517
|
+
`Confirm the certificate covers the host you connected to: ${options.host}`
|
|
1518
|
+
]);
|
|
1519
|
+
case "ENOTFOUND":
|
|
1520
|
+
return new ConnectionError("ENOTFOUND", `Host not found: ${options.host}`, [
|
|
1521
|
+
`Check the hostname spelling: ${options.host}`,
|
|
1522
|
+
`Verify DNS resolution: nslookup ${options.host}`,
|
|
1523
|
+
`If using localhost, try 127.0.0.1 (IPv4 vs IPv6 issue)`
|
|
1524
|
+
]);
|
|
1525
|
+
case "AUTH_FAILED":
|
|
1526
|
+
return new ConnectionError("AUTH_FAILED", `Authentication failed \u2014 check your username or password`, [
|
|
1527
|
+
`Verify credentials: ${SYSTEM_FACTS[system].client} ${system === "postgresql" ? "-U" : "-u"} ${options.user} -h ${options.host}`,
|
|
1528
|
+
`Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)`,
|
|
1529
|
+
`Re-run dbcli init to update credentials`
|
|
1530
|
+
]);
|
|
1531
|
+
case "TABLE_NOT_FOUND": {
|
|
1532
|
+
const mysqlMatch = errMsg.match(/Table\s+'(?:[^.']+\.)?([^']+)'/i);
|
|
1533
|
+
const pgMatch = errMsg.match(/relation\s+"([^"]+)"/i);
|
|
1534
|
+
const tableName = mysqlMatch?.[1] || pgMatch?.[1] || "unknown";
|
|
1535
|
+
return new ConnectionError("TABLE_NOT_FOUND", `Table '${tableName}' not found in database '${options.database || "unknown"}'`, [
|
|
1536
|
+
"Run `dbcli list` to see available tables",
|
|
1537
|
+
"Run `dbcli schema <table>` to confirm the exact name (ORM export names may differ from DB table names)"
|
|
1538
|
+
]);
|
|
1539
|
+
}
|
|
1540
|
+
case "COLUMN_NOT_FOUND": {
|
|
1541
|
+
const mysqlMatch = errMsg.match(/Unknown column\s+'([^']+)'/i);
|
|
1542
|
+
const pgMatch = errMsg.match(/column\s+"([^"]+)"\s+does not exist/i);
|
|
1543
|
+
const columnName = mysqlMatch?.[1] || pgMatch?.[1] || "unknown";
|
|
1544
|
+
return new ConnectionError("COLUMN_NOT_FOUND", `Column '${columnName}' not found`, [
|
|
1545
|
+
"Run `dbcli schema <table>` to see the actual column names",
|
|
1546
|
+
"Column names are case-sensitive on some databases (PostgreSQL with quoted identifiers)"
|
|
1547
|
+
]);
|
|
1548
|
+
}
|
|
1549
|
+
case "STATEMENT_TIMEOUT": {
|
|
1550
|
+
const limitMs = options.statementTimeout ?? options.timeout;
|
|
1551
|
+
return new ConnectionError("STATEMENT_TIMEOUT", `Statement timed out${limitMs ? ` (${limitMs}ms)` : ""} \u2014 the server canceled this query before it finished`, [
|
|
1552
|
+
'Inspect the query plan: dbcli explain "<sql>"',
|
|
1553
|
+
"Raise the ceiling for this run: dbcli --statement-timeout <ms> \u2026 (0 removes it)",
|
|
1554
|
+
"Narrow the query: add WHERE filters, reduce the LIMIT, or index the scanned columns"
|
|
1555
|
+
], limitMs);
|
|
1556
|
+
}
|
|
1557
|
+
case "SQL_SYNTAX_ERROR":
|
|
1558
|
+
return new ConnectionError("SQL_SYNTAX_ERROR", `SQL syntax error: ${errMsg}`, [
|
|
1559
|
+
"Check your SQL syntax near the position reported above",
|
|
1560
|
+
"In query-only mode, dbcli auto-appends LIMIT 1000; use --no-limit to disable",
|
|
1561
|
+
"For SHOW/DESCRIBE/EXPLAIN statements, LIMIT is not allowed \u2014 these are not auto-limited"
|
|
1562
|
+
]);
|
|
1563
|
+
}
|
|
1564
|
+
const reportedCode = errCode || (typeof errno === "number" ? String(errno) : "");
|
|
1565
|
+
if (reportedCode) {
|
|
1566
|
+
return new ConnectionError("UNKNOWN", `Database error (${reportedCode}): ${errMsg}`, [
|
|
1567
|
+
"Run `dbcli schema <table>` to confirm the objects referenced by this statement",
|
|
1568
|
+
`Look up ${reportedCode} in the ${system} error reference for the exact cause`
|
|
1569
|
+
]);
|
|
1570
|
+
}
|
|
1571
|
+
return new ConnectionError("UNKNOWN", `Connection failed: ${errMsg}`, [
|
|
1572
|
+
`Check connection parameters: host=${options.host}, port=${options.port}, user=${options.user}`,
|
|
1573
|
+
`View server logs: ${SYSTEM_FACTS[system].logFile}`,
|
|
1574
|
+
`Try connecting directly with the ${SYSTEM_FACTS[system].client} command-line tool`
|
|
1575
|
+
]);
|
|
1576
|
+
}
|
|
1148
1577
|
// resources/lang/en/messages.json
|
|
1149
1578
|
var messages_default = {
|
|
1579
|
+
cli: {
|
|
1580
|
+
agent_output_option: "Emit one Operation Envelope v1 (capabilities and capabilities check only)"
|
|
1581
|
+
},
|
|
1150
1582
|
init: {
|
|
1151
1583
|
description: "Initialize dbcli configuration with .env parsing and interactive prompts",
|
|
1152
1584
|
welcome: "Welcome to dbcli",
|
|
@@ -1155,6 +1587,9 @@ var messages_default = {
|
|
|
1155
1587
|
prompt_port: "Database port: ",
|
|
1156
1588
|
prompt_user: "Database user: ",
|
|
1157
1589
|
prompt_password: "Database password: ",
|
|
1590
|
+
secret_alternatives_sql: "pass --password, supply the credential through .env or the process environment, or store a reference with --use-env-refs",
|
|
1591
|
+
secret_alternatives_mongo_password: "pass --password, supply the credential through .env or the process environment, or paste the connection string with --uri",
|
|
1592
|
+
secret_alternatives_mongo_uri: "pass the connection string with --uri",
|
|
1158
1593
|
prompt_name: "Database name: ",
|
|
1159
1594
|
prompt_permission: "Permission level (query-only, read-write, data-admin, admin): ",
|
|
1160
1595
|
env_parse_note: "Note: Unable to parse .env configuration, using interactive prompts",
|
|
@@ -1310,6 +1745,14 @@ var messages_default = {
|
|
|
1310
1745
|
},
|
|
1311
1746
|
errors: {
|
|
1312
1747
|
message: "Error: {message}",
|
|
1748
|
+
query_only_boundary_failed: "Could not establish the query-only database boundary for {system}: {detail}",
|
|
1749
|
+
query_only_boundary_verify: "Verify that {system} supports read-only transactions and that this account may start one",
|
|
1750
|
+
query_only_boundary_not_executed: "The target statement was not executed; fix the database setting or connection before retrying",
|
|
1751
|
+
query_only_cleanup_completed: "The query-only target completed, but transaction cleanup failed for {system}: {detail}",
|
|
1752
|
+
query_only_cleanup_uncertain: "The query-only target failed and its outcome is uncertain because transaction cleanup also failed for {system}: {detail}",
|
|
1753
|
+
query_only_cleanup_completed_retry: "The target statement ran; do not retry it unless repeating its external effects is safe",
|
|
1754
|
+
query_only_cleanup_uncertain_retry: "The target may have produced effects before failing; do not retry it unless repeating them is safe",
|
|
1755
|
+
query_only_cleanup_reconnect: "Reconnect before running another statement because this connection was discarded",
|
|
1313
1756
|
invalid_config: "Invalid configuration: {field}",
|
|
1314
1757
|
connection_failed: "Failed to connect to database: {message}",
|
|
1315
1758
|
permission_denied: "Permission denied (required: {required})",
|
|
@@ -1463,6 +1906,9 @@ Reference: {referencePath}`,
|
|
|
1463
1906
|
};
|
|
1464
1907
|
// resources/lang/zh-TW/messages.json
|
|
1465
1908
|
var messages_default2 = {
|
|
1909
|
+
cli: {
|
|
1910
|
+
agent_output_option: "\u8F38\u51FA\u4E00\u4EFD Operation Envelope v1\uFF08\u50C5\u652F\u63F4 capabilities \u8207 capabilities check\uFF09"
|
|
1911
|
+
},
|
|
1466
1912
|
init: {
|
|
1467
1913
|
description: "\u521D\u59CB\u5316 dbcli \u914D\u7F6E\uFF0C\u89E3\u6790 .env \u4E26\u9032\u884C\u4E92\u52D5\u5F0F\u63D0\u793A",
|
|
1468
1914
|
welcome: "\u6B61\u8FCE\u4F7F\u7528 dbcli",
|
|
@@ -1471,6 +1917,9 @@ var messages_default2 = {
|
|
|
1471
1917
|
prompt_port: "\u8CC7\u6599\u5EAB\u57E0\u865F\uFF1A",
|
|
1472
1918
|
prompt_user: "\u8CC7\u6599\u5EAB\u4F7F\u7528\u8005\uFF1A",
|
|
1473
1919
|
prompt_password: "\u8CC7\u6599\u5EAB\u5BC6\u78BC\uFF1A",
|
|
1920
|
+
secret_alternatives_sql: "\u6539\u7528 --password\u3001\u628A\u6191\u8B49\u653E\u9032 .env \u6216\u884C\u7A0B\u74B0\u5883\u8B8A\u6578\uFF0C\u6216\u4EE5 --use-env-refs \u5B58\u6210\u53C3\u7167",
|
|
1921
|
+
secret_alternatives_mongo_password: "\u6539\u7528 --password\u3001\u628A\u6191\u8B49\u653E\u9032 .env \u6216\u884C\u7A0B\u74B0\u5883\u8B8A\u6578\uFF0C\u6216\u4EE5 --uri \u8CBC\u4E0A\u9023\u7DDA\u5B57\u4E32",
|
|
1922
|
+
secret_alternatives_mongo_uri: "\u4EE5 --uri \u50B3\u5165\u9023\u7DDA\u5B57\u4E32",
|
|
1474
1923
|
prompt_name: "\u8CC7\u6599\u5EAB\u540D\u7A31\uFF1A",
|
|
1475
1924
|
prompt_permission: "\u6B0A\u9650\u7B49\u7D1A\uFF08query-only, read-write, data-admin, admin\uFF09\uFF1A",
|
|
1476
1925
|
env_parse_note: "\u63D0\u793A\uFF1A\u7121\u6CD5\u89E3\u6790 .env \u914D\u7F6E\uFF0C\u4F7F\u7528\u4E92\u52D5\u5F0F\u63D0\u793A",
|
|
@@ -1626,6 +2075,14 @@ var messages_default2 = {
|
|
|
1626
2075
|
},
|
|
1627
2076
|
errors: {
|
|
1628
2077
|
message: "\u932F\u8AA4\uFF1A{message}",
|
|
2078
|
+
query_only_boundary_failed: "\u7121\u6CD5\u70BA {system} \u5EFA\u7ACB query-only \u8CC7\u6599\u5EAB\u908A\u754C\uFF1A{detail}",
|
|
2079
|
+
query_only_boundary_verify: "\u8ACB\u78BA\u8A8D {system} \u652F\u63F4\u552F\u8B80\u4EA4\u6613\uFF0C\u4E14\u6B64\u5E33\u865F\u53EF\u4EE5\u555F\u52D5\u4EA4\u6613",
|
|
2080
|
+
query_only_boundary_not_executed: "\u76EE\u6A19\u8A9E\u53E5\u5C1A\u672A\u57F7\u884C\uFF1B\u8ACB\u4FEE\u6B63\u8CC7\u6599\u5EAB\u8A2D\u5B9A\u6216\u9023\u7DDA\u5F8C\u518D\u8A66",
|
|
2081
|
+
query_only_cleanup_completed: "query-only \u76EE\u6A19\u5DF2\u5B8C\u6210\uFF0C\u4F46 {system} \u7684\u4EA4\u6613\u6E05\u7406\u5931\u6557\uFF1A{detail}",
|
|
2082
|
+
query_only_cleanup_uncertain: "query-only \u76EE\u6A19\u5931\u6557\uFF0C\u4E14 {system} \u7684\u4EA4\u6613\u6E05\u7406\u4E5F\u5931\u6557\uFF0C\u56E0\u6B64\u7D50\u679C\u4E0D\u78BA\u5B9A\uFF1A{detail}",
|
|
2083
|
+
query_only_cleanup_completed_retry: "\u76EE\u6A19\u8A9E\u53E5\u5DF2\u57F7\u884C\uFF1B\u9664\u975E\u53EF\u5B89\u5168\u91CD\u8907\u5176\u5916\u90E8\u526F\u4F5C\u7528\uFF0C\u5426\u5247\u4E0D\u8981\u91CD\u8A66",
|
|
2084
|
+
query_only_cleanup_uncertain_retry: "\u76EE\u6A19\u8A9E\u53E5\u53EF\u80FD\u5728\u5931\u6557\u524D\u7522\u751F\u526F\u4F5C\u7528\uFF1B\u9664\u975E\u53EF\u5B89\u5168\u91CD\u8907\u9019\u4E9B\u526F\u4F5C\u7528\uFF0C\u5426\u5247\u4E0D\u8981\u91CD\u8A66",
|
|
2085
|
+
query_only_cleanup_reconnect: "\u6B64\u9023\u7DDA\u5DF2\u4E1F\u68C4\uFF1B\u57F7\u884C\u4E0B\u4E00\u53E5\u524D\u8ACB\u5148\u91CD\u65B0\u9023\u7DDA",
|
|
1629
2086
|
invalid_config: "\u914D\u7F6E\u7121\u6548\uFF1A{field}",
|
|
1630
2087
|
connection_failed: "\u7121\u6CD5\u9023\u63A5\u5230\u8CC7\u6599\u5EAB\uFF1A{message}",
|
|
1631
2088
|
permission_denied: "\u6B0A\u9650\u88AB\u62D2\uFF08\u9700\u8981\uFF1A{required}\uFF09",
|
|
@@ -2028,6 +2485,495 @@ var messageLoader = MessageLoader.getInstance();
|
|
|
2028
2485
|
var t = (key) => messageLoader.t(key);
|
|
2029
2486
|
var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
|
|
2030
2487
|
|
|
2488
|
+
// src/adapters/sql-adapter-utils.ts
|
|
2489
|
+
function queryOnlyBoundaryError(system, error) {
|
|
2490
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2491
|
+
return new ConnectionError("QUERY_ONLY_BOUNDARY_FAILED", t_vars("errors.query_only_boundary_failed", { system, detail }), [
|
|
2492
|
+
t_vars("errors.query_only_boundary_verify", { system }),
|
|
2493
|
+
t_vars("errors.query_only_boundary_not_executed", {})
|
|
2494
|
+
]);
|
|
2495
|
+
}
|
|
2496
|
+
function queryOnlyCleanupError(system, error, targetCompleted) {
|
|
2497
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2498
|
+
return new ConnectionError("CONNECTION_LOST", targetCompleted ? t_vars("errors.query_only_cleanup_completed", { system, detail }) : t_vars("errors.query_only_cleanup_uncertain", { system, detail }), [
|
|
2499
|
+
targetCompleted ? t_vars("errors.query_only_cleanup_completed_retry", {}) : t_vars("errors.query_only_cleanup_uncertain_retry", {}),
|
|
2500
|
+
t_vars("errors.query_only_cleanup_reconnect", {})
|
|
2501
|
+
], undefined, false);
|
|
2502
|
+
}
|
|
2503
|
+
function requireConnected(connection) {
|
|
2504
|
+
if (!connection) {
|
|
2505
|
+
throw new ConnectionError("UNKNOWN", "Database connection not established", [
|
|
2506
|
+
"Call connect() to establish a connection"
|
|
2507
|
+
]);
|
|
2508
|
+
}
|
|
2509
|
+
return connection;
|
|
2510
|
+
}
|
|
2511
|
+
async function withMappedConnectionError(system, options, operation) {
|
|
2512
|
+
try {
|
|
2513
|
+
return await operation();
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
throw mapError(error, system, options);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/utils/colors.ts
|
|
2520
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
2521
|
+
var colors = {
|
|
2522
|
+
success: (text) => import_picocolors.default.green(text),
|
|
2523
|
+
error: (text) => import_picocolors.default.red(text),
|
|
2524
|
+
warn: (text) => import_picocolors.default.yellow(text),
|
|
2525
|
+
info: (text) => import_picocolors.default.blue(text),
|
|
2526
|
+
dim: (text) => import_picocolors.default.dim(text),
|
|
2527
|
+
bold: (text) => import_picocolors.default.bold(text),
|
|
2528
|
+
keyword: (text) => import_picocolors.default.blue(import_picocolors.default.bold(text))
|
|
2529
|
+
};
|
|
2530
|
+
|
|
2531
|
+
// src/utils/db-version-check.ts
|
|
2532
|
+
var MIN_SUPPORTED_VERSIONS = {
|
|
2533
|
+
postgresql: "12.0",
|
|
2534
|
+
mysql: "8.0",
|
|
2535
|
+
mariadb: "10.5",
|
|
2536
|
+
elasticsearch: "7.10"
|
|
2537
|
+
};
|
|
2538
|
+
function parseVersionSegments(version) {
|
|
2539
|
+
const match = version.match(/^(\d+(?:\.\d+)*)/);
|
|
2540
|
+
if (!match)
|
|
2541
|
+
return [];
|
|
2542
|
+
return (match[1] ?? "").split(".").map(Number);
|
|
2543
|
+
}
|
|
2544
|
+
function isMariaDBVersion(versionString) {
|
|
2545
|
+
return /mariadb/i.test(versionString);
|
|
2546
|
+
}
|
|
2547
|
+
function extractMariaDBVersion(versionString) {
|
|
2548
|
+
const match = versionString.match(/(\d+\.\d+\.\d+)-MariaDB/i);
|
|
2549
|
+
if (match)
|
|
2550
|
+
return match[1] ?? versionString;
|
|
2551
|
+
const prefixMatch = versionString.match(/^5\.5\.5-(\d+\.\d+\.\d+)/);
|
|
2552
|
+
if (prefixMatch)
|
|
2553
|
+
return prefixMatch[1] ?? versionString;
|
|
2554
|
+
return versionString;
|
|
2555
|
+
}
|
|
2556
|
+
function compareVersions(a, b) {
|
|
2557
|
+
const segA = parseVersionSegments(a);
|
|
2558
|
+
const segB = parseVersionSegments(b);
|
|
2559
|
+
const len = Math.max(segA.length, segB.length);
|
|
2560
|
+
for (let i = 0;i < len; i++) {
|
|
2561
|
+
const diff = (segA[i] ?? 0) - (segB[i] ?? 0);
|
|
2562
|
+
if (diff !== 0)
|
|
2563
|
+
return diff;
|
|
2564
|
+
}
|
|
2565
|
+
return 0;
|
|
2566
|
+
}
|
|
2567
|
+
function checkDbVersion(rawVersion, declaredSystem) {
|
|
2568
|
+
const isMariaDB = isMariaDBVersion(rawVersion);
|
|
2569
|
+
const system = isMariaDB ? "mariadb" : declaredSystem;
|
|
2570
|
+
const serverVersion = isMariaDB ? extractMariaDBVersion(rawVersion) : rawVersion;
|
|
2571
|
+
const minVersion = MIN_SUPPORTED_VERSIONS[system];
|
|
2572
|
+
const supported = compareVersions(serverVersion, minVersion ?? "0") >= 0;
|
|
2573
|
+
return {
|
|
2574
|
+
serverVersion,
|
|
2575
|
+
system,
|
|
2576
|
+
supported,
|
|
2577
|
+
minVersion: minVersion ?? "0"
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
function warnIfUnsupported(result) {
|
|
2581
|
+
if (result.supported)
|
|
2582
|
+
return;
|
|
2583
|
+
const message = t_vars("version.unsupported_warning", {
|
|
2584
|
+
system: result.system,
|
|
2585
|
+
version: result.serverVersion,
|
|
2586
|
+
minVersion: result.minVersion
|
|
2587
|
+
});
|
|
2588
|
+
process.stderr.write(colors.warn(`\u26A0 ${message}`) + `
|
|
2589
|
+
`);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/utils/encoding.ts
|
|
2593
|
+
var CP1252_REVERSE = {
|
|
2594
|
+
8364: 128,
|
|
2595
|
+
8218: 130,
|
|
2596
|
+
402: 131,
|
|
2597
|
+
8222: 132,
|
|
2598
|
+
8230: 133,
|
|
2599
|
+
8224: 134,
|
|
2600
|
+
8225: 135,
|
|
2601
|
+
710: 136,
|
|
2602
|
+
8240: 137,
|
|
2603
|
+
352: 138,
|
|
2604
|
+
8249: 139,
|
|
2605
|
+
338: 140,
|
|
2606
|
+
381: 142,
|
|
2607
|
+
8216: 145,
|
|
2608
|
+
8217: 146,
|
|
2609
|
+
8220: 147,
|
|
2610
|
+
8221: 148,
|
|
2611
|
+
8226: 149,
|
|
2612
|
+
8211: 150,
|
|
2613
|
+
8212: 151,
|
|
2614
|
+
732: 152,
|
|
2615
|
+
8482: 153,
|
|
2616
|
+
353: 154,
|
|
2617
|
+
8250: 155,
|
|
2618
|
+
339: 156,
|
|
2619
|
+
382: 158,
|
|
2620
|
+
376: 159
|
|
2621
|
+
};
|
|
2622
|
+
function fixDoubleEncodedUtf8(str) {
|
|
2623
|
+
if (!str)
|
|
2624
|
+
return str;
|
|
2625
|
+
const bytes = [];
|
|
2626
|
+
for (const char of str) {
|
|
2627
|
+
const code = char.codePointAt(0);
|
|
2628
|
+
if (code < 128) {
|
|
2629
|
+
bytes.push(code);
|
|
2630
|
+
} else if (code <= 255) {
|
|
2631
|
+
bytes.push(code);
|
|
2632
|
+
} else {
|
|
2633
|
+
const cp1252Byte = CP1252_REVERSE[code];
|
|
2634
|
+
if (cp1252Byte !== undefined) {
|
|
2635
|
+
bytes.push(cp1252Byte);
|
|
2636
|
+
} else {
|
|
2637
|
+
return str;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
try {
|
|
2642
|
+
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
|
|
2643
|
+
if (decoded !== str) {
|
|
2644
|
+
return decoded;
|
|
2645
|
+
}
|
|
2646
|
+
} catch {}
|
|
2647
|
+
return str;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/adapters/identifier-quote.ts
|
|
2651
|
+
function quoteCharFor(dialect) {
|
|
2652
|
+
return dialect === "postgresql" ? '"' : "`";
|
|
2653
|
+
}
|
|
2654
|
+
function assertQuotable(name, what) {
|
|
2655
|
+
if (name.length === 0) {
|
|
2656
|
+
throw new Error(`${what} must not be empty`);
|
|
2657
|
+
}
|
|
2658
|
+
if (name.includes(String.fromCharCode(0))) {
|
|
2659
|
+
throw new Error(`${what} must not contain a NUL character`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function quoteIdentifier(name, dialect) {
|
|
2663
|
+
assertQuotable(name, "identifier");
|
|
2664
|
+
const q = quoteCharFor(dialect);
|
|
2665
|
+
return `${q}${name.split(q).join(q + q)}${q}`;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// src/adapters/timeout-policy.ts
|
|
2669
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
2670
|
+
function resolveTimeoutPolicy(options) {
|
|
2671
|
+
return {
|
|
2672
|
+
connectMs: options.timeout ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
2673
|
+
statementMs: options.statementTimeout ?? options.timeout
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function statementTimeoutSql(system, ms) {
|
|
2677
|
+
if (system === "mariadb") {
|
|
2678
|
+
return `SET SESSION max_statement_time = ${ms / 1000}`;
|
|
2679
|
+
}
|
|
2680
|
+
return `SET SESSION max_execution_time = ${Math.round(ms)}`;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// src/utils/connection-error-message.ts
|
|
2684
|
+
var IS_TRANSPORT_FAILURE = {
|
|
2685
|
+
ECONNREFUSED: true,
|
|
2686
|
+
ETIMEDOUT: true,
|
|
2687
|
+
AUTH_FAILED: true,
|
|
2688
|
+
ENOTFOUND: true,
|
|
2689
|
+
EHOSTUNREACH: true,
|
|
2690
|
+
CONNECTION_LOST: true,
|
|
2691
|
+
TOO_MANY_CONNECTIONS: true,
|
|
2692
|
+
TLS_ERROR: true,
|
|
2693
|
+
SERVER_NOT_READY: true,
|
|
2694
|
+
CONNECTION_REJECTED: true,
|
|
2695
|
+
STATEMENT_TIMEOUT: false,
|
|
2696
|
+
SQL_SYNTAX_ERROR: false,
|
|
2697
|
+
TABLE_NOT_FOUND: false,
|
|
2698
|
+
COLUMN_NOT_FOUND: false,
|
|
2699
|
+
QUERY_ONLY_BOUNDARY_FAILED: false,
|
|
2700
|
+
UNKNOWN: false
|
|
2701
|
+
};
|
|
2702
|
+
function isTransportFailure(error) {
|
|
2703
|
+
if (error instanceof ConnectionError)
|
|
2704
|
+
return IS_TRANSPORT_FAILURE[error.code];
|
|
2705
|
+
const raw = error?.code;
|
|
2706
|
+
return typeof raw === "string" && isTransportDriverCode(raw);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// src/adapters/mysql-adapter.ts
|
|
2710
|
+
function parseEnumValues(columnType) {
|
|
2711
|
+
const match = columnType.match(/^enum\((.+)\)$/i);
|
|
2712
|
+
if (!match)
|
|
2713
|
+
return;
|
|
2714
|
+
return (match[1] ?? "").split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
class MySQLAdapter {
|
|
2718
|
+
db = null;
|
|
2719
|
+
executionTail = Promise.resolve();
|
|
2720
|
+
options;
|
|
2721
|
+
system;
|
|
2722
|
+
constructor(options) {
|
|
2723
|
+
this.options = options;
|
|
2724
|
+
this.system = options.system === "mariadb" ? "mariadb" : "mysql";
|
|
2725
|
+
if (options.port < 1 || options.port > 65535) {
|
|
2726
|
+
throw new Error(`Invalid port number: ${options.port}`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
async connect() {
|
|
2730
|
+
await withMappedConnectionError(this.system, this.options, async () => {
|
|
2731
|
+
const timeouts = resolveTimeoutPolicy(this.options);
|
|
2732
|
+
const { default: driver } = await import("mysql2/promise");
|
|
2733
|
+
this.db = await driver.createConnection({
|
|
2734
|
+
host: this.options.host,
|
|
2735
|
+
port: this.options.port,
|
|
2736
|
+
user: this.options.user,
|
|
2737
|
+
password: this.options.password || undefined,
|
|
2738
|
+
database: this.options.database,
|
|
2739
|
+
charset: "utf8mb4",
|
|
2740
|
+
connectTimeout: timeouts.connectMs
|
|
2741
|
+
});
|
|
2742
|
+
await this.db.execute("SET NAMES utf8mb4");
|
|
2743
|
+
if (timeouts.statementMs !== undefined) {
|
|
2744
|
+
try {
|
|
2745
|
+
await this.db.query(statementTimeoutSql(this.system, timeouts.statementMs));
|
|
2746
|
+
} catch {}
|
|
2747
|
+
}
|
|
2748
|
+
await this.testConnection();
|
|
2749
|
+
try {
|
|
2750
|
+
const rawVersion = await this.getServerVersion();
|
|
2751
|
+
const result = checkDbVersion(rawVersion, this.system);
|
|
2752
|
+
warnIfUnsupported(result);
|
|
2753
|
+
} catch {}
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
async disconnect() {
|
|
2757
|
+
try {
|
|
2758
|
+
if (this.db) {
|
|
2759
|
+
await this.db.end();
|
|
2760
|
+
this.db = null;
|
|
2761
|
+
}
|
|
2762
|
+
} catch {}
|
|
2763
|
+
}
|
|
2764
|
+
async testConnection() {
|
|
2765
|
+
requireConnected(this.db);
|
|
2766
|
+
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2767
|
+
const result = await this.execute("SELECT 1 as count");
|
|
2768
|
+
return result.rows.length > 0;
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
async execute(sql, params, options) {
|
|
2772
|
+
const db = requireConnected(this.db);
|
|
2773
|
+
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2774
|
+
const execution = this.executionTail.then(async () => {
|
|
2775
|
+
if (options?.sqlMode === "native-read-only") {
|
|
2776
|
+
return this.executeReadOnly(db, sql, params);
|
|
2777
|
+
}
|
|
2778
|
+
const [result] = params ? await db.execute(sql, params) : await db.execute(sql);
|
|
2779
|
+
if (Array.isArray(result)) {
|
|
2780
|
+
return {
|
|
2781
|
+
rows: result,
|
|
2782
|
+
affectedRows: result.length
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
const header = result;
|
|
2786
|
+
return {
|
|
2787
|
+
rows: [],
|
|
2788
|
+
affectedRows: header.affectedRows || 0,
|
|
2789
|
+
lastInsertId: header.insertId
|
|
2790
|
+
};
|
|
2791
|
+
});
|
|
2792
|
+
this.executionTail = execution.then(() => {
|
|
2793
|
+
return;
|
|
2794
|
+
}, () => {
|
|
2795
|
+
return;
|
|
2796
|
+
});
|
|
2797
|
+
return execution;
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
async executeReadOnly(db, sql, params) {
|
|
2801
|
+
let primaryError;
|
|
2802
|
+
let cleanupError;
|
|
2803
|
+
let executionResult;
|
|
2804
|
+
let transactionStarted = false;
|
|
2805
|
+
try {
|
|
2806
|
+
try {
|
|
2807
|
+
await db.query("START TRANSACTION READ ONLY");
|
|
2808
|
+
transactionStarted = true;
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
const mapped = mapError(error, this.system, this.options);
|
|
2811
|
+
if (isTransportFailure(mapped))
|
|
2812
|
+
throw mapped;
|
|
2813
|
+
throw queryOnlyBoundaryError(this.system, error);
|
|
2814
|
+
}
|
|
2815
|
+
const [result] = params ? await db.execute(sql, params) : await db.execute(sql);
|
|
2816
|
+
if (Array.isArray(result)) {
|
|
2817
|
+
executionResult = { rows: result, affectedRows: result.length };
|
|
2818
|
+
} else {
|
|
2819
|
+
const header = result;
|
|
2820
|
+
executionResult = {
|
|
2821
|
+
rows: [],
|
|
2822
|
+
affectedRows: header.affectedRows || 0,
|
|
2823
|
+
lastInsertId: header.insertId
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
primaryError = error;
|
|
2828
|
+
} finally {
|
|
2829
|
+
if (transactionStarted) {
|
|
2830
|
+
try {
|
|
2831
|
+
await db.query("ROLLBACK");
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
db.destroy();
|
|
2834
|
+
this.db = null;
|
|
2835
|
+
cleanupError = error;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (cleanupError !== undefined) {
|
|
2840
|
+
throw queryOnlyCleanupError(this.system, cleanupError, primaryError === undefined);
|
|
2841
|
+
}
|
|
2842
|
+
if (primaryError !== undefined)
|
|
2843
|
+
throw primaryError;
|
|
2844
|
+
return executionResult;
|
|
2845
|
+
}
|
|
2846
|
+
async getServerVersion() {
|
|
2847
|
+
const result = await this.execute("SELECT VERSION() as version");
|
|
2848
|
+
return result.rows[0]?.version ?? "unknown";
|
|
2849
|
+
}
|
|
2850
|
+
async listTables() {
|
|
2851
|
+
requireConnected(this.db);
|
|
2852
|
+
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2853
|
+
const query = `
|
|
2854
|
+
SELECT
|
|
2855
|
+
t.TABLE_NAME as table_name,
|
|
2856
|
+
t.TABLE_ROWS as row_count,
|
|
2857
|
+
t.ENGINE as engine,
|
|
2858
|
+
t.TABLE_TYPE as table_type,
|
|
2859
|
+
COUNT(c.COLUMN_NAME) as column_count
|
|
2860
|
+
FROM information_schema.TABLES t
|
|
2861
|
+
LEFT JOIN information_schema.COLUMNS c
|
|
2862
|
+
ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
|
|
2863
|
+
WHERE t.TABLE_SCHEMA = DATABASE()
|
|
2864
|
+
GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE, t.TABLE_TYPE
|
|
2865
|
+
ORDER BY t.TABLE_NAME
|
|
2866
|
+
`;
|
|
2867
|
+
const result = await this.execute(query);
|
|
2868
|
+
return result.rows.map((row) => ({
|
|
2869
|
+
name: row.table_name,
|
|
2870
|
+
columns: [],
|
|
2871
|
+
columnCount: row.column_count,
|
|
2872
|
+
rowCount: row.row_count || 0,
|
|
2873
|
+
engine: row.engine,
|
|
2874
|
+
estimatedRowCount: row.row_count || 0,
|
|
2875
|
+
tableType: row.table_type === "VIEW" ? "view" : "table"
|
|
2876
|
+
}));
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
async getTableSchema(tableName, options) {
|
|
2880
|
+
requireConnected(this.db);
|
|
2881
|
+
return withMappedConnectionError(this.system, this.options, async () => {
|
|
2882
|
+
const columnQuery = `
|
|
2883
|
+
SELECT
|
|
2884
|
+
COLUMN_NAME as name,
|
|
2885
|
+
COLUMN_TYPE as type,
|
|
2886
|
+
IS_NULLABLE = 'YES' as nullable,
|
|
2887
|
+
COLUMN_DEFAULT as default_value,
|
|
2888
|
+
COLUMN_KEY = 'PRI' as is_primary_key,
|
|
2889
|
+
EXTRA LIKE '%auto_increment%' as auto_increment,
|
|
2890
|
+
COLUMN_COMMENT as comment
|
|
2891
|
+
FROM information_schema.COLUMNS
|
|
2892
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
2893
|
+
AND TABLE_NAME = ?
|
|
2894
|
+
ORDER BY ORDINAL_POSITION
|
|
2895
|
+
`;
|
|
2896
|
+
const columnResult = await this.execute(columnQuery, [tableName]);
|
|
2897
|
+
const columns = columnResult.rows;
|
|
2898
|
+
const fkQuery = `
|
|
2899
|
+
SELECT
|
|
2900
|
+
rc.CONSTRAINT_NAME as name,
|
|
2901
|
+
GROUP_CONCAT(kcu.COLUMN_NAME) as columns,
|
|
2902
|
+
rc.REFERENCED_TABLE_NAME as ref_table,
|
|
2903
|
+
GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME) as ref_columns
|
|
2904
|
+
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
2905
|
+
JOIN information_schema.KEY_COLUMN_USAGE kcu
|
|
2906
|
+
ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND rc.TABLE_NAME = kcu.TABLE_NAME
|
|
2907
|
+
WHERE kcu.TABLE_NAME = ? AND rc.CONSTRAINT_SCHEMA = DATABASE()
|
|
2908
|
+
GROUP BY rc.CONSTRAINT_NAME, rc.REFERENCED_TABLE_NAME
|
|
2909
|
+
`;
|
|
2910
|
+
const fkResult = await this.execute(fkQuery, [tableName]);
|
|
2911
|
+
const fkResults = fkResult.rows;
|
|
2912
|
+
const fkMap = new Map;
|
|
2913
|
+
for (const fk of fkResults) {
|
|
2914
|
+
const cols = fk.columns.split(",").map((c) => c.trim());
|
|
2915
|
+
const refCols = fk.ref_columns.split(",").map((c) => c.trim());
|
|
2916
|
+
if (cols.length === 1 && refCols.length === 1) {
|
|
2917
|
+
fkMap.set(cols[0], { table: fk.ref_table, column: refCols[0] });
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
const primaryKeyColumns = columns.filter((col) => col.is_primary_key).map((col) => col.name);
|
|
2921
|
+
const indexQuery = `
|
|
2922
|
+
SELECT
|
|
2923
|
+
INDEX_NAME as name,
|
|
2924
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
2925
|
+
NOT NON_UNIQUE as is_unique
|
|
2926
|
+
FROM information_schema.STATISTICS
|
|
2927
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
2928
|
+
AND TABLE_NAME = ?
|
|
2929
|
+
AND INDEX_NAME != 'PRIMARY'
|
|
2930
|
+
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
2931
|
+
`;
|
|
2932
|
+
const indexResult = await this.execute(indexQuery, [tableName]);
|
|
2933
|
+
const indexResults = indexResult.rows;
|
|
2934
|
+
const tableMetaQuery = `
|
|
2935
|
+
SELECT ENGINE as engine, TABLE_ROWS as estimated_rows
|
|
2936
|
+
FROM information_schema.TABLES
|
|
2937
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
2938
|
+
`;
|
|
2939
|
+
const tableMetaResult = await this.execute(tableMetaQuery, [tableName]);
|
|
2940
|
+
const tableMeta = tableMetaResult.rows[0];
|
|
2941
|
+
const estimatedRowCount = Math.max(0, tableMeta?.estimated_rows || 0);
|
|
2942
|
+
const exactRowCount = options?.exactRowCount === false ? undefined : (await this.execute(`SELECT COUNT(*) as count FROM ${quoteIdentifier(tableName, this.system)}`)).rows[0]?.count;
|
|
2943
|
+
const schema = {
|
|
2944
|
+
name: tableName,
|
|
2945
|
+
columns: columns.map((col) => ({
|
|
2946
|
+
name: col.name,
|
|
2947
|
+
type: col.type,
|
|
2948
|
+
nullable: col.nullable,
|
|
2949
|
+
default: col.default_value || undefined,
|
|
2950
|
+
primaryKey: col.is_primary_key,
|
|
2951
|
+
foreignKey: fkMap.get(col.name),
|
|
2952
|
+
autoIncrement: col.auto_increment,
|
|
2953
|
+
comment: col.comment ? fixDoubleEncodedUtf8(col.comment) : null,
|
|
2954
|
+
enumValues: parseEnumValues(col.type)
|
|
2955
|
+
})),
|
|
2956
|
+
rowCount: exactRowCount ?? estimatedRowCount,
|
|
2957
|
+
engine: tableMeta?.engine || "MySQL",
|
|
2958
|
+
primaryKey: primaryKeyColumns,
|
|
2959
|
+
foreignKeys: fkResults.map((fk) => ({
|
|
2960
|
+
name: fk.name,
|
|
2961
|
+
columns: fk.columns.split(",").map((c) => c.trim()),
|
|
2962
|
+
refTable: fk.ref_table,
|
|
2963
|
+
refColumns: fk.ref_columns.split(",").map((c) => c.trim())
|
|
2964
|
+
})),
|
|
2965
|
+
indexes: indexResults.map((idx) => ({
|
|
2966
|
+
name: idx.name,
|
|
2967
|
+
columns: idx.columns.split(",").map((c) => c.trim()),
|
|
2968
|
+
unique: idx.is_unique
|
|
2969
|
+
})),
|
|
2970
|
+
estimatedRowCount
|
|
2971
|
+
};
|
|
2972
|
+
return schema;
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2031
2977
|
// src/utils/case-fold.ts
|
|
2032
2978
|
function foldCase(text) {
|
|
2033
2979
|
const lowered = (text.includes("\u0130") ? text.replaceAll("\u0130", "i") : text).toLowerCase();
|
|
@@ -3177,6 +4123,17 @@ function removeParameterMarkers(sql) {
|
|
|
3177
4123
|
return sql.replace(/\$\d+/g, "").replace(/\?/g, "");
|
|
3178
4124
|
}
|
|
3179
4125
|
|
|
4126
|
+
// src/core/permission/rank.ts
|
|
4127
|
+
var PERMISSION_RANK = Object.freeze({
|
|
4128
|
+
"query-only": 1,
|
|
4129
|
+
"read-write": 2,
|
|
4130
|
+
"data-admin": 3,
|
|
4131
|
+
admin: 4
|
|
4132
|
+
});
|
|
4133
|
+
function permissionAtLeast(actual, required) {
|
|
4134
|
+
return PERMISSION_RANK[actual] >= PERMISSION_RANK[required];
|
|
4135
|
+
}
|
|
4136
|
+
|
|
3180
4137
|
// src/core/permission-guard.ts
|
|
3181
4138
|
class PermissionError extends Error {
|
|
3182
4139
|
classification;
|
|
@@ -4172,7 +5129,9 @@ class QueryExecutor {
|
|
|
4172
5129
|
if (this.blacklistValidator && referencedTables.length > 0) {
|
|
4173
5130
|
this.blacklistValidator.checkTablesBlacklist(classification.type, referencedTables);
|
|
4174
5131
|
}
|
|
4175
|
-
const resultData = await this.adapter.execute(executeSql
|
|
5132
|
+
const resultData = await this.adapter.execute(executeSql, undefined, {
|
|
5133
|
+
sqlMode: this.permission === "query-only" ? "native-read-only" : "normal"
|
|
5134
|
+
});
|
|
4176
5135
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
4177
5136
|
const limitedResult = appliedLimit === undefined ? undefined : trimAppliedLimit(resultData.rows, appliedLimit);
|
|
4178
5137
|
const rows = limitedResult?.rows ?? resultData.rows;
|
|
@@ -9590,6 +10549,1106 @@ DBCLI_PASSWORD=${password}
|
|
|
9590
10549
|
}
|
|
9591
10550
|
}
|
|
9592
10551
|
};
|
|
10552
|
+
// src/core/capabilities/types.ts
|
|
10553
|
+
var CAPABILITY_CONTRACT_SCHEMA_VERSION = 1;
|
|
10554
|
+
// src/core/capabilities/registry.ts
|
|
10555
|
+
var DECLARATIONS = [
|
|
10556
|
+
{
|
|
10557
|
+
id: "connection.init",
|
|
10558
|
+
key: "init",
|
|
10559
|
+
command: "init",
|
|
10560
|
+
description: "Create or update a local connection configuration interactively.",
|
|
10561
|
+
permission: "query-only",
|
|
10562
|
+
requiresConnection: true,
|
|
10563
|
+
mutatesConfiguration: true
|
|
10564
|
+
},
|
|
10565
|
+
{
|
|
10566
|
+
id: "connection.select",
|
|
10567
|
+
key: "use",
|
|
10568
|
+
command: "use",
|
|
10569
|
+
description: "Switch the default named connection in a v2 configuration.",
|
|
10570
|
+
permission: "query-only",
|
|
10571
|
+
requiresConnection: false,
|
|
10572
|
+
mutatesConfiguration: true
|
|
10573
|
+
},
|
|
10574
|
+
{
|
|
10575
|
+
id: "connection.status",
|
|
10576
|
+
key: "status",
|
|
10577
|
+
command: "status",
|
|
10578
|
+
description: "Report the configured engine, permission and blacklist counts without credentials.",
|
|
10579
|
+
permission: "query-only",
|
|
10580
|
+
requiresConnection: false
|
|
10581
|
+
},
|
|
10582
|
+
{
|
|
10583
|
+
id: "connection.diagnose",
|
|
10584
|
+
key: "doctor",
|
|
10585
|
+
command: "doctor",
|
|
10586
|
+
description: "Run engine-specific connectivity and configuration diagnostics.",
|
|
10587
|
+
permission: "query-only",
|
|
10588
|
+
requiresConnection: true
|
|
10589
|
+
},
|
|
10590
|
+
{
|
|
10591
|
+
id: "schema.list-tables",
|
|
10592
|
+
key: "list",
|
|
10593
|
+
command: "list",
|
|
10594
|
+
description: "List the tables, collections or indices visible to the connection.",
|
|
10595
|
+
permission: "query-only",
|
|
10596
|
+
requiresConnection: true
|
|
10597
|
+
},
|
|
10598
|
+
{
|
|
10599
|
+
id: "schema.read",
|
|
10600
|
+
key: "schema",
|
|
10601
|
+
command: "schema",
|
|
10602
|
+
description: "Read visible schema metadata for one object or the whole database.",
|
|
10603
|
+
permission: "query-only",
|
|
10604
|
+
requiresConnection: true
|
|
10605
|
+
},
|
|
10606
|
+
{
|
|
10607
|
+
id: "schema.read-object",
|
|
10608
|
+
key: "schemaSingle",
|
|
10609
|
+
command: "schema",
|
|
10610
|
+
description: "Read the column or field structure of a single named object.",
|
|
10611
|
+
permission: "query-only",
|
|
10612
|
+
requiresConnection: true
|
|
10613
|
+
},
|
|
10614
|
+
{
|
|
10615
|
+
id: "schema.scan",
|
|
10616
|
+
key: "schemaFullScan",
|
|
10617
|
+
command: "schema",
|
|
10618
|
+
description: "Scan every visible object and refresh the local schema cache.",
|
|
10619
|
+
permission: "query-only",
|
|
10620
|
+
requiresConnection: true
|
|
10621
|
+
},
|
|
10622
|
+
{
|
|
10623
|
+
id: "schema.diff",
|
|
10624
|
+
key: "diff",
|
|
10625
|
+
command: "diff",
|
|
10626
|
+
description: "Compare schema snapshots and report structural differences.",
|
|
10627
|
+
permission: "query-only",
|
|
10628
|
+
requiresConnection: true
|
|
10629
|
+
},
|
|
10630
|
+
{
|
|
10631
|
+
id: "schema.migrate",
|
|
10632
|
+
key: "migrate",
|
|
10633
|
+
command: "migrate",
|
|
10634
|
+
description: "Apply guarded DDL changes such as columns, indexes and constraints.",
|
|
10635
|
+
statementType: "ALTER",
|
|
10636
|
+
requiresConnection: true
|
|
10637
|
+
},
|
|
10638
|
+
{
|
|
10639
|
+
id: "query.read",
|
|
10640
|
+
key: "query",
|
|
10641
|
+
command: "query",
|
|
10642
|
+
description: "Run a read query through the permission, blacklist and audit gates.",
|
|
10643
|
+
statementType: "SELECT",
|
|
10644
|
+
requiresConnection: true
|
|
10645
|
+
},
|
|
10646
|
+
{
|
|
10647
|
+
id: "query.lint",
|
|
10648
|
+
key: "lint",
|
|
10649
|
+
command: "lint",
|
|
10650
|
+
description: "Statically analyse SQL without connecting to the database.",
|
|
10651
|
+
permission: "query-only",
|
|
10652
|
+
requiresConnection: false
|
|
10653
|
+
},
|
|
10654
|
+
{
|
|
10655
|
+
id: "query.format",
|
|
10656
|
+
key: "queryOutput",
|
|
10657
|
+
command: "query",
|
|
10658
|
+
description: "Render query results as a table, JSON or CSV.",
|
|
10659
|
+
permission: "query-only",
|
|
10660
|
+
requiresConnection: true
|
|
10661
|
+
},
|
|
10662
|
+
{
|
|
10663
|
+
id: "query.limit-guard",
|
|
10664
|
+
key: "queryLimitGuard",
|
|
10665
|
+
command: "query",
|
|
10666
|
+
description: "Bound result size automatically and report the limit that was applied.",
|
|
10667
|
+
permission: "query-only",
|
|
10668
|
+
requiresConnection: true
|
|
10669
|
+
},
|
|
10670
|
+
{
|
|
10671
|
+
id: "query.shell",
|
|
10672
|
+
key: "shell",
|
|
10673
|
+
command: "shell",
|
|
10674
|
+
description: "Open an interactive gated query shell.",
|
|
10675
|
+
permission: "query-only",
|
|
10676
|
+
requiresConnection: true
|
|
10677
|
+
},
|
|
10678
|
+
{
|
|
10679
|
+
id: "snippet.run",
|
|
10680
|
+
key: "q",
|
|
10681
|
+
command: "q",
|
|
10682
|
+
description: "Execute a saved read-only query snippet with parameters.",
|
|
10683
|
+
permission: "query-only",
|
|
10684
|
+
requiresConnection: true
|
|
10685
|
+
},
|
|
10686
|
+
{
|
|
10687
|
+
id: "snippet.manage",
|
|
10688
|
+
key: "queries",
|
|
10689
|
+
command: "queries",
|
|
10690
|
+
description: "Create, edit, search and validate saved query snippets.",
|
|
10691
|
+
permission: "query-only",
|
|
10692
|
+
requiresConnection: false
|
|
10693
|
+
},
|
|
10694
|
+
{
|
|
10695
|
+
id: "data.insert",
|
|
10696
|
+
key: "insert",
|
|
10697
|
+
command: "insert",
|
|
10698
|
+
description: "Insert rows or documents through the write gate.",
|
|
10699
|
+
statementType: "INSERT",
|
|
10700
|
+
requiresConnection: true
|
|
10701
|
+
},
|
|
10702
|
+
{
|
|
10703
|
+
id: "data.update",
|
|
10704
|
+
key: "update",
|
|
10705
|
+
command: "update",
|
|
10706
|
+
description: "Update rows or documents through the write gate.",
|
|
10707
|
+
statementType: "UPDATE",
|
|
10708
|
+
requiresConnection: true
|
|
10709
|
+
},
|
|
10710
|
+
{
|
|
10711
|
+
id: "data.delete",
|
|
10712
|
+
key: "delete",
|
|
10713
|
+
command: "delete",
|
|
10714
|
+
description: "Delete rows or documents through the write gate.",
|
|
10715
|
+
statementType: "DELETE",
|
|
10716
|
+
requiresConnection: true
|
|
10717
|
+
},
|
|
10718
|
+
{
|
|
10719
|
+
id: "data.export",
|
|
10720
|
+
key: "export",
|
|
10721
|
+
command: "export",
|
|
10722
|
+
description: "Export query or object contents to a file, subject to the blacklist.",
|
|
10723
|
+
permission: "query-only",
|
|
10724
|
+
requiresConnection: true
|
|
10725
|
+
},
|
|
10726
|
+
{
|
|
10727
|
+
id: "data.health-check",
|
|
10728
|
+
key: "check",
|
|
10729
|
+
command: "check",
|
|
10730
|
+
description: "Check data health: nulls, orphans, duplicates and empty strings.",
|
|
10731
|
+
permission: "query-only",
|
|
10732
|
+
requiresConnection: true
|
|
10733
|
+
},
|
|
10734
|
+
{
|
|
10735
|
+
id: "blacklist.manage",
|
|
10736
|
+
key: "blacklist",
|
|
10737
|
+
command: "blacklist",
|
|
10738
|
+
description: "Inspect and edit the table, column and key rules that hide sensitive data.",
|
|
10739
|
+
permission: "query-only",
|
|
10740
|
+
requiresConnection: false,
|
|
10741
|
+
mutatesConfiguration: true
|
|
10742
|
+
},
|
|
10743
|
+
{
|
|
10744
|
+
id: "context.inspect",
|
|
10745
|
+
key: "inspect",
|
|
10746
|
+
command: "inspect",
|
|
10747
|
+
description: "Produce a bounded agent context snapshot of the configured database.",
|
|
10748
|
+
permission: "query-only",
|
|
10749
|
+
requiresConnection: true
|
|
10750
|
+
},
|
|
10751
|
+
{
|
|
10752
|
+
id: "diagnostic.report",
|
|
10753
|
+
key: "report",
|
|
10754
|
+
command: "report",
|
|
10755
|
+
description: "Produce a diagnostic report about the configured database.",
|
|
10756
|
+
permission: "query-only",
|
|
10757
|
+
requiresConnection: true
|
|
10758
|
+
},
|
|
10759
|
+
{
|
|
10760
|
+
id: "guide.plan",
|
|
10761
|
+
key: "guide",
|
|
10762
|
+
command: "guide",
|
|
10763
|
+
description: "Suggest the next deterministic command for a stated goal.",
|
|
10764
|
+
permission: "query-only",
|
|
10765
|
+
requiresConnection: true
|
|
10766
|
+
},
|
|
10767
|
+
{
|
|
10768
|
+
id: "recovery.plan",
|
|
10769
|
+
key: "recover",
|
|
10770
|
+
command: "recover",
|
|
10771
|
+
description: "Read the saved recovery envelope and plan or apply safe remediation steps.",
|
|
10772
|
+
permission: "query-only",
|
|
10773
|
+
requiresConnection: false
|
|
10774
|
+
},
|
|
10775
|
+
{
|
|
10776
|
+
id: "audit.tail",
|
|
10777
|
+
key: "auditTail",
|
|
10778
|
+
command: "audit tail",
|
|
10779
|
+
description: "Read recent audit entries from the local log.",
|
|
10780
|
+
permission: "query-only",
|
|
10781
|
+
requiresConnection: false
|
|
10782
|
+
},
|
|
10783
|
+
{
|
|
10784
|
+
id: "audit.show",
|
|
10785
|
+
key: "auditShow",
|
|
10786
|
+
command: "audit show",
|
|
10787
|
+
description: "Look up one audit entry by id prefix or recovery reference.",
|
|
10788
|
+
permission: "query-only",
|
|
10789
|
+
requiresConnection: false
|
|
10790
|
+
},
|
|
10791
|
+
{
|
|
10792
|
+
id: "audit.health",
|
|
10793
|
+
key: "auditHealth",
|
|
10794
|
+
command: "audit health",
|
|
10795
|
+
description: "Report audit log health and rotation state.",
|
|
10796
|
+
permission: "query-only",
|
|
10797
|
+
requiresConnection: false
|
|
10798
|
+
},
|
|
10799
|
+
{
|
|
10800
|
+
id: "audit.clear",
|
|
10801
|
+
key: "auditClear",
|
|
10802
|
+
command: "audit clear",
|
|
10803
|
+
description: "Remove the local audit log files for a connection.",
|
|
10804
|
+
permission: "admin",
|
|
10805
|
+
requiresConnection: false
|
|
10806
|
+
},
|
|
10807
|
+
{
|
|
10808
|
+
id: "skill.install",
|
|
10809
|
+
key: "skill",
|
|
10810
|
+
command: "skill",
|
|
10811
|
+
description: "Write the dbcli skill and task-pack assets into a platform directory.",
|
|
10812
|
+
permission: "query-only",
|
|
10813
|
+
requiresConnection: false
|
|
10814
|
+
},
|
|
10815
|
+
{
|
|
10816
|
+
id: "shell-completion.generate",
|
|
10817
|
+
key: "completion",
|
|
10818
|
+
command: "completion",
|
|
10819
|
+
description: "Emit a shell completion script for the current command tree.",
|
|
10820
|
+
permission: "query-only",
|
|
10821
|
+
requiresConnection: false
|
|
10822
|
+
},
|
|
10823
|
+
{
|
|
10824
|
+
id: "package.upgrade-check",
|
|
10825
|
+
key: "upgrade",
|
|
10826
|
+
command: "upgrade",
|
|
10827
|
+
description: "Check whether a newer dbcli release is available.",
|
|
10828
|
+
permission: "query-only",
|
|
10829
|
+
requiresConnection: false
|
|
10830
|
+
},
|
|
10831
|
+
{
|
|
10832
|
+
id: "query.explain",
|
|
10833
|
+
key: "explain",
|
|
10834
|
+
command: "explain",
|
|
10835
|
+
description: "Read the engine execution plan for a statement without running it as a write.",
|
|
10836
|
+
permission: "query-only",
|
|
10837
|
+
requiresConnection: true
|
|
10838
|
+
},
|
|
10839
|
+
{
|
|
10840
|
+
id: "query.plan-risk",
|
|
10841
|
+
key: "plan",
|
|
10842
|
+
command: "plan",
|
|
10843
|
+
description: "Assess the write risk of a statement offline, against the cached schema.",
|
|
10844
|
+
permission: "query-only",
|
|
10845
|
+
requiresConnection: false
|
|
10846
|
+
},
|
|
10847
|
+
{
|
|
10848
|
+
id: "schema.impact-assess",
|
|
10849
|
+
key: "impactAssess",
|
|
10850
|
+
command: "impact assess",
|
|
10851
|
+
description: "Assess the known effects of a proposed schema change offline.",
|
|
10852
|
+
permission: "query-only",
|
|
10853
|
+
requiresConnection: false
|
|
10854
|
+
},
|
|
10855
|
+
{
|
|
10856
|
+
id: "data.assert",
|
|
10857
|
+
key: "assert",
|
|
10858
|
+
command: "assert",
|
|
10859
|
+
description: "Check a data expectation and report a pass or fail verdict.",
|
|
10860
|
+
permission: "query-only",
|
|
10861
|
+
requiresConnection: true
|
|
10862
|
+
},
|
|
10863
|
+
{
|
|
10864
|
+
id: "data.snapshot",
|
|
10865
|
+
key: "snapshot",
|
|
10866
|
+
command: "snapshot",
|
|
10867
|
+
description: "Capture a result fingerprint for later comparison.",
|
|
10868
|
+
permission: "query-only",
|
|
10869
|
+
requiresConnection: true
|
|
10870
|
+
},
|
|
10871
|
+
{
|
|
10872
|
+
id: "verification.run",
|
|
10873
|
+
key: "verify",
|
|
10874
|
+
command: "verify",
|
|
10875
|
+
description: "Verify a named change scenario and record the outcome.",
|
|
10876
|
+
permission: "query-only",
|
|
10877
|
+
requiresConnection: true
|
|
10878
|
+
},
|
|
10879
|
+
{
|
|
10880
|
+
id: "verification.inspect",
|
|
10881
|
+
key: "verification",
|
|
10882
|
+
command: "verification",
|
|
10883
|
+
description: "Inspect locally stored verification artifacts.",
|
|
10884
|
+
permission: "query-only",
|
|
10885
|
+
requiresConnection: false
|
|
10886
|
+
},
|
|
10887
|
+
{
|
|
10888
|
+
id: "verification.prune",
|
|
10889
|
+
key: "verificationPrune",
|
|
10890
|
+
command: "verification prune",
|
|
10891
|
+
description: "Delete locally stored verification artifacts by age or subject.",
|
|
10892
|
+
permission: "query-only",
|
|
10893
|
+
requiresConnection: false
|
|
10894
|
+
},
|
|
10895
|
+
{
|
|
10896
|
+
id: "evidence.pack",
|
|
10897
|
+
key: "evidence",
|
|
10898
|
+
command: "evidence",
|
|
10899
|
+
description: "Compose, validate and render an evidence pack from local artifacts.",
|
|
10900
|
+
permission: "query-only",
|
|
10901
|
+
requiresConnection: false
|
|
10902
|
+
},
|
|
10903
|
+
{
|
|
10904
|
+
id: "semantic.context",
|
|
10905
|
+
key: "semantic",
|
|
10906
|
+
command: "semantic",
|
|
10907
|
+
description: "Author and validate governed semantic context over the cached schema.",
|
|
10908
|
+
permission: "query-only",
|
|
10909
|
+
requiresConnection: false
|
|
10910
|
+
},
|
|
10911
|
+
{
|
|
10912
|
+
id: "semantic.contract",
|
|
10913
|
+
key: "contract",
|
|
10914
|
+
command: "contract",
|
|
10915
|
+
description: "Validate and search governed business contracts offline.",
|
|
10916
|
+
permission: "query-only",
|
|
10917
|
+
requiresConnection: false
|
|
10918
|
+
},
|
|
10919
|
+
{
|
|
10920
|
+
id: "schema.design",
|
|
10921
|
+
key: "design",
|
|
10922
|
+
command: "design",
|
|
10923
|
+
description: "Author a schema design and compare it against the database or an ORM.",
|
|
10924
|
+
permission: "query-only",
|
|
10925
|
+
requiresConnection: false
|
|
10926
|
+
},
|
|
10927
|
+
{
|
|
10928
|
+
id: "diagnostic.proxy",
|
|
10929
|
+
key: "proxy",
|
|
10930
|
+
command: "proxy",
|
|
10931
|
+
description: "Run a local observing proxy in front of a supported SQL engine.",
|
|
10932
|
+
permission: "query-only",
|
|
10933
|
+
requiresConnection: true
|
|
10934
|
+
},
|
|
10935
|
+
{
|
|
10936
|
+
id: "diagnostic.proxy-analyze",
|
|
10937
|
+
key: "proxyAnalyze",
|
|
10938
|
+
command: "proxy analyze",
|
|
10939
|
+
description: "Analyse locally recorded proxy events without contacting an engine.",
|
|
10940
|
+
permission: "query-only",
|
|
10941
|
+
requiresConnection: false
|
|
10942
|
+
},
|
|
10943
|
+
{
|
|
10944
|
+
id: "recovery.codes",
|
|
10945
|
+
key: "recovery",
|
|
10946
|
+
command: "recovery",
|
|
10947
|
+
description: "Describe a recovery code and its remediation steps.",
|
|
10948
|
+
permission: "query-only",
|
|
10949
|
+
requiresConnection: false
|
|
10950
|
+
},
|
|
10951
|
+
{
|
|
10952
|
+
id: "data.backfill-artifact",
|
|
10953
|
+
key: "backfillArtifact",
|
|
10954
|
+
command: "backfill artifact",
|
|
10955
|
+
description: "Build a backfill artifact from a source manifest and a connection identity.",
|
|
10956
|
+
permission: "query-only",
|
|
10957
|
+
requiresConnection: false
|
|
10958
|
+
},
|
|
10959
|
+
{
|
|
10960
|
+
id: "connection.rotate-credential",
|
|
10961
|
+
key: "password",
|
|
10962
|
+
command: "password",
|
|
10963
|
+
description: "Change the stored credential for a single connection.",
|
|
10964
|
+
permission: "query-only",
|
|
10965
|
+
requiresConnection: true,
|
|
10966
|
+
mutatesConfiguration: true
|
|
10967
|
+
},
|
|
10968
|
+
{
|
|
10969
|
+
id: "capability.discover",
|
|
10970
|
+
key: "capabilityDiscover",
|
|
10971
|
+
command: "capabilities",
|
|
10972
|
+
description: "List the static capability catalog this build publishes.",
|
|
10973
|
+
permission: "query-only",
|
|
10974
|
+
requiresConnection: false
|
|
10975
|
+
},
|
|
10976
|
+
{
|
|
10977
|
+
id: "capability.check",
|
|
10978
|
+
key: "capabilityCheck",
|
|
10979
|
+
command: "capabilities check",
|
|
10980
|
+
description: "Check whether required capability ids are available in this project.",
|
|
10981
|
+
permission: "query-only",
|
|
10982
|
+
requiresConnection: false
|
|
10983
|
+
}
|
|
10984
|
+
];
|
|
10985
|
+
function riskForSideEffect(tier) {
|
|
10986
|
+
switch (tier) {
|
|
10987
|
+
case "readonly":
|
|
10988
|
+
case "none":
|
|
10989
|
+
return "readonly";
|
|
10990
|
+
case "dry-run":
|
|
10991
|
+
return "dry-run";
|
|
10992
|
+
case "local-write":
|
|
10993
|
+
case "db-write":
|
|
10994
|
+
case "interactive":
|
|
10995
|
+
return "write";
|
|
10996
|
+
default:
|
|
10997
|
+
return "unknown";
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
function enginesFor(key) {
|
|
11001
|
+
const statuses = DATABASE_SYSTEMS.map((system) => ({
|
|
11002
|
+
system,
|
|
11003
|
+
status: ENGINE_CAPABILITIES[system][key].status
|
|
11004
|
+
}));
|
|
11005
|
+
if (statuses.every((entry) => entry.status === "not-applicable")) {
|
|
11006
|
+
return { engines: [...DATABASE_SYSTEMS], limitedEngines: [], engineIndependent: true };
|
|
11007
|
+
}
|
|
11008
|
+
return {
|
|
11009
|
+
engines: statuses.filter((entry) => entry.status === "supported" || entry.status === "limited").map((entry) => entry.system),
|
|
11010
|
+
limitedEngines: statuses.filter((entry) => entry.status === "limited").map((entry) => entry.system),
|
|
11011
|
+
engineIndependent: false
|
|
11012
|
+
};
|
|
11013
|
+
}
|
|
11014
|
+
var TIER_SEVERITY = Object.freeze({
|
|
11015
|
+
none: 0,
|
|
11016
|
+
readonly: 1,
|
|
11017
|
+
"dry-run": 2,
|
|
11018
|
+
"local-write": 3,
|
|
11019
|
+
interactive: 4,
|
|
11020
|
+
"db-write": 5
|
|
11021
|
+
});
|
|
11022
|
+
function sideEffectFor(key, engines) {
|
|
11023
|
+
const considered = engines.length > 0 ? engines : DATABASE_SYSTEMS;
|
|
11024
|
+
let worst = "none";
|
|
11025
|
+
for (const system of considered) {
|
|
11026
|
+
const tier = ENGINE_CAPABILITIES[system][key].tier;
|
|
11027
|
+
if (TIER_SEVERITY[tier] > TIER_SEVERITY[worst])
|
|
11028
|
+
worst = tier;
|
|
11029
|
+
}
|
|
11030
|
+
return worst;
|
|
11031
|
+
}
|
|
11032
|
+
var COMMAND_SURFACE = Object.freeze({
|
|
11033
|
+
jsonCommands: Object.freeze(new Set([
|
|
11034
|
+
"use",
|
|
11035
|
+
"status",
|
|
11036
|
+
"doctor",
|
|
11037
|
+
"list",
|
|
11038
|
+
"schema",
|
|
11039
|
+
"diff",
|
|
11040
|
+
"query",
|
|
11041
|
+
"lint",
|
|
11042
|
+
"q",
|
|
11043
|
+
"insert",
|
|
11044
|
+
"update",
|
|
11045
|
+
"delete",
|
|
11046
|
+
"export",
|
|
11047
|
+
"check",
|
|
11048
|
+
"inspect",
|
|
11049
|
+
"report",
|
|
11050
|
+
"guide",
|
|
11051
|
+
"recover",
|
|
11052
|
+
"audit tail",
|
|
11053
|
+
"audit show",
|
|
11054
|
+
"audit health",
|
|
11055
|
+
"explain",
|
|
11056
|
+
"plan",
|
|
11057
|
+
"impact assess",
|
|
11058
|
+
"assert",
|
|
11059
|
+
"snapshot",
|
|
11060
|
+
"verification prune",
|
|
11061
|
+
"proxy",
|
|
11062
|
+
"proxy analyze",
|
|
11063
|
+
"recovery",
|
|
11064
|
+
"password",
|
|
11065
|
+
"capabilities",
|
|
11066
|
+
"capabilities check"
|
|
11067
|
+
])),
|
|
11068
|
+
evidenceCommands: Object.freeze(new Set([
|
|
11069
|
+
"assert",
|
|
11070
|
+
"insert",
|
|
11071
|
+
"update",
|
|
11072
|
+
"delete",
|
|
11073
|
+
"verify",
|
|
11074
|
+
"recover",
|
|
11075
|
+
"evidence",
|
|
11076
|
+
"inspect",
|
|
11077
|
+
"report",
|
|
11078
|
+
"schema",
|
|
11079
|
+
"plan",
|
|
11080
|
+
"lint",
|
|
11081
|
+
"explain",
|
|
11082
|
+
"impact assess"
|
|
11083
|
+
]))
|
|
11084
|
+
});
|
|
11085
|
+
function minimumPermissionOf(declaration) {
|
|
11086
|
+
if (declaration.statementType)
|
|
11087
|
+
return minimumPermissionFor(declaration.statementType);
|
|
11088
|
+
if (declaration.permission)
|
|
11089
|
+
return declaration.permission;
|
|
11090
|
+
throw new Error(`capability ${declaration.id} declares neither statementType nor permission`);
|
|
11091
|
+
}
|
|
11092
|
+
function build(declaration, surface) {
|
|
11093
|
+
const { engines, limitedEngines, engineIndependent } = enginesFor(declaration.key);
|
|
11094
|
+
const sideEffect = sideEffectFor(declaration.key, engines);
|
|
11095
|
+
return Object.freeze({
|
|
11096
|
+
id: declaration.id,
|
|
11097
|
+
description: declaration.description,
|
|
11098
|
+
command: declaration.command,
|
|
11099
|
+
risk: riskForSideEffect(sideEffect),
|
|
11100
|
+
sideEffect,
|
|
11101
|
+
engines: Object.freeze([...engines]),
|
|
11102
|
+
limitedEngines: Object.freeze([...limitedEngines]),
|
|
11103
|
+
engineIndependent,
|
|
11104
|
+
minimumPermission: minimumPermissionOf(declaration),
|
|
11105
|
+
requiresConnection: declaration.requiresConnection,
|
|
11106
|
+
mutatesConfiguration: declaration.mutatesConfiguration ?? false,
|
|
11107
|
+
supportsJson: surface.jsonCommands.has(declaration.command),
|
|
11108
|
+
supportsEvidence: surface.evidenceCommands.has(declaration.command)
|
|
11109
|
+
});
|
|
11110
|
+
}
|
|
11111
|
+
var CAPABILITIES = Object.freeze(DECLARATIONS.map((declaration) => build(declaration, COMMAND_SURFACE)).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
11112
|
+
var BY_ID = new Map(CAPABILITIES.map((capability) => [capability.id, capability]));
|
|
11113
|
+
function findCapability(id) {
|
|
11114
|
+
return BY_ID.get(id);
|
|
11115
|
+
}
|
|
11116
|
+
function listCapabilityIds() {
|
|
11117
|
+
return CAPABILITIES.map((capability) => capability.id);
|
|
11118
|
+
}
|
|
11119
|
+
function buildCapabilityCatalog() {
|
|
11120
|
+
return { schemaVersion: CAPABILITY_CONTRACT_SCHEMA_VERSION, capabilities: CAPABILITIES };
|
|
11121
|
+
}
|
|
11122
|
+
var DECLARED_CAPABILITY_KEYS = Object.freeze(COMMAND_CAPABILITY_KEYS);
|
|
11123
|
+
// src/core/capabilities/check.ts
|
|
11124
|
+
class CapabilityRequirementError extends Error {
|
|
11125
|
+
constructor(message) {
|
|
11126
|
+
super(message);
|
|
11127
|
+
this.name = "CapabilityRequirementError";
|
|
11128
|
+
Object.setPrototypeOf(this, CapabilityRequirementError.prototype);
|
|
11129
|
+
}
|
|
11130
|
+
}
|
|
11131
|
+
function parseRequirements(raw) {
|
|
11132
|
+
const parts = raw.split(",").map((part) => part.trim());
|
|
11133
|
+
if (parts.some((part) => part === "")) {
|
|
11134
|
+
throw new CapabilityRequirementError('--require contains an empty capability id; give a comma-separated list such as "schema.read,query.read"');
|
|
11135
|
+
}
|
|
11136
|
+
const ids = [];
|
|
11137
|
+
const duplicates = [];
|
|
11138
|
+
for (const part of parts) {
|
|
11139
|
+
if (ids.includes(part)) {
|
|
11140
|
+
if (!duplicates.includes(part))
|
|
11141
|
+
duplicates.push(part);
|
|
11142
|
+
continue;
|
|
11143
|
+
}
|
|
11144
|
+
ids.push(part);
|
|
11145
|
+
}
|
|
11146
|
+
return {
|
|
11147
|
+
ids,
|
|
11148
|
+
warnings: duplicates.map((id) => `Duplicate capability id '${id}' in --require was ignored.`)
|
|
11149
|
+
};
|
|
11150
|
+
}
|
|
11151
|
+
function evaluate(id, context, failure) {
|
|
11152
|
+
const capability = findCapability(id);
|
|
11153
|
+
if (!capability)
|
|
11154
|
+
return { id, status: "unknown", reason: "unknown-capability" };
|
|
11155
|
+
if (!context) {
|
|
11156
|
+
return {
|
|
11157
|
+
id,
|
|
11158
|
+
status: "unavailable",
|
|
11159
|
+
reason: failure === "absent" ? "context-unavailable" : "context-unresolvable"
|
|
11160
|
+
};
|
|
11161
|
+
}
|
|
11162
|
+
if (!capability.engineIndependent && !capability.engines.includes(context.engine)) {
|
|
11163
|
+
return { id, status: "unavailable", reason: "engine" };
|
|
11164
|
+
}
|
|
11165
|
+
if (context.agentMode && capability.mutatesConfiguration) {
|
|
11166
|
+
return { id, status: "unavailable", reason: "agent-mode" };
|
|
11167
|
+
}
|
|
11168
|
+
if (!permissionAtLeast(context.permission, capability.minimumPermission)) {
|
|
11169
|
+
return { id, status: "unavailable", reason: "permission" };
|
|
11170
|
+
}
|
|
11171
|
+
return { id, status: "available", reason: null };
|
|
11172
|
+
}
|
|
11173
|
+
function checkCapabilities(required, context, warnings = [], contextFailure = "absent") {
|
|
11174
|
+
const results = required.map((id) => evaluate(id, context, contextFailure));
|
|
11175
|
+
const allWarnings = [...warnings];
|
|
11176
|
+
if (!context) {
|
|
11177
|
+
allWarnings.push(contextFailure === "absent" ? "No dbcli configuration was found here, so engine and permission could not be evaluated." : "A dbcli configuration exists here but could not be resolved into an engine and a permission, so nothing was evaluated against it.");
|
|
11178
|
+
} else if (context.agentMode) {
|
|
11179
|
+
allWarnings.push("Agent mode is active (DBCLI_AGENT_MODE=1): configuration, permission and credential changes are refused regardless of permission level.");
|
|
11180
|
+
}
|
|
11181
|
+
return {
|
|
11182
|
+
schemaVersion: CAPABILITY_CONTRACT_SCHEMA_VERSION,
|
|
11183
|
+
ok: results.every((result) => result.status === "available"),
|
|
11184
|
+
required: [...required],
|
|
11185
|
+
context,
|
|
11186
|
+
results,
|
|
11187
|
+
warnings: allWarnings
|
|
11188
|
+
};
|
|
11189
|
+
}
|
|
11190
|
+
// src/core/capabilities/schema.ts
|
|
11191
|
+
class CapabilityContractError extends Error {
|
|
11192
|
+
constructor(message) {
|
|
11193
|
+
super(message);
|
|
11194
|
+
this.name = "CapabilityContractError";
|
|
11195
|
+
Object.setPrototypeOf(this, CapabilityContractError.prototype);
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
11198
|
+
var PermissionSchema2 = exports_external.enum(["query-only", "read-write", "data-admin", "admin"]);
|
|
11199
|
+
var EngineSchema = exports_external.enum(DATABASE_SYSTEMS);
|
|
11200
|
+
var RiskSchema = exports_external.enum(["readonly", "dry-run", "write", "unknown"]);
|
|
11201
|
+
var SideEffectSchema = exports_external.enum([
|
|
11202
|
+
"readonly",
|
|
11203
|
+
"dry-run",
|
|
11204
|
+
"local-write",
|
|
11205
|
+
"db-write",
|
|
11206
|
+
"interactive",
|
|
11207
|
+
"none"
|
|
11208
|
+
]);
|
|
11209
|
+
var CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
|
|
11210
|
+
var CapabilityIdSchema = exports_external.string().regex(CAPABILITY_ID_PATTERN, {
|
|
11211
|
+
message: "capability id must be dotted lower-case segments, e.g. schema.read"
|
|
11212
|
+
});
|
|
11213
|
+
var CAPABILITY_COMMAND_PATTERN = /^[a-z][a-z0-9]*(?: [a-z][a-z0-9]*)*$/;
|
|
11214
|
+
var CapabilityCommandSchema = exports_external.string().regex(CAPABILITY_COMMAND_PATTERN, {
|
|
11215
|
+
message: "capability command must be space-separated lower-case CLI command segments"
|
|
11216
|
+
});
|
|
11217
|
+
var CapabilitySchema = exports_external.object({
|
|
11218
|
+
id: CapabilityIdSchema,
|
|
11219
|
+
description: exports_external.string().min(1),
|
|
11220
|
+
command: CapabilityCommandSchema,
|
|
11221
|
+
risk: RiskSchema,
|
|
11222
|
+
sideEffect: SideEffectSchema,
|
|
11223
|
+
engines: exports_external.array(EngineSchema),
|
|
11224
|
+
limitedEngines: exports_external.array(EngineSchema),
|
|
11225
|
+
engineIndependent: exports_external.boolean(),
|
|
11226
|
+
minimumPermission: PermissionSchema2,
|
|
11227
|
+
requiresConnection: exports_external.boolean(),
|
|
11228
|
+
mutatesConfiguration: exports_external.boolean(),
|
|
11229
|
+
supportsJson: exports_external.boolean(),
|
|
11230
|
+
supportsEvidence: exports_external.boolean()
|
|
11231
|
+
}).strict();
|
|
11232
|
+
var CapabilityCatalogSchema = exports_external.object({
|
|
11233
|
+
schemaVersion: exports_external.literal(CAPABILITY_CONTRACT_SCHEMA_VERSION),
|
|
11234
|
+
capabilities: exports_external.array(CapabilitySchema)
|
|
11235
|
+
}).strict();
|
|
11236
|
+
var CheckContextSchema = exports_external.object({
|
|
11237
|
+
engine: EngineSchema,
|
|
11238
|
+
permission: PermissionSchema2,
|
|
11239
|
+
connectionName: exports_external.string().max(200).nullable(),
|
|
11240
|
+
agentMode: exports_external.boolean()
|
|
11241
|
+
}).strict();
|
|
11242
|
+
var CheckResultSchema = exports_external.object({
|
|
11243
|
+
id: exports_external.string().min(1),
|
|
11244
|
+
status: exports_external.enum(["available", "unavailable", "unknown"]),
|
|
11245
|
+
reason: exports_external.enum([
|
|
11246
|
+
"unknown-capability",
|
|
11247
|
+
"context-unavailable",
|
|
11248
|
+
"context-unresolvable",
|
|
11249
|
+
"engine",
|
|
11250
|
+
"agent-mode",
|
|
11251
|
+
"permission"
|
|
11252
|
+
]).nullable()
|
|
11253
|
+
}).strict();
|
|
11254
|
+
var CapabilityCheckReportSchema = exports_external.object({
|
|
11255
|
+
schemaVersion: exports_external.literal(CAPABILITY_CONTRACT_SCHEMA_VERSION),
|
|
11256
|
+
ok: exports_external.boolean(),
|
|
11257
|
+
required: exports_external.array(exports_external.string().min(1)),
|
|
11258
|
+
context: CheckContextSchema.nullable(),
|
|
11259
|
+
results: exports_external.array(CheckResultSchema),
|
|
11260
|
+
warnings: exports_external.array(exports_external.string())
|
|
11261
|
+
}).strict();
|
|
11262
|
+
function parse(schema, value, what) {
|
|
11263
|
+
const result = schema.safeParse(value);
|
|
11264
|
+
if (result.success)
|
|
11265
|
+
return result.data;
|
|
11266
|
+
throw new CapabilityContractError(`${what} does not match capability contract v${CAPABILITY_CONTRACT_SCHEMA_VERSION}: ${result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`);
|
|
11267
|
+
}
|
|
11268
|
+
function parseCapabilityCatalog(value) {
|
|
11269
|
+
return parse(CapabilityCatalogSchema, value, "capability catalog");
|
|
11270
|
+
}
|
|
11271
|
+
function parseCapabilityCheckReport(value) {
|
|
11272
|
+
return parse(CapabilityCheckReportSchema, value, "capability check report");
|
|
11273
|
+
}
|
|
11274
|
+
// src/core/recovery/types.ts
|
|
11275
|
+
var RECOVERY_CODES = [
|
|
11276
|
+
"CONFIG_MISSING",
|
|
11277
|
+
"CONN_REFUSED",
|
|
11278
|
+
"CONN_AUTH_FAILED",
|
|
11279
|
+
"CONN_TIMEOUT",
|
|
11280
|
+
"CONN_HOST_NOT_FOUND",
|
|
11281
|
+
"CONN_UNKNOWN",
|
|
11282
|
+
"PERMISSION_DENIED",
|
|
11283
|
+
"BLACKLIST_TABLE",
|
|
11284
|
+
"BLACKLIST_COLUMN_WRITE",
|
|
11285
|
+
"SNIPPET_NOT_FOUND",
|
|
11286
|
+
"SNIPPET_AMBIGUOUS",
|
|
11287
|
+
"SNIPPET_PARAM_MISSING",
|
|
11288
|
+
"SCHEMA_CACHE_MISSING",
|
|
11289
|
+
"UNKNOWN"
|
|
11290
|
+
];
|
|
11291
|
+
var MAX_BRANCH_STEPS = 6;
|
|
11292
|
+
var MAX_BRANCH_COUNT = 8;
|
|
11293
|
+
|
|
11294
|
+
// src/core/recovery/envelope-schema.ts
|
|
11295
|
+
var recoveryCodeSchema = exports_external.enum(RECOVERY_CODES);
|
|
11296
|
+
var recoveryCategorySchema = exports_external.enum([
|
|
11297
|
+
"config",
|
|
11298
|
+
"connection",
|
|
11299
|
+
"permission",
|
|
11300
|
+
"blacklist",
|
|
11301
|
+
"snippet",
|
|
11302
|
+
"schema-cache",
|
|
11303
|
+
"unknown"
|
|
11304
|
+
]);
|
|
11305
|
+
var guideRiskSchema = exports_external.enum(["readonly", "dry-run", "write", "unknown"]);
|
|
11306
|
+
var branchIdSchema = exports_external.string().min(1).max(64).regex(/^[a-z0-9-]+$/, "branchId must match /^[a-z0-9-]+$/");
|
|
11307
|
+
var guideStepSchema = exports_external.object({
|
|
11308
|
+
order: exports_external.number().int(),
|
|
11309
|
+
command: exports_external.string().min(1),
|
|
11310
|
+
rationale: exports_external.string(),
|
|
11311
|
+
risk: guideRiskSchema,
|
|
11312
|
+
expects: exports_external.string(),
|
|
11313
|
+
snippet: exports_external.string().optional(),
|
|
11314
|
+
intent: exports_external.string().optional(),
|
|
11315
|
+
interactive: exports_external.boolean().optional(),
|
|
11316
|
+
dbWrite: exports_external.boolean().optional(),
|
|
11317
|
+
placeholders: exports_external.array(exports_external.string()).optional(),
|
|
11318
|
+
branchId: branchIdSchema.optional()
|
|
11319
|
+
}).strict();
|
|
11320
|
+
var branchPlanSchema = exports_external.object({
|
|
11321
|
+
description: exports_external.string().min(1),
|
|
11322
|
+
steps: exports_external.array(guideStepSchema).min(1).max(MAX_BRANCH_STEPS)
|
|
11323
|
+
}).strict();
|
|
11324
|
+
var branchesSchema = exports_external.record(branchIdSchema, branchPlanSchema).superRefine((map, ctx) => {
|
|
11325
|
+
const keys = Object.keys(map);
|
|
11326
|
+
if (keys.length > MAX_BRANCH_COUNT) {
|
|
11327
|
+
ctx.addIssue({
|
|
11328
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11329
|
+
message: `branches has ${keys.length} entries; cap is ${MAX_BRANCH_COUNT}`
|
|
11330
|
+
});
|
|
11331
|
+
}
|
|
11332
|
+
for (const [id, plan] of Object.entries(map)) {
|
|
11333
|
+
for (const step of plan.steps) {
|
|
11334
|
+
if (step.branchId !== undefined && step.branchId !== id) {
|
|
11335
|
+
ctx.addIssue({
|
|
11336
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11337
|
+
message: `step.branchId '${step.branchId}' does not match enclosing key '${id}'`
|
|
11338
|
+
});
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
}
|
|
11342
|
+
});
|
|
11343
|
+
var branchForkSchema = exports_external.object({
|
|
11344
|
+
after: exports_external.number().int().min(1),
|
|
11345
|
+
branchIds: exports_external.array(branchIdSchema).min(1)
|
|
11346
|
+
}).strict();
|
|
11347
|
+
var recoveryEnvelopeSchema = exports_external.object({
|
|
11348
|
+
schemaVersion: exports_external.literal(1),
|
|
11349
|
+
generatedAt: exports_external.string().min(1),
|
|
11350
|
+
ok: exports_external.literal(false),
|
|
11351
|
+
error: exports_external.object({
|
|
11352
|
+
code: recoveryCodeSchema,
|
|
11353
|
+
category: recoveryCategorySchema,
|
|
11354
|
+
message: exports_external.string(),
|
|
11355
|
+
details: exports_external.object({
|
|
11356
|
+
table: exports_external.string().optional(),
|
|
11357
|
+
columns: exports_external.string().optional(),
|
|
11358
|
+
requiredPermission: exports_external.string().optional(),
|
|
11359
|
+
connectionCode: exports_external.string().optional(),
|
|
11360
|
+
snippet: exports_external.string().optional(),
|
|
11361
|
+
intent: exports_external.string().optional(),
|
|
11362
|
+
paramName: exports_external.string().optional()
|
|
11363
|
+
}).strict().optional()
|
|
11364
|
+
}).strict(),
|
|
11365
|
+
recovery: exports_external.array(guideStepSchema),
|
|
11366
|
+
verify: guideStepSchema.optional(),
|
|
11367
|
+
branches: branchesSchema.optional(),
|
|
11368
|
+
branchFork: branchForkSchema.optional()
|
|
11369
|
+
}).strict().superRefine((env, ctx) => {
|
|
11370
|
+
const hasBranches = env.branches !== undefined;
|
|
11371
|
+
const hasFork = env.branchFork !== undefined;
|
|
11372
|
+
if (hasBranches !== hasFork) {
|
|
11373
|
+
ctx.addIssue({
|
|
11374
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11375
|
+
message: "branches and branchFork must be set together (or both omitted)"
|
|
11376
|
+
});
|
|
11377
|
+
return;
|
|
11378
|
+
}
|
|
11379
|
+
if (hasBranches && hasFork) {
|
|
11380
|
+
const keys = Object.keys(env.branches).sort();
|
|
11381
|
+
const ids = [...env.branchFork.branchIds].sort();
|
|
11382
|
+
if (keys.length !== ids.length || keys.some((k, i) => k !== ids[i])) {
|
|
11383
|
+
ctx.addIssue({
|
|
11384
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11385
|
+
message: "branchFork.branchIds must equal Object.keys(branches)"
|
|
11386
|
+
});
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
});
|
|
11390
|
+
var savedRecoveryEnvelopeSchema = exports_external.object({
|
|
11391
|
+
schemaVersion: exports_external.literal(1),
|
|
11392
|
+
id: exports_external.string().optional(),
|
|
11393
|
+
audit_ref: exports_external.string().optional(),
|
|
11394
|
+
savedAt: exports_external.string().min(1),
|
|
11395
|
+
command: exports_external.string(),
|
|
11396
|
+
cwd: exports_external.string().min(1),
|
|
11397
|
+
envelope: recoveryEnvelopeSchema
|
|
11398
|
+
}).strict();
|
|
11399
|
+
|
|
11400
|
+
// src/core/correlation-id.ts
|
|
11401
|
+
var CORRELATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
|
|
11402
|
+
|
|
11403
|
+
// src/core/operation-envelope.ts
|
|
11404
|
+
var OPERATION_ENVELOPE_SCHEMA_VERSION = 1;
|
|
11405
|
+
var MAX_OPERATION_ENVELOPE_BYTES = 65536;
|
|
11406
|
+
var MAX_OPERATION_ENVELOPE_IDENTIFIER_LENGTH = 160;
|
|
11407
|
+
var MAX_OPERATION_ENVELOPE_ITEMS = 128;
|
|
11408
|
+
var MAX_MESSAGE_LENGTH = 2000;
|
|
11409
|
+
var MAX_EVIDENCE_ITEMS = 16;
|
|
11410
|
+
var OPERATION_ID = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/;
|
|
11411
|
+
var CODE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
|
|
11412
|
+
var EVIDENCE_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$/;
|
|
11413
|
+
var SHA256_DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
11414
|
+
var identifierSchema = exports_external.string().min(1).max(MAX_OPERATION_ENVELOPE_IDENTIFIER_LENGTH);
|
|
11415
|
+
var capabilityIdSchema = identifierSchema.regex(CAPABILITY_ID_PATTERN, "capability id must be dotted lower-case segments");
|
|
11416
|
+
var codeSchema = identifierSchema.regex(CODE, "code must be an uppercase snake-case identifier");
|
|
11417
|
+
var messageSchema = exports_external.string().min(1).max(MAX_MESSAGE_LENGTH);
|
|
11418
|
+
var contextSchema = exports_external.object({
|
|
11419
|
+
engine: exports_external.enum(DATABASE_SYSTEMS),
|
|
11420
|
+
permission: exports_external.enum(["query-only", "read-write", "data-admin", "admin"]),
|
|
11421
|
+
connectionName: identifierSchema.nullable(),
|
|
11422
|
+
agentMode: exports_external.boolean(),
|
|
11423
|
+
correlationId: exports_external.string().regex(CORRELATION_ID_PATTERN, "invalid correlation id").optional()
|
|
11424
|
+
}).strict();
|
|
11425
|
+
var resultSchema = exports_external.object({
|
|
11426
|
+
id: capabilityIdSchema,
|
|
11427
|
+
status: exports_external.enum(["available", "unavailable", "unknown"]),
|
|
11428
|
+
reason: exports_external.enum([
|
|
11429
|
+
"unknown-capability",
|
|
11430
|
+
"context-unavailable",
|
|
11431
|
+
"context-unresolvable",
|
|
11432
|
+
"engine",
|
|
11433
|
+
"agent-mode",
|
|
11434
|
+
"permission"
|
|
11435
|
+
]).nullable()
|
|
11436
|
+
}).strict().superRefine((result, ctx) => {
|
|
11437
|
+
if (result.status === "available" && result.reason !== null) {
|
|
11438
|
+
ctx.addIssue({
|
|
11439
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11440
|
+
message: "available results require reason null"
|
|
11441
|
+
});
|
|
11442
|
+
}
|
|
11443
|
+
if (result.status === "unknown" && result.reason !== "unknown-capability") {
|
|
11444
|
+
ctx.addIssue({
|
|
11445
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11446
|
+
message: "unknown results require reason unknown-capability"
|
|
11447
|
+
});
|
|
11448
|
+
}
|
|
11449
|
+
if (result.status === "unavailable" && (result.reason === null || result.reason === "unknown-capability")) {
|
|
11450
|
+
ctx.addIssue({
|
|
11451
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11452
|
+
message: "unavailable results require an availability reason"
|
|
11453
|
+
});
|
|
11454
|
+
}
|
|
11455
|
+
});
|
|
11456
|
+
var capabilitiesCheckDataSchema = exports_external.object({
|
|
11457
|
+
required: exports_external.array(capabilityIdSchema).min(1).max(MAX_OPERATION_ENVELOPE_ITEMS),
|
|
11458
|
+
results: exports_external.array(resultSchema).min(1).max(MAX_OPERATION_ENVELOPE_ITEMS)
|
|
11459
|
+
}).strict().superRefine((data, ctx) => {
|
|
11460
|
+
if (new Set(data.required).size !== data.required.length) {
|
|
11461
|
+
ctx.addIssue({
|
|
11462
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11463
|
+
message: "required capability ids must be unique"
|
|
11464
|
+
});
|
|
11465
|
+
}
|
|
11466
|
+
if (data.required.length !== data.results.length || data.required.some((id, index) => data.results[index]?.id !== id)) {
|
|
11467
|
+
ctx.addIssue({
|
|
11468
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11469
|
+
message: "results must match required capability ids in order"
|
|
11470
|
+
});
|
|
11471
|
+
}
|
|
11472
|
+
});
|
|
11473
|
+
var warningSchema = exports_external.object({
|
|
11474
|
+
code: codeSchema,
|
|
11475
|
+
message: messageSchema
|
|
11476
|
+
}).strict();
|
|
11477
|
+
var evidenceSchema = exports_external.object({
|
|
11478
|
+
kind: exports_external.enum(["receipt", "audit", "verification-artifact"]),
|
|
11479
|
+
id: exports_external.string().regex(EVIDENCE_ID, "invalid evidence id"),
|
|
11480
|
+
digest: exports_external.string().regex(SHA256_DIGEST, "invalid evidence digest").optional()
|
|
11481
|
+
}).strict();
|
|
11482
|
+
var errorSchema = exports_external.object({
|
|
11483
|
+
code: codeSchema,
|
|
11484
|
+
message: messageSchema
|
|
11485
|
+
}).strict();
|
|
11486
|
+
function hasOversizedRecoveryString(value) {
|
|
11487
|
+
if (typeof value === "string")
|
|
11488
|
+
return value.length > MAX_MESSAGE_LENGTH;
|
|
11489
|
+
if (Array.isArray(value))
|
|
11490
|
+
return value.some(hasOversizedRecoveryString);
|
|
11491
|
+
if (value === null || typeof value !== "object")
|
|
11492
|
+
return false;
|
|
11493
|
+
return Object.values(value).some(hasOversizedRecoveryString);
|
|
11494
|
+
}
|
|
11495
|
+
var operationEnvelopeFieldSchema = exports_external.object({
|
|
11496
|
+
schemaVersion: exports_external.literal(OPERATION_ENVELOPE_SCHEMA_VERSION),
|
|
11497
|
+
ok: exports_external.boolean(),
|
|
11498
|
+
operation: exports_external.enum(["capabilities.check", "capabilities.list"]).refine((value) => value.length <= MAX_OPERATION_ENVELOPE_IDENTIFIER_LENGTH && OPERATION_ID.test(value)),
|
|
11499
|
+
status: exports_external.enum(["succeeded", "failed"]),
|
|
11500
|
+
context: contextSchema.nullable(),
|
|
11501
|
+
data: exports_external.union([capabilitiesCheckDataSchema, CapabilityCatalogSchema]).nullable(),
|
|
11502
|
+
warnings: exports_external.array(warningSchema).max(MAX_OPERATION_ENVELOPE_ITEMS),
|
|
11503
|
+
evidence: exports_external.array(evidenceSchema).max(MAX_EVIDENCE_ITEMS),
|
|
11504
|
+
recovery: recoveryEnvelopeSchema.nullable(),
|
|
11505
|
+
error: errorSchema.nullable()
|
|
11506
|
+
}).strict().superRefine((envelope, ctx) => {
|
|
11507
|
+
if (envelope.ok !== (envelope.status === "succeeded")) {
|
|
11508
|
+
ctx.addIssue({
|
|
11509
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11510
|
+
message: 'ok must equal (status === "succeeded")'
|
|
11511
|
+
});
|
|
11512
|
+
}
|
|
11513
|
+
if (envelope.operation === "capabilities.check") {
|
|
11514
|
+
if (envelope.status === "succeeded") {
|
|
11515
|
+
if (envelope.error !== null) {
|
|
11516
|
+
ctx.addIssue({
|
|
11517
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11518
|
+
message: "successful envelopes require error null"
|
|
11519
|
+
});
|
|
11520
|
+
}
|
|
11521
|
+
if (envelope.data === null) {
|
|
11522
|
+
ctx.addIssue({
|
|
11523
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11524
|
+
message: "successful capabilities.check envelopes require data"
|
|
11525
|
+
});
|
|
11526
|
+
} else if ("results" in envelope.data) {
|
|
11527
|
+
if (envelope.data.results.some((result) => result.status !== "available")) {
|
|
11528
|
+
ctx.addIssue({
|
|
11529
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11530
|
+
message: "successful capabilities.check results must all be available"
|
|
11531
|
+
});
|
|
11532
|
+
}
|
|
11533
|
+
} else {
|
|
11534
|
+
ctx.addIssue({
|
|
11535
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11536
|
+
message: "capabilities.check requires capabilitiesCheckDataSchema shape"
|
|
11537
|
+
});
|
|
11538
|
+
}
|
|
11539
|
+
} else if (envelope.error === null) {
|
|
11540
|
+
ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "failed envelopes require an error" });
|
|
11541
|
+
}
|
|
11542
|
+
const completedNegative = envelope.error?.code === "CAPABILITY_REQUIREMENTS_UNMET";
|
|
11543
|
+
if (envelope.status === "failed" && completedNegative !== (envelope.data !== null)) {
|
|
11544
|
+
ctx.addIssue({
|
|
11545
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11546
|
+
message: "only completed negative capability results retain data"
|
|
11547
|
+
});
|
|
11548
|
+
}
|
|
11549
|
+
if (completedNegative && envelope.data !== null && "results" in envelope.data && envelope.data.results.every((result) => result.status === "available")) {
|
|
11550
|
+
ctx.addIssue({
|
|
11551
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11552
|
+
message: "completed negative capability results require an unmet result"
|
|
11553
|
+
});
|
|
11554
|
+
}
|
|
11555
|
+
} else if (envelope.operation === "capabilities.list") {
|
|
11556
|
+
if (envelope.status === "succeeded") {
|
|
11557
|
+
if (envelope.context !== null) {
|
|
11558
|
+
ctx.addIssue({
|
|
11559
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11560
|
+
message: "successful capabilities.list envelopes require context null"
|
|
11561
|
+
});
|
|
11562
|
+
}
|
|
11563
|
+
if (envelope.warnings.length !== 0) {
|
|
11564
|
+
ctx.addIssue({
|
|
11565
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11566
|
+
message: "successful capabilities.list envelopes require warnings empty"
|
|
11567
|
+
});
|
|
11568
|
+
}
|
|
11569
|
+
if (envelope.evidence.length !== 0) {
|
|
11570
|
+
ctx.addIssue({
|
|
11571
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11572
|
+
message: "successful capabilities.list envelopes require evidence empty"
|
|
11573
|
+
});
|
|
11574
|
+
}
|
|
11575
|
+
if (envelope.recovery !== null) {
|
|
11576
|
+
ctx.addIssue({
|
|
11577
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11578
|
+
message: "successful capabilities.list envelopes require recovery null"
|
|
11579
|
+
});
|
|
11580
|
+
}
|
|
11581
|
+
if (envelope.error !== null) {
|
|
11582
|
+
ctx.addIssue({
|
|
11583
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11584
|
+
message: "successful envelopes require error null"
|
|
11585
|
+
});
|
|
11586
|
+
}
|
|
11587
|
+
if (envelope.data === null) {
|
|
11588
|
+
ctx.addIssue({
|
|
11589
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11590
|
+
message: "successful capabilities.list envelopes require data"
|
|
11591
|
+
});
|
|
11592
|
+
} else if (!("capabilities" in envelope.data)) {
|
|
11593
|
+
ctx.addIssue({
|
|
11594
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11595
|
+
message: "capabilities.list requires CapabilityCatalogSchema shape"
|
|
11596
|
+
});
|
|
11597
|
+
}
|
|
11598
|
+
} else {
|
|
11599
|
+
if (envelope.error === null) {
|
|
11600
|
+
ctx.addIssue({
|
|
11601
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11602
|
+
message: "failed envelopes require an error"
|
|
11603
|
+
});
|
|
11604
|
+
}
|
|
11605
|
+
if (envelope.data !== null) {
|
|
11606
|
+
ctx.addIssue({
|
|
11607
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11608
|
+
message: "failed capabilities.list envelopes require data null"
|
|
11609
|
+
});
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
if (envelope.recovery !== null) {
|
|
11614
|
+
if (envelope.error === null || envelope.recovery.error.code !== envelope.error.code || envelope.recovery.error.message !== envelope.error.message) {
|
|
11615
|
+
ctx.addIssue({
|
|
11616
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11617
|
+
message: "recovery error code and message must match the envelope error"
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
11620
|
+
if (hasOversizedRecoveryString(envelope.recovery)) {
|
|
11621
|
+
ctx.addIssue({
|
|
11622
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11623
|
+
message: `recovery strings must be at most ${MAX_MESSAGE_LENGTH} characters`
|
|
11624
|
+
});
|
|
11625
|
+
}
|
|
11626
|
+
}
|
|
11627
|
+
});
|
|
11628
|
+
function summarize(error) {
|
|
11629
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
11630
|
+
}
|
|
11631
|
+
function encode(envelope) {
|
|
11632
|
+
return `${JSON.stringify(envelope)}
|
|
11633
|
+
`;
|
|
11634
|
+
}
|
|
11635
|
+
function byteLength(value) {
|
|
11636
|
+
return new TextEncoder().encode(value).byteLength;
|
|
11637
|
+
}
|
|
11638
|
+
function parseOperationEnvelope(input) {
|
|
11639
|
+
const parsed = operationEnvelopeFieldSchema.safeParse(input);
|
|
11640
|
+
if (!parsed.success)
|
|
11641
|
+
return { ok: false, reason: summarize(parsed.error) };
|
|
11642
|
+
const value = parsed.data;
|
|
11643
|
+
const size = byteLength(encode(value));
|
|
11644
|
+
if (size > MAX_OPERATION_ENVELOPE_BYTES) {
|
|
11645
|
+
return {
|
|
11646
|
+
ok: false,
|
|
11647
|
+
reason: `<root>: serialized envelope is ${size} bytes; cap is ${MAX_OPERATION_ENVELOPE_BYTES}`
|
|
11648
|
+
};
|
|
11649
|
+
}
|
|
11650
|
+
return { ok: true, value };
|
|
11651
|
+
}
|
|
9593
11652
|
// src/utils/es-index-target.ts
|
|
9594
11653
|
var MAX_DECODE_PASSES = 4;
|
|
9595
11654
|
function unwrap(target) {
|
|
@@ -9954,19 +12013,32 @@ export {
|
|
|
9954
12013
|
removeConnection,
|
|
9955
12014
|
readV2Config,
|
|
9956
12015
|
readConfig,
|
|
12016
|
+
parseRequirements,
|
|
12017
|
+
parseOperationEnvelope,
|
|
12018
|
+
parseCapabilityCheckReport,
|
|
12019
|
+
parseCapabilityCatalog,
|
|
9957
12020
|
migrateV1ToV2,
|
|
9958
12021
|
loadConnectionEnv,
|
|
9959
12022
|
listConnections,
|
|
12023
|
+
listCapabilityIds,
|
|
9960
12024
|
isGlobalConfigPath,
|
|
9961
12025
|
getProjectStoragePath,
|
|
9962
12026
|
getGlobalConfigPath,
|
|
9963
12027
|
getDbcliConfigHome,
|
|
12028
|
+
findCapability,
|
|
9964
12029
|
envVarNameFor,
|
|
9965
12030
|
detectConfigVersion,
|
|
12031
|
+
checkCapabilities,
|
|
12032
|
+
buildCapabilityCatalog,
|
|
9966
12033
|
SchemaLayeredLoader,
|
|
9967
12034
|
QueryExecutor,
|
|
12035
|
+
OPERATION_ENVELOPE_SCHEMA_VERSION,
|
|
9968
12036
|
DataExecutor,
|
|
9969
12037
|
ConnectionError,
|
|
12038
|
+
CapabilityRequirementError,
|
|
12039
|
+
CapabilityContractError,
|
|
12040
|
+
CAPABILITY_CONTRACT_SCHEMA_VERSION,
|
|
12041
|
+
CAPABILITIES,
|
|
9970
12042
|
BlacklistValidator,
|
|
9971
12043
|
BlacklistManager,
|
|
9972
12044
|
BlacklistError
|