Skip to main content

ssh_cli/output/
text.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-COMP: human-readable VPS/exec output (extracted from output monĂ³lito).
3#![forbid(unsafe_code)]
4//! Text-mode formatters for VPS CRUD and one-shot execution.
5
6use super::emit::{is_quiet, write_line_human};
7use crate::masking::mask;
8use crate::ssh::ExecutionOutput;
9use crate::vps::model::VpsRecord;
10use secrecy::ExposeSecret;
11use std::io::{self, Write};
12
13/// Prints the doctor report as human text (GAP-SSH-IO-005).
14#[allow(clippy::too_many_arguments)] // doctor report is a flat field set (stable agent surface)
15pub fn print_doctor_text(
16    layer: &str,
17    config_path: &str,
18    exists: bool,
19    perms: &str,
20    schema_version: u32,
21    hosts: usize,
22    known_hosts: &str,
23    active_file: &str,
24    secrets_at_rest: &str,
25    secrets_key_source: &str,
26    secrets_key_file: &str,
27    plaintext_opt_out: bool,
28) {
29    if is_quiet() {
30        return;
31    }
32    let stdout = io::stdout();
33    let mut out = io::BufWriter::new(stdout.lock());
34    let opt_out = if plaintext_opt_out { "yes" } else { "no" };
35    let _ = (|| -> io::Result<()> {
36        writeln!(out, "Winning layer:   {layer}")?;
37        writeln!(out, "Config path:      {config_path}")?;
38        writeln!(out, "Exists:           {exists}")?;
39        writeln!(out, "Permissions:      {perms}")?;
40        writeln!(out, "Schema:           {schema_version}")?;
41        writeln!(out, "Hosts:            {hosts}")?;
42        writeln!(out, "known_hosts:      {known_hosts}")?;
43        writeln!(out, "active file:      {active_file}")?;
44        writeln!(
45            out,
46            "Secrets at-rest:  {secrets_at_rest} (key source: {secrets_key_source})"
47        )?;
48        writeln!(out, "Secrets key file: {secrets_key_file}")?;
49        writeln!(out, "Plaintext opt-out: {opt_out}")?;
50        writeln!(out, "Telemetry:        disabled")?;
51        out.flush()
52    })();
53}
54
55/// Prints the VPS list as masked text.
56///
57/// Streams rows with `writeln!` under one stdout lock (G-MAC-02).
58pub fn print_list_text(records: &[VpsRecord]) {
59    if is_quiet() {
60        return;
61    }
62    if records.is_empty() {
63        write_line_human(&crate::i18n::t(crate::i18n::Message::VpsRegistryEmpty));
64        return;
65    }
66
67    let stdout = io::stdout();
68    let mut out = io::BufWriter::new(stdout.lock());
69    let _ = (|| -> io::Result<()> {
70        writeln!(
71            out,
72            "{:<20} {:<30} {:<6} {:<15} {:<20}",
73            "NAME", "HOST", "PORT", "USER", "PASSWORD"
74        )?;
75        for r in records {
76            writeln!(
77                out,
78                "{:<20} {:<30} {:<6} {:<15} {:<20}",
79                r.name,
80                r.host,
81                r.port,
82                r.username,
83                mask(r.password.expose_secret())
84            )?;
85        }
86        out.flush()
87    })();
88}
89
90/// Prints the VPS list as masked JSON.
91///
92/// # Errors
93pub fn print_details_text(r: &VpsRecord) {
94    if is_quiet() {
95        return;
96    }
97    // GAP-SSH-JSON-001: empty password (key-only) does not fake a masked value.
98    // mask() is &'static str (zero-alloc); keep both branches as &str.
99    let password = if r.password.expose_secret().is_empty() {
100        "(not set)"
101    } else {
102        mask(r.password.expose_secret())
103    };
104    let key_path_owned = r.key_path.as_ref().map(|k| k.to_string_lossy_owned());
105    let key_path = key_path_owned.as_deref().unwrap_or("(not set)");
106    let sudo = r
107        .sudo_password
108        .as_ref()
109        .map_or("(not set)", |s| mask(s.expose_secret()));
110    let su = r
111        .su_password
112        .as_ref()
113        .map_or("(not set)", |s| mask(s.expose_secret()));
114
115    let stdout = io::stdout();
116    let mut out = io::BufWriter::new(stdout.lock());
117    let _ = (|| -> io::Result<()> {
118        writeln!(out, "Name:            {}", r.name)?;
119        writeln!(out, "Host:           {}", r.host)?;
120        writeln!(out, "Port:            {}", r.port)?;
121        writeln!(out, "User:            {}", r.username)?;
122        writeln!(out, "Password:       {password}")?;
123        writeln!(out, "Key path:       {key_path}")?;
124        writeln!(out, "Sudo password:  {sudo}")?;
125        writeln!(out, "Su password:    {su}")?;
126        writeln!(out, "Timeout (ms):   {}", r.timeout_ms)?;
127        writeln!(out, "Max cmd chars:  {}", r.max_command_chars.wire())?;
128        writeln!(out, "Max out chars:  {}", r.max_output_chars.wire())?;
129        writeln!(out, "Disable sudo:   {}", r.disable_sudo)?;
130        writeln!(out, "Schema version: {}", r.schema_version)?;
131        writeln!(out, "Added at:        {}", r.added_at)?;
132        out.flush()
133    })();
134}
135
136/// Prints a single VPS record as masked JSON.
137///
138/// # Errors
139pub fn print_execution_output(output: &ExecutionOutput) {
140    let stdout = io::stdout();
141    let mut out = io::BufWriter::new(stdout.lock());
142    let _ = (|| -> io::Result<()> {
143        writeln!(out, "--- stdout ---")?;
144        if output.stdout.is_empty() {
145            writeln!(out, "(empty)")?;
146        } else {
147            writeln!(out, "{}", output.stdout)?;
148        }
149        writeln!(out, "--- stderr ---")?;
150        if output.stderr.is_empty() {
151            writeln!(out, "(empty)")?;
152        } else {
153            writeln!(out, "{}", output.stderr)?;
154        }
155        match output.exit_code {
156            Some(code) => writeln!(
157                out,
158                "--- exit code: {} ({}ms) ---",
159                code, output.duration_ms
160            )?,
161            None => writeln!(out, "--- exit code: N/A ({}ms) ---", output.duration_ms)?,
162        }
163        // G-IO-04: technical English only on stdout (agent contract).
164        if output.truncated_stdout {
165            writeln!(out, "(stdout was truncated)")?;
166        }
167        if output.truncated_stderr {
168            writeln!(out, "(stderr was truncated)")?;
169        }
170        out.flush()
171    })();
172}
173
174/// Prints SSH command execution output as JSON.
175///
176/// # Errors
177pub fn print_health_check(name: &str, latency_ms: u64) {
178    if is_quiet() {
179        return;
180    }
181    let msg = crate::i18n::t(crate::i18n::Message::HealthCheckOk {
182        name: name.to_string(),
183    });
184    let stdout = io::stdout();
185    let mut out = io::BufWriter::new(stdout.lock());
186    let _ = (|| -> io::Result<()> {
187        writeln!(out, "{msg}")?;
188        writeln!(out, "  latency: {latency_ms}ms")?;
189        out.flush()
190    })();
191}