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 chrono::Utc;
7use std::ffi::OsString;
8
9pub fn claude_config_json_path(home: &std::path::Path) -> PathBuf {
10    crate::core::editor_registry::claude_mcp_json_path(home)
11}
12
13pub fn claude_config_dir(home: &std::path::Path) -> PathBuf {
14    crate::core::editor_registry::claude_state_dir(home)
15}
16
17pub(crate) struct EnvVarGuard {
18    key: &'static str,
19    previous: Option<OsString>,
20}
21
22impl EnvVarGuard {
23    pub(crate) fn set(key: &'static str, value: &str) -> Self {
24        let previous = std::env::var_os(key);
25        std::env::set_var(key, value);
26        Self { key, previous }
27    }
28}
29
30impl Drop for EnvVarGuard {
31    fn drop(&mut self) {
32        if let Some(previous) = &self.previous {
33            std::env::set_var(self.key, previous);
34        } else {
35            std::env::remove_var(self.key);
36        }
37    }
38}
39
40pub fn run_setup() {
41    use crate::terminal_ui;
42
43    if crate::shell::is_non_interactive() {
44        eprintln!("Non-interactive terminal detected (no TTY on stdin).");
45        eprintln!("Running in non-interactive mode (equivalent to: lean-ctx setup --non-interactive --yes)");
46        eprintln!();
47        let opts = SetupOptions {
48            non_interactive: true,
49            yes: true,
50            fix: false,
51            json: false,
52        };
53        match run_setup_with_options(opts) {
54            Ok(report) => {
55                if !report.warnings.is_empty() {
56                    for w in &report.warnings {
57                        tracing::warn!("{w}");
58                    }
59                }
60            }
61            Err(e) => tracing::error!("Setup error: {e}"),
62        }
63        return;
64    }
65
66    let Some(home) = dirs::home_dir() else {
67        tracing::error!("Cannot determine home directory");
68        std::process::exit(1);
69    };
70
71    let binary = resolve_portable_binary();
72
73    let home_str = home.to_string_lossy().to_string();
74
75    terminal_ui::print_setup_header();
76
77    // Step 1: Shell hook (legacy aliases + universal shell hook)
78    terminal_ui::print_step_header(1, 7, "Shell Hook");
79    crate::cli::cmd_init(&["--global".to_string()]);
80    crate::shell_hook::install_all(false);
81
82    // Step 2: Editor auto-detection + configuration
83    terminal_ui::print_step_header(2, 7, "AI Tool Detection");
84
85    let targets = crate::core::editor_registry::build_targets(&home);
86    let mut newly_configured: Vec<&str> = Vec::new();
87    let mut already_configured: Vec<&str> = Vec::new();
88    let mut not_installed: Vec<&str> = Vec::new();
89    let mut errors: Vec<&str> = Vec::new();
90
91    for target in &targets {
92        let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
93
94        if !target.detect_path.exists() {
95            not_installed.push(target.name);
96            continue;
97        }
98
99        match crate::core::editor_registry::write_config_with_options(
100            target,
101            &binary,
102            WriteOptions {
103                overwrite_invalid: false,
104            },
105        ) {
106            Ok(res) if res.action == WriteAction::Already => {
107                terminal_ui::print_status_ok(&format!(
108                    "{:<20} \x1b[2m{short_path}\x1b[0m",
109                    target.name
110                ));
111                already_configured.push(target.name);
112            }
113            Ok(_) => {
114                terminal_ui::print_status_new(&format!(
115                    "{:<20} \x1b[2m{short_path}\x1b[0m",
116                    target.name
117                ));
118                newly_configured.push(target.name);
119            }
120            Err(e) => {
121                terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
122                errors.push(target.name);
123            }
124        }
125    }
126
127    let total_ok = newly_configured.len() + already_configured.len();
128    if total_ok == 0 && errors.is_empty() {
129        terminal_ui::print_status_warn(
130            "No AI tools detected. Install one and re-run: lean-ctx setup",
131        );
132    }
133
134    if !not_installed.is_empty() {
135        println!(
136            "  \x1b[2m○ {} not detected: {}\x1b[0m",
137            not_installed.len(),
138            not_installed.join(", ")
139        );
140    }
141
142    // Step 3: Agent rules injection
143    terminal_ui::print_step_header(3, 7, "Agent Rules");
144    let rules_result = crate::rules_inject::inject_all_rules(&home);
145    for name in &rules_result.injected {
146        terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
147    }
148    for name in &rules_result.updated {
149        terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
150    }
151    for name in &rules_result.already {
152        terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
153    }
154    for err in &rules_result.errors {
155        terminal_ui::print_status_warn(err);
156    }
157    if rules_result.injected.is_empty()
158        && rules_result.updated.is_empty()
159        && rules_result.already.is_empty()
160        && rules_result.errors.is_empty()
161    {
162        terminal_ui::print_status_skip("No agent rules needed");
163    }
164
165    // Legacy agent hooks
166    for target in &targets {
167        if !target.detect_path.exists() || target.agent_key.is_empty() {
168            continue;
169        }
170        crate::hooks::install_agent_hook(&target.agent_key, true);
171    }
172
173    // Step 4: API Proxy configuration
174    terminal_ui::print_step_header(4, 7, "API Proxy");
175    crate::proxy_setup::install_proxy_env(&home, crate::proxy_setup::default_port(), false);
176    println!();
177    println!("  \x1b[2mStart proxy for maximum token savings:\x1b[0m");
178    println!("    \x1b[1mlean-ctx proxy start\x1b[0m");
179    println!("  \x1b[2mEnable autostart:\x1b[0m");
180    println!("    \x1b[1mlean-ctx proxy start --autostart\x1b[0m");
181
182    // Step 5: Data directory + diagnostics
183    terminal_ui::print_step_header(5, 7, "Environment Check");
184    let lean_dir = home.join(".lean-ctx");
185    if lean_dir.exists() {
186        terminal_ui::print_status_ok("~/.lean-ctx/ ready");
187    } else {
188        let _ = std::fs::create_dir_all(&lean_dir);
189        terminal_ui::print_status_new("Created ~/.lean-ctx/");
190    }
191    crate::doctor::run_compact();
192
193    // Step 6: Data sharing
194    terminal_ui::print_step_header(6, 7, "Help Improve lean-ctx");
195    println!("  Share anonymous compression stats to make lean-ctx better.");
196    println!("  \x1b[1mNo code, no file names, no personal data — ever.\x1b[0m");
197    println!();
198    print!("  Enable anonymous data sharing? \x1b[1m[y/N]\x1b[0m ");
199    use std::io::Write;
200    std::io::stdout().flush().ok();
201
202    let mut input = String::new();
203    let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
204        let answer = input.trim().to_lowercase();
205        answer == "y" || answer == "yes"
206    } else {
207        false
208    };
209
210    if contribute {
211        let config_dir = home.join(".lean-ctx");
212        let _ = std::fs::create_dir_all(&config_dir);
213        let config_path = config_dir.join("config.toml");
214        let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
215        if !config_content.contains("[cloud]") {
216            if !config_content.is_empty() && !config_content.ends_with('\n') {
217                config_content.push('\n');
218            }
219            config_content.push_str("\n[cloud]\ncontribute_enabled = true\n");
220            let _ = std::fs::write(&config_path, config_content);
221        }
222        terminal_ui::print_status_ok("Enabled — thank you!");
223    } else {
224        terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx config");
225    }
226
227    // Step 7: Premium Features Configuration
228    terminal_ui::print_step_header(7, 7, "Premium Features");
229    configure_premium_features(&home);
230
231    // Summary
232    println!();
233    println!(
234        "  \x1b[1;32m✓ Setup complete!\x1b[0m  \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
235        newly_configured.len(),
236        already_configured.len(),
237        not_installed.len()
238    );
239
240    if !errors.is_empty() {
241        println!(
242            "  \x1b[33m⚠ {} error{}: {}\x1b[0m",
243            errors.len(),
244            if errors.len() == 1 { "" } else { "s" },
245            errors.join(", ")
246        );
247    }
248
249    // Next steps
250    let shell = std::env::var("SHELL").unwrap_or_default();
251    let source_cmd = if shell.contains("zsh") {
252        "source ~/.zshrc"
253    } else if shell.contains("fish") {
254        "source ~/.config/fish/config.fish"
255    } else if shell.contains("bash") {
256        "source ~/.bashrc"
257    } else {
258        "Restart your shell"
259    };
260
261    let dim = "\x1b[2m";
262    let bold = "\x1b[1m";
263    let cyan = "\x1b[36m";
264    let yellow = "\x1b[33m";
265    let rst = "\x1b[0m";
266
267    println!();
268    println!("  {bold}Next steps:{rst}");
269    println!();
270    println!("  {cyan}1.{rst} Reload your shell:");
271    println!("     {bold}{source_cmd}{rst}");
272    println!();
273
274    let mut tools_to_restart: Vec<String> = newly_configured
275        .iter()
276        .map(std::string::ToString::to_string)
277        .collect();
278    for name in rules_result
279        .injected
280        .iter()
281        .chain(rules_result.updated.iter())
282    {
283        if !tools_to_restart.iter().any(|t| t == name) {
284            tools_to_restart.push(name.clone());
285        }
286    }
287
288    if !tools_to_restart.is_empty() {
289        println!("  {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
290        println!("     {bold}{}{rst}", tools_to_restart.join(", "));
291        println!(
292            "     {dim}The MCP connection must be re-established for changes to take effect.{rst}"
293        );
294        println!("     {dim}Close and re-open the application completely.{rst}");
295    } else if !already_configured.is_empty() {
296        println!(
297            "  {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
298        );
299    }
300
301    println!();
302    println!(
303        "  {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
304    );
305    println!("  {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
306
307    // Logo + commands
308    println!();
309    terminal_ui::print_logo_animated();
310    terminal_ui::print_command_box();
311}
312
313#[derive(Debug, Clone, Copy, Default)]
314pub struct SetupOptions {
315    pub non_interactive: bool,
316    pub yes: bool,
317    pub fix: bool,
318    pub json: bool,
319}
320
321pub fn run_setup_with_options(opts: SetupOptions) -> Result<SetupReport, String> {
322    let _quiet_guard = opts.json.then(|| EnvVarGuard::set("LEAN_CTX_QUIET", "1"));
323    let started_at = Utc::now();
324    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
325    let binary = resolve_portable_binary();
326    let home_str = home.to_string_lossy().to_string();
327
328    let mut steps: Vec<SetupStepReport> = Vec::new();
329
330    // Step: Shell Hook
331    let mut shell_step = SetupStepReport {
332        name: "shell_hook".to_string(),
333        ok: true,
334        items: Vec::new(),
335        warnings: Vec::new(),
336        errors: Vec::new(),
337    };
338    if !opts.non_interactive || opts.yes {
339        if opts.json {
340            crate::cli::cmd_init_quiet(&["--global".to_string()]);
341        } else {
342            crate::cli::cmd_init(&["--global".to_string()]);
343        }
344        crate::shell_hook::install_all(opts.json);
345        shell_step.items.push(SetupItem {
346            name: "init --global".to_string(),
347            status: "ran".to_string(),
348            path: None,
349            note: None,
350        });
351        shell_step.items.push(SetupItem {
352            name: "universal_shell_hook".to_string(),
353            status: "installed".to_string(),
354            path: None,
355            note: Some("~/.zshenv, ~/.bashenv, agent aliases".to_string()),
356        });
357    } else {
358        shell_step
359            .warnings
360            .push("non_interactive_without_yes: shell hook not installed (use --yes)".to_string());
361        shell_step.ok = false;
362        shell_step.items.push(SetupItem {
363            name: "init --global".to_string(),
364            status: "skipped".to_string(),
365            path: None,
366            note: Some("requires --yes in --non-interactive mode".to_string()),
367        });
368    }
369    steps.push(shell_step);
370
371    // Step: Editor MCP config
372    let mut editor_step = SetupStepReport {
373        name: "editors".to_string(),
374        ok: true,
375        items: Vec::new(),
376        warnings: Vec::new(),
377        errors: Vec::new(),
378    };
379
380    let targets = crate::core::editor_registry::build_targets(&home);
381    for target in &targets {
382        let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
383        if !target.detect_path.exists() {
384            editor_step.items.push(SetupItem {
385                name: target.name.to_string(),
386                status: "not_detected".to_string(),
387                path: Some(short_path),
388                note: None,
389            });
390            continue;
391        }
392
393        let res = crate::core::editor_registry::write_config_with_options(
394            target,
395            &binary,
396            WriteOptions {
397                overwrite_invalid: opts.fix,
398            },
399        );
400        match res {
401            Ok(w) => {
402                editor_step.items.push(SetupItem {
403                    name: target.name.to_string(),
404                    status: match w.action {
405                        WriteAction::Created => "created".to_string(),
406                        WriteAction::Updated => "updated".to_string(),
407                        WriteAction::Already => "already".to_string(),
408                    },
409                    path: Some(short_path),
410                    note: w.note,
411                });
412            }
413            Err(e) => {
414                editor_step.ok = false;
415                editor_step.items.push(SetupItem {
416                    name: target.name.to_string(),
417                    status: "error".to_string(),
418                    path: Some(short_path),
419                    note: Some(e),
420                });
421            }
422        }
423    }
424    steps.push(editor_step);
425
426    // Step: Agent rules
427    let mut rules_step = SetupStepReport {
428        name: "agent_rules".to_string(),
429        ok: true,
430        items: Vec::new(),
431        warnings: Vec::new(),
432        errors: Vec::new(),
433    };
434    let rules_result = crate::rules_inject::inject_all_rules(&home);
435    for n in rules_result.injected {
436        rules_step.items.push(SetupItem {
437            name: n,
438            status: "injected".to_string(),
439            path: None,
440            note: None,
441        });
442    }
443    for n in rules_result.updated {
444        rules_step.items.push(SetupItem {
445            name: n,
446            status: "updated".to_string(),
447            path: None,
448            note: None,
449        });
450    }
451    for n in rules_result.already {
452        rules_step.items.push(SetupItem {
453            name: n,
454            status: "already".to_string(),
455            path: None,
456            note: None,
457        });
458    }
459    for e in rules_result.errors {
460        rules_step.ok = false;
461        rules_step.errors.push(e);
462    }
463    steps.push(rules_step);
464
465    // Step: Agent-specific hooks (Codex, Cursor)
466    let mut hooks_step = SetupStepReport {
467        name: "agent_hooks".to_string(),
468        ok: true,
469        items: Vec::new(),
470        warnings: Vec::new(),
471        errors: Vec::new(),
472    };
473    for target in &targets {
474        if !target.detect_path.exists() {
475            continue;
476        }
477        match target.agent_key.as_str() {
478            "codex" => {
479                crate::hooks::agents::install_codex_hook();
480                hooks_step.items.push(SetupItem {
481                    name: "Codex integration".to_string(),
482                    status: "installed".to_string(),
483                    path: Some("~/.codex/".to_string()),
484                    note: Some(
485                        "Installs AGENTS/MCP guidance and Codex-compatible SessionStart/PreToolUse hooks."
486                            .to_string(),
487                    ),
488                });
489            }
490            "cursor" => {
491                let hooks_path = home.join(".cursor/hooks.json");
492                if !hooks_path.exists() {
493                    crate::hooks::agents::install_cursor_hook(true);
494                    hooks_step.items.push(SetupItem {
495                        name: "Cursor hooks".to_string(),
496                        status: "installed".to_string(),
497                        path: Some("~/.cursor/hooks.json".to_string()),
498                        note: None,
499                    });
500                }
501            }
502            _ => {}
503        }
504    }
505    if !hooks_step.items.is_empty() {
506        steps.push(hooks_step);
507    }
508
509    // Step: Proxy env vars
510    let mut proxy_step = SetupStepReport {
511        name: "proxy_env".to_string(),
512        ok: true,
513        items: Vec::new(),
514        warnings: Vec::new(),
515        errors: Vec::new(),
516    };
517    crate::proxy_setup::install_proxy_env(&home, crate::proxy_setup::default_port(), opts.json);
518    proxy_step.items.push(SetupItem {
519        name: "proxy_env".to_string(),
520        status: "configured".to_string(),
521        path: None,
522        note: Some("ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_API_BASE_URL".to_string()),
523    });
524    steps.push(proxy_step);
525
526    // Step: Environment / doctor (compact)
527    let mut env_step = SetupStepReport {
528        name: "doctor_compact".to_string(),
529        ok: true,
530        items: Vec::new(),
531        warnings: Vec::new(),
532        errors: Vec::new(),
533    };
534    let (passed, total) = crate::doctor::compact_score();
535    env_step.items.push(SetupItem {
536        name: "doctor".to_string(),
537        status: format!("{passed}/{total}"),
538        path: None,
539        note: None,
540    });
541    if passed != total {
542        env_step.warnings.push(format!(
543            "doctor compact not fully passing: {passed}/{total}"
544        ));
545    }
546    steps.push(env_step);
547
548    let finished_at = Utc::now();
549    let success = steps.iter().all(|s| s.ok);
550    let report = SetupReport {
551        schema_version: 1,
552        started_at,
553        finished_at,
554        success,
555        platform: PlatformInfo {
556            os: std::env::consts::OS.to_string(),
557            arch: std::env::consts::ARCH.to_string(),
558        },
559        steps,
560        warnings: Vec::new(),
561        errors: Vec::new(),
562    };
563
564    let path = SetupReport::default_path()?;
565    let mut content =
566        serde_json::to_string_pretty(&report).map_err(|e| format!("serialize report: {e}"))?;
567    content.push('\n');
568    crate::config_io::write_atomic(&path, &content)?;
569
570    Ok(report)
571}
572
573pub fn configure_agent_mcp(agent: &str) -> Result<(), String> {
574    let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
575    let binary = resolve_portable_binary();
576
577    let mut targets = Vec::<EditorTarget>::new();
578
579    let push = |targets: &mut Vec<EditorTarget>,
580                name: &'static str,
581                config_path: PathBuf,
582                config_type: ConfigType| {
583        targets.push(EditorTarget {
584            name,
585            agent_key: agent.to_string(),
586            detect_path: PathBuf::from("/nonexistent"), // not used in direct agent config
587            config_path,
588            config_type,
589        });
590    };
591
592    match agent {
593        "cursor" => push(
594            &mut targets,
595            "Cursor",
596            home.join(".cursor/mcp.json"),
597            ConfigType::McpJson,
598        ),
599        "claude" | "claude-code" => push(
600            &mut targets,
601            "Claude Code",
602            crate::core::editor_registry::claude_mcp_json_path(&home),
603            ConfigType::McpJson,
604        ),
605        "windsurf" => push(
606            &mut targets,
607            "Windsurf",
608            home.join(".codeium/windsurf/mcp_config.json"),
609            ConfigType::McpJson,
610        ),
611        "codex" => push(
612            &mut targets,
613            "Codex CLI",
614            home.join(".codex/config.toml"),
615            ConfigType::Codex,
616        ),
617        "gemini" => {
618            push(
619                &mut targets,
620                "Gemini CLI",
621                home.join(".gemini/settings.json"),
622                ConfigType::GeminiSettings,
623            );
624            push(
625                &mut targets,
626                "Antigravity",
627                home.join(".gemini/antigravity/mcp_config.json"),
628                ConfigType::McpJson,
629            );
630        }
631        "antigravity" => push(
632            &mut targets,
633            "Antigravity",
634            home.join(".gemini/antigravity/mcp_config.json"),
635            ConfigType::McpJson,
636        ),
637        "copilot" => push(
638            &mut targets,
639            "VS Code / Copilot",
640            crate::core::editor_registry::vscode_mcp_path(),
641            ConfigType::VsCodeMcp,
642        ),
643        "crush" => push(
644            &mut targets,
645            "Crush",
646            home.join(".config/crush/crush.json"),
647            ConfigType::Crush,
648        ),
649        "pi" => push(
650            &mut targets,
651            "Pi Coding Agent",
652            home.join(".pi/agent/mcp.json"),
653            ConfigType::McpJson,
654        ),
655        "cline" => push(
656            &mut targets,
657            "Cline",
658            crate::core::editor_registry::cline_mcp_path(),
659            ConfigType::McpJson,
660        ),
661        "roo" => push(
662            &mut targets,
663            "Roo Code",
664            crate::core::editor_registry::roo_mcp_path(),
665            ConfigType::McpJson,
666        ),
667        "kiro" => push(
668            &mut targets,
669            "AWS Kiro",
670            home.join(".kiro/settings/mcp.json"),
671            ConfigType::McpJson,
672        ),
673        "verdent" => push(
674            &mut targets,
675            "Verdent",
676            home.join(".verdent/mcp.json"),
677            ConfigType::McpJson,
678        ),
679        "jetbrains" | "amp" => {
680            // Handled by dedicated install hooks (servers[] array / amp.mcpServers)
681        }
682        "qwen" => push(
683            &mut targets,
684            "Qwen Code",
685            home.join(".qwen/mcp.json"),
686            ConfigType::McpJson,
687        ),
688        "trae" => push(
689            &mut targets,
690            "Trae",
691            home.join(".trae/mcp.json"),
692            ConfigType::McpJson,
693        ),
694        "amazonq" => push(
695            &mut targets,
696            "Amazon Q Developer",
697            home.join(".aws/amazonq/mcp.json"),
698            ConfigType::McpJson,
699        ),
700        "opencode" => {
701            #[cfg(windows)]
702            let opencode_path = if let Ok(appdata) = std::env::var("APPDATA") {
703                std::path::PathBuf::from(appdata)
704                    .join("opencode")
705                    .join("opencode.json")
706            } else {
707                home.join(".config/opencode/opencode.json")
708            };
709            #[cfg(not(windows))]
710            let opencode_path = home.join(".config/opencode/opencode.json");
711            push(
712                &mut targets,
713                "OpenCode",
714                opencode_path,
715                ConfigType::OpenCode,
716            );
717        }
718        "aider" => push(
719            &mut targets,
720            "Aider",
721            home.join(".aider/mcp.json"),
722            ConfigType::McpJson,
723        ),
724        "hermes" => push(
725            &mut targets,
726            "Hermes Agent",
727            home.join(".hermes/config.yaml"),
728            ConfigType::HermesYaml,
729        ),
730        _ => {
731            return Err(format!("Unknown agent '{agent}'"));
732        }
733    }
734
735    for t in &targets {
736        crate::core::editor_registry::write_config_with_options(
737            t,
738            &binary,
739            WriteOptions {
740                overwrite_invalid: true,
741            },
742        )?;
743    }
744
745    if agent == "kiro" {
746        install_kiro_steering(&home);
747    }
748
749    Ok(())
750}
751
752fn install_kiro_steering(home: &std::path::Path) {
753    let cwd = std::env::current_dir().unwrap_or_else(|_| home.to_path_buf());
754    let steering_dir = cwd.join(".kiro").join("steering");
755    let steering_file = steering_dir.join("lean-ctx.md");
756
757    if steering_file.exists()
758        && std::fs::read_to_string(&steering_file)
759            .unwrap_or_default()
760            .contains("lean-ctx")
761    {
762        println!("  Kiro steering file already exists at .kiro/steering/lean-ctx.md");
763        return;
764    }
765
766    let _ = std::fs::create_dir_all(&steering_dir);
767    let _ = std::fs::write(&steering_file, crate::hooks::KIRO_STEERING_TEMPLATE);
768    println!("  \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)");
769}
770
771fn shorten_path(path: &str, home: &str) -> String {
772    if let Some(stripped) = path.strip_prefix(home) {
773        format!("~{stripped}")
774    } else {
775        path.to_string()
776    }
777}
778
779fn configure_premium_features(home: &std::path::Path) {
780    use crate::terminal_ui;
781    use std::io::Write;
782
783    let config_dir = home.join(".lean-ctx");
784    let _ = std::fs::create_dir_all(&config_dir);
785    let config_path = config_dir.join("config.toml");
786    let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
787
788    let dim = "\x1b[2m";
789    let bold = "\x1b[1m";
790    let rst = "\x1b[0m";
791
792    // Terse Agent Mode
793    println!(
794        "\n  {bold}Agent Output Optimization{rst} {dim}(reduces output tokens by 40-70%){rst}"
795    );
796    println!(
797        "  {dim}Levels: lite (concise), full (max density), ultra (expert pair-programmer){rst}"
798    );
799    print!("  Terse agent mode? {bold}[off/lite/full/ultra]{rst} {dim}(default: off){rst} ");
800    std::io::stdout().flush().ok();
801
802    let mut terse_input = String::new();
803    let terse_level = if std::io::stdin().read_line(&mut terse_input).is_ok() {
804        match terse_input.trim().to_lowercase().as_str() {
805            "lite" => "lite",
806            "full" => "full",
807            "ultra" => "ultra",
808            _ => "off",
809        }
810    } else {
811        "off"
812    };
813
814    if terse_level != "off" && !config_content.contains("terse_agent") {
815        if !config_content.is_empty() && !config_content.ends_with('\n') {
816            config_content.push('\n');
817        }
818        config_content.push_str(&format!("terse_agent = \"{terse_level}\"\n"));
819        terminal_ui::print_status_ok(&format!("Terse agent: {terse_level}"));
820    } else if terse_level == "off" {
821        terminal_ui::print_status_skip(
822            "Terse agent: off (change later with: lean-ctx terse <level>)",
823        );
824    }
825
826    // Tool Result Archive
827    println!(
828        "\n  {bold}Tool Result Archive{rst} {dim}(zero-loss: large outputs archived, retrievable via ctx_expand){rst}"
829    );
830    print!("  Enable auto-archive? {bold}[Y/n]{rst} ");
831    std::io::stdout().flush().ok();
832
833    let mut archive_input = String::new();
834    let archive_on = if std::io::stdin().read_line(&mut archive_input).is_ok() {
835        let a = archive_input.trim().to_lowercase();
836        a.is_empty() || a == "y" || a == "yes"
837    } else {
838        true
839    };
840
841    if archive_on && !config_content.contains("[archive]") {
842        if !config_content.is_empty() && !config_content.ends_with('\n') {
843            config_content.push('\n');
844        }
845        config_content.push_str("\n[archive]\nenabled = true\n");
846        terminal_ui::print_status_ok("Tool Result Archive: enabled");
847    } else if !archive_on {
848        terminal_ui::print_status_skip("Archive: off (enable later in config.toml)");
849    }
850
851    // Output Density
852    println!(
853        "\n  {bold}Output Density{rst} {dim}(compresses tool output: normal, terse, ultra){rst}"
854    );
855    print!("  Output density? {bold}[normal/terse/ultra]{rst} {dim}(default: normal){rst} ");
856    std::io::stdout().flush().ok();
857
858    let mut density_input = String::new();
859    let density = if std::io::stdin().read_line(&mut density_input).is_ok() {
860        match density_input.trim().to_lowercase().as_str() {
861            "terse" => "terse",
862            "ultra" => "ultra",
863            _ => "normal",
864        }
865    } else {
866        "normal"
867    };
868
869    if density != "normal" && !config_content.contains("output_density") {
870        if !config_content.is_empty() && !config_content.ends_with('\n') {
871            config_content.push('\n');
872        }
873        config_content.push_str(&format!("output_density = \"{density}\"\n"));
874        terminal_ui::print_status_ok(&format!("Output density: {density}"));
875    } else if density == "normal" {
876        terminal_ui::print_status_skip("Output density: normal (change later in config.toml)");
877    }
878
879    let _ = std::fs::write(&config_path, config_content);
880}