use serde::Serialize;
use serde_json::json;
use octl_core::{cancel_run, read_manifest_opt, CancelOutcome, NodeId};
use crate::error::CliError;
use crate::output::{self, OutputFormat, OutputSpec};
use crate::run::{from_core, run_paths, status_kebab};
#[derive(Serialize)]
struct CancelPayload {
run_id: String,
cancelled_nodes: Vec<String>,
nodes_already_terminal: Vec<String>,
already_cancelled: bool,
}
pub fn run(
run_id: &str,
note: Option<&str>,
spec: &OutputSpec,
warnings: &[String],
) -> Result<(), CliError> {
let root = crate::home::root_dir()?;
let paths = run_paths(&root, run_id)?;
let run_id = paths.run_id.as_str();
if read_manifest_opt(&paths).map_err(from_core)?.is_none() {
return Err(run_not_found(run_id));
}
let outcome = match cancel_run(&paths, note) {
Ok(o) => o,
Err(octl_core::Error::RunAlreadyTerminal { status }) => {
let s = status_kebab(status);
return Err(CliError::system(
"run_already_terminal",
format!("run is {s}, cannot cancel"),
)
.with_invalid_value(s)
.with_expected(json!("running|pending|blocked")));
}
Err(octl_core::Error::Io { source, .. })
if source.kind() == std::io::ErrorKind::NotFound =>
{
return Err(run_not_found(run_id));
}
Err(e) => return Err(from_core(e)),
};
emit(run_id, &outcome, spec, warnings)
}
fn run_not_found(run_id: &str) -> CliError {
CliError::user("run_not_found", format!("no run with id {run_id}")).with_invalid_value(run_id)
}
fn emit(
run_id: &str,
outcome: &CancelOutcome,
spec: &OutputSpec,
warnings: &[String],
) -> Result<(), CliError> {
let payload = CancelPayload {
run_id: run_id.to_string(),
cancelled_nodes: outcome.nodes_cancelled.iter().map(node_str).collect(),
nodes_already_terminal: outcome
.nodes_already_terminal
.iter()
.map(node_str)
.collect(),
already_cancelled: outcome.run_was_already_cancelled,
};
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
output::emit_envelope(&payload, spec, warnings)?;
}
OutputFormat::Text => {
let cancelled = payload.cancelled_nodes.len();
let already = payload.nodes_already_terminal.len();
if payload.already_cancelled {
println!(
"no-op: run {} was already cancelled, converged {cancelled} additional node(s) ({already} already terminal)",
payload.run_id,
);
} else {
println!(
"cancelled run {} ({cancelled} node(s) cancelled, {already} already terminal)",
payload.run_id,
);
}
output::emit_text_warnings(warnings);
}
}
Ok(())
}
fn node_str(id: &NodeId) -> String {
id.as_str().to_string()
}