From 04783a5726931a2ea29331440fe76f5e7919f857 Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Sat, 29 Aug 2026 23:08:34 -0700 Subject: [PATCH 1/3] Keep structured command output clean --- src/cmd_file.rs | 2 +- src/cmd_project.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmd_file.rs b/src/cmd_file.rs index c9f7a991..d2a03c66 100644 --- a/src/cmd_file.rs +++ b/src/cmd_file.rs @@ -138,7 +138,7 @@ impl crate::cmd::Command for CmdFileConvert { std::fs::write(&path, data)?; } writeln!( - ctx.io.out, + ctx.io.err_out, "wrote file `{}` to {}", filename, path.to_str().unwrap_or("") diff --git a/src/cmd_project.rs b/src/cmd_project.rs index cce333b0..c69f2d58 100644 --- a/src/cmd_project.rs +++ b/src/cmd_project.rs @@ -370,7 +370,7 @@ impl crate::cmd::Command for CmdProjectPublish { crate::project::persist_cloud_project_id(&local.project_toml, &environment, project.id)?; } writeln!( - ctx.io.out, + ctx.io.err_out, "{} Submitted Zoo cloud project {} for publication review", ctx.io.color_scheme().success_icon(), project.id @@ -444,7 +444,7 @@ impl crate::cmd::Command for CmdProjectUpload { crate::project::persist_cloud_project_id(&local.project_toml, &environment, project.id)?; writeln!( - ctx.io.out, + ctx.io.err_out, "{} {} Zoo cloud project id {} in {}", ctx.io.color_scheme().success_icon(), if existing_id.is_some() { "Updated" } else { "Stored" }, From 0b2ea91d1811881ab0a2948178d58abbe05b82ca Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Sun, 30 Aug 2026 20:22:05 -0700 Subject: [PATCH 2/3] Update file conversion stderr expectation --- src/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests.rs b/src/tests.rs index a0019f95..3cf34bbd 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -570,6 +570,7 @@ cli_tests! { ], ) .setup(setup_authenticated) + .stderr_contains("wrote file `source.stl`") .stdout_contains("Completed") } From e74fc2a5a847f2af9bc2928aa0a76d720a049e0b Mon Sep 17 00:00:00 2001 From: Jordan Noone Date: Thu, 3 Sep 2026 20:41:04 -0700 Subject: [PATCH 3/3] Add explicit JSON-only output option --- src/cmd_file.rs | 36 ++++++++++++++++++++++++++++-------- src/cmd_project.rs | 41 +++++++++++++++++++++++++++++++++-------- src/tests.rs | 20 +++++++++++++++++++- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/cmd_file.rs b/src/cmd_file.rs index d2a03c66..35ee3f2a 100644 --- a/src/cmd_file.rs +++ b/src/cmd_file.rs @@ -11,7 +11,7 @@ use kittycad_modeling_cmds::{ websocket::OkWebSocketResponseData, }; -use crate::cmd_kcl::write_deterministic_export; +use crate::{cmd_kcl::write_deterministic_export, types::FormatOutput}; /// Perform operations on CAD files. /// @@ -89,6 +89,10 @@ pub struct CmdFileConvert { #[clap(long, short, value_enum)] pub format: Option, + /// Output JSON to stdout, with human-readable status messages on stderr. + #[clap(long, conflicts_with = "format")] + pub json: bool, + /// If true, the output file should be deterministic, meaning any date or time information /// will be replaced with a fixed value. /// This is useful for when pushing to version control. @@ -126,6 +130,12 @@ impl crate::cmd::Command for CmdFileConvert { .create_conversion(self.output_format.clone(), src_format, &input.into()) .await?; + let format = if self.json { + FormatOutput::Json + } else { + ctx.format(&self.format)? + }; + // If they specified an output file, save the output to that file. if file_conversion.status == kittycad::types::ApiCallStatus::Completed { if let Some(outputs) = file_conversion.outputs { @@ -137,12 +147,21 @@ impl crate::cmd::Command for CmdFileConvert { } else { std::fs::write(&path, data)?; } - writeln!( - ctx.io.err_out, - "wrote file `{}` to {}", - filename, - path.to_str().unwrap_or("") - )?; + if format == FormatOutput::Table { + writeln!( + ctx.io.out, + "wrote file `{}` to {}", + filename, + path.to_str().unwrap_or("") + )?; + } else { + writeln!( + ctx.io.err_out, + "wrote file `{}` to {}", + filename, + path.to_str().unwrap_or("") + )?; + } } } else { anyhow::bail!( @@ -156,7 +175,6 @@ impl crate::cmd::Command for CmdFileConvert { file_conversion.outputs = None; // Print the output of the conversion. - let format = ctx.format(&self.format)?; ctx.io.write_output(&format, &file_conversion)?; Ok(()) @@ -803,6 +821,7 @@ mod test { output_format: kittycad::types::FileExportFormat::Obj, src_format: None, format: None, + json: false, deterministic:false, }), @@ -818,6 +837,7 @@ mod test { output_format: kittycad::types::FileExportFormat::Obj, src_format: None, format: None, + json: false, deterministic:false, }), stdin: "".to_string(), diff --git a/src/cmd_project.rs b/src/cmd_project.rs index c69f2d58..e6aece33 100644 --- a/src/cmd_project.rs +++ b/src/cmd_project.rs @@ -354,6 +354,10 @@ pub struct CmdProjectPublish { /// Command output format. #[clap(long, short, value_enum)] pub format: Option, + + /// Output JSON to stdout, with human-readable status messages on stderr. + #[clap(long, conflicts_with = "format")] + pub json: bool, } #[async_trait::async_trait(?Send)] @@ -369,14 +373,23 @@ impl crate::cmd::Command for CmdProjectPublish { if let ProjectTarget::Local { local, .. } = target { crate::project::persist_cloud_project_id(&local.project_toml, &environment, project.id)?; } - writeln!( - ctx.io.err_out, + + let format = if self.json { + FormatOutput::Json + } else { + ctx.format(&self.format)? + }; + let message = format!( "{} Submitted Zoo cloud project {} for publication review", ctx.io.color_scheme().success_icon(), project.id - )?; + ); + if format == FormatOutput::Table { + writeln!(ctx.io.out, "{message}")?; + } else { + writeln!(ctx.io.err_out, "{message}")?; + } - let format = ctx.format(&self.format)?; write_project_output(ctx, &format, &project)?; Ok(()) } @@ -412,6 +425,10 @@ pub struct CmdProjectUpload { /// Command output format. #[clap(long, short, value_enum)] pub format: Option, + + /// Output JSON to stdout, with human-readable status messages on stderr. + #[clap(long, conflicts_with = "format")] + pub json: bool, } #[async_trait::async_trait(?Send)] @@ -443,16 +460,24 @@ impl crate::cmd::Command for CmdProjectUpload { }; crate::project::persist_cloud_project_id(&local.project_toml, &environment, project.id)?; - writeln!( - ctx.io.err_out, + let format = if self.json { + FormatOutput::Json + } else { + ctx.format(&self.format)? + }; + let message = format!( "{} {} Zoo cloud project id {} in {}", ctx.io.color_scheme().success_icon(), if existing_id.is_some() { "Updated" } else { "Stored" }, project.id, local.project_toml.display() - )?; + ); + if format == FormatOutput::Table { + writeln!(ctx.io.out, "{message}")?; + } else { + writeln!(ctx.io.err_out, "{message}")?; + } - let format = ctx.format(&self.format)?; write_project_output(ctx, &format, &project)?; Ok(()) } diff --git a/src/tests.rs b/src/tests.rs index 3cf34bbd..eeb7b56d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -570,8 +570,26 @@ cli_tests! { ], ) .setup(setup_authenticated) + .stdout_contains("wrote file `source.stl`") + } + + convert_a_file_as_json(_ctx) => { + TestItem::new( + "convert a file as json", + svec![ + "zoo", + "file", + "convert", + "assets/in_obj.obj", + "/tmp/", + "--output-format", + "stl", + "--json", + ], + ) + .setup(setup_authenticated) .stderr_contains("wrote file `source.stl`") - .stdout_contains("Completed") + .stdout_contains(r#""status": "completed""#) } get_the_file_volume(_ctx) => {