tsk/commands/
add.rs

1use super::Command;
2use crate::context::AppContext;
3use crate::repo_utils::find_repository_root;
4use crate::task::TaskBuilder;
5use crate::task_storage::get_task_storage;
6use async_trait::async_trait;
7use std::error::Error;
8use std::path::{Path, PathBuf};
9
10pub struct AddCommand {
11    pub name: String,
12    pub r#type: String,
13    pub description: Option<String>,
14    pub instructions: Option<String>,
15    pub edit: bool,
16    pub agent: Option<String>,
17    pub timeout: u32,
18    pub tech_stack: Option<String>,
19    pub project: Option<String>,
20}
21
22#[async_trait]
23impl Command for AddCommand {
24    async fn execute(&self, ctx: &AppContext) -> Result<(), Box<dyn Error>> {
25        println!("Adding task to queue: {}", self.name);
26
27        // Find repository root
28        let repo_root = find_repository_root(Path::new("."))?;
29
30        // Create task using TaskBuilder
31        let task = TaskBuilder::new()
32            .repo_root(repo_root.clone())
33            .name(self.name.clone())
34            .task_type(self.r#type.clone())
35            .description(self.description.clone())
36            .instructions_file(self.instructions.as_ref().map(PathBuf::from))
37            .edit(self.edit)
38            .agent(self.agent.clone())
39            .timeout(self.timeout)
40            .tech_stack(self.tech_stack.clone())
41            .project(self.project.clone())
42            .build(ctx)
43            .await?;
44
45        // Try to add task via server first
46        let client = ctx.tsk_client();
47
48        if client.is_server_available().await {
49            // Server is available, use it
50            match client.add_task(repo_root.clone(), task.clone()).await {
51                Ok(_) => {
52                    println!("Task added via server");
53                }
54                Err(_) => {
55                    eprintln!("Failed to add task via server");
56                    eprintln!("Falling back to direct file write...");
57
58                    // Fall back to direct storage
59                    let storage = get_task_storage(ctx.xdg_directories(), ctx.file_system());
60                    storage
61                        .add_task(task.clone())
62                        .await
63                        .map_err(|e| e as Box<dyn Error>)?;
64                }
65            }
66        } else {
67            // Server not available, write directly
68            let storage = get_task_storage(ctx.xdg_directories(), ctx.file_system());
69            storage
70                .add_task(task.clone())
71                .await
72                .map_err(|e| e as Box<dyn Error>)?;
73        }
74
75        println!("\nTask successfully added to queue!");
76        println!("Task ID: {}", task.id);
77        println!("Type: {}", self.r#type);
78        if let Some(ref desc) = self.description {
79            println!("Description: {desc}");
80        }
81        if self.instructions.is_some() {
82            println!("Instructions: Copied to task directory");
83        }
84        if let Some(ref agent) = self.agent {
85            println!("Agent: {agent}");
86        }
87        println!("Timeout: {} minutes", self.timeout);
88        println!("\nUse 'tsk list' to view all queued tasks");
89        println!("Use 'tsk run' to execute the next task in the queue");
90
91        Ok(())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::test_utils::{NoOpDockerClient, NoOpTskClient};
99    use std::sync::Arc;
100
101    fn create_test_context() -> AppContext {
102        AppContext::builder()
103            .with_docker_client(Arc::new(NoOpDockerClient))
104            .with_tsk_client(Arc::new(NoOpTskClient))
105            .build()
106    }
107
108    #[tokio::test]
109    async fn test_add_command_validation_no_input() {
110        let cmd = AddCommand {
111            name: "test".to_string(),
112            r#type: "generic".to_string(),
113            description: None,
114            instructions: None,
115            edit: false,
116            agent: None,
117            timeout: 30,
118            tech_stack: None,
119            project: None,
120        };
121
122        let ctx = create_test_context();
123        let result = cmd.execute(&ctx).await;
124        assert!(result.is_err());
125        assert!(result.unwrap_err().to_string().contains(
126            "Either description or instructions file must be provided, or use edit mode"
127        ));
128    }
129
130    #[tokio::test]
131    async fn test_add_command_invalid_task_type() {
132        let cmd = AddCommand {
133            name: "test".to_string(),
134            r#type: "nonexistent".to_string(),
135            description: Some("test description".to_string()),
136            instructions: None,
137            edit: false,
138            agent: None,
139            timeout: 30,
140            tech_stack: None,
141            project: None,
142        };
143
144        let ctx = create_test_context();
145        let result = cmd.execute(&ctx).await;
146        assert!(result.is_err());
147        assert!(
148            result
149                .unwrap_err()
150                .to_string()
151                .contains("No template found for task type 'nonexistent'")
152        );
153    }
154}