Skip to content

Commit 3e1b7fa

Browse files
Kobzolclaude
andcommitted
Add 'hq job workdir' command for displaying job working directories
Implements the job workdir command requested in issue #644. The command displays working directories for selected jobs, supporting all job selector formats (ID, range, "last", "all"). Features: - CLI output with job headers and working directory listings - JSON output with structured job_id -> workdirs mapping - Quiet output mode support - Integrates with existing job selection and filtering - Shows both submission directories and resolved task working directories - Handles multiple jobs and deduplicates working directories Usage: - hq job workdir <selector> - hq job workdir 1 - hq job workdir 1-5 - hq job workdir last - hq job workdir all 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7503ba3 commit 3e1b7fa

File tree

10 files changed

+765
-5
lines changed

10 files changed

+765
-5
lines changed

crates/hyperqueue/src/bin/hq.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use hyperqueue::client::commands::data::command_task_data;
88
use hyperqueue::client::commands::doc::command_doc;
99
use hyperqueue::client::commands::job::{
1010
JobCancelOpts, JobCatOpts, JobCloseOpts, JobForgetOpts, JobInfoOpts, JobListOpts,
11-
JobTaskIdsOpts, cancel_job, close_job, forget_job, output_job_cat, output_job_detail,
12-
output_job_list, output_job_summary,
11+
JobTaskIdsOpts, JobWorkdirOpts, cancel_job, close_job, forget_job, output_job_cat, output_job_detail,
12+
output_job_list, output_job_summary, output_job_workdir,
1313
};
1414
use hyperqueue::client::commands::journal::command_journal;
1515
use hyperqueue::client::commands::outputlog::command_reader;
@@ -31,8 +31,8 @@ use hyperqueue::client::output::outputs::{Output, Outputs};
3131
use hyperqueue::client::output::quiet::Quiet;
3232
use hyperqueue::client::status::Status;
3333
use hyperqueue::client::task::{
34-
TaskCommand, TaskExplainOpts, TaskInfoOpts, TaskListOpts, output_job_task_explain,
35-
output_job_task_ids, output_job_task_info, output_job_task_list,
34+
TaskCommand, TaskExplainOpts, TaskInfoOpts, TaskListOpts, TaskWorkdirOpts, output_job_task_explain,
35+
output_job_task_ids, output_job_task_info, output_job_task_list, output_job_task_workdir,
3636
};
3737
use hyperqueue::common::cli::{
3838
ColorPolicy, CommonOpts, DeploySshOpts, GenerateCompletionOpts, HwDetectOpts, JobCommand,
@@ -140,6 +140,11 @@ async fn command_job_close(gsettings: &GlobalSettings, opts: JobCloseOpts) -> an
140140
close_job(gsettings, &mut connection, opts.selector).await
141141
}
142142

