pnpm-catalog-updates 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +22031 -10684
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
  6. package/src/cli/commandRegistrar.ts +785 -0
  7. package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
  8. package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
  9. package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
  10. package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
  11. package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
  12. package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
  13. package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
  14. package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
  15. package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
  16. package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
  17. package/src/cli/commands/aiCommand.ts +163 -0
  18. package/src/cli/commands/analyzeCommand.ts +219 -0
  19. package/src/cli/commands/checkCommand.ts +91 -98
  20. package/src/cli/commands/graphCommand.ts +475 -0
  21. package/src/cli/commands/initCommand.ts +64 -54
  22. package/src/cli/commands/rollbackCommand.ts +334 -0
  23. package/src/cli/commands/securityCommand.ts +165 -100
  24. package/src/cli/commands/themeCommand.ts +148 -0
  25. package/src/cli/commands/updateCommand.ts +215 -263
  26. package/src/cli/commands/workspaceCommand.ts +73 -0
  27. package/src/cli/constants/cliChoices.ts +93 -0
  28. package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
  29. package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
  30. package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
  31. package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
  32. package/src/cli/formatters/ciFormatter.ts +964 -0
  33. package/src/cli/formatters/colorUtils.ts +145 -0
  34. package/src/cli/formatters/outputFormatter.ts +615 -332
  35. package/src/cli/formatters/progressBar.ts +43 -52
  36. package/src/cli/formatters/versionFormatter.ts +132 -0
  37. package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
  38. package/src/cli/handlers/changelogHandler.ts +113 -0
  39. package/src/cli/handlers/index.ts +9 -0
  40. package/src/cli/handlers/installHandler.ts +130 -0
  41. package/src/cli/index.ts +175 -726
  42. package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
  43. package/src/cli/interactive/interactivePrompts.ts +189 -83
  44. package/src/cli/interactive/optionUtils.ts +89 -0
  45. package/src/cli/themes/colorTheme.ts +43 -16
  46. package/src/cli/utils/cliOutput.ts +118 -0
  47. package/src/cli/utils/commandHelpers.ts +249 -0
  48. package/src/cli/validators/commandValidator.ts +321 -336
  49. package/src/cli/validators/index.ts +37 -2
  50. package/src/cli/options/globalOptions.ts +0 -437
  51. package/src/cli/options/index.ts +0 -5
