use std::path::Path;
use std::process::ExitCode;
use crate::cli::TrustArgs;
use crate::config::find_project_config;
use crate::error::RippyError;
use crate::trust::{TrustDb, TrustStatus};
pub fn run(args: &TrustArgs) -> Result<ExitCode, RippyError> {
if args.list {
return list_trusted();
}
let cwd = std::env::current_dir()
.map_err(|e| RippyError::Trust(format!("could not determine working directory: {e}")))?;
let config_path = find_project_config(&cwd).ok_or_else(|| {
RippyError::Trust("no project config found (.rippy.toml, .rippy, or .dippy)".to_string())
})?;
if args.revoke {
return revoke_trust(&config_path);
}
if args.status {
return show_status(&config_path);
}
trust_config(&config_path, args.yes)
}
fn trust_config(config_path: &Path, skip_confirm: bool) -> Result<ExitCode, RippyError> {
let content = std::fs::read_to_string(config_path)
.map_err(|e| RippyError::Trust(format!("could not read {}: {e}", config_path.display())))?;
if !skip_confirm {
print_config_summary(config_path, &content);
eprintln!();
eprint!("Trust this config? [y/N] ");
let mut answer = String::new();
std::io::stdin()
.read_line(&mut answer)
.map_err(|e| RippyError::Trust(format!("could not read confirmation: {e}")))?;
if !answer.trim().eq_ignore_ascii_case("y") {
eprintln!("[rippy] trust cancelled");
return Ok(ExitCode::from(1));
}
}
let mut db = TrustDb::load();
db.trust(config_path, &content);
db.save()?;
eprintln!("[rippy] trusted: {}", config_path.display());
Ok(ExitCode::SUCCESS)
}
fn print_config_summary(path: &Path, content: &str) {
eprintln!("Project config: {}", path.display());
if let Some(repo_id) = crate::trust::detect_repo_id(path) {
eprintln!("Repository: {repo_id}");
eprintln!(" (future config changes in this repo will be auto-trusted)");
}
eprintln!("---");
for line in content.lines() {
eprintln!(" {line}");
}
eprintln!("---");
let stats = analyze_config_safety(content);
eprintln!(
"{} line(s), {} rule(s): {} allow, {} ask, {} deny",
content.lines().count(),
stats.allow + stats.ask + stats.deny,
stats.allow,
stats.ask,
stats.deny,
);
if stats.allow > 0 || stats.sets_default_allow {
eprintln!();
eprintln!("WARNING: this config WEAKENS protections:");
if stats.allow > 0 {
eprintln!(
" - {} allow rule(s) will auto-approve commands",
stats.allow
);
}
if stats.sets_default_allow {
eprintln!(" - sets default action to allow (all unknown commands auto-approved)");
}
}
}
struct ConfigSafety {
allow: usize,
ask: usize,
deny: usize,
sets_default_allow: bool,
}
fn analyze_config_safety(content: &str) -> ConfigSafety {
let mut stats = ConfigSafety {
allow: 0,
ask: 0,
deny: 0,
sets_default_allow: false,
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("allow ") {
stats.allow += 1;
} else if trimmed.starts_with("ask ") {
stats.ask += 1;
} else if trimmed.starts_with("deny") {
stats.deny += 1;
} else if trimmed.starts_with("set default allow") {
stats.sets_default_allow = true;
}
if trimmed.contains("action") && trimmed.contains("allow") && !trimmed.contains("deny") {
stats.allow += 1;
} else if trimmed.contains("action") && trimmed.contains("ask") {
stats.ask += 1;
} else if trimmed.contains("action") && trimmed.contains("deny") {
stats.deny += 1;
}
if trimmed.contains("default") && trimmed.contains("allow") && !trimmed.starts_with('#') {
stats.sets_default_allow = true;
}
}
stats
}
fn revoke_trust(config_path: &Path) -> Result<ExitCode, RippyError> {
let mut db = TrustDb::load();
if db.revoke(config_path) {
db.save()?;
eprintln!("[rippy] trust revoked: {}", config_path.display());
Ok(ExitCode::SUCCESS)
} else {
eprintln!(
"[rippy] no trust entry found for: {}",
config_path.display()
);
Ok(ExitCode::from(1))
}
}
fn show_status(config_path: &Path) -> Result<ExitCode, RippyError> {
let content = std::fs::read_to_string(config_path)
.map_err(|e| RippyError::Trust(format!("could not read {}: {e}", config_path.display())))?;
let db = TrustDb::load();
let status = db.check(config_path, &content);
match status {
TrustStatus::Trusted => {
eprintln!("[rippy] trusted: {}", config_path.display());
Ok(ExitCode::SUCCESS)
}
TrustStatus::Untrusted => {
eprintln!(
"[rippy] untrusted: {} — run `rippy trust` to approve",
config_path.display()
);
Ok(ExitCode::from(2))
}
TrustStatus::Modified { .. } => {
eprintln!(
"[rippy] modified since last trust: {} — run `rippy trust` to re-approve",
config_path.display()
);
Ok(ExitCode::from(2))
}
}
}
#[allow(clippy::unnecessary_wraps)]
fn list_trusted() -> Result<ExitCode, RippyError> {
let db = TrustDb::load();
if db.is_empty() {
eprintln!("[rippy] no trusted project configs");
} else {
for (path, entry) in db.list() {
eprintln!(
"{path} (trusted {}, hash {})",
entry.trusted_at, entry.hash
);
}
}
Ok(ExitCode::SUCCESS)
}