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,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
+ })