1use super::{Task, TaskResult};
8use crate::config::BackendConfig;
9use crate::environment::Environment;
10use crate::{Error, Result};
11use async_trait::async_trait;
12use std::path::Path;
13use std::process::Stdio;
14use std::sync::Arc;
15use tokio::process::Command;
16
17#[async_trait]
19pub trait TaskBackend: Send + Sync {
20 #[allow(clippy::too_many_arguments)] async fn execute(
23 &self,
24 name: &str,
25 task: &Task,
26 environment: &Environment,
27 project_root: &Path,
28 capture_output: bool,
29 ) -> Result<TaskResult>;
30
31 fn name(&self) -> &'static str;
33}
34
35pub struct HostBackend;
37
38impl Default for HostBackend {
39 fn default() -> Self {
40 Self
41 }
42}
43
44impl HostBackend {
45 pub fn new() -> Self {
46 Self
47 }
48}
49
50#[async_trait]
51impl TaskBackend for HostBackend {
52 async fn execute(
53 &self,
54 name: &str,
55 task: &Task,
56 environment: &Environment,
57 project_root: &Path,
58 capture_output: bool,
59 ) -> Result<TaskResult> {
60 tracing::info!(
61 task = %name,
62 backend = "host",
63 "Executing task on host"
64 );
65
66 let resolved_command = environment.resolve_command(&task.command);
68
69 let mut cmd = if let Some(shell) = &task.shell {
71 let mut c = Command::new(shell.command.as_deref().unwrap_or("bash"));
72 if let Some(flag) = &shell.flag {
73 c.arg(flag);
74 } else {
75 c.arg("-c");
76 }
77 c.arg(&task.command);
79 c
80 } else {
81 let mut c = Command::new(resolved_command);
82 c.args(&task.args);
83 c
84 };
85
86 cmd.current_dir(project_root);
88
89 cmd.env_clear();
91 for (k, v) in &environment.vars {
92 cmd.env(k, v);
93 }
94
95 if capture_output {
97 let output = cmd
98 .stdout(Stdio::piped())
99 .stderr(Stdio::piped())
100 .output()
101 .await
102 .map_err(|e| Error::Io {
103 source: e,
104 path: None,
105 operation: format!("spawn task {}", name),
106 })?;
107
108 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
109 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
110 let exit_code = output.status.code().unwrap_or(-1);
111 let success = output.status.success();
112
113 if !success {
114 tracing::warn!(task = %name, exit = exit_code, "Task failed");
115 }
116
117 Ok(TaskResult {
118 name: name.to_string(),
119 exit_code: Some(exit_code),
120 stdout,
121 stderr,
122 success,
123 })
124 } else {
125 let status = cmd
127 .stdout(Stdio::inherit())
128 .stderr(Stdio::inherit())
129 .status()
130 .await
131 .map_err(|e| Error::Io {
132 source: e,
133 path: None,
134 operation: format!("spawn task {}", name),
135 })?;
136
137 let exit_code = status.code().unwrap_or(-1);
138 let success = status.success();
139
140 if !success {
141 tracing::warn!(task = %name, exit = exit_code, "Task failed");
142 }
143
144 Ok(TaskResult {
145 name: name.to_string(),
146 exit_code: Some(exit_code),
147 stdout: String::new(), stderr: String::new(),
149 success,
150 })
151 }
152 }
153
154 fn name(&self) -> &'static str {
155 "host"
156 }
157}
158
159pub type BackendFactory = fn(Option<&BackendConfig>, std::path::PathBuf) -> Arc<dyn TaskBackend>;
161
162pub fn create_backend(
167 config: Option<&BackendConfig>,
168 project_root: std::path::PathBuf,
169 cli_backend: Option<&str>,
170) -> Arc<dyn TaskBackend> {
171 create_backend_with_factory(config, project_root, cli_backend, None)
172}
173
174pub fn create_backend_with_factory(
179 config: Option<&BackendConfig>,
180 project_root: std::path::PathBuf,
181 cli_backend: Option<&str>,
182 dagger_factory: Option<BackendFactory>,
183) -> Arc<dyn TaskBackend> {
184 let backend_type = if let Some(b) = cli_backend {
186 b.to_string()
187 } else if let Some(c) = config {
188 c.backend_type.clone()
189 } else {
190 "host".to_string()
191 };
192
193 match backend_type.as_str() {
194 "dagger" => {
195 if let Some(factory) = dagger_factory {
196 factory(config, project_root)
197 } else {
198 tracing::error!(
199 "Dagger backend requested but not available. \
200 Add cuenv-dagger dependency to enable it. \
201 Falling back to host backend."
202 );
203 Arc::new(HostBackend::new())
204 }
205 }
206 _ => Arc::new(HostBackend::new()),
207 }
208}
209
210pub fn should_use_dagger(config: Option<&BackendConfig>, cli_backend: Option<&str>) -> bool {
212 let backend_type = if let Some(b) = cli_backend {
213 b
214 } else if let Some(c) = config {
215 &c.backend_type
216 } else {
217 "host"
218 };
219
220 backend_type == "dagger"
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_host_backend_new() {
229 let backend = HostBackend::new();
230 assert_eq!(backend.name(), "host");
231 }
232
233 #[test]
234 fn test_host_backend_default() {
235 let backend = HostBackend;
236 assert_eq!(backend.name(), "host");
237 }
238
239 #[test]
240 fn test_host_backend_name() {
241 let backend = HostBackend;
242 assert_eq!(backend.name(), "host");
243 }
244
245 #[test]
246 fn test_should_use_dagger_cli_override_dagger() {
247 assert!(should_use_dagger(None, Some("dagger")));
249 }
250
251 #[test]
252 fn test_should_use_dagger_cli_override_host() {
253 assert!(!should_use_dagger(None, Some("host")));
255 }
256
257 #[test]
258 fn test_should_use_dagger_config_dagger() {
259 let config = BackendConfig {
260 backend_type: "dagger".to_string(),
261 options: None,
262 };
263 assert!(should_use_dagger(Some(&config), None));
264 }
265
266 #[test]
267 fn test_should_use_dagger_config_host() {
268 let config = BackendConfig {
269 backend_type: "host".to_string(),
270 options: None,
271 };
272 assert!(!should_use_dagger(Some(&config), None));
273 }
274
275 #[test]
276 fn test_should_use_dagger_default() {
277 assert!(!should_use_dagger(None, None));
279 }
280
281 #[test]
282 fn test_should_use_dagger_cli_overrides_config() {
283 let config = BackendConfig {
284 backend_type: "dagger".to_string(),
285 options: None,
286 };
287 assert!(!should_use_dagger(Some(&config), Some("host")));
289 }
290
291 #[test]
292 fn test_create_backend_defaults_to_host() {
293 let backend = create_backend(None, std::path::PathBuf::from("."), None);
294 assert_eq!(backend.name(), "host");
295 }
296
297 #[test]
298 fn test_create_backend_with_cli_host() {
299 let backend = create_backend(None, std::path::PathBuf::from("."), Some("host"));
300 assert_eq!(backend.name(), "host");
301 }
302
303 #[test]
304 fn test_create_backend_with_config_host() {
305 let config = BackendConfig {
306 backend_type: "host".to_string(),
307 options: None,
308 };
309 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
310 assert_eq!(backend.name(), "host");
311 }
312
313 #[test]
314 fn test_create_backend_unknown_type_defaults_to_host() {
315 let config = BackendConfig {
316 backend_type: "unknown".to_string(),
317 options: None,
318 };
319 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
320 assert_eq!(backend.name(), "host");
322 }
323
324 #[test]
325 fn test_create_backend_dagger_without_factory() {
326 let config = BackendConfig {
327 backend_type: "dagger".to_string(),
328 options: None,
329 };
330 let backend = create_backend(Some(&config), std::path::PathBuf::from("."), None);
332 assert_eq!(backend.name(), "host");
333 }
334
335 #[test]
336 fn test_create_backend_with_factory_dagger() {
337 fn mock_dagger_factory(
339 _config: Option<&BackendConfig>,
340 _project_root: std::path::PathBuf,
341 ) -> Arc<dyn TaskBackend> {
342 Arc::new(HostBackend::new())
343 }
344
345 let config = BackendConfig {
346 backend_type: "dagger".to_string(),
347 options: None,
348 };
349
350 let backend = create_backend_with_factory(
351 Some(&config),
352 std::path::PathBuf::from("."),
353 None,
354 Some(mock_dagger_factory),
355 );
356 assert_eq!(backend.name(), "host");
358 }
359
360 #[test]
361 fn test_create_backend_with_factory_cli_overrides_to_dagger() {
362 fn mock_dagger_factory(
363 _config: Option<&BackendConfig>,
364 _project_root: std::path::PathBuf,
365 ) -> Arc<dyn TaskBackend> {
366 Arc::new(HostBackend::new())
367 }
368
369 let backend = create_backend_with_factory(
371 None,
372 std::path::PathBuf::from("."),
373 Some("dagger"),
374 Some(mock_dagger_factory),
375 );
376 assert_eq!(backend.name(), "host"); }
378
379 #[test]
380 fn test_create_backend_with_factory_cli_overrides_config() {
381 fn mock_dagger_factory(
382 _config: Option<&BackendConfig>,
383 _project_root: std::path::PathBuf,
384 ) -> Arc<dyn TaskBackend> {
385 Arc::new(HostBackend::new())
386 }
387
388 let config = BackendConfig {
389 backend_type: "dagger".to_string(),
390 options: None,
391 };
392
393 let backend = create_backend_with_factory(
395 Some(&config),
396 std::path::PathBuf::from("."),
397 Some("host"),
398 Some(mock_dagger_factory),
399 );
400 assert_eq!(backend.name(), "host");
401 }
402
403 #[test]
404 fn test_backend_config_debug() {
405 let config = BackendConfig {
406 backend_type: "host".to_string(),
407 options: None,
408 };
409 let debug_str = format!("{:?}", config);
410 assert!(debug_str.contains("host"));
411 }
412}