pnpm-catalog-updates 0.7.18 → 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.
package/src/cli/index.ts CHANGED
@@ -7,73 +7,90 @@
7
7
  * This is the main entry point that handles command parsing and execution.
8
8
  */
9
9
 
10
- import { dirname, join } from 'path';
11
- import { OutputFormat, OutputFormatter } from './formatters/outputFormatter.js';
12
-
10
+ import { readFileSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
13
  // Services and Dependencies
14
+ import type { AnalysisType } from '@pcu/core'
14
15
  import {
16
+ AIAnalysisService,
17
+ AIDetector,
15
18
  CatalogUpdateService,
16
19
  FileSystemService,
17
20
  FileWorkspaceRepository,
18
21
  NpmRegistryService,
19
22
  WorkspaceService,
20
- } from '@pcu/core';
23
+ } from '@pcu/core'
21
24
  // CLI Commands
22
- import { ConfigLoader, VersionChecker } from '@pcu/utils';
23
- import chalk from 'chalk';
24
- import { Command } from 'commander';
25
- import { readFileSync } from 'fs';
26
- import { fileURLToPath } from 'url';
27
- import { CheckCommand } from './commands/checkCommand.js';
28
- import { InitCommand } from './commands/initCommand.js';
29
- import { SecurityCommand } from './commands/securityCommand.js';
30
- import { UpdateCommand } from './commands/updateCommand.js';
31
- import { InteractivePrompts } from './interactive/interactivePrompts.js';
32
- import { StyledText, ThemeManager } from './themes/colorTheme.js';
25
+ import { ConfigLoader, VersionChecker } from '@pcu/utils'
26
+ import chalk from 'chalk'
27
+ import { Command } from 'commander'
28
+ import { CheckCommand } from './commands/checkCommand.js'
29
+ import { InitCommand } from './commands/initCommand.js'
30
+ import { SecurityCommand } from './commands/securityCommand.js'
31
+ import { UpdateCommand } from './commands/updateCommand.js'
32
+ import { type OutputFormat, OutputFormatter } from './formatters/outputFormatter.js'
33
+ import { InteractivePrompts } from './interactive/interactivePrompts.js'
34
+ import { StyledText, ThemeManager } from './themes/colorTheme.js'
33
35
 
34
36
  // Get package.json for version info
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
- const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
37
+ const __filename = fileURLToPath(import.meta.url)
38
+ const __dirname = dirname(__filename)
39
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'))
38
40
 
39
41
  /**
40
42
  * Create service dependencies with configuration support
41
43
  */
42
44
  function createServices(workspacePath?: string) {
43
- const fileSystemService = new FileSystemService();
44
- const workspaceRepository = new FileWorkspaceRepository(fileSystemService);
45
+ const fileSystemService = new FileSystemService()
46
+ const workspaceRepository = new FileWorkspaceRepository(fileSystemService)
45
47
  // Use factory method to create CatalogUpdateService with configuration
46
48
  const catalogUpdateService = CatalogUpdateService.createWithConfig(
47
49
  workspaceRepository,
48
50
  workspacePath
49
- );
50
- const workspaceService = new WorkspaceService(workspaceRepository);
51
+ )
52
+ const workspaceService = new WorkspaceService(workspaceRepository)
51
53
 
52
54
  return {
53
55
  fileSystemService,
54
56
  workspaceRepository,
55
57
  catalogUpdateService,
56
58
  workspaceService,
57
- };
59
+ }
60
+ }
61
+
62
+ function parseBooleanFlag(value: unknown): boolean {
63
+ if (value === undefined || value === null) return false
64
+ if (typeof value === 'boolean') return value
65
+ if (typeof value === 'number') return value !== 0
66
+ if (typeof value === 'string') {
67
+ const normalized = value.trim().toLowerCase()
68
+ if (normalized === '') return false
69
+ if (['false', '0', 'no', 'off', 'n'].includes(normalized)) return false
70
+ if (['true', '1', 'yes', 'on', 'y'].includes(normalized)) return true
71
+ // Commander env() 会把任意非空字符串塞进来;未知字符串按“启用”处理
72
+ return true
73
+ }
74
+ return Boolean(value)
58
75
  }
59
76
 
60
77
  /**
61
78
  * Main CLI function
62
79
  */
