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,334 @@
1
+ /**
2
+ * Rollback Command
3
+ *
4
+ * CLI command to rollback catalog updates to a previous state.
5
+ * Supports listing backups and restoring from a specific backup.
6
+ *
7
+ * QUAL-006/QUAL-016: Refactored to use unified output helpers (cliOutput, StyledText).
8
+ * QUAL-007: Extracted common restore logic to reduce code duplication.
9
+ */
10
+
11
+ import path from 'node:path'
12
+ import { type BackupInfo, BackupService } from '@pcu/core'
13
+ import { t } from '@pcu/utils'
14
+ import inquirer from 'inquirer'
15
+ import { StyledText } from '../themes/colorTheme.js'
16
+ import { cliOutput } from '../utils/cliOutput.js'
17
+ import { handleCommandError } from '../utils/commandHelpers.js'
18
+
19
+ export interface RollbackCommandOptions {
20
+ workspace?: string
21
+ list?: boolean
22
+ latest?: boolean
23
+ select?: boolean
24
+ deleteAll?: boolean
25
+ verbose?: boolean
26
+ color?: boolean
27
+ }
28
+
29
+ export class RollbackCommand {
30
+ private readonly backupService: BackupService
31
+
32
+ constructor() {
33
+ this.backupService = new BackupService({ maxBackups: 10 })
34
+ }
35
+
36
+ /**
37
+ * Execute the rollback command
38
+ */
39
+ async execute(options: RollbackCommandOptions = {}): Promise<void> {
40
+ try {
41
+ const workspacePath = options.workspace || process.cwd()
42
+ const workspaceConfigPath = path.join(workspacePath, 'pnpm-workspace.yaml')
43
+
44
+ // List backups
45
+ if (options.list) {
46
+ await this.listBackups(workspaceConfigPath, options.verbose)
47
+ return
48
+ }
49
+
50
+ // Delete all backups
51
+ if (options.deleteAll) {
52
+ await this.deleteAllBackups(workspaceConfigPath)
53
+ return
54
+ }
55
+
56
+ // Restore from latest backup
57
+ if (options.latest) {
58
+ await this.restoreLatest(workspaceConfigPath)
59
+ return
60
+ }
61
+
62
+ // Interactive selection (default behavior if no flags)
63
+ await this.interactiveRestore(workspaceConfigPath)
64
+ } catch (error) {
65
+ handleCommandError(error, {
66
+ verbose: options.verbose,
67
+ errorMessage: 'Rollback command failed',
68
+ context: { options },
69
+ })
70
+ throw error
71
+ }
72
+ }
73
+
74
+ /**
75
+ * List all available backups
76
+ */
77
+ private async listBackups(workspaceConfigPath: string, verbose?: boolean): Promise<void> {
78
+ const backups = await this.backupService.listBackups(workspaceConfigPath)
79
+
80
+ if (backups.length === 0) {
81
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.noBackups')))
82
+ cliOutput.print(StyledText.muted(t('command.rollback.createBackupHint')))
83
+ return
84
+ }
85
+
86
+ cliOutput.print(
87
+ StyledText.info(`\n📋 ${t('command.rollback.availableBackups', { count: backups.length })}`)
88
+ )
89
+ cliOutput.print(StyledText.muted('─'.repeat(60)))
90
+
91
+ for (let i = 0; i < backups.length; i++) {
92
+ const backup = backups[i]
93
+ if (!backup) continue
94
+
95
+ const sizeKB = (backup.size / 1024).toFixed(2)
96
+ const isLatest = i === 0 ? StyledText.success(' (latest)') : ''
97
+
98
+ cliOutput.print(` ${StyledText.accent(`[${i + 1}]`)} ${backup.formattedTime}${isLatest}`)
99
+
100
+ if (verbose) {
101
+ cliOutput.print(StyledText.muted(` Path: ${backup.path}`))
102
+ cliOutput.print(StyledText.muted(` Size: ${sizeKB} KB`))
103
+ }
104
+ }
105
+
106
+ cliOutput.print(StyledText.muted('─'.repeat(60)))
107
+ cliOutput.print(StyledText.muted(t('command.rollback.restoreHint')))
108
+ }
109
+
110
+ /**
111
+ * QUAL-007: Common restore execution logic
112
+ * Performs the actual restore operation after user confirmation
113
+ */
114
+ private async executeRestore(
115
+ workspaceConfigPath: string,
116
+ backup: BackupInfo,
117
+ promptMessage: string
118
+ ): Promise<void> {
119
+ cliOutput.print(StyledText.success(` ✓ ${t('command.rollback.autoBackupNote')}`))
120
+
121
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>({
122
+ type: 'confirm',
123
+ name: 'confirmed',
124
+ message: promptMessage,
125
+ default: false,
126
+ })
127
+
128
+ if (!confirmed) {
129
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.cancelled')))
130
+ return
131
+ }
132
+
133
+ const preRestoreBackupPath = await this.backupService.restoreFromBackup(
134
+ workspaceConfigPath,
135
+ backup.path
136
+ )
137
+
138
+ // Verify the restored file
139
+ const verification = await this.backupService.verifyRestoredFile(workspaceConfigPath)
140
+ await this.displayVerificationResult(verification, preRestoreBackupPath)
141
+ }
142
+
143
+ /**
144
+ * Restore from the latest backup
145
+ */
146
+ private async restoreLatest(workspaceConfigPath: string): Promise<void> {
147
+ const backups = await this.backupService.listBackups(workspaceConfigPath)
148
+
149
+ if (backups.length === 0) {
150
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.noBackups')))
151
+ return
152
+ }
153
+
154
+ const latestBackup = backups[0]
155
+ if (!latestBackup) {
156
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.noBackups')))
157
+ return
158
+ }
159
+
160
+ cliOutput.print(StyledText.info(`\n🔄 ${t('command.rollback.restoringLatest')}`))
161
+ cliOutput.print(
162
+ StyledText.muted(` ${t('command.rollback.from')}: ${latestBackup.formattedTime}`)
163
+ )
164
+
165
+ await this.executeRestore(
166
+ workspaceConfigPath,
167
+ latestBackup,
168
+ t('command.rollback.confirmRestore')
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Display rollback verification results
174
+ */
175
+ private async displayVerificationResult(
176
+ verification: Awaited<ReturnType<BackupService['verifyRestoredFile']>> | undefined,
177
+ preRestoreBackupPath: string
178
+ ): Promise<void> {
179
+ if (!verification) {
180
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.verification.skipped')))
181
+ cliOutput.print(
182
+ StyledText.muted(
183
+ t('command.rollback.preRestoreBackupCreated', { path: preRestoreBackupPath })
184
+ )
185
+ )
186
+ cliOutput.print(StyledText.muted(t('command.rollback.runPnpmInstall')))
187
+ return
188
+ }
189
+
190
+ if (verification.success) {
191
+ cliOutput.print(StyledText.iconSuccess(t('command.rollback.success')))
192
+ cliOutput.print(StyledText.success(` ✓ ${t('command.rollback.verification.validYaml')}`))
193
+ cliOutput.print(
194
+ StyledText.success(
195
+ ` ✓ ${t('command.rollback.verification.catalogsFound', { count: verification.catalogs.length })}`
196
+ )
197
+ )
198
+ if (verification.catalogs.length > 0) {
199
+ cliOutput.print(
200
+ StyledText.muted(
201
+ ` ${t('command.rollback.verification.catalogs')}: ${verification.catalogs.join(', ')}`
202
+ )
203
+ )
204
+ }
205
+ cliOutput.print(
206
+ StyledText.muted(
207
+ ` ${t('command.rollback.verification.dependencies', { count: verification.dependencyCount })}`
208
+ )
209
+ )
210
+ } else {
211
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.verification.warning')))
212
+ if (!verification.isValidYaml) {
213
+ cliOutput.print(StyledText.error(` ✗ ${t('command.rollback.verification.invalidYaml')}`))
214
+ }
215
+ if (!verification.hasCatalogStructure) {
216
+ cliOutput.print(StyledText.warning(` ⚠ ${t('command.rollback.verification.noCatalogs')}`))
217
+ }
218
+ if (verification.errorMessage) {
219
+ cliOutput.print(StyledText.muted(` ${verification.errorMessage}`))
220
+ }
221
+ }
222
+
223
+ cliOutput.print(
224
+ StyledText.muted(
225
+ t('command.rollback.preRestoreBackupCreated', { path: preRestoreBackupPath })
226
+ )
227
+ )
228
+ cliOutput.print(StyledText.muted(t('command.rollback.safetyNote')))
229
+ cliOutput.print(StyledText.muted(t('command.rollback.runPnpmInstall')))
230
+ }
231
+
232
+ /**
233
+ * Interactive backup selection and restore
234
+ */
235
+ private async interactiveRestore(workspaceConfigPath: string): Promise<void> {
236
+ const backups = await this.backupService.listBackups(workspaceConfigPath)
237
+
238
+ if (backups.length === 0) {
239
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.noBackups')))
240
+ cliOutput.print(StyledText.muted(t('command.rollback.createBackupHint')))
241
+ return
242
+ }
243
+
244
+ cliOutput.print(StyledText.info(`\n🔄 ${t('command.rollback.selectBackup')}`))
245
+
246
+ const choices = backups.map((backup, index) => ({
247
+ name: `${backup.formattedTime}${index === 0 ? ' (latest)' : ''} - ${(backup.size / 1024).toFixed(2)} KB`,
248
+ value: backup,
249
+ }))
250
+
251
+ const { selectedBackup } = await inquirer.prompt<{ selectedBackup: BackupInfo }>({
252
+ type: 'list',
253
+ name: 'selectedBackup',
254
+ message: t('command.rollback.chooseBackup'),
255
+ choices,
256
+ })
257
+
258
+ cliOutput.print(StyledText.warning(`\n⚠️ ${t('command.rollback.warning')}`))
259
+ cliOutput.print(
260
+ StyledText.muted(
261
+ ` ${t('command.rollback.willRestore', { time: selectedBackup.formattedTime })}`
262
+ )
263
+ )
264
+
265
+ await this.executeRestore(
266
+ workspaceConfigPath,
267
+ selectedBackup,
268
+ t('command.rollback.confirmRestore')
269
+ )
270
+ }
271
+
272
+ /**
273
+ * Delete all backups
274
+ */
275
+ private async deleteAllBackups(workspaceConfigPath: string): Promise<void> {
276
+ const backups = await this.backupService.listBackups(workspaceConfigPath)
277
+
278
+ if (backups.length === 0) {
279
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.noBackups')))
280
+ return
281
+ }
282
+
283
+ cliOutput.print(
284
+ StyledText.warning(`\n⚠️ ${t('command.rollback.deleteWarning', { count: backups.length })}`)
285
+ )
286
+
287
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>({
288
+ type: 'confirm',
289
+ name: 'confirmed',
290
+ message: t('command.rollback.confirmDelete'),
291
+ default: false,
292
+ })
293
+
294
+ if (!confirmed) {
295
+ cliOutput.print(StyledText.iconWarning(t('command.rollback.cancelled')))
296
+ return
297
+ }
298
+
299
+ const deleted = await this.backupService.deleteAllBackups(workspaceConfigPath)
300
+ cliOutput.print(
301
+ StyledText.iconSuccess(t('command.rollback.deletedBackups', { count: deleted }))
302
+ )
303
+ }
304
+
305
+ /**
306
+ * Get command help text
307
+ */
308
+ static getHelpText(): string {
309
+ return `
310
+ Rollback catalog updates to a previous state
311
+
312
+ Usage:
313
+ pcu rollback [options]
314
+
315
+ Options:
316
+ --workspace <path> Workspace directory (default: current directory)
317
+ -l, --list List available backups
318
+ --latest Restore from the most recent backup
319
+ --delete-all Delete all backups
320
+ --verbose Show detailed information
321
+
322
+ Examples:
323
+ pcu rollback # Interactive backup selection
324
+ pcu rollback --list # List all available backups
325
+ pcu rollback --latest # Restore from the most recent backup
326
+ pcu rollback --delete-all # Delete all backups
327
+
328
+ Notes:
329
+ - Backups are automatically created when using 'pcu update -b'
330
+ - Before restoring, a new backup of the current state is created
331
+ - After rollback, run 'pnpm install' to sync the lock file
332
+ `
333
+ }
334
+ }