berget 2.2.6 → 2.2.8

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 (145) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/.github/workflows/test.yml +10 -4
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +7 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +10 -11
  9. package/dist/package.json +30 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +97 -117
  20. package/dist/src/commands/api-keys.js +75 -90
  21. package/dist/src/commands/auth.js +7 -16
  22. package/dist/src/commands/autocomplete.js +1 -1
  23. package/dist/src/commands/billing.js +6 -17
  24. package/dist/src/commands/chat.js +68 -101
  25. package/dist/src/commands/clusters.js +9 -18
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
  33. package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
  34. package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
  36. package/dist/src/commands/code/auth-sync.js +270 -0
  37. package/dist/src/commands/code/errors.js +12 -9
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +387 -281
  40. package/dist/src/commands/code.js +205 -332
  41. package/dist/src/commands/index.js +5 -5
  42. package/dist/src/commands/models.js +6 -17
  43. package/dist/src/commands/users.js +5 -16
  44. package/dist/src/constants/command-structure.js +104 -104
  45. package/dist/src/services/api-key-service.js +132 -157
  46. package/dist/src/services/auth-service.js +89 -342
  47. package/dist/src/services/browser-auth.js +268 -0
  48. package/dist/src/services/chat-service.js +371 -401
  49. package/dist/src/services/cluster-service.js +47 -62
  50. package/dist/src/services/collaborator-service.js +10 -25
  51. package/dist/src/services/flux-service.js +14 -29
  52. package/dist/src/services/helm-service.js +10 -25
  53. package/dist/src/services/kubectl-service.js +16 -33
  54. package/dist/src/utils/config-checker.js +3 -3
  55. package/dist/src/utils/config-loader.js +95 -95
  56. package/dist/src/utils/default-api-key.js +124 -134
  57. package/dist/src/utils/env-manager.js +55 -66
  58. package/dist/src/utils/error-handler.js +20 -21
  59. package/dist/src/utils/logger.js +72 -65
  60. package/dist/src/utils/markdown-renderer.js +27 -27
  61. package/dist/src/utils/opencode-validator.js +63 -68
  62. package/dist/src/utils/token-manager.js +74 -45
  63. package/dist/tests/commands/chat.test.js +16 -25
  64. package/dist/tests/commands/code.test.js +95 -104
  65. package/dist/tests/utils/config-loader.test.js +48 -48
  66. package/dist/tests/utils/env-manager.test.js +43 -52
  67. package/dist/tests/utils/opencode-validator.test.js +22 -21
  68. package/dist/vitest.config.js +1 -1
  69. package/eslint.config.mjs +67 -0
  70. package/index.ts +35 -42
  71. package/package.json +30 -2
  72. package/src/agents/app.ts +27 -0
  73. package/src/agents/backend.ts +24 -0
  74. package/src/agents/devops.ts +33 -0
  75. package/src/agents/frontend.ts +24 -0
  76. package/src/agents/fullstack.ts +24 -0
  77. package/src/agents/index.ts +73 -0
  78. package/src/agents/quality.ts +69 -0
  79. package/src/agents/security.ts +26 -0
  80. package/src/agents/types.ts +17 -0
  81. package/src/client.ts +118 -152
  82. package/src/commands/api-keys.ts +241 -333
  83. package/src/commands/auth.ts +22 -27
  84. package/src/commands/autocomplete.ts +9 -9
  85. package/src/commands/billing.ts +20 -24
  86. package/src/commands/chat.ts +248 -338
  87. package/src/commands/clusters.ts +27 -26
  88. package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
  89. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  90. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  91. package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
  92. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  93. package/src/commands/code/__tests__/fake-prompter.ts +116 -77
  94. package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
  95. package/src/commands/code/adapters/clack-prompter.ts +53 -39
  96. package/src/commands/code/adapters/fs-file-store.ts +32 -27
  97. package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
  98. package/src/commands/code/auth-sync.ts +329 -0
  99. package/src/commands/code/errors.ts +18 -18
  100. package/src/commands/code/ports/auth-services.ts +14 -0
  101. package/src/commands/code/ports/command-runner.ts +8 -4
  102. package/src/commands/code/ports/file-store.ts +5 -4
  103. package/src/commands/code/ports/prompter.ts +24 -18
  104. package/src/commands/code/setup.ts +570 -340
  105. package/src/commands/code.ts +338 -539
  106. package/src/commands/index.ts +20 -19
  107. package/src/commands/models.ts +28 -32
  108. package/src/commands/users.ts +15 -21
  109. package/src/constants/command-structure.ts +134 -157
  110. package/src/services/api-key-service.ts +105 -122
  111. package/src/services/auth-service.ts +99 -345
  112. package/src/services/browser-auth.ts +296 -0
  113. package/src/services/chat-service.ts +265 -299
  114. package/src/services/cluster-service.ts +42 -45
  115. package/src/services/collaborator-service.ts +14 -19
  116. package/src/services/flux-service.ts +23 -25
  117. package/src/services/helm-service.ts +19 -21
  118. package/src/services/kubectl-service.ts +17 -19
  119. package/src/types/api.d.ts +1905 -1907
  120. package/src/types/json.d.ts +2 -2
  121. package/src/utils/config-checker.ts +10 -10
  122. package/src/utils/config-loader.ts +162 -178
  123. package/src/utils/default-api-key.ts +114 -125
  124. package/src/utils/env-manager.ts +53 -57
  125. package/src/utils/error-handler.ts +61 -56
  126. package/src/utils/logger.ts +79 -73
  127. package/src/utils/markdown-renderer.ts +31 -31
  128. package/src/utils/opencode-validator.ts +85 -89
  129. package/src/utils/token-manager.ts +108 -87
  130. package/templates/agents/app.md +1 -0
  131. package/templates/agents/backend.md +1 -0
  132. package/templates/agents/devops.md +2 -0
  133. package/templates/agents/frontend.md +1 -0
  134. package/templates/agents/fullstack.md +1 -0
  135. package/templates/agents/quality.md +45 -40
  136. package/templates/agents/security.md +1 -0
  137. package/tests/commands/chat.test.ts +53 -62
  138. package/tests/commands/code.test.ts +265 -310
  139. package/tests/utils/config-loader.test.ts +189 -188
  140. package/tests/utils/env-manager.test.ts +110 -113
  141. package/tests/utils/opencode-validator.test.ts +52 -56
  142. package/tsconfig.json +4 -3
  143. package/vitest.config.ts +3 -3
  144. package/AGENTS.md +0 -374
  145. package/TODO.md +0 -19