63
80
  export async function main(): Promise<void> {
64
- const program = new Command();
81
+ const program = new Command()
65
82
 
66
83
  // Parse arguments first to get workspace path
67
- let workspacePath: string | undefined;
84
+ let workspacePath: string | undefined
68
85
 
69
86
  // Extract workspace path from arguments for service creation
70
- const workspaceIndex = process.argv.findIndex((arg) => arg === '-w' || arg === '--workspace');
87
+ const workspaceIndex = process.argv.findIndex((arg) => arg === '-w' || arg === '--workspace')
71
88
  if (workspaceIndex !== -1 && workspaceIndex + 1 < process.argv.length) {
72
- workspacePath = process.argv[workspaceIndex + 1];
89
+ workspacePath = process.argv[workspaceIndex + 1]
73
90
  }
74
91
 
75
92
  // Load configuration to check if version updates are enabled
76
- const config = ConfigLoader.loadConfig(workspacePath || process.cwd());
93
+ const config = ConfigLoader.loadConfig(workspacePath || process.cwd())
77
94
 
78
95
  // Check for version updates (skip in CI environments or if disabled)
79
96
  if (VersionChecker.shouldCheckForUpdates() && config.advanced?.checkForUpdates !== false) {
@@ -81,26 +98,26 @@ export async function main(): Promise<void> {
81
98
  const versionResult = await VersionChecker.checkVersion(packageJson.version, {
82
99
  skipPrompt: false,
83
100
  timeout: 3000, // Short timeout to not delay CLI startup
84
- });
101
+ })
85
102
 
86
103
  if (versionResult.shouldPrompt) {
87
- const didUpdate = await VersionChecker.promptAndUpdate(versionResult);
104
+ const didUpdate = await VersionChecker.promptAndUpdate(versionResult)
88
105
  if (didUpdate) {
89
106
  // Exit after successful update to allow user to restart with new version
90
- console.log(chalk.blue('Please run your command again to use the updated version.'));
91
- process.exit(0);
107
+ console.log(chalk.blue('Please run your command again to use the updated version.'))
108
+ process.exit(0)
92
109
  }
93
110
  }
94
111
  } catch (error) {
95
112
  // Silently fail version check to not interrupt CLI usage (only show warning in verbose mode)
96
113
  if (process.argv.includes('-v') || process.argv.includes('--verbose')) {
97
- console.warn(chalk.yellow('⚠️ Could not check for updates:'), error);
114
+ console.warn(chalk.yellow('⚠️ Could not check for updates:'), error)
98
115
  }
99
116
  }
100
117
  }
101
118
 
102
119
  // Create services with workspace path for configuration loading
103
- const services = createServices(workspacePath);
120
+ const services = createServices(workspacePath)
104
121
 
105
122
  // Configure the main command
106
123
  program
@@ -116,7 +133,7 @@ export async function main(): Promise<void> {
116
133
  .option('-s, --workspace-info', 'shorthand for workspace command')
117
134
  .option('-t, --theme', 'shorthand for theme command')
118
135
  .option('--security-audit', 'shorthand for security command')
119
- .option('--security-fix', 'shorthand for security --fix-vulns command');
136
+ .option('--security-fix', 'shorthand for security --fix-vulns command')
120
137
 
121
138
  // Check command
122
139
  program
@@ -135,8 +152,8 @@ export async function main(): Promise<void> {
135
152
  .option('--exclude <pattern>', 'exclude packages matching pattern', [])
136
153
  .action(async (options, command) => {
137
154
  try {
138
- const globalOptions = command.parent.opts();
139
- const checkCommand = new CheckCommand(services.catalogUpdateService);
155
+ const globalOptions = command.parent.opts()
156
+ const checkCommand = new CheckCommand(services.catalogUpdateService)
140
157
 
141
158
  await checkCommand.execute({
142
159
  workspace: globalOptions.workspace,
@@ -152,12 +169,13 @@ export async function main(): Promise<void> {
152
169
  : [options.exclude].filter(Boolean),
153
170
  verbose: globalOptions.verbose,
154
171
  color: !globalOptions.noColor,
155
- });
172
+ })
173
+ process.exit(0)
156
174
  } catch (error) {
157
- console.error(chalk.red('❌ Error:'), error);
158
- process.exit(1);
175
+ console.error(chalk.red('❌ Error:'), error)
176
+ process.exit(1)
159
177
  }
160
- });
178
+ })
161
179
 
162
180
  // Update command
163
181
  program
@@ -178,10 +196,18 @@ export async function main(): Promise<void> {
178
196
  .option('--prerelease', 'include prerelease versions')
179
197
  .option('-b, --create-backup', 'create backup files before updating')
180
198
  .option('-f, --format <type>', 'output format: table, json, yaml, minimal', 'table')
199
+ .option('--ai', 'enable AI-powered batch analysis for all updates')
200
+ .option('--provider <name>', 'AI provider: auto, claude, gemini, codex', 'auto')
201
+ .option(
202
+ '--analysis-type <type>',
203
+ 'AI analysis type: impact, security, compatibility, recommend',
204
+ 'impact'
205
+ )
206
+ .option('--skip-cache', 'skip AI analysis cache')
181
207
  .action(async (options, command) => {
182
208
  try {
183
- const globalOptions = command.parent.opts();
184
- const updateCommand = new UpdateCommand(services.catalogUpdateService);
209
+ const globalOptions = command.parent.opts()
210
+ const updateCommand = new UpdateCommand(services.catalogUpdateService)
185
211
 
186
212
  await updateCommand.execute({
187
213
  workspace: globalOptions.workspace,
@@ -201,52 +227,142 @@ export async function main(): Promise<void> {
201
227
  createBackup: options.createBackup,
202
228
  verbose: globalOptions.verbose,
203
229
  color: !globalOptions.noColor,
204
- });
230
+ // AI batch analysis options
231
+ ai: parseBooleanFlag(options.ai),
232
+ provider: options.provider,
233
+ analysisType: options.analysisType as AnalysisType,
234
+ skipCache: parseBooleanFlag(options.skipCache),
235
+ })
236
+ process.exit(0)
205
237
  } catch (error) {
206
- console.error(chalk.red('❌ Error:'), error);
207
- process.exit(1);
238
+ console.error(chalk.red('❌ Error:'), error)
239
+ process.exit(1)
208
240
  }
209
- });
241
+ })
210
242
 
