Skip to main content

omne_cli/commands/
status.rs

1//! `omne status [run_id]` — report per-run or global state.
2//!
3//! Two modes:
4//! - No argument: enumerate all runs under `.omne/var/runs/`, print
5//!   one line per run with pipe name, state, node progress, and last
6//!   timestamp. Orphaned runs get a `[ORPHAN?]` marker.
7//! - With `run_id`: print an indented node-by-node view with
8//!   `✓ / ⏸ / ✗` glyphs, derived from `events.jsonl`.
9
10#![allow(dead_code)]
11
12use std::io::{self, Write};
13use std::path::Path;
14
15use clap::Args as ClapArgs;
16
17use crate::error::CliError;
18use crate::event_log;
19use crate::events::ErrorKind;
20use crate::run_state::{self, NodeStatus, PipeState};
21use crate::volume;
22
23#[derive(Debug, ClapArgs)]
24pub struct Args {
25    /// Run ID to inspect. Omit for a global summary of all runs.
26    pub run_id: Option<String>,
27}
28
29pub fn run(args: &Args) -> Result<(), CliError> {
30    let cwd = std::env::current_dir()
31        .map_err(|e| CliError::Io(format!("cannot determine current directory: {e}")))?;
32    run_at_root(&cwd, args, &mut io::stdout())
33}
34
35/// Test seam: run against an arbitrary starting directory with an
36/// injectable stdout sink.
37pub fn run_at_root(start: &Path, args: &Args, out: &mut dyn Write) -> Result<(), CliError> {
38    let root = volume::find_omne_root(start).ok_or(CliError::NotAVolume)?;
39
40    match &args.run_id {
41        Some(run_id) => show_run(&root, run_id, out),
42        None => show_all(&root, out),
43    }
44}
45
46/// Global listing: one line per run.
47fn show_all(root: &Path, out: &mut dyn Write) -> Result<(), CliError> {
48    let run_ids = event_log::enumerate_runs(root)?;
49    if run_ids.is_empty() {
50        writeln!(out, "No runs found.").map_err(io_err)?;
51        return Ok(());
52    }
53
54    for run_id in &run_ids {
55        let events = match event_log::read_run(root, run_id) {
56            Ok(ev) => ev,
57            Err(e) => {
58                writeln!(out, "{run_id}  (read error: {e})").map_err(io_err)?;
59                continue;
60            }
61        };
62        let summary = run_state::summarize(run_id, &events);
63        let state_str = match &summary.state {
64            PipeState::Running => "running",
65            PipeState::Completed => "completed",
66            PipeState::Aborted { .. } => "aborted",
67        };
68        let orphan_marker = if summary.is_orphan { " [ORPHAN?]" } else { "" };
69        let progress = if summary.node_count > 0 {
70            format!(" ({}/{})", summary.completed_count, summary.node_count)
71        } else {
72            String::new()
73        };
74        writeln!(
75            out,
76            "{run_id}  {pipe} {state_str}{progress}{orphan_marker}  {ts}",
77            pipe = summary.pipe,
78            ts = summary.last_ts,
79        )
80        .map_err(io_err)?;
81    }
82    Ok(())
83}
84
85/// Detailed per-run view with node glyphs.
86fn show_run(root: &Path, run_id: &str, out: &mut dyn Write) -> Result<(), CliError> {
87    if !event_log::run_exists(root, run_id) {
88        return Err(CliError::RunNotFound(run_id.to_string()));
89    }
90
91    let events = event_log::read_run(root, run_id)?;
92    let state = run_state::derive(run_id, &events);
93
94    let pipe_status = match &state.state {
95        PipeState::Running => "running".to_string(),
96        PipeState::Completed => "completed".to_string(),
97        PipeState::Aborted { reason } => format!("aborted: {reason}"),
98    };
99    let orphan_marker = if state.is_orphan { " [ORPHAN?]" } else { "" };
100
101    writeln!(out, "run:    {}", state.run_id).map_err(io_err)?;
102    writeln!(out, "pipe:   {}", state.pipe).map_err(io_err)?;
103    writeln!(out, "status: {pipe_status}{orphan_marker}").map_err(io_err)?;
104    writeln!(out, "last:   {}", state.last_ts).map_err(io_err)?;
105
106    if !state.nodes.is_empty() {
107        writeln!(out).map_err(io_err)?;
108        writeln!(out, "nodes:").map_err(io_err)?;
109        for node in &state.nodes {
110            let (glyph, detail) = match &node.status {
111                NodeStatus::Pending => ("\u{2022}", String::new()), // •
112                NodeStatus::Running => ("\u{25b6}", String::new()), // ▶
113                NodeStatus::Completed => ("\u{2713}", String::new()), // ✓
114                NodeStatus::Failed { kind, message } => {
115                    let kind_str = error_kind_label(*kind);
116                    let msg = match message {
117                        Some(m) => format!(" {kind_str}: {m}"),
118                        None => format!(" {kind_str}"),
119                    };
120                    ("\u{2717}", msg) // ✗
121                }
122            };
123            let kind_tag = node
124                .kind
125                .map(|k| format!(" [{}]", node_kind_label(k)))
126                .unwrap_or_default();
127            writeln!(out, "  {glyph} {id}{kind_tag}{detail}", id = node.id).map_err(io_err)?;
128        }
129    }
130    Ok(())
131}
132
133fn node_kind_label(kind: crate::events::NodeKind) -> &'static str {
134    match kind {
135        crate::events::NodeKind::Command => "command",
136        crate::events::NodeKind::Prompt => "prompt",
137        crate::events::NodeKind::Bash => "bash",
138        crate::events::NodeKind::Loop => "loop",
139    }
140}
141
142fn error_kind_label(kind: ErrorKind) -> &'static str {
143    match kind {
144        ErrorKind::HostMissing => "host_missing",
145        ErrorKind::Timeout => "timeout",
146        ErrorKind::Blocked => "blocked",
147        ErrorKind::GateFailed => "gate_failed",
148        ErrorKind::GateTimeout => "gate_timeout",
149        ErrorKind::Crash => "crash",
150        ErrorKind::MaxIterationsExceeded => "max_iterations_exceeded",
151    }
152}
153
154fn io_err(e: io::Error) -> CliError {
155    CliError::Io(format!("stdout write failed: {e}"))
156}