pnpm-catalog-updates 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +22031 -10684
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
  6. package/src/cli/commandRegistrar.ts +785 -0
  7. package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
  8. package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
  9. package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
  10. package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
  11. package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
  12. package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
  13. package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
  14. package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
  15. package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
  16. package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
  17. package/src/cli/commands/aiCommand.ts +163 -0
  18. package/src/cli/commands/analyzeCommand.ts +219 -0
  19. package/src/cli/commands/checkCommand.ts +91 -98
  20. package/src/cli/commands/graphCommand.ts +475 -0
  21. package/src/cli/commands/initCommand.ts +64 -54
  22. package/src/cli/commands/rollbackCommand.ts +334 -0
  23. package/src/cli/commands/securityCommand.ts +165 -100
  24. package/src/cli/commands/themeCommand.ts +148 -0
  25. package/src/cli/commands/updateCommand.ts +215 -263
  26. package/src/cli/commands/workspaceCommand.ts +73 -0
  27. package/src/cli/constants/cliChoices.ts +93 -0
  28. package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
  29. package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
  30. package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
  31. package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
  32. package/src/cli/formatters/ciFormatter.ts +964 -0
  33. package/src/cli/formatters/colorUtils.ts +145 -0
  34. package/src/cli/formatters/outputFormatter.ts +615 -332
  35. package/src/cli/formatters/progressBar.ts +43 -52
  36. package/src/cli/formatters/versionFormatter.ts +132 -0
  37. package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
  38. package/src/cli/handlers/changelogHandler.ts +113 -0
  39. package/src/cli/handlers/index.ts +9 -0
  40. package/src/cli/handlers/installHandler.ts +130 -0
  41. package/src/cli/index.ts +175 -726
  42. package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
  43. package/src/cli/interactive/interactivePrompts.ts +189 -83
  44. package/src/cli/interactive/optionUtils.ts +89 -0
  45. package/src/cli/themes/colorTheme.ts +43 -16
  46. package/src/cli/utils/cliOutput.ts +118 -0
  47. package/src/cli/utils/commandHelpers.ts +249 -0
  48. package/src/cli/validators/commandValidator.ts +321 -336
  49. package/src/cli/validators/index.ts +37 -2
  50. package/src/cli/options/globalOptions.ts +0 -437
  51. package/src/cli/options/index.ts +0 -5
