pnpm-catalog-updates 0.7.19 → 1.0.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.
@@ -5,64 +5,80 @@
5
5
  * Supports interactive mode, dry-run, and various update strategies.
6
6
  */
7
7
 
8
- import { CatalogUpdateService, UpdateOptions, UpdatePlan, UpdateTarget } from '@pcu/core';
9
- import { ConfigLoader } from '@pcu/utils';
10
- import { OutputFormat, OutputFormatter } from '../formatters/outputFormatter.js';
11
- import { ProgressBar } from '../formatters/progressBar.js';
12
- import { InteractivePrompts } from '../interactive/interactivePrompts.js';
13
- import { StyledText, ThemeManager } from '../themes/colorTheme.js';
8
+ import {
9
+ AIAnalysisService,
10
+ type AnalysisType,
11
+ type CatalogUpdateService,
12
+ FileSystemService,
13
+ FileWorkspaceRepository,
14
+ type UpdateOptions,
15
+ type UpdatePlan,
16
+ type UpdateTarget,
17
+ WorkspaceService,
18
+ } from '@pcu/core'
19
+ import { ConfigLoader } from '@pcu/utils'
20
+ import chalk from 'chalk'
21
+ import { type OutputFormat, OutputFormatter } from '../formatters/outputFormatter.js'
22
+ import { ProgressBar } from '../formatters/progressBar.js'
23
+ import { InteractivePrompts } from '../interactive/interactivePrompts.js'
24
+ import { StyledText, ThemeManager } from '../themes/colorTheme.js'
14
25
 
15
26
  export interface UpdateCommandOptions {
16
- workspace?: string;
17
- catalog?: string;
18
- format?: OutputFormat;
19
- target?: UpdateTarget;
20
- interactive?: boolean;
21
- dryRun?: boolean;
22
- force?: boolean;
23
- prerelease?: boolean;
24
- include?: string[];
25
- exclude?: string[];
26
- createBackup?: boolean;
27
- verbose?: boolean;
28
- color?: boolean;
27
+ workspace?: string
28
+ catalog?: string
29
+ format?: OutputFormat
30
+ target?: UpdateTarget
31
+ interactive?: boolean
32
+ dryRun?: boolean
33
+ force?: boolean
34
+ prerelease?: boolean
35
+ include?: string[]
36
+ exclude?: string[]
37
+ createBackup?: boolean
38
+ verbose?: boolean
39
+ color?: boolean
40
+ // AI batch analysis options
41
+ ai?: boolean
42
+ provider?: string
43
+ analysisType?: AnalysisType
44
+ skipCache?: boolean
29
45
  }
30
46
 
31
47
  export class UpdateCommand {
32
- private readonly updateService: CatalogUpdateService;
48
+ private readonly updateService: CatalogUpdateService
33
49
 
34
50
  constructor(updateService: CatalogUpdateService) {
35
- this.updateService = updateService;
51
+ this.updateService = updateService
36
52
  }
37
53
 
38
54
  /**
39
55
  * Execute the update command
40
56
  */
41
57
  async execute(options: UpdateCommandOptions = {}): Promise<void> {
42
- let progressBar: ProgressBar | undefined;
58
+ let progressBar: ProgressBar | undefined
43
59
 
44
60
  try {
45
61
  // Initialize theme
46
- ThemeManager.setTheme('default');
62
+ ThemeManager.setTheme('default')
47
63
 
48
64
  // Create progress bar for the update process
49
65
  progressBar = new ProgressBar({
50
66
  text: '正在规划更新...',
51
67
  total: 4, // 4 main steps
52
- });
53
- progressBar.start('正在加载工作区配置...');
68
+ })
69
+ progressBar.start('正在加载工作区配置...')
54
70
 
55
71
  // Load configuration file first
56
- const config = ConfigLoader.loadConfig(options.workspace || process.cwd());
72
+ const config = ConfigLoader.loadConfig(options.workspace || process.cwd())
57
73
 
58
74
  // Use format from CLI options first, then config file, then default
59
- const effectiveFormat = options.format || config.defaults?.format || 'table';
75
+ const effectiveFormat = options.format || config.defaults?.format || 'table'
60
76
 
61
77
  // Create output formatter with effective format
62
78
  const formatter = new OutputFormatter(
63
79
  effectiveFormat as OutputFormat,
64
80
  options.color !== false
65
- );
81
+ )
66
82
 
67
83
  // Merge CLI options with configuration file settings
