Skip to main content

horto_os_ui_shared/remote/runner/
status.rs

1//! Parse and fetch remote setup status and doctor reports.
2
3use super::super::process::ProcessRunner;
4use super::options::RemoteOptions;
5use super::probe::remote_box_snapshot;
6use crate::error::Result;
7
8/// Parse the first JSON object/array from remote captured output.
9///
10/// # Errors
11///
12/// Returns [`crate::HortoError`] when no JSON is found or deserialization fails.
13pub fn parse_remote_json<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T> {
14    let start = raw.find('{').or_else(|| raw.find('[')).ok_or_else(|| {
15        crate::error::HortoError::msg("remote output had no JSON object or array")
16    })?;
17    serde_json::from_str(raw[start..].trim())
18        .map_err(|e| crate::error::HortoError::msg(format!("parse remote JSON: {e}")))
19}
20
21/// Parse human `setup status` lines from older box agents (no `--json`).
22pub fn parse_setup_status_text(raw: &str) -> Result<crate::ops::status::SetupStatusReport> {
23    use crate::ops::status::{SetupStatusReport, StepStatusRow};
24    let mut kind = "full".to_owned();
25    let mut steps = Vec::new();
26    for line in raw.lines() {
27        let line = line.trim();
28        if let Some(rest) = line.strip_prefix("Setup kind:") {
29            rest.trim().clone_into(&mut kind);
30            continue;
31        }
32        let Some(rest) = line.strip_prefix('[') else {
33            continue;
34        };
35        let Some((status_part, after_status)) = rest.split_once(']') else {
36            continue;
37        };
38        let status = status_part.trim().to_owned();
39        let after_status = after_status.trim();
40        let Some((id, after_id)) = after_status.split_once(" - ") else {
41            continue;
42        };
43        let (title, ver_flags) = after_id
44            .rfind(" (v")
45            .map_or((after_id, ""), |i| (&after_id[..i], &after_id[i..]));
46        let step_version = ver_flags
47            .trim_start_matches(" (v")
48            .split(')')
49            .next()
50            .and_then(|s| s.parse().ok())
51            .unwrap_or(0);
52        steps.push(StepStatusRow {
53            id: id.trim().to_owned(),
54            title: title.trim().to_owned(),
55            status,
56            step_version,
57            destructive: ver_flags.contains("[destructive]"),
58            needs_reboot_after: ver_flags.contains("[reboot]"),
59        });
60    }
61    if steps.is_empty() {
62        return Err(crate::error::HortoError::msg(
63            "remote setup status produced no step lines",
64        ));
65    }
66    Ok(SetupStatusReport { kind, steps })
67}
68
69/// Fetch setup step status from the box (`setup status`, JSON when supported).
70///
71/// # Errors
72///
73/// Returns [`crate::HortoError`] when SSH fails or status output cannot be parsed.
74pub fn remote_setup_status(
75    runner: &dyn ProcessRunner,
76    opts: &RemoteOptions,
77    full: bool,
78) -> Result<crate::ops::status::SetupStatusReport> {
79    let snap = remote_box_snapshot(runner, opts, full)?;
80    snap.setup.ok_or_else(|| {
81        crate::error::HortoError::msg(
82            "box CLI missing or outdated; sync CLI (s0) before setup status",
83        )
84    })
85}
86
87/// Fetch doctor JSON from the box.
88///
89/// # Errors
90///
91/// Returns [`crate::HortoError`] when SSH fails or the remote JSON cannot be parsed.
92pub fn remote_doctor(
93    runner: &dyn ProcessRunner,
94    opts: &RemoteOptions,
95) -> Result<crate::ops::doctor::DoctorReport> {
96    let snap = remote_box_snapshot(runner, opts, true)?;
97    snap.doctor.ok_or_else(|| {
98        crate::error::HortoError::msg("box CLI missing or outdated; sync CLI (s0) before doctor")
99    })
100}