@carllee1983/dbcli 1.44.1 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "@carllee1983/dbcli",
55
- version: "1.44.1",
55
+ version: "1.45.0",
56
56
  description: "Database CLI for AI agents",
57
57
  type: "module",
58
58
  publishConfig: {
@@ -125,6 +125,7 @@ var init_package = __esm(() => {
125
125
  "test:integration": "bun test tests/integration",
126
126
  "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",
127
127
  "docs:check": "bun run scripts/check-user-docs.ts",
128
+ "contract:check": "bun run scripts/check-cli-contract.ts",
128
129
  "skill:check": "bun run scripts/check-skill-parity.ts",
129
130
  "platform:check": "bun run scripts/check-platform-parity.ts",
130
131
  "agent-core:check": "bun run scripts/check-agent-core-purity.ts",
@@ -2409,6 +2410,308 @@ var init_version_check = __esm(() => {
2409
2410
  STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
2410
2411
  });
2411
2412
 
2413
+ // src/agent-core/errors.ts
2414
+ var ConfigError;
2415
+ var init_errors = __esm(() => {
2416
+ ConfigError = class ConfigError extends Error {
2417
+ constructor(message) {
2418
+ super(message);
2419
+ this.name = "ConfigError";
2420
+ if (Error.captureStackTrace)
2421
+ Error.captureStackTrace(this, ConfigError);
2422
+ }
2423
+ };
2424
+ });
2425
+
2426
+ // src/utils/errors.ts
2427
+ var EnvParseError;
2428
+ var init_errors2 = __esm(() => {
2429
+ init_errors();
2430
+ EnvParseError = class EnvParseError extends Error {
2431
+ constructor(message) {
2432
+ super(message);
2433
+ this.name = "EnvParseError";
2434
+ if (Error.captureStackTrace) {
2435
+ Error.captureStackTrace(this, EnvParseError);
2436
+ }
2437
+ }
2438
+ };
2439
+ });
2440
+
2441
+ // src/core/config-mutation-guard.ts
2442
+ function assertConfigMutationApproved() {
2443
+ if (process.env.DBCLI_AGENT_MODE !== "1")
2444
+ return;
2445
+ throw new ConfigError("Agent mode blocks configuration, permission, and credential changes. A human or administrator must run the approved workflow outside agent mode.");
2446
+ }
2447
+ var init_config_mutation_guard = __esm(() => {
2448
+ init_errors2();
2449
+ });
2450
+
2451
+ // src/core/config-integrity.ts
2452
+ import { createHash, randomUUID } from "crypto";
2453
+ import { chmod, lstat, mkdir, rename, unlink } from "fs/promises";
2454
+ import { dirname, join, resolve } from "path";
2455
+ function integrityPath(storagePath, recordName = INTEGRITY_FILE) {
2456
+ return join(storagePath, recordName);
2457
+ }
2458
+ function hash(content) {
2459
+ return createHash("sha256").update(content).digest("hex");
2460
+ }
2461
+ function protectedPath(storagePath, protectedFileName) {
2462
+ return resolve(join(storagePath, protectedFileName));
2463
+ }
2464
+ function anchorPathFor(protectedFilePath) {
2465
+ const anchorDirectory = process.env.DBCLI_CONFIG_INTEGRITY_ANCHOR_DIR?.trim();
2466
+ if (!anchorDirectory)
2467
+ return null;
2468
+ const key = createHash("sha256").update(protectedFilePath).digest("hex");
2469
+ return join(resolve(anchorDirectory), `${key}.json`);
2470
+ }
2471
+ async function bestEffortSecureMode(path, mode) {
2472
+ try {
2473
+ await chmod(path, mode);
2474
+ } catch {}
2475
+ }
2476
+ async function assertAgentReadableFile(path, description = "config") {
2477
+ if (process.env.DBCLI_AGENT_MODE !== "1")
2478
+ return;
2479
+ try {
2480
+ const fileStat = await lstat(path);
2481
+ if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
2482
+ throw new ConfigError(`Agent mode refuses a non-regular ${description} file: ${path}. Run the human/admin setup workflow to provision it.`);
2483
+ }
2484
+ if ((fileStat.mode & 18) !== 0) {
2485
+ throw new ConfigError(`Agent mode refuses a group/world-writable config: ${path}. Run the human/admin setup workflow to secure it.`);
2486
+ }
2487
+ } catch (error) {
2488
+ if (error instanceof ConfigError)
2489
+ throw error;
2490
+ throw new ConfigError(`Agent mode refuses an unreadable ${description} file: ${path}`);
2491
+ }
2492
+ }
2493
+ async function writeProtectedFileWithIntegrity(storagePath, protectedFileName, protectedContent, recordName) {
2494
+ await mkdir(storagePath, { recursive: true });
2495
+ const protectedFilePath = protectedPath(storagePath, protectedFileName);
2496
+ const localPath = integrityPath(storagePath, recordName);
2497
+ const anchorPath = anchorPathFor(protectedFilePath);
2498
+ const record = {
2499
+ version: 1,
2500
+ configSha256: hash(protectedContent),
2501
+ updatedAt: new Date().toISOString(),
2502
+ targetPath: protectedFilePath
2503
+ };
2504
+ const recordContent = JSON.stringify(record, null, 2);
2505
+ const suffix = `${process.pid}-${randomUUID()}`;
2506
+ const protectedTemporary = `${protectedFilePath}.tmp-${suffix}`;
2507
+ const localTemporary = `${localPath}.tmp-${suffix}`;
2508
+ const anchorTemporary = anchorPath ? `${anchorPath}.tmp-${suffix}` : null;
2509
+ const published = [];
2510
+ try {
2511
+ const [protectedSnapshot, localSnapshot, anchorSnapshot] = await Promise.all([
2512
+ snapshot(protectedFilePath),
2513
+ snapshot(localPath),
2514
+ anchorPath ? snapshot(anchorPath) : Promise.resolve(null)
2515
+ ]);
2516
+ await Bun.write(protectedTemporary, protectedContent);
2517
+ await Bun.write(localTemporary, recordContent);
2518
+ if (anchorPath && anchorTemporary) {
2519
+ await mkdir(dirname(anchorPath), { recursive: true });
2520
+ await Bun.write(anchorTemporary, recordContent);
2521
+ }
2522
+ const publish = async (temporary, previous) => {
2523
+ await rename(temporary, previous.path);
2524
+ published.push(previous);
2525
+ };
2526
+ if (anchorTemporary && anchorPath) {
2527
+ await publish(anchorTemporary, anchorSnapshot ?? { path: anchorPath, content: null });
2528
+ }
2529
+ await publish(localTemporary, localSnapshot);
2530
+ await publish(protectedTemporary, protectedSnapshot);
2531
+ } catch (error) {
2532
+ for (const publishedFile of published.reverse()) {
2533
+ await restore(publishedFile).catch(() => {
2534
+ return;
2535
+ });
2536
+ }
2537
+ throw error;
2538
+ } finally {
2539
+ await unlink(protectedTemporary).catch(() => {
2540
+ return;
2541
+ });
2542
+ await unlink(localTemporary).catch(() => {
2543
+ return;
2544
+ });
2545
+ if (anchorTemporary)
2546
+ await unlink(anchorTemporary).catch(() => {
2547
+ return;
2548
+ });
2549
+ }
2550
+ if (anchorPath) {
2551
+ await bestEffortSecureMode(dirname(anchorPath), 448);
2552
+ await bestEffortSecureMode(anchorPath, 384);
2553
+ }
2554
+ await bestEffortSecureMode(storagePath, 448);
2555
+ await bestEffortSecureMode(protectedFilePath, 384);
2556
+ await bestEffortSecureMode(localPath, 384);
2557
+ }
2558
+ async function snapshot(path) {
2559
+ const file = Bun.file(path);
2560
+ return { path, content: await file.exists() ? await file.text() : null };
2561
+ }
2562
+ async function restore(snapshot2) {
2563
+ if (snapshot2.content === null) {
2564
+ await unlink(snapshot2.path).catch(() => {
2565
+ return;
2566
+ });
2567
+ return;
2568
+ }
2569
+ const temporary = `${snapshot2.path}.rollback-${process.pid}-${randomUUID()}`;
2570
+ await Bun.write(temporary, snapshot2.content);
2571
+ await rename(temporary, snapshot2.path);
2572
+ }
2573
+ async function writeConfigWithIntegrity(storagePath, configContent) {
2574
+ await writeProtectedFileWithIntegrity(storagePath, "config.json", configContent, INTEGRITY_FILE);
2575
+ }
2576
+ async function writeBindingWithIntegrity(projectPath, bindingContent) {
2577
+ await writeProtectedFileWithIntegrity(projectPath, "config.json", bindingContent, ".binding-integrity.json");
2578
+ }
2579
+ async function assertIntegrityRecord(storagePath, configContent, recordName, protectedFileName, description, options = {}) {
2580
+ if (process.env.DBCLI_AGENT_MODE !== "1")
2581
+ return;
2582
+ const configPath = protectedPath(storagePath, protectedFileName);
2583
+ await assertAgentReadableFile(configPath, description);
2584
+ const verifyRecord = async (path, label) => {
2585
+ const file = Bun.file(path);
2586
+ if (!await file.exists()) {
2587
+ throw new ConfigError(`Agent mode refuses a missing ${label}: ${path}`);
2588
+ }
2589
+ let record;
2590
+ try {
2591
+ await assertAgentReadableFile(path, label);
2592
+ record = await file.json();
2593
+ } catch (error) {
2594
+ if (error instanceof ConfigError)
2595
+ throw error;
2596
+ throw new ConfigError(`Agent mode refuses an unreadable ${label}: ${path}`);
2597
+ }
2598
+ if (record.version !== 1 || typeof record.configSha256 !== "string" || record.configSha256 !== hash(configContent) || record.targetPath !== undefined && record.targetPath !== configPath) {
2599
+ throw new ConfigError(`Agent mode detected direct ${description} tampering or an out-of-band edit: ${configPath}`);
2600
+ }
2601
+ };
2602
+ const anchorPath = anchorPathFor(configPath);
2603
+ if (anchorPath)
2604
+ await verifyRecord(anchorPath, "detached config integrity anchor");
2605
+ const localPath = integrityPath(storagePath, recordName);
2606
+ const localExists = await Bun.file(localPath).exists();
2607
+ if (localExists) {
2608
+ await verifyRecord(localPath, "config integrity record");
2609
+ } else if (options.requireRecord && !anchorPath) {
2610
+ throw new ConfigError(`Agent mode refuses a missing config integrity record: ${localPath}`);
2611
+ }
2612
+ }
2613
+ async function assertConfigIntegrity(storagePath, configContent, options) {
2614
+ await assertIntegrityRecord(storagePath, configContent, INTEGRITY_FILE, "config.json", "config", options);
2615
+ }
2616
+ async function assertBindingIntegrity(projectPath, bindingContent, options) {
2617
+ await assertIntegrityRecord(projectPath, bindingContent, ".binding-integrity.json", "config.json", "project binding", options);
2618
+ }
2619
+ var INTEGRITY_FILE = ".config-integrity.json";
2620
+ var init_config_integrity = __esm(() => {
2621
+ init_errors2();
2622
+ });
2623
+
2624
+ // src/core/config-binding.ts
2625
+ import { createHash as createHash2 } from "crypto";
2626
+ import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
2627
+ import { homedir } from "os";
2628
+ import { basename, join as join2, resolve as resolve2 } from "path";
2629
+ function getDbcliConfigHome() {
2630
+ const configuredHome = process.env.DBCLI_CONFIG_HOME?.trim();
2631
+ if (configuredHome)
2632
+ return configuredHome;
2633
+ return join2(process.env.HOME?.trim() || homedir(), ".config", "dbcli");
2634
+ }
2635
+ function isProjectConfigBinding(raw) {
2636
+ if (typeof raw !== "object" || raw === null)
2637
+ return false;
2638
+ const candidate = raw;
2639
+ return candidate.version === 3 && typeof candidate.binding === "object" && candidate.binding !== null && candidate.binding.type === "home-storage" && typeof candidate.binding.storagePath === "string" && candidate.binding.storagePath.length > 0 && typeof candidate.binding.projectPath === "string" && candidate.binding.projectPath.length > 0 && typeof candidate.binding.createdAt === "string" && candidate.binding.createdAt.length > 0;
2640
+ }
2641
+ function getDbcliHomeRoot() {
2642
+ return getDbcliConfigHome();
2643
+ }
2644
+ function getGlobalConfigPath() {
2645
+ return getDbcliHomeRoot();
2646
+ }
2647
+ function isGlobalConfigPath(path) {
2648
+ return resolve2(path) === resolve2(getGlobalConfigPath());
2649
+ }
2650
+ function getProjectStoragePath(projectPath) {
2651
+ const normalizedProjectPath = resolve2(projectPath);
2652
+ const projectName = basename(normalizedProjectPath) || "project";
2653
+ const hash2 = createHash2("sha1").update(normalizedProjectPath).digest("hex").slice(0, 12);
2654
+ return join2(getDbcliHomeRoot(), "projects", `${projectName}-${hash2}`);
2655
+ }
2656
+ async function readProjectBinding(projectPath) {
2657
+ const configFile = Bun.file(join2(projectPath, BINDING_FILE_NAME));
2658
+ if (!await configFile.exists())
2659
+ return null;
2660
+ try {
2661
+ await assertAgentReadableFile(join2(projectPath, BINDING_FILE_NAME), "project binding");
2662
+ const content = await configFile.text();
2663
+ const raw = JSON.parse(content);
2664
+ if (!isProjectConfigBinding(raw))
2665
+ return null;
2666
+ await assertBindingIntegrity(projectPath, content, { requireRecord: true });
2667
+ return raw;
2668
+ } catch (error) {
2669
+ if (error instanceof ConfigError)
2670
+ throw error;
2671
+ return null;
2672
+ }
2673
+ }
2674
+ async function resolveConfigStoragePath(path) {
2675
+ const binding = await readProjectBinding(path);
2676
+ return binding?.binding.storagePath ?? path;
2677
+ }
2678
+ async function writeProjectBinding(projectPath, storagePath = getProjectStoragePath(projectPath)) {
2679
+ assertConfigMutationApproved();
2680
+ const binding = {
2681
+ version: 3,
2682
+ binding: {
2683
+ type: "home-storage",
2684
+ storagePath,
2685
+ projectPath: resolve2(projectPath),
2686
+ createdAt: new Date().toISOString()
2687
+ }
2688
+ };
2689
+ await mkdir2(projectPath, { recursive: true });
2690
+ await mkdir2(storagePath, { recursive: true });
2691
+ const content = JSON.stringify(binding, null, 2);
2692
+ await writeBindingWithIntegrity(projectPath, content);
2693
+ return binding;
2694
+ }
2695
+ async function migrateLegacyProjectEnvLocal(projectPath, storagePath = getProjectStoragePath(projectPath)) {
2696
+ assertConfigMutationApproved();
2697
+ const projectEnvPath = join2(projectPath, ".env.local");
2698
+ const projectEnvFile = Bun.file(projectEnvPath);
2699
+ if (!await projectEnvFile.exists())
2700
+ return;
2701
+ const storageEnvPath = join2(storagePath, ".env.local");
2702
+ await mkdir2(storagePath, { recursive: true });
2703
+ if (!await Bun.file(storageEnvPath).exists()) {
2704
+ await Bun.file(storageEnvPath).write(await projectEnvFile.text());
2705
+ }
2706
+ await unlink2(projectEnvPath);
2707
+ }
2708
+ var BINDING_FILE_NAME = "config.json";
2709
+ var init_config_binding = __esm(() => {
2710
+ init_config_mutation_guard();
2711
+ init_config_integrity();
2712
+ init_errors2();
2713
+ });
2714
+
2412
2715
  // src/utils/config-path.ts
2413
2716
  function resolveConfigPath(command, options, fallback = ".dbcli") {
2414
2717
  for (let current = command;current; current = current.parent ?? undefined) {
@@ -2420,11 +2723,25 @@ function resolveConfigPath(command, options, fallback = ".dbcli") {
2420
2723
  }
2421
2724
  }
2422
2725
  }
2726
+ if (typeof options?.config === "string" && options.config.length > 0 && options.config !== fallback) {
2727
+ return options.config;
2728
+ }
2729
+ for (let current = command;current; current = current.parent ?? undefined) {
2730
+ const source = current.getOptionValueSource("global");
2731
+ if (source && source !== "default" && current.opts().global === true) {
2732
+ return getGlobalConfigPath();
2733
+ }
2734
+ }
2735
+ if (options?.global === true)
2736
+ return getGlobalConfigPath();
2423
2737
  if (typeof options?.config === "string" && options.config.length > 0) {
2424
2738
  return options.config;
2425
2739
  }
2426
2740
  return fallback;
2427
2741
  }
2742
+ var init_config_path = __esm(() => {
2743
+ init_config_binding();
2744
+ });
2428
2745
 
2429
2746
  // resources/lang/en/messages.json
2430
2747
  var messages_default;
@@ -2445,6 +2762,7 @@ var init_messages = __esm(() => {
2445
2762
  connection_success: "\u2713 Database connection successful",
2446
2763
  connection_failed: "\u2717 Database connection failed",
2447
2764
  config_saved: "Configuration saved to .dbcli",
2765
+ config_saved_global: "Global configuration saved to {path}",
2448
2766
  config_exists_overwrite: "Configuration file .dbcli already exists. Overwrite? (y/n): ",
2449
2767
  cancelled: "Cancelled. Configuration not changed.",
2450
2768
  skip_test_env_ref: "Skipping connection test in env-ref mode",
@@ -2728,6 +3046,7 @@ var init_messages2 = __esm(() => {
2728
3046
  connection_success: "\u2713 \u8CC7\u6599\u5EAB\u9023\u63A5\u6210\u529F",
2729
3047
  connection_failed: "\u2717 \u8CC7\u6599\u5EAB\u9023\u63A5\u5931\u6557",
2730
3048
  config_saved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 .dbcli",
3049
+ config_saved_global: "\u5168\u57DF\u8A2D\u5B9A\u5DF2\u5132\u5B58\u81F3 {path}",
2731
3050
  config_exists_overwrite: "\u914D\u7F6E\u6A94\u6848 .dbcli \u5DF2\u5B58\u5728\u3002\u662F\u5426\u8986\u84CB\uFF1F (y/n)\uFF1A",
2732
3051
  cancelled: "\u5DF2\u53D6\u6D88\u3002\u914D\u7F6E\u672A\u66F4\u6539\u3002",
2733
3052
  skip_test_env_ref: "\u8DF3\u904E\u9023\u7DDA\u6E2C\u8A66\uFF08\u74B0\u5883\u8B8A\u6578\u53C3\u7167\u6A21\u5F0F\uFF09",
@@ -3516,7 +3835,7 @@ function getErrorMap() {
3516
3835
  return overrideErrorMap;
3517
3836
  }
3518
3837
  var overrideErrorMap;
3519
- var init_errors = __esm(() => {
3838
+ var init_errors3 = __esm(() => {
3520
3839
  init_en();
3521
3840
  overrideErrorMap = en_default;
3522
3841
  });
@@ -3618,7 +3937,7 @@ var makeIssue = (params) => {
3618
3937
  };
3619
3938
  }, EMPTY_PATH, INVALID, DIRTY = (value) => ({ status: "dirty", value }), OK = (value) => ({ status: "valid", value }), isAborted = (x) => x.status === "aborted", isDirty = (x) => x.status === "dirty", isValid = (x) => x.status === "valid", isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
3620
3939
  var init_parseUtil = __esm(() => {
3621
- init_errors();
3940
+ init_errors3();
3622
3941
  init_en();
3623
3942
  EMPTY_PATH = [];
3624
3943
  INVALID = Object.freeze({
@@ -4186,7 +4505,7 @@ var handleResult = (ctx, result) => {
4186
4505
  }) => custom((data) => data instanceof cls, params), stringType, numberType, nanType, bigIntType, booleanType, dateType, symbolType, undefinedType, nullType, anyType, unknownType, neverType, voidType, arrayType, objectType, strictObjectType, unionType, discriminatedUnionType, intersectionType, tupleType, recordType, mapType, setType, functionType, lazyType, literalType, enumType, nativeEnumType, promiseType, effectsType, optionalType, nullableType, preprocessType, pipelineType, ostring = () => stringType().optional(), onumber = () => numberType().optional(), oboolean = () => booleanType().optional(), coerce, NEVER;
4187
4506
  var init_types = __esm(() => {
4188
4507
  init_ZodError();
4189
- init_errors();
4508
+ init_errors3();
4190
4509
  init_errorUtil();
4191
4510
  init_parseUtil();
4192
4511
  init_util();
@@ -7101,7 +7420,7 @@ __export(exports_external, {
7101
7420
  BRAND: () => BRAND
7102
7421
  });
7103
7422
  var init_external = __esm(() => {
7104
- init_errors();
7423
+ init_errors3();
7105
7424
  init_parseUtil();
7106
7425
  init_typeAliases();
7107
7426
  init_util();
@@ -7122,7 +7441,7 @@ function validateFormat(value, allowedFormats, commandName) {
7122
7441
  throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
7123
7442
  }
7124
7443
  }
7125
- var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, MetadataSchema, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
7444
+ var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, EnvironmentLabelSchema, MetadataSchema, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
7126
7445
  var init_validation = __esm(() => {
7127
7446
  init_zod();
7128
7447
  EnvRefSchema = exports_external.object({
@@ -7178,6 +7497,7 @@ var init_validation = __esm(() => {
7178
7497
  ElasticsearchConnectionConfigSchema
7179
7498
  ]);
7180
7499
  PermissionSchema = exports_external.enum(["query-only", "read-write", "data-admin", "admin"]).default("query-only");
7500
+ EnvironmentLabelSchema = exports_external.string().trim().transform((value) => value || undefined).optional();
7181
7501
  MetadataSchema = exports_external.object({
7182
7502
  createdAt: exports_external.string().datetime().optional(),
7183
7503
  version: exports_external.string().default("1.0"),
@@ -7217,19 +7537,23 @@ var init_validation = __esm(() => {
7217
7537
  });
7218
7538
  SqlNamedConnectionSchema = SqlConnectionConfigSchema.extend({
7219
7539
  permission: PermissionSchema,
7220
- envFile: exports_external.string().optional()
7540
+ envFile: exports_external.string().optional(),
7541
+ environment: EnvironmentLabelSchema
7221
7542
  });
7222
7543
  MongoDBNamedConnectionSchema = MongoDBConnectionConfigSchema.extend({
7223
7544
  permission: PermissionSchema,
7224
- envFile: exports_external.string().optional()
7545
+ envFile: exports_external.string().optional(),
7546
+ environment: EnvironmentLabelSchema
7225
7547
  });
7226
7548
  RedisNamedConnectionSchema = RedisConnectionConfigSchema.extend({
7227
7549
  permission: PermissionSchema,
7228
- envFile: exports_external.string().optional()
7550
+ envFile: exports_external.string().optional(),
7551
+ environment: EnvironmentLabelSchema
7229
7552
  });
7230
7553
  ElasticsearchNamedConnectionSchema = ElasticsearchConnectionConfigSchema.extend({
7231
7554
  permission: PermissionSchema,
7232
- envFile: exports_external.string().optional()
7555
+ envFile: exports_external.string().optional(),
7556
+ environment: EnvironmentLabelSchema
7233
7557
  });
7234
7558
  NamedConnectionSchema = exports_external.union([
7235
7559
  SqlNamedConnectionSchema,
@@ -7255,34 +7579,6 @@ var init_validation = __esm(() => {
7255
7579
  });
7256
7580
  });
7257
7581
 
7258
- // src/agent-core/errors.ts
7259
- var ConfigError;
7260
- var init_errors2 = __esm(() => {
7261
- ConfigError = class ConfigError extends Error {
7262
- constructor(message) {
7263
- super(message);
7264
- this.name = "ConfigError";
7265
- if (Error.captureStackTrace)
7266
- Error.captureStackTrace(this, ConfigError);
7267
- }
7268
- };
7269
- });
7270
-
7271
- // src/utils/errors.ts
7272
- var EnvParseError;
7273
- var init_errors3 = __esm(() => {
7274
- init_errors2();
7275
- EnvParseError = class EnvParseError extends Error {
7276
- constructor(message) {
7277
- super(message);
7278
- this.name = "EnvParseError";
7279
- if (Error.captureStackTrace) {
7280
- Error.captureStackTrace(this, EnvParseError);
7281
- }
7282
- }
7283
- };
7284
- });
7285
-
7286
7582
  // src/agent-core/env-loader.ts
7287
7583
  import { readFile } from "fs/promises";
7288
7584
  function parseEnvContent(content) {
@@ -7321,7 +7617,7 @@ async function loadEnvFile(filePath) {
7321
7617
  }
7322
7618
  }
7323
7619
  var init_env_loader = __esm(() => {
7324
- init_errors2();
7620
+ init_errors();
7325
7621
  });
7326
7622
 
7327
7623
  // src/core/env-loader.ts
@@ -7329,123 +7625,129 @@ var init_env_loader2 = __esm(() => {
7329
7625
  init_env_loader();
7330
7626
  });
7331
7627
 
7332
- // src/core/config-binding.ts
7333
- import { createHash } from "crypto";
7334
- import { mkdir, unlink } from "fs/promises";
7335
- import { homedir } from "os";
7336
- import { basename, join as join2, resolve as resolve2 } from "path";
7337
- function isProjectConfigBinding(raw) {
7338
- if (typeof raw !== "object" || raw === null)
7339
- return false;
7340
- const candidate = raw;
7341
- return candidate.version === 3 && typeof candidate.binding === "object" && candidate.binding !== null && candidate.binding.type === "home-storage" && typeof candidate.binding.storagePath === "string" && candidate.binding.storagePath.length > 0 && typeof candidate.binding.projectPath === "string" && candidate.binding.projectPath.length > 0 && typeof candidate.binding.createdAt === "string" && candidate.binding.createdAt.length > 0;
7342
- }
7343
- function getDbcliHomeRoot() {
7344
- return DBCLI_HOME_ROOT;
7345
- }
7346
- function getProjectStoragePath(projectPath) {
7347
- const normalizedProjectPath = resolve2(projectPath);
7348
- const projectName = basename(normalizedProjectPath) || "project";
7349
- const hash = createHash("sha1").update(normalizedProjectPath).digest("hex").slice(0, 12);
7350
- return join2(getDbcliHomeRoot(), "projects", `${projectName}-${hash}`);
7351
- }
7352
- async function readProjectBinding(projectPath) {
7353
- const configFile = Bun.file(join2(projectPath, BINDING_FILE_NAME));
7354
- if (!await configFile.exists())
7355
- return null;
7356
- try {
7357
- const raw = JSON.parse(await configFile.text());
7358
- return isProjectConfigBinding(raw) ? raw : null;
7359
- } catch {
7360
- return null;
7628
+ // src/utils/levenshtein-distance.ts
7629
+ function levenshteinDistance(a, b) {
7630
+ const lenA = a.length;
7631
+ const lenB = b.length;
7632
+ const dp = [];
7633
+ for (let i = 0;i <= lenB; i++) {
7634
+ const row = [];
7635
+ for (let j = 0;j <= lenA; j++) {
7636
+ row[j] = 0;
7637
+ }
7638
+ dp[i] = row;
7361
7639
  }
7362
- }
7363
- async function resolveConfigStoragePath(path2) {
7364
- const binding = await readProjectBinding(path2);
7365
- return binding?.binding.storagePath ?? path2;
7366
- }
7367
- async function writeProjectBinding(projectPath, storagePath = getProjectStoragePath(projectPath)) {
7368
- const binding = {
7369
- version: 3,
7370
- binding: {
7371
- type: "home-storage",
7372
- storagePath,
7373
- projectPath: resolve2(projectPath),
7374
- createdAt: new Date().toISOString()
7640
+ for (let i = 0;i <= lenB; i++) {
7641
+ const row = dp[i];
7642
+ if (row !== undefined) {
7643
+ row[0] = i;
7644
+ }
7645
+ }
7646
+ for (let j = 0;j <= lenA; j++) {
7647
+ const firstRow = dp[0];
7648
+ if (firstRow !== undefined) {
7649
+ firstRow[j] = j;
7375
7650
  }
7376
- };
7377
- await mkdir(projectPath, { recursive: true });
7378
- await mkdir(storagePath, { recursive: true });
7379
- await Bun.file(join2(projectPath, BINDING_FILE_NAME)).write(JSON.stringify(binding, null, 2));
7380
- return binding;
7381
- }
7382
- async function migrateLegacyProjectEnvLocal(projectPath, storagePath = getProjectStoragePath(projectPath)) {
7383
- const projectEnvPath = join2(projectPath, ".env.local");
7384
- const projectEnvFile = Bun.file(projectEnvPath);
7385
- if (!await projectEnvFile.exists())
7386
- return;
7387
- const storageEnvPath = join2(storagePath, ".env.local");
7388
- await mkdir(storagePath, { recursive: true });
7389
- if (!await Bun.file(storageEnvPath).exists()) {
7390
- await Bun.file(storageEnvPath).write(await projectEnvFile.text());
7391
7651
  }
7392
- await unlink(projectEnvPath);
7652
+ for (let i = 1;i <= lenB; i++) {
7653
+ const currentRow = dp[i];
7654
+ const prevRow = dp[i - 1];
7655
+ if (!currentRow || !prevRow)
7656
+ continue;
7657
+ for (let j = 1;j <= lenA; j++) {
7658
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
7659
+ currentRow[j] = prevRow[j - 1] ?? 0;
7660
+ } else {
7661
+ const sub = (prevRow[j - 1] ?? 0) + 1;
7662
+ const ins = (currentRow[j - 1] ?? 0) + 1;
7663
+ const del = (prevRow[j] ?? 0) + 1;
7664
+ currentRow[j] = Math.min(sub, ins, del);
7665
+ }
7666
+ }
7667
+ }
7668
+ return dp[lenB]?.[lenA] ?? 0;
7393
7669
  }
7394
- var BINDING_FILE_NAME = "config.json", DBCLI_HOME_ROOT;
7395
- var init_config_binding = __esm(() => {
7396
- DBCLI_HOME_ROOT = join2(homedir(), ".config", "dbcli");
7397
- });
7398
7670
 
7399
7671
  // src/core/config-v2.ts
7400
- import { join as join3 } from "path";
7401
- import { mkdir as mkdir2, rename } from "fs/promises";
7672
+ import { join as join4 } from "path";
7402
7673
  function detectConfigVersion(raw) {
7403
7674
  if (typeof raw === "object" && raw !== null && "version" in raw && raw.version === 2 && "connections" in raw) {
7404
7675
  return 2;
7405
7676
  }
7406
7677
  return 1;
7407
7678
  }
7679
+ function assertExplicitProductionSelection(resolved, requestedName) {
7680
+ if (resolved.environment !== "production")
7681
+ return;
7682
+ if (requestedName?.trim() === resolved.name)
7683
+ return;
7684
+ throw new ConfigError(`\u9023\u7DDA '${resolved.name}' \u6A19\u8A18\u70BA production\uFF0C\u5FC5\u9808\u660E\u78BA\u4F7F\u7528 --use ${resolved.name}\uFF08\u6216\u8A2D\u5B9A DBCLI_CONNECTION=${resolved.name}\uFF09\u624D\u80FD\u57F7\u884C\u3002`);
7685
+ }
7686
+ function assertProductionDefaultConfirmation(name2, environment, confirmation) {
7687
+ if (environment !== "production")
7688
+ return;
7689
+ if (confirmation === name2)
7690
+ return;
7691
+ throw new ConfigError(`\u9023\u7DDA '${name2}' \u6A19\u8A18\u70BA production\u3002\u82E5\u8981\u8B8A\u66F4\u9810\u8A2D\u9023\u7DDA\uFF0C\u8ACB\u52A0\u5165 --confirm-production ${name2}\u3002`);
7692
+ }
7693
+ function findSimilarConnectionNames(requestedName, availableNames) {
7694
+ const requested = requestedName.toLowerCase();
7695
+ const distanceLimit = Math.max(2, Math.floor(requested.length / 3));
7696
+ return availableNames.map((name2) => {
7697
+ const candidate = name2.toLowerCase();
7698
+ const distance = levenshteinDistance(requested, candidate);
7699
+ return {
7700
+ name: name2,
7701
+ distance,
7702
+ isSimilar: candidate.includes(requested) || requested.includes(candidate) || distance <= distanceLimit
7703
+ };
7704
+ }).filter(({ isSimilar }) => isSimilar).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name)).slice(0, 3).map(({ name: name2 }) => name2);
7705
+ }
7706
+ function connectionNotFoundMessage(requestedName, availableNames) {
7707
+ const suggestions = findSimilarConnectionNames(requestedName, availableNames);
7708
+ const suggestion = suggestions.length > 0 ? ` \u4F60\u662F\u5426\u8981\u4F7F\u7528\uFF1A${suggestions.join("\u3001")}\uFF1F` : "";
7709
+ return `\u9023\u7DDA '${requestedName}' \u4E0D\u5B58\u5728\u3002${suggestion} \u53EF\u7528\u9023\u7DDA\uFF1A${availableNames.join(", ")}`;
7710
+ }
7408
7711
  function resolveConnection(config, name2) {
7409
7712
  const connectionName = name2 ?? config.default;
7410
7713
  const conn = config.connections[connectionName];
7411
7714
  if (!conn) {
7412
- const available = Object.keys(config.connections).join(", ");
7413
- throw new ConfigError(`\u9023\u7DDA '${connectionName}' \u4E0D\u5B58\u5728\u3002\u53EF\u7528\u9023\u7DDA\uFF1A${available}`);
7715
+ throw new ConfigError(connectionNotFoundMessage(connectionName, Object.keys(config.connections)));
7414
7716
  }
7415
- const { permission, envFile, ...connectionFields } = conn;
7717
+ const { permission, envFile, environment, ...connectionFields } = conn;
7416
7718
  return {
7417
7719
  name: connectionName,
7418
7720
  connection: connectionFields,
7419
7721
  permission,
7420
- envFile
7722
+ envFile,
7723
+ environment
7421
7724
  };
7422
7725
  }
7423
7726
  async function loadConnectionEnv(resolved, basePath) {
7424
7727
  if (resolved.envFile) {
7425
- const envPath = join3(basePath, resolved.envFile);
7728
+ const envPath = join4(basePath, resolved.envFile);
7426
7729
  await loadEnvFile(envPath);
7427
7730
  }
7428
7731
  }
7429
7732
  async function readV2Config(path2) {
7430
7733
  const storagePath = await resolveConfigStoragePath(path2);
7431
- const configPath = join3(storagePath, "config.json");
7734
+ const configPath = join4(storagePath, "config.json");
7432
7735
  const file = Bun.file(configPath);
7433
7736
  if (!await file.exists()) {
7434
7737
  throw new ConfigError(`\u627E\u4E0D\u5230 V2 \u8A2D\u5B9A\u6A94\uFF1A${configPath}`);
7435
7738
  }
7739
+ await assertAgentReadableFile(configPath);
7436
7740
  const content = await file.text();
7741
+ await assertConfigIntegrity(storagePath, content, { requireRecord: true });
7437
7742
  const raw = JSON.parse(content);
7438
7743
  return DbcliConfigV2Schema.parse(raw);
7439
7744
  }
7440
7745
  async function writeV2Config(path2, config) {
7746
+ assertConfigMutationApproved();
7441
7747
  DbcliConfigV2Schema.parse(config);
7442
7748
  const storagePath = await resolveConfigStoragePath(path2);
7443
- const configPath = join3(storagePath, "config.json");
7444
- const tmpPath = `${configPath}.tmp`;
7445
- await mkdir2(storagePath, { recursive: true });
7446
7749
  const json = JSON.stringify(config, null, 2);
7447
- await Bun.write(tmpPath, json);
7448
- await rename(tmpPath, configPath);
7750
+ await writeConfigWithIntegrity(storagePath, json);
7449
7751
  }
7450
7752
  async function patchConnectionSchema(dbcliPath, connectionName, schema, metadataUpdate) {
7451
7753
  const storagePath = await resolveConfigStoragePath(dbcliPath);
@@ -7465,7 +7767,9 @@ async function patchConnectionSchema(dbcliPath, connectionName, schema, metadata
7465
7767
  }
7466
7768
  var init_config_v2 = __esm(() => {
7467
7769
  init_validation();
7468
- init_errors3();
7770
+ init_errors2();
7771
+ init_config_mutation_guard();
7772
+ init_config_integrity();
7469
7773
  init_env_loader2();
7470
7774
  init_config_binding();
7471
7775
  });
@@ -7480,15 +7784,26 @@ function trimAppliedLimit(rows, limit) {
7480
7784
  }
7481
7785
 
7482
7786
  // src/agent-core/connection-selector.ts
7787
+ function normalizeSelector(value, source) {
7788
+ if (value === undefined)
7789
+ return;
7790
+ const normalized = value.trim();
7791
+ if (normalized === "") {
7792
+ throw new Error(`${source} connection selector cannot be empty`);
7793
+ }
7794
+ return normalized;
7795
+ }
7483
7796
  function resolveConnectionSelector(inputs) {
7484
- if (inputs.root !== undefined && inputs.command !== undefined && inputs.root !== inputs.command) {
7485
- throw new Error(`Conflicting connection selectors: root value '${inputs.root}' does not match command value '${inputs.command}'`);
7797
+ const root = normalizeSelector(inputs.root, "Root");
7798
+ const command = normalizeSelector(inputs.command, "Command");
7799
+ if (root !== undefined && command !== undefined && root !== command) {
7800
+ throw new Error(`Conflicting connection selectors: root value '${root}' does not match command value '${command}'`);
7486
7801
  }
7487
- const explicit = inputs.command ?? inputs.root;
7802
+ const explicit = command ?? root;
7488
7803
  if (explicit !== undefined)
7489
7804
  return explicit;
7490
7805
  const environment = inputs.environment?.trim();
7491
- return environment ? environment : undefined;
7806
+ return environment || undefined;
7492
7807
  }
7493
7808
  function parseConnectionNames(selector) {
7494
7809
  const names = selector.split(",").map((name2) => name2.trim());
@@ -7518,14 +7833,14 @@ function resolveEnvRef(value, fieldName, env = process.env) {
7518
7833
  return resolved;
7519
7834
  }
7520
7835
  var init_env_ref = __esm(() => {
7521
- init_errors2();
7836
+ init_errors();
7522
7837
  });
7523
7838
 
7524
7839
  // src/agent-core/public.ts
7525
7840
  var init_public = __esm(() => {
7526
7841
  init_env_loader();
7527
7842
  init_env_ref();
7528
- init_errors2();
7843
+ init_errors();
7529
7844
  });
7530
7845
 
7531
7846
  // node_modules/lru-cache/dist/esm/node/index.min.js
@@ -8165,16 +8480,16 @@ var init_index_min = __esm(() => {
8165
8480
  });
8166
8481
 
8167
8482
  // src/utils/schema-path.ts
8168
- import { join as join4 } from "path";
8483
+ import { join as join5 } from "path";
8169
8484
  function resolveSchemaPath(dbcliPath, connectionName) {
8170
8485
  if (!connectionName)
8171
- return join4(dbcliPath, "schemas");
8172
- return join4(dbcliPath, "schemas", connectionName);
8486
+ return join5(dbcliPath, "schemas");
8487
+ return join5(dbcliPath, "schemas", connectionName);
8173
8488
  }
8174
8489
  var init_schema_path = () => {};
8175
8490
 
8176
8491
  // src/core/schema-cache.ts
8177
- import { join as join5 } from "path";
8492
+ import { join as join6 } from "path";
8178
8493
 
8179
8494
  class SchemaCacheManager {
8180
8495
  cache;
@@ -8200,13 +8515,13 @@ class SchemaCacheManager {
8200
8515
  }
8201
8516
  async initialize() {
8202
8517
  try {
8203
- const indexPath = join5(this.schemaRoot, "index.json");
8518
+ const indexPath = join6(this.schemaRoot, "index.json");
8204
8519
  const indexFile = Bun.file(indexPath);
8205
8520
  if (await indexFile.exists()) {
8206
8521
  const indexContent = await indexFile.text();
8207
8522
  this.index = JSON.parse(indexContent);
8208
8523
  }
8209
- const hotPath = join5(this.schemaRoot, "hot-schemas.json");
8524
+ const hotPath = join6(this.schemaRoot, "hot-schemas.json");
8210
8525
  const hotFile = Bun.file(hotPath);
8211
8526
  if (await hotFile.exists()) {
8212
8527
  const hotContent = await hotFile.text();
@@ -8239,7 +8554,7 @@ class SchemaCacheManager {
8239
8554
  return null;
8240
8555
  }
8241
8556
  try {
8242
- const filePath = join5(this.schemaRoot, tableInfo.file);
8557
+ const filePath = join6(this.schemaRoot, tableInfo.file);
8243
8558
  const file = Bun.file(filePath);
8244
8559
  if (!await file.exists()) {
8245
8560
  console.error(`Cold table file not found: ${tableInfo.file} for table ${tableName}`);
@@ -8294,12 +8609,12 @@ var init_schema_cache = __esm(() => {
8294
8609
  });
8295
8610
 
8296
8611
  // src/core/schema-index.ts
8297
- import { join as join6 } from "path";
8612
+ import { join as join7 } from "path";
8298
8613
 
8299
8614
  class SchemaIndexBuilder {
8300
8615
  static async loadIndex(dbcliPath, connectionName) {
8301
8616
  try {
8302
- const indexPath = join6(resolveSchemaPath(dbcliPath, connectionName), "index.json");
8617
+ const indexPath = join7(resolveSchemaPath(dbcliPath, connectionName), "index.json");
8303
8618
  const file = Bun.file(indexPath);
8304
8619
  if (!await file.exists()) {
8305
8620
  return null;
@@ -8346,7 +8661,7 @@ class SchemaIndexBuilder {
8346
8661
  try {
8347
8662
  const schemasDir = resolveSchemaPath(dbcliPath, connectionName);
8348
8663
  await this.ensureDir(schemasDir);
8349
- const indexPath = join6(schemasDir, "index.json");
8664
+ const indexPath = join7(schemasDir, "index.json");
8350
8665
  const indexFile = Bun.file(indexPath);
8351
8666
  await indexFile.write(JSON.stringify(index, null, 2));
8352
8667
  } catch (error) {
@@ -8392,7 +8707,7 @@ var exports_schema_loader = {};
8392
8707
  __export(exports_schema_loader, {
8393
8708
  SchemaLayeredLoader: () => SchemaLayeredLoader
8394
8709
  });
8395
- import { join as join7 } from "path";
8710
+ import { join as join8 } from "path";
8396
8711
 
8397
8712
  class SchemaLayeredLoader {
8398
8713
  dbcliPath;
@@ -8459,7 +8774,7 @@ class SchemaLayeredLoader {
8459
8774
  }
8460
8775
  async ensureDirectories() {
8461
8776
  const base = resolveSchemaPath(this.dbcliPath, this.connectionName);
8462
- const dirs = [base, join7(base, "cold")];
8777
+ const dirs = [base, join8(base, "cold")];
8463
8778
  for (const dir of dirs) {
8464
8779
  try {
8465
8780
  const dirFile = Bun.file(dir);
@@ -8507,7 +8822,7 @@ __export(exports_config, {
8507
8822
  getGlobalConnectionName: () => getGlobalConnectionName,
8508
8823
  configModule: () => configModule
8509
8824
  });
8510
- import { join as join8 } from "path";
8825
+ import { join as join9 } from "path";
8511
8826
  import { mkdir as mkdir3 } from "fs/promises";
8512
8827
  function assertNoConnectionSelectorOnV1(connectionName) {
8513
8828
  if (connectionName === undefined)
@@ -8528,7 +8843,7 @@ async function getSchemaIsolationConnectionName(dbcliPath) {
8528
8843
  const isDirectory = stat?.isDirectory() ?? false;
8529
8844
  if (!isDirectory)
8530
8845
  return;
8531
- const configJsonPath = join8(storagePath, "config.json");
8846
+ const configJsonPath = join9(storagePath, "config.json");
8532
8847
  const configFile = Bun.file(configJsonPath);
8533
8848
  if (!await configFile.exists())
8534
8849
  return;
@@ -8576,10 +8891,12 @@ function parseEnvPassword(content) {
8576
8891
  var _globalConnectionName, DEFAULT_CONFIG, configModule;
8577
8892
  var init_config = __esm(() => {
8578
8893
  init_validation();
8579
- init_errors3();
8894
+ init_errors2();
8580
8895
  init_config_v2();
8581
8896
  init_config_binding();
8582
8897
  init_public();
8898
+ init_config_mutation_guard();
8899
+ init_config_integrity();
8583
8900
  DEFAULT_CONFIG = {
8584
8901
  connection: {
8585
8902
  system: "postgresql",
@@ -8607,9 +8924,9 @@ var init_config = __esm(() => {
8607
8924
  const binding = await readProjectBinding(path2);
8608
8925
  const storagePath = await resolveConfigStoragePath(path2);
8609
8926
  if (binding) {
8610
- const storageConfigExists = await Bun.file(join8(storagePath, "config.json")).exists();
8927
+ const storageConfigExists = await Bun.file(join9(storagePath, "config.json")).exists();
8611
8928
  if (!storageConfigExists) {
8612
- throw new ConfigError(`Bound dbcli config not found: ${join8(storagePath, "config.json")}`);
8929
+ throw new ConfigError(`Bound dbcli config not found: ${join9(storagePath, "config.json")}`);
8613
8930
  }
8614
8931
  }
8615
8932
  let isDirectory = false;
@@ -8620,17 +8937,20 @@ var init_config = __esm(() => {
8620
8937
  isDirectory = false;
8621
8938
  }
8622
8939
  if (isDirectory) {
8623
- const configPath = join8(storagePath, "config.json");
8940
+ const configPath = join9(storagePath, "config.json");
8624
8941
  const configFile = Bun.file(configPath);
8625
8942
  const configExists = await configFile.exists();
8626
8943
  if (configExists) {
8944
+ await assertAgentReadableFile(configPath);
8627
8945
  const content = await configFile.text();
8946
+ await assertConfigIntegrity(storagePath, content, { requireRecord: true });
8628
8947
  const config = JSON.parse(content);
8629
8948
  if (detectConfigVersion(config) === 2) {
8630
8949
  const v2Config = DbcliConfigV2Schema.parse(config);
8631
8950
  const resolved = resolveConnection(v2Config, effectiveConnectionName);
8951
+ assertExplicitProductionSelection(resolved, effectiveConnectionName);
8632
8952
  await loadConnectionEnv(resolved, storagePath);
8633
- const envLocalPath = join8(storagePath, ".env.local");
8953
+ const envLocalPath = join9(storagePath, ".env.local");
8634
8954
  const envLocalFile = Bun.file(envLocalPath);
8635
8955
  let legacyPassword = null;
8636
8956
  if (await envLocalFile.exists()) {
@@ -8667,7 +8987,7 @@ var init_config = __esm(() => {
8667
8987
  console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
8668
8988
  }
8669
8989
  }
8670
- return DbcliConfigSchema.parse({
8990
+ const parsedConfig = DbcliConfigSchema.parse({
8671
8991
  connection: resolvedConnection,
8672
8992
  permission: resolved.permission,
8673
8993
  schema,
@@ -8676,10 +8996,15 @@ var init_config = __esm(() => {
8676
8996
  audit: v2Config.audit,
8677
8997
  redis: v2Config.redis
8678
8998
  });
8999
+ return {
9000
+ ...parsedConfig,
9001
+ effectiveConnectionName: resolved.name,
9002
+ ...resolved.environment && { effectiveEnvironment: resolved.environment }
9003
+ };
8679
9004
  }
8680
9005
  assertNoConnectionSelectorOnV1(effectiveConnectionName);
8681
9006
  const resolvedConfig = resolveEnvReferences(config, process.env);
8682
- const envPath = join8(storagePath, ".env.local");
9007
+ const envPath = join9(storagePath, ".env.local");
8683
9008
  const envFile = Bun.file(envPath);
8684
9009
  if (await envFile.exists()) {
8685
9010
  const envContent = await envFile.text();
@@ -8694,12 +9019,18 @@ var init_config = __esm(() => {
8694
9019
  const file = Bun.file(path2);
8695
9020
  const exists = await file.exists();
8696
9021
  if (exists) {
9022
+ if (process.env.DBCLI_AGENT_MODE === "1") {
9023
+ throw new ConfigError(`Agent mode refuses legacy single-file config: ${path2}. Run the human/admin migration workflow to move it to V2 home storage first.`);
9024
+ }
8697
9025
  assertNoConnectionSelectorOnV1(effectiveConnectionName);
8698
9026
  const content = await file.text();
8699
9027
  const raw = JSON.parse(content);
8700
9028
  const resolved = resolveEnvReferences(raw, process.env);
8701
9029
  return DbcliConfigSchema.parse(resolved);
8702
9030
  }
9031
+ if (process.env.DBCLI_AGENT_MODE === "1") {
9032
+ throw new ConfigError(`Agent mode refuses a missing config: ${path2}. Run the human/admin setup workflow to provision it.`);
9033
+ }
8703
9034
  return { ...DEFAULT_CONFIG };
8704
9035
  } catch (error) {
8705
9036
  if (error instanceof ConfigError)
@@ -8741,6 +9072,7 @@ var init_config = __esm(() => {
8741
9072
  },
8742
9073
  async write(path2, config) {
8743
9074
  try {
9075
+ assertConfigMutationApproved();
8744
9076
  this.validate(config);
8745
9077
  const storagePath = await resolveConfigStoragePath(path2);
8746
9078
  let isDirectory = false;
@@ -8754,9 +9086,8 @@ var init_config = __esm(() => {
8754
9086
  await mkdir3(storagePath, { recursive: true });
8755
9087
  const hasEnvReferences = isEnvReference(config.connection.password);
8756
9088
  if (hasEnvReferences) {
8757
- const configPath = join8(storagePath, "config.json");
8758
9089
  const configJson = JSON.stringify(config, null, 2);
8759
- await Bun.file(configPath).write(configJson);
9090
+ await writeConfigWithIntegrity(storagePath, configJson);
8760
9091
  } else {
8761
9092
  const password = config.connection.password;
8762
9093
  const configWithoutPassword = {
@@ -8767,11 +9098,10 @@ var init_config = __esm(() => {
8767
9098
  }
8768
9099
  };
8769
9100
  delete configWithoutPassword.connection.password;
8770
- const configPath = join8(storagePath, "config.json");
8771
9101
  const configJson = JSON.stringify(configWithoutPassword, null, 2);
8772
- await Bun.file(configPath).write(configJson);
9102
+ await writeConfigWithIntegrity(storagePath, configJson);
8773
9103
  if (password) {
8774
- const envPath = join8(storagePath, ".env.local");
9104
+ const envPath = join9(storagePath, ".env.local");
8775
9105
  const envContent = `# Database Credentials - DO NOT commit to git
8776
9106
 
8777
9107
  DBCLI_PASSWORD=${password}
@@ -9888,7 +10218,7 @@ var init_parser = __esm(() => {
9888
10218
 
9889
10219
  // src/core/saved-queries/loader.ts
9890
10220
  import { readdir } from "fs/promises";
9891
- import { join as join9, relative, sep } from "path";
10221
+ import { join as join10, relative, sep } from "path";
9892
10222
  async function loadSnippets(opts) {
9893
10223
  const builtin = await walkAndParse(opts.builtinDir, "builtin");
9894
10224
  const shared = await walkAndParse(opts.sharedDir, "shared");
@@ -9955,7 +10285,7 @@ async function collectFiles(root) {
9955
10285
  async function walk(dir) {
9956
10286
  const entries = await readdir(dir, { withFileTypes: true });
9957
10287
  for (const e of entries) {
9958
- const full = join9(dir, e.name);
10288
+ const full = join10(dir, e.name);
9959
10289
  if (e.isDirectory())
9960
10290
  await walk(full);
9961
10291
  else
@@ -9978,23 +10308,23 @@ var init_loader = __esm(() => {
9978
10308
  });
9979
10309
 
9980
10310
  // src/core/saved-queries/snippet-paths.ts
9981
- import { join as join10 } from "path";
10311
+ import { join as join11 } from "path";
9982
10312
  function resolveBuiltinDir() {
9983
10313
  return packageAssetPath("snippets");
9984
10314
  }
9985
10315
  function resolveSnippetDirs(workspaceRoot) {
9986
10316
  return {
9987
10317
  builtinDir: resolveBuiltinDir(),
9988
- sharedDir: join10(workspaceRoot, ".dbcli-shared", "queries"),
9989
- localDir: join10(workspaceRoot, ".dbcli", "queries")
10318
+ sharedDir: join11(workspaceRoot, ".dbcli-shared", "queries"),
10319
+ localDir: join11(workspaceRoot, ".dbcli", "queries")
9990
10320
  };
9991
10321
  }
9992
10322
  function snippetKeyToFile(workspaceRoot, key, source) {
9993
10323
  const rel = key.replace(/^@/, "") + ".sql";
9994
10324
  if (source === "builtin")
9995
- return join10(resolveBuiltinDir(), rel);
10325
+ return join11(resolveBuiltinDir(), rel);
9996
10326
  const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
9997
- return join10(workspaceRoot, dir, rel);
10327
+ return join11(workspaceRoot, dir, rel);
9998
10328
  }
9999
10329
  var init_snippet_paths = __esm(() => {
10000
10330
  init_package_root();
@@ -10426,6 +10756,7 @@ var init_skill = __esm(() => {
10426
10756
  init_package_root();
10427
10757
  init_esm();
10428
10758
  init_context();
10759
+ init_config_path();
10429
10760
  REFERENCE_SOURCE_PATH = packageAssetPath("reference.md");
10430
10761
  SUPPORTED_PLATFORMS = [
10431
10762
  "claude",
@@ -10469,6 +10800,7 @@ var init_upgrade = __esm(() => {
10469
10800
  init_esm();
10470
10801
  init_colors();
10471
10802
  init_version_check();
10803
+ init_config_path();
10472
10804
  init_skill();
10473
10805
  init_message_loader();
10474
10806
  init_package();
@@ -10639,7 +10971,7 @@ function parseEnvDatabase(env) {
10639
10971
  return null;
10640
10972
  }
10641
10973
  var init_env_parser = __esm(() => {
10642
- init_errors3();
10974
+ init_errors2();
10643
10975
  });
10644
10976
 
10645
10977
  // node_modules/@inquirer/core/dist/lib/key.js
@@ -12052,13 +12384,13 @@ var PromisePolyfill;
12052
12384
  var init_promise_polyfill = __esm(() => {
12053
12385
  PromisePolyfill = class PromisePolyfill extends Promise {
12054
12386
  static withResolver() {
12055
- let resolve3;
12387
+ let resolve4;
12056
12388
  let reject;
12057
12389
  const promise = new Promise((res, rej) => {
12058
- resolve3 = res;
12390
+ resolve4 = res;
12059
12391
  reject = rej;
12060
12392
  });
12061
- return { promise, resolve: resolve3, reject };
12393
+ return { promise, resolve: resolve4, reject };
12062
12394
  }
12063
12395
  };
12064
12396
  });
@@ -12097,7 +12429,7 @@ function createPrompt(view) {
12097
12429
  });
12098
12430
  output.mute();
12099
12431
  const screen = new ScreenManager(rl);
12100
- const { promise, resolve: resolve3, reject } = PromisePolyfill.withResolver();
12432
+ const { promise, resolve: resolve4, reject } = PromisePolyfill.withResolver();
12101
12433
  const cancel = () => reject(new CancelPromptError);
12102
12434
  if (signal) {
12103
12435
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -12128,7 +12460,7 @@ function createPrompt(view) {
12128
12460
  try {
12129
12461
  const nextView = view(config, (value) => {
12130
12462
  if (effectsSettled) {
12131
- resolve3(value);
12463
+ resolve4(value);
12132
12464
  } else {
12133
12465
  pendingDone = { value };
12134
12466
  }
@@ -12151,7 +12483,7 @@ function createPrompt(view) {
12151
12483
  if (pendingDone !== null) {
12152
12484
  const { value } = pendingDone;
12153
12485
  pendingDone = null;
12154
- resolve3(value);
12486
+ resolve4(value);
12155
12487
  }
12156
12488
  });
12157
12489
  };
@@ -12267,7 +12599,7 @@ var init_dist7 = __esm(() => {
12267
12599
 
12268
12600
  // src/utils/prompts.ts
12269
12601
  async function readLineFromStdin(prompt = "") {
12270
- return new Promise((resolve3) => {
12602
+ return new Promise((resolve4) => {
12271
12603
  if (prompt) {
12272
12604
  process.stdout.write(prompt);
12273
12605
  }
@@ -12282,12 +12614,12 @@ async function readLineFromStdin(prompt = "") {
12282
12614
  process.stdin.pause();
12283
12615
  process.stdin.removeListener("data", onData);
12284
12616
  process.stdin.removeListener("end", onEnd);
12285
- resolve3((lines[0] ?? "").trim());
12617
+ resolve4((lines[0] ?? "").trim());
12286
12618
  }
12287
12619
  };
12288
12620
  const onEnd = () => {
12289
12621
  process.stdin.removeListener("data", onData);
12290
- resolve3(data.trim());
12622
+ resolve4(data.trim());
12291
12623
  };
12292
12624
  process.stdin.on("data", onData);
12293
12625
  process.stdin.on("end", onEnd);
@@ -15073,13 +15405,13 @@ var init_adapters = __esm(() => {
15073
15405
  });
15074
15406
 
15075
15407
  // src/commands/init.ts
15076
- import { join as join12 } from "path";
15077
- import { mkdir as mkdir4 } from "fs/promises";
15408
+ import { join as join14 } from "path";
15409
+ import { mkdir as mkdir5 } from "fs/promises";
15078
15410
  async function checkOverwrite(configPath, shouldPrompt, force) {
15079
15411
  const storagePath = await resolveConfigStoragePath(configPath);
15080
15412
  const fileExists = await Bun.file(configPath).exists();
15081
- const dirConfigExists = await Bun.file(join12(configPath, "config.json")).exists();
15082
- const storageConfigExists = await Bun.file(join12(storagePath, "config.json")).exists();
15413
+ const dirConfigExists = await Bun.file(join14(configPath, "config.json")).exists();
15414
+ const storageConfigExists = await Bun.file(join14(storagePath, "config.json")).exists();
15083
15415
  if (!fileExists && !dirConfigExists && !storageConfigExists || force)
15084
15416
  return true;
15085
15417
  if (shouldPrompt) {
@@ -15094,7 +15426,7 @@ async function checkOverwrite(configPath, shouldPrompt, force) {
15094
15426
  }
15095
15427
  async function handleRemove(configPath, name2) {
15096
15428
  const storagePath = await resolveConfigStoragePath(configPath);
15097
- const configFile = Bun.file(join12(storagePath, "config.json"));
15429
+ const configFile = Bun.file(join14(storagePath, "config.json"));
15098
15430
  if (!await configFile.exists()) {
15099
15431
  throw new Error(t("init.config_not_found"));
15100
15432
  }
@@ -15130,7 +15462,7 @@ async function handleRename(configPath, renameArg) {
15130
15462
  throw new Error(t("init.rename_invalid_format"));
15131
15463
  }
15132
15464
  const storagePath = await resolveConfigStoragePath(configPath);
15133
- const configFile = Bun.file(join12(storagePath, "config.json"));
15465
+ const configFile = Bun.file(join14(storagePath, "config.json"));
15134
15466
  if (!await configFile.exists()) {
15135
15467
  throw new Error(t("init.config_not_found"));
15136
15468
  }
@@ -15155,10 +15487,11 @@ async function handleRename(configPath, renameArg) {
15155
15487
  console.log(t_vars("init.connection_renamed", { oldName, newName }));
15156
15488
  }
15157
15489
  async function writeV2InitConfig(configPath, connectionName, connection, permission, envFile) {
15158
- const storagePath = getProjectStoragePath(configPath);
15159
- const configJsonPath = join12(storagePath, "config.json");
15490
+ const globalConfig = isGlobalConfigPath(configPath);
15491
+ const storagePath = globalConfig ? configPath : getProjectStoragePath(configPath);
15492
+ const configJsonPath = join14(storagePath, "config.json");
15160
15493
  const configFile = Bun.file(configJsonPath);
15161
- const projectConfigFile = Bun.file(join12(configPath, "config.json"));
15494
+ const projectConfigFile = Bun.file(join14(configPath, "config.json"));
15162
15495
  let existingV2 = null;
15163
15496
  if (await configFile.exists()) {
15164
15497
  const raw = JSON.parse(await configFile.text());
@@ -15272,9 +15605,11 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
15272
15605
  }
15273
15606
  };
15274
15607
  await writeV2Config(storagePath, v2Config);
15275
- await migrateLegacyProjectEnvLocal(configPath, storagePath);
15276
- await writeProjectBinding(configPath, storagePath);
15277
- console.log(t("init.config_saved"));
15608
+ if (!globalConfig) {
15609
+ await migrateLegacyProjectEnvLocal(configPath, storagePath);
15610
+ await writeProjectBinding(configPath, storagePath);
15611
+ }
15612
+ console.log(globalConfig ? t_vars("init.config_saved_global", { path: join14(configPath, "config.json") }) : t("init.config_saved"));
15278
15613
  }
15279
15614
  async function initCommandHandler(options, command) {
15280
15615
  const configPath = resolveConfigPath(command);
@@ -15286,7 +15621,8 @@ async function initCommandHandler(options, command) {
15286
15621
  await handleRename(configPath, options.rename);
15287
15622
  return;
15288
15623
  }
15289
- const isV2Init = !!(options.connName || options.envFile);
15624
+ const isGlobalConfig = isGlobalConfigPath(configPath);
15625
+ const isV2Init = !!(options.connName || options.envFile || isGlobalConfig);
15290
15626
  const connectionName = options.connName || "default";
15291
15627
  const existingConfig = await configModule.read(configPath);
15292
15628
  const isUsingEnvRefs = options.useEnvRefs;
@@ -15370,11 +15706,13 @@ async function initCommandHandler(options, command) {
15370
15706
  await writeV2InitConfig(configPath, connectionName, configForWrite, permission2, options.envFile);
15371
15707
  return;
15372
15708
  }
15373
- const storagePath2 = getProjectStoragePath(configPath);
15374
- await mkdir4(storagePath2, { recursive: true });
15709
+ const storagePath2 = isGlobalConfig ? configPath : getProjectStoragePath(configPath);
15710
+ await mkdir5(storagePath2, { recursive: true });
15375
15711
  await configModule.write(storagePath2, newConfig2);
15376
- await migrateLegacyProjectEnvLocal(configPath, storagePath2);
15377
- await writeProjectBinding(configPath, storagePath2);
15712
+ if (!isGlobalConfig) {
15713
+ await migrateLegacyProjectEnvLocal(configPath, storagePath2);
15714
+ await writeProjectBinding(configPath, storagePath2);
15715
+ }
15378
15716
  console.log(t("init.config_saved"));
15379
15717
  return;
15380
15718
  }
@@ -15484,11 +15822,13 @@ async function initCommandHandler(options, command) {
15484
15822
  await writeV2InitConfig(configPath, connectionName, configForWrite, permission, options.envFile);
15485
15823
  return;
15486
15824
  }
15487
- const storagePath = getProjectStoragePath(configPath);
15488
- await mkdir4(storagePath, { recursive: true });
15825
+ const storagePath = isGlobalConfig ? configPath : getProjectStoragePath(configPath);
15826
+ await mkdir5(storagePath, { recursive: true });
15489
15827
  await configModule.write(storagePath, newConfig);
15490
- await migrateLegacyProjectEnvLocal(configPath, storagePath);
15491
- await writeProjectBinding(configPath, storagePath);
15828
+ if (!isGlobalConfig) {
15829
+ await migrateLegacyProjectEnvLocal(configPath, storagePath);
15830
+ await writeProjectBinding(configPath, storagePath);
15831
+ }
15492
15832
  console.log(t("init.config_saved"));
15493
15833
  }
15494
15834
  async function handleMongoDBInit(ctx) {
@@ -15569,11 +15909,14 @@ async function handleMongoDBInit(ctx) {
15569
15909
  connection: mongoConfig,
15570
15910
  permission
15571
15911
  });
15572
- const storagePath = getProjectStoragePath(configPath);
15573
- await mkdir4(storagePath, { recursive: true });
15912
+ const globalConfig = isGlobalConfigPath(configPath);
15913
+ const storagePath = globalConfig ? configPath : getProjectStoragePath(configPath);
15914
+ await mkdir5(storagePath, { recursive: true });
15574
15915
  await configModule.write(storagePath, newConfig);
15575
- await migrateLegacyProjectEnvLocal(configPath, storagePath);
15576
- await writeProjectBinding(configPath, storagePath);
15916
+ if (!globalConfig) {
15917
+ await migrateLegacyProjectEnvLocal(configPath, storagePath);
15918
+ await writeProjectBinding(configPath, storagePath);
15919
+ }
15577
15920
  console.log(t("init.config_saved"));
15578
15921
  }
15579
15922
  var VALID_PERMISSIONS, initCommand;
@@ -15585,6 +15928,7 @@ var init_init = __esm(() => {
15585
15928
  init_config_v2();
15586
15929
  init_prompts();
15587
15930
  init_adapters();
15931
+ init_config_path();
15588
15932
  init_config_binding();
15589
15933
  VALID_PERMISSIONS = ["query-only", "read-write", "data-admin", "admin"];
15590
15934
  initCommand = new Command("init").description("Initialize dbcli configuration with .env parsing and interactive prompts").option("--host <host>", "Database host").option("--port <port>", "Database port").option("--user <user>", "Database user").option("--password <password>", "Database password").option("--name <name>", "Database name").option("--system <system>", "Database system (postgresql, mysql, mariadb, mongodb, redis, elasticsearch)").option("--cloud-id <id>", "Elasticsearch Cloud ID").option("--api-key <key>", "Elasticsearch API Key").option("--uri <uri>", "MongoDB connection URI (mongodb://user:pass@host:27017/db?authSource=admin)").option("--auth-source <authSource>", "MongoDB auth database (default: admin when user/password are set)").option("--permission <permission>", "Permission level (query-only, read-write, data-admin, admin)", "query-only").option("--use-env-refs", "Store env var references in config instead of actual values (for CI/CD or multi-env)", false).option("--env-host <var>", "Env var name for host (with --use-env-refs)").option("--env-port <var>", "Env var name for port (with --use-env-refs)").option("--env-user <var>", "Env var name for user (with --use-env-refs)").option("--env-password <var>", "Env var name for password (with --use-env-refs)").option("--env-database <var>", "Env var name for database (with --use-env-refs)").option("--skip-test", "Skip database connection test").option("--no-interactive", "Non-interactive mode (requires all values via flags)").option("--force", "Skip overwrite confirmation if .dbcli exists").option("--conn-name <name>", "Connection name (creates v2 multi-connection config)").option("--env-file <path>", "Path to env file for this connection").option("--remove <name>", "Remove a named connection").option("--rename <names>", "Rename a connection (format: old:new)").action(async (options) => {
@@ -15776,7 +16120,7 @@ var require_utils = __commonJS((exports, module) => {
15776
16120
  state[info.set] = info.to;
15777
16121
  }
15778
16122
  }
15779
- function readState(line) {
16123
+ function readState2(line) {
15780
16124
  let code = codeRegex(true);
15781
16125
  let controlChars = code.exec(line);
15782
16126
  let state = {};
@@ -15979,7 +16323,7 @@ var require_utils = __commonJS((exports, module) => {
15979
16323
  let output = [];
15980
16324
  for (let i = 0;i < input.length; i++) {
15981
16325
  let line = rewindState(state, input[i]);
15982
- state = readState(line);
16326
+ state = readState2(line);
15983
16327
  let temp = Object.assign({}, state);
15984
16328
  output.push(unwindState(temp, line));
15985
16329
  }
@@ -17672,6 +18016,7 @@ var init_list = __esm(() => {
17672
18016
  init_adapters();
17673
18017
  init_formatters();
17674
18018
  init_config();
18019
+ init_config_path();
17675
18020
  init_validation();
17676
18021
  init_connection_selector();
17677
18022
  ALLOWED_FORMATS = ["table", "json"];
@@ -17853,7 +18198,7 @@ class AtomicFileWriter {
17853
18198
  }
17854
18199
 
17855
18200
  // src/core/schema-writer.ts
17856
- import { join as join13 } from "path";
18201
+ import { join as join15 } from "path";
17857
18202
 
17858
18203
  class SchemaWriter {
17859
18204
  dbcliPath;
@@ -17874,7 +18219,7 @@ class SchemaWriter {
17874
18219
  hotSchemas[item.table] = tableSchema;
17875
18220
  }
17876
18221
  }
17877
- await this.writer.writeJSON(join13(schemaRoot, "hot-schemas.json"), hotSchemas);
18222
+ await this.writer.writeJSON(join15(schemaRoot, "hot-schemas.json"), hotSchemas);
17878
18223
  const coldGroups = {};
17879
18224
  for (const item of mapping.cold) {
17880
18225
  if (!coldGroups[item.file]) {
@@ -17885,10 +18230,10 @@ class SchemaWriter {
17885
18230
  coldGroups[item.file][item.table] = tableSchema;
17886
18231
  }
17887
18232
  }
17888
- const coldDir = join13(schemaRoot, "cold");
18233
+ const coldDir = join15(schemaRoot, "cold");
17889
18234
  await this.ensureDir(coldDir);
17890
18235
  for (const [fileName, tables] of Object.entries(coldGroups)) {
17891
- const filePath = join13(schemaRoot, fileName);
18236
+ const filePath = join15(schemaRoot, fileName);
17892
18237
  await this.writer.writeJSON(filePath, tables);
17893
18238
  }
17894
18239
  }
@@ -17928,6 +18273,8 @@ var init_error_recovery = () => {};
17928
18273
  var init_schema_updater = __esm(() => {
17929
18274
  init_concurrent_lock();
17930
18275
  init_error_recovery();
18276
+ init_config_mutation_guard();
18277
+ init_config_integrity();
17931
18278
  });
17932
18279
 
17933
18280
  // src/core/column-index.ts
@@ -18432,8 +18779,8 @@ var init_artifact = __esm(() => {
18432
18779
  });
18433
18780
 
18434
18781
  // src/core/verification/artifact-writer.ts
18435
- import { mkdir as mkdir5, writeFile, link, unlink as unlink2 } from "fs/promises";
18436
- import { join as join14 } from "path";
18782
+ import { mkdir as mkdir6, writeFile as writeFile2, link, unlink as unlink4 } from "fs/promises";
18783
+ import { join as join16 } from "path";
18437
18784
  function pad(n, len = 2) {
18438
18785
  return String(n).padStart(len, "0");
18439
18786
  }
@@ -18450,12 +18797,12 @@ function verificationArtifactFilename(artifact) {
18450
18797
  return `verification-${timeStamp(artifact.createdAt)}-${shortId(artifact.id)}.json`;
18451
18798
  }
18452
18799
  async function writeVerificationArtifact(storageDir, artifact) {
18453
- const dir = join14(storageDir, VERIFICATION_DIR_RELATIVE);
18454
- await mkdir5(dir, { recursive: true });
18455
- const target = join14(dir, verificationArtifactFilename(artifact));
18800
+ const dir = join16(storageDir, VERIFICATION_DIR_RELATIVE);
18801
+ await mkdir6(dir, { recursive: true });
18802
+ const target = join16(dir, verificationArtifactFilename(artifact));
18456
18803
  const tmp = `${target}.${process.pid}.tmp`;
18457
18804
  try {
18458
- await writeFile(tmp, JSON.stringify(artifact, null, 2), "utf8");
18805
+ await writeFile2(tmp, JSON.stringify(artifact, null, 2), "utf8");
18459
18806
  try {
18460
18807
  await link(tmp, target);
18461
18808
  } catch (e) {
@@ -18465,7 +18812,7 @@ async function writeVerificationArtifact(storageDir, artifact) {
18465
18812
  throw e;
18466
18813
  }
18467
18814
  } finally {
18468
- await unlink2(tmp).catch(() => {});
18815
+ await unlink4(tmp).catch(() => {});
18469
18816
  }
18470
18817
  return target;
18471
18818
  }
@@ -18712,8 +19059,8 @@ var init_assert_artifact = __esm(() => {
18712
19059
  });
18713
19060
 
18714
19061
  // src/core/verification/reader.ts
18715
- import { lstat, readdir as readdir2, readFile as readFile2 } from "fs/promises";
18716
- import { join as join15, isAbsolute, resolve as resolve3, sep as sep2 } from "path";
19062
+ import { lstat as lstat2, readdir as readdir2, readFile as readFile2 } from "fs/promises";
19063
+ import { join as join17, isAbsolute, resolve as resolve4, sep as sep2 } from "path";
18717
19064
  function isArtifactFilename(name2) {
18718
19065
  return /^verification-.*\.json$/.test(name2);
18719
19066
  }
@@ -18797,7 +19144,7 @@ function validateVerificationArtifact(value) {
18797
19144
  return value;
18798
19145
  }
18799
19146
  async function readVerificationArtifacts(storageRoot) {
18800
- const storageDir = join15(storageRoot, VERIFICATION_DIR_RELATIVE);
19147
+ const storageDir = join17(storageRoot, VERIFICATION_DIR_RELATIVE);
18801
19148
  let names;
18802
19149
  try {
18803
19150
  names = await readdir2(storageDir);
@@ -18810,9 +19157,9 @@ async function readVerificationArtifacts(storageRoot) {
18810
19157
  const artifacts = [];
18811
19158
  const invalid = [];
18812
19159
  for (const filename of names.filter(isArtifactFilename)) {
18813
- const path4 = join15(storageDir, filename);
19160
+ const path4 = join17(storageDir, filename);
18814
19161
  try {
18815
- const stats = await lstat(path4);
19162
+ const stats = await lstat2(path4);
18816
19163
  if (!stats.isFile()) {
18817
19164
  throw new Error("artifact path is not a regular file");
18818
19165
  }
@@ -18894,15 +19241,15 @@ function looksLikePath(selector) {
18894
19241
  }
18895
19242
  function findVerificationArtifact(input, selector) {
18896
19243
  if (looksLikePath(selector)) {
18897
- const resolved = resolve3(selector);
18898
- const root = resolve3(input.storageDir);
19244
+ const resolved = resolve4(selector);
19245
+ const root = resolve4(input.storageDir);
18899
19246
  if (resolved !== root && !resolved.startsWith(root + sep2)) {
18900
19247
  throw new VerificationArtifactSelectionError(`Path is outside the verification directory: ${resolved}`);
18901
19248
  }
18902
- const hit = input.artifacts.find((r) => resolve3(r.path) === resolved);
19249
+ const hit = input.artifacts.find((r) => resolve4(r.path) === resolved);
18903
19250
  if (hit)
18904
19251
  return hit;
18905
- const bad = input.invalid.find((r) => resolve3(r.path) === resolved);
19252
+ const bad = input.invalid.find((r) => resolve4(r.path) === resolved);
18906
19253
  if (bad) {
18907
19254
  throw new VerificationArtifactSelectionError(`Artifact at ${bad.filename} is invalid: ${bad.error}`);
18908
19255
  }
@@ -18939,8 +19286,8 @@ var init_reader = __esm(() => {
18939
19286
  });
18940
19287
 
18941
19288
  // src/core/verification/retention.ts
18942
- import { lstat as lstat2, unlink as unlink3 } from "fs/promises";
18943
- import { basename as basename3, resolve as resolve4, sep as sep3 } from "path";
19289
+ import { lstat as lstat3, unlink as unlink5 } from "fs/promises";
19290
+ import { basename as basename3, resolve as resolve5, sep as sep3 } from "path";
18944
19291
  function parseOlderThanDays(raw) {
18945
19292
  const match = /^(\d+)d$/.exec(raw);
18946
19293
  if (!match) {
@@ -18996,22 +19343,22 @@ function boundError2(message) {
18996
19343
  return single.length <= 200 ? single : single.slice(0, 199) + "\u2026";
18997
19344
  }
18998
19345
  function isInsideStorageDir(storageDir, candidatePath) {
18999
- const base = resolve4(storageDir);
19000
- const resolved = resolve4(candidatePath);
19346
+ const base = resolve5(storageDir);
19347
+ const resolved = resolve5(candidatePath);
19001
19348
  return resolved === base || resolved.startsWith(base + sep3);
19002
19349
  }
19003
19350
  function hasArtifactFilename(candidatePath) {
19004
19351
  return /^verification-.*\.json$/.test(basename3(candidatePath));
19005
19352
  }
19006
19353
  async function safeDeleteCandidate(storageDir, candidate) {
19007
- const resolved = resolve4(candidate.path);
19354
+ const resolved = resolve5(candidate.path);
19008
19355
  if (!isInsideStorageDir(storageDir, resolved))
19009
19356
  return { ok: false, reason: "outside-storage-dir" };
19010
19357
  if (!hasArtifactFilename(resolved))
19011
19358
  return { ok: false, reason: "filename-mismatch" };
19012
19359
  let stats;
19013
19360
  try {
19014
- stats = await lstat2(resolved);
19361
+ stats = await lstat3(resolved);
19015
19362
  } catch (e) {
19016
19363
  if (e.code === "ENOENT")
19017
19364
  return { ok: false, reason: "missing" };
@@ -19020,7 +19367,7 @@ async function safeDeleteCandidate(storageDir, candidate) {
19020
19367
  if (!stats.isFile())
19021
19368
  return { ok: false, reason: "not-regular-file" };
19022
19369
  try {
19023
- await unlink3(resolved);
19370
+ await unlink5(resolved);
19024
19371
  return { ok: true };
19025
19372
  } catch (e) {
19026
19373
  if (e.code === "ENOENT")
@@ -19037,7 +19384,7 @@ async function pruneVerificationArtifacts(storageRoot, criteria, options) {
19037
19384
  if (criteria.includeInvalid) {
19038
19385
  for (const r of read.invalid) {
19039
19386
  try {
19040
- const stats = await lstat2(r.path);
19387
+ const stats = await lstat3(r.path);
19041
19388
  invalidMtimes.set(r.path, stats.mtimeMs);
19042
19389
  } catch {}
19043
19390
  }
@@ -19290,8 +19637,8 @@ var init_core = __esm(() => {
19290
19637
 
19291
19638
  // src/core/audit/lock.ts
19292
19639
  import { hostname } from "os";
19293
- import { dirname as dirname3 } from "path";
19294
- import { mkdir as mkdir6, open, rm } from "fs/promises";
19640
+ import { dirname as dirname5 } from "path";
19641
+ import { mkdir as mkdir7, open as open2, rm } from "fs/promises";
19295
19642
 
19296
19643
  class AuditLockManager {
19297
19644
  auditFilePath;
@@ -19320,7 +19667,7 @@ class AuditLockManager {
19320
19667
  return true;
19321
19668
  }
19322
19669
  const waitTime = Math.min(backoffMs, LOCK_BACKOFF_MAX_MS);
19323
- await new Promise((resolve5) => setTimeout(resolve5, waitTime));
19670
+ await new Promise((resolve6) => setTimeout(resolve6, waitTime));
19324
19671
  backoffMs = Math.min(backoffMs * 1.5, LOCK_BACKOFF_MAX_MS);
19325
19672
  }
19326
19673
  }
@@ -19349,7 +19696,7 @@ class AuditLockManager {
19349
19696
  }
19350
19697
  async tryAcquireLock(operationName) {
19351
19698
  try {
19352
- await mkdir6(dirname3(this.lockPath), { recursive: true });
19699
+ await mkdir7(dirname5(this.lockPath), { recursive: true });
19353
19700
  const lockFile = Bun.file(this.lockPath);
19354
19701
  if (await lockFile.exists()) {
19355
19702
  const lockContent = await lockFile.json();
@@ -19367,7 +19714,7 @@ class AuditLockManager {
19367
19714
  timestamp: Date.now(),
19368
19715
  hostname: hostname()
19369
19716
  };
19370
- const handle = await open(this.lockPath, "wx");
19717
+ const handle = await open2(this.lockPath, "wx");
19371
19718
  try {
19372
19719
  await handle.writeFile(JSON.stringify(lockData), "utf8");
19373
19720
  } finally {
@@ -19383,7 +19730,7 @@ var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS =
19383
19730
  var init_lock = () => {};
19384
19731
 
19385
19732
  // src/utils/jsonl-rotation.ts
19386
- import { rename as rename2 } from "fs/promises";
19733
+ import { rename as rename3 } from "fs/promises";
19387
19734
  function shouldRotate(stats, thresholds, nextLineByteLength) {
19388
19735
  const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
19389
19736
  const entriesAfter = stats.currentEntryCount + 1;
@@ -19391,7 +19738,7 @@ function shouldRotate(stats, thresholds, nextLineByteLength) {
19391
19738
  }
19392
19739
  async function rotate(currentPath, previousPath) {
19393
19740
  try {
19394
- await rename2(currentPath, previousPath);
19741
+ await rename3(currentPath, previousPath);
19395
19742
  } catch {}
19396
19743
  }
19397
19744
  var init_jsonl_rotation = () => {};
@@ -19402,9 +19749,9 @@ var init_rotation = __esm(() => {
19402
19749
  });
19403
19750
 
19404
19751
  // src/core/audit/logger.ts
19405
- import { appendFile, mkdir as mkdir7, readFile as readFile3, stat } from "fs/promises";
19406
- import { join as join16 } from "path";
19407
- import { randomUUID } from "crypto";
19752
+ import { appendFile, mkdir as mkdir8, readFile as readFile3, stat as stat2 } from "fs/promises";
19753
+ import { join as join18 } from "path";
19754
+ import { randomUUID as randomUUID2 } from "crypto";
19408
19755
 
19409
19756
  class AuditLogger {
19410
19757
  auditDir;
@@ -19429,8 +19776,8 @@ class AuditLogger {
19429
19776
  this.enabled = opts.enabled;
19430
19777
  this.maxBytes = opts.rotation.maxBytes;
19431
19778
  this.maxEntries = opts.rotation.maxEntries;
19432
- this.auditDir = join16(opts.storagePath, ".dbcli", "audit");
19433
- this.auditFilePath = join16(this.auditDir, `${opts.connectionName}.jsonl`);
19779
+ this.auditDir = join18(opts.storagePath, ".dbcli", "audit");
19780
+ this.auditFilePath = join18(this.auditDir, `${opts.connectionName}.jsonl`);
19434
19781
  this.previousFilePath = `${this.auditFilePath}.1`;
19435
19782
  this.sessionIdService = opts.sessionIdService;
19436
19783
  this.lockManager = opts.lockManager ?? new AuditLockManager(this.auditFilePath);
@@ -19451,12 +19798,12 @@ class AuditLogger {
19451
19798
  try {
19452
19799
  const sessionId = await this.sessionIdService.resolve();
19453
19800
  this.cachedSessionId = sessionId;
19454
- await mkdir7(this.auditDir, { recursive: true });
19801
+ await mkdir8(this.auditDir, { recursive: true });
19455
19802
  if (!this.writerInitialized) {
19456
19803
  await this.syncCountersFromDisk();
19457
19804
  this.writerInitialized = true;
19458
19805
  }
19459
- const id = randomUUID();
19806
+ const id = randomUUID2();
19460
19807
  const ts = new Date().toISOString();
19461
19808
  const enriched = {
19462
19809
  ...entry,
@@ -19528,7 +19875,7 @@ class AuditLogger {
19528
19875
  }
19529
19876
  async syncCountersFromDisk() {
19530
19877
  try {
19531
- const s = await stat(this.auditFilePath);
19878
+ const s = await stat2(this.auditFilePath);
19532
19879
  this.currentSizeBytes = s.size;
19533
19880
  const raw = await readFile3(this.auditFilePath, "utf8");
19534
19881
  this.currentEntryCount = raw.split(`
@@ -19556,17 +19903,17 @@ var init_logger2 = __esm(() => {
19556
19903
  });
19557
19904
 
19558
19905
  // src/core/audit/session-id.ts
19559
- import { mkdir as mkdir8, readFile as readFile4, rename as rename3, stat as stat2, writeFile as writeFile2 } from "fs/promises";
19906
+ import { mkdir as mkdir9, readFile as readFile4, rename as rename4, stat as stat3, writeFile as writeFile3 } from "fs/promises";
19560
19907
  import { randomBytes as randomBytes2 } from "crypto";
19561
- import { dirname as dirname4, join as join17 } from "path";
19908
+ import { dirname as dirname6, join as join19 } from "path";
19562
19909
  function generateSessionId(pid, nowMs) {
19563
19910
  const random = randomBytes2(3).toString("hex");
19564
19911
  return `${pid}-${nowMs}-${random}`;
19565
19912
  }
19566
19913
  async function readSessionIdFile(storagePath) {
19567
- const target = join17(storagePath, LAST_SESSION_ID_RELATIVE);
19914
+ const target = join19(storagePath, LAST_SESSION_ID_RELATIVE);
19568
19915
  try {
19569
- await stat2(target);
19916
+ await stat3(target);
19570
19917
  } catch {
19571
19918
  return null;
19572
19919
  }
@@ -19586,12 +19933,12 @@ async function readSessionIdFile(storagePath) {
19586
19933
  }
19587
19934
  }
19588
19935
  async function writeSessionIdFile(storagePath, payload) {
19589
- const target = join17(storagePath, LAST_SESSION_ID_RELATIVE);
19936
+ const target = join19(storagePath, LAST_SESSION_ID_RELATIVE);
19590
19937
  const tmp = `${target}.tmp`;
19591
19938
  try {
19592
- await mkdir8(dirname4(target), { recursive: true });
19593
- await writeFile2(tmp, JSON.stringify(payload, null, 2), "utf8");
19594
- await rename3(tmp, target);
19939
+ await mkdir9(dirname6(target), { recursive: true });
19940
+ await writeFile3(tmp, JSON.stringify(payload, null, 2), "utf8");
19941
+ await rename4(tmp, target);
19595
19942
  } catch {}
19596
19943
  }
19597
19944
 
@@ -19686,7 +20033,8 @@ async function getAuditLogger(config, configPath, connectionName) {
19686
20033
  }
19687
20034
  async function writeAuditEntry(config, commandName, options, outcome) {
19688
20035
  try {
19689
- const logger = await getAuditLogger(config, options.config || ".dbcli", typeof options.connectionName === "string" ? options.connectionName : undefined);
20036
+ const connectionName = typeof options.connectionName === "string" && options.connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
20037
+ const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
19690
20038
  const engine = config.connection?.system || "postgresql";
19691
20039
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
19692
20040
  let tier = getEngineCapability(engine, commandName).tier;
@@ -19709,7 +20057,11 @@ async function writeAuditEntry(config, commandName, options, outcome) {
19709
20057
  ...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
19710
20058
  ...errorMessage && { error: errorMessage },
19711
20059
  ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
19712
- metadata: outcome.metadata
20060
+ metadata: {
20061
+ ...outcome.metadata ?? {},
20062
+ connection_name: connectionName,
20063
+ environment: config.effectiveEnvironment ?? null
20064
+ }
19713
20065
  };
19714
20066
  const result = await logger.write(entry);
19715
20067
  return "success" in result ? result.id : null;
@@ -19729,49 +20081,6 @@ var init_integration_helper = __esm(() => {
19729
20081
  _loggers = new Map;
19730
20082
  });
19731
20083
 
19732
- // src/utils/levenshtein-distance.ts
19733
- function levenshteinDistance(a, b) {
19734
- const lenA = a.length;
19735
- const lenB = b.length;
19736
- const dp = [];
19737
- for (let i = 0;i <= lenB; i++) {
19738
- const row = [];
19739
- for (let j2 = 0;j2 <= lenA; j2++) {
19740
- row[j2] = 0;
19741
- }
19742
- dp[i] = row;
19743
- }
19744
- for (let i = 0;i <= lenB; i++) {
19745
- const row = dp[i];
19746
- if (row !== undefined) {
19747
- row[0] = i;
19748
- }
19749
- }
19750
- for (let j2 = 0;j2 <= lenA; j2++) {
19751
- const firstRow = dp[0];
19752
- if (firstRow !== undefined) {
19753
- firstRow[j2] = j2;
19754
- }
19755
- }
19756
- for (let i = 1;i <= lenB; i++) {
19757
- const currentRow = dp[i];
19758
- const prevRow = dp[i - 1];
19759
- if (!currentRow || !prevRow)
19760
- continue;
19761
- for (let j2 = 1;j2 <= lenA; j2++) {
19762
- if (b.charAt(i - 1) === a.charAt(j2 - 1)) {
19763
- currentRow[j2] = prevRow[j2 - 1] ?? 0;
19764
- } else {
19765
- const sub = (prevRow[j2 - 1] ?? 0) + 1;
19766
- const ins = (currentRow[j2 - 1] ?? 0) + 1;
19767
- const del = (prevRow[j2] ?? 0) + 1;
19768
- currentRow[j2] = Math.min(sub, ins, del);
19769
- }
19770
- }
19771
- }
19772
- return dp[lenB]?.[lenA] ?? 0;
19773
- }
19774
-
19775
20084
  // src/utils/error-suggester.ts
19776
20085
  async function suggestTableName(errorMessage, adapter) {
19777
20086
  try {
@@ -21356,14 +21665,14 @@ var init_render_markdown = __esm(() => {
21356
21665
  });
21357
21666
 
21358
21667
  // src/core/recovery/last-envelope.ts
21359
- import { writeFile as writeFile3, readFile as readFile5, rename as rename4, mkdir as mkdir9, stat as stat3 } from "fs/promises";
21360
- import { dirname as dirname5, join as join18 } from "path";
21361
- import { randomUUID as randomUUID2 } from "crypto";
21668
+ import { writeFile as writeFile4, readFile as readFile5, rename as rename5, mkdir as mkdir10, stat as stat4 } from "fs/promises";
21669
+ import { dirname as dirname7, join as join20 } from "path";
21670
+ import { randomUUID as randomUUID3 } from "crypto";
21362
21671
  function sanitizeCommandSummary(argv) {
21363
21672
  return redactArgv(argv);
21364
21673
  }
21365
- async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID2(), auditRef) {
21366
- const target = join18(cwd, LAST_ENVELOPE_PATH);
21674
+ async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID3(), auditRef) {
21675
+ const target = join20(cwd, LAST_ENVELOPE_PATH);
21367
21676
  const tmp = `${target}.tmp`;
21368
21677
  const payload = {
21369
21678
  schemaVersion: 1,
@@ -21375,15 +21684,15 @@ async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id =
21375
21684
  envelope
21376
21685
  };
21377
21686
  try {
21378
- await mkdir9(dirname5(target), { recursive: true });
21379
- await writeFile3(tmp, JSON.stringify(payload, null, 2), "utf8");
21380
- await rename4(tmp, target);
21687
+ await mkdir10(dirname7(target), { recursive: true });
21688
+ await writeFile4(tmp, JSON.stringify(payload, null, 2), "utf8");
21689
+ await rename5(tmp, target);
21381
21690
  } catch {}
21382
21691
  }
21383
21692
  async function readLastEnvelope(cwd) {
21384
- const target = join18(cwd, LAST_ENVELOPE_PATH);
21693
+ const target = join20(cwd, LAST_ENVELOPE_PATH);
21385
21694
  try {
21386
- await stat3(target);
21695
+ await stat4(target);
21387
21696
  } catch {
21388
21697
  return null;
21389
21698
  }
@@ -21395,9 +21704,9 @@ async function readLastEnvelope(cwd) {
21395
21704
  }
21396
21705
  }
21397
21706
  async function readLastEnvelopeRaw(cwd) {
21398
- const target = join18(cwd, LAST_ENVELOPE_PATH);
21707
+ const target = join20(cwd, LAST_ENVELOPE_PATH);
21399
21708
  try {
21400
- await stat3(target);
21709
+ await stat4(target);
21401
21710
  } catch {
21402
21711
  return null;
21403
21712
  }
@@ -21420,13 +21729,13 @@ var init_last_envelope = __esm(() => {
21420
21729
 
21421
21730
  // src/core/recovery/emit.ts
21422
21731
  import { writeFileSync, mkdirSync, renameSync, writeSync } from "fs";
21423
- import { dirname as dirname6, join as join19 } from "path";
21424
- import { randomUUID as randomUUID3 } from "crypto";
21732
+ import { dirname as dirname8, join as join21 } from "path";
21733
+ import { randomUUID as randomUUID4 } from "crypto";
21425
21734
  function emitRecoveryEnvelope(error, ctx, options = {}) {
21426
21735
  const envelope = classifyError(error, ctx);
21427
21736
  const cwd = options.cwd ?? process.cwd();
21428
21737
  const argv = options.argv ?? buildArgvFromProcess();
21429
- const envelopeId = options.envelopeId ?? randomUUID3();
21738
+ const envelopeId = options.envelopeId ?? randomUUID4();
21430
21739
  writeLastEnvelopeSync(cwd, envelope, argv, envelopeId, options.auditRef);
21431
21740
  writeSync(1, renderJson(envelope, { brief: options.brief === true }) + `
21432
21741
  `);
@@ -21437,7 +21746,7 @@ function buildArgvFromProcess() {
21437
21746
  return ["dbcli", ...userArgs];
21438
21747
  }
21439
21748
  function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
21440
- const target = join19(cwd, LAST_ENVELOPE_PATH);
21749
+ const target = join21(cwd, LAST_ENVELOPE_PATH);
21441
21750
  const tmp = `${target}.tmp`;
21442
21751
  const payload = {
21443
21752
  schemaVersion: 1,
@@ -21449,7 +21758,7 @@ function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
21449
21758
  envelope
21450
21759
  };
21451
21760
  try {
21452
- mkdirSync(dirname6(target), { recursive: true });
21761
+ mkdirSync(dirname8(target), { recursive: true });
21453
21762
  writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf8");
21454
21763
  renameSync(tmp, target);
21455
21764
  } catch {}
@@ -22193,8 +22502,8 @@ function renderStep2(r) {
22193
22502
  var NEXT_SCHEMA_VERSION = 1, STEP_RESULT_SUMMARY_FIELD_CAP = 4096;
22194
22503
 
22195
22504
  // src/core/recovery/next-step-schema.ts
22196
- import { stat as stat4 } from "fs/promises";
22197
- import { resolve as resolve5 } from "path";
22505
+ import { stat as stat5 } from "fs/promises";
22506
+ import { resolve as resolve6 } from "path";
22198
22507
  function summarizeZodError(err) {
22199
22508
  return err.issues.map((iss) => {
22200
22509
  const path4 = iss.path.join(".") || "<root>";
@@ -22213,9 +22522,9 @@ async function loadStepResultSummary(arg, cwd) {
22213
22522
  }
22214
22523
  let raw;
22215
22524
  if (arg.startsWith("@")) {
22216
- const path4 = resolve5(cwd, arg.slice(1));
22525
+ const path4 = resolve6(cwd, arg.slice(1));
22217
22526
  try {
22218
- await stat4(path4);
22527
+ await stat5(path4);
22219
22528
  } catch {
22220
22529
  return { ok: false, reason: `--result @<file>: ${path4} not readable.` };
22221
22530
  }
@@ -22880,6 +23189,7 @@ var init_schema = __esm(() => {
22880
23189
  init_validation();
22881
23190
  init_connection_selector();
22882
23191
  init_error_suggester();
23192
+ init_config_path();
22883
23193
  ALLOWED_FORMATS2 = ["table", "json"];
22884
23194
  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").addOption(createConnectionSelectorOption()).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("--sample-method <method>", 'MongoDB only: "random" (default, uses $sample) or "natural" (uses find().limit()). Ignored on SQL connections.', "random").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
22885
23195
  });
@@ -22937,14 +23247,14 @@ var init_html_formatter = __esm(() => {
22937
23247
  });
22938
23248
 
22939
23249
  // src/utils/opener.ts
22940
- import open2 from "open";
23250
+ import open3 from "open";
22941
23251
  async function openInBrowser(target) {
22942
23252
  if (process.env.DBCLI_NO_OPEN === "1" || false) {
22943
23253
  console.log(`[Opener] DBCLI_NO_OPEN is set. Skipping browser launch for: ${target}`);
22944
23254
  return;
22945
23255
  }
22946
23256
  try {
22947
- await open2(target);
23257
+ await open3(target);
22948
23258
  } catch (err) {
22949
23259
  console.error(`[Opener] Failed to open ${target}:`, err);
22950
23260
  }
@@ -23369,7 +23679,7 @@ var init_query_size_guard = __esm(() => {
23369
23679
  // src/commands/query.ts
23370
23680
  import crypto3 from "crypto";
23371
23681
  import { tmpdir } from "os";
23372
- import { join as join20 } from "path";
23682
+ import { join as join22 } from "path";
23373
23683
  function requireSqlConnection2(connection) {
23374
23684
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
23375
23685
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -23680,7 +23990,7 @@ async function presentSingleResult(query, options, execution, tableCellLimit) {
23680
23990
  } : {}
23681
23991
  });
23682
23992
  if (options.ui) {
23683
- const tempPath = join20(tmpdir(), `dbcli-query-${Date.now()}.html`);
23993
+ const tempPath = join22(tmpdir(), `dbcli-query-${Date.now()}.html`);
23684
23994
  await Bun.write(tempPath, html);
23685
23995
  await openInBrowser(tempPath);
23686
23996
  } else {
@@ -23893,6 +24203,7 @@ var init_query = __esm(() => {
23893
24203
  init_query_executor();
23894
24204
  init_config();
23895
24205
  init_blacklist_validator();
24206
+ init_config_path();
23896
24207
  init_validation();
23897
24208
  init_applied_limit();
23898
24209
  init_integration_helper();
@@ -24335,6 +24646,7 @@ var ALLOWED_FORMATS4;
24335
24646
  var init_plan = __esm(() => {
24336
24647
  init_query_risk_analyzer();
24337
24648
  init_config();
24649
+ init_config_path();
24338
24650
  init_validation();
24339
24651
  init_integration_helper();
24340
24652
  ALLOWED_FORMATS4 = ["text", "json"];
@@ -24438,7 +24750,7 @@ __export(exports_q_mongo, {
24438
24750
  qMongoBranch: () => qMongoBranch
24439
24751
  });
24440
24752
  import { tmpdir as tmpdir2 } from "os";
24441
- import { join as join21 } from "path";
24753
+ import { join as join23 } from "path";
24442
24754
  async function qMongoBranch(snippet, prepared, options, config) {
24443
24755
  const collection = options.collection ?? prepared.execHints?.collection;
24444
24756
  if (!collection) {
@@ -24470,7 +24782,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
24470
24782
  ...securityNotification ? { securityNotification } : {}
24471
24783
  });
24472
24784
  if (options.ui) {
24473
- const tempPath = join21(tmpdir2(), `dbcli-report-${Date.now()}.html`);
24785
+ const tempPath = join23(tmpdir2(), `dbcli-report-${Date.now()}.html`);
24474
24786
  await Bun.write(tempPath, html);
24475
24787
  await openInBrowser(tempPath);
24476
24788
  } else {
@@ -24511,7 +24823,7 @@ var init_q_mongo = __esm(() => {
24511
24823
  // src/commands/q.ts
24512
24824
  import crypto4 from "crypto";
24513
24825
  import { tmpdir as tmpdir3 } from "os";
24514
- import { join as join22 } from "path";
24826
+ import { join as join24 } from "path";
24515
24827
  function formatDryRun(input) {
24516
24828
  const lines = ["Dry-run preview (no execution):"];
24517
24829
  if (input.family === "es") {
@@ -24612,7 +24924,7 @@ async function qCommand(name2, options, command) {
24612
24924
  ...securityNotification ? { securityNotification } : {}
24613
24925
  });
24614
24926
  if (options.ui) {
24615
- const tempPath = join22(tmpdir3(), `dbcli-report-${Date.now()}.html`);
24927
+ const tempPath = join24(tmpdir3(), `dbcli-report-${Date.now()}.html`);
24616
24928
  await Bun.write(tempPath, html);
24617
24929
  await openInBrowser(tempPath);
24618
24930
  } else {
@@ -24840,6 +25152,7 @@ var init_q = __esm(() => {
24840
25152
  init_message_loader();
24841
25153
  init_adapters();
24842
25154
  init_config();
25155
+ init_config_path();
24843
25156
  init_blacklist_validator();
24844
25157
  init_blacklist();
24845
25158
  init_permission_guard();
@@ -25060,8 +25373,8 @@ var exports_queries_rename = {};
25060
25373
  __export(exports_queries_rename, {
25061
25374
  queriesRename: () => queriesRename
25062
25375
  });
25063
- import { rename as rename5, mkdir as mkdir10 } from "fs/promises";
25064
- import { dirname as dirname7 } from "path";
25376
+ import { rename as rename6, mkdir as mkdir11 } from "fs/promises";
25377
+ import { dirname as dirname9 } from "path";
25065
25378
  async function queriesRename(oldName, newName, options = {}) {
25066
25379
  if (!oldName.startsWith("@") || !newName.startsWith("@")) {
25067
25380
  throw new Error(`Both names must start with '@'`);
@@ -25082,8 +25395,8 @@ async function queriesRename(oldName, newName, options = {}) {
25082
25395
  for (const v of local) {
25083
25396
  const dst = snippetKeyToFile(cwd, newName, "local");
25084
25397
  const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
25085
- await mkdir10(dirname7(dstWithSuffix), { recursive: true });
25086
- await rename5(v.query.file, dstWithSuffix);
25398
+ await mkdir11(dirname9(dstWithSuffix), { recursive: true });
25399
+ await rename6(v.query.file, dstWithSuffix);
25087
25400
  await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
25088
25401
  console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
25089
25402
  }
@@ -25108,8 +25421,8 @@ var exports_queries_copy = {};
25108
25421
  __export(exports_queries_copy, {
25109
25422
  queriesCopy: () => queriesCopy
25110
25423
  });
25111
- import { mkdir as mkdir11, copyFile } from "fs/promises";
25112
- import { dirname as dirname8, basename as basename4 } from "path";
25424
+ import { mkdir as mkdir12, copyFile } from "fs/promises";
25425
+ import { dirname as dirname10, basename as basename4 } from "path";
25113
25426
  async function queriesCopy(src, dst, options = {}) {
25114
25427
  if (!src.startsWith("@") || !dst.startsWith("@")) {
25115
25428
  throw new Error(`Both names must start with '@'`);
@@ -25127,7 +25440,7 @@ async function queriesCopy(src, dst, options = {}) {
25127
25440
  }
25128
25441
  for (const v of variants) {
25129
25442
  const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
25130
- await mkdir11(dirname8(dstFile), { recursive: true });
25443
+ await mkdir12(dirname10(dstFile), { recursive: true });
25131
25444
  await copyFile(v.query.file, dstFile);
25132
25445
  console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
25133
25446
  }
@@ -25145,21 +25458,21 @@ var exports_queries_import = {};
25145
25458
  __export(exports_queries_import, {
25146
25459
  queriesImport: () => queriesImport
25147
25460
  });
25148
- import { stat as stat5, mkdir as mkdir12, copyFile as copyFile2 } from "fs/promises";
25149
- import { basename as basename5, join as join23, extname } from "path";
25461
+ import { stat as stat6, mkdir as mkdir13, copyFile as copyFile2 } from "fs/promises";
25462
+ import { basename as basename5, join as join25, extname as extname2 } from "path";
25150
25463
  async function queriesImport(filePath, options = {}) {
25151
25464
  const cwd = options.cwd ?? process.cwd();
25152
- if (extname(filePath) !== ".sql") {
25465
+ if (extname2(filePath) !== ".sql") {
25153
25466
  throw new Error(`Expected .sql file, got ${filePath}`);
25154
25467
  }
25155
- await stat5(filePath);
25468
+ await stat6(filePath);
25156
25469
  const text2 = await Bun.file(filePath).text();
25157
25470
  const baseName = options.as ? options.as.replace(/^@/, "") : basename5(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
25158
25471
  const key = "@" + baseName;
25159
25472
  parseSavedQuery({ key, file: filePath, source: "local", text: text2 });
25160
- const targetDir = join23(cwd, ".dbcli/queries");
25161
- await mkdir12(targetDir, { recursive: true });
25162
- const target = join23(targetDir, basename5(filePath));
25473
+ const targetDir = join25(cwd, ".dbcli/queries");
25474
+ await mkdir13(targetDir, { recursive: true });
25475
+ const target = join25(targetDir, basename5(filePath));
25163
25476
  if (await Bun.file(target).exists()) {
25164
25477
  if (!options.force) {
25165
25478
  const ok = await dist_default4({ message: `Overwrite ${target}?`, default: false });
@@ -25180,7 +25493,7 @@ var exports_queries_export = {};
25180
25493
  __export(exports_queries_export, {
25181
25494
  queriesExport: () => queriesExport
25182
25495
  });
25183
- import { writeFile as writeFile4 } from "fs/promises";
25496
+ import { writeFile as writeFile5 } from "fs/promises";
25184
25497
  async function queriesExport(name2, options = {}) {
25185
25498
  const cwd = options.cwd ?? process.cwd();
25186
25499
  const dirs = resolveSnippetDirs(cwd);
@@ -25201,7 +25514,7 @@ async function queriesExport(name2, options = {}) {
25201
25514
  }
25202
25515
  const text2 = await Bun.file(chosen[0].query.file).text();
25203
25516
  if (options.output) {
25204
- await writeFile4(options.output, text2, "utf8");
25517
+ await writeFile5(options.output, text2, "utf8");
25205
25518
  console.log(`wrote ${options.output}`);
25206
25519
  } else {
25207
25520
  process.stdout.write(text2);
@@ -25212,12 +25525,12 @@ var init_queries_export = __esm(() => {
25212
25525
  });
25213
25526
 
25214
25527
  // src/commands/queries.ts
25215
- import { mkdir as mkdir13, writeFile as writeFile5 } from "fs/promises";
25216
- import { dirname as dirname9 } from "path";
25528
+ import { mkdir as mkdir14, writeFile as writeFile6 } from "fs/promises";
25529
+ import { dirname as dirname11 } from "path";
25217
25530
  import { spawn } from "child_process";
25218
- async function deriveEngine() {
25531
+ async function deriveEngine(command) {
25219
25532
  try {
25220
- const cfg = await configModule.read(resolveConfigPath(undefined, {}));
25533
+ const cfg = await configModule.read(resolveConfigPath(command));
25221
25534
  if (cfg.connection) {
25222
25535
  return mapSystemToEngine(cfg.connection.system);
25223
25536
  }
@@ -25279,10 +25592,10 @@ function matchesFolded(r, opts) {
25279
25592
  return false;
25280
25593
  return true;
25281
25594
  }
25282
- async function queriesShow(name2, options) {
25595
+ async function queriesShow(name2, options, command) {
25283
25596
  const map = await loadSnippets(resolveSnippetDirs(process.cwd()));
25284
25597
  try {
25285
- const engine = await deriveEngine();
25598
+ const engine = await deriveEngine(command);
25286
25599
  const snippet = resolveByName(map, name2, engine);
25287
25600
  if (options.format === "json") {
25288
25601
  console.log(JSON.stringify({ ...snippetToJson(snippet), sql: snippet.query.sqlBody.trim() }, null, 2));
@@ -25321,8 +25634,8 @@ async function queriesNew(name2, options) {
25321
25634
  process.exit(1);
25322
25635
  return;
25323
25636
  }
25324
- await mkdir13(dirname9(file), { recursive: true });
25325
- await writeFile5(file, scaffold(name2), "utf8");
25637
+ await mkdir14(dirname11(file), { recursive: true });
25638
+ await writeFile6(file, scaffold(name2), "utf8");
25326
25639
  console.log(`Created ${file}`);
25327
25640
  if (source === "shared")
25328
25641
  console.log(t("queries.first_run_hint"));
@@ -25383,7 +25696,7 @@ async function queriesCheck(options) {
25383
25696
  console.log(`\u2713 ${total} snippets parsed successfully`);
25384
25697
  }
25385
25698
  }
25386
- async function queriesSearch(keywords, options) {
25699
+ async function queriesSearch(keywords, options, command) {
25387
25700
  const query = keywords.join(" ").trim();
25388
25701
  if (!query) {
25389
25702
  console.error(t("queries.search_no_keywords"));
@@ -25408,7 +25721,7 @@ async function queriesSearch(keywords, options) {
25408
25721
  if (options.engine && options.engine !== "all") {
25409
25722
  engineFilter = options.engine;
25410
25723
  } else if (!options.engine) {
25411
- const inferred = await deriveEngineOrNull();
25724
+ const inferred = await deriveEngineOrNull(command);
25412
25725
  if (inferred)
25413
25726
  engineFilter = inferred;
25414
25727
  else
@@ -25431,9 +25744,9 @@ async function queriesSearch(keywords, options) {
25431
25744
  }
25432
25745
  renderSearchTable(hits, options.includeInternal === true);
25433
25746
  }
25434
- async function deriveEngineOrNull() {
25747
+ async function deriveEngineOrNull(command) {
25435
25748
  try {
25436
- const cfg = await configModule.read(resolveConfigPath(undefined, {}));
25749
+ const cfg = await configModule.read(resolveConfigPath(command));
25437
25750
  if (cfg.connection) {
25438
25751
  return mapSystemToEngine(cfg.connection.system);
25439
25752
  }
@@ -25466,7 +25779,7 @@ function renderSearchTable(hits, includeScore) {
25466
25779
  for (const r of rows)
25467
25780
  console.log(fmt(r));
25468
25781
  }
25469
- async function queriesSuggest(intent, options) {
25782
+ async function queriesSuggest(intent, options, command) {
25470
25783
  const map = await loadSnippets(resolveSnippetDirs(process.cwd()));
25471
25784
  const folded = [...map.entries()].map(([key, variants]) => foldVariants(key, variants));
25472
25785
  const availableIntents = collectIntentPrefixes(folded);
@@ -25492,7 +25805,7 @@ async function queriesSuggest(intent, options) {
25492
25805
  if (options.engine && options.engine !== "all") {
25493
25806
  engineFilter = options.engine;
25494
25807
  } else if (!options.engine) {
25495
- const inferred = await deriveEngineOrNull();
25808
+ const inferred = await deriveEngineOrNull(command);
25496
25809
  if (inferred)
25497
25810
  engineFilter = inferred;
25498
25811
  else
@@ -25561,9 +25874,9 @@ function scaffold(name2) {
25561
25874
  }
25562
25875
  async function openInEditor(file) {
25563
25876
  const editor = process.env.EDITOR || "vi";
25564
- await new Promise((resolve6, reject) => {
25877
+ await new Promise((resolve7, reject) => {
25565
25878
  const child = spawn(editor, [file], { stdio: "inherit" });
25566
- child.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`Editor exited with ${code}`)));
25879
+ child.on("exit", (code) => code === 0 ? resolve7() : reject(new Error(`Editor exited with ${code}`)));
25567
25880
  });
25568
25881
  }
25569
25882
  var INTENT_RE_CLI, queriesCommand;
@@ -25571,6 +25884,7 @@ var init_queries = __esm(() => {
25571
25884
  init_esm();
25572
25885
  init_message_loader();
25573
25886
  init_config();
25887
+ init_config_path();
25574
25888
  init_saved_queries();
25575
25889
  init_fold();
25576
25890
  init_search();
@@ -25580,8 +25894,8 @@ var init_queries = __esm(() => {
25580
25894
  queriesCommand.command("list").description(t("queries.list_description")).option("--format <type>", "Output format: table, json, csv", "table").option("--tag <tag>", "Filter by tag").option("--engine <engine>", "Filter by engine: postgres | mysql").option("--source <source>", "Filter by source: local | shared").action(async (options) => {
25581
25895
  await queriesList(options);
25582
25896
  });
25583
- queriesCommand.command("show <name>").description(t("queries.show_description")).option("--format <type>", "Output format: table, json, csv", "table").action(async (name2, options) => {
25584
- await queriesShow(name2, options);
25897
+ queriesCommand.command("show <name>").description(t("queries.show_description")).option("--format <type>", "Output format: table, json, csv", "table").action(async (name2, options, command) => {
25898
+ await queriesShow(name2, options, command);
25585
25899
  });
25586
25900
  queriesCommand.command("new <name>").description(t("queries.new_description")).option("--local", "Create under .dbcli/queries (gitignored) instead of .dbcli-shared/").option("--edit", "Open the created file in $EDITOR").action(async (name2, options) => {
25587
25901
  await queriesNew(name2, options);
@@ -25637,11 +25951,11 @@ var init_queries = __esm(() => {
25637
25951
  process.exit(1);
25638
25952
  }
25639
25953
  });
25640
- queriesCommand.command("search [keywords...]").description(t("queries.search_description")).option("--format <type>", "Output format: table, json", "table").option("--engine <engine>", "Filter: postgres | mysql | redis | elasticsearch | all").option("--source <source>", "Filter: local | shared | builtin | all").option("--limit <n>", "Max results (default 10)").option("--include-internal", "Show ranking score").action(async (keywords, options) => {
25641
- await queriesSearch(keywords, options);
25954
+ queriesCommand.command("search [keywords...]").description(t("queries.search_description")).option("--format <type>", "Output format: table, json", "table").option("--engine <engine>", "Filter: postgres | mysql | redis | elasticsearch | all").option("--source <source>", "Filter: local | shared | builtin | all").option("--limit <n>", "Max results (default 10)").option("--include-internal", "Show ranking score").action(async (keywords, options, command) => {
25955
+ await queriesSearch(keywords, options, command);
25642
25956
  });
25643
- queriesCommand.command("suggest [intent]").description(t("queries.suggest_description")).option("--format <type>", "Output format: table, json", "table").option("--engine <engine>", "Filter: postgres | mysql | redis | elasticsearch | all").option("--source <source>", "Filter: local | shared | builtin | all").action(async (intent, options) => {
25644
- await queriesSuggest(intent, options);
25957
+ queriesCommand.command("suggest [intent]").description(t("queries.suggest_description")).option("--format <type>", "Output format: table, json", "table").option("--engine <engine>", "Filter: postgres | mysql | redis | elasticsearch | all").option("--source <source>", "Filter: local | shared | builtin | all").action(async (intent, options, command) => {
25958
+ await queriesSuggest(intent, options, command);
25645
25959
  });
25646
25960
  });
25647
25961
 
@@ -26723,6 +27037,7 @@ var init_dml_plan4 = __esm(() => {
26723
27037
  init_query_risk_analyzer();
26724
27038
  init_config();
26725
27039
  init_plan();
27040
+ init_config_path();
26726
27041
  init_validation();
26727
27042
  init_dml_plan_sql();
26728
27043
  init_dml_plan();
@@ -27003,6 +27318,7 @@ var init_insert = __esm(() => {
27003
27318
  init_permission_guard();
27004
27319
  init_blacklist_validator();
27005
27320
  init_blacklist();
27321
+ init_config_path();
27006
27322
  init_dml_plan4();
27007
27323
  init_integration_helper();
27008
27324
  });
@@ -27305,6 +27621,7 @@ var init_update = __esm(() => {
27305
27621
  init_permission_guard();
27306
27622
  init_blacklist_validator();
27307
27623
  init_blacklist();
27624
+ init_config_path();
27308
27625
  init_dml_plan4();
27309
27626
  init_integration_helper();
27310
27627
  });
@@ -27591,6 +27908,7 @@ var init_delete = __esm(() => {
27591
27908
  init_permission_guard();
27592
27909
  init_blacklist_validator();
27593
27910
  init_blacklist();
27911
+ init_config_path();
27594
27912
  init_dml_plan4();
27595
27913
  init_integration_helper();
27596
27914
  });
@@ -28012,6 +28330,7 @@ var init_export = __esm(() => {
28012
28330
  init_query_executor();
28013
28331
  init_config();
28014
28332
  init_prompts();
28333
+ init_config_path();
28015
28334
  init_blacklist_validator();
28016
28335
  init_applied_limit();
28017
28336
  init_integration_helper();
@@ -28037,15 +28356,15 @@ var init_types6 = __esm(() => {
28037
28356
  });
28038
28357
 
28039
28358
  // src/core/agent-tasks/task-paths.ts
28040
- import { join as join24 } from "path";
28359
+ import { join as join26 } from "path";
28041
28360
  function resolveBuiltinDir2() {
28042
28361
  return packageAssetPath("tasks");
28043
28362
  }
28044
28363
  function resolveAgentTaskDirs(workspaceRoot) {
28045
28364
  return {
28046
28365
  builtinDir: resolveBuiltinDir2(),
28047
- sharedDir: join24(workspaceRoot, ".dbcli-shared", "tasks"),
28048
- localDir: join24(workspaceRoot, ".dbcli", "tasks")
28366
+ sharedDir: join26(workspaceRoot, ".dbcli-shared", "tasks"),
28367
+ localDir: join26(workspaceRoot, ".dbcli", "tasks")
28049
28368
  };
28050
28369
  }
28051
28370
  var init_task_paths = __esm(() => {
@@ -28192,7 +28511,7 @@ var init_parser2 = __esm(() => {
28192
28511
 
28193
28512
  // src/core/agent-tasks/loader.ts
28194
28513
  import { readdir as readdir3 } from "fs/promises";
28195
- import { join as join25, relative as relative2, sep as sep4 } from "path";
28514
+ import { join as join27, relative as relative2, sep as sep4 } from "path";
28196
28515
  async function loadAgentTasks(opts, flags) {
28197
28516
  const errors3 = [];
28198
28517
  const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
@@ -28249,7 +28568,7 @@ async function safeCollectMd(root) {
28249
28568
  return;
28250
28569
  }
28251
28570
  for (const e of entries) {
28252
- const full = join25(dir, e.name);
28571
+ const full = join27(dir, e.name);
28253
28572
  if (e.isDirectory())
28254
28573
  await walk(full);
28255
28574
  else
@@ -28760,9 +29079,19 @@ function getOrInitBlacklist(config) {
28760
29079
  columns: config.blacklist.columns ? { ...config.blacklist.columns } : {}
28761
29080
  };
28762
29081
  }
28763
- async function blacklistList(configPath) {
29082
+ async function blacklistList(configPath, format = "text") {
28764
29083
  const config = await configModule.read(configPath);
28765
29084
  const blacklist = getOrInitBlacklist(config);
29085
+ const audit = auditBlacklistPatterns(blacklist);
29086
+ if (format === "json") {
29087
+ const result = {
29088
+ tables: blacklist.tables,
29089
+ columns: blacklist.columns,
29090
+ warnings: audit.warnings
29091
+ };
29092
+ console.log(JSON.stringify(result, null, 2));
29093
+ return;
29094
+ }
28766
29095
  console.log(t("blacklist.list_title"));
28767
29096
  console.log("\u2500".repeat(40));
28768
29097
  if (blacklist.tables.length === 0 && Object.keys(blacklist.columns).length === 0) {
@@ -28781,7 +29110,6 @@ async function blacklistList(configPath) {
28781
29110
  } else {
28782
29111
  console.log(`${t("blacklist.columns_label")}: {}`);
28783
29112
  }
28784
- const audit = auditBlacklistPatterns(blacklist);
28785
29113
  for (const w2 of audit.warnings) {
28786
29114
  console.error(`\u26A0 blacklist.columns["${w2.collection}"]: '${w2.raw}' is ignored on mongo connections (${w2.reason}).`);
28787
29115
  }
@@ -28871,12 +29199,15 @@ var init_blacklist2 = __esm(() => {
28871
29199
  init_esm();
28872
29200
  init_message_loader();
28873
29201
  init_config();
29202
+ init_validation();
28874
29203
  VALID_TABLE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
28875
29204
  VALID_COLUMN_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
28876
29205
  blacklistCommand = new Command("blacklist").description(t("blacklist.description"));
28877
- blacklistCommand.command("list").description(t("blacklist.list_title")).option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).action(async (options) => {
29206
+ blacklistCommand.command("list").description(t("blacklist.list_title")).option("--config <path>", "Path to .dbcli config file", DEFAULT_CONFIG_PATH).option("--format <type>", "Output format: text, json", "text").action(async (options) => {
28878
29207
  try {
28879
- await blacklistList(options.config || DEFAULT_CONFIG_PATH);
29208
+ const format = options.format || "text";
29209
+ validateFormat(format, ["text", "json"], "blacklist list");
29210
+ await blacklistList(options.config || DEFAULT_CONFIG_PATH, format);
28880
29211
  } catch (error) {
28881
29212
  console.error(error.message);
28882
29213
  process.exit(1);
@@ -81115,7 +81446,7 @@ ${fence}`;
81115
81446
 
81116
81447
  // src/commands/diff.ts
81117
81448
  import crypto9 from "crypto";
81118
- import { resolve as resolve6 } from "path";
81449
+ import { resolve as resolve7 } from "path";
81119
81450
  function requireSqlConnection8(connection) {
81120
81451
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
81121
81452
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -81142,7 +81473,7 @@ async function expandOrmPaths(inputs) {
81142
81473
  const expanded = new Set;
81143
81474
  for (const path4 of paths) {
81144
81475
  if (!hasGlobMagic(path4)) {
81145
- expanded.add(resolve6(path4));
81476
+ expanded.add(resolve7(path4));
81146
81477
  continue;
81147
81478
  }
81148
81479
  const matches = await Array.fromAsync(new Bun.Glob(path4).scan({
@@ -81154,7 +81485,7 @@ async function expandOrmPaths(inputs) {
81154
81485
  throw new Error(`ORM schema glob matched no files: ${path4}`);
81155
81486
  }
81156
81487
  for (const match of matches)
81157
- expanded.add(resolve6(match));
81488
+ expanded.add(resolve7(match));
81158
81489
  }
81159
81490
  return [...expanded].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
81160
81491
  }
@@ -81490,6 +81821,7 @@ var ALLOWED_FORMATS8, statusCommand;
81490
81821
  var init_status2 = __esm(() => {
81491
81822
  init_esm();
81492
81823
  init_config();
81824
+ init_config_path();
81493
81825
  init_validation();
81494
81826
  ALLOWED_FORMATS8 = ["text", "json"];
81495
81827
  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) => {
@@ -81601,7 +81933,7 @@ var init_collect_snippets = __esm(() => {
81601
81933
  });
81602
81934
 
81603
81935
  // src/core/inspect/collect-schema-cache.ts
81604
- import { join as join26 } from "path";
81936
+ import { join as join28 } from "path";
81605
81937
  async function collectSchemaCache(opts) {
81606
81938
  const warnings = [];
81607
81939
  if (opts.system && !SQL_SYSTEMS2.includes(opts.system)) {
@@ -81611,7 +81943,7 @@ async function collectSchemaCache(opts) {
81611
81943
  };
81612
81944
  }
81613
81945
  const root = resolveSchemaPath(opts.dbcliPath, opts.connectionName);
81614
- const indexPath = join26(root, "index.json");
81946
+ const indexPath = join28(root, "index.json");
81615
81947
  const file = Bun.file(indexPath);
81616
81948
  if (!await file.exists()) {
81617
81949
  return { section: { available: false }, warnings };
@@ -81693,7 +82025,7 @@ var SAMPLE_SIZE = 10;
81693
82025
  // src/core/inspect/collect-version.ts
81694
82026
  async function collectVersion(adapter, timeoutMs) {
81695
82027
  const probe = adapter.getServerVersion().then((v) => typeof v === "string" && v !== "unknown" ? v : null, () => null);
81696
- const timeout = new Promise((resolve7) => setTimeout(() => resolve7(null), timeoutMs));
82028
+ const timeout = new Promise((resolve8) => setTimeout(() => resolve8(null), timeoutMs));
81697
82029
  return Promise.race([probe, timeout]);
81698
82030
  }
81699
82031
 
@@ -81881,7 +82213,7 @@ function buildHints(snap, ctx = {}) {
81881
82213
  }
81882
82214
 
81883
82215
  // src/core/inspect/collector.ts
81884
- import { join as join27 } from "path";
82216
+ import { join as join29 } from "path";
81885
82217
  async function collectInspect(opts) {
81886
82218
  const warnings = [];
81887
82219
  let config = null;
@@ -81923,7 +82255,7 @@ async function collectInspect(opts) {
81923
82255
  probeError = err;
81924
82256
  return "error";
81925
82257
  });
81926
- const timer = new Promise((resolve7) => setTimeout(() => resolve7("timeout"), probeTimeout));
82258
+ const timer = new Promise((resolve8) => setTimeout(() => resolve8("timeout"), probeTimeout));
81927
82259
  const outcome = await Promise.race([probe, timer]);
81928
82260
  if (outcome === "timeout") {
81929
82261
  warnings.push(`probe: timed out after ${probeTimeout}ms`);
@@ -81965,11 +82297,11 @@ async function collectInspect(opts) {
81965
82297
  return { ...snapWithoutSuggestions, suggestedCommands, hints, warnings };
81966
82298
  }
81967
82299
  async function hasConfig(configPath) {
81968
- if (await Bun.file(join27(configPath, "config.json")).exists())
82300
+ if (await Bun.file(join29(configPath, "config.json")).exists())
81969
82301
  return true;
81970
82302
  if (await Bun.file(configPath).exists()) {
81971
- const stat6 = await Bun.file(configPath).stat().catch(() => null);
81972
- return stat6?.isFile() === true;
82303
+ const stat7 = await Bun.file(configPath).stat().catch(() => null);
82304
+ return stat7?.isFile() === true;
81973
82305
  }
81974
82306
  return false;
81975
82307
  }
@@ -82132,6 +82464,7 @@ var ALLOWED_FORMATS9, inspectCommand;
82132
82464
  var init_inspect2 = __esm(() => {
82133
82465
  init_esm();
82134
82466
  init_message_loader();
82467
+ init_config_path();
82135
82468
  init_validation();
82136
82469
  init_inspect();
82137
82470
  init_config();
@@ -82311,7 +82644,7 @@ async function runDiagnostic(input) {
82311
82644
  const indexParams = family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
82312
82645
  return input.adapter.execute(prepared.driver.sql, family === "sql" ? prepared.driver.values : indexParams);
82313
82646
  })();
82314
- const timer = new Promise((resolve7) => setTimeout(() => resolve7("timeout"), input.timeoutMs));
82647
+ const timer = new Promise((resolve8) => setTimeout(() => resolve8("timeout"), input.timeoutMs));
82315
82648
  let outcome;
82316
82649
  let errorMessage2 = null;
82317
82650
  try {
@@ -82617,6 +82950,7 @@ var ALLOWED_FORMATS10, reportCommand;
82617
82950
  var init_report2 = __esm(() => {
82618
82951
  init_esm();
82619
82952
  init_message_loader();
82953
+ init_config_path();
82620
82954
  init_validation();
82621
82955
  init_config();
82622
82956
  init_integration_helper();
@@ -83058,6 +83392,7 @@ var ALLOWED_FORMATS11, guideCommand;
83058
83392
  var init_guide2 = __esm(() => {
83059
83393
  init_esm();
83060
83394
  init_message_loader();
83395
+ init_config_path();
83061
83396
  init_validation();
83062
83397
  init_config();
83063
83398
  init_integration_helper();
@@ -83946,6 +84281,7 @@ var FORMATS, CONFIDENCES;
83946
84281
  var init_guide_missing_index = __esm(() => {
83947
84282
  init_adapters();
83948
84283
  init_config();
84284
+ init_config_path();
83949
84285
  init_saved_queries();
83950
84286
  init_missing_index();
83951
84287
  init_parse_sql();
@@ -84249,6 +84585,7 @@ var init_explain3 = __esm(() => {
84249
84585
  init_esm();
84250
84586
  init_adapters();
84251
84587
  init_config();
84588
+ init_config_path();
84252
84589
  init_runner2();
84253
84590
  init_bulk_runner();
84254
84591
  init_explain2();
@@ -85049,9 +85386,9 @@ function relationBindings(statement) {
85049
85386
  const bindings = [];
85050
85387
  for (const item of statement.from) {
85051
85388
  const source = item;
85052
- const join28 = typeof source.join === "string" ? source.join.toUpperCase() : "";
85053
- const nullExtendsPrevious = join28.startsWith("RIGHT") || join28.startsWith("FULL");
85054
- const nullExtendsCurrent = join28.startsWith("LEFT") || join28.startsWith("FULL");
85389
+ const join30 = typeof source.join === "string" ? source.join.toUpperCase() : "";
85390
+ const nullExtendsPrevious = join30.startsWith("RIGHT") || join30.startsWith("FULL");
85391
+ const nullExtendsCurrent = join30.startsWith("LEFT") || join30.startsWith("FULL");
85055
85392
  if (nullExtendsPrevious) {
85056
85393
  for (const binding of bindings)
85057
85394
  binding.nullExtended = true;
@@ -86220,6 +86557,7 @@ var init_lint = __esm(() => {
86220
86557
  init_context2();
86221
86558
  init_saved_queries();
86222
86559
  init_integration_helper();
86560
+ init_config_path();
86223
86561
  FORMATS2 = ["text", "json", "markdown"];
86224
86562
  SEVERITIES = ["info", "warn", "error"];
86225
86563
  SQL_SYSTEMS5 = ["postgresql", "mysql", "mariadb"];
@@ -86245,9 +86583,9 @@ var init_lint = __esm(() => {
86245
86583
  });
86246
86584
 
86247
86585
  // src/core/result-snapshot/fingerprint.ts
86248
- import { createHash as createHash2 } from "crypto";
86586
+ import { createHash as createHash3 } from "crypto";
86249
86587
  function sha256(input) {
86250
- return createHash2("sha256").update(input).digest("hex");
86588
+ return createHash3("sha256").update(input).digest("hex");
86251
86589
  }
86252
86590
  function isNumeric(values) {
86253
86591
  return values.length > 0 && values.every((v) => typeof v === "number");
@@ -86366,7 +86704,7 @@ var init_serializer = __esm(() => {
86366
86704
  });
86367
86705
 
86368
86706
  // src/commands/snapshot.ts
86369
- import { join as join28 } from "path";
86707
+ import { join as join30 } from "path";
86370
86708
  function requireSqlConnection9(connection) {
86371
86709
  if (!SQL_SYSTEMS6.includes(connection.system)) {
86372
86710
  throw new Error(`snapshot currently supports SQL engines only, got: ${connection.system}`);
@@ -86379,13 +86717,14 @@ function pad2(n) {
86379
86717
  function defaultSnapshotPath() {
86380
86718
  const d = new Date;
86381
86719
  const stamp = `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
86382
- return join28(".dbcli", "snapshots", `snap-${stamp}.json`);
86720
+ return join30(".dbcli", "snapshots", `snap-${stamp}.json`);
86383
86721
  }
86384
86722
  var ALLOWED_FORMATS12, SQL_SYSTEMS6, snapshotCommand;
86385
86723
  var init_snapshot = __esm(() => {
86386
86724
  init_esm();
86387
86725
  init_adapters();
86388
86726
  init_config();
86727
+ init_config_path();
86389
86728
  init_validation();
86390
86729
  init_blacklist_validator();
86391
86730
  init_query_executor();
@@ -86630,6 +86969,7 @@ var init_assert = __esm(() => {
86630
86969
  init_esm();
86631
86970
  init_adapters();
86632
86971
  init_config();
86972
+ init_config_path();
86633
86973
  init_validation();
86634
86974
  init_blacklist_validator();
86635
86975
  init_query_executor();
@@ -88676,6 +89016,7 @@ var init_verify2 = __esm(() => {
88676
89016
  init_esm();
88677
89017
  init_adapters();
88678
89018
  init_config();
89019
+ init_config_path();
88679
89020
  init_blacklist_validator();
88680
89021
  init_query_executor();
88681
89022
  init_query_risk_analyzer();
@@ -89104,11 +89445,11 @@ var init_envelope_schema = __esm(() => {
89104
89445
  });
89105
89446
 
89106
89447
  // src/commands/recover.ts
89107
- import { stat as stat6 } from "fs/promises";
89108
- import { resolve as resolve7 } from "path";
89448
+ import { stat as stat7 } from "fs/promises";
89449
+ import { resolve as resolve8 } from "path";
89109
89450
  async function resolveApplySource(opts) {
89110
89451
  if (opts.from !== undefined) {
89111
- const path5 = resolve7(opts.cwd, opts.from);
89452
+ const path5 = resolve8(opts.cwd, opts.from);
89112
89453
  let raw;
89113
89454
  try {
89114
89455
  raw = await Bun.file(path5).text();
@@ -89128,7 +89469,7 @@ async function resolveApplySource(opts) {
89128
89469
  }
89129
89470
  const saved2 = r3.value;
89130
89471
  try {
89131
- await stat6(saved2.cwd);
89472
+ await stat7(saved2.cwd);
89132
89473
  } catch {
89133
89474
  throw new RecoverCliError(`--from ${opts.from}: saved cwd '${saved2.cwd}' no longer exists.`, EXIT_CODE.malformed);
89134
89475
  }
@@ -89162,7 +89503,7 @@ async function resolveApplySource(opts) {
89162
89503
  }
89163
89504
  const saved = r.value;
89164
89505
  try {
89165
- await stat6(saved.cwd);
89506
+ await stat7(saved.cwd);
89166
89507
  } catch {
89167
89508
  throw new RecoverCliError(`Auto-saved ${LAST_ENVELOPE_PATH} references cwd '${saved.cwd}' that no longer exists.`, EXIT_CODE.malformed);
89168
89509
  }
@@ -89364,13 +89705,13 @@ Verification artifact: ${verificationArtifactPath}`);
89364
89705
  });
89365
89706
 
89366
89707
  // src/commands/audit.ts
89367
- import { rm as rm3, stat as stat7 } from "fs/promises";
89368
- import { join as join29 } from "path";
89708
+ import { rm as rm3, stat as stat8 } from "fs/promises";
89709
+ import { join as join31 } from "path";
89369
89710
  async function resolveAuditPaths(configPath, config) {
89370
89711
  const storagePath = await resolveConfigStoragePath(configPath);
89371
89712
  const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
89372
- const auditDir = join29(storagePath, ".dbcli", "audit");
89373
- const auditFile = join29(auditDir, `${connName}.jsonl`);
89713
+ const auditDir = join31(storagePath, ".dbcli", "audit");
89714
+ const auditFile = join31(auditDir, `${connName}.jsonl`);
89374
89715
  return { auditDir, connectionName: connName, auditFile };
89375
89716
  }
89376
89717
  function isAuditDisabled(config) {
@@ -89519,7 +89860,7 @@ function renderHealthTable(h) {
89519
89860
  `);
89520
89861
  }
89521
89862
  function readLineFromStdinWithStderrPrompt(prompt) {
89522
- return new Promise((resolve8) => {
89863
+ return new Promise((resolve9) => {
89523
89864
  process.stderr.write(prompt);
89524
89865
  const chunks = [];
89525
89866
  let data = "";
@@ -89532,12 +89873,12 @@ function readLineFromStdinWithStderrPrompt(prompt) {
89532
89873
  process.stdin.pause();
89533
89874
  process.stdin.removeListener("data", onData);
89534
89875
  process.stdin.removeListener("end", onEnd);
89535
- resolve8((lines[0] ?? "").trim());
89876
+ resolve9((lines[0] ?? "").trim());
89536
89877
  }
89537
89878
  };
89538
89879
  const onEnd = () => {
89539
89880
  process.stdin.removeListener("data", onData);
89540
- resolve8(data.trim());
89881
+ resolve9(data.trim());
89541
89882
  };
89542
89883
  process.stdin.on("data", onData);
89543
89884
  process.stdin.on("end", onEnd);
@@ -89546,7 +89887,7 @@ function readLineFromStdinWithStderrPrompt(prompt) {
89546
89887
  }
89547
89888
  async function statAuditFile(file) {
89548
89889
  try {
89549
- const s = await stat7(file);
89890
+ const s = await stat8(file);
89550
89891
  const entries = (await readEntries(file)).length;
89551
89892
  return { entries, size: formatBytes(s.size) };
89552
89893
  } catch {
@@ -89557,6 +89898,7 @@ var ALLOWED_FORMATS18, DEFAULT_TAIL_N = 10, MAX_TAIL_N = 1e4, SHORT_ID_LEN = 8,
89557
89898
  var init_audit = __esm(() => {
89558
89899
  init_esm();
89559
89900
  init_message_loader();
89901
+ init_config_path();
89560
89902
  init_validation();
89561
89903
  init_config();
89562
89904
  init_config_binding();
@@ -89796,8 +90138,62 @@ var init_audit = __esm(() => {
89796
90138
  });
89797
90139
  });
89798
90140
 
90141
+ // src/utils/runtime-info.ts
90142
+ import { join as join32, normalize, relative as relative3, sep as sep5 } from "path";
90143
+ function normalized(path5) {
90144
+ return normalize(path5).replaceAll("\\", "/");
90145
+ }
90146
+ function isWithin(child, parent) {
90147
+ const rel = relative3(parent, child);
90148
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !rel.startsWith("/");
90149
+ }
90150
+ function inferRuntimeSource(launcherPath, packageRoot) {
90151
+ const launcher = normalized(launcherPath);
90152
+ const root = normalized(packageRoot);
90153
+ if (launcher.includes("/.bunx/") || launcher.includes("/bunx/") || root.includes("/.bun/install/cache/")) {
90154
+ return "bunx";
90155
+ }
90156
+ if (isWithin(launcher, `${root}/src`) || isWithin(launcher, `${root}/scripts`)) {
90157
+ return "workspace";
90158
+ }
90159
+ if (isWithin(launcher, `${root}/dist`))
90160
+ return "installed";
90161
+ return "unknown";
90162
+ }
90163
+ async function collectRuntimeInfo(packageVersion) {
90164
+ const packageRoot = findPackageRoot();
90165
+ const launcherPath = process.argv[1] ?? "unknown";
90166
+ let packageFileVersion = null;
90167
+ try {
90168
+ const packageFile = Bun.file(join32(packageRoot, "package.json"));
90169
+ if (await packageFile.exists()) {
90170
+ const parsed = await packageFile.json();
90171
+ if (typeof parsed.version === "string" && parsed.version.length > 0) {
90172
+ packageFileVersion = parsed.version;
90173
+ }
90174
+ }
90175
+ } catch {}
90176
+ const versions = process.versions;
90177
+ const runtimeName = versions.bun ? "bun" : versions.node ? "node" : "other";
90178
+ const runtimeVersion = versions[runtimeName] ?? "unknown";
90179
+ return {
90180
+ executablePath: process.execPath,
90181
+ launcherPath,
90182
+ packageRoot,
90183
+ packageVersion,
90184
+ packageFileVersion,
90185
+ runtimeName,
90186
+ runtimeVersion,
90187
+ source: inferRuntimeSource(launcherPath, packageRoot),
90188
+ versionMismatch: packageFileVersion !== null && packageFileVersion !== packageVersion
90189
+ };
90190
+ }
90191
+ var init_runtime_info = __esm(() => {
90192
+ init_package_root();
90193
+ });
90194
+
89799
90195
  // src/commands/doctor.ts
89800
- import { join as join30 } from "path";
90196
+ import { join as join33 } from "path";
89801
90197
  import { resolveSrv as resolveSrv2 } from "dns/promises";
89802
90198
  function requireSqlConnection12(connection) {
89803
90199
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
@@ -89805,6 +90201,87 @@ function requireSqlConnection12(connection) {
89805
90201
  }
89806
90202
  return connection;
89807
90203
  }
90204
+ function safeSqlIdentifier(value) {
90205
+ return /^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*$/.test(value) ? value : null;
90206
+ }
90207
+ function boundedSampleCommands(system, table) {
90208
+ if (system === "mongodb") {
90209
+ const collection = shellQuote(table);
90210
+ const query = shellQuote("{}");
90211
+ return {
90212
+ dryRun: `dbcli schema ${collection} --format json`,
90213
+ apply: `dbcli query ${query} --collection ${collection} --limit 100 --format json`
90214
+ };
90215
+ }
90216
+ if (system === "elasticsearch") {
90217
+ const index = shellQuote(table);
90218
+ const query = shellQuote('{"query":{"match_all":{}}}');
90219
+ return {
90220
+ dryRun: `dbcli schema ${index} --format json`,
90221
+ apply: `dbcli query ${query} --collection ${index} --limit 100 --format json`
90222
+ };
90223
+ }
90224
+ const identifier = safeSqlIdentifier(table);
90225
+ if (!identifier) {
90226
+ return {
90227
+ dryRun: `dbcli schema ${shellQuote(table)} --format json`
90228
+ };
90229
+ }
90230
+ const sql = `SELECT * FROM ${identifier} LIMIT 100`;
90231
+ const command = shellQuote(sql);
90232
+ return {
90233
+ dryRun: `dbcli plan ${command} --format json`,
90234
+ apply: `dbcli query ${command} --format json`
90235
+ };
90236
+ }
90237
+ function buildDoctorRemediationPlan(results) {
90238
+ const steps = [];
90239
+ for (const result of results) {
90240
+ if (result.label === "Blacklist completeness" && result.status === "warn") {
90241
+ const prefix = "Consider protecting: ";
90242
+ const candidates = result.message.startsWith(prefix) ? result.message.slice(prefix.length).split(", ").filter(Boolean) : [];
90243
+ for (const candidate of candidates) {
90244
+ const [table, column] = candidate.split(".");
90245
+ if (!table || !column)
90246
+ continue;
90247
+ steps.push({
90248
+ kind: "blacklist-candidate",
90249
+ status: "candidate",
90250
+ rationale: `Sensitive-looking column '${candidate}' is not currently protected.`,
90251
+ dryRun: `dbcli schema ${shellQuote(table)} --format json`,
90252
+ apply: `dbcli blacklist column add ${shellQuote(candidate)}`,
90253
+ requiresHumanConfirmation: true
90254
+ });
90255
+ }
90256
+ }
90257
+ if (result.label === "Schema cache" && result.status === "warn") {
90258
+ steps.push({
90259
+ kind: "schema-refresh",
90260
+ status: "candidate",
90261
+ rationale: result.message,
90262
+ dryRun: "dbcli schema --format json",
90263
+ apply: "dbcli schema --refresh",
90264
+ requiresHumanConfirmation: true
90265
+ });
90266
+ }
90267
+ if (result.label === "Large tables" && result.status === "warn") {
90268
+ const target = result.details?.system ?? "postgresql";
90269
+ const tables = Array.isArray(result.details?.largeTables) ? result.details.largeTables.filter((table) => typeof table === "object" && table !== null && typeof table.name === "string") : [];
90270
+ const candidates = tables.length > 0 ? tables : result.message.replace(/^Large tables:\s*/, "").split(/,\s+(?=[^,]+\s+\([\d.]+M rows\))/).map((entry) => ({ name: entry.replace(/\s+\([\d.]+M rows\)$/, "") }));
90271
+ for (const table of candidates) {
90272
+ const commands = boundedSampleCommands(target, table.name);
90273
+ steps.push({
90274
+ kind: "bounded-sample",
90275
+ status: "candidate",
90276
+ rationale: `${table.name} is large. Review a bounded sample only after confirming blacklist coverage.${commands.apply ? "" : " The identifier needs manual review before a sample query can be generated safely."}`,
90277
+ ...commands,
90278
+ requiresHumanConfirmation: true
90279
+ });
90280
+ }
90281
+ }
90282
+ }
90283
+ return steps;
90284
+ }
89808
90285
  function resolveSchemaLastUpdated(indexJson, configMetadata) {
89809
90286
  if (indexJson && typeof indexJson === "object") {
89810
90287
  const idx = indexJson;
@@ -89869,7 +90346,7 @@ async function collectMongoDoctorResults(config) {
89869
90346
  results.push(runDoctorChecks.checkLargeTables(collections.map((coll) => ({
89870
90347
  name: coll.name,
89871
90348
  estimatedRowCount: coll.estimatedRowCount
89872
- }))));
90349
+ })), "mongodb"));
89873
90350
  results.push({
89874
90351
  group: "Connection & Data",
89875
90352
  label: "Collections",
@@ -89919,7 +90396,7 @@ async function collectElasticsearchDoctorResults(config) {
89919
90396
  if (config.blacklistedColumns) {
89920
90397
  results.push(runDoctorChecks.checkBlacklistCompleteness(tableColumns, config.blacklistedColumns));
89921
90398
  }
89922
- results.push(runDoctorChecks.checkLargeTables(tables));
90399
+ results.push(runDoctorChecks.checkLargeTables(tables, "elasticsearch"));
89923
90400
  const lastUpdated = config.metadata?.schemaLastUpdated ?? null;
89924
90401
  results.push(runDoctorChecks.checkSchemaCacheFreshness(lastUpdated));
89925
90402
  } catch (error) {
@@ -89945,9 +90422,11 @@ var init_doctor = __esm(() => {
89945
90422
  init_message_loader();
89946
90423
  init_validation();
89947
90424
  init_config_v2();
90425
+ init_config_path();
89948
90426
  init_config_binding();
89949
90427
  init_package();
89950
90428
  init_schema_path();
90429
+ init_runtime_info();
89951
90430
  init_config();
89952
90431
  init_integration_helper();
89953
90432
  ALLOWED_FORMATS19 = ["text", "json"];
@@ -89968,6 +90447,33 @@ var init_doctor = __esm(() => {
89968
90447
  "credit_card"
89969
90448
  ];
89970
90449
  runDoctorChecks = {
90450
+ checkRuntime(info) {
90451
+ const fileVersion = info.packageFileVersion ?? "unavailable";
90452
+ const versionMismatch = info.versionMismatch ? ` (bundle/package mismatch: runtime=${info.packageVersion}, package.json=${fileVersion})` : "";
90453
+ return {
90454
+ group: "Environment",
90455
+ label: "Runtime identity",
90456
+ status: info.versionMismatch ? "warn" : "pass",
90457
+ message: `source=${info.source}; runtime=${info.runtimeName} ${info.runtimeVersion}; ` + `executable=${info.executablePath}; launcher=${info.launcherPath}; ` + `package=${info.packageVersion}${versionMismatch}`,
90458
+ details: {
90459
+ source: info.source,
90460
+ runtimeName: info.runtimeName,
90461
+ runtimeVersion: info.runtimeVersion,
90462
+ executablePath: info.executablePath,
90463
+ launcherPath: info.launcherPath,
90464
+ packageRoot: info.packageRoot,
90465
+ packageVersion: info.packageVersion,
90466
+ packageFileVersion: info.packageFileVersion,
90467
+ versionMismatch: info.versionMismatch
90468
+ },
90469
+ ...info.versionMismatch && {
90470
+ remediation: {
90471
+ command: "dbcli upgrade",
90472
+ risk: "interactive"
90473
+ }
90474
+ }
90475
+ };
90476
+ },
89971
90477
  checkBunVersion(current, required) {
89972
90478
  const passes = compareSemver(current, required) >= 0;
89973
90479
  return {
@@ -90003,12 +90509,18 @@ var init_doctor = __esm(() => {
90003
90509
  }
90004
90510
  },
90005
90511
  async checkConfigExists(configPath, existsFn) {
90006
- const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join30(configPath, "config.json")).exists();
90512
+ const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join33(configPath, "config.json")).exists();
90007
90513
  return {
90008
90514
  group: "Configuration",
90009
90515
  label: "Config exists",
90010
90516
  status: exists ? "pass" : "error",
90011
- message: exists ? `Config found: ${configPath}` : `No config found at ${configPath}. Run "dbcli init" first.`
90517
+ message: exists ? `Config found: ${configPath}` : `No config found at ${configPath}. Run "dbcli init" first.`,
90518
+ ...!exists && {
90519
+ remediation: {
90520
+ command: "dbcli init",
90521
+ risk: "interactive"
90522
+ }
90523
+ }
90012
90524
  };
90013
90525
  },
90014
90526
  checkBlacklistCompleteness(tableColumns, blacklistedColumns) {
@@ -90139,7 +90651,7 @@ var init_doctor = __esm(() => {
90139
90651
  }
90140
90652
  }
90141
90653
  },
90142
- checkLargeTables(tables) {
90654
+ checkLargeTables(tables, system = "postgresql") {
90143
90655
  const large = tables.filter((t2) => (t2.estimatedRowCount ?? 0) > 1e6);
90144
90656
  if (large.length === 0) {
90145
90657
  return {
@@ -90154,13 +90666,17 @@ var init_doctor = __esm(() => {
90154
90666
  group: "Connection & Data",
90155
90667
  label: "Large tables",
90156
90668
  status: "warn",
90157
- message: `Large tables: ${list}`
90669
+ message: `Large tables: ${list}`,
90670
+ details: {
90671
+ system,
90672
+ largeTables: large.map(({ name: name2, estimatedRowCount }) => ({ name: name2, estimatedRowCount }))
90673
+ }
90158
90674
  };
90159
90675
  },
90160
90676
  async checkV2Config(configPath) {
90161
90677
  const results = [];
90162
90678
  const storagePath = await resolveConfigStoragePath(configPath);
90163
- const configFile = Bun.file(join30(storagePath, "config.json"));
90679
+ const configFile = Bun.file(join33(storagePath, "config.json"));
90164
90680
  if (!await configFile.exists())
90165
90681
  return results;
90166
90682
  let raw;
@@ -90200,7 +90716,7 @@ var init_doctor = __esm(() => {
90200
90716
  }
90201
90717
  for (const [name2, conn] of Object.entries(config.connections)) {
90202
90718
  if (conn.envFile) {
90203
- const envPath = join30(storagePath, conn.envFile);
90719
+ const envPath = join33(storagePath, conn.envFile);
90204
90720
  const exists = await Bun.file(envPath).exists();
90205
90721
  results.push({
90206
90722
  group: "Configuration",
@@ -90234,7 +90750,7 @@ var init_doctor = __esm(() => {
90234
90750
  `);
90235
90751
  }
90236
90752
  };
90237
- 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) => {
90753
+ doctorCommand = new Command("doctor").description("Run diagnostic checks on dbcli configuration, environment, and connection").option("--format <type>", "Output format: text, json", "text").option("--remediation", "Include a non-mutating, human-confirmed remediation plan", false).action(async (options) => {
90238
90754
  validateFormat(options.format, ALLOWED_FORMATS19, "doctor");
90239
90755
  const logger = getLogger();
90240
90756
  const results = [];
@@ -90242,6 +90758,7 @@ var init_doctor = __esm(() => {
90242
90758
  const storagePath = await resolveConfigStoragePath(configPath);
90243
90759
  const bunVersion = process.versions.bun ?? "unknown";
90244
90760
  const requiredBun = package_default.engines?.bun?.replace(">=", "") ?? "1.3.3";
90761
+ results.push(runDoctorChecks.checkRuntime(await collectRuntimeInfo(package_default.version)));
90245
90762
  results.push(runDoctorChecks.checkBunVersion(bunVersion, requiredBun));
90246
90763
  results.push(await runDoctorChecks.checkLatestVersion(package_default.version));
90247
90764
  const configExists = await runDoctorChecks.checkConfigExists(storagePath);
@@ -90303,13 +90820,13 @@ var init_doctor = __esm(() => {
90303
90820
  tableColumns.set(t2.name, t2.columns.map((c2) => c2.name));
90304
90821
  }
90305
90822
  results.push(runDoctorChecks.checkBlacklistCompleteness(tableColumns, blacklistedColumns));
90306
- results.push(runDoctorChecks.checkLargeTables(tables));
90823
+ results.push(runDoctorChecks.checkLargeTables(tables, config.connection.system));
90307
90824
  } catch {
90308
90825
  logger.debug("Could not list tables for blacklist/large table check");
90309
90826
  }
90310
90827
  try {
90311
90828
  const schemaConnName = await getSchemaIsolationConnectionName(configPath);
90312
- const indexPath = join30(resolveSchemaPath(storagePath, schemaConnName), "index.json");
90829
+ const indexPath = join33(resolveSchemaPath(storagePath, schemaConnName), "index.json");
90313
90830
  const indexFile = Bun.file(indexPath);
90314
90831
  let indexParsed = null;
90315
90832
  if (await indexFile.exists()) {
@@ -90352,7 +90869,8 @@ var init_doctor = __esm(() => {
90352
90869
  });
90353
90870
  } catch {}
90354
90871
  if (options.format === "json") {
90355
- console.log(JSON.stringify({ results, hasError }, null, 2));
90872
+ const remediation = options.remediation ? buildDoctorRemediationPlan(results) : undefined;
90873
+ console.log(JSON.stringify({ results, hasError, ...remediation && { remediation } }, null, 2));
90356
90874
  } else {
90357
90875
  console.log(runDoctorChecks.formatTextOutput(results, package_default.version));
90358
90876
  }
@@ -90417,9 +90935,9 @@ function flattenCommandTree(root) {
90417
90935
  }
90418
90936
 
90419
90937
  // src/commands/completion.ts
90420
- import { join as join31 } from "path";
90938
+ import { join as join34 } from "path";
90421
90939
  import { homedir as homedir3 } from "os";
90422
- import { mkdir as mkdir14 } from "fs/promises";
90940
+ import { mkdir as mkdir15 } from "fs/promises";
90423
90941
  function resolveHome() {
90424
90942
  return process.env.HOME ?? homedir3();
90425
90943
  }
@@ -90669,11 +91187,11 @@ function getInstallPath2(shell) {
90669
91187
  const home = resolveHome();
90670
91188
  switch (shell) {
90671
91189
  case "bash":
90672
- return join31(home, ".bashrc");
91190
+ return join34(home, ".bashrc");
90673
91191
  case "zsh":
90674
- return join31(home, ".zshrc");
91192
+ return join34(home, ".zshrc");
90675
91193
  case "fish":
90676
- return join31(home, ".config", "fish", "completions", "dbcli.fish");
91194
+ return join34(home, ".config", "fish", "completions", "dbcli.fish");
90677
91195
  default:
90678
91196
  throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
90679
91197
  }
@@ -90691,8 +91209,8 @@ function detectShell() {
90691
91209
  async function installCompletion(shell, script) {
90692
91210
  const targetPath = getInstallPath2(shell);
90693
91211
  if (shell === "fish") {
90694
- const dir = join31(resolveHome(), ".config", "fish", "completions");
90695
- await mkdir14(dir, { recursive: true });
91212
+ const dir = join34(resolveHome(), ".config", "fish", "completions");
91213
+ await mkdir15(dir, { recursive: true });
90696
91214
  await Bun.file(targetPath).write(script);
90697
91215
  console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
90698
91216
  return;
@@ -90853,24 +91371,24 @@ var init_types11 = __esm(() => {
90853
91371
 
90854
91372
  // src/core/repl/input-classifier.ts
90855
91373
  function classifyInput(raw) {
90856
- const normalized = raw.trim();
90857
- if (normalized === "") {
90858
- return { type: "empty", raw, normalized };
91374
+ const normalized2 = raw.trim();
91375
+ if (normalized2 === "") {
91376
+ return { type: "empty", raw, normalized: normalized2 };
90859
91377
  }
90860
- if (normalized.startsWith(META_PREFIX)) {
90861
- const cmdName = (normalized.split(/\s+/)[0] ?? "").slice(1);
91378
+ if (normalized2.startsWith(META_PREFIX)) {
91379
+ const cmdName = (normalized2.split(/\s+/)[0] ?? "").slice(1);
90862
91380
  if (metaCommandNames.includes(cmdName)) {
90863
- return { type: "meta", raw, normalized };
91381
+ return { type: "meta", raw, normalized: normalized2 };
90864
91382
  }
90865
91383
  }
90866
- const firstWord = (normalized.split(/\s+/)[0] ?? "").toUpperCase();
91384
+ const firstWord = (normalized2.split(/\s+/)[0] ?? "").toUpperCase();
90867
91385
  if (sqlKeywordSet.has(firstWord)) {
90868
- return { type: "sql", raw, normalized };
91386
+ return { type: "sql", raw, normalized: normalized2 };
90869
91387
  }
90870
- if (normalized.endsWith(SQL_TERMINATOR)) {
90871
- return { type: "sql", raw, normalized };
91388
+ if (normalized2.endsWith(SQL_TERMINATOR)) {
91389
+ return { type: "sql", raw, normalized: normalized2 };
90872
91390
  }
90873
- return { type: "command", raw, normalized };
91391
+ return { type: "command", raw, normalized: normalized2 };
90874
91392
  }
90875
91393
  var META_PREFIX = ".", SQL_TERMINATOR = ";", sqlKeywordSet, metaCommandNames;
90876
91394
  var init_input_classifier = __esm(() => {
@@ -91106,7 +91624,7 @@ function isKnownCommand(name2, commandNames) {
91106
91624
 
91107
91625
  // src/core/repl/history-manager.ts
91108
91626
  import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
91109
- import { dirname as dirname10 } from "path";
91627
+ import { dirname as dirname12 } from "path";
91110
91628
 
91111
91629
  class HistoryManager {
91112
91630
  filePath;
@@ -91133,7 +91651,7 @@ class HistoryManager {
91133
91651
  return this.entries;
91134
91652
  }
91135
91653
  async save() {
91136
- const dir = dirname10(this.filePath);
91654
+ const dir = dirname12(this.filePath);
91137
91655
  if (!existsSync2(dir)) {
91138
91656
  mkdirSync2(dir, { recursive: true });
91139
91657
  }
@@ -91676,7 +92194,7 @@ var init_command_registry = __esm(() => {
91676
92194
 
91677
92195
  // src/commands/shell.ts
91678
92196
  import { createInterface as createInterface3 } from "readline";
91679
- import { join as join32 } from "path";
92197
+ import { join as join35 } from "path";
91680
92198
  import { homedir as homedir4 } from "os";
91681
92199
  function requireSqlConnection13(connection) {
91682
92200
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
@@ -91866,10 +92384,11 @@ var init_shell3 = __esm(() => {
91866
92384
  init_adapters();
91867
92385
  init_repl_engine();
91868
92386
  init_completer();
92387
+ init_config_path();
91869
92388
  init_message_loader();
91870
92389
  init_es_shell();
91871
92390
  import_picocolors5 = __toESM(require_picocolors(), 1);
91872
- HISTORY_PATH = join32(homedir4(), ".dbcli_history");
92391
+ HISTORY_PATH = join35(homedir4(), ".dbcli_history");
91873
92392
  shellCommand = new Command("shell").description("Interactive database shell with auto-completion and syntax highlighting").option("--sql", "SQL-only mode (skip dbcli command parsing)").action(async (options, command) => {
91874
92393
  const configPath = resolveConfigPath(command);
91875
92394
  await runShell(options, configPath);
@@ -92480,8 +92999,8 @@ function requireSqlConnection14(connection) {
92480
92999
  }
92481
93000
  return connection;
92482
93001
  }
92483
- async function runDDL(operation, opts) {
92484
- const configPath = resolveConfigPath(undefined, opts);
93002
+ async function runDDL(operation, opts, command) {
93003
+ const configPath = resolveConfigPath(command, opts);
92485
93004
  const config = await configModule.read(configPath);
92486
93005
  if (!config.connection) {
92487
93006
  throw new Error('Run "dbcli init" to configure database connection');
@@ -92551,8 +93070,9 @@ var init_migrate = __esm(() => {
92551
93070
  init_adapters();
92552
93071
  init_ddl2();
92553
93072
  init_ddl_executor();
93073
+ init_config_path();
92554
93074
  migrateCommand = new Command("migrate").description(t("migrate.description"));
92555
- addExecOpts(migrateCommand.command("create <table>").description(t("migrate.create_description")).option("--column <spec...>", 'Column definitions (e.g., "id:serial:pk" "name:varchar(50):not-null")')).action(async (table, opts) => {
93075
+ addExecOpts(migrateCommand.command("create <table>").description(t("migrate.create_description")).option("--column <spec...>", 'Column definitions (e.g., "id:serial:pk" "name:varchar(50):not-null")')).action(async (table, opts, command) => {
92556
93076
  try {
92557
93077
  const specs = opts.column || [];
92558
93078
  if (specs.length === 0) {
@@ -92560,19 +93080,19 @@ var init_migrate = __esm(() => {
92560
93080
  process.exit(1);
92561
93081
  }
92562
93082
  const columns = specs.map(parseColumnSpec);
92563
- await runDDL({ kind: "createTable", table, columns }, opts);
93083
+ await runDDL({ kind: "createTable", table, columns }, opts, command);
92564
93084
  } catch (e) {
92565
93085
  handleError(e);
92566
93086
  }
92567
93087
  });
92568
- addExecOpts(migrateCommand.command("drop <table>").description(t("migrate.drop_description"))).action(async (table, opts) => {
93088
+ addExecOpts(migrateCommand.command("drop <table>").description(t("migrate.drop_description"))).action(async (table, opts, command) => {
92569
93089
  try {
92570
- await runDDL({ kind: "dropTable", table }, opts);
93090
+ await runDDL({ kind: "dropTable", table }, opts, command);
92571
93091
  } catch (e) {
92572
93092
  handleError(e);
92573
93093
  }
92574
93094
  });
92575
- addExecOpts(migrateCommand.command("add-column <table> <column> <type>").description(t("migrate.add_column_description")).option("--nullable", "Allow NULL values").option("--default <value>", "Default value").option("--unique", "Add UNIQUE constraint")).action(async (table, column, type, opts) => {
93095
+ addExecOpts(migrateCommand.command("add-column <table> <column> <type>").description(t("migrate.add_column_description")).option("--nullable", "Allow NULL values").option("--default <value>", "Default value").option("--unique", "Add UNIQUE constraint")).action(async (table, column, type, opts, command) => {
92576
93096
  try {
92577
93097
  await runDDL({
92578
93098
  kind: "addColumn",
@@ -92584,19 +93104,19 @@ var init_migrate = __esm(() => {
92584
93104
  default: opts.default,
92585
93105
  unique: opts.unique
92586
93106
  }
92587
- }, opts);
93107
+ }, opts, command);
92588
93108
  } catch (e) {
92589
93109
  handleError(e);
92590
93110
  }
92591
93111
  });
92592
- addExecOpts(migrateCommand.command("drop-column <table> <column>").description(t("migrate.drop_column_description"))).action(async (table, column, opts) => {
93112
+ addExecOpts(migrateCommand.command("drop-column <table> <column>").description(t("migrate.drop_column_description"))).action(async (table, column, opts, command) => {
92593
93113
  try {
92594
- await runDDL({ kind: "dropColumn", table, column }, opts);
93114
+ await runDDL({ kind: "dropColumn", table, column }, opts, command);
92595
93115
  } catch (e) {
92596
93116
  handleError(e);
92597
93117
  }
92598
93118
  });
92599
- addExecOpts(migrateCommand.command("alter-column <table> <column>").description(t("migrate.alter_column_description")).option("--type <type>", "Change column type").option("--rename <name>", "Rename column").option("--set-default <value>", "Set default value").option("--drop-default", "Remove default value").option("--set-nullable", "Allow NULL").option("--drop-nullable", "Disallow NULL")).action(async (table, column, opts) => {
93119
+ addExecOpts(migrateCommand.command("alter-column <table> <column>").description(t("migrate.alter_column_description")).option("--type <type>", "Change column type").option("--rename <name>", "Rename column").option("--set-default <value>", "Set default value").option("--drop-default", "Remove default value").option("--set-nullable", "Allow NULL").option("--drop-nullable", "Disallow NULL")).action(async (table, column, opts, command) => {
92600
93120
  try {
92601
93121
  await runDDL({
92602
93122
  kind: "alterColumn",
@@ -92610,30 +93130,30 @@ var init_migrate = __esm(() => {
92610
93130
  setNullable: opts.setNullable,
92611
93131
  dropNullable: opts.dropNullable
92612
93132
  }
92613
- }, opts);
93133
+ }, opts, command);
92614
93134
  } catch (e) {
92615
93135
  handleError(e);
92616
93136
  }
92617
93137
  });
92618
- addExecOpts(migrateCommand.command("add-index <table>").description(t("migrate.add_index_description")).requiredOption("--columns <cols>", "Comma-separated column names").option("--unique", "Create unique index").option("--type <type>", "Index type (btree, hash, gin, gist)").option("--name <name>", "Custom index name")).action(async (table, opts) => {
93138
+ addExecOpts(migrateCommand.command("add-index <table>").description(t("migrate.add_index_description")).requiredOption("--columns <cols>", "Comma-separated column names").option("--unique", "Create unique index").option("--type <type>", "Index type (btree, hash, gin, gist)").option("--name <name>", "Custom index name")).action(async (table, opts, command) => {
92619
93139
  try {
92620
93140
  const columns = opts.columns.split(",").map((c2) => c2.trim());
92621
93141
  await runDDL({
92622
93142
  kind: "addIndex",
92623
93143
  index: { table, columns, unique: opts.unique, type: opts.type, name: opts.name }
92624
- }, opts);
93144
+ }, opts, command);
92625
93145
  } catch (e) {
92626
93146
  handleError(e);
92627
93147
  }
92628
93148
  });
92629
- addExecOpts(migrateCommand.command("drop-index <index>").description(t("migrate.drop_index_description")).option("--table <table>", "Table name (required for MySQL/MariaDB)")).action(async (indexName, opts) => {
93149
+ addExecOpts(migrateCommand.command("drop-index <index>").description(t("migrate.drop_index_description")).option("--table <table>", "Table name (required for MySQL/MariaDB)")).action(async (indexName, opts, command) => {
92630
93150
  try {
92631
- await runDDL({ kind: "dropIndex", indexName, table: opts.table }, opts);
93151
+ await runDDL({ kind: "dropIndex", indexName, table: opts.table }, opts, command);
92632
93152
  } catch (e) {
92633
93153
  handleError(e);
92634
93154
  }
92635
93155
  });
92636
- addExecOpts(migrateCommand.command("add-constraint <table>").description(t("migrate.add_constraint_description")).option("--fk <column>", "Foreign key column").option("--references <table.column>", "Referenced table.column").option("--on-delete <action>", "ON DELETE action (cascade, set null, restrict, no action)").option("--unique <columns>", "Unique constraint columns (comma-separated)").option("--check <expression>", "Check constraint expression").option("--name <name>", "Custom constraint name")).action(async (table, opts) => {
93156
+ addExecOpts(migrateCommand.command("add-constraint <table>").description(t("migrate.add_constraint_description")).option("--fk <column>", "Foreign key column").option("--references <table.column>", "Referenced table.column").option("--on-delete <action>", "ON DELETE action (cascade, set null, restrict, no action)").option("--unique <columns>", "Unique constraint columns (comma-separated)").option("--check <expression>", "Check constraint expression").option("--name <name>", "Custom constraint name")).action(async (table, opts, command) => {
92637
93157
  try {
92638
93158
  let type;
92639
93159
  let op;
@@ -92677,35 +93197,35 @@ var init_migrate = __esm(() => {
92677
93197
  process.exit(1);
92678
93198
  return;
92679
93199
  }
92680
- await runDDL(op, opts);
93200
+ await runDDL(op, opts, command);
92681
93201
  } catch (e) {
92682
93202
  handleError(e);
92683
93203
  }
92684
93204
  });
92685
- addExecOpts(migrateCommand.command("drop-constraint <table> <constraint>").description(t("migrate.drop_constraint_description"))).action(async (table, constraintName, opts) => {
93205
+ addExecOpts(migrateCommand.command("drop-constraint <table> <constraint>").description(t("migrate.drop_constraint_description"))).action(async (table, constraintName, opts, command) => {
92686
93206
  try {
92687
- await runDDL({ kind: "dropConstraint", table, constraintName }, opts);
93207
+ await runDDL({ kind: "dropConstraint", table, constraintName }, opts, command);
92688
93208
  } catch (e) {
92689
93209
  handleError(e);
92690
93210
  }
92691
93211
  });
92692
- addExecOpts(migrateCommand.command("add-enum <name> <values...>").description(t("migrate.add_enum_description"))).action(async (name2, values, opts) => {
93212
+ addExecOpts(migrateCommand.command("add-enum <name> <values...>").description(t("migrate.add_enum_description"))).action(async (name2, values, opts, command) => {
92693
93213
  try {
92694
- await runDDL({ kind: "addEnum", definition: { name: name2, values } }, opts);
93214
+ await runDDL({ kind: "addEnum", definition: { name: name2, values } }, opts, command);
92695
93215
  } catch (e) {
92696
93216
  handleError(e);
92697
93217
  }
92698
93218
  });
92699
- addExecOpts(migrateCommand.command("alter-enum <name>").description(t("migrate.alter_enum_description")).requiredOption("--add-value <value>", "Value to add")).action(async (name2, opts) => {
93219
+ addExecOpts(migrateCommand.command("alter-enum <name>").description(t("migrate.alter_enum_description")).requiredOption("--add-value <value>", "Value to add")).action(async (name2, opts, command) => {
92700
93220
  try {
92701
- await runDDL({ kind: "alterEnum", name: name2, addValue: opts.addValue }, opts);
93221
+ await runDDL({ kind: "alterEnum", name: name2, addValue: opts.addValue }, opts, command);
92702
93222
  } catch (e) {
92703
93223
  handleError(e);
92704
93224
  }
92705
93225
  });
92706
- addExecOpts(migrateCommand.command("drop-enum <name>").description(t("migrate.drop_enum_description"))).action(async (name2, opts) => {
93226
+ addExecOpts(migrateCommand.command("drop-enum <name>").description(t("migrate.drop_enum_description"))).action(async (name2, opts, command) => {
92707
93227
  try {
92708
- await runDDL({ kind: "dropEnum", name: name2 }, opts);
93228
+ await runDDL({ kind: "dropEnum", name: name2 }, opts, command);
92709
93229
  } catch (e) {
92710
93230
  handleError(e);
92711
93231
  }
@@ -92713,12 +93233,13 @@ var init_migrate = __esm(() => {
92713
93233
  });
92714
93234
 
92715
93235
  // src/commands/use.ts
92716
- import { join as join33 } from "path";
92717
- async function switchDefault(configPath, name2, config) {
92718
- if (!config.connections[name2]) {
92719
- const available = Object.keys(config.connections).join(", ");
92720
- throw new ConfigError(`${t_vars("init.connection_not_found", { name: name2 })}. ${t("use.available")}: ${available}`);
93236
+ import { join as join36 } from "path";
93237
+ async function switchDefault(configPath, name2, config, options = {}) {
93238
+ const connection = config.connections[name2];
93239
+ if (!connection) {
93240
+ throw new ConfigError(connectionNotFoundMessage(name2, Object.keys(config.connections)));
92721
93241
  }
93242
+ assertProductionDefaultConfirmation(name2, connection.environment, options.confirmProduction);
92722
93243
  const updated = {
92723
93244
  ...config,
92724
93245
  default: name2
@@ -92731,12 +93252,31 @@ function listConnectionsForDisplay(config) {
92731
93252
  const host = typeof conn.host === "object" ? `\${${conn.host.$env}}` : conn.host;
92732
93253
  const port = typeof conn.port === "object" ? `\${${conn.port.$env}}` : conn.port;
92733
93254
  const db = typeof conn.database === "object" ? `\${${conn.database.$env}}` : conn.database;
92734
- return `${marker} ${name2.padEnd(12)} ${conn.system.padEnd(12)} ${host}:${port}/${db}`;
93255
+ const environment = conn.environment ? ` [${conn.environment}]` : "";
93256
+ return `${marker} ${name2.padEnd(12)}${environment} ${conn.system.padEnd(12)} ${host}:${port}/${db}`;
93257
+ });
93258
+ }
93259
+ function listConnectionIdentities(config) {
93260
+ return Object.entries(config.connections).map(([name2, conn]) => {
93261
+ const host = typeof conn.host === "string" && conn.host.length > 0 ? conn.host : null;
93262
+ const database = typeof conn.database === "string" && conn.database.length > 0 ? conn.database : null;
93263
+ return {
93264
+ name: name2,
93265
+ environment: conn.environment ?? null,
93266
+ permission: conn.permission,
93267
+ system: conn.system,
93268
+ server: {
93269
+ host,
93270
+ port: host !== null && typeof conn.port === "number" ? conn.port : null
93271
+ },
93272
+ database,
93273
+ isDefault: name2 === config.default
93274
+ };
92735
93275
  });
92736
93276
  }
92737
93277
  async function ensureV2Config(configPath) {
92738
93278
  const storagePath = await resolveConfigStoragePath(configPath);
92739
- const configFile = Bun.file(join33(storagePath, "config.json"));
93279
+ const configFile = Bun.file(join36(storagePath, "config.json"));
92740
93280
  const legacyFile = Bun.file(configPath);
92741
93281
  if (!await configFile.exists() && !await legacyFile.exists()) {
92742
93282
  throw new ConfigError(t("init.config_not_found"));
@@ -92772,19 +93312,30 @@ async function ensureV2Config(configPath) {
92772
93312
  throw new ConfigError(t("use.requires_v2"));
92773
93313
  }
92774
93314
  }
92775
- var useCommand;
93315
+ var ALLOWED_FORMATS20, useCommand;
92776
93316
  var init_use = __esm(() => {
92777
93317
  init_esm();
92778
93318
  init_config_v2();
92779
93319
  init_config();
92780
- init_errors3();
93320
+ init_errors2();
93321
+ init_config_path();
92781
93322
  init_message_loader();
92782
93323
  init_config_binding();
92783
- useCommand = new Command("use").description("Switch or display the default database connection (v2 config)").argument("[name]", "Connection name to switch to").option("--list", "List all connections").action(async (name2, options) => {
93324
+ init_validation();
93325
+ ALLOWED_FORMATS20 = ["text", "json"];
93326
+ useCommand = new Command("use").description("Switch or display the default database connection (v2 config)").argument("[name]", "Connection name to switch to").option("--list", "List all connections").option("--format <format>", "Output format: text (default) or json", "text").option("--confirm-production <name>", "Confirm changing the persisted default to this production connection by repeating its name").action(async (name2, options) => {
92784
93327
  try {
93328
+ validateFormat(String(options.format), ALLOWED_FORMATS20, "use");
92785
93329
  const configPath = resolveConfigPath(useCommand);
92786
93330
  const config = await ensureV2Config(configPath);
93331
+ if (name2 && !options.list && options.format === "json") {
93332
+ throw new ConfigError("--format json is only supported with --list");
93333
+ }
92787
93334
  if (options.list || !name2) {
93335
+ if (options.format === "json") {
93336
+ console.log(JSON.stringify({ connections: listConnectionIdentities(config) }, null, 2));
93337
+ return;
93338
+ }
92788
93339
  if (!name2) {
92789
93340
  console.log(t_vars("use.current", { name: config.default }));
92790
93341
  }
@@ -92794,7 +93345,9 @@ var init_use = __esm(() => {
92794
93345
  }
92795
93346
  return;
92796
93347
  }
92797
- await switchDefault(configPath, name2, config);
93348
+ await switchDefault(configPath, name2, config, {
93349
+ confirmProduction: typeof options.confirmProduction === "string" ? options.confirmProduction : undefined
93350
+ });
92798
93351
  console.log(t_vars("use.switched", { name: name2 }));
92799
93352
  } catch (error) {
92800
93353
  if (error instanceof Error) {
@@ -92881,8 +93434,8 @@ var init_sql_metadata = __esm(() => {
92881
93434
  });
92882
93435
 
92883
93436
  // src/proxy/events.ts
92884
- import { appendFile as appendFile2, mkdir as mkdir15, readFile as readFile8, stat as stat8 } from "fs/promises";
92885
- import { dirname as dirname11 } from "path";
93437
+ import { appendFile as appendFile2, mkdir as mkdir16, readFile as readFile8, stat as stat9 } from "fs/promises";
93438
+ import { dirname as dirname13 } from "path";
92886
93439
  function hasSql(e) {
92887
93440
  return e.type === "query_observed" || e.type === "query_completed" || e.type === "query_errored";
92888
93441
  }
@@ -92921,7 +93474,7 @@ class EventWriter {
92921
93474
  }
92922
93475
  async writeInternal(event) {
92923
93476
  if (!this.dirEnsured) {
92924
- await mkdir15(dirname11(this.path), { recursive: true });
93477
+ await mkdir16(dirname13(this.path), { recursive: true });
92925
93478
  this.dirEnsured = true;
92926
93479
  }
92927
93480
  if (!this.initialized) {
@@ -92943,7 +93496,7 @@ class EventWriter {
92943
93496
  }
92944
93497
  async syncCountersFromDisk() {
92945
93498
  try {
92946
- const s = await stat8(this.path);
93499
+ const s = await stat9(this.path);
92947
93500
  this.currentSizeBytes = s.size;
92948
93501
  const raw = await readFile8(this.path, "utf8");
92949
93502
  this.currentEntryCount = raw.split(`
@@ -93850,7 +94403,7 @@ function renderAnalysisText(report, top) {
93850
94403
  }
93851
94404
 
93852
94405
  // src/commands/proxy.ts
93853
- import { join as join34 } from "path";
94406
+ import { join as join37 } from "path";
93854
94407
  function parseHostPort(value) {
93855
94408
  const idx = value.lastIndexOf(":");
93856
94409
  if (idx <= 0 || idx === value.length - 1) {
@@ -93890,7 +94443,7 @@ function resolveProxyConfig(input) {
93890
94443
  }
93891
94444
  async function runProxy(subcommandEngine, options, command) {
93892
94445
  try {
93893
- validateFormat(options.format ?? "text", ALLOWED_FORMATS20, "proxy");
94446
+ validateFormat(options.format ?? "text", ALLOWED_FORMATS21, "proxy");
93894
94447
  const redact = options.redact ?? "none";
93895
94448
  if (!ALLOWED_REDACT.includes(redact)) {
93896
94449
  throw new Error(`Invalid --redact "${redact}". Allowed: none, literals`);
@@ -93913,7 +94466,7 @@ async function runProxy(subcommandEngine, options, command) {
93913
94466
  target: options.target,
93914
94467
  connection
93915
94468
  });
93916
- const eventsPath = options.events ?? join34(".dbcli", "proxy", "events.jsonl");
94469
+ const eventsPath = options.events ?? join37(".dbcli", "proxy", "events.jsonl");
93917
94470
  const slowMs = Number(options.slowMs ?? 1000);
93918
94471
  if (!Number.isFinite(slowMs) || slowMs < 0) {
93919
94472
  throw new Error(`Invalid --slow-ms "${options.slowMs}". Expected a non-negative number`);
@@ -93945,12 +94498,12 @@ async function runProxy(subcommandEngine, options, command) {
93945
94498
  ` + `Press Ctrl+C to stop.
93946
94499
  `);
93947
94500
  }
93948
- await new Promise((resolve8) => {
94501
+ await new Promise((resolve9) => {
93949
94502
  const shutdown = () => {
93950
94503
  process.removeListener("SIGINT", shutdown);
93951
94504
  process.removeListener("SIGTERM", shutdown);
93952
94505
  server.stop();
93953
- resolve8();
94506
+ resolve9();
93954
94507
  };
93955
94508
  process.on("SIGINT", shutdown);
93956
94509
  process.on("SIGTERM", shutdown);
@@ -93962,7 +94515,7 @@ async function runProxy(subcommandEngine, options, command) {
93962
94515
  }
93963
94516
  }
93964
94517
  function addCommonOptions(cmd) {
93965
- return cmd.option("--listen <host:port>", "Local proxy listen address (required)").option("--target <host:port>", "Upstream DB target (optional when config provides host/port)").option("--events <path>", "Event JSONL path", join34(".dbcli", "proxy", "events.jsonl")).option("--slow-ms <number>", "Threshold (ms); queries at/above it get slow:true in the event + a terminal warning", "1000").option("--redact <mode>", "SQL redaction: none | literals", "none").option("--format <format>", "Runtime status output: text | json", "text");
94518
+ return cmd.option("--listen <host:port>", "Local proxy listen address (required)").option("--target <host:port>", "Upstream DB target (optional when config provides host/port)").option("--events <path>", "Event JSONL path", join37(".dbcli", "proxy", "events.jsonl")).option("--slow-ms <number>", "Threshold (ms); queries at/above it get slow:true in the event + a terminal warning", "1000").option("--redact <mode>", "SQL redaction: none | literals", "none").option("--format <format>", "Runtime status output: text | json", "text");
93966
94519
  }
93967
94520
  function parseNonNegInt(value, flag, fallback) {
93968
94521
  if (value === undefined)
@@ -93980,7 +94533,7 @@ async function runAnalyze(options) {
93980
94533
  const top = parseNonNegInt(options.top, "top", 20);
93981
94534
  const slowMs = parseNonNegInt(options.slowMs, "slow-ms", 1000);
93982
94535
  const nPlusOne = parseNonNegInt(options.nPlusOne, "n-plus-one", 10);
93983
- const eventsPath = options.events ?? join34(".dbcli", "proxy", "events.jsonl");
94536
+ const eventsPath = options.events ?? join37(".dbcli", "proxy", "events.jsonl");
93984
94537
  const { events, malformedLines, files } = await readEvents(eventsPath, {
93985
94538
  includeRotated: options.includeRotated !== false
93986
94539
  });
@@ -94007,16 +94560,17 @@ async function runAnalyze(options) {
94007
94560
  process.exit(1);
94008
94561
  }
94009
94562
  }
94010
- var SUPPORTED, ALLOWED_FORMATS20, ALLOWED_REDACT, proxyCommand, ANALYZE_FORMATS;
94563
+ var SUPPORTED, ALLOWED_FORMATS21, ALLOWED_REDACT, proxyCommand, ANALYZE_FORMATS;
94011
94564
  var init_proxy = __esm(() => {
94012
94565
  init_esm();
94013
94566
  init_config();
94567
+ init_config_path();
94014
94568
  init_validation();
94015
94569
  init_server();
94016
94570
  init_event_reader();
94017
94571
  init_analyze();
94018
94572
  SUPPORTED = ["mysql", "mariadb", "postgresql"];
94019
- ALLOWED_FORMATS20 = ["text", "json"];
94573
+ ALLOWED_FORMATS21 = ["text", "json"];
94020
94574
  ALLOWED_REDACT = ["none", "literals"];
94021
94575
  proxyCommand = new Command().name("proxy").description("Local development observability proxy for MySQL/MariaDB/PostgreSQL (observe-only)");
94022
94576
  proxyCommand.enablePositionalOptions();
@@ -94026,7 +94580,7 @@ var init_proxy = __esm(() => {
94026
94580
  });
94027
94581
  }
94028
94582
  ANALYZE_FORMATS = ["json", "text"];
94029
- proxyCommand.command("analyze").description("Analyze a proxy event log offline (no DB connection)").option("--events <path>", "Event JSONL path", join34(".dbcli", "proxy", "events.jsonl")).option("--format <format>", "Output format: json | text", "json").option("--top <number>", "Rows shown in text + suggestedCommands depth", "20").option("--slow-ms <number>", "Slow-query threshold (ms) for slowCount", "1000").option("--n-plus-one <number>", "Min repeats per (session,fingerprint) to flag N+1", "10").option("--no-include-rotated", "Do not merge the rotated <events>.1 segment").action(async (options) => {
94583
+ proxyCommand.command("analyze").description("Analyze a proxy event log offline (no DB connection)").option("--events <path>", "Event JSONL path", join37(".dbcli", "proxy", "events.jsonl")).option("--format <format>", "Output format: json | text", "json").option("--top <number>", "Rows shown in text + suggestedCommands depth", "20").option("--slow-ms <number>", "Slow-query threshold (ms) for slowCount", "1000").option("--n-plus-one <number>", "Min repeats per (session,fingerprint) to flag N+1", "10").option("--no-include-rotated", "Do not merge the rotated <events>.1 segment").action(async (options) => {
94030
94584
  await runAnalyze(options);
94031
94585
  });
94032
94586
  addCommonOptions(proxyCommand).action(async (options, command) => {
@@ -94034,6 +94588,264 @@ var init_proxy = __esm(() => {
94034
94588
  });
94035
94589
  });
94036
94590
 
94591
+ // src/core/backfill-artifact.ts
94592
+ import { createHash as createHash4 } from "crypto";
94593
+ function isRecord2(value) {
94594
+ return typeof value === "object" && value !== null && !Array.isArray(value);
94595
+ }
94596
+ function requireIdentifier(value, label) {
94597
+ if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_$.]*$/.test(value)) {
94598
+ throw new Error(`${label} must be a SQL identifier containing only letters, numbers, _, $, or .`);
94599
+ }
94600
+ return value;
94601
+ }
94602
+ function sqlLiteral(value, targetSystem) {
94603
+ if (value === null)
94604
+ return "NULL";
94605
+ if (typeof value === "string") {
94606
+ if (targetSystem === "mysql" || targetSystem === "mariadb") {
94607
+ const hex = Buffer.from(value, "utf8").toString("hex");
94608
+ return `CONVERT(UNHEX('${hex}') USING utf8mb4)`;
94609
+ }
94610
+ return `'${value.replaceAll("'", "''")}'`;
94611
+ }
94612
+ if (typeof value === "number") {
94613
+ if (!Number.isFinite(value))
94614
+ throw new Error("Source rows cannot contain non-finite numbers");
94615
+ return String(value);
94616
+ }
94617
+ if (typeof value === "boolean")
94618
+ return value ? "TRUE" : "FALSE";
94619
+ throw new Error("Source row values must be strings, finite numbers, booleans, or null");
94620
+ }
94621
+ function stripSqlCommentsAndStrings(sql) {
94622
+ let result = "";
94623
+ let index = 0;
94624
+ while (index < sql.length) {
94625
+ const current = sql[index];
94626
+ const next = sql[index + 1];
94627
+ if (current === "-" && next === "-") {
94628
+ index += 2;
94629
+ while (index < sql.length && sql[index] !== `
94630
+ `)
94631
+ index += 1;
94632
+ result += `
94633
+ `;
94634
+ continue;
94635
+ }
94636
+ if (current === "/" && next === "*") {
94637
+ index += 2;
94638
+ while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/"))
94639
+ index += 1;
94640
+ index += 2;
94641
+ result += " ";
94642
+ continue;
94643
+ }
94644
+ if (current === "'") {
94645
+ index += 1;
94646
+ while (index < sql.length) {
94647
+ if (sql[index] === "'" && sql[index + 1] === "'") {
94648
+ index += 2;
94649
+ continue;
94650
+ }
94651
+ if (sql[index] === "'") {
94652
+ index += 1;
94653
+ break;
94654
+ }
94655
+ index += 1;
94656
+ }
94657
+ result += " ";
94658
+ continue;
94659
+ }
94660
+ result += current;
94661
+ index += 1;
94662
+ }
94663
+ return result;
94664
+ }
94665
+ function assertReadOnlyVerifyQuery(query) {
94666
+ const normalized2 = stripSqlCommentsAndStrings(query).trim();
94667
+ if (!/^SELECT\b/i.test(normalized2) || /;/.test(normalized2)) {
94668
+ throw new Error("verifyQuery must be a single plain read-only SELECT statement");
94669
+ }
94670
+ if (/\b(?:INSERT|UPDATE|DELETE|MERGE|UPSERT|REPLACE|TRUNCATE|DROP|ALTER|CREATE|GRANT|REVOKE|RENAME|INTO|LOCK|CALL|DO|FOR\s+(?:UPDATE|SHARE|NO\s+KEY\s+UPDATE|KEY\s+SHARE))\b/i.test(normalized2)) {
94671
+ throw new Error("verifyQuery must not contain a write or DDL statement");
94672
+ }
94673
+ return query.trim();
94674
+ }
94675
+ function parseBackfillSourceManifest(raw) {
94676
+ if (!isRecord2(raw))
94677
+ throw new Error("Source manifest must be a JSON object");
94678
+ const table = requireIdentifier(raw.table, "table");
94679
+ if (!Array.isArray(raw.keyColumns) || raw.keyColumns.length === 0) {
94680
+ throw new Error("keyColumns must be a non-empty array");
94681
+ }
94682
+ const keyColumns = raw.keyColumns.map((column) => requireIdentifier(column, "keyColumns entry"));
94683
+ if (new Set(keyColumns).size !== keyColumns.length) {
94684
+ throw new Error("keyColumns must not contain duplicates");
94685
+ }
94686
+ if (!Array.isArray(raw.rows) || raw.rows.length === 0) {
94687
+ throw new Error("rows must be a non-empty array");
94688
+ }
94689
+ if (raw.rows.length > MAX_SOURCE_ROWS) {
94690
+ throw new Error(`rows exceeds the bounded maximum of ${MAX_SOURCE_ROWS}`);
94691
+ }
94692
+ const rows = raw.rows.map((row, index) => {
94693
+ if (!isRecord2(row))
94694
+ throw new Error(`rows[${index}] must be an object`);
94695
+ for (const key of keyColumns) {
94696
+ if (!(key in row))
94697
+ throw new Error(`rows[${index}] is missing key column '${key}'`);
94698
+ }
94699
+ return row;
94700
+ });
94701
+ if (typeof raw.verifyQuery !== "string" || raw.verifyQuery.trim() === "") {
94702
+ throw new Error("verifyQuery must be a non-empty string");
94703
+ }
94704
+ if (typeof raw.expect !== "string" || raw.expect.trim() === "") {
94705
+ throw new Error("expect must be a non-empty string");
94706
+ }
94707
+ if (raw.rollbackHint !== undefined && typeof raw.rollbackHint !== "string") {
94708
+ throw new Error("rollbackHint must be a string when provided");
94709
+ }
94710
+ return {
94711
+ table,
94712
+ keyColumns,
94713
+ rows,
94714
+ verifyQuery: assertReadOnlyVerifyQuery(raw.verifyQuery),
94715
+ expect: raw.expect.trim(),
94716
+ rollbackHint: raw.rollbackHint
94717
+ };
94718
+ }
94719
+ function generateBackfillSql(manifest, targetSystem = "postgresql") {
94720
+ return manifest.rows.map((row, index) => {
94721
+ const setColumns = Object.keys(row).filter((column) => !manifest.keyColumns.includes(column));
94722
+ if (setColumns.length === 0)
94723
+ throw new Error(`rows[${index}] has no non-key columns to update`);
94724
+ const set = setColumns.map((column) => `${requireIdentifier(column, `rows[${index}] column`)} = ${sqlLiteral(row[column], targetSystem)}`).join(", ");
94725
+ const where = manifest.keyColumns.map((column) => {
94726
+ const value = row[column];
94727
+ return value === null ? `${column} IS NULL` : `${column} = ${sqlLiteral(value, targetSystem)}`;
94728
+ }).join(" AND ");
94729
+ return `UPDATE ${manifest.table} SET ${set} WHERE ${where}`;
94730
+ });
94731
+ }
94732
+ function compareBackfillIdentities(source, target) {
94733
+ const differences = [];
94734
+ for (const field of ["environment", "system", "database"]) {
94735
+ if (source[field] !== target[field])
94736
+ differences.push(`${field}: ${String(source[field])} -> ${String(target[field])}`);
94737
+ }
94738
+ if (source.server.host !== target.server.host || source.server.port !== target.server.port) {
94739
+ differences.push(`server: ${source.server.host ?? "unknown"}:${source.server.port ?? "unknown"} -> ${target.server.host ?? "unknown"}:${target.server.port ?? "unknown"}`);
94740
+ }
94741
+ return differences;
94742
+ }
94743
+ function buildBackfillArtifact(input) {
94744
+ if (!SQL_TARGET_SYSTEMS.has(input.targetIdentity.system)) {
94745
+ throw new Error(`Source-to-SQL backfill artifacts require a SQL target connection; '${input.targetIdentity.system}' is not supported`);
94746
+ }
94747
+ const statements = generateBackfillSql(input.manifest, input.targetIdentity.system);
94748
+ const target = input.targetIdentity.name;
94749
+ const targetArg = shellQuote(target);
94750
+ const tableArg = shellQuote(input.manifest.table);
94751
+ const planCommand2 = (sql) => `dbcli --use ${targetArg} plan ${shellQuote(sql)} --format json`;
94752
+ const lastStatement = statements[statements.length - 1];
94753
+ return {
94754
+ schemaVersion: BACKFILL_ARTIFACT_SCHEMA_VERSION,
94755
+ kind: "source-to-sql-backfill",
94756
+ createdAt: (input.now ?? new Date).toISOString(),
94757
+ source: {
94758
+ path: input.sourcePath,
94759
+ sha256: createHash4("sha256").update(input.sourceContent).digest("hex"),
94760
+ rowCount: input.manifest.rows.length
94761
+ },
94762
+ table: input.manifest.table,
94763
+ sourceIdentity: input.sourceIdentity,
94764
+ targetIdentity: input.targetIdentity,
94765
+ identityDiff: compareBackfillIdentities(input.sourceIdentity, input.targetIdentity),
94766
+ statements: statements.map((sql) => ({ sql, planCommand: planCommand2(sql) })),
94767
+ preflight: [
94768
+ `dbcli --use ${targetArg} blacklist list --format json`,
94769
+ `dbcli --use ${targetArg} schema ${tableArg} --format json`,
94770
+ ...statements.map(planCommand2)
94771
+ ],
94772
+ readBack: {
94773
+ query: input.manifest.verifyQuery,
94774
+ expect: input.manifest.expect,
94775
+ command: `dbcli --use ${targetArg} verify safe-backfill --table ${tableArg} ` + `--query ${shellQuote(lastStatement)} --verify-query ${shellQuote(input.manifest.verifyQuery)} ` + `--expect ${shellQuote(input.manifest.expect)} --after-write --format json`
94776
+ },
94777
+ rollbackHint: input.manifest.rollbackHint ?? "Capture the prior values before applying this artifact; rollback is a separately reviewed UPDATE with its own verify rollback preflight.",
94778
+ execution: {
94779
+ mode: "dry-run",
94780
+ requiresHumanConfirmation: true,
94781
+ note: "This artifact never executes database writes. Review each plan and apply SQL only through an explicitly human-confirmed workflow."
94782
+ }
94783
+ };
94784
+ }
94785
+ var BACKFILL_ARTIFACT_SCHEMA_VERSION = 1, MAX_SOURCE_ROWS = 1000, SQL_TARGET_SYSTEMS;
94786
+ var init_backfill_artifact = __esm(() => {
94787
+ SQL_TARGET_SYSTEMS = new Set(["postgresql", "mysql", "mariadb"]);
94788
+ });
94789
+
94790
+ // src/commands/backfill.ts
94791
+ import { dirname as dirname14, resolve as resolve9 } from "path";
94792
+ import { mkdir as mkdir17 } from "fs/promises";
94793
+ function identityFor(config, name2) {
94794
+ const connection = config.connections[name2];
94795
+ if (!connection) {
94796
+ throw new Error(`Connection '${name2}' was not found. Available connections: ${Object.keys(config.connections).join(", ")}`);
94797
+ }
94798
+ const host = typeof connection.host === "string" && connection.host.length > 0 ? connection.host : null;
94799
+ const database = typeof connection.database === "string" && connection.database.length > 0 ? connection.database : null;
94800
+ return {
94801
+ name: name2,
94802
+ environment: connection.environment ?? null,
94803
+ permission: connection.permission,
94804
+ system: connection.system,
94805
+ server: {
94806
+ host,
94807
+ port: host !== null && typeof connection.port === "number" ? connection.port : null
94808
+ },
94809
+ database
94810
+ };
94811
+ }
94812
+ var backfillCommand;
94813
+ var init_backfill = __esm(() => {
94814
+ init_esm();
94815
+ init_config_v2();
94816
+ init_config_path();
94817
+ init_backfill_artifact();
94818
+ backfillCommand = new Command("backfill").description("Generate reviewable source-to-SQL backfill artifacts; never executes writes");
94819
+ backfillCommand.command("artifact").description("Build a dry-run backfill artifact from a bounded JSON source catalog and two named connections").requiredOption("--source <path>", "JSON source catalog with table, keyColumns, rows, verifyQuery, and expect").requiredOption("--source-use <name>", "Named source connection used to identify the catalog environment").requiredOption("--target-use <name>", "Named target connection for generated SQL").option("--out <path>", "Write artifact JSON to this path").option("--stdout", "Print artifact JSON instead of writing it", false).action(async (options, command) => {
94820
+ const configPath = resolveConfigPath(command);
94821
+ const config = await readV2Config(configPath);
94822
+ const sourcePath = resolve9(options.source);
94823
+ const sourceContent = await Bun.file(sourcePath).text();
94824
+ let raw;
94825
+ try {
94826
+ raw = JSON.parse(sourceContent);
94827
+ } catch {
94828
+ throw new Error(`Source catalog is not valid JSON: ${sourcePath}`);
94829
+ }
94830
+ const artifact2 = buildBackfillArtifact({
94831
+ manifest: parseBackfillSourceManifest(raw),
94832
+ sourcePath,
94833
+ sourceContent,
94834
+ sourceIdentity: identityFor(config, options.sourceUse),
94835
+ targetIdentity: identityFor(config, options.targetUse)
94836
+ });
94837
+ if (options.stdout) {
94838
+ console.log(JSON.stringify(artifact2, null, 2));
94839
+ return;
94840
+ }
94841
+ const out = resolve9(options.out ?? `.dbcli/backfills/${artifact2.source.sha256.slice(0, 12)}.json`);
94842
+ await mkdir17(dirname14(out), { recursive: true });
94843
+ await Bun.write(out, JSON.stringify(artifact2, null, 2) + `
94844
+ `);
94845
+ console.log(JSON.stringify({ path: out, artifact: artifact2 }, null, 2));
94846
+ });
94847
+ });
94848
+
94037
94849
  // src/program.ts
94038
94850
  var exports_program = {};
94039
94851
  __export(exports_program, {
@@ -94082,7 +94894,7 @@ function hasLongOption(rawArgs, option) {
94082
94894
  return false;
94083
94895
  }
94084
94896
  function buildProgram() {
94085
- const program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).enablePositionalOptions();
94897
+ const program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--global", "Use the user-global connection registry (~/.config/dbcli)", false).addOption(createConnectionSelectorOption()).enablePositionalOptions();
94086
94898
  program2.addCommand(initCommand);
94087
94899
  program2.addCommand(listCommand);
94088
94900
  program2.addCommand(schemaCommand);
@@ -94171,6 +94983,7 @@ function buildProgram() {
94171
94983
  program2.addCommand(verificationCommand);
94172
94984
  program2.addCommand(verifyCommand);
94173
94985
  program2.addCommand(proxyCommand);
94986
+ program2.addCommand(backfillCommand);
94174
94987
  return program2;
94175
94988
  }
94176
94989
  var init_program = __esm(() => {
@@ -94213,6 +95026,7 @@ var init_program = __esm(() => {
94213
95026
  init_migrate();
94214
95027
  init_use();
94215
95028
  init_proxy();
95029
+ init_backfill();
94216
95030
  init_connection_selector();
94217
95031
  init_cli_error();
94218
95032
  init_package();
@@ -94226,9 +95040,121 @@ init_version_check();
94226
95040
  init_skill();
94227
95041
  init_config();
94228
95042
  init_connection_selector();
95043
+ init_config_path();
95044
+
95045
+ // src/utils/cli-output.ts
95046
+ var HUMAN_FORMATS = new Set(["text", "table"]);
95047
+ function isMachineReadableCommand(actionCommand, rootCommand) {
95048
+ for (let current = actionCommand;current; current = current.parent ?? undefined) {
95049
+ const options = current.opts();
95050
+ if (options.forAgent === true || options.recovery === true)
95051
+ return true;
95052
+ if (typeof options.format === "string" && !HUMAN_FORMATS.has(options.format))
95053
+ return true;
95054
+ }
95055
+ if (rootCommand) {
95056
+ const options = rootCommand.opts();
95057
+ if (options.forAgent === true || options.recovery === true)
95058
+ return true;
95059
+ if (typeof options.format === "string" && !HUMAN_FORMATS.has(options.format))
95060
+ return true;
95061
+ }
95062
+ return false;
95063
+ }
95064
+
95065
+ // src/utils/update-hint-state.ts
95066
+ import { mkdir as mkdir4, open, rename as rename2, stat, unlink as unlink3, writeFile } from "fs/promises";
95067
+ import { dirname as dirname4, extname, join as join13 } from "path";
95068
+ var MAX_SESSIONS = 32;
95069
+ function defaultCliSessionKey() {
95070
+ const explicit = process.env.DBCLI_SESSION_ID || process.env.TERM_SESSION_ID;
95071
+ if (explicit)
95072
+ return explicit;
95073
+ return `ppid:${process.ppid ?? process.pid}`;
95074
+ }
95075
+ function statePath(configPath) {
95076
+ const looksLikeFile = extname(configPath) !== "" && !configPath.endsWith(".dbcli");
95077
+ const directory = looksLikeFile ? dirname4(configPath) : configPath;
95078
+ return join13(directory, "update-hints.json");
95079
+ }
95080
+ async function withHintStateLock(path3, work) {
95081
+ const lockPath = `${path3}.lock`;
95082
+ for (let attempt = 0;attempt < 20; attempt += 1) {
95083
+ try {
95084
+ const handle = await open(lockPath, "wx");
95085
+ try {
95086
+ return await work();
95087
+ } finally {
95088
+ await handle.close().catch(() => {
95089
+ return;
95090
+ });
95091
+ await unlink3(lockPath).catch(() => {
95092
+ return;
95093
+ });
95094
+ }
95095
+ } catch (error) {
95096
+ const code = error.code;
95097
+ if (code !== "EEXIST")
95098
+ return null;
95099
+ try {
95100
+ const age = Date.now() - (await stat(lockPath)).mtimeMs;
95101
+ if (age > 1e4)
95102
+ await unlink3(lockPath);
95103
+ } catch {}
95104
+ await Bun.sleep(5);
95105
+ }
95106
+ }
95107
+ return null;
95108
+ }
95109
+ async function readState(path3) {
95110
+ try {
95111
+ const file = Bun.file(path3);
95112
+ if (!await file.exists())
95113
+ return { version: 1, sessions: {} };
95114
+ const raw = await file.json();
95115
+ if (raw.version !== 1 || !raw.sessions || typeof raw.sessions !== "object") {
95116
+ return { version: 1, sessions: {} };
95117
+ }
95118
+ return { version: 1, sessions: raw.sessions };
95119
+ } catch {
95120
+ return { version: 1, sessions: {} };
95121
+ }
95122
+ }
95123
+ async function writeState(path3, state) {
95124
+ try {
95125
+ await mkdir4(dirname4(path3), { recursive: true });
95126
+ const temporary = `${path3}.tmp-${process.pid}`;
95127
+ await writeFile(temporary, JSON.stringify(state, null, 2), "utf8");
95128
+ await rename2(temporary, path3);
95129
+ } catch {}
95130
+ }
95131
+ async function claimUpdateHint(configPath, kind, sessionKey, value) {
95132
+ const path3 = statePath(configPath);
95133
+ await mkdir4(dirname4(path3), { recursive: true }).catch(() => {
95134
+ return;
95135
+ });
95136
+ const claimed = await withHintStateLock(path3, async () => {
95137
+ const state = await readState(path3);
95138
+ const current = state.sessions[sessionKey];
95139
+ if (current?.[kind] !== undefined)
95140
+ return false;
95141
+ state.sessions[sessionKey] = {
95142
+ ...current ?? {},
95143
+ [kind]: value,
95144
+ updatedAt: new Date().toISOString()
95145
+ };
95146
+ const entries = Object.entries(state.sessions).sort((a, b) => a[1].updatedAt.localeCompare(b[1].updatedAt)).slice(-MAX_SESSIONS);
95147
+ state.sessions = Object.fromEntries(entries);
95148
+ await writeState(path3, state);
95149
+ return true;
95150
+ });
95151
+ return claimed === true;
95152
+ }
95153
+
95154
+ // src/cli.ts
94229
95155
  init_program();
94230
95156
  init_cli_error();
94231
- import { join as join35 } from "path";
95157
+ import { join as join38 } from "path";
94232
95158
  import { writeSync as writeSync2 } from "fs";
94233
95159
  import { format } from "util";
94234
95160
  function installSynchronousRedirectedStdout() {
@@ -94269,13 +95195,65 @@ function installSynchronousRedirectedStdout() {
94269
95195
  }
94270
95196
  installSynchronousRedirectedStdout();
94271
95197
  var _bgVersionCheckResult;
95198
+ var _bgVersionCheckContext = null;
94272
95199
  function shouldSkipBackgroundChecks() {
94273
95200
  return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
94274
95201
  }
94275
95202
  var QUIET_OUTPUT_COMMANDS = new Set(["upgrade", "completion"]);
94276
95203
  var program2 = buildProgram();
95204
+ function misplacedConnectionSelectorHint() {
95205
+ const rawArgs = process.argv.slice(2);
95206
+ let commandName;
95207
+ let commandIndex = -1;
95208
+ for (let index = 0;index < rawArgs.length; index += 1) {
95209
+ const token = rawArgs[index];
95210
+ if (token === "--")
95211
+ break;
95212
+ if (token === "--config" || token === "--use") {
95213
+ index += 1;
95214
+ continue;
95215
+ }
95216
+ if (token.startsWith("--config=") || token.startsWith("--use=") || token.startsWith("-")) {
95217
+ continue;
95218
+ }
95219
+ commandName = token;
95220
+ commandIndex = index;
95221
+ break;
95222
+ }
95223
+ const command = program2.commands.find((candidate) => candidate.name() === commandName);
95224
+ if (!command || command.options.some((option) => option.long === "--use"))
95225
+ return;
95226
+ const misplacedOptionIndex = rawArgs.findIndex((token, index) => index > commandIndex && (token === "--use" || token.startsWith("--use=")));
95227
+ if (misplacedOptionIndex === -1)
95228
+ return;
95229
+ const commandPath = [command.name()];
95230
+ let currentCommand = command;
95231
+ for (const token of rawArgs.slice(commandIndex + 1, misplacedOptionIndex)) {
95232
+ const child = currentCommand.commands.find((candidate) => candidate.name() === token);
95233
+ if (!child)
95234
+ continue;
95235
+ commandPath.push(child.name());
95236
+ currentCommand = child;
95237
+ }
95238
+ return `Hint: Place --use before the command: dbcli --use <connection> ${commandPath.join(" ")}`;
95239
+ }
95240
+ function configureConnectionSelectorHints(command) {
95241
+ command.configureOutput({
95242
+ outputError: (message, write) => {
95243
+ const hint = /unknown option '--use(?:'|=)/.test(message) ? misplacedConnectionSelectorHint() : undefined;
95244
+ write(hint ? `${message}${hint}
95245
+ ` : message);
95246
+ }
95247
+ });
95248
+ for (const child of command.commands)
95249
+ configureConnectionSelectorHints(child);
95250
+ }
95251
+ configureConnectionSelectorHints(program2);
94277
95252
  program2.hook("preAction", (thisCommand, actionCommand) => {
95253
+ _bgVersionCheckResult = undefined;
95254
+ _bgVersionCheckContext = null;
94278
95255
  const opts = thisCommand.opts();
95256
+ const machineOutput = isMachineReadableCommand(actionCommand, thisCommand);
94279
95257
  setGlobalConnectionName(resolveConnectionSelector({
94280
95258
  root: opts.use,
94281
95259
  command: actionCommand.opts().use,
@@ -94293,13 +95271,18 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
94293
95271
  level = 2 /* VERBOSE */;
94294
95272
  }
94295
95273
  setGlobalLogger(createLogger(level));
94296
- if (!opts.quiet && !QUIET_OUTPUT_COMMANDS.has(actionCommand.name()) && !shouldSkipBackgroundChecks()) {
94297
- const configPath = resolveConfigPath(actionCommand);
95274
+ const configPath = resolveConfigPath(actionCommand);
95275
+ _bgVersionCheckContext = {
95276
+ configPath,
95277
+ sessionKey: defaultCliSessionKey(),
95278
+ machineOutput
95279
+ };
95280
+ if (!opts.quiet && !QUIET_OUTPUT_COMMANDS.has(actionCommand.name()) && !machineOutput && !shouldSkipBackgroundChecks()) {
94298
95281
  (async () => {
94299
95282
  try {
94300
95283
  let cache = null;
94301
95284
  try {
94302
- const cacheFile = Bun.file(join35(configPath, "version-check.json"));
95285
+ const cacheFile = Bun.file(join38(configPath, "version-check.json"));
94303
95286
  if (await cacheFile.exists()) {
94304
95287
  cache = await cacheFile.json();
94305
95288
  }
@@ -94313,14 +95296,15 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
94313
95296
  }
94314
95297
  });
94315
95298
  program2.hook("postAction", async (thisCommand, actionCommand) => {
94316
- if (_bgVersionCheckResult?.hasUpdate) {
95299
+ const context = _bgVersionCheckContext;
95300
+ if (_bgVersionCheckResult?.hasUpdate && context && !context.machineOutput && !thisCommand.opts().quiet && !QUIET_OUTPUT_COMMANDS.has(actionCommand.name()) && await claimUpdateHint(context.configPath, "update", context.sessionKey, _bgVersionCheckResult.latestVersion)) {
94317
95301
  process.stderr.write(formatUpdateHint(_bgVersionCheckResult.latestVersion) + `
94318
95302
  `);
94319
95303
  }
94320
95304
  const isQuietOutput = QUIET_OUTPUT_COMMANDS.has(actionCommand.name()) || actionCommand.name() === "skill";
94321
- if (!thisCommand.opts().quiet && !isQuietOutput && !shouldSkipBackgroundChecks()) {
95305
+ if (!thisCommand.opts().quiet && !isQuietOutput && !context?.machineOutput && !shouldSkipBackgroundChecks()) {
94322
95306
  const outdatedSkills = await checkSkillUpdates();
94323
- if (outdatedSkills.length > 0) {
95307
+ if (outdatedSkills.length > 0 && context && await claimUpdateHint(context.configPath, "skill", context.sessionKey, outdatedSkills.slice().sort().join(","))) {
94324
95308
  process.stderr.write(formatSkillUpdateReminder(outdatedSkills) + `
94325
95309
  `);
94326
95310
  }