use crate::analyzer::DiscoveredDockerfile;
use crate::wizard::render::{display_step_header, wizard_render_config};
use colored::Colorize;
use inquire::{Confirm, InquireError, Select, Text};
use std::fmt;
use std::path::Path;
#[derive(Debug, Clone)]
pub enum DockerfileSelectionResult {
Selected {
dockerfile: DiscoveredDockerfile,
build_context: String,
},
StartAgent(String),
Back,
Cancelled,
}
#[derive(Debug, Clone)]
enum BuildContextOption {
DockerfileDirectory(String),
RepositoryRoot,
Custom,
}
impl fmt::Display for BuildContextOption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BuildContextOption::DockerfileDirectory(path) => {
write!(f, "Dockerfile's directory {}", path.dimmed())
}
BuildContextOption::RepositoryRoot => {
write!(f, "Repository root {}", ".".dimmed())
}
BuildContextOption::Custom => {
write!(f, "Custom path...")
}
}
}
}
struct DockerfileOption<'a> {
dockerfile: &'a DiscoveredDockerfile,
project_root: &'a Path,
}
impl<'a> fmt::Display for DockerfileOption<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let relative_path = self
.dockerfile
.path
.strip_prefix(self.project_root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| self.dockerfile.path.to_string_lossy().to_string());
let build_context = if self.dockerfile.build_context == "." {
". (root)".to_string()
} else {
self.dockerfile.build_context.clone()
};
write!(
f,
"{} {} {}",
relative_path,
"→".dimmed(),
build_context.dimmed()
)
}
}
pub fn select_dockerfile(
dockerfiles: &[DiscoveredDockerfile],
project_root: &Path,
) -> DockerfileSelectionResult {
display_step_header(
5,
"Select Dockerfile",
"Choose the Dockerfile to use for deployment.",
);
match dockerfiles.len() {
0 => handle_no_dockerfiles(),
1 => handle_single_dockerfile(&dockerfiles[0], project_root),
_ => handle_multiple_dockerfiles(dockerfiles, project_root),
}
}
fn handle_no_dockerfiles() -> DockerfileSelectionResult {
println!(
"\n{} {}",
"⚠".yellow(),
"No Dockerfiles found in this project.".yellow()
);
match Confirm::new("Would you like the agent to help create one?")
.with_default(true)
.with_help_message("Start an AI-assisted session to generate a Dockerfile")
.prompt()
{
Ok(true) => {
let prompt = "Help me create a Dockerfile for this project. Analyze the codebase and suggest an appropriate Dockerfile with best practices for production deployment.".to_string();
DockerfileSelectionResult::StartAgent(prompt)
}
Ok(false) => DockerfileSelectionResult::Cancelled,
Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
DockerfileSelectionResult::Cancelled
}
Err(_) => DockerfileSelectionResult::Cancelled,
}
}
fn handle_single_dockerfile(
dockerfile: &DiscoveredDockerfile,
project_root: &Path,
) -> DockerfileSelectionResult {
let relative_path = dockerfile
.path
.strip_prefix(project_root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| dockerfile.path.to_string_lossy().to_string());
println!("\n{} Found: {}", "✓".green(), relative_path.cyan());
if let Some(ref base) = dockerfile.base_image {
println!(" {} Base image: {}", "│".dimmed(), base.dimmed());
}
if let Some(port) = dockerfile.suggested_port {
println!(
" {} Suggested port: {}",
"│".dimmed(),
port.to_string().dimmed()
);
}
select_build_context(dockerfile)
}
fn handle_multiple_dockerfiles(
dockerfiles: &[DiscoveredDockerfile],
project_root: &Path,
) -> DockerfileSelectionResult {
println!(
"\n{} Found {} Dockerfiles:",
"ℹ".blue(),
dockerfiles.len().to_string().cyan()
);
let options: Vec<DockerfileOption> = dockerfiles
.iter()
.map(|df| DockerfileOption {
dockerfile: df,
project_root,
})
.collect();
let selection = Select::new("Select Dockerfile:", options)
.with_render_config(wizard_render_config())
.with_help_message("Use ↑/↓ to navigate, Enter to select")
.prompt();
match selection {
Ok(selected) => {
let selected_df = dockerfiles
.iter()
.find(|df| std::ptr::eq(*df, selected.dockerfile))
.unwrap();
select_build_context(selected_df)
}
Err(InquireError::OperationCanceled) => DockerfileSelectionResult::Back,
Err(InquireError::OperationInterrupted) => DockerfileSelectionResult::Cancelled,
Err(_) => DockerfileSelectionResult::Cancelled,
}
}
fn select_build_context(dockerfile: &DiscoveredDockerfile) -> DockerfileSelectionResult {
println!();
println!(
"{}",
"─── Build Context ───────────────────────────".dimmed()
);
println!(
" {}",
"The build context is the directory sent to Docker during build.".dimmed()
);
let dockerfile_dir = dockerfile
.path
.parent()
.map(|p| {
if p.as_os_str().is_empty() {
".".to_string()
} else {
p.to_string_lossy().to_string()
}
})
.unwrap_or_else(|| ".".to_string());
let display_dir = if dockerfile.build_context.is_empty() || dockerfile.build_context == "." {
".".to_string()
} else {
dockerfile.build_context.clone()
};
let options = vec![
BuildContextOption::DockerfileDirectory(display_dir.clone()),
BuildContextOption::RepositoryRoot,
BuildContextOption::Custom,
];
let selection = Select::new("Build context:", options)
.with_render_config(wizard_render_config())
.with_help_message("Select the directory to use as Docker build context")
.prompt();
match selection {
Ok(BuildContextOption::DockerfileDirectory(_)) => DockerfileSelectionResult::Selected {
dockerfile: dockerfile.clone(),
build_context: display_dir,
},
Ok(BuildContextOption::RepositoryRoot) => DockerfileSelectionResult::Selected {
dockerfile: dockerfile.clone(),
build_context: ".".to_string(),
},
Ok(BuildContextOption::Custom) => {
match Text::new("Custom build context path:")
.with_default(&dockerfile_dir)
.with_help_message("Relative path from repository root")
.prompt()
{
Ok(path) => DockerfileSelectionResult::Selected {
dockerfile: dockerfile.clone(),
build_context: path,
},
Err(InquireError::OperationCanceled) => DockerfileSelectionResult::Back,
Err(InquireError::OperationInterrupted) => DockerfileSelectionResult::Cancelled,
Err(_) => DockerfileSelectionResult::Cancelled,
}
}
Err(InquireError::OperationCanceled) => DockerfileSelectionResult::Back,
Err(InquireError::OperationInterrupted) => DockerfileSelectionResult::Cancelled,
Err(_) => DockerfileSelectionResult::Cancelled,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_dockerfile(path: &str, build_context: &str) -> DiscoveredDockerfile {
DiscoveredDockerfile {
path: PathBuf::from(path),
build_context: build_context.to_string(),
suggested_service_name: "test-service".to_string(),
suggested_port: Some(8080),
base_image: Some("node:18".to_string()),
is_multistage: false,
environment: None,
}
}
#[test]
fn test_dockerfile_option_display() {
let df = create_test_dockerfile("/project/services/api/Dockerfile", "services/api");
let project_root = PathBuf::from("/project");
let option = DockerfileOption {
dockerfile: &df,
project_root: &project_root,
};
let display = format!("{}", option);
assert!(display.contains("services/api/Dockerfile"));
assert!(display.contains("→"));
}
#[test]
fn test_dockerfile_option_display_root() {
let df = create_test_dockerfile("/project/Dockerfile", ".");
let project_root = PathBuf::from("/project");
let option = DockerfileOption {
dockerfile: &df,
project_root: &project_root,
};
let display = format!("{}", option);
assert!(display.contains("Dockerfile"));
assert!(display.contains("(root)"));
}
#[test]
fn test_build_context_option_display() {
let dir_option = BuildContextOption::DockerfileDirectory("services/api".to_string());
assert!(format!("{}", dir_option).contains("services/api"));
let root_option = BuildContextOption::RepositoryRoot;
assert!(format!("{}", root_option).contains("."));
let custom_option = BuildContextOption::Custom;
assert!(format!("{}", custom_option).contains("Custom"));
}
#[test]
fn test_dockerfile_selection_result_variants() {
let df = create_test_dockerfile("/project/Dockerfile", ".");
let selected = DockerfileSelectionResult::Selected {
dockerfile: df.clone(),
build_context: ".".to_string(),
};
matches!(selected, DockerfileSelectionResult::Selected { .. });
let agent = DockerfileSelectionResult::StartAgent("prompt".to_string());
matches!(agent, DockerfileSelectionResult::StartAgent(_));
let _ = DockerfileSelectionResult::Back;
let _ = DockerfileSelectionResult::Cancelled;
}
}