From 6890926e310c65ee2f585c844d35010fe94063cf Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 19 Apr 2026 21:37:28 -0400 Subject: [PATCH] feat(cli): add helpers module (vault_dir/L8, git_command/H4, iso8601/M11) Bumps rpassword to 7.x (H7) and adds zeroize/chrono/assert_cmd dev-deps. Co-Authored-By: Claude Sonnet 4.6 --- crates/relicario-cli/Cargo.toml | 8 ++- crates/relicario-cli/src/helpers.rs | 101 ++++++++++++++++++++++++++++ crates/relicario-cli/src/main.rs | 2 + 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 crates/relicario-cli/src/helpers.rs diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml index 7dca4e1..bd87584 100644 --- a/crates/relicario-cli/Cargo.toml +++ b/crates/relicario-cli/Cargo.toml @@ -12,8 +12,9 @@ path = "src/main.rs" relicario-core = { path = "../relicario-core" } clap = { version = "4", features = ["derive"] } anyhow = "1" -rpassword = "5" +rpassword = "7" arboard = "3" +chrono = { version = "0.4", default-features = false, features = ["clock"] } dirs = "5" hex = "0.4" ed25519-dalek = { version = "2", features = ["rand_core"] } @@ -21,3 +22,8 @@ rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" zeroize = "1" + +[dev-dependencies] +assert_cmd = "2" +predicates = "3" +tempfile = "3" diff --git a/crates/relicario-cli/src/helpers.rs b/crates/relicario-cli/src/helpers.rs new file mode 100644 index 0000000..28d89f5 --- /dev/null +++ b/crates/relicario-cli/src/helpers.rs @@ -0,0 +1,101 @@ +//! CLI-side helpers: vault dir detection, hardened git shell-out, ISO-8601 +//! timestamp formatting. Kept in their own module so every command handler +//! stays terse. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use chrono::DateTime; + +/// Walk up from `start` looking for a directory containing `.relicario/`. +/// Returns the vault root (the directory that contains `.relicario/`). +/// Audit L8: refuses to operate outside an initialized vault. +pub fn find_vault_dir_from(start: &Path) -> Result { + let mut cur = start.to_path_buf(); + loop { + if cur.join(".relicario").is_dir() { + return Ok(cur); + } + if !cur.pop() { + bail!( + "no .relicario/ directory found in {} or any parent — \ + run `relicario init` first", + start.display() + ); + } + } +} + +/// Convenience wrapper that starts the search from `std::env::current_dir()`. +pub fn vault_dir() -> Result { + let cwd = std::env::current_dir().context("failed to get current directory")?; + find_vault_dir_from(&cwd) +} + +/// Path to the `.relicario/` configuration directory within the vault. +pub fn relicario_dir() -> Result { + Ok(vault_dir()?.join(".relicario")) +} + +/// Build a hardened `git` command — no hooks, no GPG signing, no editor. +/// Audit H4: prevents vault mutations from running hostile hooks, blocking on +/// GPG passphrase prompts (which would hold the master key alive), or entering +/// $EDITOR during rebase conflict markers. +pub fn git_command(repo: &Path, args: &[&str]) -> Command { + let mut cmd = Command::new("git"); + cmd.current_dir(repo); + cmd.args([ + "-c", "core.hooksPath=/dev/null", + "-c", "commit.gpgsign=false", + "-c", "core.editor=true", + ]); + cmd.args(args); + cmd +} + +/// Format a Unix-seconds timestamp as an ISO-8601 UTC string. +/// Audit M11: replaces the old `now_iso8601` helper that actually returned +/// a numeric string. +pub fn iso8601(unix_seconds: i64) -> String { + DateTime::from_timestamp(unix_seconds, 0) + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| format!("invalid-timestamp:{unix_seconds}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn vault_dir_finds_marker_in_cwd() { + let tmp = TempDir::new().unwrap(); + std::fs::create_dir(tmp.path().join(".relicario")).unwrap(); + let found = find_vault_dir_from(tmp.path()).unwrap(); + assert_eq!(found, tmp.path()); + } + + #[test] + fn vault_dir_finds_marker_in_parent() { + let tmp = TempDir::new().unwrap(); + std::fs::create_dir(tmp.path().join(".relicario")).unwrap(); + let subdir = tmp.path().join("sub/nested"); + std::fs::create_dir_all(&subdir).unwrap(); + let found = find_vault_dir_from(&subdir).unwrap(); + assert_eq!(found, tmp.path()); + } + + #[test] + fn vault_dir_errors_when_missing() { + let tmp = TempDir::new().unwrap(); + let err = find_vault_dir_from(tmp.path()).unwrap_err(); + assert!(err.to_string().contains(".relicario")); + } + + #[test] + fn iso8601_formats_fixed_timestamp() { + // 2026-04-19T00:00:00Z = 1776556800 + assert_eq!(iso8601(1_776_556_800), "2026-04-19T00:00:00Z"); + } +} diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index c41ec63..a613c66 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -37,6 +37,8 @@ //! binary small and the build simple. Every mutation (add, edit, rm, device add/revoke) //! creates a git commit, preserving an audit log of all vault changes. +mod helpers; + use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use relicario_core::{