@@ -1,552 +1,507 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { Command } from 'commander'
3
- import { registerCodeCommands } from '../../src/commands/code'
4
- import { ApiKeyService } from '../../src/services/api-key-service'
5
- import * as fs from 'fs'
6
- import { readFile, writeFile } from 'fs/promises'
7
- import { updateEnvFile } from '../../src/utils/env-manager'
1
+ import { Command } from 'commander';
2
+ import * as fs from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import { registerCodeCommands } from '../../src/commands/code';
7
+ import { ApiKeyService } from '../../src/services/api-key-service';
8
+ import { updateEnvFile as updateEnvironmentFile } from '../../src/utils/env-manager';
8
9
 
9
10
  // Mock dependencies
10
- vi.mock('../../src/services/api-key-service')
11
+ vi.mock('../../src/services/api-key-service');
11
12
  vi.mock('fs', () => ({
12
13
  default: {
13
14
  existsSync: vi.fn(),
14
15
  readFileSync: vi.fn(),
15
16
  },
16
- }))
17
+ }));
17
18
  vi.mock('fs/promises', () => ({
18
19
  readFile: vi.fn(),
19
20
  writeFile: vi.fn(),
20
- }))
21
- vi.mock('../../src/utils/env-manager')
21
+ }));
22
+ vi.mock('../../src/utils/env-manager');
22
23
  vi.mock('child_process', () => ({
23
24
  spawn: vi.fn(),
24
- }))
25
+ }));
25
26
  vi.mock('readline', () => ({
26
27
  createInterface: vi.fn(() => ({
27
- question: vi.fn(),
28
28
  close: vi.fn(),
29
+ question: vi.fn(),
29
30
  })),
30
- }))
31
+ }));
31
32
 
32
33
  describe('Code Commands', () => {
33
- let program: Command
34
- let mockApiKeyService: any
35
- let mockFs: any
36
- let mockFsPromises: any
37
- let mockSpawn: any
34
+ let program: Command;
35
+ let mockApiKeyService: any;
36
+ let mockFs: any;
37
+ let mockFsPromises: any;
38
+ let mockSpawn: any;
38
39
 
39
40
  beforeEach(() => {
40
- program = new Command()
41
+ program = new Command();
41
42
 
42
43
  // Mock ApiKeyService
43
44
  mockApiKeyService = {
44
45
  create: vi.fn(),
45
46
  list: vi.fn(),
46
47
  rotate: vi.fn(),
47
- }
48
- vi.mocked(ApiKeyService.getInstance).mockReturnValue(mockApiKeyService)
48
+ };
49
+ vi.mocked(ApiKeyService.getInstance).mockReturnValue(mockApiKeyService);
49
50
 
50
51
  // Mock fs
51
- mockFs = vi.mocked(fs)
52
- mockFs.existsSync = vi.fn()
53
- mockFs.readFileSync = vi.fn()
52
+ mockFs = vi.mocked(fs);
53
+ mockFs.existsSync = vi.fn();
54
+ mockFs.readFileSync = vi.fn();
54
55
 
55
56
  // Mock fs/promises
56
- mockFsPromises = vi.mocked({ readFile, writeFile })
57
- mockFsPromises.readFile = vi.fn()
58
- mockFsPromises.writeFile = vi.fn()
57
+ mockFsPromises = vi.mocked({ readFile, writeFile });
58
+ mockFsPromises.readFile = vi.fn();
59
+ mockFsPromises.writeFile = vi.fn();
59
60
 
60
61
  // Mock spawn
61
- mockSpawn = vi.fn()
62
- vi.doMock('child_process', () => ({ spawn: mockSpawn }))
62
+ mockSpawn = vi.fn();
63
+ vi.doMock('child_process', () => ({ spawn: mockSpawn }));
63
64
 
64
- registerCodeCommands(program)
65
- })
65
+ registerCodeCommands(program);
66
+ });
66
67
 
