noxid-cli 0.2.1

The Noxid compiler command line: check, build, test, adapt, and the agent surface
//! The repair transaction, re-exported (WO-51 round 3).
//!
//! The journal, its commit protocol, and the recovery every reader runs before
//! it reads a source byte now live in `noxid-semantic-transactions`: the
//! language server loads projects without going through this binary, and it
//! must run the same recovery. See
//! `noxid_semantic_transactions::repair_journal` for the contract, and
//! `recover_before_read` for the hook every entry point calls.

pub use noxid_semantic_transactions::repair_journal::{
    FileRepair, RecoveryError, RepairTransaction, recover_before_read, replace_file,
};

use noxid_semantic_transactions::repair_journal::{JournalReport, discard, inspect, root_for};
use noxid_source::json_escape;
use std::path::Path;

/// `noxid repair <file|project> --inspect [--discard <transaction-id>]`.
///
/// The escape hatch for the case the fail-closed rule creates. A journal
/// recovery refuses stops every reader of the project — that is the point, and
/// QA round 4 judged it correct — but until now the only way out was `rm`
/// under `.nox/transactions`, which is a bad remedy to name in an error
/// message and a worse one to perform blind. So the recovery errors name this
/// command, and this command shows what `rm` would have destroyed before it
/// destroys it.
///
/// It is the one reader that must run *before* `recover_before_read`: the hook
/// is exactly what it exists to unblock, so `main` dispatches it ahead of the
/// hook rather than through it. It settles nothing, claims nothing, and
/// changes nothing unless `--discard` names a transaction.
pub fn run_inspect(input: &Path, args: &[String]) -> Result<(), String> {
    let mut discarding: Option<String> = None;
    let mut arguments = args.iter().filter(|argument| *argument != "--inspect");
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--discard" => {
                if discarding.is_some() {
                    return Err("noxid repair --inspect accepts --discard at most once".into());
                }
                discarding = Some(
                    arguments
                        .next()
                        .ok_or(
                            "noxid repair --inspect --discard requires a transaction id; run \
                             --inspect on its own to list them",
                        )?
                        .clone(),
                );
            }
            other => {
                return Err(format!(
                    "unknown repair --inspect argument `{other}`; usage: noxid repair \
                     <file|project> --inspect [--discard <transaction-id>]"
                ));
            }
        }
    }
    let project = root_for(input);
    let journals = inspect(&project)?;
    let discarded = match discarding {
        Some(id) => discard(&project, &id)?,
        None => Vec::new(),
    };
    println!(
        "{{\"schemaVersion\":1,\"mode\":\"repair-inspect\",\"project\":\"{}\",\"journals\":[{}],\"discarded\":[{}]}}",
        json_escape(&project.display().to_string()),
        journals
            .iter()
            .map(journal_json)
            .collect::<Vec<_>>()
            .join(","),
        discarded
            .iter()
            .map(|name| format!("\"{}\"", json_escape(name)))
            .collect::<Vec<_>>()
            .join(",")
    );
    Ok(())
}

fn journal_json(journal: &JournalReport) -> String {
    let optional = |value: &Option<String>| match value {
        Some(text) => format!("\"{}\"", json_escape(text)),
        None => "null".to_string(),
    };
    format!(
        "{{\"transaction\":\"{}\",\"name\":\"{}\",\"claim\":{},\"refusal\":{},\"files\":[{}]}}",
        json_escape(&journal.id),
        json_escape(&journal.name),
        optional(&journal.claim),
        optional(&journal.refusal),
        journal
            .files
            .iter()
            .map(|file| format!(
                "{{\"target\":\"{}\",\"state\":\"{}\",\"copy\":{},\"staged\":{}}}",
                json_escape(&file.target),
                file.state,
                file.copy,
                file.staged
            ))
            .collect::<Vec<_>>()
            .join(",")
    )
}