1use crate::convert;
4use crate::lint::{binary_available, harper_binary_name, harper_core_available};
5use serde::Serialize;
6
7#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
9pub struct ToolStatus {
10 pub name: String,
11 pub present: bool,
12 pub detail: String,
13}
14
15impl ToolStatus {
16 pub fn line(&self) -> String {
17 let mark = if self.present { "ok" } else { "MISSING" };
18 format!("{}: {} — {}", self.name, mark, self.detail)
19 }
20}
21
22pub fn collect_statuses() -> Vec<ToolStatus> {
24 let mut out = Vec::new();
25
26 let pandoc = convert::tool_available();
27 out.push(ToolStatus {
28 name: "pandoc".into(),
29 present: pandoc,
30 detail: if pandoc {
31 convert::pandoc_version_line().unwrap_or_else(|| "present on PATH".into())
32 } else {
33 "not installed or not on PATH (required for `canonic convert`)".into()
34 },
35 });
36
37 let vale = binary_available("vale");
38 out.push(ToolStatus {
39 name: "vale".into(),
40 present: vale,
41 detail: if vale {
42 "present on PATH (used by `canonic lint --engine vale|all`)".into()
43 } else {
44 "not installed or not on PATH (optional; style lint skipped when missing)".into()
45 },
46 });
47
48 match harper_binary_name() {
49 Some(bin) => out.push(ToolStatus {
50 name: "harper-cli".into(),
51 present: true,
52 detail: format!("{bin} present on PATH (optional CLI fallback)"),
53 }),
54 None => out.push(ToolStatus {
55 name: "harper-cli".into(),
56 present: false,
57 detail: "no harper-cli/harper/harperls on PATH (optional; in-process harper-core still used)"
58 .into(),
59 }),
60 }
61
62 let hc = harper_core_available();
63 out.push(ToolStatus {
64 name: "harper-core".into(),
65 present: hc,
66 detail: if hc {
67 format!(
68 "in-process grammar engine linked (v{})",
69 harper_core::core_version()
70 )
71 } else {
72 "in-process harper-core not linked in this build".into()
73 },
74 });
75
76 out.push(jira_env_status());
78
79 out
80}
81
82fn jira_env_status() -> ToolStatus {
84 if std::env::var_os("JIRA_BASE_URL").is_none() {
85 return ToolStatus {
86 name: "jira".into(),
87 present: false,
88 detail: "JIRA_BASE_URL unset (optional; set for jira-probe / import-jira / jira-comment)"
89 .into(),
90 };
91 }
92 match crate::jira_import::JiraConfig::from_env() {
93 Ok(cfg) => match crate::jira_import::probe_jira(&cfg) {
94 Ok(probe) => {
95 let fmt = match probe.comment_format {
96 crate::jira_import::CommentBodyFormat::Adf => "ADF/v3",
97 crate::jira_import::CommentBodyFormat::Wiki => "wiki/v2",
98 crate::jira_import::CommentBodyFormat::Auto => "auto",
99 };
100 ToolStatus {
101 name: "jira".into(),
102 present: true,
103 detail: format!(
104 "ok free platform REST — {} ({fmt}; no Marketplace apps)",
105 probe.display_name
106 ),
107 }
108 }
109 Err(e) => ToolStatus {
110 name: "jira".into(),
111 present: false,
112 detail: format!("JIRA_BASE_URL set but probe failed: {e:#}"),
113 },
114 },
115 Err(e) => ToolStatus {
116 name: "jira".into(),
117 present: false,
118 detail: format!("Jira env incomplete: {e:#}"),
119 },
120 }
121}
122
123pub fn format_doctor(statuses: &[ToolStatus]) -> String {
125 let mut s = String::from("canonic doctor\n");
126 for st in statuses {
127 s.push_str(&st.line());
128 s.push('\n');
129 }
130 s
131}
132
133pub fn critical_missing(statuses: &[ToolStatus]) -> Vec<&ToolStatus> {
135 statuses
136 .iter()
137 .filter(|s| s.name == "pandoc" && !s.present)
138 .collect()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn doctor_names_pandoc_vale_and_harper() {
147 let prev = std::env::var_os("JIRA_BASE_URL");
149 std::env::remove_var("JIRA_BASE_URL");
150 let statuses = collect_statuses();
151 if let Some(v) = prev {
152 std::env::set_var("JIRA_BASE_URL", v);
153 }
154 let names: Vec<_> = statuses.iter().map(|s| s.name.as_str()).collect();
155 assert!(names.contains(&"pandoc"), "{names:?}");
156 assert!(names.contains(&"vale"), "{names:?}");
157 assert!(names.contains(&"jira"), "{names:?}");
158 assert!(
159 names.iter().any(|n| n.contains("harper")),
160 "expected harper status, got {names:?}"
161 );
162 let text = format_doctor(&statuses);
163 assert!(text.contains("pandoc:"));
164 assert!(text.contains("vale:"));
165 assert!(text.contains("harper"));
166 assert!(
168 text.contains("ok") || text.contains("MISSING"),
169 "expected explicit status markers: {text}"
170 );
171 }
172
173 #[test]
174 fn format_doctor_marks_missing_tools() {
175 let statuses = vec![
176 ToolStatus {
177 name: "pandoc".into(),
178 present: false,
179 detail: "not on PATH".into(),
180 },
181 ToolStatus {
182 name: "vale".into(),
183 present: true,
184 detail: "present".into(),
185 },
186 ];
187 let text = format_doctor(&statuses);
188 assert!(text.contains("pandoc: MISSING"));
189 assert!(text.contains("vale: ok"));
190 assert_eq!(critical_missing(&statuses).len(), 1);
191 }
192}