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