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,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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
65
|
+
cliOutput.print(StyledText.iconWarning(t('warning.workspaceNotDetected')))
|
|
51
66
|
if (!hasPackageJson) {
|
|
52
|
-
|
|
67
|
+
cliOutput.print(StyledText.muted(t('command.init.missingPackageJson')))
|
|
53
68
|
}
|
|
54
69
|
if (!hasWorkspaceYaml) {
|
|
55
|
-
|
|
70
|
+
cliOutput.print(StyledText.muted(t('command.init.missingWorkspaceYaml')))
|
|
56
71
|
}
|
|
57
|
-
|
|
72
|
+
cliOutput.print('')
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
// Create workspace structure
|
|
61
76
|
if (options.verbose) {
|
|
62
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
121
|
+
throw CommandExitError.success()
|
|
107
122
|
} catch (error) {
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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():
|
|
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('
|
|
326
|
+
lines.push(StyledText.iconInfo(`${t('command.init.nextSteps')}:`))
|
|
307
327
|
lines.push('')
|
|
308
|
-
lines.push(StyledText.muted('
|
|
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('
|
|
312
|
-
lines.push(StyledText.muted(
|
|
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('
|
|
316
|
-
lines.push(StyledText.muted(
|
|
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('
|
|
320
|
-
lines.push(StyledText.muted(
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|