stackenv 0.1.7

Native, zero-container multi-project dev environment manager. No Docker, no VMs.
use anyhow::{Context, Result, anyhow};
use crate::core::state::{ExternalRunEntry, ProcessEntry, State};
use crate::platform;
use std::collections::BTreeMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

pub struct Runnable<'a> {
    pub resolved_command: &'a str,
    pub cwd: &'a Path,
    pub extra_env: &'a BTreeMap<String, String>,
    pub name: &'a str,
}

pub fn log_path(name: &str) -> PathBuf {
    let base = dirs::home_dir().expect("could not resolve home directory");
    base.join(".stack").join("logs").join(format!("{name}.log"))
}

pub fn spawn(runnable: &Runnable) -> Result<u32> {
    let parts = shell_words::split(runnable.resolved_command)
        .with_context(|| format!("could not parse command '{}'", runnable.resolved_command))?;
    let (program, args) = parts.split_first().ok_or_else(|| anyhow!("command is empty"))?;

    let log_file = log_path(runnable.name);
    if let Some(parent) = log_file.parent() {
        std::fs::create_dir_all(parent).context("failed to create log directory")?;
    }
    let stdout_file = File::create(&log_file).with_context(|| format!("failed to create log file {}", log_file.display()))?;
    let stderr_file = stdout_file.try_clone().context("failed to duplicate log file handle")?;

    let mut cmd = Command::new(program);
    cmd.args(args)
        .current_dir(runnable.cwd)
        .stdin(Stdio::null())
        .stdout(Stdio::from(stdout_file))
        .stderr(Stdio::from(stderr_file));
    for (key, value) in runnable.extra_env {
        cmd.env(key, value);
    }

    platform::detach(&mut cmd);

    let child = cmd.spawn().with_context(|| format!("failed to start '{}'", runnable.resolved_command))?;
    Ok(child.id())
}

pub fn record_project(state: &mut State, name: &str, pid: u32, port: Option<u16>) {
    state.projects.insert(
        name.to_string(),
        ProcessEntry {
            pid,
            port,
            started_at: now_iso8601(),
            data_dir: None,
        },
    );
}

pub fn record_service(state: &mut State, key: &str, pid: u32, port: Option<u16>, data_dir: Option<String>) {
    state.services.insert(
        key.to_string(),
        ProcessEntry {
            pid,
            port,
            started_at: now_iso8601(),
            data_dir,
        },
    );
}

pub fn record_external_run(state: &mut State, name: &str, port: u16, domain: Option<String>) {
    state.external_runs.insert(
        name.to_string(),
        ExternalRunEntry {
            port,
            domain,
            registered_at: now_iso8601(),
        },
    );
}

fn now_iso8601() -> String {
    format!("{:?}", std::time::SystemTime::now())
}