Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions crates/blox-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,50 @@ pub fn acp_proxy_args(workspace_name: &str, command: Option<&str>) -> Vec<String
args
}

/// Strip ANSI escape sequences (CSI and OSC) from a string so that
/// downstream string-matching (e.g. `is_auth_error`) is not confused by
/// terminal colour / style codes that `sq` may emit on stderr.
fn strip_ansi_escape_sequences(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut chars = input.chars().peekable();

while let Some(ch) = chars.next() {
if ch != '\u{1b}' {
output.push(ch);
continue;
}

match chars.peek().copied() {
// CSI sequence: ESC [ … <final byte in @–~>
Some('[') => {
let _ = chars.next();
for candidate in chars.by_ref() {
if ('@'..='~').contains(&candidate) {
break;
}
}
}
// OSC sequence: ESC ] … (terminated by BEL or ST)
Some(']') => {
let _ = chars.next();
let mut previous = '\0';
for candidate in chars.by_ref() {
if candidate == '\u{0007}' {
break;
}
if previous == '\u{1b}' && candidate == '\\' {
break;
}
previous = candidate;
}
}
_ => {}
}
}

output
}

/// Heuristic: does the CLI stderr look like an authentication / login error?
fn is_auth_error(stderr: &str) -> bool {
let lower = stderr.to_lowercase();
Expand Down Expand Up @@ -248,11 +292,11 @@ fn run(args: &[&str], timeout: Duration) -> Result<String, BloxError> {
let stderr = stderr_reader.join().unwrap_or_default();

if !status.success() {
let stderr = String::from_utf8_lossy(&stderr);
let stderr = strip_ansi_escape_sequences(&String::from_utf8_lossy(&stderr));
if is_auth_error(&stderr) {
return Err(BloxError::NotAuthenticated);
}
return Err(BloxError::CommandFailed(stderr.into_owned()));
return Err(BloxError::CommandFailed(stderr));
Comment on lines +295 to +299

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve original stderr for non-auth failures

If sq formats a non-auth error with OSC sequences (for example an OSC-8 hyperlink), this now strips that data before returning CommandFailed, so callers no longer receive the exact CLI message. That matters in the start_workspace path, which explicitly preserves the original stderr so users can see the onboarding URL/action (apps/staged/src-tauri/src/branches.rs:1251-1258). A safer change is to strip a temporary copy only for is_auth_error, while leaving the original stderr in CommandFailed.

Useful? React with 👍 / 👎.

}

String::from_utf8(stdout)
Expand Down Expand Up @@ -351,3 +395,55 @@ pub fn check_auth() -> Result<(), BloxError> {
Err(_) => Ok(()),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn strip_ansi_plain_text_unchanged() {
let input = "hello world";
assert_eq!(strip_ansi_escape_sequences(input), "hello world");
}

#[test]
fn strip_ansi_csi_sequences() {
// Bold + red text with reset
let input = "\x1b[1;31merror: not logged in\x1b[0m";
assert_eq!(strip_ansi_escape_sequences(input), "error: not logged in");
}

#[test]
fn strip_ansi_osc_sequence_bel_terminated() {
// OSC to set window title, terminated by BEL (\x07)
let input = "\x1b]0;my title\x07some text";
assert_eq!(strip_ansi_escape_sequences(input), "some text");
}

#[test]
fn strip_ansi_osc_sequence_st_terminated() {
// OSC terminated by ST (ESC \)
let input = "\x1b]0;my title\x1b\\some text";
assert_eq!(strip_ansi_escape_sequences(input), "some text");
}

#[test]
fn strip_ansi_mixed_sequences() {
let input = "\x1b[31mError:\x1b[0m \x1b[1mnot authenticated\x1b[0m";
assert_eq!(
strip_ansi_escape_sequences(input),
"Error: not authenticated"
);
}

#[test]
fn strip_ansi_empty_string() {
assert_eq!(strip_ansi_escape_sequences(""), "");
}

#[test]
fn strip_ansi_preserves_non_escape_special_chars() {
let input = "line1\nline2\ttab";
assert_eq!(strip_ansi_escape_sequences(input), "line1\nline2\ttab");
}
}