From ac4b6787eb4cda4828e3095595afbe1e3fdb023e Mon Sep 17 00:00:00 2001 From: Alexey Karimov Date: Fri, 16 Oct 2026 18:25:23 +0500 Subject: [PATCH 1/2] fix(log): disable ANSI colour codes unless stderr is a terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log output went to Xcode and Gradle build logs, CI logs and the detached bugsee-cli.log file with escape codes, because tracing-subscriber enables ANSI by default. Colour now needs an interactive stderr, and a non-empty NO_COLOR turns it off too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/main.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main.rs b/src/main.rs index e6dbb09..337f037 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use std::io::IsTerminal; use tracing_subscriber::EnvFilter; mod cli; @@ -16,6 +17,13 @@ use exit_code::ExitCode; // fork (forking a live multi-threaded runtime is undefined behaviour — only the // forking thread survives, leaving the runtime deadlock-prone). So we parse, // optionally double-fork, THEN build the runtime in the surviving process. +/// Colour only an interactive terminal: Xcode / Gradle / CI logs and the +/// detached `bugsee-cli.log` file would otherwise be full of escape codes. +/// Honours (any non-empty `NO_COLOR` disables colour). +fn ansi_enabled(stderr_is_terminal: bool, no_color: Option<&std::ffi::OsStr>) -> bool { + stderr_is_terminal && no_color.is_none_or(|v| v.is_empty()) +} + fn main() { let cli = match cli::Cli::try_parse() { Ok(cli) => cli, @@ -63,6 +71,10 @@ fn main() { EnvFilter::try_from_default_env().unwrap_or_else(|_| "bugsee_cli=info".into()), ) .with_writer(std::io::stderr) + .with_ansi(ansi_enabled( + std::io::stderr().is_terminal(), + std::env::var_os("NO_COLOR").as_deref(), + )) .init(); let runtime = match tokio::runtime::Builder::new_multi_thread() @@ -87,3 +99,21 @@ fn main() { }); std::process::exit(code.as_i32()); } + +#[cfg(test)] +mod ansi_tests { + use super::ansi_enabled; + use std::ffi::OsStr; + + #[test] + fn colour_only_on_a_terminal() { + assert!(ansi_enabled(true, None)); + assert!(!ansi_enabled(false, None)); + } + + #[test] + fn no_color_disables_colour_unless_empty() { + assert!(!ansi_enabled(true, Some(OsStr::new("1")))); + assert!(ansi_enabled(true, Some(OsStr::new("")))); + } +} From 724c516a77b502c985706f65789efb9526dec017 Mon Sep 17 00:00:00 2001 From: Alexey Karimov Date: Fri, 16 Oct 2026 18:57:16 +0500 Subject: [PATCH 2/2] Keep tokio comment attached to main; pin no-ANSI-when-piped via binary test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move ansi_enabled below main so the load-bearing '// NOT #[tokio::main]' comment documents fn main again. Add an assert_cmd integration test that runs the binary with piped stderr and asserts no ESC bytes in the log output. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/main.rs | 14 +++++++------- tests/no_ansi_when_piped.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 tests/no_ansi_when_piped.rs diff --git a/src/main.rs b/src/main.rs index 337f037..94fde3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,13 +17,6 @@ use exit_code::ExitCode; // fork (forking a live multi-threaded runtime is undefined behaviour — only the // forking thread survives, leaving the runtime deadlock-prone). So we parse, // optionally double-fork, THEN build the runtime in the surviving process. -/// Colour only an interactive terminal: Xcode / Gradle / CI logs and the -/// detached `bugsee-cli.log` file would otherwise be full of escape codes. -/// Honours (any non-empty `NO_COLOR` disables colour). -fn ansi_enabled(stderr_is_terminal: bool, no_color: Option<&std::ffi::OsStr>) -> bool { - stderr_is_terminal && no_color.is_none_or(|v| v.is_empty()) -} - fn main() { let cli = match cli::Cli::try_parse() { Ok(cli) => cli, @@ -100,6 +93,13 @@ fn main() { std::process::exit(code.as_i32()); } +/// Colour only an interactive terminal: Xcode / Gradle / CI logs and the +/// detached `bugsee-cli.log` file would otherwise be full of escape codes. +/// Honours (any non-empty `NO_COLOR` disables colour). +fn ansi_enabled(stderr_is_terminal: bool, no_color: Option<&std::ffi::OsStr>) -> bool { + stderr_is_terminal && no_color.is_none_or(|v| v.is_empty()) +} + #[cfg(test)] mod ansi_tests { use super::ansi_enabled; diff --git a/tests/no_ansi_when_piped.rs b/tests/no_ansi_when_piped.rs new file mode 100644 index 0000000..3096169 --- /dev/null +++ b/tests/no_ansi_when_piped.rs @@ -0,0 +1,30 @@ +//! Log output must carry no ANSI escape codes unless stderr is a terminal. +//! +//! Xcode, Gradle and CI capture stderr into logs, and the detached daemon writes +//! it to `bugsee-cli.log`; escape codes there are noise. Under `assert_cmd` stderr +//! is a pipe, so any ESC byte means the `with_ansi(...)` wiring in `main` regressed. + +mod common; + +/// `sourcemaps inject` on an empty directory needs no network and logs one info line. +#[test] +fn piped_stderr_has_no_escape_codes() { + let dir = tempfile::tempdir().unwrap(); + let out = common::cli() + .env("RUST_LOG", "bugsee_cli=info") + .env_remove("NO_COLOR") + .args(["sourcemaps", "inject"]) + .arg(dir.path()) + .assert() + .success(); + let stderr = out.get_output().stderr.clone(); + let text = String::from_utf8_lossy(&stderr); + assert!( + text.contains("sourcemaps inject complete"), + "expected a log line on stderr, got: {text:?}" + ); + assert!( + !stderr.contains(&0x1b), + "stderr contains ANSI escapes: {text:?}" + ); +}