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
@@ -7,10 +7,14 @@
7
7
 
8
8
  import { spawnSync } from 'node:child_process'
9
9
  import * as path from 'node:path'
10
+ import { CommandExitError, getErrorCode, logger, t, toError } from '@pcu/utils'
10
11
  import * as fs from 'fs-extra'
11
12
  import type { OutputFormat, OutputFormatter } from '../formatters/outputFormatter.js'
12
13
  import { ProgressBar } from '../formatters/progressBar.js'
13
- import { StyledText, ThemeManager } from '../themes/colorTheme.js'
14
+ import { StyledText } from '../themes/colorTheme.js'
15
+ import { cliOutput } from '../utils/cliOutput.js'
16
+ import { handleCommandError, initializeTheme } from '../utils/commandHelpers.js'
17
+ import { errorsOnly, validateSecurityOptions } from '../validators/index.js'
14
18
 
15
19
  export interface SecurityCommandOptions {
16
20
  workspace?: string
@@ -54,6 +58,10 @@ export interface Vulnerability {
54
58
  paths: string[]
55
59
  cwe?: string[]
56
60
  cve?: string[]
61
+ /** Detailed description/overview of the vulnerability */
62
+ overview?: string
63
+ /** Currently installed version of the package */
64
+ installedVersion?: string
57
65
  }
58
66
 
59
67
  export interface SecurityRecommendation {
@@ -65,6 +73,54 @@ export interface SecurityRecommendation {
65
73
  impact: string
66
74
  }
67
75
 
76
+ /**
77
+ * pnpm/npm audit response vulnerability entry
78
+ */
79
+ interface AuditVulnerability {
80
+ name: string
81
+ severity: 'low' | 'moderate' | 'high' | 'critical'
82
+ title?: string
83
+ url?: string
84
+ range: string
85
+ fixAvailable: boolean | string
86
+ via?: Array<{ source?: string; name?: string }>
87
+ cwe?: string[]
88
+ cve?: string[]
89
+ }
90
+
91
+ /**
92
+ * pnpm/npm audit response structure
93
+ */
94
+ interface AuditData {
95
+ vulnerabilities?: Record<string, AuditVulnerability>
96
+ }
97
+
98
+ /**
99
+ * Snyk vulnerability entry
100
+ */
101
+ interface SnykVulnerability {
102
+ id: string
103
+ packageName: string
104
+ severity: 'low' | 'moderate' | 'high' | 'critical'
105
+ title: string
106
+ url?: string
107
+ version?: string
108
+ semver?: { vulnerable?: string[] }
109
+ upgradePath?: string[]
110
+ fixedIn?: string[]
111
+ from?: string[]
112
+ identifiers?: { CWE?: string[]; CVE?: string[] }
113
+ cwe?: string[]
114
+ cve?: string[]
115
+ }
116
+
117
+ /**
118
+ * Snyk response structure
119
+ */
120
+ interface SnykData {
121
+ vulnerabilities?: SnykVulnerability[]
122
+ }
123
+
68
124
  export class SecurityCommand {
69
125
  constructor(private readonly outputFormatter: OutputFormatter) {}
70
126
 
@@ -75,30 +131,36 @@ export class SecurityCommand {
75
131
  let progressBar: ProgressBar | undefined
76
132
 
77
133
  try {
78
- // Initialize theme
79
- ThemeManager.setTheme('default')
134
+ // Initialize theme using shared helper
135
+ initializeTheme('default')
80
136
 
81
137
  // Show loading with progress bar
82
138
  progressBar = new ProgressBar({
83
- text: 'Performing security analysis...',
139
+ text: t('progress.securityAnalyzing'),
84
140
  })
85
141
  progressBar.start()
86
142
 
87
143
  if (options.verbose) {
88
- console.log(StyledText.iconAnalysis('Security vulnerability scanning'))
89
- console.log(StyledText.muted(`Workspace: ${options.workspace || process.cwd()}`))
90
- console.log(StyledText.muted(`Severity filter: ${options.severity || 'all'}`))
91
- console.log('')
144
+ cliOutput.print(StyledText.iconAnalysis(t('command.security.scanning')))
145
+ cliOutput.print(
146
+ StyledText.muted(`${t('command.workspace.title')}: ${options.workspace || process.cwd()}`)
147
+ )
148
+ cliOutput.print(
149
+ StyledText.muted(
150
+ t('command.security.severityFilter', { severity: options.severity || 'all' })
151
+ )
152
+ )
153
+ cliOutput.print('')
92
154
  }
93
155
 
94
156
  // Execute security scan
95
157
  const report = await this.performSecurityScan(options)
96
158
 
97
- progressBar.succeed('Security analysis completed')
159
+ progressBar.succeed(t('progress.securityCompleted'))
98
160
 
99
161
  // Format and display results
100
162
  const formattedOutput = this.outputFormatter.formatSecurityReport(report)
101
- console.log(formattedOutput)
163
+ cliOutput.print(formattedOutput)
102
164
 
103
165
  // Show recommendations if available
104
166
  if (report.recommendations.length > 0) {
@@ -112,21 +174,24 @@ export class SecurityCommand {
112
174
 
113
175
  // Exit with appropriate code based on findings
114
176
  const exitCode = report.summary.critical > 0 ? 1 : 0
115
- process.exit(exitCode)
177
+ throw CommandExitError.withCode(exitCode)
116
178
  } catch (error) {
117
- if (progressBar) {
118
- progressBar.fail('Security analysis failed')
179
+ // Re-throw CommandExitError as-is
180
+ if (error instanceof CommandExitError) {
181
+ throw error
119
182
  }
120
183
 
121
- console.error(StyledText.iconError('Error performing security scan:'))
122
- console.error(StyledText.error(String(error)))
123
-
124
- if (options.verbose && error instanceof Error) {
125
- console.error(StyledText.muted('Stack trace:'))
126
- console.error(StyledText.muted(error.stack || 'No stack trace available'))
127
- }
184
+ // QUAL-007: Use unified error handling
185
+ handleCommandError(error, {
186
+ verbose: options.verbose,
187
+ progressBar,
188
+ errorMessage: 'Security scan failed',
189
+ context: { options },
190
+ failedProgressKey: 'progress.securityFailed',
191
+ errorDisplayKey: 'command.security.errorScanning',
192
+ })
128
193
 
129
- process.exit(1)
194
+ throw CommandExitError.failure('Security scan failed')
130
195
  }
131
196
  }
132
197
 
@@ -141,13 +206,13 @@ export class SecurityCommand {
141
206
  // Check if package.json exists
142
207
  const packageJsonPath = path.join(workspacePath, 'package.json')
143
208
  if (!(await fs.pathExists(packageJsonPath))) {
144
- throw new Error(`No package.json found in ${workspacePath}`)
209
+ throw new Error(t('command.security.noPackageJson', { path: workspacePath }))
145
210
  }
146
211
 
147
- // Run npm audit
212
+ // Run pnpm audit
148
213
  if (options.audit !== false) {
149
- const npmVulns = await this.runNpmAudit(workspacePath, options)
150
- vulnerabilities.push(...npmVulns)
214
+ const auditVulns = await this.runPnpmAudit(workspacePath, options)
215
+ vulnerabilities.push(...auditVulns)
151
216
  }
152
217
 
153
218
  // Run snyk scan if available
@@ -172,52 +237,50 @@ export class SecurityCommand {
172
237
  recommendations: recommendations,
173
238
  metadata: {
174
239
  scanDate: new Date().toISOString(),
175
- scanTools: ['npm-audit', ...(options.snyk ? ['snyk'] : [])],
240
+ scanTools: ['pnpm-audit', ...(options.snyk ? ['snyk'] : [])],
176
241
  workspacePath: workspacePath,
177
242
  },
178
243
  }
179
244
  }
180
245
 
181
246
  /**
182
- * Run npm audit scan
247
+ * Run pnpm audit scan
183
248
  */
184
- private async runNpmAudit(
249
+ private async runPnpmAudit(
185
250
  workspacePath: string,
186
251
  options: SecurityCommandOptions
187
252
  ): Promise<Vulnerability[]> {
188
253
  const auditArgs = ['audit', '--json']
189
254
 
190
255
  if (!options.includeDev) {
191
- auditArgs.push('--omit=dev')
256
+ auditArgs.push('--prod')
192
257
  }
193
258
 
194
- const result = spawnSync('npm', auditArgs, {
259
+ const result = spawnSync('pnpm', auditArgs, {
195
260
  cwd: workspacePath,
196
261
  encoding: 'utf8',
197
262
  stdio: ['pipe', 'pipe', 'pipe'],
198
263
  })
199
264
 
200
265
  if (result.error) {
201
- throw new Error(`npm audit failed: ${result.error.message}`)
266
+ throw new Error(t('command.security.auditFailed', { message: result.error.message }))
202
267
  }
203
268
 
204
- if (result.status === 1) {
205
- // npm audit returns 1 when vulnerabilities are found
206
- try {
207
- const auditData = JSON.parse(result.stdout)
208
- return this.parseNpmAuditResults(auditData)
209
- } catch (parseError) {
210
- throw new Error(`Failed to parse npm audit output: ${parseError}`)
211
- }
212
- } else if (result.status === 0) {
269
+ // pnpm audit returns non-zero when vulnerabilities are found
270
+ if (result.status === 0 || result.status === 1) {
213
271
  try {
214
272
  const auditData = JSON.parse(result.stdout)
215
- return this.parseNpmAuditResults(auditData)
273
+ return this.parseAuditResults(auditData)
216
274
  } catch (parseError) {
217
- throw new Error(`Failed to parse npm audit output: ${parseError}`)
275
+ throw new Error(t('command.security.auditParseError', { error: String(parseError) }))
218
276
  }
219
277
  } else {
220
- throw new Error(`npm audit failed with status ${result.status}: ${result.stderr}`)
278
+ throw new Error(
279
+ t('command.security.auditExitError', {
280
+ status: result.status ?? 'unknown',
281
+ error: result.stderr,
282
+ })
283
+ )
221
284
  }
222
285
  }
223
286
 
@@ -252,33 +315,41 @@ export class SecurityCommand {
252
315
  }
253
316
 
254
317
  if (result.status !== 0 && result.status !== 1) {
255
- throw new Error(`Snyk scan failed with status ${result.status}: ${result.stderr}`)
318
+ throw new Error(
319
+ t('command.security.snykScanExitError', {
320
+ status: result.status ?? 'unknown',
321
+ error: result.stderr,
322
+ })
323
+ )
256
324
  }
257
325
 
258
326
  const snykData = JSON.parse(result.stdout)
259
327
  return this.parseSnykResults(snykData)
260
- } catch (error: any) {
261
- if (error.code === 'ENOENT') {
262
- console.warn(StyledText.iconWarning('Snyk not found. Install with: npm install -g snyk'))
328
+ } catch (error) {
329
+ // ERR-003: Use type-safe error code extraction
330
+ const errorCode = getErrorCode(error)
331
+ if (errorCode === 'ENOENT') {
332
+ logger.debug('Snyk not found', { code: errorCode })
333
+ cliOutput.warn(StyledText.iconWarning(t('command.security.snykNotFound')))
263
334
  return []
264
335
  }
265
- throw new Error(`Snyk scan failed: ${error.message}`)
336
+ const err = toError(error)
337
+ logger.error('Snyk scan failed', err, { workspacePath })
338
+ throw new Error(t('command.security.snykScanFailed', { message: err.message }))
266
339
  }
267
340
  }
268
341
 
269
342
  /**
270
- * Parse npm audit results
343
+ * Parse pnpm/npm audit results
271
344
  */
272
- private parseNpmAuditResults(auditData: any): Vulnerability[] {
345
+ private parseAuditResults(auditData: AuditData): Vulnerability[] {
273
346
  const vulnerabilities: Vulnerability[] = []
274
347
 
275
348
  if (!auditData.vulnerabilities) {
276
349
  return vulnerabilities
277
350
  }
278
351
 
279
- for (const [id, vuln] of Object.entries(auditData.vulnerabilities)) {
280
- const vulnerability = vuln as any
281
-
352
+ for (const [id, vulnerability] of Object.entries(auditData.vulnerabilities)) {
282
353
  vulnerabilities.push({
283
354
  id: id,
284
355
  package: vulnerability.name,
@@ -287,8 +358,9 @@ export class SecurityCommand {
287
358
  url: vulnerability.url || `https://npmjs.com/advisories/${id}`,
288
359
  range: vulnerability.range,
289
360
  fixAvailable: vulnerability.fixAvailable,
290
- fixVersion: vulnerability.fixAvailable === true ? vulnerability.fixAvailable : undefined,
291
- paths: vulnerability.via?.map((v: any) => v.source || v.name) || [vulnerability.name],
361
+ fixVersion:
362
+ vulnerability.fixAvailable === true ? String(vulnerability.fixAvailable) : undefined,
363
+ paths: vulnerability.via?.map((v) => v.source || v.name || '') || [vulnerability.name],
292
364
  cwe: vulnerability.cwe,
293
365
  cve: vulnerability.cve,
294
366
  })
@@ -300,7 +372,7 @@ export class SecurityCommand {
300
372
  /**
301
373
  * Parse snyk results
302
374
  */
303
- private parseSnykResults(snykData: any): Vulnerability[] {
375
+ private parseSnykResults(snykData: SnykData): Vulnerability[] {
304
376
  const vulnerabilities: Vulnerability[] = []
305
377
 
306
378
  if (!snykData.vulnerabilities) {
@@ -313,9 +385,9 @@ export class SecurityCommand {
313
385
  package: vuln.packageName,
314
386
  severity: vuln.severity,
315
387
  title: vuln.title,
316
- url: vuln.url,
317
- range: vuln.semver?.vulnerable?.join(' || ') || vuln.version,
318
- fixAvailable: vuln.fixedIn?.length > 0,
388
+ url: vuln.url || '',
389
+ range: vuln.semver?.vulnerable?.join(' || ') || vuln.version || '',
390
+ fixAvailable: (vuln.fixedIn?.length ?? 0) > 0,
319
391
  fixVersion: vuln.fixedIn?.[0],
320
392
  paths: vuln.from || [vuln.packageName],
321
393
  cwe: vuln.identifiers?.CWE || [],
@@ -355,8 +427,8 @@ export class SecurityCommand {
355
427
  currentVersion: currentVersion,
356
428
  recommendedVersion: recommendedVersion,
357
429
  type: 'update',
358
- reason: `${criticalVulns.length} critical vulnerabilities found`,
359
- impact: 'High - Security vulnerability fix',
430
+ reason: t('command.security.criticalVulnsFound', { count: criticalVulns.length }),
431
+ impact: t('command.security.highImpactFix'),
360
432
  })
361
433
  }
362
434
  }
@@ -427,56 +499,58 @@ export class SecurityCommand {
427
499
 
428
500
  /**
429
501
  * Show security recommendations
502
+ * QUAL-011: Use unified output helpers (cliOutput, StyledText)
430
503
  */
431
504
  private showRecommendations(report: SecurityReport): void {
432
505
  if (report.recommendations.length === 0) {
433
506
  return
434
507
  }
435
508
 
436
- console.log(`\n${StyledText.iconInfo('Security Recommendations:')}`)
509
+ cliOutput.print(`\n${StyledText.iconInfo(t('command.security.recommendations'))}`)
437
510
 
438
511
  for (const rec of report.recommendations) {
439
- console.log(
512
+ cliOutput.print(
440
513
  ` ${StyledText.iconWarning()} ${rec.package}: ${rec.currentVersion} → ${rec.recommendedVersion}`
441
514
  )
442
- console.log(` ${StyledText.muted(rec.reason)}`)
443
- console.log(` ${StyledText.muted(rec.impact)}`)
515
+ cliOutput.print(` ${StyledText.muted(rec.reason)}`)
516
+ cliOutput.print(` ${StyledText.muted(rec.impact)}`)
444
517
  }
445
518
 
446
- console.log('')
447
- console.log(StyledText.iconUpdate('Run with --fix-vulns to apply automatic fixes'))
519
+ cliOutput.print('')
520
+ cliOutput.print(StyledText.iconUpdate(t('command.security.runWithFix')))
448
521
  }
449
522
 
450
523
  /**
451
524
  * Auto-fix vulnerabilities
525
+ * QUAL-011: Use unified output helpers (cliOutput, StyledText)
452
526
  */
453
527
  private async autoFixVulnerabilities(
454
528
  report: SecurityReport,
455
529
  options: SecurityCommandOptions
456
530
  ): Promise<void> {
457
531
  if (report.recommendations.length === 0) {
458
- console.log(StyledText.iconSuccess('No security fixes available'))
532
+ cliOutput.print(StyledText.iconSuccess(t('command.security.noFixesAvailable')))
459
533
  return
460
534
  }
461
535
 
462
- console.log(`\n${StyledText.iconUpdate('Applying security fixes...')}`)
536
+ cliOutput.print(`\n${StyledText.iconUpdate(t('command.security.applyingFixes'))}`)
463
537
 
464
538
  const workspacePath = options.workspace || process.cwd()
465
539
  const fixableVulns = report.recommendations.filter((r) => r.type === 'update')
466
540
 
467
541
  if (fixableVulns.length === 0) {
468
- console.log(StyledText.iconInfo('No automatic fixes available'))
542
+ cliOutput.print(StyledText.iconInfo(t('command.security.noAutoFixes')))
469
543
  return
470
544
  }
471
545
 
472
546
  try {
473
- // Run npm audit fix
474
- const fixArgs = ['audit', 'fix']
547
+ // Run pnpm audit --fix
548
+ const fixArgs = ['audit', '--fix']
475
549
  if (!options.includeDev) {
476
- fixArgs.push('--omit=dev')
550
+ fixArgs.push('--prod')
477
551
  }
478
552
 
479
- const result = spawnSync('npm', fixArgs, {
553
+ const result = spawnSync('pnpm', fixArgs, {
480
554
  cwd: workspacePath,
481
555
  encoding: 'utf8',
482
556
  stdio: 'inherit',
@@ -487,49 +561,40 @@ export class SecurityCommand {
487
561
  }
488
562
 
489
563
  if (result.status !== 0) {
490
- throw new Error(`npm audit fix failed with status ${result.status}`)
564
+ throw new Error(
565
+ t('command.security.auditFixFailed', { status: result.status ?? 'unknown' })
566
+ )
491
567
  }
492
568
 
493
- console.log(StyledText.iconSuccess('Security fixes applied successfully'))
569
+ cliOutput.print(StyledText.iconSuccess(t('command.security.fixesApplied')))
494
570
 
495
571
  // Re-run scan to verify fixes
496
- console.log(StyledText.iconInfo('Re-running security scan to verify fixes...'))
572
+ cliOutput.print(StyledText.iconInfo(t('command.security.verifyingFixes')))
497
573
  const newReport = await this.performSecurityScan({ ...options, fixVulns: false })
498
574
 
499
575
  if (newReport.summary.critical === 0 && newReport.summary.high === 0) {
500
- console.log(
501
- StyledText.iconSuccess('All critical and high severity vulnerabilities have been fixed!')
502
- )
576
+ cliOutput.print(StyledText.iconSuccess(t('command.security.allFixed')))
503
577
  } else {
504
- console.log(
578
+ cliOutput.print(
505
579
  StyledText.iconWarning(
506
580
  `${newReport.summary.critical} critical and ${newReport.summary.high} high severity vulnerabilities remain`
507
581
  )
508
582
  )
509
583
  }
510
- } catch (error: any) {
511
- console.error(StyledText.iconError('Failed to apply security fixes:'))
512
- console.error(StyledText.error(error.message))
584
+ } catch (error) {
585
+ const err = toError(error)
586
+ logger.error('Failed to apply security fixes', err, { workspacePath, options })
587
+ cliOutput.error(StyledText.iconError(t('command.security.fixesFailed')))
588
+ cliOutput.error(StyledText.error(err.message))
513
589
  }
514
590
  }
515
591
 
516
592
  /**
517
593
  * Validate command options
594
+ * QUAL-002: Uses unified validator from validators/
518
595
  */
519
596
  static validateOptions(options: SecurityCommandOptions): string[] {
520
- const errors: string[] = []
521
-
522
- // Validate format
523
- if (options.format && !['table', 'json', 'yaml', 'minimal'].includes(options.format)) {
524
- errors.push('Invalid format. Must be one of: table, json, yaml, minimal')
525
- }
526
-
527
- // Validate severity
528
- if (options.severity && !['low', 'moderate', 'high', 'critical'].includes(options.severity)) {
529
- errors.push('Invalid severity. Must be one of: low, moderate, high, critical')
530
- }
531
-
532
- return errors
597
+ return errorsOnly(validateSecurityOptions)(options)
533
598
  }
534
599
 
535
600
  /**
@@ -545,8 +610,8 @@ Usage:
545
610
  Options:
546
611
  --workspace <path> Workspace directory (default: current directory)
547
612
  --format <type> Output format: table, json, yaml, minimal (default: table)
548
- --audit Perform npm audit scan (default: true)
549
- --fix-vulns Automatically fix vulnerabilities
613
+ --audit Perform pnpm audit scan (default: true)
614
+ --fix-vulns Automatically fix vulnerabilities using pnpm audit --fix
550
615
  --severity <level> Filter by severity: low, moderate, high, critical
551
616
  --include-dev Include dev dependencies in scan
552
617
  --snyk Include Snyk scan (requires snyk CLI)
@@ -554,7 +619,7 @@ Options:
554
619
  --no-color Disable colored output
555
620
 
556
621
  Examples:
557
- pcu security # Basic security scan
622
+ pcu security # Basic security scan using pnpm audit
558
623
  pcu security --fix-vulns # Scan and fix vulnerabilities
559
624
  pcu security --severity high # Show only high severity issues
560
625
  pcu security --snyk # Include Snyk scan
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Theme Command
3
+ *
4
+ * CLI command to configure color themes.
5
+ */
6
+
7
+ import { logger, t } from '@pcu/utils'
8
+ import { InteractivePrompts } from '../interactive/interactivePrompts.js'
9
+ import { StyledText, ThemeManager } from '../themes/colorTheme.js'
10
+ import { cliOutput } from '../utils/cliOutput.js'
11
+
12
+ export interface ThemeCommandOptions {
13
+ set?: string
14
+ list?: boolean
15
+ interactive?: boolean
16
+ }
17
+
18
+ export class ThemeCommand {
19
+ /**
20
+ * Execute the theme command
21
+ */
22
+ async execute(options: ThemeCommandOptions = {}): Promise<void> {
23
+ if (options.list) {
24
+ const themes = ThemeManager.listThemes()
25
+ cliOutput.print(StyledText.iconInfo(t('command.theme.availableThemes')))
26
+ themes.forEach((theme) => {
27
+ cliOutput.print(` • ${theme}`)
28
+ })
29
+ return
30
+ }
31
+
32
+ if (options.set) {
33
+ const themes = ThemeManager.listThemes()
34
+ if (!themes.includes(options.set)) {
35
+ logger.error('Invalid theme specified', undefined, {
36
+ theme: options.set,
37
+ availableThemes: themes,
38
+ })
39
+ cliOutput.error(
40
+ StyledText.iconError(t('command.theme.invalidTheme', { theme: options.set }))
41
+ )
42
+ cliOutput.print(
43
+ StyledText.muted(`${t('command.theme.availableThemes')} ${themes.join(', ')}`)
44
+ )
45
+ throw new Error(t('command.theme.invalidTheme', { theme: options.set }))
46
+ }
47
+
48
+ ThemeManager.setTheme(options.set as keyof typeof ThemeManager.themes)
49
+ cliOutput.print(StyledText.iconSuccess(t('command.theme.setTo', { theme: options.set })))
50
+
51
+ // Show a preview
52
+ this.showThemePreview()
53
+ return
54
+ }
55
+
56
+ if (options.interactive) {
57
+ const interactivePrompts = new InteractivePrompts()
58
+ const selectedTheme = await interactivePrompts.selectTheme()
59
+
60
+ if (selectedTheme) {
61
+ ThemeManager.setTheme(selectedTheme as keyof typeof ThemeManager.themes)
62
+ cliOutput.print(StyledText.iconSuccess(t('command.theme.setTo', { theme: selectedTheme })))
63
+ this.showThemePreview()
64
+ } else {
65
+ cliOutput.print(StyledText.muted(t('command.theme.cancelled')))
66
+ }
67
+ return
68
+ }
69
+
70
+ // Default: show current theme and list
71
+ const currentTheme = ThemeManager.getTheme()
72
+ cliOutput.print(StyledText.iconInfo(t('command.theme.currentSettings')))
73
+ cliOutput.print(
74
+ ` ${t('command.theme.themeLabel')} ${currentTheme ? t('command.theme.custom') : t('command.theme.default')}`
75
+ )
76
+ cliOutput.print(`\n${t('command.theme.availableThemes')}`)
77
+ ThemeManager.listThemes().forEach((theme) => {
78
+ cliOutput.print(` • ${theme}`)
79
+ })
80
+ cliOutput.print(StyledText.muted(`\n${t('command.theme.useHint')}`))
81
+ }
82
+
83
+ /**
84
+ * Show a preview of the current theme with realistic examples
85
+ */
86
+ private showThemePreview(): void {
87
+ cliOutput.print(`\n${t('command.theme.preview')}`)
88
+ const theme = ThemeManager.getTheme()
89
+
90
+ // Package updates section
91
+ cliOutput.print(`\n ${theme.primary(`📦 ${t('command.theme.previewPackageUpdates')}`)}`)
92
+ cliOutput.print(
93
+ ` ${theme.text('react'.padEnd(14))} ${theme.muted('18.2.0')} → ${theme.major('19.0.0')} ${theme.muted(`(${t('command.theme.previewMajor')})`)}`
94
+ )
95
+ cliOutput.print(
96
+ ` ${theme.text('typescript'.padEnd(14))} ${theme.muted('5.3.0')} → ${theme.minor('5.4.0')} ${theme.muted(`(${t('command.theme.previewMinor')})`)}`
97
+ )
98
+ cliOutput.print(
99
+ ` ${theme.text('lodash'.padEnd(14))} ${theme.muted('4.17.1')} → ${theme.patch('4.17.2')} ${theme.muted(`(${t('command.theme.previewPatch')})`)}`
100
+ )
101
+ cliOutput.print(
102
+ ` ${theme.text('next'.padEnd(14))} ${theme.muted('14.2.0')} → ${theme.prerelease('15.0.0-rc.1')} ${theme.muted(`(${t('command.theme.previewPrerelease')})`)}`
103
+ )
104
+
105
+ // Status messages section
106
+ cliOutput.print(`\n ${theme.primary(`💬 ${t('command.theme.previewStatusMessages')}`)}`)
107
+ cliOutput.print(` ${theme.success(`✓ ${t('command.theme.previewUpdateComplete')}`)}`)
108
+ cliOutput.print(` ${theme.warning(`⚠ ${t('command.theme.previewPotentialIssue')}`)}`)
109
+ cliOutput.print(` ${theme.error(`✗ ${t('command.theme.previewOperationFailed')}`)}`)
110
+ cliOutput.print(
111
+ ` ${theme.info(`ℹ ${t('command.theme.previewUpdatesFound', { count: 3 })}`)}`
112
+ )
113
+
114
+ // Progress bar section
115
+ cliOutput.print(`\n ${theme.primary(`📊 ${t('command.theme.previewProgressBar')}`)}`)
116
+ const progressFilled = 24
117
+ const progressEmpty = 16
118
+ const progressBar =
119
+ theme.success('█'.repeat(progressFilled)) + theme.muted('░'.repeat(progressEmpty))
120
+ cliOutput.print(
121
+ ` [${progressBar}] ${theme.info('60%')} ${theme.muted(t('command.theme.previewCheckingDeps'))}`
122
+ )
123
+ cliOutput.print('')
124
+ }
125
+
126
+ /**
127
+ * Get command help text
128
+ */
129
+ static getHelpText(): string {
130
+ return `
131
+ Configure color theme
132
+
133
+ Usage:
134
+ pcu theme [options]
135
+
136
+ Options:
137
+ -s, --set <theme> Set theme: default, modern, minimal, neon
138
+ -l, --list List available themes
139
+ -i, --interactive Interactive theme selection
140
+
141
+ Examples:
142
+ pcu theme # Show current theme and available themes
143
+ pcu theme --list # List available themes
144
+ pcu theme --set modern # Set theme to modern
145
+ pcu theme --interactive # Interactive theme configuration
146
+ `
147
+ }
148
+ }