Skip to main content

pitchfork_cli/cli/
json_output.rs

1use crate::log_store::LogEntry;
2use serde::Serialize;
3
4#[derive(Serialize)]
5pub struct JsonListEntry {
6    pub id: String,
7    pub namespace: String,
8    pub name: String,
9    pub pid: Option<u32>,
10    pub status: String,
11    /// True for a `oneshot = true` daemon. Present so a consumer can tell a
12    /// task from a service rather than inferring it from `completed`, which a
13    /// task only reaches once it has run.
14    pub oneshot: bool,
15    pub disabled: bool,
16    pub available: bool,
17    /// Deprecated alias of `url`, kept so existing consumers keep working.
18    pub proxy_url: Option<String>,
19    /// The daemon's proxy URL.
20    pub url: Option<String>,
21    /// How the proxy handles TLS for this daemon: `terminate` or `passthrough`.
22    /// Omitted when the daemon is not routed through the proxy.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub proxy_tls: Option<String>,
25    pub error: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub active_port: Option<u16>,
28    pub port: Vec<u16>,
29}
30
31#[derive(Serialize)]
32pub struct JsonStatusEntry {
33    pub id: String,
34    pub namespace: String,
35    pub name: String,
36    pub pid: Option<u32>,
37    pub status: String,
38    /// True for a `oneshot = true` daemon. See `JsonListEntry::oneshot`.
39    pub oneshot: bool,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub active_port: Option<u16>,
42    pub port: Vec<u16>,
43    /// Deprecated alias of `url`, kept so existing consumers keep working.
44    pub proxy_url: Option<String>,
45    /// The daemon's proxy URL.
46    pub url: Option<String>,
47    /// How the proxy handles TLS for this daemon: `terminate` or `passthrough`.
48    /// Omitted when the daemon is not routed through the proxy.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub proxy_tls: Option<String>,
51}
52
53#[derive(Serialize)]
54pub struct JsonLogEntry {
55    /// SQLite row id, used by the webui for backward pagination (scroll-up
56    /// history loading). Skipped in serialization when 0 to avoid changing
57    /// the CLI --json output shape for non-web callers.
58    #[serde(skip_serializing_if = "is_zero_id")]
59    pub id: i64,
60    pub timestamp: String,
61    pub daemon_id: String,
62    pub message: String,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub level: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub msg: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub logger: Option<String>,
69    /// Parsed structured fields as a JSON object, or null if the line was
70    /// not structured (plain text).
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub fields: Option<serde_json::Value>,
73}
74
75fn is_zero_id(id: &i64) -> bool {
76    *id == 0
77}
78
79impl From<LogEntry> for JsonLogEntry {
80    fn from(e: LogEntry) -> Self {
81        let fields = e
82            .fields_json
83            .as_deref()
84            .and_then(|s| serde_json::from_str(s).ok());
85        JsonLogEntry {
86            id: e.id,
87            timestamp: e.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(),
88            daemon_id: e.daemon_id,
89            message: console::strip_ansi_codes(&e.message).to_string(),
90            level: e.level,
91            msg: e.msg,
92            logger: e.logger,
93            fields,
94        }
95    }
96}
97
98#[derive(Serialize)]
99pub struct JsonDaemonConfigEntry {
100    pub id: String,
101    pub run: String,
102}
103
104#[derive(Serialize)]
105pub struct JsonProxyStatus {
106    pub enabled: bool,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub scheme: Option<String>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub tld: Option<String>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub port: Option<u16>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub lan: Option<JsonLanInfo>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub tls_cert: Option<String>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub trusted: Option<bool>,
119    pub slugs: Vec<JsonSlugEntry>,
120    /// Automatic hostnames, grouped by project and worktree.
121    #[serde(default, skip_serializing_if = "Vec::is_empty")]
122    pub projects: Vec<JsonProxyProject>,
123    /// Labels claimed by more than one project, worktree or daemon. Nothing is
124    /// routed under them until the clash is resolved.
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub conflicts: Vec<String>,
127}
128
129#[derive(Serialize)]
130pub struct JsonProxyProject {
131    pub project: String,
132    pub dir: String,
133    /// URL of the project page.
134    pub url: String,
135    pub daemons: Vec<JsonProxyHost>,
136    #[serde(skip_serializing_if = "Vec::is_empty")]
137    pub worktrees: Vec<JsonProxyWorktree>,
138}
139
140#[derive(Serialize)]
141pub struct JsonProxyWorktree {
142    pub worktree: String,
143    pub dir: String,
144    /// URL of the stack page.
145    pub url: String,
146    pub daemons: Vec<JsonProxyHost>,
147}
148
149#[derive(Serialize)]
150pub struct JsonProxyHost {
151    pub daemon: String,
152    /// Hostname without the TLD.
153    pub host: String,
154    pub url: String,
155    pub status: String,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub port: Option<u16>,
158}
159
160#[derive(Serialize)]
161pub struct JsonLanInfo {
162    pub enabled: bool,
163    pub ip: String,
164}
165
166#[derive(Serialize)]
167pub struct JsonSlugEntry {
168    pub slug: String,
169    /// Absent when the slug is not routable (see `status`).
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub url: Option<String>,
172    pub dir: String,
173    pub daemon: String,
174    pub status: String,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub port: Option<u16>,
177}
178
179#[derive(Serialize)]
180pub struct JsonSupervisorStatus {
181    pub status: &'static str,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub web_ui: Option<String>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub error: Option<String>,
186}
187
188#[derive(Serialize)]
189pub struct JsonSettingEntry {
190    pub key: String,
191    pub value: String,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub default: Option<String>,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub r#type: Option<&'static str>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub env_var: Option<&'static str>,
198    /// Where the winning value came from, named so a script can act on it:
199    /// `PITCHFORK_LOG`, `/path/to/pitchfork.toml#general.log_level`, `the
200    /// default`. Absent only for a setting nothing set and that has no
201    /// default.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub origin: Option<String>,
204}
205
206pub fn print_json<T: Serialize>(value: &T) -> crate::Result<()> {
207    let json = serde_json::to_string_pretty(value)
208        .map_err(|e| miette::miette!("failed to serialize JSON: {e}"))?;
209    println!("{json}");
210    Ok(())
211}