tsk/commands/
docker_build.rs

1use super::Command;
2use crate::assets::layered::LayeredAssetManager;
3use crate::context::AppContext;
4use crate::docker::composer::DockerComposer;
5use crate::docker::image_manager::DockerImageManager;
6use crate::docker::layers::DockerImageConfig;
7use crate::docker::template_manager::DockerTemplateManager;
8use crate::repo_utils::find_repository_root;
9use async_trait::async_trait;
10use std::error::Error;
11use std::sync::Arc;
12
13/// Command to build TSK Docker images using the templating system
14pub struct DockerBuildCommand {
15    /// Whether to build without using Docker's cache
16    pub no_cache: bool,
17    /// Technology stack (defaults to "default")
18    pub tech_stack: Option<String>,
19    /// Agent (defaults to "claude")
20    pub agent: Option<String>,
21    /// Project (defaults to "default")
22    pub project: Option<String>,
23    /// Whether to only print the resolved Dockerfile without building
24    pub dry_run: bool,
25}
26
27#[async_trait]
28impl Command for DockerBuildCommand {
29    async fn execute(&self, ctx: &AppContext) -> Result<(), Box<dyn Error>> {
30        // Auto-detect tech_stack if not provided
31        let tech_stack = match &self.tech_stack {
32            Some(ts) => {
33                println!("Using tech stack: {ts}");
34                ts.clone()
35            }
36            None => {
37                use crate::repo_utils::find_repository_root;
38                let repo_root = find_repository_root(std::path::Path::new("."))
39                    .unwrap_or_else(|_| std::path::PathBuf::from("."));
40
41                match ctx.repository_context().detect_tech_stack(&repo_root).await {
42                    Ok(detected) => {
43                        println!("Auto-detected tech stack: {detected}");
44                        detected
45                    }
46                    Err(e) => {
47                        eprintln!("Warning: Failed to detect tech stack: {e}. Using default.");
48                        "default".to_string()
49                    }
50                }
51            }
52        };
53
54        let agent = self.agent.as_deref().unwrap_or("claude-code");
55
56        // Auto-detect project if not provided
57        let project = match &self.project {
58            Some(p) => {
59                println!("Using project: {p}");
60                Some(p.clone())
61            }
62            None => {
63                use crate::repo_utils::find_repository_root;
64                let repo_root = find_repository_root(std::path::Path::new("."))
65                    .unwrap_or_else(|_| std::path::PathBuf::from("."));
66
67                match ctx
68                    .repository_context()
69                    .detect_project_name(&repo_root)
70                    .await
71                {
72                    Ok(detected) => {
73                        println!("Auto-detected project name: {detected}");
74                        Some(detected)
75                    }
76                    Err(e) => {
77                        eprintln!("Warning: Failed to detect project name: {e}. Using default.");
78                        Some("default".to_string())
79                    }
80                }
81            }
82        };
83
84        // Get project root for Docker operations
85        let project_root = find_repository_root(std::path::Path::new(".")).ok();
86
87        if self.dry_run {
88            // Dry run mode: just print the composed Dockerfile
89            let asset_manager = Arc::new(LayeredAssetManager::new_with_standard_layers(
90                project_root.as_deref(),
91                &ctx.xdg_directories(),
92            ));
93            let template_manager = DockerTemplateManager::new(asset_manager, ctx.xdg_directories());
94            let composer = DockerComposer::new(template_manager);
95
96            let config = DockerImageConfig::new(
97                tech_stack.clone(),
98                agent.to_string(),
99                project.as_deref().unwrap_or("default").to_string(),
100            );
101
102            let composed = composer.compose(&config, project_root.as_deref())?;
103            composer.validate_dockerfile(&composed.dockerfile_content)?;
104
105            println!("# Resolved Dockerfile for image: {}", composed.image_tag);
106            println!(
107                "# Configuration: tech_stack={}, agent={}, project={}",
108                tech_stack,
109                agent,
110                project.as_deref().unwrap_or("default")
111            );
112            println!();
113            println!("{}", composed.dockerfile_content);
114
115            if !composed.additional_files.is_empty() {
116                println!("\n# Additional files that would be created:");
117                for filename in composed.additional_files.keys() {
118                    println!("#   - {filename}");
119                }
120            }
121
122            if !composed.build_args.is_empty() {
123                println!("\n# Build arguments:");
124                for arg in &composed.build_args {
125                    println!("#   - {arg}");
126                }
127            }
128        } else {
129            // Create image manager on-demand
130            let asset_manager = Arc::new(LayeredAssetManager::new_with_standard_layers(
131                project_root.as_deref(),
132                &ctx.xdg_directories(),
133            ));
134            let template_manager =
135                DockerTemplateManager::new(asset_manager.clone(), ctx.xdg_directories());
136            let composer = DockerComposer::new(DockerTemplateManager::new(
137                asset_manager,
138                ctx.xdg_directories(),
139            ));
140            let image_manager =
141                DockerImageManager::new(ctx.docker_client(), template_manager, composer);
142
143            // Build the main image
144            let image = image_manager
145                .build_image(
146                    &tech_stack,
147                    agent,
148                    project.as_deref(),
149                    project_root.as_deref(),
150                    self.no_cache,
151                )
152                .await?;
153            println!("Successfully built Docker image: {}", image.tag);
154
155            if image.used_fallback {
156                println!(
157                    "Note: Used default project layer as project-specific layer was not found"
158                );
159            }
160
161            // Always build proxy image as it's still needed
162            println!("\nBuilding tsk/proxy image...");
163            let proxy_image = image_manager.build_proxy_image(self.no_cache).await?;
164            println!("Successfully built Docker image: {}", proxy_image.tag);
165
166            println!("\nAll Docker images built successfully!");
167        }
168
169        Ok(())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_docker_build_command_creation() {
179        // Test that DockerBuildCommand can be instantiated
180        let _command = DockerBuildCommand {
181            no_cache: false,
182            tech_stack: None,
183            agent: None,
184            project: None,
185            dry_run: false,
186        };
187    }
188
189    #[test]
190    fn test_docker_build_command_with_options() {
191        // Test that DockerBuildCommand can be instantiated with all options
192        let _command = DockerBuildCommand {
193            no_cache: true,
194            tech_stack: Some("rust".to_string()),
195            agent: Some("claude".to_string()),
196            project: Some("web-api".to_string()),
197            dry_run: false,
198        };
199    }
200
201    #[test]
202    fn test_docker_build_command_dry_run() {
203        // Test that DockerBuildCommand can be instantiated with dry_run
204        let _command = DockerBuildCommand {
205            no_cache: false,
206            tech_stack: Some("python".to_string()),
207            agent: Some("claude".to_string()),
208            project: Some("test-project".to_string()),
209            dry_run: true,
210        };
211    }
212}