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