repopilot 0.10.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
#![cfg(unix)]

use std::env;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

fn install_script() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("install.sh")
}

fn write_executable(path: &Path, content: &str) {
    fs::write(path, content).unwrap_or_else(|error| {
        panic!("failed to write {}: {error}", path.display());
    });

    let mut permissions = fs::metadata(path)
        .unwrap_or_else(|error| panic!("failed to stat {}: {error}", path.display()))
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(path, permissions).unwrap_or_else(|error| {
        panic!("failed to chmod {}: {error}", path.display());
    });
}

fn fake_curl_script() -> &'static str {
    r#"#!/bin/sh
set -eu

out=""
url=""

while [ "$#" -gt 0 ]; do
  case "$1" in
    -o)
      out="$2"
      shift 2
      ;;
    -*)
      shift
      ;;
    *)
      url="$1"
      shift
      ;;
  esac
done

case "$url" in
  *api.github.com*)
    printf '{"tag_name":"v0.9.0"}\n'
    ;;
  *.sha256)
    if [ "${REPOPILOT_FAKE_CURL_MODE:-}" = "missing-checksum" ]; then
      exit 22
    fi
    printf '%s  repopilot-test.tar.gz\n' "${REPOPILOT_FAKE_EXPECTED_SHA:-0000000000000000000000000000000000000000000000000000000000000000}" > "$out"
    ;;
  *.tar.gz)
    if [ "${REPOPILOT_FAKE_CURL_MODE:-}" = "success" ]; then
      cat "$REPOPILOT_FAKE_ARCHIVE" > "$out"
    else
      printf 'not a tarball\n' > "$out"
    fi
    ;;
  *)
    exit 2
    ;;
esac
"#
}

fn fake_sha256sum_script() -> &'static str {
    r#"#!/bin/sh
set -eu

printf '%s  %s\n' "${REPOPILOT_FAKE_ACTUAL_SHA:-1111111111111111111111111111111111111111111111111111111111111111}" "$1"
"#
}

fn run_install_with_path(path: &str, mode: &str) -> Output {
    let temp = tempfile::tempdir().expect("failed to create temp dir");

    Command::new(find_executable("bash"))
        .arg(install_script())
        .env("PATH", path)
        .env("INSTALL_DIR", temp.path().join("bin"))
        .env("REPOPILOT_FAKE_CURL_MODE", mode)
        .output()
        .expect("failed to run install.sh")
}

fn path_with_fake_tools(fake_tools_dir: &Path) -> String {
    let old_path = env::var("PATH").unwrap_or_default();
    format!("{}:{old_path}", fake_tools_dir.display())
}

fn find_executable(name: &str) -> PathBuf {
    env::var_os("PATH")
        .and_then(|paths| {
            env::split_paths(&paths)
                .map(|dir| dir.join(name))
                .find(|candidate| candidate.is_file())
        })
        .unwrap_or_else(|| panic!("failed to find required test tool: {name}"))
}

fn isolated_path_without_sha_tools(fake_tools_dir: &Path) -> String {
    for tool in ["awk", "basename", "grep", "mktemp", "rm", "sed", "uname"] {
        symlink(find_executable(tool), fake_tools_dir.join(tool))
            .unwrap_or_else(|error| panic!("failed to link {tool}: {error}"));
    }

    fake_tools_dir.display().to_string()
}

