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
@@ -0,0 +1,785 @@
1
+ /**
2
+ * Command Registrar
3
+ *
4
+ * Handles registration of all CLI commands with lazy service injection.
5
+ * Extracted from index.ts to improve single responsibility and maintainability.
6
+ */
7
+
8
+ import { ExitPromptError } from '@inquirer/core'
9
+ import type { AnalysisType, IPackageManagerService } from '@pcu/core'
10
+ import {
11
+ CatalogUpdateService,
12
+ FileSystemService,
13
+ FileWorkspaceRepository,
14
+ PnpmPackageManagerService,
15
+ WorkspaceService,
16
+ } from '@pcu/core'
17
+ import {
18
+ exitProcess,
19
+ isCommandExitError,
20
+ Logger,
21
+ logger,
22
+ parseBooleanFlag,
23
+ t,
24
+ VersionChecker,
25
+ } from '@pcu/utils'
26
+ import chalk from 'chalk'
27
+ import { type Command, Option } from 'commander'
28
+ import { AiCommand } from './commands/aiCommand.js'
29
+ import { AnalyzeCommand } from './commands/analyzeCommand.js'
30
+ import { CheckCommand } from './commands/checkCommand.js'
31
+ import { GraphCommand } from './commands/graphCommand.js'
32
+ import { InitCommand } from './commands/initCommand.js'
33
+ import { RollbackCommand } from './commands/rollbackCommand.js'
34
+ import { SecurityCommand } from './commands/securityCommand.js'
35
+ import { ThemeCommand } from './commands/themeCommand.js'
36
+ import { UpdateCommand } from './commands/updateCommand.js'
37
+ import { WorkspaceCommand } from './commands/workspaceCommand.js'
38
+ import { CLI_CHOICES } from './constants/cliChoices.js'
39
+ import { type OutputFormat, OutputFormatter } from './formatters/outputFormatter.js'
40
+ import {
41
+ hasProvidedOptions,
42
+ interactiveOptionsCollector,
43
+ } from './interactive/InteractiveOptionsCollector.js'
44
+ import { cliOutput } from './utils/cliOutput.js'
45
+
46
+ /**
47
+ * Service type definitions
48
+ */
49
+ export type Services = {
50
+ fileSystemService: FileSystemService
51
+ workspaceRepository: FileWorkspaceRepository
52
+ catalogUpdateService: CatalogUpdateService
53
+ workspaceService: WorkspaceService
54
+ packageManagerService: IPackageManagerService
55
+ }
56
+
57
+ /**
58
+ * Global options passed to all commands
59
+ */
60
+ export interface GlobalOptions {
61
+ workspace?: string
62
+ verbose?: boolean
63
+ noColor?: boolean
64
+ }
65
+
66
+ /**
67
+ * Context provided to command executors
68
+ */
69
+ export interface CommandContext<TOptions> {
70
+ options: TOptions
71
+ globalOptions: GlobalOptions
72
+ services: Services
73
+ }
74
+
75
+ /**
76
+ * Configuration for creating a command action handler
77
+ */
78
+ export interface CommandActionConfig<TOptions> {
79
+ /** Command name for error reporting */
80
+ name: string
81
+ /** Whether this command needs services (default: true) */
82
+ needsServices?: boolean
83
+ /** Interactive options collector function */
84
+ interactiveCollector?: (options: TOptions & { workspace?: string }) => Promise<Partial<TOptions>>
85
+ /** Options to exclude from "hasProvidedOptions" check */
86
+ excludeFromCheck?: string[]
87
+ /** Force exit after execution (for commands with interactive mode that keep stdin open) */
88
+ forceExit?: boolean
89
+ }
90
+
91
+ /**
92
+ * Check if error is an ExitPromptError from @inquirer/core (user pressed Ctrl+C)
93
+ * Uses instanceof as primary method, with fallbacks for cross-realm scenarios
94
+ */
95
+ export function isExitPromptError(error: unknown): boolean {
96
+ if (!error || typeof error !== 'object') return false
97
+
98
+ // Primary: instanceof check (most reliable when in same realm)
99
+ if (error instanceof ExitPromptError) return true
100
+
101
+ // Fallback 1: Check by name property (for cross-realm scenarios)
102
+ if ('name' in error && (error as { name: string }).name === 'ExitPromptError') return true
103
+
104
+ // Fallback 2: Check by constructor name (for edge cases)
105
+ if (error.constructor?.name === 'ExitPromptError') return true
106
+
107
+ return false
108
+ }
109
+
110
+ /**
111
+ * Unified command error handler
112
+ * Handles common error types: CommandExitError, ExitPromptError, and general errors
113
+ *
114
+ * ARCH-002: Uses exitProcess instead of direct process.exit for better testability.
115
+ */
116
+ export function handleCommandError(error: unknown, commandName: string): never {
117
+ // Handle structured exit codes
118
+ if (isCommandExitError(error)) {
119
+ exitProcess(error.exitCode)
120
+ }
121
+
122
+ // Handle user cancellation (Ctrl+C) gracefully
123
+ if (isExitPromptError(error)) {
124
+ cliOutput.print(chalk.gray(`\n${t('cli.cancelled')}`))
125
+ exitProcess(0)
126
+ }
127
+
128
+ // Log and display general errors
129
+ logger.error(`${commandName} command failed`, error instanceof Error ? error : undefined, {
130
+ command: commandName,
131
+ })
132
+ cliOutput.error(chalk.red(`❌ ${t('cli.error')}`), error)
133
+ exitProcess(1)
134
+ }
135
+
136
+ /**
137
+ * Lazy service factory - creates services only when first accessed
138
+ *
139
+ * ARCH-001: This factory provides centralized dependency injection for CLI commands.
140
+ * Design decisions:
141
+ * - Lazy initialization: Services are only created when first accessed
142
+ * - Singleton pattern: Once created, the same services are reused
143
+ * - Async creation: Allows for async initialization (e.g., npmrc parsing)
144
+ *
145
+ * To add a new service:
146
+ * 1. Add the service type to the Services interface
147
+ * 2. Create the service instance in the get() method
148
+ * 3. Add it to the returned services object
149
+ *
150
+ * For testing, use reset() to clear cached services, or inject mock services
151
+ * via the static withServices() factory method.
152
+ */
153
+ export class LazyServiceFactory {
154
+ private services: Services | null = null
155
+ private workspacePath?: string
156
+
157
+ constructor(workspacePath?: string) {
158
+ this.workspacePath = workspacePath
159
+ }
160
+
161
+ /**
162
+ * Create a factory with pre-configured services (useful for testing)
163
+ * ARCH-001: Enables dependency injection for testing without modifying production code
164
+ */
165
+ static withServices(services: Services): LazyServiceFactory {
166
+ const factory = new LazyServiceFactory()
167
+ factory.services = services
168
+ return factory
169
+ }
170
+
171
+ /**
172
+ * Get services, creating them lazily on first access (async for npmrc parsing)
173
+ */
174
+ async get(): Promise<Services> {
175
+ if (!this.services) {
176
+ const fileSystemService = new FileSystemService()
177
+ const workspaceRepository = new FileWorkspaceRepository(fileSystemService)
178
+ const catalogUpdateService = await CatalogUpdateService.createWithConfig(
179
+ workspaceRepository,
180
+ this.workspacePath
181
+ )
182
+ const workspaceService = new WorkspaceService(workspaceRepository)
183
+ const packageManagerService = new PnpmPackageManagerService()
184
+
185
+ this.services = {
186
+ fileSystemService,
187
+ workspaceRepository,
188
+ catalogUpdateService,
189
+ workspaceService,
190
+ packageManagerService,
191
+ }
192
+ }
193
+ return this.services
194
+ }
195
+
196
+ /**
197
+ * Reset cached services (useful for testing)
198
+ * ARCH-001: Allows test isolation by clearing singleton state
199
+ */
200
+ reset(): void {
201
+ this.services = null
202
+ }
203
+
204
+ /**
205
+ * Check if services have been initialized
206
+ */
207
+ isInitialized(): boolean {
208
+ return this.services !== null
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Factory function to create command action handlers
214
+ * Eliminates repetitive boilerplate in command registration by providing:
215
+ * - Unified error handling with handleCommandError
216
+ * - Automatic globalOptions extraction
217
+ * - Interactive mode detection and options collection
218
+ * - Lazy service injection
219
+ */
220
+ function createCommandAction<TOptions>(
221
+ serviceFactory: LazyServiceFactory,
222
+ config: CommandActionConfig<TOptions>,
223
+ executor: (ctx: CommandContext<TOptions>) => Promise<void>
224
+ ): (options: TOptions, command: Command) => Promise<void> {
225
+ return async (options: TOptions, command: Command) => {
226
+ try {
227
+ const globalOptions = (command.parent?.opts() ?? {}) as GlobalOptions
228
+
229
+ // Set logger to debug level when verbose mode is enabled
230
+ if (globalOptions.verbose) {
231
+ Logger.setGlobalLevel('debug')
232
+ }
233
+
234
+ let finalOptions = options
235
+
236
+ // Handle interactive mode if collector is provided
237
+ if (config.interactiveCollector) {
238
+ const isInteractive = (options as Record<string, unknown>).interactive === true
239
+ const noMeaningfulOptions = !hasProvidedOptions(
240
+ options as Record<string, unknown>,
241
+ command,
242
+ config.excludeFromCheck
243
+ )
244
+
245
+ if (isInteractive || noMeaningfulOptions) {
246
+ const collected = await config.interactiveCollector({
247
+ ...options,
248
+ workspace: globalOptions.workspace,
249
+ })
250
+ finalOptions = { ...options, ...collected }
251
+ }
252
+ }
253
+
254
+ // Get services only if needed (some commands like theme/init don't need them)
255
+ const services =
256
+ config.needsServices !== false ? await serviceFactory.get() : ({} as Services)
257
+
258
+ await executor({ options: finalOptions, globalOptions, services })
259
+
260
+ // Force exit if configured (for interactive commands that keep stdin open)
261
+ // ARCH-002: Uses exitProcess for testability
262
+ if (config.forceExit) {
263
+ exitProcess(0)
264
+ }
265
+ } catch (error) {
266
+ handleCommandError(error, config.name)
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Register all CLI commands
273
+ * Uses lazy service factory to defer service instantiation until command execution
274
+ */
275
+ export function registerCommands(
276
+ program: Command,
277
+ serviceFactory: LazyServiceFactory,
278
+ packageJson: { version: string }
279
+ ): void {
280
+ // Check command
281
+ program
282
+ .command('check')
283
+ .description(t('cli.description.check'))
284
+ .option('-i, --interactive', t('cli.option.interactive'))
285
+ .option('--catalog <name>', t('cli.option.catalog'))
286
+ .addOption(
287
+ new Option('-f, --format <type>', t('cli.option.format'))
288
+ .choices(CLI_CHOICES.format)
289
+ .default('table')
290
+ )
291
+ .addOption(
292
+ new Option('-t, --target <type>', t('cli.option.target'))
293
+ .choices(CLI_CHOICES.target)
294
+ .default('latest')
295
+ )
296
+ .option('--prerelease', t('cli.option.prerelease'))
297
+ .option('--include <pattern...>', t('cli.option.include'))
298
+ .option('--exclude <pattern...>', t('cli.option.exclude'))
299
+ .option('--exit-code', t('cli.option.exitCode'))
300
+ .option('--no-security', t('cli.option.noSecurity'))
301
+ .action(
302
+ createCommandAction(
303
+ serviceFactory,
304
+ {
305
+ name: 'check',
306
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectCheckOptions(opts),
307
+ excludeFromCheck: ['interactive'],
308
+ },
309
+ async ({ options, globalOptions, services }) => {
310
+ const checkCommand = new CheckCommand(services.catalogUpdateService)
311
+ await checkCommand.execute({
312
+ workspace: globalOptions.workspace,
313
+ catalog: options.catalog,
314
+ format: options.format,
315
+ target: options.target,
316
+ prerelease: options.prerelease,
317
+ include: options.include ?? [],
318
+ exclude: options.exclude ?? [],
319
+ verbose: globalOptions.verbose,
320
+ color: !globalOptions.noColor,
321
+ exitCode: options.exitCode,
322
+ noSecurity: options.security === false,
323
+ })
324
+ }
325
+ )
326
+ )
327
+
328
+ // Update command
329
+ program
330
+ .command('update')
331
+ .description(t('cli.description.update'))
332
+ // Basic options
333
+ .option('-i, --interactive', t('cli.option.interactive'))
334
+ .option('-d, --dry-run', t('cli.option.dryRun'))
335
+ .option('--force', t('cli.option.force'))
336
+ .option('-b, --create-backup', t('cli.option.createBackup'), true)
337
+ .option('--no-backup', t('cli.option.noBackup'))
338
+ // Filter options
339
+ .option('--catalog <name>', t('cli.option.catalog'))
340
+ .option('--include <pattern...>', t('cli.option.include'))
341
+ .option('--exclude <pattern...>', t('cli.option.exclude'))
342
+ .addOption(
343
+ new Option('-t, --target <type>', t('cli.option.target'))
344
+ .choices(CLI_CHOICES.target)
345
+ .default('latest')
346
+ )
347
+ .option('--prerelease', t('cli.option.prerelease'))
348
+ // Output options
349
+ .addOption(
350
+ new Option('-f, --format <type>', t('cli.option.format'))
351
+ .choices(CLI_CHOICES.format)
352
+ .default('table')
353
+ )
354
+ .option('--changelog', t('cli.option.changelog'))
355
+ .option('--no-changelog', t('cli.option.noChangelog'))
356
+ // AI options (enabled by default)
357
+ .option('--ai', t('cli.option.ai'), true)
358
+ .option('--no-ai', t('cli.option.noAi'))
359
+ .addOption(
360
+ new Option('--provider <name>', t('cli.option.provider'))
361
+ .choices(CLI_CHOICES.provider)
362
+ .default('auto')
363
+ )
364
+ .addOption(
365
+ new Option('--analysis-type <type>', t('cli.option.analysisType'))
366
+ .choices(CLI_CHOICES.analysisType)
367
+ .default('impact')
368
+ )
369
+ .option('--skip-cache', t('cli.option.skipCache'))
370
+ // Post-update options
371
+ .option('--install', t('cli.option.install'), true)
372
+ .option('--no-install', t('cli.option.noInstall'))
373
+ .option('--no-security', t('cli.option.noSecurity'))
374
+ .addHelpText(
375
+ 'after',
376
+ () => `
377
+ ${t('cli.help.optionGroupsTitle')}
378
+ ${t('cli.help.groupBasic')} -i, -d, --force, -b
379
+ ${t('cli.help.groupFilter')} --catalog, --include, --exclude, -t, --prerelease
380
+ ${t('cli.help.groupOutput')} -f, --changelog
381
+ ${t('cli.help.groupAI')} --ai (default), --no-ai, --provider, --analysis-type, --skip-cache
382
+ ${t('cli.help.groupInstall')} --install, --no-security
383
+
384
+ ${t('cli.help.tipLabel')} ${t('cli.help.tipContent', { locale: I18n.getLocale() })}
385
+ `
386
+ )
387
+ .action(
388
+ createCommandAction(
389
+ serviceFactory,
390
+ {
391
+ name: 'update',
392
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectUpdateOptions(opts),
393
+ excludeFromCheck: ['interactive'],
394
+ forceExit: true, // Interactive mode keeps stdin open
395
+ },
396
+ async ({ options, globalOptions, services }) => {
397
+ const updateCommand = new UpdateCommand(
398
+ services.catalogUpdateService,
399
+ services.workspaceService,
400
+ services.packageManagerService
401
+ )
402
+ await updateCommand.execute({
403
+ workspace: globalOptions.workspace,
404
+ catalog: options.catalog,
405
+ format: options.format,
406
+ target: options.target,
407
+ interactive: options.interactive,
408
+ dryRun: options.dryRun,
409
+ force: options.force,
410
+ prerelease: options.prerelease,
411
+ include: options.include ?? [],
412
+ exclude: options.exclude ?? [],
413
+ createBackup: options.createBackup,
414
+ verbose: globalOptions.verbose,
415
+ color: !globalOptions.noColor,
416
+ ai: parseBooleanFlag(options.ai),
417
+ provider: options.provider,
418
+ analysisType: options.analysisType as AnalysisType,
419
+ skipCache: parseBooleanFlag(options.skipCache),
420
+ install: options.install,
421
+ changelog: options.changelog,
422
+ noSecurity: options.security === false,
423
+ })
424
+ }
425
+ )
426
+ )
427
+
428
+ // Analyze command
429
+ program
430
+ .command('analyze')
431
+ .description(t('cli.description.analyze'))
432
+ .argument('[package]', t('cli.argument.package'))
433
+ .argument('[version]', t('cli.argument.version'))
434
+ .option('--catalog <name>', t('cli.option.catalog'))
435
+ .addOption(
436
+ new Option('-f, --format <type>', t('cli.option.format'))
437
+ .choices(CLI_CHOICES.format)
438
+ .default('table')
439
+ )
440
+ .option('--no-ai', t('cli.option.noAi'))
441
+ .addOption(
442
+ new Option('--provider <name>', t('cli.option.provider'))
443
+ .choices(CLI_CHOICES.provider)
444
+ .default('auto')
445
+ )
446
+ .addOption(
447
+ new Option('--analysis-type <type>', t('cli.option.analysisType'))
448
+ .choices(CLI_CHOICES.analysisType)
449
+ .default('impact')
450
+ )
451
+ .option('--skip-cache', t('cli.option.skipCache'))
452
+ .action(async (packageName, version, options, command) => {
453
+ try {
454
+ const globalOptions = command.parent.opts()
455
+
456
+ let finalPackageName = packageName
457
+ let finalVersion = version
458
+ let finalOptions = options
459
+
460
+ // If no package name provided, enter interactive mode
461
+ if (!packageName) {
462
+ const collected = await interactiveOptionsCollector.collectAnalyzeOptions({
463
+ ...options,
464
+ workspace: globalOptions.workspace,
465
+ })
466
+ finalPackageName = collected.packageName
467
+ finalVersion = collected.version
468
+ finalOptions = { ...options, ...collected }
469
+ }
470
+
471
+ const services = await serviceFactory.get()
472
+ const analyzeCommand = new AnalyzeCommand(
473
+ services.catalogUpdateService,
474
+ services.workspaceService
475
+ )
476
+
477
+ await analyzeCommand.execute(finalPackageName, finalVersion, {
478
+ workspace: globalOptions.workspace,
479
+ catalog: finalOptions.catalog,
480
+ format: finalOptions.format,
481
+ ai: finalOptions.ai,
482
+ provider: finalOptions.provider,
483
+ analysisType: finalOptions.analysisType as AnalysisType,
484
+ skipCache: parseBooleanFlag(finalOptions.skipCache),
485
+ verbose: globalOptions.verbose,
486
+ color: !globalOptions.noColor,
487
+ })
488
+ } catch (error) {
489
+ handleCommandError(error, 'analyze')
490
+ }
491
+ })
492
+
493
+ // Workspace command
494
+ program
495
+ .command('workspace')
496
+ .description(t('cli.description.workspace'))
497
+ .option('--validate', t('cli.option.validate'))
498
+ .option('--info', t('cli.option.stats'))
499
+ .addOption(
500
+ new Option('-f, --format <type>', t('cli.option.format'))
501
+ .choices(CLI_CHOICES.format)
502
+ .default('table')
503
+ )
504
+ .action(
505
+ createCommandAction(
506
+ serviceFactory,
507
+ {
508
+ name: 'workspace',
509
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectWorkspaceOptions(opts),
510
+ },
511
+ async ({ options, globalOptions, services }) => {
512
+ const workspaceCommand = new WorkspaceCommand(services.workspaceService)
513
+ await workspaceCommand.execute({
514
+ workspace: globalOptions.workspace,
515
+ validate: options.validate,
516
+ stats: options.info,
517
+ format: options.format,
518
+ verbose: globalOptions.verbose,
519
+ color: !globalOptions.noColor,
520
+ })
521
+ }
522
+ )
523
+ )
524
+
525
+ // Theme command
526
+ program
527
+ .command('theme')
528
+ .description(t('cli.description.theme'))
529
+ .option('-s, --set <theme>', t('cli.option.setTheme'))
530
+ .option('-l, --list', t('cli.option.listThemes'))
531
+ .option('-i, --interactive', t('cli.option.interactive'))
532
+ .action(
533
+ createCommandAction(
534
+ serviceFactory,
535
+ {
536
+ name: 'theme',
537
+ needsServices: false,
538
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectThemeOptions(opts),
539
+ forceExit: true, // Interactive mode keeps stdin open
540
+ },
541
+ async ({ options }) => {
542
+ const themeCommand = new ThemeCommand()
543
+ await themeCommand.execute({
544
+ set: options.set,
545
+ list: options.list,
546
+ interactive: options.interactive,
547
+ })
548
+ }
549
+ )
550
+ )
551
+
552
+ // Security command
553
+ program
554
+ .command('security')
555
+ .description(t('cli.description.security'))
556
+ .addOption(
557
+ new Option('-f, --format <type>', t('cli.option.format'))
558
+ .choices(CLI_CHOICES.format)
559
+ .default('table')
560
+ )
561
+ .option('--audit', t('cli.option.audit'), true)
562
+ .option('--fix-vulns', t('cli.option.fixVulns'))
563
+ .addOption(
564
+ new Option('--severity <level>', t('cli.option.severity')).choices(CLI_CHOICES.severity)
565
+ )
566
+ .option('--include-dev', t('cli.option.includeDev'))
567
+ .option('--snyk', t('cli.option.snyk'))
568
+ .action(
569
+ createCommandAction(
570
+ serviceFactory,
571
+ {
572
+ name: 'security',
573
+ needsServices: false, // Uses custom formatter instead
574
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectSecurityOptions(opts),
575
+ },
576
+ async ({ options, globalOptions }) => {
577
+ const formatter = new OutputFormatter(
578
+ options.format as OutputFormat,
579
+ !globalOptions.noColor
580
+ )
581
+ const securityCommand = new SecurityCommand(formatter)
582
+ await securityCommand.execute({
583
+ workspace: globalOptions.workspace,
584
+ format: options.format,
585
+ audit: options.audit,
586
+ fixVulns: options.fixVulns,
587
+ severity: options.severity,
588
+ includeDev: options.includeDev,
589
+ snyk: options.snyk,
590
+ verbose: globalOptions.verbose,
591
+ color: !globalOptions.noColor,
592
+ })
593
+ }
594
+ )
595
+ )
596
+
597
+ // Init command
598
+ program
599
+ .command('init')
600
+ .description(t('cli.description.init'))
601
+ .option('--force', t('cli.option.forceOverwrite'))
602
+ .option('--full', t('cli.option.full'))
603
+ .option('--create-workspace', t('cli.option.createWorkspace'), true)
604
+ .option('--no-create-workspace', t('cli.option.noCreateWorkspace'))
605
+ .addOption(
606
+ new Option('-f, --format <type>', t('cli.option.format'))
607
+ .choices(CLI_CHOICES.format)
608
+ .default('table')
609
+ )
610
+ .action(
611
+ createCommandAction(
612
+ serviceFactory,
613
+ {
614
+ name: 'init',
615
+ needsServices: false,
616
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectInitOptions(opts),
617
+ },
618
+ async ({ options, globalOptions }) => {
619
+ const initCommand = new InitCommand()
620
+ await initCommand.execute({
621
+ workspace: globalOptions.workspace,
622
+ force: options.force,
623
+ full: options.full,
624
+ createWorkspace: options.createWorkspace,
625
+ verbose: globalOptions.verbose,
626
+ color: !globalOptions.noColor,
627
+ })
628
+ }
629
+ )
630
+ )
631
+
632
+ // Rollback command
633
+ program
634
+ .command('rollback')
635
+ .description(t('cli.description.rollback'))
636
+ .option('-l, --list', t('cli.option.listBackups'))
637
+ .option('--latest', t('cli.option.restoreLatest'))
638
+ .option('--delete-all', t('cli.option.deleteAllBackups'))
639
+ .action(
640
+ createCommandAction(
641
+ serviceFactory,
642
+ {
643
+ name: 'rollback',
644
+ needsServices: false,
645
+ interactiveCollector: (opts) => interactiveOptionsCollector.collectRollbackOptions(opts),
646
+ },
647
+ async ({ options, globalOptions }) => {
648
+ const rollbackCommand = new RollbackCommand()
649
+ await rollbackCommand.execute({
650
+ workspace: globalOptions.workspace,
651
+ list: options.list,
652
+ latest: options.latest,
653
+ deleteAll: options.deleteAll,
654
+ verbose: globalOptions.verbose,
655
+ color: !globalOptions.noColor,
656
+ })
657
+ }
658
+ )
659
+ )
660
+
661
+ // Graph command - dependency visualization
662
+ program
663
+ .command('graph')
664
+ .description(t('cli.description.graph'))
665
+ .addOption(
666
+ new Option('-f, --format <type>', t('cli.option.graphFormat'))
667
+ .choices(['text', 'mermaid', 'dot', 'json'])
668
+ .default('text')
669
+ )
670
+ .addOption(
671
+ new Option('-t, --type <type>', t('cli.option.graphType'))
672
+ .choices(['catalog', 'package', 'full'])
673
+ .default('catalog')
674
+ )
675
+ .option('--catalog <name>', t('cli.option.catalog'))
676
+ .action(
677
+ createCommandAction(
678
+ serviceFactory,
679
+ {
680
+ name: 'graph',
681
+ },
682
+ async ({ options, globalOptions, services }) => {
683
+ const graphCommand = new GraphCommand(services.workspaceService)
684
+ await graphCommand.execute({
685
+ workspace: globalOptions.workspace,
686
+ format: options.format,
687
+ type: options.type,
688
+ catalog: options.catalog,
689
+ verbose: globalOptions.verbose,
690
+ color: !globalOptions.noColor,
691
+ })
692
+ }
693
+ )
694
+ )
695
+
696
+ // AI command - check AI provider status
697
+ program
698
+ .command('ai')
699
+ .description(t('cli.description.ai'))
700
+ .option('--status', t('cli.option.aiStatus'), true)
701
+ .option('--test', t('cli.option.aiTest'))
702
+ .option('--cache-stats', t('cli.option.aiCacheStats'))
703
+ .option('--clear-cache', t('cli.option.aiClearCache'))
704
+ .action(
705
+ createCommandAction(
706
+ serviceFactory,
707
+ {
708
+ name: 'ai',
709
+ needsServices: false,
710
+ },
711
+ async ({ options }) => {
712
+ const aiCommand = new AiCommand()
713
+ await aiCommand.execute({
714
+ status: options.status,
715
+ test: options.test,
716
+ cacheStats: options.cacheStats,
717
+ clearCache: options.clearCache,
718
+ })
719
+ }
720
+ )
721
+ )
722
+
723
+ // Self-update command
724
+ program
725
+ .command('self-update')
726
+ .description(t('cli.description.selfUpdate'))
727
+ .action(
728
+ createCommandAction(
729
+ serviceFactory,
730
+ {
731
+ name: 'self-update',
732
+ needsServices: false,
733
+ },
734
+ async ({ globalOptions }) => {
735
+ cliOutput.print(chalk.cyan(t('command.selfUpdate.checking')))
736
+
737
+ try {
738
+ const versionResult = await VersionChecker.checkVersion(packageJson.version, {
739
+ skipPrompt: true,
740
+ timeout: 10000, // Longer timeout for explicit update
741
+ })
742
+
743
+ if (versionResult.isLatest) {
744
+ cliOutput.print(
745
+ chalk.green(
746
+ t('command.selfUpdate.latestAlready', { version: versionResult.currentVersion })
747
+ )
748
+ )
749
+ return
750
+ }
751
+
752
+ // User explicitly requested update, so perform it
753
+ cliOutput.print(
754
+ chalk.blue(t('command.selfUpdate.updating', { version: versionResult.latestVersion }))
755
+ )
756
+
757
+ const success = await VersionChecker.performUpdateAction()
758
+ if (success) {
759
+ cliOutput.print(
760
+ chalk.green(
761
+ t('command.selfUpdate.success', { version: versionResult.latestVersion })
762
+ )
763
+ )
764
+ cliOutput.print(chalk.gray(t('command.selfUpdate.restartHint')))
765
+ } else {
766
+ cliOutput.error(chalk.red(t('command.selfUpdate.failed')))
767
+ cliOutput.print(chalk.gray('You can manually update with: npm install -g pcu@latest'))
768
+ exitProcess(1)
769
+ }
770
+ } catch (error) {
771
+ cliOutput.error(chalk.red(t('command.selfUpdate.failed')))
772
+ if (globalOptions.verbose) {
773
+ cliOutput.error(error)
774
+ }
775
+ cliOutput.print(chalk.gray('You can manually update with: npm install -g pcu@latest'))
776
+ exitProcess(1)
777
+ }
778
+ }
779
+ )
780
+ )
781
+ }
782
+
783
+ // Re-export I18n for use in update command help text
784
+ import { I18n } from '@pcu/utils'
785
+ export { I18n }