Skip to main content

lean_ctx/cli/
profile_cmd.rs

1use 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!(translation.enabled);
140    cmp!(translation.ruleset);
141    cmp!(layout.enabled);
142    cmp!(layout.min_lines);
143    cmp!(budget.max_context_tokens);
144    cmp!(budget.max_shell_invocations);
145    cmp!(budget.max_cost_usd);
146    cmp!(pipeline.intent);
147    cmp!(pipeline.relevance);
148    cmp!(pipeline.compression);
149    cmp!(pipeline.translation);
150    cmp!(autonomy.enabled);
151    cmp!(autonomy.auto_preload);
152    cmp!(autonomy.auto_dedup);
153    cmp!(autonomy.auto_related);
154    cmp!(autonomy.silent_preload);
155    cmp!(autonomy.auto_prefetch);
156    cmp!(autonomy.auto_response);
157    cmp!(autonomy.dedup_threshold);
158    cmp!(autonomy.prefetch_max_files);
159    cmp!(autonomy.prefetch_budget_tokens);
160    cmp!(autonomy.response_min_tokens);
161    cmp!(autonomy.checkpoint_interval);
162
163    diffs
164}
165
166fn cmd_profile_create(name: &str, base: Option<&str>, global: bool) {
167    let base_profile = base
168        .and_then(profiles::load_profile)
169        .unwrap_or_else(profiles::active_profile);
170
171    let mut new_profile = base_profile;
172    new_profile.profile.name = name.to_string();
173    new_profile.profile.inherits = base.map(String::from);
174    new_profile.profile.description = String::new();
175
176    let dir = if global {
177        let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
178            eprintln!("Cannot determine global data directory.");
179            std::process::exit(1);
180        };
181        data_dir.join("profiles")
182    } else {
183        std::env::current_dir()
184            .unwrap_or_default()
185            .join(".lean-ctx")
186            .join("profiles")
187    };
188
189    if let Err(e) = std::fs::create_dir_all(&dir) {
190        eprintln!("Cannot create directory {}: {e}", dir.display());
191        std::process::exit(1);
192    }
193
194    let path = dir.join(format!("{name}.toml"));
195    let toml_content = profiles::format_as_toml(&new_profile);
196
197    if let Err(e) = std::fs::write(&path, &toml_content) {
198        eprintln!("Error writing {}: {e}", path.display());
199        std::process::exit(1);
200    }
201
202    println!("Created profile '{name}' at {}", path.display());
203    if let Some(b) = base {
204        println!("  Based on: {b}");
205    }
206    println!("\nEdit the file to customize, then activate with:");
207    println!("  LEAN_CTX_PROFILE={name}");
208}
209
210fn cmd_profile_set(name: &str) {
211    if profiles::load_profile(name).is_none() {
212        eprintln!("Profile '{name}' not found. Available profiles:");
213        for p in profiles::list_profiles() {
214            eprintln!("  {}", p.name);
215        }
216        std::process::exit(1);
217    }
218
219    println!("To activate profile '{name}', run:\n");
220    println!("  export LEAN_CTX_PROFILE={name}\n");
221    println!("Or add it to your shell config (~/.zshrc, ~/.bashrc).");
222}
223
224fn print_profile_help() {
225    eprintln!(
226        "Usage: lean-ctx profile <command>
227
228Commands:
229  list              List available profiles
230  show [name]       Show profile details (default: active)
231  active            Show the currently active profile
232  diff <a> <b>      Compare two profiles side by side
233  create <name>     Create a new profile file
234    --from <base>   Base on an existing profile
235    --global        Create in global dir instead of project
236  set <name>        Show how to activate a profile"
237    );
238}