1use anyhow::Result;
20use tracing::debug;
21
22use crate::jobstore::resolve_root;
23use crate::run::resolve_effective_cwd;
24use crate::schema::{JobSummary, ListData, Response};
25
26#[derive(Debug)]
28pub struct ListOpts<'a> {
29 pub root: Option<&'a str>,
30 pub limit: u64,
32 pub state: Option<&'a str>,
34 pub cwd: Option<&'a str>,
37 pub all: bool,
40}
41
42pub fn execute(opts: ListOpts) -> Result<()> {
44 let root = resolve_root(opts.root);
45 let root_str = root.display().to_string();
46
47 let cwd_filter: Option<String> = if opts.all {
50 None
52 } else if let Some(cwd_arg) = opts.cwd {
53 Some(resolve_effective_cwd(Some(cwd_arg)))
55 } else {
56 Some(resolve_effective_cwd(None))
58 };
59
60 debug!(
61 cwd_filter = ?cwd_filter,
62 all = opts.all,
63 "list: cwd filter determined"
64 );
65
66 if !root.exists() {
68 debug!(root = %root_str, "root does not exist; returning empty list");
69 let response = Response::new(
70 "list",
71 ListData {
72 root: root_str,
73 jobs: vec![],
74 truncated: false,
75 skipped: 0,
76 },
77 );
78 response.print();
79 return Ok(());
80 }
81
82 let read_dir = std::fs::read_dir(&root)
84 .map_err(|e| anyhow::anyhow!("failed to read root directory {}: {}", root_str, e))?;
85
86 let mut jobs: Vec<JobSummary> = Vec::new();
87 let mut skipped: u64 = 0;
88
89 for entry in read_dir {
90 let entry = match entry {
91 Ok(e) => e,
92 Err(e) => {
93 debug!(error = %e, "failed to read directory entry; skipping");
94 skipped += 1;
95 continue;
96 }
97 };
98
99 let path = entry.path();
100 if !path.is_dir() {
101 continue;
103 }
104
105 let meta_path = path.join("meta.json");
107 let meta_bytes = match std::fs::read(&meta_path) {
108 Ok(b) => b,
109 Err(_) => {
110 debug!(path = %path.display(), "meta.json missing or unreadable; skipping");
111 skipped += 1;
112 continue;
113 }
114 };
115 let meta: crate::schema::JobMeta = match serde_json::from_slice(&meta_bytes) {
116 Ok(m) => m,
117 Err(e) => {
118 debug!(path = %path.display(), error = %e, "meta.json parse error; skipping");
119 skipped += 1;
120 continue;
121 }
122 };
123
124 if let Some(ref filter_cwd) = cwd_filter {
126 match meta.cwd.as_deref() {
127 Some(job_cwd) if job_cwd == filter_cwd => {
128 }
130 _ => {
131 debug!(
133 path = %path.display(),
134 job_cwd = ?meta.cwd,
135 filter_cwd = %filter_cwd,
136 "list: skipping job (cwd mismatch)"
137 );
138 continue;
139 }
140 }
141 }
142
143 let state_opt: Option<crate::schema::JobState> = {
145 let state_path = path.join("state.json");
146 match std::fs::read(&state_path) {
147 Ok(b) => serde_json::from_slice(&b).ok(),
148 Err(_) => None,
149 }
150 };
151
152 let (state_str, exit_code, finished_at, updated_at) = if let Some(ref s) = state_opt {
153 (
154 s.status().as_str().to_string(),
155 s.exit_code(),
156 s.finished_at.clone(),
157 Some(s.updated_at.clone()),
158 )
159 } else {
160 ("unknown".to_string(), None, None, None)
161 };
162
163 jobs.push(JobSummary {
164 job_id: meta.job.id.clone(),
165 state: state_str,
166 exit_code,
167 started_at: meta.created_at.clone(),
168 finished_at,
169 updated_at,
170 });
171 }
172
173 if let Some(filter_state) = opts.state {
175 jobs.retain(|j| j.state == filter_state);
176 }
177
178 jobs.sort_by(|a, b| {
180 b.started_at
181 .cmp(&a.started_at)
182 .then_with(|| b.job_id.cmp(&a.job_id))
183 });
184
185 let truncated = opts.limit > 0 && jobs.len() as u64 > opts.limit;
187 if truncated {
188 jobs.truncate(opts.limit as usize);
189 }
190
191 debug!(
192 root = %root_str,
193 count = jobs.len(),
194 skipped,
195 truncated,
196 "list complete"
197 );
198
199 let response = Response::new(
200 "list",
201 ListData {
202 root: root_str,
203 jobs,
204 truncated,
205 skipped,
206 },
207 );
208 response.print();
209 Ok(())
210}