use crate::installers::common::command_exists;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellCommand {
pub program: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
impl ShellCommand {
pub fn to_command_string(&self) -> String {
let env_prefix = self
.env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ");
let cmd = if self.args.is_empty() {
self.program.clone()
} else {
format!("{} {}", self.program, self.args.join(" "))
};
if env_prefix.is_empty() {
cmd
} else {
format!("{env_prefix} {cmd}")
}
}
}
#[derive(Debug, Clone)]
pub struct VllmStudioConfig {
pub install_dir: String,
pub python_bin: String,
pub dry_run: bool,
}
impl Default for VllmStudioConfig {
fn default() -> Self {
Self {
install_dir: default_install_dir(),
python_bin: "python3".to_string(),
dry_run: false,
}
}
}
pub const REPO_URL: &str = "https://github.com/0xSero/vllm-studio.git";
pub const DEFAULT_BRANCH: &str = "main";
pub fn default_install_dir() -> String {
format!("{}/vllm-studio", std::env::var("HOME").unwrap_or_default())
}
pub struct VllmStudioInstaller {
config: VllmStudioConfig,
}
impl VllmStudioInstaller {
pub fn new(config: VllmStudioConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(VllmStudioConfig::default())
}
pub fn build_git_clone_command(&self) -> ShellCommand {
ShellCommand {
program: "git".to_string(),
args: vec![
"clone".to_string(),
REPO_URL.to_string(),
self.config.install_dir.clone(),
],
env: vec![],
}
}
pub fn build_git_update_commands(&self) -> Vec<ShellCommand> {
let dir = &self.config.install_dir;
vec![
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"remote".to_string(),
"set-head".to_string(),
"origin".to_string(),
"-a".to_string(),
],
env: vec![],
},
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"checkout".to_string(),
DEFAULT_BRANCH.to_string(),
],
env: vec![],
},
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"fetch".to_string(),
"--all".to_string(),
],
env: vec![],
},
ShellCommand {
program: "git".to_string(),
args: vec![
"-C".to_string(),
dir.clone(),
"reset".to_string(),
"--hard".to_string(),
format!("origin/{DEFAULT_BRANCH}"),
],
env: vec![],
},
]
}
pub fn detect_pkg_manager(&self) -> Option<String> {
if command_exists("bun") {
Some("bun".to_string())
} else if command_exists("npm") {
Some("npm".to_string())
} else {
None
}
}
pub fn build_pkg_install_command(&self, pkg_mgr: &str) -> ShellCommand {
ShellCommand {
program: pkg_mgr.to_string(),
args: vec!["install".to_string()],
env: vec![],
}
}
pub fn build_frontend_build_command(&self, pkg_mgr: &str) -> ShellCommand {
ShellCommand {
program: pkg_mgr.to_string(),
args: vec!["run".to_string(), "build".to_string()],
env: vec![],
}
}
pub fn build_shim_content(&self, pkg_mgr: &str) -> String {
format!(
"#!/bin/bash\ncd \"{}/controller\" && {} run start\n",
self.config.install_dir, pkg_mgr,
)
}
pub fn install_dir(&self) -> &str {
&self.config.install_dir
}
pub fn repo_url(&self) -> &str {
REPO_URL
}
pub fn default_branch(&self) -> &str {
DEFAULT_BRANCH
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_installer(dir: &str) -> VllmStudioInstaller {
VllmStudioInstaller::new(VllmStudioConfig {
install_dir: dir.to_string(),
python_bin: "python3".to_string(),
dry_run: false,
})
}
#[test]
fn test_git_clone_url_matches_original_script() {
let installer = make_installer("/home/user/vllm-studio");
let cmd = installer.build_git_clone_command();
assert_eq!(cmd.program, "git");
assert!(cmd.args.contains(&"clone".to_string()));
assert!(cmd.args.contains(&REPO_URL.to_string()));
assert!(cmd.args.contains(&"/home/user/vllm-studio".to_string()));
}
#[test]
fn test_git_clone_uses_correct_repo_url() {
assert_eq!(
REPO_URL, "https://github.com/0xSero/vllm-studio.git",
"URL must match install_vllm_studio.sh REPO_URL"
);
}
#[test]
fn test_git_clone_command_string() {
let installer = make_installer("/home/user/vllm-studio");
let cmd = installer.build_git_clone_command();
let s = cmd.to_command_string();
assert!(s.contains("git clone"));
assert!(s.contains(REPO_URL));
assert!(s.contains("/home/user/vllm-studio"));
}
#[test]
fn test_default_install_dir_is_home_vllm_studio() {
let config = VllmStudioConfig::default();
assert!(
config.install_dir.ends_with("/vllm-studio"),
"Default dir must end with /vllm-studio, got: {}",
config.install_dir,
);
}
#[test]
fn test_custom_install_dir() {
let installer = make_installer("/opt/apps/vllm-studio");
assert_eq!(installer.install_dir(), "/opt/apps/vllm-studio");
}
#[test]
fn test_git_update_commands_reference_correct_dir() {
let installer = make_installer("/home/test/vllm-studio");
let cmds = installer.build_git_update_commands();
assert_eq!(cmds.len(), 4);
for cmd in &cmds {
assert!(
cmd.args.contains(&"/home/test/vllm-studio".to_string()),
"Command {} should reference install dir",
cmd.to_command_string(),
);
}
}
#[test]
fn test_pkg_install_command_bun() {
let installer = make_installer("/home/user/vllm-studio");
let cmd = installer.build_pkg_install_command("bun");
assert_eq!(cmd.program, "bun");
assert_eq!(cmd.args, vec!["install"]);
}
#[test]
fn test_pkg_install_command_npm() {
let installer = make_installer("/home/user/vllm-studio");
let cmd = installer.build_pkg_install_command("npm");
assert_eq!(cmd.program, "npm");
assert_eq!(cmd.args, vec!["install"]);
}
#[test]
fn test_frontend_build_command() {
let installer = make_installer("/home/user/vllm-studio");
let cmd = installer.build_frontend_build_command("npm");
assert_eq!(cmd.program, "npm");
assert!(cmd.args.contains(&"run".to_string()));
assert!(cmd.args.contains(&"build".to_string()));
}
#[test]
fn test_shim_content_references_controller_dir() {
let installer = make_installer("/home/user/vllm-studio");
let shim = installer.build_shim_content("npm");
assert!(shim.contains("/home/user/vllm-studio/controller"));
assert!(shim.contains("npm run start"));
assert!(shim.starts_with("#!/bin/bash"));
}
#[test]
fn test_default_branch_is_main() {
assert_eq!(DEFAULT_BRANCH, "main");
}
}