67
68
  afterEach(() => {
68
- vi.clearAllMocks()
69
- })
69
+ vi.clearAllMocks();
70
+ });
70
71
 
71
72
  describe('code init command', () => {
72
73
  it('should register init command with correct description', () => {
73
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
74
- const initCommand = codeCommand?.commands.find(
75
- (cmd) => cmd.name() === 'init',
76
- )
74
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
75
+ const initCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'init');
77
76
 
78
- expect(initCommand).toBeDefined()
79
- expect(initCommand?.description()).toBe(
80
- 'Initialize project for AI coding assistant',
81
- )
82
- })
77
+ expect(initCommand).toBeDefined();
78
+ expect(initCommand?.description()).toBe('Initialize project for AI coding assistant');
79
+ });
83
80
 
84
81
  it('should have name, force, and yes options', () => {
85
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
86
- const initCommand = codeCommand?.commands.find(
87
- (cmd) => cmd.name() === 'init',
88
- )
89
-
90
- expect(initCommand).toBeDefined()
91
-
92
- const nameOption = initCommand?.options.find(
93
- (opt) => opt.long === '--name',
94
- )
95
- const forceOption = initCommand?.options.find(
96
- (opt) => opt.long === '--force',
97
- )
98
- const yesOption = initCommand?.options.find((opt) => opt.long === '--yes')
99
-
100
- expect(nameOption).toBeDefined()
101
- expect(nameOption?.description).toContain('Project name')
102
- expect(forceOption).toBeDefined()
103
- expect(forceOption?.description).toContain(
104
- 'Overwrite existing configuration',
105
- )
106
- expect(yesOption).toBeDefined()
107
- expect(yesOption?.description).toContain('Automatically answer yes')
108
- })
82
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
83
+ const initCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'init');
84
+
85
+ expect(initCommand).toBeDefined();
86
+
87
+ const nameOption = initCommand?.options.find((opt) => opt.long === '--name');
88
+ const forceOption = initCommand?.options.find((opt) => opt.long === '--force');
89
+ const yesOption = initCommand?.options.find((opt) => opt.long === '--yes');
90
+
91
+ expect(nameOption).toBeDefined();
92
+ expect(nameOption?.description).toContain('Project name');
93
+ expect(forceOption).toBeDefined();
94
+ expect(forceOption?.description).toContain('Overwrite existing configuration');
95
+ expect(yesOption).toBeDefined();
96
+ expect(yesOption?.description).toContain('Automatically answer yes');
97
+ });
109
98
 
110
99
  it('should check if opencode is installed', () => {
111
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
112
- const initCommand = codeCommand?.commands.find(
113
- (cmd) => cmd.name() === 'init',
114
- )
100
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
101
+ const initCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'init');
115
102
 
116
- expect(initCommand).toBeDefined()
103
+ expect(initCommand).toBeDefined();
117
104
 
118
105
  // The command should attempt to spawn opencode --version
119
106
  // This is tested implicitly through the spawn mock
120
- })
107
+ });
121
108
 
122
109
  it('should list existing API keys and allow selection', async () => {
123
110
  // Mock successful opencode installation check
124
- mockSpawn.mockImplementation((command: string, args: string[]) => {
125
- if (command === 'opencode' && args[0] === '--version') {
111
+ mockSpawn.mockImplementation((command: string, arguments_: string[]) => {
112
+ if (command === 'opencode' && arguments_[0] === '--version') {
126
113
  return {
127
114
  on: vi.fn().mockImplementation((event, callback) => {
128
- if (event === 'close') callback(0)
115
+ if (event === 'close') callback(0);
129
116
  }),
130
- }
117
+ };
131
118
  }
132
- return { on: vi.fn() }
133
- })
119
+ return { on: vi.fn() };
120
+ });
134
121
 
135
122
  // Mock existing API keys
136
123
  const mockExistingKeys = [
137
124
  {
125
+ created: '2023-01-01T00:00:00.000Z',
138
126
  id: 1,
127
+ lastUsed: null,
139
128
  name: 'existing-key-1',
140
129
  prefix: 'sk_ber',
141
- created: '2023-01-01T00:00:00.000Z',
142
- lastUsed: null,
143
130
  },
144
131
  {
132
+ created: '2023-01-02T00:00:00.000Z',
145
133
  id: 2,
134
+ lastUsed: '2023-01-03T00:00:00.000Z',
146
135
  name: 'existing-key-2',
147
136
  prefix: 'sk_ber',
148
- created: '2023-01-02T00:00:00.000Z',
149
- lastUsed: '2023-01-03T00:00:00.000Z',
150
137
  },
151
- ]
152
- mockApiKeyService.list.mockResolvedValue(mockExistingKeys)
138
+ ];
139
+ mockApiKeyService.list.mockResolvedValue(mockExistingKeys);
153
140
 
154
141
  // Mock file operations
155
- mockFs.existsSync.mockReturnValue(false)
156
- mockFsPromises.writeFile.mockResolvedValue(undefined)
142
+ mockFs.existsSync.mockReturnValue(false);
143
+ mockFsPromises.writeFile.mockResolvedValue();
157
144
 
158
145
  // Verify that the list method is called
159
- expect(mockApiKeyService.list).toBeDefined()
160
- })
146
+ expect(mockApiKeyService.list).toBeDefined();
147
+ });
161
148
 