211
243
  // Analyze command
212
244
  program
213
245
  .command('analyze')
214
246
  .alias('a')
215
247
  .description('analyze the impact of updating a specific dependency')
216
- .argument('<catalog>', 'catalog name')
217
248
  .argument('<package>', 'package name')
218
249
  .argument('[version]', 'new version (default: latest)')
250
+ .option('--catalog <name>', 'catalog name (auto-detected if not specified)')
219
251
  .option('-f, --format <type>', 'output format: table, json, yaml, minimal', 'table')
220
- .action(async (catalog, packageName, version, options, command) => {
252
+ .option('--no-ai', 'disable AI-powered analysis')
253
+ .option('--provider <name>', 'AI provider: auto, claude, gemini, codex', 'auto')
254
+ .option(
255
+ '--analysis-type <type>',
256
+ 'AI analysis type: impact, security, compatibility, recommend',
257
+ 'impact'
258
+ )
259
+ .option('--skip-cache', 'skip AI analysis cache')
260
+ .action(async (packageName, version, options, command) => {
221
261
  try {
222
- const globalOptions = command.parent.opts();
262
+ const globalOptions = command.parent.opts()
223
263
  const formatter = new OutputFormatter(
224
264
  options.format as OutputFormat,
225
265
  !globalOptions.noColor
226
- );
266
+ )
267
+
268
+ // Auto-detect catalog if not specified
269
+ let catalog = options.catalog
270
+ if (!catalog) {
271
+ console.log(chalk.gray(`🔍 Auto-detecting catalog for ${packageName}...`))
272
+ catalog = await services.catalogUpdateService.findCatalogForPackage(
273
+ packageName,
274
+ globalOptions.workspace
275
+ )
276
+ if (!catalog) {
277
+ console.error(chalk.red(`❌ Package "${packageName}" not found in any catalog`))
278
+ console.log(chalk.gray('Use --catalog <name> to specify the catalog manually'))
279
+ process.exit(1)
280
+ }
281
+ console.log(chalk.gray(` Found in catalog: ${catalog}`))
282
+ }
227
283
 
228
284
  // Get latest version if not specified
229
- let targetVersion = version;
285
+ let targetVersion = version
230
286
  if (!targetVersion) {
231
- // Create a temporary registry service for version fetching
232
- const tempRegistryService = new NpmRegistryService();
233
- targetVersion = (await tempRegistryService.getLatestVersion(packageName)).toString();
287
+ const tempRegistryService = new NpmRegistryService()
288
+ targetVersion = (await tempRegistryService.getLatestVersion(packageName)).toString()
234
289
  }
235
290
 
291
+ // Get basic impact analysis first
236
292
  const analysis = await services.catalogUpdateService.analyzeImpact(
237
293
  catalog,
238
294
  packageName,
239
295
  targetVersion,
240
296
  globalOptions.workspace
241
- );
297
+ )
298
+
299
+ // AI analysis is enabled by default (use --no-ai to disable)
300
+ const aiEnabled = options.ai !== false
301
+
302
+ if (aiEnabled) {
303
+ console.log(chalk.blue('🤖 Running AI-powered analysis...'))
304
+
305
+ const aiService = new AIAnalysisService({
306
+ config: {
307
+ preferredProvider: options.provider === 'auto' ? 'auto' : options.provider,
308
+ cache: { enabled: !parseBooleanFlag(options.skipCache), ttl: 3600 },
309
+ fallback: { enabled: true, useRuleEngine: true },
310
+ },
311
+ })
312
+
313
+ // Get workspace info
314
+ const workspaceInfo = await services.workspaceService.getWorkspaceInfo(
315
+ globalOptions.workspace
316
+ )
317
+
318
+ // Build packages info for AI analysis
319
+ const packages = [
320
+ {
321
+ name: packageName,
322
+ currentVersion: analysis.currentVersion,
323
+ targetVersion: analysis.proposedVersion,
324
+ updateType: analysis.updateType,
325
+ },
326
+ ]
327
+
328
+ // Build workspace info for AI
329
+ const wsInfo = {
330
+ name: workspaceInfo.name,
331
+ path: workspaceInfo.path,
332
+ packageCount: workspaceInfo.packageCount,
333
+ catalogCount: workspaceInfo.catalogCount,
334
+ }
242
335
 
243
- const formattedOutput = formatter.formatImpactAnalysis(analysis);
244
- console.log(formattedOutput);
336
+ try {
337
+ const aiResult = await aiService.analyzeUpdates(packages, wsInfo, {
338
+ analysisType: options.analysisType as AnalysisType,
339
+ skipCache: parseBooleanFlag(options.skipCache),
340
+ })
341
+
342
+ // Format and display AI analysis result
343
+ const aiOutput = formatter.formatAIAnalysis(aiResult, analysis)
344
+ console.log(aiOutput)
345
+ process.exit(0)
346
+ } catch (aiError) {
347
+ console.warn(chalk.yellow('⚠️ AI analysis failed, showing basic analysis:'))
348
+ if (globalOptions.verbose) {
349
+ console.warn(chalk.gray(String(aiError)))
350
+ }
351
+ // Fall back to basic analysis
352
+ const formattedOutput = formatter.formatImpactAnalysis(analysis)
353
+ console.log(formattedOutput)
354
+ process.exit(0)
355
+ }
356
+ } else {
357
+ // Standard analysis without AI
358
+ const formattedOutput = formatter.formatImpactAnalysis(analysis)
359
+ console.log(formattedOutput)
360
+ }
245
361
  } catch (error) {
246
- console.error(chalk.red('❌ Error:'), error);
247
- process.exit(1);
362
+ console.error(chalk.red('❌ Error:'), error)
363
+ process.exit(1)
248
364
  }
249
- });
365
+ })
250
366
 
