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
|
@@ -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 }
|