68
84
  const updateOptions: UpdateOptions = {
@@ -77,73 +93,168 @@ export class UpdateCommand {
77
93
  dryRun: options.dryRun ?? config.defaults?.dryRun ?? false,
78
94
  force: options.force ?? false,
79
95
  createBackup: options.createBackup ?? config.defaults?.createBackup ?? false,
80
- };
96
+ }
81
97
 
82
98
  // Step 1: Planning updates
83
- progressBar.update('正在检查包版本...', 1, 4);
84
- const plan = await this.updateService.planUpdates(updateOptions);
99
+ progressBar.update('正在检查包版本...', 1, 4)
100
+ const plan = await this.updateService.planUpdates(updateOptions)
85
101
 
86
102
  // Step 2: Check if any updates found
87
- progressBar.update('正在分析更新...', 2, 4);
103
+ progressBar.update('正在分析更新...', 2, 4)
88
104
 
89
105
  if (!plan.updates.length) {
90
- progressBar.succeed('所有依赖包都是最新的');
91
- console.log(StyledText.iconSuccess('All dependencies are up to date!'));
92
- return;
106
+ progressBar.succeed('所有依赖包都是最新的')
107
+ console.log(StyledText.iconSuccess('All dependencies are up to date!'))
108
+ return
93
109
  }
94
110
 
95
111
  console.log(
96
112
  StyledText.iconPackage(
97
113
  `Found ${plan.totalUpdates} update${plan.totalUpdates === 1 ? '' : 's'} available`
98
114
  )
99
- );
115
+ )
100
116
 
101
117
  // Interactive selection if enabled
102
- let finalPlan = plan;
118
+ let finalPlan = plan
103
119
  if (options.interactive) {
104
- finalPlan = await this.interactiveSelection(plan);
120
+ finalPlan = await this.interactiveSelection(plan)
105
121
  if (!finalPlan.updates.length) {
106
- progressBar.warn('未选择任何更新');
107
- console.log(StyledText.iconWarning('No updates selected'));
108
- return;
122
+ progressBar.warn('未选择任何更新')
123
+ console.log(StyledText.iconWarning('No updates selected'))
124
+ return
109
125
  }
110
126
  }
111
127
 
128
+ // AI batch analysis if enabled - analyze ALL packages in one request
129
+ if (options.ai) {
130
+ progressBar.update('🤖 正在进行 AI 批量分析...', 2.5, 4)
131
+ progressBar.stop()
132
+
133
+ console.log(
134
+ chalk.blue(
135
+ `\n🤖 Running AI-powered batch analysis for ${finalPlan.updates.length} packages...`
136
+ )
137
+ )
138
+ console.log(chalk.gray('This analyzes all packages in a single request for efficiency.\n'))
139
+
140
+ try {
141
+ const aiResult = await this.performBatchAIAnalysis(finalPlan, options)
142
+
143
+ // Display AI analysis results
144
+ console.log(chalk.blue('\n📊 AI Analysis Results:'))
145
+ console.log(chalk.gray('─'.repeat(60)))
146
+ console.log(chalk.cyan(`Provider: ${aiResult.provider}`))
147
+ console.log(chalk.cyan(`Confidence: ${(aiResult.confidence * 100).toFixed(0)}%`))
148
+ console.log(chalk.cyan(`Processing time: ${aiResult.processingTimeMs}ms`))
149
+ console.log(chalk.gray('─'.repeat(60)))
150
+ console.log(chalk.yellow('\n📝 Summary:'))
151
+ console.log(aiResult.summary)
152
+
153
+ // Display recommendations for each package
154
+ if (aiResult.recommendations.length > 0) {
155
+ console.log(chalk.yellow('\n📦 Package Recommendations:'))
156
+ for (const rec of aiResult.recommendations) {
157
+ const actionIcon = rec.action === 'update' ? '✅' : rec.action === 'skip' ? '❌' : '⚠️'
158
+ const riskColor =
159
+ rec.riskLevel === 'critical'
160
+ ? chalk.red
161
+ : rec.riskLevel === 'high'
162
+ ? chalk.yellow
163
+ : rec.riskLevel === 'medium'
164
+ ? chalk.cyan
165
+ : chalk.green
166
+
167
+ console.log(
168
+ `\n ${actionIcon} ${chalk.bold(rec.package)}: ${rec.currentVersion} → ${rec.targetVersion}`
169
+ )
170
+ console.log(
171
+ ` Action: ${chalk.bold(rec.action.toUpperCase())} | Risk: ${riskColor(rec.riskLevel)}`
172
+ )
173
+ console.log(` ${rec.reason}`)
174
+
175
+ if (rec.breakingChanges && rec.breakingChanges.length > 0) {
176
+ console.log(
177
+ chalk.red(` ⚠️ Breaking changes: ${rec.breakingChanges.join(', ')}`)
178
+ )
179
+ }
180
+ if (rec.securityFixes && rec.securityFixes.length > 0) {
181
+ console.log(chalk.green(` 🔒 Security fixes: ${rec.securityFixes.join(', ')}`))
182
+ }
183
+ }
184
+ }
185
+
186
+ // Display warnings
187
+ if (aiResult.warnings && aiResult.warnings.length > 0) {
188
+ console.log(chalk.yellow('\n⚠️ Warnings:'))
189
+ for (const warning of aiResult.warnings) {
190
+ console.log(chalk.yellow(` - ${warning}`))
191
+ }
192
+ }
193
+
194
+ console.log(chalk.gray(`\n${'─'.repeat(60)}`))
195
+
196
+ // If there are critical/skip recommendations, warn the user
197
+ const skipRecommendations = aiResult.recommendations.filter((r) => r.action === 'skip')
198
+ if (skipRecommendations.length > 0 && !options.force) {
199
+ console.log(
200
+ chalk.red(
201
+ `\n⛔ AI recommends skipping ${skipRecommendations.length} package(s) due to risks.`
202
+ )
203
+ )
204
+ console.log(chalk.yellow('Use --force to override AI recommendations.\n'))
205
+ }
206
+ } catch (aiError) {
207
+ console.warn(
208
+ chalk.yellow('\n⚠️ AI batch analysis failed, continuing without AI insights:')
209
+ )
210
+ if (options.verbose) {
211
+ console.warn(chalk.gray(String(aiError)))
212
+ }
213
+ }
214
+
215
+ // Restart progress bar
216
+ progressBar = new ProgressBar({
217
+ text: '准备应用更新...',
218
+ total: 4,
219
+ })
220
+ progressBar.start('正在准备应用更新...')
221
+ }
222
+
112
223
  // Step 3: Apply updates
