berget 2.2.5 → 2.2.7

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