Skip to main content

lean_ctx/
setup.rs

1use std::path::PathBuf;
2
3use crate::core::editor_registry::{ConfigType, EditorTarget, WriteAction, WriteOptions};
4use crate::core::portable_binary::resolve_portable_binary;
5use crate::core::setup_report::{PlatformInfo, SetupItem, SetupReport, SetupStepReport};
6use crate::hooks::{recommend_hook_mode, HookMode};
7use chrono::Utc;
8use std::ffi::OsString;
9
10pub fn claude_config_json_path(home: &std::path::Path) -> PathBuf {
11    crate::core::editor_registry::claude_mcp_json_path(home)
12}
13
14pub fn claude_config_dir(home: &std::path::Path) -> PathBuf {
15    crate::core::editor_registry::claude_state_dir(home)
16}
17
18pub(crate) struct EnvVarGuard {
19    key: &'static str,
20    previous: Option<OsString>,
21}
22
23impl EnvVarGuard {
24    pub(crate) fn set(key: &'static str, value: &str) -> Self {
25        let previous = std::env::var_os(key);
26        std::env::set_var(key, value);
27        Self { key, previous }
28    }
29}
30
31impl Drop for EnvVarGuard {
32    fn drop(&mut self) {
33        if let Some(previous) = &self.previous {
34            std::env::set_var(self.key, previous);
35        } else {
36            std::env::remove_var(self.key);
37        }
38    }
39}
40
41/// Determine the setup level from a first-run interactive menu.
42/// Returns (inject_rules, inject_skills).
43fn first_run_setup_level() -> (bool, bool) {
44    use std::io::Write;
45
46    let cfg = crate::core::config::Config::load();
47    if cfg.setup.auto_inject_rules.is_some() {
48        return (
49            cfg.setup.should_inject_rules(),
50            cfg.setup.should_inject_skills(),
51        );
52    }
53
54    println!();
55    println!("  \x1b[1mWelcome to lean-ctx!\x1b[0m");
56    println!();
57    println!("  lean-ctx compresses AI context by 60-99%, saving tokens and money.");
58    println!();
59    println!("  Choose your setup level:");
60    println!("    \x1b[36m[1]\x1b[0m Minimal  \x1b[2m— Just MCP tools, no config file changes (recommended)\x1b[0m");
61    println!("    \x1b[36m[2]\x1b[0m Standard \x1b[2m— MCP tools + agent instructions for optimal mode selection\x1b[0m");
62    println!("    \x1b[36m[3]\x1b[0m Full     \x1b[2m— Everything (tools + rules + skills + shell hooks)\x1b[0m");
63    println!();
64    print!("  Your choice \x1b[1m[1]\x1b[0m: ");
65    std::io::stdout().flush().ok();
66
67    let mut input = String::new();
68    let choice = if std::io::stdin().read_line(&mut input).is_ok() {
69        input.trim().parse::<u8>().unwrap_or(1)
70    } else {
71        1
72    };
73
74    match choice {
75        3 => (true, true),
76        2 => (true, false),
77        _ => (false, false),
78    }
79}
80
81/// Persist the user's setup level choice to config.toml.
82fn persist_setup_choice(inject_rules: bool, inject_skills: bool) {
83    let mut cfg = crate::core::config::Config::load();
84    cfg.setup.auto_inject_rules = Some(inject_rules);
85    cfg.setup.auto_inject_skills = Some(inject_skills);
86    let _ = cfg.save();
87}
88
89pub fn run_setup() {
90    use crate::terminal_ui;
91
92    if crate::shell::is_non_interactive() {
93        eprintln!("Non-interactive terminal detected (no TTY on stdin).");
94        eprintln!("Running in non-interactive mode (equivalent to: lean-ctx setup --non-interactive --yes)");
95        eprintln!();
96        let opts = SetupOptions {
97            non_interactive: true,
98            yes: true,
99            ..Default::default()
100        };
101        match run_setup_with_options(opts) {
102            Ok(report) => {
103                if !report.warnings.is_empty() {
104                    for w in &report.warnings {
105                        tracing::warn!("{w}");
106                    }
107                }
108            }
109            Err(e) => tracing::error!("Setup error: {e}"),
110        }
111        return;
112    }
113
114    let Some(home) = dirs::home_dir() else {
115        tracing::error!("Cannot determine home directory");
116        std::process::exit(1);
117    };
118
119    let binary = resolve_portable_binary();
120
121    let home_str = home.to_string_lossy().to_string();
122
123    terminal_ui::print_setup_header();
124
125    let (inject_rules, inject_skills) = first_run_setup_level();
126    persist_setup_choice(inject_rules, inject_skills);
127
128    // Step 1: Shell hook (legacy aliases + universal shell hook)
129    terminal_ui::print_step_header(1, 12, "Shell Hook");
130    crate::cli::cmd_init(&["--global".to_string()]);
131    crate::shell_hook::install_all(false);
132
133    // Step 2: Daemon (optional acceleration for CLI routing)
134    terminal_ui::print_step_header(2, 12, "Daemon");
135    if crate::daemon::is_daemon_running() {
136        terminal_ui::print_status_ok("Daemon running — restarting with current binary…");
137        let _ = crate::daemon::stop_daemon();
138        std::thread::sleep(std::time::Duration::from_millis(500));
139        if let Err(e) = crate::daemon::start_daemon(&[]) {
140            terminal_ui::print_status_warn(&format!("Daemon restart failed: {e}"));
141        }
142    } else if let Err(e) = crate::daemon::start_daemon(&[]) {
143        terminal_ui::print_status_warn(&format!("Daemon start failed: {e}"));
144    }
145
146    // Step 3: Editor auto-detection + configuration
147    terminal_ui::print_step_header(3, 12, "AI Tool Detection");
148
149    let targets = crate::core::editor_registry::build_targets(&home);
150    let mut newly_configured: Vec<&str> = Vec::new();
151    let mut already_configured: Vec<&str> = Vec::new();
152    let mut not_installed: Vec<&str> = Vec::new();
153    let mut errors: Vec<&str> = Vec::new();
154
155    for target in &targets {
156        let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
157
158        if !target.detect_path.exists() {
159            not_installed.push(target.name);
160            continue;
161        }
162
163        let mode = if target.agent_key.is_empty() {
164            HookMode::Mcp
165        } else {
166            recommend_hook_mode(&target.agent_key)
167        };
168
169        match crate::core::editor_registry::write_config_with_options(
170            target,
171            &binary,
172            WriteOptions {
173                overwrite_invalid: false,
174            },
175        ) {
176            Ok(res) if res.action == WriteAction::Already => {
177                terminal_ui::print_status_ok(&format!(
178                    "{:<20} \x1b[36m{mode}\x1b[0m  \x1b[2m{short_path}\x1b[0m",
179                    target.name
180                ));
181                already_configured.push(target.name);
182            }
183            Ok(_) => {
184                terminal_ui::print_status_new(&format!(
185                    "{:<20} \x1b[36m{mode}\x1b[0m  \x1b[2m{short_path}\x1b[0m",
186                    target.name
187                ));
188                newly_configured.push(target.name);
189            }
190            Err(e) => {
191                terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
192                errors.push(target.name);
193            }
194        }
195    }
196
197    let total_ok = newly_configured.len() + already_configured.len();
198    if total_ok == 0 && errors.is_empty() {
199        terminal_ui::print_status_warn(
200            "No AI tools detected. Install one and re-run: lean-ctx setup",
201        );
202    }
203
204    if !not_installed.is_empty() {
205        println!(
206            "  \x1b[2m○ {} not detected: {}\x1b[0m",
207            not_installed.len(),
208            not_installed.join(", ")
209        );
210    }
211
212    configure_plan_mode_settings(&newly_configured, &already_configured);
213
214    // Step 4: Agent rules injection (only if user opted in)
215    terminal_ui::print_step_header(4, 12, "Agent Rules");
216    let rules_result = if inject_rules {
217        let r = crate::rules_inject::inject_all_rules(&home);
218        for name in &r.injected {
219            terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
220        }
221        for name in &r.updated {
222            terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
223        }
224        for name in &r.already {
225            terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
226        }
227        for err in &r.errors {
228            terminal_ui::print_status_warn(err);
229        }
230        if !r.backed_up.is_empty() {
231            for bak in &r.backed_up {
232                println!("  \x1b[2m  ↳ backup: {bak}\x1b[0m");
233            }
234        }
235        if r.injected.is_empty()
236            && r.updated.is_empty()
237            && r.already.is_empty()
238            && r.errors.is_empty()
239        {
240            terminal_ui::print_status_skip("No agent rules needed");
241        }
242        r
243    } else {
244        terminal_ui::print_status_skip("Skipped (run `lean-ctx setup --inject-rules` to enable)");
245        crate::rules_inject::InjectResult::default()
246    };
247
248    // Agent hooks (mode-aware)
249    for target in &targets {
250        if !target.detect_path.exists() || target.agent_key.is_empty() {
251            continue;
252        }
253        let mode = recommend_hook_mode(&target.agent_key);
254        crate::hooks::install_agent_hook_with_mode(&target.agent_key, true, mode);
255    }
256
257    // Step 5: API Proxy (opt-in)
258    terminal_ui::print_step_header(5, 12, "API Proxy (optional)");
259    {
260        let mut cfg = crate::core::config::Config::load();
261        let proxy_port = crate::proxy_setup::default_port();
262
263        match cfg.proxy_enabled {
264            Some(true) => {
265                crate::proxy_autostart::install(proxy_port, false);
266                std::thread::sleep(std::time::Duration::from_millis(500));
267                crate::proxy_setup::install_proxy_env(&home, proxy_port, false);
268                terminal_ui::print_status_ok("Proxy active (opted in)");
269            }
270            Some(false) => {
271                terminal_ui::print_status_skip(
272                    "Proxy disabled (run `lean-ctx proxy enable` to change)",
273                );
274            }
275            None => {
276                println!(
277                    "  \x1b[2mThe API proxy routes LLM requests through lean-ctx for additional\x1b[0m"
278                );
279                println!(
280                    "  \x1b[2mtool-result compression and precise token analytics in the dashboard.\x1b[0m"
281                );
282                println!();
283                println!(
284                    "  \x1b[2mWithout it: MCP tools, shell hooks, gain tracking, and memory\x1b[0m"
285                );
286                println!(
287                    "  \x1b[2mall work normally. The proxy adds ~5-15% extra savings on top.\x1b[0m"
288                );
289                println!();
290                print!("  Enable the API proxy? [y/N] ");
291                let _ = std::io::Write::flush(&mut std::io::stdout());
292                let mut input = String::new();
293                let _ = std::io::stdin().read_line(&mut input);
294                let answer = matches!(input.trim().to_lowercase().as_str(), "y" | "yes");
295                cfg.proxy_enabled = Some(answer);
296                let _ = cfg.save();
297                if answer {
298                    crate::proxy_autostart::install(proxy_port, false);
299                    std::thread::sleep(std::time::Duration::from_millis(500));
300                    crate::proxy_setup::install_proxy_env(&home, proxy_port, false);
301                    terminal_ui::print_status_new("Proxy enabled");
302                } else {
303                    terminal_ui::print_status_skip(
304                        "Proxy skipped (run `lean-ctx proxy enable` anytime)",
305                    );
306                }
307            }
308        }
309    }
310
311    // Step 6: SKILL.md installation (only if user opted in)
312    terminal_ui::print_step_header(6, 12, "Skill Files");
313    if inject_skills {
314        let skill_result = install_skill_files(&home);
315        for (name, installed) in &skill_result {
316            if *installed {
317                terminal_ui::print_status_new(&format!(
318                    "{name:<20} \x1b[2mSKILL.md installed\x1b[0m"
319                ));
320            } else {
321                terminal_ui::print_status_ok(&format!(
322                    "{name:<20} \x1b[2mSKILL.md up-to-date\x1b[0m"
323                ));
324            }
325        }
326        if skill_result.is_empty() {
327            terminal_ui::print_status_skip("No skill directories to install");
328        }
329    } else {
330        terminal_ui::print_status_skip(
331            "Skipped (skill files install with the rules opt-in; choose Standard/Full in `lean-ctx setup`)",
332        );
333    }
334
335    // Step 7: Data directory + diagnostics
336    terminal_ui::print_step_header(7, 12, "Environment Check");
337    let lean_dir = crate::core::data_dir::lean_ctx_data_dir()
338        .unwrap_or_else(|_| home.join(".config/lean-ctx"));
339    if lean_dir.exists() {
340        terminal_ui::print_status_ok(&format!("{} ready", lean_dir.display()));
341    } else {
342        let _ = std::fs::create_dir_all(&lean_dir);
343        terminal_ui::print_status_new(&format!("Created {}", lean_dir.display()));
344    }
345    if let Some(tokens) = crate::core::data_dir::migrate_if_split() {
346        terminal_ui::print_status_new(&format!(
347            "Migrated stats from split data dir ({tokens} tokens recovered)"
348        ));
349    }
350    crate::doctor::run_compact();
351
352    // Step 8: Data sharing
353    terminal_ui::print_step_header(8, 12, "Help Improve lean-ctx");
354    println!("  Share anonymous compression stats to make lean-ctx better.");
355    println!("  \x1b[1mNo code, no file names, no personal data — ever.\x1b[0m");
356    println!();
357    print!("  Enable anonymous data sharing? \x1b[1m[y/N]\x1b[0m ");
358    use std::io::Write;
359    std::io::stdout().flush().ok();
360
361    let mut input = String::new();
362    let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
363        let answer = input.trim().to_lowercase();
364        answer == "y" || answer == "yes"
365    } else {
366        false
367    };
368
369    if contribute {
370        let config_dir = crate::core::data_dir::lean_ctx_data_dir()
371            .unwrap_or_else(|_| home.join(".config/lean-ctx"));
372        let _ = std::fs::create_dir_all(&config_dir);
373        let config_path = config_dir.join("config.toml");
374        let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
375        if !config_content.contains("[cloud]") {
376            if !config_content.is_empty() && !config_content.ends_with('\n') {
377                config_content.push('\n');
378            }
379            config_content.push_str("\n[cloud]\ncontribute_enabled = true\n");
380            let _ = crate::config_io::write_atomic_with_backup(&config_path, &config_content);
381        }
382        terminal_ui::print_status_ok("Enabled — thank you!");
383    } else {
384        terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx config");
385    }
386
387    // Step 9: Auto-Update opt-in
388    terminal_ui::print_step_header(9, 12, "Auto-Updates");
389    println!("  Keep lean-ctx up to date automatically.");
390    println!("  \x1b[1mChecks GitHub every 6h, installs only when a new release exists.\x1b[0m");
391    println!(
392        "  \x1b[2mNo restarts mid-session. Change anytime: lean-ctx update --schedule off\x1b[0m"
393    );
394    println!();
395    print!("  Enable automatic updates? \x1b[1m[y/N]\x1b[0m ");
396    std::io::stdout().flush().ok();
397
398    let mut auto_input = String::new();
399    let auto_update = if std::io::stdin().read_line(&mut auto_input).is_ok() {
400        let answer = auto_input.trim().to_lowercase();
401        answer == "y" || answer == "yes"
402    } else {
403        false
404    };
405
406    if auto_update {
407        let cfg = crate::core::config::Config::load();
408        let hours = cfg.updates.check_interval_hours;
409        match crate::core::update_scheduler::install_schedule(hours) {
410            Ok(info) => {
411                crate::core::update_scheduler::set_auto_update(true, false, hours);
412                terminal_ui::print_status_ok(&format!("Enabled — {info}"));
413            }
414            Err(e) => {
415                terminal_ui::print_status_warn(&format!("Scheduler setup failed: {e}"));
416                terminal_ui::print_status_skip("Enable later: lean-ctx update --schedule");
417            }
418        }
419    } else {
420        crate::core::update_scheduler::set_auto_update(false, false, 6);
421        terminal_ui::print_status_skip("Skipped — enable later: lean-ctx update --schedule");
422    }
423
424    // Step 10: Tool Profile selection
425    terminal_ui::print_step_header(10, 12, "Tool Profile");
426    configure_tool_profile();
427
428    // Step 11: Advanced tuning (optional power-user options)
429    terminal_ui::print_step_header(11, 12, "Advanced Tuning (optional)");
430    configure_premium_features(&home);
431
432    // Step 12: Code Intelligence — build graph in background
433    terminal_ui::print_step_header(12, 12, "Code Intelligence");
434    let cwd = std::env::current_dir().ok();
435    let cwd_is_home = cwd
436        .as_ref()
437        .is_some_and(|d| dirs::home_dir().is_some_and(|h| d.as_path() == h.as_path()));
438    if cwd_is_home {
439        terminal_ui::print_status_warn(
440            "Running from $HOME — graph build skipped to avoid scanning your entire home directory.",
441        );
442        println!();
443        println!("  \x1b[1mSet a default project root to avoid this:\x1b[0m");
444        println!("  \x1b[2mEnter your main project path (or press Enter to skip):\x1b[0m");
445        print!("  \x1b[1m>\x1b[0m ");
446        use std::io::Write;
447        std::io::stdout().flush().ok();
448        let mut root_input = String::new();
449        if std::io::stdin().read_line(&mut root_input).is_ok() {
450            let root_trimmed = root_input.trim();
451            if root_trimmed.is_empty() {
452                terminal_ui::print_status_skip("No project root set. Set later: lean-ctx config set project_root /path/to/project");
453            } else {
454                let root_path = std::path::Path::new(root_trimmed);
455                if root_path.exists() && root_path.is_dir() {
456                    let config_path = crate::core::data_dir::lean_ctx_data_dir()
457                        .unwrap_or_else(|_| home.join(".config/lean-ctx"))
458                        .join("config.toml");
459                    let mut content = std::fs::read_to_string(&config_path).unwrap_or_default();
460                    if content.contains("project_root") {
461                        if let Ok(re) = regex::Regex::new(r#"(?m)^project_root\s*=\s*"[^"]*""#) {
462                            content = re
463                                .replace(&content, &format!("project_root = \"{root_trimmed}\""))
464                                .to_string();
465                        }
466                    } else {
467                        if !content.is_empty() && !content.ends_with('\n') {
468                            content.push('\n');
469                        }
470                        content.push_str(&format!("project_root = \"{root_trimmed}\"\n"));
471                    }
472                    let _ = crate::config_io::write_atomic_with_backup(&config_path, &content);
473                    terminal_ui::print_status_ok(&format!("Project root set: {root_trimmed}"));
474                    if root_path.join(".git").exists()
475                        || root_path.join("Cargo.toml").exists()
476                        || root_path.join("package.json").exists()
477                    {
478                        spawn_index_build_background(root_path);
479                        terminal_ui::print_status_ok("Graph build started (background)");
480                    }
481                } else {
482                    terminal_ui::print_status_warn(&format!(
483                        "Path not found: {root_trimmed} — skipped"
484                    ));
485                }
486            }
487        }
488    } else {
489        let is_project = cwd.as_ref().is_some_and(|d| {
490            d.join(".git").exists()
491                || d.join("Cargo.toml").exists()
492                || d.join("package.json").exists()
493                || d.join("go.mod").exists()
494        });
495        if is_project {
496            println!("  \x1b[2mBuilding code graph for graph-aware reads, impact analysis,\x1b[0m");
497            println!("  \x1b[2mand smart search fusion in the background...\x1b[0m");
498            if let Some(ref root) = cwd {
499                spawn_index_build_background(root);
500            }
501            terminal_ui::print_status_ok("Graph build started (background)");
502        } else {
503            println!(
504                "  \x1b[2mRun `lean-ctx impact build` inside any git project to enable\x1b[0m"
505            );
506            println!(
507                "  \x1b[2mgraph-aware reads, impact analysis, and smart search fusion.\x1b[0m"
508            );
509        }
510    }
511    println!();
512
513    // Auto-approve transparency banner
514    {
515        let tools = crate::core::editor_registry::writers::auto_approve_tools();
516        println!();
517        println!(
518            "  \x1b[33m⚡ Auto-approved tools ({} total):\x1b[0m",
519            tools.len()
520        );
521        for chunk in tools.chunks(6) {
522            let names: Vec<_> = chunk.iter().map(|t| format!("\x1b[2m{t}\x1b[0m")).collect();
523            println!("    {}", names.join(", "));
524        }
525        println!("  \x1b[2mDisable with: lean-ctx setup --no-auto-approve\x1b[0m");
526    }
527
528    // Summary
529    println!();
530    println!(
531        "  \x1b[1;32m✓ Setup complete!\x1b[0m  \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
532        newly_configured.len(),
533        already_configured.len(),
534        not_installed.len()
535    );
536
537    if !errors.is_empty() {
538        println!(
539            "  \x1b[33m⚠ {} error{}: {}\x1b[0m",
540            errors.len(),
541            if errors.len() == 1 { "" } else { "s" },
542            errors.join(", ")
543        );
544    }
545
546    // Next steps
547    let source_cmd = crate::shell_hook::shell_source_command().unwrap_or("Restart your shell");
548
549    let dim = "\x1b[2m";
550    let bold = "\x1b[1m";
551    let cyan = "\x1b[36m";
552    let yellow = "\x1b[33m";
553    let rst = "\x1b[0m";
554
555    println!();
556    println!("  {bold}Next steps:{rst}");
557    println!();
558    println!("  {cyan}1.{rst} Reload your shell:");
559    println!("     {bold}{source_cmd}{rst}");
560    println!();
561
562    let mut tools_to_restart: Vec<String> = newly_configured
563        .iter()
564        .map(std::string::ToString::to_string)
565        .collect();
566    for name in rules_result
567        .injected
568        .iter()
569        .chain(rules_result.updated.iter())
570    {
571        if !tools_to_restart.iter().any(|t| t == name) {
572            tools_to_restart.push(name.clone());
573        }
574    }
575
576    if !tools_to_restart.is_empty() {
577        println!("  {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
578        println!("     {bold}{}{rst}", tools_to_restart.join(", "));
579        println!(
580            "     {dim}Changes take effect after a full restart (MCP may be enabled or disabled depending on mode).{rst}"
581        );
582        println!("     {dim}Close and re-open the application completely.{rst}");
583    } else if !already_configured.is_empty() {
584        println!(
585            "  {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
586        );
587    }
588
589    println!();
590    println!(
591        "  {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
592    );
593    println!("  {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
594
595    // Logo + commands
596    println!();
597    terminal_ui::print_logo_animated();
598    terminal_ui::print_command_box();
599}
600
601/// Friendly, non-interactive "golden path" onboarding.
602///
603/// Unlike `run_setup` (the full 12-step interactive wizard), `onboard` makes
604/// every decision for the user with sensible defaults — connect detected AI
605/// tools, install the shell hook, set the `standard` tool profile — then prints
606/// one clear "you're all set" message with a single obvious next step. This is
607/// the recommended first-run path: time-to-value in seconds, zero prompts.
608pub fn run_onboard() {
609    use crate::terminal_ui;
610
611    let dim = "\x1b[2m";
612    let bold = "\x1b[1m";
613    let cyan = "\x1b[36m";
614    let green = "\x1b[1;32m";
615    let yellow = "\x1b[33m";
616    let rst = "\x1b[0m";
617
618    println!();
619    println!("  {bold}Connecting lean-ctx to your AI tools…{rst}");
620    println!("  {dim}No questions — using recommended defaults. Run `lean-ctx setup` for full control.{rst}");
621    println!();
622
623    let opts = SetupOptions {
624        non_interactive: true,
625        yes: true,
626        fix: true,
627        ..Default::default()
628    };
629
630    let report = match run_setup_with_options(opts) {
631        Ok(r) => r,
632        Err(e) => {
633            eprintln!("  {yellow}Onboarding could not complete: {e}{rst}");
634            eprintln!("  {dim}Try the guided setup instead: lean-ctx setup{rst}");
635            std::process::exit(1);
636        }
637    };
638
639    // Which AI tools did we actually wire up?
640    let connected: Vec<String> = report
641        .steps
642        .iter()
643        .find(|s| s.name == "editors")
644        .map(|s| {
645            s.items
646                .iter()
647                .filter(|i| matches!(i.status.as_str(), "created" | "updated" | "already"))
648                .map(|i| i.name.clone())
649                .collect()
650        })
651        .unwrap_or_default();
652
653    let data_dir = crate::core::data_dir::lean_ctx_data_dir()
654        .map_or_else(|_| "~/.lean-ctx".to_string(), |p| p.display().to_string());
655
656    println!();
657    if connected.is_empty() {
658        println!("  {yellow}No AI tools detected yet.{rst}");
659        println!(
660            "  {dim}Install Cursor, Claude Code, VS Code, etc., then re-run: lean-ctx onboard{rst}"
661        );
662    } else {
663        println!("  {green}✓ lean-ctx is connected.{rst}");
664        println!();
665        println!("  {bold}Connected:{rst} {}", connected.join(", "));
666    }
667    println!("  {dim}Data dir:{rst}  {data_dir}");
668
669    let source_cmd = crate::shell_hook::shell_source_command().unwrap_or("Restart your shell");
670    println!();
671    println!("  {bold}One last step:{rst}");
672    println!("  {cyan}1.{rst} Reload your shell:  {bold}{source_cmd}{rst}");
673    if !connected.is_empty() {
674        println!(
675            "  {cyan}2.{rst} {yellow}Fully restart your AI tool{rst} {dim}(so it reconnects to lean-ctx){rst}"
676        );
677        println!(
678            "  {cyan}3.{rst} Ask your AI to read a file — lean-ctx optimizes it automatically."
679        );
680    }
681    println!();
682    println!("  {dim}Check anytime:{rst}  {bold}lean-ctx doctor{rst}  {dim}·{rst}  {bold}lean-ctx gain{rst}");
683    println!();
684    terminal_ui::print_command_box();
685}
686
687#[derive(Debug, Clone, Copy, Default)]
688pub struct SetupOptions {
689    pub non_interactive: bool,
690    pub yes: bool,
691    pub fix: bool,
692    pub json: bool,
693    pub no_auto_approve: bool,
694    pub skip_proxy: bool,
695    pub skip_rules: bool,
696    /// Explicitly request rules injection (overrides config).
697    pub force_inject_rules: bool,
698}
699
700pub fn run_setup_with_options(opts: SetupOptions) -> Result<SetupReport, String> {
701    let _quiet_guard = opts.json.then(|| EnvVarGuard::set("LEAN_CTX_QUIET", "1"));
702    let started_at = Utc::now();
703    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
704    let binary = resolve_portable_binary();
705    let home_str = home.to_string_lossy().to_string();
706
707    let mut steps: Vec<SetupStepReport> = Vec::new();
708
709    // Step: Shell Hook
710    let mut shell_step = SetupStepReport {
711        name: "shell_hook".to_string(),
712        ok: true,
713        items: Vec::new(),
714        warnings: Vec::new(),
715        errors: Vec::new(),
716    };
717    if !opts.non_interactive || opts.yes {
718        if opts.json {
719            crate::cli::cmd_init_quiet(&["--global".to_string()]);
720        } else {
721            crate::cli::cmd_init(&["--global".to_string()]);
722        }
723        crate::shell_hook::install_all(opts.json);
724        #[cfg(not(windows))]
725        {
726            let hook_content = crate::cli::generate_hook_posix(&binary);
727            if crate::shell::is_container() {
728                crate::cli::write_env_sh_for_containers(&hook_content);
729                shell_step.items.push(SetupItem {
730                    name: "env_sh".to_string(),
731                    status: "created".to_string(),
732                    path: Some("~/.lean-ctx/env.sh".to_string()),
733                    note: Some("Docker/CI helper (BASH_ENV / CLAUDE_ENV_FILE)".to_string()),
734                });
735            } else {
736                shell_step.items.push(SetupItem {
737                    name: "env_sh".to_string(),
738                    status: "skipped".to_string(),
739                    path: None,
740                    note: Some("not a container environment".to_string()),
741                });
742            }
743        }
744        shell_step.items.push(SetupItem {
745            name: "init --global".to_string(),
746            status: "ran".to_string(),
747            path: None,
748            note: None,
749        });
750        shell_step.items.push(SetupItem {
751            name: "universal_shell_hook".to_string(),
752            status: "installed".to_string(),
753            path: None,
754            note: Some("~/.zshenv, ~/.bashenv, agent aliases".to_string()),
755        });
756    } else {
757        shell_step
758            .warnings
759            .push("non_interactive_without_yes: shell hook not installed (use --yes)".to_string());
760        shell_step.ok = false;
761        shell_step.items.push(SetupItem {
762            name: "init --global".to_string(),
763            status: "skipped".to_string(),
764            path: None,
765            note: Some("requires --yes in --non-interactive mode".to_string()),
766        });
767    }
768    steps.push(shell_step);
769
770    // Step: Daemon (optional acceleration for CLI routing)
771    let mut daemon_step = SetupStepReport {
772        name: "daemon".to_string(),
773        ok: true,
774        items: Vec::new(),
775        warnings: Vec::new(),
776        errors: Vec::new(),
777    };
778    {
779        let was_running = crate::daemon::is_daemon_running();
780        if was_running {
781            let _ = crate::daemon::stop_daemon();
782            std::thread::sleep(std::time::Duration::from_millis(500));
783        }
784        match crate::daemon::start_daemon(&[]) {
785            Ok(()) => {
786                let action = if was_running { "restarted" } else { "started" };
787                daemon_step.items.push(SetupItem {
788                    name: "serve --daemon".to_string(),
789                    status: action.to_string(),
790                    path: Some(crate::daemon::daemon_addr().display()),
791                    note: Some("CLI commands can route via IPC when running".to_string()),
792                });
793            }
794            Err(e) => {
795                daemon_step
796                    .warnings
797                    .push(format!("daemon start failed (non-fatal): {e}"));
798                daemon_step.items.push(SetupItem {
799                    name: "serve --daemon".to_string(),
800                    status: "skipped".to_string(),
801                    path: None,
802                    note: Some(format!("optional — {e}")),
803                });
804            }
805        }
806    }
807    steps.push(daemon_step);
808
809    // Step: Editor MCP config
810    let mut editor_step = SetupStepReport {
811        name: "editors".to_string(),
812        ok: true,
813        items: Vec::new(),
814        warnings: Vec::new(),
815        errors: Vec::new(),
816    };
817
818    let targets = crate::core::editor_registry::build_targets(&home);
819    for target in &targets {
820        let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
821        if !target.detect_path.exists() {
822            editor_step.items.push(SetupItem {
823                name: target.name.to_string(),
824                status: "not_detected".to_string(),
825                path: Some(short_path),
826                note: None,
827            });
828            continue;
829        }
830
831        let mode = if target.agent_key.is_empty() {
832            HookMode::Mcp
833        } else {
834            recommend_hook_mode(&target.agent_key)
835        };
836
837        let res = crate::core::editor_registry::write_config_with_options(
838            target,
839            &binary,
840            WriteOptions {
841                overwrite_invalid: opts.fix,
842            },
843        );
844        match res {
845            Ok(w) => {
846                let note_parts: Vec<String> = [Some(format!("mode={mode}")), w.note]
847                    .into_iter()
848                    .flatten()
849                    .collect();
850                editor_step.items.push(SetupItem {
851                    name: target.name.to_string(),
852                    status: match w.action {
853                        WriteAction::Created => "created".to_string(),
854                        WriteAction::Updated => "updated".to_string(),
855                        WriteAction::Already => "already".to_string(),
856                    },
857                    path: Some(short_path),
858                    note: Some(note_parts.join("; ")),
859                });
860            }
861            Err(e) => {
862                editor_step.ok = false;
863                editor_step.items.push(SetupItem {
864                    name: target.name.to_string(),
865                    status: "error".to_string(),
866                    path: Some(short_path),
867                    note: Some(e),
868                });
869            }
870        }
871    }
872    steps.push(editor_step);
873
874    // Step: Agent rules — respect config unless explicitly forced or skipped
875    let mut rules_step = SetupStepReport {
876        name: "agent_rules".to_string(),
877        ok: true,
878        items: Vec::new(),
879        warnings: Vec::new(),
880        errors: Vec::new(),
881    };
882    let setup_cfg = crate::core::config::Config::load().setup;
883    let should_inject = if opts.skip_rules {
884        false
885    } else if opts.force_inject_rules {
886        true
887    } else if opts.yes && opts.non_interactive {
888        setup_cfg.should_inject_rules()
889    } else {
890        !opts.skip_rules
891    };
892
893    if should_inject {
894        let rules_result = crate::rules_inject::inject_all_rules(&home);
895        for n in rules_result.injected {
896            rules_step.items.push(SetupItem {
897                name: n,
898                status: "injected".to_string(),
899                path: None,
900                note: None,
901            });
902        }
903        for n in rules_result.updated {
904            rules_step.items.push(SetupItem {
905                name: n,
906                status: "updated".to_string(),
907                path: None,
908                note: None,
909            });
910        }
911        for n in rules_result.already {
912            rules_step.items.push(SetupItem {
913                name: n,
914                status: "already".to_string(),
915                path: None,
916                note: None,
917            });
918        }
919        if !rules_result.backed_up.is_empty() {
920            for bak in &rules_result.backed_up {
921                rules_step.items.push(SetupItem {
922                    name: "backup".to_string(),
923                    status: "created".to_string(),
924                    path: Some(bak.clone()),
925                    note: Some("previous version backed up".to_string()),
926                });
927            }
928        }
929        for e in rules_result.errors {
930            rules_step.ok = false;
931            rules_step.errors.push(e);
932        }
933    } else {
934        let reason = if opts.skip_rules {
935            "--skip-rules flag set"
936        } else {
937            "auto_inject_rules not enabled (run `lean-ctx setup --inject-rules`)"
938        };
939        rules_step.items.push(SetupItem {
940            name: "agent_rules".to_string(),
941            status: "skipped".to_string(),
942            path: None,
943            note: Some(reason.to_string()),
944        });
945    }
946    steps.push(rules_step);
947
948    // Step: Skill files — respect config
949    let mut skill_step = SetupStepReport {
950        name: "skill_files".to_string(),
951        ok: true,
952        items: Vec::new(),
953        warnings: Vec::new(),
954        errors: Vec::new(),
955    };
956    let should_install_skills = if opts.skip_rules {
957        false
958    } else if opts.force_inject_rules {
959        true
960    } else if opts.yes && opts.non_interactive {
961        setup_cfg.should_inject_skills()
962    } else {
963        !opts.skip_rules
964    };
965    if should_install_skills {
966        let skill_results = crate::rules_inject::install_all_skills(&home);
967        for (name, is_new) in &skill_results {
968            skill_step.items.push(SetupItem {
969                name: name.clone(),
970                status: if *is_new { "installed" } else { "already" }.to_string(),
971                path: None,
972                note: Some("SKILL.md".to_string()),
973            });
974        }
975    } else {
976        skill_step.items.push(SetupItem {
977            name: "skill_files".to_string(),
978            status: "skipped".to_string(),
979            path: None,
980            note: Some("auto_inject_skills not enabled".to_string()),
981        });
982    }
983    if !skill_step.items.is_empty() {
984        steps.push(skill_step);
985    }
986
987    // Step: Agent-specific hooks (all detected agents)
988    let mut hooks_step = SetupStepReport {
989        name: "agent_hooks".to_string(),
990        ok: true,
991        items: Vec::new(),
992        warnings: Vec::new(),
993        errors: Vec::new(),
994    };
995    for target in &targets {
996        if !target.detect_path.exists() || target.agent_key.is_empty() {
997            continue;
998        }
999        let mode = recommend_hook_mode(&target.agent_key);
1000        crate::hooks::install_agent_hook_with_mode(&target.agent_key, true, mode);
1001        let mcp_note = match configure_agent_mcp(&target.agent_key) {
1002            Ok(()) => "; MCP config updated".to_string(),
1003            Err(e) => format!("; MCP config skipped: {e}"),
1004        };
1005        hooks_step.items.push(SetupItem {
1006            name: format!("{} hooks", target.name),
1007            status: "installed".to_string(),
1008            path: Some(target.detect_path.to_string_lossy().to_string()),
1009            note: Some(format!(
1010                "mode={mode}; merge-based install/repair (preserves other hooks/plugins){mcp_note}"
1011            )),
1012        });
1013    }
1014    if !hooks_step.items.is_empty() {
1015        steps.push(hooks_step);
1016    }
1017
1018    // Step: Tool profile (set default for new installs)
1019    let mut tool_profile_step = SetupStepReport {
1020        name: "tool_profile".to_string(),
1021        ok: true,
1022        items: Vec::new(),
1023        warnings: Vec::new(),
1024        errors: Vec::new(),
1025    };
1026    {
1027        let cfg = crate::core::config::Config::load();
1028        if cfg.tool_profile.is_none() && std::env::var("LEAN_CTX_TOOL_PROFILE").is_err() {
1029            let default_profile = "standard";
1030            match crate::core::tool_profiles::set_profile_in_config(default_profile) {
1031                Ok(()) => {
1032                    tool_profile_step.items.push(SetupItem {
1033                        name: "tool_profile".to_string(),
1034                        status: "set".to_string(),
1035                        path: None,
1036                        note: Some(format!(
1037                            "default={default_profile} (20 tools; change with: lean-ctx profile power)"
1038                        )),
1039                    });
1040                }
1041                Err(e) => {
1042                    tool_profile_step
1043                        .warnings
1044                        .push(format!("tool_profile: {e}"));
1045                }
1046            }
1047        } else {
1048            let profile = cfg.tool_profile_effective();
1049            tool_profile_step.items.push(SetupItem {
1050                name: "tool_profile".to_string(),
1051                status: "already".to_string(),
1052                path: None,
1053                note: Some(format!("profile={}", profile.as_str())),
1054            });
1055        }
1056    }
1057    steps.push(tool_profile_step);
1058
1059    // Step: Proxy autostart + env vars (respects opt-in)
1060    let mut proxy_step = SetupStepReport {
1061        name: "proxy".to_string(),
1062        ok: true,
1063        items: Vec::new(),
1064        warnings: Vec::new(),
1065        errors: Vec::new(),
1066    };
1067    if opts.skip_proxy {
1068        proxy_step.items.push(SetupItem {
1069            name: "proxy".to_string(),
1070            status: "skipped".to_string(),
1071            path: None,
1072            note: Some("Proxy not enabled (run `lean-ctx proxy enable`)".to_string()),
1073        });
1074    } else {
1075        let proxy_cfg = crate::core::config::Config::load();
1076        if proxy_cfg.proxy_enabled == Some(true) {
1077            let proxy_port = crate::proxy_setup::default_port();
1078            crate::proxy_autostart::install(proxy_port, true);
1079            std::thread::sleep(std::time::Duration::from_millis(500));
1080            crate::proxy_setup::install_proxy_env(&home, proxy_port, opts.json);
1081            proxy_step.items.push(SetupItem {
1082                name: "proxy_autostart".to_string(),
1083                status: "installed".to_string(),
1084                path: None,
1085                note: Some("LaunchAgent/systemd auto-start on login".to_string()),
1086            });
1087            proxy_step.items.push(SetupItem {
1088                name: "proxy_env".to_string(),
1089                status: "configured".to_string(),
1090                path: None,
1091                note: Some("ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_API_BASE_URL".to_string()),
1092            });
1093        } else {
1094            proxy_step.items.push(SetupItem {
1095                name: "proxy".to_string(),
1096                status: "skipped".to_string(),
1097                path: None,
1098                note: Some(
1099                    "Proxy not opted-in (run `lean-ctx proxy enable` to activate)".to_string(),
1100                ),
1101            });
1102        }
1103    }
1104    steps.push(proxy_step);
1105
1106    // Step: Environment / doctor (compact)
1107    let mut env_step = SetupStepReport {
1108        name: "doctor_compact".to_string(),
1109        ok: true,
1110        items: Vec::new(),
1111        warnings: Vec::new(),
1112        errors: Vec::new(),
1113    };
1114    let (passed, total) = crate::doctor::compact_score();
1115    env_step.items.push(SetupItem {
1116        name: "doctor".to_string(),
1117        status: format!("{passed}/{total}"),
1118        path: None,
1119        note: None,
1120    });
1121    if passed != total {
1122        env_step.warnings.push(format!(
1123            "doctor compact not fully passing: {passed}/{total}"
1124        ));
1125    }
1126    steps.push(env_step);
1127
1128    // Project root validation: warn if no root is configured and cwd is broad
1129    {
1130        let has_env_root = std::env::var("LEAN_CTX_PROJECT_ROOT")
1131            .ok()
1132            .is_some_and(|v| !v.is_empty());
1133        let cfg = crate::core::config::Config::load();
1134        let has_cfg_root = cfg.project_root.as_ref().is_some_and(|v| !v.is_empty());
1135        if !has_env_root && !has_cfg_root {
1136            if let Ok(cwd) = std::env::current_dir() {
1137                let is_home = dirs::home_dir().is_some_and(|h| cwd == h);
1138                if is_home {
1139                    let mut root_step = SetupStepReport {
1140                        name: "project_root".to_string(),
1141                        ok: true,
1142                        items: Vec::new(),
1143                        warnings: vec![
1144                            "No project_root configured. Running from $HOME can cause excessive scanning. \
1145                             Set via: lean-ctx config set project_root /path/to/project".to_string()
1146                        ],
1147                        errors: Vec::new(),
1148                    };
1149                    root_step.items.push(SetupItem {
1150                        name: "project_root".to_string(),
1151                        status: "unconfigured".to_string(),
1152                        path: None,
1153                        note: Some(
1154                            "Set LEAN_CTX_PROJECT_ROOT or add project_root to config.toml"
1155                                .to_string(),
1156                        ),
1157                    });
1158                    steps.push(root_step);
1159                }
1160            }
1161        }
1162    }
1163
1164    // Auto-build property graph if inside any recognized project
1165    if let Ok(cwd) = std::env::current_dir() {
1166        let is_project = cwd.join(".git").exists()
1167            || cwd.join("Cargo.toml").exists()
1168            || cwd.join("package.json").exists()
1169            || cwd.join("go.mod").exists();
1170        if is_project {
1171            spawn_index_build_background(&cwd);
1172        }
1173    }
1174
1175    let finished_at = Utc::now();
1176    let success = steps.iter().all(|s| s.ok);
1177    let report = SetupReport {
1178        schema_version: 1,
1179        started_at,
1180        finished_at,
1181        success,
1182        platform: PlatformInfo {
1183            os: std::env::consts::OS.to_string(),
1184            arch: std::env::consts::ARCH.to_string(),
1185        },
1186        steps,
1187        warnings: Vec::new(),
1188        errors: Vec::new(),
1189    };
1190
1191    let path = SetupReport::default_path()?;
1192    let mut content =
1193        serde_json::to_string_pretty(&report).map_err(|e| format!("serialize report: {e}"))?;
1194    content.push('\n');
1195    crate::config_io::write_atomic(&path, &content)?;
1196
1197    Ok(report)
1198}
1199
1200fn spawn_index_build_background(root: &std::path::Path) {
1201    if std::env::var("LEAN_CTX_DISABLED").is_ok()
1202        || matches!(std::env::var("LEAN_CTX_QUIET"), Ok(v) if v.trim() == "1")
1203    {
1204        return;
1205    }
1206    let root_str = crate::core::graph_index::normalize_project_root(&root.to_string_lossy());
1207    if !crate::core::graph_index::is_safe_scan_root_public(&root_str) {
1208        tracing::info!("[setup: skipping background graph build for unsafe root {root_str}]");
1209        return;
1210    }
1211
1212    let binary = resolve_portable_binary();
1213
1214    #[cfg(unix)]
1215    {
1216        let mut cmd = std::process::Command::new("nice");
1217        cmd.args(["-n", "19"]);
1218        if which_ionice_available() {
1219            cmd.arg("ionice").args(["-c", "3"]);
1220        }
1221        cmd.arg(&binary)
1222            .args(["index", "build", "--root"])
1223            .arg(root)
1224            .stdout(std::process::Stdio::null())
1225            .stderr(std::process::Stdio::null())
1226            .stdin(std::process::Stdio::null());
1227        let _ = cmd.spawn();
1228    }
1229
1230    #[cfg(windows)]
1231    {
1232        use std::os::windows::process::CommandExt;
1233        const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
1234        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
1235        let _ = std::process::Command::new(&binary)
1236            .args(["index", "build", "--root"])
1237            .arg(root)
1238            .stdout(std::process::Stdio::null())
1239            .stderr(std::process::Stdio::null())
1240            .stdin(std::process::Stdio::null())
1241            .creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW)
1242            .spawn();
1243    }
1244}
1245
1246#[cfg(unix)]
1247fn which_ionice_available() -> bool {
1248    std::process::Command::new("ionice")
1249        .arg("--version")
1250        .stdout(std::process::Stdio::null())
1251        .stderr(std::process::Stdio::null())
1252        .status()
1253        .is_ok()
1254}
1255
1256/// Result of setting up a single agent with all steps.
1257#[derive(Debug, Default)]
1258pub struct AgentSetupResult {
1259    pub mcp_ok: bool,
1260    pub rules: crate::rules_inject::InjectResult,
1261    pub skill_installed: bool,
1262    pub errors: Vec<String>,
1263}
1264
1265/// Complete per-agent setup: MCP config + global rules + skill + hook.
1266/// Single source of truth — called by both `init --agent` and `setup`.
1267pub fn setup_single_agent(
1268    agent_name: &str,
1269    global: bool,
1270    mode: crate::hooks::HookMode,
1271) -> AgentSetupResult {
1272    let home = dirs::home_dir().unwrap_or_default();
1273    let mut result = AgentSetupResult::default();
1274
1275    crate::hooks::install_agent_hook_with_mode(agent_name, global, mode);
1276
1277    match configure_agent_mcp(agent_name) {
1278        Ok(()) => result.mcp_ok = true,
1279        Err(e) => result.errors.push(format!("MCP config: {e}")),
1280    }
1281
1282    result.rules = crate::rules_inject::inject_rules_for_agent(&home, agent_name);
1283
1284    if let Ok(path) = crate::rules_inject::install_skill_for_agent(&home, agent_name) {
1285        result.skill_installed = path.exists();
1286    }
1287
1288    result
1289}
1290
1291pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
1292    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
1293    let binary = resolve_portable_binary();
1294
1295    let targets = agent_mcp_targets(agent, &home)?;
1296
1297    let mut errors = Vec::new();
1298    for t in &targets {
1299        if let Err(e) = crate::core::editor_registry::write_config_with_options(
1300            t,
1301            &binary,
1302            WriteOptions {
1303                overwrite_invalid: true,
1304            },
1305        ) {
1306            eprintln!(
1307                "\x1b[33m⚠\x1b[0m  Could not configure {}: {}",
1308                t.config_path.display(),
1309                e
1310            );
1311            errors.push(e);
1312        }
1313    }
1314
1315    if agent == "kiro" {
1316        install_kiro_steering(&home);
1317    }
1318
1319    if agent == "vscode" || agent == "copilot" {
1320        if let Err(e) = crate::core::editor_registry::plan_mode::write_vscode_plan_settings() {
1321            eprintln!("\x1b[33m⚠\x1b[0m  VS Code plan mode: {e}");
1322        }
1323    }
1324    if agent == "claude" || agent == "claude-code" {
1325        if let Err(e) =
1326            crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions()
1327        {
1328            eprintln!("\x1b[33m⚠\x1b[0m  Claude Code plan mode: {e}");
1329        }
1330    }
1331
1332    if errors.is_empty() {
1333        Ok(())
1334    } else {
1335        Err(format!(
1336            "{} config(s) could not be written. See warnings above.",
1337            errors.len()
1338        ))
1339    }
1340}
1341
1342fn agent_mcp_targets(agent: &str, home: &std::path::Path) -> Result<Vec<EditorTarget>, String> {
1343    let mut targets = Vec::<EditorTarget>::new();
1344
1345    let push = |targets: &mut Vec<EditorTarget>,
1346                name: &'static str,
1347                config_path: PathBuf,
1348                config_type: ConfigType| {
1349        targets.push(EditorTarget {
1350            name,
1351            agent_key: agent.to_string(),
1352            detect_path: PathBuf::from("/nonexistent"), // not used in direct agent config
1353            config_path,
1354            config_type,
1355        });
1356    };
1357
1358    let pi_cfg = home.join(".pi").join("agent").join("mcp.json");
1359
1360    match agent {
1361        "cursor" => push(
1362            &mut targets,
1363            "Cursor",
1364            home.join(".cursor/mcp.json"),
1365            ConfigType::McpJson,
1366        ),
1367        "claude" | "claude-code" => push(
1368            &mut targets,
1369            "Claude Code",
1370            crate::core::editor_registry::claude_mcp_json_path(home),
1371            ConfigType::McpJson,
1372        ),
1373        "augment" => {
1374            push(
1375                &mut targets,
1376                "Augment CLI",
1377                crate::core::editor_registry::augment_cli_settings_path(home),
1378                ConfigType::McpJson,
1379            );
1380            push(
1381                &mut targets,
1382                "Augment (VS Code)",
1383                crate::core::editor_registry::augment_vscode_mcp_path(home),
1384                ConfigType::AugmentVsCode,
1385            );
1386        }
1387        "windsurf" => push(
1388            &mut targets,
1389            "Windsurf",
1390            home.join(".codeium/windsurf/mcp_config.json"),
1391            ConfigType::McpJson,
1392        ),
1393        "codex" => {
1394            let codex_dir =
1395                crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
1396            push(
1397                &mut targets,
1398                "Codex CLI",
1399                codex_dir.join("config.toml"),
1400                ConfigType::Codex,
1401            );
1402        }
1403        "gemini" => {
1404            push(
1405                &mut targets,
1406                "Gemini CLI",
1407                home.join(".gemini/settings.json"),
1408                ConfigType::GeminiSettings,
1409            );
1410            push(
1411                &mut targets,
1412                "Antigravity IDE",
1413                home.join(".gemini/antigravity/mcp_config.json"),
1414                ConfigType::McpJson,
1415            );
1416            push(
1417                &mut targets,
1418                "Antigravity CLI",
1419                home.join(".gemini/antigravity-cli/mcp_config.json"),
1420                ConfigType::McpJson,
1421            );
1422        }
1423        "antigravity" => push(
1424            &mut targets,
1425            "Antigravity IDE",
1426            home.join(".gemini/antigravity/mcp_config.json"),
1427            ConfigType::McpJson,
1428        ),
1429        "antigravity-cli" => push(
1430            &mut targets,
1431            "Antigravity CLI",
1432            home.join(".gemini/antigravity-cli/mcp_config.json"),
1433            ConfigType::McpJson,
1434        ),
1435        "copilot" => push(
1436            &mut targets,
1437            "Copilot CLI",
1438            home.join(".copilot/mcp-config.json"),
1439            ConfigType::CopilotCli,
1440        ),
1441        "crush" => push(
1442            &mut targets,
1443            "Crush",
1444            home.join(".config/crush/crush.json"),
1445            ConfigType::Crush,
1446        ),
1447        "pi" => push(&mut targets, "Pi Coding Agent", pi_cfg, ConfigType::McpJson),
1448        "qoder" => {
1449            for path in crate::core::editor_registry::qoder_all_mcp_paths(home) {
1450                push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
1451            }
1452        }
1453        "qoderwork" => push(
1454            &mut targets,
1455            "QoderWork",
1456            crate::core::editor_registry::qoderwork_mcp_path(home),
1457            ConfigType::McpJson,
1458        ),
1459        "cline" => push(
1460            &mut targets,
1461            "Cline",
1462            crate::core::editor_registry::cline_mcp_path(),
1463            ConfigType::McpJson,
1464        ),
1465        "roo" => push(
1466            &mut targets,
1467            "Roo Code",
1468            crate::core::editor_registry::roo_mcp_path(),
1469            ConfigType::McpJson,
1470        ),
1471        "kiro" => push(
1472            &mut targets,
1473            "AWS Kiro",
1474            home.join(".kiro/settings/mcp.json"),
1475            ConfigType::McpJson,
1476        ),
1477        "verdent" => push(
1478            &mut targets,
1479            "Verdent",
1480            home.join(".verdent/mcp.json"),
1481            ConfigType::McpJson,
1482        ),
1483        "jetbrains" | "amp" | "openclaw" => {
1484            // Handled by dedicated install hooks (servers[] array / amp.mcpServers / mcp.servers)
1485        }
1486        "qwen" => push(
1487            &mut targets,
1488            "Qwen Code",
1489            home.join(".qwen/settings.json"),
1490            ConfigType::McpJson,
1491        ),
1492        "trae" => push(
1493            &mut targets,
1494            "Trae",
1495            home.join(".trae/mcp.json"),
1496            ConfigType::McpJson,
1497        ),
1498        "amazonq" => push(
1499            &mut targets,
1500            "Amazon Q Developer",
1501            home.join(".aws/amazonq/default.json"),
1502            ConfigType::McpJson,
1503        ),
1504        "opencode" => {
1505            #[cfg(windows)]
1506            let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
1507                std::path::PathBuf::from(appdata)
1508                    .join("opencode")
1509                    .join("opencode.json")
1510            } else {
1511                home.join(".config/opencode/opencode.json")
1512            };
1513            #[cfg(not(windows))]
1514            let opencode_path = home.join(".config/opencode/opencode.json");
1515            push(
1516                &mut targets,
1517                "OpenCode",
1518                opencode_path,
1519                ConfigType::OpenCode,
1520            );
1521        }
1522        "hermes" => push(
1523            &mut targets,
1524            "Hermes Agent",
1525            home.join(".hermes/config.yaml"),
1526            ConfigType::HermesYaml,
1527        ),
1528        "vscode" => push(
1529            &mut targets,
1530            "VS Code",
1531            crate::core::editor_registry::vscode_mcp_path(),
1532            ConfigType::VsCodeMcp,
1533        ),
1534        "zed" => push(
1535            &mut targets,
1536            "Zed",
1537            crate::core::editor_registry::zed_settings_path(home),
1538            ConfigType::Zed,
1539        ),
1540        "aider" => push(
1541            &mut targets,
1542            "Aider",
1543            home.join(".aider/mcp.json"),
1544            ConfigType::McpJson,
1545        ),
1546        "continue" => push(
1547            &mut targets,
1548            "Continue",
1549            home.join(".continue/mcp.json"),
1550            ConfigType::McpJson,
1551        ),
1552        "neovim" => push(
1553            &mut targets,
1554            "Neovim (mcphub.nvim)",
1555            home.join(".config/mcphub/servers.json"),
1556            ConfigType::McpJson,
1557        ),
1558        "emacs" => push(
1559            &mut targets,
1560            "Emacs (mcp.el)",
1561            home.join(".emacs.d/mcp.json"),
1562            ConfigType::McpJson,
1563        ),
1564        "sublime" => push(
1565            &mut targets,
1566            "Sublime Text",
1567            home.join(".config/sublime-text/mcp.json"),
1568            ConfigType::McpJson,
1569        ),
1570        _ => {
1571            return Err(format!("Unknown agent '{agent}'"));
1572        }
1573    }
1574
1575    Ok(targets)
1576}
1577
1578pub fn disable_agent_mcp(agent: &str, overwrite_invalid: bool) -> Result<(), String> {
1579    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
1580
1581    let mut targets = Vec::<EditorTarget>::new();
1582
1583    let push = |targets: &mut Vec<EditorTarget>,
1584                name: &'static str,
1585                config_path: PathBuf,
1586                config_type: ConfigType| {
1587        targets.push(EditorTarget {
1588            name,
1589            agent_key: agent.to_string(),
1590            detect_path: PathBuf::from("/nonexistent"),
1591            config_path,
1592            config_type,
1593        });
1594    };
1595
1596    let pi_cfg = home.join(".pi").join("agent").join("mcp.json");
1597
1598    match agent {
1599        "cursor" => push(
1600            &mut targets,
1601            "Cursor",
1602            home.join(".cursor/mcp.json"),
1603            ConfigType::McpJson,
1604        ),
1605        "claude" | "claude-code" => push(
1606            &mut targets,
1607            "Claude Code",
1608            crate::core::editor_registry::claude_mcp_json_path(&home),
1609            ConfigType::McpJson,
1610        ),
1611        "augment" => {
1612            push(
1613                &mut targets,
1614                "Augment CLI",
1615                crate::core::editor_registry::augment_cli_settings_path(&home),
1616                ConfigType::McpJson,
1617            );
1618            push(
1619                &mut targets,
1620                "Augment (VS Code)",
1621                crate::core::editor_registry::augment_vscode_mcp_path(&home),
1622                ConfigType::AugmentVsCode,
1623            );
1624        }
1625        "windsurf" => push(
1626            &mut targets,
1627            "Windsurf",
1628            home.join(".codeium/windsurf/mcp_config.json"),
1629            ConfigType::McpJson,
1630        ),
1631        "codex" => {
1632            let codex_dir =
1633                crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"));
1634            push(
1635                &mut targets,
1636                "Codex CLI",
1637                codex_dir.join("config.toml"),
1638                ConfigType::Codex,
1639            );
1640        }
1641        "gemini" => {
1642            push(
1643                &mut targets,
1644                "Gemini CLI",
1645                home.join(".gemini/settings.json"),
1646                ConfigType::GeminiSettings,
1647            );
1648            push(
1649                &mut targets,
1650                "Antigravity IDE",
1651                home.join(".gemini/antigravity/mcp_config.json"),
1652                ConfigType::McpJson,
1653            );
1654            push(
1655                &mut targets,
1656                "Antigravity CLI",
1657                home.join(".gemini/antigravity-cli/mcp_config.json"),
1658                ConfigType::McpJson,
1659            );
1660        }
1661        "antigravity" => push(
1662            &mut targets,
1663            "Antigravity IDE",
1664            home.join(".gemini/antigravity/mcp_config.json"),
1665            ConfigType::McpJson,
1666        ),
1667        "antigravity-cli" => push(
1668            &mut targets,
1669            "Antigravity CLI",
1670            home.join(".gemini/antigravity-cli/mcp_config.json"),
1671            ConfigType::McpJson,
1672        ),
1673        "copilot" => push(
1674            &mut targets,
1675            "Copilot CLI",
1676            home.join(".copilot/mcp-config.json"),
1677            ConfigType::CopilotCli,
1678        ),
1679        "crush" => push(
1680            &mut targets,
1681            "Crush",
1682            home.join(".config/crush/crush.json"),
1683            ConfigType::Crush,
1684        ),
1685        "pi" => push(&mut targets, "Pi Coding Agent", pi_cfg, ConfigType::McpJson),
1686        "qoder" => {
1687            for path in crate::core::editor_registry::qoder_all_mcp_paths(&home) {
1688                push(&mut targets, "Qoder", path, ConfigType::QoderSettings);
1689            }
1690        }
1691        "qoderwork" => push(
1692            &mut targets,
1693            "QoderWork",
1694            crate::core::editor_registry::qoderwork_mcp_path(&home),
1695            ConfigType::McpJson,
1696        ),
1697        "cline" => push(
1698            &mut targets,
1699            "Cline",
1700            crate::core::editor_registry::cline_mcp_path(),
1701            ConfigType::McpJson,
1702        ),
1703        "roo" => push(
1704            &mut targets,
1705            "Roo Code",
1706            crate::core::editor_registry::roo_mcp_path(),
1707            ConfigType::McpJson,
1708        ),
1709        "kiro" => push(
1710            &mut targets,
1711            "AWS Kiro",
1712            home.join(".kiro/settings/mcp.json"),
1713            ConfigType::McpJson,
1714        ),
1715        "verdent" => push(
1716            &mut targets,
1717            "Verdent",
1718            home.join(".verdent/mcp.json"),
1719            ConfigType::McpJson,
1720        ),
1721        "jetbrains" | "amp" | "openclaw" => {
1722            // Not supported for disable via this helper.
1723        }
1724        "qwen" => push(
1725            &mut targets,
1726            "Qwen Code",
1727            home.join(".qwen/settings.json"),
1728            ConfigType::McpJson,
1729        ),
1730        "trae" => push(
1731            &mut targets,
1732            "Trae",
1733            home.join(".trae/mcp.json"),
1734            ConfigType::McpJson,
1735        ),
1736        "amazonq" => push(
1737            &mut targets,
1738            "Amazon Q Developer",
1739            home.join(".aws/amazonq/default.json"),
1740            ConfigType::McpJson,
1741        ),
1742        "opencode" => {
1743            #[cfg(windows)]
1744            let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
1745                std::path::PathBuf::from(appdata)
1746                    .join("opencode")
1747                    .join("opencode.json")
1748            } else {
1749                home.join(".config/opencode/opencode.json")
1750            };
1751            #[cfg(not(windows))]
1752            let opencode_path = home.join(".config/opencode/opencode.json");
1753            push(
1754                &mut targets,
1755                "OpenCode",
1756                opencode_path,
1757                ConfigType::OpenCode,
1758            );
1759        }
1760        "hermes" => push(
1761            &mut targets,
1762            "Hermes Agent",
1763            home.join(".hermes/config.yaml"),
1764            ConfigType::HermesYaml,
1765        ),
1766        "vscode" => push(
1767            &mut targets,
1768            "VS Code",
1769            crate::core::editor_registry::vscode_mcp_path(),
1770            ConfigType::VsCodeMcp,
1771        ),
1772        "zed" => push(
1773            &mut targets,
1774            "Zed",
1775            crate::core::editor_registry::zed_settings_path(&home),
1776            ConfigType::Zed,
1777        ),
1778        "aider" => push(
1779            &mut targets,
1780            "Aider",
1781            home.join(".aider/mcp.json"),
1782            ConfigType::McpJson,
1783        ),
1784        "continue" => push(
1785            &mut targets,
1786            "Continue",
1787            home.join(".continue/mcp.json"),
1788            ConfigType::McpJson,
1789        ),
1790        "neovim" => push(
1791            &mut targets,
1792            "Neovim (mcphub.nvim)",
1793            home.join(".config/mcphub/servers.json"),
1794            ConfigType::McpJson,
1795        ),
1796        "emacs" => push(
1797            &mut targets,
1798            "Emacs (mcp.el)",
1799            home.join(".emacs.d/mcp.json"),
1800            ConfigType::McpJson,
1801        ),
1802        "sublime" => push(
1803            &mut targets,
1804            "Sublime Text",
1805            home.join(".config/sublime-text/mcp.json"),
1806            ConfigType::McpJson,
1807        ),
1808        _ => {
1809            return Err(format!("Unknown agent '{agent}'"));
1810        }
1811    }
1812
1813    for t in &targets {
1814        crate::core::editor_registry::remove_lean_ctx_server(
1815            t,
1816            WriteOptions { overwrite_invalid },
1817        )?;
1818    }
1819
1820    Ok(())
1821}
1822
1823pub fn install_skill_files(home: &std::path::Path) -> Vec<(String, bool)> {
1824    crate::rules_inject::install_all_skills(home)
1825}
1826
1827fn install_kiro_steering(home: &std::path::Path) {
1828    let cwd = std::env::current_dir().unwrap_or_else(|_| home.to_path_buf());
1829    let steering_dir = cwd.join(".kiro").join("steering");
1830    let steering_file = steering_dir.join("lean-ctx.md");
1831
1832    if steering_file.exists()
1833        && std::fs::read_to_string(&steering_file)
1834            .unwrap_or_default()
1835            .contains("lean-ctx")
1836    {
1837        println!("  Kiro steering file already exists at .kiro/steering/lean-ctx.md");
1838        return;
1839    }
1840
1841    let _ = std::fs::create_dir_all(&steering_dir);
1842    let _ = std::fs::write(&steering_file, crate::hooks::KIRO_STEERING_TEMPLATE);
1843    println!("  \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)");
1844}
1845
1846fn configure_plan_mode_settings(newly_configured: &[&str], already_configured: &[&str]) {
1847    use crate::terminal_ui;
1848
1849    let all_configured: Vec<&str> = newly_configured
1850        .iter()
1851        .chain(already_configured.iter())
1852        .copied()
1853        .collect();
1854
1855    let has_vscode = all_configured.contains(&"VS Code");
1856    let has_claude = all_configured.contains(&"Claude Code");
1857
1858    if !has_vscode && !has_claude {
1859        return;
1860    }
1861
1862    if has_vscode {
1863        match crate::core::editor_registry::plan_mode::write_vscode_plan_settings() {
1864            Ok(r) if r.action == WriteAction::Already => {
1865                terminal_ui::print_status_ok(
1866                    "VS Code            \x1b[2mplan mode already configured\x1b[0m",
1867                );
1868            }
1869            Ok(_) => {
1870                terminal_ui::print_status_new(
1871                    "VS Code            \x1b[2mplan mode tools configured\x1b[0m",
1872                );
1873            }
1874            Err(e) => {
1875                terminal_ui::print_status_warn(&format!("VS Code plan mode: {e}"));
1876            }
1877        }
1878    }
1879
1880    if has_claude {
1881        match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
1882            Ok(r) if r.action == WriteAction::Already => {
1883                terminal_ui::print_status_ok(
1884                    "Claude Code        \x1b[2mplan mode permissions present\x1b[0m",
1885                );
1886            }
1887            Ok(_) => {
1888                terminal_ui::print_status_new(
1889                    "Claude Code        \x1b[2mplan mode permissions added\x1b[0m",
1890                );
1891            }
1892            Err(e) => {
1893                terminal_ui::print_status_warn(&format!("Claude Code plan mode: {e}"));
1894            }
1895        }
1896    }
1897}
1898
1899fn shorten_path(path: &str, home: &str) -> String {
1900    if let Some(stripped) = path.strip_prefix(home) {
1901        format!("~{stripped}")
1902    } else {
1903        path.to_string()
1904    }
1905}
1906
1907fn upsert_toml_key(content: &mut String, key: &str, value: &str) {
1908    let pattern = format!("{key} = ");
1909    if let Some(start) = content.find(&pattern) {
1910        let line_end = content[start..]
1911            .find('\n')
1912            .map_or(content.len(), |p| start + p);
1913        content.replace_range(start..line_end, &format!("{key} = \"{value}\""));
1914    } else {
1915        if !content.is_empty() && !content.ends_with('\n') {
1916            content.push('\n');
1917        }
1918        content.push_str(&format!("{key} = \"{value}\"\n"));
1919    }
1920}
1921
1922fn remove_toml_key(content: &mut String, key: &str) {
1923    let pattern = format!("{key} = ");
1924    if let Some(start) = content.find(&pattern) {
1925        let line_end = content[start..]
1926            .find('\n')
1927            .map_or(content.len(), |p| start + p + 1);
1928        content.replace_range(start..line_end, "");
1929    }
1930}
1931
1932fn configure_tool_profile() {
1933    use crate::terminal_ui;
1934    use std::io::Write;
1935
1936    let cfg = crate::core::config::Config::load();
1937    let current = cfg.tool_profile_effective();
1938
1939    if !matches!(current, crate::core::tool_profiles::ToolProfile::Power)
1940        && cfg.tool_profile.is_some()
1941    {
1942        terminal_ui::print_status_ok(&format!(
1943            "Tool profile: {} ({} tools)",
1944            current.as_str(),
1945            current.tool_count()
1946        ));
1947        return;
1948    }
1949
1950    let dim = "\x1b[2m";
1951    let bold = "\x1b[1m";
1952    let cyan = "\x1b[36m";
1953    let rst = "\x1b[0m";
1954
1955    let registry_count = crate::server::registry::tool_count();
1956
1957    println!("  {dim}Control how many MCP tools your AI agent sees.{rst}");
1958    println!("  {dim}Fewer tools = less context overhead, faster agent responses.{rst}");
1959    println!();
1960    println!(
1961        "  {cyan}minimal{rst}   — 5 tools   {dim}(ctx_read, ctx_shell, ctx_search, ctx_tree, ctx_session){rst}"
1962    );
1963    println!("  {cyan}standard{rst}  — 20 tools  {dim}(balanced set for most workflows){rst}");
1964    println!(
1965        "  {cyan}power{rst}     — {registry_count} tools  {dim}(everything, for power users){rst}"
1966    );
1967    println!();
1968    print!("  Tool profile? {bold}[minimal/standard/power]{rst} {dim}(default: standard){rst} ");
1969    std::io::stdout().flush().ok();
1970
1971    let mut profile_input = String::new();
1972    let profile_name = if std::io::stdin().read_line(&mut profile_input).is_ok() {
1973        let trimmed = profile_input.trim().to_lowercase();
1974        match trimmed.as_str() {
1975            "minimal" | "min" => "minimal",
1976            "power" | "full" | "all" => "power",
1977            _ => "standard",
1978        }
1979    } else {
1980        "standard"
1981    };
1982
1983    match crate::core::tool_profiles::set_profile_in_config(profile_name) {
1984        Ok(()) => {
1985            let profile = crate::core::tool_profiles::ToolProfile::parse(profile_name)
1986                .unwrap_or(crate::core::tool_profiles::ToolProfile::Standard);
1987            let count = match &profile {
1988                crate::core::tool_profiles::ToolProfile::Power => registry_count,
1989                other => other.tool_count(),
1990            };
1991            terminal_ui::print_status_ok(&format!("Tool profile: {profile_name} ({count} tools)"));
1992        }
1993        Err(e) => {
1994            terminal_ui::print_status_warn(&format!("Could not save tool profile: {e}"));
1995        }
1996    }
1997}
1998
1999fn configure_premium_features(home: &std::path::Path) {
2000    use crate::terminal_ui;
2001    use std::io::Write;
2002
2003    let config_dir = crate::core::data_dir::lean_ctx_data_dir()
2004        .unwrap_or_else(|_| home.join(".config/lean-ctx"));
2005    let _ = std::fs::create_dir_all(&config_dir);
2006    let config_path = config_dir.join("config.toml");
2007    let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
2008
2009    let dim = "\x1b[2m";
2010    let bold = "\x1b[1m";
2011    let cyan = "\x1b[36m";
2012    let rst = "\x1b[0m";
2013
2014    // Unified Compression Level (replaces terse_agent + output_density)
2015    println!("\n  {bold}Compression Level{rst} {dim}(controls all token optimization layers){rst}");
2016    println!("  {dim}Applies to tool output, agent prompts, and protocol mode.{rst}");
2017    println!();
2018    println!("  {cyan}off{rst}      — No compression (full verbose output)");
2019    println!("  {cyan}lite{rst}     — Light: concise output, basic terse filtering {dim}(~25% savings){rst}");
2020    println!("  {cyan}standard{rst} — Dense output + compact protocol + pattern-aware {dim}(~45% savings){rst}");
2021    println!("  {cyan}max{rst}      — Expert mode: TDD protocol, all layers active {dim}(~65% savings){rst}");
2022    println!();
2023    print!("  Compression level? {bold}[off/lite/standard/max]{rst} {dim}(default: off){rst} ");
2024    std::io::stdout().flush().ok();
2025
2026    let mut level_input = String::new();
2027    let level = if std::io::stdin().read_line(&mut level_input).is_ok() {
2028        match level_input.trim().to_lowercase().as_str() {
2029            "lite" => "lite",
2030            "standard" | "std" => "standard",
2031            "max" => "max",
2032            _ => "off",
2033        }
2034    } else {
2035        "off"
2036    };
2037
2038    let effective_level = if level != "off" {
2039        upsert_toml_key(&mut config_content, "compression_level", level);
2040        remove_toml_key(&mut config_content, "terse_agent");
2041        remove_toml_key(&mut config_content, "output_density");
2042        terminal_ui::print_status_ok(&format!("Compression: {level}"));
2043        crate::core::config::CompressionLevel::from_str_label(level)
2044    } else if config_content.contains("compression_level") {
2045        upsert_toml_key(&mut config_content, "compression_level", "off");
2046        terminal_ui::print_status_ok("Compression: off");
2047        Some(crate::core::config::CompressionLevel::Off)
2048    } else {
2049        terminal_ui::print_status_skip(
2050            "Compression: off (change later with: lean-ctx compression <level>)",
2051        );
2052        Some(crate::core::config::CompressionLevel::Off)
2053    };
2054
2055    if let Some(lvl) = effective_level {
2056        let n = crate::core::terse::rules_inject::inject(&lvl);
2057        if n > 0 {
2058            terminal_ui::print_status_ok(&format!(
2059                "Updated {n} rules file(s) with compression prompt"
2060            ));
2061        }
2062    }
2063
2064    // Tool Result Archive (unchanged)
2065    println!(
2066        "\n  {bold}Tool Result Archive{rst} {dim}(zero-loss: large outputs archived, retrievable via ctx_expand){rst}"
2067    );
2068    print!("  Enable auto-archive? {bold}[Y/n]{rst} ");
2069    std::io::stdout().flush().ok();
2070
2071    let mut archive_input = String::new();
2072    let archive_on = if std::io::stdin().read_line(&mut archive_input).is_ok() {
2073        let a = archive_input.trim().to_lowercase();
2074        a.is_empty() || a == "y" || a == "yes"
2075    } else {
2076        true
2077    };
2078
2079    if archive_on && !config_content.contains("[archive]") {
2080        if !config_content.is_empty() && !config_content.ends_with('\n') {
2081            config_content.push('\n');
2082        }
2083        config_content.push_str("\n[archive]\nenabled = true\n");
2084        terminal_ui::print_status_ok("Tool Result Archive: enabled");
2085    } else if !archive_on {
2086        terminal_ui::print_status_skip("Archive: off (enable later in config.toml)");
2087    }
2088
2089    let _ = crate::config_io::write_atomic_with_backup(&config_path, &config_content);
2090}
2091
2092#[cfg(all(test, target_os = "macos"))]
2093mod tests {
2094    use super::*;
2095
2096    #[test]
2097    #[cfg(target_os = "macos")]
2098    fn qoder_agent_targets_include_all_macos_mcp_locations() {
2099        let home = std::path::Path::new("/Users/tester");
2100        let targets = agent_mcp_targets("qoder", home).unwrap();
2101        let paths: Vec<_> = targets.iter().map(|t| t.config_path.as_path()).collect();
2102
2103        assert_eq!(
2104            paths,
2105            vec![
2106                home.join(".qoder/mcp.json").as_path(),
2107                home.join("Library/Application Support/Qoder/User/mcp.json")
2108                    .as_path(),
2109                home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json")
2110                    .as_path(),
2111            ]
2112        );
2113        assert!(targets
2114            .iter()
2115            .all(|t| t.config_type == ConfigType::QoderSettings));
2116    }
2117}