pnpm-catalog-updates 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +22031 -10684
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
  6. package/src/cli/commandRegistrar.ts +785 -0
  7. package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
  8. package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
  9. package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
  10. package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
  11. package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
  12. package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
  13. package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
  14. package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
  15. package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
  16. package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
  17. package/src/cli/commands/aiCommand.ts +163 -0
  18. package/src/cli/commands/analyzeCommand.ts +219 -0
  19. package/src/cli/commands/checkCommand.ts +91 -98
  20. package/src/cli/commands/graphCommand.ts +475 -0
  21. package/src/cli/commands/initCommand.ts +64 -54
  22. package/src/cli/commands/rollbackCommand.ts +334 -0
  23. package/src/cli/commands/securityCommand.ts +165 -100
  24. package/src/cli/commands/themeCommand.ts +148 -0
  25. package/src/cli/commands/updateCommand.ts +215 -263
  26. package/src/cli/commands/workspaceCommand.ts +73 -0
  27. package/src/cli/constants/cliChoices.ts +93 -0
  28. package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
  29. package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
  30. package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
  31. package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
  32. package/src/cli/formatters/ciFormatter.ts +964 -0
  33. package/src/cli/formatters/colorUtils.ts +145 -0
  34. package/src/cli/formatters/outputFormatter.ts +615 -332
  35. package/src/cli/formatters/progressBar.ts +43 -52
  36. package/src/cli/formatters/versionFormatter.ts +132 -0
  37. package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
  38. package/src/cli/handlers/changelogHandler.ts +113 -0
  39. package/src/cli/handlers/index.ts +9 -0
  40. package/src/cli/handlers/installHandler.ts +130 -0
  41. package/src/cli/index.ts +175 -726
  42. package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
  43. package/src/cli/interactive/interactivePrompts.ts +189 -83
  44. package/src/cli/interactive/optionUtils.ts +89 -0
  45. package/src/cli/themes/colorTheme.ts +43 -16
  46. package/src/cli/utils/cliOutput.ts +118 -0
  47. package/src/cli/utils/commandHelpers.ts +249 -0
  48. package/src/cli/validators/commandValidator.ts +321 -336
  49. package/src/cli/validators/index.ts +37 -2
  50. package/src/cli/options/globalOptions.ts +0 -437
  51. package/src/cli/options/index.ts +0 -5
@@ -1,390 +1,375 @@
1
1
  /**
2
2
  * CLI Command Validation
3
3
  *
4
- * Validates CLI command options and arguments before execution.
5
- * Provides detailed error messages and suggestions.
4
+ * QUAL-002: Unified validation logic for CLI commands.
5
+ * Provides composable validators that can be combined per command.
6
6
  */
7
7
 
8
- import { getConfig, type ValidationResult, validateCliOptions } from '@pcu/utils'
8
+ import { existsSync } from 'node:fs'
9
+ import { createValidationResult, t, type ValidationResult } from '@pcu/utils'
10
+ import {
11
+ FORMAT_CHOICES,
12
+ isValidAnalysisType,
13
+ isValidFormat,
14
+ isValidProvider,
15
+ isValidSeverity,
16
+ isValidTarget,
17
+ SEVERITY_CHOICES,
18
+ TARGET_CHOICES,
19
+ } from '../constants/cliChoices.js'
9
20
 
