1use std::path::Path;
4use std::process::ExitCode;
5
6use crate::cli::TrustArgs;
7use crate::config::find_project_config;
8use crate::error::RippyError;
9use crate::trust::{TrustDb, TrustStatus};
10
11pub fn run(args: &TrustArgs) -> Result<ExitCode, RippyError> {
17 if args.list {
18 return list_trusted();
19 }
20
21 let cwd = std::env::current_dir()
22 .map_err(|e| RippyError::Trust(format!("could not determine working directory: {e}")))?;
23
24 let config_path = find_project_config(&cwd).ok_or_else(|| {
25 RippyError::Trust("no project config found (.rippy.toml, .rippy, or .dippy)".to_string())
26 })?;
27
28 if args.revoke {
29 return revoke_trust(&config_path);
30 }
31
32 if args.status {
33 return show_status(&config_path);
34 }
35
36 trust_config(&config_path, args.yes)
37}
38
39fn trust_config(config_path: &Path, skip_confirm: bool) -> Result<ExitCode, RippyError> {
41 let content = std::fs::read_to_string(config_path)
42 .map_err(|e| RippyError::Trust(format!("could not read {}: {e}", config_path.display())))?;
43
44 if !skip_confirm {
45 print_config_summary(config_path, &content);
46 eprintln!();
47 eprint!("Trust this config? [y/N] ");
48 let mut answer = String::new();
49 std::io::stdin()
50 .read_line(&mut answer)
51 .map_err(|e| RippyError::Trust(format!("could not read confirmation: {e}")))?;
52 if !answer.trim().eq_ignore_ascii_case("y") {
53 eprintln!("[rippy] trust cancelled");
54 return Ok(ExitCode::from(1));
55 }
56 }
57
58 let mut db = TrustDb::load();
59 db.trust(config_path, &content);
60 db.save()?;
61
62 eprintln!("[rippy] trusted: {}", config_path.display());
63 Ok(ExitCode::SUCCESS)
64}
65
66fn print_config_summary(path: &Path, content: &str) {
68 eprintln!("Project config: {}", path.display());
69 if let Some(repo_id) = crate::trust::detect_repo_id(path) {
70 eprintln!("Repository: {repo_id}");
71 eprintln!(" (future config changes in this repo will be auto-trusted)");
72 }
73 eprintln!("---");
74 for line in content.lines() {
75 eprintln!(" {line}");
76 }
77 eprintln!("---");
78
79 let stats = analyze_config_safety(content);
80 eprintln!(
81 "{} line(s), {} rule(s): {} allow, {} ask, {} deny",
82 content.lines().count(),
83 stats.allow + stats.ask + stats.deny,
84 stats.allow,
85 stats.ask,
86 stats.deny,
87 );
88
89 if stats.allow > 0 || stats.sets_default_allow {
90 eprintln!();
91 eprintln!("WARNING: this config WEAKENS protections:");
92 if stats.allow > 0 {
93 eprintln!(
94 " - {} allow rule(s) will auto-approve commands",
95 stats.allow
96 );
97 }
98 if stats.sets_default_allow {
99 eprintln!(" - sets default action to allow (all unknown commands auto-approved)");
100 }
101 }
102}
103
104struct ConfigSafety {
106 allow: usize,
107 ask: usize,
108 deny: usize,
109 sets_default_allow: bool,
110}
111
112fn analyze_config_safety(content: &str) -> ConfigSafety {
113 let mut stats = ConfigSafety {
114 allow: 0,
115 ask: 0,
116 deny: 0,
117 sets_default_allow: false,
118 };
119
120 for line in content.lines() {
121 let trimmed = line.trim();
122 if trimmed.starts_with("allow ") {
124 stats.allow += 1;
125 } else if trimmed.starts_with("ask ") {
126 stats.ask += 1;
127 } else if trimmed.starts_with("deny") {
128 stats.deny += 1;
129 } else if trimmed.starts_with("set default allow") {
130 stats.sets_default_allow = true;
131 }
132 if trimmed.contains("action") && trimmed.contains("allow") && !trimmed.contains("deny") {
134 stats.allow += 1;
135 } else if trimmed.contains("action") && trimmed.contains("ask") {
136 stats.ask += 1;
137 } else if trimmed.contains("action") && trimmed.contains("deny") {
138 stats.deny += 1;
139 }
140 if trimmed.contains("default") && trimmed.contains("allow") && !trimmed.starts_with('#') {
141 stats.sets_default_allow = true;
142 }
143 }
144 stats
145}
146
147fn revoke_trust(config_path: &Path) -> Result<ExitCode, RippyError> {
149 let mut db = TrustDb::load();
150 if db.revoke(config_path) {
151 db.save()?;
152 eprintln!("[rippy] trust revoked: {}", config_path.display());
153 Ok(ExitCode::SUCCESS)
154 } else {
155 eprintln!(
156 "[rippy] no trust entry found for: {}",
157 config_path.display()
158 );
159 Ok(ExitCode::from(1))
160 }
161}
162
163fn show_status(config_path: &Path) -> Result<ExitCode, RippyError> {
165 let content = std::fs::read_to_string(config_path)
166 .map_err(|e| RippyError::Trust(format!("could not read {}: {e}", config_path.display())))?;
167
168 let db = TrustDb::load();
169 let status = db.check(config_path, &content);
170 match status {
171 TrustStatus::Trusted => {
172 eprintln!("[rippy] trusted: {}", config_path.display());
173 Ok(ExitCode::SUCCESS)
174 }
175 TrustStatus::Untrusted => {
176 eprintln!(
177 "[rippy] untrusted: {} — run `rippy trust` to approve",
178 config_path.display()
179 );
180 Ok(ExitCode::from(2))
181 }
182 TrustStatus::Modified { .. } => {
183 eprintln!(
184 "[rippy] modified since last trust: {} — run `rippy trust` to re-approve",
185 config_path.display()
186 );
187 Ok(ExitCode::from(2))
188 }
189 }
190}
191
192#[allow(clippy::unnecessary_wraps)]
194fn list_trusted() -> Result<ExitCode, RippyError> {
195 let db = TrustDb::load();
196 if db.is_empty() {
197 eprintln!("[rippy] no trusted project configs");
198 } else {
199 for (path, entry) in db.list() {
200 eprintln!(
201 "{path} (trusted {}, hash {})",
202 entry.trusted_at, entry.hash
203 );
204 }
205 }
206 Ok(ExitCode::SUCCESS)
207}