From b3a4580fbaa75dd53230be97a5baeb48a58dddca Mon Sep 17 00:00:00 2001 From: Cristhian Melo Date: Fri, 19 Jun 2026 15:18:54 -0500 Subject: [PATCH 1/4] ci(release): enable automatic publishing to crates.io via release-plz --- .github/workflows/release-plz.yml | 1 + CONTRIBUTING.md | 2 +- release-plz.toml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index a0d9f43..ba60d4a 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -30,6 +30,7 @@ jobs: command: release env: GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} release-pr: name: release pr diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 638a0e8..5fbf4c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ Releases use release-plz, cargo-dist, and Conventional Commits. - Normal development pushes do not create releases. - Merging a release pull request creates the `vX.Y.Z` tag. - cargo-dist builds release assets and creates or updates the GitHub Release. -- Publishing to crates.io is disabled for now. +- release-plz publishes to crates.io automatically after creating the tag. Review the release pull request before merging. Edit changelog entries if the automated summary is unclear. diff --git a/release-plz.toml b/release-plz.toml index 17db0cb..b3aa4d6 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -1,13 +1,13 @@ [workspace] changelog_update = true changelog_config = "cliff.toml" -git_only = true +git_only = false git_release_enable = false git_release_name = "v{{ version }}" git_tag_enable = true git_tag_name = "v{{ version }}" pr_labels = ["release"] -publish = false +publish = true release_always = false # The package was renamed from `rpass` to `rpass-cli` after the v0.1.3 tag. # cargo-semver-checks cannot compare the renamed package against older tags. From e65f9e1eba45c07d2f31f4ac78cd42acfcf02a75 Mon Sep 17 00:00:00 2001 From: Cristhian Melo Date: Fri, 19 Jun 2026 15:21:45 -0500 Subject: [PATCH 2/4] chore: remove stale TODO.md --- TODO.md | 161 -------------------------------------------------------- 1 file changed, 161 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 1f00ae6..0000000 --- a/TODO.md +++ /dev/null @@ -1,161 +0,0 @@ -# rpass TODO - -`rpass` is a native, cross-platform backend for password-store compatible -repositories. The goal is compatibility with the password-store format first, -not full command-for-command parity with the original `pass` CLI. - -## Product Principles - -- Preserve the password-store file format: - - entries are stored as `.gpg`; - - recipients are discovered from `.gpg-id`; - - decrypted content keeps the password on the first line; - - additional metadata stays as plain text lines such as `username: alice`, - `url: https://example.com`, and `otpauth://...`. -- Prefer stable JSON output for integrations such as Raycast and Vicinae. -- Use external GnuPG in the first implementation phase. -- Keep future OpenPGP-native support optional and behind a clear abstraction. -- Treat compatibility with existing stores as a product requirement. - -## Engineering Principles - -- Use conventional commits. -- Keep changes small, testable, and focused. -- Prefer standard CLI conventions for command names, flags, aliases, exit codes, - streams, and help output. -- Prefer intention-revealing functions over inline policy conditions. -- Split multi-step procedures into small functions with one responsibility. -- Treat code smells as blockers during implementation and review. -- Prefer Rust idioms over imported architecture ceremony. -- Introduce design patterns only when they solve present coupling, variation, - orchestration, or readability problems. -- Make intentional patterns visible in names when doing so improves clarity. -- Avoid speculative abstractions, duplicate code, dead code, and comments that - restate the implementation. -- Use tests as design pressure, especially for parsing, path handling, - compatibility behavior, and error cases. - -## Phase 1: Read-Only Backend - -Goal: provide reliable read-only access to existing password-store compatible -repositories with structured JSON output. - -### CLI Surface - -- [x] Keep commands and flags aligned with common CLI standards unless there is - a clear password-store compatibility reason to differ. -- [x] `rpass list` -- [x] `rpass list --json` -- [x] `rpass show ` -- [x] `rpass show --json` -- [x] `rpass otp ` -- [x] `rpass otp --json` -- [x] `rpass search ` -- [x] `rpass search --json` -- [x] `rpass doctor` -- [x] `rpass doctor --json` - -### Store Discovery - -- [x] Resolve the store directory from `PASSWORD_STORE_DIR`. -- [x] Fall back to the platform-appropriate default store path. -- [x] Validate that the selected store exists. -- [x] Return structured errors for missing, unreadable, or invalid stores. - -### Entry Listing - -- [x] Recursively discover `.gpg` files. -- [x] Convert file paths into password-store entry names. -- [x] Ignore non-entry files such as `.gpg-id` and Git metadata. -- [x] Sort entries deterministically. -- [x] Add fixtures for nested stores and edge-case entry names. - -### GPG Decryption - -- [x] Add a narrow GPG adapter for invoking external `gpg`. -- [x] Keep command construction isolated and testable. -- [x] Surface decryption failures as structured application errors. -- [x] Avoid leaking decrypted secrets into logs or debug output. - -### Entry Parsing - -- [x] Parse the first line as the password. -- [x] Parse common metadata fields such as `username`, `login`, `email`, and - `url`. -- [x] Detect `otpauth://` lines. -- [x] Preserve unrecognized lines for compatibility. -- [x] Add table-driven tests for common password-store entry shapes. - -### OTP - -- [x] Parse TOTP data from `otpauth://` URIs. -- [x] Generate current TOTP codes. -- [x] Return remaining validity seconds in JSON output. -- [x] Add deterministic tests using fixed timestamps. - -### JSON Contract - -- [x] Define stable response structs. -- [x] Define stable error structs. -- [x] Keep JSON field names explicit and integration-friendly. -- [x] Add snapshot-style tests for JSON output. - -## Phase 2: Compatible Writes - -Goal: add write operations while preserving interoperability with `pass`, iOS, -Android Password Store, and existing Git-based stores. - -- [x] `rpass insert ` -- [x] `rpass edit ` -- [x] `rpass generate ` -- [x] `rpass rm ` -- [x] `rpass mv ` -- [x] Resolve recipients from the nearest `.gpg-id`. -- [x] Encrypt with external `gpg`. -- [x] Preserve directory-level recipient behavior. -- [x] Add compatibility fixtures for encrypted writes. - -## Phase 3: Git Integration - -Goal: provide explicit, predictable Git commands for password-store workflows. - -- [x] `rpass git status` -- [x] `rpass git pull` -- [x] `rpass git push` -- [x] `rpass git log` -- [x] Auto-commit write commands when the store is a Git repository. -- [x] Keep Git integration optional. -- [x] Return structured errors for missing Git repositories. - -## Phase 4: Initialization And Store Management - -Goal: support creation and maintenance of password-store compatible stores. - -- [x] `rpass init ` -- [x] `rpass recipients` -- [x] `rpass recipients add ` -- [x] `rpass recipients remove ` -- [ ] Support multiple stores only after the single-store model is stable. - -## Phase 5: Compatibility Hardening - -Goal: earn stronger compatibility claims through automated evidence. - -- [ ] Build a fixture suite from real-world password-store layouts. -- [ ] Test behavior against the original `pass` where practical. -- [x] Document known differences from `pass`. -- [x] Add cross-platform CI for Windows, macOS, and Linux. -- [ ] Decide which `pass` CLI flags are intentionally unsupported. -- [ ] Add Debian package release asset. -- [ ] Add AppImage release asset. -- [ ] Add AUR packaging for Arch users. - -## Near-Term Technical Notes - -- Start with a small application core and a thin CLI layer. -- Keep filesystem, GPG, and clock access behind narrow boundaries for testing. -- Prefer explicit domain types for entry names, store paths, parsed entries, - OTP secrets, and JSON responses. -- Avoid primitive obsession around paths and entry names once behavior grows. -- Do not add a facade, strategy, command, or observer unless the code has a - concrete variation point that benefits from it. From 95adb8bf4d10cb4f37f89bc317c3a9295c279bc5 Mon Sep 17 00:00:00 2001 From: Cristhian Melo Date: Fri, 19 Jun 2026 15:26:22 -0500 Subject: [PATCH 3/4] docs: rewrite README for production --- Cargo.toml | 2 +- README.md | 212 +++++++++++++++++------------------------------------ 2 files changed, 69 insertions(+), 145 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 735cf06..81e9937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "rpass-cli" version = "0.1.13" edition = "2024" authors = ["rxtsel"] -description = "Native password-store compatible backend" +description = "Cross-platform pass-compatible CLI for GPG-encrypted secrets" license = "MIT" repository = "https://github.com/rxtsel/rpass-cli" exclude = [ diff --git a/README.md b/README.md index 1374445..e963e35 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,38 @@ # rpass -`rpass` is a native, cross-platform backend for password-store compatible -repositories. +[![Crates.io Version](https://img.shields.io/crates/v/rpass-cli)](https://crates.io/crates/rpass-cli) +[![Crates.io Total Downloads](https://img.shields.io/crates/d/rpass-cli)](https://crates.io/crates/rpass-cli) +[![CI](https://img.shields.io/github/actions/workflow/status/rxtsel/rpass-cli/rust.yml?branch=main)](https://github.com/rxtsel/rpass-cli/actions) +[![License](https://img.shields.io/github/license/rxtsel/rpass-cli)](LICENSE) -It keeps the existing `pass`/password-store format: +**rpass** is a cross-platform [`pass`](https://www.passwordstore.org/)-compatible CLI for managing GPG-encrypted secrets. It reads and writes password-store repositories on Linux, macOS, and Windows, and provides stable JSON output for launchers, plugins, and scripts. ```text -~/.password-store/ - example/login.gpg - .gpg-id +rpass show example/login +rpass list --json +rpass otp example/login --json +rpass generate example/login +rpass git status ``` -Decrypted entries keep the usual first-line password format: +## Features -```text -password -username: alice -url: https://example.com -otpauth://totp/... -``` - -## Why - -- Use password-store repositories from Windows, macOS, and Linux. -- Provide stable JSON output for launchers such as Raycast and Vicinae. -- Avoid Bash-specific behavior. -- Keep compatibility with existing `.gpg` entries and `.gpg-id` files. +- **Works everywhere** — Linux, macOS, Windows (no Bash dependency) +- **password-store compatible** — reads and writes existing `.gpg` entries and `.gpg-id` files +- **JSON output** — structured responses for Raycast, Vicinae, and custom integrations +- **TOTP support** — generate one-time codes from `otpauth://` lines +- **Git integration** — explicit `rpass git ` commands; write commands auto-commit +- **Shell completions** — Bash, Zsh, Fish, PowerShell +- **Non-interactive** — `--passphrase-stdin` for headless environments +- **External GPG** — no embedded crypto; uses your existing GnuPG installation ## Installation -Prebuilt binaries and installers are available on the GitHub Releases page. +### Prebuilt binaries -Install from crates.io with: +Download from the [GitHub Releases](https://github.com/rxtsel/rpass-cli/releases) page. Archives, MSI (Windows), shell installers, and checksums are available for all platforms. + +### crates.io ```bash cargo install rpass-cli @@ -41,156 +42,79 @@ The crates.io package is `rpass-cli`; the installed binary is `rpass`. ## Requirements -### Windows - -- Gpg4win or GnuPG 2.x. -- `rpass` detects common GnuPG install paths automatically. -- You can also set `PASSWORD_STORE_GPG` to a specific `gpg.exe`. -- Git is optional and only required for `rpass git ...` workflows. - -### macOS +- **GnuPG 2.x** (`gpg`) — install via Gpg4win (Windows), Homebrew (macOS), or your system package manager (Linux) +- **Git** — optional, only needed for `rpass git ...` -- GnuPG 2.x from a package manager or installer. -- `gpg` should be available in `PATH`. -- Git is optional and only required for `rpass git ...` workflows. +## Quick Start -### Linux - -- GnuPG 2.x from your distribution packages. -- `gpg` should be available in `PATH`. -- Git is optional and only required for `rpass git ...` workflows. +```bash +rpass init alice@example.com # initialize a store +rpass list # list entries +rpass show example/login # show an entry +rpass generate example/login # generate a 14-character password +rpass insert example/login # insert a password +rpass edit example/login # edit an entry +rpass rm example/login # remove an entry +rpass mv example/login archive/login # move/rename an entry +rpass otp example/login # generate a TOTP code +rpass git status # run git inside the store +rpass doctor # check your local setup +``` ## Store Directory -`rpass` resolves the store directory in this order: +`rpass` resolves the store in this order: 1. `--store-dir ` 2. `PASSWORD_STORE_DIR` 3. `~/.password-store` -## Status +## JSON Output -`rpass` can initialize, list, search, show, generate, insert, edit, remove, -move, and run Git commands for password-store entries using external GnuPG. It -also supports TOTP generation from `otpauth://` lines. +Most read commands accept `--json`. On success, stdout contains the JSON value and stderr is empty. On error, the exit code is non-zero and stderr contains: -Commands such as clipboard support are intentionally not implemented yet. - -## Commands - -```bash -rpass -h # show help -rpass list # list entries -rpass search example # search entries -rpass show example/login # show an entry explicitly -rpass init alice@example.com # initialize .gpg-id recipients -rpass recipients # list .gpg-id recipients -rpass recipients add bob@example.com # add a .gpg-id recipient -rpass recipients remove bob@example.com # remove a .gpg-id recipient -rpass generate example/login # generate and save a 14-character password -rpass insert example/login # insert a password interactively -rpass edit example/login # edit or create an entry -rpass rm example/login # remove an entry -rpass mv example/login archive/login # move or rename an entry -rpass git status # run git inside the store -rpass git init # initialize store Git history -rpass otp example/login # generate an OTP code -rpass doctor # check local setup +```json +{ + "error": { + "code": "gpg_decrypt_failed", + "message": "gpg failed to decrypt entry: ..." + } +} ``` -`init` creates the store if needed and writes `.gpg-id` recipients. Use -`--path ` or `-p ` for directory-level recipients. -`recipients` lists or updates an existing `.gpg-id` without retyping the full -recipient set. - -`generate` writes to the store by default. Use `--dry-run` to print a generated -password or passphrase without opening the store, requiring `.gpg-id`, or calling -GPG. Use `--length ` with `--dry-run` when no entry name is provided. - -`insert` prompts for a password and confirmation when run in an interactive -terminal. Use `--echo` to show input, `--multiline` to read the full entry until -EOF, and `--force` to overwrite an existing entry. In multiline mode, the first -line is the password and additional lines are metadata. - -`rpass git ` passes arguments to Git using the password store as the -repository. `rpass git init` also stages the current store and creates the same -initial commit used by `pass`. When the store is a Git repository, write -commands automatically create matching commits. Use `rpass git --json ` -for structured stdout, stderr, and exit code output. - -Most read commands support `--json` for integrations. Commands that decrypt -entries also support `--passphrase-stdin` for non-interactive integrations: +Non-interactive workflows: ```bash printf 'gpg-passphrase\n' | rpass show example/login --json --passphrase-stdin printf 'gpg-passphrase\n' | rpass otp example/login --json --passphrase-stdin ``` -Run `rpass --help` for command-specific flags. - -## Shell Completion +## Shell Completions ```bash rpass completions bash >> ~/.bashrc -``` - -For Zsh: - -```zsh rpass completions zsh > "${fpath[1]}/_rpass" -compinit -``` - -For PowerShell (run as administrator): - -```powershell -rpass completions powershell >> $PROFILE -``` - -For Fish, the file must exist in the completions directory (Fish auto-loads it): - -```fish rpass completions fish > ~/.config/fish/completions/rpass.fish +rpass completions powershell >> $PROFILE ``` -After installation, `rpass show ` completes entry names, `rpass show Personal/` filters by prefix, and `rpass list ` shows subcommands. - -## JSON Contract - -Commands that accept `--json` follow this contract: - -- exit code `0`: stdout contains one complete JSON value and stderr is empty; -- non-zero exit code: stderr contains one JSON error object and stdout is empty. - -Error responses use this shape: - -```json -{ - "error": { - "code": "gpg_decrypt_failed", - "message": "gpg failed to decrypt entry: ..." - } -} -``` - -## Password-store Compatibility +## password-store Compatibility -Supported behavior: +**Supported:** +- Entries addressed without the `.gpg` suffix +- First decrypted line is the password +- `name: value` metadata lines preserved in JSON +- `otpauth://` lines for TOTP +- Unknown lines preserved as `extra_lines` +- Directory-level recipients with `.gpg-id` -- entries are addressed without the `.gpg` suffix; -- decrypted first line is the password; -- `name: value` metadata lines are preserved in JSON fields; -- `otpauth://` lines are used for TOTP generation; -- unknown lines are preserved as `extra_lines`; -- store directory is resolved from `--store-dir`, `PASSWORD_STORE_DIR`, then - `~/.password-store`. +**Known differences from `pass`:** +- `generate`, `insert`, `edit`, `rm`, and `mv` for writes +- Git is explicit (`rpass git `) rather than automatic +- Changing recipients with `init` does not re-encrypt existing entries +- Clipboard and QR codes are not implemented +- Unsupported `pass` flags are rejected instead of ignored -Known differences from `pass`: +## License -- write support is limited to `generate`, `insert`, `edit`, `rm`, and `mv`; -- Git integration is explicit through `rpass git `; -- changing recipients with `init` does not re-encrypt existing entries yet; -- clipboard and QR code are not implemented; -- unsupported `pass` flags are rejected instead of ignored; -- JSON output is an `rpass` integration contract, not part of the original - `pass` CLI. +MIT From 0754f823f31a66e715e47ffa50cce79a02199ad4 Mon Sep 17 00:00:00 2001 From: Cristhian Melo Date: Fri, 19 Jun 2026 16:01:01 -0500 Subject: [PATCH 4/4] fix(doctor): use true instead of shell script for CI gpg mock --- src/password_store/doctor.rs | 41 ++++++++++++------------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/src/password_store/doctor.rs b/src/password_store/doctor.rs index 52d118d..ec16bac 100644 --- a/src/password_store/doctor.rs +++ b/src/password_store/doctor.rs @@ -95,6 +95,7 @@ fn gpg_version_check(gpg: &GpgCommand) -> DoctorCheck { #[cfg(test)] mod tests { use std::fs; + use std::path::PathBuf; use tempfile::TempDir; @@ -105,19 +106,19 @@ mod tests { fn reports_ready_environment() { let temp_dir = TempDir::new().expect("temp dir"); fs::write(temp_dir.path().join(".gpg-id"), "KEY").expect("gpg id"); - let gpg = fake_gpg_script(temp_dir.path(), "gpg (GnuPG) test\n"); + let gpg = fake_gpg(); let store_directory = StoreDirectory::from_path(temp_dir.path()); let report = DoctorReport::run(&store_directory, &GpgCommand::new(gpg)); - assert!(report.ok); + assert!(report.ok, "doctor report: {report:#?}"); assert!(report.checks.iter().all(|check| check.ok)); } #[test] fn reports_missing_gpg_id() { let temp_dir = TempDir::new().expect("temp dir"); - let gpg = fake_gpg_script(temp_dir.path(), "gpg (GnuPG) test\n"); + let gpg = fake_gpg(); let store_directory = StoreDirectory::from_path(temp_dir.path()); let report = DoctorReport::run(&store_directory, &GpgCommand::new(gpg)); @@ -131,29 +132,15 @@ mod tests { ); } - #[cfg(windows)] - fn fake_gpg_script(directory: &std::path::Path, output: &str) -> std::path::PathBuf { - let script = directory.join("gpg-version.cmd"); - let output_file = directory.join("gpg-version-output.txt"); - - fs::write(&output_file, output).expect("output file"); - fs::write( - &script, - format!("@echo off\r\ntype \"{}\"\r\n", output_file.display()), - ) - .expect("script"); - script - } - - #[cfg(not(windows))] - fn fake_gpg_script(directory: &std::path::Path, output: &str) -> std::path::PathBuf { - use std::os::unix::fs::PermissionsExt; - - let script = directory.join("gpg-version"); - fs::write(&script, format!("#!/bin/sh\nprintf '{}'\n", output)).expect("script"); - let mut permissions = fs::metadata(&script).expect("metadata").permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&script, permissions).expect("permissions"); - script + fn fake_gpg() -> std::path::PathBuf { + // Use a program where --version exits 0 everywhere: + // - GNU coreutils true: prints version to stdout, exits 0 + // - macOS/BSD true: exits 0 silently + // - cargo: available during tests on all platforms + if cfg!(windows) { + PathBuf::from("cargo") + } else { + PathBuf::from("true") + } } }