10
- export interface ValidatedOptions {
21
+ /**
22
+ * Validation rule function signature
23
+ */
24
+ type ValidationRule<T> = (options: T) => { errors: string[]; warnings: string[] }
25
+
26
+ /**
27
+ * Base options that many commands share
28
+ */
29
+ export interface BaseCommandOptions {
11
30
  workspace?: string
12
31
  catalog?: string
13
32
  format?: string
33
+ verbose?: boolean
34
+ color?: boolean
35
+ }
36
+
37
+ /**
38
+ * Options that include update target
39
+ */
40
+ export interface TargetOptions {
14
41
  target?: string
42
+ }
43
+
44
+ /**
45
+ * Options for interactive mode
46
+ */
47
+ export interface InteractiveOptions {
15
48
  interactive?: boolean
16
49
  dryRun?: boolean
17
- force?: boolean
18
- prerelease?: boolean
19
- include?: string[]
20
- exclude?: string[]
21
- createBackup?: boolean
22
- verbose?: boolean
23
- color?: boolean
24
- registry?: string
25
- timeout?: number
26
50
  }
27
51
 
28
- export class CommandValidator {
29
- private config = getConfig().getConfig()
52
+ /**
53
+ * Options for AI analysis
54
+ */
55
+ export interface AIOptions {
56
+ ai?: boolean
57
+ provider?: string
58
+ analysisType?: string
59
+ skipCache?: boolean
60
+ }
30
61
 
31
- /**
32
- * Validate check command options
33
- */
34
- validateCheckOptions(options: any): ValidationResult {
35
- const errors: string[] = []
36
- const warnings: string[] = []
62
+ /**
63
+ * Options for security command
64
+ */
65
+ export interface SecurityOptions {
66
+ severity?: string
67
+ }
37
68
 
38
- // Basic validation using utility
39
- const basicValidation = validateCliOptions(options)
40
- errors.push(...basicValidation.errors)
41
- warnings.push(...basicValidation.warnings)
69
+ /**
70
+ * Graph command specific options
71
+ */
72
+ export interface GraphOptions {
73
+ type?: string
74
+ }
42
75
 
43
- // Check-specific validations
44
- if (options.catalog && typeof options.catalog !== 'string') {
45
- errors.push('Catalog name must be a string')
46
- }
76
+ /**
77
+ * Create validation result from errors and warnings
78
+ */
79
+ function toValidationResult(errors: string[], warnings: string[]): ValidationResult {
80
+ return createValidationResult(errors.length === 0, errors, warnings)
81
+ }
47
82
 
48
- // Validate mutually exclusive options
49
- if (options.interactive && options.format === 'json') {
50
- warnings.push('Interactive mode is not useful with JSON output format')
51
- }
83
+ /**
84
+ * Compose multiple validation rules into a single validator
85
+ */
86
+ export function composeValidators<T>(
87
+ ...rules: ValidationRule<T>[]
88
+ ): (options: T) => ValidationResult {
89
+ return (options: T) => {
90
+ const allErrors: string[] = []
91
+ const allWarnings: string[] = []
52
92
 
53
- if (options.verbose && options.silent) {
54
- errors.push('Cannot use both --verbose and --silent options')
93
+ for (const rule of rules) {
94
+ const { errors, warnings } = rule(options)
95
+ allErrors.push(...errors)
96
+ allWarnings.push(...warnings)
55
97
  }
56
98
 
57
- return {
58
- isValid: errors.length === 0,
59
- errors,
60
- warnings,
61
- }
99
+ return toValidationResult(allErrors, allWarnings)
62
100
  }
101
+ }
63
102
 
64
- /**
65
- * Validate update command options
66
- */
67
- validateUpdateOptions(options: any): ValidationResult {
68
- const errors: string[] = []
69
- const warnings: string[] = []
70
-
71
- // Basic validation
72
- const basicValidation = validateCliOptions(options)
73
- errors.push(...basicValidation.errors)
74
- warnings.push(...basicValidation.warnings)
75
-
76
- // Update-specific validations
77
- if (options.interactive && options.dryRun) {
78
- errors.push('Cannot use --interactive with --dry-run')
79
- }
103
+ // ============================================================================
104
+ // Individual Validation Rules
105
+ // ============================================================================
80
106
 
81
- if (options.force && !options.dryRun && !options.createBackup) {
82
- warnings.push('Using --force without backup. Consider using --create-backup for safety')
83
- }
107
+ /**
108
+ * Validate output format
109
+ */
110
+ export function validateFormat<T extends { format?: string }>(
111
+ options: T
112
+ ): {
113
+ errors: string[]
114
+ warnings: string[]
115
+ } {
116
+ const errors: string[] = []
117
+ const warnings: string[] = []
118
+
119
+ if (options.format && !isValidFormat(options.format)) {
120
+ errors.push(t('validation.invalidFormat'))
121
+ }
84
122
 
85
- if (options.target === 'major' && !options.force && !options.interactive) {
86
- warnings.push(
87
- 'Major updates may contain breaking changes. Consider using --interactive or --force'
88
- )
89
- }
123
+ return { errors, warnings }
124
+ }
90
125
 
91
- // Validate include/exclude patterns
92
- if (options.include && options.exclude) {
93
- const overlapping = options.include.some((inc: string) =>
94
- options.exclude.some((exc: string) => inc === exc)
95
- )
96
- if (overlapping) {
97
- warnings.push('Some patterns appear in both include and exclude lists')
98
- }
99
- }
126
+ /**
127
+ * Validate update target
128
+ */
129
+ export function validateTarget<T extends TargetOptions>(
130
+ options: T
131
+ ): {
132
+ errors: string[]
133
+ warnings: string[]
134
+ } {
135
+ const errors: string[] = []
136
+ const warnings: string[] = []
137
+
138
+ if (options.target && !isValidTarget(options.target)) {
139
+ errors.push(t('validation.invalidTarget'))
140
+ }
100
141
 
101
- return {
102
- isValid: errors.length === 0,
103
- errors,
104
- warnings,
105
- }
142
+ return { errors, warnings }
143
+ }
144
+
145
+ /**
146
+ * Validate workspace path exists
147
+ */
148
+ export function validateWorkspacePath<T extends { workspace?: string }>(
149
+ options: T
150
+ ): {
151
+ errors: string[]
152
+ warnings: string[]
153
+ } {
154
+ const errors: string[] = []
155
+ const warnings: string[] = []
156
+
157
+ if (options.workspace && !existsSync(options.workspace)) {
158
+ errors.push(t('validation.workspaceDirNotExist', { path: options.workspace }))
106
159
  }
107
160
 
108
- /**
109
- * Validate analyze command arguments
110
- */
111
- validateAnalyzeArgs(catalog: string, packageName: string, version?: string): ValidationResult {
112
- const errors: string[] = []
113
- const warnings: string[] = []
114
-
115
- // Validate catalog name
116
- if (!catalog || catalog.trim() === '') {
117
- errors.push('Catalog name is required')
118
- } else if (catalog.includes('/') || catalog.includes('\\')) {
119
- errors.push('Catalog name cannot contain path separators')
120
- }
161
+ return { errors, warnings }
162
+ }
121
163
 
122
- // Validate package name
123
- if (!packageName || packageName.trim() === '') {
124
- errors.push('Package name is required')
125
- } else {
126
- // Basic package name validation
127
- const packageNameRegex = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
128
- if (!packageNameRegex.test(packageName)) {
129
- errors.push('Invalid package name format')
130
- }
131
- }
164
+ /**
165
+ * Validate interactive mode conflicts
166
+ */
167
+ export function validateInteractiveConflicts<T extends InteractiveOptions>(
168
+ options: T
169
+ ): {
170
+ errors: string[]
171
+ warnings: string[]
172
+ } {
173
+ const errors: string[] = []
174
+ const warnings: string[] = []
175
+
176
+ if (options.interactive && options.dryRun) {
177
+ errors.push(t('validation.interactiveWithDryRun'))
178
+ }
132
179
 
133
- // Validate version if provided
134
- if (version) {
135
- const semverRegex =
136
- /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
137
- if (!semverRegex.test(version)) {
138
- errors.push('Invalid version format. Use semantic versioning (e.g., 1.2.3)')
139
- }
140
- }
180
+ return { errors, warnings }
181
+ }
141
182
 
142
- return {
143
- isValid: errors.length === 0,
144
- errors,
145
- warnings,
146
- }
183
+ /**
184
+ * Validate AI provider options
185
+ */
186
+ export function validateAIOptions<T extends AIOptions>(
187
+ options: T
188
+ ): {
189
+ errors: string[]
190
+ warnings: string[]
191
+ } {
192
+ const errors: string[] = []
193
+ const warnings: string[] = []
194
+
195
+ if (options.provider && !isValidProvider(options.provider)) {
196
+ errors.push(t('validation.invalidProvider'))
147
197
  }
148
198
 
149
- /**
150
- * Validate workspace command options
151
- */
152
- validateWorkspaceOptions(options: any): ValidationResult {
153
- const errors: string[] = []
154
- const warnings: string[] = []
155
-
156
- // Basic validation
157
- const basicValidation = validateCliOptions(options)
158
- errors.push(...basicValidation.errors)
159
- warnings.push(...basicValidation.warnings)
160
-
161
- // Workspace-specific validations
162
- const actionCount = [options.validate, options.stats, options.info].filter(Boolean).length
163
- if (actionCount > 1) {
164
- errors.push('Cannot use multiple workspace actions simultaneously')
165
- }
199
+ if (options.analysisType && !isValidAnalysisType(options.analysisType)) {
200
+ errors.push(t('validation.invalidAnalysisType'))
201
+ }
166
202
 
167
- return {
168
- isValid: errors.length === 0,
169
- errors,
170
- warnings,
171
- }
203
+ return { errors, warnings }
204
+ }
205
+
206
+ /**
207
+ * Validate severity level
208
+ */
209
+ export function validateSeverity<T extends SecurityOptions>(
210
+ options: T
211
+ ): {
212
+ errors: string[]
213
+ warnings: string[]
214
+ } {
215
+ const errors: string[] = []
216
+ const warnings: string[] = []
217
+
218
+ if (options.severity && !isValidSeverity(options.severity)) {
219
+ errors.push(t('validation.invalidSeverity'))
172
220
  }
173
221
 
174
- /**
175
- * Validate global options
176
- */
177
- validateGlobalOptions(options: any): ValidationResult {
178
- const errors: string[] = []
179
- const warnings: string[] = []
180
-
181
- // Validate workspace path
182
- if (options.workspace) {
183
- // Future: Add path validation logic here
184
- // Currently skipped to avoid TypeScript errors
185
- }
222
+ return { errors, warnings }
223
+ }
186
224
 
187
- // Validate color options
188
- if (options.noColor && options.color) {
189
- errors.push('Cannot use both --color and --no-color')
190
- }
225
+ /**
226
+ * Validate graph type
227
+ */
228
+ export function validateGraphType<T extends GraphOptions>(
229
+ options: T
230
+ ): {
231
+ errors: string[]
232
+ warnings: string[]
233
+ } {
234
+ const errors: string[] = []
235
+ const warnings: string[] = []
236
+ const validTypes = ['catalog', 'package', 'full']
237
+
238
+ if (options.type && !validTypes.includes(options.type)) {
239
+ errors.push(t('validation.invalidGraphType', { validTypes: validTypes.join(', ') }))
240
+ }
191
241
 
192
- // Check for deprecated options (future-proofing)
193
- const deprecatedOptions = ['silent'] // Example
194
- for (const deprecated of deprecatedOptions) {
195
- if (options[deprecated]) {
196
- warnings.push(`Option --${deprecated} is deprecated and will be removed in future versions`)
197
- }
198
- }
242
+ return { errors, warnings }
243
+ }
199
244
 
200
- return {
201
- isValid: errors.length === 0,
202
- errors,
203
- warnings,
204
- }
245
+ /**
246
+ * Validate graph format (subset of formats)
247
+ */
248
+ export function validateGraphFormat<T extends { format?: string }>(
249
+ options: T
250
+ ): {
251
+ errors: string[]
252
+ warnings: string[]
253
+ } {
254
+ const errors: string[] = []
255
+ const warnings: string[] = []
256
+ const validFormats = ['text', 'mermaid', 'dot', 'json']
257
+
258
+ if (options.format && !validFormats.includes(options.format)) {
259
+ errors.push(t('validation.invalidGraphFormat', { validFormats: validFormats.join(', ') }))
205
260
  }
206
261
 
207
- /**
208
- * Validate configuration object
209
- */
210
- validateConfigFile(configPath: string): ValidationResult {
211
- const errors: string[] = []
212
- const warnings: string[] = []
213
-
214
- try {
215
- const fs = require('node:fs')
216
- // const path = require('path'); // Reserved for future use
217
-
218
- if (!fs.existsSync(configPath)) {
219
- errors.push(`Configuration file not found: ${configPath}`)
220
- return { isValid: false, errors, warnings }
221
- }
222
-
223
- let config: any
224
-
225
- if (configPath.endsWith('.js')) {
226
- // JavaScript config file
227
- try {
228
- delete require.cache[require.resolve(configPath)]
229
- config = require(configPath)
230
- } catch (error) {
231
- errors.push(`Failed to load JavaScript config: ${error}`)
232
- return { isValid: false, errors, warnings }
233
- }
234
- } else {
235
- // JSON config file
236
- try {
237
- const content = fs.readFileSync(configPath, 'utf-8')
238
- config = JSON.parse(content)
239
- } catch (error) {
240
- errors.push(`Failed to parse JSON config: ${error}`)
241
- return { isValid: false, errors, warnings }
242
- }
243
- }
244
-
245
- // Validate config structure
246
- if (typeof config !== 'object' || config === null) {
247
- errors.push('Configuration must be an object')
248
- return { isValid: false, errors, warnings }
249
- }
250
-
251
- // Validate known configuration sections
252
- if (config.registry && typeof config.registry !== 'object') {
253
- errors.push('registry configuration must be an object')
254
- }
255
-
256
- if (config.update && typeof config.update !== 'object') {
257
- errors.push('update configuration must be an object')
258
- }
259
-
260
- if (config.output && typeof config.output !== 'object') {
261
- errors.push('output configuration must be an object')
262
- }
263
-
264
- // Check for unknown top-level keys
265
- const knownKeys = ['registry', 'update', 'output', 'workspace', 'notification', 'logging']
266
- const unknownKeys = Object.keys(config).filter((key) => !knownKeys.includes(key))
267
-
268
- if (unknownKeys.length > 0) {
269
- warnings.push(`Unknown configuration keys: ${unknownKeys.join(', ')}`)
270
- }
271
- } catch (error) {
272
- errors.push(`Failed to validate configuration file: ${error}`)
273
- }
262
+ return { errors, warnings }
263
+ }
274
264
 
275
- return {
276
- isValid: errors.length === 0,
277
- errors,
278
- warnings,
279
- }
265
+ /**
266
+ * Options for pattern validation
267
+ */
268
+ export interface PatternOptions {
269
+ include?: string[]
270
+ exclude?: string[]
271
+ }
272
+
273
+ /**
274
+ * Validate include/exclude patterns
275
+ */
276
+ export function validatePatterns<T extends PatternOptions>(
277
+ options: T
278
+ ): {
279
+ errors: string[]
280
+ warnings: string[]
281
+ } {
282
+ const errors: string[] = []
283
+ const warnings: string[] = []
284
+
285
+ if (options.include?.some((pattern) => !pattern.trim())) {
286
+ errors.push(t('validation.includePatternsEmpty'))
280
287
  }
281
288
 
282
- /**
283
- * Sanitize and normalize options
284
- */
285
- sanitizeOptions(options: any): ValidatedOptions {
286
- const sanitized: ValidatedOptions = {}
289
+ if (options.exclude?.some((pattern) => !pattern.trim())) {
290
+ errors.push(t('validation.excludePatternsEmpty'))
291
+ }
287
292
 
288
- // String options
289
- if (options.workspace) {
290
- sanitized.workspace = String(options.workspace).trim()
291
- }
292
- if (options.catalog) {
293
- sanitized.catalog = String(options.catalog).trim()
294
- }
295
- if (options.format) {
296
- sanitized.format = String(options.format).toLowerCase().trim()
297
- }
298
- if (options.target) {
299
- sanitized.target = String(options.target).toLowerCase().trim()
300
- }
301
- if (options.registry) {
302
- sanitized.registry = String(options.registry).trim()
303
- }
293
+ return { errors, warnings }
294
+ }
304
295
 
305
- // Boolean options
306
- sanitized.interactive = Boolean(options.interactive)
307
- sanitized.dryRun = Boolean(options.dryRun)
308
- sanitized.force = Boolean(options.force)
309
- sanitized.prerelease = Boolean(options.prerelease)
310
- sanitized.createBackup = Boolean(options.createBackup)
311
- sanitized.verbose = Boolean(options.verbose)
312
-
313
- // Handle color option (tri-state: true, false, or undefined)
314
- if (options.color !== undefined) {
315
- sanitized.color = Boolean(options.color)
316
- } else if (options.noColor) {
317
- sanitized.color = false
318
- }
296
+ // ============================================================================
297
+ // Pre-composed Validators for Common Command Types
298
+ // ============================================================================
319
299
 
320
- // Number options
321
- if (options.timeout) {
322
- const timeout = parseInt(String(options.timeout), 10)
323
- if (!Number.isNaN(timeout) && timeout > 0) {
324
- sanitized.timeout = timeout
325
- }
326
- }
300
+ /**
301
+ * Validator for check command
302
+ */
303
+ export const validateCheckOptions = composeValidators<
304
+ BaseCommandOptions & TargetOptions & PatternOptions
305
+ >(validateFormat, validateTarget, validatePatterns)
327
306
 
328
- // Array options
329
- if (options.include) {
330
- sanitized.include = Array.isArray(options.include)
331
- ? options.include.map((p: any) => String(p).trim()).filter(Boolean)
332
- : [String(options.include).trim()].filter(Boolean)
333
- }
334
- if (options.exclude) {
335
- sanitized.exclude = Array.isArray(options.exclude)
336
- ? options.exclude.map((p: any) => String(p).trim()).filter(Boolean)
337
- : [String(options.exclude).trim()].filter(Boolean)
338
- }
307
+ /**
308
+ * Validator for update command
309
+ */
310
+ export const validateUpdateOptions = composeValidators<
311
+ BaseCommandOptions & TargetOptions & InteractiveOptions & AIOptions
312
+ >(validateFormat, validateTarget, validateInteractiveConflicts, validateAIOptions)
339
313
 
340
- return sanitized
341
- }
314
+ /**
315
+ * Validator for security command
316
+ */
317
+ export const validateSecurityOptions = composeValidators<BaseCommandOptions & SecurityOptions>(
318
+ validateFormat,
319
+ validateSeverity
320
+ )
342
321
 
343
- /**
344
- * Get validation suggestions based on common mistakes
345
- */
346
- getSuggestions(command: string, options: any): string[] {
347
- const suggestions: string[] = []
348
-
349
- switch (command) {
350
- case 'check':
351
- if (!options.workspace) {
352
- suggestions.push('Consider specifying --workspace for non-standard directory structures')
353
- }
354
- if (options.format === 'json' && options.verbose) {
355
- suggestions.push('JSON output already includes detailed info, --verbose may be redundant')
356
- }
357
- break
358
-
359
- case 'update':
360
- if (!options.dryRun && !options.createBackup && !options.force) {
361
- suggestions.push('Consider using --dry-run first to preview changes')
362
- }
363
- if (options.target === 'greatest' && !options.prerelease) {
364
- suggestions.push('Add --prerelease to include pre-release versions with greatest target')
365
- }
366
- break
367
-
368
- case 'analyze':
369
- if (!options.format) {
370
- suggestions.push('Use --format json for programmatic consumption of analysis data')
371
- }
372
- break
373
-
374
- case 'workspace':
375
- if (!options.validate && !options.stats) {
376
- suggestions.push(
377
- 'Use --validate to check workspace integrity or --stats for detailed information'
378
- )
379
- }
380
- break
381
- }
322
+ /**
323
+ * Validator for graph command
324
+ */
325
+ export const validateGraphOptions = composeValidators<BaseCommandOptions & GraphOptions>(
326
+ validateGraphFormat,
327
+ validateGraphType
328
+ )
382
329
 
383
- // General suggestions
384
- if (this.config.output.verbose && !options.verbose) {
385
- suggestions.push('Global verbose mode is enabled in config')
386
- }
330
+ /**
331
+ * Validator for analyze command
332
+ */
333
+ export const validateAnalyzeOptions = composeValidators<BaseCommandOptions & AIOptions>(
334
+ validateFormat,
335
+ validateAIOptions
336
+ )
387
337
 
388
- return suggestions
389
- }
338
+ /**
339
+ * Validator for init command
340
+ */
341
+ export const validateInitOptions = composeValidators<BaseCommandOptions>(validateWorkspacePath)
342
+
343
+ // ============================================================================
344
+ // Legacy Compatibility - Returns string[] for existing command signatures
345
+ // ============================================================================
346
+
347
+ /**
348
+ * Wrap validator to return only errors array (legacy compatibility)
349
+ */
350
+ export function errorsOnly<T>(
351
+ validator: (options: T) => ValidationResult
352
+ ): (options: T) => string[] {
353
+ return (options: T) => validator(options).errors
354
+ }
355
+
356
+ /**
357
+ * Format choices for help text
358
+ */
359
+ export function formatChoicesHelp(): string {
360
+ return FORMAT_CHOICES.join(', ')
361
+ }
362
+
363
+ /**
364
+ * Target choices for help text
365
+ */
366
+ export function targetChoicesHelp(): string {
367
+ return TARGET_CHOICES.join(', ')
368
+ }
369
+
370
+ /**
371
+ * Severity choices for help text
372
+ */
373
+ export function severityChoicesHelp(): string {
374
+ return SEVERITY_CHOICES.join(', ')
390
375
  }