lean_ctx/cli/
allow_cmd.rs1use crate::core::config;
10use crate::core::shell_allowlist;
11
12pub fn cmd_allow(args: &[String]) {
13 if args.iter().any(|a| a == "--help" || a == "-h") {
16 print_usage();
17 return;
18 }
19 match args.first().map(std::string::String::as_str) {
20 None => print_usage(),
21 Some("--list" | "list" | "ls") => print_effective(),
22 Some("--remove" | "-r" | "remove" | "rm") => remove(&args[1..]),
23 _ => add(args),
24 }
25}
26
27fn add(cmds: &[String]) {
29 let requested: Vec<String> = cmds
30 .iter()
31 .map(|c| c.trim().to_string())
32 .filter(|c| !c.is_empty())
33 .collect();
34
35 if requested.is_empty() {
36 print_usage();
37 return;
38 }
39
40 let mut extra = current_extra_from_global();
41 let mut added = Vec::new();
42 for cmd in requested {
43 if extra.iter().any(|e| e == &cmd) {
44 println!(" already allowed: {cmd}");
45 } else {
46 extra.push(cmd.clone());
47 added.push(cmd);
48 }
49 }
50
51 if added.is_empty() {
52 println!("\nNothing to add — all commands were already in the allowlist.");
53 print_effective();
54 return;
55 }
56
57 if let Err(e) = write_extra(&extra) {
58 eprintln!("Error: {e}");
59 std::process::exit(1);
60 }
61
62 println!("Allowed (additive): {}", added.join(", "));
63 println!("These are merged on top of the defaults — nothing else was removed.");
64 println!("Takes effect immediately; no MCP/daemon restart needed.");
65 print_effective();
66}
67
68fn remove(cmds: &[String]) {
70 let to_remove: Vec<String> = cmds
71 .iter()
72 .map(|c| c.trim().to_string())
73 .filter(|c| !c.is_empty())
74 .collect();
75
76 if to_remove.is_empty() {
77 eprintln!("Usage: lean-ctx allow --remove <cmd> [<cmd>...]");
78 std::process::exit(1);
79 }
80
81 let before = current_extra_from_global();
82 let after: Vec<String> = before
83 .iter()
84 .filter(|e| !to_remove.iter().any(|r| r == *e))
85 .cloned()
86 .collect();
87
88 let removed: Vec<&String> = before.iter().filter(|e| !after.contains(e)).collect();
89 if removed.is_empty() {
90 println!("None of those were in shell_allowlist_extra (nothing changed).");
91 println!(
92 "Note: built-in defaults can't be removed here — set `shell_allowlist` explicitly to override the whole list."
93 );
94 return;
95 }
96
97 if let Err(e) = write_extra(&after) {
98 eprintln!("Error: {e}");
99 std::process::exit(1);
100 }
101
102 let names: Vec<&str> = removed.iter().map(|s| s.as_str()).collect();
103 println!("Removed from extra allowlist: {}", names.join(", "));
104 print_effective();
105}
106
107fn print_effective() {
111 let effective = shell_allowlist::effective_allowlist_pub();
112 let parse_err = config::last_config_parse_error();
113 let path = config::Config::path().map_or_else(
114 || "~/.lean-ctx/config.toml".to_string(),
115 |p| p.display().to_string(),
116 );
117
118 println!("\nShell allowlist (enforced by the MCP tools):");
119 println!(" Config: {path}");
120
121 if let Some(err) = parse_err {
122 println!(" \x1b[31m⚠ config.toml FAILED to parse — running on DEFAULTS.\x1b[0m");
123 println!(" {err}");
124 println!(" Fix the TOML above, then re-run `lean-ctx allow --list`.");
125 }
126
127 if effective.is_empty() {
128 println!(" Mode: disabled (every command is allowed)");
129 return;
130 }
131
132 println!(
133 " Mode: restricted — {} command(s) permitted",
134 effective.len()
135 );
136
137 let extra = current_extra_from_global();
138 if extra.is_empty() {
139 println!(" Extra (additive, via `lean-ctx allow`): none");
140 } else {
141 println!(
142 " Extra (additive, via `lean-ctx allow`): {}",
143 extra.join(", ")
144 );
145 }
146}
147
148fn current_extra_from_global() -> Vec<String> {
151 let Some(path) = config::Config::path() else {
152 return Vec::new();
153 };
154 let Ok(raw) = std::fs::read_to_string(&path) else {
155 return Vec::new();
156 };
157 let Ok(table) = raw.parse::<toml::Table>() else {
158 return Vec::new();
159 };
160 table
161 .get("shell_allowlist_extra")
162 .and_then(toml::Value::as_array)
163 .map(|arr| {
164 arr.iter()
165 .filter_map(|v| v.as_str().map(str::to_string))
166 .collect()
167 })
168 .unwrap_or_default()
169}
170
171fn write_extra(extra: &[String]) -> Result<(), String> {
173 config::setter::set_by_key("shell_allowlist_extra", &extra.join(",")).map(|_| ())
174}
175
176fn print_usage() {
177 println!(
178 "Usage: lean-ctx allow <cmd> [<cmd>...] Add command(s) to the shell allowlist (additive)\n\
179 \x20 lean-ctx allow --list Show the effective allowlist + config path\n\
180 \x20 lean-ctx allow --remove <cmd> Remove command(s) you previously added\n\
181 \n\
182 Why this exists: editing `shell_allowlist` replaces the whole built-in list.\n\
183 `lean-ctx allow` appends to `shell_allowlist_extra`, keeping git/cargo/npm/… intact.\n\
184 Example: lean-ctx allow acli"
185 );
186 print_effective();
187}