rpi-cli 0.1.13

Terminal coding-agent CLI (the `rpi` binary) built on the rpi-* library crates — a Rust port of @earendil-works/pi-coding-agent's CLI surface
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

struct Fixture {
    _temp: tempfile::TempDir,
    cwd: PathBuf,
    agent_dir: PathBuf,
    marker: PathBuf,
}

impl Fixture {
    fn new() -> Self {
        let temp = tempfile::tempdir().expect("create test directory");
        let cwd = temp.path().join("project");
        let agent_dir = temp.path().join("agent");
        let package_dir = temp.path().join("gate-package");
        let marker = temp.path().join("extension-load-count");
        fs::create_dir_all(&cwd).expect("create project directory");
        fs::create_dir_all(&agent_dir).expect("create agent directory");
        fs::create_dir_all(&package_dir).expect("create package directory");

        let package_json = serde_json::json!({
            "name": "rpi-package-gate-test",
            "version": "1.0.0",
            "rpi": { "extensions": ["extension.js"] }
        });
        fs::write(
            package_dir.join("package.json"),
            serde_json::to_vec(&package_json).expect("serialize package manifest"),
        )
        .expect("write package manifest");

        let marker_literal = serde_json::to_string(&marker.to_string_lossy())
            .expect("serialize marker path for JavaScript");
        let extension = format!(
            "import fs from 'node:fs';\n\
             export default () => {{\n\
               const marker = {marker_literal};\n\
               const count = fs.existsSync(marker) ? Number(fs.readFileSync(marker, 'utf8')) : 0;\n\
               fs.writeFileSync(marker, String(count + 1));\n\
             }};\n"
        );
        fs::write(package_dir.join("extension.js"), extension).expect("write extension");

        let settings = serde_json::json!({
            "packages": [package_dir.to_string_lossy()]
        });
        fs::write(
            agent_dir.join("settings.json"),
            serde_json::to_vec(&settings).expect("serialize settings"),
        )
        .expect("write settings");

        Self {
            _temp: temp,
            cwd,
            agent_dir,
            marker,
        }
    }

    fn run(&self, extra_args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_rpi"))
            .current_dir(&self.cwd)
            .env("RPI_CODING_AGENT_DIR", &self.agent_dir)
            .env("RPI_DISABLE_UPDATE_CHECK", "1")
            .args([
                "--print",
                "--no-session",
                "--provider",
                "anthropic",
                "--model",
                "claude-sonnet-5",
                "--api-key",
                "package-gate-test-key",
            ])
            .args(extra_args)
            .output()
            .expect("run rpi")
    }

    fn clear_marker(&self) {
        match fs::remove_file(&self.marker) {
            Ok(()) => {}
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => panic!("remove marker: {error}"),
        }
    }
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "rpi failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn node_is_available() -> bool {
    Command::new("node")
        .arg("--version")
        .output()
        .is_ok_and(|output| output.status.success())
}

fn marker_count(path: &Path) -> Option<String> {
    fs::read_to_string(path).ok()
}

#[test]
fn configured_pi_packages_require_the_explicit_startup_gate() {
    let fixture = Fixture::new();

    let output = fixture.run(&[]);
    assert_success(&output);
    assert_eq!(marker_count(&fixture.marker), None);

    let output = fixture.run(&["--enable-pi-packages", "--no-extensions"]);
    assert_success(&output);
    assert_eq!(marker_count(&fixture.marker), None);

    if !node_is_available() {
        return;
    }

    fixture.clear_marker();
    let output = fixture.run(&["--enable-pi-packages"]);
    assert_success(&output);
    assert_eq!(marker_count(&fixture.marker).as_deref(), Some("1"));
}