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
@@ -5,6 +5,7 @@
5
5
  * with multiple styles and themes.
6
6
  */
7
7
 
8
+ import { t } from '@pcu/utils'
8
9
  import chalk from 'chalk'
9
10
 
10
11
  export interface ProgressBarOptions {
@@ -24,7 +25,7 @@ export class ProgressBar {
24
25
  private showSpeed: boolean
25
26
 
26
27
  constructor(options: ProgressBarOptions = {}) {
27
- this.text = options.text || 'Processing...'
28
+ this.text = options.text || t('progress.processing')
28
29
  this.total = options.total || 0
29
30
  this.style = options.style || 'default'
30
31
  this.showSpeed = options.showSpeed ?? true
@@ -37,9 +38,6 @@ export class ProgressBar {
37
38
  this.text = text || this.text
38
39
  this.startTime = Date.now()
39
40
 
40
- // 在开始新进度条前,彻底清理可能的残留内容
41
- this.clearPreviousOutput()
42
-
43
41
  // 强制使用percentageBar,即使没有total也要创建
44
42
  // 这样可以避免spinner导致的冲突问题
45
43
  const effectiveTotal = this.total > 0 ? this.total : 1 // 如果没有total,设为1避免除零错误
@@ -86,7 +84,9 @@ export class ProgressBar {
86
84
  // 只使用percentageBar,不使用spinner
87
85
  if (this.percentageBar) {
88
86
  const successText = text || this.getCompletionText()
87
+ // complete() will clear the progress bar output
89
88
  this.percentageBar.complete(successText)
89
+ // Now print the success message on a clean line
90
90
  console.log(this.getSuccessMessage(successText))
91
91
  this.percentageBar = null
92
92
  }
@@ -99,6 +99,9 @@ export class ProgressBar {
99
99
  // 只使用percentageBar,不使用spinner
100
100
  if (this.percentageBar) {
101
101
  const failText = text || this.getFailureText()
102
+ // Clear the progress bar first
103
+ this.percentageBar.complete(failText)
104
+ // Now print the failure message on a clean line
102
105
  console.log(this.getFailureMessage(failText))
103
106
  this.percentageBar = null
104
107
  }
@@ -111,15 +114,15 @@ export class ProgressBar {
111
114
  const elapsed = this.getElapsedTime()
112
115
  switch (this.style) {
113
116
  case 'gradient':
114
- return `${chalk.magenta.bold('')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
117
+ return `${chalk.magenta.bold('*')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
115
118
  case 'fancy':
116
- return `${chalk.cyan('🎉')} ${chalk.green.bold(text)} ${chalk.cyan('🎉')} ${chalk.gray(elapsed)}`
119
+ return `${chalk.cyan('*')} ${chalk.green.bold(text)} ${chalk.cyan('*')} ${chalk.gray(elapsed)}`
117
120
  case 'minimal':
118
121
  return chalk.green(text)
119
122
  case 'rainbow':
120
- return `${chalk.magenta('🌈')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
123
+ return `${chalk.magenta('~')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
121
124
  case 'neon':
122
- return `${chalk.green.bold('⚡ SUCCESS')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
125
+ return `${chalk.green.bold(`⚡ ${t('progress.success').toUpperCase()}`)} ${chalk.green(text)} ${chalk.gray(elapsed)}`
123
126
  default:
124
127
  return `${chalk.green('✅')} ${chalk.green(text)} ${chalk.gray(elapsed)}`
125
128
  }
@@ -132,15 +135,15 @@ export class ProgressBar {
132
135
  const elapsed = this.getElapsedTime()
133
136
  switch (this.style) {
134
137
  case 'gradient':
135
- return `${chalk.red.bold('💥')} ${chalk.red(text)} ${chalk.gray(elapsed)}`
138
+ return `${chalk.red.bold('!!')} ${chalk.red(text)} ${chalk.gray(elapsed)}`
136
139
  case 'fancy':
137
- return `${chalk.red('💔')} ${chalk.red.bold(text)} ${chalk.red('💔')} ${chalk.gray(elapsed)}`
140
+ return `${chalk.red('!!')} ${chalk.red.bold(text)} ${chalk.red('!!')} ${chalk.gray(elapsed)}`
138
141
  case 'minimal':
139
142
  return chalk.red(text)
140
143
  case 'rainbow':
141
144
  return `${chalk.red('⚠️')} ${chalk.red(text)} ${chalk.gray(elapsed)}`
142
145
  case 'neon':
143
- return `${chalk.red.bold('⚡ ERROR')} ${chalk.red(text)} ${chalk.gray(elapsed)}`
146
+ return `${chalk.red.bold(`⚡ ${t('progress.error').toUpperCase()}`)} ${chalk.red(text)} ${chalk.gray(elapsed)}`
144
147
  default:
145
148
  return `${chalk.red('❌')} ${chalk.red(text)} ${chalk.gray(elapsed)}`
146
149
  }
@@ -152,14 +155,14 @@ export class ProgressBar {
152
155
  private getCompletionText(): string {
153
156
  const elapsed = this.getElapsedTime()
154
157
  const speed = this.getAverageSpeed()
155
- return `${this.text} completed ${speed} ${elapsed}`
158
+ return `${this.text} ${t('progress.completed')} ${speed} ${elapsed}`
156
159
  }
157
160
 
158
161
  /**
159
162
  * Get failure text
160
163
  */
161
164
  private getFailureText(): string {
162
- return `${this.text} failed`
165
+ return `${this.text} ${t('progress.failed')}`
163
166
  }
164
167
 
165
168
  /**
@@ -189,6 +192,9 @@ export class ProgressBar {
189
192
  // 只使用percentageBar,不使用spinner
190
193
  if (this.percentageBar) {
191
194
  const warnText = text || this.text
195
+ // Clear the progress bar first
196
+ this.percentageBar.complete(warnText)
197
+ // Now print the warning message on a clean line
192
198
  console.log(this.getWarningMessage(warnText))
193
199
  this.percentageBar = null
194
200
  }
@@ -201,6 +207,9 @@ export class ProgressBar {
201
207
  // 只使用percentageBar,不使用spinner
202
208
  if (this.percentageBar) {
203
209
  const infoText = text || this.text
210
+ // Clear the progress bar first
211
+ this.percentageBar.complete(infoText)
212
+ // Now print the info message on a clean line
204
213
  console.log(this.getInfoMessage(infoText))
205
214
  this.percentageBar = null
206
215
  }
@@ -213,7 +222,7 @@ export class ProgressBar {
213
222
  const elapsed = this.getElapsedTime()
214
223
  switch (this.style) {
215
224
  case 'gradient':
216
- return `${chalk.yellow.bold('')} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
225
+ return `${chalk.yellow.bold('!')} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
217
226
  case 'fancy':
218
227
  return `${chalk.yellow('⚠️')} ${chalk.yellow.bold(text)} ${chalk.yellow('⚠️')} ${chalk.gray(elapsed)}`
219
228
  case 'minimal':
@@ -221,7 +230,7 @@ export class ProgressBar {
221
230
  case 'rainbow':
222
231
  return `${chalk.yellow('⚠️')} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
223
232
  case 'neon':
224
- return `${chalk.yellow.bold('⚡ WARNING')} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
233
+ return `${chalk.yellow.bold(`⚡ ${t('progress.warning').toUpperCase()}`)} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
225
234
  default:
226
235
  return `${chalk.yellow('⚠️')} ${chalk.yellow(text)} ${chalk.gray(elapsed)}`
227
236
  }
@@ -234,17 +243,17 @@ export class ProgressBar {
234
243
  const elapsed = this.getElapsedTime()
235
244
  switch (this.style) {
236
245
  case 'gradient':
237
- return `${chalk.blue.bold('ℹ️')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
246
+ return `${chalk.blue.bold('[i]')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
238
247
  case 'fancy':
239
- return `${chalk.blue('💡')} ${chalk.blue.bold(text)} ${chalk.blue('💡')} ${chalk.gray(elapsed)}`
248
+ return `${chalk.blue('[i]')} ${chalk.blue.bold(text)} ${chalk.blue('[i]')} ${chalk.gray(elapsed)}`
240
249
  case 'minimal':
241
250
  return chalk.blue(text)
242
251
  case 'rainbow':
243
- return `${chalk.blue('ℹ️')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
252
+ return `${chalk.blue('[i]')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
244
253
  case 'neon':
245
- return `${chalk.blue.bold('⚡ INFO')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
254
+ return `${chalk.blue.bold(`[i] ${t('progress.info').toUpperCase()}`)} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
246
255
  default:
247
- return `${chalk.blue('ℹ️')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
256
+ return `${chalk.blue('[i]')} ${chalk.blue(text)} ${chalk.gray(elapsed)}`
248
257
  }
249
258
  }
250
259
 
@@ -254,22 +263,12 @@ export class ProgressBar {
254
263
  stop(): void {
255
264
  // 只使用percentageBar,不使用spinner
256
265
  if (this.percentageBar) {
266
+ // Clear the progress bar before stopping
267
+ this.percentageBar.complete('')
257
268
  this.percentageBar = null
258
269
  }
259
270
  }
260
271
 
261
- /**
262
- * Clear previous output to prevent residual progress bars
263
- */
264
- private clearPreviousOutput(): void {
265
- // 清理可能的残留进度条显示(最多清理5行,应该足够了)
266
- for (let i = 0; i < 5; i++) {
267
- process.stdout.write('\x1b[1A\r\x1b[K') // 上移一行并清除
268
- }
269
- // 确保光标在正确位置
270
- process.stdout.write('\r')
271
- }
272
-
273
272
  /**
274
273
  * Create a multi-step progress indicator
275
274
  */
@@ -345,7 +344,7 @@ export class MultiStepProgress {
345
344
  }
346
345
 
347
346
  start(): void {
348
- console.log(chalk.bold('\n📋 Progress Steps:\n'))
347
+ console.log(chalk.bold(`\n📋 ${t('progress.steps')}:\n`))
349
348
  this.renderSteps()
350
349
  }
351
350
 
@@ -362,7 +361,7 @@ export class MultiStepProgress {
362
361
  }
363
362
 
364
363
  complete(): void {
365
- console.log(chalk.green('\n🎉 All steps completed!\n'))
364
+ console.log(chalk.green(`\n🎉 ${t('progress.allStepsCompleted')}\n`))
366
365
  }
367
366
 
368
367
  private renderSteps(): void {
@@ -403,24 +402,9 @@ export class PercentageProgressBar {
403
402
  this.startTime = Date.now()
404
403
  this.isFirstRender = true // 重置首次渲染标记
405
404
 
406
- // 清理可能的残留输出
407
- this.clearPreviousLines()
408
-
409
405
  this.render()
410
406
  }
411
407
 
412
- /**
413
- * Clear any previous output lines to prevent conflicts
414
- */
415
- private clearPreviousLines(): void {
416
- // 更强力的清理:清理多行可能的残留内容
417
- for (let i = 0; i < 6; i++) {
418
- process.stdout.write('\x1b[1A\r\x1b[2K') // 上移一行并完全清除该行
419
- }
420
- // 回到起始位置
421
- process.stdout.write('\r')
422
- }
423
-
424
408
  update(current: number, text?: string): void {
425
409
  this.current = current
426
410
  if (text) this.text = text
@@ -436,8 +420,15 @@ export class PercentageProgressBar {
436
420
  complete(text?: string): void {
437
421
  this.current = this.total
438
422
  if (text) this.text = text
439
- this.render()
440
- console.log('') // New line after completion
423
+
424
+ // Clear the multi-line progress bar output completely before final state
425
+ if (this.useMultiLine && !this.isFirstRender) {
426
+ // Move up 2 lines (text line + progress bar line) and clear them
427
+ process.stdout.write('\x1b[2A\r\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r')
428
+ }
429
+
430
+ // Don't render anymore - just let the caller handle the final message
431
+ this.lastRender = ''
441
432
  }
442
433
 
443
434
  private render(): void {
@@ -678,7 +669,7 @@ export class BatchProgressManager {
678
669
 
679
670
  console.log(
680
671
  chalk.cyan(
681
- `📊 Overall Progress: ${percentage}% (${this.completedOperations}/${this.totalOperations})`
672
+ `📊 ${t('progress.overallProgress')}: ${percentage}% (${this.completedOperations}/${this.totalOperations})`
682
673
  )
683
674
  )
684
675
  if (text) {
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Version Formatter Utilities
3
+ *
4
+ * Provides version parsing and colorization utilities for CLI output.
5
+ * Extracted from OutputFormatter for better maintainability.
6
+ */
7
+
8
+ import chalk from 'chalk'
9
+ import type { ColorUtils } from './colorUtils.js'
10
+
11
+ export interface VersionParts {
12
+ major: string
13
+ minor: string
14
+ patch: string
15
+ extra: string
16
+ prefix: string
17
+ }
18
+
19
+ export interface ColorizedVersions {
20
+ currentColored: string
21
+ latestColored: string
22
+ }
23
+
24
+ /**
25
+ * Version formatting utilities for CLI output
26
+ */
27
+ export class VersionFormatter {
28
+ constructor(
29
+ private readonly colorUtils: ColorUtils,
30
+ private readonly useColor: boolean = true
31
+ ) {}
32
+
33
+ /**
34
+ * Parse a version string into its component parts
35
+ */
36
+ parseVersion(version: string): VersionParts {
37
+ // Remove leading ^ or ~ or other prefix characters
38
+ const cleanVersion = version.replace(/^[\^~>=<]+/, '')
39
+ const parts = cleanVersion.split('.')
40
+ return {
41
+ major: parts[0] || '0',
42
+ minor: parts[1] || '0',
43
+ patch: parts[2] || '0',
44
+ extra: parts.slice(3).join('.'),
45
+ prefix: version.substring(0, version.length - cleanVersion.length),
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Colorize version differences between current and latest
51
+ */
52
+ colorizeVersionDiff(current: string, latest: string, updateType: string): ColorizedVersions {
53
+ if (!this.useColor) {
54
+ return { currentColored: current, latestColored: latest }
55
+ }
56
+
57
+ const currentParts = this.parseVersion(current)
58
+ const latestParts = this.parseVersion(latest)
59
+
60
+ // Determine color based on update type for highlighting differences
61
+ const diffColor = this.colorUtils.getUpdateTypeColor(updateType)
62
+
63
+ // Check which parts are different
64
+ const majorChanged = currentParts.major !== latestParts.major
65
+ const minorChanged = currentParts.minor !== latestParts.minor
66
+ const patchChanged = currentParts.patch !== latestParts.patch
67
+ const extraChanged = currentParts.extra !== latestParts.extra
68
+
69
+ // Build colored version strings
70
+ const currentColored = this.buildColoredVersion(
71
+ currentParts,
72
+ latestParts,
73
+ { majorChanged, minorChanged, patchChanged, extraChanged },
74
+ true
75
+ )
76
+
77
+ const latestColored = this.buildColoredVersion(
78
+ latestParts,
79
+ currentParts,
80
+ { majorChanged, minorChanged, patchChanged, extraChanged },
81
+ false,
82
+ diffColor
83
+ )
84
+
85
+ return { currentColored, latestColored }
86
+ }
87
+
88
+ /**
89
+ * Build a colored version string
90
+ */
91
+ private buildColoredVersion(
92
+ parts: VersionParts,
93
+ compareParts: VersionParts,
94
+ changes: {
95
+ majorChanged: boolean
96
+ minorChanged: boolean
97
+ patchChanged: boolean
98
+ extraChanged: boolean
99
+ },
100
+ isCurrent: boolean,
101
+ diffColor?: typeof chalk
102
+ ): string {
103
+ const colorPart = (part: string, comparePart: string, isChanged: boolean) => {
104
+ if (isCurrent) {
105
+ // For current version: dim white for changed parts
106
+ if (isChanged && part !== comparePart) {
107
+ return chalk.dim.white(part)
108
+ }
109
+ return chalk.white(part)
110
+ } else {
111
+ // For latest version: highlight changed parts with update type color
112
+ if (isChanged && part !== comparePart && diffColor) {
113
+ return diffColor(part)
114
+ }
115
+ return chalk.white(part)
116
+ }
117
+ }
118
+
119
+ let result = parts.prefix
120
+ result += colorPart(parts.major, compareParts.major, changes.majorChanged)
121
+ result += '.'
122
+ result += colorPart(parts.minor, compareParts.minor, changes.minorChanged)
123
+ result += '.'
124
+ result += colorPart(parts.patch, compareParts.patch, changes.patchChanged)
125
+
126
+ if (parts.extra) {
127
+ result += `.${colorPart(parts.extra, compareParts.extra, changes.extraChanged)}`
128
+ }
129
+
130
+ return result
131
+ }
132
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * AI Analysis Handler
3
+ *
4
+ * Handles AI analysis display and formatting for the update command.
5
+ */
6
+
7
+ import {
8
+ AIAnalysisService,
9
+ type AnalysisResult,
10
+ type AnalysisType,
11
+ type UpdatePlan,
12
+ type WorkspaceService,
13
+ } from '@pcu/core'
14
+ import { logger, t } from '@pcu/utils'
15
+ import chalk from 'chalk'
16
+ import { cliOutput } from '../utils/cliOutput.js'
17
+
18
+ /**
19
+ * Options for AI analysis
20
+ */
21
+ export interface AIAnalysisOptions {
22
+ workspace?: string
23
+ provider?: string
24
+ analysisType?: AnalysisType
25
+ skipCache?: boolean
26
+ verbose?: boolean
27
+ force?: boolean
28
+ }
29
+
30
+ /**
31
+ * Handles AI analysis for package updates
32
+ */
33
+ export class AIAnalysisHandler {
34
+ private readonly workspaceService: WorkspaceService
35
+
36
+ constructor(workspaceService: WorkspaceService) {
37
+ this.workspaceService = workspaceService
38
+ }
39
+
40
+ /**
41
+ * Perform AI analysis and display results
42
+ */
43
+ async analyzeAndDisplay(plan: UpdatePlan, options: AIAnalysisOptions): Promise<void> {
44
+ cliOutput.print(
45
+ chalk.blue(`\n🤖 ${t('command.update.runningBatchAI', { count: plan.updates.length })}`)
46
+ )
47
+ cliOutput.print(chalk.gray(`${t('command.update.batchAIHint')}\n`))
48
+
49
+ try {
50
+ const aiResult = await this.performBatchAnalysis(plan, options)
51
+ this.displayResults(aiResult, options.force)
52
+ } catch (aiError) {
53
+ logger.warn('AI batch analysis failed', {
54
+ error: aiError instanceof Error ? aiError.message : String(aiError),
55
+ packageCount: plan.updates.length,
56
+ provider: options.provider,
57
+ })
58
+ cliOutput.warn(chalk.yellow(`\n⚠️ ${t('command.update.aiBatchFailed')}`))
59
+ if (options.verbose) {
60
+ cliOutput.warn(chalk.gray(String(aiError)))
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Perform batch AI analysis for all packages in the update plan
67
+ */
68
+ private async performBatchAnalysis(
69
+ plan: UpdatePlan,
70
+ options: AIAnalysisOptions
71
+ ): Promise<AnalysisResult> {
72
+ const workspacePath = options.workspace || process.cwd()
73
+ const workspaceInfo = await this.workspaceService.getWorkspaceInfo(workspacePath)
74
+
75
+ const aiService = new AIAnalysisService({
76
+ config: {
77
+ preferredProvider: options.provider === 'auto' ? 'auto' : options.provider,
78
+ cache: { enabled: !options.skipCache, ttl: 3600 },
79
+ fallback: { enabled: true, useRuleEngine: true },
80
+ },
81
+ })
82
+
83
+ // Convert planned updates to PackageUpdateInfo format
84
+ const packages = plan.updates.map((update) => ({
85
+ name: update.packageName,
86
+ currentVersion: update.currentVersion,
87
+ targetVersion: update.newVersion,
88
+ updateType: update.updateType,
89
+ catalogName: update.catalogName,
90
+ }))
91
+
92
+ const wsInfo = {
93
+ name: workspaceInfo.name,
94
+ path: workspaceInfo.path,
95
+ packageCount: workspaceInfo.packageCount,
96
+ catalogCount: workspaceInfo.catalogCount,
97
+ }
98
+
99
+ return aiService.analyzeWithChunking(packages, wsInfo, {
100
+ analysisType: options.analysisType || 'impact',
101
+ skipCache: options.skipCache,
102
+ chunking: {
103
+ enabled: true,
104
+ threshold: 15,
105
+ chunkSize: 10,
106
+ onProgress: options.verbose
107
+ ? (progress) => {
108
+ cliOutput.print(
109
+ chalk.gray(
110
+ ` ${t('command.update.processingChunks', {
111
+ current: progress.currentChunk,
112
+ total: progress.totalChunks,
113
+ })} (${progress.percentComplete}%)`
114
+ )
115
+ )
116
+ }
117
+ : undefined,
118
+ },
119
+ })
120
+ }
121
+
122
+ /**
123
+ * Display AI analysis results
124
+ */
125
+ private displayResults(aiResult: AnalysisResult, force?: boolean): void {
126
+ cliOutput.print(chalk.blue(`\n📊 ${t('command.update.aiResults')}`))
127
+ cliOutput.print(chalk.gray('─'.repeat(60)))
128
+ cliOutput.print(chalk.cyan(t('command.update.provider', { provider: aiResult.provider })))
129
+ cliOutput.print(
130
+ chalk.cyan(
131
+ t('command.update.confidence', { confidence: (aiResult.confidence * 100).toFixed(0) })
132
+ )
133
+ )
134
+ cliOutput.print(
135
+ chalk.cyan(t('command.update.processingTime', { time: aiResult.processingTimeMs }))
136
+ )
137
+ cliOutput.print(chalk.gray('─'.repeat(60)))
138
+ cliOutput.print(chalk.yellow(`\n📝 ${t('command.update.summary')}`))
139
+ cliOutput.print(aiResult.summary)
140
+
141
+ if (aiResult.recommendations.length > 0) {
142
+ cliOutput.print(chalk.yellow(`\n📦 ${t('command.update.packageRecommendations')}`))
143
+ for (const rec of aiResult.recommendations) {
144
+ this.displayRecommendation(rec)
145
+ }
146
+ }
147
+
148
+ if (aiResult.warnings && aiResult.warnings.length > 0) {
149
+ cliOutput.print(chalk.yellow(`\n⚠️ ${t('command.update.warnings')}`))
150
+ for (const warning of aiResult.warnings) {
151
+ cliOutput.print(chalk.yellow(` - ${warning}`))
152
+ }
153
+ }
154
+
155
+ cliOutput.print(chalk.gray(`\n${'─'.repeat(60)}`))
156
+
157
+ const skipRecommendations = aiResult.recommendations.filter((r) => r.action === 'skip')
158
+ if (skipRecommendations.length > 0 && !force) {
159
+ cliOutput.print(
160
+ chalk.red(
161
+ `\n⛔ ${t('command.update.aiSkipRecommend', { count: skipRecommendations.length })}`
162
+ )
163
+ )
164
+ cliOutput.print(chalk.yellow(t('command.update.useForce')))
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Display a single package recommendation
170
+ */
171
+ private displayRecommendation(rec: AnalysisResult['recommendations'][0]): void {
172
+ const actionIcon = rec.action === 'update' ? '✅' : rec.action === 'skip' ? '❌' : '⚠️'
173
+ const riskColor =
174
+ rec.riskLevel === 'critical'
175
+ ? chalk.red
176
+ : rec.riskLevel === 'high'
177
+ ? chalk.yellow
178
+ : rec.riskLevel === 'medium'
179
+ ? chalk.cyan
180
+ : chalk.green
181
+
182
+ cliOutput.print(
183
+ `\n ${actionIcon} ${chalk.bold(rec.package)}: ${rec.currentVersion} → ${rec.targetVersion}`
184
+ )
185
+ cliOutput.print(
186
+ ` Action: ${chalk.bold(rec.action.toUpperCase())} | Risk: ${riskColor(rec.riskLevel)}`
187
+ )
188
+ cliOutput.print(` ${rec.reason}`)
189
+
190
+ if (rec.breakingChanges && rec.breakingChanges.length > 0) {
191
+ cliOutput.print(
192
+ chalk.red(
193
+ ` ⚠️ ${t('command.update.breakingChanges', { changes: rec.breakingChanges.join(', ') })}`
194
+ )
195
+ )
196
+ }
197
+ if (rec.securityFixes && rec.securityFixes.length > 0) {
198
+ cliOutput.print(
199
+ chalk.green(
200
+ ` 🔒 ${t('command.update.securityFixes', { fixes: rec.securityFixes.join(', ') })}`
201
+ )
202
+ )
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Changelog Handler
3
+ *
4
+ * Handles changelog fetching and display for the update command.
5
+ */
6
+
7
+ import { ChangelogService, NpmRegistryService, type UpdatePlan } from '@pcu/core'
8
+ import { logger, t } from '@pcu/utils'
9
+ import chalk from 'chalk'
10
+ import { cliOutput } from '../utils/cliOutput.js'
11
+
12
+ /**
13
+ * Handles changelog display for package updates
14
+ */
15
+ export class ChangelogHandler {
16
+ private readonly changelogService: ChangelogService
17
+ private readonly npmRegistry: NpmRegistryService
18
+
19
+ constructor() {
20
+ this.changelogService = new ChangelogService({ cacheMinutes: 30 })
21
+ this.npmRegistry = new NpmRegistryService()
22
+ }
23
+
24
+ /**
25
+ * Display changelogs for all planned updates
26
+ */
27
+ async displayChangelogs(plan: UpdatePlan, verbose?: boolean): Promise<void> {
28
+ cliOutput.print(chalk.blue(`\n📋 ${t('command.update.fetchingChangelogs')}`))
29
+
30
+ for (const update of plan.updates) {
31
+ await this.displayPackageChangelog(
32
+ update.packageName,
33
+ update.currentVersion,
34
+ update.newVersion,
35
+ verbose
36
+ )
37
+ }
38
+
39
+ cliOutput.print(chalk.gray(`\n${'─'.repeat(60)}`))
40
+ }
41
+
42
+ /**
43
+ * Display changelog for a single package
44
+ */
45
+ private async displayPackageChangelog(
46
+ packageName: string,
47
+ currentVersion: string,
48
+ newVersion: string,
49
+ verbose?: boolean
50
+ ): Promise<void> {
51
+ try {
52
+ // Get package info to find repository URL
53
+ const packageInfo = await this.npmRegistry.getPackageInfo(packageName)
54
+ const repository = this.extractRepository(packageInfo.repository)
55
+
56
+ // Fetch changelog between versions
57
+ const changelog = await this.changelogService.getChangelog(
58
+ packageName,
59
+ currentVersion,
60
+ newVersion,
61
+ repository
62
+ )
63
+
64
+ // Display formatted changelog
65
+ cliOutput.print(chalk.yellow(`\n📦 ${packageName}`))
66
+ cliOutput.print(chalk.gray(` ${currentVersion} → ${newVersion}`))
67
+ cliOutput.print(this.changelogService.formatChangelog(changelog, verbose))
68
+ } catch (error) {
69
+ logger.debug('Failed to fetch changelog', {
70
+ package: packageName,
71
+ error: error instanceof Error ? error.message : String(error),
72
+ })
73
+ this.displayFallbackInfo(packageName, currentVersion, newVersion)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Extract repository info from package.json repository field
79
+ */
80
+ private extractRepository(repository: unknown): { type: string; url: string } | undefined {
81
+ if (!repository) return undefined
82
+
83
+ if (typeof repository === 'string') {
84
+ return { type: 'git', url: repository }
85
+ }
86
+
87
+ if (typeof repository === 'object' && repository !== null) {
88
+ const repo = repository as { type?: string; url?: string }
89
+ if (repo.url) {
90
+ return { type: repo.type || 'git', url: repo.url }
91
+ }
92
+ }
93
+
94
+ return undefined
95
+ }
96
+
97
+ /**
98
+ * Display fallback info when changelog is not available
99
+ */
100
+ private displayFallbackInfo(
101
+ packageName: string,
102
+ currentVersion: string,
103
+ newVersion: string
104
+ ): void {
105
+ cliOutput.print(chalk.yellow(`\n📦 ${packageName}`))
106
+ cliOutput.print(chalk.gray(` ${currentVersion} → ${newVersion}`))
107
+ cliOutput.print(
108
+ chalk.gray(
109
+ ` 📋 ${t('command.update.changelogUnavailable')}: https://www.npmjs.com/package/${packageName}?activeTab=versions`
110
+ )
111
+ )
112
+ }
113
+ }