@carllee1983/dbcli 1.56.0 → 1.57.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/.cursor/rules/dbcli.mdc +13 -0
- package/.cursor/skills/dbcli/reference.md +41 -0
- package/.github/skills/dbcli/SKILL.md +13 -0
- package/.github/skills/dbcli/reference.md +41 -0
- package/CHANGELOG.md +16 -0
- package/assets/SKILL.md +13 -0
- package/assets/SKILL.zh-TW.md +11 -0
- package/assets/reference.md +41 -0
- package/dist/cli-runtime.mjs +344 -44
- package/dist/cli.mjs +1 -1
- package/dist/core.d.ts +144 -122
- package/dist/core.mjs +175 -37
- package/package.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +13 -0
- package/plugins/dbcli-agent/skills/dbcli/reference.md +41 -0
- package/skills/dbcli/SKILL.md +13 -0
- package/skills/dbcli/reference.md +41 -0
package/dist/core.mjs
CHANGED
|
@@ -1798,6 +1798,20 @@ Reference: {referencePath}`,
|
|
|
1798
1798
|
add_enum_description: "Create an enum type (PostgreSQL only)",
|
|
1799
1799
|
alter_enum_description: "Add a value to an enum type",
|
|
1800
1800
|
drop_enum_description: "Drop an enum type"
|
|
1801
|
+
},
|
|
1802
|
+
password: {
|
|
1803
|
+
description: "Change the password of a single connection, leaving every other setting untouched",
|
|
1804
|
+
arg_connection: "Connection name (defaults to the current default connection)",
|
|
1805
|
+
opt_stdin: "Read the new password from stdin (for rotation scripts)",
|
|
1806
|
+
opt_password: "New password given inline (visible in shell history and process list)",
|
|
1807
|
+
opt_skip_test: "Skip verifying the new password against the database before saving",
|
|
1808
|
+
prompt: "New password for connection '{name}'",
|
|
1809
|
+
source_conflict: "--password and --stdin cannot be used together; pick one source.",
|
|
1810
|
+
needs_source: "No terminal available for a masked prompt. Pass the value with --stdin or --password.",
|
|
1811
|
+
empty: "The new password is empty; nothing was changed.",
|
|
1812
|
+
updated: "Password for connection '{name}' updated \u2192 {envFile} ({varName})",
|
|
1813
|
+
converted: 'The literal password in config.json was replaced by a {{ "$env": "{varName}" }} reference; later rotations only touch the env file.',
|
|
1814
|
+
skipped_test: "Verification skipped (--skip-test): the new password was not checked against the database."
|
|
1801
1815
|
}
|
|
1802
1816
|
};
|
|
1803
1817
|
// resources/lang/zh-TW/messages.json
|
|
@@ -2078,6 +2092,20 @@ var messages_default2 = {
|
|
|
2078
2092
|
add_enum_description: "\u5EFA\u7ACB\u5217\u8209\u578B\u5225\uFF08\u50C5 PostgreSQL\uFF09",
|
|
2079
2093
|
alter_enum_description: "\u65B0\u589E\u5217\u8209\u503C",
|
|
2080
2094
|
drop_enum_description: "\u522A\u9664\u5217\u8209\u578B\u5225"
|
|
2095
|
+
},
|
|
2096
|
+
password: {
|
|
2097
|
+
description: "\u53EA\u8B8A\u66F4\u55AE\u4E00\u9023\u7DDA\u7684\u5BC6\u78BC\uFF0C\u5176\u9918\u8A2D\u5B9A\u539F\u5C01\u4E0D\u52D5",
|
|
2098
|
+
arg_connection: "\u9023\u7DDA\u540D\u7A31\uFF08\u7701\u7565\u6642\u6539\u9810\u8A2D\u9023\u7DDA\uFF09",
|
|
2099
|
+
opt_stdin: "\u5F9E stdin \u8B80\u53D6\u65B0\u5BC6\u78BC\uFF08\u4F9B\u8F2A\u66FF\u8173\u672C\u4F7F\u7528\uFF09",
|
|
2100
|
+
opt_password: "\u76F4\u63A5\u5E36\u5165\u65B0\u5BC6\u78BC\uFF08\u6703\u7559\u5728 shell \u6B77\u53F2\u8207\u884C\u7A0B\u5217\u8868\uFF09",
|
|
2101
|
+
opt_skip_test: "\u5B58\u6A94\u524D\u4E0D\u5148\u7528\u65B0\u5BC6\u78BC\u9023\u7DDA\u9A57\u8B49",
|
|
2102
|
+
prompt: "\u9023\u7DDA '{name}' \u7684\u65B0\u5BC6\u78BC",
|
|
2103
|
+
source_conflict: "--password \u8207 --stdin \u53EA\u80FD\u64C7\u4E00\u3002",
|
|
2104
|
+
needs_source: "\u6C92\u6709\u7D42\u7AEF\u6A5F\u53EF\u505A\u906E\u853D\u8F38\u5165\uFF0C\u8ACB\u6539\u7528 --stdin \u6216 --password \u50B3\u5165\u3002",
|
|
2105
|
+
empty: "\u65B0\u5BC6\u78BC\u662F\u7A7A\u7684\uFF0C\u672A\u505A\u4EFB\u4F55\u8B8A\u66F4\u3002",
|
|
2106
|
+
updated: "\u5DF2\u66F4\u65B0\u9023\u7DDA '{name}' \u7684\u5BC6\u78BC \u2192 {envFile}\uFF08{varName}\uFF09",
|
|
2107
|
+
converted: 'config.json \u88E1\u7684\u660E\u6587\u5BC6\u78BC\u5DF2\u6539\u6210 {{ "$env": "{varName}" }} \u53C3\u7167\uFF0C\u4E4B\u5F8C\u8F2A\u66FF\u53EA\u6703\u52D5 env \u6A94\u3002',
|
|
2108
|
+
skipped_test: "\u5DF2\u8DF3\u904E\u9A57\u8B49\uFF08--skip-test\uFF09\uFF1A\u65B0\u5BC6\u78BC\u672A\u7D93\u8CC7\u6599\u5EAB\u78BA\u8A8D\u3002"
|
|
2081
2109
|
}
|
|
2082
2110
|
};
|
|
2083
2111
|
// resources/lang/en/shell.json
|
|
@@ -6017,6 +6045,13 @@ async function select(message, choices) {
|
|
|
6017
6045
|
return await selectFallback(message, choices);
|
|
6018
6046
|
return await inquirer.select({ message, choices });
|
|
6019
6047
|
}
|
|
6048
|
+
async function secret(message) {
|
|
6049
|
+
const inquirer = await loadInquirer();
|
|
6050
|
+
if (!inquirer) {
|
|
6051
|
+
throw new Error("Masked input is unavailable; pass the value with --stdin or --password instead.");
|
|
6052
|
+
}
|
|
6053
|
+
return await inquirer.password({ message, mask: "*" });
|
|
6054
|
+
}
|
|
6020
6055
|
async function confirm(message) {
|
|
6021
6056
|
if (!process.stdin.isTTY)
|
|
6022
6057
|
return await confirmFallback(message);
|
|
@@ -6028,7 +6063,8 @@ async function confirm(message) {
|
|
|
6028
6063
|
var promptUser = {
|
|
6029
6064
|
text,
|
|
6030
6065
|
select,
|
|
6031
|
-
confirm
|
|
6066
|
+
confirm,
|
|
6067
|
+
secret
|
|
6032
6068
|
};
|
|
6033
6069
|
|
|
6034
6070
|
// src/types/blacklist.ts
|
|
@@ -10755,43 +10791,11 @@ function listConnections(config) {
|
|
|
10755
10791
|
});
|
|
10756
10792
|
}
|
|
10757
10793
|
// src/core/config-v2-mutations.ts
|
|
10758
|
-
import { join as join8 } from "path";
|
|
10759
10794
|
var SQL_SYSTEMS = ["postgresql", "mysql", "mariadb"];
|
|
10760
10795
|
function envVarNameFor(connName, field) {
|
|
10761
10796
|
const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
10762
10797
|
return `DBCLI_${slug}_${field.toUpperCase()}`;
|
|
10763
10798
|
}
|
|
10764
|
-
async function writeConnectionSecret(projectPath, connName, field, value) {
|
|
10765
|
-
assertConfigMutationApproved();
|
|
10766
|
-
if (value.includes(`
|
|
10767
|
-
`) || value.includes("\r")) {
|
|
10768
|
-
throw new Error("secret value \u4E0D\u53EF\u5305\u542B\u63DB\u884C\u5B57\u5143");
|
|
10769
|
-
}
|
|
10770
|
-
const config = await readV2Config(projectPath);
|
|
10771
|
-
const conn = config.connections[connName];
|
|
10772
|
-
if (!conn)
|
|
10773
|
-
throw new Error(`\u9023\u7DDA '${connName}' \u4E0D\u5B58\u5728`);
|
|
10774
|
-
const storagePath = await resolveConfigStoragePath(projectPath);
|
|
10775
|
-
const envFile = conn.envFile ?? `.env.${connName}`;
|
|
10776
|
-
const envPath = join8(storagePath, envFile);
|
|
10777
|
-
const varName = envVarNameFor(connName, field);
|
|
10778
|
-
let content = "";
|
|
10779
|
-
const file = Bun.file(envPath);
|
|
10780
|
-
if (await file.exists())
|
|
10781
|
-
content = await file.text();
|
|
10782
|
-
const line = `${varName}=${value}`;
|
|
10783
|
-
const re = new RegExp(`^${varName}=.*$`, "m");
|
|
10784
|
-
if (re.test(content)) {
|
|
10785
|
-
content = content.replace(re, line);
|
|
10786
|
-
} else {
|
|
10787
|
-
content = content.length && !content.endsWith(`
|
|
10788
|
-
`) ? `${content}
|
|
10789
|
-
${line}
|
|
10790
|
-
` : `${content}${line}
|
|
10791
|
-
`;
|
|
10792
|
-
}
|
|
10793
|
-
await Bun.write(envPath, content);
|
|
10794
|
-
}
|
|
10795
10799
|
function removeConnection(config, name) {
|
|
10796
10800
|
if (!(name in config.connections))
|
|
10797
10801
|
throw new Error(`\u9023\u7DDA '${name}' \u4E0D\u5B58\u5728`);
|
|
@@ -10853,6 +10857,135 @@ function upsertConnection(config, input) {
|
|
|
10853
10857
|
connections: { ...config.connections, [input.name]: connection }
|
|
10854
10858
|
};
|
|
10855
10859
|
}
|
|
10860
|
+
// src/core/connection-credential.ts
|
|
10861
|
+
import { join as join8 } from "path";
|
|
10862
|
+
|
|
10863
|
+
// src/core/env-file-writer.ts
|
|
10864
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile } from "fs/promises";
|
|
10865
|
+
import { dirname as dirname2 } from "path";
|
|
10866
|
+
var SECRET_FILE_MODE = 384;
|
|
10867
|
+
function escapeForRegExp(value) {
|
|
10868
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10869
|
+
}
|
|
10870
|
+
async function upsertEnvVar(envPath, varName, value) {
|
|
10871
|
+
let content = "";
|
|
10872
|
+
try {
|
|
10873
|
+
content = await readFile2(envPath, "utf8");
|
|
10874
|
+
} catch (cause) {
|
|
10875
|
+
const code = cause.code;
|
|
10876
|
+
if (code !== "ENOENT")
|
|
10877
|
+
throw cause;
|
|
10878
|
+
await mkdir3(dirname2(envPath), { recursive: true });
|
|
10879
|
+
}
|
|
10880
|
+
const line = `${varName}="${value}"`;
|
|
10881
|
+
const existing = new RegExp(`^${escapeForRegExp(varName)}=.*$`, "m");
|
|
10882
|
+
if (existing.test(content)) {
|
|
10883
|
+
content = content.replace(existing, () => line);
|
|
10884
|
+
} else {
|
|
10885
|
+
content = content.length && !content.endsWith(`
|
|
10886
|
+
`) ? `${content}
|
|
10887
|
+
${line}
|
|
10888
|
+
` : `${content}${line}
|
|
10889
|
+
`;
|
|
10890
|
+
}
|
|
10891
|
+
await writeFile(envPath, content, { mode: SECRET_FILE_MODE });
|
|
10892
|
+
await chmod2(envPath, SECRET_FILE_MODE);
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10895
|
+
// src/core/connection-credential.ts
|
|
10896
|
+
var DEFAULT_ENV_FILE = ".env.local";
|
|
10897
|
+
var V1_VAR_NAME = "DBCLI_PASSWORD";
|
|
10898
|
+
var V1_CONNECTION_NAME = "default";
|
|
10899
|
+
async function readRawConfig(storagePath) {
|
|
10900
|
+
const configPath = join8(storagePath, "config.json");
|
|
10901
|
+
const file = Bun.file(configPath);
|
|
10902
|
+
if (!await file.exists()) {
|
|
10903
|
+
throw new ConfigError(`\u627E\u4E0D\u5230\u8A2D\u5B9A\u6A94\uFF1A${configPath}`);
|
|
10904
|
+
}
|
|
10905
|
+
return JSON.parse(await file.text());
|
|
10906
|
+
}
|
|
10907
|
+
function envRefName(value) {
|
|
10908
|
+
if (typeof value === "object" && value !== null && "$env" in value) {
|
|
10909
|
+
return String(value.$env);
|
|
10910
|
+
}
|
|
10911
|
+
return;
|
|
10912
|
+
}
|
|
10913
|
+
async function resolvePasswordTarget(projectPath, connectionName) {
|
|
10914
|
+
const storagePath = await resolveConfigStoragePath(projectPath);
|
|
10915
|
+
const raw = await readRawConfig(storagePath);
|
|
10916
|
+
if (detectConfigVersion(raw) !== 2) {
|
|
10917
|
+
if (connectionName && connectionName !== V1_CONNECTION_NAME) {
|
|
10918
|
+
throw new ConfigError(`\u9019\u662F v1 \u8A2D\u5B9A\uFF0C\u53EA\u6709\u4E00\u689D\u9023\u7DDA\uFF0C\u7121\u6CD5\u6307\u5B9A\u9023\u7DDA\u540D\u7A31 '${connectionName}'\u3002` + `\u9700\u8981\u591A\u9023\u7DDA\u8ACB\u5148\u7528 dbcli init --conn-name <name> \u5347\u7D1A\u5230 v2\u3002`);
|
|
10919
|
+
}
|
|
10920
|
+
const connection2 = raw.connection ?? {};
|
|
10921
|
+
const customRef = envRefName(connection2.password);
|
|
10922
|
+
if (customRef !== undefined && customRef !== V1_VAR_NAME) {
|
|
10923
|
+
throw new ConfigError(`\u9019\u500B v1 \u8A2D\u5B9A\u7684\u5BC6\u78BC\u4F86\u81EA\u74B0\u5883\u8B8A\u6578 '${customRef}'\uFF0Cdbcli \u7121\u6CD5\u4EE3\u70BA\u8F2A\u66FF\u3002` + `\u8ACB\u76F4\u63A5\u66F4\u65B0\u8A72\u74B0\u5883\u8B8A\u6578\uFF0C\u6216\u7528 dbcli init --conn-name <name> \u5347\u7D1A\u5230 v2 \u5F8C\u518D\u8F2A\u66FF\u3002`);
|
|
10924
|
+
}
|
|
10925
|
+
return {
|
|
10926
|
+
connection: V1_CONNECTION_NAME,
|
|
10927
|
+
envFile: DEFAULT_ENV_FILE,
|
|
10928
|
+
varName: V1_VAR_NAME,
|
|
10929
|
+
convertedToEnvRef: false
|
|
10930
|
+
};
|
|
10931
|
+
}
|
|
10932
|
+
const config = await readV2Config(projectPath);
|
|
10933
|
+
const targetName = connectionName ?? config.default;
|
|
10934
|
+
const connection = config.connections[targetName];
|
|
10935
|
+
if (!connection) {
|
|
10936
|
+
throw new ConfigError(connectionNotFoundMessage(targetName, Object.keys(config.connections)));
|
|
10937
|
+
}
|
|
10938
|
+
const referencedVar = envRefName(connection.password);
|
|
10939
|
+
return {
|
|
10940
|
+
connection: targetName,
|
|
10941
|
+
envFile: connection.envFile ?? DEFAULT_ENV_FILE,
|
|
10942
|
+
varName: referencedVar ?? envVarNameFor(targetName, "password"),
|
|
10943
|
+
convertedToEnvRef: referencedVar === undefined
|
|
10944
|
+
};
|
|
10945
|
+
}
|
|
10946
|
+
function assertSingleLine(value) {
|
|
10947
|
+
if (value.includes(`
|
|
10948
|
+
`) || value.includes("\r")) {
|
|
10949
|
+
throw new ConfigError("\u5BC6\u78BC\u4E0D\u53EF\u5305\u542B\u63DB\u884C\u5B57\u5143");
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
async function stripV1LiteralPassword(storagePath, raw) {
|
|
10953
|
+
const connection = raw.connection ?? {};
|
|
10954
|
+
if (typeof connection.password !== "string")
|
|
10955
|
+
return;
|
|
10956
|
+
const stripped = { ...raw, connection: { ...connection } };
|
|
10957
|
+
delete stripped.connection.password;
|
|
10958
|
+
await writeConfigWithIntegrity(storagePath, JSON.stringify(stripped, null, 2));
|
|
10959
|
+
}
|
|
10960
|
+
async function setConnectionPassword(projectPath, connectionName, password) {
|
|
10961
|
+
assertConfigMutationApproved();
|
|
10962
|
+
assertSingleLine(password);
|
|
10963
|
+
const target = await resolvePasswordTarget(projectPath, connectionName);
|
|
10964
|
+
const storagePath = await resolveConfigStoragePath(projectPath);
|
|
10965
|
+
const raw = await readRawConfig(storagePath);
|
|
10966
|
+
await upsertEnvVar(join8(storagePath, target.envFile), target.varName, password);
|
|
10967
|
+
if (detectConfigVersion(raw) !== 2) {
|
|
10968
|
+
await stripV1LiteralPassword(storagePath, raw);
|
|
10969
|
+
return target;
|
|
10970
|
+
}
|
|
10971
|
+
const config = await readV2Config(projectPath);
|
|
10972
|
+
const connection = config.connections[target.connection];
|
|
10973
|
+
const needsEnvFile = connection.envFile === undefined;
|
|
10974
|
+
if (target.convertedToEnvRef || needsEnvFile) {
|
|
10975
|
+
await writeV2Config(projectPath, {
|
|
10976
|
+
...config,
|
|
10977
|
+
connections: {
|
|
10978
|
+
...config.connections,
|
|
10979
|
+
[target.connection]: {
|
|
10980
|
+
...connection,
|
|
10981
|
+
envFile: target.envFile,
|
|
10982
|
+
...target.convertedToEnvRef && { password: { $env: target.varName } }
|
|
10983
|
+
}
|
|
10984
|
+
}
|
|
10985
|
+
});
|
|
10986
|
+
}
|
|
10987
|
+
return target;
|
|
10988
|
+
}
|
|
10856
10989
|
// src/utils/config-error-format.ts
|
|
10857
10990
|
var SUPPORTED_SYSTEMS = [
|
|
10858
10991
|
"postgresql",
|
|
@@ -10938,7 +11071,7 @@ function isZodError(error) {
|
|
|
10938
11071
|
|
|
10939
11072
|
// src/core/config.ts
|
|
10940
11073
|
import { join as join9 } from "path";
|
|
10941
|
-
import { mkdir as
|
|
11074
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
10942
11075
|
// src/agent-core/env-ref.ts
|
|
10943
11076
|
function resolveEnvRef(value, fieldName, env = process.env) {
|
|
10944
11077
|
if (typeof value === "string")
|
|
@@ -11008,7 +11141,11 @@ function resolveEnvReferences(config, env, parentKey) {
|
|
|
11008
11141
|
}
|
|
11009
11142
|
function parseEnvPassword(content) {
|
|
11010
11143
|
const match = content.match(/^DBCLI_PASSWORD=(.+)$/m);
|
|
11011
|
-
|
|
11144
|
+
if (match?.[1] == null)
|
|
11145
|
+
return null;
|
|
11146
|
+
const raw = match[1].trim();
|
|
11147
|
+
const quoted = raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'");
|
|
11148
|
+
return quoted && raw.length >= 2 ? raw.slice(1, -1) : raw;
|
|
11012
11149
|
}
|
|
11013
11150
|
var configModule = {
|
|
11014
11151
|
async read(path, connectionName, options = {}) {
|
|
@@ -11187,7 +11324,7 @@ ${formatConfigValidationError(error, raw)}`);
|
|
|
11187
11324
|
isDirectory = false;
|
|
11188
11325
|
}
|
|
11189
11326
|
if (isDirectory || path.endsWith(".dbcli") || path === storagePath && isDirectory) {
|
|
11190
|
-
await
|
|
11327
|
+
await mkdir4(storagePath, { recursive: true });
|
|
11191
11328
|
const hasEnvReferences = isEnvReference(config.connection.password);
|
|
11192
11329
|
if (hasEnvReferences) {
|
|
11193
11330
|
const configJson = JSON.stringify(config, null, 2);
|
|
@@ -11543,9 +11680,10 @@ var readConfig = (path, connectionName) => configModule.read(path, connectionNam
|
|
|
11543
11680
|
export {
|
|
11544
11681
|
writeV2Config,
|
|
11545
11682
|
writeProjectBinding,
|
|
11546
|
-
writeConnectionSecret,
|
|
11547
11683
|
upsertConnection,
|
|
11548
11684
|
setDefaultConnection,
|
|
11685
|
+
setConnectionPassword,
|
|
11686
|
+
resolvePasswordTarget,
|
|
11549
11687
|
resolveConnection,
|
|
11550
11688
|
resolveConfigStoragePath,
|
|
11551
11689
|
removeConnection,
|
package/package.json
CHANGED
|
@@ -310,6 +310,19 @@ dbcli init --rename staging:stg # rename
|
|
|
310
310
|
dbcli init --remove stg # remove
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
Rotating one connection's password — nothing else in the config moves:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
dbcli password prod # masked prompt
|
|
317
|
+
rotate-secret | dbcli password prod --stdin # for scheduled rotation scripts
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The value goes to the env var the config actually references (a literal password
|
|
321
|
+
is converted to `{ "$env": ... }` on first use, and a connection with no
|
|
322
|
+
`envFile` gets one recorded so the reader loads it), is verified by connecting
|
|
323
|
+
before it is saved (`--skip-test` to opt out), and the env file is written
|
|
324
|
+
`0600` on POSIX.
|
|
325
|
+
|
|
313
326
|
For a connection shared across projects, use the explicit root-level `--global` scope. It stores a v2 registry at `~/.config/dbcli/config.json`; it does not create or modify a project binding:
|
|
314
327
|
|
|
315
328
|
```bash
|
|
@@ -22,6 +22,7 @@ never the right move.
|
|
|
22
22
|
**Commands** —
|
|
23
23
|
[init](#init) ·
|
|
24
24
|
[use](#use) ·
|
|
25
|
+
[password](#password) ·
|
|
25
26
|
[list](#list) ·
|
|
26
27
|
[schema](#schema) ·
|
|
27
28
|
[query](#query) ·
|
|
@@ -196,6 +197,46 @@ dbcli list --use prod
|
|
|
196
197
|
|
|
197
198
|
**Options:** `--list`, `--format <text|json>`, `--confirm-production <name>` (required when explicitly selecting a production connection as the default).
|
|
198
199
|
|
|
200
|
+
### password
|
|
201
|
+
|
|
202
|
+
Change one connection's password without touching any other setting — built for
|
|
203
|
+
environments where credentials rotate on a schedule.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
dbcli password # Masked prompt, rotates the default connection
|
|
207
|
+
dbcli password prod # Masked prompt, rotates 'prod'
|
|
208
|
+
rotate-secret | dbcli password prod --stdin # Non-interactive, nothing lands in shell history
|
|
209
|
+
dbcli password prod --password "$NEW" --skip-test --format json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Where the value lands is read from the config, never guessed: a connection whose
|
|
213
|
+
`password` is `{ "$env": "NAME" }` gets `NAME` rewritten in its `envFile`. A
|
|
214
|
+
connection that declares no `envFile` has one recorded (`.env.local`) as part of
|
|
215
|
+
the rotation — without it the reader would never load the file. A connection
|
|
216
|
+
still holding a literal password is converted to
|
|
217
|
+
`{ "$env": "DBCLI_<CONN>_PASSWORD" }` once, so later rotations only touch the env
|
|
218
|
+
file. Values are written quoted (`NAME="..."`), so leading and trailing
|
|
219
|
+
whitespace survives the round trip.
|
|
220
|
+
|
|
221
|
+
v1 configs rewrite `DBCLI_PASSWORD` in `.env.local`, matching the v1 reader. A v1
|
|
222
|
+
config whose password comes from some other environment variable is refused with
|
|
223
|
+
an explanation: v1 has no per-connection env file, so no file dbcli writes could
|
|
224
|
+
make that variable resolve — set it in the environment, or migrate to v2.
|
|
225
|
+
|
|
226
|
+
The new password is verified by connecting with it before anything is written,
|
|
227
|
+
so a bad rotation fails without leaving broken credentials behind. Pass
|
|
228
|
+
`--skip-test` when the database is unreachable from where the command runs. The
|
|
229
|
+
env file is written with `0600` permissions on POSIX systems (Windows has no
|
|
230
|
+
equivalent mode bit — the file inherits the directory's ACL), and the value is
|
|
231
|
+
never echoed or
|
|
232
|
+
logged.
|
|
233
|
+
|
|
234
|
+
**Options:** `[connection]`, `--stdin`, `--password <value>` (visible in shell
|
|
235
|
+
history and the process list — prefer `--stdin`), `--skip-test`,
|
|
236
|
+
`--format <text|json>`.
|
|
237
|
+
|
|
238
|
+
Blocked under `DBCLI_AGENT_MODE=1` like every other credential mutation.
|
|
239
|
+
|
|
199
240
|
### Agent configuration trust boundary
|
|
200
241
|
|
|
201
242
|
When `DBCLI_AGENT_MODE=1`, configuration, permission, and credential mutations
|
package/skills/dbcli/SKILL.md
CHANGED
|
@@ -310,6 +310,19 @@ dbcli init --rename staging:stg # rename
|
|
|
310
310
|
dbcli init --remove stg # remove
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
Rotating one connection's password — nothing else in the config moves:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
dbcli password prod # masked prompt
|
|
317
|
+
rotate-secret | dbcli password prod --stdin # for scheduled rotation scripts
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The value goes to the env var the config actually references (a literal password
|
|
321
|
+
is converted to `{ "$env": ... }` on first use, and a connection with no
|
|
322
|
+
`envFile` gets one recorded so the reader loads it), is verified by connecting
|
|
323
|
+
before it is saved (`--skip-test` to opt out), and the env file is written
|
|
324
|
+
`0600` on POSIX.
|
|
325
|
+
|
|
313
326
|
For a connection shared across projects, use the explicit root-level `--global` scope. It stores a v2 registry at `~/.config/dbcli/config.json`; it does not create or modify a project binding:
|
|
314
327
|
|
|
315
328
|
```bash
|
|
@@ -22,6 +22,7 @@ never the right move.
|
|
|
22
22
|
**Commands** —
|
|
23
23
|
[init](#init) ·
|
|
24
24
|
[use](#use) ·
|
|
25
|
+
[password](#password) ·
|
|
25
26
|
[list](#list) ·
|
|
26
27
|
[schema](#schema) ·
|
|
27
28
|
[query](#query) ·
|
|
@@ -196,6 +197,46 @@ dbcli list --use prod
|
|
|
196
197
|
|
|
197
198
|
**Options:** `--list`, `--format <text|json>`, `--confirm-production <name>` (required when explicitly selecting a production connection as the default).
|
|
198
199
|
|
|
200
|
+
### password
|
|
201
|
+
|
|
202
|
+
Change one connection's password without touching any other setting — built for
|
|
203
|
+
environments where credentials rotate on a schedule.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
dbcli password # Masked prompt, rotates the default connection
|
|
207
|
+
dbcli password prod # Masked prompt, rotates 'prod'
|
|
208
|
+
rotate-secret | dbcli password prod --stdin # Non-interactive, nothing lands in shell history
|
|
209
|
+
dbcli password prod --password "$NEW" --skip-test --format json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Where the value lands is read from the config, never guessed: a connection whose
|
|
213
|
+
`password` is `{ "$env": "NAME" }` gets `NAME` rewritten in its `envFile`. A
|
|
214
|
+
connection that declares no `envFile` has one recorded (`.env.local`) as part of
|
|
215
|
+
the rotation — without it the reader would never load the file. A connection
|
|
216
|
+
still holding a literal password is converted to
|
|
217
|
+
`{ "$env": "DBCLI_<CONN>_PASSWORD" }` once, so later rotations only touch the env
|
|
218
|
+
file. Values are written quoted (`NAME="..."`), so leading and trailing
|
|
219
|
+
whitespace survives the round trip.
|
|
220
|
+
|
|
221
|
+
v1 configs rewrite `DBCLI_PASSWORD` in `.env.local`, matching the v1 reader. A v1
|
|
222
|
+
config whose password comes from some other environment variable is refused with
|
|
223
|
+
an explanation: v1 has no per-connection env file, so no file dbcli writes could
|
|
224
|
+
make that variable resolve — set it in the environment, or migrate to v2.
|
|
225
|
+
|
|
226
|
+
The new password is verified by connecting with it before anything is written,
|
|
227
|
+
so a bad rotation fails without leaving broken credentials behind. Pass
|
|
228
|
+
`--skip-test` when the database is unreachable from where the command runs. The
|
|
229
|
+
env file is written with `0600` permissions on POSIX systems (Windows has no
|
|
230
|
+
equivalent mode bit — the file inherits the directory's ACL), and the value is
|
|
231
|
+
never echoed or
|
|
232
|
+
logged.
|
|
233
|
+
|
|
234
|
+
**Options:** `[connection]`, `--stdin`, `--password <value>` (visible in shell
|
|
235
|
+
history and the process list — prefer `--stdin`), `--skip-test`,
|
|
236
|
+
`--format <text|json>`.
|
|
237
|
+
|
|
238
|
+
Blocked under `DBCLI_AGENT_MODE=1` like every other credential mutation.
|
|
239
|
+
|
|
199
240
|
### Agent configuration trust boundary
|
|
200
241
|
|
|
201
242
|
When `DBCLI_AGENT_MODE=1`, configuration, permission, and credential mutations
|