use anyhow::Result;
use tracing::debug;
use crate::jobstore::resolve_root;
use crate::jobstore::short_job_id;
use crate::run::resolve_effective_cwd;
use crate::schema::{JobSummary, ListData, Response};
use crate::tag::{matches_all_patterns, validate_filter_pattern};
#[derive(Debug)]
pub struct ListOpts<'a> {
pub root: Option<&'a str>,
pub limit: u64,
pub state: Option<&'a str>,
pub cwd: Option<&'a str>,
pub all: bool,
pub tags: Vec<String>,
}
pub fn execute(opts: ListOpts) -> Result<()> {
let root = resolve_root(opts.root);
let root_str = root.display().to_string();
for pattern in &opts.tags {
validate_filter_pattern(pattern).map_err(anyhow::Error::from)?;
}
let cwd_filter: Option<String> = if opts.all {
None
} else if let Some(cwd_arg) = opts.cwd {
Some(resolve_effective_cwd(Some(cwd_arg)))
} else {
Some(resolve_effective_cwd(None))
};
debug!(
cwd_filter = ?cwd_filter,
all = opts.all,
"list: cwd filter determined"
);
if !root.exists() {
debug!(root = %root_str, "root does not exist; returning empty list");
let response = Response::new(
"list",
ListData {
root: root_str,
jobs: vec![],
truncated: false,
skipped: 0,
},
);
response.print();
return Ok(());
}
let read_dir = std::fs::read_dir(&root)
.map_err(|e| anyhow::anyhow!("failed to read root directory {}: {}", root_str, e))?;
let mut jobs: Vec<JobSummary> = Vec::new();
let mut skipped: u64 = 0;
for entry in read_dir {
let entry = match entry {
Ok(e) => e,
Err(e) => {
debug!(error = %e, "failed to read directory entry; skipping");
skipped += 1;
continue;
}
};
let path = entry.path();
if !path.is_dir() {
continue;
}
let meta_path = path.join("meta.json");
let meta_bytes = match std::fs::read(&meta_path) {
Ok(b) => b,
Err(_) => {
debug!(path = %path.display(), "meta.json missing or unreadable; skipping");
skipped += 1;
continue;
}
};
let meta: crate::schema::JobMeta = match serde_json::from_slice(&meta_bytes) {
Ok(m) => m,
Err(e) => {
debug!(path = %path.display(), error = %e, "meta.json parse error; skipping");
skipped += 1;
continue;
}
};
if let Some(ref filter_cwd) = cwd_filter {
match meta.cwd.as_deref() {
Some(job_cwd) if job_cwd == filter_cwd => {
}
_ => {
debug!(
path = %path.display(),
job_cwd = ?meta.cwd,
filter_cwd = %filter_cwd,
"list: skipping job (cwd mismatch)"
);
continue;
}
}
}
if !opts.tags.is_empty() && !matches_all_patterns(&meta.tags, &opts.tags) {
debug!(
path = %path.display(),
job_tags = ?meta.tags,
patterns = ?opts.tags,
"list: skipping job (tag mismatch)"
);
continue;
}
let state_opt: Option<crate::schema::JobState> = {
let state_path = path.join("state.json");
match std::fs::read(&state_path) {
Ok(b) => serde_json::from_slice(&b).ok(),
Err(_) => None,
}
};
let (state_str, exit_code, finished_at, updated_at) = if let Some(ref s) = state_opt {
(
s.status().as_str().to_string(),
s.exit_code(),
s.finished_at.clone(),
Some(s.updated_at.clone()),
)
} else {
("unknown".to_string(), None, None, None)
};
let job_started_at = state_opt
.as_ref()
.and_then(|s| s.started_at().map(|t| t.to_string()));
jobs.push(JobSummary {
job_id: meta.job.id.clone(),
short_job_id: short_job_id(&meta.job.id),
state: state_str,
exit_code,
created_at: meta.created_at.clone(),
started_at: job_started_at,
finished_at,
updated_at,
tags: meta.tags.clone(),
});
}
if let Some(filter_state) = opts.state {
jobs.retain(|j| j.state == filter_state);
}
jobs.sort_by(|a, b| {
b.started_at
.cmp(&a.started_at)
.then_with(|| b.job_id.cmp(&a.job_id))
});
let truncated = opts.limit > 0 && jobs.len() as u64 > opts.limit;
if truncated {
jobs.truncate(opts.limit as usize);
}
debug!(
root = %root_str,
count = jobs.len(),
skipped,
truncated,
"list complete"
);
let response = Response::new(
"list",
ListData {
root: root_str,
jobs,
truncated,
skipped,
},
);
response.print();
Ok(())
}