1mod 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
21pub 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 for target in &matching {
79 snap.record_file(&target.config_path);
80 }
81
82 eprintln!(" Installing shell hooks...");
84 crate::shell_hook::install_all(true);
85
86 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 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 eprintln!(" Starting daemon...");
140 if !crate::daemon::is_daemon_running() {
141 let _ = crate::daemon::start_daemon(&[]);
142 }
143
144 if let Err(e) = snap.save() {
146 eprintln!(" Warning: could not save wrap snapshot: {e}");
147 }
148
149 let mcp_verified = verify::probe_mcp_server(&binary);
151
152 let launch_result = launch::handle_agent_launch(agent_key);
154
155 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}