diff --git a/ROADMAP.md b/ROADMAP.md index b4ee24dc..804916fd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| -| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). | +| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | | 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections | diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 1cf07964..26139dcc 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -292,7 +292,21 @@ fn run(args: &[String]) -> Result<(), String> { let mut h = Headless::new(); let mut out = std::io::stdout().lock(); - let mut emit = |v: Value| writeln!(out, "{v}").map_err(|e| e.to_string()); + // A reader that stopped early (a closed pipe) isn't an error, and the batch goes on: its later + // steps (an export…) still run, unprinted. + let mut closed = false; + let mut emit = |v: Value| { + if closed { + return Ok(()); + } + match writeln!(out, "{v}") { + Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { + closed = true; + Ok(()) + } + r => r.map_err(|e| e.to_string()), + } + }; if let Some(path) = &input { let r = h.call("app.open", json!({"path": path})).map_err(|e| format!("open {path}: {e}"))?; emit(json!({"step": "open", "path": path, "result": r}))?; diff --git a/apps/vectorcraft-cli/tests/cli.rs b/apps/vectorcraft-cli/tests/cli.rs index d5bb2578..5b17de4b 100644 --- a/apps/vectorcraft-cli/tests/cli.rs +++ b/apps/vectorcraft-cli/tests/cli.rs @@ -297,3 +297,21 @@ fn closed_stdout_ends_quietly() { let err = String::from_utf8_lossy(&out.stderr); assert!(out.status.success() && !err.contains("panicked"), "{:?}: {err}", out.status); } + +/// #1005: `run` whose reader has gone (`vectorcraft-cli run … | true`) ends quietly with status 0, +/// and its later steps still run; a failing step still fails. +#[test] +fn run_with_a_closed_stdout_ends_quietly_and_finishes_the_batch() { + let svg = tmp("closed-run.svg"); + let _ = std::fs::remove_file(&svg); + let rect = r#"{"x":1,"y":1,"width":5,"height":5}"#; + let args = ["run", "--cmd", "document.json", "--cmd", "shape.rectangle", "--params", rect, "--export", svg.to_str().unwrap()]; + let mut child = Command::new(BIN).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); + drop(child.stdout.take()); + let out = child.wait_with_output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success() && err.is_empty(), "{:?}: {err}", out.status); + assert!(svg.is_file(), "the export after the closed reader still ran"); + let out = Command::new(BIN).args(["run", "--cmd", "not-a-command"]).stdout(Stdio::null()).output().unwrap(); + assert!(!out.status.success()); +}