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]" + ); + } }