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.
- 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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Command Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { WorkspaceService } from '@pcu/core'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import type { GraphFormat, GraphType } from '../graphCommand.js'
|
|
8
|
+
|
|
9
|
+
// Use vi.hoisted for mock values
|
|
10
|
+
const mocks = vi.hoisted(() => ({
|
|
11
|
+
cliOutputPrint: vi.fn(),
|
|
12
|
+
getCatalogs: vi.fn(),
|
|
13
|
+
getPackages: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
// Mock @pcu/utils
|
|
17
|
+
vi.mock('@pcu/utils', () => ({
|
|
18
|
+
CommandExitError: class CommandExitError extends Error {
|
|
19
|
+
exitCode: number
|
|
20
|
+
constructor(message: string, exitCode = 0) {
|
|
21
|
+
super(message)
|
|
22
|
+
this.exitCode = exitCode
|
|
23
|
+
}
|
|
24
|
+
static success() {
|
|
25
|
+
return new CommandExitError('success', 0)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
// Mock cliOutput
|
|
31
|
+
vi.mock('../../utils/cliOutput.js', () => ({
|
|
32
|
+
cliOutput: {
|
|
33
|
+
print: mocks.cliOutputPrint,
|
|
34
|
+
error: vi.fn(),
|
|
35
|
+
warn: vi.fn(),
|
|
36
|
+
info: vi.fn(),
|
|
37
|
+
},
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Mock validators
|
|
41
|
+
vi.mock('../../validators/index.js', () => ({
|
|
42
|
+
errorsOnly: (fn: (opts: unknown) => { errors: string[] }) => (opts: unknown) => fn(opts).errors,
|
|
43
|
+
validateGraphOptions: (opts: { format?: string; type?: string }) => {
|
|
44
|
+
const errors: string[] = []
|
|
45
|
+
if (opts.format && !['text', 'mermaid', 'dot', 'json'].includes(opts.format)) {
|
|
46
|
+
errors.push(`Invalid format: ${opts.format}`)
|
|
47
|
+
}
|
|
48
|
+
if (opts.type && !['catalog', 'package', 'full'].includes(opts.type)) {
|
|
49
|
+
errors.push(`Invalid type: ${opts.type}`)
|
|
50
|
+
}
|
|
51
|
+
return { errors, warnings: [] }
|
|
52
|
+
},
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
// Import after mock setup
|
|
56
|
+
const { GraphCommand } = await import('../graphCommand.js')
|
|
57
|
+
|
|
58
|
+
describe('GraphCommand', () => {
|
|
59
|
+
let command: InstanceType<typeof GraphCommand>
|
|
60
|
+
let mockWorkspaceService: {
|
|
61
|
+
getCatalogs: ReturnType<typeof vi.fn>
|
|
62
|
+
getPackages: ReturnType<typeof vi.fn>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mockCatalogs = [
|
|
66
|
+
{
|
|
67
|
+
name: 'default',
|
|
68
|
+
packageCount: 3,
|
|
69
|
+
mode: 'default',
|
|
70
|
+
packages: ['react', 'lodash', 'typescript'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'dev',
|
|
74
|
+
packageCount: 2,
|
|
75
|
+
mode: 'dev',
|
|
76
|
+
packages: ['vitest', 'eslint'],
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const mockPackages = [
|
|
81
|
+
{
|
|
82
|
+
name: 'app',
|
|
83
|
+
path: 'packages/app',
|
|
84
|
+
dependencies: [
|
|
85
|
+
{ name: 'react', type: 'dependencies', isCatalogReference: true },
|
|
86
|
+
{ name: 'lodash', type: 'dependencies', isCatalogReference: true },
|
|
87
|
+
],
|
|
88
|
+
catalogReferences: [{ catalogName: 'default', dependencyType: 'dependencies' }],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'utils',
|
|
92
|
+
path: 'packages/utils',
|
|
93
|
+
dependencies: [{ name: 'typescript', type: 'devDependencies', isCatalogReference: true }],
|
|
94
|
+
catalogReferences: [{ catalogName: 'dev', dependencyType: 'devDependencies' }],
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.clearAllMocks()
|
|
100
|
+
|
|
101
|
+
mockWorkspaceService = {
|
|
102
|
+
getCatalogs: mocks.getCatalogs,
|
|
103
|
+
getPackages: mocks.getPackages,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
mocks.getCatalogs.mockResolvedValue(mockCatalogs)
|
|
107
|
+
mocks.getPackages.mockResolvedValue(mockPackages)
|
|
108
|
+
|
|
109
|
+
command = new GraphCommand(mockWorkspaceService as unknown as WorkspaceService)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
vi.resetAllMocks()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('execute', () => {
|
|
117
|
+
it('should output text format by default', async () => {
|
|
118
|
+
await expect(command.execute({})).rejects.toThrow('success')
|
|
119
|
+
|
|
120
|
+
expect(mocks.cliOutputPrint).toHaveBeenCalled()
|
|
121
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
122
|
+
expect(output).toContain('Catalog Dependency Graph')
|
|
123
|
+
expect(output).toContain('Catalogs:')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should output mermaid format when specified', async () => {
|
|
127
|
+
await expect(command.execute({ format: 'mermaid' })).rejects.toThrow('success')
|
|
128
|
+
|
|
129
|
+
expect(mocks.cliOutputPrint).toHaveBeenCalled()
|
|
130
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
131
|
+
expect(output).toContain('```mermaid')
|
|
132
|
+
expect(output).toContain('graph TD')
|
|
133
|
+
expect(output).toContain('```')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should output dot format when specified', async () => {
|
|
137
|
+
await expect(command.execute({ format: 'dot' })).rejects.toThrow('success')
|
|
138
|
+
|
|
139
|
+
expect(mocks.cliOutputPrint).toHaveBeenCalled()
|
|
140
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
141
|
+
expect(output).toContain('digraph CatalogDependencies')
|
|
142
|
+
expect(output).toContain('rankdir=TB')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should output json format when specified', async () => {
|
|
146
|
+
await expect(command.execute({ format: 'json' })).rejects.toThrow('success')
|
|
147
|
+
|
|
148
|
+
expect(mocks.cliOutputPrint).toHaveBeenCalled()
|
|
149
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
150
|
+
const parsed = JSON.parse(output)
|
|
151
|
+
expect(parsed).toHaveProperty('nodes')
|
|
152
|
+
expect(parsed).toHaveProperty('edges')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should build catalog graph type by default', async () => {
|
|
156
|
+
await expect(command.execute({ format: 'json' })).rejects.toThrow('success')
|
|
157
|
+
|
|
158
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
159
|
+
const parsed = JSON.parse(output)
|
|
160
|
+
|
|
161
|
+
const catalogNodes = parsed.nodes.filter((n: { type: string }) => n.type === 'catalog')
|
|
162
|
+
expect(catalogNodes.length).toBe(2)
|
|
163
|
+
expect(catalogNodes.map((n: { name: string }) => n.name)).toContain('default')
|
|
164
|
+
expect(catalogNodes.map((n: { name: string }) => n.name)).toContain('dev')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should build package graph type when specified', async () => {
|
|
168
|
+
await expect(command.execute({ format: 'json', type: 'package' })).rejects.toThrow('success')
|
|
169
|
+
|
|
170
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
171
|
+
const parsed = JSON.parse(output)
|
|
172
|
+
|
|
173
|
+
const packageNodes = parsed.nodes.filter((n: { type: string }) => n.type === 'package')
|
|
174
|
+
expect(packageNodes.length).toBe(2)
|
|
175
|
+
expect(packageNodes.map((n: { name: string }) => n.name)).toContain('app')
|
|
176
|
+
expect(packageNodes.map((n: { name: string }) => n.name)).toContain('utils')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should build full graph type when specified', async () => {
|
|
180
|
+
await expect(command.execute({ format: 'json', type: 'full' })).rejects.toThrow('success')
|
|
181
|
+
|
|
182
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
183
|
+
const parsed = JSON.parse(output)
|
|
184
|
+
|
|
185
|
+
const catalogNodes = parsed.nodes.filter((n: { type: string }) => n.type === 'catalog')
|
|
186
|
+
const packageNodes = parsed.nodes.filter((n: { type: string }) => n.type === 'package')
|
|
187
|
+
expect(catalogNodes.length).toBeGreaterThan(0)
|
|
188
|
+
expect(packageNodes.length).toBeGreaterThan(0)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should filter by catalog when specified', async () => {
|
|
192
|
+
await expect(command.execute({ format: 'json', catalog: 'default' })).rejects.toThrow(
|
|
193
|
+
'success'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
197
|
+
const parsed = JSON.parse(output)
|
|
198
|
+
|
|
199
|
+
const catalogNodes = parsed.nodes.filter((n: { type: string }) => n.type === 'catalog')
|
|
200
|
+
expect(catalogNodes.length).toBe(1)
|
|
201
|
+
expect(catalogNodes[0].name).toBe('default')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should fetch catalogs and packages in parallel', async () => {
|
|
205
|
+
await expect(command.execute({})).rejects.toThrow('success')
|
|
206
|
+
|
|
207
|
+
expect(mocks.getCatalogs).toHaveBeenCalled()
|
|
208
|
+
expect(mocks.getPackages).toHaveBeenCalled()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should handle empty catalogs', async () => {
|
|
212
|
+
mocks.getCatalogs.mockResolvedValue([])
|
|
213
|
+
mocks.getPackages.mockResolvedValue([])
|
|
214
|
+
|
|
215
|
+
await expect(command.execute({ format: 'json' })).rejects.toThrow('success')
|
|
216
|
+
|
|
217
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
218
|
+
const parsed = JSON.parse(output)
|
|
219
|
+
expect(parsed.nodes).toEqual([])
|
|
220
|
+
expect(parsed.edges).toEqual([])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should respect color option in text format', async () => {
|
|
224
|
+
await expect(command.execute({ format: 'text', color: false })).rejects.toThrow('success')
|
|
225
|
+
|
|
226
|
+
expect(mocks.cliOutputPrint).toHaveBeenCalled()
|
|
227
|
+
// With color=false, output should not contain ANSI escape codes
|
|
228
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
229
|
+
expect(output).not.toMatch(/\x1b\[/)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('graph formatting', () => {
|
|
234
|
+
it('should generate valid mermaid syntax with node classes', async () => {
|
|
235
|
+
await expect(command.execute({ format: 'mermaid' })).rejects.toThrow('success')
|
|
236
|
+
|
|
237
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
238
|
+
expect(output).toContain('classDef catalog')
|
|
239
|
+
expect(output).toContain('classDef package')
|
|
240
|
+
expect(output).toContain('classDef dependency')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should generate valid DOT syntax with node styles', async () => {
|
|
244
|
+
await expect(command.execute({ format: 'dot' })).rejects.toThrow('success')
|
|
245
|
+
|
|
246
|
+
const output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
247
|
+
expect(output).toContain('shape=box')
|
|
248
|
+
expect(output).toContain('fillcolor=')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should include edge relationships in all formats', async () => {
|
|
252
|
+
// Test mermaid edges
|
|
253
|
+
await expect(command.execute({ format: 'mermaid', type: 'full' })).rejects.toThrow('success')
|
|
254
|
+
let output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
255
|
+
expect(output).toMatch(/--->/i) // contains arrow
|
|
256
|
+
|
|
257
|
+
// Test dot edges
|
|
258
|
+
mocks.cliOutputPrint.mockClear()
|
|
259
|
+
await expect(command.execute({ format: 'dot', type: 'full' })).rejects.toThrow('success')
|
|
260
|
+
output = mocks.cliOutputPrint.mock.calls[0][0]
|
|
261
|
+
expect(output).toContain('->')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('validateOptions', () => {
|
|
266
|
+
it('should return empty array for valid options', () => {
|
|
267
|
+
const errors = GraphCommand.validateOptions({
|
|
268
|
+
format: 'json',
|
|
269
|
+
type: 'catalog',
|
|
270
|
+
})
|
|
271
|
+
expect(errors).toEqual([])
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should return error for invalid format', () => {
|
|
275
|
+
const errors = GraphCommand.validateOptions({
|
|
276
|
+
format: 'invalid' as GraphFormat,
|
|
277
|
+
})
|
|
278
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
279
|
+
expect(errors[0]).toContain('format')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should return error for invalid type', () => {
|
|
283
|
+
const errors = GraphCommand.validateOptions({
|
|
284
|
+
type: 'invalid' as GraphType,
|
|
285
|
+
})
|
|
286
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
287
|
+
expect(errors[0]).toContain('type')
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('getHelpText', () => {
|
|
292
|
+
it('should return help text with all options', () => {
|
|
293
|
+
const helpText = GraphCommand.getHelpText()
|
|
294
|
+
|
|
295
|
+
expect(helpText).toContain('Visualize catalog dependency relationships')
|
|
296
|
+
expect(helpText).toContain('--format')
|
|
297
|
+
expect(helpText).toContain('--type')
|
|
298
|
+
expect(helpText).toContain('--catalog')
|
|
299
|
+
expect(helpText).toContain('text')
|
|
300
|
+
expect(helpText).toContain('mermaid')
|
|
301
|
+
expect(helpText).toContain('dot')
|
|
302
|
+
expect(helpText).toContain('json')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should include examples', () => {
|
|
306
|
+
const helpText = GraphCommand.getHelpText()
|
|
307
|
+
|
|
308
|
+
expect(helpText).toContain('Examples:')
|
|
309
|
+
expect(helpText).toContain('pcu graph')
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Mock node:fs
|
|
8
|
+
const fsMocks = vi.hoisted(() => ({
|
|
9
|
+
existsSync: vi.fn(),
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
writeFileSync: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('node:fs', () => ({
|
|
15
|
+
existsSync: fsMocks.existsSync,
|
|
16
|
+
mkdirSync: fsMocks.mkdirSync,
|
|
17
|
+
writeFileSync: fsMocks.writeFileSync,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
// Mock @pcu/utils
|
|
21
|
+
vi.mock('@pcu/utils', () => ({
|
|
22
|
+
CommandExitError: class CommandExitError extends Error {
|
|
23
|
+
code: number
|
|
24
|
+
constructor(message: string, code = 1) {
|
|
25
|
+
super(message)
|
|
26
|
+
this.code = code
|
|
27
|
+
}
|
|
28
|
+
static success() {
|
|
29
|
+
const err = new CommandExitError('Success', 0)
|
|
30
|
+
return err
|
|
31
|
+
}
|
|
32
|
+
static failure(message: string) {
|
|
33
|
+
return new CommandExitError(message, 1)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
logger: {
|
|
37
|
+
error: vi.fn(),
|
|
38
|
+
warn: vi.fn(),
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
debug: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
t: (key: string, params?: Record<string, unknown>) => {
|
|
43
|
+
if (params) {
|
|
44
|
+
let result = key
|
|
45
|
+
for (const [k, v] of Object.entries(params)) {
|
|
46
|
+
result = result.replace(`{{${k}}}`, String(v))
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
return key
|
|
51
|
+
},
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
// Mock theme module
|
|
55
|
+
vi.mock('../../themes/colorTheme.js', () => ({
|
|
56
|
+
ThemeManager: {
|
|
57
|
+
setTheme: vi.fn(),
|
|
58
|
+
},
|
|
59
|
+
StyledText: {
|
|
60
|
+
iconInfo: (text: string) => `[info] ${text}`,
|
|
61
|
+
iconSuccess: (text: string) => `[success] ${text}`,
|
|
62
|
+
iconWarning: (text: string) => `[warning] ${text}`,
|
|
63
|
+
iconError: (text: string) => `[error] ${text}`,
|
|
64
|
+
muted: (text: string) => `[muted] ${text}`,
|
|
65
|
+
warning: (text: string) => `[warning] ${text}`,
|
|
66
|
+
},
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
// Mock cliOutput
|
|
70
|
+
vi.mock('../../utils/cliOutput.js', () => ({
|
|
71
|
+
cliOutput: {
|
|
72
|
+
print: vi.fn(),
|
|
73
|
+
error: vi.fn(),
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
// Mock command helpers
|
|
78
|
+
vi.mock('../../utils/commandHelpers.js', () => ({
|
|
79
|
+
handleCommandError: vi.fn(),
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
// Mock validators
|
|
83
|
+
vi.mock('../../validators/index.js', () => ({
|
|
84
|
+
errorsOnly: () => () => [],
|
|
85
|
+
validateInitOptions: vi.fn(() => ({ valid: true, errors: [] })),
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
// Import after mock setup
|
|
89
|
+
const { InitCommand } = await import('../initCommand.js')
|
|
90
|
+
const { CommandExitError } = await import('@pcu/utils')
|
|
91
|
+
|
|
92
|
+
describe('InitCommand', () => {
|
|
93
|
+
let command: InstanceType<typeof InitCommand>
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
vi.clearAllMocks()
|
|
97
|
+
command = new InitCommand()
|
|
98
|
+
|
|
99
|
+
// Default: files don't exist
|
|
100
|
+
fsMocks.existsSync.mockReturnValue(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.resetAllMocks()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('execute', () => {
|
|
108
|
+
it('should create configuration file in current directory by default', async () => {
|
|
109
|
+
// Mock workspace files exist
|
|
110
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
111
|
+
if (path.includes('package.json')) return true
|
|
112
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
113
|
+
if (path.includes('.pcurc.json')) return false
|
|
114
|
+
return false
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await expect(command.execute({})).rejects.toThrow()
|
|
118
|
+
|
|
119
|
+
// Should write config file
|
|
120
|
+
expect(fsMocks.writeFileSync).toHaveBeenCalled()
|
|
121
|
+
const writeCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
122
|
+
String(call[0]).includes('.pcurc.json')
|
|
123
|
+
)
|
|
124
|
+
expect(writeCall).toBeDefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should create workspace structure when missing', async () => {
|
|
128
|
+
// No files exist
|
|
129
|
+
fsMocks.existsSync.mockReturnValue(false)
|
|
130
|
+
|
|
131
|
+
await expect(command.execute({ verbose: true })).rejects.toThrow()
|
|
132
|
+
|
|
133
|
+
// Should create package.json
|
|
134
|
+
const packageJsonCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
135
|
+
String(call[0]).includes('package.json')
|
|
136
|
+
)
|
|
137
|
+
expect(packageJsonCall).toBeDefined()
|
|
138
|
+
|
|
139
|
+
// Should create pnpm-workspace.yaml
|
|
140
|
+
const workspaceYamlCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
141
|
+
String(call[0]).includes('pnpm-workspace.yaml')
|
|
142
|
+
)
|
|
143
|
+
expect(workspaceYamlCall).toBeDefined()
|
|
144
|
+
|
|
145
|
+
// Should create packages directory
|
|
146
|
+
expect(fsMocks.mkdirSync).toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should skip workspace creation when createWorkspace is false', async () => {
|
|
150
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
151
|
+
if (path.includes('.pcurc.json')) return false
|
|
152
|
+
return false
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await expect(command.execute({ createWorkspace: false })).rejects.toThrow()
|
|
156
|
+
|
|
157
|
+
// Should not create package.json or workspace yaml
|
|
158
|
+
const packageJsonCalls = fsMocks.writeFileSync.mock.calls.filter((call) =>
|
|
159
|
+
String(call[0]).includes('package.json')
|
|
160
|
+
)
|
|
161
|
+
expect(packageJsonCalls.length).toBe(0)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should fail if config file exists without force flag', async () => {
|
|
165
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
166
|
+
if (path.includes('package.json')) return true
|
|
167
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
168
|
+
if (path.includes('.pcurc.json')) return true
|
|
169
|
+
return false
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await expect(command.execute({})).rejects.toThrow()
|
|
173
|
+
|
|
174
|
+
// Should not overwrite existing config
|
|
175
|
+
const configWriteCalls = fsMocks.writeFileSync.mock.calls.filter((call) =>
|
|
176
|
+
String(call[0]).includes('.pcurc.json')
|
|
177
|
+
)
|
|
178
|
+
expect(configWriteCalls.length).toBe(0)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should overwrite config file when force flag is set', async () => {
|
|
182
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
183
|
+
if (path.includes('package.json')) return true
|
|
184
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
185
|
+
if (path.includes('.pcurc.json')) return true
|
|
186
|
+
return false
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
await expect(command.execute({ force: true })).rejects.toThrow()
|
|
190
|
+
|
|
191
|
+
// Should overwrite config
|
|
192
|
+
const configWriteCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
193
|
+
String(call[0]).includes('.pcurc.json')
|
|
194
|
+
)
|
|
195
|
+
expect(configWriteCall).toBeDefined()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should generate full configuration when full flag is set', async () => {
|
|
199
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
200
|
+
if (path.includes('package.json')) return true
|
|
201
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
202
|
+
if (path.includes('.pcurc.json')) return false
|
|
203
|
+
return false
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await expect(command.execute({ full: true })).rejects.toThrow()
|
|
207
|
+
|
|
208
|
+
// Should write config with full options
|
|
209
|
+
const configWriteCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
210
|
+
String(call[0]).includes('.pcurc.json')
|
|
211
|
+
)
|
|
212
|
+
expect(configWriteCall).toBeDefined()
|
|
213
|
+
|
|
214
|
+
const configContent = JSON.parse(configWriteCall![1] as string)
|
|
215
|
+
// Full config should have packageRules, security, advanced, monorepo
|
|
216
|
+
expect(configContent.packageRules).toBeDefined()
|
|
217
|
+
expect(configContent.security).toBeDefined()
|
|
218
|
+
expect(configContent.advanced).toBeDefined()
|
|
219
|
+
expect(configContent.monorepo).toBeDefined()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('should generate minimal configuration by default', async () => {
|
|
223
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
224
|
+
if (path.includes('package.json')) return true
|
|
225
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
226
|
+
if (path.includes('.pcurc.json')) return false
|
|
227
|
+
return false
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
await expect(command.execute({})).rejects.toThrow()
|
|
231
|
+
|
|
232
|
+
const configWriteCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
233
|
+
String(call[0]).includes('.pcurc.json')
|
|
234
|
+
)
|
|
235
|
+
expect(configWriteCall).toBeDefined()
|
|
236
|
+
|
|
237
|
+
const configContent = JSON.parse(configWriteCall![1] as string)
|
|
238
|
+
// Minimal config should have defaults but no packageRules
|
|
239
|
+
expect(configContent.defaults).toBeDefined()
|
|
240
|
+
expect(configContent.defaults.target).toBe('latest')
|
|
241
|
+
expect(configContent.defaults.createBackup).toBe(true)
|
|
242
|
+
expect(configContent.packageRules).toBeUndefined()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should use custom workspace path when provided', async () => {
|
|
246
|
+
const customPath = '/custom/workspace/path'
|
|
247
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
248
|
+
if (path.includes(customPath)) {
|
|
249
|
+
if (path.includes('package.json')) return true
|
|
250
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
251
|
+
if (path.includes('.pcurc.json')) return false
|
|
252
|
+
}
|
|
253
|
+
return false
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await expect(command.execute({ workspace: customPath })).rejects.toThrow()
|
|
257
|
+
|
|
258
|
+
// Config should be written to custom path
|
|
259
|
+
const configWriteCall = fsMocks.writeFileSync.mock.calls.find((call) =>
|
|
260
|
+
String(call[0]).includes(customPath)
|
|
261
|
+
)
|
|
262
|
+
expect(configWriteCall).toBeDefined()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should create config directory if it does not exist', async () => {
|
|
266
|
+
fsMocks.existsSync.mockImplementation((path: string) => {
|
|
267
|
+
if (path.includes('package.json')) return true
|
|
268
|
+
if (path.includes('pnpm-workspace.yaml')) return true
|
|
269
|
+
// Config directory doesn't exist
|
|
270
|
+
return false
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
await expect(command.execute({})).rejects.toThrow()
|
|
274
|
+
|
|
275
|
+
// Should create directory
|
|
276
|
+
expect(fsMocks.mkdirSync).toHaveBeenCalled()
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('validateOptions', () => {
|
|
281
|
+
it('should return empty array for valid options', () => {
|
|
282
|
+
const errors = InitCommand.validateOptions({})
|
|
283
|
+
expect(errors).toEqual([])
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('getHelpText', () => {
|
|
288
|
+
it('should return help text with all options', () => {
|
|
289
|
+
const helpText = InitCommand.getHelpText()
|
|
290
|
+
|
|
291
|
+
expect(helpText).toContain('Initialize PCU configuration')
|
|
292
|
+
expect(helpText).toContain('--workspace')
|
|
293
|
+
expect(helpText).toContain('--force')
|
|
294
|
+
expect(helpText).toContain('--full')
|
|
295
|
+
expect(helpText).toContain('--create-workspace')
|
|
296
|
+
expect(helpText).toContain('--verbose')
|
|
297
|
+
expect(helpText).toContain('--no-color')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should include usage examples', () => {
|
|
301
|
+
const helpText = InitCommand.getHelpText()
|
|
302
|
+
|
|
303
|
+
expect(helpText).toContain('pcu init')
|
|
304
|
+
expect(helpText).toContain('pcu init --full')
|
|
305
|
+
expect(helpText).toContain('pcu init --force')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should describe created files', () => {
|
|
309
|
+
const helpText = InitCommand.getHelpText()
|
|
310
|
+
|
|
311
|
+
expect(helpText).toContain('.pcurc.json')
|
|
312
|
+
expect(helpText).toContain('package.json')
|
|
313
|
+
expect(helpText).toContain('pnpm-workspace.yaml')
|
|
314
|
+
expect(helpText).toContain('packages/')
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|