251
367
  // Workspace command
252
368
  program
@@ -258,39 +374,44 @@ export async function main(): Promise<void> {
258
374
  .option('-f, --format <type>', 'output format: table, json, yaml, minimal', 'table')
259
375
  .action(async (options, command) => {
260
376
  try {
261
- const globalOptions = command.parent.opts();
377
+ const globalOptions = command.parent.opts()
262
378
  const formatter = new OutputFormatter(
263
379
  options.format as OutputFormat,
264
380
  !globalOptions.noColor
265
- );
381
+ )
266
382
 
267
383
  if (options.validate) {
268
- const report = await services.workspaceService.validateWorkspace(globalOptions.workspace);
269
- const formattedOutput = formatter.formatValidationReport(report);
270
- console.log(formattedOutput);
271
- process.exit(report.isValid ? 0 : 1);
384
+ const report = await services.workspaceService.validateWorkspace(globalOptions.workspace)
385
+ const formattedOutput = formatter.formatValidationReport(report)
386
+ console.log(formattedOutput)
387
+ process.exit(0)
388
+ if (!report.isValid) {
389
+ process.exit(1)
390
+ }
272
391
  } else if (options.stats) {
273
- const stats = await services.workspaceService.getWorkspaceStats(globalOptions.workspace);
274
- const formattedOutput = formatter.formatWorkspaceStats(stats);
275
- console.log(formattedOutput);
392
+ const stats = await services.workspaceService.getWorkspaceStats(globalOptions.workspace)
393
+ const formattedOutput = formatter.formatWorkspaceStats(stats)
394
+ console.log(formattedOutput)
395
+ process.exit(0)
276
396
  } else {
277
- const info = await services.workspaceService.getWorkspaceInfo(globalOptions.workspace);
278
- console.log(formatter.formatMessage(`Workspace: ${info.name}`, 'info'));
279
- console.log(formatter.formatMessage(`Path: ${info.path}`, 'info'));
280
- console.log(formatter.formatMessage(`Packages: ${info.packageCount}`, 'info'));
281
- console.log(formatter.formatMessage(`Catalogs: ${info.catalogCount}`, 'info'));
397
+ const info = await services.workspaceService.getWorkspaceInfo(globalOptions.workspace)
398
+ console.log(formatter.formatMessage(`Workspace: ${info.name}`, 'info'))
399
+ console.log(formatter.formatMessage(`Path: ${info.path}`, 'info'))
400
+ console.log(formatter.formatMessage(`Packages: ${info.packageCount}`, 'info'))
401
+ console.log(formatter.formatMessage(`Catalogs: ${info.catalogCount}`, 'info'))
282
402
 
283
403
  if (info.catalogNames.length > 0) {
284
404
  console.log(
285
405
  formatter.formatMessage(`Catalog names: ${info.catalogNames.join(', ')}`, 'info')
286
- );
406
+ )
287
407
  }
408
+ process.exit(0)
288
409
  }
289
410
  } catch (error) {
290
- console.error(chalk.red('❌ Error:'), error);
291
- process.exit(1);
411
+ console.error(chalk.red('❌ Error:'), error)
412
+ process.exit(1)
292
413
  }
293
- });
414
+ })
294
415
 
