slave 0.3.0

Laravel-inspired CLI for Rust MCP servers: scaffold apps whose business operations are MCP tools over Streamable HTTP, with built-in auth, users, roles and Turso migrations
//! `slave serve`: build and run the application's development server.

use std::collections::HashMap;
use std::io::ErrorKind;
use std::net::TcpListener;
use std::path::Path;

use anyhow::{Context, Result, bail};

use crate::cli::ServeArgs;
use crate::{cargo, project};

const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 3000;

pub fn run(args: &ServeArgs) -> Result<()> {
    let root = project::find_root()?;
    let (host, port) = resolve_address(args, &root)?;
    ensure_port_available(&host, port)?;

    println!("Starting development server: http://{host}:{port}");
    println!("Press Ctrl+C to stop the server.\n");

    let port_value = port.to_string();
    let mut cargo_args = vec!["run"];
    if args.release {
        cargo_args.push("--release");
    }
    let envs = [
        ("APP_HOST", host.as_str()),
        ("APP_PORT", port_value.as_str()),
    ];
    let status = cargo::run(&root, &cargo_args, &envs)?;

    match status.code() {
        Some(0) | None => Ok(()), // `None` means the server was stopped by a signal.
        Some(code) => bail!("development server exited with status {code}"),
    }
}

/// Resolves host and port with the precedence: flag > process environment > `.env` > default.
fn resolve_address(args: &ServeArgs, root: &Path) -> Result<(String, u16)> {
    let dotenv = read_dotenv(&root.join(".env"))?;
    let lookup = |key: &str| std::env::var(key).ok().or_else(|| dotenv.get(key).cloned());

    let host = args
        .host
        .clone()
        .or_else(|| lookup("APP_HOST"))
        .unwrap_or_else(|| DEFAULT_HOST.to_owned());

    let port = match (args.port, lookup("APP_PORT")) {
        (Some(port), _) => port,
        (None, Some(value)) => value
            .trim()
            .parse()
            .with_context(|| format!("APP_PORT={value:?} is not a valid port"))?,
        (None, None) => DEFAULT_PORT,
    };
    Ok((host, port))
}

fn read_dotenv(path: &Path) -> Result<HashMap<String, String>> {
    let iter = match dotenvy::from_path_iter(path) {
        Ok(iter) => iter,
        Err(err) if err.not_found() => return Ok(HashMap::new()),
        Err(err) => return Err(err).with_context(|| format!("failed to read {}", path.display())),
    };
    iter.collect::<Result<_, _>>()
        .with_context(|| format!("failed to parse {}", path.display()))
}

/// Fails fast with a clear message instead of waiting for a full build to hit the same error.
fn ensure_port_available(host: &str, port: u16) -> Result<()> {
    match TcpListener::bind((host, port)) {
        Ok(_) => Ok(()),
        Err(err) if err.kind() == ErrorKind::AddrInUse => {
            bail!("port {port} is already in use on {host}; choose another with --port")
        }
        Err(err) => Err(err).with_context(|| format!("cannot bind to {host}:{port}")),
    }
}