pnpm-catalog-updates 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +22031 -10684
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
  6. package/src/cli/commandRegistrar.ts +785 -0
  7. package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
  8. package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
  9. package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
  10. package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
  11. package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
  12. package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
  13. package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
  14. package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
  15. package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
  16. package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
  17. package/src/cli/commands/aiCommand.ts +163 -0
  18. package/src/cli/commands/analyzeCommand.ts +219 -0
  19. package/src/cli/commands/checkCommand.ts +91 -98
  20. package/src/cli/commands/graphCommand.ts +475 -0
  21. package/src/cli/commands/initCommand.ts +64 -54
  22. package/src/cli/commands/rollbackCommand.ts +334 -0
  23. package/src/cli/commands/securityCommand.ts +165 -100
  24. package/src/cli/commands/themeCommand.ts +148 -0
  25. package/src/cli/commands/updateCommand.ts +215 -263
  26. package/src/cli/commands/workspaceCommand.ts +73 -0
  27. package/src/cli/constants/cliChoices.ts +93 -0
  28. package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
  29. package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
  30. package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
  31. package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
  32. package/src/cli/formatters/ciFormatter.ts +964 -0
  33. package/src/cli/formatters/colorUtils.ts +145 -0
  34. package/src/cli/formatters/outputFormatter.ts +615 -332
  35. package/src/cli/formatters/progressBar.ts +43 -52
  36. package/src/cli/formatters/versionFormatter.ts +132 -0
  37. package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
  38. package/src/cli/handlers/changelogHandler.ts +113 -0
  39. package/src/cli/handlers/index.ts +9 -0
  40. package/src/cli/handlers/installHandler.ts +130 -0
  41. package/src/cli/index.ts +175 -726
  42. package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
  43. package/src/cli/interactive/interactivePrompts.ts +189 -83
  44. package/src/cli/interactive/optionUtils.ts +89 -0
  45. package/src/cli/themes/colorTheme.ts +43 -16
  46. package/src/cli/utils/cliOutput.ts +118 -0
  47. package/src/cli/utils/commandHelpers.ts +249 -0
  48. package/src/cli/validators/commandValidator.ts +321 -336
  49. package/src/cli/validators/index.ts +37 -2
  50. package/src/cli/options/globalOptions.ts +0 -437
  51. package/src/cli/options/index.ts +0 -5
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Workspace Command Tests
3
+ */
4
+
5
+ import type { WorkspaceService } from '@pcu/core'
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ // Use vi.hoisted to ensure mocks are available during vi.mock hoisting
9
+ const mocks = vi.hoisted(() => {
10
+ // Create a mock CommandExitError class for instanceof checks
11
+ class MockCommandExitError extends Error {
12
+ public readonly exitCode: number
13
+ public readonly silent: boolean
14
+
15
+ constructor(exitCode: number, message?: string, silent = false) {
16
+ super(message || (exitCode === 0 ? 'Command completed successfully' : 'Command failed'))
17
+ this.name = 'CommandExitError'
18
+ this.exitCode = exitCode
19
+ this.silent = silent
20
+ }
21
+
22
+ static success(message?: string): MockCommandExitError {
23
+ return new MockCommandExitError(0, message, true)
24
+ }
25
+
26
+ static failure(message?: string): MockCommandExitError {
27
+ return new MockCommandExitError(1, message)
28
+ }
29
+
30
+ static withCode(code: number, message?: string): MockCommandExitError {
31
+ return new MockCommandExitError(code, message)
32
+ }
33
+ }
34
+
35
+ return {
36
+ CommandExitError: MockCommandExitError,
37
+ }
38
+ })
39
+
40
+ // Mock @pcu/utils - include CommandExitError for instanceof checks
41
+ vi.mock('@pcu/utils', () => ({
42
+ CommandExitError: mocks.CommandExitError,
43
+ logger: {
44
+ error: vi.fn(),
45
+ warn: vi.fn(),
46
+ info: vi.fn(),
47
+ debug: vi.fn(),
48
+ },
49
+ t: (key: string) => key,
50
+ }))
51
+
52
+ // Mock ThemeManager and StyledText
53
+ vi.mock('../../themes/colorTheme.js', () => ({
54
+ ThemeManager: {
55
+ setTheme: vi.fn(),
56
+ getTheme: vi.fn().mockReturnValue({
57
+ major: (text: string) => text,
58
+ minor: (text: string) => text,
59
+ patch: (text: string) => text,
60
+ }),
61
+ },
62
+ StyledText: {
63
+ iconAnalysis: (text: string) => `[analysis]${text}`,
64
+ iconSuccess: (text: string) => `[success]${text}`,
65
+ iconInfo: (text: string) => `[info]${text}`,
66
+ iconError: (text: string) => `[error]${text}`,
67
+ iconSecurity: (text: string) => `[security]${text}`,
68
+ iconUpdate: (text: string) => `[update]${text}`,
69
+ iconWarning: (text?: string) => (text ? `[warning]${text}` : '[warning]'),
70
+ muted: (text: string) => `[muted]${text}`,
71
+ error: (text: string) => `[error]${text}`,
72
+ bold: (text: string) => `[bold]${text}`,
73
+ },
74
+ }))
75
+
76
+ // Create a chainable chalk mock
77
+ const createChalkMock = () => {
78
+ const createColorFn = (text: string) => text
79
+ const colorFn = Object.assign(createColorFn, {
80
+ bold: Object.assign((text: string) => text, {
81
+ cyan: (text: string) => text,
82
+ white: (text: string) => text,
83
+ }),
84
+ dim: Object.assign((text: string) => text, {
85
+ white: (text: string) => text,
86
+ }),
87
+ red: Object.assign((text: string) => text, {
88
+ bold: (text: string) => text,
89
+ }),
90
+ green: (text: string) => text,
91
+ yellow: (text: string) => text,
92
+ blue: (text: string) => text,
93
+ gray: (text: string) => text,
94
+ cyan: (text: string) => text,
95
+ white: (text: string) => text,
96
+ })
97
+ return colorFn
98
+ }
99
+
100
+ // Mock chalk
101
+ vi.mock('chalk', () => ({
102
+ default: createChalkMock(),
103
+ }))
104
+
105
+ // Mock OutputFormatter - needs to be a proper class constructor
106
+ vi.mock('../../formatters/outputFormatter.js', () => {
107
+ return {
108
+ OutputFormatter: class MockOutputFormatter {
109
+ formatValidationReport = vi.fn().mockReturnValue('Formatted validation')
110
+ formatWorkspaceStats = vi.fn().mockReturnValue('Formatted stats')
111
+ formatWorkspaceInfo = vi.fn().mockReturnValue('Formatted workspace info: default, react17')
112
+ formatMessage = vi.fn().mockImplementation((msg: string) => msg)
113
+ formatOutdatedReport = vi.fn()
114
+ formatUpdatePlan = vi.fn()
115
+ formatUpdateResult = vi.fn()
116
+ formatSecurityReport = vi.fn()
117
+ formatImpactAnalysis = vi.fn()
118
+ },
119
+ }
120
+ })
121
+
122
+ // Import after mock setup
123
+ const { WorkspaceCommand } = await import('../workspaceCommand.js')
124
+
125
+ describe('WorkspaceCommand', () => {
126
+ let command: InstanceType<typeof WorkspaceCommand>
127
+ let mockWorkspaceService: WorkspaceService
128
+ let consoleSpy: ReturnType<typeof vi.spyOn>
129
+
130
+ beforeEach(() => {
131
+ vi.clearAllMocks()
132
+
133
+ // Mock console.log
134
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
135
+
136
+ // Create mock workspace service
137
+ mockWorkspaceService = {
138
+ discoverWorkspace: vi.fn(),
139
+ getWorkspaceInfo: vi.fn().mockResolvedValue({
140
+ path: '/test/workspace',
141
+ name: 'test-workspace',
142
+ isValid: true,
143
+ hasPackages: true,
144
+ hasCatalogs: true,
145
+ packageCount: 5,
146
+ catalogCount: 2,
147
+ catalogNames: ['default', 'react17'],
148
+ }),
149
+ validateWorkspace: vi.fn().mockResolvedValue({
150
+ isValid: true,
151
+ errors: [],
152
+ warnings: [],
153
+ recommendations: [],
154
+ workspace: { isValid: true },
155
+ catalogs: { isValid: true, errors: [], warnings: [] },
156
+ packages: { isValid: true, errors: [], warnings: [] },
157
+ }),
158
+ getWorkspaceStats: vi.fn().mockResolvedValue({
159
+ workspace: { path: '/test/workspace', name: 'test-workspace' },
160
+ packages: { total: 5, withCatalogReferences: 3 },
161
+ catalogs: { total: 2, names: ['default', 'react17'], totalEntries: 30 },
162
+ dependencies: {
163
+ total: 50,
164
+ catalogManaged: 30,
165
+ catalogReferences: 25,
166
+ byType: {
167
+ dependencies: 25,
168
+ devDependencies: 15,
169
+ peerDependencies: 5,
170
+ optionalDependencies: 5,
171
+ },
172
+ },
173
+ }),
174
+ getCatalogs: vi.fn(),
175
+ getPackages: vi.fn(),
176
+ usesCatalogs: vi.fn(),
177
+ getPackagesUsingCatalog: vi.fn(),
178
+ findWorkspaces: vi.fn(),
179
+ checkHealth: vi.fn(),
180
+ } as unknown as WorkspaceService
181
+
182
+ // Create command instance
183
+ command = new WorkspaceCommand(mockWorkspaceService)
184
+ })
185
+
186
+ afterEach(() => {
187
+ consoleSpy.mockRestore()
188
+ vi.resetAllMocks()
189
+ })
190
+
191
+ describe('execute', () => {
192
+ it('should show workspace info by default', async () => {
193
+ try {
194
+ await command.execute({})
195
+ } catch (error) {
196
+ expect((error as { exitCode: number }).exitCode).toBe(0)
197
+ }
198
+ expect(mockWorkspaceService.getWorkspaceInfo).toHaveBeenCalled()
199
+ expect(consoleSpy).toHaveBeenCalled()
200
+ })
201
+
202
+ it('should show workspace info with specified workspace path', async () => {
203
+ try {
204
+ await command.execute({ workspace: '/custom/workspace' })
205
+ } catch {
206
+ // Expected to throw CommandExitError
207
+ }
208
+
209
+ expect(mockWorkspaceService.getWorkspaceInfo).toHaveBeenCalledWith('/custom/workspace')
210
+ })
211
+
212
+ it('should validate workspace when --validate flag is set', async () => {
213
+ try {
214
+ await command.execute({ validate: true })
215
+ } catch (error) {
216
+ expect((error as { exitCode: number }).exitCode).toBe(0)
217
+ }
218
+ expect(mockWorkspaceService.validateWorkspace).toHaveBeenCalled()
219
+ })
220
+
221
+ it('should return exit code 1 when validation fails', async () => {
222
+ mockWorkspaceService.validateWorkspace = vi.fn().mockResolvedValue({
223
+ isValid: false,
224
+ errors: ['Error 1'],
225
+ warnings: [],
226
+ recommendations: [],
227
+ workspace: { isValid: false },
228
+ catalogs: { isValid: false, errors: ['Catalog error'], warnings: [] },
229
+ packages: { isValid: true, errors: [], warnings: [] },
230
+ })
231
+
232
+ try {
233
+ await command.execute({ validate: true })
234
+ // Should not reach here
235
+ expect.fail('Should have thrown CommandExitError')
236
+ } catch (error) {
237
+ expect((error as { exitCode: number }).exitCode).toBe(1)
238
+ }
239
+ })
240
+
241
+ it('should show stats when --stats flag is set', async () => {
242
+ try {
243
+ await command.execute({ stats: true })
244
+ } catch (error) {
245
+ expect((error as { exitCode: number }).exitCode).toBe(0)
246
+ }
247
+ expect(mockWorkspaceService.getWorkspaceStats).toHaveBeenCalled()
248
+ })
249
+
250
+ it('should show catalog names in output', async () => {
251
+ try {
252
+ await command.execute({})
253
+ } catch {
254
+ // Expected to throw CommandExitError
255
+ }
256
+
257
+ // Check that console.log was called with catalog names
258
+ const calls = consoleSpy.mock.calls
259
+ const catalogNamesCall = calls.find(
260
+ (call) => String(call[0]).includes('default') && String(call[0]).includes('react17')
261
+ )
262
+ expect(catalogNamesCall).toBeDefined()
263
+ })
264
+
265
+ it('should work with different output formats', async () => {
266
+ try {
267
+ await command.execute({ format: 'json' })
268
+ } catch {
269
+ // Expected to throw CommandExitError
270
+ }
271
+
272
+ expect(mockWorkspaceService.getWorkspaceInfo).toHaveBeenCalled()
273
+ })
274
+ })
275
+
276
+ describe('getHelpText', () => {
277
+ it('should return help text', () => {
278
+ const helpText = WorkspaceCommand.getHelpText()
279
+
280
+ expect(helpText).toContain('Workspace information')
281
+ expect(helpText).toContain('--validate')
282
+ expect(helpText).toContain('--stats')
283
+ expect(helpText).toContain('--format')
284
+ })
285
+ })
286
+ })
@@ -0,0 +1,163 @@
1
+ /**
2
+ * AI Command
3
+ *
4
+ * CLI command to check AI provider status and availability.
5
+ */
6
+
7
+ import { AIAnalysisService, AIDetector, analysisCache } from '@pcu/core'
8
+ import { t } from '@pcu/utils'
9
+ import chalk from 'chalk'
10
+ import { cliOutput } from '../utils/cliOutput.js'
11
+
12
+ export interface AiCommandOptions {
13
+ status?: boolean
14
+ test?: boolean
15
+ cacheStats?: boolean
16
+ clearCache?: boolean
17
+ }
18
+
19
+ export class AiCommand {
20
+ /**
21
+ * Execute the AI command
22
+ */
23
+ async execute(options: AiCommandOptions = {}): Promise<void> {
24
+ if (options.clearCache) {
25
+ analysisCache.clear()
26
+ cliOutput.print(chalk.green(`✅ ${t('command.ai.cacheCleared')}`))
27
+ return
28
+ }
29
+
30
+ if (options.cacheStats) {
31
+ const stats = analysisCache.getStats()
32
+ cliOutput.print(chalk.blue(`📊 ${t('command.ai.cacheStats')}`))
33
+ cliOutput.print(chalk.gray('─────────────────────────────────'))
34
+ cliOutput.print(` ${t('command.ai.totalEntries')}: ${chalk.cyan(stats.totalEntries)}`)
35
+ cliOutput.print(` ${t('command.ai.cacheHits')}: ${chalk.green(stats.hits)}`)
36
+ cliOutput.print(` ${t('command.ai.cacheMisses')}: ${chalk.yellow(stats.misses)}`)
37
+ cliOutput.print(
38
+ ` ${t('command.ai.hitRate')}: ${chalk.cyan(`${(stats.hitRate * 100).toFixed(1)}%`)}`
39
+ )
40
+ return
41
+ }
42
+
43
+ if (options.test) {
44
+ await this.runTest()
45
+ return
46
+ }
47
+
48
+ // Default: show status
49
+ await this.showStatus()
50
+ }
51
+
52
+ /**
53
+ * Run AI analysis test
54
+ */
55
+ private async runTest(): Promise<void> {
56
+ cliOutput.print(chalk.blue(`🧪 ${t('command.ai.testingAnalysis')}`))
57
+
58
+ const aiService = new AIAnalysisService({
59
+ config: {
60
+ fallback: { enabled: true, useRuleEngine: true },
61
+ },
62
+ })
63
+
64
+ const testPackages = [
65
+ {
66
+ name: 'lodash',
67
+ currentVersion: '4.17.20',
68
+ targetVersion: '4.17.21',
69
+ updateType: 'patch' as const,
70
+ },
71
+ ]
72
+
73
+ const testWorkspaceInfo = {
74
+ name: 'test-workspace',
75
+ path: process.cwd(),
76
+ packageCount: 1,
77
+ catalogCount: 1,
78
+ }
79
+
80
+ try {
81
+ const result = await aiService.analyzeUpdates(testPackages, testWorkspaceInfo, {
82
+ analysisType: 'impact',
83
+ })
84
+ cliOutput.print(chalk.green(`✅ ${t('command.ai.testSuccess')}`))
85
+ cliOutput.print(chalk.gray('─────────────────────────────────'))
86
+ cliOutput.print(` ${t('command.ai.providerLabel')} ${chalk.cyan(result.provider)}`)
87
+ cliOutput.print(
88
+ ` ${t('command.ai.confidenceLabel')} ${chalk.cyan(`${(result.confidence * 100).toFixed(0)}%`)}`
89
+ )
90
+ cliOutput.print(` ${t('command.ai.summaryLabel')} ${result.summary}`)
91
+ } catch (error) {
92
+ cliOutput.print(chalk.yellow(`⚠️ ${t('command.ai.testFailed')}`))
93
+ cliOutput.print(chalk.gray(String(error)))
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Show AI provider status
99
+ */
100
+ private async showStatus(): Promise<void> {
101
+ const aiDetector = new AIDetector()
102
+
103
+ cliOutput.print(chalk.blue(`🤖 ${t('command.ai.providerStatus')}`))
104
+ cliOutput.print(chalk.gray('─────────────────────────────────'))
105
+
106
+ const summary = await aiDetector.getDetectionSummary()
107
+ cliOutput.print(summary)
108
+
109
+ const providers = await aiDetector.detectAvailableProviders()
110
+ cliOutput.print('')
111
+ cliOutput.print(chalk.blue(`📋 ${t('command.ai.providerDetails')}`))
112
+ cliOutput.print(chalk.gray('─────────────────────────────────'))
113
+
114
+ for (const provider of providers) {
115
+ const statusIcon = provider.available ? chalk.green('✓') : chalk.red('✗')
116
+ const statusText = provider.available
117
+ ? chalk.green(t('command.ai.available'))
118
+ : chalk.gray(t('command.ai.notFound'))
119
+ const priorityText = chalk.gray(`(priority: ${provider.priority})`)
120
+
121
+ cliOutput.print(
122
+ ` ${statusIcon} ${chalk.cyan(provider.name)} - ${statusText} ${priorityText}`
123
+ )
124
+
125
+ if (provider.available && provider.path) {
126
+ cliOutput.print(chalk.gray(` ${t('command.ai.pathLabel')} ${provider.path}`))
127
+ }
128
+ if (provider.available && provider.version) {
129
+ cliOutput.print(chalk.gray(` ${t('command.ai.versionLabel')} ${provider.version}`))
130
+ }
131
+ }
132
+
133
+ const best = await aiDetector.getBestProvider()
134
+ if (best) {
135
+ cliOutput.print('')
136
+ cliOutput.print(chalk.green(`✨ ${t('command.ai.bestProvider', { provider: best.name })}`))
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get command help text
142
+ */
143
+ static getHelpText(): string {
144
+ return `
145
+ Check AI provider status and availability
146
+
147
+ Usage:
148
+ pcu ai [options]
149
+
150
+ Options:
151
+ --status Show status of all AI providers (default)
152
+ --test Test AI analysis with a sample request
153
+ --cache-stats Show AI analysis cache statistics
154
+ --clear-cache Clear AI analysis cache
155
+
156
+ Examples:
157
+ pcu ai # Show AI provider status
158
+ pcu ai --test # Test AI analysis
159
+ pcu ai --cache-stats # Show cache statistics
160
+ pcu ai --clear-cache # Clear the cache
161
+ `
162
+ }
163
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Analyze Command
3
+ *
4
+ * CLI command to analyze the impact of updating a specific dependency.
5
+ * Provides AI-powered and basic analysis options.
6
+ *
7
+ * QUAL-002/QUAL-003: Refactored to use unified output helpers and reduce coupling.
8
+ */
9
+
10
+ import type { AnalysisType, CatalogUpdateService, WorkspaceService } from '@pcu/core'
11
+ import { AIAnalysisService, NpmRegistryService } from '@pcu/core'
12
+ import { logger, t } from '@pcu/utils'
13
+ import type { OutputFormat } from '../formatters/outputFormatter.js'
14
+ import { StyledText } from '../themes/colorTheme.js'
15
+ import { cliOutput } from '../utils/cliOutput.js'
16
+ import { handleCommandError, initializeCommand } from '../utils/commandHelpers.js'
17
+ import { errorsOnly, validateAnalyzeOptions } from '../validators/index.js'
18
+
19
+ export interface AnalyzeCommandOptions {
20
+ workspace?: string
21
+ catalog?: string
22
+ format?: OutputFormat
23
+ ai?: boolean
24
+ provider?: string
25
+ analysisType?: AnalysisType
26
+ skipCache?: boolean
27
+ verbose?: boolean
28
+ color?: boolean
29
+ }
30
+
31
+ export class AnalyzeCommand {
32
+ /**
33
+ * QUAL-003: Optional services can be injected for testability.
34
+ * If not provided, default instances are created when needed.
35
+ */
36
+ constructor(
37
+ private readonly catalogUpdateService: CatalogUpdateService,
38
+ private readonly workspaceService: WorkspaceService,
39
+ private readonly registryService?: NpmRegistryService,
40
+ private readonly aiService?: AIAnalysisService
41
+ ) {}
42
+
43
+ /**
44
+ * Execute the analyze command
45
+ * QUAL-002/QUAL-003: Uses unified output helpers and reduced coupling
46
+ */
47
+ async execute(
48
+ packageName: string,
49
+ version: string | undefined,
50
+ options: AnalyzeCommandOptions = {}
51
+ ): Promise<void> {
52
+ // QUAL-005: Use shared command initialization
53
+ const { formatter } = await initializeCommand(options)
54
+
55
+ try {
56
+ // Auto-detect catalog if not specified
57
+ let catalog = options.catalog
58
+ if (!catalog) {
59
+ cliOutput.print(StyledText.muted(t('command.analyze.autoDetecting', { packageName })))
60
+ catalog =
61
+ (await this.catalogUpdateService.findCatalogForPackage(packageName, options.workspace)) ??
62
+ undefined
63
+ if (!catalog) {
64
+ logger.error('Package not found in any catalog', undefined, {
65
+ packageName,
66
+ workspace: options.workspace,
67
+ })
68
+ cliOutput.error(
69
+ StyledText.iconError(t('command.analyze.notFoundInCatalog', { packageName }))
70
+ )
71
+ cliOutput.print(StyledText.muted(t('command.analyze.specifyManually')))
72
+ throw new Error(t('command.analyze.notFoundInCatalog', { packageName }))
73
+ }
74
+ cliOutput.print(StyledText.muted(` ${t('command.analyze.foundInCatalog', { catalog })}`))
75
+ }
76
+
77
+ // Get latest version if not specified
78
+ // QUAL-003: Use injected registry service or create default
79
+ let targetVersion = version
80
+ if (!targetVersion) {
81
+ const registryService = this.registryService ?? new NpmRegistryService()
82
+ targetVersion = (await registryService.getLatestVersion(packageName)).toString()
83
+ }
84
+
85
+ // Get basic impact analysis first
86
+ const analysis = await this.catalogUpdateService.analyzeImpact(
87
+ catalog,
88
+ packageName,
89
+ targetVersion,
90
+ options.workspace
91
+ )
92
+
93
+ // AI analysis is enabled by default (use --no-ai to disable)
94
+ const aiEnabled = options.ai !== false
95
+
96
+ if (aiEnabled) {
97
+ cliOutput.print(StyledText.iconAnalysis(t('command.analyze.runningAI')))
98
+
99
+ // QUAL-003: Use injected AI service or create default
100
+ const aiService =
101
+ this.aiService ??
102
+ new AIAnalysisService({
103
+ config: {
104
+ preferredProvider: options.provider === 'auto' ? 'auto' : options.provider,
105
+ cache: { enabled: !options.skipCache, ttl: 3600 },
106
+ fallback: { enabled: true, useRuleEngine: true },
107
+ },
108
+ })
109
+
110
+ // Get workspace info
111
+ const workspaceInfo = await this.workspaceService.getWorkspaceInfo(options.workspace)
112
+
113
+ // Build packages info for AI analysis
114
+ const packages = [
115
+ {
116
+ name: packageName,
117
+ currentVersion: analysis.currentVersion,
118
+ targetVersion: analysis.proposedVersion,
119
+ updateType: analysis.updateType,
120
+ },
121
+ ]
122
+
123
+ // Build workspace info for AI
124
+ const wsInfo = {
125
+ name: workspaceInfo.name,
126
+ path: workspaceInfo.path,
127
+ packageCount: workspaceInfo.packageCount,
128
+ catalogCount: workspaceInfo.catalogCount,
129
+ }
130
+
131
+ try {
132
+ const aiResult = await aiService.analyzeUpdates(packages, wsInfo, {
133
+ analysisType: options.analysisType || 'impact',
134
+ skipCache: options.skipCache,
135
+ })
136
+
137
+ // Format and display AI analysis result
138
+ const aiOutput = formatter.formatAIAnalysis(aiResult, analysis)
139
+ cliOutput.print(aiOutput)
140
+ return
141
+ } catch (aiError) {
142
+ logger.warn('AI analysis failed', {
143
+ error: aiError instanceof Error ? aiError.message : String(aiError),
144
+ packageName,
145
+ targetVersion,
146
+ provider: options.provider,
147
+ })
148
+ cliOutput.warn(StyledText.iconWarning(t('command.analyze.aiFailed')))
149
+ if (options.verbose) {
150
+ cliOutput.warn(StyledText.muted(String(aiError)))
151
+ }
152
+ // Fall back to basic analysis
153
+ const formattedOutput = formatter.formatImpactAnalysis(analysis)
154
+ cliOutput.print(formattedOutput)
155
+ return
156
+ }
157
+ } else {
158
+ // Standard analysis without AI
159
+ const formattedOutput = formatter.formatImpactAnalysis(analysis)
160
+ cliOutput.print(formattedOutput)
161
+ }
162
+ } catch (error) {
163
+ // QUAL-002: Use unified error handling
164
+ handleCommandError(error, { verbose: options.verbose })
165
+ throw error
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Validate command arguments
171
+ */
172
+ static validateArgs(packageName: string): string[] {
173
+ const errors: string[] = []
174
+
175
+ if (!packageName || packageName.trim() === '') {
176
+ errors.push(t('validation.packageNameRequired'))
177
+ }
178
+
179
+ return errors
180
+ }
181
+
182
+ /**
183
+ * Validate command options
184
+ */
185
+ static validateOptions(options: AnalyzeCommandOptions): string[] {
186
+ return errorsOnly(validateAnalyzeOptions)(options)
187
+ }
188
+
189
+ /**
190
+ * Get command help text
191
+ */
192
+ static getHelpText(): string {
193
+ return `
194
+ Analyze the impact of updating a specific dependency
195
+
196
+ Usage:
197
+ pcu analyze <package> [version] [options]
198
+
199
+ Arguments:
200
+ package Package name to analyze
201
+ version New version (default: latest)
202
+
203
+ Options:
204
+ --catalog <name> Catalog name (auto-detected if not specified)
205
+ -f, --format <type> Output format: table, json, yaml, minimal (default: table)
206
+ --no-ai Disable AI-powered analysis
207
+ --provider <name> AI provider: auto, claude, gemini, codex (default: auto)
208
+ --analysis-type <t> AI analysis type: impact, security, compatibility, recommend
209
+ --skip-cache Skip AI analysis cache
210
+ --verbose Show detailed information
211
+
212
+ Examples:
213
+ pcu analyze lodash # Analyze update to latest version
214
+ pcu analyze lodash 4.18.0 # Analyze update to specific version
215
+ pcu analyze @types/node --no-ai # Disable AI analysis
216
+ pcu analyze react --format json # Output as JSON
217
+ `
218
+ }
219
+ }