@carllee1983/dbcli 1.23.1 → 1.28.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/CHANGELOG.md +43 -1
- package/README.md +70 -1
- package/README.zh-TW.md +70 -1
- package/assets/SKILL.md +4 -1
- package/assets/SKILL.zh-TW.md +4 -1
- package/assets/reference.md +96 -6
- package/dist/cli.mjs +1706 -43
- package/dist/core.d.ts +2797 -0
- package/dist/core.mjs +9753 -0
- package/package.json +10 -1
package/dist/cli.mjs
CHANGED
|
@@ -25565,7 +25565,7 @@ class AuditLockManager {
|
|
|
25565
25565
|
var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS = 50, STALE_LOCK_MULTIPLIER = 10;
|
|
25566
25566
|
var init_lock = () => {};
|
|
25567
25567
|
|
|
25568
|
-
// src/
|
|
25568
|
+
// src/utils/jsonl-rotation.ts
|
|
25569
25569
|
import { rename } from "fs/promises";
|
|
25570
25570
|
function shouldRotate(stats, thresholds, nextLineByteLength) {
|
|
25571
25571
|
const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
|
|
@@ -25577,7 +25577,12 @@ async function rotate(currentPath, previousPath) {
|
|
|
25577
25577
|
await rename(currentPath, previousPath);
|
|
25578
25578
|
} catch {}
|
|
25579
25579
|
}
|
|
25580
|
-
var
|
|
25580
|
+
var init_jsonl_rotation = () => {};
|
|
25581
|
+
|
|
25582
|
+
// src/core/audit/rotation.ts
|
|
25583
|
+
var init_rotation = __esm(() => {
|
|
25584
|
+
init_jsonl_rotation();
|
|
25585
|
+
});
|
|
25581
25586
|
|
|
25582
25587
|
// src/core/audit/logger.ts
|
|
25583
25588
|
import { appendFile, mkdir as mkdir3, readFile, stat } from "fs/promises";
|
|
@@ -81217,7 +81222,7 @@ var {
|
|
|
81217
81222
|
// package.json
|
|
81218
81223
|
var package_default = {
|
|
81219
81224
|
name: "@carllee1983/dbcli",
|
|
81220
|
-
version: "1.
|
|
81225
|
+
version: "1.28.0",
|
|
81221
81226
|
description: "Database CLI for AI agents",
|
|
81222
81227
|
type: "module",
|
|
81223
81228
|
publishConfig: {
|
|
@@ -81226,6 +81231,13 @@ var package_default = {
|
|
|
81226
81231
|
bin: {
|
|
81227
81232
|
dbcli: "./dist/cli.mjs"
|
|
81228
81233
|
},
|
|
81234
|
+
exports: {
|
|
81235
|
+
".": "./dist/cli.mjs",
|
|
81236
|
+
"./core": {
|
|
81237
|
+
types: "./dist/core.d.ts",
|
|
81238
|
+
import: "./dist/core.mjs"
|
|
81239
|
+
}
|
|
81240
|
+
},
|
|
81229
81241
|
license: "MIT",
|
|
81230
81242
|
author: "Carl Lee",
|
|
81231
81243
|
repository: {
|
|
@@ -81296,9 +81308,11 @@ var package_default = {
|
|
|
81296
81308
|
"@inquirer/prompts": "^8.4.3",
|
|
81297
81309
|
"@testing-library/react": "^16.3.2",
|
|
81298
81310
|
"@types/bun": "latest",
|
|
81311
|
+
"@types/pg": "^8.20.0",
|
|
81299
81312
|
"@types/react": "^19.2.14",
|
|
81300
81313
|
"@types/react-dom": "^19.2.3",
|
|
81301
81314
|
autoprefixer: "^10.5.0",
|
|
81315
|
+
"dts-bundle-generator": "^9.5.1",
|
|
81302
81316
|
eslint: "^10.4.0",
|
|
81303
81317
|
"happy-dom": "^20.9.0",
|
|
81304
81318
|
postcss: "^8.5.14",
|
|
@@ -87648,6 +87662,7 @@ function resolveSkillSource(lang) {
|
|
|
87648
87662
|
var SUPPORTED_PLATFORMS = [
|
|
87649
87663
|
"claude",
|
|
87650
87664
|
"gemini",
|
|
87665
|
+
"antigravity",
|
|
87651
87666
|
"copilot",
|
|
87652
87667
|
"cursor",
|
|
87653
87668
|
"codex",
|
|
@@ -87715,6 +87730,8 @@ function getInstallPath(platform) {
|
|
|
87715
87730
|
return path4.join(home, ".claude", "skills", "dbcli", "SKILL.md");
|
|
87716
87731
|
case "gemini":
|
|
87717
87732
|
return path4.join(home, ".gemini", "skills", "dbcli", "SKILL.md");
|
|
87733
|
+
case "antigravity":
|
|
87734
|
+
return path4.join(home, ".gemini", "antigravity-cli", "skills", "dbcli", "SKILL.md");
|
|
87718
87735
|
case "codex":
|
|
87719
87736
|
return path4.join(home, ".codex", "skills", "dbcli", "SKILL.md");
|
|
87720
87737
|
case "copilot":
|
|
@@ -87755,7 +87772,7 @@ async function ensureDir(dirPath) {
|
|
|
87755
87772
|
}
|
|
87756
87773
|
}
|
|
87757
87774
|
function registerSkillCommand(program2) {
|
|
87758
|
-
const skill = program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").addOption(new Option("--lang <lang>", "Source language for SKILL content").choices(["en", "zh-TW"]).default("en")).action(async (options) => {
|
|
87775
|
+
const skill = program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, antigravity, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").addOption(new Option("--lang <lang>", "Source language for SKILL content").choices(["en", "zh-TW"]).default("en")).action(async (options) => {
|
|
87759
87776
|
try {
|
|
87760
87777
|
await skillCommand(program2, options);
|
|
87761
87778
|
} catch (error) {
|
|
@@ -90355,11 +90372,454 @@ function makeSavedQueryLoader2() {
|
|
|
90355
90372
|
};
|
|
90356
90373
|
}
|
|
90357
90374
|
|
|
90375
|
+
// src/commands/snapshot.ts
|
|
90376
|
+
init_adapters();
|
|
90377
|
+
init_config();
|
|
90378
|
+
import { join as join26 } from "path";
|
|
90379
|
+
init_validation();
|
|
90380
|
+
init_blacklist_validator();
|
|
90381
|
+
init_engine_hints();
|
|
90382
|
+
|
|
90383
|
+
// src/core/result-snapshot/fingerprint.ts
|
|
90384
|
+
import { createHash as createHash2 } from "crypto";
|
|
90385
|
+
function sha256(input) {
|
|
90386
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
90387
|
+
}
|
|
90388
|
+
function isNumeric(values) {
|
|
90389
|
+
return values.length > 0 && values.every((v) => typeof v === "number");
|
|
90390
|
+
}
|
|
90391
|
+
function buildColumn(name2, type, values) {
|
|
90392
|
+
const nonNull = values.filter((v) => v !== null && v !== undefined);
|
|
90393
|
+
const nullCount = values.length - nonNull.length;
|
|
90394
|
+
const asStrings = nonNull.map((v) => String(v));
|
|
90395
|
+
const distinctCount = new Set(asStrings).size;
|
|
90396
|
+
const checksum = sha256(JSON.stringify([...asStrings].sort()));
|
|
90397
|
+
const col = { name: name2, type, nullCount, distinctCount, checksum };
|
|
90398
|
+
if (isNumeric(nonNull)) {
|
|
90399
|
+
col.min = Math.min(...nonNull);
|
|
90400
|
+
col.max = Math.max(...nonNull);
|
|
90401
|
+
col.sum = nonNull.reduce((a, b) => a + b, 0);
|
|
90402
|
+
} else if (nonNull.length > 0) {
|
|
90403
|
+
const sorted = [...asStrings].sort();
|
|
90404
|
+
col.min = sorted[0];
|
|
90405
|
+
col.max = sorted[sorted.length - 1];
|
|
90406
|
+
}
|
|
90407
|
+
return col;
|
|
90408
|
+
}
|
|
90409
|
+
function buildFingerprint(result, opts) {
|
|
90410
|
+
const columns = result.columnNames.map((name2, i) => buildColumn(name2, result.columnTypes?.[i] ?? "unknown", result.rows.map((r) => r[name2])));
|
|
90411
|
+
for (const name2 of opts.redactedColumns ?? []) {
|
|
90412
|
+
if (!columns.some((c2) => c2.name === name2)) {
|
|
90413
|
+
columns.push({
|
|
90414
|
+
name: name2,
|
|
90415
|
+
type: "redacted",
|
|
90416
|
+
nullCount: 0,
|
|
90417
|
+
distinctCount: 0,
|
|
90418
|
+
checksum: "",
|
|
90419
|
+
redacted: true
|
|
90420
|
+
});
|
|
90421
|
+
}
|
|
90422
|
+
}
|
|
90423
|
+
const rowStrings = result.rows.map((r) => JSON.stringify(result.columnNames.map((n) => r[n]))).sort();
|
|
90424
|
+
const snap = {
|
|
90425
|
+
schemaVersion: 1,
|
|
90426
|
+
query: opts.query ?? "",
|
|
90427
|
+
engine: opts.engine ?? "postgresql",
|
|
90428
|
+
createdAt: opts.createdAt ?? new Date().toISOString(),
|
|
90429
|
+
rowCount: result.rowCount,
|
|
90430
|
+
resultChecksum: sha256(JSON.stringify(rowStrings)),
|
|
90431
|
+
columns
|
|
90432
|
+
};
|
|
90433
|
+
if (opts.includeRows)
|
|
90434
|
+
snap.rows = result.rows;
|
|
90435
|
+
return snap;
|
|
90436
|
+
}
|
|
90437
|
+
function compareAgainst(current, baseline, tolerance) {
|
|
90438
|
+
const checks = [];
|
|
90439
|
+
if (tolerance === 0) {
|
|
90440
|
+
checks.push({
|
|
90441
|
+
name: "resultChecksum",
|
|
90442
|
+
expected: baseline.resultChecksum,
|
|
90443
|
+
actual: current.resultChecksum,
|
|
90444
|
+
pass: current.resultChecksum === baseline.resultChecksum
|
|
90445
|
+
});
|
|
90446
|
+
return checks;
|
|
90447
|
+
}
|
|
90448
|
+
const within = (a, b) => b === 0 ? a === 0 : Math.abs(a - b) / Math.abs(b) <= tolerance;
|
|
90449
|
+
checks.push({
|
|
90450
|
+
name: "rowCount",
|
|
90451
|
+
expected: `${baseline.rowCount} \xB1${tolerance * 100}%`,
|
|
90452
|
+
actual: String(current.rowCount),
|
|
90453
|
+
pass: within(current.rowCount, baseline.rowCount)
|
|
90454
|
+
});
|
|
90455
|
+
for (const base of baseline.columns) {
|
|
90456
|
+
if (base.sum === undefined)
|
|
90457
|
+
continue;
|
|
90458
|
+
const cur = current.columns.find((c2) => c2.name === base.name);
|
|
90459
|
+
const curSum = cur?.sum;
|
|
90460
|
+
checks.push({
|
|
90461
|
+
name: `sum(${base.name})`,
|
|
90462
|
+
expected: `${base.sum} \xB1${tolerance * 100}%`,
|
|
90463
|
+
actual: String(curSum),
|
|
90464
|
+
pass: curSum !== undefined && within(curSum, base.sum)
|
|
90465
|
+
});
|
|
90466
|
+
}
|
|
90467
|
+
return checks;
|
|
90468
|
+
}
|
|
90469
|
+
|
|
90470
|
+
// src/core/result-snapshot/types.ts
|
|
90471
|
+
class SnapshotVersionError extends Error {
|
|
90472
|
+
code = "SNAPSHOT_VERSION_MISMATCH";
|
|
90473
|
+
constructor(message) {
|
|
90474
|
+
super(message);
|
|
90475
|
+
this.name = "SnapshotVersionError";
|
|
90476
|
+
}
|
|
90477
|
+
}
|
|
90478
|
+
|
|
90479
|
+
// src/core/result-snapshot/serializer.ts
|
|
90480
|
+
async function writeSnapshot(path6, snap) {
|
|
90481
|
+
await Bun.write(path6, JSON.stringify(snap, null, 2));
|
|
90482
|
+
}
|
|
90483
|
+
async function readSnapshot(path6) {
|
|
90484
|
+
const file = Bun.file(path6);
|
|
90485
|
+
if (!await file.exists()) {
|
|
90486
|
+
const err = new Error(`Snapshot file not found: ${path6}`);
|
|
90487
|
+
err.code = "SNAPSHOT_NOT_FOUND";
|
|
90488
|
+
throw err;
|
|
90489
|
+
}
|
|
90490
|
+
const parsed = JSON.parse(await file.text());
|
|
90491
|
+
if (parsed.schemaVersion !== 1) {
|
|
90492
|
+
throw new SnapshotVersionError(`Unsupported snapshot schemaVersion ${parsed.schemaVersion} (expected 1)`);
|
|
90493
|
+
}
|
|
90494
|
+
return parsed;
|
|
90495
|
+
}
|
|
90496
|
+
|
|
90497
|
+
// src/commands/snapshot.ts
|
|
90498
|
+
init_saved_queries();
|
|
90499
|
+
init_integration_helper();
|
|
90500
|
+
var ALLOWED_FORMATS12 = ["json", "table"];
|
|
90501
|
+
var SQL_SYSTEMS5 = ["postgresql", "mysql", "mariadb"];
|
|
90502
|
+
function requireSqlConnection9(connection) {
|
|
90503
|
+
if (!SQL_SYSTEMS5.includes(connection.system)) {
|
|
90504
|
+
throw new Error(`snapshot currently supports SQL engines only, got: ${connection.system}`);
|
|
90505
|
+
}
|
|
90506
|
+
return connection;
|
|
90507
|
+
}
|
|
90508
|
+
function pad(n) {
|
|
90509
|
+
return String(n).padStart(2, "0");
|
|
90510
|
+
}
|
|
90511
|
+
function defaultSnapshotPath() {
|
|
90512
|
+
const d = new Date;
|
|
90513
|
+
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
90514
|
+
return join26(".dbcli", "snapshots", `snap-${stamp}.json`);
|
|
90515
|
+
}
|
|
90516
|
+
var snapshotCommand = new Command().name("snapshot").description("Capture a result fingerprint (rowCount + per-column aggregates) for later comparison").argument("<query>", "SQL string or @saved-query reference").option("--out <path>", "Write snapshot to this path (default: .dbcli/snapshots/snap-<timestamp>.json)").option("--rows", "Also include full (blacklist-masked) rows in the snapshot", false).option("--stdout", "Print snapshot JSON to stdout instead of writing a file", false).option("--format <format>", "Output format for --stdout: json (default) or table", "json").option("--no-limit", "Disable the automatic query-only LIMIT").action(async (query, options, command) => {
|
|
90517
|
+
try {
|
|
90518
|
+
validateFormat(options.format, ALLOWED_FORMATS12, "snapshot");
|
|
90519
|
+
const configPath = resolveConfigPath(command, options);
|
|
90520
|
+
const config = await configModule.read(configPath);
|
|
90521
|
+
if (!config.connection) {
|
|
90522
|
+
console.error("Database not configured. Run: dbcli init");
|
|
90523
|
+
process.exit(1);
|
|
90524
|
+
}
|
|
90525
|
+
let sql = query;
|
|
90526
|
+
if (query.startsWith("@")) {
|
|
90527
|
+
const engine = mapSystemToEngine(config.connection.system);
|
|
90528
|
+
const dirs = resolveSnippetDirs(process.cwd());
|
|
90529
|
+
const snippets = await loadSnippets(dirs);
|
|
90530
|
+
sql = resolveByName(snippets, query.slice(1), engine).query.sqlBody;
|
|
90531
|
+
}
|
|
90532
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection9(config.connection));
|
|
90533
|
+
await adapter.connect();
|
|
90534
|
+
try {
|
|
90535
|
+
const blacklistManager = new BlacklistManager(config);
|
|
90536
|
+
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
90537
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
|
|
90538
|
+
const result = await executor3.execute(sql, { autoLimit: options.limit !== false });
|
|
90539
|
+
const table = extractTableName(sql);
|
|
90540
|
+
const redactedColumns = table ? blacklistManager.getBlacklistedColumns(table) : [];
|
|
90541
|
+
const snap = buildFingerprint(result, {
|
|
90542
|
+
includeRows: options.rows === true,
|
|
90543
|
+
redactedColumns,
|
|
90544
|
+
query: sql,
|
|
90545
|
+
engine: config.connection.system
|
|
90546
|
+
});
|
|
90547
|
+
if (options.stdout === true) {
|
|
90548
|
+
console.log(JSON.stringify(snap, null, 2));
|
|
90549
|
+
} else {
|
|
90550
|
+
const outPath = options.out ?? defaultSnapshotPath();
|
|
90551
|
+
await writeSnapshot(outPath, snap);
|
|
90552
|
+
console.error(`Snapshot saved to ${outPath} (${snap.rowCount} rows, ${snap.columns.length} columns)`);
|
|
90553
|
+
}
|
|
90554
|
+
await writeAuditEntry(config, "snapshot", options, {
|
|
90555
|
+
success: true,
|
|
90556
|
+
sql
|
|
90557
|
+
});
|
|
90558
|
+
} finally {
|
|
90559
|
+
await adapter.disconnect();
|
|
90560
|
+
}
|
|
90561
|
+
} catch (error) {
|
|
90562
|
+
if (error instanceof Error) {
|
|
90563
|
+
console.error(error.message);
|
|
90564
|
+
if (error instanceof ConnectionError)
|
|
90565
|
+
error.hints.forEach((h) => console.error(` Hint: ${h}`));
|
|
90566
|
+
}
|
|
90567
|
+
process.exit(1);
|
|
90568
|
+
}
|
|
90569
|
+
});
|
|
90570
|
+
|
|
90571
|
+
// src/commands/assert.ts
|
|
90572
|
+
init_adapters();
|
|
90573
|
+
init_config();
|
|
90574
|
+
init_validation();
|
|
90575
|
+
init_blacklist_validator();
|
|
90576
|
+
init_saved_queries();
|
|
90577
|
+
|
|
90578
|
+
// src/core/assert/grammar.ts
|
|
90579
|
+
class AssertExpressionError extends Error {
|
|
90580
|
+
code = "ASSERT_BAD_EXPRESSION";
|
|
90581
|
+
constructor(input) {
|
|
90582
|
+
super(`Cannot parse --expect "${input}". Examples: "rows > 0", "value == 5000", ` + `"col:email not null", "col:id unique", "col:amount between 0 and 100", "col:age >= 18".`);
|
|
90583
|
+
this.name = "AssertExpressionError";
|
|
90584
|
+
}
|
|
90585
|
+
}
|
|
90586
|
+
var OP = /(>=|<=|==|!=|>|<)/;
|
|
90587
|
+
function parseScalar2(raw) {
|
|
90588
|
+
const s = raw.trim();
|
|
90589
|
+
if (/^".*"$/.test(s) || /^'.*'$/.test(s))
|
|
90590
|
+
return s.slice(1, -1);
|
|
90591
|
+
const n = Number(s);
|
|
90592
|
+
return Number.isNaN(n) ? s : n;
|
|
90593
|
+
}
|
|
90594
|
+
function parseExpect(input) {
|
|
90595
|
+
const s = input.trim();
|
|
90596
|
+
const rows = s.match(new RegExp(`^rows\\s*${OP.source}\\s*(\\d+)$`));
|
|
90597
|
+
if (rows)
|
|
90598
|
+
return { kind: "rows", op: rows[1], value: parseInt(rows[2], 10) };
|
|
90599
|
+
const value = s.match(new RegExp(`^value\\s*${OP.source}\\s*(.+)$`));
|
|
90600
|
+
if (value)
|
|
90601
|
+
return { kind: "value", op: value[1], value: parseScalar2(value[2]) };
|
|
90602
|
+
const col = s.match(/^col:(\w+)\s+(.+)$/);
|
|
90603
|
+
if (col) {
|
|
90604
|
+
const column = col[1];
|
|
90605
|
+
const rest = col[2].trim();
|
|
90606
|
+
if (/^not\s+null$/i.test(rest))
|
|
90607
|
+
return { kind: "col", column, pred: { type: "notNull" } };
|
|
90608
|
+
if (/^unique$/i.test(rest))
|
|
90609
|
+
return { kind: "col", column, pred: { type: "unique" } };
|
|
90610
|
+
const between = rest.match(/^between\s+(-?\d+(?:\.\d+)?)\s+and\s+(-?\d+(?:\.\d+)?)$/i);
|
|
90611
|
+
if (between) {
|
|
90612
|
+
return {
|
|
90613
|
+
kind: "col",
|
|
90614
|
+
column,
|
|
90615
|
+
pred: { type: "between", low: Number(between[1]), high: Number(between[2]) }
|
|
90616
|
+
};
|
|
90617
|
+
}
|
|
90618
|
+
const cmp = rest.match(new RegExp(`^${OP.source}\\s*(.+)$`));
|
|
90619
|
+
if (cmp)
|
|
90620
|
+
return {
|
|
90621
|
+
kind: "col",
|
|
90622
|
+
column,
|
|
90623
|
+
pred: { type: "cmp", op: cmp[1], value: parseScalar2(cmp[2]) }
|
|
90624
|
+
};
|
|
90625
|
+
}
|
|
90626
|
+
throw new AssertExpressionError(input);
|
|
90627
|
+
}
|
|
90628
|
+
|
|
90629
|
+
// src/core/assert/evaluator.ts
|
|
90630
|
+
class AssertShapeError extends Error {
|
|
90631
|
+
code = "ASSERT_SHAPE_MISMATCH";
|
|
90632
|
+
constructor(message) {
|
|
90633
|
+
super(message);
|
|
90634
|
+
this.name = "AssertShapeError";
|
|
90635
|
+
}
|
|
90636
|
+
}
|
|
90637
|
+
function compare(a, op, b) {
|
|
90638
|
+
switch (op) {
|
|
90639
|
+
case ">":
|
|
90640
|
+
return a > b;
|
|
90641
|
+
case ">=":
|
|
90642
|
+
return a >= b;
|
|
90643
|
+
case "<":
|
|
90644
|
+
return a < b;
|
|
90645
|
+
case "<=":
|
|
90646
|
+
return a <= b;
|
|
90647
|
+
case "==":
|
|
90648
|
+
return a === b;
|
|
90649
|
+
case "!=":
|
|
90650
|
+
return a !== b;
|
|
90651
|
+
}
|
|
90652
|
+
}
|
|
90653
|
+
function firstScalar(result) {
|
|
90654
|
+
if (result.columnNames.length !== 1) {
|
|
90655
|
+
throw new AssertShapeError(`value assertion needs a single-column result, got ${result.columnNames.length} columns. Project to one column.`);
|
|
90656
|
+
}
|
|
90657
|
+
if (result.rows.length === 0)
|
|
90658
|
+
return null;
|
|
90659
|
+
const v = result.rows[0][result.columnNames[0]];
|
|
90660
|
+
return v === null || v === undefined ? null : v;
|
|
90661
|
+
}
|
|
90662
|
+
function evaluateExpect(node, result) {
|
|
90663
|
+
if (node.kind === "rows") {
|
|
90664
|
+
const actual = result.rowCount;
|
|
90665
|
+
return {
|
|
90666
|
+
name: "rows",
|
|
90667
|
+
expected: `rows ${node.op} ${node.value}`,
|
|
90668
|
+
actual: String(actual),
|
|
90669
|
+
pass: compare(actual, node.op, node.value)
|
|
90670
|
+
};
|
|
90671
|
+
}
|
|
90672
|
+
if (node.kind === "value") {
|
|
90673
|
+
const actual = firstScalar(result);
|
|
90674
|
+
return {
|
|
90675
|
+
name: "value",
|
|
90676
|
+
expected: `value ${node.op} ${node.value}`,
|
|
90677
|
+
actual: String(actual),
|
|
90678
|
+
pass: actual !== null && compare(actual, node.op, node.value)
|
|
90679
|
+
};
|
|
90680
|
+
}
|
|
90681
|
+
const { column, pred } = node;
|
|
90682
|
+
const values = result.rows.map((r) => r[column]);
|
|
90683
|
+
const nonNull = values.filter((v) => v !== null && v !== undefined);
|
|
90684
|
+
if (pred.type === "notNull") {
|
|
90685
|
+
const nullCount = values.length - nonNull.length;
|
|
90686
|
+
return {
|
|
90687
|
+
name: `col:${column} not null`,
|
|
90688
|
+
expected: "0 nulls",
|
|
90689
|
+
actual: `${nullCount} nulls`,
|
|
90690
|
+
pass: nullCount === 0
|
|
90691
|
+
};
|
|
90692
|
+
}
|
|
90693
|
+
if (pred.type === "unique") {
|
|
90694
|
+
const distinct = new Set(nonNull.map((v) => String(v))).size;
|
|
90695
|
+
return {
|
|
90696
|
+
name: `col:${column} unique`,
|
|
90697
|
+
expected: `${nonNull.length} distinct`,
|
|
90698
|
+
actual: `${distinct} distinct`,
|
|
90699
|
+
pass: distinct === nonNull.length
|
|
90700
|
+
};
|
|
90701
|
+
}
|
|
90702
|
+
if (pred.type === "between") {
|
|
90703
|
+
const bad2 = nonNull.filter((v) => typeof v !== "number" || v < pred.low || v > pred.high);
|
|
90704
|
+
return {
|
|
90705
|
+
name: `col:${column} between ${pred.low} and ${pred.high}`,
|
|
90706
|
+
expected: "0 out of range",
|
|
90707
|
+
actual: `${bad2.length} out of range`,
|
|
90708
|
+
pass: bad2.length === 0
|
|
90709
|
+
};
|
|
90710
|
+
}
|
|
90711
|
+
const bad = nonNull.filter((v) => !compare(v, pred.op, pred.value));
|
|
90712
|
+
return {
|
|
90713
|
+
name: `col:${column} ${pred.op} ${pred.value}`,
|
|
90714
|
+
expected: "0 violations",
|
|
90715
|
+
actual: `${bad.length} violations`,
|
|
90716
|
+
pass: bad.length === 0
|
|
90717
|
+
};
|
|
90718
|
+
}
|
|
90719
|
+
function compareVs(a, b, mode) {
|
|
90720
|
+
if (mode === "rows") {
|
|
90721
|
+
return {
|
|
90722
|
+
name: "vs:rows",
|
|
90723
|
+
expected: String(b.rowCount),
|
|
90724
|
+
actual: String(a.rowCount),
|
|
90725
|
+
pass: a.rowCount === b.rowCount
|
|
90726
|
+
};
|
|
90727
|
+
}
|
|
90728
|
+
const av = firstScalar(a);
|
|
90729
|
+
const bv = firstScalar(b);
|
|
90730
|
+
return {
|
|
90731
|
+
name: "vs:value",
|
|
90732
|
+
expected: String(bv),
|
|
90733
|
+
actual: String(av),
|
|
90734
|
+
pass: av !== null && bv !== null && av === bv
|
|
90735
|
+
};
|
|
90736
|
+
}
|
|
90737
|
+
|
|
90738
|
+
// src/commands/assert.ts
|
|
90739
|
+
init_integration_helper();
|
|
90740
|
+
var ALLOWED_FORMATS13 = ["json", "table"];
|
|
90741
|
+
var SQL_SYSTEMS6 = ["postgresql", "mysql", "mariadb"];
|
|
90742
|
+
function requireSqlConnection10(connection) {
|
|
90743
|
+
if (!SQL_SYSTEMS6.includes(connection.system)) {
|
|
90744
|
+
throw new Error(`assert currently supports SQL engines only, got: ${connection.system}`);
|
|
90745
|
+
}
|
|
90746
|
+
return connection;
|
|
90747
|
+
}
|
|
90748
|
+
var assertCommand = new Command().name("assert").description("Assert an invariant on a query result (exit 1 on failure unless --no-fail)").argument("<query>", "SQL string or @saved-query reference").option("--expect <condition>", 'e.g. "rows > 0", "value == 5000", "col:email not null"').option("--vs <query>", "Second SQL/@saved query for reconciliation").option("--compare <mode>", "For --vs: rows | value (default value)", "value").option("--against <path>", "Compare current result fingerprint against a saved snapshot").option("--tolerance <pct>", "For --against: allowed relative drift, e.g. 0.01 (default 0)", (v) => parseFloat(v), 0).option("--no-fail", "Always exit 0; report pass/fail in output only").option("--format <format>", "Output format: json (default) or table", "json").action(async (query, options, command) => {
|
|
90749
|
+
try {
|
|
90750
|
+
validateFormat(options.format, ALLOWED_FORMATS13, "assert");
|
|
90751
|
+
if (!options.expect && !options.vs && !options.against) {
|
|
90752
|
+
console.error("Specify one of --expect, --vs, or --against");
|
|
90753
|
+
process.exit(1);
|
|
90754
|
+
}
|
|
90755
|
+
const configPath = resolveConfigPath(command, options);
|
|
90756
|
+
const config = await configModule.read(configPath);
|
|
90757
|
+
if (!config.connection) {
|
|
90758
|
+
console.error("Database not configured. Run: dbcli init");
|
|
90759
|
+
process.exit(1);
|
|
90760
|
+
}
|
|
90761
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection10(config.connection));
|
|
90762
|
+
await adapter.connect();
|
|
90763
|
+
let verdict;
|
|
90764
|
+
try {
|
|
90765
|
+
const blacklistManager = new BlacklistManager(config);
|
|
90766
|
+
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
90767
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
|
|
90768
|
+
const engine = mapSystemToEngine(config.connection.system);
|
|
90769
|
+
const dirs = resolveSnippetDirs(process.cwd());
|
|
90770
|
+
const snippets = await loadSnippets(dirs);
|
|
90771
|
+
const resolveSql = (q) => q.startsWith("@") ? resolveByName(snippets, q.slice(1), engine).query.sqlBody : q;
|
|
90772
|
+
const result = await executor3.execute(resolveSql(query), { autoLimit: true });
|
|
90773
|
+
const checks = [];
|
|
90774
|
+
if (options.expect)
|
|
90775
|
+
checks.push(evaluateExpect(parseExpect(options.expect), result));
|
|
90776
|
+
if (options.vs) {
|
|
90777
|
+
const other = await executor3.execute(resolveSql(options.vs), {
|
|
90778
|
+
autoLimit: true
|
|
90779
|
+
});
|
|
90780
|
+
checks.push(compareVs(result, other, options.compare ?? "value"));
|
|
90781
|
+
}
|
|
90782
|
+
if (options.against) {
|
|
90783
|
+
const baseline = await readSnapshot(options.against);
|
|
90784
|
+
const current = buildFingerprint(result, {
|
|
90785
|
+
query: resolveSql(query),
|
|
90786
|
+
engine: config.connection.system
|
|
90787
|
+
});
|
|
90788
|
+
checks.push(...compareAgainst(current, baseline, options.tolerance));
|
|
90789
|
+
}
|
|
90790
|
+
verdict = { pass: checks.every((c2) => c2.pass), checks };
|
|
90791
|
+
await writeAuditEntry(config, "assert", options, {
|
|
90792
|
+
success: verdict.pass,
|
|
90793
|
+
sql: query
|
|
90794
|
+
});
|
|
90795
|
+
} finally {
|
|
90796
|
+
await adapter.disconnect();
|
|
90797
|
+
}
|
|
90798
|
+
if (options.format === "json") {
|
|
90799
|
+
console.log(JSON.stringify(verdict, null, 2));
|
|
90800
|
+
} else {
|
|
90801
|
+
for (const c2 of verdict.checks) {
|
|
90802
|
+
console.log(`${c2.pass ? "PASS" : "FAIL"} ${c2.name} expected=${c2.expected} actual=${c2.actual}`);
|
|
90803
|
+
}
|
|
90804
|
+
console.log(`
|
|
90805
|
+
Verdict: ${verdict.pass ? "PASS" : "FAIL"}`);
|
|
90806
|
+
}
|
|
90807
|
+
process.exit(verdict.pass || options.fail === false ? 0 : 1);
|
|
90808
|
+
} catch (error) {
|
|
90809
|
+
if (error instanceof Error) {
|
|
90810
|
+
console.error(error.message);
|
|
90811
|
+
if (error instanceof ConnectionError)
|
|
90812
|
+
error.hints.forEach((h) => console.error(` Hint: ${h}`));
|
|
90813
|
+
}
|
|
90814
|
+
process.exit(1);
|
|
90815
|
+
}
|
|
90816
|
+
});
|
|
90817
|
+
|
|
90358
90818
|
// src/commands/recovery.ts
|
|
90359
90819
|
init_message_loader();
|
|
90360
90820
|
init_validation();
|
|
90361
90821
|
init_recovery();
|
|
90362
|
-
var
|
|
90822
|
+
var ALLOWED_FORMATS14 = ["json", "markdown"];
|
|
90363
90823
|
function parseCode(value) {
|
|
90364
90824
|
const normalized = value.trim();
|
|
90365
90825
|
if (!RECOVERY_CODES.includes(normalized)) {
|
|
@@ -90395,7 +90855,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
|
|
|
90395
90855
|
const forAgent = options.forAgent === true;
|
|
90396
90856
|
const format = forAgent ? "json" : options.format;
|
|
90397
90857
|
const brief = forAgent || options.brief === true;
|
|
90398
|
-
validateFormat(format,
|
|
90858
|
+
validateFormat(format, ALLOWED_FORMATS14, "recovery");
|
|
90399
90859
|
if (options.list === true) {
|
|
90400
90860
|
if (format === "markdown") {
|
|
90401
90861
|
console.log(renderCodeList());
|
|
@@ -90572,7 +91032,7 @@ function looksLikeSavedEnvelope(x) {
|
|
|
90572
91032
|
init_next_step();
|
|
90573
91033
|
init_next_step_schema();
|
|
90574
91034
|
init_validation();
|
|
90575
|
-
var
|
|
91035
|
+
var ALLOWED_FORMATS15 = ["json", "markdown"];
|
|
90576
91036
|
var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
|
|
90577
91037
|
|
|
90578
91038
|
class RecoverCliError extends Error {
|
|
@@ -90724,7 +91184,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
90724
91184
|
}
|
|
90725
91185
|
const explicitFormat = options.format;
|
|
90726
91186
|
const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
|
|
90727
|
-
validateFormat(format,
|
|
91187
|
+
validateFormat(format, ALLOWED_FORMATS15, "recover");
|
|
90728
91188
|
let allowWrite = "none";
|
|
90729
91189
|
if (options.next !== true) {
|
|
90730
91190
|
const allowWriteRaw = options.allowWrite;
|
|
@@ -90791,14 +91251,14 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
90791
91251
|
|
|
90792
91252
|
// src/commands/audit.ts
|
|
90793
91253
|
import { rm as rm3, stat as stat7 } from "fs/promises";
|
|
90794
|
-
import { join as
|
|
91254
|
+
import { join as join27 } from "path";
|
|
90795
91255
|
init_message_loader();
|
|
90796
91256
|
init_validation();
|
|
90797
91257
|
init_config();
|
|
90798
91258
|
init_config_binding();
|
|
90799
91259
|
init_reader();
|
|
90800
91260
|
init_integration_helper();
|
|
90801
|
-
var
|
|
91261
|
+
var ALLOWED_FORMATS16 = ["table", "json"];
|
|
90802
91262
|
var DEFAULT_TAIL_N = 10;
|
|
90803
91263
|
var MAX_TAIL_N = 1e4;
|
|
90804
91264
|
var SHORT_ID_LEN = 8;
|
|
@@ -90807,8 +91267,8 @@ var PREFIX_MIN = 4;
|
|
|
90807
91267
|
async function resolveAuditPaths(configPath, config) {
|
|
90808
91268
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
90809
91269
|
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
90810
|
-
const auditDir =
|
|
90811
|
-
const auditFile =
|
|
91270
|
+
const auditDir = join27(storagePath, ".dbcli", "audit");
|
|
91271
|
+
const auditFile = join27(auditDir, `${connName}.jsonl`);
|
|
90812
91272
|
return { auditDir, connectionName: connName, auditFile };
|
|
90813
91273
|
}
|
|
90814
91274
|
function isAuditDisabled(config) {
|
|
@@ -90992,13 +91452,13 @@ async function statAuditFile(file) {
|
|
|
90992
91452
|
}
|
|
90993
91453
|
}
|
|
90994
91454
|
var auditCommand = new Command("audit").description(t("audit.description"));
|
|
90995
|
-
auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${
|
|
91455
|
+
auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
|
|
90996
91456
|
const forAgent = options.forAgent === true;
|
|
90997
91457
|
const format = forAgent ? "json" : options.format;
|
|
90998
91458
|
const briefSource = command.getOptionValueSource("brief");
|
|
90999
91459
|
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
91000
91460
|
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
91001
|
-
validateFormat(format,
|
|
91461
|
+
validateFormat(format, ALLOWED_FORMATS16, "audit tail");
|
|
91002
91462
|
const n = parseTailN(options.n);
|
|
91003
91463
|
const configPath = resolveConfigPath(command, options);
|
|
91004
91464
|
const config = await configModule.read(configPath);
|
|
@@ -91041,7 +91501,7 @@ auditCommand.command("tail").description(t("audit.tail.description")).option("--
|
|
|
91041
91501
|
console.log(renderTailTable(tail));
|
|
91042
91502
|
}
|
|
91043
91503
|
});
|
|
91044
|
-
auditCommand.command("show [id]").description(t("audit.show.description")).option("--all", "Search across all connections", false).option("--recovery-ref <ref>", "Look up by entry.recovery_ref (exact match)").option("--format <format>", `Output format: ${
|
|
91504
|
+
auditCommand.command("show [id]").description(t("audit.show.description")).option("--all", "Search across all connections", false).option("--recovery-ref <ref>", "Look up by entry.recovery_ref (exact match)").option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim metadata + redacted_query", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (id, options, command) => {
|
|
91045
91505
|
if (id && options.recoveryRef) {
|
|
91046
91506
|
console.error(t("audit.show_mutex_violation"));
|
|
91047
91507
|
process.exit(1);
|
|
@@ -91055,7 +91515,7 @@ auditCommand.command("show [id]").description(t("audit.show.description")).optio
|
|
|
91055
91515
|
const briefSource = command.getOptionValueSource("brief");
|
|
91056
91516
|
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
91057
91517
|
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
91058
|
-
validateFormat(format,
|
|
91518
|
+
validateFormat(format, ALLOWED_FORMATS16, "audit show");
|
|
91059
91519
|
const configPath = resolveConfigPath(command, options);
|
|
91060
91520
|
const config = await configModule.read(configPath);
|
|
91061
91521
|
if (isAuditDisabled(config))
|
|
@@ -91197,13 +91657,13 @@ auditCommand.command("clear").description(t("audit.clear.description")).option("
|
|
|
91197
91657
|
`);
|
|
91198
91658
|
process.exit(0);
|
|
91199
91659
|
});
|
|
91200
|
-
auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${
|
|
91660
|
+
auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS16.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
|
|
91201
91661
|
const forAgent = options.forAgent === true;
|
|
91202
91662
|
const format = forAgent ? "json" : options.format;
|
|
91203
91663
|
const briefSource = command.getOptionValueSource("brief");
|
|
91204
91664
|
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
91205
91665
|
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
91206
|
-
validateFormat(format,
|
|
91666
|
+
validateFormat(format, ALLOWED_FORMATS16, "audit health");
|
|
91207
91667
|
const configPath = resolveConfigPath(command, options);
|
|
91208
91668
|
const config = await configModule.read(configPath);
|
|
91209
91669
|
const logger = await getAuditLogger(config, configPath);
|
|
@@ -91235,15 +91695,15 @@ init_config_binding();
|
|
|
91235
91695
|
init_schema_path();
|
|
91236
91696
|
init_config();
|
|
91237
91697
|
init_integration_helper();
|
|
91238
|
-
import { join as
|
|
91698
|
+
import { join as join28 } from "path";
|
|
91239
91699
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
91240
|
-
function
|
|
91700
|
+
function requireSqlConnection11(connection) {
|
|
91241
91701
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
91242
91702
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
91243
91703
|
}
|
|
91244
91704
|
return connection;
|
|
91245
91705
|
}
|
|
91246
|
-
var
|
|
91706
|
+
var ALLOWED_FORMATS17 = ["text", "json"];
|
|
91247
91707
|
var SENSITIVE_PATTERNS = [
|
|
91248
91708
|
"password",
|
|
91249
91709
|
"passwd",
|
|
@@ -91315,7 +91775,7 @@ var runDoctorChecks = {
|
|
|
91315
91775
|
}
|
|
91316
91776
|
},
|
|
91317
91777
|
async checkConfigExists(configPath, existsFn) {
|
|
91318
|
-
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(
|
|
91778
|
+
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join28(configPath, "config.json")).exists();
|
|
91319
91779
|
return {
|
|
91320
91780
|
group: "Configuration",
|
|
91321
91781
|
label: "Config exists",
|
|
@@ -91472,7 +91932,7 @@ var runDoctorChecks = {
|
|
|
91472
91932
|
async checkV2Config(configPath) {
|
|
91473
91933
|
const results = [];
|
|
91474
91934
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
91475
|
-
const configFile = Bun.file(
|
|
91935
|
+
const configFile = Bun.file(join28(storagePath, "config.json"));
|
|
91476
91936
|
if (!await configFile.exists())
|
|
91477
91937
|
return results;
|
|
91478
91938
|
let raw;
|
|
@@ -91512,7 +91972,7 @@ var runDoctorChecks = {
|
|
|
91512
91972
|
}
|
|
91513
91973
|
for (const [name2, conn] of Object.entries(config.connections)) {
|
|
91514
91974
|
if (conn.envFile) {
|
|
91515
|
-
const envPath =
|
|
91975
|
+
const envPath = join28(storagePath, conn.envFile);
|
|
91516
91976
|
const exists = await Bun.file(envPath).exists();
|
|
91517
91977
|
results.push({
|
|
91518
91978
|
group: "Configuration",
|
|
@@ -91657,7 +92117,7 @@ async function collectElasticsearchDoctorResults(config) {
|
|
|
91657
92117
|
return results;
|
|
91658
92118
|
}
|
|
91659
92119
|
var doctorCommand = new Command("doctor").description("Run diagnostic checks on dbcli configuration, environment, and connection").option("--format <type>", "Output format: text, json", "text").action(async (options) => {
|
|
91660
|
-
validateFormat(options.format,
|
|
92120
|
+
validateFormat(options.format, ALLOWED_FORMATS17, "doctor");
|
|
91661
92121
|
const logger = getLogger();
|
|
91662
92122
|
const results = [];
|
|
91663
92123
|
const configPath = resolveConfigPath(doctorCommand);
|
|
@@ -91703,7 +92163,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
91703
92163
|
blacklistedColumns
|
|
91704
92164
|
}));
|
|
91705
92165
|
} else {
|
|
91706
|
-
const adapter = AdapterFactory.createSqlAdapter(
|
|
92166
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection11(config.connection));
|
|
91707
92167
|
await adapter.connect();
|
|
91708
92168
|
results.push({
|
|
91709
92169
|
group: "Connection & Data",
|
|
@@ -91731,7 +92191,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
91731
92191
|
}
|
|
91732
92192
|
try {
|
|
91733
92193
|
const schemaConnName = await getSchemaIsolationConnectionName(configPath);
|
|
91734
|
-
const indexPath =
|
|
92194
|
+
const indexPath = join28(resolveSchemaPath(storagePath, schemaConnName), "index.json");
|
|
91735
92195
|
const indexFile = Bun.file(indexPath);
|
|
91736
92196
|
let indexParsed = null;
|
|
91737
92197
|
if (await indexFile.exists()) {
|
|
@@ -91785,7 +92245,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
91785
92245
|
|
|
91786
92246
|
// src/commands/completion.ts
|
|
91787
92247
|
init_colors();
|
|
91788
|
-
import { join as
|
|
92248
|
+
import { join as join29 } from "path";
|
|
91789
92249
|
import { homedir as homedir3 } from "os";
|
|
91790
92250
|
function extractCommands(program2) {
|
|
91791
92251
|
return program2.commands.map((cmd) => ({
|
|
@@ -91887,11 +92347,11 @@ function getInstallPath2(shell) {
|
|
|
91887
92347
|
const home = homedir3();
|
|
91888
92348
|
switch (shell) {
|
|
91889
92349
|
case "bash":
|
|
91890
|
-
return
|
|
92350
|
+
return join29(home, ".bashrc");
|
|
91891
92351
|
case "zsh":
|
|
91892
|
-
return
|
|
92352
|
+
return join29(home, ".zshrc");
|
|
91893
92353
|
case "fish":
|
|
91894
|
-
return
|
|
92354
|
+
return join29(home, ".config", "fish", "completions", "dbcli.fish");
|
|
91895
92355
|
default:
|
|
91896
92356
|
throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
91897
92357
|
}
|
|
@@ -91911,7 +92371,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
|
|
|
91911
92371
|
async function installCompletion(shell, script) {
|
|
91912
92372
|
const targetPath = getInstallPath2(shell);
|
|
91913
92373
|
if (shell === "fish") {
|
|
91914
|
-
const dir =
|
|
92374
|
+
const dir = join29(homedir3(), ".config", "fish", "completions");
|
|
91915
92375
|
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
91916
92376
|
await Bun.file(targetPath).write(script);
|
|
91917
92377
|
console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
|
|
@@ -92144,7 +92604,7 @@ ${t("upgrade.failed")}`));
|
|
|
92144
92604
|
init_config();
|
|
92145
92605
|
init_adapters();
|
|
92146
92606
|
import { createInterface as createInterface3 } from "readline";
|
|
92147
|
-
import { join as
|
|
92607
|
+
import { join as join30 } from "path";
|
|
92148
92608
|
import { homedir as homedir4 } from "os";
|
|
92149
92609
|
|
|
92150
92610
|
// src/core/repl/types.ts
|
|
@@ -93066,13 +93526,13 @@ async function runEsShell(configPath) {
|
|
|
93066
93526
|
}
|
|
93067
93527
|
|
|
93068
93528
|
// src/commands/shell.ts
|
|
93069
|
-
function
|
|
93529
|
+
function requireSqlConnection12(connection) {
|
|
93070
93530
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
93071
93531
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
93072
93532
|
}
|
|
93073
93533
|
return connection;
|
|
93074
93534
|
}
|
|
93075
|
-
var HISTORY_PATH =
|
|
93535
|
+
var HISTORY_PATH = join30(homedir4(), ".dbcli_history");
|
|
93076
93536
|
var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
|
|
93077
93537
|
async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
|
|
93078
93538
|
const columnsByTable = {};
|
|
@@ -93109,7 +93569,7 @@ async function runShell(options, configPath) {
|
|
|
93109
93569
|
const connectionOpts = config.connection;
|
|
93110
93570
|
const mongoInner = isMongoDB ? AdapterFactory.createMongoDBAdapter(connectionOpts) : null;
|
|
93111
93571
|
const redisInner = isRedis ? AdapterFactory.createRedisAdapter(connectionOpts, config.blacklist?.tables ?? [], config.redis?.mask ?? []) : null;
|
|
93112
|
-
const adapter = isMongoDB ? new MongoShellAdapter(mongoInner) : isRedis ? new RedisShellAdapter(redisInner) : AdapterFactory.createSqlAdapter(
|
|
93572
|
+
const adapter = isMongoDB ? new MongoShellAdapter(mongoInner) : isRedis ? new RedisShellAdapter(redisInner) : AdapterFactory.createSqlAdapter(requireSqlConnection12(connectionOpts));
|
|
93113
93573
|
try {
|
|
93114
93574
|
await adapter.connect();
|
|
93115
93575
|
} catch (error) {
|
|
@@ -93836,7 +94296,7 @@ class DDLExecutor {
|
|
|
93836
94296
|
}
|
|
93837
94297
|
}
|
|
93838
94298
|
// src/commands/migrate.ts
|
|
93839
|
-
function
|
|
94299
|
+
function requireSqlConnection13(connection) {
|
|
93840
94300
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
93841
94301
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
93842
94302
|
}
|
|
@@ -93860,7 +94320,7 @@ async function runDDL(operation, opts) {
|
|
|
93860
94320
|
console.error("\u6B64\u547D\u4EE4\u76EE\u524D\u4E0D\u652F\u63F4 Elasticsearch\uFF1B\u5982\u9700\u5EFA\u7ACB\u7D22\u5F15\u6216\u8ABF\u6574 mapping\uFF0C\u8ACB\u6539\u7528\u5916\u90E8\u5DE5\u5177");
|
|
93861
94321
|
process.exit(1);
|
|
93862
94322
|
}
|
|
93863
|
-
const adapter = AdapterFactory.createSqlAdapter(
|
|
94323
|
+
const adapter = AdapterFactory.createSqlAdapter(requireSqlConnection13(config.connection));
|
|
93864
94324
|
const isDryRun = !opts.execute;
|
|
93865
94325
|
if (!isDryRun) {
|
|
93866
94326
|
await adapter.connect();
|
|
@@ -94071,7 +94531,7 @@ init_config();
|
|
|
94071
94531
|
init_errors();
|
|
94072
94532
|
init_message_loader();
|
|
94073
94533
|
init_config_binding();
|
|
94074
|
-
import { join as
|
|
94534
|
+
import { join as join31 } from "path";
|
|
94075
94535
|
async function switchDefault(configPath, name2, config) {
|
|
94076
94536
|
if (!config.connections[name2]) {
|
|
94077
94537
|
const available = Object.keys(config.connections).join(", ");
|
|
@@ -94094,7 +94554,7 @@ function listConnectionsForDisplay(config) {
|
|
|
94094
94554
|
}
|
|
94095
94555
|
async function ensureV2Config(configPath) {
|
|
94096
94556
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
94097
|
-
const configFile = Bun.file(
|
|
94557
|
+
const configFile = Bun.file(join31(storagePath, "config.json"));
|
|
94098
94558
|
const legacyFile = Bun.file(configPath);
|
|
94099
94559
|
if (!await configFile.exists() && !await legacyFile.exists()) {
|
|
94100
94560
|
throw new ConfigError(t("init.config_not_found"));
|
|
@@ -94156,14 +94616,1214 @@ var useCommand = new Command("use").description("Switch or display the default d
|
|
|
94156
94616
|
}
|
|
94157
94617
|
});
|
|
94158
94618
|
|
|
94619
|
+
// src/commands/proxy.ts
|
|
94620
|
+
init_config();
|
|
94621
|
+
init_validation();
|
|
94622
|
+
import { join as join32 } from "path";
|
|
94623
|
+
|
|
94624
|
+
// src/proxy/relay.ts
|
|
94625
|
+
class TcpRelay {
|
|
94626
|
+
clientBytes = 0;
|
|
94627
|
+
serverBytes = 0;
|
|
94628
|
+
opts;
|
|
94629
|
+
constructor(opts) {
|
|
94630
|
+
this.opts = opts;
|
|
94631
|
+
}
|
|
94632
|
+
fromClient(bytes) {
|
|
94633
|
+
this.opts.writeToUpstream(bytes);
|
|
94634
|
+
this.clientBytes += bytes.length;
|
|
94635
|
+
this.feed("client_to_server", bytes);
|
|
94636
|
+
}
|
|
94637
|
+
fromUpstream(bytes) {
|
|
94638
|
+
this.opts.writeToClient(bytes);
|
|
94639
|
+
this.serverBytes += bytes.length;
|
|
94640
|
+
this.feed("server_to_client", bytes);
|
|
94641
|
+
}
|
|
94642
|
+
feed(direction, bytes) {
|
|
94643
|
+
try {
|
|
94644
|
+
this.opts.analyzer.onData(direction, bytes);
|
|
94645
|
+
} catch (err) {
|
|
94646
|
+
this.opts.onSignal({
|
|
94647
|
+
kind: "parse_error",
|
|
94648
|
+
message: err instanceof Error ? err.message : String(err)
|
|
94649
|
+
});
|
|
94650
|
+
}
|
|
94651
|
+
}
|
|
94652
|
+
}
|
|
94653
|
+
|
|
94654
|
+
// src/proxy/events.ts
|
|
94655
|
+
init_jsonl_rotation();
|
|
94656
|
+
import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile6, stat as stat8 } from "fs/promises";
|
|
94657
|
+
import { dirname as dirname11 } from "path";
|
|
94658
|
+
|
|
94659
|
+
// src/proxy/sql-metadata.ts
|
|
94660
|
+
var KNOWN_KEYWORDS = [
|
|
94661
|
+
"SELECT",
|
|
94662
|
+
"INSERT",
|
|
94663
|
+
"UPDATE",
|
|
94664
|
+
"DELETE",
|
|
94665
|
+
"CREATE",
|
|
94666
|
+
"ALTER",
|
|
94667
|
+
"DROP",
|
|
94668
|
+
"TRUNCATE",
|
|
94669
|
+
"BEGIN",
|
|
94670
|
+
"COMMIT",
|
|
94671
|
+
"ROLLBACK",
|
|
94672
|
+
"SET",
|
|
94673
|
+
"SHOW",
|
|
94674
|
+
"USE"
|
|
94675
|
+
];
|
|
94676
|
+
var KNOWN = new Set(KNOWN_KEYWORDS);
|
|
94677
|
+
function detectStatement(sql) {
|
|
94678
|
+
const m = sql.trim().match(/^([a-zA-Z]+)/);
|
|
94679
|
+
if (!m || !m[1])
|
|
94680
|
+
return "OTHER";
|
|
94681
|
+
const kw = m[1].toUpperCase();
|
|
94682
|
+
return KNOWN.has(kw) ? kw : "OTHER";
|
|
94683
|
+
}
|
|
94684
|
+
var TABLE_RE = /\b(?:FROM|JOIN|INTO|UPDATE)\s+["'`]?([A-Za-z_]\w*)["'`]?(?:\.["'`]?([A-Za-z_]\w*)["'`]?)?/gi;
|
|
94685
|
+
function extractTables(sql) {
|
|
94686
|
+
const seen = new Set;
|
|
94687
|
+
for (const m of sql.matchAll(TABLE_RE)) {
|
|
94688
|
+
const name2 = m[2] ?? m[1];
|
|
94689
|
+
if (name2)
|
|
94690
|
+
seen.add(name2);
|
|
94691
|
+
}
|
|
94692
|
+
return [...seen];
|
|
94693
|
+
}
|
|
94694
|
+
function redactLiterals(sql) {
|
|
94695
|
+
const noStrings = sql.replace(/'(?:[^']|'')*'/g, "?");
|
|
94696
|
+
return noStrings.replace(/\b\d+(?:\.\d+)?\b/g, "?");
|
|
94697
|
+
}
|
|
94698
|
+
|
|
94699
|
+
// src/proxy/events.ts
|
|
94700
|
+
var PROXY_EVENT_VERSION = 1;
|
|
94701
|
+
function hasSql(e) {
|
|
94702
|
+
return e.type === "query_observed" || e.type === "query_completed" || e.type === "query_errored";
|
|
94703
|
+
}
|
|
94704
|
+
function applyRedaction(event, mode) {
|
|
94705
|
+
if (mode === "none" || !hasSql(event))
|
|
94706
|
+
return event;
|
|
94707
|
+
return { ...event, sql: redactLiterals(event.sql) };
|
|
94708
|
+
}
|
|
94709
|
+
var DEFAULT_ROTATION = {
|
|
94710
|
+
maxBytes: 50 * 1024 * 1024,
|
|
94711
|
+
maxEntries: 200000
|
|
94712
|
+
};
|
|
94713
|
+
|
|
94714
|
+
class EventWriter {
|
|
94715
|
+
path;
|
|
94716
|
+
previousPath;
|
|
94717
|
+
redact;
|
|
94718
|
+
maxBytes;
|
|
94719
|
+
maxEntries;
|
|
94720
|
+
dirEnsured = false;
|
|
94721
|
+
initialized = false;
|
|
94722
|
+
currentSizeBytes = 0;
|
|
94723
|
+
currentEntryCount = 0;
|
|
94724
|
+
writeChain = Promise.resolve();
|
|
94725
|
+
constructor(opts) {
|
|
94726
|
+
this.path = opts.path;
|
|
94727
|
+
this.previousPath = `${opts.path}.1`;
|
|
94728
|
+
this.redact = opts.redact;
|
|
94729
|
+
this.maxBytes = opts.rotation?.maxBytes ?? DEFAULT_ROTATION.maxBytes;
|
|
94730
|
+
this.maxEntries = opts.rotation?.maxEntries ?? DEFAULT_ROTATION.maxEntries;
|
|
94731
|
+
}
|
|
94732
|
+
write(event) {
|
|
94733
|
+
const op = this.writeChain.then(() => this.writeInternal(event));
|
|
94734
|
+
this.writeChain = op.then(() => {
|
|
94735
|
+
return;
|
|
94736
|
+
}, () => {
|
|
94737
|
+
return;
|
|
94738
|
+
});
|
|
94739
|
+
return op;
|
|
94740
|
+
}
|
|
94741
|
+
async writeInternal(event) {
|
|
94742
|
+
if (!this.dirEnsured) {
|
|
94743
|
+
await mkdir10(dirname11(this.path), { recursive: true });
|
|
94744
|
+
this.dirEnsured = true;
|
|
94745
|
+
}
|
|
94746
|
+
if (!this.initialized) {
|
|
94747
|
+
await this.syncCountersFromDisk();
|
|
94748
|
+
this.initialized = true;
|
|
94749
|
+
}
|
|
94750
|
+
const redacted = applyRedaction(event, this.redact);
|
|
94751
|
+
const line = JSON.stringify(redacted) + `
|
|
94752
|
+
`;
|
|
94753
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
94754
|
+
if (shouldRotate({ currentSizeBytes: this.currentSizeBytes, currentEntryCount: this.currentEntryCount }, { maxBytes: this.maxBytes, maxEntries: this.maxEntries }, lineBytes)) {
|
|
94755
|
+
await rotate(this.path, this.previousPath);
|
|
94756
|
+
this.currentSizeBytes = 0;
|
|
94757
|
+
this.currentEntryCount = 0;
|
|
94758
|
+
}
|
|
94759
|
+
await appendFile2(this.path, line, { encoding: "utf8" });
|
|
94760
|
+
this.currentSizeBytes += lineBytes;
|
|
94761
|
+
this.currentEntryCount += 1;
|
|
94762
|
+
}
|
|
94763
|
+
async syncCountersFromDisk() {
|
|
94764
|
+
try {
|
|
94765
|
+
const s = await stat8(this.path);
|
|
94766
|
+
this.currentSizeBytes = s.size;
|
|
94767
|
+
const raw = await readFile6(this.path, "utf8");
|
|
94768
|
+
this.currentEntryCount = raw.split(`
|
|
94769
|
+
`).filter(Boolean).length;
|
|
94770
|
+
} catch {
|
|
94771
|
+
this.currentSizeBytes = 0;
|
|
94772
|
+
this.currentEntryCount = 0;
|
|
94773
|
+
}
|
|
94774
|
+
}
|
|
94775
|
+
}
|
|
94776
|
+
|
|
94777
|
+
// src/proxy/session.ts
|
|
94778
|
+
class ProxySession {
|
|
94779
|
+
o;
|
|
94780
|
+
active = null;
|
|
94781
|
+
queryCounter = 0;
|
|
94782
|
+
startedAt = 0;
|
|
94783
|
+
clientBytesAtBoundary = 0;
|
|
94784
|
+
pending = Promise.resolve();
|
|
94785
|
+
constructor(opts) {
|
|
94786
|
+
this.o = opts;
|
|
94787
|
+
}
|
|
94788
|
+
enqueue(event) {
|
|
94789
|
+
this.pending = this.pending.then(() => this.o.writeEvent(event)).catch((err) => {
|
|
94790
|
+
this.o.warn(`event write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
94791
|
+
});
|
|
94792
|
+
}
|
|
94793
|
+
async start() {
|
|
94794
|
+
this.startedAt = this.o.now();
|
|
94795
|
+
this.enqueue({
|
|
94796
|
+
version: PROXY_EVENT_VERSION,
|
|
94797
|
+
type: "session_started",
|
|
94798
|
+
timestamp: new Date().toISOString(),
|
|
94799
|
+
engine: this.o.engine,
|
|
94800
|
+
sessionId: this.o.sessionId,
|
|
94801
|
+
client: this.o.client,
|
|
94802
|
+
target: this.o.target
|
|
94803
|
+
});
|
|
94804
|
+
await this.flush();
|
|
94805
|
+
}
|
|
94806
|
+
onSignal(signal) {
|
|
94807
|
+
switch (signal.kind) {
|
|
94808
|
+
case "query":
|
|
94809
|
+
this.beginQuery(signal.sql, signal.tags ?? []);
|
|
94810
|
+
break;
|
|
94811
|
+
case "query_end":
|
|
94812
|
+
this.completeQuery(signal.rowCount ?? null);
|
|
94813
|
+
break;
|
|
94814
|
+
case "error":
|
|
94815
|
+
this.errorQuery(signal.code, signal.message);
|
|
94816
|
+
break;
|
|
94817
|
+
case "tag":
|
|
94818
|
+
if (this.active && !this.active.tags.includes(signal.tag)) {
|
|
94819
|
+
this.active.tags.push(signal.tag);
|
|
94820
|
+
}
|
|
94821
|
+
break;
|
|
94822
|
+
case "parse_error":
|
|
94823
|
+
this.enqueue({
|
|
94824
|
+
version: PROXY_EVENT_VERSION,
|
|
94825
|
+
type: "parse_error",
|
|
94826
|
+
timestamp: new Date().toISOString(),
|
|
94827
|
+
engine: this.o.engine,
|
|
94828
|
+
sessionId: this.o.sessionId,
|
|
94829
|
+
client: this.o.client,
|
|
94830
|
+
target: this.o.target,
|
|
94831
|
+
message: signal.message,
|
|
94832
|
+
tags: []
|
|
94833
|
+
});
|
|
94834
|
+
break;
|
|
94835
|
+
}
|
|
94836
|
+
}
|
|
94837
|
+
beginQuery(sql, tags) {
|
|
94838
|
+
const bytes = this.o.getBytes();
|
|
94839
|
+
this.queryCounter += 1;
|
|
94840
|
+
this.active = {
|
|
94841
|
+
queryId: `qry_${this.o.sessionId}_${this.queryCounter}`,
|
|
94842
|
+
sql,
|
|
94843
|
+
startedAt: this.o.now(),
|
|
94844
|
+
clientBytesAtStart: this.clientBytesAtBoundary,
|
|
94845
|
+
serverBytesAtStart: bytes.serverBytes,
|
|
94846
|
+
tags: [...tags]
|
|
94847
|
+
};
|
|
94848
|
+
this.enqueue({
|
|
94849
|
+
version: PROXY_EVENT_VERSION,
|
|
94850
|
+
type: "query_observed",
|
|
94851
|
+
timestamp: new Date().toISOString(),
|
|
94852
|
+
engine: this.o.engine,
|
|
94853
|
+
sessionId: this.o.sessionId,
|
|
94854
|
+
queryId: this.active.queryId,
|
|
94855
|
+
client: this.o.client,
|
|
94856
|
+
target: this.o.target,
|
|
94857
|
+
sql,
|
|
94858
|
+
statement: detectStatement(sql),
|
|
94859
|
+
tables: extractTables(sql),
|
|
94860
|
+
tags: [...this.active.tags]
|
|
94861
|
+
});
|
|
94862
|
+
}
|
|
94863
|
+
completeQuery(rowCount) {
|
|
94864
|
+
const q3 = this.active;
|
|
94865
|
+
if (!q3)
|
|
94866
|
+
return;
|
|
94867
|
+
const bytes = this.o.getBytes();
|
|
94868
|
+
const durationMs = this.o.now() - q3.startedAt;
|
|
94869
|
+
const requestBytes = bytes.clientBytes - q3.clientBytesAtStart;
|
|
94870
|
+
const responseBytes = bytes.serverBytes - q3.serverBytesAtStart;
|
|
94871
|
+
const slow = durationMs >= this.o.slowMs;
|
|
94872
|
+
this.enqueue({
|
|
94873
|
+
version: PROXY_EVENT_VERSION,
|
|
94874
|
+
type: "query_completed",
|
|
94875
|
+
timestamp: new Date().toISOString(),
|
|
94876
|
+
engine: this.o.engine,
|
|
94877
|
+
sessionId: this.o.sessionId,
|
|
94878
|
+
queryId: q3.queryId,
|
|
94879
|
+
client: this.o.client,
|
|
94880
|
+
target: this.o.target,
|
|
94881
|
+
sql: q3.sql,
|
|
94882
|
+
statement: detectStatement(q3.sql),
|
|
94883
|
+
tables: extractTables(q3.sql),
|
|
94884
|
+
durationMs,
|
|
94885
|
+
requestBytes,
|
|
94886
|
+
responseBytes,
|
|
94887
|
+
rowCount,
|
|
94888
|
+
slow,
|
|
94889
|
+
error: null,
|
|
94890
|
+
tags: [...q3.tags]
|
|
94891
|
+
});
|
|
94892
|
+
if (slow) {
|
|
94893
|
+
this.o.warn(`slow query (${durationMs}ms): ${q3.sql.slice(0, 80)}`);
|
|
94894
|
+
}
|
|
94895
|
+
this.clientBytesAtBoundary = bytes.clientBytes;
|
|
94896
|
+
this.active = null;
|
|
94897
|
+
}
|
|
94898
|
+
errorQuery(code, message) {
|
|
94899
|
+
const q3 = this.active;
|
|
94900
|
+
const bytes = this.o.getBytes();
|
|
94901
|
+
const startedAt = q3?.startedAt ?? this.o.now();
|
|
94902
|
+
this.enqueue({
|
|
94903
|
+
version: PROXY_EVENT_VERSION,
|
|
94904
|
+
type: "query_errored",
|
|
94905
|
+
timestamp: new Date().toISOString(),
|
|
94906
|
+
engine: this.o.engine,
|
|
94907
|
+
sessionId: this.o.sessionId,
|
|
94908
|
+
queryId: q3?.queryId ?? `qry_${this.o.sessionId}_err_${++this.queryCounter}`,
|
|
94909
|
+
client: this.o.client,
|
|
94910
|
+
target: this.o.target,
|
|
94911
|
+
sql: q3?.sql ?? "",
|
|
94912
|
+
statement: detectStatement(q3?.sql ?? ""),
|
|
94913
|
+
tables: extractTables(q3?.sql ?? ""),
|
|
94914
|
+
durationMs: this.o.now() - startedAt,
|
|
94915
|
+
requestBytes: q3 ? bytes.clientBytes - q3.clientBytesAtStart : 0,
|
|
94916
|
+
responseBytes: q3 ? bytes.serverBytes - q3.serverBytesAtStart : 0,
|
|
94917
|
+
rowCount: null,
|
|
94918
|
+
error: { code, message },
|
|
94919
|
+
tags: q3 ? [...q3.tags] : []
|
|
94920
|
+
});
|
|
94921
|
+
this.clientBytesAtBoundary = bytes.clientBytes;
|
|
94922
|
+
this.active = null;
|
|
94923
|
+
}
|
|
94924
|
+
async end(reason) {
|
|
94925
|
+
const bytes = this.o.getBytes();
|
|
94926
|
+
this.enqueue({
|
|
94927
|
+
version: PROXY_EVENT_VERSION,
|
|
94928
|
+
type: "session_ended",
|
|
94929
|
+
timestamp: new Date().toISOString(),
|
|
94930
|
+
engine: this.o.engine,
|
|
94931
|
+
sessionId: this.o.sessionId,
|
|
94932
|
+
client: this.o.client,
|
|
94933
|
+
target: this.o.target,
|
|
94934
|
+
durationMs: this.o.now() - this.startedAt,
|
|
94935
|
+
requestBytes: bytes.clientBytes,
|
|
94936
|
+
responseBytes: bytes.serverBytes,
|
|
94937
|
+
reason
|
|
94938
|
+
});
|
|
94939
|
+
await this.flush();
|
|
94940
|
+
}
|
|
94941
|
+
async flush() {
|
|
94942
|
+
await this.pending;
|
|
94943
|
+
}
|
|
94944
|
+
}
|
|
94945
|
+
|
|
94946
|
+
// src/proxy/analyzers/types.ts
|
|
94947
|
+
var UTF8 = new TextDecoder;
|
|
94948
|
+
|
|
94949
|
+
class FrameBuffer {
|
|
94950
|
+
buf = new Uint8Array(0);
|
|
94951
|
+
get length() {
|
|
94952
|
+
return this.buf.length;
|
|
94953
|
+
}
|
|
94954
|
+
push(chunk) {
|
|
94955
|
+
if (this.buf.length === 0) {
|
|
94956
|
+
this.buf = chunk.slice();
|
|
94957
|
+
return;
|
|
94958
|
+
}
|
|
94959
|
+
const next = new Uint8Array(this.buf.length + chunk.length);
|
|
94960
|
+
next.set(this.buf, 0);
|
|
94961
|
+
next.set(chunk, this.buf.length);
|
|
94962
|
+
this.buf = next;
|
|
94963
|
+
}
|
|
94964
|
+
peek(n) {
|
|
94965
|
+
return this.buf.subarray(0, Math.min(n, this.buf.length));
|
|
94966
|
+
}
|
|
94967
|
+
byteAt(offset) {
|
|
94968
|
+
return this.buf[offset];
|
|
94969
|
+
}
|
|
94970
|
+
consume(n) {
|
|
94971
|
+
this.buf = this.buf.subarray(Math.min(n, this.buf.length));
|
|
94972
|
+
}
|
|
94973
|
+
readUInt24LE(offset) {
|
|
94974
|
+
const b0 = this.buf[offset] ?? 0;
|
|
94975
|
+
const b1 = this.buf[offset + 1] ?? 0;
|
|
94976
|
+
const b2 = this.buf[offset + 2] ?? 0;
|
|
94977
|
+
return b0 | b1 << 8 | b2 << 16;
|
|
94978
|
+
}
|
|
94979
|
+
readUInt16LE(offset) {
|
|
94980
|
+
const b0 = this.buf[offset] ?? 0;
|
|
94981
|
+
const b1 = this.buf[offset + 1] ?? 0;
|
|
94982
|
+
return b0 | b1 << 8;
|
|
94983
|
+
}
|
|
94984
|
+
readUInt32BE(offset) {
|
|
94985
|
+
const b0 = this.buf[offset] ?? 0;
|
|
94986
|
+
const b1 = this.buf[offset + 1] ?? 0;
|
|
94987
|
+
const b2 = this.buf[offset + 2] ?? 0;
|
|
94988
|
+
const b3 = this.buf[offset + 3] ?? 0;
|
|
94989
|
+
return (b0 << 24 >>> 0) + (b1 << 16) + (b2 << 8) + b3;
|
|
94990
|
+
}
|
|
94991
|
+
text(start, end) {
|
|
94992
|
+
return UTF8.decode(this.buf.subarray(start, end));
|
|
94993
|
+
}
|
|
94994
|
+
}
|
|
94995
|
+
|
|
94996
|
+
// src/proxy/analyzers/mysql.ts
|
|
94997
|
+
var COM_QUERY = 3;
|
|
94998
|
+
var COM_STMT_PREPARE = 22;
|
|
94999
|
+
var COM_STMT_EXECUTE = 23;
|
|
95000
|
+
var HEADER = 4;
|
|
95001
|
+
function createMysqlAnalyzer(deps) {
|
|
95002
|
+
const clientBuf = new FrameBuffer;
|
|
95003
|
+
const serverBuf = new FrameBuffer;
|
|
95004
|
+
let awaitingResponse = false;
|
|
95005
|
+
function handleClientPacket(payloadStart, payloadLen) {
|
|
95006
|
+
const seqId = clientBuf.byteAt(payloadStart - 1);
|
|
95007
|
+
if (seqId !== 0)
|
|
95008
|
+
return;
|
|
95009
|
+
const cmd = clientBuf.byteAt(payloadStart);
|
|
95010
|
+
if (cmd === undefined)
|
|
95011
|
+
return;
|
|
95012
|
+
if (cmd === COM_QUERY) {
|
|
95013
|
+
const sql = clientBuf.text(payloadStart + 1, payloadStart + payloadLen);
|
|
95014
|
+
deps.emit({ kind: "query", sql });
|
|
95015
|
+
awaitingResponse = true;
|
|
95016
|
+
} else if (cmd === COM_STMT_PREPARE) {
|
|
95017
|
+
const sql = clientBuf.text(payloadStart + 1, payloadStart + payloadLen);
|
|
95018
|
+
deps.emit({ kind: "query", sql, tags: ["prepared_statement"] });
|
|
95019
|
+
awaitingResponse = true;
|
|
95020
|
+
} else if (cmd === COM_STMT_EXECUTE) {
|
|
95021
|
+
deps.emit({ kind: "tag", tag: "prepared_statement" });
|
|
95022
|
+
awaitingResponse = true;
|
|
95023
|
+
}
|
|
95024
|
+
}
|
|
95025
|
+
function handleServerPacket(payloadStart, payloadLen) {
|
|
95026
|
+
if (!awaitingResponse)
|
|
95027
|
+
return;
|
|
95028
|
+
const first = serverBuf.byteAt(payloadStart);
|
|
95029
|
+
if (first === undefined)
|
|
95030
|
+
return;
|
|
95031
|
+
if (first === 255) {
|
|
95032
|
+
const code = serverBuf.readUInt16LE(payloadStart + 1);
|
|
95033
|
+
let msgStart = payloadStart + 3;
|
|
95034
|
+
if (serverBuf.byteAt(msgStart) === 35) {
|
|
95035
|
+
msgStart += 6;
|
|
95036
|
+
}
|
|
95037
|
+
const message = serverBuf.text(msgStart, payloadStart + payloadLen);
|
|
95038
|
+
deps.emit({ kind: "error", code: String(code), message });
|
|
95039
|
+
awaitingResponse = false;
|
|
95040
|
+
} else if (first === 0) {
|
|
95041
|
+
deps.emit({ kind: "query_end", rowCount: null });
|
|
95042
|
+
awaitingResponse = false;
|
|
95043
|
+
} else {
|
|
95044
|
+
deps.emit({ kind: "tag", tag: "parse_partial" });
|
|
95045
|
+
deps.emit({ kind: "query_end", rowCount: null });
|
|
95046
|
+
awaitingResponse = false;
|
|
95047
|
+
}
|
|
95048
|
+
}
|
|
95049
|
+
function drain(buf, onPacket) {
|
|
95050
|
+
while (buf.length >= HEADER) {
|
|
95051
|
+
const payloadLen = buf.readUInt24LE(0);
|
|
95052
|
+
if (buf.length < HEADER + payloadLen)
|
|
95053
|
+
break;
|
|
95054
|
+
onPacket(HEADER, payloadLen);
|
|
95055
|
+
buf.consume(HEADER + payloadLen);
|
|
95056
|
+
}
|
|
95057
|
+
}
|
|
95058
|
+
return {
|
|
95059
|
+
onData(direction, chunk) {
|
|
95060
|
+
try {
|
|
95061
|
+
if (direction === "client_to_server") {
|
|
95062
|
+
clientBuf.push(chunk);
|
|
95063
|
+
drain(clientBuf, handleClientPacket);
|
|
95064
|
+
} else {
|
|
95065
|
+
serverBuf.push(chunk);
|
|
95066
|
+
drain(serverBuf, handleServerPacket);
|
|
95067
|
+
}
|
|
95068
|
+
} catch (err) {
|
|
95069
|
+
deps.emit({
|
|
95070
|
+
kind: "parse_error",
|
|
95071
|
+
message: err instanceof Error ? err.message : String(err)
|
|
95072
|
+
});
|
|
95073
|
+
}
|
|
95074
|
+
}
|
|
95075
|
+
};
|
|
95076
|
+
}
|
|
95077
|
+
|
|
95078
|
+
// src/proxy/analyzers/postgresql.ts
|
|
95079
|
+
function rowCountFromTag(tag) {
|
|
95080
|
+
const parts = tag.trim().split(/\s+/);
|
|
95081
|
+
const last = parts[parts.length - 1];
|
|
95082
|
+
if (last === undefined)
|
|
95083
|
+
return null;
|
|
95084
|
+
const n = Number(last);
|
|
95085
|
+
return Number.isInteger(n) ? n : null;
|
|
95086
|
+
}
|
|
95087
|
+
var MAX_STARTUP_LEN = 1e4;
|
|
95088
|
+
function createPostgresAnalyzer(deps) {
|
|
95089
|
+
const clientBuf = new FrameBuffer;
|
|
95090
|
+
const serverBuf = new FrameBuffer;
|
|
95091
|
+
let startupSeen = false;
|
|
95092
|
+
let awaitingResponse = false;
|
|
95093
|
+
function cStringEnd(buf, start, end) {
|
|
95094
|
+
let i = start;
|
|
95095
|
+
while (i < end && buf.byteAt(i) !== 0)
|
|
95096
|
+
i++;
|
|
95097
|
+
return i;
|
|
95098
|
+
}
|
|
95099
|
+
function readCString(buf, start, end) {
|
|
95100
|
+
return buf.text(start, cStringEnd(buf, start, end));
|
|
95101
|
+
}
|
|
95102
|
+
function handleClientMessage(type, bodyStart, bodyEnd) {
|
|
95103
|
+
const t2 = String.fromCharCode(type);
|
|
95104
|
+
if (t2 === "Q") {
|
|
95105
|
+
const sql = readCString(clientBuf, bodyStart, bodyEnd);
|
|
95106
|
+
deps.emit({ kind: "query", sql });
|
|
95107
|
+
awaitingResponse = true;
|
|
95108
|
+
} else if (t2 === "P" || t2 === "B" || t2 === "E" || t2 === "S" || t2 === "D") {
|
|
95109
|
+
deps.emit({ kind: "tag", tag: "extended_protocol" });
|
|
95110
|
+
if (t2 === "P") {
|
|
95111
|
+
const nameEnd = cStringEnd(clientBuf, bodyStart, bodyEnd) + 1;
|
|
95112
|
+
const sql = readCString(clientBuf, nameEnd, bodyEnd);
|
|
95113
|
+
if (sql)
|
|
95114
|
+
deps.emit({ kind: "query", sql, tags: ["extended_protocol"] });
|
|
95115
|
+
awaitingResponse = true;
|
|
95116
|
+
}
|
|
95117
|
+
}
|
|
95118
|
+
}
|
|
95119
|
+
function handleServerMessage(type, bodyStart, bodyEnd) {
|
|
95120
|
+
if (!awaitingResponse)
|
|
95121
|
+
return;
|
|
95122
|
+
const t2 = String.fromCharCode(type);
|
|
95123
|
+
if (t2 === "E") {
|
|
95124
|
+
let code = null;
|
|
95125
|
+
let message = "";
|
|
95126
|
+
let i = bodyStart;
|
|
95127
|
+
while (i < bodyEnd) {
|
|
95128
|
+
const fieldType = serverBuf.byteAt(i);
|
|
95129
|
+
if (fieldType === undefined || fieldType === 0)
|
|
95130
|
+
break;
|
|
95131
|
+
i += 1;
|
|
95132
|
+
const valEnd = cStringEnd(serverBuf, i, bodyEnd);
|
|
95133
|
+
const value = serverBuf.text(i, valEnd);
|
|
95134
|
+
i = valEnd + 1;
|
|
95135
|
+
if (fieldType === 67)
|
|
95136
|
+
code = value;
|
|
95137
|
+
else if (fieldType === 77)
|
|
95138
|
+
message = value;
|
|
95139
|
+
}
|
|
95140
|
+
deps.emit({ kind: "error", code, message });
|
|
95141
|
+
awaitingResponse = false;
|
|
95142
|
+
} else if (t2 === "C") {
|
|
95143
|
+
const tag = readCString(serverBuf, bodyStart, bodyEnd);
|
|
95144
|
+
deps.emit({ kind: "query_end", rowCount: rowCountFromTag(tag) });
|
|
95145
|
+
awaitingResponse = false;
|
|
95146
|
+
}
|
|
95147
|
+
}
|
|
95148
|
+
function drain(buf, isClient, onMessage) {
|
|
95149
|
+
if (isClient && !startupSeen) {
|
|
95150
|
+
if (buf.length < 4)
|
|
95151
|
+
return;
|
|
95152
|
+
const len = buf.readUInt32BE(0);
|
|
95153
|
+
if (len >= 4 && len <= buf.length && len <= MAX_STARTUP_LEN) {
|
|
95154
|
+
startupSeen = true;
|
|
95155
|
+
buf.consume(len);
|
|
95156
|
+
} else {
|
|
95157
|
+
startupSeen = true;
|
|
95158
|
+
}
|
|
95159
|
+
}
|
|
95160
|
+
while (buf.length >= 5) {
|
|
95161
|
+
const type = buf.byteAt(0);
|
|
95162
|
+
const len = buf.readUInt32BE(1);
|
|
95163
|
+
const total = 1 + len;
|
|
95164
|
+
if (buf.length < total)
|
|
95165
|
+
break;
|
|
95166
|
+
onMessage(type, 5, total);
|
|
95167
|
+
buf.consume(total);
|
|
95168
|
+
}
|
|
95169
|
+
}
|
|
95170
|
+
return {
|
|
95171
|
+
onData(direction, chunk) {
|
|
95172
|
+
try {
|
|
95173
|
+
if (direction === "client_to_server") {
|
|
95174
|
+
clientBuf.push(chunk);
|
|
95175
|
+
drain(clientBuf, true, handleClientMessage);
|
|
95176
|
+
} else {
|
|
95177
|
+
serverBuf.push(chunk);
|
|
95178
|
+
drain(serverBuf, false, handleServerMessage);
|
|
95179
|
+
}
|
|
95180
|
+
} catch (err) {
|
|
95181
|
+
deps.emit({
|
|
95182
|
+
kind: "parse_error",
|
|
95183
|
+
message: err instanceof Error ? err.message : String(err)
|
|
95184
|
+
});
|
|
95185
|
+
}
|
|
95186
|
+
}
|
|
95187
|
+
};
|
|
95188
|
+
}
|
|
95189
|
+
|
|
95190
|
+
// src/proxy/server.ts
|
|
95191
|
+
function makeAnalyzer(engine, deps) {
|
|
95192
|
+
return engine === "postgresql" ? createPostgresAnalyzer(deps) : createMysqlAnalyzer(deps);
|
|
95193
|
+
}
|
|
95194
|
+
|
|
95195
|
+
class ProxyServer {
|
|
95196
|
+
o;
|
|
95197
|
+
writer;
|
|
95198
|
+
listener = null;
|
|
95199
|
+
sessionCounter = 0;
|
|
95200
|
+
writeFailed = false;
|
|
95201
|
+
constructor(opts) {
|
|
95202
|
+
this.o = opts;
|
|
95203
|
+
this.writer = new EventWriter({ path: opts.eventsPath, redact: opts.redact });
|
|
95204
|
+
}
|
|
95205
|
+
get port() {
|
|
95206
|
+
return this.listener?.port ?? null;
|
|
95207
|
+
}
|
|
95208
|
+
async start() {
|
|
95209
|
+
await this.writer.write({
|
|
95210
|
+
version: PROXY_EVENT_VERSION,
|
|
95211
|
+
type: "proxy_started",
|
|
95212
|
+
timestamp: new Date().toISOString(),
|
|
95213
|
+
engine: this.o.engine,
|
|
95214
|
+
sessionId: "pxy_root",
|
|
95215
|
+
listen: `${this.o.listen.host}:${this.o.listen.port}`,
|
|
95216
|
+
target: `${this.o.target.host}:${this.o.target.port}`
|
|
95217
|
+
});
|
|
95218
|
+
this.listener = Bun.listen({
|
|
95219
|
+
hostname: this.o.listen.host,
|
|
95220
|
+
port: this.o.listen.port,
|
|
95221
|
+
socket: {
|
|
95222
|
+
open: (client) => {
|
|
95223
|
+
this.handleConnection(client);
|
|
95224
|
+
},
|
|
95225
|
+
data(client, chunk) {
|
|
95226
|
+
const ctx = client.data;
|
|
95227
|
+
ctx?.onData?.(chunk);
|
|
95228
|
+
},
|
|
95229
|
+
close(client) {
|
|
95230
|
+
const ctx = client.data;
|
|
95231
|
+
ctx?.onClose?.();
|
|
95232
|
+
},
|
|
95233
|
+
error(client) {
|
|
95234
|
+
const ctx = client.data;
|
|
95235
|
+
ctx?.onClose?.();
|
|
95236
|
+
}
|
|
95237
|
+
}
|
|
95238
|
+
});
|
|
95239
|
+
}
|
|
95240
|
+
stop() {
|
|
95241
|
+
this.listener?.stop();
|
|
95242
|
+
this.listener = null;
|
|
95243
|
+
}
|
|
95244
|
+
async handleConnection(client) {
|
|
95245
|
+
if (this.writeFailed) {
|
|
95246
|
+
client.end();
|
|
95247
|
+
return;
|
|
95248
|
+
}
|
|
95249
|
+
this.sessionCounter += 1;
|
|
95250
|
+
const sessionId = `pxy_${this.sessionCounter}`;
|
|
95251
|
+
const clientAddr = client.remoteAddress ?? "unknown";
|
|
95252
|
+
const target = `${this.o.target.host}:${this.o.target.port}`;
|
|
95253
|
+
const earlyBuffer = [];
|
|
95254
|
+
let relay = null;
|
|
95255
|
+
client.data = {
|
|
95256
|
+
onData: (chunk) => {
|
|
95257
|
+
if (relay) {
|
|
95258
|
+
relay.fromClient(chunk);
|
|
95259
|
+
} else {
|
|
95260
|
+
earlyBuffer.push(chunk);
|
|
95261
|
+
}
|
|
95262
|
+
},
|
|
95263
|
+
onClose: () => {
|
|
95264
|
+
client.end();
|
|
95265
|
+
}
|
|
95266
|
+
};
|
|
95267
|
+
const session = new ProxySession({
|
|
95268
|
+
sessionId,
|
|
95269
|
+
engine: this.o.engine,
|
|
95270
|
+
client: clientAddr,
|
|
95271
|
+
target,
|
|
95272
|
+
slowMs: this.o.slowMs,
|
|
95273
|
+
now: () => performance.now(),
|
|
95274
|
+
getBytes: () => ({
|
|
95275
|
+
clientBytes: relay?.clientBytes ?? 0,
|
|
95276
|
+
serverBytes: relay?.serverBytes ?? 0
|
|
95277
|
+
}),
|
|
95278
|
+
writeEvent: (e) => this.writer.write(e).catch((err) => {
|
|
95279
|
+
this.writeFailed = true;
|
|
95280
|
+
this.o.warn(`event write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
95281
|
+
}),
|
|
95282
|
+
warn: this.o.warn
|
|
95283
|
+
});
|
|
95284
|
+
let upstream;
|
|
95285
|
+
try {
|
|
95286
|
+
const analyzer = makeAnalyzer(this.o.engine, { emit: (s) => session.onSignal(s) });
|
|
95287
|
+
upstream = await Bun.connect({
|
|
95288
|
+
hostname: this.o.target.host,
|
|
95289
|
+
port: this.o.target.port,
|
|
95290
|
+
socket: {
|
|
95291
|
+
data(_s, chunk) {
|
|
95292
|
+
relay?.fromUpstream(chunk);
|
|
95293
|
+
},
|
|
95294
|
+
close() {
|
|
95295
|
+
session.end("upstream_closed").then(() => client.end());
|
|
95296
|
+
},
|
|
95297
|
+
error() {
|
|
95298
|
+
session.end("error").then(() => client.end());
|
|
95299
|
+
}
|
|
95300
|
+
}
|
|
95301
|
+
});
|
|
95302
|
+
relay = new TcpRelay({
|
|
95303
|
+
writeToClient: (b) => {
|
|
95304
|
+
client.write(b);
|
|
95305
|
+
},
|
|
95306
|
+
writeToUpstream: (b) => {
|
|
95307
|
+
upstream.write(b);
|
|
95308
|
+
},
|
|
95309
|
+
analyzer,
|
|
95310
|
+
onSignal: (s) => session.onSignal(s)
|
|
95311
|
+
});
|
|
95312
|
+
const liveUpstream = upstream;
|
|
95313
|
+
client.data = {
|
|
95314
|
+
onData: (chunk) => relay?.fromClient(chunk),
|
|
95315
|
+
onClose: () => void session.end("client_closed").then(() => liveUpstream.end())
|
|
95316
|
+
};
|
|
95317
|
+
for (const chunk of earlyBuffer) {
|
|
95318
|
+
relay.fromClient(chunk);
|
|
95319
|
+
}
|
|
95320
|
+
earlyBuffer.length = 0;
|
|
95321
|
+
} catch (err) {
|
|
95322
|
+
this.o.warn(`upstream connect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
95323
|
+
await session.start();
|
|
95324
|
+
await session.end("error");
|
|
95325
|
+
client.end();
|
|
95326
|
+
return;
|
|
95327
|
+
}
|
|
95328
|
+
try {
|
|
95329
|
+
await session.start();
|
|
95330
|
+
} catch (err) {
|
|
95331
|
+
this.o.warn(`session start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
95332
|
+
client.end();
|
|
95333
|
+
upstream.end();
|
|
95334
|
+
return;
|
|
95335
|
+
}
|
|
95336
|
+
}
|
|
95337
|
+
}
|
|
95338
|
+
|
|
95339
|
+
// src/proxy/event-reader.ts
|
|
95340
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
95341
|
+
async function readEvents(path6, opts) {
|
|
95342
|
+
const candidates = opts.includeRotated ? [path6, `${path6}.1`] : [path6];
|
|
95343
|
+
const files = [];
|
|
95344
|
+
const events = [];
|
|
95345
|
+
let malformedLines = 0;
|
|
95346
|
+
for (const file of candidates) {
|
|
95347
|
+
let raw;
|
|
95348
|
+
try {
|
|
95349
|
+
raw = await readFile7(file, "utf8");
|
|
95350
|
+
} catch (err) {
|
|
95351
|
+
if (err.code !== "ENOENT")
|
|
95352
|
+
throw err;
|
|
95353
|
+
continue;
|
|
95354
|
+
}
|
|
95355
|
+
files.push(file);
|
|
95356
|
+
for (const rawLine of raw.split(`
|
|
95357
|
+
`)) {
|
|
95358
|
+
const trimmed = rawLine.trim();
|
|
95359
|
+
if (!trimmed)
|
|
95360
|
+
continue;
|
|
95361
|
+
try {
|
|
95362
|
+
events.push(JSON.parse(trimmed));
|
|
95363
|
+
} catch {
|
|
95364
|
+
malformedLines += 1;
|
|
95365
|
+
}
|
|
95366
|
+
}
|
|
95367
|
+
}
|
|
95368
|
+
events.sort((a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0);
|
|
95369
|
+
return { events, malformedLines, files };
|
|
95370
|
+
}
|
|
95371
|
+
|
|
95372
|
+
// src/proxy/analyze.ts
|
|
95373
|
+
var isCompleted = (e) => e.type === "query_completed";
|
|
95374
|
+
var isErrored = (e) => e.type === "query_errored";
|
|
95375
|
+
function percentile(values, p) {
|
|
95376
|
+
if (values.length === 0)
|
|
95377
|
+
return 0;
|
|
95378
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
95379
|
+
const rank = Math.ceil(p / 100 * sorted.length);
|
|
95380
|
+
const idx = Math.min(Math.max(rank, 1), sorted.length) - 1;
|
|
95381
|
+
return sorted[idx];
|
|
95382
|
+
}
|
|
95383
|
+
function fingerprintSql(sql) {
|
|
95384
|
+
return redactLiterals(sql).replace(/\s+/g, " ").trim();
|
|
95385
|
+
}
|
|
95386
|
+
function shellEscapeDq(s) {
|
|
95387
|
+
return s.replace(/\\/g, "\\\\").replace(/\$/g, "\\$").replace(/`/g, "\\`").replace(/"/g, "\\\"");
|
|
95388
|
+
}
|
|
95389
|
+
function buildByFingerprint(events, slowMs, top) {
|
|
95390
|
+
const errorByFp = new Map;
|
|
95391
|
+
for (const e of events.filter(isErrored)) {
|
|
95392
|
+
const fp = fingerprintSql(e.sql);
|
|
95393
|
+
errorByFp.set(fp, (errorByFp.get(fp) ?? 0) + 1);
|
|
95394
|
+
}
|
|
95395
|
+
const groups = new Map;
|
|
95396
|
+
for (const e of events.filter(isCompleted)) {
|
|
95397
|
+
const fp = fingerprintSql(e.sql);
|
|
95398
|
+
let g = groups.get(fp);
|
|
95399
|
+
if (!g) {
|
|
95400
|
+
g = {
|
|
95401
|
+
fingerprint: fp,
|
|
95402
|
+
statement: e.statement,
|
|
95403
|
+
tables: e.tables,
|
|
95404
|
+
durations: [],
|
|
95405
|
+
reqBytes: 0,
|
|
95406
|
+
respBytes: 0,
|
|
95407
|
+
rows: [],
|
|
95408
|
+
slowCount: 0,
|
|
95409
|
+
exampleSql: e.sql,
|
|
95410
|
+
exampleQueryId: e.queryId,
|
|
95411
|
+
exampleDuration: e.durationMs
|
|
95412
|
+
};
|
|
95413
|
+
groups.set(fp, g);
|
|
95414
|
+
}
|
|
95415
|
+
g.durations.push(e.durationMs);
|
|
95416
|
+
g.reqBytes += e.requestBytes;
|
|
95417
|
+
g.respBytes += e.responseBytes;
|
|
95418
|
+
if (e.rowCount !== null)
|
|
95419
|
+
g.rows.push(e.rowCount);
|
|
95420
|
+
if (e.durationMs >= slowMs)
|
|
95421
|
+
g.slowCount += 1;
|
|
95422
|
+
if (e.durationMs > g.exampleDuration) {
|
|
95423
|
+
g.exampleDuration = e.durationMs;
|
|
95424
|
+
g.exampleSql = e.sql;
|
|
95425
|
+
g.exampleQueryId = e.queryId;
|
|
95426
|
+
}
|
|
95427
|
+
}
|
|
95428
|
+
const stats = [...groups.values()].map((g) => {
|
|
95429
|
+
const count = g.durations.length;
|
|
95430
|
+
const total = g.durations.reduce((sum, d) => sum + d, 0);
|
|
95431
|
+
return {
|
|
95432
|
+
fingerprint: g.fingerprint,
|
|
95433
|
+
statement: g.statement,
|
|
95434
|
+
tables: g.tables,
|
|
95435
|
+
count,
|
|
95436
|
+
durationMs: {
|
|
95437
|
+
total,
|
|
95438
|
+
avg: count ? Math.round(total / count) : 0,
|
|
95439
|
+
p95: percentile(g.durations, 95),
|
|
95440
|
+
max: count ? Math.max(...g.durations) : 0
|
|
95441
|
+
},
|
|
95442
|
+
rowsAvg: g.rows.length ? Math.round(g.rows.reduce((sum, r) => sum + r, 0) / g.rows.length) : 0,
|
|
95443
|
+
bytesAvg: {
|
|
95444
|
+
request: count ? Math.round(g.reqBytes / count) : 0,
|
|
95445
|
+
response: count ? Math.round(g.respBytes / count) : 0
|
|
95446
|
+
},
|
|
95447
|
+
errorCount: errorByFp.get(g.fingerprint) ?? 0,
|
|
95448
|
+
slowCount: g.slowCount,
|
|
95449
|
+
redacted: redactLiterals(g.exampleSql) === g.exampleSql,
|
|
95450
|
+
exampleSql: g.exampleSql,
|
|
95451
|
+
exampleQueryId: g.exampleQueryId
|
|
95452
|
+
};
|
|
95453
|
+
});
|
|
95454
|
+
stats.sort((a, b) => b.durationMs.total - a.durationMs.total);
|
|
95455
|
+
return stats.map((s, i) => {
|
|
95456
|
+
if (i < top && s.statement === "SELECT") {
|
|
95457
|
+
const sql = shellEscapeDq(s.exampleSql);
|
|
95458
|
+
return {
|
|
95459
|
+
...s,
|
|
95460
|
+
suggestedCommands: [`dbcli explain "${sql}"`, `dbcli guide missing-index-for "${sql}"`]
|
|
95461
|
+
};
|
|
95462
|
+
}
|
|
95463
|
+
return s;
|
|
95464
|
+
});
|
|
95465
|
+
}
|
|
95466
|
+
function buildSlowest(events, top) {
|
|
95467
|
+
return events.filter(isCompleted).sort((a, b) => b.durationMs - a.durationMs).slice(0, top).map((e) => ({
|
|
95468
|
+
queryId: e.queryId,
|
|
95469
|
+
durationMs: e.durationMs,
|
|
95470
|
+
sql: e.sql,
|
|
95471
|
+
statement: e.statement,
|
|
95472
|
+
tables: e.tables,
|
|
95473
|
+
timestamp: e.timestamp,
|
|
95474
|
+
sessionId: e.sessionId
|
|
95475
|
+
}));
|
|
95476
|
+
}
|
|
95477
|
+
function buildErrors(events) {
|
|
95478
|
+
const groups = new Map;
|
|
95479
|
+
for (const e of events.filter(isErrored)) {
|
|
95480
|
+
const key = `${e.error.code ?? ""} ${e.error.message}`;
|
|
95481
|
+
let g = groups.get(key);
|
|
95482
|
+
if (!g) {
|
|
95483
|
+
g = {
|
|
95484
|
+
code: e.error.code,
|
|
95485
|
+
message: e.error.message,
|
|
95486
|
+
count: 0,
|
|
95487
|
+
fingerprint: fingerprintSql(e.sql),
|
|
95488
|
+
exampleSql: e.sql
|
|
95489
|
+
};
|
|
95490
|
+
groups.set(key, g);
|
|
95491
|
+
}
|
|
95492
|
+
g.count += 1;
|
|
95493
|
+
}
|
|
95494
|
+
return [...groups.values()].sort((a, b) => b.count - a.count);
|
|
95495
|
+
}
|
|
95496
|
+
function buildHotTables(events) {
|
|
95497
|
+
const map = new Map;
|
|
95498
|
+
for (const e of events.filter(isCompleted)) {
|
|
95499
|
+
for (const t2 of e.tables) {
|
|
95500
|
+
let g = map.get(t2);
|
|
95501
|
+
if (!g) {
|
|
95502
|
+
g = { queryCount: 0, totalDurationMs: 0 };
|
|
95503
|
+
map.set(t2, g);
|
|
95504
|
+
}
|
|
95505
|
+
g.queryCount += 1;
|
|
95506
|
+
g.totalDurationMs += e.durationMs;
|
|
95507
|
+
}
|
|
95508
|
+
}
|
|
95509
|
+
return [...map.entries()].map(([table, g]) => ({ table, queryCount: g.queryCount, totalDurationMs: g.totalDurationMs })).sort((a, b) => b.queryCount - a.queryCount);
|
|
95510
|
+
}
|
|
95511
|
+
function buildRepetition(events, threshold) {
|
|
95512
|
+
const groups = new Map;
|
|
95513
|
+
for (const e of events.filter(isCompleted)) {
|
|
95514
|
+
const fp = fingerprintSql(e.sql);
|
|
95515
|
+
const key = `${e.sessionId} ${fp}`;
|
|
95516
|
+
const ts = Date.parse(e.timestamp);
|
|
95517
|
+
let g = groups.get(key);
|
|
95518
|
+
if (!g) {
|
|
95519
|
+
g = {
|
|
95520
|
+
fingerprint: fp,
|
|
95521
|
+
sessionId: e.sessionId,
|
|
95522
|
+
tables: e.tables,
|
|
95523
|
+
count: 0,
|
|
95524
|
+
totalDurationMs: 0,
|
|
95525
|
+
minTs: ts,
|
|
95526
|
+
maxTs: ts
|
|
95527
|
+
};
|
|
95528
|
+
groups.set(key, g);
|
|
95529
|
+
}
|
|
95530
|
+
g.count += 1;
|
|
95531
|
+
g.totalDurationMs += e.durationMs;
|
|
95532
|
+
if (ts < g.minTs)
|
|
95533
|
+
g.minTs = ts;
|
|
95534
|
+
if (ts > g.maxTs)
|
|
95535
|
+
g.maxTs = ts;
|
|
95536
|
+
}
|
|
95537
|
+
return [...groups.values()].filter((g) => g.count >= threshold).map((g) => ({
|
|
95538
|
+
fingerprint: g.fingerprint,
|
|
95539
|
+
sessionId: g.sessionId,
|
|
95540
|
+
count: g.count,
|
|
95541
|
+
spanMs: g.maxTs - g.minTs,
|
|
95542
|
+
totalDurationMs: g.totalDurationMs,
|
|
95543
|
+
tables: g.tables
|
|
95544
|
+
})).sort((a, b) => b.count - a.count);
|
|
95545
|
+
}
|
|
95546
|
+
function analyzeEvents(events, opts) {
|
|
95547
|
+
const timestamps = events.map((e) => e.timestamp).filter(Boolean).sort();
|
|
95548
|
+
const from = timestamps[0] ?? null;
|
|
95549
|
+
const to = timestamps[timestamps.length - 1] ?? null;
|
|
95550
|
+
return {
|
|
95551
|
+
version: 1,
|
|
95552
|
+
tool: "proxy-analyze",
|
|
95553
|
+
engine: events[0]?.engine ?? null,
|
|
95554
|
+
source: {
|
|
95555
|
+
files: opts.sourceFiles,
|
|
95556
|
+
eventsRead: events.length,
|
|
95557
|
+
malformedLines: opts.malformedLines,
|
|
95558
|
+
timeSpan: {
|
|
95559
|
+
from,
|
|
95560
|
+
to,
|
|
95561
|
+
durationMs: from && to ? Date.parse(to) - Date.parse(from) : 0
|
|
95562
|
+
}
|
|
95563
|
+
},
|
|
95564
|
+
summary: buildSummary(events, opts.slowMs),
|
|
95565
|
+
byFingerprint: buildByFingerprint(events, opts.slowMs, opts.top),
|
|
95566
|
+
slowest: buildSlowest(events, opts.top),
|
|
95567
|
+
errors: buildErrors(events),
|
|
95568
|
+
hotTables: buildHotTables(events),
|
|
95569
|
+
repetition: buildRepetition(events, opts.nPlusOne)
|
|
95570
|
+
};
|
|
95571
|
+
}
|
|
95572
|
+
function buildSummary(events, slowMs) {
|
|
95573
|
+
const completed = events.filter(isCompleted);
|
|
95574
|
+
const errored = events.filter(isErrored);
|
|
95575
|
+
const durations = completed.map((e) => e.durationMs);
|
|
95576
|
+
const queries = completed.length;
|
|
95577
|
+
const errors3 = errored.length;
|
|
95578
|
+
const denom = queries + errors3;
|
|
95579
|
+
return {
|
|
95580
|
+
sessions: new Set(events.filter((e) => e.type === "session_started").map((e) => e.sessionId)).size,
|
|
95581
|
+
queries,
|
|
95582
|
+
errors: errors3,
|
|
95583
|
+
errorRate: denom === 0 ? 0 : errors3 / denom,
|
|
95584
|
+
parseErrors: events.filter((e) => e.type === "parse_error").length,
|
|
95585
|
+
slowCount: completed.filter((e) => e.durationMs >= slowMs).length,
|
|
95586
|
+
latencyMs: {
|
|
95587
|
+
p50: percentile(durations, 50),
|
|
95588
|
+
p95: percentile(durations, 95),
|
|
95589
|
+
p99: percentile(durations, 99),
|
|
95590
|
+
max: durations.length ? Math.max(...durations) : 0
|
|
95591
|
+
},
|
|
95592
|
+
bytes: {
|
|
95593
|
+
request: completed.reduce((sum, e) => sum + e.requestBytes, 0),
|
|
95594
|
+
response: completed.reduce((sum, e) => sum + e.responseBytes, 0)
|
|
95595
|
+
}
|
|
95596
|
+
};
|
|
95597
|
+
}
|
|
95598
|
+
|
|
95599
|
+
// src/proxy/analyze-render.ts
|
|
95600
|
+
function renderAnalysisText(report, top) {
|
|
95601
|
+
if (report.summary.queries === 0 && report.summary.errors === 0) {
|
|
95602
|
+
return "no events to analyze";
|
|
95603
|
+
}
|
|
95604
|
+
const s = report.summary;
|
|
95605
|
+
const L2 = [];
|
|
95606
|
+
L2.push("SUMMARY");
|
|
95607
|
+
L2.push(` engine: ${report.engine ?? "unknown"} sessions: ${s.sessions} ` + `queries: ${s.queries} errors: ${s.errors} (${(s.errorRate * 100).toFixed(2)}%)`);
|
|
95608
|
+
L2.push(` latency ms: p50=${s.latencyMs.p50} p95=${s.latencyMs.p95} ` + `p99=${s.latencyMs.p99} max=${s.latencyMs.max} slow=${s.slowCount}`);
|
|
95609
|
+
L2.push(` bytes: req=${s.bytes.request} resp=${s.bytes.response}`);
|
|
95610
|
+
L2.push("", "TOP QUERIES BY TOTAL TIME");
|
|
95611
|
+
for (const f of report.byFingerprint.slice(0, top)) {
|
|
95612
|
+
L2.push(` [${f.count}x total=${f.durationMs.total}ms avg=${f.durationMs.avg} ` + `p95=${f.durationMs.p95}] ${f.fingerprint}`);
|
|
95613
|
+
}
|
|
95614
|
+
L2.push("", "SLOWEST SINGLE QUERIES");
|
|
95615
|
+
for (const q3 of report.slowest.slice(0, top)) {
|
|
95616
|
+
L2.push(` ${q3.durationMs}ms ${q3.sql}`);
|
|
95617
|
+
}
|
|
95618
|
+
L2.push("", "HOT TABLES");
|
|
95619
|
+
for (const t2 of report.hotTables.slice(0, top)) {
|
|
95620
|
+
L2.push(` ${t2.queryCount}x ${t2.totalDurationMs}ms ${t2.table}`);
|
|
95621
|
+
}
|
|
95622
|
+
L2.push("", "ERRORS");
|
|
95623
|
+
if (report.errors.length === 0)
|
|
95624
|
+
L2.push(" (none)");
|
|
95625
|
+
for (const e of report.errors.slice(0, top)) {
|
|
95626
|
+
L2.push(` ${e.count}x [${e.code ?? "?"}] ${e.message}`);
|
|
95627
|
+
}
|
|
95628
|
+
L2.push("", "N+1 SUSPECTS");
|
|
95629
|
+
if (report.repetition.length === 0)
|
|
95630
|
+
L2.push(" (none)");
|
|
95631
|
+
for (const r of report.repetition.slice(0, top)) {
|
|
95632
|
+
L2.push(` ${r.count}x in session ${r.sessionId} (${r.spanMs}ms) ${r.fingerprint}`);
|
|
95633
|
+
}
|
|
95634
|
+
const cmds = [...new Set(report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []))];
|
|
95635
|
+
if (cmds.length) {
|
|
95636
|
+
L2.push("", "SUGGESTED COMMANDS");
|
|
95637
|
+
for (const c2 of cmds)
|
|
95638
|
+
L2.push(` ${c2}`);
|
|
95639
|
+
}
|
|
95640
|
+
return L2.join(`
|
|
95641
|
+
`);
|
|
95642
|
+
}
|
|
95643
|
+
|
|
95644
|
+
// src/commands/proxy.ts
|
|
95645
|
+
var SUPPORTED = ["mysql", "mariadb", "postgresql"];
|
|
95646
|
+
var ALLOWED_FORMATS18 = ["text", "json"];
|
|
95647
|
+
var ALLOWED_REDACT = ["none", "literals"];
|
|
95648
|
+
function parseHostPort(value) {
|
|
95649
|
+
const idx = value.lastIndexOf(":");
|
|
95650
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
95651
|
+
throw new Error(`Invalid address "${value}". Expected host:port`);
|
|
95652
|
+
}
|
|
95653
|
+
const host = value.slice(0, idx);
|
|
95654
|
+
const port = Number(value.slice(idx + 1));
|
|
95655
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
95656
|
+
throw new Error(`Invalid port in "${value}". Expected host:port with a numeric port`);
|
|
95657
|
+
}
|
|
95658
|
+
return { host, port };
|
|
95659
|
+
}
|
|
95660
|
+
function resolveProxyConfig(input) {
|
|
95661
|
+
if (!input.listen) {
|
|
95662
|
+
throw new Error("--listen <host:port> is required");
|
|
95663
|
+
}
|
|
95664
|
+
const listen = parseHostPort(input.listen);
|
|
95665
|
+
let engine;
|
|
95666
|
+
if (input.subcommandEngine) {
|
|
95667
|
+
engine = input.subcommandEngine;
|
|
95668
|
+
} else {
|
|
95669
|
+
const sys = input.connection?.system;
|
|
95670
|
+
if (!sys || !SUPPORTED.includes(sys)) {
|
|
95671
|
+
throw new Error(`proxy supports mysql, mariadb, postgresql (got: ${sys ?? "none"})`);
|
|
95672
|
+
}
|
|
95673
|
+
engine = sys;
|
|
95674
|
+
}
|
|
95675
|
+
let target;
|
|
95676
|
+
if (input.target) {
|
|
95677
|
+
target = parseHostPort(input.target);
|
|
95678
|
+
} else if (input.connection) {
|
|
95679
|
+
target = { host: input.connection.host, port: input.connection.port };
|
|
95680
|
+
} else {
|
|
95681
|
+
throw new Error("--target <host:port> is required when config does not provide host/port");
|
|
95682
|
+
}
|
|
95683
|
+
return { engine, listen, target };
|
|
95684
|
+
}
|
|
95685
|
+
async function runProxy(subcommandEngine, options, command) {
|
|
95686
|
+
try {
|
|
95687
|
+
validateFormat(options.format ?? "text", ALLOWED_FORMATS18, "proxy");
|
|
95688
|
+
const redact = options.redact ?? "none";
|
|
95689
|
+
if (!ALLOWED_REDACT.includes(redact)) {
|
|
95690
|
+
throw new Error(`Invalid --redact "${redact}". Allowed: none, literals`);
|
|
95691
|
+
}
|
|
95692
|
+
const configPath = resolveConfigPath(command, options);
|
|
95693
|
+
let connection = null;
|
|
95694
|
+
try {
|
|
95695
|
+
const config = await configModule.read(configPath);
|
|
95696
|
+
if (config.connection) {
|
|
95697
|
+
connection = {
|
|
95698
|
+
system: config.connection.system,
|
|
95699
|
+
host: config.connection.host,
|
|
95700
|
+
port: config.connection.port
|
|
95701
|
+
};
|
|
95702
|
+
}
|
|
95703
|
+
} catch {}
|
|
95704
|
+
const resolved = resolveProxyConfig({
|
|
95705
|
+
subcommandEngine,
|
|
95706
|
+
listen: options.listen,
|
|
95707
|
+
target: options.target,
|
|
95708
|
+
connection
|
|
95709
|
+
});
|
|
95710
|
+
const eventsPath = options.events ?? join32(".dbcli", "proxy", "events.jsonl");
|
|
95711
|
+
const slowMs = Number(options.slowMs ?? 1000);
|
|
95712
|
+
if (!Number.isFinite(slowMs) || slowMs < 0) {
|
|
95713
|
+
throw new Error(`Invalid --slow-ms "${options.slowMs}". Expected a non-negative number`);
|
|
95714
|
+
}
|
|
95715
|
+
const server = new ProxyServer({
|
|
95716
|
+
engine: resolved.engine,
|
|
95717
|
+
listen: resolved.listen,
|
|
95718
|
+
target: resolved.target,
|
|
95719
|
+
eventsPath,
|
|
95720
|
+
slowMs,
|
|
95721
|
+
redact,
|
|
95722
|
+
warn: (m) => process.stderr.write(`[proxy] ${m}
|
|
95723
|
+
`)
|
|
95724
|
+
});
|
|
95725
|
+
await server.start();
|
|
95726
|
+
if (options.format === "json") {
|
|
95727
|
+
process.stdout.write(JSON.stringify({
|
|
95728
|
+
status: "listening",
|
|
95729
|
+
engine: resolved.engine,
|
|
95730
|
+
listen: `${resolved.listen.host}:${resolved.listen.port}`,
|
|
95731
|
+
target: `${resolved.target.host}:${resolved.target.port}`,
|
|
95732
|
+
events: eventsPath,
|
|
95733
|
+
redact
|
|
95734
|
+
}) + `
|
|
95735
|
+
`);
|
|
95736
|
+
} else {
|
|
95737
|
+
process.stdout.write(`dbcli proxy (${resolved.engine}) listening on ${resolved.listen.host}:${resolved.listen.port}` + ` -> ${resolved.target.host}:${resolved.target.port}
|
|
95738
|
+
` + `events: ${eventsPath} | slow-ms: ${slowMs} | redact: ${redact}
|
|
95739
|
+
` + `Press Ctrl+C to stop.
|
|
95740
|
+
`);
|
|
95741
|
+
}
|
|
95742
|
+
await new Promise((resolve5) => {
|
|
95743
|
+
const shutdown = () => {
|
|
95744
|
+
process.removeListener("SIGINT", shutdown);
|
|
95745
|
+
process.removeListener("SIGTERM", shutdown);
|
|
95746
|
+
server.stop();
|
|
95747
|
+
resolve5();
|
|
95748
|
+
};
|
|
95749
|
+
process.on("SIGINT", shutdown);
|
|
95750
|
+
process.on("SIGTERM", shutdown);
|
|
95751
|
+
});
|
|
95752
|
+
} catch (error) {
|
|
95753
|
+
if (error instanceof Error)
|
|
95754
|
+
console.error(error.message);
|
|
95755
|
+
process.exit(1);
|
|
95756
|
+
}
|
|
95757
|
+
}
|
|
95758
|
+
function addCommonOptions(cmd) {
|
|
95759
|
+
return cmd.option("--listen <host:port>", "Local proxy listen address (required)").option("--target <host:port>", "Upstream DB target (optional when config provides host/port)").option("--events <path>", "Event JSONL path", join32(".dbcli", "proxy", "events.jsonl")).option("--slow-ms <number>", "Threshold (ms); queries at/above it get slow:true in the event + a terminal warning", "1000").option("--redact <mode>", "SQL redaction: none | literals", "none").option("--format <format>", "Runtime status output: text | json", "text");
|
|
95760
|
+
}
|
|
95761
|
+
var proxyCommand = new Command().name("proxy").description("Local development observability proxy for MySQL/MariaDB/PostgreSQL (observe-only)");
|
|
95762
|
+
proxyCommand.enablePositionalOptions();
|
|
95763
|
+
for (const engine of SUPPORTED) {
|
|
95764
|
+
addCommonOptions(proxyCommand.command(engine).description(`Proxy a ${engine} connection`)).action(async (options, command) => {
|
|
95765
|
+
await runProxy(engine, options, command);
|
|
95766
|
+
});
|
|
95767
|
+
}
|
|
95768
|
+
var ANALYZE_FORMATS = ["json", "text"];
|
|
95769
|
+
function parseNonNegInt(value, flag, fallback) {
|
|
95770
|
+
if (value === undefined)
|
|
95771
|
+
return fallback;
|
|
95772
|
+
const n = Number(value);
|
|
95773
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
95774
|
+
throw new Error(`Invalid --${flag} "${value}". Expected a non-negative integer`);
|
|
95775
|
+
}
|
|
95776
|
+
return n;
|
|
95777
|
+
}
|
|
95778
|
+
async function runAnalyze(options) {
|
|
95779
|
+
try {
|
|
95780
|
+
const format = options.format ?? "json";
|
|
95781
|
+
validateFormat(format, ANALYZE_FORMATS, "proxy analyze");
|
|
95782
|
+
const top = parseNonNegInt(options.top, "top", 20);
|
|
95783
|
+
const slowMs = parseNonNegInt(options.slowMs, "slow-ms", 1000);
|
|
95784
|
+
const nPlusOne = parseNonNegInt(options.nPlusOne, "n-plus-one", 10);
|
|
95785
|
+
const eventsPath = options.events ?? join32(".dbcli", "proxy", "events.jsonl");
|
|
95786
|
+
const { events, malformedLines, files } = await readEvents(eventsPath, {
|
|
95787
|
+
includeRotated: options.includeRotated !== false
|
|
95788
|
+
});
|
|
95789
|
+
if (files.length === 0) {
|
|
95790
|
+
throw new Error(`no events found at ${eventsPath}; run 'dbcli proxy <engine>' first`);
|
|
95791
|
+
}
|
|
95792
|
+
const report = analyzeEvents(events, {
|
|
95793
|
+
slowMs,
|
|
95794
|
+
top,
|
|
95795
|
+
nPlusOne,
|
|
95796
|
+
sourceFiles: files,
|
|
95797
|
+
malformedLines
|
|
95798
|
+
});
|
|
95799
|
+
if (format === "text") {
|
|
95800
|
+
process.stdout.write(renderAnalysisText(report, top) + `
|
|
95801
|
+
`);
|
|
95802
|
+
} else {
|
|
95803
|
+
process.stdout.write(JSON.stringify(report, null, 2) + `
|
|
95804
|
+
`);
|
|
95805
|
+
}
|
|
95806
|
+
} catch (error) {
|
|
95807
|
+
if (error instanceof Error)
|
|
95808
|
+
console.error(error.message);
|
|
95809
|
+
process.exit(1);
|
|
95810
|
+
}
|
|
95811
|
+
}
|
|
95812
|
+
proxyCommand.command("analyze").description("Analyze a proxy event log offline (no DB connection)").option("--events <path>", "Event JSONL path", join32(".dbcli", "proxy", "events.jsonl")).option("--format <format>", "Output format: json | text", "json").option("--top <number>", "Rows shown in text + suggestedCommands depth", "20").option("--slow-ms <number>", "Slow-query threshold (ms) for slowCount", "1000").option("--n-plus-one <number>", "Min repeats per (session,fingerprint) to flag N+1", "10").option("--no-include-rotated", "Do not merge the rotated <events>.1 segment").action(async (options) => {
|
|
95813
|
+
await runAnalyze(options);
|
|
95814
|
+
});
|
|
95815
|
+
addCommonOptions(proxyCommand).action(async (options, command) => {
|
|
95816
|
+
await runProxy(null, options, command);
|
|
95817
|
+
});
|
|
95818
|
+
|
|
94159
95819
|
// src/cli.ts
|
|
94160
95820
|
init_config();
|
|
94161
|
-
import { join as
|
|
95821
|
+
import { join as join33 } from "path";
|
|
94162
95822
|
var _bgVersionCheckResult;
|
|
94163
95823
|
function shouldSkipBackgroundChecks() {
|
|
94164
95824
|
return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
|
|
94165
95825
|
}
|
|
94166
|
-
var program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--use <connection>", "Use a specific named connection (v2 config)");
|
|
95826
|
+
var program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--use <connection>", "Use a specific named connection (v2 config)").enablePositionalOptions();
|
|
94167
95827
|
program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
94168
95828
|
const opts = thisCommand.opts();
|
|
94169
95829
|
const useConnection = opts.use;
|
|
@@ -94187,7 +95847,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
|
94187
95847
|
try {
|
|
94188
95848
|
let cache = null;
|
|
94189
95849
|
try {
|
|
94190
|
-
const cacheFile = Bun.file(
|
|
95850
|
+
const cacheFile = Bun.file(join33(configPath, "version-check.json"));
|
|
94191
95851
|
if (await cacheFile.exists()) {
|
|
94192
95852
|
cache = await cacheFile.json();
|
|
94193
95853
|
}
|
|
@@ -94307,6 +95967,9 @@ program2.addCommand(migrateCommand);
|
|
|
94307
95967
|
program2.addCommand(useCommand);
|
|
94308
95968
|
program2.addCommand(queriesCommand);
|
|
94309
95969
|
program2.addCommand(explainCommand);
|
|
95970
|
+
program2.addCommand(snapshotCommand);
|
|
95971
|
+
program2.addCommand(assertCommand);
|
|
95972
|
+
program2.addCommand(proxyCommand);
|
|
94310
95973
|
if (!process.argv.slice(2).length) {
|
|
94311
95974
|
program2.outputHelp();
|
|
94312
95975
|
}
|