@@ -0,0 +1,161 @@
1
+ /**
2
+ * AI Command Tests
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
+
7
+ // Use vi.hoisted to ensure mocks are available during vi.mock hoisting
8
+ const mocks = vi.hoisted(() => ({
9
+ analyzeUpdates: vi.fn(),
10
+ getDetectionSummary: vi.fn(),
11
+ detectAvailableProviders: vi.fn(),
12
+ getBestProvider: vi.fn(),
13
+ cacheClear: vi.fn(),
14
+ cacheGetStats: vi.fn(),
15
+ }))
16
+
17
+ // Mock @pcu/utils
18
+ vi.mock('@pcu/utils', () => ({
19
+ t: (key: string) => key,
20
+ }))
21
+
22
+ // Mock chalk
23
+ vi.mock('chalk', () => ({
24
+ default: {
25
+ gray: (text: string) => text,
26
+ red: (text: string) => text,
27
+ blue: (text: string) => text,
28
+ yellow: (text: string) => text,
29
+ green: (text: string) => text,
30
+ cyan: (text: string) => text,
31
+ },
32
+ }))
33
+
34
+ // Mock @pcu/core - use class syntax for proper constructor mocking
35
+ vi.mock('@pcu/core', () => {
36
+ // Create a proper class mock for AIDetector
37
+ const AIDetectorMock = vi.fn(function (this: Record<string, unknown>) {
38
+ this.getDetectionSummary = mocks.getDetectionSummary
39
+ this.detectAvailableProviders = mocks.detectAvailableProviders
40
+ this.getBestProvider = mocks.getBestProvider
41
+ })
42
+
43
+ // Create a proper class mock for AIAnalysisService
44
+ const AIAnalysisServiceMock = vi.fn(function (this: Record<string, unknown>) {
45
+ this.analyzeUpdates = mocks.analyzeUpdates
46
+ })
47
+
48
+ return {
49
+ AIAnalysisService: AIAnalysisServiceMock,
50
+ AIDetector: AIDetectorMock,
51
+ analysisCache: {
52
+ clear: mocks.cacheClear,
53
+ getStats: mocks.cacheGetStats,
54
+ },
55
+ }
56
+ })
57
+
58
+ // Import after mock setup
59
+ const { AiCommand } = await import('../aiCommand.js')
60
+
61
+ describe('AiCommand', () => {
62
+ let command: InstanceType<typeof AiCommand>
63
+ let consoleSpy: ReturnType<typeof vi.spyOn>
64
+
65
+ beforeEach(() => {
66
+ vi.clearAllMocks()
67
+
68
+ // Set up default mock return values
69
+ mocks.analyzeUpdates.mockResolvedValue({
70
+ provider: 'rule-engine',
71
+ confidence: 0.85,
72
+ summary: 'Test analysis completed successfully',
73
+ })
74
+ mocks.getDetectionSummary.mockResolvedValue('AI Provider Summary:\n - Claude: Available')
75
+ mocks.detectAvailableProviders.mockResolvedValue([
76
+ { name: 'Claude', available: true, priority: 1, path: '/usr/bin/claude' },
77
+ { name: 'Gemini', available: false, priority: 2 },
78
+ { name: 'Codex', available: false, priority: 3 },
79
+ ])
80
+ mocks.getBestProvider.mockResolvedValue({ name: 'Claude', available: true })
81
+ mocks.cacheGetStats.mockReturnValue({
82
+ totalEntries: 10,
83
+ hits: 8,
84
+ misses: 2,
85
+ hitRate: 0.8,
86
+ })
87
+
88
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
89
+
90
+ command = new AiCommand()
91
+ })
92
+
93
+ afterEach(() => {
94
+ consoleSpy.mockRestore()
95
+ vi.resetAllMocks()
96
+ })
97
+
98
+ describe('execute', () => {
99
+ it('should show status by default', async () => {
100
+ await command.execute({})
101
+
102
+ expect(consoleSpy).toHaveBeenCalled()
103
+ expect(mocks.getDetectionSummary).toHaveBeenCalled()
104
+ expect(mocks.detectAvailableProviders).toHaveBeenCalled()
105
+ })
106
+
107
+ it('should clear cache when --clear-cache flag is set', async () => {
108
+ await command.execute({ clearCache: true })
109
+
110
+ expect(mocks.cacheClear).toHaveBeenCalled()
111
+ const calls = consoleSpy.mock.calls.flat().join(' ')
112
+ expect(calls).toContain('command.ai.cacheCleared')
113
+ })
114
+
115
+ it('should show cache stats when --cache-stats flag is set', async () => {
116
+ await command.execute({ cacheStats: true })
117
+
118
+ expect(mocks.cacheGetStats).toHaveBeenCalled()
119
+ const calls = consoleSpy.mock.calls.flat().join(' ')
120
+ expect(calls).toContain('command.ai.cacheStats')
121
+ expect(calls).toContain('command.ai.totalEntries')
122
+ expect(calls).toContain('command.ai.cacheHits')
123
+ expect(calls).toContain('command.ai.cacheMisses')
124
+ })
125
+
126
+ it('should run test when --test flag is set', async () => {
127
+ await command.execute({ test: true })
128
+
129
+ const calls = consoleSpy.mock.calls.flat().join(' ')
130
+ expect(calls).toContain('command.ai.testingAnalysis')
131
+ })
132
+
133
+ it('should show provider details in status', async () => {
134
+ await command.execute({ status: true })
135
+
136
+ expect(mocks.detectAvailableProviders).toHaveBeenCalled()
137
+ const calls = consoleSpy.mock.calls.flat().join(' ')
138
+ expect(calls).toContain('Claude')
139
+ })
140
+
141
+ it('should show best provider when available', async () => {
142
+ await command.execute({})
143
+
144
+ expect(mocks.getBestProvider).toHaveBeenCalled()
145
+ const calls = consoleSpy.mock.calls.flat().join(' ')
146
+ expect(calls).toContain('command.ai.bestProvider')
147
+ })
148
+ })
149
+
150
+ describe('getHelpText', () => {
151
+ it('should return help text with all options', () => {
152
+ const helpText = AiCommand.getHelpText()
153
+
154
+ expect(helpText).toContain('Check AI provider status')
155
+ expect(helpText).toContain('--status')
156
+ expect(helpText).toContain('--test')
157
+ expect(helpText).toContain('--cache-stats')
158
+ expect(helpText).toContain('--clear-cache')
159
+ })
160
+ })
161
+ })
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Analyze Command Tests
3
+ */
4
+
5
+ import type { CatalogUpdateService, 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
+ analyzeUpdates: vi.fn(),
11
+ getLatestVersion: vi.fn(),
12
+ loadConfig: vi.fn(),
13
+ // Create a chainable chalk mock that supports all color combinations
14
+ createChalkMock: () => {
15
+ const createColorFn = (text: string) => text
16
+ const colorFn = Object.assign(createColorFn, {
17
+ bold: Object.assign((text: string) => text, {
18
+ cyan: (text: string) => text,
19
+ white: (text: string) => text,
20
+ }),
21
+ dim: Object.assign((text: string) => text, {
22
+ white: (text: string) => text,
23
+ }),
24
+ red: Object.assign((text: string) => text, {
25
+ bold: (text: string) => text,
26
+ }),
27
+ green: (text: string) => text,
28
+ yellow: (text: string) => text,
29
+ blue: (text: string) => text,
30
+ gray: (text: string) => text,
31
+ cyan: (text: string) => text,
32
+ white: (text: string) => text,
33
+ })
34
+ return colorFn
35
+ },
36
+ }))
37
+
38
+ // Mock @pcu/utils
39
+ vi.mock('@pcu/utils', () => ({
40
+ logger: {
41
+ error: vi.fn(),
42
+ warn: vi.fn(),
43
+ info: vi.fn(),
44
+ debug: vi.fn(),
45
+ },
46
+ Logger: {
47
+ setGlobalLevel: vi.fn(),
48
+ },
49
+ ConfigLoader: {
50
+ loadConfig: mocks.loadConfig,
51
+ },
52
+ t: (key: string, params?: Record<string, unknown>) => {
53
+ if (params) {
54
+ let result = key
55
+ for (const [k, v] of Object.entries(params)) {
56
+ result = result.replace(`{{${k}}}`, String(v))
57
+ }
58
+ return result
59
+ }
60
+ return key
61
+ },
62
+ // Include async utilities
63
+ timeout: vi.fn().mockImplementation((promise: Promise<unknown>) => promise),
64
+ delay: vi.fn().mockResolvedValue(undefined),
65
+ retry: vi.fn().mockImplementation((fn: () => Promise<unknown>) => fn()),
66
+ }))
67
+
68
+ // Mock chalk with chainable functions
69
+ vi.mock('chalk', () => ({
70
+ default: mocks.createChalkMock(),
71
+ }))
72
+
73
+ // Mock @pcu/core - use class syntax for proper constructor mocking
74
+ vi.mock('@pcu/core', async (importOriginal) => {
75
+ const actual = await importOriginal()
76
+
77
+ // Create a proper class mock for AIAnalysisService
78
+ const AIAnalysisServiceMock = vi.fn(function (this: Record<string, unknown>) {
79
+ this.analyzeUpdates = mocks.analyzeUpdates
80
+ })
81
+
82
+ // Create a proper class mock for NpmRegistryService
83
+ const NpmRegistryServiceMock = vi.fn(function (this: Record<string, unknown>) {
84
+ this.getLatestVersion = mocks.getLatestVersion
85
+ })
86
+
87
+ return {
88
+ ...(actual as object),
89
+ AIAnalysisService: AIAnalysisServiceMock,
90
+ NpmRegistryService: NpmRegistryServiceMock,
91
+ }
92
+ })
93
+
94
+ // Import after mock setup
95
+ const { AnalyzeCommand } = await import('../analyzeCommand.js')
96
+
97
+ describe('AnalyzeCommand', () => {
98
+ let command: InstanceType<typeof AnalyzeCommand>
99
+ let mockCatalogUpdateService: CatalogUpdateService
100
+ let mockWorkspaceService: WorkspaceService
101
+ let consoleSpy: ReturnType<typeof vi.spyOn>
102
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
103
+ let consoleWarnSpy: ReturnType<typeof vi.spyOn>
104
+
105
+ beforeEach(() => {
106
+ vi.clearAllMocks()
107
+
108
+ // Set up default mock return values for hoisted mocks
109
+ mocks.analyzeUpdates.mockResolvedValue({
110
+ provider: 'rule-engine',
111
+ confidence: 0.85,
112
+ summary: 'Test analysis summary',
113
+ recommendations: ['Recommendation 1'],
114
+ risks: [],
115
+ })
116
+ mocks.getLatestVersion.mockResolvedValue({ toString: () => '4.17.21' })
117
+ mocks.loadConfig.mockResolvedValue({
118
+ defaults: { format: 'table' },
119
+ })
120
+
121
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
122
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
123
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
124
+
125
+ // Create mock catalog update service
126
+ mockCatalogUpdateService = {
127
+ findCatalogForPackage: vi.fn().mockResolvedValue('default'),
128
+ analyzeImpact: vi.fn().mockResolvedValue({
129
+ catalogName: 'default',
130
+ packageName: 'lodash',
131
+ currentVersion: '4.17.20',
132
+ proposedVersion: '4.17.21',
133
+ updateType: 'patch',
134
+ affectedPackages: ['package-a', 'package-b'],
135
+ breakingChanges: [],
136
+ recommendations: ['Safe to update'],
137
+ riskLevel: 'low',
138
+ securityImpact: {
139
+ resolvedVulnerabilities: [],
140
+ newVulnerabilities: [],
141
+ },
142
+ }),
143
+ checkOutdatedDependencies: vi.fn(),
144
+ planUpdates: vi.fn(),
145
+ executeUpdates: vi.fn(),
146
+ } as unknown as CatalogUpdateService
147
+
148
+ // Create mock workspace service
149
+ mockWorkspaceService = {
150
+ getWorkspaceInfo: vi.fn().mockResolvedValue({
151
+ path: '/test/workspace',
152
+ name: 'test-workspace',
153
+ isValid: true,
154
+ hasPackages: true,
155
+ hasCatalogs: true,
156
+ packageCount: 5,
157
+ catalogCount: 2,
158
+ catalogNames: ['default', 'react17'],
159
+ }),
160
+ discoverWorkspace: vi.fn(),
161
+ validateWorkspace: vi.fn(),
162
+ getWorkspaceStats: vi.fn(),
163
+ getCatalogs: vi.fn(),
164
+ getPackages: vi.fn(),
165
+ usesCatalogs: vi.fn(),
166
+ getPackagesUsingCatalog: vi.fn(),
167
+ findWorkspaces: vi.fn(),
168
+ checkHealth: vi.fn(),
169
+ } as unknown as WorkspaceService
170
+
171
+ command = new AnalyzeCommand(mockCatalogUpdateService, mockWorkspaceService)
172
+ })
173
+
174
+ afterEach(() => {
175
+ consoleSpy.mockRestore()
176
+ consoleErrorSpy.mockRestore()
177
+ consoleWarnSpy.mockRestore()
178
+ vi.resetAllMocks()
179
+ })
180
+
181
+ describe('execute', () => {
182
+ it('should auto-detect catalog when not specified', async () => {
183
+ await command.execute('lodash', undefined, {})
184
+
185
+ expect(mockCatalogUpdateService.findCatalogForPackage).toHaveBeenCalledWith(
186
+ 'lodash',
187
+ undefined
188
+ )
189
+ })
190
+
191
+ it('should use specified catalog', async () => {
192
+ await command.execute('lodash', undefined, { catalog: 'default' })
193
+
194
+ expect(mockCatalogUpdateService.findCatalogForPackage).not.toHaveBeenCalled()
195
+ expect(mockCatalogUpdateService.analyzeImpact).toHaveBeenCalledWith(
196
+ 'default',
197
+ 'lodash',
198
+ expect.any(String),
199
+ undefined
200
+ )
201
+ })
202
+
203
+ it('should throw error when package not found in any catalog', async () => {
204
+ mockCatalogUpdateService.findCatalogForPackage = vi.fn().mockResolvedValue(null)
205
+
206
+ await expect(command.execute('unknown-package', undefined, {})).rejects.toThrow(
207
+ 'command.analyze.notFoundInCatalog'
208
+ )
209
+ })
210
+
211
+ it('should use specified version when provided', async () => {
212
+ await command.execute('lodash', '4.18.0', { catalog: 'default' })
213
+
214
+ expect(mockCatalogUpdateService.analyzeImpact).toHaveBeenCalledWith(
215
+ 'default',
216
+ 'lodash',
217
+ '4.18.0',
218
+ undefined
219
+ )
220
+ })
221
+
222
+ it('should fetch latest version when not specified', async () => {
223
+ await command.execute('lodash', undefined, { catalog: 'default' })
224
+
225
+ // The version should be fetched from NpmRegistryService
226
+ expect(mockCatalogUpdateService.analyzeImpact).toHaveBeenCalled()
227
+ })
228
+
229
+ it('should run AI analysis by default', async () => {
230
+ await command.execute('lodash', '4.17.21', { catalog: 'default' })
231
+
232
+ expect(consoleSpy).toHaveBeenCalled()
233
+ })
234
+
235
+ it('should skip AI analysis when --no-ai is set', async () => {
236
+ await command.execute('lodash', '4.17.21', { catalog: 'default', ai: false })
237
+
238
+ expect(mockCatalogUpdateService.analyzeImpact).toHaveBeenCalled()
239
+ })
240
+
241
+ it('should use specified workspace path', async () => {
242
+ await command.execute('lodash', '4.17.21', {
243
+ catalog: 'default',
244
+ workspace: '/custom/workspace',
245
+ })
246
+
247
+ expect(mockWorkspaceService.getWorkspaceInfo).toHaveBeenCalledWith('/custom/workspace')
248
+ })
249
+ })
250
+
251
+ describe('validateArgs', () => {
252
+ it('should return error for empty package name', () => {
253
+ const errors = AnalyzeCommand.validateArgs('')
254
+
255
+ expect(errors).toContain('validation.packageNameRequired')
256
+ })
257
+
258
+ it('should return error for whitespace-only package name', () => {
259
+ const errors = AnalyzeCommand.validateArgs(' ')
260
+
261
+ expect(errors).toContain('validation.packageNameRequired')
262
+ })
263
+
264
+ it('should return no errors for valid package name', () => {
265
+ const errors = AnalyzeCommand.validateArgs('lodash')
266
+
267
+ expect(errors).toHaveLength(0)
268
+ })
269
+ })
270
+
271
+ describe('getHelpText', () => {
272
+ it('should return help text with all options', () => {
273
+ const helpText = AnalyzeCommand.getHelpText()
274
+
275
+ expect(helpText).toContain('Analyze the impact')
276
+ expect(helpText).toContain('--catalog')
277
+ expect(helpText).toContain('--format')
278
+ expect(helpText).toContain('--no-ai')
279
+ expect(helpText).toContain('--provider')
280
+ expect(helpText).toContain('--analysis-type')
281
+ })
282
+ })
283
+ })