162
149
  it('should create new API key with project-based naming', async () => {
163
150
  // Mock successful opencode installation check
164
- mockSpawn.mockImplementation((command: string, args: string[]) => {
165
- if (command === 'opencode' && args[0] === '--version') {
151
+ mockSpawn.mockImplementation((command: string, arguments_: string[]) => {
152
+ if (command === 'opencode' && arguments_[0] === '--version') {
166
153
  return {
167
154
  on: vi.fn().mockImplementation((event, callback) => {
168
- if (event === 'close') callback(0)
155
+ if (event === 'close') callback(0);
169
156
  }),
170
- }
157
+ };
171
158
  }
172
- return { on: vi.fn() }
173
- })
159
+ return { on: vi.fn() };
160
+ });
174
161
 
175
162
  // Mock no existing keys
176
- mockApiKeyService.list.mockResolvedValue([])
163
+ mockApiKeyService.list.mockResolvedValue([]);
177
164
 
178
165
  // Mock successful API key creation
179
166
  const mockApiKeyData = {
180
167
  id: 123,
181
- name: 'opencode-testproject-1234567890',
182
168
  key: 'test-api-key-12345',
183
- }
184
- mockApiKeyService.create.mockResolvedValue(mockApiKeyData)
169
+ name: 'opencode-testproject-1234567890',
170
+ };
171
+ mockApiKeyService.create.mockResolvedValue(mockApiKeyData);
185
172
 
186
173
  // Mock file operations
187
- mockFs.existsSync.mockReturnValue(false)
188
- mockFsPromises.writeFile.mockResolvedValue(undefined)
174
+ mockFs.existsSync.mockReturnValue(false);
175
+ mockFsPromises.writeFile.mockResolvedValue();
189
176
 
190
177
  // Verify that the create method is available
191
- expect(mockApiKeyService.create).toBeDefined()
192
- })
178
+ expect(mockApiKeyService.create).toBeDefined();
179
+ });
193
180
 
194
181
  it('should create opencode.json with correct structure', async () => {
195
182
  // This tests the expected config structure
196
183
  const expectedConfig = {
197
- model: 'berget/glm-4-6',
198
184
  apiKey: 'test-api-key',
185
+ created: expect.any(String),
186
+ model: 'berget/glm-4-6',
199
187
  projectName: 'testproject',
200
188
  provider: 'berget',
201
- created: expect.any(String),
202
189
  version: '1.0.0',
203
- }
190
+ };
204
191
 
205
- expect(expectedConfig.model).toBe('berget/glm-4-6')
206
- expect(expectedConfig.provider).toBe('berget')
207
- expect(expectedConfig.version).toBe('1.0.0')
208
- })
192
+ expect(expectedConfig.model).toBe('berget/glm-4-6');
193
+ expect(expectedConfig.provider).toBe('berget');
194
+ expect(expectedConfig.version).toBe('1.0.0');
195
+ });
209
196
 
210
197
  it('should handle existing config file', () => {
211
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
212
- const initCommand = codeCommand?.commands.find(
213
- (cmd) => cmd.name() === 'init',
214
- )
198
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
199
+ const initCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'init');
215
200
 
216
- expect(initCommand).toBeDefined()
201
+ expect(initCommand).toBeDefined();
217
202
 
218
203
  // Should check if opencode.json exists before proceeding
219
- expect(mockFs.existsSync).toBeDefined()
220
- })
221
- })
204
+ expect(mockFs.existsSync).toBeDefined();
205
+ });
206
+ });
222
207
 
223
208
  describe('code run command', () => {
224
209
  it('should register run command with correct description', () => {
225
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
226
- const runCommand = codeCommand?.commands.find(
227
- (cmd) => cmd.name() === 'run',
228
- )
210
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
211
+ const runCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'run');
229
212
 
230
- expect(runCommand).toBeDefined()
231
- expect(runCommand?.description()).toBe('Run AI coding assistant')
232
- })
213
+ expect(runCommand).toBeDefined();
214
+ expect(runCommand?.description()).toBe('Run AI coding assistant');
215
+ });
233
216
 
