From 97f5bc40301d6b9d66e83ba6d90177234cb424c2 Mon Sep 17 00:00:00 2001 From: Osazeme Usen Date: Sat, 5 Sep 2026 23:01:45 +0100 Subject: [PATCH] feat(pkg): compress yarn and bun install logs Extends compress_pkg_log to cover yarn and bun alongside npm and pip. yarn is handled in both dialects. Classic v1 is matched on its line prefixes: the banner, [n/m] step lines, dead info lines, and the echo of a package's deprecated field, which is the direct analogue of npm's "npm warn deprecated". Peer dependency warnings survive, because they open with a quote where those advisories do not, which is a sturdier test than matching on the message text. Berry is matched on the YNnnnn codes it emits from v2 onwards, dropping structural framing, version notices and the per-package cache chatter that makes up most of a yarn 2 or 3 log. YN0000 lines carrying a box-drawing prefix are deliberately left alone, since that is where berry reports build script stdout and stderr. bun loses its banner, its dotenv notice and the static progress lines it prints in place of a spinner when stderr is not a terminal. The install summary is hoisted the way npm's already is. The per-package "+ pkg@ver" rows collapse to a count above five, where bun is listing the project rather than reporting a change and package.json already has the list, and stay verbatim below it where they are a useful diff. Node's own DEP0169 notice shows up in every yarn v1 run on current Node and mentions "security implications", so it is matched ahead of the security check. It is the only rule placed before it. Fixtures are real captures from yarn 1.22.22, yarn 4.18.0 and bun 1.4.2 with both streams piped. Savings on those are 65% for yarn v1, 73% for berry and 81% for bun. Failure paths are asserted to survive intact, including the berry build failure case, which is the one place a filter like this can lose something that matters. Closes #3 Signed-off-by: Osazeme Usen --- README.md | 2 +- docs/README.de.md | 2 +- docs/README.es.md | 2 +- docs/README.fr.md | 2 +- docs/README.hi.md | 2 +- docs/README.ja.md | 2 +- docs/README.ko.md | 2 +- docs/README.pt.md | 2 +- docs/README.ru.md | 2 +- docs/README.tr.md | 2 +- docs/README.vi.md | 2 +- docs/README.zh.md | 2 +- src/pkg_cmd.rs | 521 +++++++++++++++++++++++++++++++++++++++++++++- 13 files changed, 532 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a317cd0..a080515 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,7 @@ Built on [RTK](https://github.com/rtk-ai/rtk) (28k⭐). All 34 RTK commands incl | Web page content extraction | ❌ | ✅ | | ANSI / spinner / decoration removal | 🟡 | ✅ | | Build error grouping (tsc/eslint/cargo) | 🟡 | ✅ | -| Package install noise (npm/pip/cargo) | ❌ | ✅ | +| Package install noise (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker build compression | 🟡 | ✅ | | Per-command savings display | ❌ | ✅ | | Session-history compression (planned) | ❌ | 🚧 | diff --git a/docs/README.de.md b/docs/README.de.md index d98a665..07443ad 100644 --- a/docs/README.de.md +++ b/docs/README.de.md @@ -527,7 +527,7 @@ Basiert auf [RTK](https://github.com/rtk-ai/rtk) (28k⭐). Alle 34 RTK-Befehle e | Web-Content-Extraktion | ❌ | ✅ | | ANSI / Spinner / Dekoration entfernen | 🟡 | ✅ | | Build-Fehler-Gruppierung (tsc/eslint/cargo) | 🟡 | ✅ | -| Paketinstallations-Rauschen (npm/pip/cargo) | ❌ | ✅ | +| Paketinstallations-Rauschen (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker Build Komprimierung | 🟡 | ✅ | | Ersparnis pro Befehl | ❌ | ✅ | diff --git a/docs/README.es.md b/docs/README.es.md index 1cd9256..d864c2c 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -527,7 +527,7 @@ Basado en [RTK](https://github.com/rtk-ai/rtk) (28k⭐). Incluye los 34 comandos | Extracción de contenido web | ❌ | ✅ | | Eliminación de ANSI / spinners / decoración | 🟡 | ✅ | | Agrupación de errores de build (tsc/eslint/cargo) | 🟡 | ✅ | -| Ruido de instalación de paquetes (npm/pip/cargo) | ❌ | ✅ | +| Ruido de instalación de paquetes (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Compresión de Docker build | 🟡 | ✅ | | Ahorro por comando | ❌ | ✅ | diff --git a/docs/README.fr.md b/docs/README.fr.md index 2ea801f..25dc9d0 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -527,7 +527,7 @@ Basé sur [RTK](https://github.com/rtk-ai/rtk) (28k⭐). Les 34 commandes RTK in | Extraction de contenu web | ❌ | ✅ | | Suppression ANSI / spinners / décoration | 🟡 | ✅ | | Groupement d'erreurs de build (tsc/eslint/cargo) | 🟡 | ✅ | -| Bruit d'installation de paquets (npm/pip/cargo) | ❌ | ✅ | +| Bruit d'installation de paquets (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Compression Docker build | 🟡 | ✅ | | Économie par commande | ❌ | ✅ | diff --git a/docs/README.hi.md b/docs/README.hi.md index a2e591f..9dc687d 100644 --- a/docs/README.hi.md +++ b/docs/README.hi.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | वेब पेज कंटेंट एक्सट्रैक्शन | ❌ | ✅ | | ANSI / स्पिनर / डेकोरेशन हटाना | 🟡 | ✅ | | बिल्ड एरर ग्रुपिंग (tsc/eslint/cargo) | 🟡 | ✅ | -| पैकेज इंस्टॉल शोर (npm/pip/cargo) | ❌ | ✅ | +| पैकेज इंस्टॉल शोर (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker बिल्ड कंप्रेशन | 🟡 | ✅ | | प्रति-कमांड बचत प्रदर्शन | ❌ | ✅ | diff --git a/docs/README.ja.md b/docs/README.ja.md index ce587c2..4f2e5ed 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | Webページコンテンツ抽出 | ❌ | ✅ | | ANSI / スピナー / デコレーション除去 | 🟡 | ✅ | | ビルドエラーグルーピング (tsc/eslint/cargo) | 🟡 | ✅ | -| パッケージインストールノイズ (npm/pip/cargo) | ❌ | ✅ | +| パッケージインストールノイズ (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Dockerビルド圧縮 | 🟡 | ✅ | | コマンド別削減量表示 | ❌ | ✅ | diff --git a/docs/README.ko.md b/docs/README.ko.md index 4ccc5c7..baa2e73 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | 웹 페이지 콘텐츠 추출 | ❌ | ✅ | | ANSI / 스피너 / 장식 제거 | 🟡 | ✅ | | 빌드 에러 그룹화 (tsc/eslint/cargo) | 🟡 | ✅ | -| 패키지 설치 노이즈 (npm/pip/cargo) | ❌ | ✅ | +| 패키지 설치 노이즈 (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker 빌드 압축 | 🟡 | ✅ | | 명령어별 절감량 표시 | ❌ | ✅ | diff --git a/docs/README.pt.md b/docs/README.pt.md index dda2153..2702c99 100644 --- a/docs/README.pt.md +++ b/docs/README.pt.md @@ -527,7 +527,7 @@ Baseado no [RTK](https://github.com/rtk-ai/rtk) (28k⭐). Inclui todos os 34 com | Extração de conteúdo web | ❌ | ✅ | | Remoção de ANSI / spinners / decoração | 🟡 | ✅ | | Agrupamento de erros de build (tsc/eslint/cargo) | 🟡 | ✅ | -| Ruído de instalação de pacotes (npm/pip/cargo) | ❌ | ✅ | +| Ruído de instalação de pacotes (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Compressão de Docker build | 🟡 | ✅ | | Economia por comando | ❌ | ✅ | diff --git a/docs/README.ru.md b/docs/README.ru.md index a3a2a11..7e080d4 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | Извлечение контента веб-страниц | ❌ | ✅ | | Удаление ANSI / спиннеров / декораций | 🟡 | ✅ | | Группировка ошибок сборки (tsc/eslint/cargo) | 🟡 | ✅ | -| Шум установки пакетов (npm/pip/cargo) | ❌ | ✅ | +| Шум установки пакетов (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Сжатие Docker build | 🟡 | ✅ | | Экономия по каждой команде | ❌ | ✅ | diff --git a/docs/README.tr.md b/docs/README.tr.md index 8b694a3..9df6c79 100644 --- a/docs/README.tr.md +++ b/docs/README.tr.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | Web sayfası içerik çıkarma | ❌ | ✅ | | ANSI / spinner / dekorasyon kaldırma | 🟡 | ✅ | | Build hata gruplama (tsc/eslint/cargo) | 🟡 | ✅ | -| Paket kurulum gürültüsü (npm/pip/cargo) | ❌ | ✅ | +| Paket kurulum gürültüsü (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker build sıkıştırma | 🟡 | ✅ | | Komut başına tasarruf gösterimi | ❌ | ✅ | diff --git a/docs/README.vi.md b/docs/README.vi.md index 9c09ce6..9668663 100644 --- a/docs/README.vi.md +++ b/docs/README.vi.md @@ -527,7 +527,7 @@ Xây dựng trên [RTK](https://github.com/rtk-ai/rtk) (28k⭐). Bao gồm toàn | Trích xuất nội dung web | ❌ | ✅ | | Xóa ANSI / spinner / trang trí | 🟡 | ✅ | | Gom nhóm lỗi build (tsc/eslint/cargo) | 🟡 | ✅ | -| Rác cài đặt package (npm/pip/cargo) | ❌ | ✅ | +| Rác cài đặt package (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Nén Docker build | 🟡 | ✅ | | Hiển thị tiết kiệm theo lệnh | ❌ | ✅ | diff --git a/docs/README.zh.md b/docs/README.zh.md index 4aee7af..0e700c5 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -527,7 +527,7 @@ Step 7/12 : RUN npm run build ← FAILED | 网页内容提取 | ❌ | ✅ | | ANSI / 加载动画 / 装饰移除 | 🟡 | ✅ | | 构建错误分组 (tsc/eslint/cargo) | 🟡 | ✅ | -| 包安装噪音 (npm/pip/cargo) | ❌ | ✅ | +| 包安装噪音 (npm/yarn/bun/pip/cargo) | ❌ | ✅ | | Docker 构建压缩 | 🟡 | ✅ | | 逐命令节省量显示 | ❌ | ✅ | diff --git a/src/pkg_cmd.rs b/src/pkg_cmd.rs index 8145eb3..cda58a9 100644 --- a/src/pkg_cmd.rs +++ b/src/pkg_cmd.rs @@ -46,12 +46,85 @@ lazy_static! { static ref VULN_SUMMARY_RE: Regex = Regex::new( r"(\d+)\s+vulnerabilit(y|ies)\s*\(([^)]+)\)" ).unwrap(); + + /// Node's own runtime deprecation notice, emitted by every yarn v1 run on + /// modern Node. Matched before SECURITY_RE because the DEP0169 text mentions + /// "security implications", which would otherwise pin it as a security line. + static ref NODE_RUNTIME_WARNING_RE: Regex = Regex::new( + r"^\((node:\d+\) \[DEP\d+\]|Use `node --trace-deprecation)" + ).unwrap(); + + /// yarn v1 banner, step lines and lockfile chatter + static ref YARN_NOISE_RE: Regex = Regex::new( + r"^(yarn (install|add|remove|upgrade|why|link|create|global|list|run) v\d|\[\d+/\d+\] |success (Saved lockfile|Already up-to-date)\.$)" + ).unwrap(); + + /// yarn v1 `info` lines that carry nothing actionable. Deliberately not a + /// blanket `^info `, because the network-retry lines explain a later failure. + static ref YARN_INFO_NOISE_RE: Regex = Regex::new( + r#"^info (No lockfile found\.$|Visit https://yarnpkg\.com/|If you think this is a bug,|\S+@\S+: The (platform|CPU architecture) "|"\S+@\S+" is an optional dependency)"# + ).unwrap(); + + /// yarn v1 missing-license noise + static ref YARN_LICENSE_NOISE_RE: Regex = Regex::new( + r"^warning (\S*package\.json: )?No license field$" + ).unwrap(); + + /// yarn v1 echo of a package's `deprecated` field, the analogue of + /// `npm warn deprecated`. The unquoted dependency path is what separates it + /// from a peer-dependency warning, which always opens with a quote. + static ref YARN_ADVISORY_RE: Regex = Regex::new( + r#"^warning ([^\s">]+ > )*[^\s">]+@[^\s:]+: "# + ).unwrap(); + + /// yarn v1 `yarn add` dependency trees. `info All dependencies` lists every + /// transitive package and runs to hundreds of lines. + static ref YARN_TREE_RE: Regex = Regex::new( + r"^[\x{251c}\x{2514}]\x{2500} \S+@" + ).unwrap(); + + /// yarn berry structural framing, version notices and per-package chatter. + /// Never matches `YN0000: | `, which carries build-script stdout/stderr. + static ref BERRY_NOISE_RE: Regex = Regex::new( + r"^\x{27a4} (YN0000: ([\x{250c}\x{2514}] |\x{b7} Yarn \d)|YN(0088|0089|0019|0085|0007|0008|0032): |YN0013: (\x{2502} )?\S+@\S+ can't be found in)" + ).unwrap(); + + /// The generic "run yarn explain peer-requirements" call to action, which + /// accompanies every YN0002/YN0060 and says nothing they do not. + /// YN0000 on yarn 2/3, YN0086 on yarn 4. + static ref BERRY_PEER_CTA_RE: Regex = Regex::new( + r"Some peer dependencies are incorrectly met" + ).unwrap(); + + /// bun banner, dotenv notice and the static progress lines it prints in + /// place of the spinner whenever stderr is not a terminal + static ref BUN_NOISE_RE: Regex = Regex::new( + r#"^(bun (install|add|remove|update|link|unlink|ci|pm) v\d|\[[\d.]+m?s\] "\.env"$|Resolving dependencies$|Resolved, downloaded and extracted \[\d+\]$|Saved (lockfile|yarn\.lock)$)"# + ).unwrap(); + + /// bun's per-package added rows, e.g. "+ express@4.18.2 (v5.2.1 available)" + static ref BUN_ADDED_RE: Regex = Regex::new( + r"^\+ \S+@\S+( \(v\S+ available\))?$" + ).unwrap(); + + /// bun install summary: "248 packages installed [8.43s]" + static ref BUN_SUMMARY_RE: Regex = Regex::new( + r"^(\d+) packages? installed \[([\d.]+(?:ms|s))\]$" + ).unwrap(); } -/// Compress package install logs from npm/yarn/pnpm/pip/cargo. +/// Above this many `+ pkg@version` rows, bun is listing the project's +/// dependencies rather than reporting a change, and the list is already in +/// package.json. Below it, the rows are a diff worth reading. +const BUN_ADDED_COLLAPSE_THRESHOLD: usize = 5; + +/// Compress package install logs from npm/yarn/bun/pnpm/pip/cargo. /// /// Removes noise (deprecated warnings, funding, progress bars, etc.) /// while preserving all security-related warnings. +/// +/// yarn is covered in both dialects: classic v1 line prefixes, and the +/// `YNnnnn` codes berry emits from v2 onwards. pub fn compress_pkg_log(input: &str) -> String { // Quick check: if input has no package manager patterns, passthrough if !is_pkg_output(input) { @@ -61,6 +134,14 @@ pub fn compress_pkg_log(input: &str) -> String { let mut kept_lines: Vec = Vec::new(); let mut pkg_count: Option<(u64, u64)> = None; // (packages, seconds) let mut vuln_summary: Option = None; + let mut bun_summary: Option = None; + + let bun_added_total = input + .lines() + .filter(|l| BUN_ADDED_RE.is_match(l.trim())) + .count(); + let collapse_bun_added = bun_added_total > BUN_ADDED_COLLAPSE_THRESHOLD; + let mut bun_added_collapsed = false; for line in input.lines() { let trimmed = line.trim(); @@ -69,6 +150,11 @@ pub fn compress_pkg_log(input: &str) -> String { continue; } + // Deliberately ahead of the security check, see NODE_RUNTIME_WARNING_RE + if NODE_RUNTIME_WARNING_RE.is_match(trimmed) { + continue; + } + // ALWAYS keep lines with security keywords if SECURITY_RE.is_match(trimmed) { // Format security deprecated warnings specially @@ -126,6 +212,42 @@ pub fn compress_pkg_log(input: &str) -> String { continue; } + // yarn (classic) + if YARN_NOISE_RE.is_match(trimmed) + || YARN_INFO_NOISE_RE.is_match(trimmed) + || YARN_LICENSE_NOISE_RE.is_match(trimmed) + || YARN_ADVISORY_RE.is_match(trimmed) + || YARN_TREE_RE.is_match(trimmed) + { + continue; + } + + // yarn (berry) + if BERRY_NOISE_RE.is_match(trimmed) || BERRY_PEER_CTA_RE.is_match(trimmed) { + continue; + } + + // bun + if BUN_NOISE_RE.is_match(trimmed) { + continue; + } + + if let Some(caps) = BUN_SUMMARY_RE.captures(trimmed) { + let count = caps.get(1).map(|m| m.as_str()).unwrap_or("?"); + let elapsed = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + let noun = if count == "1" { "package" } else { "packages" }; + bun_summary = Some(format!("\u{2713} {} {} ({})", count, noun, elapsed)); + continue; + } + + if collapse_bun_added && BUN_ADDED_RE.is_match(trimmed) { + if !bun_added_collapsed { + kept_lines.push(format!("+ {} packages added", bun_added_total)); + bun_added_collapsed = true; + } + continue; + } + // Keep everything else kept_lines.push(line.to_string()); } @@ -137,6 +259,10 @@ pub fn compress_pkg_log(input: &str) -> String { result.push(format!("\u{2713} {} packages ({}s)", count, secs)); } + if let Some(ref summary) = bun_summary { + result.push(summary.clone()); + } + if let Some(ref vuln) = vuln_summary { result.push(vuln.clone()); } @@ -167,6 +293,18 @@ fn is_pkg_output(input: &str) -> bool { "compiling", "deprecated", "audited", + "yarn install v", + "yarn add v", + "yarn remove v", + "\u{27a4} yn", + "bun install v", + "bun add v", + "bun remove v", + "saved lockfile", + "package installed", + "packages installed", + "package removed", + "packages removed", ]; indicators.iter().any(|ind| lower.contains(ind)) } @@ -337,4 +475,385 @@ To address all issues, run: savings ); } + + /// Real `yarn install` output, yarn 1.22.22 on Node 24, stdout+stderr piped. + const YARN_V1_INSTALL: &str = r#"yarn install v1.22.22 +info No lockfile found. +[1/4] Resolving packages... +(node:5606) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities. +(Use `node --trace-deprecation ...` to show where the warning was created) +warning glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. +warning glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. +warning har-validator@5.1.5: this library is no longer supported +warning request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142 +warning request > uuid@3.4.0: uuid@10 and below is no longer supported. +warning rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported +warning eslint@7.32.0: This version is no longer supported. Please see https://eslint.org/version-support for other options. +warning eslint > @humanwhocodes/config-array@0.5.0: Use @eslint/config-array instead +warning eslint > file-entry-cache > flat-cache > rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported +[2/4] Fetching packages... +[3/4] Linking dependencies... +warning "@typescript-eslint/parser > tsutils@3.21.0" has unmet peer dependency "typescript@>=2.8.0". +[4/4] Building fresh packages... +success Saved lockfile. +Done in 23.69s. +"#; + + /// Real `yarn install` output, yarn 4.18.0 (berry), stdout+stderr piped. + const YARN_BERRY_INSTALL: &str = r#"➤ YN0000: · Yarn 4.18.0 +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + @typescript-eslint/parser@npm:5.62.0, eslint@npm:7.32.0, express@npm:4.18.2, and 244 more. +➤ YN0000: └ Completed in 6s 999ms +➤ YN0000: ┌ Post-resolution validation +➤ YN0002: │ pkgtest@workspace:. doesn't provide typescript (p50b6a6), requested by @typescript-eslint/parser. +➤ YN0086: │ Some peer dependencies are incorrectly met by your project; run yarn explain peer-requirements for details, where is the six-letter p-prefixed code. +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0013: │ 178 packages were added to the project (+ 17.24 MiB). +➤ YN0000: └ Completed in 1s 811ms +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed in 0s 883ms +➤ YN0000: · Done with warnings in 9s 737ms +"#; + + /// Real `bun install` output, bun 1.4.2, stdout+stderr piped. + const BUN_INSTALL: &str = r#"bun install v1.4.2 (744846f84) +Resolving dependencies +Resolved, downloaded and extracted [997] +Saved lockfile + ++ @typescript-eslint/parser@5.62.0 (v8.69.0 available) ++ eslint@7.32.0 (v10.10.0 available) ++ express@4.18.2 (v5.2.1 available) ++ glob@7.2.3 (v13.0.6 available) ++ har-validator@5.1.5 ++ request@2.88.2 ++ rimraf@3.0.2 (v6.1.3 available) + +248 packages installed [8.43s] +"#; + + #[test] + fn test_yarn_v1_strips_ceremony_and_advisories() { + let result = compress_pkg_log(YARN_V1_INSTALL); + + assert!(!result.contains("yarn install v1.22.22")); + assert!(!result.contains("[1/4]")); + assert!(!result.contains("[4/4]")); + assert!(!result.contains("No lockfile found")); + assert!(!result.contains("Saved lockfile")); + + // Package advisories echoed from the registry go the way npm's do + assert!(!result.contains("inflight@1.0.6")); + assert!(!result.contains("har-validator")); + assert!(!result.contains("@humanwhocodes/config-array")); + + // Node's own runtime notice is dropped despite mentioning "security" + assert!(!result.contains("DEP0169")); + assert!(!result.contains("trace-deprecation")); + + assert!(result.contains("Done in 23.69s.")); + } + + #[test] + fn test_yarn_v1_keeps_peer_and_security_warnings() { + let result = compress_pkg_log(YARN_V1_INSTALL); + + // Peer warnings open with a quote, advisories do not. That is the split. + assert!(result.contains("has unmet peer dependency")); + assert!(result.contains("tsutils@3.21.0")); + + // Security wins over the advisory rule, as it does for npm + assert!(result.contains("glob@7.2.3")); + assert!(result.contains("security vulnerabilities")); + } + + #[test] + fn test_yarn_v1_token_savings() { + let result = compress_pkg_log(YARN_V1_INSTALL); + let savings = + 100.0 - (count_tokens(&result) as f64 / count_tokens(YARN_V1_INSTALL) as f64 * 100.0); + + assert!( + savings >= 60.0, + "Expected >=60% savings, got {:.1}%", + savings + ); + } + + #[test] + fn test_yarn_v1_keeps_errors_and_stack() { + let input = r#"yarn add v1.22.22 +[1/4] Resolving packages... +error Error: https://registry.yarnpkg.com/nope-xyz: Not found + at params.callback [as _callback] (/root/.cache/yarn/1.22.22/lib/cli.js:66680:18) + at Request.emit (node:events:508:28) +info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command. +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("error Error: https://registry.yarnpkg.com/nope-xyz: Not found")); + assert!(result.contains("at params.callback")); + assert!(result.contains("at Request.emit")); + + // The trailing documentation pointer carries nothing + assert!(!result.contains("for documentation about this command")); + } + + #[test] + fn test_yarn_v1_drops_dependency_trees() { + let input = r#"yarn add v1.22.22 +success Saved lockfile. +success Saved 86 new dependencies. +info Direct dependencies +└─ request@2.88.2 +info All dependencies +├─ ajv@6.12.6 +├─ asn1@0.2.6 +└─ yallist@3.1.1 +Done in 13.99s. +"#; + let result = compress_pkg_log(input); + + // The count survives, the enumeration does not + assert!(result.contains("success Saved 86 new dependencies.")); + assert!(result.contains("info All dependencies")); + assert!(!result.contains("ajv@6.12.6")); + assert!(!result.contains("yallist@3.1.1")); + } + + #[test] + fn test_yarn_berry_strips_framing_keeps_peer_problems() { + let result = compress_pkg_log(YARN_BERRY_INSTALL); + + assert!(!result.contains("┌")); + assert!(!result.contains("└")); + assert!(!result.contains("Yarn 4.18.0")); + assert!(!result.contains("YN0085")); + assert!(!result.contains("Some peer dependencies are incorrectly met")); + + // The specific peer problem is what the generic hint was pointing at + assert!(result.contains("YN0002")); + assert!(result.contains("doesn't provide typescript")); + + // Package count and outcome survive + assert!(result.contains("178 packages were added to the project")); + assert!(result.contains("Done with warnings in 9s 737ms")); + } + + #[test] + fn test_yarn_berry_token_savings() { + let result = compress_pkg_log(YARN_BERRY_INSTALL); + let savings = 100.0 + - (count_tokens(&result) as f64 / count_tokens(YARN_BERRY_INSTALL) as f64 * 100.0); + + assert!( + savings >= 60.0, + "Expected >=60% savings, got {:.1}%", + savings + ); + } + + #[test] + fn test_yarn_berry_keeps_build_script_output() { + // Berry reports build-script stdout/stderr under YN0000 with a "│" + // prefix. Dropping YN0000 by code would silently eat failing builds. + let input = r#"➤ YN0000: ┌ Link step +➤ YN0007: │ esbuild@npm:0.12.11 must be built because it never has been before or the last one failed +➤ YN0000: │ esbuild@npm:0.12.11 STDERR Failed to install "esbuild-linux-64" using npm +➤ YN0000: │ xsd-schema-validator@npm:0.7.0 STDERR Error: Java SDK required at JAVA_HOME +➤ YN0009: │ xsd-schema-validator@npm:0.7.0 couldn't be built successfully (exit code 1) +➤ YN0000: └ Completed in 0s 654ms +➤ YN0000: Failed with errors in 0s 711ms +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("STDERR Failed to install")); + assert!(result.contains("Java SDK required at JAVA_HOME")); + assert!(result.contains("couldn't be built successfully")); + assert!(result.contains("Failed with errors in 0s 711ms")); + + // Only the framing goes + assert!(!result.contains("┌ Link step")); + assert!(!result.contains("must be built because it never has been")); + } + + #[test] + fn test_yarn_berry_failure_survives_intact() { + // A resolution failure has no sections, no box characters, and a final + // line with no message code at all. + let input = r#"➤ YN0027: nope-xyz@unknown can't be resolved to a satisfying range +➤ YN0035: Package not found +➤ YN0035: Response Code: 404 (Not Found) +➤ YN0035: Request URL: https://registry.yarnpkg.com/nope-xyz + +➤ Errors happened when preparing the environment required to run this command. +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("YN0027")); + assert!(result.contains("Package not found")); + assert!(result.contains("404 (Not Found)")); + assert!(result.contains("Errors happened when preparing the environment")); + } + + #[test] + fn test_yarn_berry_drops_per_package_cache_chatter() { + // Yarn 2 and 3 re-emit these in a sliding window, hundreds of lines for + // a few dozen packages. + let input = r#"➤ YN0000: ┌ Fetch step +➤ YN0013: │ ajv@npm:6.15.0 can't be found in the cache and will be fetched from the remote registry +➤ YN0013: │ asn1@npm:0.2.6 can't be found in the cache and will be fetched from the remote registry +➤ YN0019: │ tsutils-npm-3.21.0-347e6636c5.zip appears to be unused - removing +➤ YN0000: └ Completed in 1s 766ms +➤ YN0000: Done with warnings in 7s 281ms +"#; + let result = compress_pkg_log(input); + + assert!(!result.contains("can't be found in the cache")); + assert!(!result.contains("appears to be unused")); + assert!(result.contains("Done with warnings in 7s 281ms")); + } + + #[test] + fn test_bun_install_collapses_added_list() { + let result = compress_pkg_log(BUN_INSTALL); + + assert!(!result.contains("bun install v1.4.2")); + assert!(!result.contains("Resolving dependencies")); + assert!(!result.contains("Resolved, downloaded and extracted")); + assert!(!result.contains("Saved lockfile")); + + assert!(result.contains("\u{2713} 248 packages (8.43s)")); + + // Seven rows is past the threshold, so they become a count + assert!(result.contains("+ 7 packages added")); + assert!(!result.contains("eslint@7.32.0")); + } + + #[test] + fn test_bun_token_savings() { + let result = compress_pkg_log(BUN_INSTALL); + let savings = + 100.0 - (count_tokens(&result) as f64 / count_tokens(BUN_INSTALL) as f64 * 100.0); + + assert!( + savings >= 60.0, + "Expected >=60% savings, got {:.1}%", + savings + ); + } + + #[test] + fn test_bun_keeps_short_added_list_verbatim() { + // Below the threshold the rows are a diff, not a listing + let input = r#"bun install v1.4.2 (744846f84) +Saved lockfile + ++ react@17.0.2 (v19.2.8 available) ++ react-dom@18.2.0 + +6 packages installed [1.71s] +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("+ react@17.0.2 (v19.2.8 available)")); + assert!(result.contains("+ react-dom@18.2.0")); + assert!(!result.contains("packages added")); + } + + #[test] + fn test_bun_add_keeps_installed_line_and_singularizes() { + let input = r#"bun add v1.4.2 (744846f84) +Resolving dependencies +Resolved, downloaded and extracted [4] +Saved lockfile + +installed lodash@4.17.21 + +1 package installed [1395.00ms] +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("installed lodash@4.17.21")); + assert!(result.contains("\u{2713} 1 package (1395.00ms)")); + assert!(!result.contains("1 packages")); + } + + #[test] + fn test_bun_keeps_blocked_postinstalls() { + // Blocked lifecycle scripts are a security signal, not chatter + let input = r#"bun install v1.4.2 (744846f84) +Saved lockfile + ++ core-js@3.50.0 + +1 package installed [1270.00ms] + +Blocked 1 postinstall. Run `bun pm untrusted` for details. +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("Blocked 1 postinstall. Run `bun pm untrusted` for details.")); + } + + #[test] + fn test_bun_keeps_errors_and_warnings() { + let input = r#"bun add v1.4.2 (744846f84) +Resolving dependencies +Resolved, downloaded and extracted [1] +warn: incorrect peer dependency "react@17.0.2" +error: GET https://registry.npmjs.org/nope-xyz - 404 +"#; + let result = compress_pkg_log(input); + + assert!(result.contains("error: GET https://registry.npmjs.org/nope-xyz - 404")); + assert!(result.contains("warn: incorrect peer dependency \"react@17.0.2\"")); + } + + #[test] + fn test_bun_legacy_indented_format() { + // bun 1.0.x indents its summary rows by one space and does not + // singularize the count + let input = r#"bun install v1.0.7 (b0393fba) + + svelte@3.59.2 + + typescript@4.9.5 + + vite@4.5.0 + + 168 packages installed [1065.00ms] +"#; + let result = compress_pkg_log(input); + + assert!(!result.contains("bun install v1.0.7")); + assert!(result.contains("\u{2713} 168 packages (1065.00ms)")); + } + + #[test] + fn test_bun_remove_keeps_result() { + let input = r#"bun remove v1.4.2 (744846f84) +Saved lockfile + +- lodash +1 package removed [28.00ms] +"#; + let result = compress_pkg_log(input); + + assert!(!result.contains("bun remove v1.4.2")); + assert!(result.contains("- lodash")); + assert!(result.contains("1 package removed [28.00ms]")); + } + + #[test] + fn test_bun_no_change_install_is_a_single_line() { + let input = r#"bun install v1.4.2 (744846f84) + +Checked 256 installs across 268 packages (no changes) [87.00ms] +"#; + let result = compress_pkg_log(input); + + assert_eq!( + result, + "Checked 256 installs across 268 packages (no changes) [87.00ms]" + ); + } }