1use std::io::Write;
7
8use serde::Serialize;
9use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
10
11use crate::diagnostic::Diagnostic;
12use crate::sync::SyncReport;
13use crate::sync::apply::{ActionOutcome, ActionTaken};
14
15pub fn use_color() -> bool {
19 std::env::var_os("NO_COLOR").is_none()
20}
21
22fn color_choice() -> ColorChoice {
23 if use_color() {
24 ColorChoice::Auto
25 } else {
26 ColorChoice::Never
27 }
28}
29
30#[derive(Debug, Serialize)]
32pub struct ListEntry {
33 pub source: String,
34 pub item: String,
35 pub kind: String,
36 pub version: String,
37 pub status: String,
38}
39
40#[derive(Debug, Serialize)]
42pub struct CatalogEntry {
43 pub name: String,
44 pub description: String,
45 pub kind: String,
46}
47
48pub fn print_catalog(agents: &[CatalogEntry], skills: &[CatalogEntry], kind_filter: Option<&str>) {
50 let show_agents =
51 kind_filter.is_none() || kind_filter == Some("agents") || kind_filter == Some("agent");
52 let show_skills =
53 kind_filter.is_none() || kind_filter == Some("skills") || kind_filter == Some("skill");
54
55 if show_agents && !agents.is_empty() {
56 println!("AGENTS");
57 for entry in agents {
58 if entry.description.is_empty() {
59 println!("- {}", entry.name);
60 } else {
61 println!("- {}: {}", entry.name, entry.description);
62 }
63 }
64 }
65
66 if show_agents && !agents.is_empty() && show_skills && !skills.is_empty() {
67 println!();
68 }
69
70 if show_skills && !skills.is_empty() {
71 println!("SKILLS");
72 for entry in skills {
73 if entry.description.is_empty() {
74 println!("- {}", entry.name);
75 } else {
76 println!("- {}: {}", entry.name, entry.description);
77 }
78 }
79 }
80
81 if (show_agents && agents.is_empty() && show_skills && skills.is_empty())
82 || (show_agents && !show_skills && agents.is_empty())
83 || (show_skills && !show_agents && skills.is_empty())
84 {
85 println!(" no managed items");
86 }
87}
88
89pub fn print_sync_report(report: &SyncReport, json: bool, no_upgrade_hint: bool) {
91 if json {
92 print_sync_report_json(report);
93 } else {
94 print_sync_report_human(report, no_upgrade_hint);
95 }
96}
97
98fn is_dry_run(report: &SyncReport) -> bool {
101 report.dry_run
102}
103
104fn print_sync_report_json(report: &SyncReport) {
105 println!("{}", sync_report_json(report));
106}
107
108pub fn sync_report_json(report: &SyncReport) -> serde_json::Value {
109 #[derive(Serialize)]
110 struct JsonTargetOutcome {
111 name: String,
112 synced: usize,
113 removed: usize,
114 errors: Vec<String>,
115 }
116
117 #[derive(Serialize)]
118 struct JsonReport {
119 ok: bool,
120 dry_run: bool,
121 installed: usize,
122 updated: usize,
123 removed: usize,
124 conflicts: usize,
125 kept: usize,
126 skipped: usize,
127 upgrades_available: usize,
128 targets: Vec<JsonTargetOutcome>,
129 diagnostics: Vec<Diagnostic>,
130 }
131
132 let mut installed = 0;
133 let mut updated = 0;
134 let mut removed = 0;
135 let mut conflicts = 0;
136 let mut kept = 0;
137 let mut skipped = 0;
138
139 for outcome in &report.applied.outcomes {
140 match outcome.action {
141 ActionTaken::Installed => installed += 1,
142 ActionTaken::Updated => updated += 1,
143 ActionTaken::Merged => updated += 1,
144 ActionTaken::Conflicted => conflicts += 1,
145 ActionTaken::Removed => removed += 1,
146 ActionTaken::Kept => kept += 1,
147 ActionTaken::Skipped => skipped += 1,
148 }
149 }
150
151 for outcome in &report.pruned {
152 if matches!(outcome.action, ActionTaken::Removed) {
153 removed += 1;
154 }
155 }
156
157 let targets = report
158 .target_outcomes
159 .iter()
160 .map(|outcome| JsonTargetOutcome {
161 name: outcome.target.clone(),
162 synced: outcome.items_synced,
163 removed: outcome.items_removed,
164 errors: outcome.errors.clone(),
165 })
166 .collect();
167
168 serde_json::to_value(JsonReport {
169 ok: conflicts == 0,
170 dry_run: report.dry_run,
171 installed,
172 updated,
173 removed,
174 conflicts,
175 kept,
176 skipped,
177 upgrades_available: report.upgrades_available,
178 targets,
179 diagnostics: report.diagnostics.clone(),
180 })
181 .unwrap_or_else(|_| serde_json::json!({}))
182}
183
184fn print_sync_report_human(report: &SyncReport, no_upgrade_hint: bool) {
185 let mut stdout = StandardStream::stdout(color_choice());
186
187 let mut installed = 0usize;
188 let mut updated = 0usize;
189 let mut removed = 0usize;
190 let mut conflicts = 0usize;
191 let mut kept = 0usize;
192
193 for outcome in &report.applied.outcomes {
195 match outcome.action {
196 ActionTaken::Installed => {
197 installed += 1;
198 print_action_line(&mut stdout, "+", Color::Green, outcome);
199 }
200 ActionTaken::Updated | ActionTaken::Merged => {
201 updated += 1;
202 print_action_line(&mut stdout, "~", Color::Yellow, outcome);
203 }
204 ActionTaken::Conflicted => {
205 conflicts += 1;
206 print_action_line(&mut stdout, "!", Color::Red, outcome);
207 }
208 ActionTaken::Removed => {
209 removed += 1;
210 print_action_line(&mut stdout, "-", Color::Red, outcome);
211 }
212 ActionTaken::Kept => {
213 kept += 1;
214 }
215 ActionTaken::Skipped => {}
216 }
217 }
218
219 for outcome in &report.pruned {
220 if matches!(outcome.action, ActionTaken::Removed) {
221 removed += 1;
222 print_action_line(&mut stdout, "-", Color::Red, outcome);
223 }
224 }
225
226 let _ = writeln!(stdout);
228 let dry = is_dry_run(report);
229 if installed > 0 {
230 if dry {
231 let _ = writeln!(stdout, " would install {installed} new items");
232 } else {
233 let _ = writeln!(stdout, " installed {installed} new items");
234 }
235 }
236 if updated > 0 {
237 if dry {
238 let _ = writeln!(stdout, " would update {updated} items");
239 } else {
240 let _ = writeln!(stdout, " updated {updated} items");
241 }
242 }
243 if removed > 0 {
244 if dry {
245 let _ = writeln!(stdout, " would remove {removed} orphans");
246 } else {
247 let _ = writeln!(stdout, " removed {removed} orphans");
248 }
249 }
250 if kept > 0 {
251 let _ = writeln!(stdout, " kept {kept} locally modified");
252 }
253 if conflicts > 0 {
254 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
255 let _ = writeln!(
256 stdout,
257 " conflicts {conflicts} files (run `mars resolve` after fixing)"
258 );
259 let _ = stdout.reset();
260 }
261
262 if installed == 0 && updated == 0 && removed == 0 && conflicts == 0 && kept == 0 {
263 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
264 let _ = writeln!(stdout, " already up to date");
265 let _ = stdout.reset();
266 }
267
268 let mut stderr = StandardStream::stderr(color_choice());
270 for diag in &report.diagnostics {
271 let color = match diag.level {
272 crate::diagnostic::DiagnosticLevel::Warning => Color::Yellow,
273 crate::diagnostic::DiagnosticLevel::Info => Color::Cyan,
274 };
275 let _ = stderr.set_color(ColorSpec::new().set_fg(Some(color)));
276 let _ = writeln!(stderr, " {diag}");
277 let _ = stderr.reset();
278 }
279
280 if report.upgrades_available > 0 && !report.dry_run && !no_upgrade_hint {
281 let noun = if report.upgrades_available == 1 {
282 "upgrade"
283 } else {
284 "upgrades"
285 };
286 let _ = stderr.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)));
287 let _ = writeln!(
288 stderr,
289 " ℹ {} {noun} available — run `mars upgrade --bump` to update",
290 report.upgrades_available
291 );
292 let _ = stderr.reset();
293 }
294}
295
296fn print_action_line(
297 stdout: &mut StandardStream,
298 prefix: &str,
299 color: Color,
300 outcome: &ActionOutcome,
301) {
302 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(color)));
303 let _ = write!(stdout, " {prefix} ");
304 let _ = stdout.reset();
305 let _ = writeln!(stdout, "{} ({})", outcome.dest_path, outcome.item_id.kind);
306}
307
308pub fn print_list(entries: &[ListEntry], json: bool) {
310 if json {
311 println!("{}", serde_json::to_string(entries).unwrap_or_default());
312 } else {
313 print_list_human(entries);
314 }
315}
316
317fn print_list_human(entries: &[ListEntry]) {
318 if entries.is_empty() {
319 println!(" no managed items");
320 return;
321 }
322
323 let source_w = entries
325 .iter()
326 .map(|e| e.source.len())
327 .max()
328 .unwrap_or(6)
329 .max(6);
330 let item_w = entries
331 .iter()
332 .map(|e| e.item.len())
333 .max()
334 .unwrap_or(4)
335 .max(4);
336 let version_w = entries
337 .iter()
338 .map(|e| e.version.len())
339 .max()
340 .unwrap_or(7)
341 .max(7);
342
343 println!(
345 "{:<source_w$} {:<item_w$} {:<version_w$} STATUS",
346 "SOURCE", "ITEM", "VERSION"
347 );
348
349 let mut stdout = StandardStream::stdout(color_choice());
350 for entry in entries {
351 let _ = write!(
352 stdout,
353 "{:<source_w$} {:<item_w$} {:<version_w$} ",
354 entry.source, entry.item, entry.version
355 );
356 let color = match entry.status.as_str() {
357 "ok" => Color::Green,
358 "modified" => Color::Yellow,
359 "conflicted" => Color::Red,
360 _ => Color::White,
361 };
362 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(color)));
363 let _ = writeln!(stdout, "{}", entry.status);
364 let _ = stdout.reset();
365 }
366}
367
368pub fn print_doctor(errors: &[String], warnings: &[String], json: bool) {
370 if json {
371 #[derive(Serialize)]
372 struct DoctorReport {
373 ok: bool,
374 errors: Vec<String>,
375 warnings: Vec<String>,
376 }
377 let report = DoctorReport {
378 ok: errors.is_empty(),
379 errors: errors.to_vec(),
380 warnings: warnings.to_vec(),
381 };
382 println!("{}", serde_json::to_string(&report).unwrap_or_default());
383 } else {
384 let mut stdout = StandardStream::stdout(color_choice());
385 if errors.is_empty() && warnings.is_empty() {
386 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
387 let _ = writeln!(stdout, " all checks passed");
388 let _ = stdout.reset();
389 } else {
390 for warning in warnings {
391 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)));
392 let _ = write!(stdout, " ⚠ ");
393 let _ = stdout.reset();
394 let _ = writeln!(stdout, "{warning}");
395 }
396
397 for error in errors {
398 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
399 let _ = write!(stdout, " ✗ ");
400 let _ = stdout.reset();
401 let _ = writeln!(stdout, "{error}");
402 }
403 let _ = writeln!(stdout);
404 if !warnings.is_empty() {
405 let _ = writeln!(stdout, " {} warning(s)", warnings.len());
406 }
407 if !errors.is_empty() {
408 let _ = writeln!(stdout, " {} error(s)", errors.len());
409 }
410 }
411 }
412}
413
414pub fn print_json<T: Serialize>(value: &T) {
416 println!("{}", serde_json::to_string(value).unwrap_or_default());
417}
418
419pub fn print_success(msg: &str) {
421 let mut stdout = StandardStream::stdout(color_choice());
422 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)));
423 let _ = write!(stdout, " ✓ ");
424 let _ = stdout.reset();
425 let _ = writeln!(stdout, "{msg}");
426}
427
428pub fn print_warn(msg: &str) {
430 let mut stdout = StandardStream::stdout(color_choice());
431 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)));
432 let _ = write!(stdout, " ⚠ ");
433 let _ = stdout.reset();
434 let _ = writeln!(stdout, "{msg}");
435}
436
437pub fn print_error(msg: &str) {
439 let mut stdout = StandardStream::stdout(color_choice());
440 let _ = stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)));
441 let _ = write!(stdout, " ✗ ");
442 let _ = stdout.reset();
443 let _ = writeln!(stdout, "{msg}");
444}
445
446pub fn print_info(msg: &str) {
448 println!(" {msg}");
449}