234
217
  it('should accept prompt argument and model, no-config, and yes options', () => {
235
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
236
- const runCommand = codeCommand?.commands.find(
237
- (cmd) => cmd.name() === 'run',
238
- )
239
-
240
- expect(runCommand).toBeDefined()
241
-
242
- const modelOption = runCommand?.options.find(
243
- (opt) => opt.long === '--model',
244
- )
245
- const noConfigOption = runCommand?.options.find(
246
- (opt) => opt.long === '--no-config',
247
- )
248
- const yesOption = runCommand?.options.find((opt) => opt.long === '--yes')
249
-
250
- expect(modelOption).toBeDefined()
251
- expect(modelOption?.description).toContain('Model to use')
252
- expect(noConfigOption).toBeDefined()
253
- expect(noConfigOption?.description).toContain(
254
- 'Run without loading project config',
255
- )
256
- expect(yesOption).toBeDefined()
257
- expect(yesOption?.description).toContain('Automatically answer yes')
258
- })
218
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
219
+ const runCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'run');
220
+
221
+ expect(runCommand).toBeDefined();
222
+
223
+ const modelOption = runCommand?.options.find((opt) => opt.long === '--model');
224
+ const noConfigOption = runCommand?.options.find((opt) => opt.long === '--no-config');
225
+ const yesOption = runCommand?.options.find((opt) => opt.long === '--yes');
226
+
227
+ expect(modelOption).toBeDefined();
228
+ expect(modelOption?.description).toContain('Model to use');
229
+ expect(noConfigOption).toBeDefined();
230
+ expect(noConfigOption?.description).toContain('Run without loading project config');
231
+ expect(yesOption).toBeDefined();
232
+ expect(yesOption?.description).toContain('Automatically answer yes');
233
+ });
259
234
 
260
235
  it('should load configuration from opencode.json', async () => {
261
236
  const mockConfig = {
262
- model: 'berget/glm-4-6',
263
237
  apiKey: 'test-api-key',
238
+ created: '2023-01-01T00:00:00.000Z',
239
+ model: 'berget/glm-4-6',
264
240
  projectName: 'testproject',
265
241
  provider: 'berget',
266
- created: '2023-01-01T00:00:00.000Z',
267
242
  version: '1.0.0',
268
- }
243
+ };
269
244
 
270
245
  // Mock file exists and contains config
271
- mockFs.existsSync.mockReturnValue(true)
272
- mockFsPromises.readFile.mockResolvedValue(JSON.stringify(mockConfig))
246
+ mockFs.existsSync.mockReturnValue(true);
247
+ mockFsPromises.readFile.mockResolvedValue(JSON.stringify(mockConfig));
273
248
 
274
249
  // Mock successful opencode check
275
- mockSpawn.mockImplementation((command: string, args: string[]) => {
276
- if (command === 'opencode' && args[0] === '--version') {
250
+ mockSpawn.mockImplementation((command: string, arguments_: string[]) => {
251
+ if (command === 'opencode' && arguments_[0] === '--version') {
277
252
  return {
278
253
  on: vi.fn().mockImplementation((event, callback) => {
279
- if (event === 'close') callback(0)
254
+ if (event === 'close') callback(0);
280
255
  }),
281
- }
256
+ };
282
257
  }
283
- return { on: vi.fn() }
284
- })
258
+ return { on: vi.fn() };
259
+ });
285
260
 
286
261
  // Verify config structure expectations
287
- expect(mockConfig.model).toBe('berget/glm-4-6')
288
- expect(mockConfig.apiKey).toBe('test-api-key')
289
- expect(mockConfig.projectName).toBe('testproject')
290
- })
262
+ expect(mockConfig.model).toBe('berget/glm-4-6');
263
+ expect(mockConfig.apiKey).toBe('test-api-key');
264
+ expect(mockConfig.projectName).toBe('testproject');
265
+ });
291
266
 
292
267
  it('should spawn opencode with correct arguments', () => {
293
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
294
- const runCommand = codeCommand?.commands.find(
295
- (cmd) => cmd.name() === 'run',
296
- )
268
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
269
+ const runCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'run');
297
270
 
298
- expect(runCommand).toBeDefined()
271
+ expect(runCommand).toBeDefined();
299
272
 
300
273
  // Should spawn opencode with appropriate arguments
301
- expect(mockSpawn).toBeDefined()
302
- })
274
+ expect(mockSpawn).toBeDefined();
275
+ });
303
276
 
304
277
  it('should handle missing configuration file', () => {
305
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
306
- const runCommand = codeCommand?.commands.find(
307
- (cmd) => cmd.name() === 'run',
308
- )
278
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
279
+ const runCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'run');
309
280
 
310
- expect(runCommand).toBeDefined()
281
+ expect(runCommand).toBeDefined();
311
282
 
312
283
  // Should check if opencode.json exists
313
- expect(mockFs.existsSync).toBeDefined()
314
- })
315
- })
284
+ expect(mockFs.existsSync).toBeDefined();
285
+ });
286
+ });
316
287
 
