pnpm-catalog-updates 1.0.3 → 1.1.2

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
@@ -2,38 +2,78 @@
2
2
  * Output Formatter
3
3
  *
4
4
  * Provides formatted output for CLI commands in various formats.
5
- * Supports table, JSON, YAML, and minimal output formats.
5
+ * Supports table, JSON, YAML, minimal, and CI/CD output formats.
6
+ * CI formats include GitHub Actions, GitLab CI, JUnit XML, and SARIF.
6
7
  */
7
8
 
8
9
  import type {
9
10
  AnalysisResult,
10
11
  ImpactAnalysis,
11
12
  OutdatedReport,
13
+ UpdatePlan,
12
14
  UpdateResult,
15
+ WorkspaceInfo,
13
16
  WorkspaceStats,
14
17
  WorkspaceValidationReport,
15
18
  } from '@pcu/core'
19
+ import { countUpdateTypes } from '@pcu/core'
20
+ import { t } from '@pcu/utils'
16
21
  import chalk from 'chalk'
17
22
  import Table from 'cli-table3'
18
23
  import YAML from 'yaml'
19
24
  import type { SecurityReport } from '../commands/securityCommand.js'
20
-
21
- export type OutputFormat = 'table' | 'json' | 'yaml' | 'minimal'
22
-
23
- // Build ANSI escape regex without literal control characters
24
- const ANSI_ESCAPE = String.fromCharCode(27)
25
- const ansiRegex: RegExp = new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, 'g')
25
+ import { CIFormatter, type CIOutputFormat } from './ciFormatter.js'
26
+ import { ColorUtils } from './colorUtils.js'
27
+ import { VersionFormatter } from './versionFormatter.js'
28
+
29
+ export type OutputFormat =
30
+ | 'table'
31
+ | 'json'
32
+ | 'yaml'
33
+ | 'minimal'
34
+ | 'github'
35
+ | 'gitlab'
36
+ | 'junit'
37
+ | 'sarif'
38
+
39
+ export type CIFormat = 'github' | 'gitlab' | 'junit' | 'sarif'
40
+
41
+ export function isCIFormat(format: OutputFormat): format is CIFormat {
42
+ return ['github', 'gitlab', 'junit', 'sarif'].includes(format)
43
+ }
26
44
 
27
45
  export class OutputFormatter {
46
+ private ciFormatter: CIFormatter | null = null
47
+ private readonly colorUtils: ColorUtils
48
+ private readonly versionFormatter: VersionFormatter
49
+
28
50
  constructor(
29
51
  private readonly format: OutputFormat = 'table',
30
52
  private readonly useColor: boolean = true
31
- ) {}
53
+ ) {
54
+ this.colorUtils = new ColorUtils(useColor)
55
+ this.versionFormatter = new VersionFormatter(this.colorUtils, useColor)
56
+ }
57
+
58
+ /**
59
+ * Get or create CIFormatter instance for CI output formats
60
+ */
61
+ private getCIFormatter(): CIFormatter {
62
+ if (!this.ciFormatter) {
63
+ this.ciFormatter = new CIFormatter(this.format as CIOutputFormat)
64
+ }
65
+ return this.ciFormatter
66
+ }
32
67
 
33
68
  /**
34
69
  * Format outdated dependencies report
35
70
  */
