slave 0.12.0

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles, teams and SurrealDB migrations, add server-rendered web apps, and route them by domain or path
//! `slave app:separate` and `slave app:embed`: move an app out of the runtime
//! (a process of its own, proxied by the runtime) or back into it.

use std::fs;

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

use crate::{apps, project, runtime};

/// `slave app:separate <name>`.
pub fn separate(name: &str) -> Result<()> {
    set(name, true)?;
    println!(
        "App {name} now runs on its own; `slave build` then `slave serve` start it next to the runtime."
    );
    Ok(())
}

/// `slave app:embed <name>`.
pub fn embed(name: &str) -> Result<()> {
    set(name, false)?;
    println!("App {name} is served by the runtime again; rebuild with `slave build`.");
    Ok(())
}

fn set(name: &str, separate: bool) -> Result<()> {
    let root = project::find_root()?;
    runtime::require(&root)?;
    if !root.join("apps").join(name).join("Cargo.toml").is_file() {
        bail!("apps/{name} not found");
    }
    let path = root.join(apps::CONFIG);
    let text =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", apps::CONFIG))?;
    let config = Config::parse(&text).with_context(|| format!("invalid {}", apps::CONFIG))?;
    let entries: Vec<_> = config.domains.iter().filter(|d| d.app == name).collect();
    if entries.is_empty() {
        bail!("app `{name}` is not listed in {}", apps::CONFIG);
    }
    if let Some(entry) = entries.iter().find(|d| d.upstream.is_some()) {
        bail!(
            "`{}` sends {name} to upstream {}; edit {} by hand",
            entry.label(),
            entry.upstream.as_deref().unwrap_or_default(),
            apps::CONFIG
        );
    }

    let updated = apps::set_separate(&text, name, separate);
    Config::parse(&updated).with_context(|| format!("cannot update {}", apps::CONFIG))?;
    fs::write(&path, updated).with_context(|| format!("failed to update {}", apps::CONFIG))?;
    println!("  updated {}", apps::CONFIG);
    match (separate, runtime::has_app(&root, name)) {
        (true, true) => runtime::remove_app(&root, name)?,
        (false, false) => runtime::add_app(&root, name, &name.replace('-', "_"))?,
        _ => {}
    }
    Ok(())
}