317
288
  describe('opencode installation', () => {
318
289
  it('should check if opencode is installed', () => {
319
290
  // The spawn function should be called with opencode --version
320
- expect(mockSpawn).toBeDefined()
321
- })
291
+ expect(mockSpawn).toBeDefined();
292
+ });
322
293
 
323
294
  it('should offer to install opencode if not found', () => {
324
295
  // Mock opencode not installed
325
- mockSpawn.mockImplementation((command: string, args: string[]) => {
326
- if (command === 'opencode' && args[0] === '--version') {
296
+ mockSpawn.mockImplementation((command: string, arguments_: string[]) => {
297
+ if (command === 'opencode' && arguments_[0] === '--version') {
327
298
  return {
328
299
  on: vi.fn().mockImplementation((event, callback) => {
329
- if (event === 'close') callback(1) // Non-zero exit code
300
+ if (event === 'close') callback(1); // Non-zero exit code
330
301
  }),
331
- }
302
+ };
332
303
  }
333
- return { on: vi.fn() }
334
- })
304
+ return { on: vi.fn() };
305
+ });
335
306
 
336
307
  // Should handle the case where opencode is not installed
337
- expect(mockSpawn).toBeDefined()
338
- })
308
+ expect(mockSpawn).toBeDefined();
309
+ });
339
310
 
340
311
  it('should install opencode via npm if user agrees', () => {
341
312
  // Should spawn npm install -g opencode-ai
342
- expect(mockSpawn).toBeDefined()
343
- })
344
- })
313
+ expect(mockSpawn).toBeDefined();
314
+ });
315
+ });
345
316
 
346
317
  describe('automation support', () => {
347
318
  it('should support -y flag for automated initialization', () => {
348
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
349
- const initCommand = codeCommand?.commands.find(
350
- (cmd) => cmd.name() === 'init',
351
- )
319
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
320
+ const initCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'init');
352
321
 
353
- expect(initCommand).toBeDefined()
322
+ expect(initCommand).toBeDefined();
354
323
 
355
- const yesOption = initCommand?.options.find((opt) => opt.long === '--yes')
356
- expect(yesOption).toBeDefined()
357
- expect(yesOption?.description).toContain('automation')
358
- })
324
+ const yesOption = initCommand?.options.find((opt) => opt.long === '--yes');
325
+ expect(yesOption).toBeDefined();
326
+ expect(yesOption?.description).toContain('automation');
327
+ });
359
328
 
360
329
  it('should support -y flag for automated run', () => {
361
- const codeCommand = program.commands.find((cmd) => cmd.name() === 'code')
362
- const runCommand = codeCommand?.commands.find(
363
- (cmd) => cmd.name() === 'run',
364
- )
330
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
331
+ const runCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'run');
365
332
 
366
- expect(runCommand).toBeDefined()
333
+ expect(runCommand).toBeDefined();
367
334
 
368
- const yesOption = runCommand?.options.find((opt) => opt.long === '--yes')
369
- expect(yesOption).toBeDefined()
370
- expect(yesOption?.description).toContain('automation')
371
- })
335
+ const yesOption = runCommand?.options.find((opt) => opt.long === '--yes');
336
+ expect(yesOption).toBeDefined();
337
+ expect(yesOption?.description).toContain('automation');
338
+ });
372
339
 
373
340
  it('should use BERGET_API_KEY environment variable in automation mode', () => {
374
341
  // Test that environment variable is used when -y flag is set
375
- process.env.BERGET_API_KEY = 'test-env-key'
342
+ process.env.BERGET_API_KEY = 'test-env-key';
376
343
 
377
- expect(process.env.BERGET_API_KEY).toBe('test-env-key')
344
+ expect(process.env.BERGET_API_KEY).toBe('test-env-key');
378
345
 
379
346
  // Clean up
380
- delete process.env.BERGET_API_KEY
381
- })
382
- })
347
+ delete process.env.BERGET_API_KEY;
348
+ });
349
+ });
383
350
 
384
351
  describe('.env file handling', () => {
385
- let mockUpdateEnvFile: any
352
+ let mockUpdateEnvironmentFile: any;
386
353
 
387
354
  beforeEach(() => {
388
- mockUpdateEnvFile = vi.mocked(updateEnvFile)
389
- })
355
+ mockUpdateEnvironmentFile = vi.mocked(updateEnvironmentFile);
356
+ });
390
357
 
