1use super::common::*;
4use super::{
5 ConnectivityResult, DiagnosisCheck, DiagnosisReport, SetupModule, SetupOptions, SetupResult,
6 SetupScope,
7};
8use crate::error::{IntentError, Result};
9use serde_json::json;
10use std::env;
11use std::fs;
12use std::path::PathBuf;
13
14pub struct ClaudeCodeSetup;
15
16impl ClaudeCodeSetup {
17 fn get_user_claude_dir() -> Result<PathBuf> {
19 let home = get_home_dir()?;
20 Ok(home.join(".claude"))
21 }
22
23 fn get_project_claude_dir() -> Result<PathBuf> {
25 let current_dir = env::current_dir().map_err(IntentError::IoError)?;
26 Ok(current_dir.join(".claude"))
27 }
28
29 fn setup_user_level(&self, opts: &SetupOptions) -> Result<SetupResult> {
31 let mut files_modified = Vec::new();
32 let mut backups = Vec::new();
33
34 println!("📦 Setting up user-level Claude Code integration...\n");
35
36 let claude_dir = Self::get_user_claude_dir()?;
38 let hooks_dir = claude_dir.join("hooks");
39 let hook_script = hooks_dir.join("session-start.sh");
40
41 if !opts.dry_run {
42 fs::create_dir_all(&hooks_dir).map_err(IntentError::IoError)?;
43 println!("✓ Created {}", hooks_dir.display());
44 } else {
45 println!("Would create: {}", hooks_dir.display());
46 }
47
48 if hook_script.exists() && !opts.force {
50 return Err(IntentError::InvalidInput(format!(
51 "Hook script already exists: {}. Use --force to overwrite",
52 hook_script.display()
53 )));
54 }
55
56 if hook_script.exists() && !opts.dry_run {
57 if let Some(backup) = create_backup(&hook_script)? {
58 backups.push((hook_script.clone(), backup.clone()));
59 println!("✓ Backed up hook script to {}", backup.display());
60 }
61 }
62
63 let hook_content = include_str!("../../templates/session-start.sh");
65 if !opts.dry_run {
66 fs::write(&hook_script, hook_content).map_err(IntentError::IoError)?;
67 set_executable(&hook_script)?;
68 files_modified.push(hook_script.clone());
69 println!("✓ Installed {}", hook_script.display());
70 } else {
71 println!("Would write: {}", hook_script.display());
72 }
73
74 let settings_file = claude_dir.join("settings.json");
76 let hook_abs_path = resolve_absolute_path(&hook_script)?;
77
78 if settings_file.exists() && !opts.force {
79 return Err(IntentError::InvalidInput(format!(
80 "Settings file already exists: {}. Use --force to overwrite",
81 settings_file.display()
82 )));
83 }
84
85 if settings_file.exists() && !opts.dry_run {
86 if let Some(backup) = create_backup(&settings_file)? {
87 backups.push((settings_file.clone(), backup.clone()));
88 println!("✓ Backed up settings to {}", backup.display());
89 }
90 }
91
92 let settings = json!({
93 "hooks": {
94 "SessionStart": [{
95 "hooks": [{
96 "type": "command",
97 "command": hook_abs_path.to_string_lossy()
98 }]
99 }]
100 }
101 });
102
103 if !opts.dry_run {
104 write_json_config(&settings_file, &settings)?;
105 files_modified.push(settings_file.clone());
106 println!("✓ Created {}", settings_file.display());
107 } else {
108 println!("Would write: {}", settings_file.display());
109 }
110
111 let mcp_result = self.setup_mcp_config(opts, &mut files_modified, &mut backups)?;
113
114 Ok(SetupResult {
115 success: true,
116 message: "User-level Claude Code setup complete!".to_string(),
117 files_modified,
118 connectivity_test: Some(mcp_result),
119 })
120 }
121
122 fn setup_mcp_config(
124 &self,
125 opts: &SetupOptions,
126 files_modified: &mut Vec<PathBuf>,
127 backups: &mut Vec<(PathBuf, PathBuf)>,
128 ) -> Result<ConnectivityResult> {
129 let config_path = if let Some(ref path) = opts.config_path {
130 path.clone()
131 } else {
132 let home = get_home_dir()?;
133 home.join(".claude.json")
134 };
135
136 let binary_path = find_ie_binary()?;
138 println!("✓ Found binary: {}", binary_path.display());
139
140 let project_dir = if let Some(ref dir) = opts.project_dir {
142 dir.clone()
143 } else {
144 env::current_dir().map_err(IntentError::IoError)?
145 };
146 let project_dir_abs = resolve_absolute_path(&project_dir)?;
147
148 if config_path.exists() && !opts.dry_run {
150 if let Some(backup) = create_backup(&config_path)? {
151 backups.push((config_path.clone(), backup.clone()));
152 println!("✓ Backed up MCP config to {}", backup.display());
153 }
154 }
155
156 let mut config = read_json_config(&config_path)?;
158
159 if let Some(mcp_servers) = config.get("mcpServers") {
161 if mcp_servers.get("intent-engine").is_some() && !opts.force {
162 return Ok(ConnectivityResult {
163 passed: false,
164 details: "intent-engine already configured in MCP config".to_string(),
165 });
166 }
167 }
168
169 if config.get("mcpServers").is_none() {
171 config["mcpServers"] = json!({});
172 }
173
174 config["mcpServers"]["intent-engine"] = json!({
175 "command": binary_path.to_string_lossy(),
176 "args": ["mcp-server"],
177 "env": {
178 "INTENT_ENGINE_PROJECT_DIR": project_dir_abs.to_string_lossy()
179 },
180 "description": "Strategic intent and task workflow management"
181 });
182
183 if !opts.dry_run {
184 write_json_config(&config_path, &config)?;
185 files_modified.push(config_path.clone());
186 println!("✓ Updated {}", config_path.display());
187 } else {
188 println!("Would write: {}", config_path.display());
189 }
190
191 Ok(ConnectivityResult {
192 passed: true,
193 details: format!("MCP configured at {}", config_path.display()),
194 })
195 }
196
197 fn setup_project_level(&self, opts: &SetupOptions) -> Result<SetupResult> {
199 println!("📦 Setting up project-level Claude Code integration...\n");
200 println!("⚠️ Note: Project-level setup is for advanced users.");
201 println!(" MCP config will still be in ~/.claude.json (user-level)\n");
202
203 let mut files_modified = Vec::new();
204 let claude_dir = Self::get_project_claude_dir()?;
205 let hooks_dir = claude_dir.join("hooks");
206 let hook_script = hooks_dir.join("session-start.sh");
207
208 if !opts.dry_run {
209 fs::create_dir_all(&hooks_dir).map_err(IntentError::IoError)?;
210 println!("✓ Created {}", hooks_dir.display());
211 } else {
212 println!("Would create: {}", hooks_dir.display());
213 }
214
215 if hook_script.exists() && !opts.force {
217 return Err(IntentError::InvalidInput(format!(
218 "Hook script already exists: {}. Use --force to overwrite",
219 hook_script.display()
220 )));
221 }
222
223 let hook_content = include_str!("../../templates/session-start.sh");
225 if !opts.dry_run {
226 fs::write(&hook_script, hook_content).map_err(IntentError::IoError)?;
227 set_executable(&hook_script)?;
228 files_modified.push(hook_script.clone());
229 println!("✓ Installed {}", hook_script.display());
230 } else {
231 println!("Would write: {}", hook_script.display());
232 }
233
234 let settings_file = claude_dir.join("settings.json");
236 let hook_abs_path = resolve_absolute_path(&hook_script)?;
237
238 if settings_file.exists() && !opts.force {
240 return Err(IntentError::InvalidInput(format!(
241 "Settings file already exists: {}. Use --force to overwrite",
242 settings_file.display()
243 )));
244 }
245
246 let settings = json!({
247 "hooks": {
248 "SessionStart": [{
249 "hooks": [{
250 "type": "command",
251 "command": hook_abs_path.to_string_lossy()
252 }]
253 }]
254 }
255 });
256
257 if !opts.dry_run {
258 write_json_config(&settings_file, &settings)?;
259 files_modified.push(settings_file);
260 println!("✓ Created settings.json");
261 } else {
262 println!("Would write: {}", settings_file.display());
263 }
264
265 let mut backups = Vec::new();
267 let mcp_result = self.setup_mcp_config(opts, &mut files_modified, &mut backups)?;
268
269 Ok(SetupResult {
270 success: true,
271 message: "Project-level setup complete!".to_string(),
272 files_modified,
273 connectivity_test: Some(mcp_result),
274 })
275 }
276}
277
278impl SetupModule for ClaudeCodeSetup {
279 fn name(&self) -> &str {
280 "claude-code"
281 }
282
283 fn setup(&self, opts: &SetupOptions) -> Result<SetupResult> {
284 match opts.scope {
285 SetupScope::User => self.setup_user_level(opts),
286 SetupScope::Project => self.setup_project_level(opts),
287 SetupScope::Both => {
288 let user_result = self.setup_user_level(opts)?;
290 let project_result = self.setup_project_level(opts)?;
291
292 let mut files = user_result.files_modified;
294 files.extend(project_result.files_modified);
295
296 Ok(SetupResult {
297 success: true,
298 message: "User and project setup complete!".to_string(),
299 files_modified: files,
300 connectivity_test: user_result.connectivity_test,
301 })
302 },
303 }
304 }
305
306 fn diagnose(&self) -> Result<DiagnosisReport> {
307 let mut checks = Vec::new();
308 let mut suggested_fixes = Vec::new();
309
310 let claude_dir = Self::get_user_claude_dir()?;
312 let hook_script = claude_dir.join("hooks").join("session-start.sh");
313
314 let hook_check = if hook_script.exists() {
315 if hook_script.metadata().map(|m| m.is_file()).unwrap_or(false) {
316 #[cfg(unix)]
317 {
318 use std::os::unix::fs::PermissionsExt;
319 let perms = hook_script.metadata().unwrap().permissions();
320 let is_executable = perms.mode() & 0o111 != 0;
321 if is_executable {
322 DiagnosisCheck {
323 name: "Hook script".to_string(),
324 passed: true,
325 details: format!("Found at {}", hook_script.display()),
326 }
327 } else {
328 suggested_fixes.push(format!("chmod +x {}", hook_script.display()));
329 DiagnosisCheck {
330 name: "Hook script".to_string(),
331 passed: false,
332 details: "Script exists but is not executable".to_string(),
333 }
334 }
335 }
336 #[cfg(not(unix))]
337 DiagnosisCheck {
338 name: "Hook script".to_string(),
339 passed: true,
340 details: format!("Found at {}", hook_script.display()),
341 }
342 } else {
343 DiagnosisCheck {
344 name: "Hook script".to_string(),
345 passed: false,
346 details: "Path exists but is not a file".to_string(),
347 }
348 }
349 } else {
350 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
351 DiagnosisCheck {
352 name: "Hook script".to_string(),
353 passed: false,
354 details: format!("Not found at {}", hook_script.display()),
355 }
356 };
357 checks.push(hook_check);
358
359 let settings_file = claude_dir.join("settings.json");
361 let settings_check = if settings_file.exists() {
362 match read_json_config(&settings_file) {
363 Ok(config) => {
364 if config
365 .get("hooks")
366 .and_then(|h| h.get("SessionStart"))
367 .is_some()
368 {
369 DiagnosisCheck {
370 name: "Settings file".to_string(),
371 passed: true,
372 details: "SessionStart hook configured".to_string(),
373 }
374 } else {
375 suggested_fixes
376 .push("Run: ie setup --target claude-code --force".to_string());
377 DiagnosisCheck {
378 name: "Settings file".to_string(),
379 passed: false,
380 details: "Missing SessionStart hook configuration".to_string(),
381 }
382 }
383 },
384 Err(_) => DiagnosisCheck {
385 name: "Settings file".to_string(),
386 passed: false,
387 details: "Failed to parse settings.json".to_string(),
388 },
389 }
390 } else {
391 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
392 DiagnosisCheck {
393 name: "Settings file".to_string(),
394 passed: false,
395 details: format!("Not found at {}", settings_file.display()),
396 }
397 };
398 checks.push(settings_check);
399
400 let home = get_home_dir()?;
402 let mcp_config = home.join(".claude.json");
403 let mcp_check = if mcp_config.exists() {
404 match read_json_config(&mcp_config) {
405 Ok(config) => {
406 if config
407 .get("mcpServers")
408 .and_then(|s| s.get("intent-engine"))
409 .is_some()
410 {
411 DiagnosisCheck {
412 name: "MCP configuration".to_string(),
413 passed: true,
414 details: "intent-engine MCP server configured".to_string(),
415 }
416 } else {
417 suggested_fixes
418 .push("Run: ie setup --target claude-code --force".to_string());
419 DiagnosisCheck {
420 name: "MCP configuration".to_string(),
421 passed: false,
422 details: "Missing intent-engine server entry".to_string(),
423 }
424 }
425 },
426 Err(_) => DiagnosisCheck {
427 name: "MCP configuration".to_string(),
428 passed: false,
429 details: "Failed to parse .claude.json".to_string(),
430 },
431 }
432 } else {
433 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
434 DiagnosisCheck {
435 name: "MCP configuration".to_string(),
436 passed: false,
437 details: format!("Not found at {}", mcp_config.display()),
438 }
439 };
440 checks.push(mcp_check);
441
442 let binary_check = match find_ie_binary() {
444 Ok(path) => DiagnosisCheck {
445 name: "Binary availability".to_string(),
446 passed: true,
447 details: format!("Found at {}", path.display()),
448 },
449 Err(_) => {
450 suggested_fixes.push("Install: cargo install intent-engine".to_string());
451 DiagnosisCheck {
452 name: "Binary availability".to_string(),
453 passed: false,
454 details: "intent-engine not found in PATH".to_string(),
455 }
456 },
457 };
458 checks.push(binary_check);
459
460 let overall_status = checks.iter().all(|c| c.passed);
461
462 Ok(DiagnosisReport {
463 overall_status,
464 checks,
465 suggested_fixes,
466 })
467 }
468
469 fn test_connectivity(&self) -> Result<ConnectivityResult> {
470 println!("Testing session-restore command...");
472 let output = std::process::Command::new("ie")
473 .args(["session-restore", "--workspace", "."])
474 .output();
475
476 match output {
477 Ok(result) => {
478 if result.status.success() {
479 Ok(ConnectivityResult {
480 passed: true,
481 details: "session-restore command executed successfully".to_string(),
482 })
483 } else {
484 let stderr = String::from_utf8_lossy(&result.stderr);
485 Ok(ConnectivityResult {
486 passed: false,
487 details: format!("session-restore failed: {}", stderr),
488 })
489 }
490 },
491 Err(e) => Ok(ConnectivityResult {
492 passed: false,
493 details: format!("Failed to execute session-restore: {}", e),
494 }),
495 }
496 }
497}