use std::path::Path;
use crate::config::ToolchainMode;
#[derive(Debug, Clone, PartialEq)]
pub enum DetectedToolchain {
Devbox,
Flake,
None,
}
pub fn detect_toolchain(dir: &Path) -> DetectedToolchain {
if dir.join("devbox.json").exists() {
DetectedToolchain::Devbox
} else if dir.join("flake.nix").exists() {
DetectedToolchain::Flake
} else {
DetectedToolchain::None
}
}
pub fn resolve_toolchain(mode: &ToolchainMode, dir: &Path) -> DetectedToolchain {
match mode {
ToolchainMode::Off => DetectedToolchain::None,
ToolchainMode::Devbox => DetectedToolchain::Devbox,
ToolchainMode::Flake => DetectedToolchain::Flake,
ToolchainMode::Auto => detect_toolchain(dir),
}
}
use crate::shell::shell_escape;
pub fn toolchain_wrapper_script(toolchain: &DetectedToolchain) -> Option<String> {
match toolchain {
DetectedToolchain::Devbox => Some(
concat!(
"_WM_CWD=\"$PWD\"; ",
"_WM_HASH=$(cat devbox.json devbox.lock 2>/dev/null | (md5sum 2>/dev/null || md5 -q) | cut -d\" \" -f1); ",
"_WM_CACHE=\"$HOME/.cache/workmux/devbox/$_WM_HASH\"; ",
"if [ ! -f \"$_WM_CACHE/devbox.json\" ]; then ",
"mkdir -p \"$_WM_CACHE\" && ",
"cp devbox.json \"$_WM_CACHE/\" && ",
"{ [ ! -f devbox.lock ] || cp devbox.lock \"$_WM_CACHE/\"; }; ",
"fi; ",
"export _WM_CWD; ",
"devbox run -c \"$_WM_CACHE\" -- bash -c 'cd \"$_WM_CWD\" && exec \"$@\"' -- \"$@\""
)
.to_string(),
),
DetectedToolchain::Flake => {
Some("nix develop --command bash -c 'exec \"$@\"' -- \"$@\"".to_string())
}
DetectedToolchain::None => None,
}
}
pub fn wrap_command(command: &str, toolchain: &DetectedToolchain) -> String {
match toolchain {
DetectedToolchain::Devbox => {
let escaped = shell_escape(command);
format!(
concat!(
"_WM_CWD=\"$PWD\"; ",
"_WM_HASH=$(cat devbox.json devbox.lock 2>/dev/null | (md5sum 2>/dev/null || md5 -q) | cut -d\" \" -f1); ",
"_WM_CACHE=\"$HOME/.cache/workmux/devbox/$_WM_HASH\"; ",
"if [ ! -f \"$_WM_CACHE/devbox.json\" ]; then ",
"mkdir -p \"$_WM_CACHE\" && ",
"cp devbox.json \"$_WM_CACHE/\" && ",
"{{ [ ! -f devbox.lock ] || cp devbox.lock \"$_WM_CACHE/\"; }}; ",
"fi; ",
"export _WM_CWD; ",
"devbox run -c \"$_WM_CACHE\" -- 'cd \"$_WM_CWD\" && {}'"
),
escaped
)
}
DetectedToolchain::Flake => {
let escaped = shell_escape(command);
format!("nix develop --command bash -c '{}'", escaped)
}
DetectedToolchain::None => command.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_detect_devbox() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("devbox.json"), "{}").unwrap();
assert_eq!(detect_toolchain(dir.path()), DetectedToolchain::Devbox);
}
#[test]
fn test_detect_flake() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("flake.nix"), "{}").unwrap();
assert_eq!(detect_toolchain(dir.path()), DetectedToolchain::Flake);
}
#[test]
fn test_detect_none() {
let dir = TempDir::new().unwrap();
assert_eq!(detect_toolchain(dir.path()), DetectedToolchain::None);
}
#[test]
fn test_devbox_priority_over_flake() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("devbox.json"), "{}").unwrap();
std::fs::write(dir.path().join("flake.nix"), "{}").unwrap();
assert_eq!(detect_toolchain(dir.path()), DetectedToolchain::Devbox);
}
#[test]
fn test_resolve_off_ignores_files() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("devbox.json"), "{}").unwrap();
assert_eq!(
resolve_toolchain(&ToolchainMode::Off, dir.path()),
DetectedToolchain::None
);
}
#[test]
fn test_resolve_forced_devbox() {
let dir = TempDir::new().unwrap();
assert_eq!(
resolve_toolchain(&ToolchainMode::Devbox, dir.path()),
DetectedToolchain::Devbox
);
}
#[test]
fn test_resolve_forced_flake() {
let dir = TempDir::new().unwrap();
assert_eq!(
resolve_toolchain(&ToolchainMode::Flake, dir.path()),
DetectedToolchain::Flake
);
}
#[test]
fn test_resolve_auto_delegates_to_detect() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("flake.nix"), "{}").unwrap();
assert_eq!(
resolve_toolchain(&ToolchainMode::Auto, dir.path()),
DetectedToolchain::Flake
);
}
#[test]
fn test_wrap_devbox_uses_cache() {
let wrapped = wrap_command("claude --help", &DetectedToolchain::Devbox);
assert!(wrapped.contains("_WM_CWD=\"$PWD\""));
assert!(wrapped.contains("export _WM_CWD"));
assert!(wrapped.contains("cd \"$_WM_CWD\""));
assert!(wrapped.contains("md5sum"));
assert!(wrapped.contains("md5 -q"));
assert!(wrapped.contains("devbox.json"));
assert!(wrapped.contains("devbox.lock"));
assert!(wrapped.contains(".cache/workmux/devbox/"));
assert!(wrapped.contains("cp devbox.json"));
assert!(wrapped.contains("devbox run -c"));
assert!(wrapped.contains("claude --help"));
}
#[test]
fn test_wrap_devbox_escapes_quotes() {
let wrapped = wrap_command("echo 'hello'", &DetectedToolchain::Devbox);
assert!(wrapped.contains(r"echo '\''hello'\''"));
}
#[test]
fn test_wrap_flake() {
assert_eq!(
wrap_command("claude --help", &DetectedToolchain::Flake),
"nix develop --command bash -c 'claude --help'"
);
}
#[test]
fn test_wrap_flake_escapes_single_quotes() {
let cmd = "echo 'hello world'";
let wrapped = wrap_command(cmd, &DetectedToolchain::Flake);
assert_eq!(
wrapped,
r#"nix develop --command bash -c 'echo '\''hello world'\'''"#
);
}
#[test]
fn test_wrap_none_passthrough() {
assert_eq!(
wrap_command("claude --help", &DetectedToolchain::None),
"claude --help"
);
}
#[test]
fn test_wrapper_script_none_returns_none() {
assert!(toolchain_wrapper_script(&DetectedToolchain::None).is_none());
}
#[test]
fn test_wrapper_script_flake_uses_exec_at() {
let script = toolchain_wrapper_script(&DetectedToolchain::Flake).unwrap();
assert!(script.contains(r#"exec "$@"'"#));
assert!(script.starts_with("nix develop --command bash -c"));
}
#[test]
fn test_wrapper_script_devbox_uses_exec_at() {
let script = toolchain_wrapper_script(&DetectedToolchain::Devbox).unwrap();
assert!(script.contains(r#"exec "$@"'"#));
assert!(script.contains("devbox run -c"));
assert!(script.contains("_WM_CACHE"));
}
#[test]
fn test_wrapper_script_does_not_contain_user_input() {
let script = toolchain_wrapper_script(&DetectedToolchain::Devbox).unwrap();
assert!(!script.contains("cargo"));
assert!(!script.contains("just"));
}
}