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
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Interactive Options Collector
3
+ *
4
+ * Minimal interactive prompts - only ask what's truly necessary.
5
+ * Philosophy: defaults over questions, action over configuration.
6
+ */
7
+
8
+ import * as p from '@clack/prompts'
9
+ import { CommandExitError, t } from '@pcu/utils'
10
+ import chalk from 'chalk'
11
+ import { commonSchemas, mergeOptions, type OptionsSchema } from './optionUtils.js'
12
+
13
+ const theme = {
14
+ primary: chalk.cyan,
15
+ success: chalk.green,
16
+ muted: chalk.gray,
17
+ }
18
+
19
+ /**
20
+ * Handle user cancellation
21
+ *
22
+ * QUAL-001: Use CommandExitError instead of direct process.exit()
23
+ * for better testability and consistent error handling
24
+ */
25
+ function handleCancel(): never {
26
+ p.cancel(t('interactive.cancelled'))
27
+ throw CommandExitError.success()
28
+ }
29
+
30
+ /**
31
+ * Common choices for select prompts
32
+ */
33
+ export const choices = {
34
+ target: [
35
+ { value: 'latest', label: 'latest', hint: t('interactive.choice.target.latestHint') },
36
+ { value: 'minor', label: 'minor', hint: t('interactive.choice.target.minorHint') },
37
+ { value: 'patch', label: 'patch', hint: t('interactive.choice.target.patchHint') },
38
+ ],
39
+ theme: [
40
+ { value: 'default', label: t('prompt.themeDefault') },
41
+ { value: 'modern', label: t('prompt.themeModern') },
42
+ { value: 'minimal', label: t('prompt.themeMinimal') },
43
+ { value: 'neon', label: t('prompt.themeNeon') },
44
+ ],
45
+ format: [
46
+ { value: 'table', label: 'table' },
47
+ { value: 'json', label: 'json' },
48
+ { value: 'yaml', label: 'yaml' },
49
+ { value: 'minimal', label: 'minimal' },
50
+ ],
51
+ severity: [
52
+ { value: undefined, label: 'all' },
53
+ { value: 'critical', label: 'critical' },
54
+ { value: 'high', label: 'high' },
55
+ { value: 'medium', label: 'medium' },
56
+ { value: 'low', label: 'low' },
57
+ ],
58
+ analysisType: [
59
+ { value: 'impact', label: 'impact' },
60
+ { value: 'security', label: 'security' },
61
+ { value: 'compatibility', label: 'compatibility' },
62
+ { value: 'recommend', label: 'recommend' },
63
+ ],
64
+ provider: [
65
+ { value: 'auto', label: 'auto' },
66
+ { value: 'claude', label: 'claude' },
67
+ { value: 'gemini', label: 'gemini' },
68
+ { value: 'codex', label: 'codex' },
69
+ ],
70
+ }
71
+
72
+ /**
73
+ * Commander Option interface for type safety
74
+ */
75
+ interface CommanderOption {
76
+ attributeName: () => string
77
+ }
78
+
79
+ /**
80
+ * Commander Command interface for type safety
81
+ */
82
+ interface CommanderCommand {
83
+ options?: readonly CommanderOption[]
84
+ getOptionValueSource?: (key: string) => string | undefined
85
+ }
86
+
87
+ /**
88
+ * Check if any meaningful options were provided by the user (not default values)
89
+ */
90
+ export function hasProvidedOptions(
91
+ options: Record<string, unknown>,
92
+ command?: CommanderCommand,
93
+ excludeKeys: string[] = []
94
+ ): boolean {
95
+ const allKeys = command?.options?.map((opt) => opt.attributeName()) ?? Object.keys(options)
96
+ const keysToCheck = allKeys.filter((key) => !excludeKeys.includes(key))
97
+
98
+ return keysToCheck.some((key) => {
99
+ const value = options[key]
100
+ if (value === undefined || value === null) return false
101
+
102
+ if (command?.getOptionValueSource) {
103
+ const source = command.getOptionValueSource(key)
104
+ if (source === 'default' || source === undefined) {
105
+ return false
106
+ }
107
+ }
108
+
109
+ if (typeof value === 'boolean') return true
110
+ if (typeof value === 'string') return value.trim() !== ''
111
+ if (Array.isArray(value)) return value.length > 0
112
+ return true
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Option schemas for each command
118
+ */
119
+ const checkOptionsSchema = {
120
+ catalog: commonSchemas.catalog,
121
+ format: commonSchemas.format,
122
+ target: commonSchemas.target,
123
+ prerelease: commonSchemas.prerelease,
124
+ include: commonSchemas.include,
125
+ exclude: commonSchemas.exclude,
126
+ exitCode: { type: 'boolean' as const, default: false },
127
+ } satisfies OptionsSchema<CheckOptions>
128
+
129
+ type CheckOptions = {
130
+ catalog?: string
131
+ format: string
132
+ target: string
133
+ prerelease: boolean
134
+ include: string[]
135
+ exclude: string[]
136
+ exitCode: boolean
137
+ }
138
+
139
+ type UpdateOptions = {
140
+ catalog?: string
141
+ format: string
142
+ target: string
143
+ interactive: boolean
144
+ dryRun: boolean
145
+ force: boolean
146
+ prerelease: boolean
147
+ include: string[]
148
+ exclude: string[]
149
+ createBackup: boolean
150
+ ai: boolean
151
+ provider: string
152
+ analysisType: string
153
+ install: boolean
154
+ changelog: boolean
155
+ }
156
+
157
+ const updateOptionsSchema = {
158
+ catalog: commonSchemas.catalog,
159
+ format: commonSchemas.format,
160
+ target: commonSchemas.target,
161
+ interactive: { type: 'boolean' as const, default: true },
162
+ dryRun: commonSchemas.dryRun,
163
+ force: commonSchemas.force,
164
+ prerelease: commonSchemas.prerelease,
165
+ include: commonSchemas.include,
166
+ exclude: commonSchemas.exclude,
167
+ createBackup: { type: 'boolean' as const, default: true },
168
+ ai: commonSchemas.ai,
169
+ provider: commonSchemas.provider,
170
+ analysisType: commonSchemas.analysisType,
171
+ install: { type: 'boolean' as const, default: true },
172
+ changelog: { type: 'boolean' as const, default: false },
173
+ } satisfies OptionsSchema<UpdateOptions>
174
+
175
+ type WorkspaceOptions = {
176
+ validate: boolean
177
+ stats: boolean
178
+ format: string
179
+ }
180
+
181
+ const workspaceOptionsSchema = {
182
+ validate: { type: 'boolean' as const, default: false },
183
+ stats: { type: 'boolean' as const, default: true },
184
+ format: commonSchemas.format,
185
+ } satisfies OptionsSchema<WorkspaceOptions>
186
+
187
+ type SecurityOptions = {
188
+ format: string
189
+ audit: boolean
190
+ fixVulns: boolean
191
+ severity?: string
192
+ includeDev: boolean
193
+ snyk: boolean
194
+ }
195
+
196
+ const securityOptionsSchema = {
197
+ format: commonSchemas.format,
198
+ audit: { type: 'boolean' as const, default: true },
199
+ fixVulns: { type: 'boolean' as const, default: false },
200
+ severity: commonSchemas.catalog, // optional-string
201
+ includeDev: { type: 'boolean' as const, default: false },
202
+ snyk: { type: 'boolean' as const, default: false },
203
+ } satisfies OptionsSchema<SecurityOptions>
204
+
205
+ type InitOptions = {
206
+ force: boolean
207
+ full: boolean
208
+ createWorkspace: boolean
209
+ }
210
+
211
+ const initOptionsSchema = {
212
+ force: commonSchemas.force,
213
+ full: { type: 'boolean' as const, default: false },
214
+ createWorkspace: { type: 'boolean' as const, default: true },
215
+ } satisfies OptionsSchema<InitOptions>
216
+
217
+ /**
218
+ * Interactive Options Collector - Minimal prompts, sensible defaults
219
+ */
220
+ export class InteractiveOptionsCollector {
221
+ /**
222
+ * Check command - no interaction needed, just use defaults
223
+ */
224
+ async collectCheckOptions(existingOptions: Record<string, unknown> = {}): Promise<CheckOptions> {
225
+ return mergeOptions(existingOptions, checkOptionsSchema)
226
+ }
227
+
228
+ /**
229
+ * Update command - only ask for target version strategy
230
+ */
231
+ async collectUpdateOptions(
232
+ existingOptions: Record<string, unknown> = {}
233
+ ): Promise<UpdateOptions> {
234
+ p.intro(theme.primary('pcu update'))
235
+
236
+ // Only ask for target if not already provided
237
+ let target = existingOptions.target as string
238
+ if (!target) {
239
+ const result = await p.select({
240
+ message: t('interactive.update.updateTarget'),
241
+ options: choices.target,
242
+ initialValue: 'latest',
243
+ })
244
+ if (p.isCancel(result)) handleCancel()
245
+ target = result as string
246
+ }
247
+
248
+ p.outro(theme.success(t('interactive.update.ready')))
249
+
250
+ // Merge with schema defaults, override target with user selection
251
+ return { ...mergeOptions(existingOptions, updateOptionsSchema), target }
252
+ }
253
+
254
+ /**
255
+ * Analyze command - only ask for package name if not provided
256
+ */
257
+ async collectAnalyzeOptions(existingOptions: Record<string, unknown> = {}): Promise<{
258
+ packageName: string
259
+ version?: string
260
+ catalog?: string
261
+ format: string
262
+ ai: boolean
263
+ provider: string
264
+ analysisType: string
265
+ }> {
266
+ p.intro(theme.primary('pcu analyze'))
267
+
268
+ // Only ask for package name if not provided
269
+ let packageName = existingOptions.packageName as string
270
+ if (!packageName) {
271
+ const result = await p.text({
272
+ message: t('interactive.analyze.packageName'),
273
+ placeholder: 'lodash, react, ...',
274
+ validate: (value) => {
275
+ if (!value.trim()) return t('interactive.analyze.packageNameRequired')
276
+ return undefined
277
+ },
278
+ })
279
+ if (p.isCancel(result)) handleCancel()
280
+ packageName = result as string
281
+ }
282
+
283
+ p.outro(theme.success(t('interactive.analyze.ready')))
284
+
285
+ return {
286
+ packageName,
287
+ version: (existingOptions.version as string) || undefined,
288
+ catalog: (existingOptions.catalog as string) || undefined,
289
+ format: (existingOptions.format as string) || 'table',
290
+ ai: (existingOptions.ai as boolean) ?? true,
291
+ provider: (existingOptions.provider as string) || 'auto',
292
+ analysisType: (existingOptions.analysisType as string) || 'impact',
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Workspace command - no interaction needed, use defaults
298
+ */
299
+ async collectWorkspaceOptions(
300
+ existingOptions: Record<string, unknown> = {}
301
+ ): Promise<WorkspaceOptions> {
302
+ return mergeOptions(existingOptions, workspaceOptionsSchema)
303
+ }
304
+
305
+ /**
306
+ * Theme command - ask which theme to set
307
+ */
308
+ async collectThemeOptions(existingOptions: Record<string, unknown> = {}): Promise<{
309
+ set?: string
310
+ list: boolean
311
+ }> {
312
+ if (existingOptions.list || existingOptions.set) {
313
+ return {
314
+ set: existingOptions.set as string,
315
+ list: (existingOptions.list as boolean) ?? false,
316
+ }
317
+ }
318
+
319
+ p.intro(theme.primary('pcu theme'))
320
+
321
+ const result = await p.select({
322
+ message: t('interactive.theme.choose'),
323
+ options: choices.theme,
324
+ })
325
+ if (p.isCancel(result)) handleCancel()
326
+
327
+ // Don't call p.outro() here - let the command display its own output
328
+ return { set: result as string, list: false }
329
+ }
330
+
331
+ /**
332
+ * Security command - no interaction needed, defaults to audit
333
+ */
334
+ async collectSecurityOptions(
335
+ existingOptions: Record<string, unknown> = {}
336
+ ): Promise<SecurityOptions> {
337
+ return mergeOptions(existingOptions, securityOptionsSchema)
338
+ }
339
+
340
+ /**
341
+ * Init command - no interaction needed, use quick mode
342
+ */
343
+ async collectInitOptions(existingOptions: Record<string, unknown> = {}): Promise<InitOptions> {
344
+ return mergeOptions(existingOptions, initOptionsSchema)
345
+ }
346
+
347
+ /**
348
+ * Rollback command - ask what action to take
349
+ */
350
+ async collectRollbackOptions(existingOptions: Record<string, unknown> = {}): Promise<{
351
+ list: boolean
352
+ latest: boolean
353
+ deleteAll: boolean
354
+ }> {
355
+ // Skip if action already specified
356
+ if (existingOptions.list || existingOptions.latest || existingOptions.deleteAll) {
357
+ return {
358
+ list: (existingOptions.list as boolean) ?? false,
359
+ latest: (existingOptions.latest as boolean) ?? false,
360
+ deleteAll: (existingOptions.deleteAll as boolean) ?? false,
361
+ }
362
+ }
363
+
364
+ p.intro(theme.primary('pcu rollback'))
365
+
366
+ const result = await p.select({
367
+ message: t('interactive.rollback.action'),
368
+ options: [
369
+ { value: 'list', label: t('interactive.rollback.action.list') },
370
+ { value: 'latest', label: t('interactive.rollback.action.latest') },
371
+ { value: 'deleteAll', label: t('interactive.rollback.action.deleteAll') },
372
+ ],
373
+ })
374
+ if (p.isCancel(result)) handleCancel()
375
+
376
+ return {
377
+ list: result === 'list',
378
+ latest: result === 'latest',
379
+ deleteAll: result === 'deleteAll',
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Singleton instance for convenience
386
+ */
387
+ export const interactiveOptionsCollector = new InteractiveOptionsCollector()