runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use serde::Serialize;

use super::ScaffoldError;
use super::templates::scaffold_package_files;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunxNewOptions {
    pub name: String,
    pub directory: PathBuf,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RunxNewResult {
    pub name: String,
    pub directory: PathBuf,
    pub files: Vec<String>,
    pub next_steps: Vec<String>,
}

pub fn scaffold_runx_package(options: &RunxNewOptions) -> Result<RunxNewResult, ScaffoldError> {
    let name = sanitize_runx_package_name(&options.name);
    let root = lexical_absolute(&options.directory)?;
    assert_writable_scaffold_target(&root)?;

    let writes = scaffold_package_files(&name);

    fs::create_dir_all(&root)
        .map_err(|source| ScaffoldError::io("creating scaffold root", &root, source))?;
    for file in &writes {
        write_file(&root, &file.relative_path, &file.contents)?;
    }

    Ok(RunxNewResult {
        name,
        directory: root.clone(),
        files: writes.into_iter().map(|file| file.relative_path).collect(),
        next_steps: vec![
            format!("cd {}", root.display()),
            "runx harness . --json".to_owned(),
            "runx skill . --input message=hello --json".to_owned(),
        ],
    })
}

#[must_use]
pub fn sanitize_runx_package_name(value: &str) -> String {
    let sanitized = trim_boundary_separators(&replace_runs(
        &value.trim().to_lowercase(),
        |character| {
            character.is_ascii_lowercase()
                || character.is_ascii_digit()
                || matches!(character, '_' | '.' | '-')
        },
        '-',
    ));
    if sanitized.is_empty() {
        "runx-package".to_owned()
    } else {
        sanitized
    }
}

fn assert_writable_scaffold_target(root: &Path) -> Result<(), ScaffoldError> {
    match fs::read_dir(root) {
        Ok(mut entries) => match entries.next() {
            Some(Ok(_)) => Err(ScaffoldError::NonEmptyTarget {
                path: root.to_path_buf(),
            }),
            Some(Err(source)) => Err(ScaffoldError::io("reading scaffold target", root, source)),
            None => Ok(()),
        },
        Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(source) => Err(ScaffoldError::io("reading scaffold target", root, source)),
    }
}

fn write_file(root: &Path, relative_path: &str, contents: &str) -> Result<(), ScaffoldError> {
    let file_path = root.join(relative_path);
    if let Some(parent) = file_path.parent() {
        fs::create_dir_all(parent)
            .map_err(|source| ScaffoldError::io("creating scaffold directory", parent, source))?;
    }
    let mut writable = contents.to_owned();
    if !writable.ends_with('\n') {
        writable.push('\n');
    }
    fs::write(&file_path, writable)
        .map_err(|source| ScaffoldError::io("writing scaffold file", file_path, source))
}

fn lexical_absolute(path: &Path) -> Result<PathBuf, ScaffoldError> {
    if path.is_absolute() {
        Ok(path.to_path_buf())
    } else {
        std::env::current_dir()
            .map(|cwd| cwd.join(path))
            .map_err(|source| ScaffoldError::io("resolving current directory", ".", source))
    }
}

fn replace_runs(value: &str, keep: impl Fn(char) -> bool, replacement: char) -> String {
    let mut output = String::with_capacity(value.len());
    let mut replacing = false;
    for character in value.chars() {
        if keep(character) {
            output.push(character);
            replacing = false;
        } else if !replacing {
            output.push(replacement);
            replacing = true;
        }
    }
    output
}

fn trim_boundary_separators(value: &str) -> String {
    value
        .trim_matches(|character| matches!(character, '.' | '_' | '-'))
        .to_owned()
}