tsk/commands/
docker_build.rs1use 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
13pub struct DockerBuildCommand {
15 pub no_cache: bool,
17 pub tech_stack: Option<String>,
19 pub agent: Option<String>,
21 pub project: Option<String>,
23 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 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 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 let project_root = find_repository_root(std::path::Path::new(".")).ok();
86
87 if self.dry_run {
88 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 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 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 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 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 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 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}