113
- progressBar.update('正在准备应用更新...', 3, 4);
224
+ progressBar.update('正在准备应用更新...', 3, 4)
114
225
 
115
226
  if (!options.dryRun) {
116
227
  // Replace the progress bar with one for applying updates
117
- progressBar.stop();
228
+ progressBar.stop()
118
229
  progressBar = new ProgressBar({
119
230
  text: 'Applying updates...',
120
231
  total: finalPlan.updates.length,
121
- });
122
- progressBar.start('正在应用更新...');
232
+ })
233
+ progressBar.start('正在应用更新...')
123
234
 
124
- const result = await this.updateService.executeUpdates(finalPlan, updateOptions);
125
- progressBar.succeed(`Applied ${finalPlan.updates.length} updates`);
235
+ const result = await this.updateService.executeUpdates(finalPlan, updateOptions)
236
+ progressBar.succeed(`Applied ${finalPlan.updates.length} updates`)
126
237
 
127
- console.log(formatter.formatUpdateResult(result));
238
+ console.log(formatter.formatUpdateResult(result))
128
239
  } else {
129
- progressBar.update('正在生成预览...', 4, 4);
130
- progressBar.succeed('更新预览完成');
131
- console.log(StyledText.iconInfo('Dry run - no changes made'));
132
- console.log(JSON.stringify(finalPlan, null, 2));
240
+ progressBar.update('正在生成预览...', 4, 4)
241
+ progressBar.succeed('更新预览完成')
242
+ console.log(StyledText.iconInfo('Dry run - no changes made'))
243
+ console.log(JSON.stringify(finalPlan, null, 2))
133
244
  }
134
245
 
135
- console.log(StyledText.iconComplete('Update process completed!'));
246
+ console.log(StyledText.iconComplete('Update process completed!'))
136
247
  } catch (error) {
137
248
  if (progressBar) {
138
- progressBar.fail('Operation failed');
249
+ progressBar.fail('Operation failed')
139
250
  }
140
251
 
141
252
  if (error instanceof Error) {
142
- console.error(StyledText.iconError(`Error: ${error.message}`));
253
+ console.error(StyledText.iconError(`Error: ${error.message}`))
143
254
  } else {
144
- console.error(StyledText.iconError('Unknown error occurred'));
255
+ console.error(StyledText.iconError('Unknown error occurred'))
145
256
  }
146
- throw error;
257
+ throw error
147
258
  }
148
259
  }
149
260
 
