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
@@ -4,10 +4,12 @@
4
4
  * Provides smart prompts and auto-completion for CLI commands
5
5
  */
6
6
 
7
- import { FileSystemService } from '@pcu/core'
7
+ import { FileSystemService, FileWorkspaceRepository, WorkspaceService } from '@pcu/core'
8
+ import { logger, t } from '@pcu/utils'
8
9
  import chalk from 'chalk'
9
10
  import inquirer from 'inquirer'
10
11
  import { StyledText } from '../themes/colorTheme.js'
12
+ import { cliOutput } from '../utils/cliOutput.js'
11
13
 
12
14
  export interface AutoCompleteOption {
13
15
  name: string
@@ -15,6 +17,37 @@ export interface AutoCompleteOption {
15
17
  description?: string
16
18
  }
17
19
 
20
+ /**
21
+ * Package choice for selection prompts
22
+ */
23
+ interface PackageChoice {
24
+ name: string
25
+ current: string
26
+ latest: string
27
+ type: string
28
+ }
29
+
30
+ /**
31
+ * Configuration wizard result
32
+ */
33
+ interface ConfigurationWizardResult {
34
+ theme: string
35
+ interactive: boolean
36
+ backup: boolean
37
+ updateStrategy: string
38
+ timeout: number
39
+ }
40
+
41
+ /**
42
+ * Update impact preview
43
+ */
44
+ interface UpdateImpact {
45
+ totalUpdates: number
46
+ riskLevel: string
47
+ affectedCount: number
48
+ securityUpdates: number
49
+ }
50
+
18
51
  export class InteractivePrompts {
19
52
  private fsService: FileSystemService
20
53
 
@@ -25,9 +58,7 @@ export class InteractivePrompts {
25
58
  /**
26
59
  * Interactive package selection with search
27
60
  */
28
- async selectPackages(
29
- packages: Array<{ name: string; current: string; latest: string; type: string }>
30
- ): Promise<string[]> {
61
+ async selectPackages(packages: PackageChoice[]): Promise<string[]> {
31
62
  if (packages.length === 0) {
32
63
  return []
33
64
  }
@@ -41,12 +72,12 @@ export class InteractivePrompts {
41
72
  const answers = await inquirer.prompt({
42
73
  type: 'checkbox',
43
74
  name: 'selectedPackages',
44
- message: StyledText.iconPackage('Select packages to update:'),
75
+ message: StyledText.iconPackage(t('prompt.selectPackages')),
45
76
  choices,
46
77
  pageSize: 15,
47
78
  validate: (input: unknown) => {
48
79
  const selected = input as string[]
49
- return selected.length > 0 || 'Please select at least one package'
80
+ return selected.length > 0 || t('prompt.selectAtLeastOne')
50
81
  },
51
82
  })
52
83
 
@@ -66,7 +97,7 @@ export class InteractivePrompts {
66
97
  }
67
98
 
68
99
  const choices = [
69
- { name: 'All catalogs', value: 'all' },
100
+ { name: t('prompt.allCatalogs'), value: 'all' },
70
101
  ...catalogs.map((name) => ({ name, value: name })),
71
102
  ]
72
103
 
@@ -74,7 +105,7 @@ export class InteractivePrompts {
74
105
  {
75
106
  type: 'list',
76
107
  name: 'catalog',
77
- message: StyledText.iconCatalog('Select catalog to update:'),
108
+ message: StyledText.iconCatalog(t('prompt.selectCatalog')),
78
109
  choices,
79
110
  pageSize: 10,
80
111
  },
@@ -88,18 +119,18 @@ export class InteractivePrompts {
88
119
  */
89
120
  async selectUpdateStrategy(): Promise<string> {
90
121
  const strategies = [
91
- { name: 'Latest (recommended)', value: 'latest' },
92
- { name: 'Greatest (highest version)', value: 'greatest' },
93
- { name: 'Minor (non-breaking)', value: 'minor' },
94
- { name: 'Patch (bug fixes only)', value: 'patch' },
95
- { name: 'Newest (latest release)', value: 'newest' },
122
+ { name: t('prompt.strategyLatest'), value: 'latest' },
123
+ { name: t('prompt.strategyGreatest'), value: 'greatest' },
124
+ { name: t('prompt.strategyMinor'), value: 'minor' },
125
+ { name: t('prompt.strategyPatch'), value: 'patch' },
126
+ { name: t('prompt.strategyNewest'), value: 'newest' },
96
127
  ]
97
128
 
98
129
  const answers = await inquirer.prompt([
99
130
  {
100
131
  type: 'list',
101
132
  name: 'strategy',
102
- message: StyledText.iconUpdate('Select update strategy:'),
133
+ message: StyledText.iconUpdate(t('prompt.selectUpdateStrategy')),
103
134
  choices: strategies,
104
135
  },
105
136
  ])
@@ -111,16 +142,16 @@ export class InteractivePrompts {
111
142
  * Confirm dangerous operations
112
143
  */
113
144
  async confirmDangerousOperation(operation: string, details?: string): Promise<boolean> {
114
- console.log('')
145
+ cliOutput.print('')
115
146
  if (details) {
116
- console.log(chalk.yellow('⚠️ Warning:'), details)
147
+ cliOutput.print(chalk.yellow(`⚠️ ${t('prompt.warning')}`), details)
117
148
  }
118
149
 
119
150
  const answers = await inquirer.prompt([
120
151
  {
121
152
  type: 'confirm',
122
153
  name: 'confirmed',
123
- message: StyledText.warning(`Are you sure you want to ${operation}?`),
154
+ message: StyledText.warning(t('prompt.confirmOperation', { operation })),
124
155
  default: false,
125
156
  },
126
157
  ])
@@ -133,7 +164,7 @@ export class InteractivePrompts {
133
164
  */
134
165
  async autoCompletePackage(
135
166
  packages: string[],
136
- message: string = 'Select package:'
167
+ message: string = t('prompt.selectPackage')
137
168
  ): Promise<string> {
138
169
  const answers = await inquirer.prompt([
139
170
  {
@@ -154,15 +185,15 @@ export class InteractivePrompts {
154
185
  async selectWorkspacePath(): Promise<string> {
155
186
  const currentDir = process.cwd()
156
187
  const choices = [
157
- { name: `Current directory (${currentDir})`, value: currentDir },
158
- { name: 'Browse for directory...', value: 'browse' },
188
+ { name: t('prompt.currentDirectory', { path: currentDir }), value: currentDir },
189
+ { name: t('prompt.browseDirectory'), value: 'browse' },
159
190
  ]
160
191
 
161
192
  const answers = await inquirer.prompt([
162
193
  {
163
194
  type: 'list',
164
195
  name: 'path',
165
- message: StyledText.icon('📁', 'Select workspace directory:'),
196
+ message: t('prompt.selectWorkspace'),
166
197
  choices,
167
198
  },
168
199
  ])
@@ -180,10 +211,10 @@ export class InteractivePrompts {
180
211
  private async browseDirectory(currentPath = process.cwd()): Promise<string> {
181
212
  const directoryNames = await this.fsService.listDirectories(currentPath)
182
213
  const choices = [
183
- { name: '.. (parent directory)', value: '..' },
184
- { name: `. (current: ${currentPath})`, value: '.' },
214
+ { name: t('prompt.parentDirectory'), value: '..' },
215
+ { name: t('prompt.currentDirectory', { path: currentPath }), value: '.' },
185
216
  ...directoryNames.map((name: string) => ({
186
- name: `📁 ${name}`,
217
+ name: `[dir] ${name}`,
187
218
  value: `${currentPath}/${name}`,
188
219
  })),
189
220
  ]
@@ -192,7 +223,7 @@ export class InteractivePrompts {
192
223
  {
193
224
  type: 'list',
194
225
  name: 'selected',
195
- message: `Browse: ${currentPath}`,
226
+ message: t('prompt.browsePath', { path: currentPath }),
196
227
  choices,
197
228
  pageSize: 15,
198
229
  },
@@ -222,7 +253,7 @@ export class InteractivePrompts {
222
253
  {
223
254
  type: 'confirm',
224
255
  name: 'useThis',
225
- message: `Use ${answers.selected} as workspace?`,
256
+ message: t('prompt.useAsWorkspace', { path: answers.selected }),
226
257
  default: true,
227
258
  },
228
259
  ])
@@ -235,21 +266,42 @@ export class InteractivePrompts {
235
266
  return this.browseDirectory(answers.selected)
236
267
  }
237
268
 
269
+ /**
270
+ * Simple theme selection with cancel option
271
+ */
272
+ async selectTheme(): Promise<string | null> {
273
+ const answers = await inquirer.prompt({
274
+ type: 'list',
275
+ name: 'theme',
276
+ message: t('prompt.selectTheme'),
277
+ choices: [
278
+ { name: t('prompt.themeDefault'), value: 'default' },
279
+ { name: t('prompt.themeModern'), value: 'modern' },
280
+ { name: t('prompt.themeMinimal'), value: 'minimal' },
281
+ { name: t('prompt.themeNeon'), value: 'neon' },
282
+ { name: t('prompt.cancel'), value: null },
283
+ ],
284
+ default: 'default',
285
+ })
286
+
287
+ return answers.theme
288
+ }
289
+
238
290
  /**
239
291
  * Multi-step configuration wizard
240
292
  */
241
- async configurationWizard(): Promise<any> {
242
- console.log(chalk.bold.blue('\n🧙‍♂️ Configuration Wizard\n'))
293
+ async configurationWizard(): Promise<ConfigurationWizardResult> {
294
+ cliOutput.print(chalk.bold.blue(`\n${t('prompt.configWizard')}\n`))
243
295
 
244
296
  const themeAnswer = await inquirer.prompt({
245
297
  type: 'list',
246
298
  name: 'theme',
247
- message: 'Select color theme:',
299
+ message: t('prompt.selectTheme'),
248
300
  choices: [
249
- { name: 'Default - Balanced colors', value: 'default' },
250
- { name: 'Modern - Vibrant colors', value: 'modern' },
251
- { name: 'Minimal - Clean and simple', value: 'minimal' },
252
- { name: 'Neon - High contrast', value: 'neon' },
301
+ { name: t('prompt.themeDefault'), value: 'default' },
302
+ { name: t('prompt.themeModern'), value: 'modern' },
303
+ { name: t('prompt.themeMinimal'), value: 'minimal' },
304
+ { name: t('prompt.themeNeon'), value: 'neon' },
253
305
  ],
254
306
  default: 'default',
255
307
  })
@@ -257,25 +309,25 @@ export class InteractivePrompts {
257
309
  const interactiveAnswer = await inquirer.prompt({
258
310
  type: 'confirm',
259
311
  name: 'interactive',
260
- message: 'Enable interactive mode by default?',
312
+ message: t('prompt.enableInteractive'),
261
313
  default: true,
262
314
  })
263
315
 
264
316
  const backupAnswer = await inquirer.prompt({
265
317
  type: 'confirm',
266
318
  name: 'backup',
267
- message: 'Create backups before updates?',
319
+ message: t('prompt.createBackups'),
268
320
  default: true,
269
321
  })
270
322
 
271
323
  const strategyAnswer = await inquirer.prompt({
272
324
  type: 'list',
273
325
  name: 'updateStrategy',
274
- message: 'Default update strategy:',
326
+ message: t('prompt.defaultStrategy'),
275
327
  choices: [
276
- { name: 'Latest stable versions', value: 'latest' },
277
- { name: 'Minor updates (non-breaking)', value: 'minor' },
278
- { name: 'Patch updates (bug fixes)', value: 'patch' },
328
+ { name: t('prompt.strategyLatestStable'), value: 'latest' },
329
+ { name: t('prompt.strategyMinorUpdates'), value: 'minor' },
330
+ { name: t('prompt.strategyPatchUpdates'), value: 'patch' },
279
331
  ],
280
332
  default: 'latest',
281
333
  })
@@ -283,20 +335,20 @@ export class InteractivePrompts {
283
335
  const timeoutAnswer = await inquirer.prompt({
284
336
  type: 'number',
285
337
  name: 'timeout',
286
- message: 'Network timeout (seconds):',
338
+ message: t('prompt.networkTimeout'),
287
339
  default: 30,
288
340
  validate: (input: number | undefined) => {
289
- if (input === undefined) return 'Timeout is required'
290
- return input > 0 || 'Timeout must be positive'
341
+ if (input === undefined) return t('prompt.timeoutRequired')
342
+ return input > 0 || t('prompt.timeoutPositive')
291
343
  },
292
344
  })
293
345
 
294
- const answers = {
295
- ...themeAnswer,
296
- ...interactiveAnswer,
297
- ...backupAnswer,
298
- ...strategyAnswer,
299
- ...timeoutAnswer,
346
+ const answers: ConfigurationWizardResult = {
347
+ theme: themeAnswer.theme as string,
348
+ interactive: interactiveAnswer.interactive as boolean,
349
+ backup: backupAnswer.backup as boolean,
350
+ updateStrategy: strategyAnswer.updateStrategy as string,
351
+ timeout: timeoutAnswer.timeout as number,
300
352
  }
301
353
 
302
354
  return answers
@@ -305,25 +357,27 @@ export class InteractivePrompts {
305
357
  /**
306
358
  * Impact preview before update
307
359
  */
308
- async previewImpact(impact: any): Promise<boolean> {
309
- console.log(chalk.bold.blue('\n📊 Impact Preview\n'))
360
+ async previewImpact(impact: UpdateImpact): Promise<boolean> {
361
+ cliOutput.print(chalk.bold.blue(`\n${t('prompt.impactPreview')}\n`))
310
362
 
311
363
  // Display impact summary
312
- console.log(`Packages to update: ${impact.totalUpdates}`)
313
- console.log(`Risk level: ${impact.riskLevel}`)
314
- console.log(`Affected packages: ${impact.affectedCount}`)
364
+ cliOutput.print(t('prompt.packagesToUpdate', { count: impact.totalUpdates }))
365
+ cliOutput.print(t('prompt.riskLevel', { level: impact.riskLevel }))
366
+ cliOutput.print(t('prompt.affectedPackages', { count: impact.affectedCount }))
315
367
 
316
368
  if (impact.securityUpdates > 0) {
317
- console.log(StyledText.iconSecurity(`${impact.securityUpdates} security updates`))
369
+ cliOutput.print(
370
+ StyledText.iconSecurity(t('prompt.securityUpdatesCount', { count: impact.securityUpdates }))
371
+ )
318
372
  }
319
373
 
320
- console.log('')
374
+ cliOutput.print('')
321
375
 
322
376
  const answers = await inquirer.prompt([
323
377
  {
324
378
  type: 'confirm',
325
379
  name: 'proceed',
326
- message: 'Proceed with update?',
380
+ message: t('prompt.proceedWithUpdate'),
327
381
  default: true,
328
382
  },
329
383
  ])
@@ -336,17 +390,17 @@ export class InteractivePrompts {
336
390
  */
337
391
  async errorRecoveryOptions(error: string): Promise<string> {
338
392
  const options = [
339
- { name: 'Retry operation', value: 'retry' },
340
- { name: 'Skip this package', value: 'skip' },
341
- { name: 'Continue with remaining', value: 'continue' },
342
- { name: 'Abort operation', value: 'abort' },
393
+ { name: t('prompt.retryOperation'), value: 'retry' },
394
+ { name: t('prompt.skipPackage'), value: 'skip' },
395
+ { name: t('prompt.continueRemaining'), value: 'continue' },
396
+ { name: t('prompt.abortOperation'), value: 'abort' },
343
397
  ]
344
398
 
345
399
  const answers = await inquirer.prompt([
346
400
  {
347
401
  type: 'list',
348
402
  name: 'action',
349
- message: StyledText.iconError(`Error: ${error}`),
403
+ message: StyledText.iconError(t('prompt.errorMessage', { error })),
350
404
  choices: options,
351
405
  },
352
406
  ])
@@ -373,8 +427,8 @@ export class InteractivePrompts {
373
427
  /**
374
428
  * Format package choice for display
375
429
  */
376
- private formatPackageChoice(pkg: any): string {
377
- const updateTypeColor: Record<string, any> = {
430
+ private formatPackageChoice(pkg: PackageChoice): string {
431
+ const updateTypeColor: Record<string, (text: string) => string> = {
378
432
  major: chalk.red,
379
433
  minor: chalk.yellow,
380
434
  patch: chalk.green,
@@ -390,6 +444,15 @@ export class InteractivePrompts {
390
444
  * Auto-completion utilities
391
445
  */
392
446
  export class AutoCompleteManager {
447
+ /**
448
+ * Get workspace service instance for auto-completion
449
+ */
450
+ private static getWorkspaceService(): WorkspaceService {
451
+ const fsService = new FileSystemService()
452
+ const repository = new FileWorkspaceRepository(fsService)
453
+ return new WorkspaceService(repository)
454
+ }
455
+
393
456
  static async suggestWorkspaces(current: string): Promise<string[]> {
394
457
  const suggestions: string[] = []
395
458
 
@@ -411,58 +474,101 @@ export class AutoCompleteManager {
411
474
  suggestions.push(dir)
412
475
  }
413
476
  })
414
- } catch {
415
- // Ignore errors
477
+ } catch (error) {
478
+ // ERR-002: Log glob errors for debugging instead of silently ignoring
479
+ logger.debug('Failed to glob workspace pattern', { pattern, error })
416
480
  }
417
481
  }
418
482
 
419
483
  return suggestions.filter((s) => s.toLowerCase().includes(current.toLowerCase()))
420
484
  }
421
485
 
486
+ /**
487
+ * STUB-001: Suggest catalog names from current workspace
488
+ */
422
489
  static async suggestCatalogs(): Promise<string[]> {
423
- return []
490
+ try {
491
+ const workspaceService = AutoCompleteManager.getWorkspaceService()
492
+ const catalogs = await workspaceService.getCatalogs()
493
+ return catalogs.map((catalog) => catalog.name)
494
+ } catch (error) {
495
+ // Workspace not found or other error - return empty for auto-complete
496
+ logger.debug('Failed to get catalogs for auto-complete', { error })
497
+ return []
498
+ }
424
499
  }
425
500
 
501
+ /**
502
+ * STUB-001: Suggest package names from current workspace catalogs
503
+ */
426
504
  static async suggestPackages(): Promise<string[]> {
427
- return []
505
+ try {
506
+ const workspaceService = AutoCompleteManager.getWorkspaceService()
507
+ const catalogs = await workspaceService.getCatalogs()
508
+
509
+ // Collect all unique package names from all catalogs
510
+ const packageNames = new Set<string>()
511
+ for (const catalog of catalogs) {
512
+ for (const pkgName of catalog.packages) {
513
+ packageNames.add(pkgName)
514
+ }
515
+ }
516
+
517
+ return Array.from(packageNames).sort()
518
+ } catch (error) {
519
+ // Workspace not found or other error - return empty for auto-complete
520
+ logger.debug('Failed to get packages for auto-complete', { error })
521
+ return []
522
+ }
428
523
  }
429
524
  }
430
525
 
526
+ /**
527
+ * Command builder options type
528
+ */
529
+ interface CommandBuilderOptions {
530
+ format?: string
531
+ interactive?: boolean
532
+ dryRun?: boolean
533
+ backup?: boolean
534
+ prerelease?: boolean
535
+ }
536
+
431
537
  /**
432
538
  * Interactive command builder
433
539
  */
434
540
  export class InteractiveCommandBuilder {
435
541
  static async buildCommand(): Promise<{
436
542
  command: string
437
- options: Record<string, any>
543
+ options: CommandBuilderOptions
438
544
  }> {
439
545
  const baseCommand = await inquirer.prompt([
440
546
  {
441
547
  type: 'list',
442
548
  name: 'command',
443
- message: 'What would you like to do?',
549
+ message: t('prompt.whatToDo'),
444
550
  choices: [
445
- { name: 'Check for updates', value: 'check' },
446
- { name: 'Update dependencies', value: 'update' },
447
- { name: 'Analyze impact', value: 'analyze' },
448
- { name: 'Show workspace info', value: 'workspace' },
551
+ { name: t('prompt.checkForUpdates'), value: 'check' },
552
+ { name: t('prompt.updateDependencies'), value: 'update' },
553
+ { name: t('prompt.analyzeImpact'), value: 'analyze' },
554
+ { name: t('prompt.showWorkspaceInfo'), value: 'workspace' },
449
555
  ],
450
556
  },
451
557
  ])
452
558
 
453
- const options: Record<string, any> = {}
559
+ const options: CommandBuilderOptions = {}
454
560
 
455
561
  // Common options
456
562
  const common = await inquirer.prompt([
457
563
  {
458
564
  type: 'list',
459
565
  name: 'format',
460
- message: 'Output format:',
566
+ message: t('prompt.outputFormat'),
461
567
  choices: [
462
- { name: 'Table (rich)', value: 'table' },
463
- { name: 'JSON', value: 'json' },
464
- { name: 'YAML', value: 'yaml' },
465
- { name: 'Minimal', value: 'minimal' },
568
+ { name: t('prompt.formatTable'), value: 'table' },
569
+ { name: t('prompt.formatJson'), value: 'json' },
570
+ { name: t('prompt.formatYaml'), value: 'yaml' },
571
+ { name: t('prompt.formatMinimal'), value: 'minimal' },
466
572
  ],
467
573
  default: 'table',
468
574
  },
@@ -477,19 +583,19 @@ export class InteractiveCommandBuilder {
477
583
  {
478
584
  type: 'confirm',
479
585
  name: 'interactive',
480
- message: 'Interactive mode?',
586
+ message: t('prompt.interactiveMode'),
481
587
  default: true,
482
588
  },
483
589
  {
484
590
  type: 'confirm',
485
591
  name: 'dryRun',
486
- message: 'Dry run mode?',
592
+ message: t('prompt.dryRunMode'),
487
593
  default: false,
488
594
  },
489
595
  {
490
596
  type: 'confirm',
491
597
  name: 'backup',
492
- message: 'Create backup?',
598
+ message: t('prompt.createBackup'),
493
599
  default: true,
494
600
  },
495
601
  ])
@@ -502,7 +608,7 @@ export class InteractiveCommandBuilder {
502
608
  {
503
609
  type: 'confirm',
504
610
  name: 'includePrerelease',
505
- message: 'Include pre-release versions?',
611
+ message: t('prompt.includePrerelease'),
506
612
  default: false,
507
613
  },
508
614
  ])
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Option Utilities
3
+ *
4
+ * Common utilities for merging CLI options with defaults.
5
+ */
6
+
7
+ /**
8
+ * Option types supported by the merge utility
9
+ */
10
+ type OptionType = 'string' | 'boolean' | 'number' | 'array' | 'optional-string'
11
+
12
+ /**
13
+ * Schema definition for a single option
14
+ */
15
+ interface OptionSchema<T = unknown> {
16
+ type: OptionType
17
+ default: T
18
+ }
19
+
20
+ /**
21
+ * Schema for a complete set of options
22
+ */
23
+ export type OptionsSchema<T> = {
24
+ [K in keyof T]: OptionSchema<T[K]>
25
+ }
26
+
27
+ /**
28
+ * Merge a single value with its default based on type
29
+ */
30
+ function mergeValue<T>(value: unknown, schema: OptionSchema<T>): T {
31
+ switch (schema.type) {
32
+ case 'string':
33
+ return ((value as string) || schema.default) as T
34
+
35
+ case 'optional-string':
36
+ return ((value as string) || undefined) as T
37
+
38
+ case 'boolean':
39
+ return ((value as boolean | undefined) ?? schema.default) as T
40
+
41
+ case 'number':
42
+ return ((value as number | undefined) ?? schema.default) as T
43
+
44
+ case 'array':
45
+ return ((value as T) || schema.default) as T
46
+
47
+ default:
48
+ return (value ?? schema.default) as T
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Merge existing options with defaults based on schema
54
+ *
55
+ * @param existing - Options provided by user/CLI
56
+ * @param schema - Schema defining types and defaults
57
+ * @returns Merged options with proper types
58
+ */
59
+ export function mergeOptions<T extends Record<string, unknown>>(
60
+ existing: Record<string, unknown>,
61
+ schema: OptionsSchema<T>
62
+ ): T {
63
+ const result = {} as T
64
+
65
+ for (const key of Object.keys(schema) as (keyof T)[]) {
66
+ const optionSchema = schema[key]
67
+ const value = existing[key as string]
68
+ result[key] = mergeValue(value, optionSchema)
69
+ }
70
+
71
+ return result
72
+ }
73
+
74
+ /**
75
+ * Common option schemas reused across commands
76
+ */
77
+ export const commonSchemas = {
78
+ catalog: { type: 'optional-string' as const, default: undefined as string | undefined },
79
+ format: { type: 'string' as const, default: 'table' as string },
80
+ target: { type: 'string' as const, default: 'latest' as string },
81
+ prerelease: { type: 'boolean' as const, default: false as boolean },
82
+ include: { type: 'array' as const, default: [] as string[] },
83
+ exclude: { type: 'array' as const, default: [] as string[] },
84
+ force: { type: 'boolean' as const, default: false as boolean },
85
+ dryRun: { type: 'boolean' as const, default: false as boolean },
86
+ ai: { type: 'boolean' as const, default: false as boolean },
87
+ provider: { type: 'string' as const, default: 'auto' as string },
88
+ analysisType: { type: 'string' as const, default: 'impact' as string },
89
+ }