tracevault_cli/hooks/
mod.rs1use std::path::Path;
2use tracevault_core::streaming::StreamEventRequest;
3
4#[allow(dead_code)]
6pub mod claude_code;
7#[allow(dead_code)]
8pub mod cursor;
9
10#[allow(dead_code)]
11pub trait HookAdapter: Send + Sync {
12 fn tool_name(&self) -> &str;
13 fn parse_event(&self, raw: &str) -> Result<StreamEventRequest, String>;
14 fn parse_transcript(&self, path: &Path) -> Result<Vec<serde_json::Value>, String>;
15}
16
17#[derive(Debug, Clone, Copy)]
18pub enum DetectedTool {
19 ClaudeCode,
20 Cursor,
21}
22
23impl DetectedTool {
24 pub fn name(&self) -> &str {
25 match self {
26 DetectedTool::ClaudeCode => "claude-code",
27 DetectedTool::Cursor => "cursor",
28 }
29 }
30
31 #[allow(dead_code)]
32 pub fn adapter(&self) -> Box<dyn HookAdapter> {
33 match self {
34 DetectedTool::ClaudeCode => Box::new(claude_code::ClaudeCodeAdapter),
35 DetectedTool::Cursor => Box::new(cursor::CursorAdapter),
36 }
37 }
38}
39
40pub fn detect_tools(cwd: &Path) -> Vec<DetectedTool> {
41 let mut tools = vec![];
42 if cwd.join(".claude").exists() {
43 tools.push(DetectedTool::ClaudeCode);
44 }
45 if cwd.join(".cursor").exists() {
46 tools.push(DetectedTool::Cursor);
47 }
48 tools
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use std::fs;
55
56 #[test]
57 fn detect_tools_claude_only() {
58 let dir = tempfile::tempdir().unwrap();
59 fs::create_dir(dir.path().join(".claude")).unwrap();
60 let tools = detect_tools(dir.path());
61 assert_eq!(tools.len(), 1);
62 assert!(matches!(tools[0], DetectedTool::ClaudeCode));
63 }
64
65 #[test]
66 fn detect_tools_neither() {
67 let dir = tempfile::tempdir().unwrap();
68 assert!(detect_tools(dir.path()).is_empty());
69 }
70
71 #[test]
72 fn detect_tools_both() {
73 let dir = tempfile::tempdir().unwrap();
74 fs::create_dir(dir.path().join(".claude")).unwrap();
75 fs::create_dir(dir.path().join(".cursor")).unwrap();
76 assert_eq!(detect_tools(dir.path()).len(), 2);
77 }
78}