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.
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,475 @@
1
+ /**
2
+ * Graph Command
3
+ *
4
+ * CLI command to visualize catalog dependency relationships.
5
+ * Supports multiple output formats: text, mermaid, dot, json.
6
+ */
7
+
8
+ import type { WorkspaceService } from '@pcu/core'
9
+ import { CommandExitError } from '@pcu/utils'
10
+ import chalk from 'chalk'
11
+ import { cliOutput } from '../utils/cliOutput.js'
12
+ import { errorsOnly, validateGraphOptions } from '../validators/index.js'
13
+
14
+ export type GraphFormat = 'text' | 'mermaid' | 'dot' | 'json'
15
+ export type GraphType = 'catalog' | 'package' | 'full'
16
+
17
+ export interface GraphCommandOptions {
18
+ workspace?: string
19
+ format?: GraphFormat
20
+ type?: GraphType
21
+ catalog?: string
22
+ verbose?: boolean
23
+ color?: boolean
24
+ }
25
+
26
+ interface GraphNode {
27
+ id: string
28
+ type: 'catalog' | 'package' | 'dependency'
29
+ name: string
30
+ metadata?: Record<string, unknown>
31
+ }
32
+
33
+ interface GraphEdge {
34
+ source: string
35
+ target: string
36
+ type: 'contains' | 'uses' | 'depends'
37
+ label?: string
38
+ }
39
+
40
+ interface DependencyGraph {
41
+ nodes: GraphNode[]
42
+ edges: GraphEdge[]
43
+ }
44
+
45
+ export class GraphCommand {
46
+ constructor(private readonly workspaceService: WorkspaceService) {}
47
+
48
+ /**
49
+ * Execute the graph command
50
+ */
51
+ async execute(options: GraphCommandOptions = {}): Promise<void> {
52
+ const format = options.format || 'text'
53
+ const graphType = options.type || 'catalog'
54
+ const useColor = options.color !== false
55
+
56
+ // Build the dependency graph
57
+ const graph = await this.buildGraph(options.workspace, graphType, options.catalog)
58
+
59
+ // Output in requested format
60
+ const output = this.formatGraph(graph, format, useColor)
61
+ cliOutput.print(output)
62
+
63
+ throw CommandExitError.success()
64
+ }
65
+
66
+ /**
67
+ * Build the dependency graph from workspace data
68
+ */
69
+ private async buildGraph(
70
+ workspacePath?: string,
71
+ graphType: GraphType = 'catalog',
72
+ catalogFilter?: string
73
+ ): Promise<DependencyGraph> {
74
+ const nodes: GraphNode[] = []
75
+ const edges: GraphEdge[] = []
76
+
77
+ // PERF-001: Parallel fetch for independent async operations
78
+ const [catalogs, packages] = await Promise.all([
79
+ this.workspaceService.getCatalogs(workspacePath),
80
+ this.workspaceService.getPackages(workspacePath),
81
+ ])
82
+
83
+ // Filter catalogs if specified
84
+ const filteredCatalogs = catalogFilter
85
+ ? catalogs.filter((c) => c.name === catalogFilter)
86
+ : catalogs
87
+
88
+ if (graphType === 'catalog' || graphType === 'full') {
89
+ // Add catalog nodes
90
+ for (const catalog of filteredCatalogs) {
91
+ nodes.push({
92
+ id: `catalog:${catalog.name}`,
93
+ type: 'catalog',
94
+ name: catalog.name,
95
+ metadata: {
96
+ packageCount: catalog.packageCount,
97
+ mode: catalog.mode,
98
+ },
99
+ })
100
+
101
+ // Add dependency nodes for catalog entries
102
+ for (const pkgName of catalog.packages) {
103
+ const depId = `dep:${pkgName}`
104
+ if (!nodes.find((n) => n.id === depId)) {
105
+ nodes.push({
106
+ id: depId,
107
+ type: 'dependency',
108
+ name: pkgName,
109
+ })
110
+ }
111
+
112
+ edges.push({
113
+ source: `catalog:${catalog.name}`,
114
+ target: depId,
115
+ type: 'contains',
116
+ })
117
+ }
118
+ }
119
+ }
120
+
121
+ if (graphType === 'package' || graphType === 'full') {
122
+ // Add package nodes
123
+ for (const pkg of packages) {
124
+ nodes.push({
125
+ id: `package:${pkg.name}`,
126
+ type: 'package',
127
+ name: pkg.name,
128
+ metadata: {
129
+ path: pkg.path,
130
+ dependencyCount: pkg.dependencies.length,
131
+ },
132
+ })
133
+
134
+ // Add edges for catalog references
135
+ for (const ref of pkg.catalogReferences) {
136
+ if (!catalogFilter || ref.catalogName === catalogFilter) {
137
+ edges.push({
138
+ source: `package:${pkg.name}`,
139
+ target: `catalog:${ref.catalogName}`,
140
+ type: 'uses',
141
+ label: ref.dependencyType,
142
+ })
143
+ }
144
+ }
145
+
146
+ // Add edges for dependencies (in full mode)
147
+ if (graphType === 'full') {
148
+ for (const dep of pkg.dependencies) {
149
+ if (dep.isCatalogReference) {
150
+ // Link to the dependency node if it exists
151
+ const depId = `dep:${dep.name}`
152
+ if (nodes.find((n) => n.id === depId)) {
153
+ edges.push({
154
+ source: `package:${pkg.name}`,
155
+ target: depId,
156
+ type: 'depends',
157
+ label: dep.type,
158
+ })
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ return { nodes, edges }
167
+ }
168
+
169
+ /**
170
+ * Format the graph for output
171
+ */
172
+ private formatGraph(graph: DependencyGraph, format: GraphFormat, useColor: boolean): string {
173
+ switch (format) {
174
+ case 'mermaid':
175
+ return this.formatMermaid(graph)
176
+ case 'dot':
177
+ return this.formatDot(graph)
178
+ case 'json':
179
+ return this.formatJson(graph)
180
+ default:
181
+ return this.formatText(graph, useColor)
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Format as text-based tree
187
+ */
188
+ private formatText(graph: DependencyGraph, useColor: boolean): string {
189
+ const lines: string[] = []
190
+ const c = useColor
191
+ ? chalk
192
+ : {
193
+ cyan: (s: string) => s,
194
+ green: (s: string) => s,
195
+ yellow: (s: string) => s,
196
+ gray: (s: string) => s,
197
+ bold: (s: string) => s,
198
+ }
199
+
200
+ lines.push(c.bold('Catalog Dependency Graph'))
201
+ lines.push('')
202
+
203
+ // Group by catalogs
204
+ const catalogNodes = graph.nodes.filter((n) => n.type === 'catalog')
205
+ const packageNodes = graph.nodes.filter((n) => n.type === 'package')
206
+
207
+ if (catalogNodes.length > 0) {
208
+ lines.push(c.cyan('📦 Catalogs:'))
209
+
210
+ for (const catalog of catalogNodes) {
211
+ const meta = catalog.metadata || {}
212
+ lines.push(
213
+ ` ${c.bold(catalog.name)} (${meta.packageCount || 0} packages, mode: ${meta.mode || 'default'})`
214
+ )
215
+
216
+ // Find dependencies contained in this catalog
217
+ const containedEdges = graph.edges.filter(
218
+ (e) => e.source === catalog.id && e.type === 'contains'
219
+ )
220
+ for (const edge of containedEdges) {
221
+ const depNode = graph.nodes.find((n) => n.id === edge.target)
222
+ if (depNode) {
223
+ lines.push(` ├─ ${c.yellow(depNode.name)}`)
224
+ }
225
+ }
226
+
227
+ // Find packages using this catalog
228
+ const usingEdges = graph.edges.filter((e) => e.target === catalog.id && e.type === 'uses')
229
+ if (usingEdges.length > 0) {
230
+ lines.push(` ${c.gray('Used by packages:')}`)
231
+ for (const edge of usingEdges) {
232
+ const pkgNode = graph.nodes.find((n) => n.id === edge.source)
233
+ if (pkgNode) {
234
+ lines.push(` └─ ${c.green(pkgNode.name)} (${edge.label || 'dependencies'})`)
235
+ }
236
+ }
237
+ }
238
+ lines.push('')
239
+ }
240
+ }
241
+
242
+ if (packageNodes.length > 0 && catalogNodes.length === 0) {
243
+ lines.push(c.green('📁 Packages:'))
244
+
245
+ for (const pkg of packageNodes) {
246
+ const meta = pkg.metadata || {}
247
+ lines.push(` ${c.bold(pkg.name)}`)
248
+ lines.push(` ${c.gray(`Path: ${meta.path || 'unknown'}`)}`)
249
+
250
+ // Find catalog references
251
+ const usesEdges = graph.edges.filter((e) => e.source === pkg.id && e.type === 'uses')
252
+ if (usesEdges.length > 0) {
253
+ lines.push(` ${c.gray('Uses catalogs:')}`)
254
+ for (const edge of usesEdges) {
255
+ const catalogNode = graph.nodes.find((n) => n.id === edge.target)
256
+ if (catalogNode) {
257
+ lines.push(` └─ ${c.cyan(catalogNode.name)} (${edge.label || 'dependencies'})`)
258
+ }
259
+ }
260
+ }
261
+ lines.push('')
262
+ }
263
+ }
264
+
265
+ // Summary
266
+ lines.push(c.gray('─'.repeat(40)))
267
+ lines.push(`Nodes: ${graph.nodes.length} | Edges: ${graph.edges.length}`)
268
+
269
+ return lines.join('\n')
270
+ }
271
+
272
+ /**
273
+ * Format as Mermaid diagram
274
+ */
275
+ private formatMermaid(graph: DependencyGraph): string {
276
+ const lines: string[] = []
277
+ lines.push('```mermaid')
278
+ lines.push('graph TD')
279
+
280
+ // Add nodes with styling
281
+ for (const node of graph.nodes) {
282
+ const shape = this.getMermaidShape(node.type)
283
+ lines.push(` ${this.sanitizeId(node.id)}${shape[0]}"${node.name}"${shape[1]}`)
284
+ }
285
+
286
+ lines.push('')
287
+
288
+ // Add edges
289
+ for (const edge of graph.edges) {
290
+ const arrow = this.getMermaidArrow(edge.type)
291
+ const label = edge.label ? `|${edge.label}|` : ''
292
+ lines.push(
293
+ ` ${this.sanitizeId(edge.source)} ${arrow}${label} ${this.sanitizeId(edge.target)}`
294
+ )
295
+ }
296
+
297
+ lines.push('')
298
+
299
+ // Add styling
300
+ lines.push(' classDef catalog fill:#e1f5fe,stroke:#01579b')
301
+ lines.push(' classDef package fill:#e8f5e9,stroke:#2e7d32')
302
+ lines.push(' classDef dependency fill:#fff3e0,stroke:#e65100')
303
+
304
+ // Apply classes
305
+ const catalogIds = graph.nodes
306
+ .filter((n) => n.type === 'catalog')
307
+ .map((n) => this.sanitizeId(n.id))
308
+ const packageIds = graph.nodes
309
+ .filter((n) => n.type === 'package')
310
+ .map((n) => this.sanitizeId(n.id))
311
+ const depIds = graph.nodes
312
+ .filter((n) => n.type === 'dependency')
313
+ .map((n) => this.sanitizeId(n.id))
314
+
315
+ if (catalogIds.length > 0) lines.push(` class ${catalogIds.join(',')} catalog`)
316
+ if (packageIds.length > 0) lines.push(` class ${packageIds.join(',')} package`)
317
+ if (depIds.length > 0) lines.push(` class ${depIds.join(',')} dependency`)
318
+
319
+ lines.push('```')
320
+
321
+ return lines.join('\n')
322
+ }
323
+
324
+ /**
325
+ * Format as DOT (Graphviz)
326
+ */
327
+ private formatDot(graph: DependencyGraph): string {
328
+ const lines: string[] = []
329
+ lines.push('digraph CatalogDependencies {')
330
+ lines.push(' rankdir=TB;')
331
+ lines.push(' node [fontname="Arial"];')
332
+ lines.push('')
333
+
334
+ // Add nodes with styling
335
+ for (const node of graph.nodes) {
336
+ const style = this.getDotStyle(node.type)
337
+ lines.push(` "${node.id}" [label="${node.name}" ${style}];`)
338
+ }
339
+
340
+ lines.push('')
341
+
342
+ // Add edges
343
+ for (const edge of graph.edges) {
344
+ const style = this.getDotEdgeStyle(edge.type)
345
+ const label = edge.label ? ` label="${edge.label}"` : ''
346
+ lines.push(` "${edge.source}" -> "${edge.target}" [${style}${label}];`)
347
+ }
348
+
349
+ lines.push('}')
350
+
351
+ return lines.join('\n')
352
+ }
353
+
354
+ /**
355
+ * Format as JSON
356
+ */
357
+ private formatJson(graph: DependencyGraph): string {
358
+ return JSON.stringify(graph, null, 2)
359
+ }
360
+
361
+ /**
362
+ * Get Mermaid shape for node type
363
+ */
364
+ private getMermaidShape(type: string): [string, string] {
365
+ switch (type) {
366
+ case 'catalog':
367
+ return ['[/', '/]'] // Parallelogram
368
+ case 'package':
369
+ return ['([', '])'] // Stadium/pill shape
370
+ case 'dependency':
371
+ return ['((', '))'] // Circle
372
+ default:
373
+ return ['[', ']']
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Get Mermaid arrow for edge type
379
+ */
380
+ private getMermaidArrow(type: string): string {
381
+ switch (type) {
382
+ case 'contains':
383
+ return '--->'
384
+ case 'uses':
385
+ return '==>'
386
+ case 'depends':
387
+ return '-.->'
388
+ default:
389
+ return '-->'
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get DOT style for node type
395
+ */
396
+ private getDotStyle(type: string): string {
397
+ switch (type) {
398
+ case 'catalog':
399
+ return 'shape=box style=filled fillcolor="#e1f5fe"'
400
+ case 'package':
401
+ return 'shape=ellipse style=filled fillcolor="#e8f5e9"'
402
+ case 'dependency':
403
+ return 'shape=circle style=filled fillcolor="#fff3e0"'
404
+ default:
405
+ return ''
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Get DOT style for edge type
411
+ */
412
+ private getDotEdgeStyle(type: string): string {
413
+ switch (type) {
414
+ case 'contains':
415
+ return 'style=solid'
416
+ case 'uses':
417
+ return 'style=bold color=blue'
418
+ case 'depends':
419
+ return 'style=dashed'
420
+ default:
421
+ return ''
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Sanitize ID for Mermaid (remove special characters)
427
+ */
428
+ private sanitizeId(id: string): string {
429
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
430
+ }
431
+
432
+ /**
433
+ * Validate command options
434
+ * QUAL-002: Uses unified validator from validators/
435
+ */
436
+ static validateOptions(options: GraphCommandOptions): string[] {
437
+ return errorsOnly(validateGraphOptions)(options)
438
+ }
439
+
440
+ /**
441
+ * Get command help text
442
+ */
443
+ static getHelpText(): string {
444
+ return `
445
+ Visualize catalog dependency relationships
446
+
447
+ Usage:
448
+ pcu graph [options]
449
+
450
+ Options:
451
+ -f, --format <type> Output format: text, mermaid, dot, json (default: text)
452
+ -t, --type <type> Graph type: catalog, package, full (default: catalog)
453
+ --catalog <name> Filter by specific catalog
454
+ --verbose Show detailed information
455
+
456
+ Graph Types:
457
+ catalog Show catalogs and their contained dependencies
458
+ package Show packages and their catalog references
459
+ full Show complete dependency graph
460
+
461
+ Output Formats:
462
+ text Human-readable text tree
463
+ mermaid Mermaid diagram (for Markdown/documentation)
464
+ dot DOT format (for Graphviz)
465
+ json JSON structure (for programmatic use)
466
+
467
+ Examples:
468
+ pcu graph # Show catalog dependency tree
469
+ pcu graph --type full # Show complete dependency graph
470
+ pcu graph --format mermaid # Output as Mermaid diagram
471
+ pcu graph --catalog default # Show only default catalog
472
+ pcu graph --format dot > deps.dot && dot -Tpng deps.dot -o deps.png
473
+ `
474
+ }
475
+ }
@@ -7,8 +7,11 @@
7
7
 
8
8
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
9
9
  import { dirname, join } from 'node:path'
10
- import type { PackageFilterConfig } from '@pcu/utils'
10
+ import { CommandExitError, type PackageFilterConfig, t } from '@pcu/utils'
11
11
  import { StyledText, ThemeManager } from '../themes/colorTheme.js'
12
+ import { cliOutput } from '../utils/cliOutput.js'
13
+ import { handleCommandError } from '../utils/commandHelpers.js'
14
+ import { errorsOnly, validateInitOptions } from '../validators/index.js'
12
15
 
13
16
  export interface InitCommandOptions {
14
17
  workspace?: string
@@ -19,6 +22,18 @@ export interface InitCommandOptions {
19
22
  full?: boolean
20
23
  }
21
24
 
25
+ /**
26
+ * Workspace package.json structure
27
+ */
28
+ interface WorkspacePackageJson {
29
+ name: string
30
+ version: string
31
+ private: boolean
32
+ description: string
33
+ scripts: Record<string, string>
34
+ devDependencies: Record<string, string>
35
+ }
36
+
22
37
  export class InitCommand {
23
38
  /**
24
39
  * Execute the init command
@@ -34,10 +49,10 @@ export class InitCommand {
34
49
  const workspaceYamlPath = join(workspacePath, 'pnpm-workspace.yaml')
35
50
 
36
51
  if (options.verbose) {
37
- console.log(StyledText.iconInfo('Initializing PCU configuration'))
38
- console.log(StyledText.muted(`Workspace: ${workspacePath}`))
39
- console.log(StyledText.muted(`Config file: ${configPath}`))
40
- console.log('')
52
+ cliOutput.print(StyledText.iconInfo(t('command.init.creating')))
53
+ cliOutput.print(StyledText.muted(`${t('command.workspace.title')}: ${workspacePath}`))
54
+ cliOutput.print(StyledText.muted(t('command.init.configFileLabel', { path: configPath })))
55
+ cliOutput.print('')
41
56
  }
42
57
 
43
58
  // Check if this is a pnpm workspace
@@ -47,19 +62,19 @@ export class InitCommand {
47
62
 
48
63
  if (!isWorkspace && options.createWorkspace !== false) {
49
64
  if (options.verbose) {
50
- console.log(StyledText.iconWarning('PNPM workspace structure not detected'))
65
+ cliOutput.print(StyledText.iconWarning(t('warning.workspaceNotDetected')))
51
66
  if (!hasPackageJson) {
52
- console.log(StyledText.muted('Missing: package.json'))
67
+ cliOutput.print(StyledText.muted(t('command.init.missingPackageJson')))
53
68
  }
54
69
  if (!hasWorkspaceYaml) {
55
- console.log(StyledText.muted('Missing: pnpm-workspace.yaml'))
70
+ cliOutput.print(StyledText.muted(t('command.init.missingWorkspaceYaml')))
56
71
  }
57
- console.log('')
72
+ cliOutput.print('')
58
73
  }
59
74
 
60
75
  // Create workspace structure
61
76
  if (options.verbose) {
62
- console.log(StyledText.iconInfo('Creating PNPM workspace structure...'))
77
+ cliOutput.print(StyledText.iconInfo(t('command.init.creatingWorkspace')))
63
78
  }
64
79
 
65
80
  await this.createWorkspaceStructure(
@@ -70,17 +85,17 @@ export class InitCommand {
70
85
  )
71
86
 
72
87
  if (options.verbose) {
73
- console.log(StyledText.iconSuccess('PNPM workspace structure created'))
74
- console.log('')
88
+ cliOutput.print(StyledText.iconSuccess(t('command.init.workspaceCreated')))
89
+ cliOutput.print('')
75
90
  }
76
91
  }
77
92
 
78
93
  // Check if config file already exists
79
94
  if (existsSync(configPath) && !options.force) {
80
- console.log(StyledText.iconWarning('Configuration file already exists!'))
81
- console.log(StyledText.muted(`Found: ${configPath}`))
82
- console.log(StyledText.muted('Use --force to overwrite existing configuration'))
83
- process.exit(1)
95
+ cliOutput.print(StyledText.iconWarning(t('warning.configExists')))
96
+ cliOutput.print(StyledText.muted(t('command.init.foundLabel', { path: configPath })))
97
+ cliOutput.print(StyledText.muted(t('command.init.useForceOverwrite')))
98
+ throw CommandExitError.failure('Configuration file already exists')
84
99
  }
85
100
 
86
101
  // Create directory if it doesn't exist
@@ -96,24 +111,29 @@ export class InitCommand {
96
111
  writeFileSync(configPath, JSON.stringify(basicConfig, null, 2), 'utf-8')
97
112
 
98
113
  // Success message
99
- console.log(StyledText.iconSuccess('PCU configuration initialized successfully!'))
100
- console.log(StyledText.muted(`Created: ${configPath}`))
101
- console.log('')
114
+ cliOutput.print(StyledText.iconSuccess(t('command.init.success')))
115
+ cliOutput.print(StyledText.muted(t('command.init.createdLabel', { path: configPath })))
116
+ cliOutput.print('')
102
117
 
103
118
  // Show next steps
104
119
  this.showNextSteps(configPath)
105
120
 
106
- process.exit(0)
121
+ throw CommandExitError.success()
107
122
  } catch (error) {
108
- console.error(StyledText.iconError('Error initializing configuration:'))
109
- console.error(StyledText.error(String(error)))
110
-
111
- if (options.verbose && error instanceof Error) {
112
- console.error(StyledText.muted('Stack trace:'))
113
- console.error(StyledText.muted(error.stack || 'No stack trace available'))
123
+ // Re-throw CommandExitError as-is
124
+ if (error instanceof CommandExitError) {
125
+ throw error
114
126
  }
115
127
 
116
- process.exit(1)
128
+ // QUAL-007: Use unified error handling
129
+ handleCommandError(error, {
130
+ verbose: options.verbose,
131
+ errorMessage: 'Init command failed',
132
+ context: { options },
133
+ errorDisplayKey: 'command.init.errorInitializing',
134
+ })
135
+
136
+ throw CommandExitError.failure('Init command failed')
117
137
  }
118
138
  }
119
139
 
@@ -133,7 +153,7 @@ export class InitCommand {
133
153
  writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf-8')
134
154
 
135
155
  if (options.verbose) {
136
- console.log(StyledText.muted('Created: package.json'))
156
+ cliOutput.print(StyledText.muted(t('command.init.createdPackageJson')))
137
157
  }
138
158
  }
139
159
 
@@ -144,7 +164,7 @@ export class InitCommand {
144
164
  writeFileSync(workspaceYamlPath, workspaceYaml, 'utf-8')
145
165
 
146
166
  if (options.verbose) {
147
- console.log(StyledText.muted('Created: pnpm-workspace.yaml'))
167
+ cliOutput.print(StyledText.muted(t('command.init.createdWorkspaceYaml')))
148
168
  }
149
169
  }
150
170
 
@@ -154,7 +174,7 @@ export class InitCommand {
154
174
  mkdirSync(packagesDir, { recursive: true })
155
175
 
156
176
  if (options.verbose) {
157
- console.log(StyledText.muted('Created: packages/ directory'))
177
+ cliOutput.print(StyledText.muted(t('command.init.createdPackagesDir')))
158
178
  }
159
179
  }
160
180
  }
@@ -162,7 +182,7 @@ export class InitCommand {
162
182
  /**
163
183
  * Generate workspace package.json
164
184
  */
165
- private generateWorkspacePackageJson(): any {
185
+ private generateWorkspacePackageJson(): WorkspacePackageJson {
166
186
  return {
167
187
  name: 'my-workspace',
168
188
  version: '1.0.0',
@@ -303,43 +323,33 @@ catalogs:
303
323
  private showNextSteps(configPath: string): void {
304
324
  const lines: string[] = []
305
325
 
306
- lines.push(StyledText.iconInfo('Next steps:'))
326
+ lines.push(StyledText.iconInfo(`${t('command.init.nextSteps')}:`))
307
327
  lines.push('')
308
- lines.push(StyledText.muted('1. Review and customize the configuration:'))
328
+ lines.push(StyledText.muted(t('command.init.step1')))
309
329
  lines.push(StyledText.muted(` ${configPath}`))
310
330
  lines.push('')
311
- lines.push(StyledText.muted('2. Add packages to your workspace:'))
312
- lines.push(StyledText.muted(' mkdir packages/my-app && cd packages/my-app'))
313
- lines.push(StyledText.muted(' pnpm init'))
331
+ lines.push(StyledText.muted(t('command.init.step2')))
332
+ lines.push(StyledText.muted(` ${t('command.init.step2Commands')}`))
314
333
  lines.push('')
315
- lines.push(StyledText.muted('3. Install dependencies and check for updates:'))
316
- lines.push(StyledText.muted(' pnpm install'))
317
- lines.push(StyledText.muted(' pcu check'))
334
+ lines.push(StyledText.muted(t('command.init.step3')))
335
+ lines.push(StyledText.muted(` ${t('command.init.step3Commands')}`))
318
336
  lines.push('')
319
- lines.push(StyledText.muted('4. Update dependencies interactively:'))
320
- lines.push(StyledText.muted(' pcu update --interactive'))
337
+ lines.push(StyledText.muted(t('command.init.step4')))
338
+ lines.push(StyledText.muted(` ${t('command.init.step4Commands')}`))
321
339
  lines.push('')
322
- lines.push(StyledText.muted('5. Learn more about PNPM workspace and PCU:'))
340
+ lines.push(StyledText.muted(t('command.init.step5')))
323
341
  lines.push(StyledText.muted(' https://pnpm.io/workspaces'))
324
- lines.push(
325
- StyledText.muted(' https://github.com/your-repo/pnpm-catalog-updates#configuration')
326
- )
342
+ lines.push(StyledText.muted(' https://github.com/houko/pnpm-catalog-updates#configuration'))
327
343
 
328
- console.log(lines.join('\n'))
344
+ cliOutput.print(lines.join('\n'))
329
345
  }
330
346
 
331
347
  /**
332
348
  * Validate command options
349
+ * QUAL-002: Uses unified validator from validators/
333
350
  */
334
351
  static validateOptions(options: InitCommandOptions): string[] {
335
- const errors: string[] = []
336
-
337
- // Validate workspace path exists if provided
338
- if (options.workspace && !existsSync(options.workspace)) {
339
- errors.push(`Workspace directory does not exist: ${options.workspace}`)
340
- }
341
-
342
- return errors
352
+ return errorsOnly(validateInitOptions)(options)
343
353
  }
344
354
 
345
355
  /**