innate_core/install/
wizard.rs1use super::{agents::*, path::*, settings::*, skills::*, ui::*, *};
2
3pub 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 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 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 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(¤t_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 let auto_allow = prompt_confirm(
103 "Auto-allow Innate MCP tools? (Skips permission prompts in agents)",
104 true,
105 );
106
107 configure_llm_interactive();
109
110 configure_daemon_interactive();
112
113 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 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 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 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 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 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 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 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}