mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 14:08:23 -06:00
Per step: number, tool, status (ok/error/skipped) and result or error. Without the flag the output is unchanged; exit codes are unchanged. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
9c42c0c5fe
commit
b870ad32e9
@@ -64,7 +64,8 @@ pdfcraft-cli batch <files or dirs...> --steps convert,ocr,optimize --out-dir DIR
|
||||
[--ppi 150] [--quality 60] [--dpi 300] [--language en] [--ocr-all-pages]
|
||||
pdfcraft-cli tools automation tools and their JSON Schemas
|
||||
pdfcraft-cli run <tool> [key=value …] [--root DIR] [--out image.png]
|
||||
pdfcraft-cli run --script steps.json [--root DIR] [{\"tool\": \"doc_open\", \"args\": {…}}, …]
|
||||
pdfcraft-cli run --script steps.json [--root DIR] [--report]
|
||||
[{\"tool\": \"doc_open\", \"args\": {…}}, …]; --report prints one JSON document for the run
|
||||
",
|
||||
mcp_usage!(),
|
||||
"\
|
||||
@@ -256,6 +257,7 @@ const RUN_OPTIONS: &[OptionSpec] = &[
|
||||
OptionSpec { name: "--root", takes_value: true },
|
||||
OptionSpec { name: "--script", takes_value: true },
|
||||
OptionSpec { name: "--out", takes_value: true },
|
||||
OptionSpec { name: "--report", takes_value: false },
|
||||
];
|
||||
const UI_OPTIONS: &[OptionSpec] = &[OptionSpec { name: "--control", takes_value: true }, OptionSpec { name: "--out", takes_value: true }];
|
||||
|
||||
@@ -783,22 +785,88 @@ fn print_output(auto: &pdfcraft_automation::Automation, content: Vec<pdfcraft_au
|
||||
let written = auto.write_output(path, &data).map_err(|e| e.to_string())?;
|
||||
stdout_line(format_args!("{}", serde_json::json!({ "image": written.to_string_lossy(), "width": width, "height": height })))?;
|
||||
}
|
||||
None => stdout_line(format_args!(
|
||||
"{}",
|
||||
serde_json::json!({ "image": "png", "width": width, "height": height, "bytes": data.len(), "hint": "pass --out file.png to save it" })
|
||||
))?,
|
||||
None => stdout_line(format_args!("{}", png_summary(&data, width, height)))?,
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What a PNG result is reported as when it isn't written to a file.
|
||||
fn png_summary(data: &[u8], width: u32, height: u32) -> serde_json::Value {
|
||||
serde_json::json!({ "image": "png", "width": width, "height": height, "bytes": data.len(), "hint": "pass --out file.png to save it" })
|
||||
}
|
||||
|
||||
/// A tool's result as JSON values, for the `--report` of a script. Same content as
|
||||
/// [`print_output`], collected instead of printed.
|
||||
fn output_values(
|
||||
auto: &pdfcraft_automation::Automation,
|
||||
content: Vec<pdfcraft_automation::Content>,
|
||||
out: Option<&str>,
|
||||
) -> Result<Vec<serde_json::Value>, String> {
|
||||
let mut values = Vec::new();
|
||||
for c in content {
|
||||
values.push(match c {
|
||||
pdfcraft_automation::Content::Json(v) => v,
|
||||
pdfcraft_automation::Content::Png { data, width, height } => match out {
|
||||
Some(path) => {
|
||||
let written = auto.write_output(path, &data).map_err(|e| e.to_string())?;
|
||||
serde_json::json!({ "image": written.to_string_lossy(), "width": width, "height": height })
|
||||
}
|
||||
None => png_summary(&data, width, height),
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// Run a script and print one JSON document: per step its number, tool and `ok`/`error`/`skipped`
|
||||
/// with the result or the message. The first failing step ends the run, as without `--report`;
|
||||
/// the steps after it are listed as skipped and the exit status is the same.
|
||||
fn run_script_report(auto: &mut pdfcraft_automation::Automation, steps: &[serde_json::Value]) -> Result<(), CliError> {
|
||||
let mut report = Vec::with_capacity(steps.len());
|
||||
let mut failure: Option<String> = None;
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
let n = i + 1;
|
||||
let tool = step["tool"].as_str();
|
||||
if failure.is_some() {
|
||||
report.push(serde_json::json!({ "step": n, "tool": tool, "status": "skipped" }));
|
||||
continue;
|
||||
}
|
||||
let outcome = match tool {
|
||||
None => Err(format!("step {n}: missing \"tool\"")),
|
||||
Some(t) => auto
|
||||
.call(t, &step["args"])
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|content| output_values(auto, content, step["out"].as_str()))
|
||||
.map_err(|e| format!("step {n} ({t}): {e}")),
|
||||
};
|
||||
match outcome {
|
||||
Ok(result) => report.push(serde_json::json!({ "step": n, "tool": tool, "status": "ok", "result": result })),
|
||||
Err(message) => {
|
||||
report.push(serde_json::json!({ "step": n, "tool": tool, "status": "error", "error": message }));
|
||||
failure = Some(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
let document = serde_json::json!({ "ok": failure.is_none(), "steps": report });
|
||||
stdout_line(format_args!("{}", serde_json::to_string_pretty(&document).unwrap_or_default()))?;
|
||||
failure.map_or(Ok(()), |message| Err(CliError::Message(message)))
|
||||
}
|
||||
|
||||
fn run(args: &[String]) -> Result<(), CliError> {
|
||||
validate_options("run", args, RUN_OPTIONS)?;
|
||||
let report = args.iter().any(|a| a == "--report");
|
||||
if report && flag(args, "--script").is_none() {
|
||||
return Err("run: --report needs --script".to_string().into());
|
||||
}
|
||||
let mut auto = automation(args)?;
|
||||
if let Some(script) = flag(args, "--script") {
|
||||
let text = std::fs::read_to_string(script).map_err(|e| format!("{script}: {e}"))?;
|
||||
let steps: Vec<serde_json::Value> = serde_json::from_str(&text).map_err(|e| format!("{script}: {e}"))?;
|
||||
if report {
|
||||
return run_script_report(&mut auto, &steps);
|
||||
}
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
let tool = step["tool"].as_str().ok_or(format!("step {}: missing \"tool\"", i + 1))?;
|
||||
let content = auto.call(tool, &step["args"]).map_err(|e| format!("step {} ({tool}): {e}", i + 1))?;
|
||||
|
||||
Reference in New Issue
Block a user