295
416
  // Theme command
296
417
  program
@@ -303,65 +424,65 @@ export async function main(): Promise<void> {
303
424
  .action(async (options, _command) => {
304
425
  try {
305
426
  if (options.list) {
306
- const themes = ThemeManager.listThemes();
307
- console.log(StyledText.iconInfo('Available themes:'));
427
+ const themes = ThemeManager.listThemes()
428
+ console.log(StyledText.iconInfo('Available themes:'))
308
429
  themes.forEach((theme) => {
309
- console.log(` • ${theme}`);
310
- });
311
- return;
430
+ console.log(` • ${theme}`)
431
+ })
432
+ return
312
433
  }
313
434
 
314
435
  if (options.set) {
315
- const themes = ThemeManager.listThemes();
436
+ const themes = ThemeManager.listThemes()
316
437
  if (!themes.includes(options.set)) {
317
- console.error(StyledText.iconError(`Invalid theme: ${options.set}`));
318
- console.log(StyledText.muted(`Available themes: ${themes.join(', ')}`));
319
- process.exit(1);
438
+ console.error(StyledText.iconError(`Invalid theme: ${options.set}`))
439
+ console.log(StyledText.muted(`Available themes: ${themes.join(', ')}`))
440
+ process.exit(1)
320
441
  }
321
442
 
322
- ThemeManager.setTheme(options.set);
323
- console.log(StyledText.iconSuccess(`Theme set to: ${options.set}`));
443
+ ThemeManager.setTheme(options.set)
444
+ console.log(StyledText.iconSuccess(`Theme set to: ${options.set}`))
324
445
 
325
446
  // Show a preview
326
- console.log('\nTheme preview:');
327
- const theme = ThemeManager.getTheme();
328
- console.log(` ${theme.success('✓ Success message')}`);
329
- console.log(` ${theme.warning('⚠ Warning message')}`);
330
- console.log(` ${theme.error('✗ Error message')}`);
331
- console.log(` ${theme.info('ℹ Info message')}`);
447
+ console.log('\nTheme preview:')
448
+ const theme = ThemeManager.getTheme()
449
+ console.log(` ${theme.success('✓ Success message')}`)
450
+ console.log(` ${theme.warning('⚠ Warning message')}`)
451
+ console.log(` ${theme.error('✗ Error message')}`)
452
+ console.log(` ${theme.info('ℹ Info message')}`)
332
453
  console.log(
333
454
  ` ${theme.major('Major update')} | ${theme.minor('Minor update')} | ${theme.patch('Patch update')}`
334
- );
335
- return;
455
+ )
456
+ return
336
457
  }
337
458
 
338
459
  if (options.interactive) {
339
- const interactivePrompts = new InteractivePrompts();
340
- const config = await interactivePrompts.configurationWizard();
460
+ const interactivePrompts = new InteractivePrompts()
461
+ const config = await interactivePrompts.configurationWizard()
341
462
 
342
463
  if (config.theme) {
343
- ThemeManager.setTheme(config.theme);
344
- console.log(StyledText.iconSuccess(`Theme configured: ${config.theme}`));
464
+ ThemeManager.setTheme(config.theme)
465
+ console.log(StyledText.iconSuccess(`Theme configured: ${config.theme}`))
345
466
  }
346
- return;
467
+ return
347
468
  }
348
469
 
349
470
  // Default: show current theme and list
350
- const currentTheme = ThemeManager.getTheme();
351
- console.log(StyledText.iconInfo('Current theme settings:'));
352
- console.log(` Theme: ${currentTheme ? 'custom' : 'default'}`);
353
- console.log('\nAvailable themes:');
471
+ const currentTheme = ThemeManager.getTheme()
472
+ console.log(StyledText.iconInfo('Current theme settings:'))
473
+ console.log(` Theme: ${currentTheme ? 'custom' : 'default'}`)
474
+ console.log('\nAvailable themes:')
354
475
  ThemeManager.listThemes().forEach((theme) => {
355
- console.log(` • ${theme}`);
356
- });
476
+ console.log(` • ${theme}`)
477
+ })
357
478
  console.log(
358
479
  StyledText.muted('\nUse --set <theme> to change theme or --interactive for guided setup')
359
- );
480
+ )
360
481
  } catch (error) {
361
- console.error(StyledText.iconError('Error configuring theme:'), error);
362
- process.exit(1);
482
+ console.error(StyledText.iconError('Error configuring theme:'), error)
483
+ process.exit(1)
363
484
  }
364
- });
485
+ })
365
486
 
366
487
  // Security command
367
488
  program
