@@ -3,9 +3,6 @@ import { PackageDetector } from './package-detector'
33import { InteractiveUI } from '../interactive-ui'
44import { PackageUpgrader } from './upgrader'
55import {
6- HeadlessOptions ,
7- HeadlessReport ,
8- HeadlessReportEntry ,
96 PackageInfo ,
107 PackageLoadProgress ,
118 PackageSelectionState ,
@@ -14,8 +11,6 @@ import {
1411 PackageManagerInfo ,
1512} from '../types'
1613import { PackageManagerDetector } from '../services/package-manager-detector'
17- import { fetchVulnerabilities } from '../services'
18- import { createVulnerabilitySummary } from '../ui/presenters/vulnerability'
1914import { ConsoleUtils } from '../ui/utils'
2015import { getPerformanceTracker } from '../features/debug'
2116
@@ -189,124 +184,6 @@ export class UpgradeRunner {
189184 }
190185 }
191186
192- /**
193- * Non-interactive entry point: resolve the outdated list without rendering the
194- * TUI, then either emit a JSON report (--json) or a plain line-based report.
195- * With --check, sets a non-zero exit code when updates exist so CI can gate on it.
196- * Read-only: never writes package.json or installs.
197- */
198- public async runHeadless ( options : HeadlessOptions ) : Promise < void > {
199- try {
200- this . checkPrerequisites ( )
201-
202- const packages = await this . detector . getOutdatedPackages ( )
203- const outdated = this . detector . getOutdatedPackagesOnly ( packages )
204-
205- // Enrich with security advisories (one bulk request, best-effort) — the same audit the
206- // TUI runs on `s`, so --json/--check carry the vuln signal CI cares about.
207- await this . enrichWithVulnerabilities ( outdated )
208-
209- if ( options . json ) {
210- // stdout is reserved for the JSON document only.
211- console . log ( JSON . stringify ( this . buildHeadlessReport ( packages , outdated ) , null , 2 ) )
212- } else {
213- this . printPlainReport ( outdated )
214- }
215-
216- // Exit 1 only means "updates exist" (like `prettier --check`); 2 is reserved for errors.
217- if ( options . check && outdated . length > 0 ) {
218- process . exitCode = 1
219- }
220- } catch ( error ) {
221- console . error ( chalk . red ( `Error: ${ error instanceof Error ? error . message : String ( error ) } ` ) )
222- process . exit ( 2 )
223- }
224- }
225-
226- private buildHeadlessReport ( all : PackageInfo [ ] , outdated : PackageInfo [ ] ) : HeadlessReport {
227- return {
228- summary : {
229- total : all . length ,
230- outdated : outdated . length ,
231- major : outdated . filter ( ( pkg ) => pkg . hasMajorUpdate ) . length ,
232- vulnerable : outdated . filter ( ( pkg ) => ( pkg . vulnerability ?. count ?? 0 ) > 0 ) . length ,
233- } ,
234- outdated : outdated . map ( ( pkg ) => {
235- const entry : HeadlessReportEntry = {
236- name : pkg . name ,
237- current : pkg . currentVersion ,
238- range : pkg . rangeVersion ,
239- latest : pkg . latestVersion ,
240- type : pkg . type ,
241- packageJsonPath : pkg . packageJsonPath ,
242- hasMajorUpdate : pkg . hasMajorUpdate ,
243- }
244- if ( pkg . deprecated ) entry . deprecated = pkg . deprecated
245- if ( pkg . enginesNode ) entry . enginesNode = pkg . enginesNode
246- if ( pkg . vulnerability ) entry . vulnerability = pkg . vulnerability
247- return entry
248- } ) ,
249- }
250- }
251-
252- /**
253- * Attach known security advisories to the outdated packages in place. Audits each package's
254- * currently-installed specifier (matching the interactive audit) via one bulk request.
255- * Best-effort: `fetchVulnerabilities` swallows network errors and returns an empty map, so a
256- * failed audit never blocks the report.
257- */
258- private async enrichWithVulnerabilities ( outdated : PackageInfo [ ] ) : Promise < void > {
259- if ( outdated . length === 0 ) return
260-
261- // The bulk advisory API is keyed by package name (one version per name), so dedupe by name.
262- const versions = new Map < string , string > ( )
263- for ( const pkg of outdated ) {
264- if ( ! versions . has ( pkg . name ) ) versions . set ( pkg . name , pkg . currentVersion )
265- }
266-
267- const advisories = await fetchVulnerabilities ( versions )
268- if ( advisories . size === 0 ) return
269-
270- for ( const pkg of outdated ) {
271- const found = advisories . get ( pkg . name )
272- if ( ! found || found . vulnerabilities . length === 0 || ! found . highestSeverity ) continue
273- pkg . vulnerability = createVulnerabilitySummary (
274- undefined ,
275- found . vulnerabilities . map ( ( item ) => ( {
276- id : item . id ,
277- title : item . title ,
278- severity : item . severity ,
279- url : item . url ,
280- } ) ) ,
281- found . highestSeverity
282- )
283- }
284- }
285-
286- private printPlainReport ( outdated : PackageInfo [ ] ) : void {
287- if ( outdated . length === 0 ) {
288- console . log ( 'All dependencies are up to date — no upgrades needed.' )
289- return
290- }
291-
292- for ( const pkg of outdated ) {
293- const major = pkg . hasMajorUpdate ? ' (major)' : ''
294- const vuln =
295- pkg . vulnerability && pkg . vulnerability . count > 0
296- ? ` [vuln: ${ pkg . vulnerability . count } ${ pkg . vulnerability . highestSeverity } ]`
297- : ''
298- const deprecated = pkg . deprecated ? ' [deprecated]' : ''
299- console . log (
300- `${ pkg . name } ${ pkg . currentVersion } → ${ pkg . latestVersion } [${ pkg . type } ]${ major } ${ vuln } ${ deprecated } `
301- )
302- }
303-
304- const fileCount = new Set ( outdated . map ( ( pkg ) => pkg . packageJsonPath ) ) . size
305- const vulnerable = outdated . filter ( ( pkg ) => ( pkg . vulnerability ?. count ?? 0 ) > 0 ) . length
306- const vulnNote = vulnerable > 0 ? ` — ${ vulnerable } with known vulnerabilities` : ''
307- console . log ( `\n${ outdated . length } package(s) outdated across ${ fileCount } file(s)${ vulnNote } .` )
308- }
309-
310187 private checkPrerequisites ( ) : void {
311188 // Check if package.json exists
312189 if ( ! this . detector . hasPackageJson ( ) ) {
0 commit comments