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 monolith).
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(
95    output: &ExecutionOutput,
96    target: &json_wire::ExecTarget,
97) -> io::Result<()> {
98    let v = ExecutionJson::with_target(output, target);
99    match json_wire::print_json_line(&v) {
100        Ok(()) => Ok(()),
101        Err(e) => {
102            report_json_serialize_error(&e);
103            Err(e)
104        }
105    }
106}
107
108/// Prints a health-check result as JSON.
109pub fn print_health_check_json(
110    name: &str,
111    latency_ms: u64,
112    host_source: json_wire::TargetSource,
113) -> io::Result<()> {
114    let v = HealthCheckJson {
115        name: name.to_string(),
116        target: json_wire::TargetEcho::from_parts(name, host_source),
117        status: "ok".into(),
118        latency_ms,
119    };
120    match json_wire::print_json_line(&v) {
121        Ok(()) => Ok(()),
122        Err(e) => {
123            report_json_serialize_error(&e);
124            Err(e)
125        }
126    }
127}