Skip to main content

xbp_cli/
project_detector.rs

1use crate::strategies::project_detector::{ProjectDetector, ProjectHint};
2use colored::Colorize;
3use std::path::Path;
4
5use crate::utils::{preferred_pip_command, preferred_python_command};
6
7pub use crate::strategies::project_detector::ProjectHint as ProjectType;
8
9pub fn detect_project_types(path: &Path) -> Vec<ProjectType> {
10    ProjectDetector::detect_project_hints(path).unwrap_or_default()
11}
12
13pub fn suggest_commands(project_types: &[ProjectType]) -> Vec<String> {
14    let mut commands = Vec::new();
15
16    for project_type in project_types {
17        match project_type {
18            ProjectType::Docker => {
19                commands.push("docker build -t <image-name> .".to_string());
20                commands.push("docker run -p <port>:<port> <image-name>".to_string());
21            }
22            ProjectType::DockerCompose => {
23                commands.push("docker-compose up -d".to_string());
24                commands.push("docker-compose down".to_string());
25            }
26            ProjectType::Python => {
27                let pip = preferred_pip_command();
28                let python = preferred_python_command();
29                commands.push(format!("{pip} install -r requirements.txt"));
30                commands.push(format!("{python} main.py"));
31            }
32            ProjectType::NodeJs => {
33                commands.push("npm install".to_string());
34                commands.push("npm start".to_string());
35            }
36            ProjectType::Rust => {
37                commands.push("cargo build --release".to_string());
38                commands.push("cargo run".to_string());
39            }
40            ProjectType::Railway => {
41                commands.push("railway up".to_string());
42                commands.push("railway logs".to_string());
43            }
44            ProjectType::Vercel => {
45                commands.push("vercel".to_string());
46                commands.push("vercel deploy --prod".to_string());
47                commands.push("vercel --version".to_string());
48            }
49            ProjectType::Go => {
50                commands.push("go build".to_string());
51                commands.push("go run .".to_string());
52            }
53        }
54    }
55
56    commands.dedup();
57    commands
58}
59
60pub fn display_project_types(project_types: &[ProjectType]) {
61    if project_types.is_empty() {
62        return;
63    }
64
65    let types_str = project_types
66        .iter()
67        .map(|project_type| {
68            format!(
69                "{} {}",
70                project_type_icon(*project_type),
71                project_type_name(*project_type)
72            )
73        })
74        .collect::<Vec<_>>()
75        .join(", ");
76
77    tracing::info!("{} {}", "Detected:".bright_blue(), types_str);
78}
79
80pub fn display_suggested_commands(project_types: &[ProjectType]) {
81    let commands = suggest_commands(project_types);
82
83    if commands.is_empty() {
84        return;
85    }
86
87    tracing::info!("{}", "Suggested commands:".bright_blue());
88    for cmd in commands.iter().take(3) {
89        tracing::info!("  {}", cmd.dimmed());
90    }
91}
92
93fn project_type_name(project_type: ProjectHint) -> &'static str {
94    match project_type {
95        ProjectType::Docker => "Docker",
96        ProjectType::DockerCompose => "Docker Compose",
97        ProjectType::Python => "Python",
98        ProjectType::NodeJs => "Node.js",
99        ProjectType::Rust => "Rust",
100        ProjectType::Railway => "Railway",
101        ProjectType::Vercel => "Vercel",
102        ProjectType::Go => "Go",
103    }
104}
105
106fn project_type_icon(project_type: ProjectHint) -> &'static str {
107    match project_type {
108        ProjectType::Docker => "docker",
109        ProjectType::DockerCompose => "compose",
110        ProjectType::Python => "python",
111        ProjectType::NodeJs => "node",
112        ProjectType::Rust => "rust",
113        ProjectType::Railway => "railway",
114        ProjectType::Vercel => "vercel",
115        ProjectType::Go => "go",
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::{detect_project_types, suggest_commands, ProjectType};
122    use std::fs;
123    use std::sync::atomic::{AtomicU64, Ordering};
124
125    #[test]
126    fn detect_project_types_delegates_to_shared_build_hints() {
127        let project_root = temp_dir("xbp-cli-project-hints");
128        fs::create_dir_all(&project_root).expect("create temp dir");
129        fs::write(project_root.join("go.mod"), "module demo\n").expect("write go mod");
130        fs::write(project_root.join("Cargo.toml"), "[package]\nname='demo'\n")
131            .expect("write cargo toml");
132
133        let project_types = detect_project_types(&project_root);
134
135        assert!(project_types.contains(&ProjectType::Go));
136        assert!(project_types.contains(&ProjectType::Rust));
137
138        let _ = fs::remove_dir_all(project_root);
139    }
140
141    #[test]
142    fn suggest_commands_keeps_go_cli_guidance() {
143        let commands = suggest_commands(&[ProjectType::Go]);
144
145        assert_eq!(
146            commands,
147            vec!["go build".to_string(), "go run .".to_string()]
148        );
149    }
150
151    #[test]
152    fn suggest_commands_include_vercel_deploy_guidance() {
153        let commands = suggest_commands(&[ProjectType::Vercel]);
154
155        assert_eq!(
156            commands,
157            vec![
158                "vercel".to_string(),
159                "vercel deploy --prod".to_string(),
160                "vercel --version".to_string(),
161            ]
162        );
163    }
164
165    fn temp_dir(prefix: &str) -> std::path::PathBuf {
166        static COUNTER: AtomicU64 = AtomicU64::new(0);
167        let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
168        std::env::temp_dir().join(format!("{prefix}-{unique}"))
169    }
170}