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.
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,467 @@
1
+ /**
2
+ * Security Command Tests
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
+ import type { OutputFormatter } from '../../formatters/outputFormatter.js'
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
+ // Create a chainable chalk mock that supports all color combinations
36
+ const createChalkMock = () => {
37
+ const createColorFn = (text: string) => text
38
+ const colorFn = Object.assign(createColorFn, {
39
+ bold: Object.assign((text: string) => text, {
40
+ cyan: (text: string) => text,
41
+ white: (text: string) => text,
42
+ }),
43
+ dim: Object.assign((text: string) => text, {
44
+ white: (text: string) => text,
45
+ }),
46
+ red: Object.assign((text: string) => text, {
47
+ bold: (text: string) => text,
48
+ }),
49
+ green: (text: string) => text,
50
+ yellow: (text: string) => text,
51
+ blue: (text: string) => text,
52
+ gray: (text: string) => text,
53
+ cyan: (text: string) => text,
54
+ white: (text: string) => text,
55
+ })
56
+ return colorFn
57
+ }
58
+
59
+ return {
60
+ spawnSync: vi.fn(),
61
+ exec: vi.fn(
62
+ (
63
+ _cmd: string,
64
+ callback: (error: Error | null, result: { stdout: string; stderr: string }) => void
65
+ ) => {
66
+ callback(null, { stdout: '', stderr: '' })
67
+ }
68
+ ),
69
+ pathExists: vi.fn(),
70
+ formatSecurityReport: vi.fn().mockReturnValue('Formatted security report'),
71
+ CommandExitError: MockCommandExitError,
72
+ createChalkMock,
73
+ }
74
+ })
75
+
76
+ // Mock @pcu/utils - include CommandExitError for instanceof checks
77
+ vi.mock('@pcu/utils', () => ({
78
+ CommandExitError: mocks.CommandExitError,
79
+ logger: {
80
+ error: vi.fn(),
81
+ warn: vi.fn(),
82
+ info: vi.fn(),
83
+ debug: vi.fn(),
84
+ },
85
+ t: (key: string) => key,
86
+ // Include async utilities that are used by the code
87
+ timeout: vi.fn().mockImplementation((promise: Promise<unknown>) => promise),
88
+ delay: vi.fn().mockResolvedValue(undefined),
89
+ retry: vi.fn().mockImplementation((fn: () => Promise<unknown>) => fn()),
90
+ // Include validation utilities
91
+ createValidationResult: (isValid = true, errors: string[] = [], warnings: string[] = []) => ({
92
+ isValid,
93
+ errors,
94
+ warnings,
95
+ }),
96
+ // ERR-003: Include error handling utilities
97
+ getErrorCode: (error: unknown) => {
98
+ if (error instanceof Error && 'code' in error) {
99
+ return (error as NodeJS.ErrnoException).code
100
+ }
101
+ return undefined
102
+ },
103
+ toError: (error: unknown) => (error instanceof Error ? error : new Error(String(error))),
104
+ }))
105
+
106
+ // Mock child_process
107
+ vi.mock('node:child_process', () => ({
108
+ spawnSync: mocks.spawnSync,
109
+ exec: mocks.exec,
110
+ }))
111
+
112
+ // Mock fs-extra
113
+ vi.mock('fs-extra', () => ({
114
+ default: {
115
+ pathExists: mocks.pathExists,
116
+ },
117
+ pathExists: mocks.pathExists,
118
+ }))
119
+
120
+ // Mock ProgressBar - needs to be a proper class constructor
121
+ vi.mock('../../formatters/progressBar.js', () => ({
122
+ ProgressBar: class MockProgressBar {
123
+ start = vi.fn()
124
+ succeed = vi.fn()
125
+ fail = vi.fn()
126
+ stop = vi.fn()
127
+ update = vi.fn()
128
+ },
129
+ }))
130
+
131
+ // Mock ThemeManager and StyledText - QUAL-011: Updated for unified output helpers
132
+ vi.mock('../../themes/colorTheme.js', () => ({
133
+ ThemeManager: {
134
+ setTheme: vi.fn(),
135
+ getTheme: vi.fn().mockReturnValue({
136
+ major: (text: string) => text,
137
+ minor: (text: string) => text,
138
+ patch: (text: string) => text,
139
+ }),
140
+ },
141
+ StyledText: {
142
+ iconAnalysis: (text?: string) => `[analysis]${text ?? ''}`,
143
+ iconSuccess: (text?: string) => `[success]${text ?? ''}`,
144
+ iconInfo: (text?: string) => `[info]${text ?? ''}`,
145
+ iconError: (text?: string) => `[error]${text ?? ''}`,
146
+ iconSecurity: (text?: string) => `[security]${text ?? ''}`,
147
+ iconUpdate: (text?: string) => `[update]${text ?? ''}`,
148
+ iconWarning: (text?: string) => `[warning]${text ?? ''}`,
149
+ muted: (text: string) => `[muted]${text}`,
150
+ error: (text: string) => `[error]${text}`,
151
+ },
152
+ }))
153
+
154
+ // Mock cliOutput - QUAL-011: Added for unified output helpers
155
+ vi.mock('../../utils/cliOutput.js', () => ({
156
+ cliOutput: {
157
+ print: vi.fn((...args: unknown[]) => console.log(...args)),
158
+ error: vi.fn((...args: unknown[]) => console.error(...args)),
159
+ warn: vi.fn((...args: unknown[]) => console.warn(...args)),
160
+ },
161
+ }))
162
+
163
+ // Mock chalk with chainable functions from hoisted mock
164
+ vi.mock('chalk', () => ({
165
+ default: mocks.createChalkMock(),
166
+ }))
167
+
168
+ // Import after mock setup
169
+ const { SecurityCommand } = await import('../securityCommand.js')
170
+
171
+ describe('SecurityCommand', () => {
172
+ let command: InstanceType<typeof SecurityCommand>
173
+ let mockOutputFormatter: OutputFormatter
174
+ let consoleSpy: ReturnType<typeof vi.spyOn>
175
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
176
+ let consoleWarnSpy: ReturnType<typeof vi.spyOn>
177
+ let processExitSpy: ReturnType<typeof vi.spyOn>
178
+
179
+ const mockNpmAuditResult = {
180
+ vulnerabilities: {
181
+ 'example-vuln-1': {
182
+ name: 'vulnerable-package',
183
+ severity: 'high',
184
+ title: 'Remote Code Execution',
185
+ url: 'https://npmjs.com/advisories/1234',
186
+ range: '>=1.0.0 <2.0.0',
187
+ fixAvailable: true,
188
+ via: [{ source: '1234', name: 'vulnerable-package' }],
189
+ cwe: ['CWE-94'],
190
+ cve: ['CVE-2023-1234'],
191
+ },
192
+ },
193
+ }
194
+
195
+ beforeEach(() => {
196
+ vi.clearAllMocks()
197
+
198
+ // Set up default mock return values
199
+ mocks.pathExists.mockResolvedValue(true)
200
+ mocks.spawnSync.mockReturnValue({
201
+ status: 1,
202
+ stdout: JSON.stringify(mockNpmAuditResult),
203
+ stderr: '',
204
+ error: undefined,
205
+ })
206
+
207
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
208
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
209
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
210
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
211
+
212
+ // Create mock output formatter using hoisted mock
213
+ mockOutputFormatter = {
214
+ formatSecurityReport: mocks.formatSecurityReport,
215
+ formatOutdatedReport: vi.fn(),
216
+ formatUpdateResult: vi.fn(),
217
+ formatUpdatePlan: vi.fn(),
218
+ formatImpactAnalysis: vi.fn(),
219
+ formatJSON: vi.fn(),
220
+ formatYAML: vi.fn(),
221
+ formatTable: vi.fn(),
222
+ formatMinimal: vi.fn(),
223
+ } as unknown as OutputFormatter
224
+
225
+ command = new SecurityCommand(mockOutputFormatter)
226
+ })
227
+
228
+ afterEach(() => {
229
+ consoleSpy.mockRestore()
230
+ consoleErrorSpy.mockRestore()
231
+ consoleWarnSpy.mockRestore()
232
+ processExitSpy.mockRestore()
233
+ vi.resetAllMocks()
234
+ })
235
+
236
+ describe('execute', () => {
237
+ it('should perform security scan', async () => {
238
+ try {
239
+ await command.execute({})
240
+ } catch {
241
+ // Expected to throw CommandExitError
242
+ }
243
+
244
+ expect(mocks.spawnSync).toHaveBeenCalled()
245
+ expect(mockOutputFormatter.formatSecurityReport).toHaveBeenCalled()
246
+ expect(consoleSpy).toHaveBeenCalled()
247
+ })
248
+
249
+ it('should use specified workspace path', async () => {
250
+ try {
251
+ await command.execute({ workspace: '/custom/workspace' })
252
+ } catch {
253
+ // Expected to throw CommandExitError
254
+ }
255
+
256
+ expect(mocks.spawnSync).toHaveBeenCalledWith(
257
+ 'pnpm',
258
+ expect.arrayContaining(['audit']),
259
+ expect.objectContaining({
260
+ cwd: '/custom/workspace',
261
+ })
262
+ )
263
+ })
264
+
265
+ it('should show verbose output when specified', async () => {
266
+ try {
267
+ await command.execute({ verbose: true })
268
+ } catch {
269
+ // Expected to throw CommandExitError
270
+ }
271
+
272
+ expect(consoleSpy).toHaveBeenCalled()
273
+ const calls = consoleSpy.mock.calls.flat().join(' ')
274
+ // The mock t() returns the translation key, not the actual translated value
275
+ expect(calls).toContain('command.workspace.title')
276
+ })
277
+
278
+ it('should exit with code 1 when critical vulnerabilities found', async () => {
279
+ mocks.spawnSync.mockReturnValue({
280
+ status: 1,
281
+ stdout: JSON.stringify({
282
+ vulnerabilities: {
283
+ 'critical-vuln': {
284
+ name: 'critical-package',
285
+ severity: 'critical',
286
+ title: 'Critical Issue',
287
+ url: '',
288
+ range: '*',
289
+ fixAvailable: true,
290
+ },
291
+ },
292
+ }),
293
+ stderr: '',
294
+ error: undefined,
295
+ })
296
+
297
+ try {
298
+ await command.execute({})
299
+ expect.fail('Should have thrown CommandExitError')
300
+ } catch (error) {
301
+ expect((error as { exitCode: number }).exitCode).toBe(1)
302
+ }
303
+ })
304
+
305
+ it('should exit with code 0 when no critical vulnerabilities found', async () => {
306
+ mocks.spawnSync.mockReturnValue({
307
+ status: 0,
308
+ stdout: JSON.stringify({ vulnerabilities: {} }),
309
+ stderr: '',
310
+ error: undefined,
311
+ })
312
+
313
+ try {
314
+ await command.execute({})
315
+ } catch (error) {
316
+ expect((error as { exitCode: number }).exitCode).toBe(0)
317
+ }
318
+ })
319
+
320
+ it('should handle error when package.json not found', async () => {
321
+ mocks.pathExists.mockResolvedValue(false)
322
+
323
+ try {
324
+ await command.execute({})
325
+ expect.fail('Should have thrown CommandExitError')
326
+ } catch (error) {
327
+ expect((error as { exitCode: number }).exitCode).toBe(1)
328
+ }
329
+ expect(consoleErrorSpy).toHaveBeenCalled()
330
+ })
331
+
332
+ it('should handle npm audit failure', async () => {
333
+ mocks.spawnSync.mockReturnValue({
334
+ status: 2,
335
+ stdout: '',
336
+ stderr: 'npm audit failed',
337
+ error: undefined,
338
+ })
339
+
340
+ try {
341
+ await command.execute({})
342
+ expect.fail('Should have thrown CommandExitError')
343
+ } catch (error) {
344
+ expect((error as { exitCode: number }).exitCode).toBe(1)
345
+ }
346
+ expect(consoleErrorSpy).toHaveBeenCalled()
347
+ })
348
+
349
+ it('should filter vulnerabilities by severity', async () => {
350
+ mocks.spawnSync.mockReturnValue({
351
+ status: 1,
352
+ stdout: JSON.stringify({
353
+ vulnerabilities: {
354
+ 'low-vuln': {
355
+ name: 'low-package',
356
+ severity: 'low',
357
+ title: 'Low Issue',
358
+ range: '*',
359
+ fixAvailable: false,
360
+ },
361
+ 'high-vuln': {
362
+ name: 'high-package',
363
+ severity: 'high',
364
+ title: 'High Issue',
365
+ range: '*',
366
+ fixAvailable: true,
367
+ },
368
+ },
369
+ }),
370
+ stderr: '',
371
+ error: undefined,
372
+ })
373
+
374
+ try {
375
+ await command.execute({ severity: 'high' })
376
+ } catch {
377
+ // Expected to throw CommandExitError
378
+ }
379
+
380
+ expect(mockOutputFormatter.formatSecurityReport).toHaveBeenCalledWith(
381
+ expect.objectContaining({
382
+ vulnerabilities: expect.arrayContaining([expect.objectContaining({ severity: 'high' })]),
383
+ })
384
+ )
385
+ })
386
+
387
+ it('should include dev dependencies when specified', async () => {
388
+ try {
389
+ await command.execute({ includeDev: true })
390
+ } catch {
391
+ // Expected to throw CommandExitError
392
+ }
393
+
394
+ expect(mocks.spawnSync).toHaveBeenCalledWith(
395
+ 'pnpm',
396
+ expect.not.arrayContaining(['--omit=dev']),
397
+ expect.any(Object)
398
+ )
399
+ })
400
+
401
+ it('should warn when snyk not found', async () => {
402
+ mocks.spawnSync.mockImplementation((cmd) => {
403
+ if (cmd === 'snyk') {
404
+ const error = new Error('ENOENT') as NodeJS.ErrnoException
405
+ error.code = 'ENOENT'
406
+ return { error, status: null, stdout: '', stderr: '' }
407
+ }
408
+ return {
409
+ status: 0,
410
+ stdout: JSON.stringify({ vulnerabilities: {} }),
411
+ stderr: '',
412
+ error: undefined,
413
+ }
414
+ })
415
+
416
+ try {
417
+ await command.execute({ snyk: true })
418
+ } catch {
419
+ // Expected to throw CommandExitError
420
+ }
421
+
422
+ expect(consoleWarnSpy).toHaveBeenCalled()
423
+ })
424
+ })
425
+
426
+ describe('validateOptions', () => {
427
+ it('should return no errors for valid options', () => {
428
+ const errors = SecurityCommand.validateOptions({
429
+ format: 'json',
430
+ severity: 'high',
431
+ })
432
+
433
+ expect(errors).toHaveLength(0)
434
+ })
435
+
436
+ it('should return error for invalid format', () => {
437
+ const errors = SecurityCommand.validateOptions({
438
+ format: 'invalid' as never,
439
+ })
440
+
441
+ expect(errors).toContain('validation.invalidFormat')
442
+ })
443
+
444
+ it('should return error for invalid severity', () => {
445
+ const errors = SecurityCommand.validateOptions({
446
+ severity: 'invalid' as never,
447
+ })
448
+
449
+ expect(errors).toContain('validation.invalidSeverity')
450
+ })
451
+ })
452
+
453
+ describe('getHelpText', () => {
454
+ it('should return help text with all options', () => {
455
+ const helpText = SecurityCommand.getHelpText()
456
+
457
+ expect(helpText).toContain('Security vulnerability scanning')
458
+ expect(helpText).toContain('--workspace')
459
+ expect(helpText).toContain('--format')
460
+ expect(helpText).toContain('--audit')
461
+ expect(helpText).toContain('--fix-vulns')
462
+ expect(helpText).toContain('--severity')
463
+ expect(helpText).toContain('--include-dev')
464
+ expect(helpText).toContain('--snyk')
465
+ })
466
+ })
467
+ })
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Theme Command Tests
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
+
7
+ // Mock @pcu/utils
8
+ vi.mock('@pcu/utils', () => ({
9
+ logger: {
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ debug: vi.fn(),
14
+ },
15
+ t: (key: string, params?: Record<string, unknown>) => {
16
+ if (params) {
17
+ let result = key
18
+ for (const [k, v] of Object.entries(params)) {
19
+ result = result.replace(`{{${k}}}`, String(v))
20
+ }
21
+ return result
22
+ }
23
+ return key
24
+ },
25
+ }))
26
+
27
+ // Use vi.hoisted to ensure mocks are available during vi.mock hoisting
28
+ const mocks = vi.hoisted(() => ({
29
+ listThemes: vi.fn(),
30
+ setTheme: vi.fn(),
31
+ getTheme: vi.fn(),
32
+ configurationWizard: vi.fn(),
33
+ selectTheme: vi.fn(),
34
+ }))
35
+
36
+ // Mock the theme module
37
+ vi.mock('../../themes/colorTheme.js', () => ({
38
+ ThemeManager: {
39
+ listThemes: mocks.listThemes,
40
+ setTheme: mocks.setTheme,
41
+ getTheme: mocks.getTheme,
42
+ themes: {
43
+ default: {},
44
+ modern: {},
45
+ minimal: {},
46
+ neon: {},
47
+ },
48
+ },
49
+ StyledText: {
50
+ iconInfo: (text: string) => `[info]${text}`,
51
+ iconSuccess: (text: string) => `[success]${text}`,
52
+ iconError: (text: string) => `[error]${text}`,
53
+ muted: (text: string) => `[muted]${text}`,
54
+ },
55
+ }))
56
+
57
+ // Mock interactive prompts - use class syntax for proper constructor
58
+ vi.mock('../../interactive/interactivePrompts.js', () => {
59
+ const InteractivePromptsMock = vi.fn(function (this: Record<string, unknown>) {
60
+ this.configurationWizard = mocks.configurationWizard
61
+ this.selectTheme = mocks.selectTheme
62
+ })
63
+
64
+ return {
65
+ InteractivePrompts: InteractivePromptsMock,
66
+ }
67
+ })
68
+
69
+ // Import after mock setup
70
+ const { ThemeCommand } = await import('../themeCommand.js')
71
+
72
+ describe('ThemeCommand', () => {
73
+ let command: InstanceType<typeof ThemeCommand>
74
+ let consoleSpy: ReturnType<typeof vi.spyOn>
75
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>
76
+
77
+ beforeEach(() => {
78
+ vi.clearAllMocks()
79
+
80
+ // Set up default mock return values
81
+ mocks.listThemes.mockReturnValue(['default', 'modern', 'minimal', 'neon'])
82
+ mocks.getTheme.mockReturnValue({
83
+ success: (text: string) => `[success]${text}`,
84
+ warning: (text: string) => `[warning]${text}`,
85
+ error: (text: string) => `[error]${text}`,
86
+ info: (text: string) => `[info]${text}`,
87
+ major: (text: string) => `[major]${text}`,
88
+ minor: (text: string) => `[minor]${text}`,
89
+ patch: (text: string) => `[patch]${text}`,
90
+ primary: (text: string) => `[primary]${text}`,
91
+ text: (text: string) => `[text]${text}`,
92
+ muted: (text: string) => `[muted]${text}`,
93
+ prerelease: (text: string) => `[prerelease]${text}`,
94
+ })
95
+ mocks.configurationWizard.mockResolvedValue({ theme: 'modern' })
96
+ mocks.selectTheme.mockResolvedValue('modern')
97
+
98
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
99
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
100
+
101
+ command = new ThemeCommand()
102
+ })
103
+
104
+ afterEach(() => {
105
+ consoleSpy.mockRestore()
106
+ consoleErrorSpy.mockRestore()
107
+ vi.resetAllMocks()
108
+ })
109
+
110
+ describe('execute', () => {
111
+ it('should list themes when --list flag is set', async () => {
112
+ await command.execute({ list: true })
113
+
114
+ expect(consoleSpy).toHaveBeenCalled()
115
+ const calls = consoleSpy.mock.calls.flat().join(' ')
116
+ expect(calls).toContain('default')
117
+ expect(calls).toContain('modern')
118
+ expect(calls).toContain('minimal')
119
+ expect(calls).toContain('neon')
120
+ })
121
+
122
+ it('should set theme when --set flag is used with valid theme', async () => {
123
+ await command.execute({ set: 'modern' })
124
+
125
+ expect(mocks.setTheme).toHaveBeenCalledWith('modern')
126
+ expect(consoleSpy).toHaveBeenCalled()
127
+ })
128
+
129
+ it('should throw error when --set flag is used with invalid theme', async () => {
130
+ await expect(command.execute({ set: 'invalid-theme' })).rejects.toThrow(
131
+ 'command.theme.invalidTheme'
132
+ )
133
+
134
+ expect(consoleErrorSpy).toHaveBeenCalled()
135
+ })
136
+
137
+ it('should run interactive mode when --interactive flag is set', async () => {
138
+ await command.execute({ interactive: true })
139
+
140
+ expect(mocks.selectTheme).toHaveBeenCalled()
141
+ expect(mocks.setTheme).toHaveBeenCalledWith('modern')
142
+ })
143
+
144
+ it('should show current theme and list by default', async () => {
145
+ await command.execute({})
146
+
147
+ expect(consoleSpy).toHaveBeenCalled()
148
+ expect(mocks.listThemes).toHaveBeenCalled()
149
+ })
150
+ })
151
+
152
+ describe('getHelpText', () => {
153
+ it('should return help text with all options', () => {
154
+ const helpText = ThemeCommand.getHelpText()
155
+
156
+ expect(helpText).toContain('Configure color theme')
157
+ expect(helpText).toContain('--set')
158
+ expect(helpText).toContain('--list')
159
+ expect(helpText).toContain('--interactive')
160
+ expect(helpText).toContain('default')
161
+ expect(helpText).toContain('modern')
162
+ expect(helpText).toContain('minimal')
163
+ expect(helpText).toContain('neon')
164
+ })
165
+ })
166
+ })