Skip to main content

ssh_cli/output/
json.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-COMP: JSON VPS/exec emitters (extracted from output monólito).
3#![forbid(unsafe_code)]
4//! JSON-mode formatters for VPS inventory and one-shot results.
5
6use super::emit::report_json_serialize_error;
7use crate::json_wire::{
8    self, ExecutionJson, ExportHostJson, HealthCheckJson, MaskedVpsJson, VpsExportJson,
9};
10use crate::ssh::ExecutionOutput;
11use crate::vps::model::VpsRecord;
12use std::collections::BTreeMap;
13use std::io;
14
15/// Prints VPS list as compact JSON array on stdout.
16pub fn print_list_json(records: &[VpsRecord]) -> io::Result<()> {
17    let list: Vec<MaskedVpsJson> = records.iter().map(MaskedVpsJson::from).collect();
18    match json_wire::print_json_line(&list) {
19        Ok(()) => Ok(()),
20        Err(err) => {
21            report_json_serialize_error(&err);
22            Err(err)
23        }
24    }
25}
26
27/// Prints a single VPS record as masked text.
28///
29pub fn print_details_json(r: &VpsRecord) -> io::Result<()> {
30    let v = MaskedVpsJson::from(r);
31    match json_wire::print_json_line(&v) {
32        Ok(()) => Ok(()),
33        Err(err) => {
34            report_json_serialize_error(&err);
35            Err(err)
36        }
37    }
38}
39
40/// Builds a masked VPS JSON DTO (GAP-SSH-JSON-001).
41#[must_use]
42pub fn record_to_masked_json(r: &VpsRecord) -> MaskedVpsJson {
43    MaskedVpsJson::from(r)
44}
45
46/// GAP-SSH-UX-001: hosts for `vps export --json`.
47///
48/// - Redacted (`include_secrets=false`): empty secrets / null optional, **never**
49///   ciphertext `sshcli-enc:` (EXP-001 parity). Empty password → `""` skeleton.
50/// - With secrets: plaintext only if `--include-secrets` (same risk as TOML).
51#[must_use]
52pub fn export_hosts_to_json(
53    hosts: &BTreeMap<String, VpsRecord>,
54    include_secrets: bool,
55) -> BTreeMap<String, ExportHostJson> {
56    let mut map = BTreeMap::new();
57    for (name, r) in hosts {
58        map.insert(
59            name.clone(),
60            ExportHostJson::from_record(r, include_secrets),
61        );
62    }
63    map
64}
65
66/// Full `vps export --json` envelope (typed).
67#[must_use]
68pub fn export_envelope_json(
69    hosts: &BTreeMap<String, VpsRecord>,
70    schema_version: u32,
71    include_secrets: bool,
72) -> VpsExportJson {
73    VpsExportJson {
74        ok: true,
75        event: "vps-export".into(),
76        schema_version,
77        include_secrets,
78        hosts: export_hosts_to_json(hosts, include_secrets),
79    }
80}
81
82/// Prints stdout/stderr from an SSH command execution.
83///
84/// Format:
85/// ```text
86/// --- stdout ---
87/// <stdout>
88/// --- stderr ---
89/// <stderr>
90/// --- exit code: <code> (<duration_ms>ms) ---
91/// ```
92///
93/// Streams under one stdout lock with `writeln!` (G-MAC-02) — no cloned
94pub fn print_execution_output_json(output: &ExecutionOutput) -> io::Result<()> {
95    let v = ExecutionJson::from(output);
96    match json_wire::print_json_line(&v) {
97        Ok(()) => Ok(()),
98        Err(e) => {
99            report_json_serialize_error(&e);
100            Err(e)
101        }
102    }
103}
104
105/// Prints a health-check result as JSON.
106pub fn print_health_check_json(name: &str, latency_ms: u64) -> io::Result<()> {
107    let v = HealthCheckJson {
108        name: name.to_string(),
109        status: "ok".into(),
110        latency_ms,
111    };
112    match json_wire::print_json_line(&v) {
113        Ok(()) => Ok(()),
114        Err(e) => {
115            report_json_serialize_error(&e);
116            Err(e)
117        }
118    }
119}