use serde_json::Value;
use std::fs;
use std::path::Path;
pub fn detect_command(path: &str) -> Option<String> {
let base = Path::new(path);
let package_json = base.join("package.json");
if package_json.exists() {
return Some(detect_node_command(&package_json));
}
if base.join("Cargo.toml").exists() {
return Some("cargo run".to_string());
}
if base.join("main.py").exists() {
return Some("python main.py".to_string());
}
if base.join("app.py").exists() {
return Some("python app.py".to_string());
}
None
}
fn detect_node_command(package_json: &Path) -> String {
match fs::read_to_string(package_json) {
Ok(contents) => match serde_json::from_str::<Value>(&contents) {
Ok(package) if has_dev_script(&package) => String::from("npm run dev"),
Ok(_) | Err(_) => String::from("npm start"),
},
Err(_) => String::from("npm start"),
}
}
fn has_dev_script(package: &Value) -> bool {
package
.get("scripts")
.and_then(Value::as_object)
.is_some_and(|scripts| scripts.contains_key("dev"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn test_dir(name: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("switchdev-detector-{name}-{unique}"))
}
#[test]
fn detects_node_command_with_dev_script() {
let dir = test_dir("node-dev");
fs::create_dir_all(&dir).expect("test directory should be created");
fs::write(
dir.join("package.json"),
r#"{
"scripts": {
"dev": "vite"
}
}"#,
)
.expect("package.json should be written");
assert_eq!(
detect_command(dir.to_string_lossy().as_ref()),
Some(String::from("npm run dev"))
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn falls_back_to_npm_start_without_dev_script() {
let dir = test_dir("node-start");
fs::create_dir_all(&dir).expect("test directory should be created");
fs::write(
dir.join("package.json"),
r#"{
"scripts": {
"start": "node server.js"
}
}"#,
)
.expect("package.json should be written");
assert_eq!(
detect_command(dir.to_string_lossy().as_ref()),
Some(String::from("npm start"))
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detects_rust_projects() {
let dir = test_dir("rust");
fs::create_dir_all(&dir).expect("test directory should be created");
fs::write(dir.join("Cargo.toml"), "").expect("Cargo.toml should be written");
assert_eq!(
detect_command(dir.to_string_lossy().as_ref()),
Some(String::from("cargo run"))
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detects_python_main_file() {
let dir = test_dir("python-main");
fs::create_dir_all(&dir).expect("test directory should be created");
fs::write(dir.join("main.py"), "print('hello')").expect("main.py should be written");
assert_eq!(
detect_command(dir.to_string_lossy().as_ref()),
Some(String::from("python main.py"))
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn detects_python_app_file() {
let dir = test_dir("python-app");
fs::create_dir_all(&dir).expect("test directory should be created");
fs::write(dir.join("app.py"), "print('hello')").expect("app.py should be written");
assert_eq!(
detect_command(dir.to_string_lossy().as_ref()),
Some(String::from("python app.py"))
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn returns_none_when_no_project_files_match() {
let dir = test_dir("no-match");
fs::create_dir_all(&dir).expect("test directory should be created");
assert_eq!(detect_command(dir.to_string_lossy().as_ref()), None);
let _ = fs::remove_dir_all(&dir);
}
}