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            HookMode::Replace => "Replace",
135        }
136    );
137    hooks::install_agent_hook_with_mode(agent_key, true, mode);
138
139    // --- Step 5: Daemon ---
140    eprintln!("  Starting daemon...");
141    if !crate::daemon::is_daemon_running() {
142        let _ = crate::daemon::start_daemon(&[]);
143    }
144
145    // --- Step 6: Save snapshot for unwrap ---
146    if let Err(e) = snap.save() {
147        eprintln!("  Warning: could not save wrap snapshot: {e}");
148    }
149
150    // --- Step 7: Verify MCP ---
151    let mcp_verified = verify::probe_mcp_server(&binary);
152
153    // --- Step 8: Launch / restart hint ---
154    let launch_result = launch::handle_agent_launch(agent_key);
155
156    // --- Step 9: Summary ---
157    print_summary(agent_key, mcp_verified, &launch_result);
158}
159
160fn print_summary(agent_key: &str, mcp_ok: bool, launch_hint: &str) {
161    let tool_count = crate::server::registry::tool_count();
162
163    eprintln!();
164    eprintln!("\x1b[1;32mlean-ctx wrapped {agent_key} successfully.\x1b[0m");
165    eprintln!();
166
167    let mcp_status = if mcp_ok {
168        format!(
169            "\x1b[32m{tool_count} tools verified\x1b[0m (ctx_read, ctx_search, ctx_shell + more)"
170        )
171    } else {
172        format!("{tool_count} tools \x1b[33m(pending IDE restart)\x1b[0m")
173    };
174    eprintln!("  MCP server:  {mcp_status}");
175    eprintln!("  Shell hooks: \x1b[32minstalled\x1b[0m (git, cargo, npm, docker + 90 patterns)");
176    eprintln!("  Agent hooks: \x1b[32minstalled\x1b[0m");
177    eprintln!();
178
179    if !launch_hint.is_empty() {
180        eprintln!("  \x1b[33m{launch_hint}\x1b[0m");
181        eprintln!();
182    }
183
184    eprintln!("  Undo:   \x1b[2mlean-ctx unwrap {agent_key}\x1b[0m");
185    eprintln!("  Verify: \x1b[2mlean-ctx doctor\x1b[0m");
186    eprintln!("  Stats:  \x1b[2mlean-ctx gain\x1b[0m (after first use)");
187}
188
189fn print_help() {
190    println!("Usage: lean-ctx wrap <agent>");
191    println!();
192    println!("One-command setup: installs shell hooks, MCP server registration,");
193    println!("agent hooks, and starts the daemon. Everything needed to use lean-ctx");
194    println!("with the specified agent.");
195    println!();
196    println!("Supported agents:");
197    for name in available_agent_keys() {
198        println!("  {name}");
199    }
200    println!();
201    println!("Examples:");
202    println!("  lean-ctx wrap cursor     # Set up lean-ctx for Cursor");
203    println!("  lean-ctx wrap claude     # Set up lean-ctx for Claude Code");
204    println!("  lean-ctx wrap codex      # Set up lean-ctx for Codex CLI");
205    println!();
206    println!("Undo:  lean-ctx unwrap <agent>");
207    println!("Full:  lean-ctx setup  (interactive wizard with all options)");
208}
209
210fn available_agent_keys() -> Vec<String> {
211    let home = dirs::home_dir().unwrap_or_default();
212    let targets = editor_registry::build_targets(&home);
213    let mut keys: Vec<String> = targets.into_iter().map(|t| t.agent_key).collect();
214    keys.sort_unstable();
215    keys.dedup();
216    keys
217}
218
219fn detect_single_agent() -> Option<String> {
220    let home = dirs::home_dir()?;
221    let targets = editor_registry::build_targets(&home);
222    let installed: Vec<String> = targets
223        .iter()
224        .filter(|t| t.detect_path.exists())
225        .map(|t| t.agent_key.clone())
226        .collect::<std::collections::BTreeSet<_>>()
227        .into_iter()
228        .collect();
229
230    if installed.len() == 1 {
231        installed.into_iter().next()
232    } else {
233        None
234    }
235}