@carllee1983/dbcli 1.55.1 → 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.
@@ -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
@@ -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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ 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.57.0] - 2026-08-14 - One connection's password, rotated on its own
9
+
10
+ ### Added
11
+
12
+ - **`dbcli password [connection]` — rotating one connection's password no longer means editing the rest of its config.** A connection whose password is rotated on a schedule previously had two options: re-run `init` and re-enter every other field, or hand-edit the env file and hope the key name matched what the reader looks for. The new command changes the password and nothing else. Where the value lands is read from the config rather than derived from a naming rule: `password: { "$env": "NAME" }` rewrites `NAME` in that connection's `envFile`, and a connection still holding a literal password is converted to `{ "$env": "DBCLI_<CONN>_PASSWORD" }` once, so every later rotation touches only the env file. v1 configs rewrite `DBCLI_PASSWORD` in `.env.local`, matching the v1 reader at `src/core/config.ts:200`; a v1 config whose password comes from any other environment variable is refused with the reason, because v1 has no per-connection env file and no file dbcli could write would make that variable resolve. The new password is verified by connecting with it **before** anything is written — a rejected credential exits 1 with the stored value untouched, rather than leaving a config that no longer opens the database — with `--skip-test` for when the database is unreachable from where the rotation runs. Three input paths, one of which must be chosen: a masked prompt (never a plain-text fallback, since that would print the secret into the scrollback), `--stdin` for rotation scripts, and `--password` for callers that accept shell-history exposure. The env file is written `0600` on POSIX (Windows has no equivalent mode bit, so the file inherits the directory's ACL), the value never reaches stdout, stderr, or the audit log, and `DBCLI_AGENT_MODE=1` refuses at the first line of the action rather than after prompting and connecting.
13
+
14
+ ### Fixed
15
+
16
+ - **A password written to a connection that declared no `envFile` could not be read back.** `loadConnectionEnv` (`src/core/config-v2.ts:164`) loads only the file a connection names, and the `.env.local` fallback in `src/core/config.ts:274` recognizes exactly one key, v1's `DBCLI_PASSWORD`. So writing `DBCLI_<CONN>_PASSWORD` into `.env.local` for a v2 connection with no `envFile` produced a config whose `$env` reference resolved to nothing: `Environment variable not defined`, on every subsequent command. Rotation now records `envFile` on the connection as part of the write. `tests/unit/core/connection-credential.test.ts` asserts the round trip through `configModule.read()` for all four paths (v2 with and without `envFile`, v2 converted from a literal, v1) rather than asserting the file merely contains the expected line — the file being right while the reader cannot see it is precisely the failure that shipped otherwise.
17
+
18
+ - **A password containing `$&`, `$1`, or `` $` `` was silently stored as something else.** The in-place rewrite passed the new line as `String.replace`'s second argument, where `$`-patterns are substitution syntax: rotating to `a$&b` over an existing `K=old` wrote `K=aK=oldb`. Verification passed because it used the in-memory value, so the command reported success and the next connection attempt failed with a password nobody could reproduce. The replacement is a function now, which does no `$` expansion. Values are also written quoted (`NAME="…"`), because both env parsers trim the whole line before splitting — an unquoted value silently lost leading and trailing whitespace. `parseEnvPassword` strips one layer of matching quotes to match `parseEnvContent`, so values written by either path read back identically.
19
+
20
+ ### Removed
21
+
22
+ - **`writeConnectionSecret` — the exported helper wrote to a file the reader never opens.** Exported from `@carllee1983/dbcli/core`, it derived the env var name from the connection name and defaulted the file to `.env.<connection>` when a connection declared no `envFile`, while the reader falls back to `.env.local` and, for connections created with `init --use-env-refs --env-password <VAR>`, looks up `<VAR>` rather than the derived name. Both mismatches ended the same way: a write that succeeded and a password that could not be read. `setConnectionPassword` and `resolvePasswordTarget` replace it on the same barrel — they resolve the target from the config, convert a literal password once, and record `envFile` when it is missing. Callers of the old function should switch to `setConnectionPassword(projectPath, connectionName, value)`; the `field` parameter is gone, since `'password'` was its only accepted value.
23
+
24
+ ## [1.56.0] - 2026-08-13 - The package no longer runs the CLI when you import it
25
+
26
+ ### Removed
27
+
28
+ - **`exports["."]` — importing the package ran the CLI instead of returning a module.** The root export pointed at `dist/cli.mjs`, and `src/cli-runtime.ts:280-290` calls `outputHelp()` and `parseAsync(process.argv)` at module top level with no guard. So any import that reached the runtime executed the CLI against the host process's argv: under Bun, `await import('@carllee1983/dbcli')` printed 186 lines of help and exited 1 — the statement after the import never ran — and under Node it threw `ERR_MODULE_NOT_FOUND` for the same extensionless `./cli-runtime` specifier described in v1.55.0. `.` is gone; `./core` and `./agent-core`, both with `types`, are the library surface, and neither bundle contains `cli-runtime`. `bin` does not resolve through `exports`, so `dbcli` itself is untouched. Anyone importing the root now gets `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a process that exits — a declaration withdrawn, not a capability. `tests/integration/runtime-contract.test.ts` asserts no `exports` target is the `bin` target, and keeps a positive control on the top-level side effect, so if `cli-runtime` ever stops executing on import the test says so rather than leaving the export permanently unreconsidered (#67).
29
+
8
30
  ## [1.55.1] - 2026-08-13 - The Bun-missing warning actually reaches the user
9
31
 
10
32
  ### Fixed
package/README.dev.md CHANGED
@@ -108,6 +108,7 @@ Then ship a patch version with the fix. Prefer **deprecate** over **unpublish**
108
108
  - **`prepublishOnly`:** `bun run build` so `dist/cli.mjs` matches current source.
109
109
  - **`engines`:** Declares `bun >= 1.3.3` only. `node` was removed because the published bundles cannot run on Node: `dist/cli.mjs` dynamic-imports the extensionless `./cli-runtime` (Bun's resolver appends `.mjs`, Node's does not) and `dist/core.mjs` calls Bun globals. `tests/integration/runtime-contract.test.ts` fails if `engines.node` comes back without the bundles being fixed to match, and if `dist/agent-core.mjs` — the one entry point that is Node-importable — regresses. See `docs/adr/0008-dbcli-is-a-bun-program-and-engines-says-so.md`.
110
110
  - **Shebang:** `scripts/build.ts` prepends `#!/usr/bin/env bun` to `dist/cli.mjs`; the `bin` field in `package.json` points at that file.
111
+ - **`exports`:** `./core` and `./agent-core` only — there is deliberately no `.` entry. It used to point at `dist/cli.mjs`, and `src/cli-runtime.ts` calls `outputHelp()` and `parseAsync(process.argv)` at module top level, so importing the package ran the CLI against the host's argv and never returned. `bin` does not resolve through `exports`, so the executable is unaffected. `tests/integration/runtime-contract.test.ts` pins both the absence of `.` and the side effect that justifies it.
111
112
  - **`postinstall`:** `scripts/postinstall-check-bun.mjs` reports a missing Bun — npm ignores `engines.bun`, so this is the only signal an npm-only machine gets before `dbcli` refuses to start. The command is `bun … || node …` so it runs under whichever runtime is present: Bun-only machines have no `node` to invoke it with, and a hard `node` dependency there would break the primary install path. It must **not** end in `|| exit 0`; v1.55.0 shipped with that mask and npm, which hides lifecycle output unless a script fails, therefore showed nothing at all. On a global install (`npm_config_global === 'true'`) a missing Bun exits 1, so the reason is printed and npm rolls the `bin` back; a dependency install only warns, because that is the `./agent-core` consumer. Both outcomes are pinned in `tests/integration/runtime-contract.test.ts`.
112
113
  - **Live DB tests:** `tests/integration/live-db.test.ts` uses project `.dbcli` by default or `LIVE_DB_CONFIG_PATH` when you point at another config directory. Set `SKIP_INTEGRATION_TESTS=true` to skip all integration tests.
113
114
 
package/assets/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
@@ -246,6 +246,17 @@ dbcli init --rename staging:stg # rename
246
246
  dbcli init --remove stg # remove
247
247
  ```
248
248
 
249
+ 只輪替單一連線的密碼,其餘設定不動:
250
+
251
+ ```bash
252
+ dbcli password prod # 遮蔽輸入
253
+ rotate-secret | dbcli password prod --stdin # 供排程輪替腳本使用
254
+ ```
255
+
256
+ 新密碼會寫進 config 實際參照的 env 變數(明文密碼會在第一次使用時轉成
257
+ `{ "$env": ... }`;連線沒宣告 `envFile` 時會一併補記錄,讀取端才載得到),
258
+ 存檔前先用它連一次驗證(`--skip-test` 可跳過),env 檔在 POSIX 上以 0600 權限寫入。
259
+
249
260
  若要讓多個專案共用連線,請使用明確的 root-level `--global` scope。它會把 v2 registry 儲存在 `~/.config/dbcli/config.json`,不會建立或修改專案 binding:
250
261
 
251
262
  ```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