@carllee1983/dbcli 1.19.1 → 1.20.1
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 +48 -0
- package/README.md +42 -0
- package/README.zh-TW.md +40 -0
- package/assets/SKILL.md +38 -0
- package/assets/SKILL.zh-TW.md +392 -0
- package/assets/reference.md +87 -0
- package/dist/cli.mjs +2642 -310
- 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);
|
|
@@ -20672,6 +20763,7 @@ var init_capabilities = __esm(() => {
|
|
|
20672
20763
|
"init",
|
|
20673
20764
|
"use",
|
|
20674
20765
|
"list",
|
|
20766
|
+
"schema",
|
|
20675
20767
|
"schemaSingle",
|
|
20676
20768
|
"schemaFullScan",
|
|
20677
20769
|
"query",
|
|
@@ -20696,18 +20788,27 @@ var init_capabilities = __esm(() => {
|
|
|
20696
20788
|
"completion",
|
|
20697
20789
|
"upgrade",
|
|
20698
20790
|
"recover",
|
|
20699
|
-
"skill"
|
|
20791
|
+
"skill",
|
|
20792
|
+
"auditTail",
|
|
20793
|
+
"auditShow",
|
|
20794
|
+
"auditClear",
|
|
20795
|
+
"auditHealth"
|
|
20700
20796
|
]);
|
|
20701
20797
|
ENGINE_INDEPENDENT = {
|
|
20702
20798
|
completion: cap("not-applicable", "none", "Shell completion is engine-independent."),
|
|
20703
20799
|
upgrade: cap("not-applicable", "local-write", "Package update checks are engine-independent."),
|
|
20704
20800
|
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.")
|
|
20801
|
+
skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent."),
|
|
20802
|
+
auditTail: cap("supported", "readonly", "Reads JSONL audit entries; never writes to engines."),
|
|
20803
|
+
auditShow: cap("supported", "readonly", "Looks up a single audit entry by id prefix or recovery_ref."),
|
|
20804
|
+
auditHealth: cap("supported", "readonly", "Renders AuditLogger.getHealth() snapshot."),
|
|
20805
|
+
auditClear: cap("supported", "local-write", "Removes <conn>.jsonl + .jsonl.1 from local disk; never touches DB.")
|
|
20706
20806
|
};
|
|
20707
20807
|
SQL_BASE = {
|
|
20708
20808
|
init: cap("supported", "interactive", "SQL connection initialization is supported."),
|
|
20709
20809
|
use: cap("supported", "local-write", "V2 config supports named SQL connections."),
|
|
20710
20810
|
list: cap("supported", "readonly", "Lists relational tables."),
|
|
20811
|
+
schema: cap("supported", "readonly", "Unified schema command (single-table or full scan)."),
|
|
20711
20812
|
schemaSingle: cap("supported", "readonly", "Reads a single table schema."),
|
|
20712
20813
|
schemaFullScan: cap("supported", "readonly", "Full scan, refresh, and reset are supported."),
|
|
20713
20814
|
query: cap("supported", "readonly", "Runs SQL through permission and blacklist guards."),
|
|
@@ -24225,6 +24326,507 @@ var init_blacklist = __esm(() => {
|
|
|
24225
24326
|
};
|
|
24226
24327
|
});
|
|
24227
24328
|
|
|
24329
|
+
// src/core/audit/lock.ts
|
|
24330
|
+
import { hostname } from "os";
|
|
24331
|
+
import { dirname } from "path";
|
|
24332
|
+
import { mkdir as mkdir2, open, rm } from "fs/promises";
|
|
24333
|
+
|
|
24334
|
+
class AuditLockManager {
|
|
24335
|
+
auditFilePath;
|
|
24336
|
+
lockPath;
|
|
24337
|
+
lockAcquiredAt = null;
|
|
24338
|
+
constructor(auditFilePath) {
|
|
24339
|
+
this.auditFilePath = auditFilePath;
|
|
24340
|
+
this.lockPath = `${auditFilePath}.lock`;
|
|
24341
|
+
}
|
|
24342
|
+
getLockfilePath() {
|
|
24343
|
+
return this.lockPath;
|
|
24344
|
+
}
|
|
24345
|
+
isLockHeld() {
|
|
24346
|
+
return this.lockAcquiredAt !== null;
|
|
24347
|
+
}
|
|
24348
|
+
async acquireLock(operationName = "audit-write") {
|
|
24349
|
+
const startTime = Date.now();
|
|
24350
|
+
let backoffMs = LOCK_BACKOFF_START_MS;
|
|
24351
|
+
while (true) {
|
|
24352
|
+
const elapsed = Date.now() - startTime;
|
|
24353
|
+
if (elapsed > LOCK_RETRY_BUDGET_MS) {
|
|
24354
|
+
return false;
|
|
24355
|
+
}
|
|
24356
|
+
if (await this.tryAcquireLock(operationName)) {
|
|
24357
|
+
this.lockAcquiredAt = Date.now();
|
|
24358
|
+
return true;
|
|
24359
|
+
}
|
|
24360
|
+
const waitTime = Math.min(backoffMs, LOCK_BACKOFF_MAX_MS);
|
|
24361
|
+
await new Promise((resolve2) => setTimeout(resolve2, waitTime));
|
|
24362
|
+
backoffMs = Math.min(backoffMs * 1.5, LOCK_BACKOFF_MAX_MS);
|
|
24363
|
+
}
|
|
24364
|
+
}
|
|
24365
|
+
async releaseLock() {
|
|
24366
|
+
if (!this.lockAcquiredAt)
|
|
24367
|
+
return false;
|
|
24368
|
+
try {
|
|
24369
|
+
await rm(this.lockPath, { force: true });
|
|
24370
|
+
this.lockAcquiredAt = null;
|
|
24371
|
+
return true;
|
|
24372
|
+
} catch {
|
|
24373
|
+
this.lockAcquiredAt = null;
|
|
24374
|
+
return false;
|
|
24375
|
+
}
|
|
24376
|
+
}
|
|
24377
|
+
async withLock(operation, operationName = "audit-write") {
|
|
24378
|
+
const acquired = await this.acquireLock(operationName);
|
|
24379
|
+
if (!acquired) {
|
|
24380
|
+
return { skipped: "lock-budget-exhausted" };
|
|
24381
|
+
}
|
|
24382
|
+
try {
|
|
24383
|
+
return await operation();
|
|
24384
|
+
} finally {
|
|
24385
|
+
await this.releaseLock();
|
|
24386
|
+
}
|
|
24387
|
+
}
|
|
24388
|
+
async tryAcquireLock(operationName) {
|
|
24389
|
+
try {
|
|
24390
|
+
await mkdir2(dirname(this.lockPath), { recursive: true });
|
|
24391
|
+
const lockFile = Bun.file(this.lockPath);
|
|
24392
|
+
if (await lockFile.exists()) {
|
|
24393
|
+
const lockContent = await lockFile.json();
|
|
24394
|
+
const lockAge = Date.now() - lockContent.timestamp;
|
|
24395
|
+
const staleLockThresholdMs = LOCK_RETRY_BUDGET_MS * STALE_LOCK_MULTIPLIER;
|
|
24396
|
+
if (lockAge > staleLockThresholdMs) {
|
|
24397
|
+
await rm(this.lockPath, { force: true });
|
|
24398
|
+
} else {
|
|
24399
|
+
return false;
|
|
24400
|
+
}
|
|
24401
|
+
}
|
|
24402
|
+
const lockData = {
|
|
24403
|
+
pid: process.pid,
|
|
24404
|
+
operation: operationName,
|
|
24405
|
+
timestamp: Date.now(),
|
|
24406
|
+
hostname: hostname()
|
|
24407
|
+
};
|
|
24408
|
+
const handle = await open(this.lockPath, "wx");
|
|
24409
|
+
try {
|
|
24410
|
+
await handle.writeFile(JSON.stringify(lockData), "utf8");
|
|
24411
|
+
} finally {
|
|
24412
|
+
await handle.close();
|
|
24413
|
+
}
|
|
24414
|
+
return true;
|
|
24415
|
+
} catch {
|
|
24416
|
+
return false;
|
|
24417
|
+
}
|
|
24418
|
+
}
|
|
24419
|
+
}
|
|
24420
|
+
var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS = 50, STALE_LOCK_MULTIPLIER = 10;
|
|
24421
|
+
var init_lock = () => {};
|
|
24422
|
+
|
|
24423
|
+
// src/core/audit/rotation.ts
|
|
24424
|
+
import { rename } from "fs/promises";
|
|
24425
|
+
function shouldRotate(stats, thresholds, nextLineByteLength) {
|
|
24426
|
+
const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
|
|
24427
|
+
const entriesAfter = stats.currentEntryCount + 1;
|
|
24428
|
+
return bytesAfter >= thresholds.maxBytes || entriesAfter > thresholds.maxEntries;
|
|
24429
|
+
}
|
|
24430
|
+
async function rotate(currentPath, previousPath) {
|
|
24431
|
+
try {
|
|
24432
|
+
await rename(currentPath, previousPath);
|
|
24433
|
+
} catch {}
|
|
24434
|
+
}
|
|
24435
|
+
var init_rotation = () => {};
|
|
24436
|
+
|
|
24437
|
+
// src/core/audit/logger.ts
|
|
24438
|
+
import { appendFile, mkdir as mkdir3, readFile, stat } from "fs/promises";
|
|
24439
|
+
import { join as join10 } from "path";
|
|
24440
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
24441
|
+
|
|
24442
|
+
class AuditLogger {
|
|
24443
|
+
auditDir;
|
|
24444
|
+
auditFilePath;
|
|
24445
|
+
previousFilePath;
|
|
24446
|
+
enabled;
|
|
24447
|
+
maxBytes;
|
|
24448
|
+
maxEntries;
|
|
24449
|
+
sessionIdService;
|
|
24450
|
+
lockManager;
|
|
24451
|
+
writerInitialized = false;
|
|
24452
|
+
currentSizeBytes = 0;
|
|
24453
|
+
currentEntryCount = 0;
|
|
24454
|
+
lastWrite = null;
|
|
24455
|
+
lastError = null;
|
|
24456
|
+
warnedOnceThisProcess = false;
|
|
24457
|
+
cachedSessionId = null;
|
|
24458
|
+
lastRotatedAt;
|
|
24459
|
+
lastRotatedPrevious;
|
|
24460
|
+
writeChain = Promise.resolve();
|
|
24461
|
+
constructor(opts) {
|
|
24462
|
+
this.enabled = opts.enabled;
|
|
24463
|
+
this.maxBytes = opts.rotation.maxBytes;
|
|
24464
|
+
this.maxEntries = opts.rotation.maxEntries;
|
|
24465
|
+
this.auditDir = join10(opts.storagePath, ".dbcli", "audit");
|
|
24466
|
+
this.auditFilePath = join10(this.auditDir, `${opts.connectionName}.jsonl`);
|
|
24467
|
+
this.previousFilePath = `${this.auditFilePath}.1`;
|
|
24468
|
+
this.sessionIdService = opts.sessionIdService;
|
|
24469
|
+
this.lockManager = opts.lockManager ?? new AuditLockManager(this.auditFilePath);
|
|
24470
|
+
}
|
|
24471
|
+
async write(entry) {
|
|
24472
|
+
const writeOp = this.writeChain.then(() => this.writeInternal(entry));
|
|
24473
|
+
this.writeChain = writeOp.then(() => {
|
|
24474
|
+
return;
|
|
24475
|
+
}, () => {
|
|
24476
|
+
return;
|
|
24477
|
+
});
|
|
24478
|
+
return writeOp;
|
|
24479
|
+
}
|
|
24480
|
+
async writeInternal(entry) {
|
|
24481
|
+
if (!this.enabled) {
|
|
24482
|
+
return { skipped: "disabled" };
|
|
24483
|
+
}
|
|
24484
|
+
try {
|
|
24485
|
+
const sessionId = await this.sessionIdService.resolve();
|
|
24486
|
+
this.cachedSessionId = sessionId;
|
|
24487
|
+
await mkdir3(this.auditDir, { recursive: true });
|
|
24488
|
+
if (!this.writerInitialized) {
|
|
24489
|
+
await this.syncCountersFromDisk();
|
|
24490
|
+
this.writerInitialized = true;
|
|
24491
|
+
}
|
|
24492
|
+
const id = randomUUID2();
|
|
24493
|
+
const ts = new Date().toISOString();
|
|
24494
|
+
const enriched = {
|
|
24495
|
+
...entry,
|
|
24496
|
+
id,
|
|
24497
|
+
ts,
|
|
24498
|
+
session_id: sessionId
|
|
24499
|
+
};
|
|
24500
|
+
const line = JSON.stringify(enriched) + `
|
|
24501
|
+
`;
|
|
24502
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
24503
|
+
const lockResult = await this.lockManager.withLock(async () => {
|
|
24504
|
+
let rotated = false;
|
|
24505
|
+
if (shouldRotate({
|
|
24506
|
+
currentSizeBytes: this.currentSizeBytes,
|
|
24507
|
+
currentEntryCount: this.currentEntryCount
|
|
24508
|
+
}, { maxBytes: this.maxBytes, maxEntries: this.maxEntries }, lineBytes)) {
|
|
24509
|
+
await rotate(this.auditFilePath, this.previousFilePath);
|
|
24510
|
+
this.lastRotatedAt = new Date().toISOString();
|
|
24511
|
+
this.lastRotatedPrevious = this.previousFilePath;
|
|
24512
|
+
this.currentSizeBytes = 0;
|
|
24513
|
+
this.currentEntryCount = 0;
|
|
24514
|
+
rotated = true;
|
|
24515
|
+
}
|
|
24516
|
+
await appendFile(this.auditFilePath, line, { encoding: "utf8" });
|
|
24517
|
+
this.currentSizeBytes += lineBytes;
|
|
24518
|
+
this.currentEntryCount += 1;
|
|
24519
|
+
return { rotated };
|
|
24520
|
+
}, "audit-write");
|
|
24521
|
+
if (lockResult !== null && typeof lockResult === "object" && "skipped" in lockResult) {
|
|
24522
|
+
this.handleFailure("lock-budget-exhausted");
|
|
24523
|
+
return { skipped: "lock-budget-exhausted" };
|
|
24524
|
+
}
|
|
24525
|
+
this.lastWrite = { ts, success: true };
|
|
24526
|
+
return { success: true, rotated: lockResult.rotated, id };
|
|
24527
|
+
} catch (err) {
|
|
24528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24529
|
+
this.handleFailure(message);
|
|
24530
|
+
return { skipped: "write-failed", error: message };
|
|
24531
|
+
}
|
|
24532
|
+
}
|
|
24533
|
+
getHealth() {
|
|
24534
|
+
return {
|
|
24535
|
+
enabled: this.enabled,
|
|
24536
|
+
writerInitialized: this.writerInitialized,
|
|
24537
|
+
currentFile: this.auditFilePath,
|
|
24538
|
+
currentSizeBytes: this.currentSizeBytes,
|
|
24539
|
+
currentEntryCount: this.currentEntryCount,
|
|
24540
|
+
rotationUsage: {
|
|
24541
|
+
bytes: {
|
|
24542
|
+
current: this.currentSizeBytes,
|
|
24543
|
+
max: this.maxBytes,
|
|
24544
|
+
pct: this.maxBytes > 0 ? this.currentSizeBytes / this.maxBytes * 100 : 0
|
|
24545
|
+
},
|
|
24546
|
+
entries: {
|
|
24547
|
+
current: this.currentEntryCount,
|
|
24548
|
+
max: this.maxEntries,
|
|
24549
|
+
pct: this.maxEntries > 0 ? this.currentEntryCount / this.maxEntries * 100 : 0
|
|
24550
|
+
}
|
|
24551
|
+
},
|
|
24552
|
+
lock: { state: this.lockManager.isLockHeld() ? "held" : "free" },
|
|
24553
|
+
lastWrite: this.lastWrite,
|
|
24554
|
+
lastError: this.lastError,
|
|
24555
|
+
sessionId: this.cachedSessionId,
|
|
24556
|
+
rotation: {
|
|
24557
|
+
lastRotatedAt: this.lastRotatedAt,
|
|
24558
|
+
previousFile: this.lastRotatedPrevious
|
|
24559
|
+
}
|
|
24560
|
+
};
|
|
24561
|
+
}
|
|
24562
|
+
async syncCountersFromDisk() {
|
|
24563
|
+
try {
|
|
24564
|
+
const s = await stat(this.auditFilePath);
|
|
24565
|
+
this.currentSizeBytes = s.size;
|
|
24566
|
+
const raw = await readFile(this.auditFilePath, "utf8");
|
|
24567
|
+
this.currentEntryCount = raw.split(`
|
|
24568
|
+
`).filter(Boolean).length;
|
|
24569
|
+
} catch {
|
|
24570
|
+
this.currentSizeBytes = 0;
|
|
24571
|
+
this.currentEntryCount = 0;
|
|
24572
|
+
}
|
|
24573
|
+
}
|
|
24574
|
+
handleFailure(message) {
|
|
24575
|
+
const ts = new Date().toISOString();
|
|
24576
|
+
this.lastError = { ts, message };
|
|
24577
|
+
this.lastWrite = { ts, success: false, error: message };
|
|
24578
|
+
if (!this.warnedOnceThisProcess) {
|
|
24579
|
+
process.stderr.write(`${WARN_PREFIX} warning: audit write failed (${message}); subsequent failures suppressed this process.
|
|
24580
|
+
`);
|
|
24581
|
+
this.warnedOnceThisProcess = true;
|
|
24582
|
+
}
|
|
24583
|
+
}
|
|
24584
|
+
}
|
|
24585
|
+
var WARN_PREFIX = "[dbcli audit]";
|
|
24586
|
+
var init_logger = __esm(() => {
|
|
24587
|
+
init_lock();
|
|
24588
|
+
init_rotation();
|
|
24589
|
+
});
|
|
24590
|
+
|
|
24591
|
+
// src/core/audit/session-id.ts
|
|
24592
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename2, stat as stat2, writeFile } from "fs/promises";
|
|
24593
|
+
import { randomBytes } from "crypto";
|
|
24594
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
24595
|
+
function generateSessionId(pid, nowMs) {
|
|
24596
|
+
const random = randomBytes(3).toString("hex");
|
|
24597
|
+
return `${pid}-${nowMs}-${random}`;
|
|
24598
|
+
}
|
|
24599
|
+
async function readSessionIdFile(storagePath) {
|
|
24600
|
+
const target = join11(storagePath, LAST_SESSION_ID_RELATIVE);
|
|
24601
|
+
try {
|
|
24602
|
+
await stat2(target);
|
|
24603
|
+
} catch {
|
|
24604
|
+
return null;
|
|
24605
|
+
}
|
|
24606
|
+
try {
|
|
24607
|
+
const raw = await readFile2(target, "utf8");
|
|
24608
|
+
const parsed = JSON.parse(raw);
|
|
24609
|
+
if (parsed && typeof parsed.sessionId === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string") {
|
|
24610
|
+
return {
|
|
24611
|
+
sessionId: parsed.sessionId,
|
|
24612
|
+
pid: parsed.pid,
|
|
24613
|
+
createdAt: parsed.createdAt
|
|
24614
|
+
};
|
|
24615
|
+
}
|
|
24616
|
+
return null;
|
|
24617
|
+
} catch {
|
|
24618
|
+
return null;
|
|
24619
|
+
}
|
|
24620
|
+
}
|
|
24621
|
+
async function writeSessionIdFile(storagePath, payload) {
|
|
24622
|
+
const target = join11(storagePath, LAST_SESSION_ID_RELATIVE);
|
|
24623
|
+
const tmp = `${target}.tmp`;
|
|
24624
|
+
try {
|
|
24625
|
+
await mkdir4(dirname2(target), { recursive: true });
|
|
24626
|
+
await writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
24627
|
+
await rename2(tmp, target);
|
|
24628
|
+
} catch {}
|
|
24629
|
+
}
|
|
24630
|
+
|
|
24631
|
+
class SessionIdService {
|
|
24632
|
+
storagePath;
|
|
24633
|
+
cached = null;
|
|
24634
|
+
constructor(storagePath) {
|
|
24635
|
+
this.storagePath = storagePath;
|
|
24636
|
+
}
|
|
24637
|
+
async resolve() {
|
|
24638
|
+
if (this.cached !== null)
|
|
24639
|
+
return this.cached;
|
|
24640
|
+
const fromEnv = process.env.DBCLI_SESSION_ID;
|
|
24641
|
+
if (fromEnv && fromEnv.length > 0) {
|
|
24642
|
+
this.cached = fromEnv;
|
|
24643
|
+
return fromEnv;
|
|
24644
|
+
}
|
|
24645
|
+
const saved = await readSessionIdFile(this.storagePath);
|
|
24646
|
+
if (saved && saved.pid === process.pid) {
|
|
24647
|
+
this.cached = saved.sessionId;
|
|
24648
|
+
return saved.sessionId;
|
|
24649
|
+
}
|
|
24650
|
+
const id = generateSessionId(process.pid, Date.now());
|
|
24651
|
+
await writeSessionIdFile(this.storagePath, {
|
|
24652
|
+
sessionId: id,
|
|
24653
|
+
pid: process.pid,
|
|
24654
|
+
createdAt: new Date().toISOString()
|
|
24655
|
+
});
|
|
24656
|
+
this.cached = id;
|
|
24657
|
+
return id;
|
|
24658
|
+
}
|
|
24659
|
+
reset() {
|
|
24660
|
+
this.cached = null;
|
|
24661
|
+
}
|
|
24662
|
+
}
|
|
24663
|
+
var LAST_SESSION_ID_RELATIVE = ".dbcli/last-session-id";
|
|
24664
|
+
var init_session_id = () => {};
|
|
24665
|
+
|
|
24666
|
+
// src/utils/redaction.ts
|
|
24667
|
+
function redactArgv(argv) {
|
|
24668
|
+
if (argv.length === 0)
|
|
24669
|
+
return "<unknown>";
|
|
24670
|
+
const out = [argv[0]];
|
|
24671
|
+
if (argv.length === 1)
|
|
24672
|
+
return out.join(" ");
|
|
24673
|
+
const sub = argv[1];
|
|
24674
|
+
out.push(sub);
|
|
24675
|
+
for (let i = 2;i < argv.length; i++) {
|
|
24676
|
+
const tok = argv[i];
|
|
24677
|
+
if (tok.startsWith("--")) {
|
|
24678
|
+
const [name, inlineValue] = tok.split("=");
|
|
24679
|
+
if (REDACTED_VALUE_FLAGS.has(name)) {
|
|
24680
|
+
out.push(`${name} <redacted>`);
|
|
24681
|
+
if (inlineValue === undefined)
|
|
24682
|
+
i++;
|
|
24683
|
+
continue;
|
|
24684
|
+
}
|
|
24685
|
+
if (KEEP_VALUE_FLAGS.has(name)) {
|
|
24686
|
+
if (inlineValue !== undefined) {
|
|
24687
|
+
out.push(tok);
|
|
24688
|
+
} else {
|
|
24689
|
+
out.push(name);
|
|
24690
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
24691
|
+
out.push(argv[++i]);
|
|
24692
|
+
}
|
|
24693
|
+
}
|
|
24694
|
+
continue;
|
|
24695
|
+
}
|
|
24696
|
+
out.push(tok);
|
|
24697
|
+
continue;
|
|
24698
|
+
}
|
|
24699
|
+
if (i === 2 && SQL_SUBCOMMANDS.has(sub)) {
|
|
24700
|
+
out.push("<sql>");
|
|
24701
|
+
continue;
|
|
24702
|
+
}
|
|
24703
|
+
out.push(tok);
|
|
24704
|
+
}
|
|
24705
|
+
return out.join(" ");
|
|
24706
|
+
}
|
|
24707
|
+
function redactSensitive(text2) {
|
|
24708
|
+
return text2.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
|
|
24709
|
+
}
|
|
24710
|
+
function redactSql(sql) {
|
|
24711
|
+
const redacted = sql.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g, "'?'").replace(/\b\d+(\.\d+)?\b/g, "0");
|
|
24712
|
+
return redactSensitive(redacted);
|
|
24713
|
+
}
|
|
24714
|
+
var SQL_SUBCOMMANDS, REDACTED_VALUE_FLAGS, KEEP_VALUE_FLAGS;
|
|
24715
|
+
var init_redaction = __esm(() => {
|
|
24716
|
+
SQL_SUBCOMMANDS = new Set(["query", "export"]);
|
|
24717
|
+
REDACTED_VALUE_FLAGS = new Set([
|
|
24718
|
+
"--where",
|
|
24719
|
+
"--set",
|
|
24720
|
+
"--data",
|
|
24721
|
+
"--param",
|
|
24722
|
+
"--param-file",
|
|
24723
|
+
"--config",
|
|
24724
|
+
"--use",
|
|
24725
|
+
"--password",
|
|
24726
|
+
"--token",
|
|
24727
|
+
"--secret"
|
|
24728
|
+
]);
|
|
24729
|
+
KEEP_VALUE_FLAGS = new Set(["--format", "--conn-name"]);
|
|
24730
|
+
});
|
|
24731
|
+
|
|
24732
|
+
// src/utils/engine-hints.ts
|
|
24733
|
+
var exports_engine_hints = {};
|
|
24734
|
+
__export(exports_engine_hints, {
|
|
24735
|
+
getOperationTarget: () => getOperationTarget,
|
|
24736
|
+
extractTableName: () => extractTableName
|
|
24737
|
+
});
|
|
24738
|
+
function extractTableName(sql) {
|
|
24739
|
+
const match = sql.match(TABLE_NAME_RE);
|
|
24740
|
+
return match ? match[1] ?? null : null;
|
|
24741
|
+
}
|
|
24742
|
+
function getOperationTarget(system, command, options, sql) {
|
|
24743
|
+
if (system === "mongodb") {
|
|
24744
|
+
return options.collection || "<unknown-collection>";
|
|
24745
|
+
}
|
|
24746
|
+
if (system === "elasticsearch") {
|
|
24747
|
+
return options.index || options.collection || "<unknown-index>";
|
|
24748
|
+
}
|
|
24749
|
+
if (system === "redis") {
|
|
24750
|
+
return options.table || options.key || "<unknown-key>";
|
|
24751
|
+
}
|
|
24752
|
+
if (sql) {
|
|
24753
|
+
const table = extractTableName(sql);
|
|
24754
|
+
if (table)
|
|
24755
|
+
return table;
|
|
24756
|
+
}
|
|
24757
|
+
return options.table || "<unknown-target>";
|
|
24758
|
+
}
|
|
24759
|
+
var TABLE_NAME_RE;
|
|
24760
|
+
var init_engine_hints = __esm(() => {
|
|
24761
|
+
TABLE_NAME_RE = /\b(?:FROM|INTO|UPDATE)\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i;
|
|
24762
|
+
});
|
|
24763
|
+
|
|
24764
|
+
// src/core/audit/integration-helper.ts
|
|
24765
|
+
async function getAuditLogger(config, configPath) {
|
|
24766
|
+
const storagePath = await resolveConfigStoragePath(configPath);
|
|
24767
|
+
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
24768
|
+
const key = `${storagePath}:${connName}`;
|
|
24769
|
+
if (!_loggers.has(key)) {
|
|
24770
|
+
if (!_sessionIdService) {
|
|
24771
|
+
_sessionIdService = new SessionIdService(storagePath);
|
|
24772
|
+
}
|
|
24773
|
+
_loggers.set(key, new AuditLogger({
|
|
24774
|
+
storagePath,
|
|
24775
|
+
connectionName: connName,
|
|
24776
|
+
enabled: config.audit?.enabled ?? true,
|
|
24777
|
+
rotation: {
|
|
24778
|
+
maxBytes: config.audit?.rotation?.max_bytes ?? 10485760,
|
|
24779
|
+
maxEntries: config.audit?.rotation?.max_entries ?? 1000
|
|
24780
|
+
},
|
|
24781
|
+
sessionIdService: _sessionIdService
|
|
24782
|
+
}));
|
|
24783
|
+
}
|
|
24784
|
+
return _loggers.get(key);
|
|
24785
|
+
}
|
|
24786
|
+
async function writeAuditEntry(config, commandName, options, outcome) {
|
|
24787
|
+
try {
|
|
24788
|
+
const logger = await getAuditLogger(config, options.config || ".dbcli");
|
|
24789
|
+
const engine = config.connection?.system || "postgresql";
|
|
24790
|
+
const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
|
|
24791
|
+
let tier = getEngineCapability(engine, commandName).tier;
|
|
24792
|
+
if (options.dryRun || options.plan) {
|
|
24793
|
+
tier = "dry-run";
|
|
24794
|
+
}
|
|
24795
|
+
let errorMessage;
|
|
24796
|
+
if (outcome.error) {
|
|
24797
|
+
errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
24798
|
+
errorMessage = redactSensitive(errorMessage);
|
|
24799
|
+
}
|
|
24800
|
+
const entry = {
|
|
24801
|
+
engine,
|
|
24802
|
+
command: commandName,
|
|
24803
|
+
side_effect_tier: tier,
|
|
24804
|
+
target,
|
|
24805
|
+
success: outcome.success,
|
|
24806
|
+
redacted_query: redactArgv(process.argv),
|
|
24807
|
+
...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
|
|
24808
|
+
...errorMessage && { error: errorMessage },
|
|
24809
|
+
...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
|
|
24810
|
+
metadata: outcome.metadata
|
|
24811
|
+
};
|
|
24812
|
+
const result = await logger.write(entry);
|
|
24813
|
+
return "success" in result ? result.id : null;
|
|
24814
|
+
} catch {
|
|
24815
|
+
return null;
|
|
24816
|
+
}
|
|
24817
|
+
}
|
|
24818
|
+
var _sessionIdService = null, _loggers;
|
|
24819
|
+
var init_integration_helper = __esm(() => {
|
|
24820
|
+
init_logger();
|
|
24821
|
+
init_session_id();
|
|
24822
|
+
init_config_binding();
|
|
24823
|
+
init_config();
|
|
24824
|
+
init_capabilities();
|
|
24825
|
+
init_redaction();
|
|
24826
|
+
init_engine_hints();
|
|
24827
|
+
_loggers = new Map;
|
|
24828
|
+
});
|
|
24829
|
+
|
|
24228
24830
|
// src/core/recovery/types.ts
|
|
24229
24831
|
var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, SchemaCacheMissingError, RECOVERY_CODE_METADATA;
|
|
24230
24832
|
var init_types3 = __esm(() => {
|
|
@@ -25417,8 +26019,9 @@ var init_render_markdown = __esm(() => {
|
|
|
25417
26019
|
});
|
|
25418
26020
|
|
|
25419
26021
|
// src/core/recovery/last-envelope.ts
|
|
25420
|
-
import { writeFile, readFile, rename, mkdir as
|
|
25421
|
-
import { dirname, join as
|
|
26022
|
+
import { writeFile as writeFile2, readFile as readFile3, rename as rename3, mkdir as mkdir5, stat as stat3 } from "fs/promises";
|
|
26023
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
26024
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
25422
26025
|
function sanitizeCommandSummary(argv) {
|
|
25423
26026
|
if (argv.length === 0)
|
|
25424
26027
|
return "<unknown>";
|
|
@@ -25431,13 +26034,13 @@ function sanitizeCommandSummary(argv) {
|
|
|
25431
26034
|
const tok = argv[i];
|
|
25432
26035
|
if (tok.startsWith("--")) {
|
|
25433
26036
|
const [name, inlineValue] = tok.split("=");
|
|
25434
|
-
if (
|
|
26037
|
+
if (REDACTED_VALUE_FLAGS2.has(name)) {
|
|
25435
26038
|
out.push(`${name} <redacted>`);
|
|
25436
26039
|
if (inlineValue === undefined)
|
|
25437
26040
|
i++;
|
|
25438
26041
|
continue;
|
|
25439
26042
|
}
|
|
25440
|
-
if (
|
|
26043
|
+
if (KEEP_VALUE_FLAGS2.has(name)) {
|
|
25441
26044
|
if (inlineValue !== undefined) {
|
|
25442
26045
|
out.push(tok);
|
|
25443
26046
|
} else {
|
|
@@ -25451,7 +26054,7 @@ function sanitizeCommandSummary(argv) {
|
|
|
25451
26054
|
out.push(tok);
|
|
25452
26055
|
continue;
|
|
25453
26056
|
}
|
|
25454
|
-
if (i === 2 &&
|
|
26057
|
+
if (i === 2 && SQL_SUBCOMMANDS2.has(sub)) {
|
|
25455
26058
|
out.push("<sql>");
|
|
25456
26059
|
continue;
|
|
25457
26060
|
}
|
|
@@ -25459,46 +26062,48 @@ function sanitizeCommandSummary(argv) {
|
|
|
25459
26062
|
}
|
|
25460
26063
|
return out.join(" ");
|
|
25461
26064
|
}
|
|
25462
|
-
async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date) {
|
|
25463
|
-
const target =
|
|
26065
|
+
async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID3(), auditRef) {
|
|
26066
|
+
const target = join12(cwd, LAST_ENVELOPE_PATH);
|
|
25464
26067
|
const tmp = `${target}.tmp`;
|
|
25465
26068
|
const payload = {
|
|
25466
26069
|
schemaVersion: 1,
|
|
26070
|
+
id,
|
|
26071
|
+
...auditRef !== undefined && { audit_ref: auditRef },
|
|
25467
26072
|
savedAt: now().toISOString(),
|
|
25468
26073
|
command: sanitizeCommandSummary(argv),
|
|
25469
26074
|
cwd,
|
|
25470
26075
|
envelope
|
|
25471
26076
|
};
|
|
25472
26077
|
try {
|
|
25473
|
-
await
|
|
25474
|
-
await
|
|
25475
|
-
await
|
|
26078
|
+
await mkdir5(dirname3(target), { recursive: true });
|
|
26079
|
+
await writeFile2(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
26080
|
+
await rename3(tmp, target);
|
|
25476
26081
|
} catch {}
|
|
25477
26082
|
}
|
|
25478
26083
|
async function readLastEnvelope(cwd) {
|
|
25479
|
-
const target =
|
|
26084
|
+
const target = join12(cwd, LAST_ENVELOPE_PATH);
|
|
25480
26085
|
try {
|
|
25481
|
-
await
|
|
26086
|
+
await stat3(target);
|
|
25482
26087
|
} catch {
|
|
25483
26088
|
return null;
|
|
25484
26089
|
}
|
|
25485
26090
|
try {
|
|
25486
|
-
const raw = await
|
|
26091
|
+
const raw = await readFile3(target, "utf8");
|
|
25487
26092
|
return JSON.parse(raw);
|
|
25488
26093
|
} catch {
|
|
25489
26094
|
return null;
|
|
25490
26095
|
}
|
|
25491
26096
|
}
|
|
25492
26097
|
async function readLastEnvelopeRaw(cwd) {
|
|
25493
|
-
const target =
|
|
26098
|
+
const target = join12(cwd, LAST_ENVELOPE_PATH);
|
|
25494
26099
|
try {
|
|
25495
|
-
await
|
|
26100
|
+
await stat3(target);
|
|
25496
26101
|
} catch {
|
|
25497
26102
|
return null;
|
|
25498
26103
|
}
|
|
25499
26104
|
let raw;
|
|
25500
26105
|
try {
|
|
25501
|
-
raw = await
|
|
26106
|
+
raw = await readFile3(target, "utf8");
|
|
25502
26107
|
} catch {
|
|
25503
26108
|
return null;
|
|
25504
26109
|
}
|
|
@@ -25508,10 +26113,10 @@ async function readLastEnvelopeRaw(cwd) {
|
|
|
25508
26113
|
return { __malformedJson: true, raw };
|
|
25509
26114
|
}
|
|
25510
26115
|
}
|
|
25511
|
-
var LAST_ENVELOPE_PATH = ".dbcli/last-recovery.json",
|
|
26116
|
+
var LAST_ENVELOPE_PATH = ".dbcli/last-recovery.json", SQL_SUBCOMMANDS2, REDACTED_VALUE_FLAGS2, KEEP_VALUE_FLAGS2;
|
|
25512
26117
|
var init_last_envelope = __esm(() => {
|
|
25513
|
-
|
|
25514
|
-
|
|
26118
|
+
SQL_SUBCOMMANDS2 = new Set(["query", "export"]);
|
|
26119
|
+
REDACTED_VALUE_FLAGS2 = new Set([
|
|
25515
26120
|
"--where",
|
|
25516
26121
|
"--set",
|
|
25517
26122
|
"--data",
|
|
@@ -25520,17 +26125,19 @@ var init_last_envelope = __esm(() => {
|
|
|
25520
26125
|
"--config",
|
|
25521
26126
|
"--use"
|
|
25522
26127
|
]);
|
|
25523
|
-
|
|
26128
|
+
KEEP_VALUE_FLAGS2 = new Set(["--format"]);
|
|
25524
26129
|
});
|
|
25525
26130
|
|
|
25526
26131
|
// src/core/recovery/emit.ts
|
|
25527
26132
|
import { writeFileSync as writeFileSync2, mkdirSync, renameSync } from "fs";
|
|
25528
|
-
import { dirname as
|
|
26133
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
26134
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
25529
26135
|
function emitRecoveryEnvelope(error, ctx, options = {}) {
|
|
25530
26136
|
const envelope = classifyError(error, ctx);
|
|
25531
26137
|
const cwd = options.cwd ?? process.cwd();
|
|
25532
26138
|
const argv = options.argv ?? buildArgvFromProcess();
|
|
25533
|
-
|
|
26139
|
+
const envelopeId = options.envelopeId ?? randomUUID4();
|
|
26140
|
+
writeLastEnvelopeSync(cwd, envelope, argv, envelopeId, options.auditRef);
|
|
25534
26141
|
process.stdout.write(renderJson(envelope, { brief: options.brief === true }) + `
|
|
25535
26142
|
`);
|
|
25536
26143
|
process.exit(options.exitCode ?? 1);
|
|
@@ -25539,18 +26146,20 @@ function buildArgvFromProcess() {
|
|
|
25539
26146
|
const userArgs = process.argv.slice(2);
|
|
25540
26147
|
return ["dbcli", ...userArgs];
|
|
25541
26148
|
}
|
|
25542
|
-
function writeLastEnvelopeSync(cwd, envelope, argv) {
|
|
25543
|
-
const target =
|
|
26149
|
+
function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
|
|
26150
|
+
const target = join13(cwd, LAST_ENVELOPE_PATH);
|
|
25544
26151
|
const tmp = `${target}.tmp`;
|
|
25545
26152
|
const payload = {
|
|
25546
26153
|
schemaVersion: 1,
|
|
26154
|
+
id,
|
|
26155
|
+
...auditRef !== undefined && { audit_ref: auditRef },
|
|
25547
26156
|
savedAt: new Date().toISOString(),
|
|
25548
26157
|
command: sanitizeCommandSummary(argv),
|
|
25549
26158
|
cwd,
|
|
25550
26159
|
envelope
|
|
25551
26160
|
};
|
|
25552
26161
|
try {
|
|
25553
|
-
mkdirSync(
|
|
26162
|
+
mkdirSync(dirname4(target), { recursive: true });
|
|
25554
26163
|
writeFileSync2(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
25555
26164
|
renameSync(tmp, target);
|
|
25556
26165
|
} catch {}
|
|
@@ -26278,7 +26887,7 @@ function renderStep2(r) {
|
|
|
26278
26887
|
var NEXT_SCHEMA_VERSION = 1, STEP_RESULT_SUMMARY_FIELD_CAP = 4096;
|
|
26279
26888
|
|
|
26280
26889
|
// src/core/recovery/next-step-schema.ts
|
|
26281
|
-
import { stat as
|
|
26890
|
+
import { stat as stat4 } from "fs/promises";
|
|
26282
26891
|
import { resolve as resolve2 } from "path";
|
|
26283
26892
|
function summarizeZodError(err) {
|
|
26284
26893
|
return err.issues.map((iss) => {
|
|
@@ -26300,7 +26909,7 @@ async function loadStepResultSummary(arg, cwd) {
|
|
|
26300
26909
|
if (arg.startsWith("@")) {
|
|
26301
26910
|
const path3 = resolve2(cwd, arg.slice(1));
|
|
26302
26911
|
try {
|
|
26303
|
-
await
|
|
26912
|
+
await stat4(path3);
|
|
26304
26913
|
} catch {
|
|
26305
26914
|
return { ok: false, reason: `--result @<file>: ${path3} not readable.` };
|
|
26306
26915
|
}
|
|
@@ -27430,7 +28039,7 @@ var init_parser = __esm(() => {
|
|
|
27430
28039
|
|
|
27431
28040
|
// src/core/saved-queries/loader.ts
|
|
27432
28041
|
import { readdir } from "fs/promises";
|
|
27433
|
-
import { join as
|
|
28042
|
+
import { join as join16, relative, sep } from "path";
|
|
27434
28043
|
async function loadSnippets(opts) {
|
|
27435
28044
|
const builtin = await walkAndParse(opts.builtinDir, "builtin");
|
|
27436
28045
|
const shared = await walkAndParse(opts.sharedDir, "shared");
|
|
@@ -27497,7 +28106,7 @@ async function collectFiles(root) {
|
|
|
27497
28106
|
async function walk(dir) {
|
|
27498
28107
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
27499
28108
|
for (const e of entries) {
|
|
27500
|
-
const full =
|
|
28109
|
+
const full = join16(dir, e.name);
|
|
27501
28110
|
if (e.isDirectory())
|
|
27502
28111
|
await walk(full);
|
|
27503
28112
|
else
|
|
@@ -27581,23 +28190,23 @@ var init_runner = __esm(() => {
|
|
|
27581
28190
|
});
|
|
27582
28191
|
|
|
27583
28192
|
// src/core/saved-queries/snippet-paths.ts
|
|
27584
|
-
import { join as
|
|
28193
|
+
import { join as join17 } from "path";
|
|
27585
28194
|
function resolveBuiltinDir() {
|
|
27586
28195
|
return packageAssetPath("snippets");
|
|
27587
28196
|
}
|
|
27588
28197
|
function resolveSnippetDirs(workspaceRoot) {
|
|
27589
28198
|
return {
|
|
27590
28199
|
builtinDir: resolveBuiltinDir(),
|
|
27591
|
-
sharedDir:
|
|
27592
|
-
localDir:
|
|
28200
|
+
sharedDir: join17(workspaceRoot, ".dbcli-shared", "queries"),
|
|
28201
|
+
localDir: join17(workspaceRoot, ".dbcli", "queries")
|
|
27593
28202
|
};
|
|
27594
28203
|
}
|
|
27595
28204
|
function snippetKeyToFile(workspaceRoot, key, source) {
|
|
27596
28205
|
const rel = key.replace(/^@/, "") + ".sql";
|
|
27597
28206
|
if (source === "builtin")
|
|
27598
|
-
return
|
|
28207
|
+
return join17(resolveBuiltinDir(), rel);
|
|
27599
28208
|
const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
|
|
27600
|
-
return
|
|
28209
|
+
return join17(workspaceRoot, dir, rel);
|
|
27601
28210
|
}
|
|
27602
28211
|
var init_snippet_paths = __esm(() => {
|
|
27603
28212
|
init_package_root();
|
|
@@ -27660,7 +28269,7 @@ var exports_queries_delete = {};
|
|
|
27660
28269
|
__export(exports_queries_delete, {
|
|
27661
28270
|
queriesDelete: () => queriesDelete
|
|
27662
28271
|
});
|
|
27663
|
-
import { rm } from "fs/promises";
|
|
28272
|
+
import { rm as rm2 } from "fs/promises";
|
|
27664
28273
|
async function queriesDelete(name, options = {}) {
|
|
27665
28274
|
if (!name.startsWith("@")) {
|
|
27666
28275
|
throw new Error(`Snippet name must start with '@' (got '${name}')`);
|
|
@@ -27685,7 +28294,7 @@ async function queriesDelete(name, options = {}) {
|
|
|
27685
28294
|
return;
|
|
27686
28295
|
}
|
|
27687
28296
|
for (const v of localVariants) {
|
|
27688
|
-
await
|
|
28297
|
+
await rm2(v.query.file, { force: true });
|
|
27689
28298
|
console.log(`deleted ${v.query.file}`);
|
|
27690
28299
|
}
|
|
27691
28300
|
}
|
|
@@ -27699,8 +28308,8 @@ var exports_queries_rename = {};
|
|
|
27699
28308
|
__export(exports_queries_rename, {
|
|
27700
28309
|
queriesRename: () => queriesRename
|
|
27701
28310
|
});
|
|
27702
|
-
import { rename as
|
|
27703
|
-
import { dirname as
|
|
28311
|
+
import { rename as rename4, mkdir as mkdir6 } from "fs/promises";
|
|
28312
|
+
import { dirname as dirname6 } from "path";
|
|
27704
28313
|
async function queriesRename(oldName, newName, options = {}) {
|
|
27705
28314
|
if (!oldName.startsWith("@") || !newName.startsWith("@")) {
|
|
27706
28315
|
throw new Error(`Both names must start with '@'`);
|
|
@@ -27721,8 +28330,8 @@ async function queriesRename(oldName, newName, options = {}) {
|
|
|
27721
28330
|
for (const v of local) {
|
|
27722
28331
|
const dst = snippetKeyToFile(cwd, newName, "local");
|
|
27723
28332
|
const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
|
|
27724
|
-
await
|
|
27725
|
-
await
|
|
28333
|
+
await mkdir6(dirname6(dstWithSuffix), { recursive: true });
|
|
28334
|
+
await rename4(v.query.file, dstWithSuffix);
|
|
27726
28335
|
await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
|
|
27727
28336
|
console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
|
|
27728
28337
|
}
|
|
@@ -27747,8 +28356,8 @@ var exports_queries_copy = {};
|
|
|
27747
28356
|
__export(exports_queries_copy, {
|
|
27748
28357
|
queriesCopy: () => queriesCopy
|
|
27749
28358
|
});
|
|
27750
|
-
import { mkdir as
|
|
27751
|
-
import { dirname as
|
|
28359
|
+
import { mkdir as mkdir7, copyFile } from "fs/promises";
|
|
28360
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
27752
28361
|
async function queriesCopy(src, dst, options = {}) {
|
|
27753
28362
|
if (!src.startsWith("@") || !dst.startsWith("@")) {
|
|
27754
28363
|
throw new Error(`Both names must start with '@'`);
|
|
@@ -27766,7 +28375,7 @@ async function queriesCopy(src, dst, options = {}) {
|
|
|
27766
28375
|
}
|
|
27767
28376
|
for (const v of variants) {
|
|
27768
28377
|
const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
|
|
27769
|
-
await
|
|
28378
|
+
await mkdir7(dirname7(dstFile), { recursive: true });
|
|
27770
28379
|
await copyFile(v.query.file, dstFile);
|
|
27771
28380
|
console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
|
|
27772
28381
|
}
|
|
@@ -27784,21 +28393,21 @@ var exports_queries_import = {};
|
|
|
27784
28393
|
__export(exports_queries_import, {
|
|
27785
28394
|
queriesImport: () => queriesImport
|
|
27786
28395
|
});
|
|
27787
|
-
import { stat as
|
|
27788
|
-
import { basename as basename3, join as
|
|
28396
|
+
import { stat as stat5, mkdir as mkdir8, copyFile as copyFile2 } from "fs/promises";
|
|
28397
|
+
import { basename as basename3, join as join19, extname } from "path";
|
|
27789
28398
|
async function queriesImport(filePath, options = {}) {
|
|
27790
28399
|
const cwd = options.cwd ?? process.cwd();
|
|
27791
28400
|
if (extname(filePath) !== ".sql") {
|
|
27792
28401
|
throw new Error(`Expected .sql file, got ${filePath}`);
|
|
27793
28402
|
}
|
|
27794
|
-
await
|
|
28403
|
+
await stat5(filePath);
|
|
27795
28404
|
const text2 = await Bun.file(filePath).text();
|
|
27796
28405
|
const baseName = options.as ? options.as.replace(/^@/, "") : basename3(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
|
|
27797
28406
|
const key = "@" + baseName;
|
|
27798
28407
|
parseSavedQuery({ key, file: filePath, source: "local", text: text2 });
|
|
27799
|
-
const targetDir =
|
|
27800
|
-
await
|
|
27801
|
-
const target =
|
|
28408
|
+
const targetDir = join19(cwd, ".dbcli/queries");
|
|
28409
|
+
await mkdir8(targetDir, { recursive: true });
|
|
28410
|
+
const target = join19(targetDir, basename3(filePath));
|
|
27802
28411
|
if (await Bun.file(target).exists()) {
|
|
27803
28412
|
if (!options.force) {
|
|
27804
28413
|
const ok = await dist_default6({ message: `Overwrite ${target}?`, default: false });
|
|
@@ -27819,7 +28428,7 @@ var exports_queries_export = {};
|
|
|
27819
28428
|
__export(exports_queries_export, {
|
|
27820
28429
|
queriesExport: () => queriesExport
|
|
27821
28430
|
});
|
|
27822
|
-
import { writeFile as
|
|
28431
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
27823
28432
|
async function queriesExport(name, options = {}) {
|
|
27824
28433
|
const cwd = options.cwd ?? process.cwd();
|
|
27825
28434
|
const dirs = resolveSnippetDirs(cwd);
|
|
@@ -27840,7 +28449,7 @@ async function queriesExport(name, options = {}) {
|
|
|
27840
28449
|
}
|
|
27841
28450
|
const text2 = await Bun.file(chosen[0].query.file).text();
|
|
27842
28451
|
if (options.output) {
|
|
27843
|
-
await
|
|
28452
|
+
await writeFile3(options.output, text2, "utf8");
|
|
27844
28453
|
console.log(`wrote ${options.output}`);
|
|
27845
28454
|
} else {
|
|
27846
28455
|
process.stdout.write(text2);
|
|
@@ -27923,17 +28532,17 @@ var init_collect_snippets = __esm(() => {
|
|
|
27923
28532
|
});
|
|
27924
28533
|
|
|
27925
28534
|
// src/core/inspect/collect-schema-cache.ts
|
|
27926
|
-
import { join as
|
|
28535
|
+
import { join as join23 } from "path";
|
|
27927
28536
|
async function collectSchemaCache(opts) {
|
|
27928
28537
|
const warnings = [];
|
|
27929
|
-
if (opts.system && !
|
|
28538
|
+
if (opts.system && !SQL_SYSTEMS2.includes(opts.system)) {
|
|
27930
28539
|
return {
|
|
27931
28540
|
section: { available: false, unavailable: true, reason: "schema cache is sql-only" },
|
|
27932
28541
|
warnings
|
|
27933
28542
|
};
|
|
27934
28543
|
}
|
|
27935
28544
|
const root = resolveSchemaPath(opts.dbcliPath, opts.connectionName);
|
|
27936
|
-
const indexPath =
|
|
28545
|
+
const indexPath = join23(root, "index.json");
|
|
27937
28546
|
const file = Bun.file(indexPath);
|
|
27938
28547
|
if (!await file.exists()) {
|
|
27939
28548
|
return { section: { available: false }, warnings };
|
|
@@ -27960,11 +28569,11 @@ async function collectSchemaCache(opts) {
|
|
|
27960
28569
|
};
|
|
27961
28570
|
}
|
|
27962
28571
|
}
|
|
27963
|
-
var STALE_AFTER_MS,
|
|
28572
|
+
var STALE_AFTER_MS, SQL_SYSTEMS2;
|
|
27964
28573
|
var init_collect_schema_cache = __esm(() => {
|
|
27965
28574
|
init_schema_path();
|
|
27966
28575
|
STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
27967
|
-
|
|
28576
|
+
SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
|
|
27968
28577
|
});
|
|
27969
28578
|
|
|
27970
28579
|
// src/core/inspect/collect-objects.ts
|
|
@@ -28024,7 +28633,7 @@ function suggestCommands(snap, options = {}) {
|
|
|
28024
28633
|
if (!snap.system)
|
|
28025
28634
|
return ["dbcli init"];
|
|
28026
28635
|
const out = [];
|
|
28027
|
-
const isSql =
|
|
28636
|
+
const isSql = SQL_SYSTEMS3.includes(snap.system);
|
|
28028
28637
|
if (isSql && (!snap.schemaCache.available || snap.schemaCache.stale)) {
|
|
28029
28638
|
out.push("dbcli schema --refresh");
|
|
28030
28639
|
}
|
|
@@ -28039,13 +28648,13 @@ function suggestCommands(snap, options = {}) {
|
|
|
28039
28648
|
const cap2 = options.brief ? 1 : 5;
|
|
28040
28649
|
return out.slice(0, cap2);
|
|
28041
28650
|
}
|
|
28042
|
-
var
|
|
28651
|
+
var SQL_SYSTEMS3;
|
|
28043
28652
|
var init_suggest_commands = __esm(() => {
|
|
28044
|
-
|
|
28653
|
+
SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
|
|
28045
28654
|
});
|
|
28046
28655
|
|
|
28047
28656
|
// src/core/inspect/collector.ts
|
|
28048
|
-
import { join as
|
|
28657
|
+
import { join as join24 } from "path";
|
|
28049
28658
|
async function collectInspect(opts) {
|
|
28050
28659
|
const warnings = [];
|
|
28051
28660
|
let config = null;
|
|
@@ -28121,11 +28730,11 @@ async function collectInspect(opts) {
|
|
|
28121
28730
|
return { ...snapWithoutSuggestions, suggestedCommands, warnings };
|
|
28122
28731
|
}
|
|
28123
28732
|
async function hasConfig(configPath) {
|
|
28124
|
-
if (await Bun.file(
|
|
28733
|
+
if (await Bun.file(join24(configPath, "config.json")).exists())
|
|
28125
28734
|
return true;
|
|
28126
28735
|
if (await Bun.file(configPath).exists()) {
|
|
28127
|
-
const
|
|
28128
|
-
return
|
|
28736
|
+
const stat6 = await Bun.file(configPath).stat().catch(() => null);
|
|
28737
|
+
return stat6?.isFile() === true;
|
|
28129
28738
|
}
|
|
28130
28739
|
return false;
|
|
28131
28740
|
}
|
|
@@ -28247,16 +28856,16 @@ function renderMarkdown2(snap, options = {}) {
|
|
|
28247
28856
|
function requireSchemaCacheOrThrow(section, system) {
|
|
28248
28857
|
if (!system)
|
|
28249
28858
|
return;
|
|
28250
|
-
if (!
|
|
28859
|
+
if (!SQL_SYSTEMS4.includes(system))
|
|
28251
28860
|
return;
|
|
28252
28861
|
if (section.available === true)
|
|
28253
28862
|
return;
|
|
28254
28863
|
throw new SchemaCacheMissingError(`Schema cache unavailable for ${system} connection; run 'dbcli schema --refresh' to populate it.`);
|
|
28255
28864
|
}
|
|
28256
|
-
var
|
|
28865
|
+
var SQL_SYSTEMS4;
|
|
28257
28866
|
var init_require_schema_cache = __esm(() => {
|
|
28258
28867
|
init_recovery();
|
|
28259
|
-
|
|
28868
|
+
SQL_SYSTEMS4 = ["postgresql", "mysql", "mariadb"];
|
|
28260
28869
|
});
|
|
28261
28870
|
|
|
28262
28871
|
// src/core/inspect/index.ts
|
|
@@ -28273,6 +28882,125 @@ var init_inspect = __esm(() => {
|
|
|
28273
28882
|
init_collector();
|
|
28274
28883
|
});
|
|
28275
28884
|
|
|
28885
|
+
// src/core/audit/reader.ts
|
|
28886
|
+
import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
|
|
28887
|
+
async function readSingle(path5) {
|
|
28888
|
+
let raw;
|
|
28889
|
+
try {
|
|
28890
|
+
raw = await readFile4(path5, "utf8");
|
|
28891
|
+
} catch (err) {
|
|
28892
|
+
if (err.code === "ENOENT")
|
|
28893
|
+
return [];
|
|
28894
|
+
throw err;
|
|
28895
|
+
}
|
|
28896
|
+
const lines = raw.split(`
|
|
28897
|
+
`).filter(Boolean);
|
|
28898
|
+
const out = [];
|
|
28899
|
+
for (let i = 0;i < lines.length; i++) {
|
|
28900
|
+
try {
|
|
28901
|
+
out.push(JSON.parse(lines[i]));
|
|
28902
|
+
} catch {
|
|
28903
|
+
if (i === lines.length - 1) {
|
|
28904
|
+
process.stderr.write(`${WARN_PREFIX2} skipping truncated last line in ${path5}
|
|
28905
|
+
`);
|
|
28906
|
+
continue;
|
|
28907
|
+
}
|
|
28908
|
+
throw new Error(`${WARN_PREFIX2} corrupted line ${i + 1} in ${path5}. Run \`dbcli audit clear\` to reset.`);
|
|
28909
|
+
}
|
|
28910
|
+
}
|
|
28911
|
+
return out;
|
|
28912
|
+
}
|
|
28913
|
+
async function readEntries(auditFilePath, opts) {
|
|
28914
|
+
if (opts?.include_rotated === true) {
|
|
28915
|
+
const rotated = await readSingle(`${auditFilePath}.1`);
|
|
28916
|
+
const current = await readSingle(auditFilePath);
|
|
28917
|
+
return rotated.concat(current);
|
|
28918
|
+
}
|
|
28919
|
+
return readSingle(auditFilePath);
|
|
28920
|
+
}
|
|
28921
|
+
async function discoverConnections(auditDir) {
|
|
28922
|
+
let entries;
|
|
28923
|
+
try {
|
|
28924
|
+
entries = await readdir3(auditDir, { withFileTypes: true });
|
|
28925
|
+
} catch (err) {
|
|
28926
|
+
if (err.code === "ENOENT")
|
|
28927
|
+
return [];
|
|
28928
|
+
throw err;
|
|
28929
|
+
}
|
|
28930
|
+
const map = new Map;
|
|
28931
|
+
for (const e of entries) {
|
|
28932
|
+
if (!e.isFile())
|
|
28933
|
+
continue;
|
|
28934
|
+
if (!(e.name.endsWith(".jsonl") || e.name.endsWith(".jsonl.1")))
|
|
28935
|
+
continue;
|
|
28936
|
+
const conn = e.name.replace(/\.jsonl(?:\.1)?$/, "");
|
|
28937
|
+
const full = `${auditDir}/${e.name}`;
|
|
28938
|
+
const list = map.get(conn) ?? [];
|
|
28939
|
+
list.push(full);
|
|
28940
|
+
map.set(conn, list);
|
|
28941
|
+
}
|
|
28942
|
+
for (const files of map.values()) {
|
|
28943
|
+
files.sort((a, b) => Number(b.endsWith(".jsonl.1")) - Number(a.endsWith(".jsonl.1")));
|
|
28944
|
+
}
|
|
28945
|
+
return Array.from(map.entries()).map(([connection, files]) => ({ connection, files })).sort((a, b) => a.connection.localeCompare(b.connection));
|
|
28946
|
+
}
|
|
28947
|
+
function tailEntries(entries, n) {
|
|
28948
|
+
if (n <= 0)
|
|
28949
|
+
return [];
|
|
28950
|
+
return entries.slice().sort((a, b) => a.ts.localeCompare(b.ts)).slice(-n);
|
|
28951
|
+
}
|
|
28952
|
+
function mergeByTimestamp(byConn) {
|
|
28953
|
+
const flat = [];
|
|
28954
|
+
for (const [connection, entries] of byConn) {
|
|
28955
|
+
for (const entry of entries)
|
|
28956
|
+
flat.push({ connection, entry });
|
|
28957
|
+
}
|
|
28958
|
+
flat.sort((a, b) => {
|
|
28959
|
+
const t2 = a.entry.ts.localeCompare(b.entry.ts);
|
|
28960
|
+
return t2 !== 0 ? t2 : a.connection.localeCompare(b.connection);
|
|
28961
|
+
});
|
|
28962
|
+
return flat;
|
|
28963
|
+
}
|
|
28964
|
+
var WARN_PREFIX2 = "[dbcli audit]";
|
|
28965
|
+
var init_reader = () => {};
|
|
28966
|
+
|
|
28967
|
+
// src/core/audit/recent.ts
|
|
28968
|
+
var exports_recent = {};
|
|
28969
|
+
__export(exports_recent, {
|
|
28970
|
+
shouldEmbedRecent: () => shouldEmbedRecent,
|
|
28971
|
+
loadRecentAudit: () => loadRecentAudit,
|
|
28972
|
+
RECENT_AUDIT_DEFAULT_N: () => RECENT_AUDIT_DEFAULT_N
|
|
28973
|
+
});
|
|
28974
|
+
function shouldEmbedRecent(opts) {
|
|
28975
|
+
return opts.forAgent === true || opts.format === "json";
|
|
28976
|
+
}
|
|
28977
|
+
function briefifyForRecent(entry) {
|
|
28978
|
+
return {
|
|
28979
|
+
id: entry.id,
|
|
28980
|
+
ts: entry.ts,
|
|
28981
|
+
command: entry.command,
|
|
28982
|
+
target: entry.target,
|
|
28983
|
+
success: entry.success
|
|
28984
|
+
};
|
|
28985
|
+
}
|
|
28986
|
+
async function loadRecentAudit(config, configPath, n = RECENT_AUDIT_DEFAULT_N) {
|
|
28987
|
+
try {
|
|
28988
|
+
if (config.audit?.enabled === false)
|
|
28989
|
+
return [];
|
|
28990
|
+
const logger = await getAuditLogger(config, configPath);
|
|
28991
|
+
const auditFile = logger.getHealth().currentFile;
|
|
28992
|
+
const entries = await readEntries(auditFile, { include_rotated: true });
|
|
28993
|
+
return tailEntries(entries, n).map(briefifyForRecent);
|
|
28994
|
+
} catch {
|
|
28995
|
+
return [];
|
|
28996
|
+
}
|
|
28997
|
+
}
|
|
28998
|
+
var RECENT_AUDIT_DEFAULT_N = 5;
|
|
28999
|
+
var init_recent = __esm(() => {
|
|
29000
|
+
init_integration_helper();
|
|
29001
|
+
init_reader();
|
|
29002
|
+
});
|
|
29003
|
+
|
|
28276
29004
|
// node_modules/commander/esm.mjs
|
|
28277
29005
|
var import__ = __toESM(require_commander(), 1);
|
|
28278
29006
|
var {
|
|
@@ -28291,7 +29019,7 @@ var {
|
|
|
28291
29019
|
// package.json
|
|
28292
29020
|
var package_default = {
|
|
28293
29021
|
name: "@carllee1983/dbcli",
|
|
28294
|
-
version: "1.
|
|
29022
|
+
version: "1.20.1",
|
|
28295
29023
|
description: "Database CLI for AI agents",
|
|
28296
29024
|
type: "module",
|
|
28297
29025
|
publishConfig: {
|
|
@@ -28341,6 +29069,7 @@ var package_default = {
|
|
|
28341
29069
|
"test:unit": "bun test tests/unit tests/core",
|
|
28342
29070
|
"test:integration": "bun test tests/integration",
|
|
28343
29071
|
"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",
|
|
29072
|
+
"docs:check": "bun run scripts/check-user-docs.ts",
|
|
28344
29073
|
typecheck: "tsc --noEmit --pretty false",
|
|
28345
29074
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
28346
29075
|
lint: "eslint src tests --ext .ts --max-warnings=0",
|
|
@@ -28741,7 +29470,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28741
29470
|
schema: v1Config.schema || {},
|
|
28742
29471
|
schemas: { default: v1Config.schema || {} },
|
|
28743
29472
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28744
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29473
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29474
|
+
audit: v1Config.audit
|
|
28745
29475
|
};
|
|
28746
29476
|
}
|
|
28747
29477
|
} else if (await projectConfigFile.exists()) {
|
|
@@ -28760,7 +29490,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28760
29490
|
schema: v1Config.schema || {},
|
|
28761
29491
|
schemas: { default: v1Config.schema || {} },
|
|
28762
29492
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28763
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29493
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29494
|
+
audit: v1Config.audit
|
|
28764
29495
|
};
|
|
28765
29496
|
} else {
|
|
28766
29497
|
const v1Config = await configModule.read(configPath);
|
|
@@ -28776,7 +29507,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28776
29507
|
schema: v1Config.schema || {},
|
|
28777
29508
|
schemas: { default: v1Config.schema || {} },
|
|
28778
29509
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28779
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29510
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29511
|
+
audit: v1Config.audit
|
|
28780
29512
|
};
|
|
28781
29513
|
}
|
|
28782
29514
|
} else {
|
|
@@ -28797,7 +29529,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28797
29529
|
schema: v1Config.schema || {},
|
|
28798
29530
|
schemas: { default: v1Config.schema || {} },
|
|
28799
29531
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
28800
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
29532
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
29533
|
+
audit: v1Config.audit
|
|
28801
29534
|
};
|
|
28802
29535
|
}
|
|
28803
29536
|
}
|
|
@@ -28824,7 +29557,11 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
|
|
|
28824
29557
|
schema: {},
|
|
28825
29558
|
schemas: {},
|
|
28826
29559
|
metadata: { version: "1.0", createdAt: new Date().toISOString() },
|
|
28827
|
-
blacklist: { tables: [], columns: {} }
|
|
29560
|
+
blacklist: { tables: [], columns: {} },
|
|
29561
|
+
audit: {
|
|
29562
|
+
enabled: true,
|
|
29563
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
29564
|
+
}
|
|
28828
29565
|
};
|
|
28829
29566
|
await writeV2Config(storagePath, v2Config);
|
|
28830
29567
|
await migrateLegacyProjectEnvLocal(configPath, storagePath);
|
|
@@ -29482,6 +30219,7 @@ async function elasticsearchListBranch(config, format, includeSystem) {
|
|
|
29482
30219
|
}
|
|
29483
30220
|
|
|
29484
30221
|
// src/commands/schema.ts
|
|
30222
|
+
import crypto from "crypto";
|
|
29485
30223
|
init_message_loader();
|
|
29486
30224
|
init_adapters();
|
|
29487
30225
|
init_config();
|
|
@@ -30155,15 +30893,17 @@ function rowsOf(result) {
|
|
|
30155
30893
|
init_schema_loader();
|
|
30156
30894
|
|
|
30157
30895
|
// src/commands/schema.ts
|
|
30896
|
+
init_integration_helper();
|
|
30158
30897
|
init_validation();
|
|
30159
30898
|
var ALLOWED_FORMATS2 = ["table", "json"];
|
|
30160
30899
|
var schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 50, max 1000). Ignored on SQL connections.").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
|
|
30161
30900
|
async function schemaAction(table, options, command) {
|
|
30901
|
+
let config;
|
|
30162
30902
|
try {
|
|
30163
30903
|
validateFormat(options.format, ALLOWED_FORMATS2, "schema");
|
|
30164
30904
|
const configPath = resolveConfigPath(command, options);
|
|
30165
30905
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
30166
|
-
|
|
30906
|
+
config = await configModule.read(configPath);
|
|
30167
30907
|
if (!config.connection) {
|
|
30168
30908
|
console.error("Database not configured. Run: dbcli init");
|
|
30169
30909
|
process.exit(1);
|
|
@@ -30219,6 +30959,10 @@ Key: ${schema.name}`);
|
|
|
30219
30959
|
console.log(` ${col.name}: ${col.type}`);
|
|
30220
30960
|
}
|
|
30221
30961
|
}
|
|
30962
|
+
await writeAuditEntry(config, "schema", options, {
|
|
30963
|
+
success: true,
|
|
30964
|
+
target: table
|
|
30965
|
+
});
|
|
30222
30966
|
} finally {
|
|
30223
30967
|
await redisAdapter.disconnect();
|
|
30224
30968
|
}
|
|
@@ -30232,9 +30976,17 @@ Key: ${schema.name}`);
|
|
|
30232
30976
|
if (!esAdapter.getTableSchema)
|
|
30233
30977
|
throw new Error("Elasticsearch adapter does not implement getTableSchema");
|
|
30234
30978
|
await handleSingleTableSchema(esAdapter, table, options.format, inferenceOptions);
|
|
30979
|
+
await writeAuditEntry(config, "schema", options, {
|
|
30980
|
+
success: true,
|
|
30981
|
+
target: table
|
|
30982
|
+
});
|
|
30235
30983
|
return;
|
|
30236
30984
|
}
|
|
30237
30985
|
await handleFullDatabaseScan(esAdapter, config, options, connectionName, existingSchemaCount, storagePath, inferenceOptions);
|
|
30986
|
+
await writeAuditEntry(config, "schema", options, {
|
|
30987
|
+
success: true,
|
|
30988
|
+
target: "*"
|
|
30989
|
+
});
|
|
30238
30990
|
return;
|
|
30239
30991
|
} finally {
|
|
30240
30992
|
await esAdapter.disconnect();
|
|
@@ -30252,13 +31004,30 @@ Key: ${schema.name}`);
|
|
|
30252
31004
|
} else {
|
|
30253
31005
|
await handleFullDatabaseScan(adapter, config, options, connectionName, existingSchemaCount, storagePath, inferenceOptions);
|
|
30254
31006
|
}
|
|
31007
|
+
await writeAuditEntry(config, "schema", options, {
|
|
31008
|
+
success: true,
|
|
31009
|
+
target: table ?? "*"
|
|
31010
|
+
});
|
|
30255
31011
|
} finally {
|
|
30256
31012
|
await adapter.disconnect();
|
|
30257
31013
|
}
|
|
30258
31014
|
} catch (error) {
|
|
31015
|
+
let auditId = null;
|
|
31016
|
+
let envelopeId;
|
|
30259
31017
|
if (options.recovery === true) {
|
|
31018
|
+
envelopeId = crypto.randomUUID();
|
|
31019
|
+
}
|
|
31020
|
+
if (config) {
|
|
31021
|
+
auditId = await writeAuditEntry(config, "schema", options, {
|
|
31022
|
+
success: false,
|
|
31023
|
+
target: table ?? "*",
|
|
31024
|
+
error,
|
|
31025
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
31026
|
+
});
|
|
31027
|
+
}
|
|
31028
|
+
if (envelopeId !== undefined) {
|
|
30260
31029
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
30261
|
-
emitRecoveryEnvelope2(error, { operation: "schema", table });
|
|
31030
|
+
emitRecoveryEnvelope2(error, { operation: "schema", table }, { envelopeId, auditRef: auditId ?? undefined });
|
|
30262
31031
|
}
|
|
30263
31032
|
if (error instanceof Error) {
|
|
30264
31033
|
console.error(t_vars("errors.message", { message: error.message }));
|
|
@@ -30496,6 +31265,7 @@ async function writeSchema(configPath, config, connectionName) {
|
|
|
30496
31265
|
// src/commands/query.ts
|
|
30497
31266
|
init_message_loader();
|
|
30498
31267
|
init_adapters();
|
|
31268
|
+
import crypto2 from "crypto";
|
|
30499
31269
|
|
|
30500
31270
|
// src/formatters/html-formatter.ts
|
|
30501
31271
|
init_package_root();
|
|
@@ -30513,14 +31283,14 @@ async function generateHtmlReport(payload) {
|
|
|
30513
31283
|
}
|
|
30514
31284
|
|
|
30515
31285
|
// src/utils/opener.ts
|
|
30516
|
-
import
|
|
31286
|
+
import open2 from "open";
|
|
30517
31287
|
async function openInBrowser(target) {
|
|
30518
31288
|
if (process.env.DBCLI_NO_OPEN === "1" || false) {
|
|
30519
31289
|
console.log(`[Opener] DBCLI_NO_OPEN is set. Skipping browser launch for: ${target}`);
|
|
30520
31290
|
return;
|
|
30521
31291
|
}
|
|
30522
31292
|
try {
|
|
30523
|
-
await
|
|
31293
|
+
await open2(target);
|
|
30524
31294
|
} catch (err) {
|
|
30525
31295
|
console.error(`[Opener] Failed to open ${target}:`, err);
|
|
30526
31296
|
}
|
|
@@ -30528,7 +31298,7 @@ async function openInBrowser(target) {
|
|
|
30528
31298
|
|
|
30529
31299
|
// src/commands/query.ts
|
|
30530
31300
|
import { tmpdir } from "os";
|
|
30531
|
-
import { join as
|
|
31301
|
+
import { join as join15 } from "path";
|
|
30532
31302
|
|
|
30533
31303
|
// src/core/query-executor.ts
|
|
30534
31304
|
init_permission_guard();
|
|
@@ -30596,20 +31366,30 @@ async function getAllTablesFromAdapter(adapter) {
|
|
|
30596
31366
|
}
|
|
30597
31367
|
}
|
|
30598
31368
|
|
|
31369
|
+
// src/core/query-executor.ts
|
|
31370
|
+
init_engine_hints();
|
|
31371
|
+
|
|
30599
31372
|
// src/core/limits.ts
|
|
30600
31373
|
var DEFAULT_QUERY_ONLY_LIMIT = 1000;
|
|
30601
31374
|
|
|
30602
31375
|
// src/core/query-executor.ts
|
|
31376
|
+
init_integration_helper();
|
|
31377
|
+
|
|
30603
31378
|
class QueryExecutor {
|
|
30604
31379
|
adapter;
|
|
30605
31380
|
permission;
|
|
30606
31381
|
blacklistValidator;
|
|
30607
|
-
|
|
31382
|
+
config;
|
|
31383
|
+
options;
|
|
31384
|
+
constructor(adapter, permission, blacklistValidator, config, options = {}) {
|
|
30608
31385
|
this.adapter = adapter;
|
|
30609
31386
|
this.permission = permission;
|
|
30610
31387
|
this.blacklistValidator = blacklistValidator;
|
|
31388
|
+
this.config = config;
|
|
31389
|
+
this.options = options;
|
|
30611
31390
|
}
|
|
30612
31391
|
async execute(sql, options) {
|
|
31392
|
+
const start = performance.now();
|
|
30613
31393
|
try {
|
|
30614
31394
|
const classification = enforcePermission(sql, this.permission);
|
|
30615
31395
|
if (classification.isDangerous && this.permission === "admin") {
|
|
@@ -30627,7 +31407,6 @@ class QueryExecutor {
|
|
|
30627
31407
|
this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
|
|
30628
31408
|
}
|
|
30629
31409
|
}
|
|
30630
|
-
const start = performance.now();
|
|
30631
31410
|
const resultData = await this.adapter.execute(executeSql);
|
|
30632
31411
|
const executionTimeMs = Math.round(performance.now() - start);
|
|
30633
31412
|
const rows = resultData.rows;
|
|
@@ -30662,8 +31441,28 @@ class QueryExecutor {
|
|
|
30662
31441
|
...securityNotification ? { securityNotification } : {}
|
|
30663
31442
|
}
|
|
30664
31443
|
};
|
|
31444
|
+
if (this.config) {
|
|
31445
|
+
await writeAuditEntry(this.config, "query", this.options, {
|
|
31446
|
+
success: true,
|
|
31447
|
+
sql,
|
|
31448
|
+
metadata: {
|
|
31449
|
+
rows_affected: affectedRows,
|
|
31450
|
+
execution_ms: executionTimeMs
|
|
31451
|
+
}
|
|
31452
|
+
});
|
|
31453
|
+
}
|
|
30665
31454
|
return result;
|
|
30666
31455
|
} catch (error) {
|
|
31456
|
+
if (this.config) {
|
|
31457
|
+
await writeAuditEntry(this.config, "query", this.options, {
|
|
31458
|
+
success: false,
|
|
31459
|
+
sql,
|
|
31460
|
+
error,
|
|
31461
|
+
metadata: {
|
|
31462
|
+
execution_ms: Math.round(performance.now() - start)
|
|
31463
|
+
}
|
|
31464
|
+
});
|
|
31465
|
+
}
|
|
30667
31466
|
if (error instanceof PermissionError) {
|
|
30668
31467
|
throw error;
|
|
30669
31468
|
}
|
|
@@ -30685,25 +31484,6 @@ class QueryExecutor {
|
|
|
30685
31484
|
}
|
|
30686
31485
|
}
|
|
30687
31486
|
}
|
|
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
31487
|
function inferColumnType(value) {
|
|
30708
31488
|
if (value === null || value === undefined) {
|
|
30709
31489
|
return "null";
|
|
@@ -30728,6 +31508,7 @@ init_config();
|
|
|
30728
31508
|
init_permission_guard();
|
|
30729
31509
|
init_blacklist();
|
|
30730
31510
|
init_validation();
|
|
31511
|
+
init_integration_helper();
|
|
30731
31512
|
function requireSqlConnection2(connection) {
|
|
30732
31513
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
30733
31514
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
@@ -30736,6 +31517,7 @@ function requireSqlConnection2(connection) {
|
|
|
30736
31517
|
}
|
|
30737
31518
|
var ALLOWED_FORMATS3 = ["table", "json", "csv"];
|
|
30738
31519
|
async function queryCommand(sql, options, command) {
|
|
31520
|
+
let config;
|
|
30739
31521
|
try {
|
|
30740
31522
|
if (!sql || sql.trim() === "") {
|
|
30741
31523
|
throw new Error("SQL query required");
|
|
@@ -30745,7 +31527,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30745
31527
|
}
|
|
30746
31528
|
sql = sql.trim();
|
|
30747
31529
|
const configPath = resolveConfigPath(command, options);
|
|
30748
|
-
|
|
31530
|
+
config = await configModule.read(configPath);
|
|
30749
31531
|
if (!config.connection) {
|
|
30750
31532
|
throw new Error('Run "dbcli init" first');
|
|
30751
31533
|
}
|
|
@@ -30758,7 +31540,8 @@ async function queryCommand(sql, options, command) {
|
|
|
30758
31540
|
if (config.connection.system === "elasticsearch") {
|
|
30759
31541
|
return await elasticsearchQueryBranch(sql, options, config);
|
|
30760
31542
|
}
|
|
30761
|
-
const
|
|
31543
|
+
const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
|
|
31544
|
+
const mainTable = extractTableName2(sql);
|
|
30762
31545
|
if (mainTable && config.schema && !options.noLimit) {
|
|
30763
31546
|
const tableSchema = config.schema[mainTable];
|
|
30764
31547
|
if (tableSchema) {
|
|
@@ -30774,7 +31557,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30774
31557
|
try {
|
|
30775
31558
|
const blacklistManager = new BlacklistManager(config);
|
|
30776
31559
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
30777
|
-
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator);
|
|
31560
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
|
|
30778
31561
|
const autoLimit = !options.noLimit;
|
|
30779
31562
|
const result = await executor3.execute(sql, {
|
|
30780
31563
|
autoLimit,
|
|
@@ -30792,7 +31575,7 @@ async function queryCommand(sql, options, command) {
|
|
|
30792
31575
|
rows: result.rows
|
|
30793
31576
|
});
|
|
30794
31577
|
if (options.ui) {
|
|
30795
|
-
const tempPath =
|
|
31578
|
+
const tempPath = join15(tmpdir(), `dbcli-query-${Date.now()}.html`);
|
|
30796
31579
|
await Bun.write(tempPath, html);
|
|
30797
31580
|
await openInBrowser(tempPath);
|
|
30798
31581
|
} else {
|
|
@@ -30809,11 +31592,27 @@ async function queryCommand(sql, options, command) {
|
|
|
30809
31592
|
await adapter.disconnect();
|
|
30810
31593
|
}
|
|
30811
31594
|
} catch (error) {
|
|
31595
|
+
let auditId = null;
|
|
31596
|
+
let envelopeId;
|
|
30812
31597
|
if (options.recovery === true) {
|
|
31598
|
+
envelopeId = crypto2.randomUUID();
|
|
31599
|
+
}
|
|
31600
|
+
if (config) {
|
|
31601
|
+
auditId = await writeAuditEntry(config, "query", options, {
|
|
31602
|
+
success: false,
|
|
31603
|
+
sql,
|
|
31604
|
+
error,
|
|
31605
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
31606
|
+
});
|
|
31607
|
+
}
|
|
31608
|
+
if (envelopeId !== undefined) {
|
|
30813
31609
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
30814
31610
|
emitRecoveryEnvelope2(error, {
|
|
30815
31611
|
operation: "query",
|
|
30816
|
-
table:
|
|
31612
|
+
table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(sql) ?? undefined
|
|
31613
|
+
}, {
|
|
31614
|
+
envelopeId,
|
|
31615
|
+
auditRef: auditId ?? undefined
|
|
30817
31616
|
});
|
|
30818
31617
|
}
|
|
30819
31618
|
if (error instanceof BlacklistError) {
|
|
@@ -30834,21 +31633,17 @@ async function queryCommand(sql, options, command) {
|
|
|
30834
31633
|
process.exit(1);
|
|
30835
31634
|
}
|
|
30836
31635
|
}
|
|
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
31636
|
async function mongoQueryBranch(queryStr, options, config) {
|
|
30843
31637
|
const collection = options.collection;
|
|
30844
31638
|
const format = options.format ?? "table";
|
|
31639
|
+
if (!collection) {
|
|
31640
|
+
throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
|
|
31641
|
+
}
|
|
31642
|
+
const SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
30845
31643
|
if (SQL_PATTERN.test(queryStr)) {
|
|
30846
31644
|
throw new Error(`\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u8A9E\u6CD5\u3002
|
|
30847
31645
|
\u7BC4\u4F8B\uFF1Adbcli query '{"field": "value"}' --collection <name>`);
|
|
30848
31646
|
}
|
|
30849
|
-
if (!collection) {
|
|
30850
|
-
throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
|
|
30851
|
-
}
|
|
30852
31647
|
try {
|
|
30853
31648
|
JSON.parse(queryStr);
|
|
30854
31649
|
} catch {
|
|
@@ -30881,8 +31676,18 @@ async function mongoQueryBranch(queryStr, options, config) {
|
|
|
30881
31676
|
}
|
|
30882
31677
|
const mongoAdapter = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
30883
31678
|
await mongoAdapter.connect();
|
|
31679
|
+
const start = performance.now();
|
|
30884
31680
|
try {
|
|
30885
31681
|
const result = await mongoAdapter.execute(queryStr, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
|
|
31682
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31683
|
+
await writeAuditEntry(config, "query", options, {
|
|
31684
|
+
success: true,
|
|
31685
|
+
target: collection,
|
|
31686
|
+
metadata: {
|
|
31687
|
+
rows_affected: result.rows.length,
|
|
31688
|
+
execution_ms: executionTimeMs
|
|
31689
|
+
}
|
|
31690
|
+
});
|
|
30886
31691
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
|
|
30887
31692
|
const filterResult = blacklistValidator.filterColumns(collection, result.rows, columnNames);
|
|
30888
31693
|
const queryResult = {
|
|
@@ -30894,8 +31699,8 @@ async function mongoQueryBranch(queryStr, options, config) {
|
|
|
30894
31699
|
const output = formatter.format(queryResult, {
|
|
30895
31700
|
format
|
|
30896
31701
|
});
|
|
30897
|
-
const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
|
|
30898
31702
|
console.log(output);
|
|
31703
|
+
const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
|
|
30899
31704
|
if (securityNote) {
|
|
30900
31705
|
console.log(`
|
|
30901
31706
|
\u2139 ${securityNote}`);
|
|
@@ -30909,15 +31714,24 @@ async function redisQueryBranch(command, options, config) {
|
|
|
30909
31714
|
const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
|
|
30910
31715
|
const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
|
|
30911
31716
|
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/");
|
|
31717
|
+
console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers.');
|
|
30915
31718
|
}
|
|
30916
31719
|
enforceRedisPermission2(command, config.permission);
|
|
30917
31720
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
|
|
30918
31721
|
await redisAdapter.connect();
|
|
31722
|
+
const start = performance.now();
|
|
30919
31723
|
try {
|
|
30920
31724
|
const result = await redisAdapter.execute(command);
|
|
31725
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31726
|
+
const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
31727
|
+
await writeAuditEntry(config, "query", options, {
|
|
31728
|
+
success: true,
|
|
31729
|
+
target,
|
|
31730
|
+
metadata: {
|
|
31731
|
+
rows_affected: result.rows.length,
|
|
31732
|
+
execution_ms: executionTimeMs
|
|
31733
|
+
}
|
|
31734
|
+
});
|
|
30921
31735
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
|
|
30922
31736
|
const queryResult = {
|
|
30923
31737
|
rows: result.rows,
|
|
@@ -30949,23 +31763,28 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
|
|
|
30949
31763
|
if (options.noLimit) {
|
|
30950
31764
|
effectiveLimit = 1e4;
|
|
30951
31765
|
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
31766
|
} 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
|
-
}
|
|
31767
|
+
effectiveLimit = options.limit || DEFAULT_QUERY_ONLY_LIMIT;
|
|
30959
31768
|
}
|
|
30960
31769
|
const blacklistManager = new BlacklistManager(config);
|
|
30961
31770
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
30962
31771
|
blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
|
|
30963
31772
|
const esAdapter = AdapterFactory.createElasticsearchAdapter(config.connection);
|
|
30964
31773
|
await esAdapter.connect();
|
|
31774
|
+
const start = performance.now();
|
|
30965
31775
|
try {
|
|
30966
31776
|
const result = await esAdapter.execute(queryStr, [indexName], {
|
|
30967
31777
|
limit: effectiveLimit
|
|
30968
31778
|
});
|
|
31779
|
+
const executionTimeMs = Math.round(performance.now() - start);
|
|
31780
|
+
await writeAuditEntry(config, "query", options, {
|
|
31781
|
+
success: true,
|
|
31782
|
+
target: indexName,
|
|
31783
|
+
metadata: {
|
|
31784
|
+
rows_affected: result.rows.length,
|
|
31785
|
+
execution_ms: executionTimeMs
|
|
31786
|
+
}
|
|
31787
|
+
});
|
|
30969
31788
|
const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
|
|
30970
31789
|
const filterResult = blacklistValidator.filterColumns(indexName, result.rows, columnNames);
|
|
30971
31790
|
const queryResult = {
|
|
@@ -30977,11 +31796,12 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
|
|
|
30977
31796
|
const output = formatter.format(queryResult, {
|
|
30978
31797
|
format
|
|
30979
31798
|
});
|
|
30980
|
-
const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
|
|
30981
31799
|
console.log(output);
|
|
30982
|
-
|
|
31800
|
+
const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
|
|
31801
|
+
if (securityNote) {
|
|
30983
31802
|
console.log(`
|
|
30984
31803
|
\u2139 ${securityNote}`);
|
|
31804
|
+
}
|
|
30985
31805
|
} finally {
|
|
30986
31806
|
await esAdapter.disconnect();
|
|
30987
31807
|
}
|
|
@@ -31338,8 +32158,10 @@ function buildSuggestedCommands(facts, gap) {
|
|
|
31338
32158
|
// src/commands/plan.ts
|
|
31339
32159
|
init_config();
|
|
31340
32160
|
init_validation();
|
|
32161
|
+
init_integration_helper();
|
|
31341
32162
|
var ALLOWED_FORMATS4 = ["text", "json"];
|
|
31342
32163
|
async function planCommand(sql, options, command) {
|
|
32164
|
+
let config;
|
|
31343
32165
|
try {
|
|
31344
32166
|
if (!sql || sql.trim() === "") {
|
|
31345
32167
|
throw new Error("SQL statement required");
|
|
@@ -31347,7 +32169,7 @@ async function planCommand(sql, options, command) {
|
|
|
31347
32169
|
const format = options.format ?? "text";
|
|
31348
32170
|
validateFormat(format, ALLOWED_FORMATS4, "plan");
|
|
31349
32171
|
const configPath = resolveConfigPath(command, options);
|
|
31350
|
-
|
|
32172
|
+
config = await configModule.read(configPath);
|
|
31351
32173
|
if (!config.connection) {
|
|
31352
32174
|
throw new Error('Run "dbcli init" first');
|
|
31353
32175
|
}
|
|
@@ -31363,7 +32185,25 @@ async function planCommand(sql, options, command) {
|
|
|
31363
32185
|
}
|
|
31364
32186
|
});
|
|
31365
32187
|
console.log(formatPlanResult(result, format));
|
|
32188
|
+
if (config) {
|
|
32189
|
+
await writeAuditEntry(config, "plan", { ...options, plan: true }, {
|
|
32190
|
+
success: true,
|
|
32191
|
+
sql,
|
|
32192
|
+
target: result.targetTables[0] || "*",
|
|
32193
|
+
metadata: {
|
|
32194
|
+
decision: result.decision,
|
|
32195
|
+
risk_factors: result.riskFactors.map((f) => f.code)
|
|
32196
|
+
}
|
|
32197
|
+
});
|
|
32198
|
+
}
|
|
31366
32199
|
} catch (error) {
|
|
32200
|
+
if (config) {
|
|
32201
|
+
await writeAuditEntry(config, "plan", { ...options, plan: true }, {
|
|
32202
|
+
success: false,
|
|
32203
|
+
sql,
|
|
32204
|
+
error
|
|
32205
|
+
});
|
|
32206
|
+
}
|
|
31367
32207
|
console.error(error.message);
|
|
31368
32208
|
process.exit(1);
|
|
31369
32209
|
}
|
|
@@ -31400,12 +32240,15 @@ function formatPlanText(result) {
|
|
|
31400
32240
|
init_message_loader();
|
|
31401
32241
|
init_adapters();
|
|
31402
32242
|
init_config();
|
|
32243
|
+
import crypto3 from "crypto";
|
|
31403
32244
|
init_blacklist();
|
|
31404
32245
|
init_permission_guard();
|
|
32246
|
+
init_engine_hints();
|
|
32247
|
+
init_integration_helper();
|
|
31405
32248
|
init_saved_queries();
|
|
31406
32249
|
init_strategies();
|
|
31407
32250
|
import { tmpdir as tmpdir2 } from "os";
|
|
31408
|
-
import { join as
|
|
32251
|
+
import { join as join18 } from "path";
|
|
31409
32252
|
function formatDryRun(input) {
|
|
31410
32253
|
const lines = ["Dry-run preview (no execution):"];
|
|
31411
32254
|
if (input.family === "es") {
|
|
@@ -31430,12 +32273,14 @@ function formatDryRun(input) {
|
|
|
31430
32273
|
`);
|
|
31431
32274
|
}
|
|
31432
32275
|
async function qCommand(name, options, command) {
|
|
32276
|
+
let config;
|
|
32277
|
+
let targetNameForAudit = name;
|
|
31433
32278
|
try {
|
|
31434
32279
|
if (!name?.startsWith("@")) {
|
|
31435
32280
|
throw new Error(`Snippet name must start with '@' (got '${name}')`);
|
|
31436
32281
|
}
|
|
31437
32282
|
const configPath = resolveConfigPath(command, options);
|
|
31438
|
-
|
|
32283
|
+
config = await configModule.read(configPath);
|
|
31439
32284
|
if (!config.connection)
|
|
31440
32285
|
throw new Error('Run "dbcli init" first');
|
|
31441
32286
|
const engine = mapSystemToEngine(config.connection.system);
|
|
@@ -31459,12 +32304,19 @@ async function qCommand(name, options, command) {
|
|
|
31459
32304
|
values: prepared.driver.values,
|
|
31460
32305
|
execHints: prepared.execHints
|
|
31461
32306
|
}));
|
|
32307
|
+
await writeAuditEntry(config, "q", options, {
|
|
32308
|
+
success: true,
|
|
32309
|
+
target: name,
|
|
32310
|
+
sql: prepared.driver.sql,
|
|
32311
|
+
metadata: { dry_run: true }
|
|
32312
|
+
});
|
|
31462
32313
|
return;
|
|
31463
32314
|
}
|
|
31464
32315
|
const blacklistManager = new BlacklistManager(config);
|
|
31465
32316
|
const blacklistValidator = new BlacklistValidator(blacklistManager);
|
|
31466
32317
|
const family = engineFamily(engine);
|
|
31467
32318
|
const targetName = family === "sql" ? extractTableName(prepared.rewrittenSql) ?? "" : family === "es" ? prepared.execHints?.index ?? "" : "";
|
|
32319
|
+
targetNameForAudit = targetName || name;
|
|
31468
32320
|
if (family !== "redis" && targetName) {
|
|
31469
32321
|
blacklistValidator.checkTableBlacklist("SELECT", targetName);
|
|
31470
32322
|
}
|
|
@@ -31486,12 +32338,21 @@ async function qCommand(name, options, command) {
|
|
|
31486
32338
|
rows: filtered.filteredRows
|
|
31487
32339
|
});
|
|
31488
32340
|
if (options.ui) {
|
|
31489
|
-
const tempPath =
|
|
32341
|
+
const tempPath = join18(tmpdir2(), `dbcli-report-${Date.now()}.html`);
|
|
31490
32342
|
await Bun.write(tempPath, html);
|
|
31491
32343
|
await openInBrowser(tempPath);
|
|
31492
32344
|
} else {
|
|
31493
32345
|
console.log(html);
|
|
31494
32346
|
}
|
|
32347
|
+
await writeAuditEntry(config, "q", options, {
|
|
32348
|
+
success: true,
|
|
32349
|
+
target: targetName || name,
|
|
32350
|
+
sql: prepared.driver.sql,
|
|
32351
|
+
metadata: {
|
|
32352
|
+
rows_affected: filtered.filteredRows.length,
|
|
32353
|
+
execution_ms: executionTimeMs
|
|
32354
|
+
}
|
|
32355
|
+
});
|
|
31495
32356
|
return;
|
|
31496
32357
|
}
|
|
31497
32358
|
const formatter = new QueryResultFormatter;
|
|
@@ -31510,11 +32371,20 @@ async function qCommand(name, options, command) {
|
|
|
31510
32371
|
}
|
|
31511
32372
|
}, { format: options.format ?? "table" });
|
|
31512
32373
|
console.log(out);
|
|
32374
|
+
await writeAuditEntry(config, "q", options, {
|
|
32375
|
+
success: true,
|
|
32376
|
+
target: targetName || name,
|
|
32377
|
+
sql: prepared.driver.sql,
|
|
32378
|
+
metadata: {
|
|
32379
|
+
rows_affected: filtered.filteredRows.length,
|
|
32380
|
+
execution_ms: executionTimeMs
|
|
32381
|
+
}
|
|
32382
|
+
});
|
|
31513
32383
|
} finally {
|
|
31514
32384
|
await adapter.disconnect();
|
|
31515
32385
|
}
|
|
31516
32386
|
} catch (error) {
|
|
31517
|
-
await handleQError(error,
|
|
32387
|
+
await handleQError(error, targetNameForAudit, options, config);
|
|
31518
32388
|
}
|
|
31519
32389
|
}
|
|
31520
32390
|
function parseCliParams(list) {
|
|
@@ -31537,14 +32407,24 @@ async function readParamFile(path4) {
|
|
|
31537
32407
|
}
|
|
31538
32408
|
return parsed;
|
|
31539
32409
|
}
|
|
31540
|
-
async function handleQError(error, snippetName,
|
|
31541
|
-
|
|
31542
|
-
|
|
31543
|
-
|
|
31544
|
-
|
|
31545
|
-
|
|
32410
|
+
async function handleQError(error, snippetName, options, config) {
|
|
32411
|
+
let auditId = null;
|
|
32412
|
+
let envelopeId;
|
|
32413
|
+
if (options.recovery === true) {
|
|
32414
|
+
envelopeId = crypto3.randomUUID();
|
|
32415
|
+
}
|
|
32416
|
+
if (config) {
|
|
32417
|
+
auditId = await writeAuditEntry(config, "q", options, {
|
|
32418
|
+
success: false,
|
|
32419
|
+
target: snippetName,
|
|
32420
|
+
error,
|
|
32421
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
31546
32422
|
});
|
|
31547
32423
|
}
|
|
32424
|
+
if (envelopeId !== undefined) {
|
|
32425
|
+
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
32426
|
+
emitRecoveryEnvelope2(error, { operation: "q", snippet: snippetName }, { envelopeId, auditRef: auditId ?? undefined });
|
|
32427
|
+
}
|
|
31548
32428
|
if (error instanceof SavedQueryError) {
|
|
31549
32429
|
console.error(error.message);
|
|
31550
32430
|
process.exit(1);
|
|
@@ -31568,8 +32448,8 @@ async function handleQError(error, snippetName, useRecovery) {
|
|
|
31568
32448
|
// src/commands/queries.ts
|
|
31569
32449
|
init_message_loader();
|
|
31570
32450
|
init_config();
|
|
31571
|
-
import { mkdir as
|
|
31572
|
-
import { dirname as
|
|
32451
|
+
import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
|
|
32452
|
+
import { dirname as dirname8 } from "path";
|
|
31573
32453
|
import { spawn as spawn2 } from "child_process";
|
|
31574
32454
|
init_saved_queries();
|
|
31575
32455
|
init_fold();
|
|
@@ -31817,8 +32697,8 @@ async function queriesNew(name, options) {
|
|
|
31817
32697
|
process.exit(1);
|
|
31818
32698
|
return;
|
|
31819
32699
|
}
|
|
31820
|
-
await
|
|
31821
|
-
await
|
|
32700
|
+
await mkdir9(dirname8(file), { recursive: true });
|
|
32701
|
+
await writeFile4(file, scaffold(name), "utf8");
|
|
31822
32702
|
console.log(`Created ${file}`);
|
|
31823
32703
|
if (source === "shared")
|
|
31824
32704
|
console.log(t("queries.first_run_hint"));
|
|
@@ -32136,6 +33016,7 @@ queriesCommand.command("suggest [intent]").description(t("queries.suggest_descri
|
|
|
32136
33016
|
// src/commands/insert.ts
|
|
32137
33017
|
init_message_loader();
|
|
32138
33018
|
init_adapters();
|
|
33019
|
+
import crypto4 from "crypto";
|
|
32139
33020
|
|
|
32140
33021
|
// src/core/data-executor.ts
|
|
32141
33022
|
init_permission_guard();
|
|
@@ -32441,7 +33322,695 @@ function previewDelete(collection, filter) {
|
|
|
32441
33322
|
return `db.${collection}.deleteMany(${pretty(filter)})`;
|
|
32442
33323
|
}
|
|
32443
33324
|
|
|
33325
|
+
// src/commands/dml-plan.ts
|
|
33326
|
+
init_config();
|
|
33327
|
+
init_validation();
|
|
33328
|
+
|
|
33329
|
+
// src/core/dml-plan-sql.ts
|
|
33330
|
+
var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33331
|
+
function assertIdentifier(value, role) {
|
|
33332
|
+
if (!value || !IDENTIFIER_RE.test(value)) {
|
|
33333
|
+
throw new Error(`Invalid ${role} identifier: ${JSON.stringify(value)}. ` + `${role === "table" ? "Table" : "Column"} names must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
33334
|
+
}
|
|
33335
|
+
}
|
|
33336
|
+
function normalizeTable(table) {
|
|
33337
|
+
if (!table || table.trim() === "") {
|
|
33338
|
+
throw new Error("Table name required for plan SQL");
|
|
33339
|
+
}
|
|
33340
|
+
const trimmed = table.trim();
|
|
33341
|
+
assertIdentifier(trimmed, "table");
|
|
33342
|
+
return trimmed;
|
|
33343
|
+
}
|
|
33344
|
+
function normalizeColumns(data, role) {
|
|
33345
|
+
const keys = Object.keys(data);
|
|
33346
|
+
if (keys.length === 0) {
|
|
33347
|
+
throw new Error(role === "data" ? "INSERT plan requires at least one column in --data" : "UPDATE plan requires at least one column in --set");
|
|
33348
|
+
}
|
|
33349
|
+
for (const key of keys) {
|
|
33350
|
+
assertIdentifier(key, "column");
|
|
33351
|
+
}
|
|
33352
|
+
return keys;
|
|
33353
|
+
}
|
|
33354
|
+
function normalizeWhere(where) {
|
|
33355
|
+
const keys = Object.keys(where);
|
|
33356
|
+
if (keys.length === 0) {
|
|
33357
|
+
throw new Error("WHERE clause is required for plan SQL");
|
|
33358
|
+
}
|
|
33359
|
+
for (const key of keys) {
|
|
33360
|
+
assertIdentifier(key, "column");
|
|
33361
|
+
}
|
|
33362
|
+
return keys;
|
|
33363
|
+
}
|
|
33364
|
+
function buildInsertPlanSql(table, data) {
|
|
33365
|
+
const t2 = normalizeTable(table);
|
|
33366
|
+
const columns = normalizeColumns(data, "data");
|
|
33367
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
33368
|
+
return `INSERT INTO ${t2} (${columns.join(", ")}) VALUES (${placeholders})`;
|
|
33369
|
+
}
|
|
33370
|
+
function buildUpdatePlanSql(table, set, where) {
|
|
33371
|
+
const t2 = normalizeTable(table);
|
|
33372
|
+
const setCols = normalizeColumns(set, "set");
|
|
33373
|
+
const whereCols = normalizeWhere(where);
|
|
33374
|
+
const setClause = setCols.map((col) => `${col} = ?`).join(", ");
|
|
33375
|
+
const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
|
|
33376
|
+
return `UPDATE ${t2} SET ${setClause} WHERE ${whereClause}`;
|
|
33377
|
+
}
|
|
33378
|
+
function buildDeletePlanSql(table, where) {
|
|
33379
|
+
const t2 = normalizeTable(table);
|
|
33380
|
+
const whereCols = normalizeWhere(where);
|
|
33381
|
+
const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
|
|
33382
|
+
return `DELETE FROM ${t2} WHERE ${whereClause}`;
|
|
33383
|
+
}
|
|
33384
|
+
|
|
33385
|
+
// src/utils/where-parser.ts
|
|
33386
|
+
function parseWhereClause(whereClause) {
|
|
33387
|
+
if (!whereClause || whereClause.trim() === "") {
|
|
33388
|
+
throw new Error("WHERE clause cannot be empty");
|
|
33389
|
+
}
|
|
33390
|
+
const conditions = {};
|
|
33391
|
+
const andParts = whereClause.split(/\s+AND\s+/i);
|
|
33392
|
+
for (const part of andParts) {
|
|
33393
|
+
const match = part.match(/^(\w+)\s*=\s*(.+)$/);
|
|
33394
|
+
if (!match) {
|
|
33395
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33396
|
+
}
|
|
33397
|
+
const column = match[1];
|
|
33398
|
+
const valueStr = match[2];
|
|
33399
|
+
if (valueStr === undefined || column === undefined) {
|
|
33400
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33401
|
+
}
|
|
33402
|
+
const trimmed = valueStr.trim();
|
|
33403
|
+
const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
|
|
33404
|
+
if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
|
|
33405
|
+
throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
|
|
33406
|
+
}
|
|
33407
|
+
const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
|
|
33408
|
+
let value = stripped;
|
|
33409
|
+
if (stripped !== "" && !isNaN(Number(stripped))) {
|
|
33410
|
+
value = Number(stripped);
|
|
33411
|
+
}
|
|
33412
|
+
if (stripped === "true")
|
|
33413
|
+
value = true;
|
|
33414
|
+
else if (stripped === "false")
|
|
33415
|
+
value = false;
|
|
33416
|
+
else if (stripped === "null")
|
|
33417
|
+
value = null;
|
|
33418
|
+
conditions[column] = value;
|
|
33419
|
+
}
|
|
33420
|
+
return conditions;
|
|
33421
|
+
}
|
|
33422
|
+
|
|
33423
|
+
// src/core/mongo/dml-plan.ts
|
|
33424
|
+
init_permission_guard();
|
|
33425
|
+
var MONGO_UPDATE_OPERATOR_ALLOWLIST = new Set(["$set", "$unset"]);
|
|
33426
|
+
var MONGO_BROAD_FILTER_OPERATORS = new Set([
|
|
33427
|
+
"$regex",
|
|
33428
|
+
"$in",
|
|
33429
|
+
"$nin",
|
|
33430
|
+
"$gt",
|
|
33431
|
+
"$gte",
|
|
33432
|
+
"$lt",
|
|
33433
|
+
"$lte",
|
|
33434
|
+
"$ne",
|
|
33435
|
+
"$exists",
|
|
33436
|
+
"$or",
|
|
33437
|
+
"$and",
|
|
33438
|
+
"$nor",
|
|
33439
|
+
"$not",
|
|
33440
|
+
"$expr",
|
|
33441
|
+
"$text"
|
|
33442
|
+
]);
|
|
33443
|
+
function permissionSql(operation) {
|
|
33444
|
+
if (operation === "insert")
|
|
33445
|
+
return "INSERT INTO dummy";
|
|
33446
|
+
if (operation === "update")
|
|
33447
|
+
return "UPDATE dummy";
|
|
33448
|
+
return "DELETE FROM dummy";
|
|
33449
|
+
}
|
|
33450
|
+
function broadOperation(operation) {
|
|
33451
|
+
if (operation === "insert")
|
|
33452
|
+
return "INSERT";
|
|
33453
|
+
if (operation === "update")
|
|
33454
|
+
return "UPDATE";
|
|
33455
|
+
return "DELETE";
|
|
33456
|
+
}
|
|
33457
|
+
function pushFactor2(factors, code, severity, message) {
|
|
33458
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33459
|
+
return;
|
|
33460
|
+
factors.push({ code, severity, message });
|
|
33461
|
+
}
|
|
33462
|
+
function decide2(factors) {
|
|
33463
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33464
|
+
return "BLOCK";
|
|
33465
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33466
|
+
return "WARN";
|
|
33467
|
+
return "ALLOW";
|
|
33468
|
+
}
|
|
33469
|
+
function extractMongoWrittenFields(setDoc) {
|
|
33470
|
+
const fields = new Set;
|
|
33471
|
+
const operators = [];
|
|
33472
|
+
let hasUnsupportedOperator = false;
|
|
33473
|
+
const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
|
|
33474
|
+
if (!hasAnyOperator) {
|
|
33475
|
+
for (const k of Object.keys(setDoc))
|
|
33476
|
+
fields.add(k);
|
|
33477
|
+
return { fields: Array.from(fields), operators: [], hasUnsupportedOperator: false };
|
|
33478
|
+
}
|
|
33479
|
+
for (const [op, payload] of Object.entries(setDoc)) {
|
|
33480
|
+
operators.push(op);
|
|
33481
|
+
if (!MONGO_UPDATE_OPERATOR_ALLOWLIST.has(op)) {
|
|
33482
|
+
hasUnsupportedOperator = true;
|
|
33483
|
+
continue;
|
|
33484
|
+
}
|
|
33485
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
33486
|
+
for (const k of Object.keys(payload))
|
|
33487
|
+
fields.add(k);
|
|
33488
|
+
}
|
|
33489
|
+
}
|
|
33490
|
+
return { fields: Array.from(fields), operators, hasUnsupportedOperator };
|
|
33491
|
+
}
|
|
33492
|
+
function hasIdEquality(where) {
|
|
33493
|
+
if (!("_id" in where))
|
|
33494
|
+
return false;
|
|
33495
|
+
const value = where._id;
|
|
33496
|
+
if (value === null || value === undefined)
|
|
33497
|
+
return false;
|
|
33498
|
+
if (typeof value === "object")
|
|
33499
|
+
return false;
|
|
33500
|
+
return true;
|
|
33501
|
+
}
|
|
33502
|
+
function hasNonIdEquality(where) {
|
|
33503
|
+
for (const [key, value] of Object.entries(where)) {
|
|
33504
|
+
if (key === "_id")
|
|
33505
|
+
continue;
|
|
33506
|
+
if (key.startsWith("$"))
|
|
33507
|
+
continue;
|
|
33508
|
+
if (value === null)
|
|
33509
|
+
continue;
|
|
33510
|
+
if (typeof value === "object")
|
|
33511
|
+
continue;
|
|
33512
|
+
return true;
|
|
33513
|
+
}
|
|
33514
|
+
return false;
|
|
33515
|
+
}
|
|
33516
|
+
function hasBroadFilter(where) {
|
|
33517
|
+
for (const [key, value] of Object.entries(where)) {
|
|
33518
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(key))
|
|
33519
|
+
return true;
|
|
33520
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
33521
|
+
for (const inner of Object.keys(value)) {
|
|
33522
|
+
if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
|
|
33523
|
+
return true;
|
|
33524
|
+
}
|
|
33525
|
+
}
|
|
33526
|
+
}
|
|
33527
|
+
return false;
|
|
33528
|
+
}
|
|
33529
|
+
function applyPermission(operation, context, factors) {
|
|
33530
|
+
const permResult = checkPermission(permissionSql(operation), context.permission);
|
|
33531
|
+
if (!permResult.allowed) {
|
|
33532
|
+
pushFactor2(factors, "permission_denied", "block", permResult.reason);
|
|
33533
|
+
}
|
|
33534
|
+
}
|
|
33535
|
+
function applyTableBlacklist(target, context, factors) {
|
|
33536
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33537
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33538
|
+
pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
|
|
33539
|
+
}
|
|
33540
|
+
}
|
|
33541
|
+
function applyColumnBlacklist(target, fields, context, factors) {
|
|
33542
|
+
const columns = context.blacklist.columns ?? {};
|
|
33543
|
+
const lower = target.toLowerCase();
|
|
33544
|
+
let blacklistedFields = [];
|
|
33545
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33546
|
+
if (t2.toLowerCase() === lower) {
|
|
33547
|
+
blacklistedFields = cols;
|
|
33548
|
+
break;
|
|
33549
|
+
}
|
|
33550
|
+
}
|
|
33551
|
+
if (blacklistedFields.length === 0)
|
|
33552
|
+
return;
|
|
33553
|
+
for (const field of fields) {
|
|
33554
|
+
if (blacklistedFields.includes(field)) {
|
|
33555
|
+
pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted field ${target}.${field}.`);
|
|
33556
|
+
}
|
|
33557
|
+
}
|
|
33558
|
+
}
|
|
33559
|
+
function applySchemaCoverage(target, context, factors) {
|
|
33560
|
+
const schemaTables = Object.keys(context.schema);
|
|
33561
|
+
if (schemaTables.length === 0) {
|
|
33562
|
+
pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
|
|
33563
|
+
return;
|
|
33564
|
+
}
|
|
33565
|
+
const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
33566
|
+
if (!known) {
|
|
33567
|
+
pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
|
|
33568
|
+
}
|
|
33569
|
+
}
|
|
33570
|
+
function buildRecommendations2(factors) {
|
|
33571
|
+
const out = new Set;
|
|
33572
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33573
|
+
if (codes.has("nonsql_filter_empty")) {
|
|
33574
|
+
out.add("Add an _id equality filter before executing this MongoDB write.");
|
|
33575
|
+
}
|
|
33576
|
+
if (codes.has("nonsql_missing_id")) {
|
|
33577
|
+
out.add("Prefer an _id equality filter before executing this MongoDB write.");
|
|
33578
|
+
}
|
|
33579
|
+
if (codes.has("nonsql_filter_broad")) {
|
|
33580
|
+
out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
|
|
33581
|
+
}
|
|
33582
|
+
if (codes.has("nonsql_unsupported_operator")) {
|
|
33583
|
+
out.add("Restrict the update document to $set / $unset for planner-safe writes.");
|
|
33584
|
+
}
|
|
33585
|
+
if (codes.has("permission_denied")) {
|
|
33586
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33587
|
+
}
|
|
33588
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33589
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33590
|
+
}
|
|
33591
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33592
|
+
out.add("Refresh schema cache for the target collection before executing.");
|
|
33593
|
+
}
|
|
33594
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33595
|
+
return Array.from(out);
|
|
33596
|
+
}
|
|
33597
|
+
function buildSuggestedCommands2(target, factors) {
|
|
33598
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33599
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33600
|
+
return [`dbcli schema ${target} --format json`];
|
|
33601
|
+
}
|
|
33602
|
+
return [];
|
|
33603
|
+
}
|
|
33604
|
+
var analyzeMongoDmlRisk = (intent, context) => {
|
|
33605
|
+
const factors = [];
|
|
33606
|
+
applyPermission(intent.operation, context, factors);
|
|
33607
|
+
applyTableBlacklist(intent.target, context, factors);
|
|
33608
|
+
if (intent.operation === "insert") {
|
|
33609
|
+
applyColumnBlacklist(intent.target, Object.keys(intent.data), context, factors);
|
|
33610
|
+
} else if (intent.operation === "update") {
|
|
33611
|
+
const { fields, hasUnsupportedOperator } = extractMongoWrittenFields(intent.set);
|
|
33612
|
+
if (hasUnsupportedOperator) {
|
|
33613
|
+
pushFactor2(factors, "nonsql_unsupported_operator", "block", "MongoDB update uses an operator outside the MVP allowlist ($set, $unset).");
|
|
33614
|
+
}
|
|
33615
|
+
applyColumnBlacklist(intent.target, fields, context, factors);
|
|
33616
|
+
const where = intent.where ?? {};
|
|
33617
|
+
if (Object.keys(where).length === 0) {
|
|
33618
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
|
|
33619
|
+
} else if (!hasIdEquality(where)) {
|
|
33620
|
+
if (hasBroadFilter(where)) {
|
|
33621
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
|
|
33622
|
+
} else if (hasNonIdEquality(where)) {
|
|
33623
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
|
|
33624
|
+
} else {
|
|
33625
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
|
|
33626
|
+
}
|
|
33627
|
+
}
|
|
33628
|
+
} else {
|
|
33629
|
+
const where = intent.where ?? {};
|
|
33630
|
+
if (Object.keys(where).length === 0) {
|
|
33631
|
+
pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
|
|
33632
|
+
} else if (!hasIdEquality(where)) {
|
|
33633
|
+
if (hasBroadFilter(where)) {
|
|
33634
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
|
|
33635
|
+
} else if (hasNonIdEquality(where)) {
|
|
33636
|
+
pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
|
|
33637
|
+
} else {
|
|
33638
|
+
pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
|
|
33639
|
+
}
|
|
33640
|
+
}
|
|
33641
|
+
}
|
|
33642
|
+
applySchemaCoverage(intent.target, context, factors);
|
|
33643
|
+
return {
|
|
33644
|
+
decision: decide2(factors),
|
|
33645
|
+
operation: broadOperation(intent.operation),
|
|
33646
|
+
targetTables: [intent.target],
|
|
33647
|
+
riskFactors: factors,
|
|
33648
|
+
recommendations: buildRecommendations2(factors),
|
|
33649
|
+
suggestedCommands: buildSuggestedCommands2(intent.target, factors)
|
|
33650
|
+
};
|
|
33651
|
+
};
|
|
33652
|
+
|
|
33653
|
+
// src/core/redis/dml-plan.ts
|
|
33654
|
+
init_permission_guard();
|
|
33655
|
+
function permissionSql2(operation) {
|
|
33656
|
+
if (operation === "insert")
|
|
33657
|
+
return "INSERT INTO dummy";
|
|
33658
|
+
if (operation === "update")
|
|
33659
|
+
return "UPDATE dummy";
|
|
33660
|
+
return "DELETE FROM dummy";
|
|
33661
|
+
}
|
|
33662
|
+
function broadOperation2(operation) {
|
|
33663
|
+
if (operation === "insert")
|
|
33664
|
+
return "INSERT";
|
|
33665
|
+
if (operation === "update")
|
|
33666
|
+
return "UPDATE";
|
|
33667
|
+
return "DELETE";
|
|
33668
|
+
}
|
|
33669
|
+
function pushFactor3(factors, code, severity, message) {
|
|
33670
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33671
|
+
return;
|
|
33672
|
+
factors.push({ code, severity, message });
|
|
33673
|
+
}
|
|
33674
|
+
function decide3(factors) {
|
|
33675
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33676
|
+
return "BLOCK";
|
|
33677
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33678
|
+
return "WARN";
|
|
33679
|
+
return "ALLOW";
|
|
33680
|
+
}
|
|
33681
|
+
function classifyKey(target) {
|
|
33682
|
+
if (target === "*")
|
|
33683
|
+
return "all";
|
|
33684
|
+
if (target.includes("*") || target.includes("?") || /\[.+\]/.test(target))
|
|
33685
|
+
return "pattern";
|
|
33686
|
+
return "single";
|
|
33687
|
+
}
|
|
33688
|
+
function applyPermission2(operation, context, factors) {
|
|
33689
|
+
const result = checkPermission(permissionSql2(operation), context.permission);
|
|
33690
|
+
if (!result.allowed) {
|
|
33691
|
+
pushFactor3(factors, "permission_denied", "block", result.reason);
|
|
33692
|
+
}
|
|
33693
|
+
}
|
|
33694
|
+
function applyTableBlacklist2(target, context, factors) {
|
|
33695
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33696
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33697
|
+
pushFactor3(factors, "table_blacklisted", "block", `Redis key ${target} is blacklisted.`);
|
|
33698
|
+
}
|
|
33699
|
+
}
|
|
33700
|
+
function applyColumnBlacklist2(target, fields, context, factors) {
|
|
33701
|
+
const columns = context.blacklist.columns ?? {};
|
|
33702
|
+
const lower = target.toLowerCase();
|
|
33703
|
+
let blacklistedFields = [];
|
|
33704
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33705
|
+
if (t2.toLowerCase() === lower) {
|
|
33706
|
+
blacklistedFields = cols;
|
|
33707
|
+
break;
|
|
33708
|
+
}
|
|
33709
|
+
}
|
|
33710
|
+
if (blacklistedFields.length === 0)
|
|
33711
|
+
return;
|
|
33712
|
+
for (const field of fields) {
|
|
33713
|
+
if (blacklistedFields.includes(field)) {
|
|
33714
|
+
pushFactor3(factors, "blacklisted_column", "block", `Redis write would touch blacklisted field ${target}.${field}.`);
|
|
33715
|
+
}
|
|
33716
|
+
}
|
|
33717
|
+
}
|
|
33718
|
+
function redisSchemaKeyMatches(schemaKey, target) {
|
|
33719
|
+
const lowerKey = schemaKey.toLowerCase();
|
|
33720
|
+
const lowerTarget = target.toLowerCase();
|
|
33721
|
+
if (lowerKey === lowerTarget)
|
|
33722
|
+
return true;
|
|
33723
|
+
if (!lowerKey.includes("*") && !lowerKey.includes("?"))
|
|
33724
|
+
return false;
|
|
33725
|
+
const pattern = lowerKey.split("").map((ch) => {
|
|
33726
|
+
if (ch === "*")
|
|
33727
|
+
return ".*";
|
|
33728
|
+
if (ch === "?")
|
|
33729
|
+
return ".";
|
|
33730
|
+
return ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
33731
|
+
}).join("");
|
|
33732
|
+
return new RegExp(`^${pattern}$`).test(lowerTarget);
|
|
33733
|
+
}
|
|
33734
|
+
function applySchemaCoverage2(target, context, factors) {
|
|
33735
|
+
const tables = Object.keys(context.schema);
|
|
33736
|
+
if (tables.length === 0) {
|
|
33737
|
+
pushFactor3(factors, "schema_cache_missing", "warn", "Schema/key metadata is missing for the selected Redis connection.");
|
|
33738
|
+
return;
|
|
33739
|
+
}
|
|
33740
|
+
const known = tables.some((t2) => redisSchemaKeyMatches(t2, target));
|
|
33741
|
+
if (!known) {
|
|
33742
|
+
pushFactor3(factors, "schema_table_unknown", "warn", `Redis key namespace ${target} is missing from cached metadata.`);
|
|
33743
|
+
}
|
|
33744
|
+
}
|
|
33745
|
+
function buildRecommendations3(factors) {
|
|
33746
|
+
const out = new Set;
|
|
33747
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33748
|
+
if (codes.has("nonsql_key_pattern_broad")) {
|
|
33749
|
+
out.add("Specify a concrete single key instead of a wildcard pattern.");
|
|
33750
|
+
}
|
|
33751
|
+
if (codes.has("nonsql_overwrite_unknown")) {
|
|
33752
|
+
out.add("Provide --set field information so the planner can verify the write target.");
|
|
33753
|
+
}
|
|
33754
|
+
if (codes.has("permission_denied")) {
|
|
33755
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33756
|
+
}
|
|
33757
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33758
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33759
|
+
}
|
|
33760
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33761
|
+
out.add("Refresh schema cache for the target Redis namespace before executing.");
|
|
33762
|
+
}
|
|
33763
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33764
|
+
return Array.from(out);
|
|
33765
|
+
}
|
|
33766
|
+
function buildSuggestedCommands3(target, factors) {
|
|
33767
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33768
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33769
|
+
return [`dbcli schema ${target} --format json`];
|
|
33770
|
+
}
|
|
33771
|
+
return [];
|
|
33772
|
+
}
|
|
33773
|
+
var analyzeRedisDmlRisk = (intent, context) => {
|
|
33774
|
+
const factors = [];
|
|
33775
|
+
applyPermission2(intent.operation, context, factors);
|
|
33776
|
+
applyTableBlacklist2(intent.target, context, factors);
|
|
33777
|
+
const keyClass = classifyKey(intent.target);
|
|
33778
|
+
if (keyClass === "all") {
|
|
33779
|
+
pushFactor3(factors, "nonsql_key_pattern_broad", "block", 'Redis key "*" matches every key and is unsafe for planner-level writes.');
|
|
33780
|
+
} else if (keyClass === "pattern") {
|
|
33781
|
+
pushFactor3(factors, "nonsql_key_pattern_broad", "warn", `Redis target ${intent.target} contains wildcard characters and may match multiple keys.`);
|
|
33782
|
+
}
|
|
33783
|
+
if (intent.operation === "insert") {
|
|
33784
|
+
applyColumnBlacklist2(intent.target, Object.keys(intent.data), context, factors);
|
|
33785
|
+
} else if (intent.operation === "update") {
|
|
33786
|
+
const fields = Object.keys(intent.set);
|
|
33787
|
+
if (fields.length === 0) {
|
|
33788
|
+
pushFactor3(factors, "nonsql_overwrite_unknown", "warn", "Redis update has no field information and overwrite impact cannot be verified.");
|
|
33789
|
+
}
|
|
33790
|
+
applyColumnBlacklist2(intent.target, fields, context, factors);
|
|
33791
|
+
}
|
|
33792
|
+
applySchemaCoverage2(intent.target, context, factors);
|
|
33793
|
+
return {
|
|
33794
|
+
decision: decide3(factors),
|
|
33795
|
+
operation: broadOperation2(intent.operation),
|
|
33796
|
+
targetTables: [intent.target],
|
|
33797
|
+
riskFactors: factors,
|
|
33798
|
+
recommendations: buildRecommendations3(factors),
|
|
33799
|
+
suggestedCommands: buildSuggestedCommands3(intent.target, factors)
|
|
33800
|
+
};
|
|
33801
|
+
};
|
|
33802
|
+
|
|
33803
|
+
// src/core/elasticsearch/dml-plan.ts
|
|
33804
|
+
init_permission_guard();
|
|
33805
|
+
function permissionSql3(operation) {
|
|
33806
|
+
if (operation === "insert")
|
|
33807
|
+
return "INSERT INTO dummy";
|
|
33808
|
+
if (operation === "update")
|
|
33809
|
+
return "UPDATE dummy";
|
|
33810
|
+
return "DELETE FROM dummy";
|
|
33811
|
+
}
|
|
33812
|
+
function broadOperation3(operation) {
|
|
33813
|
+
if (operation === "insert")
|
|
33814
|
+
return "INSERT";
|
|
33815
|
+
if (operation === "update")
|
|
33816
|
+
return "UPDATE";
|
|
33817
|
+
return "DELETE";
|
|
33818
|
+
}
|
|
33819
|
+
function pushFactor4(factors, code, severity, message) {
|
|
33820
|
+
if (factors.some((f) => f.code === code && f.message === message))
|
|
33821
|
+
return;
|
|
33822
|
+
factors.push({ code, severity, message });
|
|
33823
|
+
}
|
|
33824
|
+
function decide4(factors) {
|
|
33825
|
+
if (factors.some((f) => f.severity === "block"))
|
|
33826
|
+
return "BLOCK";
|
|
33827
|
+
if (factors.some((f) => f.severity === "warn"))
|
|
33828
|
+
return "WARN";
|
|
33829
|
+
return "ALLOW";
|
|
33830
|
+
}
|
|
33831
|
+
function hasIdEquality2(where) {
|
|
33832
|
+
if (!where)
|
|
33833
|
+
return false;
|
|
33834
|
+
if (!("_id" in where))
|
|
33835
|
+
return false;
|
|
33836
|
+
const value = where._id;
|
|
33837
|
+
if (value === null || value === undefined)
|
|
33838
|
+
return false;
|
|
33839
|
+
if (typeof value === "object")
|
|
33840
|
+
return false;
|
|
33841
|
+
return true;
|
|
33842
|
+
}
|
|
33843
|
+
function applyPermission3(operation, context, factors) {
|
|
33844
|
+
const result = checkPermission(permissionSql3(operation), context.permission);
|
|
33845
|
+
if (!result.allowed) {
|
|
33846
|
+
pushFactor4(factors, "permission_denied", "block", result.reason);
|
|
33847
|
+
}
|
|
33848
|
+
}
|
|
33849
|
+
function applyTableBlacklist3(target, context, factors) {
|
|
33850
|
+
const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
|
|
33851
|
+
if (blacklisted.includes(target.toLowerCase())) {
|
|
33852
|
+
pushFactor4(factors, "table_blacklisted", "block", `Elasticsearch index ${target} is blacklisted.`);
|
|
33853
|
+
}
|
|
33854
|
+
}
|
|
33855
|
+
function applyColumnBlacklist3(target, fields, context, factors) {
|
|
33856
|
+
const columns = context.blacklist.columns ?? {};
|
|
33857
|
+
const lower = target.toLowerCase();
|
|
33858
|
+
let blacklistedFields = [];
|
|
33859
|
+
for (const [t2, cols] of Object.entries(columns)) {
|
|
33860
|
+
if (t2.toLowerCase() === lower) {
|
|
33861
|
+
blacklistedFields = cols;
|
|
33862
|
+
break;
|
|
33863
|
+
}
|
|
33864
|
+
}
|
|
33865
|
+
if (blacklistedFields.length === 0)
|
|
33866
|
+
return;
|
|
33867
|
+
for (const field of fields) {
|
|
33868
|
+
if (blacklistedFields.includes(field)) {
|
|
33869
|
+
pushFactor4(factors, "blacklisted_column", "block", `Elasticsearch write would touch blacklisted field ${target}.${field}.`);
|
|
33870
|
+
}
|
|
33871
|
+
}
|
|
33872
|
+
}
|
|
33873
|
+
function applySchemaCoverage3(target, context, factors) {
|
|
33874
|
+
const tables = Object.keys(context.schema);
|
|
33875
|
+
if (tables.length === 0) {
|
|
33876
|
+
pushFactor4(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected Elasticsearch connection.");
|
|
33877
|
+
return;
|
|
33878
|
+
}
|
|
33879
|
+
const known = tables.some((t2) => t2.toLowerCase() === target.toLowerCase());
|
|
33880
|
+
if (!known) {
|
|
33881
|
+
pushFactor4(factors, "schema_table_unknown", "warn", `Target index ${target} is missing from schema cache.`);
|
|
33882
|
+
}
|
|
33883
|
+
}
|
|
33884
|
+
function buildRecommendations4(factors) {
|
|
33885
|
+
const out = new Set;
|
|
33886
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33887
|
+
if (codes.has("nonsql_missing_id")) {
|
|
33888
|
+
out.add("Add an _id equality filter (or include _id in the document) before executing.");
|
|
33889
|
+
}
|
|
33890
|
+
if (codes.has("nonsql_unsupported_bulk")) {
|
|
33891
|
+
out.add("Use a document-level write instead of a bulk or by-query operation.");
|
|
33892
|
+
}
|
|
33893
|
+
if (codes.has("permission_denied")) {
|
|
33894
|
+
out.add("Switch to a connection with sufficient permission only if the operation is intended.");
|
|
33895
|
+
}
|
|
33896
|
+
if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
|
|
33897
|
+
out.add("Review blacklist rules before accessing sensitive data.");
|
|
33898
|
+
}
|
|
33899
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33900
|
+
out.add("Refresh schema cache for the target index before executing.");
|
|
33901
|
+
}
|
|
33902
|
+
out.add("Use --dry-run on the actual write command.");
|
|
33903
|
+
return Array.from(out);
|
|
33904
|
+
}
|
|
33905
|
+
function buildSuggestedCommands4(target, factors) {
|
|
33906
|
+
const codes = new Set(factors.map((f) => f.code));
|
|
33907
|
+
if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
|
|
33908
|
+
return [`dbcli schema ${target} --format json`];
|
|
33909
|
+
}
|
|
33910
|
+
return [];
|
|
33911
|
+
}
|
|
33912
|
+
var analyzeElasticsearchDmlRisk = (intent, context) => {
|
|
33913
|
+
const factors = [];
|
|
33914
|
+
applyPermission3(intent.operation, context, factors);
|
|
33915
|
+
applyTableBlacklist3(intent.target, context, factors);
|
|
33916
|
+
if (intent.operation === "insert") {
|
|
33917
|
+
const fields = Object.keys(intent.data);
|
|
33918
|
+
applyColumnBlacklist3(intent.target, fields, context, factors);
|
|
33919
|
+
if (intent.data._id === undefined || intent.data._id === null || intent.data._id === "") {
|
|
33920
|
+
pushFactor4(factors, "nonsql_missing_id", "warn", "Elasticsearch insert has no _id; auto-id generation will make later correlation harder.");
|
|
33921
|
+
}
|
|
33922
|
+
} else if (intent.operation === "update") {
|
|
33923
|
+
applyColumnBlacklist3(intent.target, Object.keys(intent.set), context, factors);
|
|
33924
|
+
if (!hasIdEquality2(intent.where)) {
|
|
33925
|
+
pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch update requires _id in --where for planner-safe document-level update.");
|
|
33926
|
+
}
|
|
33927
|
+
} else {
|
|
33928
|
+
if (!hasIdEquality2(intent.where)) {
|
|
33929
|
+
pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch delete requires _id in --where for planner-safe document-level deletion.");
|
|
33930
|
+
}
|
|
33931
|
+
}
|
|
33932
|
+
applySchemaCoverage3(intent.target, context, factors);
|
|
33933
|
+
return {
|
|
33934
|
+
decision: decide4(factors),
|
|
33935
|
+
operation: broadOperation3(intent.operation),
|
|
33936
|
+
targetTables: [intent.target],
|
|
33937
|
+
riskFactors: factors,
|
|
33938
|
+
recommendations: buildRecommendations4(factors),
|
|
33939
|
+
suggestedCommands: buildSuggestedCommands4(intent.target, factors)
|
|
33940
|
+
};
|
|
33941
|
+
};
|
|
33942
|
+
|
|
33943
|
+
// src/commands/dml-plan.ts
|
|
33944
|
+
var ALLOWED_FORMATS5 = ["text", "json"];
|
|
33945
|
+
var SQL_SYSTEMS = new Set(["postgresql", "mysql", "mariadb"]);
|
|
33946
|
+
function buildPlanSqlForIntent(intent) {
|
|
33947
|
+
if (intent.operation === "insert") {
|
|
33948
|
+
return buildInsertPlanSql(intent.target, intent.data);
|
|
33949
|
+
}
|
|
33950
|
+
if (intent.operation === "update") {
|
|
33951
|
+
if (!intent.where || Object.keys(intent.where).length === 0) {
|
|
33952
|
+
throw new Error("WHERE clause is required for SQL update plan");
|
|
33953
|
+
}
|
|
33954
|
+
return buildUpdatePlanSql(intent.target, intent.set, intent.where);
|
|
33955
|
+
}
|
|
33956
|
+
if (!intent.where || Object.keys(intent.where).length === 0) {
|
|
33957
|
+
throw new Error("WHERE clause is required for SQL delete plan");
|
|
33958
|
+
}
|
|
33959
|
+
return buildDeletePlanSql(intent.target, intent.where);
|
|
33960
|
+
}
|
|
33961
|
+
async function runDmlPlanAnalysis(intent, options, command) {
|
|
33962
|
+
try {
|
|
33963
|
+
const format = options.format ?? "text";
|
|
33964
|
+
validateFormat(format, ALLOWED_FORMATS5, "plan");
|
|
33965
|
+
const configPath = resolveConfigPath(command, options);
|
|
33966
|
+
const config = await configModule.read(configPath);
|
|
33967
|
+
if (!config.connection) {
|
|
33968
|
+
throw new Error('Run "dbcli init" to configure database connection');
|
|
33969
|
+
}
|
|
33970
|
+
const system = config.connection.system;
|
|
33971
|
+
const schema = config.schema ?? {};
|
|
33972
|
+
const blacklist = config.blacklist ?? { tables: [], columns: {} };
|
|
33973
|
+
if (SQL_SYSTEMS.has(system)) {
|
|
33974
|
+
const planSql = buildPlanSqlForIntent(intent);
|
|
33975
|
+
const schemaTableCount = Object.keys(schema).length;
|
|
33976
|
+
const result = analyzeQueryRisk({
|
|
33977
|
+
sql: planSql,
|
|
33978
|
+
permission: config.permission,
|
|
33979
|
+
blacklist,
|
|
33980
|
+
schemaLookup: {
|
|
33981
|
+
tables: schema,
|
|
33982
|
+
cacheAvailable: schemaTableCount > 0
|
|
33983
|
+
}
|
|
33984
|
+
});
|
|
33985
|
+
console.log(formatPlanResult(result, format));
|
|
33986
|
+
return;
|
|
33987
|
+
}
|
|
33988
|
+
const context = {
|
|
33989
|
+
permission: config.permission,
|
|
33990
|
+
blacklist,
|
|
33991
|
+
schema
|
|
33992
|
+
};
|
|
33993
|
+
if (system === "mongodb") {
|
|
33994
|
+
console.log(formatPlanResult(analyzeMongoDmlRisk(intent, context), format));
|
|
33995
|
+
return;
|
|
33996
|
+
}
|
|
33997
|
+
if (system === "redis") {
|
|
33998
|
+
console.log(formatPlanResult(analyzeRedisDmlRisk(intent, context), format));
|
|
33999
|
+
return;
|
|
34000
|
+
}
|
|
34001
|
+
if (system === "elasticsearch") {
|
|
34002
|
+
console.log(formatPlanResult(analyzeElasticsearchDmlRisk(intent, context), format));
|
|
34003
|
+
return;
|
|
34004
|
+
}
|
|
34005
|
+
throw new Error(`--plan does not support connection system "${system}" yet`);
|
|
34006
|
+
} catch (error) {
|
|
34007
|
+
console.error(error.message);
|
|
34008
|
+
process.exit(1);
|
|
34009
|
+
}
|
|
34010
|
+
}
|
|
34011
|
+
|
|
32444
34012
|
// src/commands/insert.ts
|
|
34013
|
+
init_integration_helper();
|
|
32445
34014
|
function requireSqlConnection3(connection) {
|
|
32446
34015
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
32447
34016
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
@@ -32467,6 +34036,7 @@ function isStdinAvailable() {
|
|
|
32467
34036
|
}
|
|
32468
34037
|
}
|
|
32469
34038
|
async function insertCommand(table, options, command) {
|
|
34039
|
+
let config;
|
|
32470
34040
|
try {
|
|
32471
34041
|
if (!table || table.trim() === "") {
|
|
32472
34042
|
throw new Error("Table name required");
|
|
@@ -32493,10 +34063,24 @@ async function insertCommand(table, options, command) {
|
|
|
32493
34063
|
throw new Error('JSON must be an object (e.g. {"name":"Alice","email":"a@b.com"})');
|
|
32494
34064
|
}
|
|
32495
34065
|
const configPath = resolveConfigPath(command, options);
|
|
32496
|
-
|
|
34066
|
+
config = await configModule.read(configPath);
|
|
32497
34067
|
if (!config.connection) {
|
|
32498
34068
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
32499
34069
|
}
|
|
34070
|
+
if (options.plan) {
|
|
34071
|
+
if (options.dryRun) {
|
|
34072
|
+
console.error("--plan cannot be used with --dry-run");
|
|
34073
|
+
process.exit(1);
|
|
34074
|
+
return;
|
|
34075
|
+
}
|
|
34076
|
+
await runDmlPlanAnalysis({ operation: "insert", target: table, data }, { format: options.format, config: options.config }, command);
|
|
34077
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34078
|
+
success: true,
|
|
34079
|
+
target: table,
|
|
34080
|
+
metadata: { plan: true }
|
|
34081
|
+
});
|
|
34082
|
+
return;
|
|
34083
|
+
}
|
|
32500
34084
|
if (config.connection.system === "redis") {
|
|
32501
34085
|
enforcePermission("INSERT INTO dummy", config.permission);
|
|
32502
34086
|
const blacklistManager = new BlacklistManager(config);
|
|
@@ -32512,6 +34096,11 @@ async function insertCommand(table, options, command) {
|
|
|
32512
34096
|
timestamp: new Date().toISOString()
|
|
32513
34097
|
};
|
|
32514
34098
|
console.log(JSON.stringify(output, null, 2));
|
|
34099
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34100
|
+
success: true,
|
|
34101
|
+
target: table,
|
|
34102
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34103
|
+
});
|
|
32515
34104
|
return;
|
|
32516
34105
|
}
|
|
32517
34106
|
const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
|
|
@@ -32525,6 +34114,11 @@ async function insertCommand(table, options, command) {
|
|
|
32525
34114
|
timestamp: new Date().toISOString()
|
|
32526
34115
|
};
|
|
32527
34116
|
console.log(JSON.stringify(output, null, 2));
|
|
34117
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34118
|
+
success: true,
|
|
34119
|
+
target: table,
|
|
34120
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34121
|
+
});
|
|
32528
34122
|
return;
|
|
32529
34123
|
} finally {
|
|
32530
34124
|
await adapter2.disconnect();
|
|
@@ -32538,6 +34132,11 @@ async function insertCommand(table, options, command) {
|
|
|
32538
34132
|
timestamp: new Date().toISOString(),
|
|
32539
34133
|
error: "Elasticsearch \u4E0D\u652F\u63F4 insert \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u5BEB\u5165"
|
|
32540
34134
|
};
|
|
34135
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34136
|
+
success: false,
|
|
34137
|
+
target: table,
|
|
34138
|
+
error: new Error("Elasticsearch does not support insert command")
|
|
34139
|
+
});
|
|
32541
34140
|
console.log(JSON.stringify(output, null, 2));
|
|
32542
34141
|
process.exit(1);
|
|
32543
34142
|
}
|
|
@@ -32556,6 +34155,11 @@ async function insertCommand(table, options, command) {
|
|
|
32556
34155
|
timestamp: new Date().toISOString()
|
|
32557
34156
|
};
|
|
32558
34157
|
console.log(JSON.stringify(output, null, 2));
|
|
34158
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34159
|
+
success: true,
|
|
34160
|
+
target: table,
|
|
34161
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34162
|
+
});
|
|
32559
34163
|
return;
|
|
32560
34164
|
}
|
|
32561
34165
|
const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
@@ -32570,6 +34174,11 @@ async function insertCommand(table, options, command) {
|
|
|
32570
34174
|
lastInsertId: result.lastInsertId
|
|
32571
34175
|
};
|
|
32572
34176
|
console.log(JSON.stringify(output, null, 2));
|
|
34177
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34178
|
+
success: true,
|
|
34179
|
+
target: table,
|
|
34180
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34181
|
+
});
|
|
32573
34182
|
return;
|
|
32574
34183
|
} finally {
|
|
32575
34184
|
await adapter2.disconnect();
|
|
@@ -32596,6 +34205,18 @@ async function insertCommand(table, options, command) {
|
|
|
32596
34205
|
...result.error && { error: result.error }
|
|
32597
34206
|
};
|
|
32598
34207
|
console.log(JSON.stringify(output, null, 2));
|
|
34208
|
+
await writeAuditEntry(config, "insert", options, {
|
|
34209
|
+
success: result.status === "success",
|
|
34210
|
+
target: table,
|
|
34211
|
+
...result.sql && { sql: result.sql },
|
|
34212
|
+
metadata: {
|
|
34213
|
+
rows_affected: result.rows_affected,
|
|
34214
|
+
...options.dryRun && { dry_run: true }
|
|
34215
|
+
},
|
|
34216
|
+
...result.status === "error" && {
|
|
34217
|
+
error: new Error(result.error ?? "insert failed")
|
|
34218
|
+
}
|
|
34219
|
+
});
|
|
32599
34220
|
if (result.status === "error") {
|
|
32600
34221
|
process.exit(1);
|
|
32601
34222
|
}
|
|
@@ -32603,14 +34224,23 @@ async function insertCommand(table, options, command) {
|
|
|
32603
34224
|
await adapter.disconnect();
|
|
32604
34225
|
}
|
|
32605
34226
|
} catch (error) {
|
|
34227
|
+
let auditId = null;
|
|
34228
|
+
let envelopeId;
|
|
32606
34229
|
if (options.recovery === true) {
|
|
32607
|
-
|
|
32608
|
-
|
|
32609
|
-
|
|
32610
|
-
|
|
32611
|
-
|
|
34230
|
+
envelopeId = crypto4.randomUUID();
|
|
34231
|
+
}
|
|
34232
|
+
if (config) {
|
|
34233
|
+
auditId = await writeAuditEntry(config, "insert", options, {
|
|
34234
|
+
success: false,
|
|
34235
|
+
target: table,
|
|
34236
|
+
error,
|
|
34237
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
32612
34238
|
});
|
|
32613
34239
|
}
|
|
34240
|
+
if (envelopeId !== undefined) {
|
|
34241
|
+
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34242
|
+
emitRecoveryEnvelope2(error, { operation: "insert", table, writeOperation: "INSERT" }, { envelopeId, auditRef: auditId ?? undefined });
|
|
34243
|
+
}
|
|
32614
34244
|
if (error instanceof BlacklistError) {
|
|
32615
34245
|
const output2 = {
|
|
32616
34246
|
status: "error",
|
|
@@ -32645,61 +34275,32 @@ async function insertCommand(table, options, command) {
|
|
|
32645
34275
|
// src/commands/update.ts
|
|
32646
34276
|
init_message_loader();
|
|
32647
34277
|
init_adapters();
|
|
34278
|
+
import crypto5 from "crypto";
|
|
32648
34279
|
init_config();
|
|
32649
34280
|
init_permission_guard();
|
|
32650
34281
|
init_blacklist();
|
|
34282
|
+
init_integration_helper();
|
|
32651
34283
|
function requireSqlConnection4(connection) {
|
|
32652
34284
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
32653
34285
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
32654
34286
|
}
|
|
32655
34287
|
return connection;
|
|
32656
34288
|
}
|
|
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
34289
|
async function updateCommand(table, options, command) {
|
|
34290
|
+
let config;
|
|
32690
34291
|
try {
|
|
32691
34292
|
if (!table || table.trim() === "") {
|
|
32692
34293
|
throw new Error("Table name required");
|
|
32693
34294
|
}
|
|
32694
34295
|
table = table.trim();
|
|
32695
|
-
if (!options.where || options.where.trim() === "") {
|
|
34296
|
+
if (!options.plan && (!options.where || options.where.trim() === "")) {
|
|
32696
34297
|
throw new Error('UPDATE requires --where clause (e.g. --where "id=1")');
|
|
32697
34298
|
}
|
|
32698
34299
|
if (!options.set || options.set.trim() === "") {
|
|
32699
34300
|
throw new Error(`UPDATE requires --set flag with JSON data (e.g. --set '{"name":"Bob"}')`);
|
|
32700
34301
|
}
|
|
32701
34302
|
const configPath = resolveConfigPath(command, options);
|
|
32702
|
-
|
|
34303
|
+
config = await configModule.read(configPath);
|
|
32703
34304
|
if (!config.connection) {
|
|
32704
34305
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
32705
34306
|
}
|
|
@@ -32712,6 +34313,45 @@ async function updateCommand(table, options, command) {
|
|
|
32712
34313
|
if (!setData || typeof setData !== "object" || Array.isArray(setData)) {
|
|
32713
34314
|
throw new Error('JSON in --set must be an object (e.g. {"name":"Bob","email":"b@example.com"})');
|
|
32714
34315
|
}
|
|
34316
|
+
if (options.plan) {
|
|
34317
|
+
if (options.dryRun) {
|
|
34318
|
+
console.error("--plan cannot be used with --dry-run");
|
|
34319
|
+
process.exit(1);
|
|
34320
|
+
return;
|
|
34321
|
+
}
|
|
34322
|
+
const rawWhere = options.where ?? "";
|
|
34323
|
+
let whereForPlan = null;
|
|
34324
|
+
if (rawWhere.trim() !== "") {
|
|
34325
|
+
try {
|
|
34326
|
+
const parsed = JSON.parse(rawWhere);
|
|
34327
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
34328
|
+
whereForPlan = parsed;
|
|
34329
|
+
}
|
|
34330
|
+
} catch {
|
|
34331
|
+
whereForPlan = null;
|
|
34332
|
+
}
|
|
34333
|
+
if (whereForPlan === null) {
|
|
34334
|
+
try {
|
|
34335
|
+
whereForPlan = parseWhereClause(rawWhere);
|
|
34336
|
+
} catch {
|
|
34337
|
+
whereForPlan = {};
|
|
34338
|
+
}
|
|
34339
|
+
}
|
|
34340
|
+
}
|
|
34341
|
+
await runDmlPlanAnalysis({
|
|
34342
|
+
operation: "update",
|
|
34343
|
+
target: table,
|
|
34344
|
+
set: setData,
|
|
34345
|
+
where: whereForPlan,
|
|
34346
|
+
rawWhere
|
|
34347
|
+
}, { format: options.format, config: options.config }, command);
|
|
34348
|
+
await writeAuditEntry(config, "update", options, {
|
|
34349
|
+
success: true,
|
|
34350
|
+
target: table,
|
|
34351
|
+
metadata: { plan: true }
|
|
34352
|
+
});
|
|
34353
|
+
return;
|
|
34354
|
+
}
|
|
32715
34355
|
if (config.connection?.system === "redis") {
|
|
32716
34356
|
enforcePermission("UPDATE dummy", config.permission);
|
|
32717
34357
|
const blacklistManager = new BlacklistManager(config);
|
|
@@ -32726,6 +34366,11 @@ async function updateCommand(table, options, command) {
|
|
|
32726
34366
|
timestamp: new Date().toISOString()
|
|
32727
34367
|
};
|
|
32728
34368
|
console.log(JSON.stringify(output, null, 2));
|
|
34369
|
+
await writeAuditEntry(config, "update", options, {
|
|
34370
|
+
success: true,
|
|
34371
|
+
target: table,
|
|
34372
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34373
|
+
});
|
|
32729
34374
|
return;
|
|
32730
34375
|
}
|
|
32731
34376
|
const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
|
|
@@ -32739,6 +34384,11 @@ async function updateCommand(table, options, command) {
|
|
|
32739
34384
|
timestamp: new Date().toISOString()
|
|
32740
34385
|
};
|
|
32741
34386
|
console.log(JSON.stringify(output, null, 2));
|
|
34387
|
+
await writeAuditEntry(config, "update", options, {
|
|
34388
|
+
success: true,
|
|
34389
|
+
target: table,
|
|
34390
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34391
|
+
});
|
|
32742
34392
|
return;
|
|
32743
34393
|
} finally {
|
|
32744
34394
|
await adapter2.disconnect();
|
|
@@ -32752,6 +34402,11 @@ async function updateCommand(table, options, command) {
|
|
|
32752
34402
|
timestamp: new Date().toISOString(),
|
|
32753
34403
|
error: "Elasticsearch \u4E0D\u652F\u63F4 update \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u66F4\u65B0"
|
|
32754
34404
|
};
|
|
34405
|
+
await writeAuditEntry(config, "update", options, {
|
|
34406
|
+
success: false,
|
|
34407
|
+
target: table,
|
|
34408
|
+
error: new Error("Elasticsearch does not support update command")
|
|
34409
|
+
});
|
|
32755
34410
|
console.log(JSON.stringify(output, null, 2));
|
|
32756
34411
|
process.exit(1);
|
|
32757
34412
|
}
|
|
@@ -32791,6 +34446,11 @@ async function updateCommand(table, options, command) {
|
|
|
32791
34446
|
timestamp: new Date().toISOString()
|
|
32792
34447
|
};
|
|
32793
34448
|
console.log(JSON.stringify(output, null, 2));
|
|
34449
|
+
await writeAuditEntry(config, "update", options, {
|
|
34450
|
+
success: true,
|
|
34451
|
+
target: table,
|
|
34452
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34453
|
+
});
|
|
32794
34454
|
return;
|
|
32795
34455
|
}
|
|
32796
34456
|
const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
@@ -32804,6 +34464,11 @@ async function updateCommand(table, options, command) {
|
|
|
32804
34464
|
timestamp: new Date().toISOString()
|
|
32805
34465
|
};
|
|
32806
34466
|
console.log(JSON.stringify(output, null, 2));
|
|
34467
|
+
await writeAuditEntry(config, "update", options, {
|
|
34468
|
+
success: true,
|
|
34469
|
+
target: table,
|
|
34470
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34471
|
+
});
|
|
32807
34472
|
return;
|
|
32808
34473
|
} finally {
|
|
32809
34474
|
await adapter2.disconnect();
|
|
@@ -32836,6 +34501,18 @@ async function updateCommand(table, options, command) {
|
|
|
32836
34501
|
...result.error && { error: result.error }
|
|
32837
34502
|
};
|
|
32838
34503
|
console.log(JSON.stringify(output, null, 2));
|
|
34504
|
+
await writeAuditEntry(config, "update", options, {
|
|
34505
|
+
success: result.status === "success",
|
|
34506
|
+
target: table,
|
|
34507
|
+
...result.sql && { sql: result.sql },
|
|
34508
|
+
metadata: {
|
|
34509
|
+
rows_affected: result.rows_affected,
|
|
34510
|
+
...options.dryRun && { dry_run: true }
|
|
34511
|
+
},
|
|
34512
|
+
...result.status === "error" && {
|
|
34513
|
+
error: new Error(result.error ?? "update failed")
|
|
34514
|
+
}
|
|
34515
|
+
});
|
|
32839
34516
|
if (result.status === "error") {
|
|
32840
34517
|
process.exit(1);
|
|
32841
34518
|
}
|
|
@@ -32843,14 +34520,23 @@ async function updateCommand(table, options, command) {
|
|
|
32843
34520
|
await adapter.disconnect();
|
|
32844
34521
|
}
|
|
32845
34522
|
} catch (error) {
|
|
34523
|
+
let auditId = null;
|
|
34524
|
+
let envelopeId;
|
|
32846
34525
|
if (options.recovery === true) {
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
34526
|
+
envelopeId = crypto5.randomUUID();
|
|
34527
|
+
}
|
|
34528
|
+
if (config) {
|
|
34529
|
+
auditId = await writeAuditEntry(config, "update", options, {
|
|
34530
|
+
success: false,
|
|
34531
|
+
target: table,
|
|
34532
|
+
error,
|
|
34533
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
32852
34534
|
});
|
|
32853
34535
|
}
|
|
34536
|
+
if (envelopeId !== undefined) {
|
|
34537
|
+
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34538
|
+
emitRecoveryEnvelope2(error, { operation: "update", table, writeOperation: "UPDATE" }, { envelopeId, auditRef: auditId ?? undefined });
|
|
34539
|
+
}
|
|
32854
34540
|
if (error instanceof BlacklistError) {
|
|
32855
34541
|
const output2 = {
|
|
32856
34542
|
status: "error",
|
|
@@ -32885,57 +34571,70 @@ async function updateCommand(table, options, command) {
|
|
|
32885
34571
|
// src/commands/delete.ts
|
|
32886
34572
|
init_message_loader();
|
|
32887
34573
|
init_adapters();
|
|
34574
|
+
import crypto6 from "crypto";
|
|
32888
34575
|
init_config();
|
|
32889
34576
|
init_permission_guard();
|
|
32890
34577
|
init_blacklist();
|
|
34578
|
+
init_integration_helper();
|
|
32891
34579
|
function requireSqlConnection5(connection) {
|
|
32892
34580
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
32893
34581
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
32894
34582
|
}
|
|
32895
34583
|
return connection;
|
|
32896
34584
|
}
|
|
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
34585
|
async function deleteCommand(table, options, command) {
|
|
34586
|
+
let config;
|
|
32926
34587
|
try {
|
|
32927
34588
|
if (!table || table.trim() === "") {
|
|
32928
34589
|
throw new Error("Table name required");
|
|
32929
34590
|
}
|
|
32930
34591
|
table = table.trim();
|
|
32931
|
-
if (!options.where || options.where.trim() === "") {
|
|
34592
|
+
if (!options.plan && (!options.where || options.where.trim() === "")) {
|
|
32932
34593
|
throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
|
|
32933
34594
|
}
|
|
32934
34595
|
const configPath = resolveConfigPath(command, options);
|
|
32935
|
-
|
|
34596
|
+
config = await configModule.read(configPath);
|
|
32936
34597
|
if (!config.connection) {
|
|
32937
34598
|
throw new Error('Run "dbcli init" to configure database connection');
|
|
32938
34599
|
}
|
|
34600
|
+
if (options.plan) {
|
|
34601
|
+
if (options.dryRun) {
|
|
34602
|
+
console.error("--plan cannot be used with --dry-run");
|
|
34603
|
+
process.exit(1);
|
|
34604
|
+
return;
|
|
34605
|
+
}
|
|
34606
|
+
const rawWhere = options.where ?? "";
|
|
34607
|
+
let whereForPlan = null;
|
|
34608
|
+
if (rawWhere.trim() !== "") {
|
|
34609
|
+
try {
|
|
34610
|
+
const parsed = JSON.parse(rawWhere);
|
|
34611
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
34612
|
+
whereForPlan = parsed;
|
|
34613
|
+
}
|
|
34614
|
+
} catch {
|
|
34615
|
+
whereForPlan = null;
|
|
34616
|
+
}
|
|
34617
|
+
if (whereForPlan === null) {
|
|
34618
|
+
try {
|
|
34619
|
+
whereForPlan = parseWhereClause(rawWhere);
|
|
34620
|
+
} catch {
|
|
34621
|
+
whereForPlan = {};
|
|
34622
|
+
}
|
|
34623
|
+
}
|
|
34624
|
+
}
|
|
34625
|
+
await runDmlPlanAnalysis({
|
|
34626
|
+
operation: "delete",
|
|
34627
|
+
target: table,
|
|
34628
|
+
where: whereForPlan,
|
|
34629
|
+
rawWhere
|
|
34630
|
+
}, { format: options.format, config: options.config }, command);
|
|
34631
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34632
|
+
success: true,
|
|
34633
|
+
target: table,
|
|
34634
|
+
metadata: { plan: true }
|
|
34635
|
+
});
|
|
34636
|
+
return;
|
|
34637
|
+
}
|
|
32939
34638
|
if (config.permission !== "data-admin" && config.permission !== "admin") {
|
|
32940
34639
|
throw new PermissionError(t("delete.admin_only"), {
|
|
32941
34640
|
type: "DELETE",
|
|
@@ -32953,7 +34652,7 @@ async function deleteCommand(table, options, command) {
|
|
|
32953
34652
|
try {
|
|
32954
34653
|
filter = JSON.parse(options.where);
|
|
32955
34654
|
} catch {
|
|
32956
|
-
filter =
|
|
34655
|
+
filter = parseWhereClause(options.where);
|
|
32957
34656
|
}
|
|
32958
34657
|
if (options.dryRun) {
|
|
32959
34658
|
const output = {
|
|
@@ -32964,6 +34663,11 @@ async function deleteCommand(table, options, command) {
|
|
|
32964
34663
|
timestamp: new Date().toISOString()
|
|
32965
34664
|
};
|
|
32966
34665
|
console.log(JSON.stringify(output, null, 2));
|
|
34666
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34667
|
+
success: true,
|
|
34668
|
+
target: table,
|
|
34669
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34670
|
+
});
|
|
32967
34671
|
return;
|
|
32968
34672
|
}
|
|
32969
34673
|
const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
|
|
@@ -32977,6 +34681,11 @@ async function deleteCommand(table, options, command) {
|
|
|
32977
34681
|
timestamp: new Date().toISOString()
|
|
32978
34682
|
};
|
|
32979
34683
|
console.log(JSON.stringify(output, null, 2));
|
|
34684
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34685
|
+
success: true,
|
|
34686
|
+
target: table,
|
|
34687
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34688
|
+
});
|
|
32980
34689
|
return;
|
|
32981
34690
|
} finally {
|
|
32982
34691
|
await adapter2.disconnect();
|
|
@@ -32990,6 +34699,11 @@ async function deleteCommand(table, options, command) {
|
|
|
32990
34699
|
timestamp: new Date().toISOString(),
|
|
32991
34700
|
error: "Elasticsearch \u4E0D\u652F\u63F4 delete \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u522A\u9664"
|
|
32992
34701
|
};
|
|
34702
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34703
|
+
success: false,
|
|
34704
|
+
target: table,
|
|
34705
|
+
error: new Error("Elasticsearch does not support delete command")
|
|
34706
|
+
});
|
|
32993
34707
|
console.log(JSON.stringify(output, null, 2));
|
|
32994
34708
|
process.exit(1);
|
|
32995
34709
|
}
|
|
@@ -33001,7 +34715,7 @@ async function deleteCommand(table, options, command) {
|
|
|
33001
34715
|
try {
|
|
33002
34716
|
filter = JSON.parse(options.where);
|
|
33003
34717
|
} catch {
|
|
33004
|
-
filter =
|
|
34718
|
+
filter = parseWhereClause(options.where);
|
|
33005
34719
|
}
|
|
33006
34720
|
if (options.dryRun) {
|
|
33007
34721
|
const output = {
|
|
@@ -33012,6 +34726,11 @@ async function deleteCommand(table, options, command) {
|
|
|
33012
34726
|
timestamp: new Date().toISOString()
|
|
33013
34727
|
};
|
|
33014
34728
|
console.log(JSON.stringify(output, null, 2));
|
|
34729
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34730
|
+
success: true,
|
|
34731
|
+
target: table,
|
|
34732
|
+
metadata: { rows_affected: 0, dry_run: true }
|
|
34733
|
+
});
|
|
33015
34734
|
return;
|
|
33016
34735
|
}
|
|
33017
34736
|
const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
|
|
@@ -33025,6 +34744,11 @@ async function deleteCommand(table, options, command) {
|
|
|
33025
34744
|
timestamp: new Date().toISOString()
|
|
33026
34745
|
};
|
|
33027
34746
|
console.log(JSON.stringify(output, null, 2));
|
|
34747
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34748
|
+
success: true,
|
|
34749
|
+
target: table,
|
|
34750
|
+
metadata: { rows_affected: result.affectedRows }
|
|
34751
|
+
});
|
|
33028
34752
|
return;
|
|
33029
34753
|
} finally {
|
|
33030
34754
|
await adapter2.disconnect();
|
|
@@ -33032,7 +34756,7 @@ async function deleteCommand(table, options, command) {
|
|
|
33032
34756
|
}
|
|
33033
34757
|
let whereConditions;
|
|
33034
34758
|
try {
|
|
33035
|
-
whereConditions =
|
|
34759
|
+
whereConditions = parseWhereClause(options.where);
|
|
33036
34760
|
} catch (error) {
|
|
33037
34761
|
throw new Error(`WHERE clause parsing failed: ${error.message}`);
|
|
33038
34762
|
}
|
|
@@ -33057,6 +34781,18 @@ async function deleteCommand(table, options, command) {
|
|
|
33057
34781
|
...result.error && { error: result.error }
|
|
33058
34782
|
};
|
|
33059
34783
|
console.log(JSON.stringify(output, null, 2));
|
|
34784
|
+
await writeAuditEntry(config, "delete", options, {
|
|
34785
|
+
success: result.status === "success",
|
|
34786
|
+
target: table,
|
|
34787
|
+
...result.sql && { sql: result.sql },
|
|
34788
|
+
metadata: {
|
|
34789
|
+
rows_affected: result.rows_affected,
|
|
34790
|
+
...options.dryRun && { dry_run: true }
|
|
34791
|
+
},
|
|
34792
|
+
...result.status === "error" && {
|
|
34793
|
+
error: new Error(result.error ?? "delete failed")
|
|
34794
|
+
}
|
|
34795
|
+
});
|
|
33060
34796
|
if (result.status === "error") {
|
|
33061
34797
|
process.exit(1);
|
|
33062
34798
|
}
|
|
@@ -33064,14 +34800,23 @@ async function deleteCommand(table, options, command) {
|
|
|
33064
34800
|
await adapter.disconnect();
|
|
33065
34801
|
}
|
|
33066
34802
|
} catch (error) {
|
|
34803
|
+
let auditId = null;
|
|
34804
|
+
let envelopeId;
|
|
33067
34805
|
if (options.recovery === true) {
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
34806
|
+
envelopeId = crypto6.randomUUID();
|
|
34807
|
+
}
|
|
34808
|
+
if (config) {
|
|
34809
|
+
auditId = await writeAuditEntry(config, "delete", options, {
|
|
34810
|
+
success: false,
|
|
34811
|
+
target: table,
|
|
34812
|
+
error,
|
|
34813
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
33073
34814
|
});
|
|
33074
34815
|
}
|
|
34816
|
+
if (envelopeId !== undefined) {
|
|
34817
|
+
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34818
|
+
emitRecoveryEnvelope2(error, { operation: "delete", table, writeOperation: "DELETE" }, { envelopeId, auditRef: auditId ?? undefined });
|
|
34819
|
+
}
|
|
33075
34820
|
if (error instanceof BlacklistError) {
|
|
33076
34821
|
const output2 = {
|
|
33077
34822
|
status: "error",
|
|
@@ -33106,17 +34851,21 @@ async function deleteCommand(table, options, command) {
|
|
|
33106
34851
|
// src/commands/export.ts
|
|
33107
34852
|
init_message_loader();
|
|
33108
34853
|
init_adapters();
|
|
34854
|
+
import crypto7 from "crypto";
|
|
33109
34855
|
init_config();
|
|
33110
34856
|
init_permission_guard();
|
|
33111
34857
|
init_blacklist();
|
|
34858
|
+
init_integration_helper();
|
|
34859
|
+
init_engine_hints();
|
|
33112
34860
|
function requireSqlConnection6(connection) {
|
|
33113
34861
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
33114
34862
|
throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
|
|
33115
34863
|
}
|
|
33116
34864
|
return connection;
|
|
33117
34865
|
}
|
|
33118
|
-
var
|
|
34866
|
+
var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
|
|
33119
34867
|
async function exportCommand(sql, options, command) {
|
|
34868
|
+
let config;
|
|
33120
34869
|
try {
|
|
33121
34870
|
if (!sql || sql.trim() === "") {
|
|
33122
34871
|
throw new Error("Query required");
|
|
@@ -33126,7 +34875,7 @@ async function exportCommand(sql, options, command) {
|
|
|
33126
34875
|
}
|
|
33127
34876
|
sql = sql.trim();
|
|
33128
34877
|
const configPath = resolveConfigPath(command, options);
|
|
33129
|
-
|
|
34878
|
+
config = await configModule.read(configPath);
|
|
33130
34879
|
if (!config.connection) {
|
|
33131
34880
|
throw new Error('Run "dbcli init" first');
|
|
33132
34881
|
}
|
|
@@ -33182,18 +34931,39 @@ async function exportCommand(sql, options, command) {
|
|
|
33182
34931
|
} else {
|
|
33183
34932
|
console.log(formatted);
|
|
33184
34933
|
}
|
|
34934
|
+
await writeAuditEntry(config, "export", options, {
|
|
34935
|
+
success: true,
|
|
34936
|
+
target: extractTableName(sql) ?? "*",
|
|
34937
|
+
sql,
|
|
34938
|
+
metadata: {
|
|
34939
|
+
rows_affected: result.rowCount,
|
|
34940
|
+
output_format: options.format,
|
|
34941
|
+
...options.output && { output_file: options.output }
|
|
34942
|
+
}
|
|
34943
|
+
});
|
|
33185
34944
|
} finally {
|
|
33186
34945
|
await adapter.disconnect();
|
|
33187
34946
|
}
|
|
33188
34947
|
} catch (error) {
|
|
34948
|
+
let auditId = null;
|
|
34949
|
+
let envelopeId;
|
|
33189
34950
|
if (options.recovery === true) {
|
|
33190
|
-
|
|
33191
|
-
|
|
33192
|
-
|
|
33193
|
-
|
|
33194
|
-
|
|
34951
|
+
envelopeId = crypto7.randomUUID();
|
|
34952
|
+
}
|
|
34953
|
+
const m = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
|
|
34954
|
+
if (config) {
|
|
34955
|
+
auditId = await writeAuditEntry(config, "export", options, {
|
|
34956
|
+
success: false,
|
|
34957
|
+
target: m?.[1] ?? "*",
|
|
34958
|
+
sql,
|
|
34959
|
+
error,
|
|
34960
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
33195
34961
|
});
|
|
33196
34962
|
}
|
|
34963
|
+
if (envelopeId !== undefined) {
|
|
34964
|
+
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34965
|
+
emitRecoveryEnvelope2(error, { operation: "export", table: m?.[1] }, { envelopeId, auditRef: auditId ?? undefined });
|
|
34966
|
+
}
|
|
33197
34967
|
if (error instanceof PermissionError) {
|
|
33198
34968
|
console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
|
|
33199
34969
|
console.error(` Operation: ${error.classification.type}`);
|
|
@@ -33214,17 +34984,7 @@ async function exportCommand(sql, options, command) {
|
|
|
33214
34984
|
}
|
|
33215
34985
|
async function redisExportBranch(command, options, config) {
|
|
33216
34986
|
const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
|
|
33217
|
-
|
|
33218
|
-
enforceRedisPermission2(command, config.permission);
|
|
33219
|
-
} catch (error) {
|
|
33220
|
-
if (error instanceof PermissionError) {
|
|
33221
|
-
console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
|
|
33222
|
-
console.error(` Operation: ${error.classification.type}`);
|
|
33223
|
-
console.error(` Message: ${error.message}`);
|
|
33224
|
-
process.exit(1);
|
|
33225
|
-
}
|
|
33226
|
-
throw error;
|
|
33227
|
-
}
|
|
34987
|
+
enforceRedisPermission2(command, config.permission);
|
|
33228
34988
|
const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
|
|
33229
34989
|
await redisAdapter.connect();
|
|
33230
34990
|
try {
|
|
@@ -33255,12 +35015,22 @@ async function redisExportBranch(command, options, config) {
|
|
|
33255
35015
|
} else {
|
|
33256
35016
|
console.log(formatted);
|
|
33257
35017
|
}
|
|
35018
|
+
const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
|
|
35019
|
+
await writeAuditEntry(config, "export", options, {
|
|
35020
|
+
success: true,
|
|
35021
|
+
target,
|
|
35022
|
+
metadata: {
|
|
35023
|
+
rows_affected: result.rowCount ?? result.rows.length ?? 0,
|
|
35024
|
+
output_format: options.format,
|
|
35025
|
+
...options.output && { output_file: options.output }
|
|
35026
|
+
}
|
|
35027
|
+
});
|
|
33258
35028
|
} finally {
|
|
33259
35029
|
await redisAdapter.disconnect();
|
|
33260
35030
|
}
|
|
33261
35031
|
}
|
|
33262
35032
|
async function mongoExportBranch(query, options, config) {
|
|
33263
|
-
if (
|
|
35033
|
+
if (SQL_PATTERN.test(query)) {
|
|
33264
35034
|
console.error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002");
|
|
33265
35035
|
console.error(`\u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
|
|
33266
35036
|
process.exit(1);
|
|
@@ -33318,6 +35088,15 @@ async function mongoExportBranch(query, options, config) {
|
|
|
33318
35088
|
if (securityNote) {
|
|
33319
35089
|
console.error(`\u2139 ${securityNote}`);
|
|
33320
35090
|
}
|
|
35091
|
+
await writeAuditEntry(config, "export", options, {
|
|
35092
|
+
success: true,
|
|
35093
|
+
target: collection,
|
|
35094
|
+
metadata: {
|
|
35095
|
+
rows_affected: filterResult.filteredRows.length,
|
|
35096
|
+
output_format: options.format,
|
|
35097
|
+
...options.output && { output_file: options.output }
|
|
35098
|
+
}
|
|
35099
|
+
});
|
|
33321
35100
|
} finally {
|
|
33322
35101
|
await adapter.disconnect();
|
|
33323
35102
|
}
|
|
@@ -33380,6 +35159,11 @@ import * as path4 from "path";
|
|
|
33380
35159
|
import { homedir as homedir2 } from "os";
|
|
33381
35160
|
var SKILL_SOURCE_PATH = packageAssetPath("SKILL.md");
|
|
33382
35161
|
var REFERENCE_SOURCE_PATH = packageAssetPath("reference.md");
|
|
35162
|
+
function resolveSkillSource(lang) {
|
|
35163
|
+
if (lang === "zh-TW")
|
|
35164
|
+
return packageAssetPath("SKILL.zh-TW.md");
|
|
35165
|
+
return packageAssetPath("SKILL.md");
|
|
35166
|
+
}
|
|
33383
35167
|
var SUPPORTED_PLATFORMS = [
|
|
33384
35168
|
"claude",
|
|
33385
35169
|
"gemini",
|
|
@@ -33390,9 +35174,11 @@ var SUPPORTED_PLATFORMS = [
|
|
|
33390
35174
|
];
|
|
33391
35175
|
async function skillCommand(_program, options) {
|
|
33392
35176
|
try {
|
|
33393
|
-
const
|
|
35177
|
+
const lang = options.lang ?? "en";
|
|
35178
|
+
const skillSourcePath = resolveSkillSource(lang);
|
|
35179
|
+
const skillFile = Bun.file(skillSourcePath);
|
|
33394
35180
|
if (!await skillFile.exists()) {
|
|
33395
|
-
throw new Error(`Skill source not found: ${
|
|
35181
|
+
throw new Error(`Skill source not found: ${skillSourcePath}`);
|
|
33396
35182
|
}
|
|
33397
35183
|
const refFile = Bun.file(REFERENCE_SOURCE_PATH);
|
|
33398
35184
|
if (!await refFile.exists()) {
|
|
@@ -33480,15 +35266,15 @@ async function ensureDir(dirPath) {
|
|
|
33480
35266
|
await $`mkdir -p ${dirPath}`.quiet();
|
|
33481
35267
|
} catch {
|
|
33482
35268
|
try {
|
|
33483
|
-
const { mkdir:
|
|
33484
|
-
await
|
|
35269
|
+
const { mkdir: mkdir10 } = await import("fs/promises");
|
|
35270
|
+
await mkdir10(dirPath, { recursive: true });
|
|
33485
35271
|
} catch {
|
|
33486
35272
|
throw new Error(`Cannot create directory: ${dirPath}`);
|
|
33487
35273
|
}
|
|
33488
35274
|
}
|
|
33489
35275
|
}
|
|
33490
35276
|
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) => {
|
|
35277
|
+
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
35278
|
try {
|
|
33493
35279
|
await skillCommand(program2, options);
|
|
33494
35280
|
} catch (error) {
|
|
@@ -33512,15 +35298,15 @@ class AgentTaskError extends Error {
|
|
|
33512
35298
|
}
|
|
33513
35299
|
// src/core/agent-tasks/task-paths.ts
|
|
33514
35300
|
init_package_root();
|
|
33515
|
-
import { join as
|
|
35301
|
+
import { join as join21 } from "path";
|
|
33516
35302
|
function resolveBuiltinDir2() {
|
|
33517
35303
|
return packageAssetPath("tasks");
|
|
33518
35304
|
}
|
|
33519
35305
|
function resolveAgentTaskDirs(workspaceRoot) {
|
|
33520
35306
|
return {
|
|
33521
35307
|
builtinDir: resolveBuiltinDir2(),
|
|
33522
|
-
sharedDir:
|
|
33523
|
-
localDir:
|
|
35308
|
+
sharedDir: join21(workspaceRoot, ".dbcli-shared", "tasks"),
|
|
35309
|
+
localDir: join21(workspaceRoot, ".dbcli", "tasks")
|
|
33524
35310
|
};
|
|
33525
35311
|
}
|
|
33526
35312
|
// src/core/agent-tasks/parser.ts
|
|
@@ -33658,7 +35444,7 @@ function parseSteps(value, input) {
|
|
|
33658
35444
|
}
|
|
33659
35445
|
// src/core/agent-tasks/loader.ts
|
|
33660
35446
|
import { readdir as readdir2 } from "fs/promises";
|
|
33661
|
-
import { join as
|
|
35447
|
+
import { join as join22, relative as relative2, sep as sep2 } from "path";
|
|
33662
35448
|
async function loadAgentTasks(opts, flags) {
|
|
33663
35449
|
const errors3 = [];
|
|
33664
35450
|
const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
|
|
@@ -33715,7 +35501,7 @@ async function safeCollectMd(root) {
|
|
|
33715
35501
|
return;
|
|
33716
35502
|
}
|
|
33717
35503
|
for (const e of entries) {
|
|
33718
|
-
const full =
|
|
35504
|
+
const full = join22(dir, e.name);
|
|
33719
35505
|
if (e.isDirectory())
|
|
33720
35506
|
await walk(full);
|
|
33721
35507
|
else
|
|
@@ -34303,11 +36089,11 @@ function requireSqlConnection7(connection) {
|
|
|
34303
36089
|
}
|
|
34304
36090
|
return connection;
|
|
34305
36091
|
}
|
|
34306
|
-
var
|
|
36092
|
+
var ALLOWED_FORMATS6 = ["json", "table"];
|
|
34307
36093
|
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
36094
|
async function checkAction(table, options) {
|
|
34309
36095
|
try {
|
|
34310
|
-
validateFormat(options.format,
|
|
36096
|
+
validateFormat(options.format, ALLOWED_FORMATS6, "check");
|
|
34311
36097
|
const config = await configModule.read(options.config);
|
|
34312
36098
|
if (!config.connection) {
|
|
34313
36099
|
console.error("Database not configured. Run: dbcli init");
|
|
@@ -34440,7 +36226,7 @@ function requireSqlConnection8(connection) {
|
|
|
34440
36226
|
}
|
|
34441
36227
|
return connection;
|
|
34442
36228
|
}
|
|
34443
|
-
var
|
|
36229
|
+
var ALLOWED_FORMATS7 = ["json", "table"];
|
|
34444
36230
|
function compareSnapshots(before, after) {
|
|
34445
36231
|
const beforeTables = new Set(Object.keys(before.tables));
|
|
34446
36232
|
const afterTables = new Set(Object.keys(after.tables));
|
|
@@ -34534,7 +36320,7 @@ function compareSnapshots(before, after) {
|
|
|
34534
36320
|
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
36321
|
async function diffAction(options) {
|
|
34536
36322
|
try {
|
|
34537
|
-
validateFormat(options.format,
|
|
36323
|
+
validateFormat(options.format, ALLOWED_FORMATS7, "diff");
|
|
34538
36324
|
if (!options.snapshot && !options.against) {
|
|
34539
36325
|
console.error("Specify --snapshot <path> to save, or --against <path> to compare");
|
|
34540
36326
|
process.exit(1);
|
|
@@ -34643,10 +36429,10 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
|
|
|
34643
36429
|
// src/commands/status.ts
|
|
34644
36430
|
init_config();
|
|
34645
36431
|
init_validation();
|
|
34646
|
-
var
|
|
36432
|
+
var ALLOWED_FORMATS8 = ["text", "json"];
|
|
34647
36433
|
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
36434
|
try {
|
|
34649
|
-
validateFormat(options.format,
|
|
36435
|
+
validateFormat(options.format, ALLOWED_FORMATS8, "status");
|
|
34650
36436
|
const configPath = resolveConfigPath(statusCommand);
|
|
34651
36437
|
const config = await configModule.read(configPath);
|
|
34652
36438
|
if (!config.connection) {
|
|
@@ -34681,16 +36467,21 @@ var statusCommand = new Command("status").description("Show current configuratio
|
|
|
34681
36467
|
|
|
34682
36468
|
// src/commands/inspect.ts
|
|
34683
36469
|
init_message_loader();
|
|
36470
|
+
import crypto8 from "crypto";
|
|
34684
36471
|
init_validation();
|
|
34685
36472
|
init_inspect();
|
|
34686
|
-
|
|
36473
|
+
init_config();
|
|
36474
|
+
init_integration_helper();
|
|
36475
|
+
var ALLOWED_FORMATS9 = ["json", "markdown"];
|
|
34687
36476
|
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) => {
|
|
36477
|
+
let config;
|
|
34688
36478
|
try {
|
|
34689
36479
|
const forAgent = options.forAgent === true;
|
|
34690
36480
|
const format = forAgent ? "json" : options.format;
|
|
34691
36481
|
const brief = forAgent || options.brief === true;
|
|
34692
|
-
validateFormat(format,
|
|
36482
|
+
validateFormat(format, ALLOWED_FORMATS9, "inspect");
|
|
34693
36483
|
const configPath = resolveConfigPath(command, options);
|
|
36484
|
+
config = await configModule.read(configPath);
|
|
34694
36485
|
const snap = await collectInspect({
|
|
34695
36486
|
workspace: process.cwd(),
|
|
34696
36487
|
configPath,
|
|
@@ -34702,12 +36493,40 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
|
|
|
34702
36493
|
const { requireSchemaCacheOrThrow: requireSchemaCacheOrThrow2 } = await Promise.resolve().then(() => (init_inspect(), exports_inspect));
|
|
34703
36494
|
requireSchemaCacheOrThrow2(snap.schemaCache, snap.system ?? null);
|
|
34704
36495
|
}
|
|
36496
|
+
if (config) {
|
|
36497
|
+
const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
36498
|
+
if (shouldEmbedRecent2({ forAgent, format })) {
|
|
36499
|
+
snap.audit_recent = await loadRecentAudit2(config, configPath);
|
|
36500
|
+
}
|
|
36501
|
+
}
|
|
34705
36502
|
const out = format === "markdown" ? renderMarkdown2(snap, { brief }) : renderJson2(snap, { brief });
|
|
34706
36503
|
console.log(out);
|
|
36504
|
+
if (config) {
|
|
36505
|
+
await writeAuditEntry(config, "inspect", options, {
|
|
36506
|
+
success: true,
|
|
36507
|
+
target: "*"
|
|
36508
|
+
});
|
|
36509
|
+
}
|
|
34707
36510
|
} catch (err) {
|
|
36511
|
+
let auditId = null;
|
|
36512
|
+
let envelopeId;
|
|
34708
36513
|
if (options.recovery === true) {
|
|
36514
|
+
envelopeId = crypto8.randomUUID();
|
|
36515
|
+
}
|
|
36516
|
+
if (config) {
|
|
36517
|
+
auditId = await writeAuditEntry(config, "inspect", options, {
|
|
36518
|
+
success: false,
|
|
36519
|
+
target: "*",
|
|
36520
|
+
error: err,
|
|
36521
|
+
...envelopeId && { recovery_ref: envelopeId }
|
|
36522
|
+
});
|
|
36523
|
+
}
|
|
36524
|
+
if (envelopeId !== undefined) {
|
|
34709
36525
|
const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
|
|
34710
|
-
emitRecoveryEnvelope2(err, { operation: "inspect" }
|
|
36526
|
+
emitRecoveryEnvelope2(err, { operation: "inspect" }, {
|
|
36527
|
+
envelopeId,
|
|
36528
|
+
auditRef: auditId ?? undefined
|
|
36529
|
+
});
|
|
34711
36530
|
}
|
|
34712
36531
|
console.error(err.message);
|
|
34713
36532
|
process.exit(1);
|
|
@@ -34717,6 +36536,8 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
|
|
|
34717
36536
|
// src/commands/report.ts
|
|
34718
36537
|
init_message_loader();
|
|
34719
36538
|
init_validation();
|
|
36539
|
+
init_config();
|
|
36540
|
+
init_integration_helper();
|
|
34720
36541
|
|
|
34721
36542
|
// src/core/report/types.ts
|
|
34722
36543
|
var REPORT_SCHEMA_VERSION = 1;
|
|
@@ -34971,10 +36792,10 @@ function finalize(input) {
|
|
|
34971
36792
|
context: input.context,
|
|
34972
36793
|
sections: input.sections,
|
|
34973
36794
|
warnings: input.warnings,
|
|
34974
|
-
suggestedCommands:
|
|
36795
|
+
suggestedCommands: buildSuggestedCommands5(input.context, input.sections)
|
|
34975
36796
|
};
|
|
34976
36797
|
}
|
|
34977
|
-
function
|
|
36798
|
+
function buildSuggestedCommands5(context, sections) {
|
|
34978
36799
|
if (!context.system)
|
|
34979
36800
|
return ["dbcli init"];
|
|
34980
36801
|
const out = ["dbcli inspect --format json"];
|
|
@@ -35092,7 +36913,7 @@ function formatCell(value) {
|
|
|
35092
36913
|
return String(value);
|
|
35093
36914
|
}
|
|
35094
36915
|
// src/commands/report.ts
|
|
35095
|
-
var
|
|
36916
|
+
var ALLOWED_FORMATS10 = ["json", "markdown"];
|
|
35096
36917
|
function parseSections(value) {
|
|
35097
36918
|
const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
35098
36919
|
for (const p of parts) {
|
|
@@ -35103,12 +36924,14 @@ function parseSections(value) {
|
|
|
35103
36924
|
return parts;
|
|
35104
36925
|
}
|
|
35105
36926
|
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) => {
|
|
36927
|
+
let config;
|
|
35106
36928
|
try {
|
|
35107
36929
|
const forAgent = options.forAgent === true;
|
|
35108
36930
|
const format = forAgent ? "json" : options.format;
|
|
35109
36931
|
const brief = forAgent || options.brief === true;
|
|
35110
|
-
validateFormat(format,
|
|
36932
|
+
validateFormat(format, ALLOWED_FORMATS10, "report");
|
|
35111
36933
|
const configPath = resolveConfigPath(command, options);
|
|
36934
|
+
config = await configModule.read(configPath);
|
|
35112
36935
|
const snap = await collectReport({
|
|
35113
36936
|
workspace: process.cwd(),
|
|
35114
36937
|
configPath,
|
|
@@ -35121,7 +36944,23 @@ var reportCommand = new Command().name("report").description(t("report.descripti
|
|
|
35121
36944
|
});
|
|
35122
36945
|
const out = format === "markdown" ? renderMarkdown3(snap, { brief }) : renderJson3(snap, { brief });
|
|
35123
36946
|
console.log(out);
|
|
36947
|
+
if (config) {
|
|
36948
|
+
await writeAuditEntry(config, "report", options, {
|
|
36949
|
+
success: true,
|
|
36950
|
+
target: "*",
|
|
36951
|
+
metadata: {
|
|
36952
|
+
sections: options.section || ALLOWED_SECTIONS
|
|
36953
|
+
}
|
|
36954
|
+
});
|
|
36955
|
+
}
|
|
35124
36956
|
} catch (err) {
|
|
36957
|
+
if (config) {
|
|
36958
|
+
await writeAuditEntry(config, "report", options, {
|
|
36959
|
+
success: false,
|
|
36960
|
+
target: "*",
|
|
36961
|
+
error: err
|
|
36962
|
+
});
|
|
36963
|
+
}
|
|
35125
36964
|
console.error(err.message);
|
|
35126
36965
|
process.exit(1);
|
|
35127
36966
|
}
|
|
@@ -35130,6 +36969,8 @@ var reportCommand = new Command().name("report").description(t("report.descripti
|
|
|
35130
36969
|
// src/commands/guide.ts
|
|
35131
36970
|
init_message_loader();
|
|
35132
36971
|
init_validation();
|
|
36972
|
+
init_config();
|
|
36973
|
+
init_integration_helper();
|
|
35133
36974
|
|
|
35134
36975
|
// src/core/guide/types.ts
|
|
35135
36976
|
var GUIDE_SCHEMA_VERSION = 1;
|
|
@@ -35471,7 +37312,7 @@ function renderGoalList() {
|
|
|
35471
37312
|
`);
|
|
35472
37313
|
}
|
|
35473
37314
|
// src/commands/guide.ts
|
|
35474
|
-
var
|
|
37315
|
+
var ALLOWED_FORMATS11 = ["json", "markdown"];
|
|
35475
37316
|
function parseGoal(value) {
|
|
35476
37317
|
const normalized = value.trim();
|
|
35477
37318
|
if (!ALLOWED_GOALS.includes(normalized)) {
|
|
@@ -35480,11 +37321,12 @@ function parseGoal(value) {
|
|
|
35480
37321
|
return normalized;
|
|
35481
37322
|
}
|
|
35482
37323
|
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) => {
|
|
37324
|
+
let config;
|
|
35483
37325
|
try {
|
|
35484
37326
|
const forAgent = options.forAgent === true;
|
|
35485
37327
|
const format = forAgent ? "json" : options.format;
|
|
35486
37328
|
const brief = forAgent || options.brief === true;
|
|
35487
|
-
validateFormat(format,
|
|
37329
|
+
validateFormat(format, ALLOWED_FORMATS11, "guide");
|
|
35488
37330
|
if (options.list === true) {
|
|
35489
37331
|
if (format === "markdown") {
|
|
35490
37332
|
console.log(renderGoalList());
|
|
@@ -35503,6 +37345,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35503
37345
|
}
|
|
35504
37346
|
const validated = parseGoal(goal);
|
|
35505
37347
|
const configPath = resolveConfigPath(command, options);
|
|
37348
|
+
config = await configModule.read(configPath);
|
|
35506
37349
|
const snap = await collectGuide({
|
|
35507
37350
|
workspace: process.cwd(),
|
|
35508
37351
|
configPath,
|
|
@@ -35511,9 +37354,28 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35511
37354
|
brief,
|
|
35512
37355
|
probeTimeoutMs: options.probeTimeout
|
|
35513
37356
|
});
|
|
37357
|
+
if (config) {
|
|
37358
|
+
const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
37359
|
+
if (shouldEmbedRecent2({ forAgent, format })) {
|
|
37360
|
+
snap.audit_recent = await loadRecentAudit2(config, configPath);
|
|
37361
|
+
}
|
|
37362
|
+
}
|
|
35514
37363
|
const out = format === "markdown" ? renderMarkdown4(snap, { brief }) : renderJson4(snap, { brief });
|
|
35515
37364
|
console.log(out);
|
|
37365
|
+
if (config) {
|
|
37366
|
+
await writeAuditEntry(config, "guide", options, {
|
|
37367
|
+
success: true,
|
|
37368
|
+
target: goal
|
|
37369
|
+
});
|
|
37370
|
+
}
|
|
35516
37371
|
} catch (err) {
|
|
37372
|
+
if (config) {
|
|
37373
|
+
await writeAuditEntry(config, "guide", options, {
|
|
37374
|
+
success: false,
|
|
37375
|
+
target: goal || "*",
|
|
37376
|
+
error: err
|
|
37377
|
+
});
|
|
37378
|
+
}
|
|
35517
37379
|
console.error(err.message);
|
|
35518
37380
|
process.exit(1);
|
|
35519
37381
|
}
|
|
@@ -35523,7 +37385,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
|
|
|
35523
37385
|
init_message_loader();
|
|
35524
37386
|
init_validation();
|
|
35525
37387
|
init_recovery();
|
|
35526
|
-
var
|
|
37388
|
+
var ALLOWED_FORMATS12 = ["json", "markdown"];
|
|
35527
37389
|
function parseCode(value) {
|
|
35528
37390
|
const normalized = value.trim();
|
|
35529
37391
|
if (!RECOVERY_CODES.includes(normalized)) {
|
|
@@ -35559,7 +37421,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
|
|
|
35559
37421
|
const forAgent = options.forAgent === true;
|
|
35560
37422
|
const format = forAgent ? "json" : options.format;
|
|
35561
37423
|
const brief = forAgent || options.brief === true;
|
|
35562
|
-
validateFormat(format,
|
|
37424
|
+
validateFormat(format, ALLOWED_FORMATS12, "recovery");
|
|
35563
37425
|
if (options.list === true) {
|
|
35564
37426
|
if (format === "markdown") {
|
|
35565
37427
|
console.log(renderCodeList());
|
|
@@ -35600,7 +37462,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
|
|
|
35600
37462
|
// src/commands/recover.ts
|
|
35601
37463
|
init_recovery();
|
|
35602
37464
|
init_last_envelope();
|
|
35603
|
-
import { stat as
|
|
37465
|
+
import { stat as stat6 } from "fs/promises";
|
|
35604
37466
|
import { resolve as resolve4 } from "path";
|
|
35605
37467
|
|
|
35606
37468
|
// src/core/recovery/envelope-schema.ts
|
|
@@ -35652,6 +37514,8 @@ var recoveryEnvelopeSchema = exports_external.object({
|
|
|
35652
37514
|
}).strict();
|
|
35653
37515
|
var savedRecoveryEnvelopeSchema = exports_external.object({
|
|
35654
37516
|
schemaVersion: exports_external.literal(1),
|
|
37517
|
+
id: exports_external.string().optional(),
|
|
37518
|
+
audit_ref: exports_external.string().optional(),
|
|
35655
37519
|
savedAt: exports_external.string().min(1),
|
|
35656
37520
|
command: exports_external.string(),
|
|
35657
37521
|
cwd: exports_external.string().min(1),
|
|
@@ -35682,7 +37546,7 @@ function looksLikeSavedEnvelope(x) {
|
|
|
35682
37546
|
// src/commands/recover.ts
|
|
35683
37547
|
init_next_step_schema();
|
|
35684
37548
|
init_validation();
|
|
35685
|
-
var
|
|
37549
|
+
var ALLOWED_FORMATS13 = ["json", "markdown"];
|
|
35686
37550
|
var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
|
|
35687
37551
|
|
|
35688
37552
|
class RecoverCliError extends Error {
|
|
@@ -35716,7 +37580,7 @@ async function resolveApplySource(opts) {
|
|
|
35716
37580
|
}
|
|
35717
37581
|
const saved2 = r3.value;
|
|
35718
37582
|
try {
|
|
35719
|
-
await
|
|
37583
|
+
await stat6(saved2.cwd);
|
|
35720
37584
|
} catch {
|
|
35721
37585
|
throw new RecoverCliError(`--from ${opts.from}: saved cwd '${saved2.cwd}' no longer exists.`, EXIT_CODE.malformed);
|
|
35722
37586
|
}
|
|
@@ -35750,7 +37614,7 @@ async function resolveApplySource(opts) {
|
|
|
35750
37614
|
}
|
|
35751
37615
|
const saved = r.value;
|
|
35752
37616
|
try {
|
|
35753
|
-
await
|
|
37617
|
+
await stat6(saved.cwd);
|
|
35754
37618
|
} catch {
|
|
35755
37619
|
throw new RecoverCliError(`Auto-saved ${LAST_ENVELOPE_PATH} references cwd '${saved.cwd}' that no longer exists.`, EXIT_CODE.malformed);
|
|
35756
37620
|
}
|
|
@@ -35824,7 +37688,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35824
37688
|
}
|
|
35825
37689
|
const explicitFormat = options.format;
|
|
35826
37690
|
const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
|
|
35827
|
-
validateFormat(format,
|
|
37691
|
+
validateFormat(format, ALLOWED_FORMATS13, "recover");
|
|
35828
37692
|
let allowWrite = "none";
|
|
35829
37693
|
if (options.next !== true) {
|
|
35830
37694
|
const allowWriteRaw = options.allowWrite;
|
|
@@ -35839,15 +37703,32 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35839
37703
|
from: options.from,
|
|
35840
37704
|
cwd: process.cwd()
|
|
35841
37705
|
});
|
|
37706
|
+
let audit_recent = [];
|
|
37707
|
+
if (format === "json" && options.next !== true) {
|
|
37708
|
+
try {
|
|
37709
|
+
const { configModule: configModule2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
37710
|
+
const configPath = `${process.cwd()}/.dbcli`;
|
|
37711
|
+
const config = await configModule2.read(configPath);
|
|
37712
|
+
if (config) {
|
|
37713
|
+
const { loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
37714
|
+
audit_recent = await loadRecentAudit2(config, configPath);
|
|
37715
|
+
}
|
|
37716
|
+
} catch {
|
|
37717
|
+
audit_recent = [];
|
|
37718
|
+
}
|
|
37719
|
+
}
|
|
35842
37720
|
if (options.next === true) {
|
|
35843
37721
|
const result2 = await runNext(options, source);
|
|
35844
|
-
const
|
|
35845
|
-
console.log(
|
|
37722
|
+
const out = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
|
|
37723
|
+
console.log(out);
|
|
35846
37724
|
return;
|
|
35847
37725
|
}
|
|
35848
37726
|
if (options.apply !== true) {
|
|
35849
|
-
|
|
35850
|
-
|
|
37727
|
+
if (format === "markdown") {
|
|
37728
|
+
console.log(renderMarkdown(source.envelope));
|
|
37729
|
+
} else {
|
|
37730
|
+
console.log(JSON.stringify({ ...source.envelope, audit_recent }, null, 2));
|
|
37731
|
+
}
|
|
35851
37732
|
return;
|
|
35852
37733
|
}
|
|
35853
37734
|
const noVerify = options.verify === false;
|
|
@@ -35856,8 +37737,11 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35856
37737
|
cwd: source.cwd,
|
|
35857
37738
|
source: { kind: source.kind, path: source.path }
|
|
35858
37739
|
}, { allowWrite, noVerify });
|
|
35859
|
-
|
|
35860
|
-
|
|
37740
|
+
if (format === "markdown") {
|
|
37741
|
+
console.log(renderApplyMarkdown(result));
|
|
37742
|
+
} else {
|
|
37743
|
+
console.log(JSON.stringify({ ...result, audit_recent }, null, 2));
|
|
37744
|
+
}
|
|
35861
37745
|
process.exit(exitCodeFor(result.finalStatus));
|
|
35862
37746
|
} catch (err) {
|
|
35863
37747
|
if (err instanceof RecoverCliError) {
|
|
@@ -35869,6 +37753,440 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
|
|
|
35869
37753
|
}
|
|
35870
37754
|
});
|
|
35871
37755
|
|
|
37756
|
+
// src/commands/audit.ts
|
|
37757
|
+
import { rm as rm3, stat as stat7 } from "fs/promises";
|
|
37758
|
+
import { join as join25 } from "path";
|
|
37759
|
+
init_message_loader();
|
|
37760
|
+
init_validation();
|
|
37761
|
+
init_config();
|
|
37762
|
+
init_config_binding();
|
|
37763
|
+
init_reader();
|
|
37764
|
+
init_integration_helper();
|
|
37765
|
+
var ALLOWED_FORMATS14 = ["table", "json"];
|
|
37766
|
+
var DEFAULT_TAIL_N = 10;
|
|
37767
|
+
var MAX_TAIL_N = 1e4;
|
|
37768
|
+
var SHORT_ID_LEN = 8;
|
|
37769
|
+
var MISSING_PLACEHOLDER = "\u2014";
|
|
37770
|
+
var PREFIX_MIN = 4;
|
|
37771
|
+
async function resolveAuditPaths(configPath, config) {
|
|
37772
|
+
const storagePath = await resolveConfigStoragePath(configPath);
|
|
37773
|
+
const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
|
|
37774
|
+
const auditDir = join25(storagePath, ".dbcli", "audit");
|
|
37775
|
+
const auditFile = join25(auditDir, `${connName}.jsonl`);
|
|
37776
|
+
return { auditDir, connectionName: connName, auditFile };
|
|
37777
|
+
}
|
|
37778
|
+
function isAuditDisabled(config) {
|
|
37779
|
+
return config?.audit?.enabled === false;
|
|
37780
|
+
}
|
|
37781
|
+
function emitDisabledAndExit0() {
|
|
37782
|
+
console.error(t("audit.disabled_hint"));
|
|
37783
|
+
process.exit(0);
|
|
37784
|
+
}
|
|
37785
|
+
function parseTailN(raw) {
|
|
37786
|
+
const requested = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
37787
|
+
if (!Number.isFinite(requested) || !Number.isInteger(requested) || requested <= 0) {
|
|
37788
|
+
console.error(t("audit.n_must_be_positive"));
|
|
37789
|
+
process.exit(1);
|
|
37790
|
+
}
|
|
37791
|
+
if (requested > MAX_TAIL_N) {
|
|
37792
|
+
console.error(t_vars("audit.n_capped_warning", {
|
|
37793
|
+
requested: String(requested),
|
|
37794
|
+
max: String(MAX_TAIL_N)
|
|
37795
|
+
}));
|
|
37796
|
+
return MAX_TAIL_N;
|
|
37797
|
+
}
|
|
37798
|
+
return requested;
|
|
37799
|
+
}
|
|
37800
|
+
function briefify(entry) {
|
|
37801
|
+
return {
|
|
37802
|
+
ts: entry.ts,
|
|
37803
|
+
command: entry.command,
|
|
37804
|
+
target: entry.target,
|
|
37805
|
+
success: entry.success
|
|
37806
|
+
};
|
|
37807
|
+
}
|
|
37808
|
+
function shortId(id) {
|
|
37809
|
+
if (!id)
|
|
37810
|
+
return MISSING_PLACEHOLDER;
|
|
37811
|
+
return id.length <= SHORT_ID_LEN ? id : id.slice(0, SHORT_ID_LEN);
|
|
37812
|
+
}
|
|
37813
|
+
function renderTable(rows, headers) {
|
|
37814
|
+
const allRows = [headers, ...rows];
|
|
37815
|
+
const widths = headers.map((_, col) => Math.max(...allRows.map((r) => (r[col] ?? "").length)));
|
|
37816
|
+
const fmt = (r) => r.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
37817
|
+
const sep3 = widths.map((w) => "-".repeat(w)).join(" ").trimEnd();
|
|
37818
|
+
return [fmt(headers), sep3, ...rows.map(fmt)].join(`
|
|
37819
|
+
`);
|
|
37820
|
+
}
|
|
37821
|
+
function renderTailTable(entries) {
|
|
37822
|
+
const headers = ["ts", "command", "target", "tier", "success", "id", "recovery_ref"];
|
|
37823
|
+
const rows = entries.map((e) => [
|
|
37824
|
+
e.ts,
|
|
37825
|
+
e.command,
|
|
37826
|
+
e.target,
|
|
37827
|
+
e.side_effect_tier,
|
|
37828
|
+
String(e.success),
|
|
37829
|
+
shortId(e.id),
|
|
37830
|
+
shortId(e.recovery_ref)
|
|
37831
|
+
]);
|
|
37832
|
+
return renderTable(rows, headers);
|
|
37833
|
+
}
|
|
37834
|
+
function renderTailAllTable(envelopes) {
|
|
37835
|
+
const headers = ["connection", "ts", "command", "target", "tier", "success", "id", "recovery_ref"];
|
|
37836
|
+
const rows = envelopes.map(({ connection, entry: e }) => [
|
|
37837
|
+
connection,
|
|
37838
|
+
e.ts,
|
|
37839
|
+
e.command,
|
|
37840
|
+
e.target,
|
|
37841
|
+
e.side_effect_tier,
|
|
37842
|
+
String(e.success),
|
|
37843
|
+
shortId(e.id),
|
|
37844
|
+
shortId(e.recovery_ref)
|
|
37845
|
+
]);
|
|
37846
|
+
return renderTable(rows, headers);
|
|
37847
|
+
}
|
|
37848
|
+
function briefifyShow(entry) {
|
|
37849
|
+
const { metadata, redacted_query, ...rest } = entry;
|
|
37850
|
+
return rest;
|
|
37851
|
+
}
|
|
37852
|
+
function renderEntryTable(entry) {
|
|
37853
|
+
const lines = [];
|
|
37854
|
+
if (entry.id !== undefined)
|
|
37855
|
+
lines.push(`Id: ${entry.id}`);
|
|
37856
|
+
if (entry.ts !== undefined)
|
|
37857
|
+
lines.push(`Ts: ${entry.ts}`);
|
|
37858
|
+
if (entry.session_id !== undefined)
|
|
37859
|
+
lines.push(`Session id: ${entry.session_id}`);
|
|
37860
|
+
if (entry.engine !== undefined)
|
|
37861
|
+
lines.push(`Engine: ${entry.engine}`);
|
|
37862
|
+
if (entry.command !== undefined)
|
|
37863
|
+
lines.push(`Command: ${entry.command}`);
|
|
37864
|
+
if (entry.side_effect_tier !== undefined)
|
|
37865
|
+
lines.push(`Side effect tier: ${entry.side_effect_tier}`);
|
|
37866
|
+
if (entry.target !== undefined)
|
|
37867
|
+
lines.push(`Target: ${entry.target}`);
|
|
37868
|
+
if (entry.success !== undefined)
|
|
37869
|
+
lines.push(`Success: ${entry.success}`);
|
|
37870
|
+
if (entry.recovery_ref !== undefined)
|
|
37871
|
+
lines.push(`Recovery ref: ${entry.recovery_ref}`);
|
|
37872
|
+
if (entry.redacted_query !== undefined)
|
|
37873
|
+
lines.push(`Redacted query: ${entry.redacted_query}`);
|
|
37874
|
+
if (entry.redacted_sql !== undefined)
|
|
37875
|
+
lines.push(`Redacted SQL: ${entry.redacted_sql}`);
|
|
37876
|
+
if (entry.error !== undefined)
|
|
37877
|
+
lines.push(`Error: ${entry.error}`);
|
|
37878
|
+
if (entry.metadata !== undefined)
|
|
37879
|
+
lines.push(`Metadata: ${JSON.stringify(entry.metadata)}`);
|
|
37880
|
+
return lines.join(`
|
|
37881
|
+
`);
|
|
37882
|
+
}
|
|
37883
|
+
function renderShowResult(hit, opts) {
|
|
37884
|
+
if (opts.format === "json") {
|
|
37885
|
+
const entryView = opts.brief ? briefifyShow(hit.entry) : hit.entry;
|
|
37886
|
+
const payload = opts.all ? { connection: hit.connection, entry: entryView } : entryView;
|
|
37887
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37888
|
+
} else {
|
|
37889
|
+
if (opts.all)
|
|
37890
|
+
console.log(`Connection: ${hit.connection}`);
|
|
37891
|
+
console.log(renderEntryTable(opts.brief ? briefifyShow(hit.entry) : hit.entry));
|
|
37892
|
+
}
|
|
37893
|
+
}
|
|
37894
|
+
function briefifyHealth(h) {
|
|
37895
|
+
return { enabled: h.enabled, lastWrite: h.lastWrite, rotationUsage: h.rotationUsage };
|
|
37896
|
+
}
|
|
37897
|
+
function formatBytes(n) {
|
|
37898
|
+
if (n < 1024)
|
|
37899
|
+
return `${n} B`;
|
|
37900
|
+
if (n < 1024 * 1024)
|
|
37901
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
37902
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
37903
|
+
}
|
|
37904
|
+
function renderHealthTable(h) {
|
|
37905
|
+
const sizePct = Math.round(h.rotationUsage.bytes.pct);
|
|
37906
|
+
const entriesPct = Math.round(h.rotationUsage.entries.pct);
|
|
37907
|
+
const lastWrite = h.lastWrite ? `${h.lastWrite.ts} (${h.lastWrite.success ? "success" : "failed"}${h.lastWrite.error ? `: ${h.lastWrite.error}` : ""})` : MISSING_PLACEHOLDER;
|
|
37908
|
+
const lastError = h.lastError ? `${h.lastError.ts} ${h.lastError.message}` : MISSING_PLACEHOLDER;
|
|
37909
|
+
const lastRotation = h.rotation.lastRotatedAt ? `${h.rotation.lastRotatedAt} (${h.rotation.previousFile ?? MISSING_PLACEHOLDER})` : MISSING_PLACEHOLDER;
|
|
37910
|
+
return [
|
|
37911
|
+
`Enabled: ${h.enabled}`,
|
|
37912
|
+
`File: ${h.currentFile}`,
|
|
37913
|
+
`Size: ${formatBytes(h.currentSizeBytes)} / ${formatBytes(h.rotationUsage.bytes.max)} (${sizePct}%)`,
|
|
37914
|
+
`Entries: ${h.currentEntryCount} / ${h.rotationUsage.entries.max} (${entriesPct}%)`,
|
|
37915
|
+
`Lock: ${h.lock.state}${h.lock.heldByPid !== undefined ? ` (pid ${h.lock.heldByPid})` : ""}`,
|
|
37916
|
+
`Last write: ${lastWrite}`,
|
|
37917
|
+
`Last error: ${lastError}`,
|
|
37918
|
+
`Session id: ${h.sessionId ?? MISSING_PLACEHOLDER}`,
|
|
37919
|
+
`Last rotation: ${lastRotation}`
|
|
37920
|
+
].join(`
|
|
37921
|
+
`);
|
|
37922
|
+
}
|
|
37923
|
+
function readLineFromStdinWithStderrPrompt(prompt) {
|
|
37924
|
+
return new Promise((resolve5) => {
|
|
37925
|
+
process.stderr.write(prompt);
|
|
37926
|
+
const chunks = [];
|
|
37927
|
+
let data = "";
|
|
37928
|
+
const onData = (chunk) => {
|
|
37929
|
+
chunks.push(chunk);
|
|
37930
|
+
data = Buffer.concat(chunks).toString();
|
|
37931
|
+
const lines = data.split(`
|
|
37932
|
+
`);
|
|
37933
|
+
if (lines.length > 1) {
|
|
37934
|
+
process.stdin.pause();
|
|
37935
|
+
process.stdin.removeListener("data", onData);
|
|
37936
|
+
process.stdin.removeListener("end", onEnd);
|
|
37937
|
+
resolve5((lines[0] ?? "").trim());
|
|
37938
|
+
}
|
|
37939
|
+
};
|
|
37940
|
+
const onEnd = () => {
|
|
37941
|
+
process.stdin.removeListener("data", onData);
|
|
37942
|
+
resolve5(data.trim());
|
|
37943
|
+
};
|
|
37944
|
+
process.stdin.on("data", onData);
|
|
37945
|
+
process.stdin.on("end", onEnd);
|
|
37946
|
+
process.stdin.resume();
|
|
37947
|
+
});
|
|
37948
|
+
}
|
|
37949
|
+
async function statAuditFile(file) {
|
|
37950
|
+
try {
|
|
37951
|
+
const s = await stat7(file);
|
|
37952
|
+
const entries = (await readEntries(file)).length;
|
|
37953
|
+
return { entries, size: formatBytes(s.size) };
|
|
37954
|
+
} catch {
|
|
37955
|
+
return null;
|
|
37956
|
+
}
|
|
37957
|
+
}
|
|
37958
|
+
var auditCommand = new Command("audit").description(t("audit.description"));
|
|
37959
|
+
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) => {
|
|
37960
|
+
const forAgent = options.forAgent === true;
|
|
37961
|
+
const format = forAgent ? "json" : options.format;
|
|
37962
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
37963
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
37964
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
37965
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit tail");
|
|
37966
|
+
const n = parseTailN(options.n);
|
|
37967
|
+
const configPath = resolveConfigPath(command, options);
|
|
37968
|
+
const config = await configModule.read(configPath);
|
|
37969
|
+
if (isAuditDisabled(config))
|
|
37970
|
+
emitDisabledAndExit0();
|
|
37971
|
+
const { auditDir, auditFile } = await resolveAuditPaths(configPath, config);
|
|
37972
|
+
if (options.all === true) {
|
|
37973
|
+
const conns = await discoverConnections(auditDir);
|
|
37974
|
+
const byConn = new Map;
|
|
37975
|
+
for (const c of conns) {
|
|
37976
|
+
const merged = [];
|
|
37977
|
+
for (const f of c.files) {
|
|
37978
|
+
const part = await readEntries(f);
|
|
37979
|
+
merged.push(...part);
|
|
37980
|
+
}
|
|
37981
|
+
byConn.set(c.connection, merged);
|
|
37982
|
+
}
|
|
37983
|
+
const envelopes = mergeByTimestamp(byConn).slice(-n);
|
|
37984
|
+
if (format === "json") {
|
|
37985
|
+
const payload = envelopes.map((e) => ({
|
|
37986
|
+
connection: e.connection,
|
|
37987
|
+
entry: brief ? briefify(e.entry) : e.entry
|
|
37988
|
+
}));
|
|
37989
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37990
|
+
} else if (envelopes.length === 0) {
|
|
37991
|
+
console.error(t("audit.no_entries"));
|
|
37992
|
+
} else {
|
|
37993
|
+
console.log(renderTailAllTable(envelopes));
|
|
37994
|
+
}
|
|
37995
|
+
return;
|
|
37996
|
+
}
|
|
37997
|
+
const entries = await readEntries(auditFile, { include_rotated: true });
|
|
37998
|
+
const tail = tailEntries(entries, n);
|
|
37999
|
+
if (format === "json") {
|
|
38000
|
+
const payload = brief ? tail.map(briefify) : tail;
|
|
38001
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
38002
|
+
} else if (tail.length === 0) {
|
|
38003
|
+
console.error(t("audit.no_entries"));
|
|
38004
|
+
} else {
|
|
38005
|
+
console.log(renderTailTable(tail));
|
|
38006
|
+
}
|
|
38007
|
+
});
|
|
38008
|
+
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) => {
|
|
38009
|
+
if (id && options.recoveryRef) {
|
|
38010
|
+
console.error(t("audit.show_mutex_violation"));
|
|
38011
|
+
process.exit(1);
|
|
38012
|
+
}
|
|
38013
|
+
if (!id && !options.recoveryRef) {
|
|
38014
|
+
console.error(t("audit.show_mutex_violation"));
|
|
38015
|
+
process.exit(1);
|
|
38016
|
+
}
|
|
38017
|
+
const forAgent = options.forAgent === true;
|
|
38018
|
+
const format = forAgent ? "json" : options.format;
|
|
38019
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
38020
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
38021
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
38022
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit show");
|
|
38023
|
+
const configPath = resolveConfigPath(command, options);
|
|
38024
|
+
const config = await configModule.read(configPath);
|
|
38025
|
+
if (isAuditDisabled(config))
|
|
38026
|
+
emitDisabledAndExit0();
|
|
38027
|
+
const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
|
|
38028
|
+
const all = options.all === true;
|
|
38029
|
+
if (options.recoveryRef) {
|
|
38030
|
+
const ref = String(options.recoveryRef);
|
|
38031
|
+
const matches2 = [];
|
|
38032
|
+
if (all) {
|
|
38033
|
+
const conns = await discoverConnections(auditDir);
|
|
38034
|
+
for (const c of conns) {
|
|
38035
|
+
for (const f of c.files) {
|
|
38036
|
+
for (const e of await readEntries(f)) {
|
|
38037
|
+
if (e.recovery_ref === ref)
|
|
38038
|
+
matches2.push({ connection: c.connection, entry: e });
|
|
38039
|
+
}
|
|
38040
|
+
}
|
|
38041
|
+
}
|
|
38042
|
+
} else {
|
|
38043
|
+
for (const e of await readEntries(auditFile, { include_rotated: true })) {
|
|
38044
|
+
if (e.recovery_ref === ref)
|
|
38045
|
+
matches2.push({ connection: connectionName, entry: e });
|
|
38046
|
+
}
|
|
38047
|
+
}
|
|
38048
|
+
if (matches2.length === 0) {
|
|
38049
|
+
console.error(t_vars("audit.show_recovery_no_match", { ref }));
|
|
38050
|
+
process.exit(1);
|
|
38051
|
+
}
|
|
38052
|
+
if (matches2.length > 1) {
|
|
38053
|
+
console.error(t_vars("audit.show_recovery_ambiguous", {
|
|
38054
|
+
ref,
|
|
38055
|
+
count: String(matches2.length)
|
|
38056
|
+
}));
|
|
38057
|
+
process.exit(1);
|
|
38058
|
+
}
|
|
38059
|
+
renderShowResult(matches2[0], { all, format, brief });
|
|
38060
|
+
return;
|
|
38061
|
+
}
|
|
38062
|
+
const lookup = String(id);
|
|
38063
|
+
if (lookup.length < PREFIX_MIN) {
|
|
38064
|
+
console.error(t("audit.show_prefix_too_short"));
|
|
38065
|
+
process.exit(1);
|
|
38066
|
+
}
|
|
38067
|
+
const matches = [];
|
|
38068
|
+
if (all) {
|
|
38069
|
+
const conns = await discoverConnections(auditDir);
|
|
38070
|
+
for (const c of conns) {
|
|
38071
|
+
for (const f of c.files) {
|
|
38072
|
+
for (const e of await readEntries(f)) {
|
|
38073
|
+
if (e.id === lookup || e.id.startsWith(lookup))
|
|
38074
|
+
matches.push({ connection: c.connection, entry: e });
|
|
38075
|
+
}
|
|
38076
|
+
}
|
|
38077
|
+
}
|
|
38078
|
+
} else {
|
|
38079
|
+
for (const e of await readEntries(auditFile, { include_rotated: true })) {
|
|
38080
|
+
if (e.id === lookup || e.id.startsWith(lookup))
|
|
38081
|
+
matches.push({ connection: connectionName, entry: e });
|
|
38082
|
+
}
|
|
38083
|
+
}
|
|
38084
|
+
if (matches.length === 0) {
|
|
38085
|
+
console.error(t_vars("audit.show_no_match", { prefix: lookup }));
|
|
38086
|
+
process.exit(1);
|
|
38087
|
+
}
|
|
38088
|
+
if (matches.length > 1) {
|
|
38089
|
+
console.error(t_vars("audit.show_ambiguous", {
|
|
38090
|
+
prefix: lookup,
|
|
38091
|
+
count: String(matches.length)
|
|
38092
|
+
}));
|
|
38093
|
+
process.exit(1);
|
|
38094
|
+
}
|
|
38095
|
+
renderShowResult(matches[0], { all, format, brief });
|
|
38096
|
+
});
|
|
38097
|
+
auditCommand.command("clear").description(t("audit.clear.description")).option("--yes", "Skip confirmation prompt and delete immediately", false).action(async (options, command) => {
|
|
38098
|
+
const configPath = resolveConfigPath(command, options);
|
|
38099
|
+
const config = await configModule.read(configPath);
|
|
38100
|
+
const { auditFile, connectionName } = await resolveAuditPaths(configPath, config);
|
|
38101
|
+
const rotatedFile = `${auditFile}.1`;
|
|
38102
|
+
const lockFile = `${auditFile}.lock`;
|
|
38103
|
+
const currentInfo = await statAuditFile(auditFile);
|
|
38104
|
+
const rotatedInfo = await statAuditFile(rotatedFile);
|
|
38105
|
+
if (currentInfo === null && rotatedInfo === null) {
|
|
38106
|
+
process.stderr.write(t("audit.clear.summary_nothing") + `
|
|
38107
|
+
`);
|
|
38108
|
+
process.exit(0);
|
|
38109
|
+
}
|
|
38110
|
+
if (options.yes !== true) {
|
|
38111
|
+
if (!process.stdin.isTTY) {
|
|
38112
|
+
console.error(t("audit.clear.requires_tty_or_yes"));
|
|
38113
|
+
process.exit(1);
|
|
38114
|
+
}
|
|
38115
|
+
process.stderr.write(t_vars("audit.clear.prompt_header", { conn: connectionName }) + `
|
|
38116
|
+
`);
|
|
38117
|
+
if (currentInfo) {
|
|
38118
|
+
process.stderr.write(t_vars("audit.clear.prompt_file_line", {
|
|
38119
|
+
file: auditFile,
|
|
38120
|
+
entries: String(currentInfo.entries),
|
|
38121
|
+
size: currentInfo.size
|
|
38122
|
+
}) + `
|
|
38123
|
+
`);
|
|
38124
|
+
}
|
|
38125
|
+
if (rotatedInfo) {
|
|
38126
|
+
process.stderr.write(t_vars("audit.clear.prompt_file_line", {
|
|
38127
|
+
file: rotatedFile,
|
|
38128
|
+
entries: String(rotatedInfo.entries),
|
|
38129
|
+
size: rotatedInfo.size
|
|
38130
|
+
}) + `
|
|
38131
|
+
`);
|
|
38132
|
+
}
|
|
38133
|
+
const answer = await readLineFromStdinWithStderrPrompt(t("audit.clear.prompt_continue"));
|
|
38134
|
+
const lower = answer.toLowerCase();
|
|
38135
|
+
const proceed = lower === "y" || lower === "yes";
|
|
38136
|
+
if (!proceed) {
|
|
38137
|
+
process.stderr.write(t("audit.clear.summary_nothing") + `
|
|
38138
|
+
`);
|
|
38139
|
+
process.exit(0);
|
|
38140
|
+
}
|
|
38141
|
+
}
|
|
38142
|
+
let cleared = 0;
|
|
38143
|
+
try {
|
|
38144
|
+
if (currentInfo)
|
|
38145
|
+
cleared += currentInfo.entries;
|
|
38146
|
+
if (rotatedInfo)
|
|
38147
|
+
cleared += rotatedInfo.entries;
|
|
38148
|
+
await rm3(auditFile, { force: true });
|
|
38149
|
+
await rm3(rotatedFile, { force: true });
|
|
38150
|
+
await rm3(lockFile, { force: true });
|
|
38151
|
+
} catch (err) {
|
|
38152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38153
|
+
process.stderr.write(t_vars("audit.clear.summary_failed", { message: msg }) + `
|
|
38154
|
+
`);
|
|
38155
|
+
process.exit(1);
|
|
38156
|
+
}
|
|
38157
|
+
process.stderr.write(t_vars("audit.clear.summary_cleared", {
|
|
38158
|
+
count: String(cleared),
|
|
38159
|
+
conn: connectionName
|
|
38160
|
+
}) + `
|
|
38161
|
+
`);
|
|
38162
|
+
process.exit(0);
|
|
38163
|
+
});
|
|
38164
|
+
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) => {
|
|
38165
|
+
const forAgent = options.forAgent === true;
|
|
38166
|
+
const format = forAgent ? "json" : options.format;
|
|
38167
|
+
const briefSource = command.getOptionValueSource("brief");
|
|
38168
|
+
const briefExplicit = briefSource !== undefined && briefSource !== "default";
|
|
38169
|
+
const brief = briefExplicit ? options.brief === true : forAgent;
|
|
38170
|
+
validateFormat(format, ALLOWED_FORMATS14, "audit health");
|
|
38171
|
+
const configPath = resolveConfigPath(command, options);
|
|
38172
|
+
const config = await configModule.read(configPath);
|
|
38173
|
+
const logger = await getAuditLogger(config, configPath);
|
|
38174
|
+
const health = logger.getHealth();
|
|
38175
|
+
if (format === "json") {
|
|
38176
|
+
const payload = brief ? briefifyHealth(health) : health;
|
|
38177
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
38178
|
+
} else if (brief) {
|
|
38179
|
+
const sizePct = Math.round(health.rotationUsage.bytes.pct);
|
|
38180
|
+
const entriesPct = Math.round(health.rotationUsage.entries.pct);
|
|
38181
|
+
const lastWriteLine = health.lastWrite ? `${health.lastWrite.ts} (${health.lastWrite.success ? "success" : "failed"})` : MISSING_PLACEHOLDER;
|
|
38182
|
+
console.log(`Enabled: ${health.enabled}`);
|
|
38183
|
+
console.log(`Last write: ${lastWriteLine}`);
|
|
38184
|
+
console.log(`Rotation usage: ${sizePct}% bytes, ${entriesPct}% entries`);
|
|
38185
|
+
} else {
|
|
38186
|
+
console.log(renderHealthTable(health));
|
|
38187
|
+
}
|
|
38188
|
+
});
|
|
38189
|
+
|
|
35872
38190
|
// src/commands/doctor.ts
|
|
35873
38191
|
init_colors();
|
|
35874
38192
|
init_config();
|
|
@@ -35880,7 +38198,8 @@ init_config_v2();
|
|
|
35880
38198
|
init_config_binding();
|
|
35881
38199
|
init_schema_path();
|
|
35882
38200
|
init_config();
|
|
35883
|
-
|
|
38201
|
+
init_integration_helper();
|
|
38202
|
+
import { join as join26 } from "path";
|
|
35884
38203
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
35885
38204
|
function requireSqlConnection9(connection) {
|
|
35886
38205
|
if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
|
|
@@ -35888,7 +38207,7 @@ function requireSqlConnection9(connection) {
|
|
|
35888
38207
|
}
|
|
35889
38208
|
return connection;
|
|
35890
38209
|
}
|
|
35891
|
-
var
|
|
38210
|
+
var ALLOWED_FORMATS15 = ["text", "json"];
|
|
35892
38211
|
var SENSITIVE_PATTERNS = [
|
|
35893
38212
|
"password",
|
|
35894
38213
|
"passwd",
|
|
@@ -35960,7 +38279,7 @@ var runDoctorChecks = {
|
|
|
35960
38279
|
}
|
|
35961
38280
|
},
|
|
35962
38281
|
async checkConfigExists(configPath, existsFn) {
|
|
35963
|
-
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(
|
|
38282
|
+
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join26(configPath, "config.json")).exists();
|
|
35964
38283
|
return {
|
|
35965
38284
|
group: "Configuration",
|
|
35966
38285
|
label: "Config exists",
|
|
@@ -36117,7 +38436,7 @@ var runDoctorChecks = {
|
|
|
36117
38436
|
async checkV2Config(configPath) {
|
|
36118
38437
|
const results = [];
|
|
36119
38438
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
36120
|
-
const configFile = Bun.file(
|
|
38439
|
+
const configFile = Bun.file(join26(storagePath, "config.json"));
|
|
36121
38440
|
if (!await configFile.exists())
|
|
36122
38441
|
return results;
|
|
36123
38442
|
let raw;
|
|
@@ -36157,7 +38476,7 @@ var runDoctorChecks = {
|
|
|
36157
38476
|
}
|
|
36158
38477
|
for (const [name, conn] of Object.entries(config.connections)) {
|
|
36159
38478
|
if (conn.envFile) {
|
|
36160
|
-
const envPath =
|
|
38479
|
+
const envPath = join26(storagePath, conn.envFile);
|
|
36161
38480
|
const exists = await Bun.file(envPath).exists();
|
|
36162
38481
|
results.push({
|
|
36163
38482
|
group: "Configuration",
|
|
@@ -36302,7 +38621,7 @@ async function collectElasticsearchDoctorResults(config) {
|
|
|
36302
38621
|
return results;
|
|
36303
38622
|
}
|
|
36304
38623
|
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,
|
|
38624
|
+
validateFormat(options.format, ALLOWED_FORMATS15, "doctor");
|
|
36306
38625
|
const logger = getLogger();
|
|
36307
38626
|
const results = [];
|
|
36308
38627
|
const configPath = resolveConfigPath(doctorCommand);
|
|
@@ -36376,7 +38695,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36376
38695
|
}
|
|
36377
38696
|
try {
|
|
36378
38697
|
const schemaConnName = await getSchemaIsolationConnectionName(configPath);
|
|
36379
|
-
const indexPath =
|
|
38698
|
+
const indexPath = join26(resolveSchemaPath(storagePath, schemaConnName), "index.json");
|
|
36380
38699
|
const indexFile = Bun.file(indexPath);
|
|
36381
38700
|
let indexParsed = null;
|
|
36382
38701
|
if (await indexFile.exists()) {
|
|
@@ -36407,6 +38726,17 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36407
38726
|
}
|
|
36408
38727
|
}
|
|
36409
38728
|
const hasError = results.some((r) => r.status === "error");
|
|
38729
|
+
try {
|
|
38730
|
+
const config = await configModule.read(configPath);
|
|
38731
|
+
await writeAuditEntry(config, "doctor", options, {
|
|
38732
|
+
success: !hasError,
|
|
38733
|
+
target: "*",
|
|
38734
|
+
metadata: {
|
|
38735
|
+
error_count: results.filter((r) => r.status === "error").length,
|
|
38736
|
+
warn_count: results.filter((r) => r.status === "warn").length
|
|
38737
|
+
}
|
|
38738
|
+
});
|
|
38739
|
+
} catch {}
|
|
36410
38740
|
if (options.format === "json") {
|
|
36411
38741
|
console.log(JSON.stringify({ results, hasError }, null, 2));
|
|
36412
38742
|
} else {
|
|
@@ -36419,7 +38749,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
36419
38749
|
|
|
36420
38750
|
// src/commands/completion.ts
|
|
36421
38751
|
init_colors();
|
|
36422
|
-
import { join as
|
|
38752
|
+
import { join as join27 } from "path";
|
|
36423
38753
|
import { homedir as homedir3 } from "os";
|
|
36424
38754
|
function extractCommands(program2) {
|
|
36425
38755
|
return program2.commands.map((cmd) => ({
|
|
@@ -36521,11 +38851,11 @@ function getInstallPath2(shell) {
|
|
|
36521
38851
|
const home = homedir3();
|
|
36522
38852
|
switch (shell) {
|
|
36523
38853
|
case "bash":
|
|
36524
|
-
return
|
|
38854
|
+
return join27(home, ".bashrc");
|
|
36525
38855
|
case "zsh":
|
|
36526
|
-
return
|
|
38856
|
+
return join27(home, ".zshrc");
|
|
36527
38857
|
case "fish":
|
|
36528
|
-
return
|
|
38858
|
+
return join27(home, ".config", "fish", "completions", "dbcli.fish");
|
|
36529
38859
|
default:
|
|
36530
38860
|
throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
36531
38861
|
}
|
|
@@ -36545,7 +38875,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
|
|
|
36545
38875
|
async function installCompletion(shell, script) {
|
|
36546
38876
|
const targetPath = getInstallPath2(shell);
|
|
36547
38877
|
if (shell === "fish") {
|
|
36548
|
-
const dir =
|
|
38878
|
+
const dir = join27(homedir3(), ".config", "fish", "completions");
|
|
36549
38879
|
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
36550
38880
|
await Bun.file(targetPath).write(script);
|
|
36551
38881
|
console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
|
|
@@ -36778,7 +39108,7 @@ ${t("upgrade.failed")}`));
|
|
|
36778
39108
|
init_config();
|
|
36779
39109
|
init_adapters();
|
|
36780
39110
|
import { createInterface as createInterface2 } from "readline";
|
|
36781
|
-
import { join as
|
|
39111
|
+
import { join as join28 } from "path";
|
|
36782
39112
|
import { homedir as homedir4 } from "os";
|
|
36783
39113
|
|
|
36784
39114
|
// src/core/repl/types.ts
|
|
@@ -37123,7 +39453,7 @@ function isKnownCommand(name) {
|
|
|
37123
39453
|
|
|
37124
39454
|
// src/core/repl/history-manager.ts
|
|
37125
39455
|
import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
37126
|
-
import { dirname as
|
|
39456
|
+
import { dirname as dirname10 } from "path";
|
|
37127
39457
|
var DEFAULT_MAX = 1000;
|
|
37128
39458
|
|
|
37129
39459
|
class HistoryManager {
|
|
@@ -37151,7 +39481,7 @@ class HistoryManager {
|
|
|
37151
39481
|
return this.entries;
|
|
37152
39482
|
}
|
|
37153
39483
|
async save() {
|
|
37154
|
-
const dir =
|
|
39484
|
+
const dir = dirname10(this.filePath);
|
|
37155
39485
|
if (!existsSync(dir)) {
|
|
37156
39486
|
mkdirSync2(dir, { recursive: true });
|
|
37157
39487
|
}
|
|
@@ -37509,7 +39839,7 @@ function requireSqlConnection10(connection) {
|
|
|
37509
39839
|
}
|
|
37510
39840
|
return connection;
|
|
37511
39841
|
}
|
|
37512
|
-
var HISTORY_PATH =
|
|
39842
|
+
var HISTORY_PATH = join28(homedir4(), ".dbcli_history");
|
|
37513
39843
|
var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
|
|
37514
39844
|
async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
|
|
37515
39845
|
const columnsByTable = {};
|
|
@@ -38496,7 +40826,7 @@ init_config();
|
|
|
38496
40826
|
init_errors();
|
|
38497
40827
|
init_message_loader();
|
|
38498
40828
|
init_config_binding();
|
|
38499
|
-
import { join as
|
|
40829
|
+
import { join as join29 } from "path";
|
|
38500
40830
|
async function switchDefault(configPath, name, config) {
|
|
38501
40831
|
if (!config.connections[name]) {
|
|
38502
40832
|
const available = Object.keys(config.connections).join(", ");
|
|
@@ -38519,7 +40849,7 @@ function listConnectionsForDisplay(config) {
|
|
|
38519
40849
|
}
|
|
38520
40850
|
async function ensureV2Config(configPath) {
|
|
38521
40851
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
38522
|
-
const configFile = Bun.file(
|
|
40852
|
+
const configFile = Bun.file(join29(storagePath, "config.json"));
|
|
38523
40853
|
const legacyFile = Bun.file(configPath);
|
|
38524
40854
|
if (!await configFile.exists() && !await legacyFile.exists()) {
|
|
38525
40855
|
throw new ConfigError(t("init.config_not_found"));
|
|
@@ -38544,7 +40874,8 @@ async function ensureV2Config(configPath) {
|
|
|
38544
40874
|
schema: v1Config.schema || {},
|
|
38545
40875
|
schemas: { default: v1Config.schema || {} },
|
|
38546
40876
|
metadata: v1Config.metadata || { version: "1.0" },
|
|
38547
|
-
blacklist: v1Config.blacklist || { tables: [], columns: {} }
|
|
40877
|
+
blacklist: v1Config.blacklist || { tables: [], columns: {} },
|
|
40878
|
+
audit: v1Config.audit
|
|
38548
40879
|
};
|
|
38549
40880
|
}
|
|
38550
40881
|
return readV2Config(storagePath);
|
|
@@ -38582,7 +40913,7 @@ var useCommand = new Command("use").description("Switch or display the default d
|
|
|
38582
40913
|
|
|
38583
40914
|
// src/cli.ts
|
|
38584
40915
|
init_config();
|
|
38585
|
-
import { join as
|
|
40916
|
+
import { join as join30 } from "path";
|
|
38586
40917
|
var _bgVersionCheckResult;
|
|
38587
40918
|
function shouldSkipBackgroundChecks() {
|
|
38588
40919
|
return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
|
|
@@ -38611,7 +40942,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
|
38611
40942
|
try {
|
|
38612
40943
|
let cache = null;
|
|
38613
40944
|
try {
|
|
38614
|
-
const cacheFile = Bun.file(
|
|
40945
|
+
const cacheFile = Bun.file(join30(configPath, "version-check.json"));
|
|
38615
40946
|
if (await cacheFile.exists()) {
|
|
38616
40947
|
cache = await cacheFile.json();
|
|
38617
40948
|
}
|
|
@@ -38659,7 +40990,7 @@ program2.command("plan <sql>").description("Analyze SQL risk without executing")
|
|
|
38659
40990
|
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
40991
|
await qCommand(name, options, command);
|
|
38661
40992
|
});
|
|
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) => {
|
|
40993
|
+
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
40994
|
try {
|
|
38664
40995
|
await insertCommand(table, options, command);
|
|
38665
40996
|
} catch (error) {
|
|
@@ -38671,7 +41002,7 @@ program2.command("insert <table>").description(t("insert.description")).option("
|
|
|
38671
41002
|
process.exit(1);
|
|
38672
41003
|
}
|
|
38673
41004
|
});
|
|
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) => {
|
|
41005
|
+
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
41006
|
try {
|
|
38676
41007
|
await updateCommand(table, options, command);
|
|
38677
41008
|
} catch (error) {
|
|
@@ -38683,7 +41014,7 @@ program2.command("update <table>").description(t("update.description")).option("
|
|
|
38683
41014
|
process.exit(1);
|
|
38684
41015
|
}
|
|
38685
41016
|
});
|
|
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) => {
|
|
41017
|
+
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
41018
|
try {
|
|
38688
41019
|
await deleteCommand(table, options, command);
|
|
38689
41020
|
} catch (error) {
|
|
@@ -38721,6 +41052,7 @@ program2.addCommand(reportCommand);
|
|
|
38721
41052
|
program2.addCommand(guideCommand);
|
|
38722
41053
|
program2.addCommand(recoveryCommand);
|
|
38723
41054
|
program2.addCommand(recoverCommand);
|
|
41055
|
+
program2.addCommand(auditCommand);
|
|
38724
41056
|
program2.addCommand(doctorCommand);
|
|
38725
41057
|
program2.addCommand(completionCommand);
|
|
38726
41058
|
program2.addCommand(upgradeCommand);
|