lean_ctx/cli/
profile_cmd.rs1use crate::core::profiles;
2
3pub fn cmd_profile(args: &[String]) {
4 let action = args.first().map_or("list", String::as_str);
5
6 match action {
7 "list" | "ls" => cmd_profile_list(),
8 "show" => {
9 let name = args
10 .get(1)
11 .map_or_else(profiles::active_profile_name, Clone::clone);
12 cmd_profile_show(&name);
13 }
14 "active" | "current" => cmd_profile_active(),
15 "diff" => {
16 if args.len() < 3 {
17 eprintln!("Usage: lean-ctx profile diff <profile-a> <profile-b>");
18 std::process::exit(1);
19 }
20 cmd_profile_diff(&args[1], &args[2]);
21 }
22 "create" => {
23 if args.len() < 2 {
24 eprintln!("Usage: lean-ctx profile create <name> [--from <base>] [--global]");
25 std::process::exit(1);
26 }
27 let name = &args[1];
28 let base = args
29 .iter()
30 .position(|a| a == "--from")
31 .and_then(|i| args.get(i + 1))
32 .map(String::as_str);
33 let global = args.iter().any(|a| a == "--global");
34 cmd_profile_create(name, base, global);
35 }
36 "set" => {
37 if args.len() < 2 {
38 eprintln!("Usage: lean-ctx profile set <name>");
39 eprintln!(" Sets LEAN_CTX_PROFILE for the current shell.");
40 std::process::exit(1);
41 }
42 cmd_profile_set(&args[1]);
43 }
44 _ => {
45 if profiles::load_profile(action).is_some() {
46 cmd_profile_show(action);
47 } else {
48 print_profile_help();
49 std::process::exit(1);
50 }
51 }
52 }
53}
54
55fn cmd_profile_list() {
56 let list = profiles::list_profiles();
57 let active = profiles::active_profile_name();
58
59 let header = format!(" {:<16} {:<10} {}", "Name", "Source", "Description");
60 let sep = format!(" {}", "\u{2500}".repeat(60));
61 println!("Available profiles:\n");
62 println!("{header}");
63 println!("{sep}");
64
65 for p in &list {
66 let marker = if p.name == active { " *" } else { " " };
67 println!("{marker}{:<16} {:<10} {}", p.name, p.source, p.description);
68 }
69
70 println!("\n Active: {active}");
71 println!(" Set via: LEAN_CTX_PROFILE=<name> or lean-ctx profile set <name>");
72}
73
74fn cmd_profile_show(name: &str) {
75 if let Some(profile) = profiles::load_profile(name) {
76 println!("Profile: {name}\n");
77 println!("{}", profiles::format_as_toml(&profile));
78 } else {
79 eprintln!("Profile '{name}' not found.");
80 eprintln!("Run 'lean-ctx profile list' to see available profiles.");
81 std::process::exit(1);
82 }
83}
84
85fn cmd_profile_active() {
86 let name = profiles::active_profile_name();
87 let profile = profiles::active_profile();
88 println!("Active profile: {name}\n");
89 println!("{}", profiles::format_as_toml(&profile));
90}
91
92fn cmd_profile_diff(name_a: &str, name_b: &str) {
93 let Some(a) = profiles::load_profile(name_a) else {
94 eprintln!("Profile '{name_a}' not found.");
95 std::process::exit(1);
96 };
97 let Some(b) = profiles::load_profile(name_b) else {
98 eprintln!("Profile '{name_b}' not found.");
99 std::process::exit(1);
100 };
101
102 println!("Profile diff: {name_a} vs {name_b}\n");
103
104 let diffs = collect_diffs(&a, &b);
105 if diffs.is_empty() {
106 println!(" No differences.");
107 } else {
108 println!(" {:<32} {:<20} {:<20}", "Field", name_a, name_b);
109 println!(" {}", "\u{2500}".repeat(72));
110 for (field, val_a, val_b) in &diffs {
111 println!(" {field:<32} {val_a:<20} {val_b:<20}");
112 }
113 }
114}
115
116fn collect_diffs(a: &profiles::Profile, b: &profiles::Profile) -> Vec<(String, String, String)> {
117 let mut diffs = Vec::new();
118
119 macro_rules! cmp {
120 ($section:ident . $field:ident) => {
121 let va = format!("{:?}", a.$section.$field);
122 let vb = format!("{:?}", b.$section.$field);
123 if va != vb {
124 diffs.push((
125 format!("{}.{}", stringify!($section), stringify!($field)),
126 va,
127 vb,
128 ));
129 }
130 };
131 }
132
133 cmp!(read.default_mode);
134 cmp!(read.max_tokens_per_file);
135 cmp!(read.prefer_cache);
136 cmp!(compression.crp_mode);
137 cmp!(compression.output_density);
138 cmp!(compression.entropy_threshold);
139 cmp!(budget.max_context_tokens);
140 cmp!(budget.max_shell_invocations);
141 cmp!(budget.max_cost_usd);
142 cmp!(pipeline.intent);
143 cmp!(pipeline.relevance);
144 cmp!(pipeline.compression);
145 cmp!(pipeline.translation);
146 cmp!(autonomy.auto_dedup);
147 cmp!(autonomy.checkpoint_interval);
148
149 diffs
150}
151
152fn cmd_profile_create(name: &str, base: Option<&str>, global: bool) {
153 let base_profile = base
154 .and_then(profiles::load_profile)
155 .unwrap_or_else(profiles::active_profile);
156
157 let mut new_profile = base_profile;
158 new_profile.profile.name = name.to_string();
159 new_profile.profile.inherits = base.map(String::from);
160 new_profile.profile.description = String::new();
161
162 let dir = if global {
163 let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
164 eprintln!("Cannot determine global data directory.");
165 std::process::exit(1);
166 };
167 data_dir.join("profiles")
168 } else {
169 std::env::current_dir()
170 .unwrap_or_default()
171 .join(".lean-ctx")
172 .join("profiles")
173 };
174
175 if let Err(e) = std::fs::create_dir_all(&dir) {
176 eprintln!("Cannot create directory {}: {e}", dir.display());
177 std::process::exit(1);
178 }
179
180 let path = dir.join(format!("{name}.toml"));
181 let toml_content = profiles::format_as_toml(&new_profile);
182
183 if let Err(e) = std::fs::write(&path, &toml_content) {
184 eprintln!("Error writing {}: {e}", path.display());
185 std::process::exit(1);
186 }
187
188 println!("Created profile '{name}' at {}", path.display());
189 if let Some(b) = base {
190 println!(" Based on: {b}");
191 }
192 println!("\nEdit the file to customize, then activate with:");
193 println!(" LEAN_CTX_PROFILE={name}");
194}
195
196fn cmd_profile_set(name: &str) {
197 if profiles::load_profile(name).is_none() {
198 eprintln!("Profile '{name}' not found. Available profiles:");
199 for p in profiles::list_profiles() {
200 eprintln!(" {}", p.name);
201 }
202 std::process::exit(1);
203 }
204
205 println!("To activate profile '{name}', run:\n");
206 println!(" export LEAN_CTX_PROFILE={name}\n");
207 println!("Or add it to your shell config (~/.zshrc, ~/.bashrc).");
208}
209
210fn print_profile_help() {
211 eprintln!(
212 "Usage: lean-ctx profile <command>
213
214Commands:
215 list List available profiles
216 show [name] Show profile details (default: active)
217 active Show the currently active profile
218 diff <a> <b> Compare two profiles side by side
219 create <name> Create a new profile file
220 --from <base> Base on an existing profile
221 --global Create in global dir instead of project
222 set <name> Show how to activate a profile"
223 );
224}