From b7a1ff8f2b7fe8ef69231cd3f68631791619b2f6 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 8 Oct 2026 01:32:21 -0400 Subject: [PATCH] CLI: run rejects an export option with no export after it (#67) --page, --scale and --pdf-options apply to the exports that follow them; one given after the last --export or --all-pages did nothing and said nothing. The usage text and docs/agents.md say so. --- apps/designcraft-cli/src/main.rs | 34 +++++++++++++++++++++++++++++-- apps/designcraft-cli/tests/cli.rs | 22 ++++++++++++++++++++ docs/agents.md | 4 ++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/apps/designcraft-cli/src/main.rs b/apps/designcraft-cli/src/main.rs index 9e9eba25d..a564befc2 100644 --- a/apps/designcraft-cli/src/main.rs +++ b/apps/designcraft-cli/src/main.rs @@ -1,7 +1,8 @@ //! Headless DesignCraft. //! //! ```text -//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub] [--pdf-options JSON] [--all-pages DIR] +//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub] [--all-pages DIR] +//! # --page, --scale and --pdf-options apply to the exports that follow them //! designcraft-cli commands [FILTER] # list commands (JSON), optionally only ids/labels/menus containing FILTER //! designcraft-cli describe ID # one command: label, menu, shortcut, parameters //! designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going] @@ -77,7 +78,7 @@ fn main() -> ExitCode { } _ => { eprintln!( - "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version" + "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n (--page, --scale and --pdf-options apply to the exports that follow them)\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version" ); eprintln!( "\nCommunity: {} · {} · {}", @@ -136,7 +137,36 @@ fn mcp(args: &[String]) -> Result<(), String> { Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string()) } +/// The first `--page`, `--scale` or `--pdf-options` with no `--export` or `--all-pages` after it. +/// They apply to the exports that follow them, so one after the last export would do nothing. +fn option_after_last_export(args: &[String]) -> Option<&str> { + let mut unused = None; + let mut it = args.iter(); + while let Some(a) = it.next() { + match a.as_str() { + "--page" | "--scale" | "--pdf-options" => { + unused = unused.or(Some(a.as_str())); + it.next(); + } + "--export" | "--all-pages" => { + unused = None; + it.next(); + } + "--in" | "--cmd" => { + it.next(); + } + _ => {} + } + } + unused +} + fn run(args: &[String]) -> Result<(), String> { + if let Some(opt) = option_after_last_export(args) { + return Err(format!( + "{opt} has no --export or --all-pages after it; options apply to the exports that follow them, so put it before its --export" + )); + } let mut s = Session::new(); let mut page = 0usize; let mut scale = 1.0f64; diff --git a/apps/designcraft-cli/tests/cli.rs b/apps/designcraft-cli/tests/cli.rs index 75f6d90b9..8c2b0f14b 100644 --- a/apps/designcraft-cli/tests/cli.rs +++ b/apps/designcraft-cli/tests/cli.rs @@ -80,3 +80,25 @@ fn closed_stdout_ends_quietly() { let err = String::from_utf8_lossy(&out.stderr); assert!(out.status.success() && !err.contains("panicked"), "{:?}: {err}", out.status); } + +/// `--scale`, `--page` and `--pdf-options` apply to the exports after them; one with no export +/// after it was silently ignored. +#[test] +fn run_export_options_come_before_their_export() { + let dir = std::env::temp_dir().join(format!("dc-cli-order-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let (a, b) = (dir.join("a.png"), dir.join("b.png")); + let (a, b) = (a.to_str().unwrap(), b.to_str().unwrap()); + for (opt, val) in [("--scale", "0.5"), ("--page", "1"), ("--pdf-options", r#"{"pages":"1"}"#)] { + let out = cli().args(["run", "--sample", "--export", a, opt, val]).output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert!(!out.status.success(), "{opt} after the last --export: {err}"); + assert!(err.contains(opt) && err.contains("before"), "{err}"); + } + assert!(!std::path::Path::new(a).exists(), "nothing is exported when the arguments are wrong"); + let out = cli().args(["run", "--sample", "--scale", "0.5", "--export", a, "--scale", "0.25", "--export", b]).output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "{err}"); + assert!(err.contains("wrote") && std::path::Path::new(a).exists() && std::path::Path::new(b).exists(), "{err}"); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/docs/agents.md b/docs/agents.md index 4571b2797..0dd23f062 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -41,6 +41,10 @@ The result is JSON: `{"completed": n, "results": [...]}`, plus `failedIndex` / ` fails (the exit status is non-zero; `--keep-going` records errors and continues). `run --cmd ID=JSON` accepts the same references, and MCP's `batch` tool takes `commands` or `script` text with them. +`run` works through its arguments in order. `--page`, `--scale` and `--pdf-options` apply to the `--export` / +`--all-pages` that follow them, so one run can write several sizes +(`run --sample --scale 0.5 --export small.png --scale 2 --export large.png`); one with no export after it is an error. + ## Data merge `data.source.select {path, sheet?}` links a CSV/TSV/`.xlsx` file to the document (`data.source.update` re-reads it,