@@ -376,13 +497,13 @@ export async function main(): Promise<void> {
376
497
  .option('--snyk', 'include Snyk scan (requires snyk CLI)')
377
498
  .action(async (options, command) => {
378
499
  try {
379
- const globalOptions = command.parent.opts();
500
+ const globalOptions = command.parent.opts()
380
501
  const formatter = new OutputFormatter(
381
502
  options.format as OutputFormat,
382
503
  !globalOptions.noColor
383
- );
504
+ )
384
505
 
385
- const securityCommand = new SecurityCommand(formatter);
506
+ const securityCommand = new SecurityCommand(formatter)
386
507
 
387
508
  await securityCommand.execute({
388
509
  workspace: globalOptions.workspace,
@@ -394,12 +515,13 @@ export async function main(): Promise<void> {
394
515
  snyk: options.snyk,
395
516
  verbose: globalOptions.verbose,
396
517
  color: !globalOptions.noColor,
397
- });
518
+ })
519
+ process.exit(0)
398
520
  } catch (error) {
399
- console.error(chalk.red('❌ Error:'), error);
400
- process.exit(1);
521
+ console.error(chalk.red('❌ Error:'), error)
522
+ process.exit(1)
401
523
  }
402
- });
524
+ })
403
525
 
404
526
  // Init command
405
527
  program
@@ -417,8 +539,8 @@ export async function main(): Promise<void> {
417
539
  .option('-f, --format <type>', 'output format: table, json, yaml, minimal', 'table')
418
540
  .action(async (options, command) => {
419
541
  try {
420
- const globalOptions = command.parent.opts();
421
- const initCommand = new InitCommand();
542
+ const globalOptions = command.parent.opts()
543
+ const initCommand = new InitCommand()
422
544
 
423
545
  await initCommand.execute({
424
546
  workspace: globalOptions.workspace,
@@ -427,12 +549,130 @@ export async function main(): Promise<void> {
427
549
  createWorkspace: options.createWorkspace,
428
550
  verbose: globalOptions.verbose,
429
551
  color: !globalOptions.noColor,
430
- });
552
+ })
553
+ process.exit(0)
554
+ } catch (error) {
555
+ console.error(chalk.red('❌ Error:'), error)
556
+ process.exit(1)
557
+ }
558
+ })
559
+
560
+ // AI command - check AI provider status and availability
561
+ program
562
+ .command('ai')
563
+ .description('check AI provider status and availability')
564
+ .option('--status', 'show status of all AI providers (default)')
565
+ .option('--test', 'test AI analysis with a sample request')
566
+ .option('--cache-stats', 'show AI analysis cache statistics')
567
+ .option('--clear-cache', 'clear AI analysis cache')
568
+ .action(async (options) => {
569
+ try {
570
+ const aiDetector = new AIDetector()
571
+
572
+ if (options.clearCache) {
573
+ // Import the cache singleton
574
+ const { analysisCache } = await import('@pcu/core')
575
+ analysisCache.clear()
576
+ console.log(chalk.green('✅ AI analysis cache cleared'))
577
+ process.exit(0)
578
+ return
579
+ }
580
+
581
+ if (options.cacheStats) {
582
+ const { analysisCache } = await import('@pcu/core')
583
+ const stats = analysisCache.getStats()
584
+ console.log(chalk.blue('📊 AI Analysis Cache Statistics'))
585
+ console.log(chalk.gray('─────────────────────────────────'))
586
+ console.log(` Total entries: ${chalk.cyan(stats.totalEntries)}`)
587
+ console.log(` Cache hits: ${chalk.green(stats.hits)}`)
588
+ console.log(` Cache misses: ${chalk.yellow(stats.misses)}`)
589
+ console.log(` Hit rate: ${chalk.cyan(`${(stats.hitRate * 100).toFixed(1)}%`)}`)
590
+ process.exit(0)
591
+ return
592
+ }
593
+
594
+ if (options.test) {
595
+ console.log(chalk.blue('🧪 Testing AI analysis...'))
596
+
597
+ const aiService = new AIAnalysisService({
598
+ config: {
599
+ fallback: { enabled: true, useRuleEngine: true },
600
+ },
601
+ })
602
+
603
+ const testPackages = [
604
+ {
605
+ name: 'lodash',
606
+ currentVersion: '4.17.20',
607
+ targetVersion: '4.17.21',
608
+ updateType: 'patch' as const,
609
+ },
610
+ ]
611
+
612
+ const testWorkspaceInfo = {
613
+ name: 'test-workspace',
614
+ path: process.cwd(),
615
+ packageCount: 1,
616
+ catalogCount: 1,
617
+ }
618
+
619
+ try {
620
+ const result = await aiService.analyzeUpdates(testPackages, testWorkspaceInfo, {
621
+ analysisType: 'impact',
622
+ })
623
+ console.log(chalk.green('✅ AI analysis test successful!'))
624
+ console.log(chalk.gray('─────────────────────────────────'))
625
+ console.log(` Provider: ${chalk.cyan(result.provider)}`)
626
+ console.log(` Confidence: ${chalk.cyan(`${(result.confidence * 100).toFixed(0)}%`)}`)
627
+ console.log(` Summary: ${result.summary}`)
628
+ } catch (error) {
629
+ console.log(chalk.yellow('⚠️ AI analysis test failed:'))
630
+ console.log(chalk.gray(String(error)))
631
+ }
632
+ process.exit(0)
633
+ return
634
+ }
635
+
636
+ // Default: show status
637
+ console.log(chalk.blue('🤖 AI Provider Status'))
638
+ console.log(chalk.gray('─────────────────────────────────'))
639
+
640
+ const summary = await aiDetector.getDetectionSummary()
641
+ console.log(summary)
642
+
643
+ const providers = await aiDetector.detectAvailableProviders()
644
+ console.log('')
645
+ console.log(chalk.blue('📋 Provider Details'))
646
+ console.log(chalk.gray('─────────────────────────────────'))
647
+
648
+ for (const provider of providers) {
649
+ const statusIcon = provider.available ? chalk.green('✓') : chalk.red('✗')
650
+ const statusText = provider.available ? chalk.green('Available') : chalk.gray('Not found')
651
+ const priorityText = chalk.gray(`(priority: ${provider.priority})`)
652
+
653
+ console.log(
654
+ ` ${statusIcon} ${chalk.cyan(provider.name)} - ${statusText} ${priorityText}`
655
+ )
656
+
657
+ if (provider.available && provider.path) {
658
+ console.log(chalk.gray(` Path: ${provider.path}`))
659
+ }
660
+ if (provider.available && provider.version) {
661
+ console.log(chalk.gray(` Version: ${provider.version}`))
662
+ }
663
+ }
664
+
665
+ const best = await aiDetector.getBestProvider()
666
+ if (best) {
667
+ console.log('')
668
+ console.log(chalk.green(`✨ Best available provider: ${best.name}`))
669
+ }
670
+ process.exit(0)
431
671
  } catch (error) {
432
- console.error(chalk.red('❌ Error:'), error);
433
- process.exit(1);
672
+ console.error(chalk.red('❌ Error:'), error)
673
+ process.exit(1)
434
674
  }
435
- });
675
+ })
436
676
 