391
358
  it('should call updateEnvFile when creating new project', async () => {
392
- mockUpdateEnvFile.mockResolvedValue(true)
393
- mockFs.existsSync.mockReturnValue(false) // .env doesn't exist
394
- mockFsPromises.writeFile.mockResolvedValue(undefined)
359
+ mockUpdateEnvironmentFile.mockResolvedValue(true);
360
+ mockFs.existsSync.mockReturnValue(false); // .env doesn't exist
361
+ mockFsPromises.writeFile.mockResolvedValue();
395
362
 
396
363
  // This would be tested by actually calling the init command
397
364
  // For now we verify the mock is properly set up
398
- expect(mockUpdateEnvFile).toBeDefined()
399
- })
365
+ expect(mockUpdateEnvironmentFile).toBeDefined();
366
+ });
400
367
 
401
368
  it('should not overwrite existing BERGET_API_KEY in .env', async () => {
402
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
369
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
403
370
 
404
371
  // Mock existing .env with BERGET_API_KEY
405
- mockFs.existsSync.mockReturnValue(true)
406
- mockFs.readFileSync.mockReturnValue(
407
- 'BERGET_API_KEY=existing_key\nOTHER_KEY=value\n',
408
- )
372
+ mockFs.existsSync.mockReturnValue(true);
373
+ mockFs.readFileSync.mockReturnValue('BERGET_API_KEY=existing_key\nOTHER_KEY=value\n');
409
374
 
410
375
  // Mock updateEnvFile to simulate the check
411
- mockUpdateEnvFile.mockImplementation(async (options: any) => {
376
+ mockUpdateEnvironmentFile.mockImplementation(async (options: any) => {
412
377
  if (options.key === 'BERGET_API_KEY' && !options.force) {
413
- console.log(
414
- `⚠ ${options.key} already exists in .env - leaving unchanged`,
415
- )
416
- return false
378
+ console.log(`⚠ ${options.key} already exists in .env - leaving unchanged`);
379
+ return false;
417
380
  }
418
- return true
419
- })
381
+ return true;
382
+ });
420
383
 
421
- await updateEnvFile({
384
+ await updateEnvironmentFile({
422
385
  key: 'BERGET_API_KEY',
423
386
  value: 'new_key',
424
- })
387
+ });
425
388
 
426
389
  expect(consoleSpy).toHaveBeenCalledWith(
427
- expect.stringContaining(
428
- 'BERGET_API_KEY already exists in .env - leaving unchanged',
429
- ),
430
- )
390
+ expect.stringContaining('BERGET_API_KEY already exists in .env - leaving unchanged'),
391
+ );
431
392
 
432
- consoleSpy.mockRestore()
433
- })
393
+ consoleSpy.mockRestore();
394
+ });
434
395
 
435
396
  it('should add new key to existing .env file', async () => {
436
- mockFs.existsSync.mockReturnValue(true)
437
- mockFs.readFileSync.mockReturnValue('EXISTING_KEY=value\n')
438
- mockUpdateEnvFile.mockResolvedValue(true)
397
+ mockFs.existsSync.mockReturnValue(true);
398
+ mockFs.readFileSync.mockReturnValue('EXISTING_KEY=value\n');
399
+ mockUpdateEnvironmentFile.mockResolvedValue(true);
439
400
 
440
- await updateEnvFile({
401
+ await updateEnvironmentFile({
402
+ comment: 'Berget AI Configuration',
441
403
  key: 'BERGET_API_KEY',
442
404
  value: 'new_api_key',
443
- comment: 'Berget AI Configuration',
444
- })
405
+ });
445
406
 
446
- expect(mockUpdateEnvFile).toHaveBeenCalledWith({
407
+ expect(mockUpdateEnvironmentFile).toHaveBeenCalledWith({
408
+ comment: 'Berget AI Configuration',
447
409
  key: 'BERGET_API_KEY',
448
410
  value: 'new_api_key',
449
- comment: 'Berget AI Configuration',
450
- })
451
- })
411
+ });
412
+ });
452
413
 
453
414
  it('should create new .env file when none exists', async () => {
454
- mockFs.existsSync.mockReturnValue(false)
455
- mockUpdateEnvFile.mockResolvedValue(true)
415
+ mockFs.existsSync.mockReturnValue(false);
416
+ mockUpdateEnvironmentFile.mockResolvedValue(true);
456
417
 
457
- await updateEnvFile({
418
+ await updateEnvironmentFile({
458
419
  key: 'BERGET_API_KEY',
459
420
  value: 'new_api_key',
460
- })
421
+ });
461
422
 
462
- expect(mockUpdateEnvFile).toHaveBeenCalledWith({
423
+ expect(mockUpdateEnvironmentFile).toHaveBeenCalledWith({
463
424
  key: 'BERGET_API_KEY',
464
425
  value: 'new_api_key',
465
- })
466
- })
467
- })
426
+ });
427
+ });
428
+ });
468
429
 
469
430
  describe('error handling', () => {
470
431
  it('should handle API key creation failures', () => {
471
432
  // Mock API key service to throw error
472
- mockApiKeyService.create.mockRejectedValue(new Error('API Error'))
433
+ mockApiKeyService.create.mockRejectedValue(new Error('API Error'));
473
434
 
474
- expect(mockApiKeyService.create).toBeDefined()
475
- })
435
+ expect(mockApiKeyService.create).toBeDefined();
436
+ });
476
437
 
