//! 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"); } }