@carllee1983/dbcli 1.19.1 → 1.20.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 +29 -0
- package/README.md +42 -0
- package/README.zh-TW.md +40 -0
- package/assets/SKILL.md +39 -0
- package/assets/SKILL.zh-TW.md +392 -0
- package/assets/reference.md +87 -0
- package/dist/cli.mjs +2302 -239
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -2161,6 +2161,38 @@ var init_messages = __esm(() => {
|
|
|
2161
2161
|
snippet_not_found: "Snippet not found: {name}",
|
|
2162
2162
|
dry_run_header: "Dry-run preview (no execution):"
|
|
2163
2163
|
},
|
|
2164
|
+
audit: {
|
|
2165
|
+
description: "Inspect, look up, clear, or check the health of the audit log",
|
|
2166
|
+
tail: {
|
|
2167
|
+
description: "Show recent audit entries (default: last 10 from current connection)"
|
|
2168
|
+
},
|
|
2169
|
+
show: {
|
|
2170
|
+
description: "Look up a single audit entry by id (UUID prefix >=4) or --recovery-ref"
|
|
2171
|
+
},
|
|
2172
|
+
clear: {
|
|
2173
|
+
description: "Delete audit log files (.jsonl + .jsonl.1) for the current connection",
|
|
2174
|
+
prompt_header: "About to clear audit log for connection '{conn}':",
|
|
2175
|
+
prompt_file_line: " {file} \u2014 {entries} entries, {size}",
|
|
2176
|
+
prompt_continue: "Continue? [y/N] ",
|
|
2177
|
+
requires_tty_or_yes: "Cannot prompt for confirmation in non-interactive session. Use --yes to clear without prompt.",
|
|
2178
|
+
summary_cleared: "Cleared {count} entries from '{conn}'.",
|
|
2179
|
+
summary_nothing: "Nothing to clear.",
|
|
2180
|
+
summary_failed: "Failed to clear: {message}."
|
|
2181
|
+
},
|
|
2182
|
+
health: {
|
|
2183
|
+
description: "Show AuditLogger health snapshot (size, entries, lock state, last write)"
|
|
2184
|
+
},
|
|
2185
|
+
disabled_hint: "Audit is disabled (audit.enabled = false in .dbcli). Use 'dbcli audit health' for details.",
|
|
2186
|
+
no_entries: "No audit entries.",
|
|
2187
|
+
show_no_match: "No audit entry matches '{prefix}'.",
|
|
2188
|
+
show_ambiguous: "Ambiguous prefix '{prefix}': matches {count} entries. Please use a longer prefix.",
|
|
2189
|
+
show_prefix_too_short: "Prefix must be at least 4 characters.",
|
|
2190
|
+
show_recovery_no_match: "No audit entry has recovery_ref '{ref}'.",
|
|
2191
|
+
show_recovery_ambiguous: "Multiple entries reference recovery_ref '{ref}': matches {count}. This should be rare; inspect file directly.",
|
|
2192
|
+
show_mutex_violation: "Provide either <id> argument or --recovery-ref, not both.",
|
|
2193
|
+
n_capped_warning: "--n value {requested} exceeds max {max}; capped to {max}.",
|
|
2194
|
+
n_must_be_positive: "--n must be a positive integer."
|
|
2195
|
+
},
|
|
2164
2196
|
queries: {
|
|
2165
2197
|
description: "Manage saved query snippets",
|
|
2166
2198
|
list_description: "List saved snippets",
|
|
@@ -2411,6 +2443,38 @@ var init_messages2 = __esm(() => {
|
|
|
2411
2443
|
snippet_not_found: "\u627E\u4E0D\u5230\u7247\u6BB5\uFF1A{name}",
|
|
2412
2444
|
dry_run_header: "Dry-run \u9810\u89BD\uFF08\u4E0D\u6703\u57F7\u884C\uFF09\uFF1A"
|
|
2413
2445
|
},
|
|
2446
|
+
audit: {
|
|
2447
|
+
description: "\u6AA2\u8996\u3001\u67E5\u8A62\u3001\u6E05\u7A7A\u6216\u6AA2\u67E5 audit log \u5065\u5EB7\u72C0\u614B",
|
|
2448
|
+
tail: {
|
|
2449
|
+
description: "\u986F\u793A\u6700\u8FD1\u7684 audit \u7D00\u9304\uFF08\u9810\u8A2D\uFF1A\u7576\u524D\u9023\u7DDA\u6700\u5F8C 10 \u7B46\uFF09"
|
|
2450
|
+
},
|
|
2451
|
+
show: {
|
|
2452
|
+
description: "\u4F9D id\uFF08UUID \u524D\u7DB4 \u22654 \u78BC\uFF09\u6216 --recovery-ref \u67E5\u8A62\u55AE\u7B46 audit \u7D00\u9304"
|
|
2453
|
+
},
|
|
2454
|
+
clear: {
|
|
2455
|
+
description: "\u522A\u9664\u7576\u524D\u9023\u7DDA\u7684 audit log \u6A94\u6848\uFF08.jsonl \u8207 .jsonl.1\uFF09",
|
|
2456
|
+
prompt_header: "\u5373\u5C07\u6E05\u7A7A\u9023\u7DDA '{conn}' \u7684 audit log\uFF1A",
|
|
2457
|
+
prompt_file_line: " {file} \u2014 {entries} \u7B46\uFF0C{size}",
|
|
2458
|
+
prompt_continue: "\u78BA\u8A8D\u57F7\u884C\uFF1F[y/N] ",
|
|
2459
|
+
requires_tty_or_yes: "\u975E\u4E92\u52D5 session \u7121\u6CD5\u986F\u793A\u78BA\u8A8D\u63D0\u793A\u3002\u8ACB\u4F7F\u7528 --yes \u76F4\u63A5\u6E05\u7A7A\u3002",
|
|
2460
|
+
summary_cleared: "\u5DF2\u5F9E '{conn}' \u6E05\u9664 {count} \u7B46 audit \u7D00\u9304\u3002",
|
|
2461
|
+
summary_nothing: "\u6C92\u6709\u53EF\u6E05\u9664\u7684\u5167\u5BB9\u3002",
|
|
2462
|
+
summary_failed: "\u6E05\u9664\u5931\u6557\uFF1A{message}\u3002"
|
|
2463
|
+
},
|
|
2464
|
+
health: {
|
|
2465
|
+
description: "\u986F\u793A AuditLogger \u5065\u5EB7\u72C0\u614B\uFF08\u6A94\u6848\u5927\u5C0F\u3001\u7B46\u6578\u3001lock \u72C0\u614B\u3001\u6700\u5F8C\u5BEB\u5165\u7D50\u679C\uFF09"
|
|
2466
|
+
},
|
|
2467
|
+
disabled_hint: "Audit \u5DF2\u505C\u7528\uFF08.dbcli \u4E2D audit.enabled = false\uFF09\u3002\u57F7\u884C `dbcli audit health` \u67E5\u770B\u8A73\u60C5\u3002",
|
|
2468
|
+
no_entries: "\u6C92\u6709 audit \u7D00\u9304\u3002",
|
|
2469
|
+
show_no_match: "\u627E\u4E0D\u5230\u7B26\u5408 '{prefix}' \u7684 audit \u7D00\u9304\u3002",
|
|
2470
|
+
show_ambiguous: "\u524D\u7DB4 '{prefix}' \u4E0D\u5920\u7CBE\u78BA\uFF1A\u5339\u914D\u5230 {count} \u7B46\u7D00\u9304\uFF0C\u8ACB\u6539\u7528\u66F4\u9577\u7684\u524D\u7DB4\u3002",
|
|
2471
|
+
show_prefix_too_short: "\u524D\u7DB4\u81F3\u5C11\u9700 4 \u500B\u5B57\u5143\u3002",
|
|
2472
|
+
show_recovery_no_match: "\u6C92\u6709\u4EFB\u4F55 audit \u7D00\u9304\u7684 recovery_ref \u70BA '{ref}'\u3002",
|
|
2473
|
+
show_recovery_ambiguous: "\u6709 {count} \u7B46\u7D00\u9304\u7684 recovery_ref \u70BA '{ref}'\uFF0C\u9019\u61C9\u5C6C\u7F55\u898B\u60C5\u6CC1\uFF1B\u8ACB\u76F4\u63A5\u6AA2\u8996\u6A94\u6848\u3002",
|
|
2474
|
+
show_mutex_violation: "\u8ACB\u64C7\u4E00\u63D0\u4F9B <id> \u53C3\u6578\u6216 --recovery-ref\uFF0C\u5169\u8005\u4E0D\u53EF\u540C\u6642\u4F7F\u7528\u3002",
|
|
2475
|
+
n_capped_warning: "--n \u503C {requested} \u8D85\u904E\u4E0A\u9650 {max}\uFF0C\u5DF2\u81EA\u52D5\u7E2E\u6E1B\u70BA {max}\u3002",
|
|
2476
|
+
n_must_be_positive: "--n \u5FC5\u9808\u70BA\u6B63\u6574\u6578\u3002"
|
|
2477
|
+
},
|
|
2414
2478
|
queries: {
|
|
2415
2479
|
description: "\u7BA1\u7406\u5DF2\u4FDD\u5B58\u7684\u67E5\u8A62\u7247\u6BB5",
|
|
2416
2480
|
list_description: "\u5217\u51FA\u6240\u6709\u7247\u6BB5",
|
|
@@ -6776,7 +6840,9 @@ __export(exports_validation, {
|
|
|
6776
6840
|
DbcliConfigV2Schema: () => DbcliConfigV2Schema,
|
|
6777
6841
|
DbcliConfigSchema: () => DbcliConfigSchema,
|
|
6778
6842
|
ConnectionConfigSchema: () => ConnectionConfigSchema,
|
|
6779
|
-
BlacklistConfigSchema: () => BlacklistConfigSchema
|
|
6843
|
+
BlacklistConfigSchema: () => BlacklistConfigSchema,
|
|
6844
|
+
AuditRotationConfigSchema: () => AuditRotationConfigSchema,
|
|
6845
|
+
AuditConfigSchema: () => AuditConfigSchema
|
|
6780
6846
|
});
|
|
6781
6847
|
function validateFormat(value, allowedFormats, commandName) {
|
|
6782
6848
|
if (!allowedFormats.includes(value)) {
|
|
@@ -6784,7 +6850,7 @@ function validateFormat(value, allowedFormats, commandName) {
|
|
|
6784
6850
|
throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
|
|
6785
6851
|
}
|
|
6786
6852
|
}
|
|
6787
|
-
var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, MetadataSchema, BlacklistConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
|
|
6853
|
+
var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, MetadataSchema, BlacklistConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
|
|
6788
6854
|
var init_validation = __esm(() => {
|
|
6789
6855
|
init_zod();
|
|
6790
6856
|
EnvRefSchema = exports_external.object({
|
|
@@ -6850,12 +6916,24 @@ var init_validation = __esm(() => {
|
|
|
6850
6916
|
tables: exports_external.array(exports_external.string()).default([]),
|
|
6851
6917
|
columns: exports_external.record(exports_external.array(exports_external.string())).default({})
|
|
6852
6918
|
}).optional().default({ tables: [], columns: {} });
|
|
6919
|
+
AuditRotationConfigSchema = exports_external.object({
|
|
6920
|
+
max_bytes: exports_external.number().int().positive().default(10485760),
|
|
6921
|
+
max_entries: exports_external.number().int().positive().default(1000)
|
|
6922
|
+
}).optional().default({ max_bytes: 10485760, max_entries: 1000 });
|
|
6923
|
+
AuditConfigSchema = exports_external.object({
|
|
6924
|
+
enabled: exports_external.boolean().default(true),
|
|
6925
|
+
rotation: AuditRotationConfigSchema
|
|
6926
|
+
}).optional().default({
|
|
6927
|
+
enabled: true,
|
|
6928
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
6929
|
+
});
|
|
6853
6930
|
DbcliConfigSchema = exports_external.object({
|
|
6854
6931
|
connection: ConnectionConfigSchema,
|
|
6855
6932
|
permission: PermissionSchema,
|
|
6856
6933
|
schema: exports_external.record(exports_external.any()).optional().default({}),
|
|
6857
6934
|
metadata: MetadataSchema,
|
|
6858
|
-
blacklist: BlacklistConfigSchema
|
|
6935
|
+
blacklist: BlacklistConfigSchema,
|
|
6936
|
+
audit: AuditConfigSchema
|
|
6859
6937
|
});
|
|
6860
6938
|
SqlNamedConnectionSchema = SqlConnectionConfigSchema.extend({
|
|
6861
6939
|
permission: PermissionSchema,
|
|
@@ -6888,7 +6966,8 @@ var init_validation = __esm(() => {
|
|
|
6888
6966
|
schema: exports_external.record(exports_external.any()).optional().default({}),
|
|
6889
6967
|
schemas: exports_external.record(exports_external.record(exports_external.any())).optional().default({}),
|
|
6890
6968
|
metadata: MetadataSchema,
|
|
6891
|
-
blacklist: BlacklistConfigSchema
|
|
6969
|
+
blacklist: BlacklistConfigSchema,
|
|
6970
|
+
audit: AuditConfigSchema
|
|
6892
6971
|
}).refine((config) => (config.default in config.connections), {
|
|
6893
6972
|
message: "Default connection must exist in connections",
|
|
6894
6973
|
path: ["default"]
|
|
@@ -8043,6 +8122,13 @@ var init_schema_loader = __esm(() => {
|
|
|
8043
8122
|
});
|
|
8044
8123
|
|
|
8045
8124
|
// src/core/config.ts
|
|
8125
|
+
var exports_config = {};
|
|
8126
|
+
__export(exports_config, {
|
|
8127
|
+
setGlobalConnectionName: () => setGlobalConnectionName,
|
|
8128
|
+
getSchemaIsolationConnectionName: () => getSchemaIsolationConnectionName,
|
|
8129
|
+
getGlobalConnectionName: () => getGlobalConnectionName,
|
|
8130
|
+
configModule: () => configModule
|
|
8131
|
+
});
|
|
8046
8132
|
import { join as join7 } from "path";
|
|
8047
8133
|
function setGlobalConnectionName(name) {
|
|
8048
8134
|
_globalConnectionName = name;
|
|
@@ -8131,7 +8217,11 @@ var init_config = __esm(() => {
|
|
|
8131
8217
|
metadata: {
|
|
8132
8218
|
version: "1.0"
|
|
8133
8219
|
},
|
|
8134
|
-
blacklist: { tables: [], columns: {} }
|
|
8220
|
+
blacklist: { tables: [], columns: {} },
|
|
8221
|
+
audit: {
|
|
8222
|
+
enabled: true,
|
|
8223
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
8224
|
+
}
|
|
8135
8225
|
};
|
|
8136
8226
|
configModule = {
|
|
8137
8227
|
async read(path, connectionName) {
|
|
@@ -8201,7 +8291,8 @@ var init_config = __esm(() => {
|
|
|
8201
8291
|
permission: resolved.permission,
|
|
8202
8292
|
schema,
|
|
8203
8293
|
metadata: v2Config.metadata,
|
|
8204
|
-
blacklist: v2Config.blacklist
|
|
8294
|
+
blacklist: v2Config.blacklist,
|
|
8295
|
+
audit: v2Config.audit
|
|
8205
8296
|
});
|
|
8206
8297
|
}
|
|
8207
8298
|
const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
|
|
@@ -20696,13 +20787,21 @@ var init_capabilities = __esm(() => {
|
|
|
20696
20787
|
"completion",
|
|
20697
20788
|
"upgrade",
|
|
20698
20789
|
"recover",
|
|
20699
|
-
"skill"
|
|
20790
|
+
"skill",
|
|
20791
|
+
"auditTail",
|
|
20792
|
+
"auditShow",
|
|
20793
|
+
"auditClear",
|
|
20794
|
+
"auditHealth"
|
|
20700
20795
|
]);
|
|
20701
20796
|
ENGINE_INDEPENDENT = {
|
|
20702
20797
|
completion: cap("not-applicable", "none", "Shell completion is engine-independent."),
|
|
20703
20798
|
upgrade: cap("not-applicable", "local-write", "Package update checks are engine-independent."),
|
|
20704
20799
|
recover: cap("not-applicable", "dry-run", "Recovery operates on saved envelopes and gated command steps."),
|
|
20705
|
-
skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent.")
|
|
20800
|
+
skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent."),
|
|
20801
|
+
auditTail: cap("supported", "readonly", "Reads JSONL audit entries; never writes to engines."),
|
|
20802
|
+
auditShow: cap("supported", "readonly", "Looks up a single audit entry by id prefix or recovery_ref."),
|
|
20803
|
+
auditHealth: cap("supported", "readonly", "Renders AuditLogger.getHealth() snapshot."),
|
|
20804
|
+
auditClear: cap("supported", "local-write", "Removes <conn>.jsonl + .jsonl.1 from local disk; never touches DB.")
|
|
20706
20805
|
};
|
|
20707
20806
|
SQL_BASE = {
|
|
20708
20807
|
init: cap("supported", "interactive", "SQL connection initialization is supported."),
|
|
@@ -25419,6 +25518,7 @@ var init_render_markdown = __esm(() => {
|
|
|
25419
25518
|
// src/core/recovery/last-envelope.ts
|
|
25420
25519
|
import { writeFile, readFile, rename, mkdir as mkdir2, stat } from "fs/promises";
|
|
25421
25520
|
import { dirname, join as join10 } from "path";
|
|
25521
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
25422
25522
|
function sanitizeCommandSummary(argv) {
|
|
25423
25523
|
if (argv.length === 0)
|
|
25424
25524
|
return "<unknown>";
|
|
@@ -25459,11 +25559,13 @@ function sanitizeCommandSummary(argv) {
|
|
|
25459
25559
|
}
|
|
25460
25560
|
return out.join(" ");
|
|
25461
25561
|
}
|
|
25462
|
-
async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date) {
|
|
25562
|
+
async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID2(), auditRef) {
|
|
25463
25563
|
const target = join10(cwd, LAST_ENVELOPE_PATH);
|
|
25464
25564
|
const tmp = `${target}.tmp`;
|
|
25465
25565
|
const payload = {
|
|
25466
25566
|
schemaVersion: 1,
|
|
25567
|
+
id,
|
|
25568
|
+
...auditRef !== undefined && { audit_ref: auditRef },
|
|
25467
25569
|
savedAt: now().toISOString(),
|
|
25468
25570
|
command: sanitizeCommandSummary(argv),
|
|
25469
25571
|
cwd,
|
|
@@ -25526,11 +25628,13 @@ var init_last_envelope = __esm(() => {
|
|
|
25526
25628
|
// src/core/recovery/emit.ts
|
|
25527
25629
|
import { writeFileSync as writeFileSync2, mkdirSync, renameSync } from "fs";
|
|
25528
25630
|
import { dirname as dirname2, join as join11 } from "path";
|
|
25631
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
25529
25632
|
function emitRecoveryEnvelope(error, ctx, options = {}) {
|
|
25530
25633
|
const envelope = classifyError(error, ctx);
|
|
25531
25634
|
const cwd = options.cwd ?? process.cwd();
|
|
25532
25635
|
const argv = options.argv ?? buildArgvFromProcess();
|
|
25533
|
-
|
|
25636
|
+
const envelopeId = options.envelopeId ?? randomUUID3();
|
|
25637
|
+
writeLastEnvelopeSync(cwd, envelope, argv, envelopeId, options.auditRef);
|
|
25534
25638
|
process.stdout.write(renderJson(envelope, { brief: options.brief === true }) + `
|
|
25535
25639
|
`);
|
|
25536
25640
|
process.exit(options.exitCode ?? 1);
|
|
@@ -25539,11 +25643,13 @@ function buildArgvFromProcess() {
|
|
|
25539
25643
|
const userArgs = process.argv.slice(2);
|
|
25540
25644
|
return ["dbcli", ...userArgs];
|
|
25541
25645
|
}
|
|
25542
|
-
function writeLastEnvelopeSync(cwd, envelope, argv) {
|
|
25646
|
+
function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
|
|
25543
25647
|
const target = join11(cwd, LAST_ENVELOPE_PATH);
|
|
25544
25648
|
const tmp = `${target}.tmp`;
|
|
25545
25649
|
const payload = {
|
|
25546
25650
|
schemaVersion: 1,
|
|
25651
|
+
id,
|
|
25652
|
+
...auditRef !== undefined && { audit_ref: auditRef },
|
|
25547
25653
|
savedAt: new Date().toISOString(),
|
|
25548
25654
|
command: sanitizeCommandSummary(argv),
|
|
25549
25655
|
cwd,
|
|
@@ -26518,6 +26624,507 @@ function levenshteinDistance(a, b) {
|
|
|
26518
26624
|
return dp[lenB]?.[lenA] ?? 0;
|
|
26519
26625
|
}
|
|
26520
26626
|
|
|
26627
|
+
// src/utils/engine-hints.ts
|
|
26628
|
+
var exports_engine_hints = {};
|
|
26629
|
+
__export(exports_engine_hints, {
|
|
26630
|
+
getOperationTarget: () => getOperationTarget,
|
|
26631
|
+
extractTableName: () => extractTableName
|
|
26632
|
+
});
|
|
26633
|
+
function extractTableName(sql) {
|
|
26634
|
+
const match = sql.match(TABLE_NAME_RE);
|
|
26635
|
+
return match ? match[1] ?? null : null;
|
|
26636
|
+
}
|
|
26637
|
+
function getOperationTarget(system, command, options, sql) {
|
|
26638
|
+
if (system === "mongodb") {
|
|
26639
|
+
return options.collection || "<unknown-collection>";
|
|
26640
|
+
}
|
|
26641
|
+
if (system === "elasticsearch") {
|
|
26642
|
+
return options.index || options.collection || "<unknown-index>";
|
|
26643
|
+
}
|
|
26644
|
+
if (system === "redis") {
|
|
26645
|
+
return options.table || options.key || "<unknown-key>";
|
|
26646
|
+
}
|
|
26647
|
+
if (sql) {
|
|
26648
|
+
const table = extractTableName(sql);
|
|
26649
|
+
if (table)
|
|
26650
|
+
return table;
|
|
26651
|
+
}
|
|
26652
|
+
return options.table || "<unknown-target>";
|
|
26653
|
+
}
|
|
26654
|
+
var TABLE_NAME_RE;
|
|
26655
|
+
var init_engine_hints = __esm(() => {
|
|
26656
|
+
TABLE_NAME_RE = /\b(?:FROM|INTO|UPDATE)\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i;
|
|
26657
|
+
});
|
|
26658
|
+
|
|
26659
|
+
// src/core/audit/lock.ts
|
|
26660
|
+
import { hostname } from "os";
|
|
26661
|
+
import { dirname as dirname4 } from "path";
|
|
26662
|
+
import { mkdir as mkdir3, open as open2, rm } from "fs/promises";
|
|
26663
|
+
|
|
26664
|
+
class AuditLockManager {
|
|
26665
|
+
auditFilePath;
|
|
26666
|
+
lockPath;
|
|
26667
|
+
lockAcquiredAt = null;
|
|
26668
|
+
constructor(auditFilePath) {
|
|
26669
|
+
this.auditFilePath = auditFilePath;
|
|
26670
|
+
this.lockPath = `${auditFilePath}.lock`;
|
|
26671
|
+
}
|
|
26672
|
+
getLockfilePath() {
|
|
26673
|
+
return this.lockPath;
|
|
26674
|
+
}
|
|
26675
|
+
isLockHeld() {
|
|
26676
|
+
return this.lockAcquiredAt !== null;
|
|
26677
|
+
}
|
|
26678
|
+
async acquireLock(operationName = "audit-write") {
|
|
26679
|
+
const startTime = Date.now();
|
|
26680
|
+
let backoffMs = LOCK_BACKOFF_START_MS;
|
|
26681
|
+
while (true) {
|
|
26682
|
+
const elapsed = Date.now() - startTime;
|
|
26683
|
+
if (elapsed > LOCK_RETRY_BUDGET_MS) {
|
|
26684
|
+
return false;
|
|
26685
|
+
}
|
|
26686
|
+
if (await this.tryAcquireLock(operationName)) {
|
|
26687
|
+
this.lockAcquiredAt = Date.now();
|
|
26688
|
+
return true;
|
|
26689
|
+
}
|
|
26690
|
+
const waitTime = Math.min(backoffMs, LOCK_BACKOFF_MAX_MS);
|
|
26691
|
+
await new Promise((resolve4) => setTimeout(resolve4, waitTime));
|
|
26692
|
+
backoffMs = Math.min(backoffMs * 1.5, LOCK_BACKOFF_MAX_MS);
|
|
26693
|
+
}
|
|
26694
|
+
}
|
|
26695
|
+
async releaseLock() {
|
|
26696
|
+
if (!this.lockAcquiredAt)
|
|
26697
|
+
return false;
|
|
26698
|
+
try {
|
|
26699
|
+
await rm(this.lockPath, { force: true });
|
|
26700
|
+
this.lockAcquiredAt = null;
|
|
26701
|
+
return true;
|
|
26702
|
+
} catch {
|
|
26703
|
+
this.lockAcquiredAt = null;
|
|
26704
|
+
return false;
|
|
26705
|
+
}
|
|
26706
|
+
}
|
|
26707
|
+
async withLock(operation, operationName = "audit-write") {
|
|
26708
|
+
const acquired = await this.acquireLock(operationName);
|
|
26709
|
+
if (!acquired) {
|
|
26710
|
+
return { skipped: "lock-budget-exhausted" };
|
|
26711
|
+
}
|
|
26712
|
+
try {
|
|
26713
|
+
return await operation();
|
|
26714
|
+
} finally {
|
|
26715
|
+
await this.releaseLock();
|
|
26716
|
+
}
|
|
26717
|
+
}
|
|
26718
|
+
async tryAcquireLock(operationName) {
|
|
26719
|
+
try {
|
|
26720
|
+
await mkdir3(dirname4(this.lockPath), { recursive: true });
|
|
26721
|
+
const lockFile = Bun.file(this.lockPath);
|
|
26722
|
+
if (await lockFile.exists()) {
|
|
26723
|
+
const lockContent = await lockFile.json();
|
|
26724
|
+
const lockAge = Date.now() - lockContent.timestamp;
|
|
26725
|
+
const staleLockThresholdMs = LOCK_RETRY_BUDGET_MS * STALE_LOCK_MULTIPLIER;
|
|
26726
|
+
if (lockAge > staleLockThresholdMs) {
|
|
26727
|
+
await rm(this.lockPath, { force: true });
|
|
26728
|
+
} else {
|
|
26729
|
+
return false;
|
|
26730
|
+
}
|
|
26731
|
+
}
|
|
26732
|
+
const lockData = {
|
|
26733
|
+
pid: process.pid,
|
|
26734
|
+
operation: operationName,
|
|
26735
|
+
timestamp: Date.now(),
|
|
26736
|
+
hostname: hostname()
|
|
26737
|
+
};
|
|
26738
|
+
const handle = await open2(this.lockPath, "wx");
|
|
26739
|
+
try {
|
|
26740
|
+
await handle.writeFile(JSON.stringify(lockData), "utf8");
|
|
26741
|
+
} finally {
|
|
26742
|
+
await handle.close();
|
|
26743
|
+
}
|
|
26744
|
+
return true;
|
|
26745
|
+
} catch {
|
|
26746
|
+
return false;
|
|
26747
|
+
}
|
|
26748
|
+
}
|
|
26749
|
+
}
|
|
26750
|
+
var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS = 50, STALE_LOCK_MULTIPLIER = 10;
|
|
26751
|
+
var init_lock = () => {};
|
|
26752
|
+
|
|
26753
|
+
// src/core/audit/rotation.ts
|
|
26754
|
+
import { rename as rename2 } from "fs/promises";
|
|
26755
|
+
function shouldRotate(stats, thresholds, nextLineByteLength) {
|
|
26756
|
+
const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
|
|
26757
|
+
const entriesAfter = stats.currentEntryCount + 1;
|
|
26758
|
+
return bytesAfter >= thresholds.maxBytes || entriesAfter > thresholds.maxEntries;
|
|
26759
|
+
}
|
|
26760
|
+
async function rotate(currentPath, previousPath) {
|
|
26761
|
+
try {
|
|
26762
|
+
await rename2(currentPath, previousPath);
|
|
26763
|
+
} catch {}
|
|
26764
|
+
}
|
|
26765
|
+
var init_rotation = () => {};
|
|
26766
|
+
|
|
26767
|
+
// src/core/audit/logger.ts
|
|
26768
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
26769
|
+
import { join as join13 } from "path";
|
|
26770
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
26771
|
+
|
|
26772
|
+
class AuditLogger {
|
|
26773
|
+
auditDir;
|
|
26774
|
+
auditFilePath;
|
|
26775
|
+
previousFilePath;
|
|
26776
|
+
enabled;
|
|
26777
|
+
maxBytes;
|
|
26778
|
+
maxEntries;
|
|
26779
|
+
sessionIdService;
|
|
26780
|
+
lockManager;
|
|
26781
|
+
writerInitialized = false;
|
|
26782
|
+
currentSizeBytes = 0;
|
|
26783
|
+
currentEntryCount = 0;
|
|
26784
|
+
lastWrite = null;
|
|
26785
|
+
lastError = null;
|
|
26786
|
+
warnedOnceThisProcess = false;
|
|
26787
|
+
cachedSessionId = null;
|
|
26788
|
+
lastRotatedAt;
|
|
26789
|
+
lastRotatedPrevious;
|
|
26790
|
+
writeChain = Promise.resolve();
|
|
26791
|
+
constructor(opts) {
|
|
26792
|
+
this.enabled = opts.enabled;
|
|
26793
|
+
this.maxBytes = opts.rotation.maxBytes;
|
|
26794
|
+
this.maxEntries = opts.rotation.maxEntries;
|
|
26795
|
+
this.auditDir = join13(opts.storagePath, ".dbcli", "audit");
|
|
26796
|
+
this.auditFilePath = join13(this.auditDir, `${opts.connectionName}.jsonl`);
|
|
26797
|
+
this.previousFilePath = `${this.auditFilePath}.1`;
|
|
26798
|
+
this.sessionIdService = opts.sessionIdService;
|
|
26799
|
+
this.lockManager = opts.lockManager ?? new AuditLockManager(this.auditFilePath);
|
|
26800
|
+
}
|
|
26801
|
+
async write(entry) {
|
|
26802
|
+
const writeOp = this.writeChain.then(() => this.writeInternal(entry));
|
|
26803
|
+
this.writeChain = writeOp.then(() => {
|
|
26804
|
+
return;
|
|
26805
|
+
}, () => {
|
|
26806
|
+
return;
|
|
26807
|
+
});
|
|
26808
|
+
return writeOp;
|
|
26809
|
+
}
|
|
26810
|
+
async writeInternal(entry) {
|
|
26811
|
+
if (!this.enabled) {
|
|
26812
|
+
return { skipped: "disabled" };
|
|
26813
|
+
}
|
|
26814
|
+
try {
|
|
26815
|
+
const sessionId = await this.sessionIdService.resolve();
|
|
26816
|
+
this.cachedSessionId = sessionId;
|
|
26817
|
+
await mkdir4(this.auditDir, { recursive: true });
|
|
26818
|
+
if (!this.writerInitialized) {
|
|
26819
|
+
await this.syncCountersFromDisk();
|
|
26820
|
+
this.writerInitialized = true;
|
|
26821
|
+
}
|
|
26822
|
+
const id = randomUUID4();
|
|
26823
|
+
const ts = new Date().toISOString();
|
|
26824
|
+
const enriched = {
|
|
26825
|
+
...entry,
|
|
26826
|
+
id,
|
|
26827
|
+
ts,
|
|
26828
|
+
session_id: sessionId
|
|
26829
|
+
};
|
|
26830
|
+
const line = JSON.stringify(enriched) + `
|
|
26831
|
+
`;
|
|
26832
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
26833
|
+
const lockResult = await this.lockManager.withLock(async () => {
|
|
26834
|
+
let rotated = false;
|
|
26835
|
+
if (shouldRotate({
|
|
26836
|
+
currentSizeBytes: this.currentSizeBytes,
|
|
26837
|
+
currentEntryCount: this.currentEntryCount
|
|
26838
|
+
}, { maxBytes: this.maxBytes, maxEntries: this.maxEntries }, lineBytes)) {
|
|
26839
|
+
await rotate(this.auditFilePath, this.previousFilePath);
|
|
26840
|
+
this.lastRotatedAt = new Date().toISOString();
|
|
26841
|
+
this.lastRotatedPrevious = this.previousFilePath;
|
|
26842
|
+
this.currentSizeBytes = 0;
|
|
26843
|
+
this.currentEntryCount = 0;
|
|
26844
|
+
rotated = true;
|
|
26845
|
+
}
|
|
26846
|
+
await appendFile(this.auditFilePath, line, { encoding: "utf8" });
|
|
26847
|
+
this.currentSizeBytes += lineBytes;
|
|
26848
|
+
this.currentEntryCount += 1;
|
|
26849
|
+
return { rotated };
|
|
26850
|
+
}, "audit-write");
|
|
26851
|
+
if (lockResult !== null && typeof lockResult === "object" && "skipped" in lockResult) {
|
|
26852
|
+
this.handleFailure("lock-budget-exhausted");
|
|
26853
|
+
return { skipped: "lock-budget-exhausted" };
|
|
26854
|
+
}
|
|
26855
|
+
this.lastWrite = { ts, success: true };
|
|
26856
|
+
return { success: true, rotated: lockResult.rotated, id };
|
|
26857
|
+
} catch (err) {
|
|
26858
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26859
|
+
this.handleFailure(message);
|
|
26860
|
+
return { skipped: "write-failed", error: message };
|
|
26861
|
+
}
|
|
26862
|
+
}
|
|
26863
|
+
getHealth() {
|
|
26864
|
+
return {
|
|
26865
|
+
enabled: this.enabled,
|
|
26866
|
+
writerInitialized: this.writerInitialized,
|
|
26867
|
+
currentFile: this.auditFilePath,
|
|
26868
|
+
currentSizeBytes: this.currentSizeBytes,
|
|
26869
|
+
currentEntryCount: this.currentEntryCount,
|
|
26870
|
+
rotationUsage: {
|
|
26871
|
+
bytes: {
|
|
26872
|
+
current: this.currentSizeBytes,
|
|
26873
|
+
max: this.maxBytes,
|
|
26874
|
+
pct: this.maxBytes > 0 ? this.currentSizeBytes / this.maxBytes * 100 : 0
|
|
26875
|
+
},
|
|
26876
|
+
entries: {
|
|
26877
|
+
current: this.currentEntryCount,
|
|
26878
|
+
max: this.maxEntries,
|
|
26879
|
+
pct: this.maxEntries > 0 ? this.currentEntryCount / this.maxEntries * 100 : 0
|
|
26880
|
+
}
|
|
26881
|
+
},
|
|
26882
|
+
lock: { state: this.lockManager.isLockHeld() ? "held" : "free" },
|
|
26883
|
+
lastWrite: this.lastWrite,
|
|
26884
|
+
lastError: this.lastError,
|
|
26885
|
+
sessionId: this.cachedSessionId,
|
|
26886
|
+
rotation: {
|
|
26887
|
+
lastRotatedAt: this.lastRotatedAt,
|
|
26888
|
+
previousFile: this.lastRotatedPrevious
|
|
26889
|
+
}
|
|
26890
|
+
};
|
|
26891
|
+
}
|
|
26892
|
+
async syncCountersFromDisk() {
|
|
26893
|
+
try {
|
|
26894
|
+
const s = await stat3(this.auditFilePath);
|
|
26895
|
+
this.currentSizeBytes = s.size;
|
|
26896
|
+
const raw = await readFile2(this.auditFilePath, "utf8");
|
|
26897
|
+
this.currentEntryCount = raw.split(`
|
|
26898
|
+
`).filter(Boolean).length;
|
|
26899
|
+
} catch {
|
|
26900
|
+
this.currentSizeBytes = 0;
|
|
26901
|
+
this.currentEntryCount = 0;
|
|
26902
|
+
}
|
|
26903
|
+
}
|
|
26904
|
+
handleFailure(message) {
|
|
26905
|
+
const ts = new Date().toISOString();
|
|
26906
|
+
this.lastError = { ts, message };
|
|
26907
|
+
this.lastWrite = { ts, success: false, error: message };
|
|
26908
|
+
if (!this.warnedOnceThisProcess) {
|
|
26909
|
+
process.stderr.write(`${WARN_PREFIX} warning: audit write failed (${message}); subsequent failures suppressed this process.
|
|
26910
|
+
`);
|
|
26911
|
+
this.warnedOnceThisProcess = true;
|
|
26912
|
+
}
|
|
26913
|
+
}
|
|
26914
|
+
}
|
|
26915
|
+
var WARN_PREFIX = "[dbcli audit]";
|
|
26916
|
+
var init_logger = __esm(() => {
|
|
26917
|
+
init_lock();
|
|
26918
|
+
init_rotation();
|
|
26919
|
+
});
|
|
26920
|
+
|
|
26921
|
+
// src/core/audit/session-id.ts
|
|
26922
|
+
import { mkdir as mkdir5, readFile as readFile3, rename as rename3, stat as stat4, writeFile as writeFile2 } from "fs/promises";
|
|
26923
|
+
import { randomBytes } from "crypto";
|
|
26924
|
+
import { dirname as dirname5, join as join14 } from "path";
|
|
26925
|
+
function generateSessionId(pid, nowMs) {
|
|
26926
|
+
const random = randomBytes(3).toString("hex");
|
|
26927
|
+
return `${pid}-${nowMs}-${random}`;
|
|
26928
|
+
}
|
|
26929
|
+
async function readSessionIdFile(storagePath) {
|
|
26930
|
+
const target = join14(storagePath, LAST_SESSION_ID_RELATIVE);
|
|
26931
|
+
try {
|
|
26932
|
+
await stat4(target);
|
|
26933
|
+
} catch {
|
|
26934
|
+
return null;
|
|
26935
|
+
}
|
|
26936
|
+
try {
|
|
26937
|
+
const raw = await readFile3(target, "utf8");
|
|
26938
|
+
const parsed = JSON.parse(raw);
|
|
26939
|
+
if (parsed && typeof parsed.sessionId === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string") {
|
|
26940
|
+
return {
|
|
26941
|
+
sessionId: parsed.sessionId,
|
|
26942
|
+
pid: parsed.pid,
|
|
26943
|
+
createdAt: parsed.createdAt
|
|
26944
|
+
};
|
|
26945
|
+
}
|
|
26946
|
+
return null;
|
|
26947
|
+
} catch {
|
|
26948
|
+
return null;
|
|
26949
|
+
}
|
|
26950
|
+
}
|
|
26951
|
+
async function writeSessionIdFile(storagePath, payload) {
|
|
26952
|
+
const target = join14(storagePath, LAST_SESSION_ID_RELATIVE);
|
|
26953
|
+
const tmp = `${target}.tmp`;
|
|
26954
|
+
try {
|
|
26955
|
+
await mkdir5(dirname5(target), { recursive: true });
|
|
26956
|
+
await writeFile2(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
26957
|
+
await rename3(tmp, target);
|
|
26958
|
+
} catch {}
|
|
26959
|
+
}
|
|
26960
|
+
|
|
26961
|
+
class SessionIdService {
|
|
26962
|
+
storagePath;
|
|
26963
|
+
cached = null;
|
|
26964
|
+
constructor(storagePath) {
|
|
26965
|
+
this.storagePath = storagePath;
|
|
26966
|
+
}
|
|
26967
|
+
async resolve() {
|
|
26968
|
+
if (this.cached !== null)
|
|
26969
|
+
return this.cached;
|
|
26970
|
+
const fromEnv = process.env.DBCLI_SESSION_ID;
|
|
26971
|
+
if (fromEnv && fromEnv.length > 0) {
|
|
26972
|
+
this.cached = fromEnv;
|
|
26973
|
+
return fromEnv;
|
|
26974
|
+
}
|
|
26975
|
+
const saved = await readSessionIdFile(this.storagePath);
|
|
26976
|
+
if (saved && saved.pid === process.pid) {
|
|
26977
|
+
this.cached = saved.sessionId;
|
|
26978
|
+
return saved.sessionId;
|
|
26979
|
+
}
|
|
26980
|
+
const id = generateSessionId(process.pid, Date.now());
|
|
26981
|
+
await writeSessionIdFile(this.storagePath, {
|
|
26982
|
+
sessionId: id,
|
|
26983
|
+
pid: process.pid,
|
|
26984
|
+
createdAt: new Date().toISOString()
|
|
26985
|
+
});
|
|
26986
|
+
this.cached = id;
|
|
26987
|
+
return id;
|
|
26988
|
+
}
|
|
26989
|
+
reset() {
|
|
26990
|
+
this.cached = null;
|
|
26991
|
+
}
|
|
26992
|
+
}
|
|
26993
|
+
var LAST_SESSION_ID_RELATIVE = ".dbcli/last-session-id";
|
|
26994
|
+
var init_session_id = () => {};
|
|
26995
|
+
|
|
26996
|
+
// src/utils/redaction.ts
|
|
26997
|
+
function redactArgv(argv) {
|
|
26998
|
+
if (argv.length === 0)
|
|
26999
|
+
return "<unknown>";
|
|
27000
|
+
const out = [argv[0]];
|
|
27001
|
+
if (argv.length === 1)
|
|
27002
|
+
return out.join(" ");
|
|
27003
|
+
const sub = argv[1];
|
|
27004
|
+
out.push(sub);
|
|
27005
|
+
for (let i = 2;i < argv.length; i++) {
|
|
27006
|
+
const tok = argv[i];
|
|
27007
|
+
if (tok.startsWith("--")) {
|
|
27008
|
+
const [name, inlineValue] = tok.split("=");
|
|
27009
|
+
if (REDACTED_VALUE_FLAGS2.has(name)) {
|
|
27010
|
+
out.push(`${name} <redacted>`);
|
|
27011
|
+
if (inlineValue === undefined)
|
|
27012
|
+
i++;
|
|
27013
|
+
continue;
|
|
27014
|
+
}
|
|
27015
|
+
if (KEEP_VALUE_FLAGS2.has(name)) {
|
|
27016
|
+
if (inlineValue !== undefined) {
|
|
27017
|
+
out.push(tok);
|
|
27018
|
+
} else {
|
|
27019
|
+
out.push(name);
|
|
27020
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
27021
|
+
out.push(argv[++i]);
|
|
27022
|
+
}
|
|
27023
|
+
}
|
|
27024
|
+
continue;
|
|
27025
|
+
}
|
|
27026
|
+
out.push(tok);
|
|
27027
|
+
continue;
|
|
27028
|
+
}
|
|
27029
|
+
if (i === 2 && SQL_SUBCOMMANDS2.has(sub)) {
|
|
27030
|
+
out.push("<sql>");
|
|
27031
|
+
continue;
|
|
27032
|
+
}
|
|
27033
|
+
out.push(tok);
|
|
27034
|
+
}
|
|
27035
|
+
return out.join(" ");
|
|
27036
|
+
}
|
|
27037
|
+
function redactSensitive(text2) {
|
|
27038
|
+
return text2.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
|
|
27039
|
+
}
|
|
27040
|
+
function redactSql(sql) {
|
|
27041
|
+
const redacted = sql.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g, "'?'").replace(/\b\d+(\.\d+)?\b/g, "0");
|
|
27042
|
+
return redactSensitive(redacted);
|
|
27043
|
+
}
|
|
27044
|
+
var SQL_SUBCOMMANDS2, REDACTED_VALUE_FLAGS2, KEEP_VALUE_FLAGS2;
|
|
27045
|
+
var init_redaction = __esm(() => {
|
|
27046
|
+
SQL_SUBCOMMANDS2 = new Set(["query", "export"]);
|
|
27047
|
+
REDACTED_VALUE_FLAGS2 = new Set([
|
|
27048
|
+
"--where",
|
|
27049
|
+
"--set",
|
|
27050
|
+
"--data",
|
|
27051
|
+
"--param",
|
|
27052
|
+
"--param-file",
|
|
27053
|
+
"--config",
|
|
27054
|
+
"--use",
|
|
27055
|
+
"--password",
|
|
27056
|
+
"--token",
|
|
27057
|
+
"--secret"
|
|
27058
|
+
]);
|
|
27059
|
+
KEEP_VALUE_FLAGS2 = new Set(["--format", "--conn-name"]);
|
|
27060
|
+
});
|
|
27061
|
+
|
|
27062
|
+
// src/core/audit/integration-helper.ts
|
|
27063
|
+
async function getAuditLogger(config, configPath) {
|
|
27064
|
+
const storagePath = await resolveConfigStoragePath(configPath);
|
|
27065
|
+
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
27066
|
+
const key = `${storagePath}:${connName}`;
|
|
27067
|
+
if (!_loggers.has(key)) {
|
|
27068
|
+
if (!_sessionIdService) {
|
|
27069
|
+
_sessionIdService = new SessionIdService(storagePath);
|
|
27070
|
+
}
|
|
27071
|
+
_loggers.set(key, new AuditLogger({
|
|
27072
|
+
storagePath,
|
|
27073
|
+
connectionName: connName,
|
|
27074
|
+
enabled: config.audit?.enabled ?? true,
|
|
27075
|
+
rotation: {
|
|
27076
|
+
maxBytes: config.audit?.rotation?.max_bytes ?? 10485760,
|
|
27077
|
+
maxEntries: config.audit?.rotation?.max_entries ?? 1000
|
|
27078
|
+
},
|
|
27079
|
+
sessionIdService: _sessionIdService
|
|
27080
|
+
}));
|
|
27081
|
+
}
|
|
27082
|
+
return _loggers.get(key);
|
|
27083
|
+
}
|
|
27084
|
+
async function writeAuditEntry(config, commandName, options, outcome) {
|
|
27085
|
+
try {
|
|
27086
|
+
const logger = await getAuditLogger(config, options.config || ".dbcli");
|
|
27087
|
+
const engine = config.connection?.system || "postgresql";
|
|
27088
|
+
const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
|
|
27089
|
+
let tier = getEngineCapability(engine, commandName).tier;
|
|
27090
|
+
if (options.dryRun || options.plan) {
|
|
27091
|
+
tier = "dry-run";
|
|
27092
|
+
}
|
|
27093
|
+
let errorMessage;
|
|
27094
|
+
if (outcome.error) {
|
|
27095
|
+
errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
27096
|
+
errorMessage = redactSensitive(errorMessage);
|
|
27097
|
+
}
|
|
27098
|
+
const entry = {
|
|
27099
|
+
engine,
|
|
27100
|
+
command: commandName,
|
|
27101
|
+
side_effect_tier: tier,
|
|
27102
|
+
target,
|
|
27103
|
+
success: outcome.success,
|
|
27104
|
+
redacted_query: redactArgv(process.argv),
|
|
27105
|
+
...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
|
|
27106
|
+
...errorMessage && { error: errorMessage },
|
|
27107
|
+
...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
|
|
27108
|
+
metadata: outcome.metadata
|
|
27109
|
+
};
|
|
27110
|
+
const result = await logger.write(entry);
|
|
27111
|
+
return "success" in result ? result.id : null;
|
|
27112
|
+
} catch {
|
|
27113
|
+
return null;
|
|
27114
|
+
}
|
|
27115
|
+
}
|
|
27116
|
+
var _sessionIdService = null, _loggers;
|
|
27117
|
+
var init_integration_helper = __esm(() => {
|
|
27118
|
+
init_logger();
|
|
27119
|
+
init_session_id();
|
|
27120
|
+
init_config_binding();
|
|
27121
|
+
init_config();
|
|
27122
|
+
init_capabilities();
|
|
27123
|
+
init_redaction();
|
|
27124
|
+
init_engine_hints();
|
|
27125
|
+
_loggers = new Map;
|
|
27126
|
+
});
|
|
27127
|
+
|
|
26521
27128
|
// src/commands/query-size-guard.ts
|
|
26522
27129
|
var exports_query_size_guard = {};
|
|
26523
27130
|
__export(exports_query_size_guard, {
|
|
@@ -27430,7 +28037,7 @@ var init_parser = __esm(() => {
|
|
|
27430
28037
|
|
|
27431
28038
|
// src/core/saved-queries/loader.ts
|
|
27432
28039
|
import { readdir } from "fs/promises";
|
|
27433
|
-
import { join as
|
|
28040
|
+
import { join as join16, relative, sep } from "path";
|
|
27434
28041
|
async function loadSnippets(opts) {
|
|
27435
28042
|
const builtin = await walkAndParse(opts.builtinDir, "builtin");
|
|
27436
28043
|
const shared = await walkAndParse(opts.sharedDir, "shared");
|
|
@@ -27497,7 +28104,7 @@ async function collectFiles(root) {
|
|
|
27497
28104
|
async function walk(dir) {
|
|
27498
28105
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
27499
28106
|
for (const e of entries) {
|
|
27500
|
-
const full =
|
|
28107
|
+
const full = join16(dir, e.name);
|
|
27501
28108
|
if (e.isDirectory())
|
|
27502
28109
|
await walk(full);
|
|
27503
28110
|
else
|
|
@@ -27581,23 +28188,23 @@ var init_runner = __esm(() => {
|
|
|
27581
28188
|
});
|
|
27582
28189
|
|
|
27583
28190
|
// src/core/saved-queries/snippet-paths.ts
|
|
27584
|
-
import { join as
|
|
28191
|
+
import { join as join17 } from "path";
|
|
27585
28192
|
function resolveBuiltinDir() {
|
|
27586
28193
|
return packageAssetPath("snippets");
|
|
27587
28194
|
}
|
|
27588
28195
|
function resolveSnippetDirs(workspaceRoot) {
|
|
27589
28196
|
return {
|
|
27590
28197
|
builtinDir: resolveBuiltinDir(),
|
|
27591
|
-
sharedDir:
|
|
27592
|
-
localDir:
|
|
28198
|
+
sharedDir: join17(workspaceRoot, ".dbcli-shared", "queries"),
|
|
28199
|
+
localDir: join17(workspaceRoot, ".dbcli", "queries")
|
|
27593
28200
|
};
|
|
27594
28201
|
}
|
|
27595
28202
|
function snippetKeyToFile(workspaceRoot, key, source) {
|
|
27596
28203
|
const rel = key.replace(/^@/, "") + ".sql";
|
|
27597
28204
|
if (source === "builtin")
|
|
27598
|
-
return
|
|
28205
|
+
return join17(resolveBuiltinDir(), rel);
|
|
27599
28206
|
const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
|
|
27600
|
-
return
|
|
28207
|
+
return join17(workspaceRoot, dir, rel);
|
|
27601
28208
|
}
|
|
27602
28209
|
var init_snippet_paths = __esm(() => {
|
|
27603
28210
|
init_package_root();
|
|
@@ -27660,7 +28267,7 @@ var exports_queries_delete = {};
|
|
|
27660
28267
|
__export(exports_queries_delete, {
|
|
27661
28268
|
queriesDelete: () => queriesDelete
|
|
27662
28269
|
});
|
|
27663
|
-
import { rm } from "fs/promises";
|
|
28270
|
+
import { rm as rm2 } from "fs/promises";
|
|
27664
28271
|
async function queriesDelete(name, options = {}) {
|
|
27665
28272
|
if (!name.startsWith("@")) {
|
|
27666
28273
|
throw new Error(`Snippet name must start with '@' (got '${name}')`);
|
|
@@ -27685,7 +28292,7 @@ async function queriesDelete(name, options = {}) {
|
|
|
27685
28292
|
return;
|
|
27686
28293
|
}
|
|
27687
28294
|
for (const v of localVariants) {
|
|
27688
|
-
await
|
|
28295
|
+
await rm2(v.query.file, { force: true });
|
|
27689
28296
|
console.log(`deleted ${v.query.file}`);
|
|
27690
28297
|
}
|
|
27691
28298
|
}
|
|
@@ -27699,8 +28306,8 @@ var exports_queries_rename = {};
|
|
|
27699
28306
|
__export(exports_queries_rename, {
|
|
27700
28307
|
queriesRename: () => queriesRename
|
|
27701
28308
|
});
|
|
27702
|
-
import { rename as
|
|
27703
|
-
import { dirname as
|
|
28309
|
+
import { rename as rename4, mkdir as mkdir6 } from "fs/promises";
|
|
28310
|
+
import { dirname as dirname6 } from "path";
|
|
27704
28311
|
async function queriesRename(oldName, newName, options = {}) {
|
|
27705
28312
|
if (!oldName.startsWith("@") || !newName.startsWith("@")) {
|
|
27706
28313
|
throw new Error(`Both names must start with '@'`);
|
|
@@ -27721,8 +28328,8 @@ async function queriesRename(oldName, newName, options = {}) {
|
|
|
27721
28328
|
for (const v of local) {
|
|
27722
28329
|
const dst = snippetKeyToFile(cwd, newName, "local");
|
|
27723
28330
|
const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
|
|
27724
|
-
await
|
|
27725
|
-
await
|
|
28331
|
+
await mkdir6(dirname6(dstWithSuffix), { recursive: true });
|
|
28332
|
+
await rename4(v.query.file, dstWithSuffix);
|
|
27726
28333
|
await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
|
|
27727
28334
|
console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
|
|
27728
28335
|
}
|
|
@@ -27747,8 +28354,8 @@ var exports_queries_copy = {};
|
|
|
27747
28354
|
__export(exports_queries_copy, {
|
|
27748
28355
|
queriesCopy: () => queriesCopy
|
|
27749
28356
|
});
|
|
27750
|
-
import { mkdir as
|
|
27751
|
-
import { dirname as
|
|
28357
|
+
import { mkdir as mkdir7, copyFile } from "fs/promises";
|
|
28358
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
27752
28359
|
async function queriesCopy(src, dst, options = {}) {
|
|
27753
28360
|
if (!src.startsWith("@") || !dst.startsWith("@")) {
|
|
27754
28361
|
throw new Error(`Both names must start with '@'`);
|
|
@@ -27766,7 +28373,7 @@ async function queriesCopy(src, dst, options = {}) {
|
|
|
27766
28373
|
}
|
|
27767
28374
|
for (const v of variants) {
|
|
27768
28375
|
const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
|
|
27769
|
-
await
|
|
28376
|
+
await mkdir7(dirname7(dstFile), { recursive: true });
|
|
27770
28377
|
await copyFile(v.query.file, dstFile);
|
|
27771
28378
|
console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
|
|
27772
28379
|
}
|
|
@@ -27784,21 +28391,21 @@ var exports_queries_import = {};
|
|
|
27784
28391
|
__export(exports_queries_import, {
|
|
27785
28392
|
queriesImport: () => queriesImport
|
|
27786
28393
|
});
|
|
27787
|
-
import { stat as
|
|
27788
|
-
import { basename as basename3, join as
|
|
28394
|
+
import { stat as stat5, mkdir as mkdir8, copyFile as copyFile2 } from "fs/promises";
|
|
28395
|
+
import { basename as basename3, join as join19, extname } from "path";
|
|
27789
28396
|
async function queriesImport(filePath, options = {}) {
|
|
27790
28397
|
const cwd = options.cwd ?? process.cwd();
|
|
27791
28398
|
if (extname(filePath) !== ".sql") {
|
|
27792
28399
|
throw new Error(`Expected .sql file, got ${filePath}`);
|
|
27793
28400
|
}
|
|
27794
|
-
await
|
|
28401
|
+
await stat5(filePath);
|
|
27795
28402
|
const text2 = await Bun.file(filePath).text();
|
|
27796
28403
|
const baseName = options.as ? options.as.replace(/^@/, "") : basename3(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
|
|
27797
28404
|
const key = "@" + baseName;
|
|
27798
28405
|
parseSavedQuery({ key, file: filePath, source: "local", text: text2 });
|
|
27799
|
-
const targetDir =
|
|
27800
|
-
await
|
|
27801
|
-
const target =
|
|
28406
|
+
const targetDir = join19(cwd, ".dbcli/queries");
|
|
28407
|
+
await mkdir8(targetDir, { recursive: true });
|
|
28408
|
+
const target = join19(targetDir, basename3(filePath));
|
|
27802
28409
|
if (await Bun.file(target).exists()) {
|
|
27803
28410
|
if (!options.force) {
|
|
27804
28411
|
const ok = await dist_default6({ message: `Overwrite ${target}?`, default: false });
|
|
@@ -27819,7 +28426,7 @@ var exports_queries_export = {};
|
|
|
27819
28426
|
__export(exports_queries_export, {
|
|
27820
28427
|
queriesExport: () => queriesExport
|
|
27821
28428
|
});
|
|
27822
|
-
import { writeFile as
|
|
28429
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
27823
28430
|
async function queriesExport(name, options = {}) {
|
|
27824
28431
|
const cwd = options.cwd ?? process.cwd();
|
|
27825
28432
|
const dirs = resolveSnippetDirs(cwd);
|
|
@@ -27840,7 +28447,7 @@ async function queriesExport(name, options = {}) {
|
|
|
27840
28447
|
}
|
|
27841
28448
|
const text2 = await Bun.file(chosen[0].query.file).text();
|
|
27842
28449
|
if (options.output) {
|
|
27843
|
-
await
|
|
28450
|
+
await writeFile3(options.output, text2, "utf8");
|
|
27844
28451
|
console.log(`wrote ${options.output}`);
|
|
27845
28452
|
} else {
|
|
27846
28453
|
process.stdout.write(text2);
|
|
@@ -27923,17 +28530,17 @@ var init_collect_snippets = __esm(() => {
|
|
|
27923
28530
|
});
|
|
27924
28531
|
|
|
27925
28532
|
// src/core/inspect/collect-schema-cache.ts
|
|
27926
|
-
import { join as
|
|
28533
|
+
import { join as join23 } from "path";
|
|
27927
28534
|
async function collectSchemaCache(opts) {
|
|
27928
28535
|
const warnings = [];
|
|
27929
|
-
if (opts.system && !
|
|
28536
|
+
if (opts.system && !SQL_SYSTEMS2.includes(opts.system)) {
|
|
27930
28537
|
return {
|
|
27931
28538
|
section: { available: false, unavailable: true, reason: "schema cache is sql-only" },
|
|
27932
28539
|
warnings
|
|
27933
28540
|
};
|
|
27934
28541
|
}
|
|
27935
28542
|
const root = resolveSchemaPath(opts.dbcliPath, opts.connectionName);
|
|
27936
|
-
const indexPath =
|
|
28543
|
+
const indexPath = join23(root, "index.json");
|
|
27937
28544
|
const file = Bun.file(indexPath);
|
|
27938
28545
|
if (!await file.exists()) {
|
|
27939
28546
|
return { section: { available: false }, warnings };
|
|
@@ -27960,11 +28567,11 @@ async function collectSchemaCache(opts) {
|
|
|
27960
28567
|
};
|
|
27961
28568
|
}
|
|
27962
28569
|
}
|
|
27963
|
-
var STALE_AFTER_MS,
|
|
28570
|
+
var STALE_AFTER_MS, SQL_SYSTEMS2;
|
|
27964
28571
|
var init_collect_schema_cache = __esm(() => {
|
|
27965
28572
|
init_schema_path();
|
|
27966
28573
|
STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
27967
|
-
|
|
28574
|
+
SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
|
|
27968
28575
|
});
|
|
27969
28576
|
|
|
27970
28577
|
// src/core/inspect/collect-objects.ts
|
|
@@ -28024,7 +28631,7 @@ function suggestCommands(snap, options = {}) {
|
|
|
28024
28631
|
if (!snap.system)
|
|
28025
28632
|
return ["dbcli init"];
|
|
28026
28633
|
const out = [];
|
|
28027
|
-
const isSql =
|
|
28634
|
+
const isSql = SQL_SYSTEMS3.includes(snap.system);
|
|
28028
28635
|
if (isSql && (!snap.schemaCache.available || snap.schemaCache.stale)) {
|
|
28029
28636
|
out.push("dbcli schema --refresh");
|
|
28030
28637
|
}
|
|
@@ -28039,13 +28646,13 @@ function suggestCommands(snap, options = {}) {
|
|
|
28039
28646
|
const cap2 = options.brief ? 1 : 5;
|
|
28040
28647
|
return out.slice(0, cap2);
|
|
28041
28648
|
}
|
|
28042
|
-
var
|
|
28649
|
+
var SQL_SYSTEMS3;
|
|
28043
28650
|
var init_suggest_commands = __esm(() => {
|
|
28044
|
-
|
|
28651
|
+
SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
|
|
28045
28652
|
});
|
|
28046
28653
|
|
|
28047
28654
|
// src/core/inspect/collector.ts
|
|
28048
|
-
import { join as
|
|
28655
|
+
import { join as join24 } from "path";
|
|
28049
28656
|
async function collectInspect(opts) {
|
|
28050
28657
|
const warnings = [];
|
|
28051
28658
|
let config = null;
|
|
@@ -28121,11 +28728,11 @@ async function collectInspect(opts) {
|
|
|
28121
28728
|
return { ...snapWithoutSuggestions, suggestedCommands, warnings };
|
|
28122
28729
|
}
|
|
28123
28730
|
async function hasConfig(configPath) {
|
|
28124
|
-
if (await Bun.file(
|
|
28731
|
+
if (await Bun.file(join24(configPath, "config.json")).exists())
|
|
28125
28732
|
return true;
|
|
28126
28733
|
if (await Bun.file(configPath).exists()) {
|
|
28127
|
-
const
|
|
28128
|
-
return
|
|
28734
|
+
const stat6 = await Bun.file(configPath).stat().catch(() => null);
|
|
28735
|
+
return stat6?.isFile() === true;
|
|
28129
28736
|
}
|
|
28130
28737
|
return false;
|
|
28131
28738
|
}
|
|
@@ -28247,16 +28854,16 @@ function renderMarkdown2(snap, options = {}) {
|
|
|
28247
28854
|
function requireSchemaCacheOrThrow(section, system) {
|
|
28248
28855
|
if (!system)
|
|
28249
28856
|
return;
|
|
28250
|
-
if (!
|
|
28857
|
+
if (!SQL_SYSTEMS4.includes(system))
|
|
28251
28858
|
return;
|
|
28252
28859
|
if (section.available === true)
|
|
28253
28860
|
return;
|
|
28254
28861
|
throw new SchemaCacheMissingError(`Schema cache unavailable for ${system} connection; run 'dbcli schema --refresh' to populate it.`);
|
|
28255
28862
|
}
|
|
28256
|
-
var
|
|
28863
|
+
var SQL_SYSTEMS4;
|
|
28257
28864
|
var init_require_schema_cache = __esm(() => {
|
|
28258
28865
|
init_recovery();
|
|
28259
|
-
|
|
28866
|
+
SQL_SYSTEMS4 = ["postgresql", "mysql", "mariadb"];
|
|
28260
28867
|
});
|
|
28261
28868
|
|
|
28262
28869
|
// src/core/inspect/index.ts
|
|
@@ -28273,6 +28880,125 @@ var init_inspect = __esm(() => {
|
|
|
28273
28880
|
init_collector();
|
|
28274
28881
|
});
|
|
28275
28882
|
|
|
28883
|
+
// src/core/audit/reader.ts
|
|
28884
|
+
import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
|
|
28885
|
+
async function readSingle(path5) {
|
|
28886
|
+
let raw;
|
|
28887
|
+
try {
|
|
28888
|
+
raw = await readFile4(path5, "utf8");
|
|
28889
|
+
} catch (err) {
|
|
28890
|
+
if (err.code === "ENOENT")
|
|
28891
|
+
return [];
|
|
28892
|
+
throw err;
|
|
28893
|
+
}
|
|
28894
|
+
const lines = raw.split(`
|
|
28895
|
+
`).filter(Boolean);
|
|
28896
|
+
const out = [];
|
|
28897
|
+
for (let i = 0;i < lines.length; i++) {
|
|
28898
|
+
try {
|
|
28899
|
+
out.push(JSON.parse(lines[i]));
|
|
28900
|
+
} catch {
|
|
28901
|
+
if (i === lines.length - 1) {
|
|
28902
|
+
process.stderr.write(`${WARN_PREFIX2} skipping truncated last line in ${path5}
|
|
28903
|
+
`);
|
|
28904
|
+
continue;
|
|
28905
|
+
}
|
|
28906
|
+
throw new Error(`${WARN_PREFIX2} corrupted line ${i + 1} in ${path5}. Run \`dbcli audit clear\` to reset.`);
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
return out;
|
|
28910
|
+
}
|
|
28911
|
+
async function readEntries(auditFilePath, opts) {
|
|
28912
|
+
if (opts?.include_rotated === true) {
|
|
28913
|
+
const rotated = await readSingle(`${auditFilePath}.1`);
|
|
28914
|
+
const current = await readSingle(auditFilePath);
|
|
28915
|
+
return rotated.concat(current);
|
|
28916
|
+
}
|
|
28917
|
+
return readSingle(auditFilePath);
|
|
28918
|
+
}
|
|
28919
|
+
async function discoverConnections(auditDir) {
|
|
28920
|
+
let entries;
|
|
28921
|
+
try {
|
|
28922
|
+
entries = await readdir3(auditDir, { withFileTypes: true });
|
|
28923
|
+
} catch (err) {
|
|
28924
|
+
if (err.code === "ENOENT")
|
|
28925
|
+
return [];
|
|
28926
|
+
throw err;
|
|
28927
|
+
}
|
|
28928
|
+
const map = new Map;
|
|
28929
|
+
for (const e of entries) {
|
|
28930
|
+
if (!e.isFile())
|
|
28931
|
+
continue;
|
|
28932
|
+
if (!(e.name.endsWith(".jsonl") || e.name.endsWith(".jsonl.1")))
|
|
28933
|
+
continue;
|
|
28934
|
+
const conn = e.name.replace(/\.jsonl(?:\.1)?$/, "");
|
|
28935
|
+
const full = `${auditDir}/${e.name}`;
|
|
28936
|
+
const list = map.get(conn) ?? [];
|
|
28937
|
+
list.push(full);
|
|
28938
|
+
map.set(conn, list);
|
|
28939
|
+
}
|
|
28940
|
+
for (const files of map.values()) {
|
|
28941
|
+
files.sort((a, b) => Number(b.endsWith(".jsonl.1")) - Number(a.endsWith(".jsonl.1")));
|
|
28942
|
+
}
|
|
28943
|
+
return Array.from(map.entries()).map(([connection, files]) => ({ connection, files })).sort((a, b) => a.connection.localeCompare(b.connection));
|
|
28944
|
+
}
|
|
28945
|
+
function tailEntries(entries, n) {
|
|
28946
|
+
if (n <= 0)
|
|
28947
|
+
return [];
|
|
28948
|
+
return entries.slice().sort((a, b) => a.ts.localeCompare(b.ts)).slice(-n);
|
|
28949
|
+
}
|
|
28950
|
+
function mergeByTimestamp(byConn) {
|
|
28951
|
+
const flat = [];
|
|
28952
|
+
for (const [connection, entries] of byConn) {
|
|
28953
|
+
for (const entry of entries)
|
|
28954
|
+
flat.push({ connection, entry });
|
|
28955
|
+
}
|
|
28956
|
+
flat.sort((a, b) => {
|
|
28957
|
+
const t2 = a.entry.ts.localeCompare(b.entry.ts);
|
|
28958
|
+
return t2 !== 0 ? t2 : a.connection.localeCompare(b.connection);
|
|
28959
|
+
});
|
|
28960
|
+
return flat;
|
|
28961
|
+
}
|
|
28962
|
+
var WARN_PREFIX2 = "[dbcli audit]";
|
|
28963
|
+
var init_reader = () => {};
|
|
28964
|
+
|
|
28965
|
+
// src/core/audit/recent.ts
|
|
28966
|
+
var exports_recent = {};
|
|
28967
|
+
__export(exports_recent, {
|
|
28968
|
+
shouldEmbedRecent: () => shouldEmbedRecent,
|
|
28969
|
+
loadRecentAudit: () => loadRecentAudit,
|
|
28970
|
+
RECENT_AUDIT_DEFAULT_N: () => RECENT_AUDIT_DEFAULT_N
|
|
28971
|
+
});
|
|
28972
|
+
function shouldEmbedRecent(opts) {
|
|
28973
|
+
return opts.forAgent === true || opts.format === "json";
|
|
28974
|
+
}
|
|
28975
|
+
function briefifyForRecent(entry) {
|
|
28976
|
+
return {
|
|
28977
|
+
id: entry.id,
|
|
28978
|
+
ts: entry.ts,
|
|
28979
|
+
command: entry.command,
|
|
28980
|
+
target: entry.target,
|
|
28981
|
+
success: entry.success
|
|
28982
|
+
};
|
|
28983
|
+
}
|
|
28984
|
+
async function loadRecentAudit(config, configPath, n = RECENT_AUDIT_DEFAULT_N) {
|
|
28985
|
+
try {
|
|
28986
|
+
if (config.audit?.enabled === false)
|
|
28987
|
+
return [];
|
|
28988
|
+
const logger = await getAuditLogger(config, configPath);
|
|
28989
|
+
const auditFile = logger.getHealth().currentFile;
|
|
28990
|
+
const entries = await readEntries(auditFile, { include_rotated: true });
|
|
28991
|
+
return tailEntries(entries, n).map(briefifyForRecent);
|
|
28992
|
+
} catch {
|
|
28993
|
+
return [];
|
|
28994
|
+
}
|
|
28995
|
+
}
|
|
28996
|
+
var RECENT_AUDIT_DEFAULT_N = 5;
|
|
28997
|
+
var init_recent = __esm(() => {
|
|
28998
|
+
init_integration_helper();
|
|
28999
|
+
init_reader();
|
|
29000
|
+
});
|
|
29001
|
+
|
|
28276
29002
|
// node_modules/commander/esm.mjs
|
|
28277
29003
|
var import__ = __toESM(require_commander(), 1);
|
|
28278
29004
|
var {
|
|
@@ -28291,7 +29017,7 @@ var {
|
|
|
28291
29017
|
// package.json
|
|
28292
29018
|
var package_default = {
|
|
28293
29019
|
name: "@carllee1983/dbcli",
|
|
28294
|
-
version: "1.
|
|
29020
|
+
version: "1.20.0",
|
|
28295
29021
|
description: "Database CLI for AI agents",
|
|
28296
29022
|
type: "module",
|
|
28297
29023
|
publishConfig: {
|
|
@@ -28341,6 +29067,7 @@ var package_default = {
|
|
|
28341
29067
|
"test:unit": "bun test tests/unit tests/core",
|
|
28342
29068
|
"test:integration": "bun test tests/integration",
|
|
28343
29069
|
"test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration/adapters; docker compose -f docker-compose.test.yml down",
|
|
29070
|
+
"docs:check": "bun run scripts/check-user-docs.ts",
|
|
28344
29071
|
typecheck: "tsc --noEmit --pretty false",
|
|
28345
29072
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
28346
29073
|
lint: "eslint src tests --ext .ts --max-warnings=0",
|
|
@@ -28741,7 +29468,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28741
29468
|
schema: v1Config.schema || {},
|
|
28742
29469
|
schemas: { default: v1Config.schema || {} },
|
|
28743
29470
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28744
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29471
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29472
|
+
audit: v1Config.audit
|
|
28745
29473
|
};
|
|
28746
29474
|
}
|
|
28747
29475
|
} else if (await projectConfigFile.exists()) {
|
|
@@ -28760,7 +29488,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28760
29488
|
schema: v1Config.schema || {},
|
|
28761
29489
|
schemas: { default: v1Config.schema || {} },
|
|
28762
29490
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28763
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29491
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29492
|
+
audit: v1Config.audit
|
|
28764
29493
|
};
|
|
28765
29494
|
} else {
|
|
28766
29495
|
const v1Config = await configModule.read(configPath);
|
|
@@ -28776,7 +29505,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28776
29505
|
schema: v1Config.schema || {},
|
|
28777
29506
|
schemas: { default: v1Config.schema || {} },
|
|
28778
29507
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28779
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29508
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29509
|
+
audit: v1Config.audit
|
|
28780
29510
|
};
|
|
28781
29511
|
}
|
|
28782
29512
|
} else {
|
|
@@ -28797,7 +29527,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28797
29527
|
schema: v1Config.schema || {},
|
|
28798
29528
|
schemas: { default: v1Config.schema || {} },
|
|
28799
29529
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28800
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29530
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29531
|
+
audit: v1Config.audit
|
|
28801
29532
|
};
|
|
28802
29533
|
}
|
|
28803
29534
|
}
|
|
@@ -28824,7 +29555,11 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28824
29555
|
schema: {},
|
|
28825
29556
|
schemas: {},
|
|
28826
29557
|
metadata: { version: "1.0", createdAt: new Date().toISOString() },
|
|
28827
|
-
blacklist: { tables: [], columns: {} }
|
|
29558
|
+
blacklist: { tables: [], columns: {} },
|
|
29559
|
+
audit: {
|
|
29560
|
+
enabled: true,
|
|
29561
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
29562
|
+
}
|
|
28828
29563
|
};
|
|
28829
29564
|
await writeV2Config(storagePath, v2Config);
|
|
28830
29565
|
await migrateLegacyProjectEnvLocal(configPath, storagePath);
|
|
@@ -30496,6 +31231,7 @@ async function writeSchema(configPath, config, connectionName) {
|
|
|
30496
31231
|
// src/commands/query.ts
|
|
30497
31232
|
init_message_loader();
|
|
30498
31233
|
init_adapters();
|
|
31234
|
+
import crypto from "crypto";
|
|
30499
31235
|
|
|
30500
31236
|
// src/formatters/html-formatter.ts
|
|
30501
31237
|
init_package_root();
|
|
@@ -30528,7 +31264,7 @@ async function openInBrowser(target) {
|
|
|
30528
31264
|
|
|
30529
31265
|
// src/commands/query.ts
|
|
30530
31266
|
import { tmpdir } from "os";
|
|
30531
|
-
import { join as
|
|
31267
|
+
import { join as join15 } from "path";
|
|
30532
31268
|
|
|
30533
31269
|
// src/core/query-executor.ts
|
|
30534
31270
|
init_permission_guard();
|
|
@@ -30596,20 +31332,30 @@ async function getAllTablesFromAdapter(adapter) {
|
|
|
30596
31332
|
}
|
|
30597
31333
|
}
|
|
30598
31334
|
|
|
31335
|
+
// src/core/query-executor.ts
|
|
31336
|
+
init_engine_hints();
|
|
31337
|
+
|
|
30599
31338
|
// src/core/limits.ts
|
|
30600
31339
|
var DEFAULT_QUERY_ONLY_LIMIT = 1000;
|
|
30601
31340
|
|
|
30602
31341
|
// src/core/query-executor.ts
|
|
31342
|
+
init_integration_helper();
|
|
31343
|
+
|
|
30603
31344
|
class QueryExecutor {
|
|
30604
31345
|
adapter;
|
|
30605
31346
|
permission;
|
|
30606
31347
|
blacklistValidator;
|
|
30607
|
-
|
|
31348
|
+
config;
|
|
31349
|
+
options;
|
|
31350
|
+
constructor(adapter, permission, blacklistValidator, config, options = {}) {
|
|
30608
31351
|
this.adapter = adapter;
|
|
30609
31352
|
this.permission = permission;
|
|
30610
31353
|
this.blacklistValidator = blacklistValidator;
|
|
31354
|
+
this.config = config;
|
|
31355
|
+
this.options = options;
|
|
30611
31356
|
}
|
|
30612
31357
|
async execute(sql, options) {
|
|
31358
|
+
const start = performance.now();
|
|
30613
31359
|
try {
|
|
30614
31360
|
const classification = enforcePermission(sql, this.permission);
|
|
30615
31361
|
if (classification.isDangerous && this.permission === "admin") {
|
|
@@ -30627,7 +31373,6 @@ class QueryExecutor {
|
|
|
30627
31373
|
this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
|
|
30628
31374
|
}
|
|
30629
31375
|
}
|
|
30630
|
-
const start = performance.now();
|
|
30631
31376
|
const resultData = await this.adapter.execute(executeSql);
|
|
30632
31377
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
30633
31378
|
const rows = resultData.rows;
|
|
@@ -30662,8 +31407,28 @@ class QueryExecutor {
|
|
|
30662
31407
|
...securityNotification ? { securityNotification } : {}
|
|
30663
31408
|
}
|
|
30664
31409
|
};
|
|
31410
|
+
if (this.config) {
|
|
31411
|
+
await writeAuditEntry(this.config, "query", this.options, {
|
|
31412
|
+
success: true,
|
|
31413
|
+
sql,
|
|
31414
|
+
metadata: {
|
|
31415
|
+
rows_affected: affectedRows,
|
|
31416
|
+
execution_ms: executionTimeMs
|
|
31417
|
+
}
|
|
31418
|
+
});
|
|
31419
|
+
}
|
|
30665
31420
|
return result;
|
|
30666
31421
|
} catch (error) {
|
|
31422
|
+
if (this.config) {
|
|
31423
|
+
await writeAuditEntry(this.config, "query", this.options, {
|
|
31424
|
+
success: false,
|
|
31425
|
+
sql,
|
|
31426
|
+
error,
|
|
31427
|
+
metadata: {
|
|
31428
|
+
execution_ms: Math.round(performance.now() - start)
|
|
31429
|
+
}
|
|
31430
|
+
});
|
|
31431
|
+
}
|
|
30667
31432
|
if (error instanceof PermissionError) {
|
|
30668
31433
|
throw error;
|
|
30669
31434
|
}
|
|
@@ -30685,25 +31450,6 @@ class QueryExecutor {
|
|
|
30685
31450
|
}
|
|
30686
31451
|
}
|
|
30687
31452
|
}
|
|
30688
|
-
function extractTableName(sql) {
|
|
30689
|
-
const match = sql.match(/\bFROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
|
|
30690
|
-
if (match) {
|
|
30691
|
-
return match[1] ?? null;
|
|
30692
|
-
}
|
|
30693
|
-
const insertMatch = sql.match(/\bINSERT\s+INTO\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
|
|
30694
|
-
if (insertMatch) {
|
|
30695
|
-
return insertMatch[1] ?? null;
|
|
30696
|
-
}
|
|
30697
|
-
const updateMatch = sql.match(/\bUPDATE\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
|
|
30698
|
-
if (updateMatch) {
|
|
30699
|
-
return updateMatch[1] ?? null;
|
|
30700
|
-
}
|
|
30701
|
-
const deleteMatch = sql.match(/\bDELETE\s+FROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
|
|
30702
|
-
if (deleteMatch) {
|
|
30703
|
-
return deleteMatch[1] ?? null;
|
|
30704
|
-
}
|
|
30705
|
-
return null;
|
|
30706
|
-
}
|
|
30707
31453
|
function inferColumnType(value) {
|
|
30708
31454
|
if (value === null || value === undefined) {
|
|
30709
31455
|
return "null";
|
|
@@ -30728,6 +31474,7 @@ init_config();
|
|
|
30728
31474
|
init_permission_guard();
|
|
30729
31475
|
init_blacklist();
|
|
30730
31476
|
init_validation();
|
|
31477
|
+
init_integration_helper();
|
|
30731
31478
|
function requireSqlConnection2(connection) {
|
|
30732
31479
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
30733
31480
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
@@ -30736,6 +31483,7 @@ function requireSqlConnection2(connection) {
|
|
|
30736
31483
|
}
|
|
30737
31484
|
var ALLOWED_FORMATS3 = ["table", "json", "csv"];
|
|
30738
31485
|
async function queryCommand(sql, options, command) {
|
|
31486
|
+
let config;
|
|
30739
31487
|
try {
|
|
30740
31488
|
if (!sql || sql.trim() === "") {
|
|
30741
31489
|
throw new Error("SQL query required");
|
|
@@ -30745,7 +31493,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30745
31493
|
}
|
|
30746
31494
|
sql = sql.trim();
|
|
30747
31495
|
const configPath = resolveConfigPath(command, options);
|
|
30748
|
-
|
|
31496
|
+
config = await configModule.read(configPath);
|
|
30749
31497
|
if (!config.connection) {
|
|
30750
31498
|
throw new Error('Run "dbcli init" first');
|
|
30751
31499
|
}
|
|
@@ -30758,7 +31506,8 @@ async function queryCommand(sql, options, command) {
|
|
|
30758
31506
|
if (config.connection.system === "elasticsearch") {
|
|
30759
31507
|
return await elasticsearchQueryBranch(sql, options, config);
|
|
30760
31508
|
}
|
|
30761
|
-
const
|
|
31509
|
+
const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
|
|
31510
|
+
const mainTable = extractTableName2(sql);
|
|
30762
31511
|
if (mainTable && config.schema && !options.noLimit) {
|
|
30763
31512
|
const tableSchema = config.schema[mainTable];
|
|
30764
31513
|
if (tableSchema) {
|
|
@@ -30774,7 +31523,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30774
31523
|
try {
|
|
30775
31524
|
const blacklistManager = new BlacklistManager(config);
|
|
30776
31525
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
30777
|
-
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator);
|
|
31526
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
|
|
30778
31527
|
const autoLimit = !options.noLimit;
|
|
30779
31528
|
const result = await executor3.execute(sql, {
|
|
30780
31529
|
autoLimit,
|
|
@@ -30792,7 +31541,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30792
31541
|
rows: result.rows
|
|
30793
31542
|
});
|
|
30794
31543
|
if (options.ui) {
|
|
30795
|
-
const tempPath =
|
|
31544
|
+
const tempPath = join15(tmpdir(), `dbcli-query-${Date.now()}.html`);
|
|
30796
31545
|
await Bun.write(tempPath, html);
|
|
30797
31546
|
await openInBrowser(tempPath);
|
|
30798
31547
|
} else {
|
|
@@ -30809,11 +31558,27 @@ async function queryCommand(sql, options, command) {
|
|
|
30809
31558
|
await adapter.disconnect();
|
|
30810
31559
|
}
|
|
30811
31560
|
} catch (error) {
|
|
31561
|
+
let auditId = null;
|
|
31562
|
+
let envelopeId;
|
|
30812
31563
|
if (options.recovery === true) {
|
|
31564
|
+
envelopeId = crypto.randomUUID();
|
|
31565
|
+
}
|
|
31566
|
+
if (config) {
|
|
31567
|
+
auditId = await writeAuditEntry(config, "query", options, {
|
|
31568
|
+
success: false,
|
|
31569
|
+
sql,
|
|
31570
|
+
error,
|
|
31571
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
31572
|
+
});
|
|
31573
|
+
}
|
|
31574
|
+
if (envelopeId !== undefined) {
|
|
30813
31575
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
30814
31576
|
emitRecoveryEnvelope2(error, {
|
|
30815
31577
|
operation: "query",
|
|
30816
|
-
table:
|
|
31578
|
+
table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(sql) ?? undefined
|
|
31579
|
+
}, {
|
|
31580
|
+
envelopeId,
|
|
31581
|
+
auditRef: auditId ?? undefined
|
|
30817
31582
|
});
|
|
30818
31583
|
}
|
|
30819
31584
|
if (error instanceof BlacklistError) {
|
|
@@ -30834,21 +31599,17 @@ async function queryCommand(sql, options, command) {
|
|
|
30834
31599
|
process.exit(1);
|
|
30835
31600
|
}
|
|
30836
31601
|
}
|
|
30837
|
-
function extractMainTable(sql) {
|
|
30838
|
-
const match = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
|
|
30839
|
-
return match?.[1] ?? null;
|
|
30840
|
-
}
|
|
30841
|
-
var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
30842
31602
|
async function mongoQueryBranch(queryStr, options, config) {
|
|
30843
31603
|
const collection = options.collection;
|
|
30844
31604
|
const format = options.format ?? "table";
|
|
31605
|
+
if (!collection) {
|
|
31606
|
+
throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
|
|
31607
|
+
}
|
|
31608
|
+
const SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
30845
31609
|
if (SQL_PATTERN.test(queryStr)) {
|
|
30846
31610
|
throw new Error(`\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u8A9E\u6CD5\u3002
|
|
30847
31611
|
\u7BC4\u4F8B\uFF1Adbcli query '{"field": "value"}' --collection <name>`);
|
|
30848
31612
|
}
|
|
30849
|
-
if (!collection) {
|
|
30850
|
-
throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
|
|
30851
|
-
}
|
|
30852
31613
|
try {
|
|
30853
31614
|
JSON.parse(queryStr);
|
|
30854
31615
|
} catch {
|
|
@@ -30881,8 +31642,18 @@ async function mongoQueryBranch(queryStr, options, config) {
|
|
|
30881
31642
|
}
|
|
30882
31643
|
const mongoAdapter = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
30883
31644
|
await mongoAdapter.connect();
|
|
31645
|
+
const start = performance.now();
|
|
30884
31646
|
try {
|
|
30885
31647
|
const result = await mongoAdapter.execute(queryStr, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
|
|
31648
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31649
|
+
await writeAuditEntry(config, "query", options, {
|
|
31650
|
+
success: true,
|
|
31651
|
+
target: collection,
|
|
31652
|
+
metadata: {
|
|
31653
|
+
rows_affected: result.rows.length,
|
|
31654
|
+
execution_ms: executionTimeMs
|
|
31655
|
+
}
|
|
31656
|
+
});
|
|
30886
31657
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
|
|
30887
31658
|
const filterResult = blacklistValidator.filterColumns(collection, result.rows, columnNames);
|
|
30888
31659
|
const queryResult = {
|
|
@@ -30894,8 +31665,8 @@ async function mongoQueryBranch(queryStr, options, config) {
|
|
|
30894
31665
|
const output = formatter.format(queryResult, {
|
|
30895
31666
|
format
|
|
30896
31667
|
});
|
|
30897
|
-
const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
|
|
30898
31668
|
console.log(output);
|
|
31669
|
+
const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
|
|
30899
31670
|
if (securityNote) {
|
|
30900
31671
|
console.log(`
|
|
30901
31672
|
\u2139 ${securityNote}`);
|
|
@@ -30909,15 +31680,24 @@ async function redisQueryBranch(command, options, config) {
|
|
|
30909
31680
|
const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
|
|
30910
31681
|
const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
|
|
30911
31682
|
if (head === "KEYS" && options.recovery !== true) {
|
|
30912
|
-
console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers
|
|
30913
|
-
console.error(' Please use "SCAN" instead for better performance and safety.');
|
|
30914
|
-
console.error(" For more info: https://redis.io/commands/keys/");
|
|
31683
|
+
console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers.');
|
|
30915
31684
|
}
|
|
30916
31685
|
enforceRedisPermission2(command, config.permission);
|
|
30917
31686
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
|
|
30918
31687
|
await redisAdapter.connect();
|
|
31688
|
+
const start = performance.now();
|
|
30919
31689
|
try {
|
|
30920
31690
|
const result = await redisAdapter.execute(command);
|
|
31691
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31692
|
+
const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
31693
|
+
await writeAuditEntry(config, "query", options, {
|
|
31694
|
+
success: true,
|
|
31695
|
+
target,
|
|
31696
|
+
metadata: {
|
|
31697
|
+
rows_affected: result.rows.length,
|
|
31698
|
+
execution_ms: executionTimeMs
|
|
31699
|
+
}
|
|
31700
|
+
});
|
|
30921
31701
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
|
|
30922
31702
|
const queryResult = {
|
|
30923
31703
|
rows: result.rows,
|
|
@@ -30949,23 +31729,28 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
|
|
|
30949
31729
|
if (options.noLimit) {
|
|
30950
31730
|
effectiveLimit = 1e4;
|
|
30951
31731
|
console.error("Elasticsearch --no-limit is capped at size 10000; for more rows use saved-query with search_after.");
|
|
30952
|
-
} else if (typeof options.limit === "number") {
|
|
30953
|
-
effectiveLimit = options.limit;
|
|
30954
31732
|
} else {
|
|
30955
|
-
effectiveLimit = DEFAULT_QUERY_ONLY_LIMIT;
|
|
30956
|
-
if (config.permission === "query-only") {
|
|
30957
|
-
console.error(`Query-only mode: auto-limiting to ${effectiveLimit} rows`);
|
|
30958
|
-
}
|
|
31733
|
+
effectiveLimit = options.limit || DEFAULT_QUERY_ONLY_LIMIT;
|
|
30959
31734
|
}
|
|
30960
31735
|
const blacklistManager = new BlacklistManager(config);
|
|
30961
31736
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
30962
31737
|
blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
|
|
30963
31738
|
const esAdapter = AdapterFactory.createElasticsearchAdapter(config.connection);
|
|
30964
31739
|
await esAdapter.connect();
|
|
31740
|
+
const start = performance.now();
|
|
30965
31741
|
try {
|
|
30966
31742
|
const result = await esAdapter.execute(queryStr, [indexName], {
|
|
30967
31743
|
limit: effectiveLimit
|
|
30968
31744
|
});
|
|
31745
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31746
|
+
await writeAuditEntry(config, "query", options, {
|
|
31747
|
+
success: true,
|
|
31748
|
+
target: indexName,
|
|
31749
|
+
metadata: {
|
|
31750
|
+
rows_affected: result.rows.length,
|
|
31751
|
+
execution_ms: executionTimeMs
|
|
31752
|
+
}
|
|
31753
|
+
});
|
|
30969
31754
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
|
|
30970
31755
|
const filterResult = blacklistValidator.filterColumns(indexName, result.rows, columnNames);
|
|
30971
31756
|
const queryResult = {
|
|
@@ -30977,11 +31762,12 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
|
|
|
30977
31762
|
const output = formatter.format(queryResult, {
|
|
30978
31763
|
format
|
|
30979
31764
|
});
|
|
30980
|
-
const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
|
|
30981
31765
|
console.log(output);
|
|
30982
|
-
|
|
31766
|
+
const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
|
|
31767
|
+
if (securityNote) {
|
|
30983
31768
|
console.log(`
|
|
30984
31769
|
\u2139 ${securityNote}`);
|
|
31770
|
+
}
|
|
30985
31771
|
} finally {
|
|
30986
31772
|
await esAdapter.disconnect();
|
|
30987
31773
|
}
|
|
@@ -31338,8 +32124,10 @@ function buildSuggestedCommands(facts, gap) {
|
|
|
31338
32124
|
// src/commands/plan.ts
|
|
31339
32125
|
init_config();
|
|
31340
32126
|
init_validation();
|
|
32127
|
+
init_integration_helper();
|
|
31341
32128
|
var ALLOWED_FORMATS4 = ["text", "json"];
|
|
31342
32129
|
async function planCommand(sql, options, command) {
|
|
32130
|
+
let config;
|
|
31343
32131
|
try {
|
|
31344
32132
|
if (!sql || sql.trim() === "") {
|
|
31345
32133
|
throw new Error("SQL statement required");
|
|
@@ -31347,7 +32135,7 @@ async function planCommand(sql, options, command) {
|
|
|
31347
32135
|
const format = options.format ?? "text";
|
|
31348
32136
|
validateFormat(format, ALLOWED_FORMATS4, "plan");
|
|
31349
32137
|
const configPath = resolveConfigPath(command, options);
|
|
31350
|
-
|
|
32138
|
+
config = await configModule.read(configPath);
|
|
31351
32139
|
if (!config.connection) {
|
|
31352
32140
|
throw new Error('Run "dbcli init" first');
|
|
31353
32141
|
}
|
|
@@ -31363,7 +32151,25 @@ async function planCommand(sql, options, command) {
|
|
|
31363
32151
|
}
|
|
31364
32152
|
});
|
|
31365
32153
|
console.log(formatPlanResult(result, format));
|
|
32154
|
+
if (config) {
|
|
32155
|
+
await writeAuditEntry(config, "plan", { ...options, plan: true }, {
|
|
32156
|
+
success: true,
|
|
32157
|
+
sql,
|
|
32158
|
+
target: result.targetTables[0] || "*",
|
|
32159
|
+
metadata: {
|
|
32160
|
+
decision: result.decision,
|
|
32161
|
+
risk_factors: result.riskFactors.map((f) => f.code)
|
|
32162
|
+
}
|
|
32163
|
+
});
|
|
32164
|
+
}
|
|
31366
32165
|
} catch (error) {
|
|
32166
|
+
if (config) {
|
|
32167
|
+
await writeAuditEntry(config, "plan", { ...options, plan: true }, {
|
|
32168
|
+
success: false,
|
|
32169
|
+
sql,
|
|
32170
|
+
error
|
|
32171
|
+
});
|
|
32172
|
+
}
|
|
31367
32173
|
console.error(error.message);
|
|
31368
32174
|
process.exit(1);
|
|
31369
32175
|
}
|
|
@@ -31402,10 +32208,11 @@ init_adapters();
|
|
|
31402
32208
|
init_config();
|
|
31403
32209
|
init_blacklist();
|
|
31404
32210
|
init_permission_guard();
|
|
32211
|
+
init_engine_hints();
|
|
31405
32212
|
init_saved_queries();
|
|
31406
32213
|
init_strategies();
|
|
31407
32214
|
import { tmpdir as tmpdir2 } from "os";
|
|
31408
|
-
import { join as
|
|
32215
|
+
import { join as join18 } from "path";
|
|
31409
32216
|
function formatDryRun(input) {
|
|
31410
32217
|
const lines = ["Dry-run preview (no execution):"];
|
|
31411
32218
|
if (input.family === "es") {
|
|
@@ -31486,7 +32293,7 @@ async function qCommand(name, options, command) {
|
|
|
31486
32293
|
rows: filtered.filteredRows
|
|
31487
32294
|
});
|
|
31488
32295
|
if (options.ui) {
|
|
31489
|
-
const tempPath =
|
|
32296
|
+
const tempPath = join18(tmpdir2(), `dbcli-report-${Date.now()}.html`);
|
|
31490
32297
|
await Bun.write(tempPath, html);
|
|
31491
32298
|
await openInBrowser(tempPath);
|
|
31492
32299
|
} else {
|
|
@@ -31568,8 +32375,8 @@ async function handleQError(error, snippetName, useRecovery) {
|
|
|
31568
32375
|
// src/commands/queries.ts
|
|
31569
32376
|
init_message_loader();
|
|
31570
32377
|
init_config();
|
|
31571
|
-
import { mkdir as
|
|
31572
|
-
import { dirname as
|
|
32378
|
+
import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
|
|
32379
|
+
import { dirname as dirname8 } from "path";
|
|
31573
32380
|
import { spawn as spawn2 } from "child_process";
|
|
31574
32381
|
init_saved_queries();
|
|
31575
32382
|
init_fold();
|
|
@@ -31817,8 +32624,8 @@ async function queriesNew(name, options) {
|
|
|
31817
32624
|
process.exit(1);
|
|
31818
32625
|
return;
|
|
31819
32626
|
}
|
|
31820
|
-
await
|
|
31821
|
-
await
|
|
32627
|
+
await mkdir9(dirname8(file), { recursive: true });
|
|
32628
|
+
await writeFile4(file, scaffold(name), "utf8");
|
|
31822
32629
|
console.log(`Created ${file}`);
|
|
31823
32630
|
if (source === "shared")
|
|
31824
32631
|
console.log(t("queries.first_run_hint"));
|
|
@@ -32441,6 +33248,693 @@ function previewDelete(collection, filter) {
|
|
|
32441
33248
|
return `db.${collection}.deleteMany(${pretty(filter)})`;
|
|
32442
33249
|
}
|
|
32443
33250
|
|
|
33251
|
+
// src/commands/dml-plan.ts
|
|
33252
|
+
init_config();
|
|
33253
|
+
init_validation();
|
|
33254
|
+
|
|
33255
|
+
// src/core/dml-plan-sql.ts
|
|
33256
|
+
var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33257
|
+
function assertIdentifier(value, role) {
|
|
33258
|
+
if (!value || !IDENTIFIER_RE.test(value)) {
|
|
33259
|
+
throw new Error(`Invalid ${role} identifier: ${JSON.stringify(value)}. ` + `${role === "table" ? "Table" : "Column"} names must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
33260
|
+
}
|
|
33261
|
+
}
|
|
33262
|
+
function normalizeTable(table) {
|
|
33263
|
+
if (!table || table.trim() === "") {
|
|
33264
|
+
throw new Error("Table name required for plan SQL");
|
|
33265
|
+
}
|
|
33266
|
+
const trimmed = table.trim();
|
|
33267
|
+
assertIdentifier(trimmed, "table");
|
|
33268
|
+
return trimmed;
|
|
33269
|
+
}
|
|
33270
|
+
function normalizeColumns(data, role) {
|
|
33271
|
+
const keys = Object.keys(data);
|
|
33272
|
+
if (keys.length === 0) {
|
|
33273
|
+
throw new Error(role === "data" ? "INSERT plan requires at least one column in --data" : "UPDATE plan requires at least one column in --set");
|
|
33274
|
+
}
|
|
33275
|
+
for (const key of keys) {
|
|
33276
|
+
assertIdentifier(key, "column");
|
|
33277
|
+
}
|
|
33278
|
+
return keys;
|
|
33279
|
+
}
|
|
33280
|
+
function normalizeWhere(where) {
|
|
33281
|
+
const keys = Object.keys(where);
|
|
33282
|
+
if (keys.length === 0) {
|
|
33283
|
+
throw new Error("WHERE clause is required for plan SQL");
|
|
33284
|
+
}
|
|
33285
|
+
for (const key of keys) {
|
|
33286
|
+
assertIdentifier(key, "column");
|
|
33287
|
+
}
|
|
33288
|
+
return keys;
|
|
33289
|
+
}
|
|
33290
|
+
function buildInsertPlanSql(table, data) {
|
|
33291
|
+
const t2 = normalizeTable(table);
|
|
33292
|
+
const columns = normalizeColumns(data, "data");
|
|
33293
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
33294
|
+
return `INSERT INTO ${t2} (${columns.join(", ")}) VALUES (${placeholders})`;
|
|
33295
|
+
}
|
|
33296
|
+
function buildUpdatePlanSql(table, set, where) {
|
|
33297
|
+
const t2 = normalizeTable(table);
|
|
33298
|
+
const setCols = normalizeColumns(set, "set");
|
|
33299
|
+
const whereCols = normalizeWhere(where);
|
|
33300
|
+
const setClause = setCols.map((col) => `${col} = ?`).join(", ");
|
|
33301
|
+
const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
|
|
33302
|
+
return `UPDATE ${t2} SET ${setClause} WHERE ${whereClause}`;
|
|
33303
|
+
}
|
|
33304
|
+
function buildDeletePlanSql(table, where) {
|
|
33305
|
+
const t2 = normalizeTable(table);
|
|
33306
|
+
const whereCols = normalizeWhere(where);
|
|
33307
|
+
const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
|
|
33308
|
+
return `DELETE FROM ${t2} WHERE ${whereClause}`;
|
|
33309
|
+
}
|
|
33310
|
+
|
|
33311
|
+
// src/utils/where-parser.ts
|
|
33312
|
+
function parseWhereClause(whereClause) {
|
|
33313
|
+
if (!whereClause || whereClause.trim() === "") {
|
|
33314
|
+
throw new Error("WHERE clause cannot be empty");
|
|
33315
|
+
}
|
|
33316
|
+
const conditions = {};
|
|
33317
|
+
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
33318
|
+
for (const part of andParts) {
|
|
33319
|
+
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
33320
|
+
if (!match) {
|
|
33321
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33322
|
+
}
|
|
33323
|
+
const column = match[1];
|
|
33324
|
+
const valueStr = match[2];
|
|
33325
|
+
if (valueStr === undefined || column === undefined) {
|
|
33326
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33327
|
+
}
|
|
33328
|
+
const trimmed = valueStr.trim();
|
|
33329
|
+
const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
|
|
33330
|
+
if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
|
|
33331
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33332
|
+
}
|
|
33333
|
+
const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
|
|
33334
|
+
let value = stripped;
|
|
33335
|
+
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
33336
|
+
value = Number(stripped);
|
|
33337
|
+
}
|
|
33338
|
+
if (stripped === "true")
|
|
33339
|
+
value = true;
|
|
33340
|
+
else if (stripped === "false")
|
|
33341
|
+
value = false;
|
|
33342
|
+
else if (stripped === "null")
|
|
33343
|
+
value = null;
|
|
33344
|
+
conditions[column] = value;
|
|
33345
|
+
}
|
|
33346
|
+
return conditions;
|
|
33347
|
+
}
|
|
33348
|
+
|
|
33349
|
+
// src/core/mongo/dml-plan.ts
|
|
33350
|
+
init_permission_guard();
|
|
33351
|
+
var MONGO_UPDATE_OPERATOR_ALLOWLIST = new Set(["$set", "$unset"]);
|
|
33352
|
+
var MONGO_BROAD_FILTER_OPERATORS = new Set([
|
|
33353
|
+
"$regex",
|
|
33354
|
+
"$in",
|
|
33355
|
+
"$nin",
|
|
33356
|
+
"$gt",
|
|
33357
|
+
"$gte",
|
|
33358
|
+
"$lt",
|
|
33359
|
+
"$lte",
|
|
33360
|
+
"$ne",
|
|
33361
|
+
"$exists",
|
|
33362
|
+
"$or",
|
|
33363
|
+
"$and",
|
|
33364
|
+
"$nor",
|
|
33365
|
+
"$not",
|
|
33366
|
+
"$expr",
|
|
33367
|
+
"$text"
|
|
33368
|
+
]);
|
|
33369
|
+
function permissionSql(operation) {
|
|
33370
|
+
if (operation === "insert")
|
|
33371
|
+
return "INSERT INTO dummy";
|
|
33372
|
+
if (operation === "update")
|
|
33373
|
+
return "UPDATE dummy";
|
|
33374
|
+
return "DELETE FROM dummy";
|
|
33375
|
+
}
|
|
33376
|
+
function broadOperation(operation) {
|
|
33377
|
+
if (operation === "insert")
|
|
33378
|
+
return "INSERT";
|
|
33379
|
+
if (operation === "update")
|
|
33380
|
+
return "UPDATE";
|
|
33381
|
+
return "DELETE";
|
|
33382
|
+
}
|
|
33383
|
+
function pushFactor2(factors, code, severity, message) {
|
|
33384
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33385
|
+
return;
|
|
33386
|
+
factors.push({ code, severity, message });
|
|
33387
|
+
}
|
|
33388
|
+
function decide2(factors) {
|
|
33389
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33390
|
+
return "BLOCK";
|
|
33391
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33392
|
+
return "WARN";
|
|
33393
|
+
return "ALLOW";
|
|
33394
|
+
}
|
|
33395
|
+
function extractMongoWrittenFields(setDoc) {
|
|
33396
|
+
const fields = new Set;
|
|
33397
|
+
const operators = [];
|
|
33398
|
+
let hasUnsupportedOperator = false;
|
|
33399
|
+
const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
|
|
33400
|
+
if (!hasAnyOperator) {
|
|
33401
|
+
for (const k of Object.keys(setDoc))
|
|
33402
|
+
fields.add(k);
|
|
33403
|
+
return { fields: Array.from(fields), operators: [], hasUnsupportedOperator: false };
|
|
33404
|
+
}
|
|
33405
|
+
for (const [op, payload] of Object.entries(setDoc)) {
|
|
33406
|
+
operators.push(op);
|
|
33407
|
+
if (!MONGO_UPDATE_OPERATOR_ALLOWLIST.has(op)) {
|
|
33408
|
+
hasUnsupportedOperator = true;
|
|
33409
|
+
continue;
|
|
33410
|
+
}
|
|
33411
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
33412
|
+
for (const k of Object.keys(payload))
|
|
33413
|
+
fields.add(k);
|
|
33414
|
+
}
|
|
33415
|
+
}
|
|
33416
|
+
return { fields: Array.from(fields), operators, hasUnsupportedOperator };
|
|
33417
|
+
}
|
|
33418
|
+
function hasIdEquality(where) {
|
|
33419
|
+
if (!("_id" in where))
|
|
33420
|
+
return false;
|
|
33421
|
+
const value = where._id;
|
|
33422
|
+
if (value === null || value === undefined)
|
|
33423
|
+
return false;
|
|
33424
|
+
if (typeof value === "object")
|
|
33425
|
+
return false;
|
|
33426
|
+
return true;
|
|
33427
|
+
}
|
|
33428
|
+
function hasNonIdEquality(where) {
|
|
33429
|
+
for (const [key, value] of Object.entries(where)) {
|
|
33430
|
+
if (key === "_id")
|
|
33431
|
+
continue;
|
|
33432
|
+
if (key.startsWith("$"))
|
|
33433
|
+
continue;
|
|
33434
|
+
if (value === null)
|
|
33435
|
+
continue;
|
|
33436
|
+
if (typeof value === "object")
|
|
33437
|
+
continue;
|
|
33438
|
+
return true;
|
|
33439
|
+
}
|
|
33440
|
+
return false;
|
|
33441
|
+
}
|
|
33442
|
+
function hasBroadFilter(where) {
|
|
33443
|
+
for (const [key, value] of Object.entries(where)) {
|
|
33444
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(key))
|
|
33445
|
+
return true;
|
|
33446
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
33447
|
+
for (const inner of Object.keys(value)) {
|
|
33448
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
|
|
33449
|
+
return true;
|
|
33450
|
+
}
|
|
33451
|
+
}
|
|
33452
|
+
}
|
|
33453
|
+
return false;
|
|
33454
|
+
}
|
|
33455
|
+
function applyPermission(operation, context, factors) {
|
|
33456
|
+
const permResult = checkPermission(permissionSql(operation), context.permission);
|
|
33457
|
+
if (!permResult.allowed) {
|
|
33458
|
+
pushFactor2(factors, "permission_denied", "block", permResult.reason);
|
|
33459
|
+
}
|
|
33460
|
+
}
|
|
33461
|
+
function applyTableBlacklist(target, context, factors) {
|
|
33462
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33463
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33464
|
+
pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
|
|
33465
|
+
}
|
|
33466
|
+
}
|
|
33467
|
+
function applyColumnBlacklist(target, fields, context, factors) {
|
|
33468
|
+
const columns = context.blacklist.columns ?? {};
|
|
33469
|
+
const lower = target.toLowerCase();
|
|
33470
|
+
let blacklistedFields = [];
|
|
33471
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33472
|
+
if (t2.toLowerCase() === lower) {
|
|
33473
|
+
blacklistedFields = cols;
|
|
33474
|
+
break;
|
|
33475
|
+
}
|
|
33476
|
+
}
|
|
33477
|
+
if (blacklistedFields.length === 0)
|
|
33478
|
+
return;
|
|
33479
|
+
for (const field of fields) {
|
|
33480
|
+
if (blacklistedFields.includes(field)) {
|
|
33481
|
+
pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted field ${target}.${field}.`);
|
|
33482
|
+
}
|
|
33483
|
+
}
|
|
33484
|
+
}
|
|
33485
|
+
function applySchemaCoverage(target, context, factors) {
|
|
33486
|
+
const schemaTables = Object.keys(context.schema);
|
|
33487
|
+
if (schemaTables.length === 0) {
|
|
33488
|
+
pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
|
|
33489
|
+
return;
|
|
33490
|
+
}
|
|
33491
|
+
const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
33492
|
+
if (!known) {
|
|
33493
|
+
pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
|
|
33494
|
+
}
|
|
33495
|
+
}
|
|
33496
|
+
function buildRecommendations2(factors) {
|
|
33497
|
+
const out = new Set;
|
|
33498
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33499
|
+
if (codes.has("nonsql_filter_empty")) {
|
|
33500
|
+
out.add("Add an _id equality filter before executing this MongoDB write.");
|
|
33501
|
+
}
|
|
33502
|
+
if (codes.has("nonsql_missing_id")) {
|
|
33503
|
+
out.add("Prefer an _id equality filter before executing this MongoDB write.");
|
|
33504
|
+
}
|
|
33505
|
+
if (codes.has("nonsql_filter_broad")) {
|
|
33506
|
+
out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
|
|
33507
|
+
}
|
|
33508
|
+
if (codes.has("nonsql_unsupported_operator")) {
|
|
33509
|
+
out.add("Restrict the update document to $set / $unset for planner-safe writes.");
|
|
33510
|
+
}
|
|
33511
|
+
if (codes.has("permission_denied")) {
|
|
33512
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33513
|
+
}
|
|
33514
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33515
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33516
|
+
}
|
|
33517
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33518
|
+
out.add("Refresh schema cache for the target collection before executing.");
|
|
33519
|
+
}
|
|
33520
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33521
|
+
return Array.from(out);
|
|
33522
|
+
}
|
|
33523
|
+
function buildSuggestedCommands2(target, factors) {
|
|
33524
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33525
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33526
|
+
return [`dbcli schema ${target} --format json`];
|
|
33527
|
+
}
|
|
33528
|
+
return [];
|
|
33529
|
+
}
|
|
33530
|
+
var analyzeMongoDmlRisk = (intent, context) => {
|
|
33531
|
+
const factors = [];
|
|
33532
|
+
applyPermission(intent.operation, context, factors);
|
|
33533
|
+
applyTableBlacklist(intent.target, context, factors);
|
|
33534
|
+
if (intent.operation === "insert") {
|
|
33535
|
+
applyColumnBlacklist(intent.target, Object.keys(intent.data), context, factors);
|
|
33536
|
+
} else if (intent.operation === "update") {
|
|
33537
|
+
const { fields, hasUnsupportedOperator } = extractMongoWrittenFields(intent.set);
|
|
33538
|
+
if (hasUnsupportedOperator) {
|
|
33539
|
+
pushFactor2(factors, "nonsql_unsupported_operator", "block", "MongoDB update uses an operator outside the MVP allowlist ($set, $unset).");
|
|
33540
|
+
}
|
|
33541
|
+
applyColumnBlacklist(intent.target, fields, context, factors);
|
|
33542
|
+
const where = intent.where ?? {};
|
|
33543
|
+
if (Object.keys(where).length === 0) {
|
|
33544
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
|
|
33545
|
+
} else if (!hasIdEquality(where)) {
|
|
33546
|
+
if (hasBroadFilter(where)) {
|
|
33547
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
|
|
33548
|
+
} else if (hasNonIdEquality(where)) {
|
|
33549
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
|
|
33550
|
+
} else {
|
|
33551
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
|
|
33552
|
+
}
|
|
33553
|
+
}
|
|
33554
|
+
} else {
|
|
33555
|
+
const where = intent.where ?? {};
|
|
33556
|
+
if (Object.keys(where).length === 0) {
|
|
33557
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
|
|
33558
|
+
} else if (!hasIdEquality(where)) {
|
|
33559
|
+
if (hasBroadFilter(where)) {
|
|
33560
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
|
|
33561
|
+
} else if (hasNonIdEquality(where)) {
|
|
33562
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
|
|
33563
|
+
} else {
|
|
33564
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
|
|
33565
|
+
}
|
|
33566
|
+
}
|
|
33567
|
+
}
|
|
33568
|
+
applySchemaCoverage(intent.target, context, factors);
|
|
33569
|
+
return {
|
|
33570
|
+
decision: decide2(factors),
|
|
33571
|
+
operation: broadOperation(intent.operation),
|
|
33572
|
+
targetTables: [intent.target],
|
|
33573
|
+
riskFactors: factors,
|
|
33574
|
+
recommendations: buildRecommendations2(factors),
|
|
33575
|
+
suggestedCommands: buildSuggestedCommands2(intent.target, factors)
|
|
33576
|
+
};
|
|
33577
|
+
};
|
|
33578
|
+
|
|
33579
|
+
// src/core/redis/dml-plan.ts
|
|
33580
|
+
init_permission_guard();
|
|
33581
|
+
function permissionSql2(operation) {
|
|
33582
|
+
if (operation === "insert")
|
|
33583
|
+
return "INSERT INTO dummy";
|
|
33584
|
+
if (operation === "update")
|
|
33585
|
+
return "UPDATE dummy";
|
|
33586
|
+
return "DELETE FROM dummy";
|
|
33587
|
+
}
|
|
33588
|
+
function broadOperation2(operation) {
|
|
33589
|
+
if (operation === "insert")
|
|
33590
|
+
return "INSERT";
|
|
33591
|
+
if (operation === "update")
|
|
33592
|
+
return "UPDATE";
|
|
33593
|
+
return "DELETE";
|
|
33594
|
+
}
|
|
33595
|
+
function pushFactor3(factors, code, severity, message) {
|
|
33596
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33597
|
+
return;
|
|
33598
|
+
factors.push({ code, severity, message });
|
|
33599
|
+
}
|
|
33600
|
+
function decide3(factors) {
|
|
33601
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33602
|
+
return "BLOCK";
|
|
33603
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33604
|
+
return "WARN";
|
|
33605
|
+
return "ALLOW";
|
|
33606
|
+
}
|
|
33607
|
+
function classifyKey(target) {
|
|
33608
|
+
if (target === "*")
|
|
33609
|
+
return "all";
|
|
33610
|
+
if (target.includes("*") || target.includes("?") || /\[.+\]/.test(target))
|
|
33611
|
+
return "pattern";
|
|
33612
|
+
return "single";
|
|
33613
|
+
}
|
|
33614
|
+
function applyPermission2(operation, context, factors) {
|
|
33615
|
+
const result = checkPermission(permissionSql2(operation), context.permission);
|
|
33616
|
+
if (!result.allowed) {
|
|
33617
|
+
pushFactor3(factors, "permission_denied", "block", result.reason);
|
|
33618
|
+
}
|
|
33619
|
+
}
|
|
33620
|
+
function applyTableBlacklist2(target, context, factors) {
|
|
33621
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33622
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33623
|
+
pushFactor3(factors, "table_blacklisted", "block", `Redis key ${target} is blacklisted.`);
|
|
33624
|
+
}
|
|
33625
|
+
}
|
|
33626
|
+
function applyColumnBlacklist2(target, fields, context, factors) {
|
|
33627
|
+
const columns = context.blacklist.columns ?? {};
|
|
33628
|
+
const lower = target.toLowerCase();
|
|
33629
|
+
let blacklistedFields = [];
|
|
33630
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33631
|
+
if (t2.toLowerCase() === lower) {
|
|
33632
|
+
blacklistedFields = cols;
|
|
33633
|
+
break;
|
|
33634
|
+
}
|
|
33635
|
+
}
|
|
33636
|
+
if (blacklistedFields.length === 0)
|
|
33637
|
+
return;
|
|
33638
|
+
for (const field of fields) {
|
|
33639
|
+
if (blacklistedFields.includes(field)) {
|
|
33640
|
+
pushFactor3(factors, "blacklisted_column", "block", `Redis write would touch blacklisted field ${target}.${field}.`);
|
|
33641
|
+
}
|
|
33642
|
+
}
|
|
33643
|
+
}
|
|
33644
|
+
function redisSchemaKeyMatches(schemaKey, target) {
|
|
33645
|
+
const lowerKey = schemaKey.toLowerCase();
|
|
33646
|
+
const lowerTarget = target.toLowerCase();
|
|
33647
|
+
if (lowerKey === lowerTarget)
|
|
33648
|
+
return true;
|
|
33649
|
+
if (!lowerKey.includes("*") && !lowerKey.includes("?"))
|
|
33650
|
+
return false;
|
|
33651
|
+
const pattern = lowerKey.split("").map((ch) => {
|
|
33652
|
+
if (ch === "*")
|
|
33653
|
+
return ".*";
|
|
33654
|
+
if (ch === "?")
|
|
33655
|
+
return ".";
|
|
33656
|
+
return ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
33657
|
+
}).join("");
|
|
33658
|
+
return new RegExp(`^${pattern}$`).test(lowerTarget);
|
|
33659
|
+
}
|
|
33660
|
+
function applySchemaCoverage2(target, context, factors) {
|
|
33661
|
+
const tables = Object.keys(context.schema);
|
|
33662
|
+
if (tables.length === 0) {
|
|
33663
|
+
pushFactor3(factors, "schema_cache_missing", "warn", "Schema/key metadata is missing for the selected Redis connection.");
|
|
33664
|
+
return;
|
|
33665
|
+
}
|
|
33666
|
+
const known = tables.some((t2) => redisSchemaKeyMatches(t2, target));
|
|
33667
|
+
if (!known) {
|
|
33668
|
+
pushFactor3(factors, "schema_table_unknown", "warn", `Redis key namespace ${target} is missing from cached metadata.`);
|
|
33669
|
+
}
|
|
33670
|
+
}
|
|
33671
|
+
function buildRecommendations3(factors) {
|
|
33672
|
+
const out = new Set;
|
|
33673
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33674
|
+
if (codes.has("nonsql_key_pattern_broad")) {
|
|
33675
|
+
out.add("Specify a concrete single key instead of a wildcard pattern.");
|
|
33676
|
+
}
|
|
33677
|
+
if (codes.has("nonsql_overwrite_unknown")) {
|
|
33678
|
+
out.add("Provide --set field information so the planner can verify the write target.");
|
|
33679
|
+
}
|
|
33680
|
+
if (codes.has("permission_denied")) {
|
|
33681
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33682
|
+
}
|
|
33683
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33684
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33685
|
+
}
|
|
33686
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33687
|
+
out.add("Refresh schema cache for the target Redis namespace before executing.");
|
|
33688
|
+
}
|
|
33689
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33690
|
+
return Array.from(out);
|
|
33691
|
+
}
|
|
33692
|
+
function buildSuggestedCommands3(target, factors) {
|
|
33693
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33694
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33695
|
+
return [`dbcli schema ${target} --format json`];
|
|
33696
|
+
}
|
|
33697
|
+
return [];
|
|
33698
|
+
}
|
|
33699
|
+
var analyzeRedisDmlRisk = (intent, context) => {
|
|
33700
|
+
const factors = [];
|
|
33701
|
+
applyPermission2(intent.operation, context, factors);
|
|
33702
|
+
applyTableBlacklist2(intent.target, context, factors);
|
|
33703
|
+
const keyClass = classifyKey(intent.target);
|
|
33704
|
+
if (keyClass === "all") {
|
|
33705
|
+
pushFactor3(factors, "nonsql_key_pattern_broad", "block", 'Redis key "*" matches every key and is unsafe for planner-level writes.');
|
|
33706
|
+
} else if (keyClass === "pattern") {
|
|
33707
|
+
pushFactor3(factors, "nonsql_key_pattern_broad", "warn", `Redis target ${intent.target} contains wildcard characters and may match multiple keys.`);
|
|
33708
|
+
}
|
|
33709
|
+
if (intent.operation === "insert") {
|
|
33710
|
+
applyColumnBlacklist2(intent.target, Object.keys(intent.data), context, factors);
|
|
33711
|
+
} else if (intent.operation === "update") {
|
|
33712
|
+
const fields = Object.keys(intent.set);
|
|
33713
|
+
if (fields.length === 0) {
|
|
33714
|
+
pushFactor3(factors, "nonsql_overwrite_unknown", "warn", "Redis update has no field information and overwrite impact cannot be verified.");
|
|
33715
|
+
}
|
|
33716
|
+
applyColumnBlacklist2(intent.target, fields, context, factors);
|
|
33717
|
+
}
|
|
33718
|
+
applySchemaCoverage2(intent.target, context, factors);
|
|
33719
|
+
return {
|
|
33720
|
+
decision: decide3(factors),
|
|
33721
|
+
operation: broadOperation2(intent.operation),
|
|
33722
|
+
targetTables: [intent.target],
|
|
33723
|
+
riskFactors: factors,
|
|
33724
|
+
recommendations: buildRecommendations3(factors),
|
|
33725
|
+
suggestedCommands: buildSuggestedCommands3(intent.target, factors)
|
|
33726
|
+
};
|
|
33727
|
+
};
|
|
33728
|
+
|
|
33729
|
+
// src/core/elasticsearch/dml-plan.ts
|
|
33730
|
+
init_permission_guard();
|
|
33731
|
+
function permissionSql3(operation) {
|
|
33732
|
+
if (operation === "insert")
|
|
33733
|
+
return "INSERT INTO dummy";
|
|
33734
|
+
if (operation === "update")
|
|
33735
|
+
return "UPDATE dummy";
|
|
33736
|
+
return "DELETE FROM dummy";
|
|
33737
|
+
}
|
|
33738
|
+
function broadOperation3(operation) {
|
|
33739
|
+
if (operation === "insert")
|
|
33740
|
+
return "INSERT";
|
|
33741
|
+
if (operation === "update")
|
|
33742
|
+
return "UPDATE";
|
|
33743
|
+
return "DELETE";
|
|
33744
|
+
}
|
|
33745
|
+
function pushFactor4(factors, code, severity, message) {
|
|
33746
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33747
|
+
return;
|
|
33748
|
+
factors.push({ code, severity, message });
|
|
33749
|
+
}
|
|
33750
|
+
function decide4(factors) {
|
|
33751
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33752
|
+
return "BLOCK";
|
|
33753
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33754
|
+
return "WARN";
|
|
33755
|
+
return "ALLOW";
|
|
33756
|
+
}
|
|
33757
|
+
function hasIdEquality2(where) {
|
|
33758
|
+
if (!where)
|
|
33759
|
+
return false;
|
|
33760
|
+
if (!("_id" in where))
|
|
33761
|
+
return false;
|
|
33762
|
+
const value = where._id;
|
|
33763
|
+
if (value === null || value === undefined)
|
|
33764
|
+
return false;
|
|
33765
|
+
if (typeof value === "object")
|
|
33766
|
+
return false;
|
|
33767
|
+
return true;
|
|
33768
|
+
}
|
|
33769
|
+
function applyPermission3(operation, context, factors) {
|
|
33770
|
+
const result = checkPermission(permissionSql3(operation), context.permission);
|
|
33771
|
+
if (!result.allowed) {
|
|
33772
|
+
pushFactor4(factors, "permission_denied", "block", result.reason);
|
|
33773
|
+
}
|
|
33774
|
+
}
|
|
33775
|
+
function applyTableBlacklist3(target, context, factors) {
|
|
33776
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33777
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33778
|
+
pushFactor4(factors, "table_blacklisted", "block", `Elasticsearch index ${target} is blacklisted.`);
|
|
33779
|
+
}
|
|
33780
|
+
}
|
|
33781
|
+
function applyColumnBlacklist3(target, fields, context, factors) {
|
|
33782
|
+
const columns = context.blacklist.columns ?? {};
|
|
33783
|
+
const lower = target.toLowerCase();
|
|
33784
|
+
let blacklistedFields = [];
|
|
33785
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33786
|
+
if (t2.toLowerCase() === lower) {
|
|
33787
|
+
blacklistedFields = cols;
|
|
33788
|
+
break;
|
|
33789
|
+
}
|
|
33790
|
+
}
|
|
33791
|
+
if (blacklistedFields.length === 0)
|
|
33792
|
+
return;
|
|
33793
|
+
for (const field of fields) {
|
|
33794
|
+
if (blacklistedFields.includes(field)) {
|
|
33795
|
+
pushFactor4(factors, "blacklisted_column", "block", `Elasticsearch write would touch blacklisted field ${target}.${field}.`);
|
|
33796
|
+
}
|
|
33797
|
+
}
|
|
33798
|
+
}
|
|
33799
|
+
function applySchemaCoverage3(target, context, factors) {
|
|
33800
|
+
const tables = Object.keys(context.schema);
|
|
33801
|
+
if (tables.length === 0) {
|
|
33802
|
+
pushFactor4(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected Elasticsearch connection.");
|
|
33803
|
+
return;
|
|
33804
|
+
}
|
|
33805
|
+
const known = tables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
33806
|
+
if (!known) {
|
|
33807
|
+
pushFactor4(factors, "schema_table_unknown", "warn", `Target index ${target} is missing from schema cache.`);
|
|
33808
|
+
}
|
|
33809
|
+
}
|
|
33810
|
+
function buildRecommendations4(factors) {
|
|
33811
|
+
const out = new Set;
|
|
33812
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33813
|
+
if (codes.has("nonsql_missing_id")) {
|
|
33814
|
+
out.add("Add an _id equality filter (or include _id in the document) before executing.");
|
|
33815
|
+
}
|
|
33816
|
+
if (codes.has("nonsql_unsupported_bulk")) {
|
|
33817
|
+
out.add("Use a document-level write instead of a bulk or by-query operation.");
|
|
33818
|
+
}
|
|
33819
|
+
if (codes.has("permission_denied")) {
|
|
33820
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33821
|
+
}
|
|
33822
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33823
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33824
|
+
}
|
|
33825
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33826
|
+
out.add("Refresh schema cache for the target index before executing.");
|
|
33827
|
+
}
|
|
33828
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33829
|
+
return Array.from(out);
|
|
33830
|
+
}
|
|
33831
|
+
function buildSuggestedCommands4(target, factors) {
|
|
33832
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33833
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33834
|
+
return [`dbcli schema ${target} --format json`];
|
|
33835
|
+
}
|
|
33836
|
+
return [];
|
|
33837
|
+
}
|
|
33838
|
+
var analyzeElasticsearchDmlRisk = (intent, context) => {
|
|
33839
|
+
const factors = [];
|
|
33840
|
+
applyPermission3(intent.operation, context, factors);
|
|
33841
|
+
applyTableBlacklist3(intent.target, context, factors);
|
|
33842
|
+
if (intent.operation === "insert") {
|
|
33843
|
+
const fields = Object.keys(intent.data);
|
|
33844
|
+
applyColumnBlacklist3(intent.target, fields, context, factors);
|
|
33845
|
+
if (intent.data._id === undefined || intent.data._id === null || intent.data._id === "") {
|
|
33846
|
+
pushFactor4(factors, "nonsql_missing_id", "warn", "Elasticsearch insert has no _id; auto-id generation will make later correlation harder.");
|
|
33847
|
+
}
|
|
33848
|
+
} else if (intent.operation === "update") {
|
|
33849
|
+
applyColumnBlacklist3(intent.target, Object.keys(intent.set), context, factors);
|
|
33850
|
+
if (!hasIdEquality2(intent.where)) {
|
|
33851
|
+
pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch update requires _id in --where for planner-safe document-level update.");
|
|
33852
|
+
}
|
|
33853
|
+
} else {
|
|
33854
|
+
if (!hasIdEquality2(intent.where)) {
|
|
33855
|
+
pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch delete requires _id in --where for planner-safe document-level deletion.");
|
|
33856
|
+
}
|
|
33857
|
+
}
|
|
33858
|
+
applySchemaCoverage3(intent.target, context, factors);
|
|
33859
|
+
return {
|
|
33860
|
+
decision: decide4(factors),
|
|
33861
|
+
operation: broadOperation3(intent.operation),
|
|
33862
|
+
targetTables: [intent.target],
|
|
33863
|
+
riskFactors: factors,
|
|
33864
|
+
recommendations: buildRecommendations4(factors),
|
|
33865
|
+
suggestedCommands: buildSuggestedCommands4(intent.target, factors)
|
|
33866
|
+
};
|
|
33867
|
+
};
|
|
33868
|
+
|
|
33869
|
+
// src/commands/dml-plan.ts
|
|
33870
|
+
var ALLOWED_FORMATS5 = ["text", "json"];
|
|
33871
|
+
var SQL_SYSTEMS = new Set(["postgresql", "mysql", "mariadb"]);
|
|
33872
|
+
function buildPlanSqlForIntent(intent) {
|
|
33873
|
+
if (intent.operation === "insert") {
|
|
33874
|
+
return buildInsertPlanSql(intent.target, intent.data);
|
|
33875
|
+
}
|
|
33876
|
+
if (intent.operation === "update") {
|
|
33877
|
+
if (!intent.where || Object.keys(intent.where).length === 0) {
|
|
33878
|
+
throw new Error("WHERE clause is required for SQL update plan");
|
|
33879
|
+
}
|
|
33880
|
+
return buildUpdatePlanSql(intent.target, intent.set, intent.where);
|
|
33881
|
+
}
|
|
33882
|
+
if (!intent.where || Object.keys(intent.where).length === 0) {
|
|
33883
|
+
throw new Error("WHERE clause is required for SQL delete plan");
|
|
33884
|
+
}
|
|
33885
|
+
return buildDeletePlanSql(intent.target, intent.where);
|
|
33886
|
+
}
|
|
33887
|
+
async function runDmlPlanAnalysis(intent, options, command) {
|
|
33888
|
+
try {
|
|
33889
|
+
const format = options.format ?? "text";
|
|
33890
|
+
validateFormat(format, ALLOWED_FORMATS5, "plan");
|
|
33891
|
+
const configPath = resolveConfigPath(command, options);
|
|
33892
|
+
const config = await configModule.read(configPath);
|
|
33893
|
+
if (!config.connection) {
|
|
33894
|
+
throw new Error('Run "dbcli init" to configure database connection');
|
|
33895
|
+
}
|
|
33896
|
+
const system = config.connection.system;
|
|
33897
|
+
const schema = config.schema ?? {};
|
|
33898
|
+
const blacklist = config.blacklist ?? { tables: [], columns: {} };
|
|
33899
|
+
if (SQL_SYSTEMS.has(system)) {
|
|
33900
|
+
const planSql = buildPlanSqlForIntent(intent);
|
|
33901
|
+
const schemaTableCount = Object.keys(schema).length;
|
|
33902
|
+
const result = analyzeQueryRisk({
|
|
33903
|
+
sql: planSql,
|
|
33904
|
+
permission: config.permission,
|
|
33905
|
+
blacklist,
|
|
33906
|
+
schemaLookup: {
|
|
33907
|
+
tables: schema,
|
|
33908
|
+
cacheAvailable: schemaTableCount > 0
|
|
33909
|
+
}
|
|
33910
|
+
});
|
|
33911
|
+
console.log(formatPlanResult(result, format));
|
|
33912
|
+
return;
|
|
33913
|
+
}
|
|
33914
|
+
const context = {
|
|
33915
|
+
permission: config.permission,
|
|
33916
|
+
blacklist,
|
|
33917
|
+
schema
|
|
33918
|
+
};
|
|
33919
|
+
if (system === "mongodb") {
|
|
33920
|
+
console.log(formatPlanResult(analyzeMongoDmlRisk(intent, context), format));
|
|
33921
|
+
return;
|
|
33922
|
+
}
|
|
33923
|
+
if (system === "redis") {
|
|
33924
|
+
console.log(formatPlanResult(analyzeRedisDmlRisk(intent, context), format));
|
|
33925
|
+
return;
|
|
33926
|
+
}
|
|
33927
|
+
if (system === "elasticsearch") {
|
|
33928
|
+
console.log(formatPlanResult(analyzeElasticsearchDmlRisk(intent, context), format));
|
|
33929
|
+
return;
|
|
33930
|
+
}
|
|
33931
|
+
throw new Error(`--plan does not support connection system "${system}" yet`);
|
|
33932
|
+
} catch (error) {
|
|
33933
|
+
console.error(error.message);
|
|
33934
|
+
process.exit(1);
|
|
33935
|
+
}
|
|
33936
|
+
}
|
|
33937
|
+
|
|
32444
33938
|
// src/commands/insert.ts
|
|
32445
33939
|
function requireSqlConnection3(connection) {
|
|
32446
33940
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
@@ -32492,6 +33986,15 @@ async function insertCommand(table, options, command) {
|
|
|
32492
33986
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
32493
33987
|
throw new Error('JSON must be an object (e.g. {"name":"Alice","email":"a@b.com"})');
|
|
32494
33988
|
}
|
|
33989
|
+
if (options.plan) {
|
|
33990
|
+
if (options.dryRun) {
|
|
33991
|
+
console.error("--plan cannot be used with --dry-run");
|
|
33992
|
+
process.exit(1);
|
|
33993
|
+
return;
|
|
33994
|
+
}
|
|
33995
|
+
await runDmlPlanAnalysis({ operation: "insert", target: table, data }, { format: options.format, config: options.config }, command);
|
|
33996
|
+
return;
|
|
33997
|
+
}
|
|
32495
33998
|
const configPath = resolveConfigPath(command, options);
|
|
32496
33999
|
const config = await configModule.read(configPath);
|
|
32497
34000
|
if (!config.connection) {
|
|
@@ -32654,45 +34157,13 @@ function requireSqlConnection4(connection) {
|
|
|
32654
34157
|
}
|
|
32655
34158
|
return connection;
|
|
32656
34159
|
}
|
|
32657
|
-
function parseWhereClause(whereClause) {
|
|
32658
|
-
if (!whereClause || whereClause.trim() === "") {
|
|
32659
|
-
throw new Error("WHERE clause cannot be empty");
|
|
32660
|
-
}
|
|
32661
|
-
const conditions = {};
|
|
32662
|
-
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
32663
|
-
for (const part of andParts) {
|
|
32664
|
-
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
32665
|
-
if (!match) {
|
|
32666
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
32667
|
-
}
|
|
32668
|
-
const column = match[1];
|
|
32669
|
-
const valueStr = match[2];
|
|
32670
|
-
if (valueStr === undefined || column === undefined) {
|
|
32671
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
32672
|
-
}
|
|
32673
|
-
const trimmed = valueStr.trim();
|
|
32674
|
-
const stripped = trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
32675
|
-
let value = stripped;
|
|
32676
|
-
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
32677
|
-
value = Number(stripped);
|
|
32678
|
-
}
|
|
32679
|
-
if (stripped === "true")
|
|
32680
|
-
value = true;
|
|
32681
|
-
else if (stripped === "false")
|
|
32682
|
-
value = false;
|
|
32683
|
-
else if (stripped === "null")
|
|
32684
|
-
value = null;
|
|
32685
|
-
conditions[column] = value;
|
|
32686
|
-
}
|
|
32687
|
-
return conditions;
|
|
32688
|
-
}
|
|
32689
34160
|
async function updateCommand(table, options, command) {
|
|
32690
34161
|
try {
|
|
32691
34162
|
if (!table || table.trim() === "") {
|
|
32692
34163
|
throw new Error("Table name required");
|
|
32693
34164
|
}
|
|
32694
34165
|
table = table.trim();
|
|
32695
|
-
if (!options.where || options.where.trim() === "") {
|
|
34166
|
+
if (!options.plan && (!options.where || options.where.trim() === "")) {
|
|
32696
34167
|
throw new Error('UPDATE requires --where clause (e.g. --where "id=1")');
|
|
32697
34168
|
}
|
|
32698
34169
|
if (!options.set || options.set.trim() === "") {
|
|
@@ -32712,6 +34183,40 @@ async function updateCommand(table, options, command) {
|
|
|
32712
34183
|
if (!setData || typeof setData !== "object" || Array.isArray(setData)) {
|
|
32713
34184
|
throw new Error('JSON in --set must be an object (e.g. {"name":"Bob","email":"b@example.com"})');
|
|
32714
34185
|
}
|
|
34186
|
+
if (options.plan) {
|
|
34187
|
+
if (options.dryRun) {
|
|
34188
|
+
console.error("--plan cannot be used with --dry-run");
|
|
34189
|
+
process.exit(1);
|
|
34190
|
+
return;
|
|
34191
|
+
}
|
|
34192
|
+
const rawWhere = options.where ?? "";
|
|
34193
|
+
let whereForPlan = null;
|
|
34194
|
+
if (rawWhere.trim() !== "") {
|
|
34195
|
+
try {
|
|
34196
|
+
const parsed = JSON.parse(rawWhere);
|
|
34197
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
34198
|
+
whereForPlan = parsed;
|
|
34199
|
+
}
|
|
34200
|
+
} catch {
|
|
34201
|
+
whereForPlan = null;
|
|
34202
|
+
}
|
|
34203
|
+
if (whereForPlan === null) {
|
|
34204
|
+
try {
|
|
34205
|
+
whereForPlan = parseWhereClause(rawWhere);
|
|
34206
|
+
} catch {
|
|
34207
|
+
whereForPlan = {};
|
|
34208
|
+
}
|
|
34209
|
+
}
|
|
34210
|
+
}
|
|
34211
|
+
await runDmlPlanAnalysis({
|
|
34212
|
+
operation: "update",
|
|
34213
|
+
target: table,
|
|
34214
|
+
set: setData,
|
|
34215
|
+
where: whereForPlan,
|
|
34216
|
+
rawWhere
|
|
34217
|
+
}, { format: options.format, config: options.config }, command);
|
|
34218
|
+
return;
|
|
34219
|
+
}
|
|
32715
34220
|
if (config.connection?.system === "redis") {
|
|
32716
34221
|
enforcePermission("UPDATE dummy", config.permission);
|
|
32717
34222
|
const blacklistManager = new BlacklistManager(config);
|
|
@@ -32894,41 +34399,13 @@ function requireSqlConnection5(connection) {
|
|
|
32894
34399
|
}
|
|
32895
34400
|
return connection;
|
|
32896
34401
|
}
|
|
32897
|
-
function parseWhereClause2(whereClause) {
|
|
32898
|
-
if (!whereClause || whereClause.trim() === "") {
|
|
32899
|
-
throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
|
|
32900
|
-
}
|
|
32901
|
-
const conditions = {};
|
|
32902
|
-
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
32903
|
-
for (const part of andParts) {
|
|
32904
|
-
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
32905
|
-
if (!match || !match[1] || !match[2]) {
|
|
32906
|
-
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
32907
|
-
}
|
|
32908
|
-
const column = match[1];
|
|
32909
|
-
const trimmed = match[2].trim();
|
|
32910
|
-
const stripped = trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
32911
|
-
let value = stripped;
|
|
32912
|
-
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
32913
|
-
value = Number(stripped);
|
|
32914
|
-
}
|
|
32915
|
-
if (stripped === "true")
|
|
32916
|
-
value = true;
|
|
32917
|
-
else if (stripped === "false")
|
|
32918
|
-
value = false;
|
|
32919
|
-
else if (stripped === "null")
|
|
32920
|
-
value = null;
|
|
32921
|
-
conditions[column] = value;
|
|
32922
|
-
}
|
|
32923
|
-
return conditions;
|
|
32924
|
-
}
|
|
32925
34402
|
async function deleteCommand(table, options, command) {
|
|
32926
34403
|
try {
|
|
32927
34404
|
if (!table || table.trim() === "") {
|
|
32928
34405
|
throw new Error("Table name required");
|
|
32929
34406
|
}
|
|
32930
34407
|
table = table.trim();
|
|
32931
|
-
if (!options.where || options.where.trim() === "") {
|
|
34408
|
+
if (!options.plan && (!options.where || options.where.trim() === "")) {
|
|
32932
34409
|
throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
|
|
32933
34410
|
}
|
|
32934
34411
|
const configPath = resolveConfigPath(command, options);
|
|
@@ -32936,6 +34413,39 @@ async function deleteCommand(table, options, command) {
|
|
|
32936
34413
|
if (!config.connection) {
|
|
32937
34414
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
32938
34415
|
}
|
|
34416
|
+
if (options.plan) {
|
|
34417
|
+
if (options.dryRun) {
|
|
34418
|
+
console.error("--plan cannot be used with --dry-run");
|
|
34419
|
+
process.exit(1);
|
|
34420
|
+
return;
|
|
34421
|
+
}
|
|
34422
|
+
const rawWhere = options.where ?? "";
|
|
34423
|
+
let whereForPlan = null;
|
|
34424
|
+
if (rawWhere.trim() !== "") {
|
|
34425
|
+
try {
|
|
34426
|
+
const parsed = JSON.parse(rawWhere);
|
|
34427
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
34428
|
+
whereForPlan = parsed;
|
|
34429
|
+
}
|
|
34430
|
+
} catch {
|
|
34431
|
+
whereForPlan = null;
|
|
34432
|
+
}
|
|
34433
|
+
if (whereForPlan === null) {
|
|
34434
|
+
try {
|
|
34435
|
+
whereForPlan = parseWhereClause(rawWhere);
|
|
34436
|
+
} catch {
|
|
34437
|
+
whereForPlan = {};
|
|
34438
|
+
}
|
|
34439
|
+
}
|
|
34440
|
+
}
|
|
34441
|
+
await runDmlPlanAnalysis({
|
|
34442
|
+
operation: "delete",
|
|
34443
|
+
target: table,
|
|
34444
|
+
where: whereForPlan,
|
|
34445
|
+
rawWhere
|
|
34446
|
+
}, { format: options.format, config: options.config }, command);
|
|
34447
|
+
return;
|
|
34448
|
+
}
|
|
32939
34449
|
if (config.permission !== "data-admin" && config.permission !== "admin") {
|
|
32940
34450
|
throw new PermissionError(t("delete.admin_only"), {
|
|
32941
34451
|
type: "DELETE",
|
|
@@ -32953,7 +34463,7 @@ async function deleteCommand(table, options, command) {
|
|
|
32953
34463
|
try {
|
|
32954
34464
|
filter = JSON.parse(options.where);
|
|
32955
34465
|
} catch {
|
|
32956
|
-
filter =
|
|
34466
|
+
filter = parseWhereClause(options.where);
|
|
32957
34467
|
}
|
|
32958
34468
|
if (options.dryRun) {
|
|
32959
34469
|
const output = {
|
|
@@ -33001,7 +34511,7 @@ async function deleteCommand(table, options, command) {
|
|
|
33001
34511
|
try {
|
|
33002
34512
|
filter = JSON.parse(options.where);
|
|
33003
34513
|
} catch {
|
|
33004
|
-
filter =
|
|
34514
|
+
filter = parseWhereClause(options.where);
|
|
33005
34515
|
}
|
|
33006
34516
|
if (options.dryRun) {
|
|
33007
34517
|
const output = {
|
|
@@ -33032,7 +34542,7 @@ async function deleteCommand(table, options, command) {
|
|
|
33032
34542
|
}
|
|
33033
34543
|
let whereConditions;
|
|
33034
34544
|
try {
|
|
33035
|
-
whereConditions =
|
|
34545
|
+
whereConditions = parseWhereClause(options.where);
|
|
33036
34546
|
} catch (error) {
|
|
33037
34547
|
throw new Error(`WHERE clause parsing failed: ${error.message}`);
|
|
33038
34548
|
}
|
|
@@ -33115,7 +34625,7 @@ function requireSqlConnection6(connection) {
|
|
|
33115
34625
|
}
|
|
33116
34626
|
return connection;
|
|
33117
34627
|
}
|
|
33118
|
-
var
|
|
34628
|
+
var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
33119
34629
|
async function exportCommand(sql, options, command) {
|
|
33120
34630
|
try {
|
|
33121
34631
|
if (!sql || sql.trim() === "") {
|
|
@@ -33260,7 +34770,7 @@ async function redisExportBranch(command, options, config) {
|
|
|
33260
34770
|
}
|
|
33261
34771
|
}
|
|
33262
34772
|
async function mongoExportBranch(query, options, config) {
|
|
33263
|
-
if (
|
|
34773
|
+
if (SQL_PATTERN.test(query)) {
|
|
33264
34774
|
console.error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002");
|
|
33265
34775
|
console.error(`\u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
|
|
33266
34776
|
process.exit(1);
|
|
@@ -33380,6 +34890,11 @@ import * as path4 from "path";
|
|
|
33380
34890
|
import { homedir as homedir2 } from "os";
|
|
33381
34891
|
var SKILL_SOURCE_PATH = packageAssetPath("SKILL.md");
|
|
33382
34892
|
var REFERENCE_SOURCE_PATH = packageAssetPath("reference.md");
|
|
34893
|
+
function resolveSkillSource(lang) {
|
|
34894
|
+
if (lang === "zh-TW")
|
|
34895
|
+
return packageAssetPath("SKILL.zh-TW.md");
|
|
34896
|
+
return packageAssetPath("SKILL.md");
|
|
34897
|
+
}
|
|
33383
34898
|
var SUPPORTED_PLATFORMS = [
|
|
33384
34899
|
"claude",
|
|
33385
34900
|
"gemini",
|
|
@@ -33390,9 +34905,11 @@ var SUPPORTED_PLATFORMS = [
|
|
|
33390
34905
|
];
|
|
33391
34906
|
async function skillCommand(_program, options) {
|
|
33392
34907
|
try {
|
|
33393
|
-
const
|
|
34908
|
+
const lang = options.lang ?? "en";
|
|
34909
|
+
const skillSourcePath = resolveSkillSource(lang);
|
|
34910
|
+
const skillFile = Bun.file(skillSourcePath);
|
|
33394
34911
|
if (!await skillFile.exists()) {
|
|
33395
|
-
throw new Error(`Skill source not found: ${
|
|
34912
|
+
throw new Error(`Skill source not found: ${skillSourcePath}`);
|
|
33396
34913
|
}
|
|
33397
34914
|
const refFile = Bun.file(REFERENCE_SOURCE_PATH);
|
|
33398
34915
|
if (!await refFile.exists()) {
|
|
@@ -33480,15 +34997,15 @@ async function ensureDir(dirPath) {
|
|
|
33480
34997
|
await $`mkdir -p ${dirPath}`.quiet();
|
|
33481
34998
|
} catch {
|
|
33482
34999
|
try {
|
|
33483
|
-
const { mkdir:
|
|
33484
|
-
await
|
|
35000
|
+
const { mkdir: mkdir10 } = await import("fs/promises");
|
|
35001
|
+
await mkdir10(dirPath, { recursive: true });
|
|
33485
35002
|
} catch {
|
|
33486
35003
|
throw new Error(`Cannot create directory: ${dirPath}`);
|
|
33487
35004
|
}
|
|
33488
35005
|
}
|
|
33489
35006
|
}
|
|
33490
35007
|
function registerSkillCommand(program2) {
|
|
33491
|
-
return 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").action(async (options) => {
|
|
35008
|
+
return 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) => {
|
|
33492
35009
|
try {
|
|
33493
35010
|
await skillCommand(program2, options);
|
|
33494
35011
|
} catch (error) {
|
|
@@ -33512,15 +35029,15 @@ class AgentTaskError extends Error {
|
|
|
33512
35029
|
}
|
|
33513
35030
|
// src/core/agent-tasks/task-paths.ts
|
|
33514
35031
|
init_package_root();
|
|
33515
|
-
import { join as
|
|
35032
|
+
import { join as join21 } from "path";
|
|
33516
35033
|
function resolveBuiltinDir2() {
|
|
33517
35034
|
return packageAssetPath("tasks");
|
|
33518
35035
|
}
|
|
33519
35036
|
function resolveAgentTaskDirs(workspaceRoot) {
|
|
33520
35037
|
return {
|
|
33521
35038
|
builtinDir: resolveBuiltinDir2(),
|
|
33522
|
-
sharedDir:
|
|
33523
|
-
localDir:
|
|
35039
|
+
sharedDir: join21(workspaceRoot, ".dbcli-shared", "tasks"),
|
|
35040
|
+
localDir: join21(workspaceRoot, ".dbcli", "tasks")
|
|
33524
35041
|
};
|
|
33525
35042
|
}
|
|
33526
35043
|
// src/core/agent-tasks/parser.ts
|
|
@@ -33658,7 +35175,7 @@ function parseSteps(value, input) {
|
|
|
33658
35175
|
}
|
|
33659
35176
|
// src/core/agent-tasks/loader.ts
|
|
33660
35177
|
import { readdir as readdir2 } from "fs/promises";
|
|
33661
|
-
import { join as
|
|
35178
|
+
import { join as join22, relative as relative2, sep as sep2 } from "path";
|
|
33662
35179
|
async function loadAgentTasks(opts, flags) {
|
|
33663
35180
|
const errors3 = [];
|
|
33664
35181
|
const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
|
|
@@ -33715,7 +35232,7 @@ async function safeCollectMd(root) {
|
|
|
33715
35232
|
return;
|
|
33716
35233
|
}
|
|
33717
35234
|
for (const e of entries) {
|
|
33718
|
-
const full =
|
|
35235
|
+
const full = join22(dir, e.name);
|
|
33719
35236
|
if (e.isDirectory())
|
|
33720
35237
|
await walk(full);
|
|
33721
35238
|
else
|
|
@@ -34303,11 +35820,11 @@ function requireSqlConnection7(connection) {
|
|
|
34303
35820
|
}
|
|
34304
35821
|
return connection;
|
|
34305
35822
|
}
|
|
34306
|
-
var
|
|
35823
|
+
var ALLOWED_FORMATS6 = ["json", "table"];
|
|
34307
35824
|
var checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(checkAction);
|
|
34308
35825
|
async function checkAction(table, options) {
|
|
34309
35826
|
try {
|
|
34310
|
-
validateFormat(options.format,
|
|
35827
|
+
validateFormat(options.format, ALLOWED_FORMATS6, "check");
|
|
34311
35828
|
const config = await configModule.read(options.config);
|
|
34312
35829
|
if (!config.connection) {
|
|
34313
35830
|
console.error("Database not configured. Run: dbcli init");
|
|
@@ -34440,7 +35957,7 @@ function requireSqlConnection8(connection) {
|
|
|
34440
35957
|
}
|
|
34441
35958
|
return connection;
|
|
34442
35959
|
}
|
|
34443
|
-
var
|
|
35960
|
+
var ALLOWED_FORMATS7 = ["json", "table"];
|
|
34444
35961
|
function compareSnapshots(before, after) {
|
|
34445
35962
|
const beforeTables = new Set(Object.keys(before.tables));
|
|
34446
35963
|
const afterTables = new Set(Object.keys(after.tables));
|
|
@@ -34534,7 +36051,7 @@ function compareSnapshots(before, after) {
|
|
|
34534
36051
|
var diffCommand = new Command().name("diff").description("Compare schema snapshots to detect changes").option("--snapshot <path>", "Save current schema snapshot to file").option("--against <path>", "Compare current schema against a snapshot file").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(diffAction);
|
|
34535
36052
|
async function diffAction(options) {
|
|
34536
36053
|
try {
|
|
34537
|
-
validateFormat(options.format,
|
|
36054
|
+
validateFormat(options.format, ALLOWED_FORMATS7, "diff");
|
|
34538
36055
|
if (!options.snapshot && !options.against) {
|
|
34539
36056
|
console.error("Specify --snapshot <path> to save, or --against <path> to compare");
|
|
34540
36057
|
process.exit(1);
|
|
@@ -34643,10 +36160,10 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
|
|
|
34643
36160
|
// src/commands/status.ts
|
|
34644
36161
|
init_config();
|
|
34645
36162
|
init_validation();
|
|
34646
|
-
var
|
|
36163
|
+
var ALLOWED_FORMATS8 = ["text", "json"];
|
|
34647
36164
|
var statusCommand = new Command("status").description("Show current configuration status (safe for AI agents, no credentials exposed)").option("--format <type>", "Output format: text, json", "json").action(async (options) => {
|
|
34648
36165
|
try {
|
|
34649
|
-
validateFormat(options.format,
|
|
36166
|
+
validateFormat(options.format, ALLOWED_FORMATS8, "status");
|
|
34650
36167
|
const configPath = resolveConfigPath(statusCommand);
|
|
34651
36168
|
const config = await configModule.read(configPath);
|
|
34652
36169
|
if (!config.connection) {
|
|
@@ -34681,16 +36198,21 @@ var statusCommand = new Command("status").description("Show current configuratio
|
|
|
34681
36198
|
|
|
34682
36199
|
// src/commands/inspect.ts
|
|
34683
36200
|
init_message_loader();
|
|
36201
|
+
import crypto2 from "crypto";
|
|
34684
36202
|
init_validation();
|
|
34685
36203
|
init_inspect();
|
|
34686
|
-
|
|
36204
|
+
init_config();
|
|
36205
|
+
init_integration_helper();
|
|
36206
|
+
var ALLOWED_FORMATS9 = ["json", "markdown"];
|
|
34687
36207
|
var inspectCommand = new Command().name("inspect").description(t("inspect.description")).option("--format <format>", "Output format: json (default) or markdown", "json").option("--brief", "Trim samples / intents / commands for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--no-connect", "Skip the database connect + version probe").option("--probe-timeout <ms>", "Hard timeout for cheap version/object probe (default 1500)", (v) => parseInt(v, 10), 1500).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).option("--require-schema-cache", "Throw SCHEMA_CACHE_MISSING (recovery code) when the active SQL connection has no usable schema cache", false).action(async (options, command) => {
|
|
36208
|
+
let config;
|
|
34688
36209
|
try {
|
|
34689
36210
|
const forAgent = options.forAgent === true;
|
|
34690
36211
|
const format = forAgent ? "json" : options.format;
|
|
34691
36212
|
const brief = forAgent || options.brief === true;
|
|
34692
|
-
validateFormat(format,
|
|
36213
|
+
validateFormat(format, ALLOWED_FORMATS9, "inspect");
|
|
34693
36214
|
const configPath = resolveConfigPath(command, options);
|
|
36215
|
+
config = await configModule.read(configPath);
|
|
34694
36216
|
const snap = await collectInspect({
|
|
34695
36217
|
workspace: process.cwd(),
|
|
34696
36218
|
configPath,
|
|
@@ -34702,12 +36224,40 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
|
|
|
34702
36224
|
const { requireSchemaCacheOrThrow: requireSchemaCacheOrThrow2 } = await Promise.resolve().then(() => (init_inspect(), exports_inspect));
|
|
34703
36225
|
requireSchemaCacheOrThrow2(snap.schemaCache, snap.system ?? null);
|
|
34704
36226
|
}
|
|
36227
|
+
if (config) {
|
|
36228
|
+
const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
36229
|
+
if (shouldEmbedRecent2({ forAgent, format })) {
|
|
36230
|
+
snap.audit_recent = await loadRecentAudit2(config, configPath);
|
|
36231
|
+
}
|
|
36232
|
+
}
|
|
34705
36233
|
const out = format === "markdown" ? renderMarkdown2(snap, { brief }) : renderJson2(snap, { brief });
|
|
34706
36234
|
console.log(out);
|
|
36235
|
+
if (config) {
|
|
36236
|
+
await writeAuditEntry(config, "inspect", options, {
|
|
36237
|
+
success: true,
|
|
36238
|
+
target: "*"
|
|
36239
|
+
});
|
|
36240
|
+
}
|
|
34707
36241
|
} catch (err) {
|
|
36242
|
+
let auditId = null;
|
|
36243
|
+
let envelopeId;
|
|
34708
36244
|
if (options.recovery === true) {
|
|
36245
|
+
envelopeId = crypto2.randomUUID();
|
|
36246
|
+
}
|
|
36247
|
+
if (config) {
|
|
36248
|
+
auditId = await writeAuditEntry(config, "inspect", options, {
|
|
36249
|
+
success: false,
|
|
36250
|
+
target: "*",
|
|
36251
|
+
error: err,
|
|
36252
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
36253
|
+
});
|
|
36254
|
+
}
|
|
36255
|
+
if (envelopeId !== undefined) {
|
|
34709
36256
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34710
|
-
emitRecoveryEnvelope2(err, { operation: "inspect" }
|
|
36257
|
+
emitRecoveryEnvelope2(err, { operation: "inspect" }, {
|
|
36258
|
+
envelopeId,
|
|
36259
|
+
auditRef: auditId ?? undefined
|
|
36260
|
+
});
|
|
34711
36261
|
}
|
|
34712
36262
|
console.error(err.message);
|
|
34713
36263
|
process.exit(1);
|
|
@@ -34717,6 +36267,8 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
|
|
|
34717
36267
|
// src/commands/report.ts
|
|
34718
36268
|
init_message_loader();
|
|
34719
36269
|
init_validation();
|
|
36270
|
+
init_config();
|
|
36271
|
+
init_integration_helper();
|
|
34720
36272
|
|
|
34721
36273
|
// src/core/report/types.ts
|
|
34722
36274
|
var REPORT_SCHEMA_VERSION = 1;
|
|
@@ -34971,10 +36523,10 @@ function finalize(input) {
|
|
|
34971
36523
|
context: input.context,
|
|
34972
36524
|
sections: input.sections,
|
|
34973
36525
|
warnings: input.warnings,
|
|
34974
|
-
suggestedCommands:
|
|
36526
|
+
suggestedCommands: buildSuggestedCommands5(input.context, input.sections)
|
|
34975
36527
|
};
|
|
34976
36528
|
}
|
|
34977
|
-
function
|
|
36529
|
+
function buildSuggestedCommands5(context, sections) {
|
|
34978
36530
|
if (!context.system)
|
|
34979
36531
|
return ["dbcli init"];
|
|
34980
36532
|
const out = ["dbcli inspect --format json"];
|
|
@@ -35092,7 +36644,7 @@ function formatCell(value) {
|
|
|
35092
36644
|
return String(value);
|
|
35093
36645
|
}
|
|
35094
36646
|
// src/commands/report.ts
|
|
35095
|
-
var
|
|
36647
|
+
var ALLOWED_FORMATS10 = ["json", "markdown"];
|
|
35096
36648
|
function parseSections(value) {
|
|
35097
36649
|
const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
35098
36650
|
for (const p of parts) {
|
|
@@ -35103,12 +36655,14 @@ function parseSections(value) {
|
|
|
35103
36655
|
return parts;
|
|
35104
36656
|
}
|
|
35105
36657
|
var reportCommand = new Command().name("report").description(t("report.description")).option("--format <format>", "Output format: json (default) or markdown", "json").option("--section <list>", `Comma-separated subset of [${ALLOWED_SECTIONS.join(", ")}] (default: all)`, parseSections).option("--brief", "Trim evidence rows for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--no-connect", "Emit context-only snapshot (skip diagnostics)").option("--per-snippet-timeout <ms>", "Hard timeout per diagnostic snippet (default 3000)", (v) => parseInt(v, 10), 3000).option("--max-rows-per-evidence <n>", "Cap rows kept per evidence (default 50)", (v) => parseInt(v, 10), 50).option("--probe-timeout <ms>", "Inspect probe timeout for cheap version/object check (default 1500)", (v) => parseInt(v, 10), 1500).action(async (options, command) => {
|
|
36658
|
+
let config;
|
|
35106
36659
|
try {
|
|
35107
36660
|
const forAgent = options.forAgent === true;
|
|
35108
36661
|
const format = forAgent ? "json" : options.format;
|
|
35109
36662
|
const brief = forAgent || options.brief === true;
|
|
35110
|
-
validateFormat(format,
|
|
36663
|
+
validateFormat(format, ALLOWED_FORMATS10, "report");
|
|
35111
36664
|
const configPath = resolveConfigPath(command, options);
|
|
36665
|
+
config = await configModule.read(configPath);
|
|
35112
36666
|
const snap = await collectReport({
|
|
35113
36667
|
workspace: process.cwd(),
|
|
35114
36668
|
configPath,
|
|
@@ -35121,7 +36675,23 @@ var reportCommand = new Command().name("report").description(t("report.descripti
|
|
|
35121
36675
|
});
|
|
35122
36676
|
const out = format === "markdown" ? renderMarkdown3(snap, { brief }) : renderJson3(snap, { brief });
|
|
35123
36677
|
console.log(out);
|
|
36678
|
+
if (config) {
|
|
36679
|
+
await writeAuditEntry(config, "report", options, {
|
|
36680
|
+
success: true,
|
|
36681
|
+
target: "*",
|
|
36682
|
+
metadata: {
|
|
36683
|
+
sections: options.section || ALLOWED_SECTIONS
|
|
36684
|
+
}
|
|
36685
|
+
});
|
|
36686
|
+
}
|
|
35124
36687
|
} catch (err) {
|
|
36688
|
+
if (config) {
|
|
36689
|
+
await writeAuditEntry(config, "report", options, {
|
|
36690
|
+
success: false,
|
|
36691
|
+
target: "*",
|
|
36692
|
+
error: err
|
|
36693
|
+
});
|
|
36694
|
+
}
|
|
35125
36695
|
console.error(err.message);
|
|
35126
36696
|
process.exit(1);
|
|
35127
36697
|
}
|
|
@@ -35130,6 +36700,8 @@ var reportCommand = new Command().name("report").description(t("report.descripti
|
|
|
35130
36700
|
// src/commands/guide.ts
|
|
35131
36701
|
init_message_loader();
|
|
35132
36702
|
init_validation();
|
|
36703
|
+
init_config();
|
|
36704
|
+
init_integration_helper();
|
|
35133
36705
|
|
|
35134
36706
|
// src/core/guide/types.ts
|
|
35135
36707
|
var GUIDE_SCHEMA_VERSION = 1;
|
|
@@ -35471,7 +37043,7 @@ function renderGoalList() {
|
|
|
35471
37043
|
`);
|
|
35472
37044
|
}
|
|
35473
37045
|
// src/commands/guide.ts
|
|
35474
|
-
var
|
|
37046
|
+
var ALLOWED_FORMATS11 = ["json", "markdown"];
|
|
35475
37047
|
function parseGoal(value) {
|
|
35476
37048
|
const normalized = value.trim();
|
|
35477
37049
|
if (!ALLOWED_GOALS.includes(normalized)) {
|
|
@@ -35480,11 +37052,12 @@ function parseGoal(value) {
|
|
|
35480
37052
|
return normalized;
|
|
35481
37053
|
}
|
|
35482
37054
|
var guideCommand = new Command().name("guide").description(t("guide.description")).argument("[goal]", `Guide goal ID; one of [${ALLOWED_GOALS.join(", ")}]`).option("--format <format>", "Output format: json (default) or markdown", "json").option("--brief", "Trim rationale/expects for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--list", "List available guide goals and exit", false).option("--probe", "Refresh inspect context via live probe (default: cache-first)", false).option("--probe-timeout <ms>", "Inspect probe timeout for cheap version/object check (default 1500)", (v) => parseInt(v, 10), 1500).action(async (goal, options, command) => {
|
|
37055
|
+
let config;
|
|
35483
37056
|
try {
|
|
35484
37057
|
const forAgent = options.forAgent === true;
|
|
35485
37058
|
const format = forAgent ? "json" : options.format;
|
|
35486
37059
|
const brief = forAgent || options.brief === true;
|
|
35487
|
-
validateFormat(format,
|
|
37060
|
+
validateFormat(format, ALLOWED_FORMATS11, "guide");
|
|
35488
37061
|
if (options.list === true) {
|
|
35489
37062
|
if (format === "markdown") {
|
|
35490
37063
|
console.log(renderGoalList());
|
|
@@ -35503,6 +37076,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35503
37076
|
}
|
|
35504
37077
|
const validated = parseGoal(goal);
|
|
35505
37078
|
const configPath = resolveConfigPath(command, options);
|
|
37079
|
+
config = await configModule.read(configPath);
|
|
35506
37080
|
const snap = await collectGuide({
|
|
35507
37081
|
workspace: process.cwd(),
|
|
35508
37082
|
configPath,
|
|
@@ -35511,9 +37085,28 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35511
37085
|
brief,
|
|
35512
37086
|
probeTimeoutMs: options.probeTimeout
|
|
35513
37087
|
});
|
|
37088
|
+
if (config) {
|
|
37089
|
+
const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
37090
|
+
if (shouldEmbedRecent2({ forAgent, format })) {
|
|
37091
|
+
snap.audit_recent = await loadRecentAudit2(config, configPath);
|
|
37092
|
+
}
|
|
37093
|
+
}
|
|
35514
37094
|
const out = format === "markdown" ? renderMarkdown4(snap, { brief }) : renderJson4(snap, { brief });
|
|
35515
37095
|
console.log(out);
|
|
37096
|
+
if (config) {
|
|
37097
|
+
await writeAuditEntry(config, "guide", options, {
|
|
37098
|
+
success: true,
|
|
37099
|
+
target: goal
|
|
37100
|
+
});
|
|
37101
|
+
}
|
|
35516
37102
|
} catch (err) {
|
|
37103
|
+
if (config) {
|
|
37104
|
+
await writeAuditEntry(config, "guide", options, {
|
|
37105
|
+
success: false,
|
|
37106
|
+
target: goal || "*",
|
|
37107
|
+
error: err
|
|
37108
|
+
});
|
|
37109
|
+
}
|
|
35517
37110
|
console.error(err.message);
|
|
35518
37111
|
process.exit(1);
|
|
35519
37112
|
}
|
|
@@ -35523,7 +37116,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35523
37116
|
init_message_loader();
|
|
35524
37117
|
init_validation();
|
|
35525
37118
|
init_recovery();
|
|
35526
|
-
var
|
|
37119
|
+
var ALLOWED_FORMATS12 = ["json", "markdown"];
|
|
35527
37120
|
function parseCode(value) {
|
|
35528
37121
|
const normalized = value.trim();
|
|
35529
37122
|
if (!RECOVERY_CODES.includes(normalized)) {
|
|
@@ -35559,7 +37152,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
|
|
|
35559
37152
|
const forAgent = options.forAgent === true;
|
|
35560
37153
|
const format = forAgent ? "json" : options.format;
|
|
35561
37154
|
const brief = forAgent || options.brief === true;
|
|
35562
|
-
validateFormat(format,
|
|
37155
|
+
validateFormat(format, ALLOWED_FORMATS12, "recovery");
|
|
35563
37156
|
if (options.list === true) {
|
|
35564
37157
|
if (format === "markdown") {
|
|
35565
37158
|
console.log(renderCodeList());
|
|
@@ -35600,7 +37193,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
|
|
|
35600
37193
|
// src/commands/recover.ts
|
|
35601
37194
|
init_recovery();
|
|
35602
37195
|
init_last_envelope();
|
|
35603
|
-
import { stat as
|
|
37196
|
+
import { stat as stat6 } from "fs/promises";
|
|
35604
37197
|
import { resolve as resolve4 } from "path";
|
|
35605
37198
|
|
|
35606
37199
|
// src/core/recovery/envelope-schema.ts
|
|
@@ -35652,6 +37245,8 @@ var recoveryEnvelopeSchema = exports_external.object({
|
|
|
35652
37245
|
}).strict();
|
|
35653
37246
|
var savedRecoveryEnvelopeSchema = exports_external.object({
|
|
35654
37247
|
schemaVersion: exports_external.literal(1),
|
|
37248
|
+
id: exports_external.string().optional(),
|
|
37249
|
+
audit_ref: exports_external.string().optional(),
|
|
35655
37250
|
savedAt: exports_external.string().min(1),
|
|
35656
37251
|
command: exports_external.string(),
|
|
35657
37252
|
cwd: exports_external.string().min(1),
|
|
@@ -35682,7 +37277,7 @@ function looksLikeSavedEnvelope(x) {
|
|
|
35682
37277
|
// src/commands/recover.ts
|
|
35683
37278
|
init_next_step_schema();
|
|
35684
37279
|
init_validation();
|
|
35685
|
-
var
|
|
37280
|
+
var ALLOWED_FORMATS13 = ["json", "markdown"];
|
|
35686
37281
|
var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
|
|
35687
37282
|
|
|
35688
37283
|
class RecoverCliError extends Error {
|
|
@@ -35716,7 +37311,7 @@ async function resolveApplySource(opts) {
|
|
|
35716
37311
|
}
|
|
35717
37312
|
const saved2 = r3.value;
|
|
35718
37313
|
try {
|
|
35719
|
-
await
|
|
37314
|
+
await stat6(saved2.cwd);
|
|
35720
37315
|
} catch {
|
|
35721
37316
|
throw new RecoverCliError(`--from ${opts.from}: saved cwd '${saved2.cwd}' no longer exists.`, EXIT_CODE.malformed);
|
|
35722
37317
|
}
|
|
@@ -35750,7 +37345,7 @@ async function resolveApplySource(opts) {
|
|
|
35750
37345
|
}
|
|
35751
37346
|
const saved = r.value;
|
|
35752
37347
|
try {
|
|
35753
|
-
await
|
|
37348
|
+
await stat6(saved.cwd);
|
|
35754
37349
|
} catch {
|
|
35755
37350
|
throw new RecoverCliError(`Auto-saved ${LAST_ENVELOPE_PATH} references cwd '${saved.cwd}' that no longer exists.`, EXIT_CODE.malformed);
|
|
35756
37351
|
}
|
|
@@ -35824,7 +37419,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35824
37419
|
}
|
|
35825
37420
|
const explicitFormat = options.format;
|
|
35826
37421
|
const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
|
|
35827
|
-
validateFormat(format,
|
|
37422
|
+
validateFormat(format, ALLOWED_FORMATS13, "recover");
|
|
35828
37423
|
let allowWrite = "none";
|
|
35829
37424
|
if (options.next !== true) {
|
|
35830
37425
|
const allowWriteRaw = options.allowWrite;
|
|
@@ -35839,15 +37434,32 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35839
37434
|
from: options.from,
|
|
35840
37435
|
cwd: process.cwd()
|
|
35841
37436
|
});
|
|
37437
|
+
let audit_recent = [];
|
|
37438
|
+
if (format === "json" && options.next !== true) {
|
|
37439
|
+
try {
|
|
37440
|
+
const { configModule: configModule2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
37441
|
+
const configPath = `${process.cwd()}/.dbcli`;
|
|
37442
|
+
const config = await configModule2.read(configPath);
|
|
37443
|
+
if (config) {
|
|
37444
|
+
const { loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
37445
|
+
audit_recent = await loadRecentAudit2(config, configPath);
|
|
37446
|
+
}
|
|
37447
|
+
} catch {
|
|
37448
|
+
audit_recent = [];
|
|
37449
|
+
}
|
|
37450
|
+
}
|
|
35842
37451
|
if (options.next === true) {
|
|
35843
37452
|
const result2 = await runNext(options, source);
|
|
35844
|
-
const
|
|
35845
|
-
console.log(
|
|
37453
|
+
const out = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
|
|
37454
|
+
console.log(out);
|
|
35846
37455
|
return;
|
|
35847
37456
|
}
|
|
35848
37457
|
if (options.apply !== true) {
|
|
35849
|
-
|
|
35850
|
-
|
|
37458
|
+
if (format === "markdown") {
|
|
37459
|
+
console.log(renderMarkdown(source.envelope));
|
|
37460
|
+
} else {
|
|
37461
|
+
console.log(JSON.stringify({ ...source.envelope, audit_recent }, null, 2));
|
|
37462
|
+
}
|
|
35851
37463
|
return;
|
|
35852
37464
|
}
|
|
35853
37465
|
const noVerify = options.verify === false;
|
|
@@ -35856,8 +37468,11 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35856
37468
|
cwd: source.cwd,
|
|
35857
37469
|
source: { kind: source.kind, path: source.path }
|
|
35858
37470
|
}, { allowWrite, noVerify });
|
|
35859
|
-
|
|
35860
|
-
|
|
37471
|
+
if (format === "markdown") {
|
|
37472
|
+
console.log(renderApplyMarkdown(result));
|
|
37473
|
+
} else {
|
|
37474
|
+
console.log(JSON.stringify({ ...result, audit_recent }, null, 2));
|
|
37475
|
+
}
|
|
35861
37476
|
process.exit(exitCodeFor(result.finalStatus));
|
|
35862
37477
|
} catch (err) {
|
|
35863
37478
|
if (err instanceof RecoverCliError) {
|
|
@@ -35869,6 +37484,440 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35869
37484
|
}
|
|
35870
37485
|
});
|
|
35871
37486
|
|
|
37487
|
+
// src/commands/audit.ts
|
|
37488
|
+
import { rm as rm3, stat as stat7 } from "fs/promises";
|
|
37489
|
+
import { join as join25 } from "path";
|
|
37490
|
+
init_message_loader();
|
|
37491
|
+
init_validation();
|
|
37492
|
+
init_config();
|
|
37493
|
+
init_config_binding();
|
|
37494
|
+
init_reader();
|
|
37495
|
+
init_integration_helper();
|
|
37496
|
+
var ALLOWED_FORMATS14 = ["table", "json"];
|
|
37497
|
+
var DEFAULT_TAIL_N = 10;
|
|
37498
|
+
var MAX_TAIL_N = 1e4;
|
|
37499
|
+
var SHORT_ID_LEN = 8;
|
|
37500
|
+
var MISSING_PLACEHOLDER = "\u2014";
|
|
37501
|
+
var PREFIX_MIN = 4;
|
|
37502
|
+
async function resolveAuditPaths(configPath, config) {
|
|
37503
|
+
const storagePath = await resolveConfigStoragePath(configPath);
|
|
37504
|
+
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
37505
|
+
const auditDir = join25(storagePath, ".dbcli", "audit");
|
|
37506
|
+
const auditFile = join25(auditDir, `${connName}.jsonl`);
|
|
37507
|
+
return { auditDir, connectionName: connName, auditFile };
|
|
37508
|
+
}
|
|
37509
|
+
function isAuditDisabled(config) {
|
|
37510
|
+
return config?.audit?.enabled === false;
|
|
37511
|
+
}
|
|
37512
|
+
function emitDisabledAndExit0() {
|
|
37513
|
+
console.error(t("audit.disabled_hint"));
|
|
37514
|
+
process.exit(0);
|
|
37515
|
+
}
|
|
37516
|
+
function parseTailN(raw) {
|
|
37517
|
+
const requested = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
37518
|
+
if (!Number.isFinite(requested) || !Number.isInteger(requested) || requested <= 0) {
|
|
37519
|
+
console.error(t("audit.n_must_be_positive"));
|
|
37520
|
+
process.exit(1);
|
|
37521
|
+
}
|
|
37522
|
+
if (requested > MAX_TAIL_N) {
|
|
37523
|
+
console.error(t_vars("audit.n_capped_warning", {
|
|
37524
|
+
requested: String(requested),
|
|
37525
|
+
max: String(MAX_TAIL_N)
|
|
37526
|
+
}));
|
|
37527
|
+
return MAX_TAIL_N;
|
|
37528
|
+
}
|
|
37529
|
+
return requested;
|
|
37530
|
+
}
|
|
37531
|
+
function briefify(entry) {
|
|
37532
|
+
return {
|
|
37533
|
+
ts: entry.ts,
|
|
37534
|
+
command: entry.command,
|
|
37535
|
+
target: entry.target,
|
|
37536
|
+
success: entry.success
|
|
37537
|
+
};
|
|
37538
|
+
}
|
|
37539
|
+
function shortId(id) {
|
|
37540
|
+
if (!id)
|
|
37541
|
+
return MISSING_PLACEHOLDER;
|
|
37542
|
+
return id.length <= SHORT_ID_LEN ? id : id.slice(0, SHORT_ID_LEN);
|
|
37543
|
+
}
|
|
37544
|
+
function renderTable(rows, headers) {
|
|
37545
|
+
const allRows = [headers, ...rows];
|
|
37546
|
+
const widths = headers.map((_, col) => Math.max(...allRows.map((r) => (r[col] ?? "").length)));
|
|
37547
|
+
const fmt = (r) => r.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
37548
|
+
const sep3 = widths.map((w) => "-".repeat(w)).join(" ").trimEnd();
|
|
37549
|
+
return [fmt(headers), sep3, ...rows.map(fmt)].join(`
|
|
37550
|
+
`);
|
|
37551
|
+
}
|
|
37552
|
+
function renderTailTable(entries) {
|
|
37553
|
+
const headers = ["ts", "command", "target", "tier", "success", "id", "recovery_ref"];
|
|
37554
|
+
const rows = entries.map((e) => [
|
|
37555
|
+
e.ts,
|
|
37556
|
+
e.command,
|
|
37557
|
+
e.target,
|
|
37558
|
+
e.side_effect_tier,
|
|
37559
|
+
String(e.success),
|
|
37560
|
+
shortId(e.id),
|
|
37561
|
+
shortId(e.recovery_ref)
|
|
37562
|
+
]);
|
|
37563
|
+
return renderTable(rows, headers);
|
|
37564
|
+
}
|
|
37565
|
+
function renderTailAllTable(envelopes) {
|
|
37566
|
+
const headers = ["connection", "ts", "command", "target", "tier", "success", "id", "recovery_ref"];
|
|
37567
|
+
const rows = envelopes.map(({ connection, entry: e }) => [
|
|
37568
|
+
connection,
|
|
37569
|
+
e.ts,
|
|
37570
|
+
e.command,
|
|
37571
|
+
e.target,
|
|
37572
|
+
e.side_effect_tier,
|
|
37573
|
+
String(e.success),
|
|
37574
|
+
shortId(e.id),
|
|
37575
|
+
shortId(e.recovery_ref)
|
|
37576
|
+
]);
|
|
37577
|
+
return renderTable(rows, headers);
|
|
37578
|
+
}
|
|
37579
|
+
function briefifyShow(entry) {
|
|
37580
|
+
const { metadata, redacted_query, ...rest } = entry;
|
|
37581
|
+
return rest;
|
|
37582
|
+
}
|
|
37583
|
+
function renderEntryTable(entry) {
|
|
37584
|
+
const lines = [];
|
|
37585
|
+
if (entry.id !== undefined)
|
|
37586
|
+
lines.push(`Id: ${entry.id}`);
|
|
37587
|
+
if (entry.ts !== undefined)
|
|
37588
|
+
lines.push(`Ts: ${entry.ts}`);
|
|
37589
|
+
if (entry.session_id !== undefined)
|
|
37590
|
+
lines.push(`Session id: ${entry.session_id}`);
|
|
37591
|
+
if (entry.engine !== undefined)
|
|
37592
|
+
lines.push(`Engine: ${entry.engine}`);
|
|
37593
|
+
if (entry.command !== undefined)
|
|
37594
|
+
lines.push(`Command: ${entry.command}`);
|
|
37595
|
+
if (entry.side_effect_tier !== undefined)
|
|
37596
|
+
lines.push(`Side effect tier: ${entry.side_effect_tier}`);
|
|
37597
|
+
if (entry.target !== undefined)
|
|
37598
|
+
lines.push(`Target: ${entry.target}`);
|
|
37599
|
+
if (entry.success !== undefined)
|
|
37600
|
+
lines.push(`Success: ${entry.success}`);
|
|
37601
|
+
if (entry.recovery_ref !== undefined)
|
|
37602
|
+
lines.push(`Recovery ref: ${entry.recovery_ref}`);
|
|
37603
|
+
if (entry.redacted_query !== undefined)
|
|
37604
|
+
lines.push(`Redacted query: ${entry.redacted_query}`);
|
|
37605
|
+
if (entry.redacted_sql !== undefined)
|
|
37606
|
+
lines.push(`Redacted SQL: ${entry.redacted_sql}`);
|
|
37607
|
+
if (entry.error !== undefined)
|
|
37608
|
+
lines.push(`Error: ${entry.error}`);
|
|
37609
|
+
if (entry.metadata !== undefined)
|
|
37610
|
+
lines.push(`Metadata: ${JSON.stringify(entry.metadata)}`);
|
|
37611
|
+
return lines.join(`
|
|
37612
|
+
`);
|
|
37613
|
+
}
|
|
37614
|
+
function renderShowResult(hit, opts) {
|
|
37615
|
+
if (opts.format === "json") {
|
|
37616
|
+
const entryView = opts.brief ? briefifyShow(hit.entry) : hit.entry;
|
|
37617
|
+
const payload = opts.all ? { connection: hit.connection, entry: entryView } : entryView;
|
|
37618
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37619
|
+
} else {
|
|
37620
|
+
if (opts.all)
|
|
37621
|
+
console.log(`Connection: ${hit.connection}`);
|
|
37622
|
+
console.log(renderEntryTable(opts.brief ? briefifyShow(hit.entry) : hit.entry));
|
|
37623
|
+
}
|
|
37624
|
+
}
|
|
37625
|
+
function briefifyHealth(h) {
|
|
37626
|
+
return { enabled: h.enabled, lastWrite: h.lastWrite, rotationUsage: h.rotationUsage };
|
|
37627
|
+
}
|
|
37628
|
+
function formatBytes(n) {
|
|
37629
|
+
if (n < 1024)
|
|
37630
|
+
return `${n} B`;
|
|
37631
|
+
if (n < 1024 * 1024)
|
|
37632
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
37633
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
37634
|
+
}
|
|
37635
|
+
function renderHealthTable(h) {
|
|
37636
|
+
const sizePct = Math.round(h.rotationUsage.bytes.pct);
|
|
37637
|
+
const entriesPct = Math.round(h.rotationUsage.entries.pct);
|
|
37638
|
+
const lastWrite = h.lastWrite ? `${h.lastWrite.ts} (${h.lastWrite.success ? "success" : "failed"}${h.lastWrite.error ? `: ${h.lastWrite.error}` : ""})` : MISSING_PLACEHOLDER;
|
|
37639
|
+
const lastError = h.lastError ? `${h.lastError.ts} ${h.lastError.message}` : MISSING_PLACEHOLDER;
|
|
37640
|
+
const lastRotation = h.rotation.lastRotatedAt ? `${h.rotation.lastRotatedAt} (${h.rotation.previousFile ?? MISSING_PLACEHOLDER})` : MISSING_PLACEHOLDER;
|
|
37641
|
+
return [
|
|
37642
|
+
`Enabled: ${h.enabled}`,
|
|
37643
|
+
`File: ${h.currentFile}`,
|
|
37644
|
+
`Size: ${formatBytes(h.currentSizeBytes)} / ${formatBytes(h.rotationUsage.bytes.max)} (${sizePct}%)`,
|
|
37645
|
+
`Entries: ${h.currentEntryCount} / ${h.rotationUsage.entries.max} (${entriesPct}%)`,
|
|
37646
|
+
`Lock: ${h.lock.state}${h.lock.heldByPid !== undefined ? ` (pid ${h.lock.heldByPid})` : ""}`,
|
|
37647
|
+
`Last write: ${lastWrite}`,
|
|
37648
|
+
`Last error: ${lastError}`,
|
|
37649
|
+
`Session id: ${h.sessionId ?? MISSING_PLACEHOLDER}`,
|
|
37650
|
+
`Last rotation: ${lastRotation}`
|
|
37651
|
+
].join(`
|
|
37652
|
+
`);
|
|
37653
|
+
}
|
|
37654
|
+
function readLineFromStdinWithStderrPrompt(prompt) {
|
|
37655
|
+
return new Promise((resolve5) => {
|
|
37656
|
+
process.stderr.write(prompt);
|
|
37657
|
+
const chunks = [];
|
|
37658
|
+
let data = "";
|
|
37659
|
+
const onData = (chunk) => {
|
|
37660
|
+
chunks.push(chunk);
|
|
37661
|
+
data = Buffer.concat(chunks).toString();
|
|
37662
|
+
const lines = data.split(`
|
|
37663
|
+
`);
|
|
37664
|
+
if (lines.length > 1) {
|
|
37665
|
+
process.stdin.pause();
|
|
37666
|
+
process.stdin.removeListener("data", onData);
|
|
37667
|
+
process.stdin.removeListener("end", onEnd);
|
|
37668
|
+
resolve5((lines[0] ?? "").trim());
|
|
37669
|
+
}
|
|
37670
|
+
};
|
|
37671
|
+
const onEnd = () => {
|
|
37672
|
+
process.stdin.removeListener("data", onData);
|
|
37673
|
+
resolve5(data.trim());
|
|
37674
|
+
};
|
|
37675
|
+
process.stdin.on("data", onData);
|
|
37676
|
+
process.stdin.on("end", onEnd);
|
|
37677
|
+
process.stdin.resume();
|
|
37678
|
+
});
|
|
37679
|
+
}
|
|
37680
|
+
async function statAuditFile(file) {
|
|
37681
|
+
try {
|
|
37682
|
+
const s = await stat7(file);
|
|
37683
|
+
const entries = (await readEntries(file)).length;
|
|
37684
|
+
return { entries, size: formatBytes(s.size) };
|
|
37685
|
+
} catch {
|
|
37686
|
+
return null;
|
|
37687
|
+
}
|
|
37688
|
+
}
|
|
37689
|
+
var auditCommand = new Command("audit").description(t("audit.description"));
|
|
37690
|
+
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_FORMATS14.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) => {
|
|
37691
|
+
const forAgent = options.forAgent === true;
|
|
37692
|
+
const format = forAgent ? "json" : options.format;
|
|
37693
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
37694
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
37695
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
37696
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit tail");
|
|
37697
|
+
const n = parseTailN(options.n);
|
|
37698
|
+
const configPath = resolveConfigPath(command, options);
|
|
37699
|
+
const config = await configModule.read(configPath);
|
|
37700
|
+
if (isAuditDisabled(config))
|
|
37701
|
+
emitDisabledAndExit0();
|
|
37702
|
+
const { auditDir, auditFile } = await resolveAuditPaths(configPath, config);
|
|
37703
|
+
if (options.all === true) {
|
|
37704
|
+
const conns = await discoverConnections(auditDir);
|
|
37705
|
+
const byConn = new Map;
|
|
37706
|
+
for (const c of conns) {
|
|
37707
|
+
const merged = [];
|
|
37708
|
+
for (const f of c.files) {
|
|
37709
|
+
const part = await readEntries(f);
|
|
37710
|
+
merged.push(...part);
|
|
37711
|
+
}
|
|
37712
|
+
byConn.set(c.connection, merged);
|
|
37713
|
+
}
|
|
37714
|
+
const envelopes = mergeByTimestamp(byConn).slice(-n);
|
|
37715
|
+
if (format === "json") {
|
|
37716
|
+
const payload = envelopes.map((e) => ({
|
|
37717
|
+
connection: e.connection,
|
|
37718
|
+
entry: brief ? briefify(e.entry) : e.entry
|
|
37719
|
+
}));
|
|
37720
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37721
|
+
} else if (envelopes.length === 0) {
|
|
37722
|
+
console.error(t("audit.no_entries"));
|
|
37723
|
+
} else {
|
|
37724
|
+
console.log(renderTailAllTable(envelopes));
|
|
37725
|
+
}
|
|
37726
|
+
return;
|
|
37727
|
+
}
|
|
37728
|
+
const entries = await readEntries(auditFile, { include_rotated: true });
|
|
37729
|
+
const tail = tailEntries(entries, n);
|
|
37730
|
+
if (format === "json") {
|
|
37731
|
+
const payload = brief ? tail.map(briefify) : tail;
|
|
37732
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37733
|
+
} else if (tail.length === 0) {
|
|
37734
|
+
console.error(t("audit.no_entries"));
|
|
37735
|
+
} else {
|
|
37736
|
+
console.log(renderTailTable(tail));
|
|
37737
|
+
}
|
|
37738
|
+
});
|
|
37739
|
+
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_FORMATS14.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) => {
|
|
37740
|
+
if (id && options.recoveryRef) {
|
|
37741
|
+
console.error(t("audit.show_mutex_violation"));
|
|
37742
|
+
process.exit(1);
|
|
37743
|
+
}
|
|
37744
|
+
if (!id && !options.recoveryRef) {
|
|
37745
|
+
console.error(t("audit.show_mutex_violation"));
|
|
37746
|
+
process.exit(1);
|
|
37747
|
+
}
|
|
37748
|
+
const forAgent = options.forAgent === true;
|
|
37749
|
+
const format = forAgent ? "json" : options.format;
|
|
37750
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
37751
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
37752
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
37753
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit show");
|
|
37754
|
+
const configPath = resolveConfigPath(command, options);
|
|
37755
|
+
const config = await configModule.read(configPath);
|
|
37756
|
+
if (isAuditDisabled(config))
|
|
37757
|
+
emitDisabledAndExit0();
|
|
37758
|
+
const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
|
|
37759
|
+
const all = options.all === true;
|
|
37760
|
+
if (options.recoveryRef) {
|
|
37761
|
+
const ref = String(options.recoveryRef);
|
|
37762
|
+
const matches2 = [];
|
|
37763
|
+
if (all) {
|
|
37764
|
+
const conns = await discoverConnections(auditDir);
|
|
37765
|
+
for (const c of conns) {
|
|
37766
|
+
for (const f of c.files) {
|
|
37767
|
+
for (const e of await readEntries(f)) {
|
|
37768
|
+
if (e.recovery_ref === ref)
|
|
37769
|
+
matches2.push({ connection: c.connection, entry: e });
|
|
37770
|
+
}
|
|
37771
|
+
}
|
|
37772
|
+
}
|
|
37773
|
+
} else {
|
|
37774
|
+
for (const e of await readEntries(auditFile, { include_rotated: true })) {
|
|
37775
|
+
if (e.recovery_ref === ref)
|
|
37776
|
+
matches2.push({ connection: connectionName, entry: e });
|
|
37777
|
+
}
|
|
37778
|
+
}
|
|
37779
|
+
if (matches2.length === 0) {
|
|
37780
|
+
console.error(t_vars("audit.show_recovery_no_match", { ref }));
|
|
37781
|
+
process.exit(1);
|
|
37782
|
+
}
|
|
37783
|
+
if (matches2.length > 1) {
|
|
37784
|
+
console.error(t_vars("audit.show_recovery_ambiguous", {
|
|
37785
|
+
ref,
|
|
37786
|
+
count: String(matches2.length)
|
|
37787
|
+
}));
|
|
37788
|
+
process.exit(1);
|
|
37789
|
+
}
|
|
37790
|
+
renderShowResult(matches2[0], { all, format, brief });
|
|
37791
|
+
return;
|
|
37792
|
+
}
|
|
37793
|
+
const lookup = String(id);
|
|
37794
|
+
if (lookup.length < PREFIX_MIN) {
|
|
37795
|
+
console.error(t("audit.show_prefix_too_short"));
|
|
37796
|
+
process.exit(1);
|
|
37797
|
+
}
|
|
37798
|
+
const matches = [];
|
|
37799
|
+
if (all) {
|
|
37800
|
+
const conns = await discoverConnections(auditDir);
|
|
37801
|
+
for (const c of conns) {
|
|
37802
|
+
for (const f of c.files) {
|
|
37803
|
+
for (const e of await readEntries(f)) {
|
|
37804
|
+
if (e.id === lookup || e.id.startsWith(lookup))
|
|
37805
|
+
matches.push({ connection: c.connection, entry: e });
|
|
37806
|
+
}
|
|
37807
|
+
}
|
|
37808
|
+
}
|
|
37809
|
+
} else {
|
|
37810
|
+
for (const e of await readEntries(auditFile, { include_rotated: true })) {
|
|
37811
|
+
if (e.id === lookup || e.id.startsWith(lookup))
|
|
37812
|
+
matches.push({ connection: connectionName, entry: e });
|
|
37813
|
+
}
|
|
37814
|
+
}
|
|
37815
|
+
if (matches.length === 0) {
|
|
37816
|
+
console.error(t_vars("audit.show_no_match", { prefix: lookup }));
|
|
37817
|
+
process.exit(1);
|
|
37818
|
+
}
|
|
37819
|
+
if (matches.length > 1) {
|
|
37820
|
+
console.error(t_vars("audit.show_ambiguous", {
|
|
37821
|
+
prefix: lookup,
|
|
37822
|
+
count: String(matches.length)
|
|
37823
|
+
}));
|
|
37824
|
+
process.exit(1);
|
|
37825
|
+
}
|
|
37826
|
+
renderShowResult(matches[0], { all, format, brief });
|
|
37827
|
+
});
|
|
37828
|
+
auditCommand.command("clear").description(t("audit.clear.description")).option("--yes", "Skip confirmation prompt and delete immediately", false).action(async (options, command) => {
|
|
37829
|
+
const configPath = resolveConfigPath(command, options);
|
|
37830
|
+
const config = await configModule.read(configPath);
|
|
37831
|
+
const { auditFile, connectionName } = await resolveAuditPaths(configPath, config);
|
|
37832
|
+
const rotatedFile = `${auditFile}.1`;
|
|
37833
|
+
const lockFile = `${auditFile}.lock`;
|
|
37834
|
+
const currentInfo = await statAuditFile(auditFile);
|
|
37835
|
+
const rotatedInfo = await statAuditFile(rotatedFile);
|
|
37836
|
+
if (currentInfo === null && rotatedInfo === null) {
|
|
37837
|
+
process.stderr.write(t("audit.clear.summary_nothing") + `
|
|
37838
|
+
`);
|
|
37839
|
+
process.exit(0);
|
|
37840
|
+
}
|
|
37841
|
+
if (options.yes !== true) {
|
|
37842
|
+
if (!process.stdin.isTTY) {
|
|
37843
|
+
console.error(t("audit.clear.requires_tty_or_yes"));
|
|
37844
|
+
process.exit(1);
|
|
37845
|
+
}
|
|
37846
|
+
process.stderr.write(t_vars("audit.clear.prompt_header", { conn: connectionName }) + `
|
|
37847
|
+
`);
|
|
37848
|
+
if (currentInfo) {
|
|
37849
|
+
process.stderr.write(t_vars("audit.clear.prompt_file_line", {
|
|
37850
|
+
file: auditFile,
|
|
37851
|
+
entries: String(currentInfo.entries),
|
|
37852
|
+
size: currentInfo.size
|
|
37853
|
+
}) + `
|
|
37854
|
+
`);
|
|
37855
|
+
}
|
|
37856
|
+
if (rotatedInfo) {
|
|
37857
|
+
process.stderr.write(t_vars("audit.clear.prompt_file_line", {
|
|
37858
|
+
file: rotatedFile,
|
|
37859
|
+
entries: String(rotatedInfo.entries),
|
|
37860
|
+
size: rotatedInfo.size
|
|
37861
|
+
}) + `
|
|
37862
|
+
`);
|
|
37863
|
+
}
|
|
37864
|
+
const answer = await readLineFromStdinWithStderrPrompt(t("audit.clear.prompt_continue"));
|
|
37865
|
+
const lower = answer.toLowerCase();
|
|
37866
|
+
const proceed = lower === "y" || lower === "yes";
|
|
37867
|
+
if (!proceed) {
|
|
37868
|
+
process.stderr.write(t("audit.clear.summary_nothing") + `
|
|
37869
|
+
`);
|
|
37870
|
+
process.exit(0);
|
|
37871
|
+
}
|
|
37872
|
+
}
|
|
37873
|
+
let cleared = 0;
|
|
37874
|
+
try {
|
|
37875
|
+
if (currentInfo)
|
|
37876
|
+
cleared += currentInfo.entries;
|
|
37877
|
+
if (rotatedInfo)
|
|
37878
|
+
cleared += rotatedInfo.entries;
|
|
37879
|
+
await rm3(auditFile, { force: true });
|
|
37880
|
+
await rm3(rotatedFile, { force: true });
|
|
37881
|
+
await rm3(lockFile, { force: true });
|
|
37882
|
+
} catch (err) {
|
|
37883
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37884
|
+
process.stderr.write(t_vars("audit.clear.summary_failed", { message: msg }) + `
|
|
37885
|
+
`);
|
|
37886
|
+
process.exit(1);
|
|
37887
|
+
}
|
|
37888
|
+
process.stderr.write(t_vars("audit.clear.summary_cleared", {
|
|
37889
|
+
count: String(cleared),
|
|
37890
|
+
conn: connectionName
|
|
37891
|
+
}) + `
|
|
37892
|
+
`);
|
|
37893
|
+
process.exit(0);
|
|
37894
|
+
});
|
|
37895
|
+
auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.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) => {
|
|
37896
|
+
const forAgent = options.forAgent === true;
|
|
37897
|
+
const format = forAgent ? "json" : options.format;
|
|
37898
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
37899
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
37900
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
37901
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit health");
|
|
37902
|
+
const configPath = resolveConfigPath(command, options);
|
|
37903
|
+
const config = await configModule.read(configPath);
|
|
37904
|
+
const logger = await getAuditLogger(config, configPath);
|
|
37905
|
+
const health = logger.getHealth();
|
|
37906
|
+
if (format === "json") {
|
|
37907
|
+
const payload = brief ? briefifyHealth(health) : health;
|
|
37908
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37909
|
+
} else if (brief) {
|
|
37910
|
+
const sizePct = Math.round(health.rotationUsage.bytes.pct);
|
|
37911
|
+
const entriesPct = Math.round(health.rotationUsage.entries.pct);
|
|
37912
|
+
const lastWriteLine = health.lastWrite ? `${health.lastWrite.ts} (${health.lastWrite.success ? "success" : "failed"})` : MISSING_PLACEHOLDER;
|
|
37913
|
+
console.log(`Enabled: ${health.enabled}`);
|
|
37914
|
+
console.log(`Last write: ${lastWriteLine}`);
|
|
37915
|
+
console.log(`Rotation usage: ${sizePct}% bytes, ${entriesPct}% entries`);
|
|
37916
|
+
} else {
|
|
37917
|
+
console.log(renderHealthTable(health));
|
|
37918
|
+
}
|
|
37919
|
+
});
|
|
37920
|
+
|
|
35872
37921
|
// src/commands/doctor.ts
|
|
35873
37922
|
init_colors();
|
|
35874
37923
|
init_config();
|
|
@@ -35880,7 +37929,8 @@ init_config_v2();
|
|
|
35880
37929
|
init_config_binding();
|
|
35881
37930
|
init_schema_path();
|
|
35882
37931
|
init_config();
|
|
35883
|
-
|
|
37932
|
+
init_integration_helper();
|
|
37933
|
+
import { join as join26 } from "path";
|
|
35884
37934
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
35885
37935
|
function requireSqlConnection9(connection) {
|
|
35886
37936
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
@@ -35888,7 +37938,7 @@ function requireSqlConnection9(connection) {
|
|
|
35888
37938
|
}
|
|
35889
37939
|
return connection;
|
|
35890
37940
|
}
|
|
35891
|
-
var
|
|
37941
|
+
var ALLOWED_FORMATS15 = ["text", "json"];
|
|
35892
37942
|
var SENSITIVE_PATTERNS = [
|
|
35893
37943
|
"password",
|
|
35894
37944
|
"passwd",
|
|
@@ -35960,7 +38010,7 @@ var runDoctorChecks = {
|
|
|
35960
38010
|
}
|
|
35961
38011
|
},
|
|
35962
38012
|
async checkConfigExists(configPath, existsFn) {
|
|
35963
|
-
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(
|
|
38013
|
+
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join26(configPath, "config.json")).exists();
|
|
35964
38014
|
return {
|
|
35965
38015
|
group: "Configuration",
|
|
35966
38016
|
label: "Config exists",
|
|
@@ -36117,7 +38167,7 @@ var runDoctorChecks = {
|
|
|
36117
38167
|
async checkV2Config(configPath) {
|
|
36118
38168
|
const results = [];
|
|
36119
38169
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
36120
|
-
const configFile = Bun.file(
|
|
38170
|
+
const configFile = Bun.file(join26(storagePath, "config.json"));
|
|
36121
38171
|
if (!await configFile.exists())
|
|
36122
38172
|
return results;
|
|
36123
38173
|
let raw;
|
|
@@ -36157,7 +38207,7 @@ var runDoctorChecks = {
|
|
|
36157
38207
|
}
|
|
36158
38208
|
for (const [name, conn] of Object.entries(config.connections)) {
|
|
36159
38209
|
if (conn.envFile) {
|
|
36160
|
-
const envPath =
|
|
38210
|
+
const envPath = join26(storagePath, conn.envFile);
|
|
36161
38211
|
const exists = await Bun.file(envPath).exists();
|
|
36162
38212
|
results.push({
|
|
36163
38213
|
group: "Configuration",
|
|
@@ -36302,7 +38352,7 @@ async function collectElasticsearchDoctorResults(config) {
|
|
|
36302
38352
|
return results;
|
|
36303
38353
|
}
|
|
36304
38354
|
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) => {
|
|
36305
|
-
validateFormat(options.format,
|
|
38355
|
+
validateFormat(options.format, ALLOWED_FORMATS15, "doctor");
|
|
36306
38356
|
const logger = getLogger();
|
|
36307
38357
|
const results = [];
|
|
36308
38358
|
const configPath = resolveConfigPath(doctorCommand);
|
|
@@ -36376,7 +38426,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36376
38426
|
}
|
|
36377
38427
|
try {
|
|
36378
38428
|
const schemaConnName = await getSchemaIsolationConnectionName(configPath);
|
|
36379
|
-
const indexPath =
|
|
38429
|
+
const indexPath = join26(resolveSchemaPath(storagePath, schemaConnName), "index.json");
|
|
36380
38430
|
const indexFile = Bun.file(indexPath);
|
|
36381
38431
|
let indexParsed = null;
|
|
36382
38432
|
if (await indexFile.exists()) {
|
|
@@ -36407,6 +38457,17 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36407
38457
|
}
|
|
36408
38458
|
}
|
|
36409
38459
|
const hasError = results.some((r) => r.status === "error");
|
|
38460
|
+
try {
|
|
38461
|
+
const config = await configModule.read(configPath);
|
|
38462
|
+
await writeAuditEntry(config, "doctor", options, {
|
|
38463
|
+
success: !hasError,
|
|
38464
|
+
target: "*",
|
|
38465
|
+
metadata: {
|
|
38466
|
+
error_count: results.filter((r) => r.status === "error").length,
|
|
38467
|
+
warn_count: results.filter((r) => r.status === "warn").length
|
|
38468
|
+
}
|
|
38469
|
+
});
|
|
38470
|
+
} catch {}
|
|
36410
38471
|
if (options.format === "json") {
|
|
36411
38472
|
console.log(JSON.stringify({ results, hasError }, null, 2));
|
|
36412
38473
|
} else {
|
|
@@ -36419,7 +38480,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36419
38480
|
|
|
36420
38481
|
// src/commands/completion.ts
|
|
36421
38482
|
init_colors();
|
|
36422
|
-
import { join as
|
|
38483
|
+
import { join as join27 } from "path";
|
|
36423
38484
|
import { homedir as homedir3 } from "os";
|
|
36424
38485
|
function extractCommands(program2) {
|
|
36425
38486
|
return program2.commands.map((cmd) => ({
|
|
@@ -36521,11 +38582,11 @@ function getInstallPath2(shell) {
|
|
|
36521
38582
|
const home = homedir3();
|
|
36522
38583
|
switch (shell) {
|
|
36523
38584
|
case "bash":
|
|
36524
|
-
return
|
|
38585
|
+
return join27(home, ".bashrc");
|
|
36525
38586
|
case "zsh":
|
|
36526
|
-
return
|
|
38587
|
+
return join27(home, ".zshrc");
|
|
36527
38588
|
case "fish":
|
|
36528
|
-
return
|
|
38589
|
+
return join27(home, ".config", "fish", "completions", "dbcli.fish");
|
|
36529
38590
|
default:
|
|
36530
38591
|
throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
36531
38592
|
}
|
|
@@ -36545,7 +38606,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
|
|
|
36545
38606
|
async function installCompletion(shell, script) {
|
|
36546
38607
|
const targetPath = getInstallPath2(shell);
|
|
36547
38608
|
if (shell === "fish") {
|
|
36548
|
-
const dir =
|
|
38609
|
+
const dir = join27(homedir3(), ".config", "fish", "completions");
|
|
36549
38610
|
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
36550
38611
|
await Bun.file(targetPath).write(script);
|
|
36551
38612
|
console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
|
|
@@ -36778,7 +38839,7 @@ ${t("upgrade.failed")}`));
|
|
|
36778
38839
|
init_config();
|
|
36779
38840
|
init_adapters();
|
|
36780
38841
|
import { createInterface as createInterface2 } from "readline";
|
|
36781
|
-
import { join as
|
|
38842
|
+
import { join as join28 } from "path";
|
|
36782
38843
|
import { homedir as homedir4 } from "os";
|
|
36783
38844
|
|
|
36784
38845
|
// src/core/repl/types.ts
|
|
@@ -37123,7 +39184,7 @@ function isKnownCommand(name) {
|
|
|
37123
39184
|
|
|
37124
39185
|
// src/core/repl/history-manager.ts
|
|
37125
39186
|
import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
37126
|
-
import { dirname as
|
|
39187
|
+
import { dirname as dirname10 } from "path";
|
|
37127
39188
|
var DEFAULT_MAX = 1000;
|
|
37128
39189
|
|
|
37129
39190
|
class HistoryManager {
|
|
@@ -37151,7 +39212,7 @@ class HistoryManager {
|
|
|
37151
39212
|
return this.entries;
|
|
37152
39213
|
}
|
|
37153
39214
|
async save() {
|
|
37154
|
-
const dir =
|
|
39215
|
+
const dir = dirname10(this.filePath);
|
|
37155
39216
|
if (!existsSync(dir)) {
|
|
37156
39217
|
mkdirSync2(dir, { recursive: true });
|
|
37157
39218
|
}
|
|
@@ -37509,7 +39570,7 @@ function requireSqlConnection10(connection) {
|
|
|
37509
39570
|
}
|
|
37510
39571
|
return connection;
|
|
37511
39572
|
}
|
|
37512
|
-
var HISTORY_PATH =
|
|
39573
|
+
var HISTORY_PATH = join28(homedir4(), ".dbcli_history");
|
|
37513
39574
|
var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
|
|
37514
39575
|
async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
|
|
37515
39576
|
const columnsByTable = {};
|
|
@@ -38496,7 +40557,7 @@ init_config();
|
|
|
38496
40557
|
init_errors();
|
|
38497
40558
|
init_message_loader();
|
|
38498
40559
|
init_config_binding();
|
|
38499
|
-
import { join as
|
|
40560
|
+
import { join as join29 } from "path";
|
|
38500
40561
|
async function switchDefault(configPath, name, config) {
|
|
38501
40562
|
if (!config.connections[name]) {
|
|
38502
40563
|
const available = Object.keys(config.connections).join(", ");
|
|
@@ -38519,7 +40580,7 @@ function listConnectionsForDisplay(config) {
|
|
|
38519
40580
|
}
|
|
38520
40581
|
async function ensureV2Config(configPath) {
|
|
38521
40582
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
38522
|
-
const configFile = Bun.file(
|
|
40583
|
+
const configFile = Bun.file(join29(storagePath, "config.json"));
|
|
38523
40584
|
const legacyFile = Bun.file(configPath);
|
|
38524
40585
|
if (!await configFile.exists() && !await legacyFile.exists()) {
|
|
38525
40586
|
throw new ConfigError(t("init.config_not_found"));
|
|
@@ -38544,7 +40605,8 @@ async function ensureV2Config(configPath) {
|
|
|
38544
40605
|
schema: v1Config.schema || {},
|
|
38545
40606
|
schemas: { default: v1Config.schema || {} },
|
|
38546
40607
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
38547
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
40608
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
40609
|
+
audit: v1Config.audit
|
|
38548
40610
|
};
|
|
38549
40611
|
}
|
|
38550
40612
|
return readV2Config(storagePath);
|
|
@@ -38582,7 +40644,7 @@ var useCommand = new Command("use").description("Switch or display the default d
|
|
|
38582
40644
|
|
|
38583
40645
|
// src/cli.ts
|
|
38584
40646
|
init_config();
|
|
38585
|
-
import { join as
|
|
40647
|
+
import { join as join30 } from "path";
|
|
38586
40648
|
var _bgVersionCheckResult;
|
|
38587
40649
|
function shouldSkipBackgroundChecks() {
|
|
38588
40650
|
return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
|
|
@@ -38611,7 +40673,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
|
38611
40673
|
try {
|
|
38612
40674
|
let cache = null;
|
|
38613
40675
|
try {
|
|
38614
|
-
const cacheFile = Bun.file(
|
|
40676
|
+
const cacheFile = Bun.file(join30(configPath, "version-check.json"));
|
|
38615
40677
|
if (await cacheFile.exists()) {
|
|
38616
40678
|
cache = await cacheFile.json();
|
|
38617
40679
|
}
|
|
@@ -38659,7 +40721,7 @@ program2.command("plan <sql>").description("Analyze SQL risk without executing")
|
|
|
38659
40721
|
program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--no-limit", "Disable size guard wrap (LIMIT 1000)").option("--dry-run", "Show final SQL + bind values; do not execute").option("--param <kv>", "Pass parameter as key=value (repeatable)", (val, prev = []) => prev.concat([val]), []).option("--param-file <path>", "JSON file containing param values").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (name, options, command) => {
|
|
38660
40722
|
await qCommand(name, options, command);
|
|
38661
40723
|
});
|
|
38662
|
-
program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
40724
|
+
program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
38663
40725
|
try {
|
|
38664
40726
|
await insertCommand(table, options, command);
|
|
38665
40727
|
} catch (error) {
|
|
@@ -38671,7 +40733,7 @@ program2.command("insert <table>").description(t("insert.description")).option("
|
|
|
38671
40733
|
process.exit(1);
|
|
38672
40734
|
}
|
|
38673
40735
|
});
|
|
38674
|
-
program2.command("update <table>").description(t("update.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--set <json>", `JSON with fields to update (required, e.g. '{"name":"Bob"}')`).option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
40736
|
+
program2.command("update <table>").description(t("update.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--set <json>", `JSON with fields to update (required, e.g. '{"name":"Bob"}')`).option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
38675
40737
|
try {
|
|
38676
40738
|
await updateCommand(table, options, command);
|
|
38677
40739
|
} catch (error) {
|
|
@@ -38683,7 +40745,7 @@ program2.command("update <table>").description(t("update.description")).option("
|
|
|
38683
40745
|
process.exit(1);
|
|
38684
40746
|
}
|
|
38685
40747
|
});
|
|
38686
|
-
program2.command("delete <table>").description(t("delete.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
40748
|
+
program2.command("delete <table>").description(t("delete.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
38687
40749
|
try {
|
|
38688
40750
|
await deleteCommand(table, options, command);
|
|
38689
40751
|
} catch (error) {
|
|
@@ -38721,6 +40783,7 @@ program2.addCommand(reportCommand);
|
|
|
38721
40783
|
program2.addCommand(guideCommand);
|
|
38722
40784
|
program2.addCommand(recoveryCommand);
|
|
38723
40785
|
program2.addCommand(recoverCommand);
|
|
40786
|
+
program2.addCommand(auditCommand);
|
|
38724
40787
|
program2.addCommand(doctorCommand);
|
|
38725
40788
|
program2.addCommand(completionCommand);
|
|
38726
40789
|
program2.addCommand(upgradeCommand);
|