477
438
  it('should handle file system errors', () => {
478
439
  // Mock file operations to throw errors
479
- mockFsPromises.writeFile.mockRejectedValue(new Error('File write error'))
440
+ mockFsPromises.writeFile.mockRejectedValue(new Error('File write error'));
480
441
 
481
- expect(mockFsPromises.writeFile).toBeDefined()
482
- })
442
+ expect(mockFsPromises.writeFile).toBeDefined();
443
+ });
483
444
 
484
445
  it('should handle spawn errors', () => {
485
446
  // Mock spawn to throw error
486
447
  mockSpawn.mockImplementation(() => {
487
- throw new Error('Command not found')
488
- })
448
+ throw new Error('Command not found');
449
+ });
489
450
 
490
- expect(mockSpawn).toBeDefined()
491
- })
451
+ expect(mockSpawn).toBeDefined();
452
+ });
492
453
 
493
454
  it('should handle .env update failures', async () => {
494
- const mockUpdateEnvFile = vi.mocked(updateEnvFile)
495
- mockUpdateEnvFile.mockRejectedValue(new Error('Env update failed'))
455
+ const mockUpdateEnvironmentFile = vi.mocked(updateEnvironmentFile);
456
+ mockUpdateEnvironmentFile.mockRejectedValue(new Error('Env update failed'));
496
457
 
497
458
  await expect(
498
- updateEnvFile({
459
+ updateEnvironmentFile({
499
460
  key: 'TEST_KEY',
500
461
  value: 'test_value',
501
462
  }),
502
- ).rejects.toThrow('Env update failed')
503
- })
504
- })
463
+ ).rejects.toThrow('Env update failed');
464
+ });
465
+ });
505
466
 
506
467
  describe('experimental features', () => {
507
- let originalEnv: string | undefined
468
+ let originalEnvironment: string | undefined;
508
469
 
509
470
  beforeEach(() => {
510
- originalEnv = process.env.BERGET_EXPERIMENTAL
511
- })
471
+ originalEnvironment = process.env.BERGET_EXPERIMENTAL;
472
+ });
512
473
 
513
474
  afterEach(() => {
514
- if (originalEnv === undefined) {
515
- delete process.env.BERGET_EXPERIMENTAL
475
+ if (originalEnvironment === undefined) {
476
+ delete process.env.BERGET_EXPERIMENTAL;
516
477
  } else {
517
- process.env.BERGET_EXPERIMENTAL = originalEnv
478
+ process.env.BERGET_EXPERIMENTAL = originalEnvironment;
518
479
  }
519
- })
480
+ });
520
481
 
521
482
  it('should NOT show setup command when BERGET_EXPERIMENTAL is not set', () => {
522
- delete process.env.BERGET_EXPERIMENTAL
483
+ delete process.env.BERGET_EXPERIMENTAL;
523
484
 
524
- const freshProgram = new Command()
525
- registerCodeCommands(freshProgram)
485
+ const freshProgram = new Command();
486
+ registerCodeCommands(freshProgram);
526
487
 
527
- const codeCommand = freshProgram.commands.find((cmd) => cmd.name() === 'code')
528
- const setupCommand = codeCommand?.commands.find(
529
- (cmd) => cmd.name() === 'setup',
530
- )
488
+ const codeCommand = freshProgram.commands.find((cmd) => cmd.name() === 'code');
489
+ const setupCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'setup');
531
490
 
532
- expect(setupCommand).toBeUndefined()
533
- })
491
+ expect(setupCommand).toBeUndefined();
492
+ });
534
493
 
535
494
  it('should show setup command when BERGET_EXPERIMENTAL is set', () => {
536
- process.env.BERGET_EXPERIMENTAL = '1'
537
-
538
- const freshProgram = new Command()
539
- registerCodeCommands(freshProgram)
540
-
541
- const codeCommand = freshProgram.commands.find((cmd) => cmd.name() === 'code')
542
- const setupCommand = codeCommand?.commands.find(
543
- (cmd) => cmd.name() === 'setup',
544
- )
545
-
546
- expect(setupCommand).toBeDefined()
547
- expect(setupCommand?.description()).toBe(
548
- 'Interactive setup for Berget AI coding tools',
549
- )
550
- })
551
- })
552
- })
495
+ process.env.BERGET_EXPERIMENTAL = '1';
496
+
497
+ const freshProgram = new Command();
498
+ registerCodeCommands(freshProgram);
499
+
500
+ const codeCommand = freshProgram.commands.find((cmd) => cmd.name() === 'code');
501
+ const setupCommand = codeCommand?.commands.find((cmd) => cmd.name() === 'setup');
502
+
503
+ expect(setupCommand).toBeDefined();
504
+ expect(setupCommand?.description()).toBe('Interactive setup for Berget AI coding tools');
505
+ });
506
+ });
507
+ });