@@ -135,17 +135,24 @@ fn run_filtered(name: &str, args: &[String], verbose: u8, skip_env: bool) -> Res
135135/// Filter npm run output - strip boilerplate, progress bars, npm WARN
136136fn filter_npm_output ( output : & str ) -> String {
137137 let mut result = Vec :: new ( ) ;
138+ let mut seen_substantive_output = false ;
139+ let mut leading_gt_lines = Vec :: new ( ) ;
138140
139141 for line in output. lines ( ) {
140- // Skip npm boilerplate
141- if line. starts_with ( '>' ) && line. contains ( '@' ) {
142+ let trimmed = line. trim_start ( ) ;
143+
144+ // npm 7+ emits script name and command as leading `> ` lines without
145+ // package@version. Treat 2+ leading lines as lifecycle echo so silent
146+ // scripts can fall back to `ok`, but preserve one as possible output.
147+ if !seen_substantive_output && trimmed. starts_with ( "> " ) {
148+ leading_gt_lines. push ( line. to_string ( ) ) ;
142149 continue ;
143150 }
144151 // Skip npm lifecycle scripts
145- if line . trim_start ( ) . starts_with ( "npm WARN" ) {
152+ if trimmed . starts_with ( "npm WARN" ) {
146153 continue ;
147154 }
148- if line . trim_start ( ) . starts_with ( "npm notice" ) {
155+ if trimmed . starts_with ( "npm notice" ) {
149156 continue ;
150157 }
151158 // Skip progress indicators
@@ -157,9 +164,21 @@ fn filter_npm_output(output: &str) -> String {
157164 continue ;
158165 }
159166
167+ if !seen_substantive_output {
168+ if leading_gt_lines. len ( ) == 1 {
169+ result. extend ( leading_gt_lines. drain ( ..) ) ;
170+ } else {
171+ leading_gt_lines. clear ( ) ;
172+ }
173+ }
174+ seen_substantive_output = true ;
160175 result. push ( line. to_string ( ) ) ;
161176 }
162177
178+ if !seen_substantive_output && leading_gt_lines. len ( ) == 1 {
179+ result. extend ( leading_gt_lines) ;
180+ }
181+
163182 if result. is_empty ( ) {
164183 "ok" . to_string ( )
165184 } else {
@@ -234,4 +253,46 @@ npm notice
234253 let result = filter_npm_output ( output) ;
235254 assert_eq ! ( result, "ok" ) ;
236255 }
256+
257+ #[ test]
258+ fn test_filter_npm_output_lifecycle_only_success ( ) {
259+ let output = r#"
260+ > typecheck
261+ > tsc --noEmit
262+ "# ;
263+ let result = filter_npm_output ( output) ;
264+ assert_eq ! ( result, "ok" ) ;
265+ }
266+
267+ #[ test]
268+ fn test_filter_npm_output_preserves_substantive_output ( ) {
269+ let output = r#"
270+ > test
271+ > node test.js
272+ TEST_PASS
273+ "# ;
274+ let result = filter_npm_output ( output) ;
275+ assert_eq ! ( result, "TEST_PASS" ) ;
276+ }
277+
278+ #[ test]
279+ fn test_filter_npm_output_preserves_gt_after_substantive_output ( ) {
280+ let output = r#"
281+ > test
282+ > node test.js
283+ error:
284+ > pointer context
285+ "# ;
286+ let result = filter_npm_output ( output) ;
287+ assert_eq ! ( result, "error:\n > pointer context" ) ;
288+ }
289+
290+ #[ test]
291+ fn test_filter_npm_output_preserves_single_leading_gt_output ( ) {
292+ let output = r#"
293+ > user output
294+ "# ;
295+ let result = filter_npm_output ( output) ;
296+ assert_eq ! ( result, "> user output" ) ;
297+ }
237298}
0 commit comments