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,435 @@
1
+ /**
2
+ * Check Command Tests
3
+ */
4
+
5
+ import type { CatalogUpdateService, OutdatedReport } 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
+ loadConfig: vi.fn(),
37
+ CommandExitError: MockCommandExitError,
38
+ }
39
+ })
40
+
41
+ const formatterMocks = vi.hoisted(() => ({
42
+ formatOutdatedReport: vi.fn().mockReturnValue('Formatted output'),
43
+ }))
44
+
45
+ // Mock @pcu/utils - include CommandExitError for instanceof checks
46
+ vi.mock('@pcu/utils', () => ({
47
+ CommandExitError: mocks.CommandExitError,
48
+ logger: {
49
+ error: vi.fn(),
50
+ warn: vi.fn(),
51
+ info: vi.fn(),
52
+ debug: vi.fn(),
53
+ },
54
+ Logger: {
55
+ setGlobalLevel: vi.fn(),
56
+ },
57
+ ConfigLoader: {
58
+ loadConfig: mocks.loadConfig,
59
+ },
60
+ t: (key: string, params?: Record<string, unknown>) => {
61
+ if (params) {
62
+ return `${key} ${JSON.stringify(params)}`
63
+ }
64
+ return key
65
+ },
66
+ // Include async utilities that are used by the code
67
+ timeout: vi.fn().mockImplementation((promise: Promise<unknown>) => promise),
68
+ delay: vi.fn().mockResolvedValue(undefined),
69
+ retry: vi.fn().mockImplementation((fn: () => Promise<unknown>) => fn()),
70
+ // Include validation utilities
71
+ createValidationResult: (isValid = true, errors: string[] = [], warnings: string[] = []) => ({
72
+ isValid,
73
+ errors,
74
+ warnings,
75
+ }),
76
+ }))
77
+
78
+ // Mock OutputFormatter - needs to be a proper class constructor
79
+ vi.mock('../../formatters/outputFormatter.js', () => {
80
+ return {
81
+ OutputFormatter: class MockOutputFormatter {
82
+ formatOutdatedReport = formatterMocks.formatOutdatedReport
83
+ formatUpdatePlan = vi.fn()
84
+ formatUpdateResult = vi.fn()
85
+ formatSecurityReport = vi.fn()
86
+ formatImpactAnalysis = vi.fn()
87
+ },
88
+ }
89
+ })
90
+
91
+ // Mock ThemeManager and StyledText
92
+ vi.mock('../../themes/colorTheme.js', () => ({
93
+ ThemeManager: {
94
+ setTheme: vi.fn(),
95
+ getTheme: vi.fn().mockReturnValue({
96
+ major: (text: string) => text,
97
+ minor: (text: string) => text,
98
+ patch: (text: string) => text,
99
+ }),
100
+ },
101
+ StyledText: {
102
+ iconAnalysis: (text: string) => `[analysis]${text}`,
103
+ iconSuccess: (text: string) => `[success]${text}`,
104
+ iconInfo: (text: string) => `[info]${text}`,
105
+ iconError: (text: string) => `[error]${text}`,
106
+ iconSecurity: (text: string) => `[security]${text}`,
107
+ iconUpdate: (text: string) => `[update]${text}`,
108
+ iconWarning: (text: string) => `[warning]${text}`,
109
+ muted: (text: string) => `[muted]${text}`,
110
+ error: (text: string) => `[error]${text}`,
111
+ },
112
+ }))
113
+
114
+ // Create a chainable chalk mock that supports all color combinations
115
+ const createChalkMock = () => {
116
+ const createColorFn = (text: string) => text
117
+ const colorFn = Object.assign(createColorFn, {
118
+ bold: Object.assign((text: string) => text, {
119
+ cyan: (text: string) => text,
120
+ white: (text: string) => text,
121
+ }),
122
+ dim: Object.assign((text: string) => text, {
123
+ white: (text: string) => text,
124
+ }),
125
+ red: Object.assign((text: string) => text, {
126
+ bold: (text: string) => text,
127
+ }),
128
+ green: (text: string) => text,
129
+ yellow: (text: string) => text,
130
+ blue: (text: string) => text,
131
+ gray: (text: string) => text,
132
+ cyan: (text: string) => text,
133
+ white: (text: string) => text,
134
+ })
135
+ return colorFn
136
+ }
137
+
138
+ // Mock chalk with chainable functions
139
+ vi.mock('chalk', () => ({
140
+ default: createChalkMock(),
141
+ }))
142
+
143
+ // Import after mock setup
144
+ const { CheckCommand } = await import('../checkCommand.js')
145
+
146
+ describe('CheckCommand', () => {
147
+ let command: InstanceType<typeof CheckCommand>
148
+ let mockCatalogUpdateService: CatalogUpdateService
149
+ let consoleSpy: ReturnType<typeof vi.spyOn>
150
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
151
+ let processExitSpy: ReturnType<typeof vi.spyOn>
152
+
153
+ const mockOutdatedReport: OutdatedReport = {
154
+ hasUpdates: true,
155
+ totalOutdated: 2,
156
+ catalogs: [
157
+ {
158
+ catalogName: 'default',
159
+ totalPackages: 10,
160
+ outdatedCount: 2,
161
+ outdatedDependencies: [
162
+ {
163
+ packageName: 'lodash',
164
+ currentVersion: '4.17.20',
165
+ latestVersion: '4.17.21',
166
+ updateType: 'patch',
167
+ isSecurityUpdate: false,
168
+ changelog: null,
169
+ },
170
+ {
171
+ packageName: 'typescript',
172
+ currentVersion: '5.0.0',
173
+ latestVersion: '5.3.0',
174
+ updateType: 'minor',
175
+ isSecurityUpdate: false,
176
+ changelog: null,
177
+ },
178
+ ],
179
+ },
180
+ ],
181
+ checkedAt: new Date(),
182
+ options: {
183
+ target: 'latest',
184
+ includePrerelease: false,
185
+ },
186
+ }
187
+
188
+ beforeEach(() => {
189
+ vi.clearAllMocks()
190
+
191
+ // Set up default mock return values for ConfigLoader
192
+ mocks.loadConfig.mockReturnValue({
193
+ defaults: {
194
+ target: 'latest',
195
+ format: 'table',
196
+ },
197
+ include: [],
198
+ exclude: [],
199
+ })
200
+
201
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
202
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
203
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
204
+
205
+ // Create mock catalog update service
206
+ mockCatalogUpdateService = {
207
+ checkOutdatedDependencies: vi.fn().mockResolvedValue(mockOutdatedReport),
208
+ findCatalogForPackage: vi.fn(),
209
+ analyzeImpact: vi.fn(),
210
+ planUpdates: vi.fn(),
211
+ executeUpdates: vi.fn(),
212
+ } as unknown as CatalogUpdateService
213
+
214
+ command = new CheckCommand(mockCatalogUpdateService)
215
+ })
216
+
217
+ afterEach(() => {
218
+ consoleSpy.mockRestore()
219
+ consoleErrorSpy.mockRestore()
220
+ processExitSpy.mockRestore()
221
+ vi.resetAllMocks()
222
+ })
223
+
224
+ describe('execute', () => {
225
+ it('should check for outdated dependencies', async () => {
226
+ try {
227
+ await command.execute({})
228
+ } catch (error) {
229
+ expect((error as { exitCode: number }).exitCode).toBe(0)
230
+ }
231
+
232
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalled()
233
+ expect(consoleSpy).toHaveBeenCalled()
234
+ })
235
+
236
+ it('should use specified workspace path', async () => {
237
+ try {
238
+ await command.execute({ workspace: '/custom/workspace' })
239
+ } catch {
240
+ // Expected to throw CommandExitError
241
+ }
242
+
243
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
244
+ expect.objectContaining({
245
+ workspacePath: '/custom/workspace',
246
+ })
247
+ )
248
+ })
249
+
250
+ it('should filter by catalog name', async () => {
251
+ try {
252
+ await command.execute({ catalog: 'react17' })
253
+ } catch {
254
+ // Expected to throw CommandExitError
255
+ }
256
+
257
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ catalogName: 'react17',
260
+ })
261
+ )
262
+ })
263
+
264
+ it('should use specified target', async () => {
265
+ try {
266
+ await command.execute({ target: 'minor' })
267
+ } catch {
268
+ // Expected to throw CommandExitError
269
+ }
270
+
271
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
272
+ expect.objectContaining({
273
+ target: 'minor',
274
+ })
275
+ )
276
+ })
277
+
278
+ it('should include prerelease versions when specified', async () => {
279
+ try {
280
+ await command.execute({ prerelease: true })
281
+ } catch {
282
+ // Expected to throw CommandExitError
283
+ }
284
+
285
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
286
+ expect.objectContaining({
287
+ includePrerelease: true,
288
+ })
289
+ )
290
+ })
291
+
292
+ it('should apply include patterns', async () => {
293
+ try {
294
+ await command.execute({ include: ['lodash', 'react*'] })
295
+ } catch {
296
+ // Expected to throw CommandExitError
297
+ }
298
+
299
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
300
+ expect.objectContaining({
301
+ include: ['lodash', 'react*'],
302
+ })
303
+ )
304
+ })
305
+
306
+ it('should apply exclude patterns', async () => {
307
+ try {
308
+ await command.execute({ exclude: ['@types/*'] })
309
+ } catch {
310
+ // Expected to throw CommandExitError
311
+ }
312
+
313
+ expect(mockCatalogUpdateService.checkOutdatedDependencies).toHaveBeenCalledWith(
314
+ expect.objectContaining({
315
+ exclude: ['@types/*'],
316
+ })
317
+ )
318
+ })
319
+
320
+ it('should show verbose output when specified', async () => {
321
+ try {
322
+ await command.execute({ verbose: true })
323
+ } catch {
324
+ // Expected to throw CommandExitError
325
+ }
326
+
327
+ expect(consoleSpy).toHaveBeenCalled()
328
+ // Verbose mode shows additional information
329
+ // The mock t() returns the translation key, not the actual translated value
330
+ const calls = consoleSpy.mock.calls.flat().join(' ')
331
+ expect(calls).toContain('command.workspace.title')
332
+ })
333
+
334
+ it('should handle error and exit with code 1', async () => {
335
+ mockCatalogUpdateService.checkOutdatedDependencies = vi
336
+ .fn()
337
+ .mockRejectedValue(new Error('Check failed'))
338
+
339
+ try {
340
+ await command.execute({})
341
+ expect.fail('Should have thrown CommandExitError')
342
+ } catch (error) {
343
+ expect((error as { exitCode: number }).exitCode).toBe(1)
344
+ }
345
+ expect(consoleErrorSpy).toHaveBeenCalled()
346
+ })
347
+
348
+ it('should show summary when no updates found', async () => {
349
+ const noUpdatesReport: OutdatedReport = {
350
+ hasUpdates: false,
351
+ totalOutdated: 0,
352
+ catalogs: [
353
+ {
354
+ catalogName: 'default',
355
+ totalPackages: 10,
356
+ outdatedCount: 0,
357
+ outdatedDependencies: [],
358
+ },
359
+ ],
360
+ checkedAt: new Date(),
361
+ options: { target: 'latest', includePrerelease: false },
362
+ }
363
+
364
+ mockCatalogUpdateService.checkOutdatedDependencies = vi
365
+ .fn()
366
+ .mockResolvedValue(noUpdatesReport)
367
+
368
+ try {
369
+ await command.execute({})
370
+ } catch (error) {
371
+ expect((error as { exitCode: number }).exitCode).toBe(0)
372
+ }
373
+
374
+ expect(consoleSpy).toHaveBeenCalled()
375
+ })
376
+ })
377
+
378
+ describe('validateOptions', () => {
379
+ it('should return no errors for valid options', () => {
380
+ const errors = CheckCommand.validateOptions({
381
+ format: 'json',
382
+ target: 'minor',
383
+ })
384
+
385
+ expect(errors).toHaveLength(0)
386
+ })
387
+
388
+ it('should return error for invalid format', () => {
389
+ const errors = CheckCommand.validateOptions({
390
+ format: 'invalid' as never,
391
+ })
392
+
393
+ expect(errors).toContain('validation.invalidFormat')
394
+ })
395
+
396
+ it('should return error for invalid target', () => {
397
+ const errors = CheckCommand.validateOptions({
398
+ target: 'invalid' as never,
399
+ })
400
+
401
+ expect(errors).toContain('validation.invalidTarget')
402
+ })
403
+
404
+ it('should return error for empty include patterns', () => {
405
+ const errors = CheckCommand.validateOptions({
406
+ include: ['lodash', ' '],
407
+ })
408
+
409
+ expect(errors).toContain('validation.includePatternsEmpty')
410
+ })
411
+
412
+ it('should return error for empty exclude patterns', () => {
413
+ const errors = CheckCommand.validateOptions({
414
+ exclude: ['', '@types/*'],
415
+ })
416
+
417
+ expect(errors).toContain('validation.excludePatternsEmpty')
418
+ })
419
+ })
420
+
421
+ describe('getHelpText', () => {
422
+ it('should return help text with all options', () => {
423
+ const helpText = CheckCommand.getHelpText()
424
+
425
+ expect(helpText).toContain('Check for outdated catalog dependencies')
426
+ expect(helpText).toContain('--workspace')
427
+ expect(helpText).toContain('--catalog')
428
+ expect(helpText).toContain('--format')
429
+ expect(helpText).toContain('--target')
430
+ expect(helpText).toContain('--prerelease')
431
+ expect(helpText).toContain('--include')
432
+ expect(helpText).toContain('--exclude')
433
+ })
434
+ })
435
+ })