143+
async fn command_job_workdir(gsettings: &GlobalSettings, opts: JobWorkdirOpts) -> anyhow::Result<()> {
144+
let mut connection = get_client_session(gsettings.server_directory()).await?;
145+
output_job_workdir(gsettings, &mut connection, opts.selector).await
146+
}
147+
143148
async fn command_job_delete(gsettings: &GlobalSettings, opts: JobForgetOpts) -> anyhow::Result<()> {
144149
let mut connection = get_client_session(gsettings.server_directory()).await?;
145150
forget_job(gsettings, &mut connection, opts).await
@@ -212,6 +217,14 @@ async fn command_task_explain(
212217
output_job_task_explain(gsettings, &mut session, opts).await
213218
}
214219

220+
async fn command_task_workdir(
221+
gsettings: &GlobalSettings,
222+
opts: TaskWorkdirOpts,
223+
) -> anyhow::Result<()> {
224+
let mut session = get_client_session(gsettings.server_directory()).await?;
225+
output_job_task_workdir(gsettings, &mut session, opts).await
226+
}
227+
215228
async fn command_worker_start(
216229
gsettings: &GlobalSettings,
217230
opts: WorkerStartOpts,
@@ -496,6 +509,7 @@ async fn main() -> hyperqueue::Result<()> {
496509
JobCommand::TaskIds(opts) => command_job_task_ids(&gsettings, opts).await,
497510
JobCommand::Open(opts) => command_job_open(&gsettings, opts).await,
498511
JobCommand::Close(opts) => command_job_close(&gsettings, opts).await,
512+
JobCommand::Workdir(opts) => command_job_workdir(&gsettings, opts).await,
499513
},
500514
SubCommand::Submit(opts) => {
501515
command_job_submit(&gsettings, OptsWithMatches::new(opts, matches)).await
@@ -504,6 +518,7 @@ async fn main() -> hyperqueue::Result<()> {
504518
TaskCommand::List(opts) => command_task_list(&gsettings, opts).await,
505519
TaskCommand::Info(opts) => command_task_info(&gsettings, opts).await,
506520
TaskCommand::Explain(opts) => command_task_explain(&gsettings, opts).await,
521+
TaskCommand::Workdir(opts) => command_task_workdir(&gsettings, opts).await,
507522
},
508523
SubCommand::Data(opts) => command_task_data(&gsettings, opts).await,
509524
#[cfg(feature = "dashboard")]

crates/hyperqueue/src/client/commands/job.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ pub struct JobCatOpts {
113113
pub stream: OutputStream,
114114
}
115115

116+
#[derive(Parser)]
117+
pub struct JobWorkdirOpts {
118+
/// Single ID, ID range or `last` to display the most recently submitted job
119+
#[arg(value_parser = parse_last_all_range)]
120+
pub selector: IdSelector,
121+
}
122+
116123
pub async fn output_job_list(
117124
gsettings: &GlobalSettings,
118125
session: &mut ClientSession,
@@ -340,3 +347,37 @@ pub async fn forget_job(
340347

341348
Ok(())
342349
}
350+
351+
pub async fn output_job_workdir(
352+
gsettings: &GlobalSettings,
353+
session: &mut ClientSession,
354+
selector: IdSelector,
355+
) -> anyhow::Result<()> {
356+
let message = FromClientMessage::JobDetail(JobDetailRequest {
357+
job_id_selector: selector,
358+
task_selector: Some(TaskSelector {
359+
id_selector: TaskIdSelector::All,
360+
status_selector: TaskStatusSelector::All,
361+
}),
362+
});
363+
let response =
364+
rpc_call!(session.connection(), message, ToClientMessage::JobDetailResponse(r) => r)
365+
.await?;
366+
367+
let jobs: Vec<JobDetail> = response
368+
.details
369+
.into_iter()
370+
.filter_map(|(id, job)| match job {
371+
Some(job) => Some(job),
372+
None => {
373+
log::error!("Job {id} not found");
374+
None
375+
}
376+
})
377+
.collect();
378+
379+
gsettings
380+
.printer()
381+
.print_job_workdir(jobs, &response.server_uid);
382+
Ok(())
383+
}

crates/hyperqueue/src/client/output/cli.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ impl Output for CliOutput {
472472
}
473473
}
474474

475+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str) {
476+
for (job_id, job) in jobs {
477+
let task_paths = resolve_task_paths(&job, server_uid);
478+
479+
println!("Job {}:", job_id);
480+
for (task_id, resolved_paths) in task_paths.iter() {
481+
if let Some(paths) = resolved_paths {
482+
println!(" Task {}: {}", task_id.as_num(), paths.cwd.display());
483+
}
484+
}
485+
}
486+
}
487+
475488
fn print_job_list(&self, jobs: Vec<JobInfo>, total_jobs: usize) {
476489
let job_count = jobs.len();
477490
let mut has_opened = false;
@@ -632,6 +645,33 @@ impl Output for CliOutput {
632645
}
633646
}
634647

648+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str) {
649+
for job in jobs {
650+
let task_paths = resolve_task_paths(&job, server_uid);
651+
652+
// Collect unique working directories
653+
let mut workdirs: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
654+
655+
// Add submission directory(s)
656+
for submit_desc in &job.submit_descs {
657+
workdirs.insert(submit_desc.description().submit_dir.to_string_lossy().to_string());
658+
}
659+
660+
// Add task working directories
661+
for (_, resolved_paths) in task_paths.iter() {
662+
if let Some(paths) = resolved_paths {
663+
workdirs.insert(paths.cwd.to_string_lossy().to_string());
664+
}
665+
}
666+
667+
// Print job header and working directories
668+
println!("Job {}:", job.info.id);
669+
for workdir in workdirs {
670+
println!(" {}", workdir);
671+
}
672+
}
673+
}
674+
635675
fn print_job_wait(
636676
&self,
637677
duration: Duration,

crates/hyperqueue/src/client/output/json.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,36 @@ impl Output for JsonOutput {
154154
self.print(Value::Array(job_details));
155155
}
156156

157+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str) {
158+
let job_workdirs: Vec<_> = jobs
159+
.into_iter()
160+
.map(|job| {
161+
let task_paths = resolve_task_paths(&job, server_uid);
162+
163+
// Collect unique working directories
164+
let mut workdirs: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
165+
166+
// Add submission directory(s)
167+
for submit_desc in &job.submit_descs {
168+
workdirs.insert(submit_desc.description().submit_dir.to_string_lossy().to_string());
169+
}
170+
171+
// Add task working directories
172+
for (_, resolved_paths) in task_paths.iter() {
173+
if let Some(paths) = resolved_paths {
174+
workdirs.insert(paths.cwd.to_string_lossy().to_string());
175+
}
176+
}
177+
178+
json!({
179+
"job_id": job.info.id,
180+
"workdirs": workdirs.into_iter().collect::<Vec<_>>()
181+
})
182+
})
183+
.collect();
184+
self.print(Value::Array(job_workdirs));
185+
}
186+
157187
fn print_job_wait(
158188
&self,
159189
duration: Duration,
@@ -221,6 +251,29 @@ impl Output for JsonOutput {
221251
self.print(json!(map));
222252
}
223253

254+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str) {
255+
let task_workdirs: Vec<_> = jobs
256+
.into_iter()
257+
.map(|(job_id, job)| {
258+
let task_paths = resolve_task_paths(&job, server_uid);
259+
let tasks: HashMap<u32, String> = task_paths
260+
.iter()
261+
.filter_map(|(task_id, resolved_paths)| {
262+
resolved_paths.as_ref().map(|paths| {
263+
(task_id.as_num(), paths.cwd.to_string_lossy().to_string())
264+
})
265+
})
266+
.collect();
267+
268+
json!({
269+
"job_id": job_id,
270+
"tasks": tasks
271+
})
272+
})
273+
.collect();
274+
self.print(Value::Array(task_workdirs));
275+
}
276+
224277
fn print_summary(&self, filename: &Path, summary: Summary) {
225278
let json = json!({
226279
"filename": filename,

crates/hyperqueue/src/client/output/outputs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub trait Output {
4848
fn print_job_list(&self, jobs: Vec<JobInfo>, total_jobs: usize);
4949
fn print_job_summary(&self, jobs: Vec<JobInfo>);
5050
fn print_job_detail(&self, jobs: Vec<JobDetail>, worker_map: WorkerMap, server_uid: &str);
51+
fn print_job_workdir(&self, jobs: Vec<JobDetail>, server_uid: &str);
5152
fn print_job_wait(
5253
&self,
5354
duration: Duration,
@@ -80,6 +81,7 @@ pub trait Output {
8081
verbosity: Verbosity,
8182
);
8283
fn print_task_ids(&self, jobs_task_id: Vec<(JobId, IntArray)>);
84+
fn print_task_workdir(&self, jobs: Vec<(JobId, JobDetail)>, server_uid: &str);
8385

8486
// Stream
8587
fn print_summary(&self, path: &Path, summary: Summary);

crates/hyperqueue/src/client/output/quiet.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ impl Output for Quiet {
9999
}
100100
fn print_job_detail(&self, _jobs: Vec<JobDetail>, _worker_map: WorkerMap, _server_uid: &str) {}
101101

102+
fn print_job_workdir(&self, _jobs: Vec<JobDetail>, _server_uid: &str) {}
103+
102104
fn print_job_wait(
103105
&self,
104106
_duration: Duration,
@@ -139,6 +141,8 @@ impl Output for Quiet {
139141

140142
fn print_task_ids(&self, _job_task_ids: Vec<(JobId, IntArray)>) {}
141143

144+
fn print_task_workdir(&self, _jobs: Vec<(JobId, JobDetail)>, _server_uid: &str) {}
145+
142146
// Stream
143147
fn print_summary(&self, _filename: &Path, _summary: Summary) {}
144148

crates/hyperqueue/src/common/cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::client::commands::data::DataOpts;
1111
use crate::client::commands::doc::DocOpts;
1212
use crate::client::commands::job::{
1313
JobCancelOpts, JobCatOpts, JobCloseOpts, JobForgetOpts, JobInfoOpts, JobListOpts,
14-
JobTaskIdsOpts,
14+
JobTaskIdsOpts, JobWorkdirOpts,
1515
};
1616
use crate::client::commands::journal::JournalOpts;
1717
use crate::client::commands::outputlog::OutputLogOpts;
@@ -374,6 +374,8 @@ pub enum JobCommand {
374374
Open(SubmitJobConfOpts),
375375
/// Close an open job
376376
Close(JobCloseOpts),
377+
/// Display working directory of selected job(s)
378+
Workdir(JobWorkdirOpts),
377379
}
378380

379381
#[derive(Parser)]

0 commit comments

Comments
 (0)