feat(cli): --*-stdin secret flags for personal add (non-interactive secrets)

This commit is contained in:
adlee-was-taken
2026-06-20 17:51:15 -04:00
parent 751e4e9bb1
commit 6eb1275710
4 changed files with 88 additions and 36 deletions

View File

@@ -37,15 +37,28 @@ under `src/commands/`. Each source file has one job.
`cmd_history`), `edit`, `trash` (rm / restore / purge / trash empty),
`backup` (export / restore), `import` (lastpass), `attach` (attach /
attachments / extract / detach), `generate`, `settings`, `sync`, `status`,
`rate`, `device`, `recovery_qr`. `add` and `edit` each fan out internally to
per-`ItemCore` helpers (`build_<type>_item`, `edit_<type>`) so each
builder/editor reads top-to-bottom and can be tested through the same
integration paths.
`rate`, `device`, `recovery_qr`. `add` and `edit` resolve their non-secret
fields then delegate to the shared `item_build` module's per-`ItemCore`
`build_*` / `edit_*` helpers (see the next bullet), so each builder/editor
reads top-to-bottom and can be tested through the same integration paths.
- **`src/commands/item_build.rs`** — shared per-type item construction and
interactive editing used by BOTH personal (`add.rs`, `edit.rs`) and org
(`org.rs`) handlers, so the two surfaces cannot drift. Contains: secret
resolution (`resolve_secret_line` — reads one line from stdin or falls back
to an interactive masked prompt; `resolve_secret_multiline` — reads stdin to
EOF, printing an optional hint in the interactive case); type parsers
(`parse_card_kind`, `parse_totp_algorithm`); the seven `build_*` builders
(`build_login`, `build_secure_note`, `build_identity`, `build_card`,
`build_key`, `build_document`, `build_totp`); per-type `edit_*` helpers
(`edit_login`, `edit_secure_note`, `edit_card`, `edit_key`, `edit_totp`,
`edit_identity`, `edit_document_message`); and `push_history`.
- **`src/prompt.rs`** — interactive prompt primitives shared across commands:
`prompt`, `prompt_optional`, `prompt_keep`, `prompt_keep_opt`,
`prompt_yesno`, `prompt_secret`. `prompt_secret` honours
`RELICARIO_TEST_ITEM_SECRET` before falling back to `rpassword`.
`prompt_keep`, `prompt_keep_opt`, `prompt_yesno`, `prompt_secret`, and the
flag-or-prompt pair `prompt_or_flag` / `prompt_or_flag_optional`.
`prompt_secret` honours `RELICARIO_TEST_ITEM_SECRET` before falling back to
`rpassword`.
- **`src/parse.rs`** — pure parsers for CLI-typed inputs (e.g. MonthYear
expiries, TOTP `otpauth://` URIs, comma-separated tag lists). No I/O.
@@ -167,7 +180,7 @@ in code; cite the line if you change it.
works without any setup.
- **Item IDs are minted by core.** The CLI never constructs an `ItemId`
directly; `Item::new` (called inside every `build_*_item`) does it via
directly; `Item::new` (called inside every `item_build::build_*`) does it via
`relicario-core::ids::new_item_id`. `ItemId`s are 8-char hex.
- **Manifest is always saved last.** Within a single command, the order is:
@@ -237,15 +250,23 @@ in code; cite the line if you change it.
### Item add (`cmd_add`, `main.rs:419-456`)
1. Unlock the vault and load the manifest.
2. Match on the `AddKind` variant and dispatch to the matching
`build_<type>_item` helper (`main.rs:423-438`). Seven variants → seven
builders; only `build_document_item` takes `&UnlockedVault` because it
needs `attachment_caps` and writes the encrypted blob alongside the item.
3. The builder returns a fully-populated `Item` (with title, group, tags,
2. Match on the `AddKind` variant: resolve `title` and non-secret fields
(username, URL, holder, expiry, etc.) via `prompt_or_flag` /
`prompt_or_flag_optional`, then delegate to the matching `build_*` builder
in `commands/item_build.rs`. Seven variants → seven builders; only
`build_document` takes `&UnlockedVault` because it needs `attachment_caps`
and writes the encrypted blob alongside the item.
3. Single-line secrets (Login password, Card number/CVV/PIN, TOTP secret)
accept a `--*-stdin` flag that reads one line from stdin instead of
prompting; multiline secrets (SecureNote body, Key material) always read
stdin to EOF — `--body-stdin` / `--material-stdin` suppress the interactive
Ctrl-D hint. Secret-resolution rule: `commands/item_build.rs`
`resolve_secret_line` / `resolve_secret_multiline`.
4. The builder returns a fully-populated `Item` (with title, group, tags,
favorite-flag, primary attachment if any).
4. Common wrap-up: `vault.save_item(&item)`, `manifest.upsert(&item)`,
5. Common wrap-up: `vault.save_item(&item)`, `manifest.upsert(&item)`,
`vault.save_manifest(&manifest)`.
5. Build the path list — `items/<id>.enc`, `manifest.enc`, plus one
6. Build the path list — `items/<id>.enc`, `manifest.enc`, plus one
`attachments/<id>/<aid>.enc` per attachment — and call `commit_paths`
with message `add: <title> (<id>)` (`main.rs:444-452`).
@@ -578,11 +599,12 @@ applies to `relicario-core` unit tests, not these CLI integration tests.
instead. Non-primary attachments on a Document (e.g., a scanned
contract with an addendum) detach normally.
- **Per-type `build_*_item` / `edit_*` helpers exist by design after the
`3f0f5b1` refactor.** Before the refactor, `cmd_add` and `cmd_edit`
carried 217-line `match` arms. The split-out functions are easier to
read, easier to test individually (the existing integration tests still
drive them through the same paths), and easier to grow when a new
- **Per-type `build_*` / `edit_*` helpers exist by design** (extracted in the
`3f0f5b1` refactor, then centralized in `item_build.rs` for v0.8.1 so the
personal and org surfaces share one set). Before the extraction, `cmd_add`
and `cmd_edit` carried 217-line `match` arms. The split-out functions are
easier to read, easier to test individually (the existing integration tests
still drive them through the same paths), and easier to grow when a new
`ItemCore` variant lands. Keep this shape — don't fold them back.
- **Why the CLI shells out to `git`, not libgit2 / gitoxide.** Three