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 HookMode::Replace => "Replace",
135 }
136 );
137 hooks::install_agent_hook_with_mode(agent_key, true, mode);
138
139 eprintln!(" Starting daemon...");
141 if !crate::daemon::is_daemon_running() {
142 let _ = crate::daemon::start_daemon(&[]);
143 }
144
145 if let Err(e) = snap.save() {
147 eprintln!(" Warning: could not save wrap snapshot: {e}");
148 }
149
150 let mcp_verified = verify::probe_mcp_server(&binary);
152
153 let launch_result = launch::handle_agent_launch(agent_key);
155
156 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 Some(installed.into_iter().next().unwrap())
232 } else {
233 None
234 }
235}