437
677
  // Add help command
438
678
  program
@@ -442,22 +682,22 @@ export async function main(): Promise<void> {
442
682
  .description('display help for command')
443
683
  .action((command) => {
444
684
  if (command) {
445
- const cmd = program.commands.find((c) => c.name() === command);
685
+ const cmd = program.commands.find((c) => c.name() === command)
446
686
  if (cmd) {
447
- cmd.help();
687
+ cmd.help()
448
688
  } else {
449
- console.log(chalk.red(`Unknown command: ${command}`));
689
+ console.log(chalk.red(`Unknown command: ${command}`))
450
690
  }
451
691
  } else {
452
- program.help();
692
+ program.help()
453
693
  }
454
- });
694
+ })
455
695
 
456
696
  // Let commander handle help and version normally
457
697
  // program.exitOverride() removed to fix help/version output
458
698
 
459
699
  // Handle shorthand options and single-letter commands by rewriting arguments
460
- const args = [...process.argv];
700
+ const args = [...process.argv]
461
701
  // Map single-letter command 'i' -> init (changed from interactive mode)
462
702
  if (
463
703
  args.includes('i') &&
@@ -471,95 +711,95 @@ export async function main(): Promise<void> {
471
711
  a === '--interactive'
472
712
  )
473
713
  ) {
474
- const index = args.findIndex((arg) => arg === 'i');
475
- args.splice(index, 1, 'init');
714
+ const index = args.indexOf('i')
715
+ args.splice(index, 1, 'init')
476
716
  }
477
717
 