36
71
  formatOutdatedReport(report: OutdatedReport): string {
72
+ // Delegate to CIFormatter for CI output formats
73
+ if (isCIFormat(this.format)) {
74
+ return this.getCIFormatter().formatOutdatedReport(report)
75
+ }
76
+
37
77
  switch (this.format) {
38
78
  case 'json':
39
79
  return JSON.stringify(report, null, 2)
@@ -50,6 +90,11 @@ export class OutputFormatter {
50
90
  * Format update result
51
91
  */
52
92
  formatUpdateResult(result: UpdateResult): string {
93
+ // Delegate to CIFormatter for CI output formats
94
+ if (isCIFormat(this.format)) {
95
+ return this.getCIFormatter().formatUpdateResult(result)
96
+ }
97
+
53
98
  switch (this.format) {
54
99
  case 'json':
55
100
  return JSON.stringify(result, null, 2)
@@ -62,6 +107,27 @@ export class OutputFormatter {
62
107
  }
63
108
  }
64
109
 
110
+ /**
111
+ * Format update plan (for dry-run mode)
112
+ */
113
+ formatUpdatePlan(plan: UpdatePlan): string {
114
+ // Delegate to CIFormatter for CI output formats
115
+ if (isCIFormat(this.format)) {
116
+ return this.getCIFormatter().formatUpdatePlan(plan)
117
+ }
118
+
119
+ switch (this.format) {
120
+ case 'json':
121
+ return JSON.stringify(plan, null, 2)
122
+ case 'yaml':
123
+ return YAML.stringify(plan)
124
+ case 'minimal':
125
+ return this.formatUpdatePlanMinimal(plan)
126
+ default:
127
+ return this.formatUpdatePlanTable(plan)
128
+ }
129
+ }
130
+
65
131
  /**
66
132
  * Format impact analysis
67
133
  */
@@ -82,6 +148,11 @@ export class OutputFormatter {
82
148
  * Format workspace validation report
83
149
  */
84
150
  formatValidationReport(report: WorkspaceValidationReport): string {
151
+ // Delegate to CIFormatter for CI output formats
152
+ if (isCIFormat(this.format)) {
153
+ return this.getCIFormatter().formatValidationReport(report)
154
+ }
155
+
85
156
  switch (this.format) {
86
157
  case 'json':
87
158
  return JSON.stringify(report, null, 2)
@@ -94,6 +165,22 @@ export class OutputFormatter {
94
165
  }
95
166
  }
96
167
 
168
+ /**
169
+ * Format workspace information
170
+ */
171
+ formatWorkspaceInfo(info: WorkspaceInfo): string {
172
+ switch (this.format) {
173
+ case 'json':
174
+ return JSON.stringify(info, null, 2)
175
+ case 'yaml':
176
+ return YAML.stringify(info)
177
+ case 'minimal':
178
+ return this.formatWorkspaceInfoMinimal(info)
179
+ default:
180
+ return this.formatWorkspaceInfoTable(info)
181
+ }
182
+ }
183
+
97
184
  /**
98
185
  * Format workspace statistics
99
186
  */
@@ -114,6 +201,11 @@ export class OutputFormatter {
114
201
  * Format security report
115
202
  */
116
203
  formatSecurityReport(report: SecurityReport): string {
204
+ // Delegate to CIFormatter for CI output formats
205
+ if (isCIFormat(this.format)) {
206
+ return this.getCIFormatter().formatSecurityReport(report)
207
+ }
208
+
117
209
  switch (this.format) {
118
210
  case 'json':
119
211
  return JSON.stringify(report, null, 2)
@@ -153,35 +245,50 @@ export class OutputFormatter {
153
245
  const lines: string[] = []
154
246
 
155
247
  // Header
156
- lines.push(this.colorize(chalk.bold, `\n📦 Workspace: ${report.workspace.name}`))
157
- lines.push(this.colorize(chalk.gray, `Path: ${report.workspace.path}`))
248
+ lines.push(
249
+ this.colorUtils.colorize(chalk.bold, `\n${t('format.workspace')}: ${report.workspace.name}`)
250
+ )
251
+ lines.push(
252
+ this.colorUtils.colorize(chalk.gray, `${t('format.path')}: ${report.workspace.path}`)
253
+ )
158
254
 
159
255
  if (!report.hasUpdates) {
160
- lines.push(this.colorize(chalk.green, '\n✅ All catalog dependencies are up to date!'))
256
+ lines.push(this.colorUtils.colorize(chalk.green, `\n✅ ${t('format.allUpToDate')}`))
161
257
  return lines.join('\n')
162
258
  }
163
259
 
164
260
  lines.push(
165
- this.colorize(chalk.yellow, `\n🔄 Found ${report.totalOutdated} outdated dependencies\n`)
261
+ this.colorUtils.colorize(
262
+ chalk.yellow,
263
+ `\n${t('format.foundOutdated', { count: String(report.totalOutdated) })}\n`
264
+ )
166
265
  )
167
266
 
168
267
  for (const catalogInfo of report.catalogs) {
169
268
  if (catalogInfo.outdatedCount === 0) continue
170
269
 
171
- lines.push(this.colorize(chalk.bold, `📋 Catalog: ${catalogInfo.catalogName}`))
270
+ lines.push(
271
+ this.colorUtils.colorize(chalk.bold, `${t('format.catalog')}: ${catalogInfo.catalogName}`)
272
+ )
172
273
 
173
274
  const table = new Table({
174
- head: this.colorizeHeaders(['Package', 'Current', 'Latest', 'Type', 'Packages']),
275
+ head: this.colorUtils.colorizeHeaders([
276
+ t('table.header.package'),
277
+ t('table.header.current'),
278
+ t('table.header.latest'),
279
+ t('table.header.type'),
280
+ t('table.header.packagesCount'),
281
+ ]),
175
282
  style: { head: [], border: [] },
176
283
  colWidths: [25, 15, 15, 8, 20],
177
284
  })
178
285
 
179
286
  for (const dep of catalogInfo.outdatedDependencies) {
180
- const typeColor = this.getUpdateTypeColor(dep.updateType)
181
- const securityIcon = dep.isSecurityUpdate ? '🔒 ' : ''
287
+ const typeColor = this.colorUtils.getUpdateTypeColor(dep.updateType)
288
+ const securityIcon = dep.isSecurityUpdate ? '[SEC] ' : ''
182
289
 
183
290
  // Colorize version differences
184
- const { currentColored, latestColored } = this.colorizeVersionDiff(
291
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
185
292
  dep.currentVersion,
186
293
  dep.latestVersion,
187
294
  dep.updateType
@@ -191,8 +298,8 @@ export class OutputFormatter {
191
298
  `${securityIcon}${dep.packageName}`,
192
299
  currentColored,
193
300
  latestColored,
194
- this.colorize(typeColor, dep.updateType),
195
- `${dep.affectedPackages.length} package(s)`,
301
+ this.colorUtils.colorize(typeColor, dep.updateType),
302
+ t('common.packagesCount', { count: String(dep.affectedPackages.length) }),
196
303
  ])
197
304
  }
198
305
 
@@ -208,7 +315,7 @@ export class OutputFormatter {
208
315
  */
209
316
  private formatOutdatedMinimal(report: OutdatedReport): string {
210
317
  if (!report.hasUpdates) {
211
- return 'All dependencies up to date'
318
+ return t('format.allUpToDate')
212
319
  }
213
320
 
214
321
  // Collect all dependencies first to calculate max package name width
@@ -221,8 +328,8 @@ export class OutputFormatter {
221
328
 
222
329
  for (const catalogInfo of report.catalogs) {
223
330
  for (const dep of catalogInfo.outdatedDependencies) {
224
- const securityIcon = dep.isSecurityUpdate ? '🔒 ' : ''
225
- const { currentColored, latestColored } = this.colorizeVersionDiff(
331
+ const securityIcon = dep.isSecurityUpdate ? '[SEC] ' : ''
332
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
226
333
  dep.currentVersion,
227
334
  dep.latestVersion,
228
335
  dep.updateType
@@ -242,19 +349,16 @@ export class OutputFormatter {
242
349
  )
243
350
 
244
351
  // Calculate max version widths (need to strip color codes for accurate width calculation)
245
- const stripAnsi = (str: string) => str.replace(ansiRegex, '')
246
- const maxCurrentWidth = Math.max(...allDeps.map((dep) => stripAnsi(dep.currentColored).length))
352
+ const maxCurrentWidth = Math.max(
353
+ ...allDeps.map((dep) => this.colorUtils.stripAnsi(dep.currentColored).length)
354
+ )
247
355
 
248
356
  // Format lines with proper alignment
249
357
  const lines: string[] = []
250
358
  for (const dep of allDeps) {
251
359
  const nameWithIcon = dep.securityIcon + dep.packageName
252
360
  const paddedName = nameWithIcon.padEnd(maxNameWidth)
253
-
254
- // For current version alignment, we need to pad the visible text, not the colored version
255
- const currentVisible = stripAnsi(dep.currentColored)
256
- const currentPadding = maxCurrentWidth - currentVisible.length
257
- const paddedCurrent = dep.currentColored + ' '.repeat(currentPadding)
361
+ const paddedCurrent = this.colorUtils.padAnsi(dep.currentColored, maxCurrentWidth)
258
362
 
259
363
  lines.push(`${paddedName} ${paddedCurrent} → ${dep.latestColored}`)
260
364
  }
@@ -269,31 +373,44 @@ export class OutputFormatter {
269
373
  const lines: string[] = []
270
374
 
271
375
  // Header
272
- lines.push(this.colorize(chalk.bold, `\n📦 Workspace: ${result.workspace.name}`))
376
+ lines.push(
377
+ this.colorUtils.colorize(chalk.bold, `\n${t('format.workspace')}: ${result.workspace.name}`)
378
+ )
273
379
 
274
380
  if (result.success) {
275
- lines.push(this.colorize(chalk.green, '✅ Update completed successfully!'))
381
+ lines.push(this.colorUtils.colorize(chalk.green, `✅ ${t('format.updateCompleted')}`))
276
382
  } else {
277
- lines.push(this.colorize(chalk.red, '❌ Update completed with errors'))
383
+ lines.push(this.colorUtils.colorize(chalk.red, `❌ ${t('format.updateFailed')}`))
278
384
  }
279
385
 
280
386
  lines.push('')
281
387
 
282
388
  // Updated dependencies
283
389
  if (result.updatedDependencies.length > 0) {
284
- lines.push(this.colorize(chalk.green, `🎉 Updated ${result.totalUpdated} dependencies:`))
390
+ lines.push(
391
+ this.colorUtils.colorize(
392
+ chalk.green,
393
+ `${t('format.updatedCount', { count: String(result.totalUpdated) })}:`
394
+ )
395
+ )
285
396
 
286
397
  const table = new Table({
287
- head: this.colorizeHeaders(['Catalog', 'Package', 'From', 'To', 'Type']),
398
+ head: this.colorUtils.colorizeHeaders([
399
+ t('table.header.catalog'),
400
+ t('table.header.package'),
401
+ t('table.header.from'),
402
+ t('table.header.to'),
403
+ t('table.header.type'),
404
+ ]),
288
405
  style: { head: [], border: [] },
289
406
  colWidths: [15, 25, 15, 15, 8],
290
407
  })
291
408
 
292
409
  for (const dep of result.updatedDependencies) {
293
- const typeColor = this.getUpdateTypeColor(dep.updateType)
410
+ const typeColor = this.colorUtils.getUpdateTypeColor(dep.updateType)
294
411
 
295
412
  // Colorize version differences
296
- const { currentColored, latestColored } = this.colorizeVersionDiff(
413
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
297
414
  dep.fromVersion,
298
415
  dep.toVersion,
299
416
  dep.updateType
@@ -304,7 +421,7 @@ export class OutputFormatter {
304
421
  dep.packageName,
305
422
  currentColored,
306
423
  latestColored,
307
- this.colorize(typeColor, dep.updateType),
424
+ this.colorUtils.colorize(typeColor, dep.updateType),
308
425
  ])
309
426
  }
310
427
 
@@ -314,7 +431,12 @@ export class OutputFormatter {
314
431
 
315
432
  // Skipped dependencies
316
433
  if (result.skippedDependencies.length > 0) {
317
- lines.push(this.colorize(chalk.yellow, `⚠️ Skipped ${result.totalSkipped} dependencies:`))
434
+ lines.push(
435
+ this.colorUtils.colorize(
436
+ chalk.yellow,
437
+ `⚠️ ${t('format.skippedDeps')} (${result.totalSkipped}):`
438
+ )
439
+ )
318
440
 
319
441
  for (const dep of result.skippedDependencies) {
320
442
  lines.push(` ${dep.catalogName}:${dep.packageName} - ${dep.reason}`)
@@ -324,10 +446,15 @@ export class OutputFormatter {
324
446
 
325
447
  // Errors
326
448
  if (result.errors.length > 0) {
327
- lines.push(this.colorize(chalk.red, `❌ ${result.totalErrors} errors occurred:`))
449
+ lines.push(
450
+ this.colorUtils.colorize(
451
+ chalk.red,
452
+ `❌ ${t('format.errorCount', { count: String(result.totalErrors) })}:`
453
+ )
454
+ )
328
455
 
329
456
  for (const error of result.errors) {
330
- const prefix = error.fatal ? '💥' : '⚠️ '
457
+ const prefix = error.fatal ? '!!' : '⚠️'
331
458
  lines.push(` ${prefix} ${error.catalogName}:${error.packageName} - ${error.error}`)
332
459
  }
333
460
  }
@@ -342,15 +469,15 @@ export class OutputFormatter {
342
469
  const lines: string[] = []
343
470
 
344
471
  if (result.success) {
345
- lines.push(`Updated ${result.totalUpdated} dependencies`)
472
+ lines.push(t('format.updatedCount', { count: String(result.totalUpdated) }))
346
473
  } else {
347
- lines.push(`Update failed with ${result.totalErrors} errors`)
474
+ lines.push(t('format.errorCount', { count: String(result.totalErrors) }))
348
475
  }
349
476
 
350
477
  if (result.updatedDependencies.length > 0) {
351
478
  // Collect version info for alignment calculation
352
479
  const depsWithVersions = result.updatedDependencies.map((dep) => {
353
- const { currentColored, latestColored } = this.colorizeVersionDiff(
480
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
354
481
  dep.fromVersion,
355
482
  dep.toVersion,
356
483
  dep.updateType
@@ -364,19 +491,13 @@ export class OutputFormatter {
364
491
 
365
492
  // Calculate max widths for alignment
366
493
  const maxNameWidth = Math.max(...depsWithVersions.map((dep) => dep.packageName.length))
367
-
368
- const stripAnsi = (str: string) => str.replace(ansiRegex, '')
369
494
  const maxCurrentWidth = Math.max(
370
- ...depsWithVersions.map((dep) => stripAnsi(dep.currentColored).length)
495
+ ...depsWithVersions.map((dep) => this.colorUtils.stripAnsi(dep.currentColored).length)
371
496
  )
372
497
 
373
498
  for (const dep of depsWithVersions) {
374
499
  const paddedName = dep.packageName.padEnd(maxNameWidth)
375
-
376
- // Pad current version for alignment
377
- const currentVisible = stripAnsi(dep.currentColored)
378
- const currentPadding = maxCurrentWidth - currentVisible.length
379
- const paddedCurrent = dep.currentColored + ' '.repeat(currentPadding)
500
+ const paddedCurrent = this.colorUtils.padAnsi(dep.currentColored, maxCurrentWidth)
380
501
 
381
502
  lines.push(`${paddedName} ${paddedCurrent} → ${dep.latestColored}`)
382
503
  }
@@ -385,6 +506,167 @@ export class OutputFormatter {
385
506
  return lines.join('\n')
386
507
  }
387
508
 
509
+ /**
510
+ * Format update plan as table (for dry-run mode)
511
+ */
512
+ private formatUpdatePlanTable(plan: UpdatePlan): string {
513
+ const lines: string[] = []
514
+
515
+ // Header
516
+ lines.push(
517
+ this.colorUtils.colorize(chalk.bold, `\n${t('format.workspace')}: ${plan.workspace.name}`)
518
+ )
519
+ lines.push(this.colorUtils.colorize(chalk.gray, `${t('format.path')}: ${plan.workspace.path}`))
520
+
521
+ if (plan.totalUpdates === 0) {
522
+ lines.push(this.colorUtils.colorize(chalk.green, `\n✅ ${t('format.noUpdatesPlanned')}`))
523
+ return lines.join('\n')
524
+ }
525
+
526
+ lines.push(
527
+ this.colorUtils.colorize(
528
+ chalk.cyan,
529
+ `\n${t('format.plannedUpdates', { count: String(plan.totalUpdates) })}`
530
+ )
531
+ )
532
+ lines.push('')
533
+
534
+ // Updates table
535
+ if (plan.updates.length > 0) {
536
+ const table = new Table({
537
+ head: this.colorUtils.colorizeHeaders([
538
+ t('table.header.catalog'),
539
+ t('table.header.package'),
540
+ t('table.header.current'),
541
+ t('table.header.new'),
542
+ t('table.header.type'),
543
+ ]),
544
+ style: { head: [], border: [] },
545
+ colWidths: [15, 30, 15, 15, 8],
546
+ })
547
+
548
+ for (const update of plan.updates) {
549
+ const typeColor = this.colorUtils.getUpdateTypeColor(update.updateType)
550
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
551
+ update.currentVersion,
552
+ update.newVersion,
553
+ update.updateType
554
+ )
555
+
556
+ table.push([
557
+ update.catalogName,
558
+ update.packageName,
559
+ currentColored,
560
+ latestColored,
561
+ this.colorUtils.colorize(typeColor, update.updateType),
562
+ ])
563
+ }
564
+
565
+ lines.push(table.toString())
566
+ lines.push('')
567
+ }
568
+
569
+ // Conflicts warning
570
+ if (plan.hasConflicts && plan.conflicts.length > 0) {
571
+ lines.push(this.colorUtils.colorize(chalk.yellow, `⚠️ ${t('format.versionConflicts')}:`))
572
+ lines.push('')
573
+
574
+ for (const conflict of plan.conflicts) {
575
+ lines.push(this.colorUtils.colorize(chalk.bold, ` ${conflict.packageName}:`))
576
+ for (const catalog of conflict.catalogs) {
577
+ lines.push(
578
+ ` ${catalog.catalogName}: ${catalog.currentVersion} → ${catalog.proposedVersion}`
579
+ )
580
+ }
581
+ if (conflict.recommendation) {
582
+ lines.push(
583
+ this.colorUtils.colorize(
584
+ chalk.gray,
585
+ ` ${t('format.recommendation')}: ${conflict.recommendation}`
586
+ )
587
+ )
588
+ }
589
+ lines.push('')
590
+ }
591
+ }
592
+
593
+ // Summary breakdown
594
+ const updateTypes = countUpdateTypes(plan.updates)
595
+
596
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.summary')}:`))
597
+ if (updateTypes.major > 0) {
598
+ lines.push(
599
+ this.colorUtils.colorize(
600
+ chalk.red,
601
+ ` • ${t('command.check.majorUpdates', { count: String(updateTypes.major) })}`
602
+ )
603
+ )
604
+ }
605
+ if (updateTypes.minor > 0) {
606
+ lines.push(
607
+ this.colorUtils.colorize(
608
+ chalk.yellow,
609
+ ` • ${t('command.check.minorUpdates', { count: String(updateTypes.minor) })}`
610
+ )
611
+ )
612
+ }
613
+ if (updateTypes.patch > 0) {
614
+ lines.push(
615
+ this.colorUtils.colorize(
616
+ chalk.green,
617
+ ` • ${t('command.check.patchUpdates', { count: String(updateTypes.patch) })}`
618
+ )
619
+ )
620
+ }
621
+
622
+ return lines.join('\n')
623
+ }
624
+
625
+ /**
626
+ * Format update plan minimally (for dry-run mode)
627
+ */
628
+ private formatUpdatePlanMinimal(plan: UpdatePlan): string {
629
+ if (plan.totalUpdates === 0) {
630
+ return t('format.noUpdatesPlanned')
631
+ }
632
+
633
+ // Collect update info for alignment calculation
634
+ const updatesWithVersions = plan.updates.map((update) => {
635
+ const { currentColored, latestColored } = this.versionFormatter.colorizeVersionDiff(
636
+ update.currentVersion,
637
+ update.newVersion,
638
+ update.updateType
639
+ )
640
+ return {
641
+ packageName: update.packageName,
642
+ currentColored,
643
+ latestColored,
644
+ }
645
+ })
646
+
647
+ // Calculate max widths for alignment
648
+ const maxNameWidth = Math.max(...updatesWithVersions.map((u) => u.packageName.length))
649
+ const maxCurrentWidth = Math.max(
650
+ ...updatesWithVersions.map((u) => this.colorUtils.stripAnsi(u.currentColored).length)
651
+ )
652
+
653
+ const lines: string[] = []
654
+ for (const update of updatesWithVersions) {
655
+ const paddedName = update.packageName.padEnd(maxNameWidth)
656
+ const paddedCurrent = this.colorUtils.padAnsi(update.currentColored, maxCurrentWidth)
657
+
658
+ lines.push(`${paddedName} ${paddedCurrent} → ${update.latestColored}`)
659
+ }
660
+
661
+ // Add conflicts warning if any
662
+ if (plan.hasConflicts && plan.conflicts.length > 0) {
663
+ lines.push('')
664
+ lines.push(`⚠️ ${plan.conflicts.length} ${t('format.conflictsDetected')}`)
665
+ }
666
+
667
+ return lines.join('\n')
668
+ }
669
+
388
670
  /**
389
671
  * Format impact analysis as table
390
672
  */
@@ -392,35 +674,57 @@ export class OutputFormatter {
392
674
  const lines: string[] = []
393
675
 
394
676
  // Header
395
- lines.push(this.colorize(chalk.bold, `\n🔍 Impact Analysis: ${analysis.packageName}`))
396
- lines.push(this.colorize(chalk.gray, `Catalog: ${analysis.catalogName}`))
397
677
  lines.push(
398
- this.colorize(chalk.gray, `Update: ${analysis.currentVersion} → ${analysis.proposedVersion}`)
678
+ this.colorUtils.colorize(
679
+ chalk.bold,
680
+ `\n${t('format.impactAnalysis')}: ${analysis.packageName}`
681
+ )
682
+ )
683
+ lines.push(
684
+ this.colorUtils.colorize(chalk.gray, `${t('format.catalog')}: ${analysis.catalogName}`)
685
+ )
686
+ lines.push(
687
+ this.colorUtils.colorize(
688
+ chalk.gray,
689
+ `${t('format.updateInfo')}: ${analysis.currentVersion} → ${analysis.proposedVersion}`
690
+ )
691
+ )
692
+ lines.push(
693
+ this.colorUtils.colorize(chalk.gray, `${t('table.header.type')}: ${analysis.updateType}`)
399
694
  )
400
- lines.push(this.colorize(chalk.gray, `Type: ${analysis.updateType}`))
401
695
 
402
696
  // Risk level
403
- const riskColor = this.getRiskColor(analysis.riskLevel)
404
- lines.push(this.colorize(riskColor, `Risk Level: ${analysis.riskLevel.toUpperCase()}`))
697
+ const riskColor = this.colorUtils.getRiskColor(analysis.riskLevel)
698
+ lines.push(
699
+ this.colorUtils.colorize(
700
+ riskColor,
701
+ `${t('format.riskLevel')}: ${analysis.riskLevel.toUpperCase()}`
702
+ )
703
+ )
405
704
  lines.push('')
406
705
 
407
706
  // Affected packages
408
707
  if (analysis.affectedPackages.length > 0) {
409
- lines.push(this.colorize(chalk.bold, '📦 Affected Packages:'))
708
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.affectedPackages')}:`))
410
709
 
411
710
  const table = new Table({
412
- head: this.colorizeHeaders(['Package', 'Path', 'Dependency Type', 'Risk']),
711
+ head: this.colorUtils.colorizeHeaders([
712
+ t('table.header.package'),
713
+ t('table.header.path'),
714
+ t('table.header.dependencyType'),
715
+ t('table.header.risk'),
716
+ ]),
413
717
  style: { head: [], border: [] },
414
718
  colWidths: [20, 30, 15, 10],
415
719
  })
416
720
 
417
721
  for (const pkg of analysis.affectedPackages) {
418
- const riskColor = this.getRiskColor(pkg.compatibilityRisk)
722
+ const riskColor = this.colorUtils.getRiskColor(pkg.compatibilityRisk)
419
723
  table.push([
420
724
  pkg.packageName,
421
725
  pkg.packagePath,
422
726
  pkg.dependencyType,
423
- this.colorize(riskColor, pkg.compatibilityRisk),
727
+ this.colorUtils.colorize(riskColor, pkg.compatibilityRisk),
424
728
  ])
425
729
  }
426
730
 
@@ -430,22 +734,22 @@ export class OutputFormatter {
430
734
 
431
735
  // Security impact
432
736
  if (analysis.securityImpact.hasVulnerabilities) {
433
- lines.push(this.colorize(chalk.bold, '🔒 Security Impact:'))
737
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.securityImpact')}:`))
434
738
 
435
739
  if (analysis.securityImpact.fixedVulnerabilities > 0) {
436
740
  lines.push(
437
- this.colorize(
741
+ this.colorUtils.colorize(
438
742
  chalk.green,
439
- ` ✅ Fixes ${analysis.securityImpact.fixedVulnerabilities} vulnerabilities`
743
+ ` ✅ ${t('format.fixesVulns', { count: String(analysis.securityImpact.fixedVulnerabilities) })}`
440
744
  )
441
745
  )
442
746
  }
443
747
 
444
748
  if (analysis.securityImpact.newVulnerabilities > 0) {
445
749
  lines.push(
446
- this.colorize(
750
+ this.colorUtils.colorize(
447
751
  chalk.red,
448
- ` ⚠️ Introduces ${analysis.securityImpact.newVulnerabilities} vulnerabilities`
752
+ ` ⚠️ ${t('format.introducesVulns', { count: String(analysis.securityImpact.newVulnerabilities) })}`
449
753
  )
450
754
  )
451
755
  }
@@ -455,7 +759,7 @@ export class OutputFormatter {
455
759
 
456
760
  // Recommendations
457
761
  if (analysis.recommendations.length > 0) {
458
- lines.push(this.colorize(chalk.bold, '💡 Recommendations:'))
762
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.recommendations')}:`))
459
763
  for (const rec of analysis.recommendations) {
460
764
  lines.push(` ${rec}`)
461
765
  }
@@ -470,8 +774,8 @@ export class OutputFormatter {
470
774
  private formatImpactMinimal(analysis: ImpactAnalysis): string {
471
775
  return [
472
776
  `${analysis.packageName}: ${analysis.currentVersion} → ${analysis.proposedVersion}`,
473
- `Risk: ${analysis.riskLevel}`,
474
- `Affected: ${analysis.affectedPackages.length} packages`,
777
+ `${t('format.riskLevel')}: ${analysis.riskLevel}`,
778
+ `${t('format.affectedPackages')}: ${analysis.affectedPackages.length} ${t('format.packages')}`,
475
779
  ].join('\n')
476
780
  }
477
781
 
@@ -485,21 +789,28 @@ export class OutputFormatter {
485
789
  const statusIcon = report.isValid ? '✅' : '❌'
486
790
  const statusColor = report.isValid ? chalk.green : chalk.red
487
791
 
488
- lines.push(this.colorize(chalk.bold, `\n${statusIcon} Workspace Validation`))
489
- lines.push(this.colorize(statusColor, `Status: ${report.isValid ? 'VALID' : 'INVALID'}`))
792
+ lines.push(
793
+ this.colorUtils.colorize(chalk.bold, `\n${statusIcon} ${t('format.workspaceValidation')}`)
794
+ )
795
+ lines.push(
796
+ this.colorUtils.colorize(
797
+ statusColor,
798
+ `${t('format.status')}: ${report.isValid ? t('format.valid') : t('format.invalid')}`
799
+ )
800
+ )
490
801
  lines.push('')
491
802
 
492
803
  // Workspace info
493
- lines.push(this.colorize(chalk.bold, '📦 Workspace Information:'))
494
- lines.push(` Path: ${report.workspace.path}`)
495
- lines.push(` Name: ${report.workspace.name}`)
496
- lines.push(` Packages: ${report.workspace.packageCount}`)
497
- lines.push(` Catalogs: ${report.workspace.catalogCount}`)
804
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.workspaceInfo')}:`))
805
+ lines.push(` ${t('format.path')}: ${report.workspace.path}`)
806
+ lines.push(` ${t('format.name')}: ${report.workspace.name}`)
807
+ lines.push(` ${t('format.packages')}: ${report.workspace.packageCount}`)
808
+ lines.push(` ${t('format.catalogs')}: ${report.workspace.catalogCount}`)
498
809
  lines.push('')
499
810
 
500
811
  // Errors
501
812
  if (report.errors.length > 0) {
502
- lines.push(this.colorize(chalk.red, '❌ Errors:'))
813
+ lines.push(this.colorUtils.colorize(chalk.red, `❌ ${t('format.errors')}:`))
503
814
  for (const error of report.errors) {
504
815
  lines.push(` • ${error}`)
505
816
  }
@@ -508,7 +819,7 @@ export class OutputFormatter {
508
819
 
509
820
  // Warnings
510
821
  if (report.warnings.length > 0) {
511
- lines.push(this.colorize(chalk.yellow, '⚠️ Warnings:'))
822
+ lines.push(this.colorUtils.colorize(chalk.yellow, `⚠️ ${t('format.warnings')}:`))
512
823
  for (const warning of report.warnings) {
513
824
  lines.push(` • ${warning}`)
514
825
  }
@@ -517,7 +828,7 @@ export class OutputFormatter {
517
828
 
518
829
  // Recommendations
519
830
  if (report.recommendations.length > 0) {
520
- lines.push(this.colorize(chalk.blue, '💡 Recommendations:'))
831
+ lines.push(this.colorUtils.colorize(chalk.blue, `${t('format.recommendations')}:`))
521
832
  for (const rec of report.recommendations) {
522
833
  lines.push(` • ${rec}`)
523
834
  }
@@ -530,11 +841,124 @@ export class OutputFormatter {
530
841
  * Format validation report minimally
531
842
  */
532
843
  private formatValidationMinimal(report: WorkspaceValidationReport): string {
533
- const status = report.isValid ? 'VALID' : 'INVALID'
844
+ const status = report.isValid ? t('format.valid') : t('format.invalid')
534
845
  const errors = report.errors.length
535
846
  const warnings = report.warnings.length
536
847
 
537
- return `${status} (${errors} errors, ${warnings} warnings)`
848
+ return `${status} (${errors} ${t('format.errors')}, ${warnings} ${t('format.warnings')})`
849
+ }
850
+
851
+ /**
852
+ * Format workspace information as beautiful table
853
+ */
854
+ private formatWorkspaceInfoTable(info: WorkspaceInfo): string {
855
+ const lines: string[] = []
856
+
857
+ // Use cli-table3 for reliable alignment
858
+ const table = new Table({
859
+ chars: {
860
+ top: '─',
861
+ 'top-mid': '┬',
862
+ 'top-left': '╭',
863
+ 'top-right': '╮',
864
+ bottom: '─',
865
+ 'bottom-mid': '┴',
866
+ 'bottom-left': '╰',
867
+ 'bottom-right': '╯',
868
+ left: '│',
869
+ 'left-mid': '├',
870
+ mid: '─',
871
+ 'mid-mid': '┼',
872
+ right: '│',
873
+ 'right-mid': '┤',
874
+ middle: '│',
875
+ },
876
+ style: {
877
+ head: [],
878
+ border: this.useColor ? ['cyan'] : [],
879
+ 'padding-left': 1,
880
+ 'padding-right': 1,
881
+ },
882
+ colWidths: [20, 55],
883
+ wordWrap: true,
884
+ })
885
+
886
+ // Header row
887
+ table.push([
888
+ {
889
+ colSpan: 2,
890
+ content: this.colorUtils.colorize(chalk.bold.cyan, 'WORKSPACE'),
891
+ hAlign: 'center',
892
+ },
893
+ ])
894
+
895
+ // Status icon (default to valid if not specified)
896
+ const isValid = info.isValid ?? true
897
+ const statusIcon = isValid
898
+ ? this.colorUtils.colorize(chalk.green, '✓')
899
+ : this.colorUtils.colorize(chalk.red, '✗')
900
+ const statusText = isValid
901
+ ? this.colorUtils.colorize(chalk.green, 'Valid')
902
+ : this.colorUtils.colorize(chalk.red, 'Invalid')
903
+
904
+ // Data rows - clean, no emojis
905
+ table.push([
906
+ this.colorUtils.colorize(chalk.gray, 'Name'),
907
+ this.colorUtils.colorize(chalk.bold.white, info.name),
908
+ ])
909
+
910
+ table.push([
911
+ this.colorUtils.colorize(chalk.gray, 'Path'),
912
+ this.colorUtils.colorize(chalk.dim, info.path),
913
+ ])
914
+
915
+ table.push([this.colorUtils.colorize(chalk.gray, 'Status'), `${statusIcon} ${statusText}`])
916
+
917
+ table.push([
918
+ this.colorUtils.colorize(chalk.gray, 'Packages'),
919
+ this.colorUtils.colorize(
920
+ info.packageCount > 0 ? chalk.green : chalk.yellow,
921
+ String(info.packageCount)
922
+ ),
923
+ ])
924
+
925
+ table.push([
926
+ this.colorUtils.colorize(chalk.gray, 'Catalogs'),
927
+ this.colorUtils.colorize(
928
+ info.catalogCount > 0 ? chalk.green : chalk.yellow,
929
+ String(info.catalogCount)
930
+ ),
931
+ ])
932
+
933
+ const catalogNames = info.catalogNames ?? []
934
+ if (catalogNames.length > 0) {
935
+ const catalogTags = catalogNames
936
+ .map((name: string) => this.colorUtils.colorize(chalk.cyan, name))
937
+ .join(this.colorUtils.colorize(chalk.gray, ', '))
938
+
939
+ table.push([this.colorUtils.colorize(chalk.gray, 'Catalog Names'), catalogTags])
940
+ }
941
+
942
+ lines.push('')
943
+ lines.push(table.toString())
944
+ lines.push('')
945
+
946
+ return lines.join('\n')
947
+ }
948
+
949
+ /**
950
+ * Format workspace information minimally
951
+ */
952
+ private formatWorkspaceInfoMinimal(info: WorkspaceInfo): string {
953
+ const lines: string[] = []
954
+ lines.push(`${info.name} (${info.path})`)
955
+ lines.push(
956
+ `${t('command.workspace.packages')}: ${info.packageCount}, ${t('command.workspace.catalogs')}: ${info.catalogCount}`
957
+ )
958
+ if (info.catalogNames && info.catalogNames.length > 0) {
959
+ lines.push(`${t('command.workspace.catalogNames')}: ${info.catalogNames.join(', ')}`)
960
+ }
961
+ return lines.join('\n')
538
962
  }
539
963
 
540
964
  /**
@@ -543,26 +967,34 @@ export class OutputFormatter {
543
967
  private formatStatsTable(stats: WorkspaceStats): string {
544
968
  const lines: string[] = []
545
969
 
546
- lines.push(this.colorize(chalk.bold, `\n📊 Workspace Statistics`))
547
- lines.push(this.colorize(chalk.gray, `Workspace: ${stats.workspace.name}`))
970
+ lines.push(this.colorUtils.colorize(chalk.bold, `\n${t('format.workspaceStatistics')}`))
971
+ lines.push(
972
+ this.colorUtils.colorize(chalk.gray, `${t('format.workspace')}: ${stats.workspace.name}`)
973
+ )
548
974
  lines.push('')
549
975
 
550
976
  const table = new Table({
551
- head: this.colorizeHeaders(['Metric', 'Count']),
977
+ head: this.colorUtils.colorizeHeaders([t('table.header.metric'), t('table.header.count')]),
552
978
  style: { head: [], border: [] },
553
979
  colWidths: [30, 10],
554
980
  })
555
981
 
556
- table.push(['Total Packages', stats.packages.total.toString()])
557
- table.push(['Packages with Catalog Refs', stats.packages.withCatalogReferences.toString()])
558
- table.push(['Total Catalogs', stats.catalogs.total.toString()])
559
- table.push(['Catalog Entries', stats.catalogs.totalEntries.toString()])
560
- table.push(['Total Dependencies', stats.dependencies.total.toString()])
561
- table.push(['Catalog References', stats.dependencies.catalogReferences.toString()])
562
- table.push(['Dependencies', stats.dependencies.byType.dependencies.toString()])
563
- table.push(['Dev Dependencies', stats.dependencies.byType.devDependencies.toString()])
564
- table.push(['Peer Dependencies', stats.dependencies.byType.peerDependencies.toString()])
565
- table.push(['Optional Dependencies', stats.dependencies.byType.optionalDependencies.toString()])
982
+ table.push([t('stats.totalPackages'), stats.packages.total.toString()])
983
+ table.push([
984
+ t('stats.packagesWithCatalogRefs'),
985
+ stats.packages.withCatalogReferences.toString(),
986
+ ])
987
+ table.push([t('stats.totalCatalogs'), stats.catalogs.total.toString()])
988
+ table.push([t('stats.catalogEntries'), stats.catalogs.totalEntries.toString()])
989
+ table.push([t('stats.totalDependencies'), stats.dependencies.total.toString()])
990
+ table.push([t('stats.catalogReferences'), stats.dependencies.catalogReferences.toString()])
991
+ table.push([t('stats.dependencies'), stats.dependencies.byType.dependencies.toString()])
992
+ table.push([t('stats.devDependencies'), stats.dependencies.byType.devDependencies.toString()])
993
+ table.push([t('stats.peerDependencies'), stats.dependencies.byType.peerDependencies.toString()])
994
+ table.push([
995
+ t('stats.optionalDependencies'),
996
+ stats.dependencies.byType.optionalDependencies.toString(),
997
+ ])
566
998
 
567
999
  lines.push(table.toString())
568
1000
 
@@ -574,9 +1006,9 @@ export class OutputFormatter {
574
1006
  */
575
1007
  private formatStatsMinimal(stats: WorkspaceStats): string {
576
1008
  return [
577
- `Packages: ${stats.packages.total}`,
578
- `Catalogs: ${stats.catalogs.total}`,
579
- `Dependencies: ${stats.dependencies.total}`,
1009
+ `${t('format.packages')}: ${stats.packages.total}`,
1010
+ `${t('format.catalogs')}: ${stats.catalogs.total}`,
1011
+ `${t('stats.totalDependencies')}: ${stats.dependencies.total}`,
580
1012
  ].join(', ')
581
1013
  }
582
1014
 
@@ -587,31 +1019,59 @@ export class OutputFormatter {
587
1019
  const lines: string[] = []
588
1020
 
589
1021
  // Header
590
- lines.push(this.colorize(chalk.bold, '\n🔒 Security Report'))
591
- lines.push(this.colorize(chalk.gray, `Workspace: ${report.metadata.workspacePath}`))
1022
+ lines.push(this.colorUtils.colorize(chalk.bold, `\n${t('format.securityReport')}`))
592
1023
  lines.push(
593
- this.colorize(chalk.gray, `Scan Date: ${new Date(report.metadata.scanDate).toLocaleString()}`)
1024
+ this.colorUtils.colorize(
1025
+ chalk.gray,
1026
+ `${t('format.workspace')}: ${report.metadata.workspacePath}`
1027
+ )
1028
+ )
1029
+ lines.push(
1030
+ this.colorUtils.colorize(
1031
+ chalk.gray,
1032
+ `${t('format.scanDate')}: ${new Date(report.metadata.scanDate).toLocaleString()}`
1033
+ )
1034
+ )
1035
+ lines.push(
1036
+ this.colorUtils.colorize(
1037
+ chalk.gray,
1038
+ `${t('format.tools')}: ${report.metadata.scanTools.join(', ')}`
1039
+ )
594
1040
  )
595
- lines.push(this.colorize(chalk.gray, `Tools: ${report.metadata.scanTools.join(', ')}`))
596
1041
 
597
1042
  // Summary
598
1043
  lines.push('')
599
- lines.push(this.colorize(chalk.bold, '📊 Summary:'))
1044
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.summary')}:`))
600
1045
 
601
1046
  const summaryTable = new Table({
602
- head: this.colorizeHeaders(['Severity', 'Count']),
1047
+ head: this.colorUtils.colorizeHeaders([t('table.header.severity'), t('table.header.count')]),
603
1048
  style: { head: [], border: [] },
604
1049
  colWidths: [15, 10],
605
1050
  })
606
1051
 
607
- summaryTable.push(['Critical', this.colorize(chalk.red, report.summary.critical.toString())])
608
- summaryTable.push(['High', this.colorize(chalk.yellow, report.summary.high.toString())])
609
- summaryTable.push(['Moderate', this.colorize(chalk.blue, report.summary.moderate.toString())])
610
- summaryTable.push(['Low', this.colorize(chalk.green, report.summary.low.toString())])
611
- summaryTable.push(['Info', this.colorize(chalk.gray, report.summary.info.toString())])
612
1052
  summaryTable.push([
613
- 'Total',
614
- this.colorize(chalk.bold, report.summary.totalVulnerabilities.toString()),
1053
+ t('severity.critical'),
1054
+ this.colorUtils.colorize(chalk.red, report.summary.critical.toString()),
1055
+ ])
1056
+ summaryTable.push([
1057
+ t('severity.high'),
1058
+ this.colorUtils.colorize(chalk.yellow, report.summary.high.toString()),
1059
+ ])
1060
+ summaryTable.push([
1061
+ t('severity.moderate'),
1062
+ this.colorUtils.colorize(chalk.blue, report.summary.moderate.toString()),
1063
+ ])
1064
+ summaryTable.push([
1065
+ t('severity.low'),
1066
+ this.colorUtils.colorize(chalk.green, report.summary.low.toString()),
1067
+ ])
1068
+ summaryTable.push([
1069
+ t('severity.info'),
1070
+ this.colorUtils.colorize(chalk.gray, report.summary.info.toString()),
1071
+ ])
1072
+ summaryTable.push([
1073
+ t('severity.total'),
1074
+ this.colorUtils.colorize(chalk.bold, report.summary.totalVulnerabilities.toString()),
615
1075
  ])
616
1076
 
617
1077
  lines.push(summaryTable.toString())
@@ -619,25 +1079,30 @@ export class OutputFormatter {
619
1079
  // Vulnerabilities
620
1080
  if (report.vulnerabilities.length > 0) {
621
1081
  lines.push('')
622
- lines.push(this.colorize(chalk.bold, '🐛 Vulnerabilities:'))
1082
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.vulnerabilities')}:`))
623
1083
 
624
1084
  const vulnTable = new Table({
625
- head: this.colorizeHeaders(['Package', 'Severity', 'Title', 'Fix Available']),
1085
+ head: this.colorUtils.colorizeHeaders([
1086
+ t('table.header.package'),
1087
+ t('table.header.severity'),
1088
+ t('table.header.title'),
1089
+ t('table.header.fixAvailable'),
1090
+ ]),
626
1091
  style: { head: [], border: [] },
627
1092
  colWidths: [20, 12, 40, 15],
628
1093
  })
629
1094
 
630
1095
  for (const vuln of report.vulnerabilities) {
631
- const severityColor = this.getSeverityColor(vuln.severity)
1096
+ const severityColor = this.colorUtils.getSeverityColor(vuln.severity)
632
1097
  const fixStatus = vuln.fixAvailable
633
1098
  ? typeof vuln.fixAvailable === 'string'
634
1099
  ? vuln.fixAvailable
635
- : 'Yes'
636
- : 'No'
1100
+ : t('common.yes')
1101
+ : t('common.no')
637
1102
 
638
1103
  vulnTable.push([
639
1104
  vuln.package,
640
- this.colorize(severityColor, vuln.severity.toUpperCase()),
1105
+ this.colorUtils.colorize(severityColor, vuln.severity.toUpperCase()),
641
1106
  vuln.title.length > 35 ? `${vuln.title.substring(0, 35)}...` : vuln.title,
642
1107
  fixStatus,
643
1108
  ])
@@ -649,7 +1114,7 @@ export class OutputFormatter {
649
1114
  // Recommendations
650
1115
  if (report.recommendations.length > 0) {
651
1116
  lines.push('')
652
- lines.push(this.colorize(chalk.bold, '💡 Recommendations:'))
1117
+ lines.push(this.colorUtils.colorize(chalk.bold, `${t('format.recommendations')}:`))
653
1118
 
654
1119
  for (const rec of report.recommendations) {
655
1120
  lines.push(` ${rec.package}: ${rec.currentVersion} → ${rec.recommendedVersion}`)
@@ -666,147 +1131,18 @@ export class OutputFormatter {
666
1131
  private formatSecurityMinimal(report: SecurityReport): string {
667
1132
  const vulnerabilities = report.summary.totalVulnerabilities
668
1133
  if (vulnerabilities === 0) {
669
- return 'No vulnerabilities found'
1134
+ return t('format.noVulnsFound')
670
1135
  }
671
1136
 
672
1137
  return [
673
- `${vulnerabilities} vulnerabilities found:`,
674
- ` Critical: ${report.summary.critical}`,
675
- ` High: ${report.summary.high}`,
676
- ` Moderate: ${report.summary.moderate}`,
677
- ` Low: ${report.summary.low}`,
1138
+ `${t('format.vulnerabilities')}: ${vulnerabilities}`,
1139
+ ` ${t('severity.critical')}: ${report.summary.critical}`,
1140
+ ` ${t('severity.high')}: ${report.summary.high}`,
1141
+ ` ${t('severity.moderate')}: ${report.summary.moderate}`,
1142
+ ` ${t('severity.low')}: ${report.summary.low}`,
678
1143
  ].join('\n')
679
1144
  }
680
1145
 
681
- /**
682
- * Get color for severity level
683
- */
684
- private getSeverityColor(severity: string): typeof chalk {
685
- switch (severity.toLowerCase()) {
686
- case 'critical':
687
- return chalk.red
688
- case 'high':
689
- return chalk.yellow
690
- case 'moderate':
691
- return chalk.blue
692
- case 'low':
693
- return chalk.green
694
- default:
695
- return chalk.gray
696
- }
697
- }
698
-
699
- /**
700
- * Apply color if color is enabled
701
- */
702
- private colorize(colorFn: typeof chalk, text: string): string {
703
- return this.useColor ? colorFn(text) : text
704
- }
705
-
706
- /**
707
- * Colorize table headers
708
- */
709
- private colorizeHeaders(headers: string[]): string[] {
710
- return this.useColor ? headers.map((h) => chalk.bold.cyan(h)) : headers
711
- }
712
-
713
- /**
714
- * Get color for update type
715
- */
716
- private getUpdateTypeColor(updateType: string): typeof chalk {
717
- switch (updateType) {
718
- case 'major':
719
- return chalk.red
720
- case 'minor':
721
- return chalk.yellow
722
- case 'patch':
723
- return chalk.green
724
- default:
725
- return chalk.gray
726
- }
727
- }
728
-
729
- /**
730
- * Colorize version differences between current and latest
731
- */
732
- private colorizeVersionDiff(
733
- current: string,
734
- latest: string,
735
- updateType: string
736
- ): {
737
- currentColored: string
738
- latestColored: string
739
- } {
740
- if (!this.useColor) {
741
- return { currentColored: current, latestColored: latest }
742
- }
743
-
744
- // Parse version numbers to identify different parts
745
- const parseVersion = (version: string) => {
746
- // Remove leading ^ or ~ or other prefix characters
747
- const cleanVersion = version.replace(/^[\^~>=<]+/, '')
748
- const parts = cleanVersion.split('.')
749
- return {
750
- major: parts[0] || '0',
751
- minor: parts[1] || '0',
752
- patch: parts[2] || '0',
753
- extra: parts.slice(3).join('.'),
754
- prefix: version.substring(0, version.length - cleanVersion.length),
755
- }
756
- }
757
-
758
- const currentParts = parseVersion(current)
759
- const latestParts = parseVersion(latest)
760
-
761
- // Determine color based on update type for highlighting differences
762
- const diffColor = this.getUpdateTypeColor(updateType)
763
-
764
- // Build colored version strings by comparing each part
765
- const colorCurrentPart = (part: string, latestPart: string, isChanged: boolean) => {
766
- if (isChanged && part !== latestPart) {
767
- return chalk.dim.white(part) // Dim white for old version part
768
- }
769
- return chalk.white(part) // Unchanged parts in white
770
- }
771
-
772
- const colorLatestPart = (part: string, currentPart: string, isChanged: boolean) => {
773
- if (isChanged && part !== currentPart) {
774
- return diffColor(part) // Highlight the new version part with update type color
775
- }
776
- return chalk.white(part) // Unchanged parts in white
777
- }
778
-
779
- // Check which parts are different
780
- const majorChanged = currentParts.major !== latestParts.major
781
- const minorChanged = currentParts.minor !== latestParts.minor
782
- const patchChanged = currentParts.patch !== latestParts.patch
783
- const extraChanged = currentParts.extra !== latestParts.extra
784
-
785
- // Build colored current version
786
- let currentColored = currentParts.prefix
787
- currentColored += colorCurrentPart(currentParts.major, latestParts.major, majorChanged)
788
- currentColored += '.'
789
- currentColored += colorCurrentPart(currentParts.minor, latestParts.minor, minorChanged)
790
- currentColored += '.'
791
- currentColored += colorCurrentPart(currentParts.patch, latestParts.patch, patchChanged)
792
- if (currentParts.extra) {
793
- currentColored += `.${colorCurrentPart(currentParts.extra, latestParts.extra, extraChanged)}`
794
- }
795
-
796
- // Build colored latest version
797
- let latestColored = latestParts.prefix
798
- latestColored += colorLatestPart(latestParts.major, currentParts.major, majorChanged)
799
- latestColored += '.'
800
- latestColored += colorLatestPart(latestParts.minor, currentParts.minor, minorChanged)
801
- latestColored += '.'
802
- latestColored += colorLatestPart(latestParts.patch, currentParts.patch, patchChanged)
803
- if (latestParts.extra) {
804
- latestColored += `.${colorLatestPart(latestParts.extra, currentParts.extra, extraChanged)}`
805
- }
806
-
807
- return { currentColored, latestColored }
808
- }
809
-
810
1146
  /**
811
1147
  * Format AI analysis result
812
1148
  */
@@ -840,37 +1176,45 @@ export class OutputFormatter {
840
1176
 
841
1177
  lines.push('')
842
1178
  lines.push(headerColor('═══════════════════════════════════════════════════════════════'))
843
- lines.push(headerColor(' 🤖 AI Analysis Report'))
1179
+ lines.push(headerColor(` ${t('aiReport.title')}`))
844
1180
  lines.push(headerColor('═══════════════════════════════════════════════════════════════'))
845
1181
  lines.push('')
846
1182
 
847
1183
  // Provider and analysis info
848
- lines.push(`${infoColor('Provider:')} ${providerColor(aiResult.provider)}`)
849
- lines.push(`${infoColor('Analysis Type:')} ${aiResult.analysisType}`)
850
- lines.push(`${infoColor('Confidence:')} ${this.formatConfidence(aiResult.confidence)}`)
1184
+ lines.push(`${infoColor(t('aiReport.provider'))} ${providerColor(aiResult.provider)}`)
1185
+ lines.push(`${infoColor(t('aiReport.analysisType'))} ${aiResult.analysisType}`)
1186
+ lines.push(
1187
+ `${infoColor(t('aiReport.confidence'))} ${this.colorUtils.formatConfidence(aiResult.confidence)}`
1188
+ )
851
1189
  lines.push('')
852
1190
 
853
1191
  // Summary
854
- lines.push(headerColor('📋 Summary'))
1192
+ lines.push(headerColor(t('aiReport.summary')))
855
1193
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
856
1194
  lines.push(aiResult.summary)
857
1195
  lines.push('')
858
1196
 
859
1197
  // Recommendations table
860
1198
  if (aiResult.recommendations.length > 0) {
861
- lines.push(headerColor('💡 Recommendations'))
1199
+ lines.push(headerColor(t('aiReport.recommendations')))
862
1200
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
863
1201
 
864
1202
  const table = new Table({
865
- head: ['Package', 'Version Change', 'Action', 'Risk', 'Reason'],
1203
+ head: [
1204
+ t('aiReport.tablePackage'),
1205
+ t('aiReport.tableVersionChange'),
1206
+ t('aiReport.tableAction'),
1207
+ t('aiReport.tableRisk'),
1208
+ t('aiReport.tableReason'),
1209
+ ],
866
1210
  style: { head: this.useColor ? ['cyan'] : [] },
867
1211
  colWidths: [20, 20, 10, 10, 35],
868
1212
  wordWrap: true,
869
1213
  })
870
1214
 
871
1215
  for (const rec of aiResult.recommendations) {
872
- const riskColor = this.getRiskColor(rec.riskLevel)
873
- const actionColor = this.getActionColor(rec.action)
1216
+ const riskColor = this.colorUtils.getRiskColor(rec.riskLevel)
1217
+ const actionColor = this.colorUtils.getActionColor(rec.action)
874
1218
 
875
1219
  table.push([
876
1220
  rec.package,
@@ -891,7 +1235,7 @@ export class OutputFormatter {
891
1235
  .flatMap((r) => r.breakingChanges || [])
892
1236
 
893
1237
  if (allBreakingChanges.length > 0) {
894
- lines.push(errorColor('⚠️ Breaking Changes'))
1238
+ lines.push(errorColor(t('aiReport.breakingChanges')))
895
1239
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
896
1240
  for (const change of allBreakingChanges) {
897
1241
  lines.push(` ${warningColor('•')} ${change}`)
@@ -905,7 +1249,7 @@ export class OutputFormatter {
905
1249
  .flatMap((r) => r.securityFixes || [])
906
1250
 
907
1251
  if (allSecurityFixes.length > 0) {
908
- lines.push(successColor('🔒 Security Fixes'))
1252
+ lines.push(successColor(t('aiReport.securityFixes')))
909
1253
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
910
1254
  for (const fix of allSecurityFixes) {
911
1255
  lines.push(` ${successColor('•')} ${fix}`)
@@ -915,7 +1259,7 @@ export class OutputFormatter {
915
1259
 
916
1260
  // Warnings
917
1261
  if (aiResult.warnings && aiResult.warnings.length > 0) {
918
- lines.push(warningColor('⚡ Warnings'))
1262
+ lines.push(warningColor(t('aiReport.warnings')))
919
1263
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
920
1264
  for (const warning of aiResult.warnings) {
921
1265
  lines.push(` ${warningColor('•')} ${warning}`)
@@ -925,7 +1269,7 @@ export class OutputFormatter {
925
1269
 
926
1270
  // Details
927
1271
  if (aiResult.details) {
928
- lines.push(infoColor('📝 Details'))
1272
+ lines.push(infoColor(t('aiReport.details')))
929
1273
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
930
1274
  lines.push(aiResult.details)
931
1275
  lines.push('')
@@ -933,7 +1277,7 @@ export class OutputFormatter {
933
1277
 
934
1278
  // Basic analysis info (if provided)
935
1279
  if (basicAnalysis) {
936
- lines.push(mutedColor('📦 Affected Packages'))
1280
+ lines.push(mutedColor(t('aiReport.affectedPackages')))
937
1281
  lines.push(headerColor('───────────────────────────────────────────────────────────────'))
938
1282
  if (basicAnalysis.affectedPackages.length > 0) {
939
1283
  for (const pkg of basicAnalysis.affectedPackages.slice(0, 10)) {
@@ -945,11 +1289,11 @@ export class OutputFormatter {
945
1289
  }
946
1290
  if (basicAnalysis.affectedPackages.length > 10) {
947
1291
  lines.push(
948
- ` ${mutedColor(` ... and ${basicAnalysis.affectedPackages.length - 10} more`)}`
1292
+ ` ${mutedColor(` ${t('aiReport.andMore', { count: basicAnalysis.affectedPackages.length - 10 })}`)}`
949
1293
  )
950
1294
  }
951
1295
  } else {
952
- lines.push(` ${mutedColor('No packages directly affected')}`)
1296
+ lines.push(` ${mutedColor(t('aiReport.noPackagesAffected'))}`)
953
1297
  }
954
1298
  lines.push('')
955
1299
  }
@@ -960,12 +1304,12 @@ export class OutputFormatter {
960
1304
  aiResult.timestamp instanceof Date
961
1305
  ? aiResult.timestamp.toISOString()
962
1306
  : String(aiResult.timestamp)
963
- lines.push(mutedColor(`Generated at: ${timestamp}`))
1307
+ lines.push(mutedColor(t('aiReport.generatedAt', { timestamp })))
964
1308
  if (aiResult.processingTimeMs) {
965
- lines.push(mutedColor(`Processing time: ${aiResult.processingTimeMs}ms`))
1309
+ lines.push(mutedColor(t('aiReport.processingTime', { time: aiResult.processingTimeMs })))
966
1310
  }
967
1311
  if (aiResult.tokensUsed) {
968
- lines.push(mutedColor(`Tokens used: ${aiResult.tokensUsed}`))
1312
+ lines.push(mutedColor(t('aiReport.tokensUsed', { tokens: aiResult.tokensUsed })))
969
1313
  }
970
1314
  lines.push('')
971
1315
 
@@ -991,65 +1335,4 @@ export class OutputFormatter {
991
1335
 
992
1336
  return lines.join('\n')
993
1337
  }
994
-
995
- /**
996
- * Format confidence score with color
997
- */
998
- private formatConfidence(confidence: number): string {
999
- const percentage = Math.round(confidence * 100)
1000
- const bar =
1001
- '█'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10))
1002
-
1003
- if (!this.useColor) {
1004
- return `${bar} ${percentage}%`
1005
- }
1006
-
1007
- if (confidence >= 0.8) {
1008
- return chalk.green(`${bar} ${percentage}%`)
1009
- } else if (confidence >= 0.5) {
1010
- return chalk.yellow(`${bar} ${percentage}%`)
1011
- } else {
1012
- return chalk.red(`${bar} ${percentage}%`)
1013
- }
1014
- }
1015
-
1016
- /**
1017
- * Get color for risk level
1018
- */
1019
- private getRiskColor(riskLevel: string): typeof chalk {
1020
- if (!this.useColor) return chalk
1021
-
1022
- switch (riskLevel) {
1023
- case 'critical':
1024
- return chalk.red.bold
1025
- case 'high':
1026
- return chalk.red
1027
- case 'medium':
1028
- return chalk.yellow
1029
- case 'low':
1030
- return chalk.green
1031
- default:
1032
- return chalk.gray
1033
- }
1034
- }
1035
-
1036
- /**
1037
- * Get color for action
1038
- */
1039
- private getActionColor(action: string): typeof chalk {
1040
- if (!this.useColor) return chalk
1041
-
1042
- switch (action) {
1043
- case 'update':
1044
- return chalk.green
1045
- case 'wait':
1046
- return chalk.yellow
1047
- case 'skip':
1048
- return chalk.red
1049
- case 'review':
1050
- return chalk.cyan
1051
- default:
1052
- return chalk.white
1053
- }
1054
- }
1055
1338
  }