tsk/commands/
debug.rs

1use super::Command;
2use crate::agent::AgentProvider;
3use crate::assets::layered::LayeredAssetManager;
4use crate::context::AppContext;
5use crate::docker::DockerManager;
6use crate::docker::composer::DockerComposer;
7use crate::docker::image_manager::DockerImageManager;
8use crate::docker::template_manager::DockerTemplateManager;
9use crate::git::RepoManager;
10use crate::repo_utils::find_repository_root;
11use crate::task::Task;
12use crate::task_runner::TaskRunner;
13use async_trait::async_trait;
14use std::error::Error;
15use std::path::Path;
16use std::sync::Arc;
17
18pub struct DebugCommand {
19    pub name: String,
20    pub agent: Option<String>,
21    pub tech_stack: Option<String>,
22    pub project: Option<String>,
23}
24
25#[async_trait]
26impl Command for DebugCommand {
27    async fn execute(&self, ctx: &AppContext) -> Result<(), Box<dyn Error>> {
28        println!("Starting debug session: {}", self.name);
29
30        let repo_root = find_repository_root(Path::new("."))?;
31
32        // Auto-detect tech_stack if not provided
33        let tech_stack = match &self.tech_stack {
34            Some(ts) => {
35                println!("Using tech stack: {ts}");
36                ts.clone()
37            }
38            None => match ctx.repository_context().detect_tech_stack(&repo_root).await {
39                Ok(detected) => {
40                    println!("Auto-detected tech stack: {detected}");
41                    detected
42                }
43                Err(e) => {
44                    eprintln!("Warning: Failed to detect tech stack: {e}. Using default.");
45                    "default".to_string()
46                }
47            },
48        };
49
50        // Auto-detect project if not provided
51        let project = match &self.project {
52            Some(p) => {
53                println!("Using project: {p}");
54                Some(p.clone())
55            }
56            None => {
57                match ctx
58                    .repository_context()
59                    .detect_project_name(&repo_root)
60                    .await
61                {
62                    Ok(detected) => {
63                        println!("Auto-detected project name: {detected}");
64                        Some(detected)
65                    }
66                    Err(e) => {
67                        eprintln!("Warning: Failed to detect project name: {e}. Using default.");
68                        Some("default".to_string())
69                    }
70                }
71            }
72        };
73
74        // Create a debug instructions file
75        let debug_instructions = format!(
76            "# Debug Session: {}\n\nThis is an interactive debug session for exploring and testing.",
77            self.name
78        );
79        let temp_dir = ctx.xdg_directories().runtime_dir().join("tmp");
80        ctx.file_system().create_dir(&temp_dir).await?;
81        let instructions_file = temp_dir.join(format!("{}-debug.md", self.name));
82        ctx.file_system()
83            .write_file(&instructions_file, &debug_instructions)
84            .await?;
85
86        // Get current commit for the task
87        let source_commit = ctx
88            .git_operations()
89            .get_current_commit(&repo_root)
90            .await
91            .unwrap_or_else(|_| "HEAD".to_string());
92
93        // Create a minimal task for debug session
94        let timestamp = chrono::Local::now();
95        let task_id = format!("{}-debug-{}", timestamp.format("%Y-%m-%d-%H%M"), self.name);
96        let branch_name = format!("tsk/{task_id}");
97
98        let agent = self
99            .agent
100            .clone()
101            .unwrap_or_else(|| AgentProvider::default_agent().to_string());
102
103        let mut task = Task::new(
104            task_id.clone(),
105            repo_root.clone(),
106            self.name.clone(),
107            "debug".to_string(),
108            instructions_file.to_string_lossy().to_string(),
109            agent,
110            0, // No timeout for debug sessions
111            branch_name,
112            source_commit,
113            tech_stack,
114            project.unwrap_or_else(|| "default".to_string()),
115            chrono::Local::now(),
116            repo_root.clone(), // temporary, will be updated after repo copy
117        );
118
119        let repo_manager = RepoManager::new(
120            ctx.xdg_directories(),
121            ctx.file_system(),
122            ctx.git_operations(),
123        );
124
125        // Copy the repository for the debug task
126        let (copied_repo_path, _) = repo_manager
127            .copy_repo(&task_id, &repo_root, Some(&task.source_commit))
128            .await
129            .map_err(|e| format!("Failed to copy repository: {e}"))?;
130
131        // Update the task with the copied repository path
132        task.copied_repo_path = copied_repo_path;
133        let docker_manager = DockerManager::new(ctx.docker_client(), ctx.file_system());
134
135        // Create image manager on-demand for the task's repository
136        let asset_manager = Arc::new(LayeredAssetManager::new_with_standard_layers(
137            Some(&repo_root),
138            &ctx.xdg_directories(),
139        ));
140        let template_manager =
141            DockerTemplateManager::new(asset_manager.clone(), ctx.xdg_directories());
142        let composer = DockerComposer::new(DockerTemplateManager::new(
143            asset_manager,
144            ctx.xdg_directories(),
145        ));
146        let image_manager = Arc::new(DockerImageManager::new(
147            ctx.docker_client(),
148            template_manager,
149            composer,
150        ));
151
152        let task_runner = TaskRunner::new(
153            repo_manager,
154            docker_manager,
155            image_manager,
156            ctx.file_system(),
157            ctx.notification_client(),
158        );
159
160        // Execute task in interactive mode
161        task_runner
162            .execute_task(&task, true)
163            .await
164            .map_err(|e| e.message)?;
165
166        // Clean up the temporary instructions file
167        let _ = ctx.file_system().remove_file(&instructions_file).await;
168
169        Ok(())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_debug_command_structure() {
179        let cmd = DebugCommand {
180            name: "test-debug".to_string(),
181            agent: Some("claude_code".to_string()),
182            tech_stack: None,
183            project: None,
184        };
185
186        // Verify the command has the expected fields
187        assert_eq!(cmd.name, "test-debug");
188        assert_eq!(cmd.agent, Some("claude_code".to_string()));
189        assert_eq!(cmd.tech_stack, None);
190        assert_eq!(cmd.project, None);
191    }
192}