93 lines
2.7 KiB
Rust
93 lines
2.7 KiB
Rust
//! projects: which agent works on which project.
|
|
|
|
use super::*;
|
|
use crate::output::{print_json, Table};
|
|
use crate::projects::{self, ProjectSummary};
|
|
|
|
pub fn run(app: &App, name: Option<&str>) -> Result<i32> {
|
|
let summaries = projects::aggregate(app);
|
|
if let Some(n) = name {
|
|
return show_one(app, n, &summaries);
|
|
}
|
|
if app.json() {
|
|
print_json(&summaries);
|
|
return Ok(0);
|
|
}
|
|
let mut table = Table::new(vec!["PROJECT", "AGENTS", "SESSIONS", "DURATION", "LAST"]);
|
|
for s in &summaries {
|
|
table.row(vec![
|
|
short_path(&s.root),
|
|
s.dominant
|
|
.clone()
|
|
.unwrap_or_else(|| "—".to_string()),
|
|
s.sessions.to_string(),
|
|
format_duration(s.total_duration_s),
|
|
s.last_activity
|
|
.as_deref()
|
|
.map(|t| t[..t.len().min(10)].to_string())
|
|
.unwrap_or_else(|| "—".to_string()),
|
|
]);
|
|
}
|
|
print!("{}", table.render());
|
|
if summaries.is_empty() {
|
|
app.log.info("no project activity recorded yet — run or start an agent inside a git repository");
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
fn show_one(app: &App, name: &str, summaries: &[ProjectSummary]) -> Result<i32> {
|
|
let s = summaries
|
|
.iter()
|
|
.find(|s| {
|
|
s.root == name
|
|
|| s.root.ends_with(&format!("/{name}"))
|
|
|| s.root.ends_with(&format!("\\{name}"))
|
|
})
|
|
.ok_or_else(|| anyhow!("unknown project '{name}' — 'am projects' lists them"))?;
|
|
if app.json() {
|
|
print_json(s);
|
|
return Ok(0);
|
|
}
|
|
println!("project {}", s.root);
|
|
println!(" agents: {}", s.agents.join(", "));
|
|
println!(
|
|
" dominant: {}",
|
|
s.dominant.as_deref().unwrap_or("—")
|
|
);
|
|
println!(" sessions: {}", s.sessions);
|
|
println!(" launches: {}", s.launches);
|
|
println!(" duration: {}", format_duration(s.total_duration_s));
|
|
println!(
|
|
" last: {}",
|
|
s.last_activity.as_deref().unwrap_or("—")
|
|
);
|
|
Ok(0)
|
|
}
|
|
|
|
/// Keep the tail of a long path.
|
|
fn short_path(p: &str) -> String {
|
|
let s = p.trim_end_matches(['/', '\\']);
|
|
let name = s.rsplit(['/', '\\']).next().unwrap_or(s);
|
|
let mut out = format!("…/{name}");
|
|
let full = p.chars().count();
|
|
let shown = out.chars().count();
|
|
if shown >= full || full <= 30 {
|
|
return p.to_string();
|
|
}
|
|
out.truncate(30);
|
|
out
|
|
}
|
|
|
|
fn format_duration(secs: u64) -> String {
|
|
if secs == 0 {
|
|
return "—".to_string();
|
|
}
|
|
if secs < 60 {
|
|
format!("{secs}s")
|
|
} else if secs < 3600 {
|
|
format!("{}m{:02}s", secs / 60, secs % 60)
|
|
} else {
|
|
format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60)
|
|
}
|
|
}
|