From e6c38ca276b3d943733463a90e81bb87cc003d7d Mon Sep 17 00:00:00 2001 From: anthony Date: Thu, 19 Feb 2026 20:21:27 +0100 Subject: [PATCH 1/7] feat(01-01): create yarn_cmd module with workspace parsing and boilerplate filtering - Add yarn_cmd.rs with run() for workspace command execution - Implement filter_yarn_output() stripping YN-prefixed, resolution/fetch/link, classic boilerplate - Add 7 unit tests covering clean output, YN prefix, resolution steps, empty, classic, mixed, and token savings - Add mod yarn_cmd declaration in main.rs for compilation - Follow npm_cmd pattern: capture output, filter stdout, pass stderr, track tokens, tee on failure, preserve exit codes Co-Authored-By: Claude Opus 4.6 --- src/yarn_cmd.rs | 314 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/yarn_cmd.rs diff --git a/src/yarn_cmd.rs b/src/yarn_cmd.rs new file mode 100644 index 0000000..0c676f8 --- /dev/null +++ b/src/yarn_cmd.rs @@ -0,0 +1,314 @@ +use crate::tee; +use crate::tracking; +use anyhow::{Context, Result}; +use regex::Regex; +use std::process::Command; + +/// Filter yarn output - strip boilerplate, keep meaningful content. +/// +/// Strips: YN-prefixed info lines, resolution/fetch/link progress, +/// empty lines, and yarn classic boilerplate (defensive). +/// Returns "ok checkmark" if all output was boilerplate. +pub(crate) fn filter_yarn_output(output: &str) -> String { + lazy_static::lazy_static! { + // YN0000-prefixed info lines from yarn berry (with or without arrow prefix) + static ref YN_PREFIX: Regex = Regex::new(r"^[\u{27a4}\u{2794}]?\s*YN\d{4}:").unwrap(); + // Yarn classic version header: "yarn run v1.22.19" + static ref CLASSIC_VERSION: Regex = Regex::new(r"^yarn run v\d").unwrap(); + // Yarn classic done message: "Done in 3.42s." + static ref CLASSIC_DONE: Regex = Regex::new(r"^Done in \d").unwrap(); + } + + let mut result = Vec::new(); + + for line in output.lines() { + let trimmed = line.trim(); + + // Skip empty/whitespace-only lines + if trimmed.is_empty() { + continue; + } + + // Skip yarn berry YN-prefixed info lines + if YN_PREFIX.is_match(trimmed) { + continue; + } + + // Skip resolution/fetch/link progress from workspace-tools + if trimmed.starts_with("Resolution step") + || trimmed.starts_with("Fetch step") + || trimmed.starts_with("Link step") + { + continue; + } + + // Skip yarn classic boilerplate (defensive) + if CLASSIC_VERSION.is_match(trimmed) { + continue; + } + if CLASSIC_DONE.is_match(trimmed) { + continue; + } + if trimmed.starts_with("info ") { + continue; + } + + result.push(line.to_string()); + } + + if result.is_empty() { + "ok \u{2713}".to_string() + } else { + result.join("\n") + } +} + +pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<()> { + // Expect: workspace [run]