1use std::collections::BTreeSet;
4use std::path::PathBuf;
5use std::process::ExitCode;
6
7use crate::cli::{ListArgs, ListTarget};
8use crate::error::RippyError;
9use crate::{allowlists, handlers, inspect};
10
11pub fn run(args: &ListArgs) -> Result<ExitCode, RippyError> {
17 match &args.target {
18 ListTarget::Safe => list_safe(),
19 ListTarget::Handlers => list_handlers(),
20 ListTarget::Rules(rules_args) => list_rules(rules_args.filter.as_deref())?,
21 }
22 Ok(ExitCode::SUCCESS)
23}
24
25fn list_safe() {
26 let safe = allowlists::all_simple_safe();
27 println!("Safe commands (auto-approved):");
28 print_columns(&safe);
29 println!(" ({} commands)\n", safe.len());
30
31 let wrappers = allowlists::all_wrappers();
32 println!("Wrapper commands (pass through to inner command):");
33 print_columns(&wrappers);
34 println!(" ({} commands)", wrappers.len());
35}
36
37fn list_handlers() {
38 let all_cmds = handlers::all_handler_commands();
39 let mut groups: BTreeSet<Vec<&str>> = BTreeSet::new();
40
41 for cmd in &all_cmds {
42 if let Some(handler) = handlers::get_handler(cmd) {
43 groups.insert(handler.commands().to_vec());
44 }
45 }
46
47 println!("Handler commands:");
48 for cmds in &groups {
49 let joined = cmds.join(", ");
50 println!(" {joined}");
51 }
52 println!(
53 "\n ({} commands across {} handlers)",
54 all_cmds.len(),
55 groups.len()
56 );
57}
58
59fn list_rules(filter: Option<&str>) -> Result<(), RippyError> {
60 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
61 let output = inspect::collect_list_data(&cwd, None)?;
62
63 println!("Rules:\n");
64
65 for source in &output.config_sources {
66 let rules: Vec<_> = source
67 .rules
68 .iter()
69 .filter(|r| matches_filter(r, filter))
70 .collect();
71 if rules.is_empty() {
72 continue;
73 }
74 println!(" {}:", source.path);
75 for rule in &rules {
76 let msg = rule
77 .message
78 .as_ref()
79 .map_or(String::new(), |m| format!(" \"{m}\""));
80 println!(" {:<6} {}{msg}", rule.action, rule.pattern);
81 }
82 println!();
83 }
84
85 for source in &output.cc_sources {
86 let rules: Vec<_> = source
87 .rules
88 .iter()
89 .filter(|r| matches_filter(r, filter))
90 .collect();
91 if rules.is_empty() {
92 continue;
93 }
94 println!(" {}:", source.path);
95 for rule in &rules {
96 println!(" {:<6} {}", rule.action, rule.pattern);
97 }
98 println!();
99 }
100 Ok(())
101}
102
103fn matches_filter(rule: &inspect::RuleDisplay, filter: Option<&str>) -> bool {
104 let Some(f) = filter else { return true };
105 rule.pattern.contains(f) || rule.action.contains(f)
106}
107
108fn print_columns(items: &[&str]) {
110 for chunk in items.chunks(6) {
111 let row: Vec<String> = chunk.iter().map(|s| format!("{s:<14}")).collect();
112 println!(" {}", row.join(""));
113 }
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn safe_list_is_sorted_and_nonempty() {
123 let safe = allowlists::all_simple_safe();
124 assert!(!safe.is_empty());
125 let mut sorted = safe.clone();
126 sorted.sort_unstable();
127 assert_eq!(safe, sorted);
128 }
129
130 #[test]
131 fn wrapper_list_is_sorted_and_nonempty() {
132 let wrappers = allowlists::all_wrappers();
133 assert!(!wrappers.is_empty());
134 let mut sorted = wrappers.clone();
135 sorted.sort_unstable();
136 assert_eq!(wrappers, sorted);
137 }
138
139 #[test]
140 fn handler_commands_is_sorted_and_nonempty() {
141 let cmds = handlers::all_handler_commands();
142 assert!(!cmds.is_empty());
143 let mut sorted = cmds.clone();
144 sorted.sort_unstable();
145 assert_eq!(cmds, sorted);
146 }
147
148 #[test]
149 fn filter_matches_pattern() {
150 let rule = inspect::RuleDisplay {
151 action: "allow".into(),
152 pattern: "git status".into(),
153 message: None,
154 };
155 assert!(matches_filter(&rule, Some("git")));
156 assert!(!matches_filter(&rule, Some("docker")));
157 assert!(matches_filter(&rule, None));
158 }
159
160 #[test]
161 fn filter_matches_action() {
162 let rule = inspect::RuleDisplay {
163 action: "deny".into(),
164 pattern: "rm -rf".into(),
165 message: None,
166 };
167 assert!(matches_filter(&rule, Some("deny")));
168 assert!(!matches_filter(&rule, Some("allow")));
169 }
170}