Skip to main content

ssh_cli/json_wire/
execution.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-COMP: exec/health/scp/sftp/tunnel JSON DTOs (extracted from json_wire monĂ³lito).
3#![forbid(unsafe_code)]
4//! Typed JSON DTOs for one-shot SSH operation results.
5
6use crate::ssh::ExecutionOutput;
7use serde::{Deserialize, Serialize};
8
9/// `exec` / `sudo-exec` / `su-exec` JSON stdout.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct ExecutionJson {
12    /// Captured remote stdout.
13    pub stdout: String,
14    /// Captured remote stderr.
15    pub stderr: String,
16    /// Remote exit code when available.
17    pub exit_code: Option<i32>,
18    /// Whether stdout was truncated by max_output_chars.
19    pub truncated_stdout: bool,
20    /// Whether stderr was truncated by max_output_chars.
21    pub truncated_stderr: bool,
22    /// Wall-clock duration in milliseconds.
23    pub duration_ms: u64,
24}
25
26impl From<&ExecutionOutput> for ExecutionJson {
27    fn from(o: &ExecutionOutput) -> Self {
28        Self {
29            stdout: o.stdout.clone(),
30            stderr: o.stderr.clone(),
31            exit_code: o.exit_code,
32            truncated_stdout: o.truncated_stdout,
33            truncated_stderr: o.truncated_stderr,
34            duration_ms: o.duration_ms,
35        }
36    }
37}
38
39/// `health-check --json` stdout (single host).
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct HealthCheckJson {
42    /// VPS name checked.
43    pub name: String,
44    /// Always `"ok"` on success path.
45    pub status: String,
46    /// Round-trip latency in milliseconds.
47    pub latency_ms: u64,
48}
49
50/// One entry in `health-check --all --json`.
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct HealthHostJson {
53    /// VPS name.
54    pub name: String,
55    /// `"ok"` or `"error"`.
56    pub status: String,
57    /// Latency when measured.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub latency_ms: Option<u64>,
60    /// Error detail when status is error.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub error: Option<String>,
63}
64
65/// `health-check --all --json` batch envelope.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct HealthBatchJson {
68    /// Discriminator.
69    pub event: String,
70    /// Time-ordered UUID v7 correlating this multi-host run (G-DOM-05).
71    pub batch_run_id: String,
72    /// Concurrency budget used for the fan-out.
73    pub max_concurrency: u32,
74    /// Per-host results (stable name order when possible).
75    pub results: Vec<HealthHostJson>,
76}
77
78/// One entry in multi-host `exec --all --json`.
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct ExecHostJson {
81    /// VPS name.
82    pub name: String,
83    /// Whether remote exit was 0.
84    pub ok: bool,
85    /// Remote exit code.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub exit_code: Option<i32>,
88    /// Captured stdout.
89    pub stdout: String,
90    /// Captured stderr.
91    pub stderr: String,
92    /// Duration in milliseconds.
93    pub duration_ms: u64,
94    /// Error summary.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub error: Option<String>,
97}
98
99/// Multi-host exec batch envelope.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101pub struct ExecBatchJson {
102    /// Discriminator.
103    pub event: String,
104    /// Time-ordered UUID v7 correlating this multi-host run (G-DOM-05).
105    pub batch_run_id: String,
106    /// Concurrency budget used.
107    pub max_concurrency: u32,
108    /// Per-host results.
109    pub results: Vec<ExecHostJson>,
110}
111
112/// One entry in multi-host `scp --all --json`.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct ScpHostJson {
115    /// VPS name.
116    pub name: String,
117    /// Whether transfer succeeded.
118    pub ok: bool,
119    /// Bytes transferred when ok.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub bytes: Option<u64>,
122    /// Duration ms when measured.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub duration_ms: Option<u64>,
125    /// Local path used (download may be host-suffixed).
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub local: Option<String>,
128    /// Error detail.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub error: Option<String>,
131}
132
133/// Multi-host SCP batch envelope.
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub struct ScpBatchJson {
136    /// Discriminator.
137    pub event: String,
138    /// Time-ordered UUID v7 correlating this multi-host run (G-DOM-05).
139    pub batch_run_id: String,
140    /// `"upload"` or `"download"`.
141    pub direction: String,
142    /// Concurrency budget used.
143    pub max_concurrency: u32,
144    /// Per-host results.
145    pub results: Vec<ScpHostJson>,
146}
147
148/// `scp upload|download --json` success stdout.
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150pub struct ScpTransferJson {
151    /// Always `true`.
152    pub ok: bool,
153    /// Discriminator: `"scp-transfer"`.
154    pub event: String,
155    /// `"upload"` or `"download"`.
156    pub direction: String,
157    /// VPS name.
158    pub vps: String,
159    /// Local path.
160    pub local: String,
161    /// Remote path.
162    pub remote: String,
163    /// Bytes transferred.
164    pub bytes: u64,
165    /// Duration in milliseconds.
166    pub duration_ms: u64,
167}
168
169/// `sftp upload|download --json` success stdout (G-SFTP-09).
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171pub struct SftpTransferJson {
172    /// Always `true`.
173    pub ok: bool,
174    /// Discriminator: `"sftp-transfer"`.
175    pub event: String,
176    /// `"upload"` or `"download"`.
177    pub direction: String,
178    /// VPS name.
179    pub vps: String,
180    /// Local path.
181    pub local: String,
182    /// Remote path.
183    pub remote: String,
184    /// Bytes transferred.
185    pub bytes: u64,
186    /// Duration in milliseconds.
187    pub duration_ms: u64,
188    /// Whether the transfer was recursive.
189    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
190    pub recursive: bool,
191}
192
193/// One entry in `sftp ls --json`.
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
195pub struct SftpListEntryJson {
196    /// Base name.
197    pub name: String,
198    /// Full remote path.
199    pub path: String,
200    /// `file` | `dir` | `symlink` | `other`.
201    pub kind: String,
202    /// Size when known.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub size: Option<u64>,
205    /// Mode bits when known.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub mode: Option<u32>,
208}
209
210/// `sftp ls --json` success stdout.
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212pub struct SftpListJson {
213    /// Always `true`.
214    pub ok: bool,
215    /// Discriminator: `"sftp-list"`.
216    pub event: String,
217    /// VPS name.
218    pub vps: String,
219    /// Directory path listed.
220    pub path: String,
221    /// Entries.
222    pub entries: Vec<SftpListEntryJson>,
223}
224
225/// `sftp mkdir|rmdir|rm|rename|stat --json` (stat uses size/mode fields).
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227pub struct SftpFsOpJson {
228    /// Always `true`.
229    pub ok: bool,
230    /// Discriminator: `"sftp-fs-op"`.
231    pub event: String,
232    /// Operation name.
233    pub op: String,
234    /// VPS name.
235    pub vps: String,
236    /// Primary path.
237    pub path: String,
238    /// Rename target when applicable.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub to: Option<String>,
241    /// Duration ms.
242    pub duration_ms: u64,
243    /// Stat kind when op=stat.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub kind: Option<String>,
246    /// Stat size when op=stat.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub size: Option<u64>,
249    /// Stat mode when op=stat.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub mode: Option<u32>,
252    /// Stat mtime when op=stat.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub mtime: Option<u32>,
255}
256
257/// Multi-host SFTP batch (reuses scp-host shape).
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259pub struct SftpBatchJson {
260    /// Discriminator: `"sftp-batch"`.
261    pub event: String,
262    /// UUID v7 batch id.
263    pub batch_run_id: String,
264    /// `"upload"` or `"download"`.
265    pub direction: String,
266    /// Concurrency budget.
267    pub max_concurrency: u32,
268    /// Per-host results.
269    pub results: Vec<ScpHostJson>,
270}
271
272/// `tunnel --json` post-bind event.
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274pub struct TunnelListeningJson {
275    /// Always `true`.
276    pub ok: bool,
277    /// Discriminator: `"tunnel_listening"`.
278    pub event: String,
279    /// VPS name.
280    pub vps: String,
281    /// Local listen port.
282    pub local_port: u16,
283    /// Remote target host.
284    pub remote_host: String,
285    /// Remote target port.
286    pub remote_port: u16,
287    /// One-shot timeout in milliseconds.
288    pub timeout_ms: u64,
289}