fn create_release_archive(root: &Path) -> PathBuf {
    let payload = root.join("payload");
    fs::create_dir(&payload).expect("failed to create payload dir");
    write_executable(
        &payload.join("repopilot"),
        "#!/bin/sh\nprintf 'repopilot 0.9.0\\n'\n",
    );

    let archive = root.join("repopilot.tar.gz");
    let output = Command::new(find_executable("tar"))
        .args([
            "-czf",
            archive.to_str().expect("non-utf8 archive path"),
            "-C",
            payload.to_str().expect("non-utf8 payload path"),
            "repopilot",
        ])
        .output()
        .expect("failed to create release archive");

    assert!(
        output.status.success(),
        "failed to create release archive\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );

    archive
}

#[test]
fn install_script_passes_bash_syntax_check() {
    let output = Command::new(find_executable("bash"))
        .arg("-n")
        .arg(install_script())
        .output()
        .expect("failed to run bash -n install.sh");

    assert!(
        output.status.success(),
        "install.sh should pass bash -n\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn install_script_aborts_when_checksum_download_fails() {
    let temp = tempfile::tempdir().expect("failed to create temp dir");
    let fake_bin = temp.path().join("bin");
    fs::create_dir(&fake_bin).expect("failed to create fake bin");
    write_executable(&fake_bin.join("curl"), fake_curl_script());

    let output = run_install_with_path(&path_with_fake_tools(&fake_bin), "missing-checksum");

    assert!(
        !output.status.success(),
        "install.sh should fail when checksum download fails"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Failed to download required checksum file"),
        "stderr should name the missing checksum\nstderr:\n{stderr}"
    );
    assert!(
        stderr.contains("Installation aborted for safety"),
        "stderr should explain the safety abort\nstderr:\n{stderr}"
    );
}

#[test]
fn install_script_installs_when_checksum_verification_succeeds() {
    let temp = tempfile::tempdir().expect("failed to create temp dir");
    let fake_bin = temp.path().join("bin");
    let install_dir = temp.path().join("install");
    let archive = create_release_archive(temp.path());
    let fake_sha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

    fs::create_dir(&fake_bin).expect("failed to create fake bin");
    write_executable(&fake_bin.join("curl"), fake_curl_script());
    write_executable(&fake_bin.join("sha256sum"), fake_sha256sum_script());

    let output = Command::new(find_executable("bash"))
        .arg(install_script())
        .env("PATH", path_with_fake_tools(&fake_bin))
        .env("INSTALL_DIR", &install_dir)
        .env("REPOPILOT_FAKE_CURL_MODE", "success")
        .env("REPOPILOT_FAKE_ARCHIVE", archive)
        .env("REPOPILOT_FAKE_EXPECTED_SHA", fake_sha)
        .env("REPOPILOT_FAKE_ACTUAL_SHA", fake_sha)
        .output()
        .expect("failed to run install.sh");

    assert!(
        output.status.success(),
        "install.sh should install when checksum verification succeeds\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        install_dir.join("repopilot").is_file(),
        "installer should place repopilot in INSTALL_DIR"
    );
}

#[test]
fn install_script_aborts_when_sha256_tool_is_missing() {
    let temp = tempfile::tempdir().expect("failed to create temp dir");
    let fake_bin = temp.path().join("bin");
    fs::create_dir(&fake_bin).expect("failed to create fake bin");
    write_executable(&fake_bin.join("curl"), fake_curl_script());

    let output = run_install_with_path(
        &isolated_path_without_sha_tools(&fake_bin),
        "invalid-checksum",
    );

    assert!(
        !output.status.success(),
        "install.sh should fail when no SHA256 tool is available"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("No SHA256 verification tool found"),
        "stderr should explain the missing SHA256 tool\nstderr:\n{stderr}"
    );
    assert!(
        stderr.contains("Installation aborted for safety"),
        "stderr should explain the safety abort\nstderr:\n{stderr}"
    );
}

#[test]
fn install_script_aborts_when_checksum_verification_fails() {
    let temp = tempfile::tempdir().expect("failed to create temp dir");
    let fake_bin = temp.path().join("bin");
    fs::create_dir(&fake_bin).expect("failed to create fake bin");
    write_executable(&fake_bin.join("curl"), fake_curl_script());
    write_executable(&fake_bin.join("sha256sum"), fake_sha256sum_script());

    let output = run_install_with_path(&path_with_fake_tools(&fake_bin), "invalid-checksum");

    assert!(
        !output.status.success(),
        "install.sh should fail when checksum verification fails"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("SHA256 verification failed"),
        "stderr should explain the checksum mismatch\nstderr:\n{stderr}"
    );
    assert!(
        stderr.contains("Installation aborted for safety"),
        "stderr should explain the safety abort\nstderr:\n{stderr}"
    );
}