Skip to content

Commit 39a912a

Browse files
feat(repx-rs): add show command and improve list jobs filtering (#13)
1 parent 535a8f7 commit 39a912a

File tree

9 files changed

+588
-25
lines changed

9 files changed

+588
-25
lines changed

crates/repx-cli/src/main.rs

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use which::which;
1212
#[command(version)]
1313
struct Cli {
1414
#[command(subcommand)]
15-
command: Commands,
15+
command: Option<Commands>,
16+
17+
#[arg(long, help = "Print help for all commands recursively")]
18+
help_all: bool,
1619

1720
#[arg(short, long, global = true, default_value = "./result")]
1821
pub lab: PathBuf,
@@ -74,8 +77,8 @@ struct VizArgs {
7477

7578
#[derive(Args)]
7679
struct TraceParamsArgs {
77-
#[arg(help = "Job ID to trace")]
78-
job_id: String,
80+
#[arg(help = "Job ID to trace (optional, shows all jobs if omitted)")]
81+
job_id: Option<String>,
7982
}
8083

8184
#[derive(Args)]
@@ -93,10 +96,60 @@ struct CompletionsArgs {
9396
shell: Shell,
9497
}
9598

99+
fn print_help_all() {
100+
let cmd = Cli::command();
101+
print_command_help(&cmd, 0);
102+
}
103+
104+
fn print_command_help(cmd: &clap::Command, depth: usize) {
105+
let indent = " ".repeat(depth);
106+
let name = cmd.get_name();
107+
108+
if cmd.is_hide_set() {
109+
return;
110+
}
111+
112+
if depth == 0 {
113+
println!("{}", "=".repeat(60));
114+
println!("REPX - Complete Command Reference");
115+
println!("{}", "=".repeat(60));
116+
println!();
117+
} else {
118+
println!();
119+
println!("{}{}", indent, "-".repeat(50 - indent.len()));
120+
println!("{}Command: {}", indent, name);
121+
println!("{}{}", indent, "-".repeat(50 - indent.len()));
122+
}
123+
124+
let mut help_cmd = cmd.clone();
125+
let help_text = help_cmd.render_help();
126+
127+
for line in help_text.to_string().lines() {
128+
println!("{}{}", indent, line);
129+
}
130+
131+
for subcmd in cmd.get_subcommands() {
132+
print_command_help(subcmd, depth + 1);
133+
}
134+
}
135+
96136
fn main() {
97137
let cli = Cli::parse();
98138

99-
match cli.command {
139+
if cli.help_all {
140+
print_help_all();
141+
return;
142+
}
143+
144+
let command = match cli.command {
145+
Some(cmd) => cmd,
146+
None => {
147+
Cli::command().print_help().unwrap();
148+
return;
149+
}
150+
};
151+
152+
match command {
100153
Commands::Runner(cmd) => {
101154
let is_internal = matches!(
102155
cmd.as_ref(),
@@ -153,15 +206,17 @@ fn main() {
153206
}
154207
Commands::TraceParams(args) => {
155208
run_python_tool("repx_py.cli.trace_params", |cmd| {
156-
cmd.arg(args.job_id);
157-
cmd.arg("--lab").arg(cli.lab);
209+
cmd.arg(&cli.lab);
210+
if let Some(job_id) = &args.job_id {
211+
cmd.arg("--job").arg(job_id);
212+
}
158213
});
159214
}
160215
Commands::DebugRun(args) => {
161216
run_python_tool("repx_py.cli.debug_runner", |cmd| {
162-
cmd.arg(args.job_id);
163-
cmd.arg("--lab").arg(cli.lab);
164-
if let Some(c) = args.command {
217+
cmd.arg(&args.job_id);
218+
cmd.arg("--lab").arg(&cli.lab);
219+
if let Some(c) = &args.command {
165220
cmd.arg("--command").arg(c);
166221
}
167222
});

crates/repx-core/src/store/outcomes.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ pub fn get_job_outcomes(
3737
continue;
3838
}
3939

40-
let success_marker = job_path.join(markers::SUCCESS);
41-
let fail_marker = job_path.join(markers::FAIL);
40+
let repx_dir = job_path.join(dirs::REPX);
41+
let success_marker = repx_dir.join(markers::SUCCESS);
42+
let fail_marker = repx_dir.join(markers::FAIL);
4243

4344
if success_marker.exists() {
4445
outcomes.insert(

crates/repx-runner/src/cli.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ pub enum Commands {
6161

6262
#[command(about = "List runs, jobs, or dependencies")]
6363
List(ListArgs),
64+
65+
#[command(about = "Show detailed information")]
66+
Show(ShowArgs),
6467
}
6568

6669
#[derive(Args)]
@@ -69,18 +72,61 @@ pub struct ListArgs {
6972
pub entity: Option<ListEntity>,
7073
}
7174

75+
#[derive(Args)]
76+
pub struct ShowArgs {
77+
#[command(subcommand)]
78+
pub entity: ShowEntity,
79+
}
80+
81+
#[derive(Subcommand)]
82+
pub enum ShowEntity {
83+
#[command(about = "Show detailed information about a job")]
84+
Job(ShowJobArgs),
85+
#[command(about = "Show contents of a job's output file")]
86+
Output(ShowOutputArgs),
87+
}
88+
89+
#[derive(Args)]
90+
pub struct ShowJobArgs {
91+
#[arg(help = "Job ID (or prefix) to inspect")]
92+
pub job_id: String,
93+
}
94+
95+
#[derive(Args)]
96+
pub struct ShowOutputArgs {
97+
#[arg(help = "Job ID (or prefix)")]
98+
pub job_id: String,
99+
#[arg(help = "Path to the output file (relative to job's out/ directory)")]
100+
pub path: Option<String>,
101+
}
102+
72103
#[derive(Subcommand)]
73104
pub enum ListEntity {
74105
Runs {
75106
#[arg(required = false, value_name = "RUN_NAME")]
76107
name: Option<String>,
77108
},
78-
Jobs {
79-
#[arg(required = false, value_name = "RUN_NAME")]
80-
name: Option<String>,
81-
},
109+
Jobs(ListJobsArgs),
82110
#[command(alias = "deps")]
83-
Dependencies { job_id: String },
111+
Dependencies {
112+
job_id: String,
113+
},
114+
}
115+
116+
#[derive(Args)]
117+
pub struct ListJobsArgs {
118+
#[arg(required = false, value_name = "RUN_NAME")]
119+
pub name: Option<String>,
120+
121+
#[arg(
122+
long,
123+
short = 's',
124+
help = "Filter jobs by stage name (substring match)"
125+
)]
126+
pub stage: Option<String>,
127+
128+
#[arg(long, help = "Show output directory paths for each job")]
129+
pub output_paths: bool,
84130
}
85131

86132
#[derive(Args)]

crates/repx-runner/src/commands/list.rs

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::cli::{ListArgs, ListEntity};
2+
use crate::error::CliError;
23
use repx_core::{
4+
config,
5+
constants::dirs,
36
errors::{ConfigError, DomainError},
47
lab,
58
model::{JobId, Lab, RunId},
@@ -8,17 +11,20 @@ use repx_core::{
811
use std::path::Path;
912
use std::str::FromStr;
1013

11-
use crate::error::CliError;
12-
1314
pub fn handle_list(args: ListArgs, lab_path: &Path) -> Result<(), CliError> {
1415
let lab = lab::load_from_path(lab_path)?;
1516

1617
match args.entity.unwrap_or(ListEntity::Runs { name: None }) {
1718
ListEntity::Runs { name } => match name {
18-
Some(n) => list_jobs(&lab, Some(&n)),
19+
Some(n) => list_jobs(&lab, Some(&n), None, false),
1920
None => list_runs(&lab, lab_path),
2021
},
21-
ListEntity::Jobs { name } => list_jobs(&lab, name.as_deref()),
22+
ListEntity::Jobs(job_args) => list_jobs(
23+
&lab,
24+
job_args.name.as_deref(),
25+
job_args.stage.as_deref(),
26+
job_args.output_paths,
27+
),
2228
ListEntity::Dependencies { job_id } => list_dependencies(&lab, &job_id),
2329
}
2430
}
@@ -35,7 +41,30 @@ fn list_runs(lab: &Lab, lab_path: &Path) -> Result<(), CliError> {
3541
Ok(())
3642
}
3743

38-
fn list_jobs(lab: &Lab, run_id_opt: Option<&str>) -> Result<(), CliError> {
44+
fn list_jobs(
45+
lab: &Lab,
46+
run_id_opt: Option<&str>,
47+
stage_filter: Option<&str>,
48+
show_output_paths: bool,
49+
) -> Result<(), CliError> {
50+
let store_path = if show_output_paths {
51+
let config = config::load_config()?;
52+
let target_name = config.submission_target.as_ref().ok_or_else(|| {
53+
CliError::Config(ConfigError::General(
54+
"No submission target configured".to_string(),
55+
))
56+
})?;
57+
let target = config.targets.get(target_name).ok_or_else(|| {
58+
CliError::Config(ConfigError::General(format!(
59+
"Target '{}' not found in config",
60+
target_name
61+
)))
62+
})?;
63+
Some(target.base_path.clone())
64+
} else {
65+
None
66+
};
67+
3968
let run_id_str = match run_id_opt {
4069
Some(s) => s,
4170
None => {
@@ -49,8 +78,17 @@ fn list_jobs(lab: &Lab, run_id_opt: Option<&str>) -> Result<(), CliError> {
4978
let run = &lab.runs[*run_id];
5079
let mut jobs: Vec<_> = run.jobs.iter().collect();
5180
jobs.sort();
81+
82+
let jobs: Vec<_> = if let Some(stage) = stage_filter {
83+
jobs.into_iter()
84+
.filter(|job_id| job_id.0.contains(stage))
85+
.collect()
86+
} else {
87+
jobs
88+
};
89+
5290
for job in jobs {
53-
println!(" {}", job);
91+
print_job_line(job, show_output_paths, &store_path);
5492
}
5593
}
5694
return Ok(());
@@ -86,8 +124,24 @@ fn list_jobs(lab: &Lab, run_id_opt: Option<&str>) -> Result<(), CliError> {
86124
println!("Jobs in run '{}':", id);
87125
let mut jobs: Vec<_> = run.jobs.iter().collect();
88126
jobs.sort();
127+
128+
let jobs: Vec<_> = if let Some(stage) = stage_filter {
129+
jobs.into_iter()
130+
.filter(|job_id| job_id.0.contains(stage))
131+
.collect()
132+
} else {
133+
jobs
134+
};
135+
136+
if jobs.is_empty() && stage_filter.is_some() {
137+
println!(
138+
" (no jobs matching stage filter '{}')",
139+
stage_filter.unwrap()
140+
);
141+
}
142+
89143
for job in jobs {
90-
println!(" {}", job);
144+
print_job_line(job, show_output_paths, &store_path);
91145
}
92146
Ok(())
93147
} else {
@@ -117,7 +171,7 @@ fn list_jobs(lab: &Lab, run_id_opt: Option<&str>) -> Result<(), CliError> {
117171
}
118172
if found_runs.len() == 1 {
119173
println!();
120-
return list_jobs(lab, Some(&found_runs[0].0));
174+
return list_jobs(lab, Some(&found_runs[0].0), stage_filter, show_output_paths);
121175
}
122176
return Ok(());
123177
}
@@ -127,6 +181,27 @@ fn list_jobs(lab: &Lab, run_id_opt: Option<&str>) -> Result<(), CliError> {
127181
}
128182
}
129183

184+
fn print_job_line(
185+
job_id: &JobId,
186+
show_output_paths: bool,
187+
store_path: &Option<std::path::PathBuf>,
188+
) {
189+
if show_output_paths {
190+
if let Some(store) = store_path {
191+
let output_path = store.join(dirs::OUTPUTS).join(&job_id.0).join(dirs::OUT);
192+
if output_path.exists() {
193+
println!(" {} {}", job_id, output_path.display());
194+
} else {
195+
println!(" {} (not executed)", job_id);
196+
}
197+
} else {
198+
println!(" {}", job_id);
199+
}
200+
} else {
201+
println!(" {}", job_id);
202+
}
203+
}
204+
130205
fn list_dependencies(lab: &Lab, job_id_str: &str) -> Result<(), CliError> {
131206
let target_input = RunId(job_id_str.to_string());
132207
let job_id = resolver::resolve_target_job_id(lab, &target_input)?;

crates/repx-runner/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod internal;
77
pub mod list;
88
pub mod run;
99
pub mod scatter_gather;
10+
pub mod show;
1011

1112
pub struct AppContext<'a> {
1213
pub lab_path: &'a PathBuf,

0 commit comments

Comments
 (0)