1+ import contextlib
2+ import itertools
13import json
4+ import sys
5+ import threading
6+ import time
27
38import click
49import requests
@@ -186,6 +191,41 @@ def check(dev):
186191 utils .echo_info ("\U00002714 No vulnerabilities found." )
187192
188193
194+ @contextlib .contextmanager
195+ def _spinner (message ):
196+ """Show an animated spinner next to ``message`` until the block exits.
197+
198+ No-op (prints the message once) when stdout is not a TTY so CI logs
199+ stay clean.
200+ """
201+ styled = click .style (message , fg = "cyan" )
202+ if not sys .stdout .isatty ():
203+ click .echo (styled )
204+ yield
205+ return
206+
207+ done = threading .Event ()
208+
209+ def spin ():
210+ for ch in itertools .cycle ("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" ):
211+ if done .is_set ():
212+ break
213+ sys .stdout .write (f"\r { styled } { click .style (ch , fg = 'cyan' )} " )
214+ sys .stdout .flush ()
215+ time .sleep (0.1 )
216+
217+ t = threading .Thread (target = spin , daemon = True )
218+ t .start ()
219+ try :
220+ yield
221+ finally :
222+ done .set ()
223+ t .join ()
224+ # Leave the message in place; swap the spinner glyph for a check mark.
225+ sys .stdout .write (f"\r { styled } { click .style ('✓' , fg = 'cyan' )} \n " )
226+ sys .stdout .flush ()
227+
228+
189229def do_checks (dev = True ):
190230 """Run Python (pip-audit) and npm (npm audit) vulnerability scans.
191231
@@ -204,11 +244,12 @@ def do_checks(dev=True):
204244
205245 # --- Python vulnerability check via pip-audit (PyPA tool, OSV data) ---
206246 try :
207- result = subprocess .run (
208- ["uvx" , "pip-audit" , "--format" , "json" , "--vulnerability-service" , "osv" ],
209- capture_output = True ,
210- text = True ,
211- )
247+ with _spinner ("Scanning Python dependencies (pip-audit)" ):
248+ result = subprocess .run (
249+ ["uvx" , "pip-audit" , "--format" , "json" , "--vulnerability-service" , "osv" ],
250+ capture_output = True ,
251+ text = True ,
252+ )
212253 except FileNotFoundError :
213254 echo_warning ("uvx not found; skipping Python vulnerability check." )
214255 else :
@@ -229,7 +270,7 @@ def do_checks(dev=True):
229270 vuln_deps = [d for d in deps if d .get ("vulns" )]
230271 total_vulns = sum (len (d ["vulns" ]) for d in vuln_deps )
231272
232- click .echo ("\n " + "=" * 60 )
273+ click .echo ("\n \n " + "=" * 60 )
233274 click .echo ("Python Security Scan Results (pip-audit, OSV)" )
234275 click .echo ("=" * 60 )
235276 click .echo (f"Total Packages: { len (deps )} " )
@@ -240,6 +281,7 @@ def do_checks(dev=True):
240281 if vuln_deps :
241282 has_vulnerabilities = True
242283 click .echo ("Run `uvx pip-audit --vulnerability-service osv` for per-package detail." )
284+ click .echo ()
243285
244286 # --- npm vulnerability check ---
245287 if shutil .which ("npm" ):
@@ -258,7 +300,7 @@ def do_checks(dev=True):
258300 npm_builds .append ({"name" : build_name , "path" : audit_path })
259301
260302 if not npm_builds :
261- click .echo ("\n " + "=" * 60 )
303+ click .echo ("\n \n " + "=" * 60 )
262304 click .echo ("No npm builds found in config - skipping npm audit" )
263305 click .echo ("=" * 60 )
264306 else :
@@ -267,12 +309,13 @@ def do_checks(dev=True):
267309 try :
268310 npm_audit_dir = build ["path" ]
269311
270- npm_result = subprocess .run (
271- ["npm" , "audit" , "--json" ],
272- capture_output = True ,
273- text = True ,
274- cwd = npm_audit_dir ,
275- )
312+ with _spinner (f"Scanning npm dependencies ({ build ['name' ]} )" ):
313+ npm_result = subprocess .run (
314+ ["npm" , "audit" , "--json" ],
315+ capture_output = True ,
316+ text = True ,
317+ cwd = npm_audit_dir ,
318+ )
276319
277320 npm_data = json .loads (npm_result .stdout , strict = False )
278321
@@ -288,7 +331,7 @@ def do_checks(dev=True):
288331 has_vulnerabilities = True
289332
290333 # Display npm scan summary
291- click .echo ("\n " + "=" * 60 )
334+ click .echo ("\n \n " + "=" * 60 )
292335 click .echo (f"npm Security Scan Results - { build ['name' ]} " )
293336 click .echo ("=" * 60 )
294337 click .echo (f"Build Path: { npm_audit_dir } " )
@@ -309,6 +352,7 @@ def do_checks(dev=True):
309352
310353 if total_npm_vulns > 0 :
311354 click .echo (f"Run `npm audit` in { build ['path' ]} for per-package detail." )
355+ click .echo ()
312356
313357 except FileNotFoundError :
314358 echo_warning (f"npm audit directory not found: { build ['path' ]} " )
@@ -321,7 +365,7 @@ def do_checks(dev=True):
321365 f"Unexpected error during npm security check for { build ['name' ]} : { e } "
322366 )
323367 else :
324- click .echo ("\n " + "=" * 60 )
368+ click .echo ("\n \n " + "=" * 60 )
325369 click .echo ("npm not found - skipping npm audit" )
326370 click .echo ("=" * 60 )
327371
0 commit comments