@carllee1983/dbcli 1.44.1 → 1.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.mjs CHANGED
@@ -753,16 +753,16 @@ var init_index_min = __esm(() => {
753
753
  });
754
754
 
755
755
  // src/utils/schema-path.ts
756
- import { join as join5 } from "path";
756
+ import { join as join6 } from "path";
757
757
  function resolveSchemaPath(dbcliPath, connectionName) {
758
758
  if (!connectionName)
759
- return join5(dbcliPath, "schemas");
760
- return join5(dbcliPath, "schemas", connectionName);
759
+ return join6(dbcliPath, "schemas");
760
+ return join6(dbcliPath, "schemas", connectionName);
761
761
  }
762
762
  var init_schema_path = () => {};
763
763
 
764
764
  // src/core/schema-cache.ts
765
- import { join as join6 } from "path";
765
+ import { join as join7 } from "path";
766
766
 
767
767
  class SchemaCacheManager {
768
768
  cache;
@@ -788,13 +788,13 @@ class SchemaCacheManager {
788
788
  }
789
789
  async initialize() {
790
790
  try {
791
- const indexPath = join6(this.schemaRoot, "index.json");
791
+ const indexPath = join7(this.schemaRoot, "index.json");
792
792
  const indexFile = Bun.file(indexPath);
793
793
  if (await indexFile.exists()) {
794
794
  const indexContent = await indexFile.text();
795
795
  this.index = JSON.parse(indexContent);
796
796
  }
797
- const hotPath = join6(this.schemaRoot, "hot-schemas.json");
797
+ const hotPath = join7(this.schemaRoot, "hot-schemas.json");
798
798
  const hotFile = Bun.file(hotPath);
799
799
  if (await hotFile.exists()) {
800
800
  const hotContent = await hotFile.text();
@@ -827,7 +827,7 @@ class SchemaCacheManager {
827
827
  return null;
828
828
  }
829
829
  try {
830
- const filePath = join6(this.schemaRoot, tableInfo.file);
830
+ const filePath = join7(this.schemaRoot, tableInfo.file);
831
831
  const file = Bun.file(filePath);
832
832
  if (!await file.exists()) {
833
833
  console.error(`Cold table file not found: ${tableInfo.file} for table ${tableName}`);
@@ -882,12 +882,12 @@ var init_schema_cache = __esm(() => {
882
882
  });
883
883
 
884
884
  // src/core/schema-index.ts
885
- import { join as join7 } from "path";
885
+ import { join as join8 } from "path";
886
886
 
887
887
  class SchemaIndexBuilder {
888
888
  static async loadIndex(dbcliPath, connectionName) {
889
889
  try {
890
- const indexPath = join7(resolveSchemaPath(dbcliPath, connectionName), "index.json");
890
+ const indexPath = join8(resolveSchemaPath(dbcliPath, connectionName), "index.json");
891
891
  const file = Bun.file(indexPath);
892
892
  if (!await file.exists()) {
893
893
  return null;
@@ -934,7 +934,7 @@ class SchemaIndexBuilder {
934
934
  try {
935
935
  const schemasDir = resolveSchemaPath(dbcliPath, connectionName);
936
936
  await this.ensureDir(schemasDir);
937
- const indexPath = join7(schemasDir, "index.json");
937
+ const indexPath = join8(schemasDir, "index.json");
938
938
  const indexFile = Bun.file(indexPath);
939
939
  await indexFile.write(JSON.stringify(index, null, 2));
940
940
  } catch (error) {
@@ -980,7 +980,7 @@ var exports_schema_loader = {};
980
980
  __export(exports_schema_loader, {
981
981
  SchemaLayeredLoader: () => SchemaLayeredLoader
982
982
  });
983
- import { join as join8 } from "path";
983
+ import { join as join9 } from "path";
984
984
 
985
985
  class SchemaLayeredLoader {
986
986
  dbcliPath;
@@ -1047,7 +1047,7 @@ class SchemaLayeredLoader {
1047
1047
  }
1048
1048
  async ensureDirectories() {
1049
1049
  const base = resolveSchemaPath(this.dbcliPath, this.connectionName);
1050
- const dirs = [base, join8(base, "cold")];
1050
+ const dirs = [base, join9(base, "cold")];
1051
1051
  for (const dir of dirs) {
1052
1052
  try {
1053
1053
  const dirFile = Bun.file(dir);
@@ -2584,13 +2584,13 @@ var PromisePolyfill;
2584
2584
  var init_promise_polyfill = __esm(() => {
2585
2585
  PromisePolyfill = class PromisePolyfill extends Promise {
2586
2586
  static withResolver() {
2587
- let resolve2;
2587
+ let resolve3;
2588
2588
  let reject;
2589
2589
  const promise = new Promise((res, rej) => {
2590
- resolve2 = res;
2590
+ resolve3 = res;
2591
2591
  reject = rej;
2592
2592
  });
2593
- return { promise, resolve: resolve2, reject };
2593
+ return { promise, resolve: resolve3, reject };
2594
2594
  }
2595
2595
  };
2596
2596
  });
@@ -2629,7 +2629,7 @@ function createPrompt(view) {
2629
2629
  });
2630
2630
  output.mute();
2631
2631
  const screen = new ScreenManager(rl);
2632
- const { promise, resolve: resolve2, reject } = PromisePolyfill.withResolver();
2632
+ const { promise, resolve: resolve3, reject } = PromisePolyfill.withResolver();
2633
2633
  const cancel = () => reject(new CancelPromptError);
2634
2634
  if (signal) {
2635
2635
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -2660,7 +2660,7 @@ function createPrompt(view) {
2660
2660
  try {
2661
2661
  const nextView = view(config, (value) => {
2662
2662
  if (effectsSettled) {
2663
- resolve2(value);
2663
+ resolve3(value);
2664
2664
  } else {
2665
2665
  pendingDone = { value };
2666
2666
  }
@@ -2683,7 +2683,7 @@ function createPrompt(view) {
2683
2683
  if (pendingDone !== null) {
2684
2684
  const { value } = pendingDone;
2685
2685
  pendingDone = null;
2686
- resolve2(value);
2686
+ resolve3(value);
2687
2687
  }
2688
2688
  });
2689
2689
  };
@@ -8538,7 +8538,7 @@ var require_lib2 = __commonJS((exports) => {
8538
8538
  return matches;
8539
8539
  };
8540
8540
  exports.analyse = analyse;
8541
- var detectFile = (filepath, opts = {}) => new Promise((resolve2, reject) => {
8541
+ var detectFile = (filepath, opts = {}) => new Promise((resolve3, reject) => {
8542
8542
  let fd;
8543
8543
  const fs = (0, node_1.default)();
8544
8544
  const handler = (err, buffer) => {
@@ -8548,7 +8548,7 @@ var require_lib2 = __commonJS((exports) => {
8548
8548
  if (err) {
8549
8549
  reject(err);
8550
8550
  } else if (buffer) {
8551
- resolve2((0, exports.detect)(buffer));
8551
+ resolve3((0, exports.detect)(buffer));
8552
8552
  } else {
8553
8553
  reject(new Error("No error and no buffer received"));
8554
8554
  }
@@ -12320,7 +12320,7 @@ import { spawn, spawnSync } from "child_process";
12320
12320
  import { readFileSync, unlinkSync, writeFileSync } from "fs";
12321
12321
  import path2 from "path";
12322
12322
  import os from "os";
12323
- import { randomUUID as randomUUID2 } from "crypto";
12323
+ import { randomUUID as randomUUID3 } from "crypto";
12324
12324
  function sanitizeAffix(affix) {
12325
12325
  if (!affix)
12326
12326
  return "";
@@ -12355,12 +12355,12 @@ class ExternalEditor {
12355
12355
  }
12356
12356
  runAsync(callback) {
12357
12357
  this.createTempFile();
12358
- const promise = new Promise((resolve2, reject) => {
12358
+ const promise = new Promise((resolve3, reject) => {
12359
12359
  try {
12360
12360
  const editorProcess = spawn(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: "inherit" });
12361
12361
  editorProcess.on("exit", (code) => {
12362
12362
  this.lastExitStatus = code;
12363
- resolve2();
12363
+ resolve3();
12364
12364
  });
12365
12365
  } catch (launchError) {
12366
12366
  reject(new LaunchEditorError(launchError));
@@ -12389,7 +12389,7 @@ class ExternalEditor {
12389
12389
  createTempFile() {
12390
12390
  try {
12391
12391
  const baseDir = this.fileOptions.dir ?? os.tmpdir();
12392
- const id = randomUUID2();
12392
+ const id = randomUUID3();
12393
12393
  const prefix = sanitizeAffix(this.fileOptions.prefix);
12394
12394
  const postfix = sanitizeAffix(this.fileOptions.postfix);
12395
12395
  const filename = `${prefix}${id}${postfix}`;
@@ -13658,6 +13658,7 @@ var messages_default = {
13658
13658
  connection_success: "\u2713 Database connection successful",
13659
13659
  connection_failed: "\u2717 Database connection failed",
13660
13660
  config_saved: "Configuration saved to .dbcli",
13661
+ config_saved_global: "Global configuration saved to {path}",
13661
13662
  config_exists_overwrite: "Configuration file .dbcli already exists. Overwrite? (y/n): ",
13662
13663
  cancelled: "Cancelled. Configuration not changed.",
13663
13664
  skip_test_env_ref: "Skipping connection test in env-ref mode",
@@ -13937,6 +13938,7 @@ var messages_default2 = {
13937
13938
  connection_success: "\u2713 \u8CC7\u6599\u5EAB\u9023\u63A5\u6210\u529F",
13938
13939
  connection_failed: "\u2717 \u8CC7\u6599\u5EAB\u9023\u63A5\u5931\u6557",
13939
13940
  config_saved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 .dbcli",
13941
+ config_saved_global: "\u5168\u57DF\u8A2D\u5B9A\u5DF2\u5132\u5B58\u81F3 {path}",
13940
13942
  config_exists_overwrite: "\u914D\u7F6E\u6A94\u6848 .dbcli \u5DF2\u5B58\u5728\u3002\u662F\u5426\u8986\u84CB\uFF1F (y/n)\uFF1A",
13941
13943
  cancelled: "\u5DF2\u53D6\u6D88\u3002\u914D\u7F6E\u672A\u66F4\u6539\u3002",
13942
13944
  skip_test_env_ref: "\u8DF3\u904E\u9023\u7DDA\u6E2C\u8A66\uFF08\u74B0\u5883\u8B8A\u6578\u53C3\u7167\u6A21\u5F0F\uFF09",
@@ -17549,12 +17551,210 @@ class SessionIdService {
17549
17551
  }
17550
17552
 
17551
17553
  // src/core/config-binding.ts
17552
- import { createHash } from "crypto";
17553
- import { mkdir as mkdir4, unlink } from "fs/promises";
17554
+ import { createHash as createHash2 } from "crypto";
17555
+ import { mkdir as mkdir5, unlink as unlink2 } from "fs/promises";
17554
17556
  import { homedir } from "os";
17555
- import { basename, join as join3, resolve } from "path";
17557
+ import { basename, join as join4, resolve as resolve2 } from "path";
17558
+
17559
+ // src/agent-core/errors.ts
17560
+ class ConfigError extends Error {
17561
+ constructor(message) {
17562
+ super(message);
17563
+ this.name = "ConfigError";
17564
+ if (Error.captureStackTrace)
17565
+ Error.captureStackTrace(this, ConfigError);
17566
+ }
17567
+ }
17568
+ // src/core/config-mutation-guard.ts
17569
+ function assertConfigMutationApproved() {
17570
+ if (process.env.DBCLI_AGENT_MODE !== "1")
17571
+ return;
17572
+ throw new ConfigError("Agent mode blocks configuration, permission, and credential changes. A human or administrator must run the approved workflow outside agent mode.");
17573
+ }
17574
+
17575
+ // src/core/config-integrity.ts
17576
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
17577
+ import { chmod, lstat, mkdir as mkdir4, rename as rename3, unlink } from "fs/promises";
17578
+ import { dirname as dirname3, join as join3, resolve } from "path";
17579
+ var INTEGRITY_FILE = ".config-integrity.json";
17580
+ function integrityPath(storagePath, recordName = INTEGRITY_FILE) {
17581
+ return join3(storagePath, recordName);
17582
+ }
17583
+ function hash(content) {
17584
+ return createHash("sha256").update(content).digest("hex");
17585
+ }
17586
+ function protectedPath(storagePath, protectedFileName) {
17587
+ return resolve(join3(storagePath, protectedFileName));
17588
+ }
17589
+ function anchorPathFor(protectedFilePath) {
17590
+ const anchorDirectory = process.env.DBCLI_CONFIG_INTEGRITY_ANCHOR_DIR?.trim();
17591
+ if (!anchorDirectory)
17592
+ return null;
17593
+ const key = createHash("sha256").update(protectedFilePath).digest("hex");
17594
+ return join3(resolve(anchorDirectory), `${key}.json`);
17595
+ }
17596
+ async function bestEffortSecureMode(path, mode) {
17597
+ try {
17598
+ await chmod(path, mode);
17599
+ } catch {}
17600
+ }
17601
+ function refusesGroupOrWorldWritable(mode, platform = process.platform) {
17602
+ if (platform === "win32")
17603
+ return false;
17604
+ return (mode & 18) !== 0;
17605
+ }
17606
+ async function assertAgentReadableFile(path, description = "config") {
17607
+ if (process.env.DBCLI_AGENT_MODE !== "1")
17608
+ return;
17609
+ try {
17610
+ const fileStat = await lstat(path);
17611
+ if (fileStat.isSymbolicLink() || !fileStat.isFile()) {
17612
+ throw new ConfigError(`Agent mode refuses a non-regular ${description} file: ${path}. Run the human/admin setup workflow to provision it.`);
17613
+ }
17614
+ if (refusesGroupOrWorldWritable(fileStat.mode)) {
17615
+ throw new ConfigError(`Agent mode refuses a group/world-writable config: ${path}. Run the human/admin setup workflow to secure it.`);
17616
+ }
17617
+ } catch (error) {
17618
+ if (error instanceof ConfigError)
17619
+ throw error;
17620
+ throw new ConfigError(`Agent mode refuses an unreadable ${description} file: ${path}`);
17621
+ }
17622
+ }
17623
+ async function writeProtectedFileWithIntegrity(storagePath, protectedFileName, protectedContent, recordName) {
17624
+ await mkdir4(storagePath, { recursive: true });
17625
+ const protectedFilePath = protectedPath(storagePath, protectedFileName);
17626
+ const localPath = integrityPath(storagePath, recordName);
17627
+ const anchorPath = anchorPathFor(protectedFilePath);
17628
+ const record = {
17629
+ version: 1,
17630
+ configSha256: hash(protectedContent),
17631
+ updatedAt: new Date().toISOString(),
17632
+ targetPath: protectedFilePath
17633
+ };
17634
+ const recordContent = JSON.stringify(record, null, 2);
17635
+ const suffix = `${process.pid}-${randomUUID2()}`;
17636
+ const protectedTemporary = `${protectedFilePath}.tmp-${suffix}`;
17637
+ const localTemporary = `${localPath}.tmp-${suffix}`;
17638
+ const anchorTemporary = anchorPath ? `${anchorPath}.tmp-${suffix}` : null;
17639
+ const published = [];
17640
+ try {
17641
+ const [protectedSnapshot, localSnapshot, anchorSnapshot] = await Promise.all([
17642
+ snapshot(protectedFilePath),
17643
+ snapshot(localPath),
17644
+ anchorPath ? snapshot(anchorPath) : Promise.resolve(null)
17645
+ ]);
17646
+ await Bun.write(protectedTemporary, protectedContent);
17647
+ await Bun.write(localTemporary, recordContent);
17648
+ if (anchorPath && anchorTemporary) {
17649
+ await mkdir4(dirname3(anchorPath), { recursive: true });
17650
+ await Bun.write(anchorTemporary, recordContent);
17651
+ }
17652
+ const publish = async (temporary, previous) => {
17653
+ await rename3(temporary, previous.path);
17654
+ published.push(previous);
17655
+ };
17656
+ if (anchorTemporary && anchorPath) {
17657
+ await publish(anchorTemporary, anchorSnapshot ?? { path: anchorPath, content: null });
17658
+ }
17659
+ await publish(localTemporary, localSnapshot);
17660
+ await publish(protectedTemporary, protectedSnapshot);
17661
+ } catch (error) {
17662
+ for (const publishedFile of published.reverse()) {
17663
+ await restore(publishedFile).catch(() => {
17664
+ return;
17665
+ });
17666
+ }
17667
+ throw error;
17668
+ } finally {
17669
+ await unlink(protectedTemporary).catch(() => {
17670
+ return;
17671
+ });
17672
+ await unlink(localTemporary).catch(() => {
17673
+ return;
17674
+ });
17675
+ if (anchorTemporary)
17676
+ await unlink(anchorTemporary).catch(() => {
17677
+ return;
17678
+ });
17679
+ }
17680
+ if (anchorPath) {
17681
+ await bestEffortSecureMode(dirname3(anchorPath), 448);
17682
+ await bestEffortSecureMode(anchorPath, 384);
17683
+ }
17684
+ await bestEffortSecureMode(storagePath, 448);
17685
+ await bestEffortSecureMode(protectedFilePath, 384);
17686
+ await bestEffortSecureMode(localPath, 384);
17687
+ }
17688
+ async function snapshot(path) {
17689
+ const file = Bun.file(path);
17690
+ return { path, content: await file.exists() ? await file.text() : null };
17691
+ }
17692
+ async function restore(snapshot2) {
17693
+ if (snapshot2.content === null) {
17694
+ await unlink(snapshot2.path).catch(() => {
17695
+ return;
17696
+ });
17697
+ return;
17698
+ }
17699
+ const temporary = `${snapshot2.path}.rollback-${process.pid}-${randomUUID2()}`;
17700
+ await Bun.write(temporary, snapshot2.content);
17701
+ await rename3(temporary, snapshot2.path);
17702
+ }
17703
+ async function writeConfigWithIntegrity(storagePath, configContent) {
17704
+ await writeProtectedFileWithIntegrity(storagePath, "config.json", configContent, INTEGRITY_FILE);
17705
+ }
17706
+ async function writeBindingWithIntegrity(projectPath, bindingContent) {
17707
+ await writeProtectedFileWithIntegrity(projectPath, "config.json", bindingContent, ".binding-integrity.json");
17708
+ }
17709
+ async function assertIntegrityRecord(storagePath, configContent, recordName, protectedFileName, description, options = {}) {
17710
+ if (process.env.DBCLI_AGENT_MODE !== "1")
17711
+ return;
17712
+ const configPath = protectedPath(storagePath, protectedFileName);
17713
+ await assertAgentReadableFile(configPath, description);
17714
+ const verifyRecord = async (path, label) => {
17715
+ const file = Bun.file(path);
17716
+ if (!await file.exists()) {
17717
+ throw new ConfigError(`Agent mode refuses a missing ${label}: ${path}`);
17718
+ }
17719
+ let record;
17720
+ try {
17721
+ await assertAgentReadableFile(path, label);
17722
+ record = await file.json();
17723
+ } catch (error) {
17724
+ if (error instanceof ConfigError)
17725
+ throw error;
17726
+ throw new ConfigError(`Agent mode refuses an unreadable ${label}: ${path}`);
17727
+ }
17728
+ if (record.version !== 1 || typeof record.configSha256 !== "string" || record.configSha256 !== hash(configContent) || record.targetPath !== undefined && record.targetPath !== configPath) {
17729
+ throw new ConfigError(`Agent mode detected direct ${description} tampering or an out-of-band edit: ${configPath}`);
17730
+ }
17731
+ };
17732
+ const anchorPath = anchorPathFor(configPath);
17733
+ if (anchorPath)
17734
+ await verifyRecord(anchorPath, "detached config integrity anchor");
17735
+ const localPath = integrityPath(storagePath, recordName);
17736
+ const localExists = await Bun.file(localPath).exists();
17737
+ if (localExists) {
17738
+ await verifyRecord(localPath, "config integrity record");
17739
+ } else if (options.requireRecord && !anchorPath) {
17740
+ throw new ConfigError(`Agent mode refuses a missing config integrity record: ${localPath}`);
17741
+ }
17742
+ }
17743
+ async function assertConfigIntegrity(storagePath, configContent, options) {
17744
+ await assertIntegrityRecord(storagePath, configContent, INTEGRITY_FILE, "config.json", "config", options);
17745
+ }
17746
+ async function assertBindingIntegrity(projectPath, bindingContent, options) {
17747
+ await assertIntegrityRecord(projectPath, bindingContent, ".binding-integrity.json", "config.json", "project binding", options);
17748
+ }
17749
+
17750
+ // src/core/config-binding.ts
17556
17751
  var BINDING_FILE_NAME = "config.json";
17557
- var DBCLI_HOME_ROOT = join3(homedir(), ".config", "dbcli");
17752
+ function getDbcliConfigHome() {
17753
+ const configuredHome = process.env.DBCLI_CONFIG_HOME?.trim();
17754
+ if (configuredHome)
17755
+ return configuredHome;
17756
+ return join4(process.env.HOME?.trim() || homedir(), ".config", "dbcli");
17757
+ }
17558
17758
  function isProjectConfigBinding(raw) {
17559
17759
  if (typeof raw !== "object" || raw === null)
17560
17760
  return false;
@@ -17562,22 +17762,35 @@ function isProjectConfigBinding(raw) {
17562
17762
  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;
17563
17763
  }
17564
17764
  function getDbcliHomeRoot() {
17565
- return DBCLI_HOME_ROOT;
17765
+ return getDbcliConfigHome();
17766
+ }
17767
+ function getGlobalConfigPath() {
17768
+ return getDbcliHomeRoot();
17769
+ }
17770
+ function isGlobalConfigPath(path) {
17771
+ return resolve2(path) === resolve2(getGlobalConfigPath());
17566
17772
  }
17567
17773
  function getProjectStoragePath(projectPath) {
17568
- const normalizedProjectPath = resolve(projectPath);
17774
+ const normalizedProjectPath = resolve2(projectPath);
17569
17775
  const projectName = basename(normalizedProjectPath) || "project";
17570
- const hash = createHash("sha1").update(normalizedProjectPath).digest("hex").slice(0, 12);
17571
- return join3(getDbcliHomeRoot(), "projects", `${projectName}-${hash}`);
17776
+ const hash2 = createHash2("sha1").update(normalizedProjectPath).digest("hex").slice(0, 12);
17777
+ return join4(getDbcliHomeRoot(), "projects", `${projectName}-${hash2}`);
17572
17778
  }
17573
17779
  async function readProjectBinding(projectPath) {
17574
- const configFile = Bun.file(join3(projectPath, BINDING_FILE_NAME));
17780
+ const configFile = Bun.file(join4(projectPath, BINDING_FILE_NAME));
17575
17781
  if (!await configFile.exists())
17576
17782
  return null;
17577
17783
  try {
17578
- const raw = JSON.parse(await configFile.text());
17579
- return isProjectConfigBinding(raw) ? raw : null;
17580
- } catch {
17784
+ await assertAgentReadableFile(join4(projectPath, BINDING_FILE_NAME), "project binding");
17785
+ const content = await configFile.text();
17786
+ const raw = JSON.parse(content);
17787
+ if (!isProjectConfigBinding(raw))
17788
+ return null;
17789
+ await assertBindingIntegrity(projectPath, content, { requireRecord: true });
17790
+ return raw;
17791
+ } catch (error) {
17792
+ if (error instanceof ConfigError)
17793
+ throw error;
17581
17794
  return null;
17582
17795
  }
17583
17796
  }
@@ -17586,18 +17799,20 @@ async function resolveConfigStoragePath(path) {
17586
17799
  return binding?.binding.storagePath ?? path;
17587
17800
  }
17588
17801
  async function writeProjectBinding(projectPath, storagePath = getProjectStoragePath(projectPath)) {
17802
+ assertConfigMutationApproved();
17589
17803
  const binding = {
17590
17804
  version: 3,
17591
17805
  binding: {
17592
17806
  type: "home-storage",
17593
17807
  storagePath,
17594
- projectPath: resolve(projectPath),
17808
+ projectPath: resolve2(projectPath),
17595
17809
  createdAt: new Date().toISOString()
17596
17810
  }
17597
17811
  };
17598
- await mkdir4(projectPath, { recursive: true });
17599
- await mkdir4(storagePath, { recursive: true });
17600
- await Bun.file(join3(projectPath, BINDING_FILE_NAME)).write(JSON.stringify(binding, null, 2));
17812
+ await mkdir5(projectPath, { recursive: true });
17813
+ await mkdir5(storagePath, { recursive: true });
17814
+ const content = JSON.stringify(binding, null, 2);
17815
+ await writeBindingWithIntegrity(projectPath, content);
17601
17816
  return binding;
17602
17817
  }
17603
17818
 
@@ -21628,6 +21843,7 @@ var ConnectionConfigSchema = exports_external.union([
21628
21843
  ElasticsearchConnectionConfigSchema
21629
21844
  ]);
21630
21845
  var PermissionSchema = exports_external.enum(["query-only", "read-write", "data-admin", "admin"]).default("query-only");
21846
+ var EnvironmentLabelSchema = exports_external.string().trim().transform((value) => value || undefined).optional();
21631
21847
  var MetadataSchema = exports_external.object({
21632
21848
  createdAt: exports_external.string().datetime().optional(),
21633
21849
  version: exports_external.string().default("1.0"),
@@ -21667,19 +21883,23 @@ var DbcliConfigSchema = exports_external.object({
21667
21883
  });
21668
21884
  var SqlNamedConnectionSchema = SqlConnectionConfigSchema.extend({
21669
21885
  permission: PermissionSchema,
21670
- envFile: exports_external.string().optional()
21886
+ envFile: exports_external.string().optional(),
21887
+ environment: EnvironmentLabelSchema
21671
21888
  });
21672
21889
  var MongoDBNamedConnectionSchema = MongoDBConnectionConfigSchema.extend({
21673
21890
  permission: PermissionSchema,
21674
- envFile: exports_external.string().optional()
21891
+ envFile: exports_external.string().optional(),
21892
+ environment: EnvironmentLabelSchema
21675
21893
  });
21676
21894
  var RedisNamedConnectionSchema = RedisConnectionConfigSchema.extend({
21677
21895
  permission: PermissionSchema,
21678
- envFile: exports_external.string().optional()
21896
+ envFile: exports_external.string().optional(),
21897
+ environment: EnvironmentLabelSchema
21679
21898
  });
21680
21899
  var ElasticsearchNamedConnectionSchema = ElasticsearchConnectionConfigSchema.extend({
21681
21900
  permission: PermissionSchema,
21682
- envFile: exports_external.string().optional()
21901
+ envFile: exports_external.string().optional(),
21902
+ environment: EnvironmentLabelSchema
21683
21903
  });
21684
21904
  var NamedConnectionSchema = exports_external.union([
21685
21905
  SqlNamedConnectionSchema,
@@ -21704,15 +21924,6 @@ var DbcliConfigV2Schema = exports_external.object({
21704
21924
  path: ["default"]
21705
21925
  });
21706
21926
 
21707
- // src/agent-core/errors.ts
21708
- class ConfigError extends Error {
21709
- constructor(message) {
21710
- super(message);
21711
- this.name = "ConfigError";
21712
- if (Error.captureStackTrace)
21713
- Error.captureStackTrace(this, ConfigError);
21714
- }
21715
- }
21716
21927
  // src/agent-core/env-loader.ts
21717
21928
  import { readFile as readFile3 } from "fs/promises";
21718
21929
  function parseEnvContent(content) {
@@ -21751,55 +21962,78 @@ async function loadEnvFile(filePath) {
21751
21962
  }
21752
21963
  }
21753
21964
  // src/core/config-v2.ts
21754
- import { join as join4 } from "path";
21755
- import { mkdir as mkdir5, rename as rename3 } from "fs/promises";
21965
+ import { join as join5 } from "path";
21756
21966
  function detectConfigVersion(raw) {
21757
21967
  if (typeof raw === "object" && raw !== null && "version" in raw && raw.version === 2 && "connections" in raw) {
21758
21968
  return 2;
21759
21969
  }
21760
21970
  return 1;
21761
21971
  }
21972
+ function assertExplicitProductionSelection(resolved, requestedName) {
21973
+ if (resolved.environment !== "production")
21974
+ return;
21975
+ if (requestedName?.trim() === resolved.name)
21976
+ return;
21977
+ 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`);
21978
+ }
21979
+ function findSimilarConnectionNames(requestedName, availableNames) {
21980
+ const requested = requestedName.toLowerCase();
21981
+ const distanceLimit = Math.max(2, Math.floor(requested.length / 3));
21982
+ return availableNames.map((name) => {
21983
+ const candidate = name.toLowerCase();
21984
+ const distance = levenshteinDistance(requested, candidate);
21985
+ return {
21986
+ name,
21987
+ distance,
21988
+ isSimilar: candidate.includes(requested) || requested.includes(candidate) || distance <= distanceLimit
21989
+ };
21990
+ }).filter(({ isSimilar }) => isSimilar).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name)).slice(0, 3).map(({ name }) => name);
21991
+ }
21992
+ function connectionNotFoundMessage(requestedName, availableNames) {
21993
+ const suggestions = findSimilarConnectionNames(requestedName, availableNames);
21994
+ const suggestion = suggestions.length > 0 ? ` \u4F60\u662F\u5426\u8981\u4F7F\u7528\uFF1A${suggestions.join("\u3001")}\uFF1F` : "";
21995
+ return `\u9023\u7DDA '${requestedName}' \u4E0D\u5B58\u5728\u3002${suggestion} \u53EF\u7528\u9023\u7DDA\uFF1A${availableNames.join(", ")}`;
21996
+ }
21762
21997
  function resolveConnection(config, name) {
21763
21998
  const connectionName = name ?? config.default;
21764
21999
  const conn = config.connections[connectionName];
21765
22000
  if (!conn) {
21766
- const available = Object.keys(config.connections).join(", ");
21767
- throw new ConfigError(`\u9023\u7DDA '${connectionName}' \u4E0D\u5B58\u5728\u3002\u53EF\u7528\u9023\u7DDA\uFF1A${available}`);
22001
+ throw new ConfigError(connectionNotFoundMessage(connectionName, Object.keys(config.connections)));
21768
22002
  }
21769
- const { permission, envFile, ...connectionFields } = conn;
22003
+ const { permission, envFile, environment, ...connectionFields } = conn;
21770
22004
  return {
21771
22005
  name: connectionName,
21772
22006
  connection: connectionFields,
21773
22007
  permission,
21774
- envFile
22008
+ envFile,
22009
+ environment
21775
22010
  };
21776
22011
  }
21777
22012
  async function loadConnectionEnv(resolved, basePath) {
21778
22013
  if (resolved.envFile) {
21779
- const envPath = join4(basePath, resolved.envFile);
22014
+ const envPath = join5(basePath, resolved.envFile);
21780
22015
  await loadEnvFile(envPath);
21781
22016
  }
21782
22017
  }
21783
22018
  async function readV2Config(path) {
21784
22019
  const storagePath = await resolveConfigStoragePath(path);
21785
- const configPath = join4(storagePath, "config.json");
22020
+ const configPath = join5(storagePath, "config.json");
21786
22021
  const file = Bun.file(configPath);
21787
22022
  if (!await file.exists()) {
21788
22023
  throw new ConfigError(`\u627E\u4E0D\u5230 V2 \u8A2D\u5B9A\u6A94\uFF1A${configPath}`);
21789
22024
  }
22025
+ await assertAgentReadableFile(configPath);
21790
22026
  const content = await file.text();
22027
+ await assertConfigIntegrity(storagePath, content, { requireRecord: true });
21791
22028
  const raw = JSON.parse(content);
21792
22029
  return DbcliConfigV2Schema.parse(raw);
21793
22030
  }
21794
22031
  async function writeV2Config(path, config) {
22032
+ assertConfigMutationApproved();
21795
22033
  DbcliConfigV2Schema.parse(config);
21796
22034
  const storagePath = await resolveConfigStoragePath(path);
21797
- const configPath = join4(storagePath, "config.json");
21798
- const tmpPath = `${configPath}.tmp`;
21799
- await mkdir5(storagePath, { recursive: true });
21800
22035
  const json = JSON.stringify(config, null, 2);
21801
- await Bun.write(tmpPath, json);
21802
- await rename3(tmpPath, configPath);
22036
+ await writeConfigWithIntegrity(storagePath, json);
21803
22037
  }
21804
22038
  function listConnections(config) {
21805
22039
  return Object.entries(config.connections).map(([name, conn]) => {
@@ -21817,7 +22051,7 @@ function listConnections(config) {
21817
22051
  }
21818
22052
 
21819
22053
  // src/core/config.ts
21820
- import { join as join9 } from "path";
22054
+ import { join as join10 } from "path";
21821
22055
  import { mkdir as mkdir6 } from "fs/promises";
21822
22056
  // src/agent-core/env-ref.ts
21823
22057
  function resolveEnvRef(value, fieldName, env = process.env) {
@@ -21900,9 +22134,9 @@ var configModule = {
21900
22134
  const binding = await readProjectBinding(path);
21901
22135
  const storagePath = await resolveConfigStoragePath(path);
21902
22136
  if (binding) {
21903
- const storageConfigExists = await Bun.file(join9(storagePath, "config.json")).exists();
22137
+ const storageConfigExists = await Bun.file(join10(storagePath, "config.json")).exists();
21904
22138
  if (!storageConfigExists) {
21905
- throw new ConfigError(`Bound dbcli config not found: ${join9(storagePath, "config.json")}`);
22139
+ throw new ConfigError(`Bound dbcli config not found: ${join10(storagePath, "config.json")}`);
21906
22140
  }
21907
22141
  }
21908
22142
  let isDirectory = false;
@@ -21913,17 +22147,20 @@ var configModule = {
21913
22147
  isDirectory = false;
21914
22148
  }
21915
22149
  if (isDirectory) {
21916
- const configPath = join9(storagePath, "config.json");
22150
+ const configPath = join10(storagePath, "config.json");
21917
22151
  const configFile = Bun.file(configPath);
21918
22152
  const configExists = await configFile.exists();
21919
22153
  if (configExists) {
22154
+ await assertAgentReadableFile(configPath);
21920
22155
  const content = await configFile.text();
22156
+ await assertConfigIntegrity(storagePath, content, { requireRecord: true });
21921
22157
  const config = JSON.parse(content);
21922
22158
  if (detectConfigVersion(config) === 2) {
21923
22159
  const v2Config = DbcliConfigV2Schema.parse(config);
21924
22160
  const resolved = resolveConnection(v2Config, effectiveConnectionName);
22161
+ assertExplicitProductionSelection(resolved, effectiveConnectionName);
21925
22162
  await loadConnectionEnv(resolved, storagePath);
21926
- const envLocalPath = join9(storagePath, ".env.local");
22163
+ const envLocalPath = join10(storagePath, ".env.local");
21927
22164
  const envLocalFile = Bun.file(envLocalPath);
21928
22165
  let legacyPassword = null;
21929
22166
  if (await envLocalFile.exists()) {
@@ -21960,7 +22197,7 @@ var configModule = {
21960
22197
  console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
21961
22198
  }
21962
22199
  }
21963
- return DbcliConfigSchema.parse({
22200
+ const parsedConfig = DbcliConfigSchema.parse({
21964
22201
  connection: resolvedConnection,
21965
22202
  permission: resolved.permission,
21966
22203
  schema,
@@ -21969,10 +22206,15 @@ var configModule = {
21969
22206
  audit: v2Config.audit,
21970
22207
  redis: v2Config.redis
21971
22208
  });
22209
+ return {
22210
+ ...parsedConfig,
22211
+ effectiveConnectionName: resolved.name,
22212
+ ...resolved.environment && { effectiveEnvironment: resolved.environment }
22213
+ };
21972
22214
  }
21973
22215
  assertNoConnectionSelectorOnV1(effectiveConnectionName);
21974
22216
  const resolvedConfig = resolveEnvReferences(config, process.env);
21975
- const envPath = join9(storagePath, ".env.local");
22217
+ const envPath = join10(storagePath, ".env.local");
21976
22218
  const envFile = Bun.file(envPath);
21977
22219
  if (await envFile.exists()) {
21978
22220
  const envContent = await envFile.text();
@@ -21987,12 +22229,18 @@ var configModule = {
21987
22229
  const file = Bun.file(path);
21988
22230
  const exists = await file.exists();
21989
22231
  if (exists) {
22232
+ if (process.env.DBCLI_AGENT_MODE === "1") {
22233
+ throw new ConfigError(`Agent mode refuses legacy single-file config: ${path}. Run the human/admin migration workflow to move it to V2 home storage first.`);
22234
+ }
21990
22235
  assertNoConnectionSelectorOnV1(effectiveConnectionName);
21991
22236
  const content = await file.text();
21992
22237
  const raw = JSON.parse(content);
21993
22238
  const resolved = resolveEnvReferences(raw, process.env);
21994
22239
  return DbcliConfigSchema.parse(resolved);
21995
22240
  }
22241
+ if (process.env.DBCLI_AGENT_MODE === "1") {
22242
+ throw new ConfigError(`Agent mode refuses a missing config: ${path}. Run the human/admin setup workflow to provision it.`);
22243
+ }
21996
22244
  return { ...DEFAULT_CONFIG };
21997
22245
  } catch (error) {
21998
22246
  if (error instanceof ConfigError)
@@ -22034,6 +22282,7 @@ var configModule = {
22034
22282
  },
22035
22283
  async write(path, config) {
22036
22284
  try {
22285
+ assertConfigMutationApproved();
22037
22286
  this.validate(config);
22038
22287
  const storagePath = await resolveConfigStoragePath(path);
22039
22288
  let isDirectory = false;
@@ -22047,9 +22296,8 @@ var configModule = {
22047
22296
  await mkdir6(storagePath, { recursive: true });
22048
22297
  const hasEnvReferences = isEnvReference(config.connection.password);
22049
22298
  if (hasEnvReferences) {
22050
- const configPath = join9(storagePath, "config.json");
22051
22299
  const configJson = JSON.stringify(config, null, 2);
22052
- await Bun.file(configPath).write(configJson);
22300
+ await writeConfigWithIntegrity(storagePath, configJson);
22053
22301
  } else {
22054
22302
  const password = config.connection.password;
22055
22303
  const configWithoutPassword = {
@@ -22060,11 +22308,10 @@ var configModule = {
22060
22308
  }
22061
22309
  };
22062
22310
  delete configWithoutPassword.connection.password;
22063
- const configPath = join9(storagePath, "config.json");
22064
22311
  const configJson = JSON.stringify(configWithoutPassword, null, 2);
22065
- await Bun.file(configPath).write(configJson);
22312
+ await writeConfigWithIntegrity(storagePath, configJson);
22066
22313
  if (password) {
22067
- const envPath = join9(storagePath, ".env.local");
22314
+ const envPath = join10(storagePath, ".env.local");
22068
22315
  const envContent = `# Database Credentials - DO NOT commit to git
22069
22316
 
22070
22317
  DBCLI_PASSWORD=${password}
@@ -22286,7 +22533,8 @@ async function getAuditLogger(config, configPath, connectionName) {
22286
22533
  }
22287
22534
  async function writeAuditEntry(config, commandName, options, outcome) {
22288
22535
  try {
22289
- const logger = await getAuditLogger(config, options.config || ".dbcli", typeof options.connectionName === "string" ? options.connectionName : undefined);
22536
+ const connectionName = typeof options.connectionName === "string" && options.connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
22537
+ const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
22290
22538
  const engine = config.connection?.system || "postgresql";
22291
22539
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
22292
22540
  let tier = getEngineCapability(engine, commandName).tier;
@@ -22309,7 +22557,11 @@ async function writeAuditEntry(config, commandName, options, outcome) {
22309
22557
  ...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
22310
22558
  ...errorMessage && { error: errorMessage },
22311
22559
  ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
22312
- metadata: outcome.metadata
22560
+ metadata: {
22561
+ ...outcome.metadata ?? {},
22562
+ connection_name: connectionName,
22563
+ environment: config.effectiveEnvironment ?? null
22564
+ }
22313
22565
  };
22314
22566
  const result = await logger.write(entry);
22315
22567
  return "success" in result ? result.id : null;
@@ -22612,7 +22864,7 @@ function inferColumnType(value) {
22612
22864
  }
22613
22865
  // src/utils/prompts.ts
22614
22866
  async function readLineFromStdin(prompt = "") {
22615
- return new Promise((resolve2) => {
22867
+ return new Promise((resolve3) => {
22616
22868
  if (prompt) {
22617
22869
  process.stdout.write(prompt);
22618
22870
  }
@@ -22627,12 +22879,12 @@ async function readLineFromStdin(prompt = "") {
22627
22879
  process.stdin.pause();
22628
22880
  process.stdin.removeListener("data", onData);
22629
22881
  process.stdin.removeListener("end", onEnd);
22630
- resolve2((lines[0] ?? "").trim());
22882
+ resolve3((lines[0] ?? "").trim());
22631
22883
  }
22632
22884
  };
22633
22885
  const onEnd = () => {
22634
22886
  process.stdin.removeListener("data", onData);
22635
- resolve2(data.trim());
22887
+ resolve3(data.trim());
22636
22888
  };
22637
22889
  process.stdin.on("data", onData);
22638
22890
  process.stdin.on("end", onEnd);
@@ -22999,13 +23251,14 @@ Parameters:`);
22999
23251
  init_schema_loader();
23000
23252
 
23001
23253
  // src/core/config-v2-mutations.ts
23002
- import { join as join10 } from "path";
23254
+ import { join as join11 } from "path";
23003
23255
  var SQL_SYSTEMS = ["postgresql", "mysql", "mariadb"];
23004
23256
  function envVarNameFor(connName, field) {
23005
23257
  const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
23006
23258
  return `DBCLI_${slug}_${field.toUpperCase()}`;
23007
23259
  }
23008
23260
  async function writeConnectionSecret(projectPath, connName, field, value) {
23261
+ assertConfigMutationApproved();
23009
23262
  if (value.includes(`
23010
23263
  `) || value.includes("\r")) {
23011
23264
  throw new Error("secret value \u4E0D\u53EF\u5305\u542B\u63DB\u884C\u5B57\u5143");
@@ -23016,7 +23269,7 @@ async function writeConnectionSecret(projectPath, connName, field, value) {
23016
23269
  throw new Error(`\u9023\u7DDA '${connName}' \u4E0D\u5B58\u5728`);
23017
23270
  const storagePath = await resolveConfigStoragePath(projectPath);
23018
23271
  const envFile = conn.envFile ?? `.env.${connName}`;
23019
- const envPath = join10(storagePath, envFile);
23272
+ const envPath = join11(storagePath, envFile);
23020
23273
  const varName = envVarNameFor(connName, field);
23021
23274
  let content = "";
23022
23275
  const file = Bun.file(envPath);
@@ -23260,7 +23513,10 @@ export {
23260
23513
  migrateV1ToV2,
23261
23514
  loadConnectionEnv,
23262
23515
  listConnections,
23516
+ isGlobalConfigPath,
23263
23517
  getProjectStoragePath,
23518
+ getGlobalConfigPath,
23519
+ getDbcliConfigHome,
23264
23520
  envVarNameFor,
23265
23521
  detectConfigVersion,
23266
23522
  SchemaLayeredLoader,