478
718
  if (args.includes('-u') || args.includes('--update')) {
479
- const index = args.findIndex((arg) => arg === '-u' || arg === '--update');
480
- args.splice(index, 1, 'update');
719
+ const index = args.findIndex((arg) => arg === '-u' || arg === '--update')
720
+ args.splice(index, 1, 'update')
481
721
  } else if (
482
722
  (args.includes('-i') || args.includes('--interactive')) &&
483
723
  !args.some((a) => a === 'update' || a === '-u' || a === '--update')
484
724
  ) {
485
725
  // Map standalone -i to `update -i`
486
- const index = args.findIndex((arg) => arg === '-i' || arg === '--interactive');
726
+ const index = args.findIndex((arg) => arg === '-i' || arg === '--interactive')
487
727
  // Replace the flag position with 'update' and keep the flag after it
488
- args.splice(index, 1, 'update', '-i');
728
+ args.splice(index, 1, 'update', '-i')
489
729
  } else if (args.includes('-c') || args.includes('--check')) {
490
- const index = args.findIndex((arg) => arg === '-c' || arg === '--check');
491
- args.splice(index, 1, 'check');
730
+ const index = args.findIndex((arg) => arg === '-c' || arg === '--check')
731
+ args.splice(index, 1, 'check')
492
732
  } else if (args.includes('-a') || args.includes('--analyze')) {
493
- const index = args.findIndex((arg) => arg === '-a' || arg === '--analyze');
494
- args.splice(index, 1, 'analyze');
733
+ const index = args.findIndex((arg) => arg === '-a' || arg === '--analyze')
734
+ args.splice(index, 1, 'analyze')
495
735
  } else if (args.includes('-s') || args.includes('--workspace-info')) {
496
- const index = args.findIndex((arg) => arg === '-s' || arg === '--workspace-info');
497
- args.splice(index, 1, 'workspace');
736
+ const index = args.findIndex((arg) => arg === '-s' || arg === '--workspace-info')
737
+ args.splice(index, 1, 'workspace')
498
738
  } else if (args.includes('-t') || args.includes('--theme')) {
499
- const index = args.findIndex((arg) => arg === '-t' || arg === '--theme');
500
- args.splice(index, 1, 'theme');
739
+ const index = args.findIndex((arg) => arg === '-t' || arg === '--theme')
740
+ args.splice(index, 1, 'theme')
501
741
  } else if (args.includes('--security-audit')) {
502
- const index = args.findIndex((arg) => arg === '--security-audit');
503
- args.splice(index, 1, 'security');
742
+ const index = args.indexOf('--security-audit')
743
+ args.splice(index, 1, 'security')
504
744
  } else if (args.includes('--security-fix')) {
505
- const index = args.findIndex((arg) => arg === '--security-fix');
506
- args.splice(index, 1, 'security', '--fix-vulns');
745
+ const index = args.indexOf('--security-fix')
746
+ args.splice(index, 1, 'security', '--fix-vulns')
507
747
  }
508
748
 
509
749
  // Show help if no arguments provided
510
750
  if (args.length <= 2) {
511
- program.help();
751
+ program.help()
512
752
  }
513
753
 
514
754
  // Handle custom --version with update checking
515
755
  if (args.includes('--version')) {
516
- console.log(packageJson.version);
756
+ console.log(packageJson.version)
517
757
 
518
758
  // Check for updates if not in CI and enabled in config
519
759
  if (VersionChecker.shouldCheckForUpdates() && config.advanced?.checkForUpdates !== false) {
520
760
  try {
521
- console.log(chalk.gray('Checking for updates...'));
761
+ console.log(chalk.gray('Checking for updates...'))
522
762
  const versionResult = await VersionChecker.checkVersion(packageJson.version, {
523
763
  skipPrompt: false,
524
764
  timeout: 5000, // Longer timeout for explicit version check
525
- });
765
+ })
526
766
 
527
767
  if (versionResult.shouldPrompt) {
528
- const didUpdate = await VersionChecker.promptAndUpdate(versionResult);
768
+ const didUpdate = await VersionChecker.promptAndUpdate(versionResult)
529
769
  if (didUpdate) {
530
- console.log(chalk.blue('Please run your command again to use the updated version.'));
531
- process.exit(0);
770
+ console.log(chalk.blue('Please run your command again to use the updated version.'))
771
+ process.exit(0)
532
772
  }
533
773
  } else if (versionResult.isLatest) {
534
- console.log(chalk.green('You are using the latest version!'));
774
+ console.log(chalk.green('You are using the latest version!'))
535
775
  }
536
776
  } catch (error) {
537
777
  // Silently fail update check for version command
538
778
  if (args.includes('-v') || args.includes('--verbose')) {
539
- console.warn(chalk.yellow('⚠️ Could not check for updates:'), error);
779
+ console.warn(chalk.yellow('⚠️ Could not check for updates:'), error)
540
780
  }
541
781
  }
542
782
  }
543
783
 
544
- process.exit(0);
784
+ process.exit(0)
545
785
  }
546
786
 
547
787
  // Parse command line arguments
548
788
  try {
549
- await program.parseAsync(args);
789
+ await program.parseAsync(args)
550
790
  } catch (error) {
551
- console.error(chalk.red('❌ Unexpected error:'), error);
791
+ console.error(chalk.red('❌ Unexpected error:'), error)
552
792
  if (error instanceof Error && error.stack) {
553
- console.error(chalk.gray(error.stack));
793
+ console.error(chalk.gray(error.stack))
554
794
  }
555
- process.exit(1);
795
+ process.exit(1)
556
796
  }
557
797
  }
558
798
 
559
799
  // Run the CLI if this file is executed directly
560
800
  if (import.meta.url === `file://${process.argv[1]}`) {
561
801
  main().catch((error) => {
562
- console.error(chalk.red('❌ Fatal error:'), error);
563
- process.exit(1);
564
- });
802
+ console.error(chalk.red('❌ Fatal error:'), error)
803
+ process.exit(1)
804
+ })
565
805
  }