@@ -39,7 +39,14 @@ import {
3939 getNonUniversalAgents ,
4040 isUniversalAgent ,
4141} from './agents.ts' ;
42- import { track , setVersion } from './telemetry.ts' ;
42+ import {
43+ track ,
44+ setVersion ,
45+ fetchAuditData ,
46+ type AuditResponse ,
47+ type SkillAuditData ,
48+ type PartnerAudit ,
49+ } from './telemetry.ts' ;
4350import { findProvider , wellKnownProvider , type WellKnownSkill } from './providers/index.ts' ;
4451import { fetchMintlifySkill } from './mintlify.ts' ;
4552import {
@@ -56,6 +63,91 @@ export function initTelemetry(version: string): void {
5663 setVersion ( version ) ;
5764}
5865
66+ // ─── Security Advisory ───
67+
68+ function riskLabel ( risk : string ) : string {
69+ switch ( risk ) {
70+ case 'critical' :
71+ return pc . red ( pc . bold ( 'Critical Risk' ) ) ;
72+ case 'high' :
73+ return pc . red ( 'High Risk' ) ;
74+ case 'medium' :
75+ return pc . yellow ( 'Med Risk' ) ;
76+ case 'low' :
77+ return pc . green ( 'Low Risk' ) ;
78+ case 'safe' :
79+ return pc . green ( 'Safe' ) ;
80+ default :
81+ return pc . dim ( '--' ) ;
82+ }
83+ }
84+
85+ function socketLabel ( audit : PartnerAudit | undefined ) : string {
86+ if ( ! audit ) return pc . dim ( '--' ) ;
87+ const count = audit . alerts ?? 0 ;
88+ return count > 0 ? pc . red ( `${ count } alert${ count !== 1 ? 's' : '' } ` ) : pc . green ( '0 alerts' ) ;
89+ }
90+
91+ /** Pad a string to a given visible width (ignoring ANSI escape codes). */
92+ function padEnd ( str : string , width : number ) : string {
93+ // Strip ANSI codes to measure visible length
94+ const visible = str . replace ( / \x1b \[ [ 0 - 9 ; ] * m / g, '' ) ;
95+ const pad = Math . max ( 0 , width - visible . length ) ;
96+ return str + ' ' . repeat ( pad ) ;
97+ }
98+
99+ /**
100+ * Render a compact security table showing partner audit results.
101+ * Returns the lines to display, or empty array if no data.
102+ */
103+ function buildSecurityLines (
104+ auditData : AuditResponse | null ,
105+ skills : Array < { slug : string ; displayName : string } > ,
106+ source : string
107+ ) : string [ ] {
108+ if ( ! auditData ) return [ ] ;
109+
110+ // Check if we have any audit data at all
111+ const hasAny = skills . some ( ( s ) => {
112+ const data = auditData [ s . slug ] ;
113+ return data && Object . keys ( data ) . length > 0 ;
114+ } ) ;
115+ if ( ! hasAny ) return [ ] ;
116+
117+ // Compute column width for skill names
118+ const nameWidth = Math . min ( Math . max ( ...skills . map ( ( s ) => s . displayName . length ) ) , 36 ) ;
119+
120+ // Header
121+ const lines : string [ ] = [ ] ;
122+ const header =
123+ padEnd ( '' , nameWidth + 2 ) +
124+ padEnd ( pc . dim ( 'Gen' ) , 18 ) +
125+ padEnd ( pc . dim ( 'Socket' ) , 18 ) +
126+ pc . dim ( 'Snyk' ) ;
127+ lines . push ( header ) ;
128+
129+ // Rows
130+ for ( const skill of skills ) {
131+ const data = auditData [ skill . slug ] ;
132+ const name =
133+ skill . displayName . length > nameWidth
134+ ? skill . displayName . slice ( 0 , nameWidth - 1 ) + '\u2026'
135+ : skill . displayName ;
136+
137+ const ath = data ?. ath ? riskLabel ( data . ath . risk ) : pc . dim ( '--' ) ;
138+ const socket = data ?. socket ? socketLabel ( data . socket ) : pc . dim ( '--' ) ;
139+ const snyk = data ?. snyk ? riskLabel ( data . snyk . risk ) : pc . dim ( '--' ) ;
140+
141+ lines . push ( padEnd ( pc . cyan ( name ) , nameWidth + 2 ) + padEnd ( ath , 18 ) + padEnd ( socket , 18 ) + snyk ) ;
142+ }
143+
144+ // Footer link
145+ lines . push ( '' ) ;
146+ lines . push ( `${ pc . dim ( 'Details:' ) } ${ pc . dim ( `https://skills.sh/${ source } ` ) } ` ) ;
147+
148+ return lines ;
149+ }
150+
59151/**
60152 * Shortens a path for display: replaces homedir with ~ and cwd with .
61153 * Handles both Unix and Windows path separators.
@@ -1586,6 +1678,16 @@ export async function runAdd(args: string[], options: AddOptions = {}): Promise<
15861678 selectedSkills = selected as Skill [ ] ;
15871679 }
15881680
1681+ // Kick off security audit fetch early (non-blocking) so it runs
1682+ // in parallel with agent selection, scope, and mode prompts.
1683+ const ownerRepoForAudit = getOwnerRepo ( parsed ) ;
1684+ const auditPromise = ownerRepoForAudit
1685+ ? fetchAuditData (
1686+ ownerRepoForAudit ,
1687+ selectedSkills . map ( ( s ) => getSkillDisplayName ( s ) )
1688+ )
1689+ : Promise . resolve ( null ) ;
1690+
15891691 let targetAgents : AgentType [ ] ;
15901692 const validAgents = Object . keys ( agents ) ;
15911693
@@ -1760,6 +1862,27 @@ export async function runAdd(args: string[], options: AddOptions = {}): Promise<
17601862 console . log ( ) ;
17611863 p . note ( summaryLines . join ( '\n' ) , 'Installation Summary' ) ;
17621864
1865+ // Await and display security audit results (started earlier in parallel)
1866+ // Wrapped in try/catch so a failed audit fetch never blocks installation.
1867+ try {
1868+ const auditData = await auditPromise ;
1869+ if ( auditData && ownerRepoForAudit ) {
1870+ const securityLines = buildSecurityLines (
1871+ auditData ,
1872+ selectedSkills . map ( ( s ) => ( {
1873+ slug : getSkillDisplayName ( s ) ,
1874+ displayName : getSkillDisplayName ( s ) ,
1875+ } ) ) ,
1876+ ownerRepoForAudit
1877+ ) ;
1878+ if ( securityLines . length > 0 ) {
1879+ p . note ( securityLines . join ( '\n' ) , 'Security Risk Assessments' ) ;
1880+ }
1881+ }
1882+ } catch {
1883+ // Silently skip — security info is advisory only
1884+ }
1885+
17631886 if ( ! options . yes ) {
17641887 const confirmed = await p . confirm ( { message : 'Proceed with installation?' } ) ;
17651888
0 commit comments