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
105        .key_path
106        .as_ref()
107        .map(|k| k.to_string_lossy_owned());
108    let key_path = key_path_owned.as_deref().unwrap_or("(not set)");
109    let sudo = r
110        .sudo_password
111        .as_ref()
112        .map_or("(not set)", |s| mask(s.expose_secret()));
113    let su = r
114        .su_password
115        .as_ref()
116        .map_or("(not set)", |s| mask(s.expose_secret()));
117
118    let stdout = io::stdout();
119    let mut out = io::BufWriter::new(stdout.lock());
120    let _ = (|| -> io::Result<()> {
121        writeln!(out, "Name:            {}", r.name)?;
122        writeln!(out, "Host:           {}", r.host)?;
123        writeln!(out, "Port:            {}", r.port)?;
124        writeln!(out, "User:            {}", r.username)?;
125        writeln!(out, "Password:       {password}")?;
126        writeln!(out, "Key path:       {key_path}")?;
127        writeln!(out, "Sudo password:  {sudo}")?;
128        writeln!(out, "Su password:    {su}")?;
129        writeln!(out, "Timeout (ms):   {}", r.timeout_ms)?;
130        writeln!(out, "Max cmd chars:  {}", r.max_command_chars.wire())?;
131        writeln!(out, "Max out chars:  {}", r.max_output_chars.wire())?;
132        writeln!(out, "Disable sudo:   {}", r.disable_sudo)?;
133        writeln!(out, "Schema version: {}", r.schema_version)?;
134        writeln!(out, "Added at:        {}", r.added_at)?;
135        out.flush()
136    })();
137}
138
139/// Prints a single VPS record as masked JSON.
140///
141/// # Errors
142pub fn print_execution_output(output: &ExecutionOutput) {
143    let stdout = io::stdout();
144    let mut out = io::BufWriter::new(stdout.lock());
145    let _ = (|| -> io::Result<()> {
146        writeln!(out, "--- stdout ---")?;
147        if output.stdout.is_empty() {
148            writeln!(out, "(empty)")?;
149        } else {
150            writeln!(out, "{}", output.stdout)?;
151        }
152        writeln!(out, "--- stderr ---")?;
153        if output.stderr.is_empty() {
154            writeln!(out, "(empty)")?;
155        } else {
156            writeln!(out, "{}", output.stderr)?;
157        }
158        match output.exit_code {
159            Some(code) => writeln!(
160                out,
161                "--- exit code: {} ({}ms) ---",
162                code, output.duration_ms
163            )?,
164            None => writeln!(
165                out,
166                "--- exit code: N/A ({}ms) ---",
167                output.duration_ms
168            )?,
169        }
170        // G-IO-04: technical English only on stdout (agent contract).
171        if output.truncated_stdout {
172            writeln!(out, "(stdout was truncated)")?;
173        }
174        if output.truncated_stderr {
175            writeln!(out, "(stderr was truncated)")?;
176        }
177        out.flush()
178    })();
179}
180
181/// Prints SSH command execution output as JSON.
182///
183/// # Errors
184pub fn print_health_check(name: &str, latency_ms: u64) {
185    if is_quiet() {
186        return;
187    }
188    let msg = crate::i18n::t(crate::i18n::Message::HealthCheckOk {
189        name: name.to_string(),
190    });
191    let stdout = io::stdout();
192    let mut out = io::BufWriter::new(stdout.lock());
193    let _ = (|| -> io::Result<()> {
194        writeln!(out, "{msg}")?;
195        writeln!(out, "  latency: {latency_ms}ms")?;
196        out.flush()
197    })();
198}
199