opencode-ralph-loop-cli 0.1.0

Scaffolder CLI for OpenCode Ralph Loop plugin — one command setup
Documentation
use std::path::{Path, PathBuf};
use std::time::Instant;

use crate::cli::{OutputFormat, UninstallArgs};
use crate::error::CliError;
use crate::output::{Action, FileEntry, Report};

pub fn run(args: &UninstallArgs, output: &OutputFormat) -> Result<(), CliError> {
    let start = Instant::now();

    let target = resolve_target(&args.path)?;
    let opencode_dir = target.join(".opencode");

    let manifest = if args.force {
        crate::manifest::load(&opencode_dir)
            .unwrap_or_else(|_| crate::manifest::Manifest::new("unknown"))
    } else {
        crate::manifest::load(&opencode_dir)?
    };

    let plugin_version = manifest.plugin_version.clone();
    let mut report = Report::new("uninstall", &plugin_version);

    for file in &manifest.files {
        if args.keep_state && file.path == "ralph-loop.local.md" {
            continue;
        }
        if args.keep_node_modules && file.path.starts_with("node_modules") {
            continue;
        }

        let dest = opencode_dir.join(&file.path);

        let action = if dest.exists() {
            if !args.dry_run {
                std::fs::remove_file(&dest)
                    .map_err(|e| CliError::io(dest.to_string_lossy().into_owned(), e))?;
            }
            Action::Removed
        } else {
            Action::Missing
        };

        report.files.push(FileEntry {
            path: file.path.clone(),
            action,
            sha256: file.sha256.clone(),
            expected_sha256: None,
            size_bytes: file.size_bytes,
        });
    }

    if !args.dry_run {
        crate::manifest::remove(&opencode_dir)?;
    }

    report.duration_ms = start.elapsed().as_millis() as u64;
    report.finalize();
    emit_report(&report, output);

    Ok(())
}

fn resolve_target(path: &Path) -> Result<PathBuf, CliError> {
    let resolved = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .map_err(|e| CliError::io("current directory", e))?
            .join(path)
    };

    if !resolved.exists() {
        return Err(CliError::Io {
            path: resolved.to_string_lossy().to_string(),
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "directory does not exist"),
        });
    }

    Ok(resolved)
}

fn emit_report(report: &Report, format: &OutputFormat) {
    match format {
        OutputFormat::Text => crate::output::text::print_report(report),
        OutputFormat::Json => crate::output::json::print_report(report),
        OutputFormat::Ndjson => crate::output::ndjson::print_report(report),
        OutputFormat::Quiet => {}
    }
}