pnpm-catalog-updates 1.0.3 → 1.1.2
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,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()
|