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