@@ -151,7 +262,7 @@ export class UpdateCommand {
151
262
  * Interactive update selection
152
263
  */
153
264
  private async interactiveSelection(plan: UpdatePlan): Promise<UpdatePlan> {
154
- const interactivePrompts = new InteractivePrompts();
265
+ const interactivePrompts = new InteractivePrompts()
155
266
 
156
267
  // Transform PlannedUpdate to the format expected by InteractivePrompts
157
268
  const packages = plan.updates.map((update) => ({
@@ -159,31 +270,79 @@ export class UpdateCommand {
159
270
  current: update.currentVersion,
160
271
  latest: update.newVersion,
161
272
  type: update.updateType,
162
- }));
273
+ }))
163
274
 
164
- const selectedPackageNames = await interactivePrompts.selectPackages(packages);
275
+ const selectedPackageNames = await interactivePrompts.selectPackages(packages)
165
276
 
166
277
  // Filter the plan to only include selected packages
167
278
  const selectedUpdates = plan.updates.filter((update) =>
168
279
  selectedPackageNames.includes(update.packageName)
169
- );
280
+ )
170
281
 
171
282
  return {
172
283
  ...plan,
173
284
  updates: selectedUpdates,
174
285
  totalUpdates: selectedUpdates.length,
175
- };
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Perform batch AI analysis for all packages in the update plan
291
+ * This analyzes ALL packages in a single AI request for efficiency
292
+ */
293
+ private async performBatchAIAnalysis(plan: UpdatePlan, options: UpdateCommandOptions) {
294
+ const workspacePath = options.workspace || process.cwd()
295
+
296
+ // Create workspace service to get workspace info
297
+ const fileSystemService = new FileSystemService()
298
+ const workspaceRepository = new FileWorkspaceRepository(fileSystemService)
299
+ const workspaceService = new WorkspaceService(workspaceRepository)
300
+ const workspaceInfo = await workspaceService.getWorkspaceInfo(workspacePath)
301
+
302
+ // Create AI service
303
+ const aiService = new AIAnalysisService({
304
+ config: {
305
+ preferredProvider: options.provider === 'auto' ? 'auto' : options.provider,
306
+ cache: { enabled: !options.skipCache, ttl: 3600 },
307
+ fallback: { enabled: true, useRuleEngine: true },
308
+ },
309
+ })
310
+
311
+ // Convert all planned updates to PackageUpdateInfo format for batch analysis
312
+ const packages = plan.updates.map((update) => ({
313
+ name: update.packageName,
314
+ currentVersion: update.currentVersion,
315
+ targetVersion: update.newVersion,
316
+ updateType: update.updateType,
317
+ catalogName: update.catalogName,
318
+ }))
319
+
320
+ // Build workspace info for AI
321
+ const wsInfo = {
322
+ name: workspaceInfo.name,
323
+ path: workspaceInfo.path,
324
+ packageCount: workspaceInfo.packageCount,
325
+ catalogCount: workspaceInfo.catalogCount,
326
+ }
327
+
328
+ // Perform single batch analysis for ALL packages
329
+ const result = await aiService.analyzeUpdates(packages, wsInfo, {
330
+ analysisType: options.analysisType || 'impact',
331
+ skipCache: options.skipCache,
332
+ })
333
+
334
+ return result
176
335
  }
177
336
 
178
337
  /**
179
338
  * Validate command options
180
339
  */
181
340
  static validateOptions(options: UpdateCommandOptions): string[] {
182
- const errors: string[] = [];
341
+ const errors: string[] = []
183
342
 
184
343
  // Validate format
185
344
  if (options.format && !['table', 'json', 'yaml', 'minimal'].includes(options.format)) {
186
- errors.push('Invalid format. Must be one of: table, json, yaml, minimal');
345
+ errors.push('Invalid format. Must be one of: table, json, yaml, minimal')
187
346
  }
188
347
 
189
348
  // Validate target
@@ -191,15 +350,15 @@ export class UpdateCommand {
191
350
  options.target &&
192
351
  !['latest', 'greatest', 'minor', 'patch', 'newest'].includes(options.target)
193
352
  ) {
194
- errors.push('Invalid target. Must be one of: latest, greatest, minor, patch, newest');
353
+ errors.push('Invalid target. Must be one of: latest, greatest, minor, patch, newest')
195
354
  }
196
355
 
197
356
  // Interactive and dry-run conflict
198
357
  if (options.interactive && options.dryRun) {
199
- errors.push('Cannot use --interactive with --dry-run');
358
+ errors.push('Cannot use --interactive with --dry-run')
200
359
  }
201
360
 
202
- return errors;
361
+ return errors
203
362
  }
204
363
 
205
364
  /**
@@ -240,6 +399,6 @@ Exit Codes:
240
399
  0 Updates completed successfully
241
400
  1 Updates failed or were cancelled
242
401
  2 Error occurred
243
- `;
402
+ `
244
403
  }
245
404
  }