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 let repo_root = find_repository_root(Path::new("."))?;
29
30 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 let client = ctx.tsk_client();
47
48 if client.is_server_available().await {
49 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 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 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}