Skip to main content

blotter/
output.rs

1use crate::error::AppError;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::io::{self, Write};
5
6pub const CONTRACT: u8 = 6;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Meta {
10    pub contract: u8,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub file: Option<String>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub agent_source: Option<String>,
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub warnings: Vec<String>,
17}
18
19impl Meta {
20    pub fn new() -> Self {
21        Self::default()
22    }
23}
24
25impl Default for Meta {
26    fn default() -> Self {
27        Self {
28            contract: CONTRACT,
29            file: None,
30            agent_source: None,
31            warnings: Vec::new(),
32        }
33    }
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37pub struct SuccessEnvelope<T> {
38    pub ok: bool,
39    pub data: T,
40    pub meta: Meta,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44pub struct ErrorEnvelope {
45    pub ok: bool,
46    pub error: ErrorBody,
47    pub meta: Meta,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct ErrorBody {
52    pub code: String,
53    pub message: String,
54    pub details: Value,
55    pub retryable: bool,
56    pub suggested_fix: String,
57}
58
59#[cfg(unix)]
60pub fn stdout_writer() -> io::Result<Box<dyn Write>> {
61    use std::os::fd::AsFd;
62
63    let stdout = io::stdout();
64    let stdout = std::fs::File::from(stdout.as_fd().try_clone_to_owned()?);
65    Ok(Box::new(io::BufWriter::new(stdout)))
66}
67
68#[cfg(windows)]
69pub fn stdout_writer() -> io::Result<Box<dyn Write>> {
70    use std::io::IsTerminal;
71    use std::os::windows::io::AsHandle;
72
73    let stdout = io::stdout();
74    // An interactive console needs Rust's console-aware writer (UTF-8 →
75    // WriteConsoleW); a raw duplicated handle goes through WriteFile and the
76    // active code page, producing mojibake for non-ASCII text. The handle dup
77    // is only for redirected stdout, where write errors must not be suppressed.
78    if stdout.is_terminal() {
79        return Ok(Box::new(io::BufWriter::new(stdout)));
80    }
81    let stdout = std::fs::File::from(stdout.as_handle().try_clone_to_owned()?);
82    Ok(Box::new(io::BufWriter::new(stdout)))
83}
84
85#[cfg(not(any(unix, windows)))]
86pub fn stdout_writer() -> io::Result<Box<dyn Write>> {
87    Ok(Box::new(io::BufWriter::new(io::stdout())))
88}
89
90pub fn write_success<T: Serialize>(data: T, pretty: bool, meta: Meta) -> io::Result<()> {
91    let envelope = SuccessEnvelope {
92        ok: true,
93        data,
94        meta,
95    };
96    let mut output = stdout_writer()?;
97    if pretty {
98        serde_json::to_writer_pretty(&mut output, &envelope)?;
99    } else {
100        serde_json::to_writer(&mut output, &envelope)?;
101    }
102    writeln!(output)?;
103    output.flush()
104}
105
106pub fn collapse_markdown_text(text: &str) -> String {
107    text.split_whitespace().collect::<Vec<_>>().join(" ")
108}
109
110pub fn write_error(error: &AppError) -> i32 {
111    let envelope = ErrorEnvelope {
112        ok: false,
113        error: ErrorBody {
114            code: error.code.into(),
115            message: error.message.clone(),
116            details: error.details.clone(),
117            retryable: error.retryable,
118            suggested_fix: error.suggested_fix.clone(),
119        },
120        meta: Meta::new(),
121    };
122    let mut output = io::BufWriter::new(io::stderr().lock());
123    // There is no way to report a failure to report a failure.
124    let _ = serde_json::to_writer(&mut output, &envelope);
125    let _ = writeln!(output);
126    error.exit_code
127}