wsup 0.1.4

A beautiful TUI localhost process manager with real-time graphs
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process::{Child, Command};

#[cfg(unix)]
fn getShell() -> (&'static str, &'static str) {
    ("sh", "-c")
}

#[cfg(windows)]
fn getShell() -> (&'static str, &'static str) {
    ("cmd", "/C")
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
    pub id: String,
    pub name: String,
    pub path: String,
    pub port: u16,
    pub build_command: Option<String>,
    pub run_command: String,
    pub deploy_command: Option<String>,
    pub env_vars: HashMap<String, String>,
}

impl Project {
    pub fn new(name: String, path: String, port: u16, run_command: String) -> Self {
        let id = uuid::Uuid::new_v4().to_string();
        Self {
            id,
            name,
            path,
            port,
            build_command: None,
            run_command,
            deploy_command: None,
            env_vars: HashMap::new(),
        }
    }
}

#[derive(Debug)]
pub struct ProjectManager {
    projects: Vec<Project>,
    running: HashMap<String, Child>,
    config_path: PathBuf,
}

impl ProjectManager {
    pub fn new() -> Self {
        let config_path = Self::getConfigPath();
        let projects = Self::loadProjects(&config_path);

        Self {
            projects,
            running: HashMap::new(),
            config_path,
        }
    }

    fn getConfigPath() -> PathBuf {
        let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
        path.push("wsup");
        fs::create_dir_all(&path).ok();
        path.push("projects.json");
        path
    }

    fn loadProjects(path: &PathBuf) -> Vec<Project> {
        if let Ok(content) = fs::read_to_string(path) {
            serde_json::from_str(&content).unwrap_or_default()
        } else {
            Vec::new()
        }
    }

    pub fn saveProjects(&self) -> Result<(), String> {
        let json = serde_json::to_string_pretty(&self.projects)
            .map_err(|e| format!("Failed to serialize: {}", e))?;
        fs::write(&self.config_path, json)
            .map_err(|e| format!("Failed to write config: {}", e))?;
        Ok(())
    }

    pub fn getProjects(&self) -> &Vec<Project> {
        &self.projects
    }

    pub fn addProject(&mut self, project: Project) {
        self.projects.push(project);
        self.saveProjects().ok();
    }

    pub fn removeProject(&mut self, id: &str) {
        self.projects.retain(|p| p.id != id);
        self.saveProjects().ok();
    }

    pub fn startProject(&mut self, id: &str) -> Result<(), String> {
        let project = self.projects.iter()
            .find(|p| p.id == id)
            .ok_or("Project not found")?;

        if let Some(build_cmd) = &project.build_command {
            let (shell, flag) = getShell();
            let build_result = Command::new(shell)
                .arg(flag)
                .arg(build_cmd)
                .current_dir(&project.path)
                .status()
                .map_err(|e| format!("Build failed: {}", e))?;

            if !build_result.success() {
                return Err("Build command failed".to_string());
            }
        }

        let (shell, flag) = getShell();
        let child = Command::new(shell)
            .arg(flag)
            .arg(&project.run_command)
            .current_dir(&project.path)
            .envs(&project.env_vars)
            .spawn()
            .map_err(|e| format!("Failed to start: {}", e))?;

        self.running.insert(id.to_string(), child);
        Ok(())
    }

    pub fn stopProject(&mut self, id: &str) -> Result<(), String> {
        if let Some(mut child) = self.running.remove(id) {
            child.kill().map_err(|e| format!("Failed to kill: {}", e))?;
            Ok(())
        } else {
            Err("Project not running".to_string())
        }
    }

    pub fn isRunning(&self, id: &str) -> bool {
        self.running.contains_key(id)
    }

    pub fn deployProject(&self, id: &str) -> Result<(), String> {
        let project = self.projects.iter()
            .find(|p| p.id == id)
            .ok_or("Project not found")?;

        if let Some(deploy_cmd) = &project.deploy_command {
            if let Some(build_cmd) = &project.build_command {
                let (shell, flag) = getShell();
                let build_result = Command::new(shell)
                    .arg(flag)
                    .arg(build_cmd)
                    .current_dir(&project.path)
                    .status()
                    .map_err(|e| format!("Build failed: {}", e))?;

                if !build_result.success() {
                    return Err("Build command failed".to_string());
                }
            }

            let (shell, flag) = getShell();
            let deploy_result = Command::new(shell)
                .arg(flag)
                .arg(deploy_cmd)
                .current_dir(&project.path)
                .envs(&project.env_vars)
                .status()
                .map_err(|e| format!("Deploy failed: {}", e))?;

            if deploy_result.success() {
                Ok(())
            } else {
                Err("Deploy command failed".to_string())
            }
        } else {
            Err("No deploy command configured".to_string())
        }
    }
}