@@ -0,0 +1,964 @@
1
+ /**
2
+ * CI/CD Output Formatter
3
+ *
4
+ * Provides formatted output specifically designed for CI/CD pipelines.
5
+ * Supports GitHub Actions, GitLab CI, JUnit XML, and SARIF formats.
6
+ */
7
+
8
+ import type { OutdatedReport, UpdatePlan, UpdateResult, WorkspaceValidationReport } from '@pcu/core'
9
+ import { t } from '@pcu/utils'
10
+ import type { SecurityReport } from '../commands/securityCommand.js'
11
+
12
+ export type CIOutputFormat = 'github' | 'gitlab' | 'junit' | 'sarif'
13
+
14
+ /**
15
+ * GitHub Actions annotation levels
16
+ */
17
+ type GitHubAnnotationLevel = 'error' | 'warning' | 'notice'
18
+
19
+ /**
20
+ * SARIF severity levels
21
+ */
22
+ type SARIFLevel = 'error' | 'warning' | 'note' | 'none'
23
+
24
+ export class CIFormatter {
25
+ constructor(private readonly format: CIOutputFormat) {}
26
+
27
+ /**
28
+ * Format outdated dependencies report for CI
29
+ */
30
+ formatOutdatedReport(report: OutdatedReport): string {
31
+ switch (this.format) {
32
+ case 'github':
33
+ return this.formatOutdatedGitHub(report)
34
+ case 'gitlab':
35
+ return this.formatOutdatedGitLab(report)
36
+ case 'junit':
37
+ return this.formatOutdatedJUnit(report)
38
+ case 'sarif':
39
+ return this.formatOutdatedSARIF(report)
40
+ default:
41
+ return JSON.stringify(report, null, 2)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Format update result for CI
47
+ */
48
+ formatUpdateResult(result: UpdateResult): string {
49
+ switch (this.format) {
50
+ case 'github':
51
+ return this.formatUpdateResultGitHub(result)
52
+ case 'gitlab':
53
+ return this.formatUpdateResultGitLab(result)
54
+ case 'junit':
55
+ return this.formatUpdateResultJUnit(result)
56
+ case 'sarif':
57
+ return this.formatUpdateResultSARIF(result)
58
+ default:
59
+ return JSON.stringify(result, null, 2)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Format update plan for CI
65
+ */
66
+ formatUpdatePlan(plan: UpdatePlan): string {
67
+ switch (this.format) {
68
+ case 'github':
69
+ return this.formatUpdatePlanGitHub(plan)
70
+ case 'gitlab':
71
+ return this.formatUpdatePlanGitLab(plan)
72
+ case 'junit':
73
+ return this.formatUpdatePlanJUnit(plan)
74
+ case 'sarif':
75
+ return this.formatUpdatePlanSARIF(plan)
76
+ default:
77
+ return JSON.stringify(plan, null, 2)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Format validation report for CI
83
+ */
84
+ formatValidationReport(report: WorkspaceValidationReport): string {
85
+ switch (this.format) {
86
+ case 'github':
87
+ return this.formatValidationGitHub(report)
88
+ case 'gitlab':
89
+ return this.formatValidationGitLab(report)
90
+ case 'junit':
91
+ return this.formatValidationJUnit(report)
92
+ case 'sarif':
93
+ return this.formatValidationSARIF(report)
94
+ default:
95
+ return JSON.stringify(report, null, 2)
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Format security report for CI
101
+ */
102
+ formatSecurityReport(report: SecurityReport): string {
103
+ switch (this.format) {
104
+ case 'github':
105
+ return this.formatSecurityGitHub(report)
106
+ case 'gitlab':
107
+ return this.formatSecurityGitLab(report)
108
+ case 'junit':
109
+ return this.formatSecurityJUnit(report)
110
+ case 'sarif':
111
+ return this.formatSecuritySARIF(report)
112
+ default:
113
+ return JSON.stringify(report, null, 2)
114
+ }
115
+ }
116
+
117
+ // ==================== GitHub Actions Format ====================
118
+
119
+ /**
120
+ * Create GitHub Actions annotation
121
+ * @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
122
+ */
123
+ private createGitHubAnnotation(
124
+ level: GitHubAnnotationLevel,
125
+ message: string,
126
+ options?: {
127
+ file?: string
128
+ line?: number
129
+ endLine?: number
130
+ col?: number
131
+ endColumn?: number
132
+ title?: string
133
+ }
134
+ ): string {
135
+ const params: string[] = []
136
+ if (options?.file) params.push(`file=${options.file}`)
137
+ if (options?.line) params.push(`line=${options.line}`)
138
+ if (options?.endLine) params.push(`endLine=${options.endLine}`)
139
+ if (options?.col) params.push(`col=${options.col}`)
140
+ if (options?.endColumn) params.push(`endColumn=${options.endColumn}`)
141
+ if (options?.title) params.push(`title=${options.title}`)
142
+
143
+ const paramStr = params.length > 0 ? ` ${params.join(',')}` : ''
144
+ return `::${level}${paramStr}::${message.replace(/\n/g, '%0A')}`
145
+ }
146
+
147
+ /**
148
+ * Create GitHub Actions group
149
+ */
150
+ private createGitHubGroup(name: string, content: string): string {
151
+ return `::group::${name}\n${content}\n::endgroup::`
152
+ }
153
+
154
+ private formatOutdatedGitHub(report: OutdatedReport): string {
155
+ const lines: string[] = []
156
+
157
+ if (!report.hasUpdates) {
158
+ lines.push(
159
+ this.createGitHubAnnotation('notice', t('format.allUpToDate'), { title: 'pcu check' })
160
+ )
161
+ return lines.join('\n')
162
+ }
163
+
164
+ // Summary notice
165
+ lines.push(
166
+ this.createGitHubAnnotation(
167
+ 'warning',
168
+ t('format.foundOutdated', { count: String(report.totalOutdated) }),
169
+ { title: 'pcu check' }
170
+ )
171
+ )
172
+
173
+ // Group outdated dependencies
174
+ const depLines: string[] = []
175
+ for (const catalog of report.catalogs) {
176
+ for (const dep of catalog.outdatedDependencies) {
177
+ const level: GitHubAnnotationLevel = dep.updateType === 'major' ? 'warning' : 'notice'
178
+ const securityNote = dep.isSecurityUpdate ? ' [SECURITY]' : ''
179
+ depLines.push(
180
+ this.createGitHubAnnotation(
181
+ level,
182
+ `${dep.packageName}: ${dep.currentVersion} → ${dep.latestVersion} (${dep.updateType})${securityNote}`,
183
+ {
184
+ file: 'pnpm-workspace.yaml',
185
+ title: `Outdated: ${dep.packageName}`,
186
+ }
187
+ )
188
+ )
189
+ }
190
+ }
191
+ lines.push(this.createGitHubGroup('Outdated Dependencies', depLines.join('\n')))
192
+
193
+ return lines.join('\n')
194
+ }
195
+
196
+ private formatUpdateResultGitHub(result: UpdateResult): string {
197
+ const lines: string[] = []
198
+
199
+ if (result.success) {
200
+ lines.push(
201
+ this.createGitHubAnnotation(
202
+ 'notice',
203
+ t('format.updatedCount', { count: String(result.totalUpdated) }),
204
+ { title: 'pcu update' }
205
+ )
206
+ )
207
+ } else {
208
+ lines.push(
209
+ this.createGitHubAnnotation(
210
+ 'error',
211
+ t('format.errorCount', { count: String(result.totalErrors) }),
212
+ { title: 'pcu update' }
213
+ )
214
+ )
215
+ }
216
+
217
+ // Updated dependencies
218
+ if (result.updatedDependencies.length > 0) {
219
+ const updateLines = result.updatedDependencies.map(
220
+ (dep) => `${dep.packageName}: ${dep.fromVersion} → ${dep.toVersion}`
221
+ )
222
+ lines.push(this.createGitHubGroup('Updated Dependencies', updateLines.join('\n')))
223
+ }
224
+
225
+ // Errors
226
+ for (const error of result.errors) {
227
+ lines.push(
228
+ this.createGitHubAnnotation('error', `${error.packageName}: ${error.error}`, {
229
+ file: 'pnpm-workspace.yaml',
230
+ title: 'Update Error',
231
+ })
232
+ )
233
+ }
234
+
235
+ return lines.join('\n')
236
+ }
237
+
238
+ private formatUpdatePlanGitHub(plan: UpdatePlan): string {
239
+ const lines: string[] = []
240
+
241
+ if (plan.totalUpdates === 0) {
242
+ lines.push(
243
+ this.createGitHubAnnotation('notice', t('format.noUpdatesPlanned'), {
244
+ title: 'pcu update --dry-run',
245
+ })
246
+ )
247
+ return lines.join('\n')
248
+ }
249
+
250
+ lines.push(
251
+ this.createGitHubAnnotation(
252
+ 'notice',
253
+ t('format.plannedUpdates', { count: String(plan.totalUpdates) }),
254
+ { title: 'pcu update --dry-run' }
255
+ )
256
+ )
257
+
258
+ // Planned updates
259
+ const updateLines = plan.updates.map(
260
+ (update) =>
261
+ `${update.packageName}: ${update.currentVersion} → ${update.newVersion} (${update.updateType})`
262
+ )
263
+ lines.push(this.createGitHubGroup('Planned Updates', updateLines.join('\n')))
264
+
265
+ // Conflicts
266
+ if (plan.hasConflicts && plan.conflicts.length > 0) {
267
+ for (const conflict of plan.conflicts) {
268
+ lines.push(
269
+ this.createGitHubAnnotation('warning', `Version conflict: ${conflict.packageName}`, {
270
+ file: 'pnpm-workspace.yaml',
271
+ title: 'Version Conflict',
272
+ })
273
+ )
274
+ }
275
+ }
276
+
277
+ return lines.join('\n')
278
+ }
279
+
280
+ private formatValidationGitHub(report: WorkspaceValidationReport): string {
281
+ const lines: string[] = []
282
+
283
+ if (report.isValid) {
284
+ lines.push(
285
+ this.createGitHubAnnotation(
286
+ 'notice',
287
+ `${t('format.workspaceValidation')}: ${t('format.valid')}`,
288
+ {
289
+ title: 'pcu workspace --validate',
290
+ }
291
+ )
292
+ )
293
+ } else {
294
+ lines.push(
295
+ this.createGitHubAnnotation(
296
+ 'error',
297
+ `${t('format.workspaceValidation')}: ${t('format.invalid')}`,
298
+ {
299
+ title: 'pcu workspace --validate',
300
+ }
301
+ )
302
+ )
303
+ }
304
+
305
+ for (const error of report.errors) {
306
+ lines.push(this.createGitHubAnnotation('error', error, { title: 'Validation Error' }))
307
+ }
308
+
309
+ for (const warning of report.warnings) {
310
+ lines.push(this.createGitHubAnnotation('warning', warning, { title: 'Validation Warning' }))
311
+ }
312
+
313
+ return lines.join('\n')
314
+ }
315
+
316
+ private formatSecurityGitHub(report: SecurityReport): string {
317
+ const lines: string[] = []
318
+
319
+ const total = report.summary.totalVulnerabilities
320
+ if (total === 0) {
321
+ lines.push(
322
+ this.createGitHubAnnotation('notice', t('format.noVulnsFound'), { title: 'pcu security' })
323
+ )
324
+ return lines.join('\n')
325
+ }
326
+
327
+ // Summary
328
+ const level: GitHubAnnotationLevel =
329
+ report.summary.critical > 0 || report.summary.high > 0 ? 'error' : 'warning'
330
+ lines.push(
331
+ this.createGitHubAnnotation(
332
+ level,
333
+ `Found ${total} vulnerabilities (${report.summary.critical} critical, ${report.summary.high} high)`,
334
+ { title: 'pcu security' }
335
+ )
336
+ )
337
+
338
+ // Individual vulnerabilities
339
+ for (const vuln of report.vulnerabilities) {
340
+ const vulnLevel: GitHubAnnotationLevel =
341
+ vuln.severity === 'critical' || vuln.severity === 'high' ? 'error' : 'warning'
342
+ lines.push(
343
+ this.createGitHubAnnotation(vulnLevel, `${vuln.package}: ${vuln.title}`, {
344
+ title: `${vuln.severity.toUpperCase()}: ${vuln.package}`,
345
+ })
346
+ )
347
+ }
348
+
349
+ return lines.join('\n')
350
+ }
351
+
352
+ // ==================== GitLab CI Format ====================
353
+
354
+ private formatOutdatedGitLab(report: OutdatedReport): string {
355
+ // GitLab CI uses code quality report format for dependency issues
356
+ const issues = []
357
+
358
+ for (const catalog of report.catalogs) {
359
+ for (const dep of catalog.outdatedDependencies) {
360
+ issues.push({
361
+ description: `${dep.packageName} is outdated: ${dep.currentVersion} → ${dep.latestVersion}`,
362
+ check_name: 'outdated-dependency',
363
+ fingerprint: `outdated-${catalog.catalogName}-${dep.packageName}`,
364
+ severity:
365
+ dep.updateType === 'major' ? 'major' : dep.updateType === 'minor' ? 'minor' : 'info',
366
+ location: {
367
+ path: 'pnpm-workspace.yaml',
368
+ lines: { begin: 1 },
369
+ },
370
+ categories: ['Dependency'],
371
+ })
372
+ }
373
+ }
374
+
375
+ return JSON.stringify(issues, null, 2)
376
+ }
377
+
378
+ private formatUpdateResultGitLab(result: UpdateResult): string {
379
+ const issues = []
380
+
381
+ for (const error of result.errors) {
382
+ issues.push({
383
+ description: `Update failed for ${error.packageName}: ${error.error}`,
384
+ check_name: 'update-error',
385
+ fingerprint: `update-error-${error.catalogName}-${error.packageName}`,
386
+ severity: error.fatal ? 'critical' : 'major',
387
+ location: {
388
+ path: 'pnpm-workspace.yaml',
389
+ lines: { begin: 1 },
390
+ },
391
+ categories: ['Dependency Update'],
392
+ })
393
+ }
394
+
395
+ return JSON.stringify(issues, null, 2)
396
+ }
397
+
398
+ private formatUpdatePlanGitLab(plan: UpdatePlan): string {
399
+ const issues = []
400
+
401
+ for (const conflict of plan.conflicts) {
402
+ issues.push({
403
+ description: `Version conflict for ${conflict.packageName}`,
404
+ check_name: 'version-conflict',
405
+ fingerprint: `conflict-${conflict.packageName}`,
406
+ severity: 'major',
407
+ location: {
408
+ path: 'pnpm-workspace.yaml',
409
+ lines: { begin: 1 },
410
+ },
411
+ categories: ['Dependency'],
412
+ })
413
+ }
414
+
415
+ return JSON.stringify(issues, null, 2)
416
+ }
417
+
418
+ private formatValidationGitLab(report: WorkspaceValidationReport): string {
419
+ const issues = []
420
+
421
+ for (const error of report.errors) {
422
+ issues.push({
423
+ description: error,
424
+ check_name: 'workspace-validation',
425
+ fingerprint: `validation-error-${Buffer.from(error).toString('base64').slice(0, 20)}`,
426
+ severity: 'critical',
427
+ location: {
428
+ path: 'pnpm-workspace.yaml',
429
+ lines: { begin: 1 },
430
+ },
431
+ categories: ['Workspace'],
432
+ })
433
+ }
434
+
435
+ for (const warning of report.warnings) {
436
+ issues.push({
437
+ description: warning,
438
+ check_name: 'workspace-validation',
439
+ fingerprint: `validation-warning-${Buffer.from(warning).toString('base64').slice(0, 20)}`,
440
+ severity: 'minor',
441
+ location: {
442
+ path: 'pnpm-workspace.yaml',
443
+ lines: { begin: 1 },
444
+ },
445
+ categories: ['Workspace'],
446
+ })
447
+ }
448
+
449
+ return JSON.stringify(issues, null, 2)
450
+ }
451
+
452
+ private formatSecurityGitLab(report: SecurityReport): string {
453
+ // GitLab Security Report format
454
+ const securityReport = {
455
+ version: '15.0.0',
456
+ vulnerabilities: report.vulnerabilities.map((vuln) => ({
457
+ id: vuln.id || `vuln-${vuln.package}-${Date.now()}`,
458
+ category: 'dependency_scanning',
459
+ name: vuln.title,
460
+ message: vuln.title,
461
+ description: vuln.overview || vuln.title,
462
+ severity: this.mapSeverityToGitLab(vuln.severity),
463
+ solution: vuln.fixAvailable
464
+ ? typeof vuln.fixAvailable === 'string'
465
+ ? `Update to ${vuln.fixAvailable}`
466
+ : 'Update available'
467
+ : 'No fix available',
468
+ scanner: {
469
+ id: 'pcu-security',
470
+ name: 'PCU Security Scanner',
471
+ },
472
+ location: {
473
+ file: 'pnpm-workspace.yaml',
474
+ dependency: {
475
+ package: { name: vuln.package },
476
+ version: vuln.installedVersion || 'unknown',
477
+ },
478
+ },
479
+ identifiers: vuln.cwe
480
+ ? [
481
+ {
482
+ type: 'cwe',
483
+ name: `CWE-${vuln.cwe}`,
484
+ value: String(vuln.cwe),
485
+ },
486
+ ]
487
+ : [],
488
+ links: vuln.url ? [{ url: vuln.url }] : [],
489
+ })),
490
+ scan: {
491
+ scanner: {
492
+ id: 'pcu-security',
493
+ name: 'PCU Security Scanner',
494
+ version: '1.0.0',
495
+ vendor: { name: 'PCU' },
496
+ },
497
+ type: 'dependency_scanning',
498
+ start_time: report.metadata.scanDate,
499
+ end_time: new Date().toISOString(),
500
+ status: 'success',
501
+ },
502
+ }
503
+
504
+ return JSON.stringify(securityReport, null, 2)
505
+ }
506
+
507
+ private mapSeverityToGitLab(severity: string): string {
508
+ switch (severity.toLowerCase()) {
509
+ case 'critical':
510
+ return 'Critical'
511
+ case 'high':
512
+ return 'High'
513
+ case 'moderate':
514
+ case 'medium':
515
+ return 'Medium'
516
+ case 'low':
517
+ return 'Low'
518
+ default:
519
+ return 'Info'
520
+ }
521
+ }
522
+
523
+ // ==================== JUnit XML Format ====================
524
+
525
+ private escapeXml(str: string): string {
526
+ return str
527
+ .replace(/&/g, '&')
528
+ .replace(/</g, '&lt;')
529
+ .replace(/>/g, '&gt;')
530
+ .replace(/"/g, '&quot;')
531
+ .replace(/'/g, '&apos;')
532
+ }
533
+
534
+ private formatOutdatedJUnit(report: OutdatedReport): string {
535
+ const testcases: string[] = []
536
+ let failures = 0
537
+
538
+ for (const catalog of report.catalogs) {
539
+ for (const dep of catalog.outdatedDependencies) {
540
+ const isFailure = dep.updateType === 'major' || dep.isSecurityUpdate
541
+ if (isFailure) failures++
542
+
543
+ const failureElement = isFailure
544
+ ? `<failure message="${this.escapeXml(`${dep.packageName} needs ${dep.updateType} update`)}" type="OutdatedDependency">
545
+ Current: ${this.escapeXml(dep.currentVersion)}
546
+ Latest: ${this.escapeXml(dep.latestVersion)}
547
+ Type: ${dep.updateType}${dep.isSecurityUpdate ? '\nSecurity Update Required' : ''}
548
+ </failure>`
549
+ : ''
550
+
551
+ testcases.push(`
552
+ <testcase classname="pcu.check.${this.escapeXml(catalog.catalogName)}" name="${this.escapeXml(dep.packageName)}" time="0">
553
+ ${failureElement}
554
+ </testcase>`)
555
+ }
556
+ }
557
+
558
+ return `<?xml version="1.0" encoding="UTF-8"?>
559
+ <testsuites name="pcu-check" tests="${report.totalOutdated}" failures="${failures}" errors="0" time="0">
560
+ <testsuite name="outdated-dependencies" tests="${report.totalOutdated}" failures="${failures}" errors="0">
561
+ ${testcases.join('\n')}
562
+ </testsuite>
563
+ </testsuites>`
564
+ }
565
+
566
+ private formatUpdateResultJUnit(result: UpdateResult): string {
567
+ const testcases: string[] = []
568
+
569
+ for (const dep of result.updatedDependencies) {
570
+ testcases.push(`
571
+ <testcase classname="pcu.update.${this.escapeXml(dep.catalogName)}" name="${this.escapeXml(dep.packageName)}" time="0">
572
+ </testcase>`)
573
+ }
574
+
575
+ for (const error of result.errors) {
576
+ testcases.push(`
577
+ <testcase classname="pcu.update.${this.escapeXml(error.catalogName)}" name="${this.escapeXml(error.packageName)}" time="0">
578
+ <failure message="${this.escapeXml(error.error)}" type="UpdateError">${this.escapeXml(error.error)}</failure>
579
+ </testcase>`)
580
+ }
581
+
582
+ const total = result.updatedDependencies.length + result.errors.length
583
+
584
+ return `<?xml version="1.0" encoding="UTF-8"?>
585
+ <testsuites name="pcu-update" tests="${total}" failures="${result.errors.length}" errors="0" time="0">
586
+ <testsuite name="dependency-updates" tests="${total}" failures="${result.errors.length}" errors="0">
587
+ ${testcases.join('\n')}
588
+ </testsuite>
589
+ </testsuites>`
590
+ }
591
+
592
+ private formatUpdatePlanJUnit(plan: UpdatePlan): string {
593
+ const testcases: string[] = []
594
+ let failures = 0
595
+
596
+ for (const update of plan.updates) {
597
+ const hasConflict = plan.conflicts.some((c) => c.packageName === update.packageName)
598
+ if (hasConflict) failures++
599
+
600
+ const failureElement = hasConflict
601
+ ? `<failure message="Version conflict detected" type="VersionConflict">Multiple catalogs have different versions</failure>`
602
+ : ''
603
+
604
+ testcases.push(`
605
+ <testcase classname="pcu.plan.${this.escapeXml(update.catalogName)}" name="${this.escapeXml(update.packageName)}" time="0">
606
+ ${failureElement}
607
+ </testcase>`)
608
+ }
609
+
610
+ return `<?xml version="1.0" encoding="UTF-8"?>
611
+ <testsuites name="pcu-update-plan" tests="${plan.totalUpdates}" failures="${failures}" errors="0" time="0">
612
+ <testsuite name="planned-updates" tests="${plan.totalUpdates}" failures="${failures}" errors="0">
613
+ ${testcases.join('\n')}
614
+ </testsuite>
615
+ </testsuites>`
616
+ }
617
+
618
+ private formatValidationJUnit(report: WorkspaceValidationReport): string {
619
+ const testcases: string[] = []
620
+
621
+ testcases.push(`
622
+ <testcase classname="pcu.validation" name="workspace-structure" time="0">
623
+ ${!report.isValid ? `<failure message="Workspace validation failed" type="ValidationError">${this.escapeXml(report.errors.join('\n'))}</failure>` : ''}
624
+ </testcase>`)
625
+
626
+ for (const warning of report.warnings) {
627
+ testcases.push(`
628
+ <testcase classname="pcu.validation" name="warning" time="0">
629
+ <system-out>${this.escapeXml(warning)}</system-out>
630
+ </testcase>`)
631
+ }
632
+
633
+ return `<?xml version="1.0" encoding="UTF-8"?>
634
+ <testsuites name="pcu-validation" tests="${1 + report.warnings.length}" failures="${report.isValid ? 0 : 1}" errors="0" time="0">
635
+ <testsuite name="workspace-validation" tests="${1 + report.warnings.length}" failures="${report.isValid ? 0 : 1}" errors="0">
636
+ ${testcases.join('\n')}
637
+ </testsuite>
638
+ </testsuites>`
639
+ }
640
+
641
+ private formatSecurityJUnit(report: SecurityReport): string {
642
+ const testcases: string[] = []
643
+
644
+ for (const vuln of report.vulnerabilities) {
645
+ const severity = vuln.severity.toLowerCase()
646
+ const isFailure = severity === 'critical' || severity === 'high'
647
+
648
+ testcases.push(`
649
+ <testcase classname="pcu.security.${this.escapeXml(vuln.package)}" name="${this.escapeXml(vuln.title)}" time="0">
650
+ ${
651
+ isFailure
652
+ ? `<failure message="${this.escapeXml(vuln.title)}" type="SecurityVulnerability">
653
+ Severity: ${vuln.severity}
654
+ Package: ${this.escapeXml(vuln.package)}
655
+ ${vuln.overview ? `Overview: ${this.escapeXml(vuln.overview)}` : ''}
656
+ ${vuln.fixAvailable ? `Fix: ${typeof vuln.fixAvailable === 'string' ? vuln.fixAvailable : 'Available'}` : 'No fix available'}
657
+ </failure>`
658
+ : ''
659
+ }
660
+ </testcase>`)
661
+ }
662
+
663
+ const failures = report.vulnerabilities.filter(
664
+ (v) => v.severity === 'critical' || v.severity === 'high'
665
+ ).length
666
+
667
+ return `<?xml version="1.0" encoding="UTF-8"?>
668
+ <testsuites name="pcu-security" tests="${report.summary.totalVulnerabilities}" failures="${failures}" errors="0" time="0">
669
+ <testsuite name="security-vulnerabilities" tests="${report.summary.totalVulnerabilities}" failures="${failures}" errors="0">
670
+ ${testcases.join('\n')}
671
+ </testsuite>
672
+ </testsuites>`
673
+ }
674
+
675
+ // ==================== SARIF Format ====================
676
+
677
+ private mapSeverityToSARIF(severity: string): SARIFLevel {
678
+ switch (severity.toLowerCase()) {
679
+ case 'critical':
680
+ case 'high':
681
+ return 'error'
682
+ case 'moderate':
683
+ case 'medium':
684
+ return 'warning'
685
+ case 'low':
686
+ return 'note'
687
+ default:
688
+ return 'none'
689
+ }
690
+ }
691
+
692
+ private formatOutdatedSARIF(report: OutdatedReport): string {
693
+ const results: object[] = []
694
+ const rules: object[] = []
695
+ const ruleIds = new Set<string>()
696
+
697
+ for (const catalog of report.catalogs) {
698
+ for (const dep of catalog.outdatedDependencies) {
699
+ const ruleId = `outdated-${dep.updateType}`
700
+ if (!ruleIds.has(ruleId)) {
701
+ ruleIds.add(ruleId)
702
+ rules.push({
703
+ id: ruleId,
704
+ name: `Outdated${dep.updateType.charAt(0).toUpperCase() + dep.updateType.slice(1)}Dependency`,
705
+ shortDescription: { text: `Outdated ${dep.updateType} dependency` },
706
+ fullDescription: { text: `A dependency has an outdated ${dep.updateType} version` },
707
+ defaultConfiguration: {
708
+ level: dep.updateType === 'major' ? 'warning' : 'note',
709
+ },
710
+ })
711
+ }
712
+
713
+ results.push({
714
+ ruleId,
715
+ level: dep.updateType === 'major' ? 'warning' : 'note',
716
+ message: {
717
+ text: `${dep.packageName} is outdated: ${dep.currentVersion} → ${dep.latestVersion}`,
718
+ },
719
+ locations: [
720
+ {
721
+ physicalLocation: {
722
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
723
+ region: { startLine: 1 },
724
+ },
725
+ },
726
+ ],
727
+ properties: {
728
+ packageName: dep.packageName,
729
+ currentVersion: dep.currentVersion,
730
+ latestVersion: dep.latestVersion,
731
+ updateType: dep.updateType,
732
+ isSecurityUpdate: dep.isSecurityUpdate,
733
+ catalog: catalog.catalogName,
734
+ },
735
+ })
736
+ }
737
+ }
738
+
739
+ return this.createSARIFDocument('pcu-check', rules, results)
740
+ }
741
+
742
+ private formatUpdateResultSARIF(result: UpdateResult): string {
743
+ const results: object[] = []
744
+ const rules: object[] = [
745
+ {
746
+ id: 'update-success',
747
+ name: 'DependencyUpdateSuccess',
748
+ shortDescription: { text: 'Dependency updated successfully' },
749
+ defaultConfiguration: { level: 'note' },
750
+ },
751
+ {
752
+ id: 'update-error',
753
+ name: 'DependencyUpdateError',
754
+ shortDescription: { text: 'Dependency update failed' },
755
+ defaultConfiguration: { level: 'error' },
756
+ },
757
+ ]
758
+
759
+ for (const dep of result.updatedDependencies) {
760
+ results.push({
761
+ ruleId: 'update-success',
762
+ level: 'note',
763
+ message: { text: `${dep.packageName} updated: ${dep.fromVersion} → ${dep.toVersion}` },
764
+ locations: [
765
+ {
766
+ physicalLocation: {
767
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
768
+ region: { startLine: 1 },
769
+ },
770
+ },
771
+ ],
772
+ })
773
+ }
774
+
775
+ for (const error of result.errors) {
776
+ results.push({
777
+ ruleId: 'update-error',
778
+ level: 'error',
779
+ message: { text: `Failed to update ${error.packageName}: ${error.error}` },
780
+ locations: [
781
+ {
782
+ physicalLocation: {
783
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
784
+ region: { startLine: 1 },
785
+ },
786
+ },
787
+ ],
788
+ })
789
+ }
790
+
791
+ return this.createSARIFDocument('pcu-update', rules, results)
792
+ }
793
+
794
+ private formatUpdatePlanSARIF(plan: UpdatePlan): string {
795
+ const results: object[] = []
796
+ const rules: object[] = [
797
+ {
798
+ id: 'planned-update',
799
+ name: 'PlannedDependencyUpdate',
800
+ shortDescription: { text: 'Dependency update planned' },
801
+ defaultConfiguration: { level: 'note' },
802
+ },
803
+ {
804
+ id: 'version-conflict',
805
+ name: 'VersionConflict',
806
+ shortDescription: { text: 'Version conflict detected' },
807
+ defaultConfiguration: { level: 'warning' },
808
+ },
809
+ ]
810
+
811
+ for (const update of plan.updates) {
812
+ results.push({
813
+ ruleId: 'planned-update',
814
+ level: 'note',
815
+ message: {
816
+ text: `${update.packageName}: ${update.currentVersion} → ${update.newVersion} (${update.updateType})`,
817
+ },
818
+ locations: [
819
+ {
820
+ physicalLocation: {
821
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
822
+ region: { startLine: 1 },
823
+ },
824
+ },
825
+ ],
826
+ })
827
+ }
828
+
829
+ for (const conflict of plan.conflicts) {
830
+ results.push({
831
+ ruleId: 'version-conflict',
832
+ level: 'warning',
833
+ message: { text: `Version conflict for ${conflict.packageName}` },
834
+ locations: [
835
+ {
836
+ physicalLocation: {
837
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
838
+ region: { startLine: 1 },
839
+ },
840
+ },
841
+ ],
842
+ })
843
+ }
844
+
845
+ return this.createSARIFDocument('pcu-update-plan', rules, results)
846
+ }
847
+
848
+ private formatValidationSARIF(report: WorkspaceValidationReport): string {
849
+ const results: object[] = []
850
+ const rules: object[] = [
851
+ {
852
+ id: 'validation-error',
853
+ name: 'WorkspaceValidationError',
854
+ shortDescription: { text: 'Workspace validation error' },
855
+ defaultConfiguration: { level: 'error' },
856
+ },
857
+ {
858
+ id: 'validation-warning',
859
+ name: 'WorkspaceValidationWarning',
860
+ shortDescription: { text: 'Workspace validation warning' },
861
+ defaultConfiguration: { level: 'warning' },
862
+ },
863
+ ]
864
+
865
+ for (const error of report.errors) {
866
+ results.push({
867
+ ruleId: 'validation-error',
868
+ level: 'error',
869
+ message: { text: error },
870
+ locations: [
871
+ {
872
+ physicalLocation: {
873
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
874
+ region: { startLine: 1 },
875
+ },
876
+ },
877
+ ],
878
+ })
879
+ }
880
+
881
+ for (const warning of report.warnings) {
882
+ results.push({
883
+ ruleId: 'validation-warning',
884
+ level: 'warning',
885
+ message: { text: warning },
886
+ locations: [
887
+ {
888
+ physicalLocation: {
889
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
890
+ region: { startLine: 1 },
891
+ },
892
+ },
893
+ ],
894
+ })
895
+ }
896
+
897
+ return this.createSARIFDocument('pcu-validation', rules, results)
898
+ }
899
+
900
+ private formatSecuritySARIF(report: SecurityReport): string {
901
+ const results: object[] = []
902
+ const rules: object[] = []
903
+ const ruleIds = new Set<string>()
904
+
905
+ for (const vuln of report.vulnerabilities) {
906
+ const ruleId = `security-${vuln.severity.toLowerCase()}`
907
+ if (!ruleIds.has(ruleId)) {
908
+ ruleIds.add(ruleId)
909
+ rules.push({
910
+ id: ruleId,
911
+ name: `Security${vuln.severity.charAt(0).toUpperCase() + vuln.severity.slice(1)}`,
912
+ shortDescription: { text: `${vuln.severity} severity vulnerability` },
913
+ defaultConfiguration: { level: this.mapSeverityToSARIF(vuln.severity) },
914
+ })
915
+ }
916
+
917
+ results.push({
918
+ ruleId,
919
+ level: this.mapSeverityToSARIF(vuln.severity),
920
+ message: { text: `${vuln.package}: ${vuln.title}` },
921
+ locations: [
922
+ {
923
+ physicalLocation: {
924
+ artifactLocation: { uri: 'pnpm-workspace.yaml' },
925
+ region: { startLine: 1 },
926
+ },
927
+ },
928
+ ],
929
+ properties: {
930
+ package: vuln.package,
931
+ severity: vuln.severity,
932
+ cwe: vuln.cwe,
933
+ fixAvailable: vuln.fixAvailable,
934
+ url: vuln.url,
935
+ },
936
+ })
937
+ }
938
+
939
+ return this.createSARIFDocument('pcu-security', rules, results)
940
+ }
941
+
942
+ private createSARIFDocument(toolName: string, rules: object[], results: object[]): string {
943
+ const sarif = {
944
+ $schema:
945
+ 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
946
+ version: '2.1.0',
947
+ runs: [
948
+ {
949
+ tool: {
950
+ driver: {
951
+ name: toolName,
952
+ informationUri: 'https://github.com/user/pnpm-catalog-updates',
953
+ version: '1.0.0',
954
+ rules,
955
+ },
956
+ },
957
+ results,
958
+ },
959
+ ],
960
+ }
961
+
962
+ return JSON.stringify(sarif, null, 2)
963
+ }
964
+ }