Skip to main content

lean_ctx/wrap/
mod.rs

1//! `lean-ctx wrap <agent>` — one-command setup for any supported agent.
2//!
3//! Orchestrates shell hooks, MCP registration, agent hooks, daemon, and
4//! optional IDE launch into a single idempotent operation.  Every file
5//! mutation is recorded in a snapshot so `lean-ctx unwrap <agent>` can
6//! restore the pre-wrap state byte-for-byte.
7
8mod launch;
9mod snapshot;
10mod unwrap;
11mod verify;
12
13use crate::core::editor_registry::{self, EditorTarget, WriteOptions};
14use crate::core::portable_binary::resolve_portable_binary;
15use crate::hooks::{self, HookMode};
16
17use snapshot::WrapSnapshot;
18
19pub use unwrap::run_unwrap;
20
21/// Entry point for `lean-ctx wrap <agent>`.
22pub fn run_wrap(args: &[String]) {
23    if args.iter().any(|a| a == "--help" || a == "-h") {
24        print_help();
25        return;
26    }
27
28    let agent_key = match args.first() {
29        Some(a) if !a.starts_with('-') => a.as_str(),
30        _ => {
31            let detected = detect_single_agent();
32            if let Some(agent) = detected {
33                eprintln!("Detected: {agent}");
34                run_wrap_for_agent(&agent);
35                return;
36            }
37            eprintln!("Usage: lean-ctx wrap <agent>");
38            eprintln!();
39            eprintln!("Supported agents:");
40            for name in available_agent_keys() {
41                eprintln!("  {name}");
42            }
43            eprintln!();
44            eprintln!("Example: lean-ctx wrap cursor");
45            std::process::exit(1);
46        }
47    };
48
49    run_wrap_for_agent(agent_key);
50}
51
52fn run_wrap_for_agent(agent_key: &str) {
53    let Some(home) = dirs::home_dir() else {
54        eprintln!("Cannot determine home directory");
55        std::process::exit(1);
56    };
57
58    let targets = editor_registry::build_targets(&home);
59    let matching: Vec<&EditorTarget> = targets
60        .iter()
61        .filter(|t| t.agent_key == agent_key)
62        .collect();
63
64    if matching.is_empty() {
65        eprintln!("Unknown agent: '{agent_key}'");
66        eprintln!();
67        eprintln!("Supported agents:");
68        for name in available_agent_keys() {
69            eprintln!("  {name}");
70        }
71        std::process::exit(1);
72    }
73
74    let binary = resolve_portable_binary();
75    let mut snap = WrapSnapshot::new(agent_key);
76
77    // --- Step 1: Snapshot existing configs ---
78    for target in &matching {
79        snap.record_file(&target.config_path);
80    }
81
82    // --- Step 2: Shell hooks ---
83    eprintln!("  Installing shell hooks...");
84    crate::shell_hook::install_all(true);
85
86    // --- Step 3: MCP config ---
87    eprintln!("  Registering MCP server...");
88    let mut mcp_ok = false;
89    for target in &matching {
90        if !target.detect_path.exists() {
91            eprintln!(
92                "    {}: not installed ({})",
93                target.name,
94                target.detect_path.display()
95            );
96            continue;
97        }
98        match editor_registry::write_config_with_options(
99            target,
100            &binary,
101            WriteOptions {
102                overwrite_invalid: true,
103            },
104        ) {
105            Ok(result) => {
106                let action = match result.action {
107                    editor_registry::WriteAction::Created => "created",
108                    editor_registry::WriteAction::Updated => "updated",
109                    editor_registry::WriteAction::Already => "already configured",
110                };
111                eprintln!("    {}: {action}", target.name);
112                mcp_ok = true;
113            }
114            Err(e) => eprintln!("    {}: error: {e}", target.name),
115        }
116    }
117
118    if !mcp_ok {
119        eprintln!();
120        eprintln!(
121            "No installed instance of '{agent_key}' found. \
122             Install {agent_key}, then re-run: lean-ctx wrap {agent_key}"
123        );
124        std::process::exit(1);
125    }
126
127    // --- Step 4: Agent hooks ---
128    let mode = hooks::recommend_hook_mode(agent_key);
129    eprintln!(
130        "  Installing agent hooks ({})...",
131        match mode {
132            HookMode::Mcp => "MCP",
133            HookMode::Hybrid => "Hybrid",
134        }
135    );
136    hooks::install_agent_hook_with_mode(agent_key, true, mode);
137
138    // --- Step 5: Daemon ---
139    eprintln!("  Starting daemon...");
140    if !crate::daemon::is_daemon_running() {
141        let _ = crate::daemon::start_daemon(&[]);
142    }
143
144    // --- Step 6: Save snapshot for unwrap ---
145    if let Err(e) = snap.save() {
146        eprintln!("  Warning: could not save wrap snapshot: {e}");
147    }
148
149    // --- Step 7: Verify MCP ---
150    let mcp_verified = verify::probe_mcp_server(&binary);
151
152    // --- Step 8: Launch / restart hint ---
153    let launch_result = launch::handle_agent_launch(agent_key);
154
155    // --- Step 9: Summary ---
156    print_summary(agent_key, mcp_verified, &launch_result);
157}
158
159fn print_summary(agent_key: &str, mcp_ok: bool, launch_hint: &str) {
160    let tool_count = crate::server::registry::tool_count();
161
162    eprintln!();
163    eprintln!("\x1b[1;32mlean-ctx wrapped {agent_key} successfully.\x1b[0m");
164    eprintln!();
165
166    let mcp_status = if mcp_ok {
167        format!(
168            "\x1b[32m{tool_count} tools verified\x1b[0m (ctx_read, ctx_search, ctx_shell + more)"
169        )
170    } else {
171        format!("{tool_count} tools \x1b[33m(pending IDE restart)\x1b[0m")
172    };
173    eprintln!("  MCP server:  {mcp_status}");
174    eprintln!("  Shell hooks: \x1b[32minstalled\x1b[0m (git, cargo, npm, docker + 90 patterns)");
175    eprintln!("  Agent hooks: \x1b[32minstalled\x1b[0m");
176    eprintln!();
177
178    if !launch_hint.is_empty() {
179        eprintln!("  \x1b[33m{launch_hint}\x1b[0m");
180        eprintln!();
181    }
182
183    eprintln!("  Undo:   \x1b[2mlean-ctx unwrap {agent_key}\x1b[0m");
184    eprintln!("  Verify: \x1b[2mlean-ctx doctor\x1b[0m");
185    eprintln!("  Stats:  \x1b[2mlean-ctx gain\x1b[0m (after first use)");
186}
187
188fn print_help() {
189    println!("Usage: lean-ctx wrap <agent>");
190    println!();
191    println!("One-command setup: installs shell hooks, MCP server registration,");
192    println!("agent hooks, and starts the daemon. Everything needed to use lean-ctx");
193    println!("with the specified agent.");
194    println!();
195    println!("Supported agents:");
196    for name in available_agent_keys() {
197        println!("  {name}");
198    }
199    println!();
200    println!("Examples:");
201    println!("  lean-ctx wrap cursor     # Set up lean-ctx for Cursor");
202    println!("  lean-ctx wrap claude     # Set up lean-ctx for Claude Code");
203    println!("  lean-ctx wrap codex      # Set up lean-ctx for Codex CLI");
204    println!();
205    println!("Undo:  lean-ctx unwrap <agent>");
206    println!("Full:  lean-ctx setup  (interactive wizard with all options)");
207}
208
209fn available_agent_keys() -> Vec<String> {
210    let home = dirs::home_dir().unwrap_or_default();
211    let targets = editor_registry::build_targets(&home);
212    let mut keys: Vec<String> = targets.into_iter().map(|t| t.agent_key).collect();
213    keys.sort_unstable();
214    keys.dedup();
215    keys
216}
217
218fn detect_single_agent() -> Option<String> {
219    let home = dirs::home_dir()?;
220    let targets = editor_registry::build_targets(&home);
221    let installed: Vec<String> = targets
222        .iter()
223        .filter(|t| t.detect_path.exists())
224        .map(|t| t.agent_key.clone())
225        .collect::<std::collections::BTreeSet<_>>()
226        .into_iter()
227        .collect();
228
229    if installed.len() == 1 {
230        Some(installed.into_iter().next().unwrap())
231    } else {
232        None
233    }
234}