Skip to main content

innate_core/install/
wizard.rs

1use super::{agents::*, path::*, settings::*, skills::*, ui::*, *};
2
3// ── Main entry point ──────────────────────────────────────────────────────────
4
5pub fn run_install() -> anyhow::Result<()> {
6    let version = env!("CARGO_PKG_VERSION");
7    box_open(&format!("Innate v{version}"));
8
9    let current_exe = std::env::current_exe()?;
10
11    // ── 1. Scope: global vs project ────────────────────────────────────────
12    let scope_options = ["All projects (global)", "Just this project"];
13    let scope_idx = prompt_select(
14        "Apply agent configs to all your projects, or just this one?",
15        &scope_options,
16    );
17    let global = scope_idx == 0;
18
19    // ── 2. Agent selection ─────────────────────────────────────────────────
20    let agents = detect_agents(global);
21    let options: Vec<(&str, bool)> = agents
22        .iter()
23        .map(|a| (a.label.as_str(), a.detected))
24        .collect();
25    let selected = prompt_multi_select("Which agents should Innate configure?", &options);
26    let chosen_agents: Vec<&Agent> = agents
27        .iter()
28        .zip(selected.iter())
29        .filter(|(_, &s)| s)
30        .map(|(a, _)| a)
31        .collect();
32
33    // ── 3. PATH installation ───────────────────────────────────────────────
34    // When the binary is on PATH, write just "innate" into agent configs so
35    // the config is portable across machines and users.  Only fall back to an
36    // absolute path when the user declines PATH installation.
37    let on_path = check_on_path();
38    let binary_path: PathBuf = if let Some(p) = &on_path {
39        question("Install innate CLI on your PATH?");
40        info(&format!(
41            "Already on PATH {}",
42            gray(&format!("({})", p.display()))
43        ));
44        sep();
45        PathBuf::from(binary_name())
46    } else {
47        let do_install = prompt_confirm(
48            "Install innate CLI on your PATH? (Required for agents to launch the MCP server)",
49            true,
50        );
51        if do_install {
52            match install_to_path(&current_exe) {
53                Ok(dest) => {
54                    result_line(&format!(
55                        "Installed innate to {}",
56                        bold(&dest.display().to_string())
57                    ));
58                    if !path_has_local_bin() {
59                        let written = write_path_to_profiles();
60                        if written.is_empty() {
61                            #[cfg(windows)]
62                            warn_line(&yellow(
63                                "Add .local\\bin to PATH:\
64                                \n│    [Environment]::SetEnvironmentVariable('PATH', $env:USERPROFILE + '\\.local\\bin;' + $env:PATH, 'User')",
65                            ));
66                            #[cfg(not(windows))]
67                            warn_line(&yellow(
68                                "Add ~/.local/bin to PATH in your shell profile:\
69                                \n│    export PATH=\"$HOME/.local/bin:$PATH\"",
70                            ));
71                        } else {
72                            for p in &written {
73                                result_line(&format!(
74                                    "Added PATH export to {}",
75                                    bold(&tilde_path(p))
76                                ));
77                            }
78                            #[cfg(windows)]
79                            info(&dim(
80                                "Open a new terminal for the PATH change to take effect",
81                            ));
82                            #[cfg(not(windows))]
83                            info(&dim("Run: source ~/.bashrc  (or open a new terminal)"));
84                        }
85                    }
86                    sep();
87                    PathBuf::from(binary_name())
88                }
89                Err(e) => {
90                    warn_line(&format!("Could not install to PATH: {e}"));
91                    info("Falling back to current binary location");
92                    sep();
93                    current_exe.clone()
94                }
95            }
96        } else {
97            current_exe.clone()
98        }
99    };
100
101    // ── 4. Auto-allow ──────────────────────────────────────────────────────
102    let auto_allow = prompt_confirm(
103        "Auto-allow Innate MCP tools? (Skips permission prompts in agents)",
104        true,
105    );
106
107    // ── 5. LLM configuration (optional) ───────────────────────────────────
108    configure_llm_interactive();
109
110    // ── 5b. Daemon watch dirs (optional) ──────────────────────────────────
111    configure_daemon_interactive();
112
113    // ── 6. Apply configs ───────────────────────────────────────────────────
114    for agent in &chosen_agents {
115        let status = match agent.id {
116            "claude" => configure_claude(agent, &binary_path, auto_allow),
117            "codex" => configure_codex(agent, &binary_path, auto_allow),
118            "opencode" => configure_opencode(agent, &binary_path, auto_allow),
119            _ => ConfigStatus::Skipped("unknown agent".into()),
120        };
121
122        match &status {
123            ConfigStatus::Updated(p) => {
124                result_line(&format!(
125                    "{}: Updated {}",
126                    bold(agent.id),
127                    gray(&tilde_path(p))
128                ));
129            }
130            ConfigStatus::Unchanged(p) => {
131                result_line(&format!(
132                    "{}: {}",
133                    bold(agent.id),
134                    gray(&format!("Unchanged {}", tilde_path(p)))
135                ));
136            }
137            ConfigStatus::Skipped(reason) => {
138                warn_line(&format!(
139                    "{}: {}",
140                    bold(agent.id),
141                    yellow(&format!("Skipped — {reason}"))
142                ));
143            }
144            ConfigStatus::Error(e) => {
145                warn_line(&format!("{}: \x1b[31mError — {e}\x1b[0m", bold(agent.id)));
146            }
147        }
148
149        if agent.id == "claude" {
150            // Hooks must live in a settings.json file — Claude Code does NOT read hooks from
151            // ~/.claude.json (that file is MCP/OAuth/state only). In global scope agent.config
152            // points at ~/.claude.json (correct for MCP), so derive the hooks target separately.
153            let hook_config = if global {
154                home_dir().join(".claude").join("settings.json")
155            } else {
156                agent.config.clone()
157            };
158            match install_skill() {
159                ConfigStatus::Updated(p) => {
160                    result_line(&format!(
161                        "{}: Installed skill {}",
162                        bold("claude"),
163                        gray(&tilde_path(&p))
164                    ));
165                }
166                ConfigStatus::Unchanged(p) => {
167                    result_line(&format!(
168                        "{}: {}",
169                        bold("claude"),
170                        gray(&format!("Skill unchanged {}", tilde_path(&p)))
171                    ));
172                }
173                ConfigStatus::Skipped(reason) => {
174                    warn_line(&format!(
175                        "{}: {}",
176                        bold("claude"),
177                        yellow(&format!("Skill skipped — {reason}"))
178                    ));
179                }
180                ConfigStatus::Error(e) => {
181                    warn_line(&format!(
182                        "{}: \x1b[31mSkill error — {e}\x1b[0m",
183                        bold("claude")
184                    ));
185                }
186            }
187
188            // Install slash commands alongside the skill
189            for (name, status) in install_commands() {
190                match status {
191                    ConfigStatus::Updated(p) => {
192                        result_line(&format!(
193                            "{}: /{name} {}",
194                            bold("claude"),
195                            gray(&tilde_path(&p))
196                        ));
197                    }
198                    ConfigStatus::Unchanged(_) => {}
199                    ConfigStatus::Skipped(_) => {}
200                    ConfigStatus::Error(e) => {
201                        warn_line(&format!(
202                            "{}: \x1b[31mCommand /{name} error — {e}\x1b[0m",
203                            bold("claude")
204                        ));
205                    }
206                }
207            }
208
209            // Install Stop hook so daemon gets session events automatically.
210            match configure_claude_stop_hook(&hook_config, &binary_path) {
211                ConfigStatus::Updated(p) => {
212                    result_line(&format!(
213                        "{}: Stop hook → {}",
214                        bold("claude"),
215                        gray(&tilde_path(&p))
216                    ));
217                }
218                ConfigStatus::Unchanged(_) => {}
219                ConfigStatus::Skipped(_) => {}
220                ConfigStatus::Error(e) => {
221                    warn_line(&format!(
222                        "{}: \x1b[31mStop hook error — {e}\x1b[0m",
223                        bold("claude")
224                    ));
225                }
226            }
227
228            // Install UserPromptSubmit hook: relevance-gated auto-recall on every prompt.
229            match configure_claude_prompt_hook(&hook_config, &binary_path) {
230                ConfigStatus::Updated(p) => {
231                    result_line(&format!(
232                        "{}: UserPromptSubmit hook → {}",
233                        bold("claude"),
234                        gray(&tilde_path(&p))
235                    ));
236                }
237                ConfigStatus::Unchanged(_) => {}
238                ConfigStatus::Skipped(_) => {}
239                ConfigStatus::Error(e) => {
240                    warn_line(&format!(
241                        "{}: \x1b[31mUserPromptSubmit hook error — {e}\x1b[0m",
242                        bold("claude")
243                    ));
244                }
245            }
246
247            // Install SessionStart hook: warm up project knowledge at session start.
248            match configure_claude_session_start_hook(&hook_config, &binary_path) {
249                ConfigStatus::Updated(p) => {
250                    result_line(&format!(
251                        "{}: SessionStart hook → {}",
252                        bold("claude"),
253                        gray(&tilde_path(&p))
254                    ));
255                }
256                ConfigStatus::Unchanged(_) => {}
257                ConfigStatus::Skipped(_) => {}
258                ConfigStatus::Error(e) => {
259                    warn_line(&format!(
260                        "{}: \x1b[31mSessionStart hook error — {e}\x1b[0m",
261                        bold("claude")
262                    ));
263                }
264            }
265
266            // Install SubagentStop hook so Task-tool subagents also feed session events.
267            match configure_claude_subagent_stop_hook(&hook_config, &binary_path) {
268                ConfigStatus::Updated(p) => {
269                    result_line(&format!(
270                        "{}: SubagentStop hook → {}",
271                        bold("claude"),
272                        gray(&tilde_path(&p))
273                    ));
274                }
275                ConfigStatus::Unchanged(_) => {}
276                ConfigStatus::Skipped(_) => {}
277                ConfigStatus::Error(e) => {
278                    warn_line(&format!(
279                        "{}: \x1b[31mSubagentStop hook error — {e}\x1b[0m",
280                        bold("claude")
281                    ));
282                }
283            }
284        }
285    }
286    sep();
287
288    // ── 7. Quick start ─────────────────────────────────────────────────────
289    // Box: inner display width = 28.  Total row width = │  {28}│ = 32 chars.
290    // Header dashes = 28 - 12 ("Quick start ") = 16.
291    // Bottom dashes = 28 + 2 = 30.
292    const INNER: usize = 28;
293    let bar = gray("│");
294    let qs_top = format!(
295        "{}  Quick start {}{}",
296        cyan("◇"),
297        gray(&"─".repeat(INNER - 12)),
298        gray("╮")
299    );
300    let qs_row = |s: &str| -> String {
301        let pad = INNER.saturating_sub(s.chars().count());
302        format!("{bar}  {s}{}{bar}", " ".repeat(pad))
303    };
304    let qs_empty = qs_row("");
305    let qs_sep = format!("{}{}╯", gray("├"), gray(&"─".repeat(INNER + 2)));
306
307    println!("{qs_top}");
308    println!("{qs_empty}");
309    println!("{}", qs_row("innate recall \"query\""));
310    println!("{}", qs_row("innate record <trace_id>"));
311    println!("{}", qs_row("innate evolve"));
312    println!("{qs_empty}");
313    println!("{qs_sep}");
314
315    box_close("Done! Restart your agents to use Innate.");
316    Ok(())
317}