@carllee1983/dbcli 1.28.0 → 1.29.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 +1 -1
- package/dist/core.d.ts +12 -3
- package/dist/core.mjs +255 -1
- 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
|
@@ -81222,7 +81222,7 @@ var {
|
|
|
81222
81222
|
// package.json
|
|
81223
81223
|
var package_default = {
|
|
81224
81224
|
name: "@carllee1983/dbcli",
|
|
81225
|
-
version: "1.
|
|
81225
|
+
version: "1.29.0",
|
|
81226
81226
|
description: "Database CLI for AI agents",
|
|
81227
81227
|
type: "module",
|
|
81228
81228
|
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
|
*/
|
|
@@ -2789,6 +2789,15 @@ export declare function listConnections(config: DbcliConfigV2): Array<{
|
|
|
2789
2789
|
};
|
|
2790
2790
|
isDefault: boolean;
|
|
2791
2791
|
}>;
|
|
2792
|
+
export declare function resolveConfigStoragePath(path: string): Promise<string>;
|
|
2793
|
+
/**
|
|
2794
|
+
* Read and fully resolve a `.dbcli` project config: handles project-binding
|
|
2795
|
+
* indirection, v1/v2 formats, per-connection `.env` loading and `{$env}`
|
|
2796
|
+
* expansion. `path` is the `.dbcli` directory (or legacy file). Returns the
|
|
2797
|
+
* default config if none exists. Thin wrapper over the same entrypoint the
|
|
2798
|
+
* CLI commands use.
|
|
2799
|
+
*/
|
|
2800
|
+
export declare const readConfig: (path: string, connectionName?: string) => Promise<DbcliConfig$1>;
|
|
2792
2801
|
|
|
2793
2802
|
export {
|
|
2794
2803
|
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 {
|
|
@@ -9312,10 +9316,256 @@ function listConnections(config) {
|
|
|
9312
9316
|
}
|
|
9313
9317
|
|
|
9314
9318
|
// src/core/config.ts
|
|
9319
|
+
import { join as join9 } from "path";
|
|
9315
9320
|
var _globalConnectionName;
|
|
9316
9321
|
function getGlobalConnectionName() {
|
|
9317
9322
|
return _globalConnectionName;
|
|
9318
9323
|
}
|
|
9324
|
+
var DEFAULT_CONFIG = {
|
|
9325
|
+
connection: {
|
|
9326
|
+
system: "postgresql",
|
|
9327
|
+
host: "localhost",
|
|
9328
|
+
port: 5432,
|
|
9329
|
+
user: "",
|
|
9330
|
+
password: "",
|
|
9331
|
+
database: ""
|
|
9332
|
+
},
|
|
9333
|
+
permission: "query-only",
|
|
9334
|
+
schema: {},
|
|
9335
|
+
metadata: {
|
|
9336
|
+
version: "1.0"
|
|
9337
|
+
},
|
|
9338
|
+
blacklist: { tables: [], columns: {} },
|
|
9339
|
+
audit: {
|
|
9340
|
+
enabled: true,
|
|
9341
|
+
rotation: { max_bytes: 10485760, max_entries: 1000 }
|
|
9342
|
+
}
|
|
9343
|
+
};
|
|
9344
|
+
function isEnvReference(value) {
|
|
9345
|
+
return typeof value === "object" && value !== null && "$env" in value && typeof value.$env === "string";
|
|
9346
|
+
}
|
|
9347
|
+
function resolveEnvReferences(config, env, parentKey, strict = false) {
|
|
9348
|
+
if (isEnvReference(config)) {
|
|
9349
|
+
const envKey = config.$env;
|
|
9350
|
+
const value = env[envKey];
|
|
9351
|
+
if (!value) {
|
|
9352
|
+
if (!strict) {
|
|
9353
|
+
return config;
|
|
9354
|
+
}
|
|
9355
|
+
throw new ConfigError(`Environment variable not defined: ${envKey}
|
|
9356
|
+
` + `Please set ${envKey} in .env or your environment.
|
|
9357
|
+
` + `Hint: check your .env file or run 'export ${envKey}=<value>'`);
|
|
9358
|
+
}
|
|
9359
|
+
if (parentKey === "port") {
|
|
9360
|
+
const portNum = parseInt(value, 10);
|
|
9361
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
9362
|
+
throw new ConfigError(`${envKey} must be a valid port number (1-65535), got: ${value}`);
|
|
9363
|
+
}
|
|
9364
|
+
return portNum;
|
|
9365
|
+
}
|
|
9366
|
+
return value;
|
|
9367
|
+
}
|
|
9368
|
+
if (Array.isArray(config)) {
|
|
9369
|
+
return config.map((item) => resolveEnvReferences(item, env, parentKey, strict));
|
|
9370
|
+
}
|
|
9371
|
+
if (typeof config === "object" && config !== null) {
|
|
9372
|
+
const resolved = {};
|
|
9373
|
+
for (const [key, value] of Object.entries(config)) {
|
|
9374
|
+
resolved[key] = resolveEnvReferences(value, env, key, strict);
|
|
9375
|
+
}
|
|
9376
|
+
return resolved;
|
|
9377
|
+
}
|
|
9378
|
+
return config;
|
|
9379
|
+
}
|
|
9380
|
+
function parseEnvPassword(content) {
|
|
9381
|
+
const match = content.match(/^DBCLI_PASSWORD=(.+)$/m);
|
|
9382
|
+
return match?.[1] != null ? match[1].trim() : null;
|
|
9383
|
+
}
|
|
9384
|
+
var configModule = {
|
|
9385
|
+
async read(path, connectionName) {
|
|
9386
|
+
const effectiveConnectionName = connectionName ?? _globalConnectionName;
|
|
9387
|
+
try {
|
|
9388
|
+
const binding = await readProjectBinding(path);
|
|
9389
|
+
const storagePath = await resolveConfigStoragePath(path);
|
|
9390
|
+
if (binding) {
|
|
9391
|
+
const storageConfigExists = await Bun.file(join9(storagePath, "config.json")).exists();
|
|
9392
|
+
if (!storageConfigExists) {
|
|
9393
|
+
throw new ConfigError(`Bound dbcli config not found: ${join9(storagePath, "config.json")}`);
|
|
9394
|
+
}
|
|
9395
|
+
}
|
|
9396
|
+
let isDirectory = false;
|
|
9397
|
+
try {
|
|
9398
|
+
const stat3 = await Bun.file(storagePath).stat();
|
|
9399
|
+
isDirectory = stat3?.isDirectory() ?? false;
|
|
9400
|
+
} catch {
|
|
9401
|
+
isDirectory = false;
|
|
9402
|
+
}
|
|
9403
|
+
if (isDirectory) {
|
|
9404
|
+
const configPath = join9(storagePath, "config.json");
|
|
9405
|
+
const configFile = Bun.file(configPath);
|
|
9406
|
+
const configExists = await configFile.exists();
|
|
9407
|
+
if (configExists) {
|
|
9408
|
+
const content = await configFile.text();
|
|
9409
|
+
const config = JSON.parse(content);
|
|
9410
|
+
if (detectConfigVersion(config) === 2) {
|
|
9411
|
+
const v2Config = DbcliConfigV2Schema.parse(config);
|
|
9412
|
+
const resolved = resolveConnection(v2Config, effectiveConnectionName);
|
|
9413
|
+
await loadConnectionEnv(resolved, storagePath);
|
|
9414
|
+
const envLocalPath = join9(storagePath, ".env.local");
|
|
9415
|
+
const envLocalFile = Bun.file(envLocalPath);
|
|
9416
|
+
let legacyPassword = null;
|
|
9417
|
+
if (await envLocalFile.exists()) {
|
|
9418
|
+
const envContent = await envLocalFile.text();
|
|
9419
|
+
legacyPassword = parseEnvPassword(envContent);
|
|
9420
|
+
if (legacyPassword && !process.env.DBCLI_PASSWORD) {
|
|
9421
|
+
process.env.DBCLI_PASSWORD = legacyPassword;
|
|
9422
|
+
}
|
|
9423
|
+
}
|
|
9424
|
+
const resolvedConnection = resolveEnvReferences(resolved.connection, process.env, undefined, false);
|
|
9425
|
+
if (!resolvedConnection.password && legacyPassword) {
|
|
9426
|
+
resolvedConnection.password = legacyPassword;
|
|
9427
|
+
}
|
|
9428
|
+
let schema = (v2Config.schemas ?? {})[resolved.name] ?? v2Config.schema;
|
|
9429
|
+
try {
|
|
9430
|
+
const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
|
|
9431
|
+
const loader = new SchemaLayeredLoader2(storagePath, { connectionName: resolved.name });
|
|
9432
|
+
const { cache, index } = await loader.initialize();
|
|
9433
|
+
if (index && Object.keys(index.tables).length > 0) {
|
|
9434
|
+
const layeredSchema = {};
|
|
9435
|
+
for (const tableName of Object.keys(index.tables)) {
|
|
9436
|
+
const s = await cache.getTableSchema(tableName);
|
|
9437
|
+
if (s)
|
|
9438
|
+
layeredSchema[tableName] = s;
|
|
9439
|
+
}
|
|
9440
|
+
if (Object.keys(layeredSchema).length > 0) {
|
|
9441
|
+
schema = layeredSchema;
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
} catch {
|
|
9445
|
+
console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
|
|
9446
|
+
}
|
|
9447
|
+
return DbcliConfigSchema.parse({
|
|
9448
|
+
connection: resolvedConnection,
|
|
9449
|
+
permission: resolved.permission,
|
|
9450
|
+
schema,
|
|
9451
|
+
metadata: v2Config.metadata,
|
|
9452
|
+
blacklist: v2Config.blacklist,
|
|
9453
|
+
audit: v2Config.audit,
|
|
9454
|
+
redis: v2Config.redis
|
|
9455
|
+
});
|
|
9456
|
+
}
|
|
9457
|
+
const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
|
|
9458
|
+
const envPath = join9(storagePath, ".env.local");
|
|
9459
|
+
const envFile = Bun.file(envPath);
|
|
9460
|
+
if (await envFile.exists()) {
|
|
9461
|
+
const envContent = await envFile.text();
|
|
9462
|
+
const password = parseEnvPassword(envContent);
|
|
9463
|
+
if (password && !resolvedConfig.connection.password) {
|
|
9464
|
+
resolvedConfig.connection.password = password;
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
return DbcliConfigSchema.parse(resolvedConfig);
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
const file = Bun.file(path);
|
|
9471
|
+
const exists = await file.exists();
|
|
9472
|
+
if (exists) {
|
|
9473
|
+
const content = await file.text();
|
|
9474
|
+
const raw = JSON.parse(content);
|
|
9475
|
+
const resolved = resolveEnvReferences(raw, process.env, undefined, false);
|
|
9476
|
+
return DbcliConfigSchema.parse(resolved);
|
|
9477
|
+
}
|
|
9478
|
+
return { ...DEFAULT_CONFIG };
|
|
9479
|
+
} catch (error) {
|
|
9480
|
+
if (error instanceof ConfigError)
|
|
9481
|
+
throw error;
|
|
9482
|
+
if (error instanceof Error && error.message.includes("JSON")) {
|
|
9483
|
+
throw new ConfigError(`Failed to parse .dbcli file: ${error.message}`);
|
|
9484
|
+
}
|
|
9485
|
+
throw new ConfigError(`Failed to read .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
9486
|
+
}
|
|
9487
|
+
},
|
|
9488
|
+
validate(raw) {
|
|
9489
|
+
try {
|
|
9490
|
+
return DbcliConfigSchema.parse(raw);
|
|
9491
|
+
} catch (error) {
|
|
9492
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9493
|
+
throw new ConfigError(`Invalid .dbcli config structure: ${errorMessage}`);
|
|
9494
|
+
}
|
|
9495
|
+
},
|
|
9496
|
+
merge(existing, updates) {
|
|
9497
|
+
const mergedConfig = {
|
|
9498
|
+
...existing,
|
|
9499
|
+
...updates,
|
|
9500
|
+
connection: {
|
|
9501
|
+
...existing.connection,
|
|
9502
|
+
...updates.connection || {}
|
|
9503
|
+
},
|
|
9504
|
+
schema: {
|
|
9505
|
+
...existing.schema,
|
|
9506
|
+
...updates.schema || {}
|
|
9507
|
+
},
|
|
9508
|
+
metadata: {
|
|
9509
|
+
...existing.metadata,
|
|
9510
|
+
...updates.metadata || {},
|
|
9511
|
+
createdAt: existing.metadata?.createdAt || new Date().toISOString(),
|
|
9512
|
+
version: existing.metadata?.version || "1.0"
|
|
9513
|
+
}
|
|
9514
|
+
};
|
|
9515
|
+
return mergedConfig;
|
|
9516
|
+
},
|
|
9517
|
+
async write(path, config) {
|
|
9518
|
+
try {
|
|
9519
|
+
this.validate(config);
|
|
9520
|
+
const storagePath = await resolveConfigStoragePath(path);
|
|
9521
|
+
let isDirectory = false;
|
|
9522
|
+
try {
|
|
9523
|
+
const stat3 = await Bun.file(storagePath).stat();
|
|
9524
|
+
isDirectory = stat3?.isDirectory() ?? false;
|
|
9525
|
+
} catch {
|
|
9526
|
+
isDirectory = false;
|
|
9527
|
+
}
|
|
9528
|
+
if (isDirectory || path.endsWith(".dbcli") || path === storagePath && isDirectory) {
|
|
9529
|
+
await Bun.$`mkdir -p ${storagePath}`;
|
|
9530
|
+
const hasEnvReferences = isEnvReference(config.connection.password);
|
|
9531
|
+
if (hasEnvReferences) {
|
|
9532
|
+
const configPath = join9(storagePath, "config.json");
|
|
9533
|
+
const configJson = JSON.stringify(config, null, 2);
|
|
9534
|
+
await Bun.file(configPath).write(configJson);
|
|
9535
|
+
} else {
|
|
9536
|
+
const password = config.connection.password;
|
|
9537
|
+
const configWithoutPassword = {
|
|
9538
|
+
...config,
|
|
9539
|
+
connection: {
|
|
9540
|
+
...config.connection,
|
|
9541
|
+
password: undefined
|
|
9542
|
+
}
|
|
9543
|
+
};
|
|
9544
|
+
delete configWithoutPassword.connection.password;
|
|
9545
|
+
const configPath = join9(storagePath, "config.json");
|
|
9546
|
+
const configJson = JSON.stringify(configWithoutPassword, null, 2);
|
|
9547
|
+
await Bun.file(configPath).write(configJson);
|
|
9548
|
+
if (password) {
|
|
9549
|
+
const envPath = join9(storagePath, ".env.local");
|
|
9550
|
+
const envContent = `# Database Credentials - DO NOT commit to git
|
|
9551
|
+
|
|
9552
|
+
DBCLI_PASSWORD=${password}
|
|
9553
|
+
`;
|
|
9554
|
+
await Bun.file(envPath).write(envContent);
|
|
9555
|
+
}
|
|
9556
|
+
}
|
|
9557
|
+
} else {
|
|
9558
|
+
const json = JSON.stringify(config, null, 2);
|
|
9559
|
+
await Bun.file(path).write(json);
|
|
9560
|
+
}
|
|
9561
|
+
} catch (error) {
|
|
9562
|
+
if (error instanceof ConfigError) {
|
|
9563
|
+
throw error;
|
|
9564
|
+
}
|
|
9565
|
+
throw new ConfigError(`Failed to write .dbcli config: ${error instanceof Error ? error.message : String(error)}`);
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
};
|
|
9319
9569
|
|
|
9320
9570
|
// src/utils/redaction.ts
|
|
9321
9571
|
var SQL_SUBCOMMANDS = new Set(["query", "export"]);
|
|
@@ -9569,7 +9819,6 @@ function inferColumnType(value) {
|
|
|
9569
9819
|
|
|
9570
9820
|
// src/core/public.ts
|
|
9571
9821
|
init_schema_loader();
|
|
9572
|
-
|
|
9573
9822
|
// src/core/blacklist-manager.ts
|
|
9574
9823
|
class BlacklistManager {
|
|
9575
9824
|
config;
|
|
@@ -9737,9 +9986,14 @@ class BlacklistValidator {
|
|
|
9737
9986
|
});
|
|
9738
9987
|
}
|
|
9739
9988
|
}
|
|
9989
|
+
|
|
9990
|
+
// src/core/public.ts
|
|
9991
|
+
var readConfig = (path, connectionName) => configModule.read(path, connectionName);
|
|
9740
9992
|
export {
|
|
9741
9993
|
resolveConnection,
|
|
9994
|
+
resolveConfigStoragePath,
|
|
9742
9995
|
readV2Config,
|
|
9996
|
+
readConfig,
|
|
9743
9997
|
loadConnectionEnv,
|
|
9744
9998
|
listConnections,
|
|
9745
9999
|
detectConfigVersion,
|