pnpm-catalog-updates 1.0.3 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +22031 -10684
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
- package/src/cli/commandRegistrar.ts +785 -0
- package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
- package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
- package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
- package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
- package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
- package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
- package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
- package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
- package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
- package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
- package/src/cli/commands/aiCommand.ts +163 -0
- package/src/cli/commands/analyzeCommand.ts +219 -0
- package/src/cli/commands/checkCommand.ts +91 -98
- package/src/cli/commands/graphCommand.ts +475 -0
- package/src/cli/commands/initCommand.ts +64 -54
- package/src/cli/commands/rollbackCommand.ts +334 -0
- package/src/cli/commands/securityCommand.ts +165 -100
- package/src/cli/commands/themeCommand.ts +148 -0
- package/src/cli/commands/updateCommand.ts +215 -263
- package/src/cli/commands/workspaceCommand.ts +73 -0
- package/src/cli/constants/cliChoices.ts +93 -0
- package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
- package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
- package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
- package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
- package/src/cli/formatters/ciFormatter.ts +964 -0
- package/src/cli/formatters/colorUtils.ts +145 -0
- package/src/cli/formatters/outputFormatter.ts +615 -332
- package/src/cli/formatters/progressBar.ts +43 -52
- package/src/cli/formatters/versionFormatter.ts +132 -0
- package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
- package/src/cli/handlers/changelogHandler.ts +113 -0
- package/src/cli/handlers/index.ts +9 -0
- package/src/cli/handlers/installHandler.ts +130 -0
- package/src/cli/index.ts +175 -726
- package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
- package/src/cli/interactive/interactivePrompts.ts +189 -83
- package/src/cli/interactive/optionUtils.ts +89 -0
- package/src/cli/themes/colorTheme.ts +43 -16
- package/src/cli/utils/cliOutput.ts +118 -0
- package/src/cli/utils/commandHelpers.ts +249 -0
- package/src/cli/validators/commandValidator.ts +321 -336
- package/src/cli/validators/index.ts +37 -2
- package/src/cli/options/globalOptions.ts +0 -437
- 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
|
+
}
|