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.
- 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
|
@@ -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
|
|
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
|
-
|
|
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: '
|
|
139
|
+
text: t('progress.securityAnalyzing'),
|
|
84
140
|
})
|
|
85
141
|
progressBar.start()
|
|
86
142
|
|
|
87
143
|
if (options.verbose) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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('
|
|
159
|
+
progressBar.succeed(t('progress.securityCompleted'))
|
|
98
160
|
|
|
99
161
|
// Format and display results
|
|
100
162
|
const formattedOutput = this.outputFormatter.formatSecurityReport(report)
|
|
101
|
-
|
|
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
|
-
|
|
177
|
+
throw CommandExitError.withCode(exitCode)
|
|
116
178
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
// Re-throw CommandExitError as-is
|
|
180
|
+
if (error instanceof CommandExitError) {
|
|
181
|
+
throw error
|
|
119
182
|
}
|
|
120
183
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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(
|
|
209
|
+
throw new Error(t('command.security.noPackageJson', { path: workspacePath }))
|
|
145
210
|
}
|
|
146
211
|
|
|
147
|
-
// Run
|
|
212
|
+
// Run pnpm audit
|
|
148
213
|
if (options.audit !== false) {
|
|
149
|
-
const
|
|
150
|
-
vulnerabilities.push(...
|
|
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: ['
|
|
240
|
+
scanTools: ['pnpm-audit', ...(options.snyk ? ['snyk'] : [])],
|
|
176
241
|
workspacePath: workspacePath,
|
|
177
242
|
},
|
|
178
243
|
}
|
|
179
244
|
}
|
|
180
245
|
|
|
181
246
|
/**
|
|
182
|
-
* Run
|
|
247
|
+
* Run pnpm audit scan
|
|
183
248
|
*/
|
|
184
|
-
private async
|
|
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('--
|
|
256
|
+
auditArgs.push('--prod')
|
|
192
257
|
}
|
|
193
258
|
|
|
194
|
-
const result = spawnSync('
|
|
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(
|
|
266
|
+
throw new Error(t('command.security.auditFailed', { message: result.error.message }))
|
|
202
267
|
}
|
|
203
268
|
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
273
|
+
return this.parseAuditResults(auditData)
|
|
216
274
|
} catch (parseError) {
|
|
217
|
-
throw new Error(
|
|
275
|
+
throw new Error(t('command.security.auditParseError', { error: String(parseError) }))
|
|
218
276
|
}
|
|
219
277
|
} else {
|
|
220
|
-
throw new Error(
|
|
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(
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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:
|
|
291
|
-
|
|
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:
|
|
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:
|
|
359
|
-
impact: '
|
|
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
|
-
|
|
509
|
+
cliOutput.print(`\n${StyledText.iconInfo(t('command.security.recommendations'))}`)
|
|
437
510
|
|
|
438
511
|
for (const rec of report.recommendations) {
|
|
439
|
-
|
|
512
|
+
cliOutput.print(
|
|
440
513
|
` ${StyledText.iconWarning()} ${rec.package}: ${rec.currentVersion} → ${rec.recommendedVersion}`
|
|
441
514
|
)
|
|
442
|
-
|
|
443
|
-
|
|
515
|
+
cliOutput.print(` ${StyledText.muted(rec.reason)}`)
|
|
516
|
+
cliOutput.print(` ${StyledText.muted(rec.impact)}`)
|
|
444
517
|
}
|
|
445
518
|
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
532
|
+
cliOutput.print(StyledText.iconSuccess(t('command.security.noFixesAvailable')))
|
|
459
533
|
return
|
|
460
534
|
}
|
|
461
535
|
|
|
462
|
-
|
|
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
|
-
|
|
542
|
+
cliOutput.print(StyledText.iconInfo(t('command.security.noAutoFixes')))
|
|
469
543
|
return
|
|
470
544
|
}
|
|
471
545
|
|
|
472
546
|
try {
|
|
473
|
-
// Run
|
|
474
|
-
const fixArgs = ['audit', 'fix']
|
|
547
|
+
// Run pnpm audit --fix
|
|
548
|
+
const fixArgs = ['audit', '--fix']
|
|
475
549
|
if (!options.includeDev) {
|
|
476
|
-
fixArgs.push('--
|
|
550
|
+
fixArgs.push('--prod')
|
|
477
551
|
}
|
|
478
552
|
|
|
479
|
-
const result = spawnSync('
|
|
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(
|
|
564
|
+
throw new Error(
|
|
565
|
+
t('command.security.auditFixFailed', { status: result.status ?? 'unknown' })
|
|
566
|
+
)
|
|
491
567
|
}
|
|
492
568
|
|
|
493
|
-
|
|
569
|
+
cliOutput.print(StyledText.iconSuccess(t('command.security.fixesApplied')))
|
|
494
570
|
|
|
495
571
|
// Re-run scan to verify fixes
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|