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