mars_agents/cli/
outdated.rs1use serde::Serialize;
5
6use crate::error::MarsError;
7
8use super::output;
9
10#[derive(Debug, clap::Args)]
12pub struct OutdatedArgs {}
13
14#[derive(Debug, Serialize)]
16struct OutdatedEntry {
17 source: String,
18 locked: String,
19 constraint: String,
20 updateable: String,
21 latest: String,
22}
23
24pub fn run(_args: &OutdatedArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
26 let lock = crate::lock::load(&ctx.managed_root)?;
27 let config = crate::config::load(&ctx.managed_root)?;
28 let cache = crate::source::GlobalCache::new()?;
29
30 let mut entries = Vec::new();
31
32 for (name, source_entry) in &config.sources {
33 let url = match &source_entry.url {
35 Some(u) => u,
36 None => continue, };
38
39 let locked_version = lock
40 .sources
41 .get(name)
42 .and_then(|s| s.version.clone())
43 .unwrap_or_else(|| "-".to_string());
44
45 let constraint = source_entry
46 .version
47 .clone()
48 .unwrap_or_else(|| "latest".to_string());
49
50 let versions = match crate::source::list_versions(url, &cache) {
52 Ok(v) => v,
53 Err(_) => continue,
54 };
55
56 if versions.is_empty() {
57 let current_head = crate::source::git::ls_remote_head(url.as_ref())
59 .map(|sha| if sha.len() >= 12 { sha[..12].to_string() } else { sha })
60 .unwrap_or_else(|_| "-".to_string());
61 let locked_commit = lock
62 .sources
63 .get(name)
64 .and_then(|s| s.commit.as_ref().map(|c| c.to_string()))
65 .unwrap_or_else(|| "-".to_string());
66 let locked_short = if locked_commit.len() >= 12 {
67 locked_commit[..12].to_string()
68 } else {
69 locked_commit
70 };
71 entries.push(OutdatedEntry {
72 source: name.to_string(),
73 locked: locked_short,
74 constraint: "HEAD".to_string(),
75 updateable: current_head.clone(),
76 latest: current_head,
77 });
78 continue;
79 }
80
81 let latest = versions
83 .iter()
84 .max_by(|a, b| a.version.cmp(&b.version))
85 .map(|v| v.tag.clone())
86 .unwrap_or_else(|| "-".to_string());
87
88 let parsed_constraint =
90 crate::resolve::parse_version_constraint(source_entry.version.as_deref());
91 let updateable = match &parsed_constraint {
92 crate::resolve::VersionConstraint::Semver(req) => versions
93 .iter()
94 .filter(|v| req.matches(&v.version))
95 .max_by(|a, b| a.version.cmp(&b.version))
96 .map(|v| v.tag.clone())
97 .unwrap_or_else(|| locked_version.clone()),
98 crate::resolve::VersionConstraint::Latest => latest.clone(),
99 crate::resolve::VersionConstraint::RefPin(_) => locked_version.clone(),
100 };
101
102 entries.push(OutdatedEntry {
103 source: name.to_string(),
104 locked: locked_version,
105 constraint,
106 updateable,
107 latest,
108 });
109 }
110
111 if json {
112 output::print_json(&entries);
113 } else {
114 print_outdated_table(&entries);
115 }
116
117 Ok(0)
118}
119
120fn print_outdated_table(entries: &[OutdatedEntry]) {
121 if entries.is_empty() {
122 output::print_info("no git sources to check");
123 return;
124 }
125
126 let name_w = entries
127 .iter()
128 .map(|e| e.source.len())
129 .max()
130 .unwrap_or(6)
131 .max(6);
132 let locked_w = entries
133 .iter()
134 .map(|e| e.locked.len())
135 .max()
136 .unwrap_or(6)
137 .max(6);
138 let constraint_w = entries
139 .iter()
140 .map(|e| e.constraint.len())
141 .max()
142 .unwrap_or(10)
143 .max(10);
144 let update_w = entries
145 .iter()
146 .map(|e| e.updateable.len())
147 .max()
148 .unwrap_or(10)
149 .max(10);
150
151 println!(
152 "{:<name_w$} {:<locked_w$} {:<constraint_w$} {:<update_w$} LATEST",
153 "SOURCE", "LOCKED", "CONSTRAINT", "UPDATEABLE"
154 );
155
156 for entry in entries {
157 println!(
158 "{:<name_w$} {:<locked_w$} {:<constraint_w$} {:<update_w$} {}",
159 entry.source, entry.locked, entry.constraint, entry.updateable, entry.latest
160 );
161 }
162}