arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
use std::process::{Child, Command, Stdio};

use super::output::sanitize;
use super::{ProcessError, ProcessSpec};

pub(crate) fn spawn(spec: &ProcessSpec) -> Result<Child, ProcessError> {
    let mut command = Command::new(spec.program());
    command
        .args(spec.arguments())
        .current_dir(spec.directory())
        .envs(spec.environment().iter().cloned())
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit());
    #[cfg(windows)]
    if spec.new_process_group_enabled() {
        use std::os::windows::process::CommandExt;
        // CREATE_NEW_PROCESS_GROUP = 0x00000200. Children in their own group
        // do not receive console control events sent to the supervisor's
        // group, so an intentional `arc dev` shutdown signal does not
        // propagate to supervised children. Ctrl+C is also disabled for
        // the new group; the supervisor terminates children explicitly.
        const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
        command.creation_flags(CREATE_NEW_PROCESS_GROUP);
    }
    command.spawn().map_err(|source| ProcessError::Spawn {
        program: spec.program().to_string_lossy().into_owned(),
        directory: spec.directory().to_path_buf(),
        source,
    })
}

pub(crate) fn run(spec: &ProcessSpec) -> Result<(), ProcessError> {
    let mut child = spawn(spec)?;
    let status = child.wait().map_err(|source| ProcessError::Wait {
        program: spec.program().to_string_lossy().into_owned(),
        source,
    })?;
    if status.success() {
        Ok(())
    } else {
        Err(ProcessError::Failed {
            program: spec.program().to_string_lossy().into_owned(),
            directory: spec.directory().to_path_buf(),
            status,
        })
    }
}

pub(crate) fn run_quiet(spec: &ProcessSpec) -> Result<(), ProcessError> {
    let output = Command::new(spec.program())
        .args(spec.arguments())
        .current_dir(spec.directory())
        .envs(spec.environment().iter().cloned())
        .stdin(Stdio::null())
        .output()
        .map_err(|source| ProcessError::Spawn {
            program: spec.program().to_string_lossy().into_owned(),
            directory: spec.directory().to_path_buf(),
            source,
        })?;
    if output.status.success() {
        Ok(())
    } else {
        let bytes = if output.stderr.is_empty() {
            &output.stdout
        } else {
            &output.stderr
        };
        Err(ProcessError::FailedOutput {
            program: spec.program().to_string_lossy().into_owned(),
            directory: spec.directory().to_path_buf(),
            status: output.status,
            output: sanitize(bytes),
        })
    }
}

pub(crate) fn run_capture(spec: &ProcessSpec) -> Result<Vec<u8>, ProcessError> {
    let output = Command::new(spec.program())
        .args(spec.arguments())
        .current_dir(spec.directory())
        .envs(spec.environment().iter().cloned())
        .stdin(Stdio::null())
        .output()
        .map_err(|source| ProcessError::Spawn {
            program: spec.program().to_string_lossy().into_owned(),
            directory: spec.directory().to_path_buf(),
            source,
        })?;
    if output.status.success() {
        Ok(output.stdout)
    } else {
        let bytes = if output.stderr.is_empty() {
            &output.stdout
        } else {
            &output.stderr
        };
        Err(ProcessError::FailedOutput {
            program: spec.program().to_string_lossy().into_owned(),
            directory: spec.directory().to_path_buf(),
            status: output.status,
            output: sanitize(bytes),
        })
    }
}