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 format_hook_script = hooks_dir.join("format-ie-output.sh");
76 let format_hook_content = include_str!("../../templates/format-ie-output.sh");
77
78 if format_hook_script.exists() && !opts.force {
79 return Err(IntentError::InvalidInput(format!(
80 "Format hook already exists: {}. Use --force to overwrite",
81 format_hook_script.display()
82 )));
83 }
84
85 if format_hook_script.exists() && !opts.dry_run {
86 if let Some(backup) = create_backup(&format_hook_script)? {
87 backups.push((format_hook_script.clone(), backup.clone()));
88 println!("✓ Backed up format hook to {}", backup.display());
89 }
90 }
91
92 if !opts.dry_run {
93 fs::write(&format_hook_script, format_hook_content).map_err(IntentError::IoError)?;
94 set_executable(&format_hook_script)?;
95 files_modified.push(format_hook_script.clone());
96 println!("✓ Installed {}", format_hook_script.display());
97 } else {
98 println!("Would write: {}", format_hook_script.display());
99 }
100
101 let settings_file = claude_dir.join("settings.json");
103 let hook_abs_path = resolve_absolute_path(&hook_script)?;
104 let format_hook_abs_path = resolve_absolute_path(&format_hook_script)?;
105
106 if settings_file.exists() && !opts.force {
107 return Err(IntentError::InvalidInput(format!(
108 "Settings file already exists: {}. Use --force to overwrite",
109 settings_file.display()
110 )));
111 }
112
113 if settings_file.exists() && !opts.dry_run {
114 if let Some(backup) = create_backup(&settings_file)? {
115 backups.push((settings_file.clone(), backup.clone()));
116 println!("✓ Backed up settings to {}", backup.display());
117 }
118 }
119
120 let settings = json!({
121 "hooks": {
122 "SessionStart": [{
123 "hooks": [{
124 "type": "command",
125 "command": hook_abs_path.to_string_lossy()
126 }]
127 }],
128 "PostToolUse": [
129 {
130 "matcher": "mcp__intent-engine__task_context",
131 "hooks": [{
132 "type": "command",
133 "command": format_hook_abs_path.to_string_lossy()
134 }]
135 },
136 {
137 "matcher": "mcp__intent-engine__task_get",
138 "hooks": [{
139 "type": "command",
140 "command": format_hook_abs_path.to_string_lossy()
141 }]
142 },
143 {
144 "matcher": "mcp__intent-engine__current_task_get",
145 "hooks": [{
146 "type": "command",
147 "command": format_hook_abs_path.to_string_lossy()
148 }]
149 },
150 {
151 "matcher": "mcp__intent-engine__task_list",
152 "hooks": [{
153 "type": "command",
154 "command": format_hook_abs_path.to_string_lossy()
155 }]
156 },
157 {
158 "matcher": "mcp__intent-engine__task_pick_next",
159 "hooks": [{
160 "type": "command",
161 "command": format_hook_abs_path.to_string_lossy()
162 }]
163 },
164 {
165 "matcher": "mcp__intent-engine__unified_search",
166 "hooks": [{
167 "type": "command",
168 "command": format_hook_abs_path.to_string_lossy()
169 }]
170 },
171 {
172 "matcher": "mcp__intent-engine__event_list",
173 "hooks": [{
174 "type": "command",
175 "command": format_hook_abs_path.to_string_lossy()
176 }]
177 }
178 ]
179 }
180 });
181
182 if !opts.dry_run {
183 write_json_config(&settings_file, &settings)?;
184 files_modified.push(settings_file.clone());
185 println!("✓ Created {}", settings_file.display());
186 } else {
187 println!("Would write: {}", settings_file.display());
188 }
189
190 let mcp_result = self.setup_mcp_config(opts, &mut files_modified, &mut backups)?;
192
193 Ok(SetupResult {
194 success: true,
195 message: "User-level Claude Code setup complete!".to_string(),
196 files_modified,
197 connectivity_test: Some(mcp_result),
198 })
199 }
200
201 fn setup_mcp_config(
203 &self,
204 opts: &SetupOptions,
205 files_modified: &mut Vec<PathBuf>,
206 backups: &mut Vec<(PathBuf, PathBuf)>,
207 ) -> Result<ConnectivityResult> {
208 let config_path = if let Some(ref path) = opts.config_path {
209 path.clone()
210 } else {
211 let home = get_home_dir()?;
212 home.join(".claude.json")
213 };
214
215 let binary_path = find_ie_binary()?;
217 println!("✓ Found binary: {}", binary_path.display());
218
219 let project_dir = if let Some(ref dir) = opts.project_dir {
221 dir.clone()
222 } else {
223 env::current_dir().map_err(IntentError::IoError)?
224 };
225 let project_dir_abs = resolve_absolute_path(&project_dir)?;
226
227 if config_path.exists() && !opts.dry_run {
229 if let Some(backup) = create_backup(&config_path)? {
230 backups.push((config_path.clone(), backup.clone()));
231 println!("✓ Backed up MCP config to {}", backup.display());
232 }
233 }
234
235 let mut config = read_json_config(&config_path)?;
237
238 if let Some(mcp_servers) = config.get("mcpServers") {
240 if mcp_servers.get("intent-engine").is_some() && !opts.force {
241 return Ok(ConnectivityResult {
242 passed: false,
243 details: "intent-engine already configured in MCP config".to_string(),
244 });
245 }
246 }
247
248 if config.get("mcpServers").is_none() {
250 config["mcpServers"] = json!({});
251 }
252
253 config["mcpServers"]["intent-engine"] = json!({
254 "command": binary_path.to_string_lossy(),
255 "args": ["mcp-server"],
256 "env": {
257 "INTENT_ENGINE_PROJECT_DIR": project_dir_abs.to_string_lossy()
258 },
259 "description": "Strategic intent and task workflow management"
260 });
261
262 if !opts.dry_run {
263 write_json_config(&config_path, &config)?;
264 files_modified.push(config_path.clone());
265 println!("✓ Updated {}", config_path.display());
266 } else {
267 println!("Would write: {}", config_path.display());
268 }
269
270 Ok(ConnectivityResult {
271 passed: true,
272 details: format!("MCP configured at {}", config_path.display()),
273 })
274 }
275
276 fn setup_project_level(&self, opts: &SetupOptions) -> Result<SetupResult> {
278 println!("📦 Setting up project-level Claude Code integration...\n");
279 println!("⚠️ Note: Project-level setup is for advanced users.");
280 println!(" MCP config will still be in ~/.claude.json (user-level)\n");
281
282 let mut files_modified = Vec::new();
283 let claude_dir = Self::get_project_claude_dir()?;
284 let hooks_dir = claude_dir.join("hooks");
285 let hook_script = hooks_dir.join("session-start.sh");
286
287 if !opts.dry_run {
288 fs::create_dir_all(&hooks_dir).map_err(IntentError::IoError)?;
289 println!("✓ Created {}", hooks_dir.display());
290 } else {
291 println!("Would create: {}", hooks_dir.display());
292 }
293
294 if hook_script.exists() && !opts.force {
296 return Err(IntentError::InvalidInput(format!(
297 "Hook script already exists: {}. Use --force to overwrite",
298 hook_script.display()
299 )));
300 }
301
302 let hook_content = include_str!("../../templates/session-start.sh");
304 if !opts.dry_run {
305 fs::write(&hook_script, hook_content).map_err(IntentError::IoError)?;
306 set_executable(&hook_script)?;
307 files_modified.push(hook_script.clone());
308 println!("✓ Installed {}", hook_script.display());
309 } else {
310 println!("Would write: {}", hook_script.display());
311 }
312
313 let format_hook_script = hooks_dir.join("format-ie-output.sh");
315 let format_hook_content = include_str!("../../templates/format-ie-output.sh");
316
317 if format_hook_script.exists() && !opts.force {
318 return Err(IntentError::InvalidInput(format!(
319 "Format hook already exists: {}. Use --force to overwrite",
320 format_hook_script.display()
321 )));
322 }
323
324 if !opts.dry_run {
325 fs::write(&format_hook_script, format_hook_content).map_err(IntentError::IoError)?;
326 set_executable(&format_hook_script)?;
327 files_modified.push(format_hook_script.clone());
328 println!("✓ Installed {}", format_hook_script.display());
329 } else {
330 println!("Would write: {}", format_hook_script.display());
331 }
332
333 let settings_file = claude_dir.join("settings.json");
335 let hook_abs_path = resolve_absolute_path(&hook_script)?;
336 let format_hook_abs_path = resolve_absolute_path(&format_hook_script)?;
337
338 if settings_file.exists() && !opts.force {
340 return Err(IntentError::InvalidInput(format!(
341 "Settings file already exists: {}. Use --force to overwrite",
342 settings_file.display()
343 )));
344 }
345
346 let settings = json!({
347 "hooks": {
348 "SessionStart": [{
349 "hooks": [{
350 "type": "command",
351 "command": hook_abs_path.to_string_lossy()
352 }]
353 }],
354 "PostToolUse": [
355 {
356 "matcher": "mcp__intent-engine__task_context",
357 "hooks": [{
358 "type": "command",
359 "command": format_hook_abs_path.to_string_lossy()
360 }]
361 },
362 {
363 "matcher": "mcp__intent-engine__task_get",
364 "hooks": [{
365 "type": "command",
366 "command": format_hook_abs_path.to_string_lossy()
367 }]
368 },
369 {
370 "matcher": "mcp__intent-engine__current_task_get",
371 "hooks": [{
372 "type": "command",
373 "command": format_hook_abs_path.to_string_lossy()
374 }]
375 },
376 {
377 "matcher": "mcp__intent-engine__task_list",
378 "hooks": [{
379 "type": "command",
380 "command": format_hook_abs_path.to_string_lossy()
381 }]
382 },
383 {
384 "matcher": "mcp__intent-engine__task_pick_next",
385 "hooks": [{
386 "type": "command",
387 "command": format_hook_abs_path.to_string_lossy()
388 }]
389 },
390 {
391 "matcher": "mcp__intent-engine__unified_search",
392 "hooks": [{
393 "type": "command",
394 "command": format_hook_abs_path.to_string_lossy()
395 }]
396 },
397 {
398 "matcher": "mcp__intent-engine__event_list",
399 "hooks": [{
400 "type": "command",
401 "command": format_hook_abs_path.to_string_lossy()
402 }]
403 }
404 ]
405 }
406 });
407
408 if !opts.dry_run {
409 write_json_config(&settings_file, &settings)?;
410 files_modified.push(settings_file);
411 println!("✓ Created settings.json");
412 } else {
413 println!("Would write: {}", settings_file.display());
414 }
415
416 let mut backups = Vec::new();
418 let mcp_result = self.setup_mcp_config(opts, &mut files_modified, &mut backups)?;
419
420 Ok(SetupResult {
421 success: true,
422 message: "Project-level setup complete!".to_string(),
423 files_modified,
424 connectivity_test: Some(mcp_result),
425 })
426 }
427}
428
429impl SetupModule for ClaudeCodeSetup {
430 fn name(&self) -> &str {
431 "claude-code"
432 }
433
434 fn setup(&self, opts: &SetupOptions) -> Result<SetupResult> {
435 match opts.scope {
436 SetupScope::User => self.setup_user_level(opts),
437 SetupScope::Project => self.setup_project_level(opts),
438 SetupScope::Both => {
439 let user_result = self.setup_user_level(opts)?;
441 let project_result = self.setup_project_level(opts)?;
442
443 let mut files = user_result.files_modified;
445 files.extend(project_result.files_modified);
446
447 Ok(SetupResult {
448 success: true,
449 message: "User and project setup complete!".to_string(),
450 files_modified: files,
451 connectivity_test: user_result.connectivity_test,
452 })
453 },
454 }
455 }
456
457 fn diagnose(&self) -> Result<DiagnosisReport> {
458 let mut checks = Vec::new();
459 let mut suggested_fixes = Vec::new();
460
461 let claude_dir = Self::get_user_claude_dir()?;
463 let hook_script = claude_dir.join("hooks").join("session-start.sh");
464
465 let hook_check = if hook_script.exists() {
466 if hook_script.metadata().map(|m| m.is_file()).unwrap_or(false) {
467 #[cfg(unix)]
468 {
469 use std::os::unix::fs::PermissionsExt;
470 let perms = hook_script.metadata().unwrap().permissions();
471 let is_executable = perms.mode() & 0o111 != 0;
472 if is_executable {
473 DiagnosisCheck {
474 name: "Hook script".to_string(),
475 passed: true,
476 details: format!("Found at {}", hook_script.display()),
477 }
478 } else {
479 suggested_fixes.push(format!("chmod +x {}", hook_script.display()));
480 DiagnosisCheck {
481 name: "Hook script".to_string(),
482 passed: false,
483 details: "Script exists but is not executable".to_string(),
484 }
485 }
486 }
487 #[cfg(not(unix))]
488 DiagnosisCheck {
489 name: "Hook script".to_string(),
490 passed: true,
491 details: format!("Found at {}", hook_script.display()),
492 }
493 } else {
494 DiagnosisCheck {
495 name: "Hook script".to_string(),
496 passed: false,
497 details: "Path exists but is not a file".to_string(),
498 }
499 }
500 } else {
501 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
502 DiagnosisCheck {
503 name: "Hook script".to_string(),
504 passed: false,
505 details: format!("Not found at {}", hook_script.display()),
506 }
507 };
508 checks.push(hook_check);
509
510 let format_hook_script = claude_dir.join("hooks").join("format-ie-output.sh");
512 let format_hook_check = if format_hook_script.exists() {
513 if format_hook_script
514 .metadata()
515 .map(|m| m.is_file())
516 .unwrap_or(false)
517 {
518 #[cfg(unix)]
519 {
520 use std::os::unix::fs::PermissionsExt;
521 let perms = format_hook_script.metadata().unwrap().permissions();
522 let is_executable = perms.mode() & 0o111 != 0;
523 if is_executable {
524 DiagnosisCheck {
525 name: "Format hook script".to_string(),
526 passed: true,
527 details: format!("Found at {}", format_hook_script.display()),
528 }
529 } else {
530 suggested_fixes.push(format!("chmod +x {}", format_hook_script.display()));
531 DiagnosisCheck {
532 name: "Format hook script".to_string(),
533 passed: false,
534 details: "Script exists but is not executable".to_string(),
535 }
536 }
537 }
538 #[cfg(not(unix))]
539 DiagnosisCheck {
540 name: "Format hook script".to_string(),
541 passed: true,
542 details: format!("Found at {}", format_hook_script.display()),
543 }
544 } else {
545 DiagnosisCheck {
546 name: "Format hook script".to_string(),
547 passed: false,
548 details: "Path exists but is not a file".to_string(),
549 }
550 }
551 } else {
552 suggested_fixes.push("Run: ie setup --target claude-code --force".to_string());
553 DiagnosisCheck {
554 name: "Format hook script".to_string(),
555 passed: false,
556 details: format!("Not found at {}", format_hook_script.display()),
557 }
558 };
559 checks.push(format_hook_check);
560
561 let settings_file = claude_dir.join("settings.json");
563 let settings_check = if settings_file.exists() {
564 match read_json_config(&settings_file) {
565 Ok(config) => {
566 if config
567 .get("hooks")
568 .and_then(|h| h.get("SessionStart"))
569 .is_some()
570 {
571 DiagnosisCheck {
572 name: "Settings file".to_string(),
573 passed: true,
574 details: "SessionStart hook configured".to_string(),
575 }
576 } else {
577 suggested_fixes
578 .push("Run: ie setup --target claude-code --force".to_string());
579 DiagnosisCheck {
580 name: "Settings file".to_string(),
581 passed: false,
582 details: "Missing SessionStart hook configuration".to_string(),
583 }
584 }
585 },
586 Err(_) => DiagnosisCheck {
587 name: "Settings file".to_string(),
588 passed: false,
589 details: "Failed to parse settings.json".to_string(),
590 },
591 }
592 } else {
593 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
594 DiagnosisCheck {
595 name: "Settings file".to_string(),
596 passed: false,
597 details: format!("Not found at {}", settings_file.display()),
598 }
599 };
600 checks.push(settings_check);
601
602 let posttool_check = if settings_file.exists() {
604 match read_json_config(&settings_file) {
605 Ok(config) => {
606 if config
607 .get("hooks")
608 .and_then(|h| h.get("PostToolUse"))
609 .is_some()
610 {
611 DiagnosisCheck {
612 name: "PostToolUse hooks".to_string(),
613 passed: true,
614 details: "PostToolUse hook configured".to_string(),
615 }
616 } else {
617 suggested_fixes
618 .push("Run: ie setup --target claude-code --force".to_string());
619 DiagnosisCheck {
620 name: "PostToolUse hooks".to_string(),
621 passed: false,
622 details: "Missing PostToolUse hook configuration".to_string(),
623 }
624 }
625 },
626 Err(_) => DiagnosisCheck {
627 name: "PostToolUse hooks".to_string(),
628 passed: false,
629 details: "Failed to parse settings.json".to_string(),
630 },
631 }
632 } else {
633 DiagnosisCheck {
634 name: "PostToolUse hooks".to_string(),
635 passed: false,
636 details: "Settings file not found".to_string(),
637 }
638 };
639 checks.push(posttool_check);
640
641 let home = get_home_dir()?;
643 let mcp_config = home.join(".claude.json");
644 let mcp_check = if mcp_config.exists() {
645 match read_json_config(&mcp_config) {
646 Ok(config) => {
647 if config
648 .get("mcpServers")
649 .and_then(|s| s.get("intent-engine"))
650 .is_some()
651 {
652 DiagnosisCheck {
653 name: "MCP configuration".to_string(),
654 passed: true,
655 details: "intent-engine MCP server configured".to_string(),
656 }
657 } else {
658 suggested_fixes
659 .push("Run: ie setup --target claude-code --force".to_string());
660 DiagnosisCheck {
661 name: "MCP configuration".to_string(),
662 passed: false,
663 details: "Missing intent-engine server entry".to_string(),
664 }
665 }
666 },
667 Err(_) => DiagnosisCheck {
668 name: "MCP configuration".to_string(),
669 passed: false,
670 details: "Failed to parse .claude.json".to_string(),
671 },
672 }
673 } else {
674 suggested_fixes.push("Run: ie setup --target claude-code".to_string());
675 DiagnosisCheck {
676 name: "MCP configuration".to_string(),
677 passed: false,
678 details: format!("Not found at {}", mcp_config.display()),
679 }
680 };
681 checks.push(mcp_check);
682
683 let binary_check = match find_ie_binary() {
685 Ok(path) => DiagnosisCheck {
686 name: "Binary availability".to_string(),
687 passed: true,
688 details: format!("Found at {}", path.display()),
689 },
690 Err(_) => {
691 suggested_fixes.push("Install: cargo install intent-engine".to_string());
692 DiagnosisCheck {
693 name: "Binary availability".to_string(),
694 passed: false,
695 details: "intent-engine not found in PATH".to_string(),
696 }
697 },
698 };
699 checks.push(binary_check);
700
701 let overall_status = checks.iter().all(|c| c.passed);
702
703 Ok(DiagnosisReport {
704 overall_status,
705 checks,
706 suggested_fixes,
707 })
708 }
709
710 fn test_connectivity(&self) -> Result<ConnectivityResult> {
711 println!("Testing session-restore command...");
713 let output = std::process::Command::new("ie")
714 .args(["session-restore", "--workspace", "."])
715 .output();
716
717 match output {
718 Ok(result) => {
719 if result.status.success() {
720 Ok(ConnectivityResult {
721 passed: true,
722 details: "session-restore command executed successfully".to_string(),
723 })
724 } else {
725 let stderr = String::from_utf8_lossy(&result.stderr);
726 Ok(ConnectivityResult {
727 passed: false,
728 details: format!("session-restore failed: {}", stderr),
729 })
730 }
731 },
732 Err(e) => Ok(ConnectivityResult {
733 passed: false,
734 details: format!("Failed to execute session-restore: {}", e),
735 }),
736 }
737 }
738}