xbp_cli/
project_detector.rs1use 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}