intent_engine/setup/
claude_code.rs1use super::common::*;
4use super::{ConnectivityResult, SetupModule, SetupOptions, SetupResult, SetupScope};
5use crate::error::{IntentError, Result};
6use serde_json::json;
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11pub struct ClaudeCodeSetup;
12
13impl ClaudeCodeSetup {
14 fn get_user_claude_dir() -> Result<PathBuf> {
16 let home = get_home_dir()?;
17 Ok(home.join(".claude"))
18 }
19
20 fn get_project_claude_dir() -> Result<PathBuf> {
22 let current_dir = env::current_dir().map_err(IntentError::IoError)?;
23 Ok(current_dir.join(".claude"))
24 }
25
26 fn create_claude_settings(hook_path: &Path) -> serde_json::Value {
34 json!({
35 "hooks": {
36 "SessionStart": [{
37 "hooks": [{
38 "type": "command",
39 "command": hook_path.to_string_lossy()
40 }]
41 }]
42 }
43 })
44 }
45
46 fn setup_hooks_and_settings(
56 claude_dir: &Path,
57 opts: &SetupOptions,
58 files_modified: &mut Vec<PathBuf>,
59 ) -> Result<()> {
60 let hooks_dir = claude_dir.join("hooks");
61 let hook_script = hooks_dir.join("session-start.sh");
62
63 fs::create_dir_all(&hooks_dir).map_err(IntentError::IoError)?;
65 println!("✓ Created {}", hooks_dir.display());
66
67 if hook_script.exists() && !opts.force {
69 return Err(IntentError::InvalidInput(format!(
70 "Hook script already exists: {}. Use --force to overwrite",
71 hook_script.display()
72 )));
73 }
74
75 let hook_content = include_str!("../../templates/session-start.sh");
77 fs::write(&hook_script, hook_content).map_err(IntentError::IoError)?;
78 set_executable(&hook_script)?;
79 files_modified.push(hook_script.clone());
80 println!("✓ Installed {}", hook_script.display());
81
82 let settings_file = claude_dir.join("settings.json");
84 let hook_abs_path = resolve_absolute_path(&hook_script)?;
85
86 if settings_file.exists() && !opts.force {
88 return Err(IntentError::InvalidInput(format!(
89 "Settings file already exists: {}. Use --force to overwrite",
90 settings_file.display()
91 )));
92 }
93
94 let settings = Self::create_claude_settings(&hook_abs_path);
95
96 write_json_config(&settings_file, &settings)?;
97 files_modified.push(settings_file.clone());
98 println!("✓ Created {}", settings_file.display());
99
100 Ok(())
101 }
102
103 fn setup_user_level(&self, opts: &SetupOptions) -> Result<SetupResult> {
105 let mut files_modified = Vec::new();
106
107 println!("📦 Setting up user-level Claude Code integration...\n");
108
109 let claude_dir = Self::get_user_claude_dir()?;
111 Self::setup_hooks_and_settings(&claude_dir, opts, &mut files_modified)?;
112
113 let mcp_result = self.setup_mcp_config(opts, &mut files_modified)?;
115
116 Ok(SetupResult {
117 success: true,
118 message: "User-level Claude Code setup complete!".to_string(),
119 files_modified,
120 connectivity_test: Some(mcp_result),
121 })
122 }
123
124 fn setup_mcp_config(
126 &self,
127 opts: &SetupOptions,
128 files_modified: &mut Vec<PathBuf>,
129 ) -> Result<ConnectivityResult> {
130 let config_path = if let Some(ref path) = opts.config_path {
131 path.clone()
132 } else {
133 let home = get_home_dir()?;
134 home.join(".claude.json")
135 };
136
137 let binary_path = find_ie_binary()?;
139 println!("✓ Found binary: {}", binary_path.display());
140
141 let mut config = read_json_config(&config_path)?;
143
144 if let Some(mcp_servers) = config.get("mcpServers") {
146 if mcp_servers.get("intent-engine").is_some() && !opts.force {
147 return Ok(ConnectivityResult {
148 passed: false,
149 details: "intent-engine already configured in MCP config".to_string(),
150 });
151 }
152 }
153
154 if config.get("mcpServers").is_none() {
156 config["mcpServers"] = json!({});
157 }
158
159 config["mcpServers"]["intent-engine"] = json!({
160 "command": binary_path.to_string_lossy(),
161 "args": ["mcp-server"],
162 "description": "Strategic intent and task workflow management"
163 });
164
165 write_json_config(&config_path, &config)?;
166 files_modified.push(config_path.clone());
167 println!("✓ Updated {}", config_path.display());
168
169 Ok(ConnectivityResult {
170 passed: true,
171 details: format!("MCP configured at {}", config_path.display()),
172 })
173 }
174
175 fn setup_project_level(&self, opts: &SetupOptions) -> Result<SetupResult> {
177 println!("📦 Setting up project-level Claude Code integration...\n");
178 println!("⚠️ Note: Project-level setup is for advanced users.");
179 println!(" MCP config will still be in ~/.claude.json (user-level)\n");
180
181 let mut files_modified = Vec::new();
182
183 let claude_dir = Self::get_project_claude_dir()?;
185 Self::setup_hooks_and_settings(&claude_dir, opts, &mut files_modified)?;
186
187 let mcp_result = self.setup_mcp_config(opts, &mut files_modified)?;
189
190 Ok(SetupResult {
191 success: true,
192 message: "Project-level setup complete!".to_string(),
193 files_modified,
194 connectivity_test: Some(mcp_result),
195 })
196 }
197}
198
199impl SetupModule for ClaudeCodeSetup {
200 fn name(&self) -> &str {
201 "claude-code"
202 }
203
204 fn setup(&self, opts: &SetupOptions) -> Result<SetupResult> {
205 match opts.scope {
206 SetupScope::User => self.setup_user_level(opts),
207 SetupScope::Project => self.setup_project_level(opts),
208 SetupScope::Both => {
209 let user_result = self.setup_user_level(opts)?;
211 let project_result = self.setup_project_level(opts)?;
212
213 let mut files = user_result.files_modified;
215 files.extend(project_result.files_modified);
216
217 Ok(SetupResult {
218 success: true,
219 message: "User and project setup complete!".to_string(),
220 files_modified: files,
221 connectivity_test: user_result.connectivity_test,
222 })
223 },
224 }
225 }
226
227 fn test_connectivity(&self) -> Result<ConnectivityResult> {
228 println!("Testing session-restore command...");
230 let output = std::process::Command::new("ie")
231 .args(["session-restore", "--workspace", "."])
232 .output();
233
234 match output {
235 Ok(result) => {
236 if result.status.success() {
237 Ok(ConnectivityResult {
238 passed: true,
239 details: "session-restore command executed successfully".to_string(),
240 })
241 } else {
242 let stderr = String::from_utf8_lossy(&result.stderr);
243 Ok(ConnectivityResult {
244 passed: false,
245 details: format!("session-restore failed: {}", stderr),
246 })
247 }
248 },
249 Err(e) => Ok(ConnectivityResult {
250 passed: false,
251 details: format!("Failed to execute session-restore: {}", e),
252 }),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::path::PathBuf;
261
262 #[test]
265 fn test_create_claude_settings_structure() {
266 let hook_path = PathBuf::from("/tmp/session-start.sh");
267
268 let settings = ClaudeCodeSetup::create_claude_settings(&hook_path);
269
270 assert!(settings.get("hooks").is_some());
272
273 let hooks = &settings["hooks"];
275 assert!(hooks.get("SessionStart").is_some());
276 let session_start = &hooks["SessionStart"];
277 assert!(session_start.is_array());
278 assert_eq!(session_start.as_array().unwrap().len(), 1);
279 }
280
281 #[test]
282 fn test_create_claude_settings_session_start_hook() {
283 let hook_path = PathBuf::from("/home/user/.claude/hooks/session-start.sh");
284
285 let settings = ClaudeCodeSetup::create_claude_settings(&hook_path);
286
287 let session_start = &settings["hooks"]["SessionStart"][0];
288 assert!(session_start.get("hooks").is_some());
289
290 let hooks_array = session_start["hooks"].as_array().unwrap();
291 assert_eq!(hooks_array.len(), 1);
292
293 let hook = &hooks_array[0];
294 assert_eq!(hook["type"], "command");
295 assert_eq!(hook["command"], "/home/user/.claude/hooks/session-start.sh");
296 }
297
298 #[test]
301 fn test_get_user_claude_dir() {
302 let result = ClaudeCodeSetup::get_user_claude_dir();
304 assert!(result.is_ok());
305
306 let dir = result.unwrap();
307 assert!(dir.ends_with(".claude"));
308 }
309
310 #[test]
311 fn test_get_project_claude_dir() {
312 let result = ClaudeCodeSetup::get_project_claude_dir();
313 assert!(result.is_ok());
314
315 let dir = result.unwrap();
316 assert!(dir.ends_with(".claude"));
317 }
318
319 #[test]
320 fn test_claude_code_setup_name() {
321 let setup = ClaudeCodeSetup;
322 assert_eq!(setup.name(), "claude-code");
323 }
324
325 #[test]
328 fn test_create_claude_settings_paths_preserved() {
329 let hook_path = PathBuf::from("/home/user name/with spaces/.claude/hooks/session-start.sh");
331
332 let settings = ClaudeCodeSetup::create_claude_settings(&hook_path);
333
334 let session_start_cmd = settings["hooks"]["SessionStart"][0]["hooks"][0]["command"]
336 .as_str()
337 .unwrap();
338 assert!(session_start_cmd.contains("with spaces"));
339 }
340}