omne-cli 0.2.0

CLI for managing omne volumes: init, upgrade, and validate kernel and distro releases
Documentation
//! `omne status [run_id]` — report per-run or global state.
//!
//! Two modes:
//! - No argument: enumerate all runs under `.omne/var/runs/`, print
//!   one line per run with pipe name, state, node progress, and last
//!   timestamp. Orphaned runs get a `[ORPHAN?]` marker.
//! - With `run_id`: print an indented node-by-node view with
//!   `✓ / ⏸ / ✗` glyphs, derived from `events.jsonl`.

#![allow(dead_code)]

use std::io::{self, Write};
use std::path::Path;

use clap::Args as ClapArgs;

use crate::error::CliError;
use crate::event_log;
use crate::events::ErrorKind;
use crate::run_state::{self, NodeStatus, PipeState};
use crate::volume;

#[derive(Debug, ClapArgs)]
pub struct Args {
    /// Run ID to inspect. Omit for a global summary of all runs.
    pub run_id: Option<String>,
}

pub fn run(args: &Args) -> Result<(), CliError> {
    let cwd = std::env::current_dir()
        .map_err(|e| CliError::Io(format!("cannot determine current directory: {e}")))?;
    run_at_root(&cwd, args, &mut io::stdout())
}

/// Test seam: run against an arbitrary starting directory with an
/// injectable stdout sink.
pub fn run_at_root(start: &Path, args: &Args, out: &mut dyn Write) -> Result<(), CliError> {
    let root = volume::find_omne_root(start).ok_or(CliError::NotAVolume)?;

    match &args.run_id {
        Some(run_id) => show_run(&root, run_id, out),
        None => show_all(&root, out),
    }
}

/// Global listing: one line per run.
fn show_all(root: &Path, out: &mut dyn Write) -> Result<(), CliError> {
    let run_ids = event_log::enumerate_runs(root)?;
    if run_ids.is_empty() {
        writeln!(out, "No runs found.").map_err(io_err)?;
        return Ok(());
    }

    for run_id in &run_ids {
        let events = match event_log::read_run(root, run_id) {
            Ok(ev) => ev,
            Err(e) => {
                writeln!(out, "{run_id}  (read error: {e})").map_err(io_err)?;
                continue;
            }
        };
        let summary = run_state::summarize(run_id, &events);
        let state_str = match &summary.state {
            PipeState::Running => "running",
            PipeState::Completed => "completed",
            PipeState::Aborted { .. } => "aborted",
        };
        let orphan_marker = if summary.is_orphan { " [ORPHAN?]" } else { "" };
        let progress = if summary.node_count > 0 {
            format!(" ({}/{})", summary.completed_count, summary.node_count)
        } else {
            String::new()
        };
        writeln!(
            out,
            "{run_id}  {pipe} {state_str}{progress}{orphan_marker}  {ts}",
            pipe = summary.pipe,
            ts = summary.last_ts,
        )
        .map_err(io_err)?;
    }
    Ok(())
}

/// Detailed per-run view with node glyphs.
fn show_run(root: &Path, run_id: &str, out: &mut dyn Write) -> Result<(), CliError> {
    if !event_log::run_exists(root, run_id) {
        return Err(CliError::RunNotFound(run_id.to_string()));
    }

    let events = event_log::read_run(root, run_id)?;
    let state = run_state::derive(run_id, &events);

    let pipe_status = match &state.state {
        PipeState::Running => "running".to_string(),
        PipeState::Completed => "completed".to_string(),
        PipeState::Aborted { reason } => format!("aborted: {reason}"),
    };
    let orphan_marker = if state.is_orphan { " [ORPHAN?]" } else { "" };

    writeln!(out, "run:    {}", state.run_id).map_err(io_err)?;
    writeln!(out, "pipe:   {}", state.pipe).map_err(io_err)?;
    writeln!(out, "status: {pipe_status}{orphan_marker}").map_err(io_err)?;
    writeln!(out, "last:   {}", state.last_ts).map_err(io_err)?;

    if !state.nodes.is_empty() {
        writeln!(out).map_err(io_err)?;
        writeln!(out, "nodes:").map_err(io_err)?;
        for node in &state.nodes {
            let (glyph, detail) = match &node.status {
                NodeStatus::Pending => ("\u{2022}", String::new()), //                NodeStatus::Running => ("\u{25b6}", String::new()), //                NodeStatus::Completed => ("\u{2713}", String::new()), //                NodeStatus::Failed { kind, message } => {
                    let kind_str = error_kind_label(*kind);
                    let msg = match message {
                        Some(m) => format!(" {kind_str}: {m}"),
                        None => format!(" {kind_str}"),
                    };
                    ("\u{2717}", msg) //                }
            };
            let kind_tag = node
                .kind
                .map(|k| format!(" [{}]", node_kind_label(k)))
                .unwrap_or_default();
            writeln!(out, "  {glyph} {id}{kind_tag}{detail}", id = node.id).map_err(io_err)?;
        }
    }
    Ok(())
}

fn node_kind_label(kind: crate::events::NodeKind) -> &'static str {
    match kind {
        crate::events::NodeKind::Command => "command",
        crate::events::NodeKind::Prompt => "prompt",
        crate::events::NodeKind::Bash => "bash",
        crate::events::NodeKind::Loop => "loop",
    }
}

fn error_kind_label(kind: ErrorKind) -> &'static str {
    match kind {
        ErrorKind::HostMissing => "host_missing",
        ErrorKind::Timeout => "timeout",
        ErrorKind::Blocked => "blocked",
        ErrorKind::GateFailed => "gate_failed",
        ErrorKind::GateTimeout => "gate_timeout",
        ErrorKind::Crash => "crash",
        ErrorKind::MaxIterationsExceeded => "max_iterations_exceeded",
    }
}

fn io_err(e: io::Error) -> CliError {
    CliError::Io(format!("stdout write failed: {e}"))
}