use std::{
io::{self, Read, Write},
path::{Path, PathBuf},
};
use clap::Subcommand;
use serde_json::{Value, json};
use super::{cancellation_listener, print_json};
use crate::{
human, query, store,
tools::human::Content,
types::{RequestId, RunId},
};
#[derive(Debug, Subcommand)]
pub enum RequestCommand {
List {
#[arg(long, value_name = "RUN_ID")]
run: Option<RunId>,
},
Show { id: RequestId },
Claim {
id: RequestId,
#[arg(long, value_name = "NAME")]
reviewer: Option<crate::types::ReviewerId>,
},
Unclaim {
id: RequestId,
#[arg(long, value_name = "NAME")]
reviewer: Option<crate::types::ReviewerId>,
},
Tool {
id: RequestId,
tool: crate::config::ToolName,
#[arg(long, value_name = "NAME")]
reviewer: Option<crate::types::ReviewerId>,
},
Submit {
id: RequestId,
#[arg(long, value_enum)]
verdict: crate::runtime::Verdict,
#[arg(long, value_name = "JSON", conflicts_with = "fields_file")]
fields: Option<String>,
#[arg(long, value_name = "PATH")]
fields_file: Option<PathBuf>,
#[arg(long, value_name = "NAME")]
reviewer: Option<crate::types::ReviewerId>,
},
}
pub(super) async fn execute(
state: &Path,
command: RequestCommand,
json: bool,
) -> Result<u8, String> {
match command {
RequestCommand::List { run } => {
let views = store::read_requests(state, run.as_deref()).await?;
if json {
print_json(
&views
.iter()
.map(|view| query::request_output(view, time::OffsetDateTime::now_utc()))
.collect::<Vec<_>>(),
)?;
} else {
let mut out = io::stdout().lock();
writeln!(out, "REQUEST\tRUN\tEVAL\tSTATUS\tREVIEWER").map_err(|e| e.to_string())?;
for view in views {
writeln!(
out,
"{}\t{}\t{}\t{}\t{}",
view.request.id,
view.request.run_id,
view.request.eval_id,
view.request.status(),
view.claim
.as_ref()
.map_or("-", |claim| claim.reviewer.as_str())
)
.map_err(|e| e.to_string())?;
}
}
}
RequestCommand::Show { id } => {
print_json(&query::request_output(
&store::read_request(state, &id).await?,
time::OffsetDateTime::now_utc(),
))?;
}
RequestCommand::Claim { id, reviewer } => {
let reviewer = reviewer.map_or_else(human::default_reviewer, Ok)?;
let (receipts, _) = human::open(state, &id).await?;
print_json(&human::claim(&receipts, &id, &reviewer).await?)?;
}
RequestCommand::Unclaim { id, reviewer } => {
let reviewer = reviewer.map_or_else(human::default_reviewer, Ok)?;
let (receipts, _) = human::open(state, &id).await?;
print_json(&human::unclaim(&receipts, &id, &reviewer).await?)?;
}
RequestCommand::Tool { id, tool, reviewer } => {
let reviewer = reviewer.map_or_else(human::default_reviewer, Ok)?;
let (receipts, _) = human::open(state, &id).await?;
let (cancellation, listener) = cancellation_listener()?;
let result =
human::run_human_tool(&receipts, &id, &reviewer, &tool, cancellation).await;
listener.abort();
let result = result?;
if json {
print_json(&result)?;
} else {
let mut out = io::stdout().lock();
for content in result.content() {
match content {
Content::Text { text } => writeln!(out, "{text}"),
Content::Launch { launched } => writeln!(
out,
"Tool {tool}: {}",
if *launched {
"launched"
} else {
"not launched"
}
),
}
.map_err(|e| e.to_string())?;
}
}
return Ok(if result.is_error() { 2 } else { 0 });
}
RequestCommand::Submit {
id,
verdict,
fields,
fields_file,
reviewer,
} => {
let result = submission(verdict, fields, fields_file)?;
let reviewer = reviewer.map_or_else(human::default_reviewer, Ok)?;
let (cancellation, listener) = cancellation_listener()?;
let result =
human::submit_and_publish(state, &id, &reviewer, &result, cancellation).await;
listener.abort();
result?;
print_json(&query::request_output(
&store::read_request(state, &id).await?,
time::OffsetDateTime::now_utc(),
))?;
}
}
Ok(0)
}
fn submission(
verdict: crate::runtime::Verdict,
fields: Option<String>,
file: Option<PathBuf>,
) -> Result<Value, String> {
let fields = if let Some(path) = file {
let file = crate::platform::open_nonblocking(&path).map_err(|e| e.to_string())?;
if !file.metadata().map_err(|e| e.to_string())?.is_file() {
return Err("Human fields must be a regular JSON file.".into());
}
let mut bytes = Vec::new();
file.take(human::FIELDS_READ_BYTES)
.read_to_end(&mut bytes)
.map_err(|e| e.to_string())?;
String::from_utf8(bytes).map_err(|e| e.to_string())?
} else {
fields.unwrap_or_else(|| "{}".into())
};
if fields.len() > human::MAX_RESULT_BYTES {
return Err("Human fields exceed 256000 bytes.".into());
}
let mut result: Value = serde_json::from_str(&fields).map_err(|e| e.to_string())?;
let object = result
.as_object_mut()
.ok_or("Human fields must be a JSON object.")?;
if object.contains_key("verdict") {
return Err("Use --verdict, not a verdict inside --fields.".into());
}
object.insert("verdict".into(), json!(verdict));
Ok(result)
}