@carllee1983/dbcli 1.29.0 → 1.30.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
@@ -7136,9 +7136,11 @@ async function writeV2Config(path, config) {
7136
7136
  DbcliConfigV2Schema.parse(config);
7137
7137
  const storagePath = await resolveConfigStoragePath(path);
7138
7138
  const configPath = join2(storagePath, "config.json");
7139
+ const tmpPath = `${configPath}.tmp`;
7139
7140
  await Bun.$`mkdir -p ${storagePath}`;
7140
7141
  const json = JSON.stringify(config, null, 2);
7141
- await Bun.write(configPath, json);
7142
+ await Bun.write(tmpPath, json);
7143
+ await Bun.$`mv -f ${tmpPath} ${configPath}`;
7142
7144
  }
7143
7145
  async function patchConnectionSchema(dbcliPath, connectionName, schema, metadataUpdate) {
7144
7146
  const storagePath = await resolveConfigStoragePath(dbcliPath);
@@ -81222,7 +81224,7 @@ var {
81222
81224
  // package.json
81223
81225
  var package_default = {
81224
81226
  name: "@carllee1983/dbcli",
81225
- version: "1.29.0",
81227
+ version: "1.30.0",
81226
81228
  description: "Database CLI for AI agents",
81227
81229
  type: "module",
81228
81230
  publishConfig: {
package/dist/core.d.ts CHANGED
@@ -2769,6 +2769,12 @@ export declare function loadConnectionEnv(resolved: ResolvedConnection, basePath
2769
2769
  * Read and validate a v2 config from disk
2770
2770
  */
2771
2771
  export declare function readV2Config(path: string): Promise<DbcliConfigV2>;
2772
+ /**
2773
+ * Write a v2 config to disk atomically (temp file + rename).
2774
+ * Writing to a temp file then renaming over the target is an atomic operation
2775
+ * on the same filesystem, so a crash mid-write can never leave a corrupt config.
2776
+ */
2777
+ export declare function writeV2Config(path: string, config: DbcliConfigV2): Promise<void>;
2772
2778
  /**
2773
2779
  * List all connection names in a v2 config
2774
2780
  */
@@ -2789,7 +2795,47 @@ export declare function listConnections(config: DbcliConfigV2): Array<{
2789
2795
  };
2790
2796
  isDefault: boolean;
2791
2797
  }>;
2798
+ export type SqlSystem = "postgresql" | "mysql" | "mariadb";
2799
+ /**
2800
+ * `$env` 變數名。per-connection 命名空間化:常駐 sidecar 共用 process.env,
2801
+ * 且 loadEnvFile 不覆寫既有 key——若兩連線都用 DB_PASSWORD 會撞名取到對方的值。
2802
+ */
2803
+ export declare function envVarNameFor(connName: string, field: "password"): string;
2804
+ /** 把 secret 寫進該連線的 envFile(KEY=VALUE);既有同名 key 就地覆寫,否則追加。 */
2805
+ export declare function writeConnectionSecret(projectPath: string, connName: string, field: "password", value: string): Promise<void>;
2806
+ export interface ConnectionInput {
2807
+ name: string;
2808
+ system: SqlSystem;
2809
+ host: string;
2810
+ port: number;
2811
+ user: string;
2812
+ database: string;
2813
+ }
2814
+ /** 刪除連線(immutable)。刪預設則改派為剩餘第一條;刪最後一條則擋下(v2 需至少一條)。 */
2815
+ export declare function removeConnection(config: DbcliConfigV2, name: string): DbcliConfigV2;
2816
+ /** 設定預設連線(immutable)。 */
2817
+ export declare function setDefaultConnection(config: DbcliConfigV2, name: string): DbcliConfigV2;
2818
+ /**
2819
+ * v1 單連線 → v2,產生唯一 'default' 連線。沿用 v1 既有密碼慣例:legacy
2820
+ * `.env.local` 的 `DB_PASSWORD`,故 default 連線 envFile 指向 '.env.local'、
2821
+ * password 設 {$env:'DB_PASSWORD'},不搬動既有 secret。blacklist/audit/metadata 原樣帶過。
2822
+ */
2823
+ export declare function migrateV1ToV2(v1: DbcliConfig$1): DbcliConfigV2;
2824
+ /** 新增或就地覆寫同名連線(immutable)。非機密欄存字面值,password 存 {$env} 參照 +
2825
+ * per-connection envFile。編輯時保留既有 permission;新建預設 'query-only'。 */
2826
+ export declare function upsertConnection(config: DbcliConfigV2, input: ConnectionInput): DbcliConfigV2;
2827
+ interface ProjectConfigBinding {
2828
+ version: 3;
2829
+ binding: {
2830
+ type: "home-storage";
2831
+ storagePath: string;
2832
+ projectPath: string;
2833
+ createdAt: string;
2834
+ };
2835
+ }
2836
+ export declare function getProjectStoragePath(projectPath: string): string;
2792
2837
  export declare function resolveConfigStoragePath(path: string): Promise<string>;
2838
+ export declare function writeProjectBinding(projectPath: string, storagePath?: string): Promise<ProjectConfigBinding>;
2793
2839
  /**
2794
2840
  * Read and fully resolve a `.dbcli` project config: handles project-binding
2795
2841
  * indirection, v1/v2 formats, per-connection `.env` loading and `{$env}`
package/dist/core.mjs CHANGED
@@ -5085,6 +5085,7 @@ class SessionIdService {
5085
5085
  }
5086
5086
 
5087
5087
  // src/core/config-binding.ts
5088
+ import { createHash } from "crypto";
5088
5089
  import { homedir } from "os";
5089
5090
  import { basename, join as join3, resolve } from "path";
5090
5091
  var BINDING_FILE_NAME = "config.json";
@@ -5095,6 +5096,15 @@ function isProjectConfigBinding(raw) {
5095
5096
  const candidate = raw;
5096
5097
  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;
5097
5098
  }
5099
+ function getDbcliHomeRoot() {
5100
+ return DBCLI_HOME_ROOT;
5101
+ }
5102
+ function getProjectStoragePath(projectPath) {
5103
+ const normalizedProjectPath = resolve(projectPath);
5104
+ const projectName = basename(normalizedProjectPath) || "project";
5105
+ const hash = createHash("sha1").update(normalizedProjectPath).digest("hex").slice(0, 12);
5106
+ return join3(getDbcliHomeRoot(), "projects", `${projectName}-${hash}`);
5107
+ }
5098
5108
  async function readProjectBinding(projectPath) {
5099
5109
  const configFile = Bun.file(join3(projectPath, BINDING_FILE_NAME));
5100
5110
  if (!await configFile.exists())
@@ -5110,6 +5120,21 @@ async function resolveConfigStoragePath(path) {
5110
5120
  const binding = await readProjectBinding(path);
5111
5121
  return binding?.binding.storagePath ?? path;
5112
5122
  }
5123
+ async function writeProjectBinding(projectPath, storagePath = getProjectStoragePath(projectPath)) {
5124
+ const binding = {
5125
+ version: 3,
5126
+ binding: {
5127
+ type: "home-storage",
5128
+ storagePath,
5129
+ projectPath: resolve(projectPath),
5130
+ createdAt: new Date().toISOString()
5131
+ }
5132
+ };
5133
+ await Bun.$`mkdir -p ${projectPath}`;
5134
+ await Bun.$`mkdir -p ${storagePath}`;
5135
+ await Bun.file(join3(projectPath, BINDING_FILE_NAME)).write(JSON.stringify(binding, null, 2));
5136
+ return binding;
5137
+ }
5113
5138
 
5114
5139
  // node_modules/zod/v3/external.js
5115
5140
  var exports_external = {};
@@ -9300,6 +9325,16 @@ async function readV2Config(path) {
9300
9325
  const raw = JSON.parse(content);
9301
9326
  return DbcliConfigV2Schema.parse(raw);
9302
9327
  }
9328
+ async function writeV2Config(path, config) {
9329
+ DbcliConfigV2Schema.parse(config);
9330
+ const storagePath = await resolveConfigStoragePath(path);
9331
+ const configPath = join4(storagePath, "config.json");
9332
+ const tmpPath = `${configPath}.tmp`;
9333
+ await Bun.$`mkdir -p ${storagePath}`;
9334
+ const json = JSON.stringify(config, null, 2);
9335
+ await Bun.write(tmpPath, json);
9336
+ await Bun.$`mv -f ${tmpPath} ${configPath}`;
9337
+ }
9303
9338
  function listConnections(config) {
9304
9339
  return Object.entries(config.connections).map(([name, conn]) => {
9305
9340
  const c = conn;
@@ -9819,6 +9854,105 @@ function inferColumnType(value) {
9819
9854
 
9820
9855
  // src/core/public.ts
9821
9856
  init_schema_loader();
9857
+
9858
+ // src/core/config-v2-mutations.ts
9859
+ import { join as join10 } from "path";
9860
+ var SQL_SYSTEMS = ["postgresql", "mysql", "mariadb"];
9861
+ function envVarNameFor(connName, field) {
9862
+ const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9863
+ return `DBCLI_${slug}_${field.toUpperCase()}`;
9864
+ }
9865
+ async function writeConnectionSecret(projectPath, connName, field, value) {
9866
+ if (value.includes(`
9867
+ `) || value.includes("\r")) {
9868
+ throw new Error("secret value \u4E0D\u53EF\u5305\u542B\u63DB\u884C\u5B57\u5143");
9869
+ }
9870
+ const config = await readV2Config(projectPath);
9871
+ const conn = config.connections[connName];
9872
+ if (!conn)
9873
+ throw new Error(`\u9023\u7DDA '${connName}' \u4E0D\u5B58\u5728`);
9874
+ const storagePath = await resolveConfigStoragePath(projectPath);
9875
+ const envFile = conn.envFile ?? `.env.${connName}`;
9876
+ const envPath = join10(storagePath, envFile);
9877
+ const varName = envVarNameFor(connName, field);
9878
+ let content = "";
9879
+ const file = Bun.file(envPath);
9880
+ if (await file.exists())
9881
+ content = await file.text();
9882
+ const line = `${varName}=${value}`;
9883
+ const re = new RegExp(`^${varName}=.*$`, "m");
9884
+ if (re.test(content)) {
9885
+ content = content.replace(re, line);
9886
+ } else {
9887
+ content = content.length && !content.endsWith(`
9888
+ `) ? `${content}
9889
+ ${line}
9890
+ ` : `${content}${line}
9891
+ `;
9892
+ }
9893
+ await Bun.write(envPath, content);
9894
+ }
9895
+ function removeConnection(config, name) {
9896
+ if (!(name in config.connections))
9897
+ throw new Error(`\u9023\u7DDA '${name}' \u4E0D\u5B58\u5728`);
9898
+ const rest = { ...config.connections };
9899
+ delete rest[name];
9900
+ const remaining = Object.keys(rest);
9901
+ if (remaining.length === 0)
9902
+ throw new Error("\u7121\u6CD5\u522A\u9664\u6700\u5F8C\u4E00\u689D\u9023\u7DDA");
9903
+ const nextDefault = config.default === name ? remaining[0] : config.default;
9904
+ return { ...config, connections: rest, default: nextDefault };
9905
+ }
9906
+ function setDefaultConnection(config, name) {
9907
+ if (!(name in config.connections))
9908
+ throw new Error(`\u9023\u7DDA '${name}' \u4E0D\u5B58\u5728`);
9909
+ return { ...config, default: name };
9910
+ }
9911
+ function migrateV1ToV2(v1) {
9912
+ const system = v1.connection.system;
9913
+ if (!SQL_SYSTEMS.includes(system)) {
9914
+ throw new Error(`v1\u2192v2 \u81EA\u52D5\u5347\u7D1A\u76EE\u524D\u50C5\u652F\u63F4 SQL \u9023\u7DDA(mysql/postgresql/mariadb),\u4E0D\u652F\u63F4 '${system}'`);
9915
+ }
9916
+ const c = v1.connection;
9917
+ return {
9918
+ version: 2,
9919
+ default: "default",
9920
+ connections: {
9921
+ default: {
9922
+ system: c.system,
9923
+ host: c.host,
9924
+ port: c.port,
9925
+ user: c.user,
9926
+ database: c.database,
9927
+ password: { $env: "DB_PASSWORD" },
9928
+ permission: v1.permission ?? "query-only",
9929
+ envFile: ".env.local"
9930
+ }
9931
+ },
9932
+ schema: {},
9933
+ schemas: {},
9934
+ metadata: v1.metadata ?? { version: "2.0" },
9935
+ blacklist: v1.blacklist ?? { tables: [], columns: {} },
9936
+ audit: v1.audit ?? { enabled: true, rotation: { max_bytes: 10485760, max_entries: 1000 } }
9937
+ };
9938
+ }
9939
+ function upsertConnection(config, input) {
9940
+ const existing = config.connections[input.name];
9941
+ const connection = {
9942
+ system: input.system,
9943
+ host: input.host,
9944
+ port: input.port,
9945
+ user: input.user,
9946
+ database: input.database,
9947
+ password: { $env: envVarNameFor(input.name, "password") },
9948
+ permission: existing?.permission ?? "query-only",
9949
+ envFile: `.env.${input.name}`
9950
+ };
9951
+ return {
9952
+ ...config,
9953
+ connections: { ...config.connections, [input.name]: connection }
9954
+ };
9955
+ }
9822
9956
  // src/core/blacklist-manager.ts
9823
9957
  class BlacklistManager {
9824
9958
  config;
@@ -9990,12 +10124,21 @@ class BlacklistValidator {
9990
10124
  // src/core/public.ts
9991
10125
  var readConfig = (path, connectionName) => configModule.read(path, connectionName);
9992
10126
  export {
10127
+ writeV2Config,
10128
+ writeProjectBinding,
10129
+ writeConnectionSecret,
10130
+ upsertConnection,
10131
+ setDefaultConnection,
9993
10132
  resolveConnection,
9994
10133
  resolveConfigStoragePath,
10134
+ removeConnection,
9995
10135
  readV2Config,
9996
10136
  readConfig,
10137
+ migrateV1ToV2,
9997
10138
  loadConnectionEnv,
9998
10139
  listConnections,
10140
+ getProjectStoragePath,
10141
+ envVarNameFor,
9999
10142
  detectConfigVersion,
10000
10143
  SchemaLayeredLoader,
10001
10144
  QueryExecutor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {