1use crate::core::editor_registry::{WriteAction, WriteOptions};
2use crate::core::portable_binary::resolve_portable_binary;
3use crate::hooks::{HookMode, recommend_hook_mode};
4
5use super::first_run::{first_run_setup_level, persist_setup_choice};
6use super::helpers::{
7 configure_plan_mode_settings, configure_premium_features, configure_tool_profile,
8 install_skill_files, shorten_path,
9};
10use super::index_build::spawn_index_build_background;
11use super::options::SetupOptions;
12use super::with_options::run_setup_with_options;
13
14pub fn run_setup() {
15 use crate::terminal_ui;
16
17 if crate::shell::is_non_interactive() {
18 eprintln!("Non-interactive terminal detected (no TTY on stdin).");
19 eprintln!(
20 "Running in non-interactive mode (equivalent to: lean-ctx setup --non-interactive --yes)"
21 );
22 eprintln!();
23 let opts = SetupOptions {
24 non_interactive: true,
25 yes: true,
26 ..Default::default()
27 };
28 match run_setup_with_options(opts) {
29 Ok(report) => {
30 for w in &report.warnings {
31 tracing::warn!("{w}");
32 }
33 }
34 Err(e) => tracing::error!("Setup error: {e}"),
35 }
36 return;
37 }
38
39 let Some(home) = dirs::home_dir() else {
40 tracing::error!("Cannot determine home directory");
41 std::process::exit(1);
42 };
43
44 let binary = resolve_portable_binary();
45
46 let home_str = home.to_string_lossy().to_string();
47
48 terminal_ui::print_setup_header();
49
50 let (inject_rules, inject_skills) = first_run_setup_level();
51 persist_setup_choice(inject_rules, inject_skills);
52
53 terminal_ui::print_step_header(1, 13, "Shell Hook");
54 crate::cli::cmd_init(&["--global".to_string()]);
55 crate::shell_hook::install_all(false);
56
57 terminal_ui::print_step_header(2, 13, "Daemon");
58 if crate::daemon::is_daemon_running() {
59 terminal_ui::print_status_ok("Daemon running — restarting with current binary…");
60 let _ = crate::daemon::stop_daemon();
61 std::thread::sleep(std::time::Duration::from_millis(500));
62 if let Err(e) = crate::daemon::start_daemon(&[]) {
63 terminal_ui::print_status_warn(&format!("Daemon restart failed: {e}"));
64 }
65 } else if let Err(e) = crate::daemon::start_daemon(&[]) {
66 terminal_ui::print_status_warn(&format!("Daemon start failed: {e}"));
67 }
68
69 terminal_ui::print_step_header(3, 13, "AI Tool Detection");
70
71 let targets = crate::core::editor_registry::build_targets(&home);
72 let update_mcp = crate::core::config::Config::load()
76 .setup
77 .should_update_mcp();
78 let mut newly_configured: Vec<&str> = Vec::new();
79 let mut already_configured: Vec<&str> = Vec::new();
80 let mut not_installed: Vec<&str> = Vec::new();
81 let mut mcp_skipped: Vec<&str> = Vec::new();
82 let mut errors: Vec<&str> = Vec::new();
83
84 for target in &targets {
85 let short_path = shorten_path(&target.config_path.to_string_lossy(), &home_str);
86
87 if !target.detect_path.exists() {
88 not_installed.push(target.name);
89 continue;
90 }
91
92 if !update_mcp {
93 terminal_ui::print_status_ok(&format!(
94 "{:<20} \x1b[2mMCP registration skipped (auto_update_mcp=false)\x1b[0m",
95 target.name
96 ));
97 mcp_skipped.push(target.name);
98 continue;
99 }
100
101 let mode = if target.agent_key.is_empty() {
102 HookMode::Mcp
103 } else {
104 recommend_hook_mode(&target.agent_key)
105 };
106
107 match crate::core::editor_registry::write_config_with_options(
108 target,
109 &binary,
110 WriteOptions {
111 overwrite_invalid: false,
112 },
113 ) {
114 Ok(res) if res.action == WriteAction::Already => {
115 terminal_ui::print_status_ok(&format!(
116 "{:<20} \x1b[36m{mode}\x1b[0m \x1b[2m{short_path}\x1b[0m",
117 target.name
118 ));
119 already_configured.push(target.name);
120 }
121 Ok(_) => {
122 terminal_ui::print_status_new(&format!(
123 "{:<20} \x1b[36m{mode}\x1b[0m \x1b[2m{short_path}\x1b[0m",
124 target.name
125 ));
126 newly_configured.push(target.name);
127 }
128 Err(e) => {
129 terminal_ui::print_status_warn(&format!("{}: {e}", target.name));
130 errors.push(target.name);
131 }
132 }
133 }
134
135 let total_ok = newly_configured.len() + already_configured.len();
136 if total_ok == 0 && errors.is_empty() && mcp_skipped.is_empty() {
137 terminal_ui::print_status_warn(
138 "No AI tools detected. Install one and re-run: lean-ctx setup",
139 );
140 }
141
142 if !not_installed.is_empty() {
143 println!(
144 " \x1b[2m○ {} not detected: {}\x1b[0m",
145 not_installed.len(),
146 not_installed.join(", ")
147 );
148 }
149
150 configure_plan_mode_settings(&newly_configured, &already_configured);
151
152 terminal_ui::print_step_header(4, 13, "Agent Rules");
153 let rules_result = if inject_rules {
154 let r = crate::rules_inject::inject_all_rules(&home);
155 for name in &r.injected {
156 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules injected\x1b[0m"));
157 }
158 for name in &r.updated {
159 terminal_ui::print_status_new(&format!("{name:<20} \x1b[2mrules updated\x1b[0m"));
160 }
161 for name in &r.already {
162 terminal_ui::print_status_ok(&format!("{name:<20} \x1b[2mrules up-to-date\x1b[0m"));
163 }
164 for err in &r.errors {
165 terminal_ui::print_status_warn(err);
166 }
167 if !r.backed_up.is_empty() {
168 for bak in &r.backed_up {
169 println!(" \x1b[2m ↳ backup: {bak}\x1b[0m");
170 }
171 }
172 if r.injected.is_empty()
173 && r.updated.is_empty()
174 && r.already.is_empty()
175 && r.errors.is_empty()
176 {
177 terminal_ui::print_status_skip("No agent rules needed");
178 }
179 r
180 } else {
181 terminal_ui::print_status_skip(
182 "Skipped (run `lean-ctx setup` or set auto_inject_rules = true in config)",
183 );
184 crate::rules_inject::InjectResult::default()
185 };
186
187 for target in &targets {
188 if !target.detect_path.exists() || target.agent_key.is_empty() {
189 continue;
190 }
191 let mode = recommend_hook_mode(&target.agent_key);
192 crate::hooks::install_agent_hook_with_mode(&target.agent_key, true, mode);
193 }
194
195 terminal_ui::print_step_header(5, 13, "API Proxy (optional)");
196 {
197 let cfg = crate::core::config::Config::load();
198 let proxy_port = crate::proxy_setup::default_port();
199
200 match cfg.proxy_enabled {
201 Some(true) => {
202 crate::proxy_autostart::install(proxy_port, false);
203 std::thread::sleep(std::time::Duration::from_millis(500));
204 crate::proxy_setup::install_proxy_env(&home, proxy_port, false);
205 terminal_ui::print_status_ok("Proxy active (opted in)");
206 }
207 Some(false) => {
208 terminal_ui::print_status_skip(
209 "Proxy disabled (run `lean-ctx proxy enable` to change)",
210 );
211 }
212 None => {
213 println!(
214 " \x1b[2mThe API proxy routes LLM requests through lean-ctx for additional\x1b[0m"
215 );
216 println!(
217 " \x1b[2mtool-result compression and precise token analytics in the dashboard.\x1b[0m"
218 );
219 println!();
220 println!(
221 " \x1b[2mWithout it: MCP tools, shell hooks, gain tracking, and memory\x1b[0m"
222 );
223 println!(
224 " \x1b[2mall work normally. The proxy adds ~5-15% extra savings on top.\x1b[0m"
225 );
226 println!();
227 print!(" Enable the API proxy? [y/N] ");
228 let _ = std::io::Write::flush(&mut std::io::stdout());
229 let mut input = String::new();
230 let _ = std::io::stdin().read_line(&mut input);
231 let answer = matches!(input.trim().to_lowercase().as_str(), "y" | "yes");
232 if let Err(e) =
233 crate::core::config::Config::update_global(|c| c.proxy_enabled = Some(answer))
234 {
235 tracing::warn!("could not persist proxy choice: {e}");
236 }
237 if answer {
238 crate::proxy_autostart::install(proxy_port, false);
239 std::thread::sleep(std::time::Duration::from_millis(500));
240 crate::proxy_setup::install_proxy_env(&home, proxy_port, false);
241 terminal_ui::print_status_new("Proxy enabled");
242 } else {
243 terminal_ui::print_status_skip(
244 "Proxy skipped (run `lean-ctx proxy enable` anytime)",
245 );
246 }
247 }
248 }
249 }
250
251 terminal_ui::print_step_header(6, 13, "IDE Config Access (optional)");
252 {
253 let cfg = crate::core::config::Config::load();
254 match cfg.allow_ide_config_dirs {
255 Some(true) => {
256 terminal_ui::print_status_ok(
257 "Enabled — the agent can read your editors' config dirs",
258 );
259 }
260 Some(false) => {
261 terminal_ui::print_status_skip(
262 "Off (enable: lean-ctx config set allow_ide_config_dirs true)",
263 );
264 }
265 None => {
266 println!(
267 " \x1b[2mlean-ctx tools are jailed to the current project. Enabling this lets\x1b[0m"
268 );
269 println!(
270 " \x1b[2mthe agent read every supported editor's config dir (~/.cursor, VS Code,\x1b[0m"
271 );
272 println!(
273 " \x1b[2mCline/Roo, JetBrains, …) to manage MCP setup across editors.\x1b[0m"
274 );
275 println!();
276 println!(
277 " \x1b[33mTrade-off:\x1b[0m \x1b[2mthose dirs can hold other agents' sessions & credentials.\x1b[0m"
278 );
279 println!();
280 print!(" Allow the agent to read IDE config dirs? [y/N] ");
281 let _ = std::io::Write::flush(&mut std::io::stdout());
282 let mut input = String::new();
283 let _ = std::io::stdin().read_line(&mut input);
284 let answer = matches!(input.trim().to_lowercase().as_str(), "y" | "yes");
285 if let Err(e) = crate::core::config::Config::update_global(|c| {
286 c.allow_ide_config_dirs = Some(answer);
287 }) {
288 tracing::warn!("could not persist IDE-config-access choice: {e}");
289 }
290 if answer {
291 terminal_ui::print_status_new("IDE config access enabled");
292 } else {
293 terminal_ui::print_status_skip(
294 "Skipped (enable later: lean-ctx config set allow_ide_config_dirs true)",
295 );
296 }
297 }
298 }
299 }
300
301 terminal_ui::print_step_header(7, 13, "Skill Files");
302 if inject_skills {
303 let skill_result = install_skill_files(&home);
304 for (name, installed) in &skill_result {
305 if *installed {
306 terminal_ui::print_status_new(&format!(
307 "{name:<20} \x1b[2mSKILL.md installed\x1b[0m"
308 ));
309 } else {
310 terminal_ui::print_status_ok(&format!(
311 "{name:<20} \x1b[2mSKILL.md up-to-date\x1b[0m"
312 ));
313 }
314 }
315 if skill_result.is_empty() {
316 terminal_ui::print_status_skip("No skill directories to install");
317 }
318 } else {
319 terminal_ui::print_status_skip(
320 "Skipped (skill files install with the rules opt-in; choose Standard/Full in `lean-ctx setup`)",
321 );
322 }
323
324 terminal_ui::print_step_header(8, 13, "Environment Check");
325 let lean_dir = crate::core::data_dir::lean_ctx_data_dir()
326 .unwrap_or_else(|_| home.join(".config/lean-ctx"));
327 if lean_dir.exists() {
328 terminal_ui::print_status_ok(&format!("{} ready", lean_dir.display()));
329 } else {
330 let _ = std::fs::create_dir_all(&lean_dir);
331 terminal_ui::print_status_new(&format!("Created {}", lean_dir.display()));
332 }
333 if let Some(report) = crate::core::data_consolidate::consolidate()
334 && report.files_moved > 0
335 {
336 terminal_ui::print_status_new(&format!(
337 "Consolidated {} file(s) from a split data dir into {}",
338 report.files_moved,
339 report.canonical.display()
340 ));
341 }
342 if let Some(report) = crate::core::config_heal::heal() {
345 match report.action {
346 crate::core::config_heal::HealAction::Adopted => {
347 terminal_ui::print_status_new(&format!(
348 "Recovered your config into {}",
349 report.to.display()
350 ));
351 }
352 crate::core::config_heal::HealAction::Superseded => {
353 terminal_ui::print_status_ok("Unified config (archived a stale data-dir copy)");
354 }
355 }
356 }
357 crate::doctor::run_compact();
358
359 crate::core::layout_pin::heal();
362
363 terminal_ui::print_step_header(9, 13, "Help Improve lean-ctx");
364 println!(" Share anonymous telemetry to make lean-ctx better:");
365 println!(" [2m • Version, OS, architecture, random install ID[0m");
366 println!(" [2m • Compression patterns: file-type, size bucket, mode, ratio[0m");
367 println!(" [1mNo code, no file names, no personal data — ever.[0m");
368 println!(" [2mInspect anytime: lean-ctx telemetry show[0m");
369 println!();
370 print!(" Enable anonymous telemetry? [1m[y/N][0m ");
371 use std::io::Write;
372 std::io::stdout().flush().ok();
373
374 let mut input = String::new();
375 let contribute = if std::io::stdin().read_line(&mut input).is_ok() {
376 let answer = input.trim().to_lowercase();
377 answer == "y" || answer == "yes"
378 } else {
379 false
380 };
381
382 if contribute {
383 let config_path = crate::core::config::Config::path()
384 .unwrap_or_else(|| home.join(".config/lean-ctx").join("config.toml"));
385 if let Some(dir) = config_path.parent() {
386 let _ = std::fs::create_dir_all(dir);
387 }
388 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
389 if !config_content.contains("[telemetry]") {
390 if !config_content.ends_with('\n') {
391 config_content.push('\n');
392 }
393 config_content.push_str("\n[telemetry]\nenabled = true\n");
394 }
395 let _ = crate::config_io::write_atomic_with_backup(&config_path, &config_content);
396 terminal_ui::print_status_ok("Enabled — thank you!");
397 } else {
398 terminal_ui::print_status_skip("Skipped — enable later with: lean-ctx telemetry on");
399 }
400
401 terminal_ui::print_step_header(10, 13, "Auto-Updates");
402 println!(" Keep lean-ctx up to date automatically.");
403 println!(" \x1b[1mChecks GitHub every 6h, installs only when a new release exists.\x1b[0m");
404 println!(
405 " \x1b[2mNo restarts mid-session. Change anytime: lean-ctx update --schedule off\x1b[0m"
406 );
407 println!();
408 print!(" Enable automatic updates? \x1b[1m[y/N]\x1b[0m ");
409 std::io::stdout().flush().ok();
410
411 let mut auto_input = String::new();
412 let auto_update = if std::io::stdin().read_line(&mut auto_input).is_ok() {
413 let answer = auto_input.trim().to_lowercase();
414 answer == "y" || answer == "yes"
415 } else {
416 false
417 };
418
419 if auto_update {
420 let cfg = crate::core::config::Config::load();
421 let hours = cfg.updates.check_interval_hours;
422 match crate::core::update_scheduler::install_schedule(hours) {
423 Ok(info) => {
424 crate::core::update_scheduler::set_auto_update(true, false, hours);
425 terminal_ui::print_status_ok(&format!("Enabled — {info}"));
426 }
427 Err(e) => {
428 terminal_ui::print_status_warn(&format!("Scheduler setup failed: {e}"));
429 terminal_ui::print_status_skip("Enable later: lean-ctx update --schedule");
430 }
431 }
432 } else {
433 crate::core::update_scheduler::set_auto_update(false, false, 6);
434 terminal_ui::print_status_skip("Skipped — enable later: lean-ctx update --schedule");
435 }
436
437 terminal_ui::print_step_header(11, 13, "Tool Profile");
438 configure_tool_profile();
439
440 terminal_ui::print_step_header(12, 13, "Advanced Tuning (optional)");
441 configure_premium_features(&home);
442
443 terminal_ui::print_step_header(13, 13, "Code Intelligence");
444 let cwd = std::env::current_dir().ok();
445 let cwd_is_home = cwd
446 .as_ref()
447 .is_some_and(|d| dirs::home_dir().is_some_and(|h| d.as_path() == h.as_path()));
448 if cwd_is_home {
449 terminal_ui::print_status_warn(
450 "Running from $HOME — graph build skipped to avoid scanning your entire home directory.",
451 );
452 println!();
453 println!(" \x1b[1mSet a default project root to avoid this:\x1b[0m");
454 println!(" \x1b[2mEnter your main project path (or press Enter to skip):\x1b[0m");
455 print!(" \x1b[1m>\x1b[0m ");
456 use std::io::Write;
457 std::io::stdout().flush().ok();
458 let mut root_input = String::new();
459 if std::io::stdin().read_line(&mut root_input).is_ok() {
460 let root_trimmed = root_input.trim();
461 if root_trimmed.is_empty() {
462 terminal_ui::print_status_skip(
463 "No project root set. Set later: lean-ctx config set project_root /path/to/project",
464 );
465 } else {
466 let root_path = std::path::Path::new(root_trimmed);
467 if root_path.exists() && root_path.is_dir() {
468 let config_path = crate::core::config::Config::path()
469 .unwrap_or_else(|| home.join(".config/lean-ctx").join("config.toml"));
470 let mut content = std::fs::read_to_string(&config_path).unwrap_or_default();
471 if content.contains("project_root") {
472 if let Ok(re) = regex::Regex::new(r#"(?m)^project_root\s*=\s*"[^"]*""#) {
473 content = re
474 .replace(&content, &format!("project_root = \"{root_trimmed}\""))
475 .to_string();
476 }
477 } else {
478 if !content.is_empty() && !content.ends_with('\n') {
479 content.push('\n');
480 }
481 content.push_str(&format!("project_root = \"{root_trimmed}\"\n"));
482 }
483 let _ = crate::config_io::write_atomic_with_backup(&config_path, &content);
484 terminal_ui::print_status_ok(&format!("Project root set: {root_trimmed}"));
485 if crate::core::pathutil::has_project_marker(root_path) {
486 spawn_index_build_background(root_path);
487 terminal_ui::print_status_ok("Graph build started (background)");
488 }
489 } else {
490 terminal_ui::print_status_warn(&format!(
491 "Path not found: {root_trimmed} — skipped"
492 ));
493 }
494 }
495 }
496 } else {
497 let is_project = cwd
498 .as_ref()
499 .is_some_and(|d| crate::core::pathutil::has_project_marker(d));
500 if is_project {
501 println!(" \x1b[2mBuilding code graph for graph-aware reads, impact analysis,\x1b[0m");
502 println!(" \x1b[2mand smart search fusion in the background...\x1b[0m");
503 if let Some(ref root) = cwd {
504 spawn_index_build_background(root);
505 }
506 terminal_ui::print_status_ok("Graph build started (background)");
507 } else {
508 println!(" \x1b[2mRun `lean-ctx graph build` inside any git project to enable\x1b[0m");
509 println!(
510 " \x1b[2mgraph-aware reads, impact analysis, and smart search fusion.\x1b[0m"
511 );
512 }
513 }
514 println!();
515
516 {
517 let tools = crate::core::editor_registry::writers::auto_approve_tools();
518 println!();
519 println!(
520 " \x1b[33m⚡ Auto-approved tools ({} total):\x1b[0m",
521 tools.len()
522 );
523 for chunk in tools.chunks(6) {
524 let names: Vec<_> = chunk.iter().map(|t| format!("\x1b[2m{t}\x1b[0m")).collect();
525 println!(" {}", names.join(", "));
526 }
527 println!(" \x1b[2mDisable with: lean-ctx setup --no-auto-approve\x1b[0m");
528 }
529
530 println!();
531 println!(
532 " \x1b[1;32m✓ Setup complete!\x1b[0m \x1b[1m{}\x1b[0m configured, \x1b[2m{} already set, {} skipped\x1b[0m",
533 newly_configured.len(),
534 already_configured.len(),
535 not_installed.len()
536 );
537
538 if !errors.is_empty() {
539 println!(
540 " \x1b[33m⚠ {} error{}: {}\x1b[0m",
541 errors.len(),
542 if errors.len() == 1 { "" } else { "s" },
543 errors.join(", ")
544 );
545 }
546
547 let source_cmd = crate::shell_hook::shell_source_command().unwrap_or("Restart your shell");
548
549 let dim = "\x1b[2m";
550 let bold = "\x1b[1m";
551 let cyan = "\x1b[36m";
552 let yellow = "\x1b[33m";
553 let rst = "\x1b[0m";
554
555 println!();
556 println!(" {bold}Next steps:{rst}");
557 println!();
558 println!(" {cyan}1.{rst} Reload your shell:");
559 println!(" {bold}{source_cmd}{rst}");
560 println!();
561
562 let mut tools_to_restart: Vec<String> = newly_configured
563 .iter()
564 .map(std::string::ToString::to_string)
565 .collect();
566 for name in rules_result
567 .injected
568 .iter()
569 .chain(rules_result.updated.iter())
570 {
571 if !tools_to_restart.contains(name) {
572 tools_to_restart.push(name.clone());
573 }
574 }
575
576 if !tools_to_restart.is_empty() {
577 println!(" {cyan}2.{rst} {yellow}{bold}Restart your IDE / AI tool:{rst}");
578 println!(" {bold}{}{rst}", tools_to_restart.join(", "));
579 println!(
580 " {dim}Changes take effect after a full restart (MCP may be enabled or disabled depending on mode).{rst}"
581 );
582 println!(" {dim}Close and re-open the application completely.{rst}");
583 } else if !already_configured.is_empty() {
584 println!(
585 " {cyan}2.{rst} {dim}Your tools are already configured — no restart needed.{rst}"
586 );
587 }
588
589 println!();
590 println!(
591 " {dim}After restart, lean-ctx will automatically optimize every AI interaction.{rst}"
592 );
593 println!(" {dim}Verify with:{rst} {bold}lean-ctx gain{rst}");
594
595 println!();
596 terminal_ui::print_logo_animated();
597 terminal_ui::print_command_box();
598
599 crate::cli::show_first_run_wow();
600}