@carllee1983/dbcli 1.28.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/CHANGELOG.md +6 -0
- package/dist/cli.mjs +4 -2
- package/dist/core.d.ts +58 -3
- package/dist/core.mjs +397 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to dbcli are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.29.0] - 2026-06-08 - Core Config-Read Entrypoint
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`@carllee1983/dbcli/core` 新增設定載入入口。** 在 `./core` 子路徑公開 `readConfig(path, connectionName?)`(binding-aware、v1/v2、`{$env}` 展開的統一設定讀取,與 CLI 指令同源)、`resolveConfigStoragePath(path)`(project-binding 解參)與型別 `DbcliConfigV2`、`SqlConnectionOptions`/`QueryableConnectionOptions`(SQL adapter 連線型別收窄)。讓外部消費者(如 `dbcli-gui` sidecar)能從 `.dbcli` 專案路徑解出含真實連線資訊的 `DbcliConfig`,不必重寫內部 binding/env 邏輯。CLI 行為不變。
|
|
13
|
+
|
|
8
14
|
## [1.28.0] - 2026-06-08 - Core Subpath Export
|
|
9
15
|
|
|
10
16
|
### Added
|
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(
|
|
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.
|
|
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
|
@@ -44,10 +44,10 @@ export interface ConnectionOptions {
|
|
|
44
44
|
/** Whether to reject unauthorized TLS connections (default: true) */
|
|
45
45
|
rejectUnauthorized?: boolean;
|
|
46
46
|
}
|
|
47
|
-
type SqlConnectionOptions = ConnectionOptions & {
|
|
47
|
+
export type SqlConnectionOptions = ConnectionOptions & {
|
|
48
48
|
system: SqlDatabaseSystem;
|
|
49
49
|
};
|
|
50
|
-
type QueryableConnectionOptions = ConnectionOptions & {
|
|
50
|
+
export type QueryableConnectionOptions = ConnectionOptions & {
|
|
51
51
|
system: QueryableDatabaseSystem;
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
@@ -2516,7 +2516,7 @@ declare const DbcliConfigV2Schema: z.ZodEffects<z.ZodObject<{
|
|
|
2516
2516
|
} | undefined;
|
|
2517
2517
|
schemas?: Record<string, Record<string, any>> | undefined;
|
|
2518
2518
|
}>;
|
|
2519
|
-
type DbcliConfigV2 = z.infer<typeof DbcliConfigV2Schema>;
|
|
2519
|
+
export type DbcliConfigV2 = z.infer<typeof DbcliConfigV2Schema>;
|
|
2520
2520
|
/**
|
|
2521
2521
|
* QueryExecutor class for executing SQL queries with permission checks
|
|
2522
2522
|
*/
|
|
@@ -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,6 +2795,55 @@ 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;
|
|
2837
|
+
export declare function resolveConfigStoragePath(path: string): Promise<string>;
|
|
2838
|
+
export declare function writeProjectBinding(projectPath: string, storagePath?: string): Promise<ProjectConfigBinding>;
|
|
2839
|
+
/**
|
|
2840
|
+
* Read and fully resolve a `.dbcli` project config: handles project-binding
|
|
2841
|
+
* indirection, v1/v2 formats, per-connection `.env` loading and `{$env}`
|
|
2842
|
+
* expansion. `path` is the `.dbcli` directory (or legacy file). Returns the
|
|
2843
|
+
* default config if none exists. Thin wrapper over the same entrypoint the
|
|
2844
|
+
* CLI commands use.
|
|
2845
|
+
*/
|
|
2846
|
+
export declare const readConfig: (path: string, connectionName?: string) => Promise<DbcliConfig$1>;
|
|
2792
2847
|
|
|
2793
2848
|
export {
|
|
2794
2849
|
DbcliConfig$1 as DbcliConfig,
|
package/dist/core.mjs
CHANGED
|
@@ -976,6 +976,10 @@ var init_schema_index = __esm(() => {
|
|
|
976
976
|
});
|
|
977
977
|
|
|
978
978
|
// src/core/schema-loader.ts
|
|
979
|
+
var exports_schema_loader = {};
|
|
980
|
+
__export(exports_schema_loader, {
|
|
981
|
+
SchemaLayeredLoader: () => SchemaLayeredLoader
|
|
982
|
+
});
|
|
979
983
|
import { join as join8 } from "path";
|
|
980
984
|
|
|
981
985
|
class SchemaLayeredLoader {
|
|
@@ -5081,6 +5085,7 @@ class SessionIdService {
|
|
|
5081
5085
|
}
|
|
5082
5086
|
|
|
5083
5087
|
// src/core/config-binding.ts
|
|
5088
|
+
import { createHash } from "crypto";
|
|
5084
5089
|
import { homedir } from "os";
|
|
5085
5090
|
import { basename, join as join3, resolve } from "path";
|
|
5086
5091
|
var BINDING_FILE_NAME = "config.json";
|
|
@@ -5091,6 +5096,15 @@ function isProjectConfigBinding(raw) {
|
|
|
5091
5096
|
const candidate = raw;
|
|
5092
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;
|
|
5093
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
|
+
}
|
|
5094
5108
|
async function readProjectBinding(projectPath) {
|
|
5095
5109
|
const configFile = Bun.file(join3(projectPath, BINDING_FILE_NAME));
|
|
5096
5110
|
if (!await configFile.exists())
|
|
@@ -5106,6 +5120,21 @@ async function resolveConfigStoragePath(path) {
|
|
|
5106
5120
|
const binding = await readProjectBinding(path);
|
|
5107
5121
|
return binding?.binding.storagePath ?? path;
|
|
5108
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
|
+
}
|
|
5109
5138
|
|
|
5110
5139
|
// node_modules/zod/v3/external.js
|
|
5111
5140
|
var exports_external = {};
|
|
@@ -9296,6 +9325,16 @@ async function readV2Config(path) {
|
|
|
9296
9325
|
const raw = JSON.parse(content);
|
|
9297
9326
|
return DbcliConfigV2Schema.parse(raw);
|
|
9298
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
|
+
}
|
|
9299
9338
|
function listConnections(config) {
|
|
9300
9339
|
return Object.entries(config.connections).map(([name, conn]) => {
|
|
9301
9340
|
const c = conn;
|
|
@@ -9312,10 +9351,256 @@ function listConnections(config) {
|
|
|
9312
9351
|
}
|
|
9313
9352
|
|
|
9314
9353
|
// src/core/config.ts
|
|
9354
|
+
import { join as join9 } from "path";
|
|
9315
9355
|
var _globalConnectionName;
|
|
9316
9356
|
function getGlobalConnectionName() {
|
|
9317
9357
|
return _globalConnectionName;
|
|
9318
9358
|
}
|
|
9359
|
+
var DEFAULT_CONFIG = {
|
|
9360
|
+
connection: {
|
|
9361
|
+
system: "postgresql",
|
|
9362
|
+
host: "localhost",
|
|
9363
|
+
port: 5432,
|
|
9364
|
+
user: "",
|
|
9365
|
+
password: "",
|
|
9366
|
+
database: ""
|
|
9367
|
+
},
|
|
9368
|
+
permission: "query-only",
|
|
9369
|
+
schema: {},
|
|
9370
|
+
metadata: {
|
|
9371
|
+
version: "1.0"
|
|
9372
|
+
},
|
|
9373
|
+
blacklist: { tables: [], columns: {} },
|
|
9374
|
+
audit: {
|
|
9375
|
+
enabled: true,
|
|
9376
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
9377
|
+
}
|
|
9378
|
+
};
|
|
9379
|
+
function isEnvReference(value) {
|
|
9380
|
+
return typeof value === "object" && value !== null && "$env" in value && typeof value.$env === "string";
|
|
9381
|
+
}
|
|
9382
|
+
function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
9383
|
+
if (isEnvReference(config)) {
|
|
9384
|
+
const envKey = config.$env;
|
|
9385
|
+
const value = env[envKey];
|
|
9386
|
+
if (!value) {
|
|
9387
|
+
if (!strict) {
|
|
9388
|
+
return config;
|
|
9389
|
+
}
|
|
9390
|
+
throw new ConfigError(`Environment variable not defined: ${envKey}
|
|
9391
|
+
` + `Please set ${envKey} in .env or your environment.
|
|
9392
|
+
` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
|
|
9393
|
+
}
|
|
9394
|
+
if (parentKey === "port") {
|
|
9395
|
+
const portNum = parseInt(value, 10);
|
|
9396
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
9397
|
+
throw new ConfigError(`${envKey} must be a valid port number (1-65535), got: ${value}`);
|
|
9398
|
+
}
|
|
9399
|
+
return portNum;
|
|
9400
|
+
}
|
|
9401
|
+
return value;
|
|
9402
|
+
}
|
|
9403
|
+
if (Array.isArray(config)) {
|
|
9404
|
+
return config.map((item) => resolveEnvReferences(item, env, parentKey, strict));
|
|
9405
|
+
}
|
|
9406
|
+
if (typeof config === "object" && config !== null) {
|
|
9407
|
+
const resolved = {};
|
|
9408
|
+
for (const [key, value] of Object.entries(config)) {
|
|
9409
|
+
resolved[key] = resolveEnvReferences(value, env, key, strict);
|
|
9410
|
+
}
|
|
9411
|
+
return resolved;
|
|
9412
|
+
}
|
|
9413
|
+
return config;
|
|
9414
|
+
}
|
|
9415
|
+
function parseEnvPassword(content) {
|
|
9416
|
+
const match = content.match(/^DBCLI_PASSWORD=(.+)$/m);
|
|
9417
|
+
return match?.[1] != null ? match[1].trim() : null;
|
|
9418
|
+
}
|
|
9419
|
+
var configModule = {
|
|
9420
|
+
async read(path, connectionName) {
|
|
9421
|
+
const effectiveConnectionName = connectionName ?? _globalConnectionName;
|
|
9422
|
+
try {
|
|
9423
|
+
const binding = await readProjectBinding(path);
|
|
9424
|
+
const storagePath = await resolveConfigStoragePath(path);
|
|
9425
|
+
if (binding) {
|
|
9426
|
+
const storageConfigExists = await Bun.file(join9(storagePath, "config.json")).exists();
|
|
9427
|
+
if (!storageConfigExists) {
|
|
9428
|
+
throw new ConfigError(`Bound dbcli config not found: ${join9(storagePath, "config.json")}`);
|
|
9429
|
+
}
|
|
9430
|
+
}
|
|
9431
|
+
let isDirectory = false;
|
|
9432
|
+
try {
|
|
9433
|
+
const stat3 = await Bun.file(storagePath).stat();
|
|
9434
|
+
isDirectory = stat3?.isDirectory() ?? false;
|
|
9435
|
+
} catch {
|
|
9436
|
+
isDirectory = false;
|
|
9437
|
+
}
|
|
9438
|
+
if (isDirectory) {
|
|
9439
|
+
const configPath = join9(storagePath, "config.json");
|
|
9440
|
+
const configFile = Bun.file(configPath);
|
|
9441
|
+
const configExists = await configFile.exists();
|
|
9442
|
+
if (configExists) {
|
|
9443
|
+
const content = await configFile.text();
|
|
9444
|
+
const config = JSON.parse(content);
|
|
9445
|
+
if (detectConfigVersion(config) === 2) {
|
|
9446
|
+
const v2Config = DbcliConfigV2Schema.parse(config);
|
|
9447
|
+
const resolved = resolveConnection(v2Config, effectiveConnectionName);
|
|
9448
|
+
await loadConnectionEnv(resolved, storagePath);
|
|
9449
|
+
const envLocalPath = join9(storagePath, ".env.local");
|
|
9450
|
+
const envLocalFile = Bun.file(envLocalPath);
|
|
9451
|
+
let legacyPassword = null;
|
|
9452
|
+
if (await envLocalFile.exists()) {
|
|
9453
|
+
const envContent = await envLocalFile.text();
|
|
9454
|
+
legacyPassword = parseEnvPassword(envContent);
|
|
9455
|
+
if (legacyPassword && !process.env.DBCLI_PASSWORD) {
|
|
9456
|
+
process.env.DBCLI_PASSWORD = legacyPassword;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env, undefined, false);
|
|
9460
|
+
if (!resolvedConnection.password && legacyPassword) {
|
|
9461
|
+
resolvedConnection.password = legacyPassword;
|
|
9462
|
+
}
|
|
9463
|
+
let schema = (v2Config.schemas ?? {})[resolved.name] ?? v2Config.schema;
|
|
9464
|
+
try {
|
|
9465
|
+
const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
|
|
9466
|
+
const loader = new SchemaLayeredLoader2(storagePath, { connectionName: resolved.name });
|
|
9467
|
+
const { cache, index } = await loader.initialize();
|
|
9468
|
+
if (index && Object.keys(index.tables).length > 0) {
|
|
9469
|
+
const layeredSchema = {};
|
|
9470
|
+
for (const tableName of Object.keys(index.tables)) {
|
|
9471
|
+
const s = await cache.getTableSchema(tableName);
|
|
9472
|
+
if (s)
|
|
9473
|
+
layeredSchema[tableName] = s;
|
|
9474
|
+
}
|
|
9475
|
+
if (Object.keys(layeredSchema).length > 0) {
|
|
9476
|
+
schema = layeredSchema;
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
} catch {
|
|
9480
|
+
console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
|
|
9481
|
+
}
|
|
9482
|
+
return DbcliConfigSchema.parse({
|
|
9483
|
+
connection: resolvedConnection,
|
|
9484
|
+
permission: resolved.permission,
|
|
9485
|
+
schema,
|
|
9486
|
+
metadata: v2Config.metadata,
|
|
9487
|
+
blacklist: v2Config.blacklist,
|
|
9488
|
+
audit: v2Config.audit,
|
|
9489
|
+
redis: v2Config.redis
|
|
9490
|
+
});
|
|
9491
|
+
}
|
|
9492
|
+
const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
|
|
9493
|
+
const envPath = join9(storagePath, ".env.local");
|
|
9494
|
+
const envFile = Bun.file(envPath);
|
|
9495
|
+
if (await envFile.exists()) {
|
|
9496
|
+
const envContent = await envFile.text();
|
|
9497
|
+
const password = parseEnvPassword(envContent);
|
|
9498
|
+
if (password && !resolvedConfig.connection.password) {
|
|
9499
|
+
resolvedConfig.connection.password = password;
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
return DbcliConfigSchema.parse(resolvedConfig);
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
const file = Bun.file(path);
|
|
9506
|
+
const exists = await file.exists();
|
|
9507
|
+
if (exists) {
|
|
9508
|
+
const content = await file.text();
|
|
9509
|
+
const raw = JSON.parse(content);
|
|
9510
|
+
const resolved = resolveEnvReferences(raw, process.env, undefined, false);
|
|
9511
|
+
return DbcliConfigSchema.parse(resolved);
|
|
9512
|
+
}
|
|
9513
|
+
return { ...DEFAULT_CONFIG };
|
|
9514
|
+
} catch (error) {
|
|
9515
|
+
if (error instanceof ConfigError)
|
|
9516
|
+
throw error;
|
|
9517
|
+
if (error instanceof Error && error.message.includes("JSON")) {
|
|
9518
|
+
throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
|
|
9519
|
+
}
|
|
9520
|
+
throw new ConfigError(`Failed to read .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
9521
|
+
}
|
|
9522
|
+
},
|
|
9523
|
+
validate(raw) {
|
|
9524
|
+
try {
|
|
9525
|
+
return DbcliConfigSchema.parse(raw);
|
|
9526
|
+
} catch (error) {
|
|
9527
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9528
|
+
throw new ConfigError(`Invalid .dbcli config structure: ${errorMessage}`);
|
|
9529
|
+
}
|
|
9530
|
+
},
|
|
9531
|
+
merge(existing, updates) {
|
|
9532
|
+
const mergedConfig = {
|
|
9533
|
+
...existing,
|
|
9534
|
+
...updates,
|
|
9535
|
+
connection: {
|
|
9536
|
+
...existing.connection,
|
|
9537
|
+
...updates.connection || {}
|
|
9538
|
+
},
|
|
9539
|
+
schema: {
|
|
9540
|
+
...existing.schema,
|
|
9541
|
+
...updates.schema || {}
|
|
9542
|
+
},
|
|
9543
|
+
metadata: {
|
|
9544
|
+
...existing.metadata,
|
|
9545
|
+
...updates.metadata || {},
|
|
9546
|
+
createdAt: existing.metadata?.createdAt || new Date().toISOString(),
|
|
9547
|
+
version: existing.metadata?.version || "1.0"
|
|
9548
|
+
}
|
|
9549
|
+
};
|
|
9550
|
+
return mergedConfig;
|
|
9551
|
+
},
|
|
9552
|
+
async write(path, config) {
|
|
9553
|
+
try {
|
|
9554
|
+
this.validate(config);
|
|
9555
|
+
const storagePath = await resolveConfigStoragePath(path);
|
|
9556
|
+
let isDirectory = false;
|
|
9557
|
+
try {
|
|
9558
|
+
const stat3 = await Bun.file(storagePath).stat();
|
|
9559
|
+
isDirectory = stat3?.isDirectory() ?? false;
|
|
9560
|
+
} catch {
|
|
9561
|
+
isDirectory = false;
|
|
9562
|
+
}
|
|
9563
|
+
if (isDirectory || path.endsWith(".dbcli") || path === storagePath && isDirectory) {
|
|
9564
|
+
await Bun.$`mkdir -p ${storagePath}`;
|
|
9565
|
+
const hasEnvReferences = isEnvReference(config.connection.password);
|
|
9566
|
+
if (hasEnvReferences) {
|
|
9567
|
+
const configPath = join9(storagePath, "config.json");
|
|
9568
|
+
const configJson = JSON.stringify(config, null, 2);
|
|
9569
|
+
await Bun.file(configPath).write(configJson);
|
|
9570
|
+
} else {
|
|
9571
|
+
const password = config.connection.password;
|
|
9572
|
+
const configWithoutPassword = {
|
|
9573
|
+
...config,
|
|
9574
|
+
connection: {
|
|
9575
|
+
...config.connection,
|
|
9576
|
+
password: undefined
|
|
9577
|
+
}
|
|
9578
|
+
};
|
|
9579
|
+
delete configWithoutPassword.connection.password;
|
|
9580
|
+
const configPath = join9(storagePath, "config.json");
|
|
9581
|
+
const configJson = JSON.stringify(configWithoutPassword, null, 2);
|
|
9582
|
+
await Bun.file(configPath).write(configJson);
|
|
9583
|
+
if (password) {
|
|
9584
|
+
const envPath = join9(storagePath, ".env.local");
|
|
9585
|
+
const envContent = `# Database Credentials - DO NOT commit to git
|
|
9586
|
+
|
|
9587
|
+
DBCLI_PASSWORD=${password}
|
|
9588
|
+
`;
|
|
9589
|
+
await Bun.file(envPath).write(envContent);
|
|
9590
|
+
}
|
|
9591
|
+
}
|
|
9592
|
+
} else {
|
|
9593
|
+
const json = JSON.stringify(config, null, 2);
|
|
9594
|
+
await Bun.file(path).write(json);
|
|
9595
|
+
}
|
|
9596
|
+
} catch (error) {
|
|
9597
|
+
if (error instanceof ConfigError) {
|
|
9598
|
+
throw error;
|
|
9599
|
+
}
|
|
9600
|
+
throw new ConfigError(`Failed to write .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
9601
|
+
}
|
|
9602
|
+
}
|
|
9603
|
+
};
|
|
9319
9604
|
|
|
9320
9605
|
// src/utils/redaction.ts
|
|
9321
9606
|
var SQL_SUBCOMMANDS = new Set(["query", "export"]);
|
|
@@ -9570,6 +9855,104 @@ function inferColumnType(value) {
|
|
|
9570
9855
|
// src/core/public.ts
|
|
9571
9856
|
init_schema_loader();
|
|
9572
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
|
+
}
|
|
9573
9956
|
// src/core/blacklist-manager.ts
|
|
9574
9957
|
class BlacklistManager {
|
|
9575
9958
|
config;
|
|
@@ -9737,11 +10120,25 @@ class BlacklistValidator {
|
|
|
9737
10120
|
});
|
|
9738
10121
|
}
|
|
9739
10122
|
}
|
|
10123
|
+
|
|
10124
|
+
// src/core/public.ts
|
|
10125
|
+
var readConfig = (path, connectionName) => configModule.read(path, connectionName);
|
|
9740
10126
|
export {
|
|
10127
|
+
writeV2Config,
|
|
10128
|
+
writeProjectBinding,
|
|
10129
|
+
writeConnectionSecret,
|
|
10130
|
+
upsertConnection,
|
|
10131
|
+
setDefaultConnection,
|
|
9741
10132
|
resolveConnection,
|
|
10133
|
+
resolveConfigStoragePath,
|
|
10134
|
+
removeConnection,
|
|
9742
10135
|
readV2Config,
|
|
10136
|
+
readConfig,
|
|
10137
|
+
migrateV1ToV2,
|
|
9743
10138
|
loadConnectionEnv,
|
|
9744
10139
|
listConnections,
|
|
10140
|
+
getProjectStoragePath,
|
|
10141
|
+
envVarNameFor,
|
|
9745
10142
|
detectConfigVersion,
|
|
9746
10143
|
SchemaLayeredLoader,
|
|
9747
10144
|
QueryExecutor,
|