berget 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +184 -0
  3. package/TODO.md +2 -0
  4. package/blog-post.md +176 -0
  5. package/dist/index.js +11 -8
  6. package/dist/package.json +7 -2
  7. package/dist/src/commands/api-keys.js +4 -2
  8. package/dist/src/commands/chat.js +21 -11
  9. package/dist/src/commands/code.js +1424 -0
  10. package/dist/src/commands/index.js +2 -0
  11. package/dist/src/constants/command-structure.js +12 -0
  12. package/dist/src/schemas/opencode-schema.json +1121 -0
  13. package/dist/src/services/cluster-service.js +1 -1
  14. package/dist/src/utils/default-api-key.js +2 -2
  15. package/dist/src/utils/env-manager.js +86 -0
  16. package/dist/src/utils/error-handler.js +10 -3
  17. package/dist/src/utils/markdown-renderer.js +4 -4
  18. package/dist/src/utils/opencode-validator.js +122 -0
  19. package/dist/src/utils/token-manager.js +2 -2
  20. package/dist/tests/commands/chat.test.js +20 -18
  21. package/dist/tests/commands/code.test.js +414 -0
  22. package/dist/tests/utils/env-manager.test.js +148 -0
  23. package/dist/tests/utils/opencode-validator.test.js +103 -0
  24. package/index.ts +67 -32
  25. package/opencode.json +182 -0
  26. package/package.json +7 -2
  27. package/src/client.ts +20 -20
  28. package/src/commands/api-keys.ts +93 -60
  29. package/src/commands/auth.ts +4 -2
  30. package/src/commands/billing.ts +6 -3
  31. package/src/commands/chat.ts +149 -107
  32. package/src/commands/clusters.ts +2 -2
  33. package/src/commands/code.ts +1696 -0
  34. package/src/commands/index.ts +2 -0
  35. package/src/commands/models.ts +3 -3
  36. package/src/commands/users.ts +2 -2
  37. package/src/constants/command-structure.ts +112 -58
  38. package/src/schemas/opencode-schema.json +991 -0
  39. package/src/services/api-key-service.ts +1 -1
  40. package/src/services/auth-service.ts +27 -25
  41. package/src/services/chat-service.ts +26 -23
  42. package/src/services/cluster-service.ts +5 -5
  43. package/src/services/collaborator-service.ts +3 -3
  44. package/src/services/flux-service.ts +2 -2
  45. package/src/services/helm-service.ts +2 -2
  46. package/src/services/kubectl-service.ts +3 -6
  47. package/src/types/api.d.ts +1032 -1010
  48. package/src/types/json.d.ts +3 -3
  49. package/src/utils/default-api-key.ts +54 -42
  50. package/src/utils/env-manager.ts +98 -0
  51. package/src/utils/error-handler.ts +24 -15
  52. package/src/utils/logger.ts +12 -12
  53. package/src/utils/markdown-renderer.ts +18 -18
  54. package/src/utils/opencode-validator.ts +134 -0
  55. package/src/utils/token-manager.ts +35 -23
  56. package/tests/commands/chat.test.ts +43 -31
  57. package/tests/commands/code.test.ts +505 -0
  58. package/tests/utils/env-manager.test.ts +199 -0
  59. package/tests/utils/opencode-validator.test.ts +118 -0
  60. package/tsconfig.json +8 -8
  61. package/-27b-it +0 -0
  62. package/examples/README.md +0 -95
  63. package/examples/ai-review.sh +0 -30
  64. package/examples/install-global-security-hook.sh +0 -170
  65. package/examples/security-check.sh +0 -102
  66. package/examples/smart-commit.sh +0 -26
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ const vitest_1 = require("vitest");
36
+ const commander_1 = require("commander");
37
+ const code_1 = require("../../src/commands/code");
38
+ const api_key_service_1 = require("../../src/services/api-key-service");
39
+ const fs = __importStar(require("fs"));
40
+ const promises_1 = require("fs/promises");
41
+ const env_manager_1 = require("../../src/utils/env-manager");
42
+ // Mock dependencies
43
+ vitest_1.vi.mock('../../src/services/api-key-service');
44
+ vitest_1.vi.mock('fs', () => ({
45
+ default: {
46
+ existsSync: vitest_1.vi.fn(),
47
+ readFileSync: vitest_1.vi.fn(),
48
+ },
49
+ }));
50
+ vitest_1.vi.mock('fs/promises', () => ({
51
+ readFile: vitest_1.vi.fn(),
52
+ writeFile: vitest_1.vi.fn(),
53
+ }));
54
+ vitest_1.vi.mock('../../src/utils/env-manager');
55
+ vitest_1.vi.mock('child_process', () => ({
56
+ spawn: vitest_1.vi.fn(),
57
+ }));
58
+ vitest_1.vi.mock('readline', () => ({
59
+ createInterface: vitest_1.vi.fn(() => ({
60
+ question: vitest_1.vi.fn(),
61
+ close: vitest_1.vi.fn(),
62
+ })),
63
+ }));
64
+ (0, vitest_1.describe)('Code Commands', () => {
65
+ let program;
66
+ let mockApiKeyService;
67
+ let mockFs;
68
+ let mockFsPromises;
69
+ let mockSpawn;
70
+ (0, vitest_1.beforeEach)(() => {
71
+ program = new commander_1.Command();
72
+ // Mock ApiKeyService
73
+ mockApiKeyService = {
74
+ create: vitest_1.vi.fn(),
75
+ list: vitest_1.vi.fn(),
76
+ rotate: vitest_1.vi.fn(),
77
+ };
78
+ vitest_1.vi.mocked(api_key_service_1.ApiKeyService.getInstance).mockReturnValue(mockApiKeyService);
79
+ // Mock fs
80
+ mockFs = vitest_1.vi.mocked(fs);
81
+ mockFs.existsSync = vitest_1.vi.fn();
82
+ mockFs.readFileSync = vitest_1.vi.fn();
83
+ // Mock fs/promises
84
+ mockFsPromises = vitest_1.vi.mocked({ readFile: promises_1.readFile, writeFile: promises_1.writeFile });
85
+ mockFsPromises.readFile = vitest_1.vi.fn();
86
+ mockFsPromises.writeFile = vitest_1.vi.fn();
87
+ // Mock spawn
88
+ mockSpawn = vitest_1.vi.fn();
89
+ vitest_1.vi.doMock('child_process', () => ({ spawn: mockSpawn }));
90
+ (0, code_1.registerCodeCommands)(program);
91
+ });
92
+ (0, vitest_1.afterEach)(() => {
93
+ vitest_1.vi.clearAllMocks();
94
+ });
95
+ (0, vitest_1.describe)('code init command', () => {
96
+ (0, vitest_1.it)('should register init command with correct description', () => {
97
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
98
+ const initCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'init');
99
+ (0, vitest_1.expect)(initCommand).toBeDefined();
100
+ (0, vitest_1.expect)(initCommand === null || initCommand === void 0 ? void 0 : initCommand.description()).toBe('Initialize project for AI coding assistant');
101
+ });
102
+ (0, vitest_1.it)('should have name, force, and yes options', () => {
103
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
104
+ const initCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'init');
105
+ (0, vitest_1.expect)(initCommand).toBeDefined();
106
+ const nameOption = initCommand === null || initCommand === void 0 ? void 0 : initCommand.options.find((opt) => opt.long === '--name');
107
+ const forceOption = initCommand === null || initCommand === void 0 ? void 0 : initCommand.options.find((opt) => opt.long === '--force');
108
+ const yesOption = initCommand === null || initCommand === void 0 ? void 0 : initCommand.options.find((opt) => opt.long === '--yes');
109
+ (0, vitest_1.expect)(nameOption).toBeDefined();
110
+ (0, vitest_1.expect)(nameOption === null || nameOption === void 0 ? void 0 : nameOption.description).toContain('Project name');
111
+ (0, vitest_1.expect)(forceOption).toBeDefined();
112
+ (0, vitest_1.expect)(forceOption === null || forceOption === void 0 ? void 0 : forceOption.description).toContain('Overwrite existing configuration');
113
+ (0, vitest_1.expect)(yesOption).toBeDefined();
114
+ (0, vitest_1.expect)(yesOption === null || yesOption === void 0 ? void 0 : yesOption.description).toContain('Automatically answer yes');
115
+ });
116
+ (0, vitest_1.it)('should check if opencode is installed', () => {
117
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
118
+ const initCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'init');
119
+ (0, vitest_1.expect)(initCommand).toBeDefined();
120
+ // The command should attempt to spawn opencode --version
121
+ // This is tested implicitly through the spawn mock
122
+ });
123
+ (0, vitest_1.it)('should list existing API keys and allow selection', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ // Mock successful opencode installation check
125
+ mockSpawn.mockImplementation((command, args) => {
126
+ if (command === 'opencode' && args[0] === '--version') {
127
+ return {
128
+ on: vitest_1.vi.fn().mockImplementation((event, callback) => {
129
+ if (event === 'close')
130
+ callback(0);
131
+ }),
132
+ };
133
+ }
134
+ return { on: vitest_1.vi.fn() };
135
+ });
136
+ // Mock existing API keys
137
+ const mockExistingKeys = [
138
+ {
139
+ id: 1,
140
+ name: 'existing-key-1',
141
+ prefix: 'sk_ber',
142
+ created: '2023-01-01T00:00:00.000Z',
143
+ lastUsed: null,
144
+ },
145
+ {
146
+ id: 2,
147
+ name: 'existing-key-2',
148
+ prefix: 'sk_ber',
149
+ created: '2023-01-02T00:00:00.000Z',
150
+ lastUsed: '2023-01-03T00:00:00.000Z',
151
+ },
152
+ ];
153
+ mockApiKeyService.list.mockResolvedValue(mockExistingKeys);
154
+ // Mock file operations
155
+ mockFs.existsSync.mockReturnValue(false);
156
+ mockFsPromises.writeFile.mockResolvedValue(undefined);
157
+ // Verify that the list method is called
158
+ (0, vitest_1.expect)(mockApiKeyService.list).toBeDefined();
159
+ }));
160
+ (0, vitest_1.it)('should create new API key with project-based naming', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ // Mock successful opencode installation check
162
+ mockSpawn.mockImplementation((command, args) => {
163
+ if (command === 'opencode' && args[0] === '--version') {
164
+ return {
165
+ on: vitest_1.vi.fn().mockImplementation((event, callback) => {
166
+ if (event === 'close')
167
+ callback(0);
168
+ }),
169
+ };
170
+ }
171
+ return { on: vitest_1.vi.fn() };
172
+ });
173
+ // Mock no existing keys
174
+ mockApiKeyService.list.mockResolvedValue([]);
175
+ // Mock successful API key creation
176
+ const mockApiKeyData = {
177
+ id: 123,
178
+ name: 'opencode-testproject-1234567890',
179
+ key: 'test-api-key-12345',
180
+ };
181
+ mockApiKeyService.create.mockResolvedValue(mockApiKeyData);
182
+ // Mock file operations
183
+ mockFs.existsSync.mockReturnValue(false);
184
+ mockFsPromises.writeFile.mockResolvedValue(undefined);
185
+ // Verify that the create method is available
186
+ (0, vitest_1.expect)(mockApiKeyService.create).toBeDefined();
187
+ }));
188
+ (0, vitest_1.it)('should create opencode.json with correct structure', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ // This tests the expected config structure
190
+ const expectedConfig = {
191
+ model: 'berget/deepseek-r1',
192
+ apiKey: 'test-api-key',
193
+ projectName: 'testproject',
194
+ provider: 'berget',
195
+ created: vitest_1.expect.any(String),
196
+ version: '1.0.0',
197
+ };
198
+ (0, vitest_1.expect)(expectedConfig.model).toBe('berget/deepseek-r1');
199
+ (0, vitest_1.expect)(expectedConfig.provider).toBe('berget');
200
+ (0, vitest_1.expect)(expectedConfig.version).toBe('1.0.0');
201
+ }));
202
+ (0, vitest_1.it)('should handle existing config file', () => {
203
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
204
+ const initCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'init');
205
+ (0, vitest_1.expect)(initCommand).toBeDefined();
206
+ // Should check if opencode.json exists before proceeding
207
+ (0, vitest_1.expect)(mockFs.existsSync).toBeDefined();
208
+ });
209
+ });
210
+ (0, vitest_1.describe)('code run command', () => {
211
+ (0, vitest_1.it)('should register run command with correct description', () => {
212
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
213
+ const runCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'run');
214
+ (0, vitest_1.expect)(runCommand).toBeDefined();
215
+ (0, vitest_1.expect)(runCommand === null || runCommand === void 0 ? void 0 : runCommand.description()).toBe('Run AI coding assistant');
216
+ });
217
+ (0, vitest_1.it)('should accept prompt argument and model, no-config, and yes options', () => {
218
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
219
+ const runCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'run');
220
+ (0, vitest_1.expect)(runCommand).toBeDefined();
221
+ const modelOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--model');
222
+ const noConfigOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--no-config');
223
+ const yesOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--yes');
224
+ (0, vitest_1.expect)(modelOption).toBeDefined();
225
+ (0, vitest_1.expect)(modelOption === null || modelOption === void 0 ? void 0 : modelOption.description).toContain('Model to use');
226
+ (0, vitest_1.expect)(noConfigOption).toBeDefined();
227
+ (0, vitest_1.expect)(noConfigOption === null || noConfigOption === void 0 ? void 0 : noConfigOption.description).toContain('Run without loading project config');
228
+ (0, vitest_1.expect)(yesOption).toBeDefined();
229
+ (0, vitest_1.expect)(yesOption === null || yesOption === void 0 ? void 0 : yesOption.description).toContain('Automatically answer yes');
230
+ });
231
+ (0, vitest_1.it)('should load configuration from opencode.json', () => __awaiter(void 0, void 0, void 0, function* () {
232
+ const mockConfig = {
233
+ model: 'berget/deepseek-r1',
234
+ apiKey: 'test-api-key',
235
+ projectName: 'testproject',
236
+ provider: 'berget',
237
+ created: '2023-01-01T00:00:00.000Z',
238
+ version: '1.0.0',
239
+ };
240
+ // Mock file exists and contains config
241
+ mockFs.existsSync.mockReturnValue(true);
242
+ mockFsPromises.readFile.mockResolvedValue(JSON.stringify(mockConfig));
243
+ // Mock successful opencode check
244
+ mockSpawn.mockImplementation((command, args) => {
245
+ if (command === 'opencode' && args[0] === '--version') {
246
+ return {
247
+ on: vitest_1.vi.fn().mockImplementation((event, callback) => {
248
+ if (event === 'close')
249
+ callback(0);
250
+ }),
251
+ };
252
+ }
253
+ return { on: vitest_1.vi.fn() };
254
+ });
255
+ // Verify config structure expectations
256
+ (0, vitest_1.expect)(mockConfig.model).toBe('berget/deepseek-r1');
257
+ (0, vitest_1.expect)(mockConfig.apiKey).toBe('test-api-key');
258
+ (0, vitest_1.expect)(mockConfig.projectName).toBe('testproject');
259
+ }));
260
+ (0, vitest_1.it)('should spawn opencode with correct arguments', () => {
261
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
262
+ const runCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'run');
263
+ (0, vitest_1.expect)(runCommand).toBeDefined();
264
+ // Should spawn opencode with appropriate arguments
265
+ (0, vitest_1.expect)(mockSpawn).toBeDefined();
266
+ });
267
+ (0, vitest_1.it)('should handle missing configuration file', () => {
268
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
269
+ const runCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'run');
270
+ (0, vitest_1.expect)(runCommand).toBeDefined();
271
+ // Should check if opencode.json exists
272
+ (0, vitest_1.expect)(mockFs.existsSync).toBeDefined();
273
+ });
274
+ });
275
+ (0, vitest_1.describe)('opencode installation', () => {
276
+ (0, vitest_1.it)('should check if opencode is installed', () => {
277
+ // The spawn function should be called with opencode --version
278
+ (0, vitest_1.expect)(mockSpawn).toBeDefined();
279
+ });
280
+ (0, vitest_1.it)('should offer to install opencode if not found', () => {
281
+ // Mock opencode not installed
282
+ mockSpawn.mockImplementation((command, args) => {
283
+ if (command === 'opencode' && args[0] === '--version') {
284
+ return {
285
+ on: vitest_1.vi.fn().mockImplementation((event, callback) => {
286
+ if (event === 'close')
287
+ callback(1); // Non-zero exit code
288
+ }),
289
+ };
290
+ }
291
+ return { on: vitest_1.vi.fn() };
292
+ });
293
+ // Should handle the case where opencode is not installed
294
+ (0, vitest_1.expect)(mockSpawn).toBeDefined();
295
+ });
296
+ (0, vitest_1.it)('should install opencode via npm if user agrees', () => {
297
+ // Should spawn npm install -g opencode-ai
298
+ (0, vitest_1.expect)(mockSpawn).toBeDefined();
299
+ });
300
+ });
301
+ (0, vitest_1.describe)('automation support', () => {
302
+ (0, vitest_1.it)('should support -y flag for automated initialization', () => {
303
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
304
+ const initCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'init');
305
+ (0, vitest_1.expect)(initCommand).toBeDefined();
306
+ const yesOption = initCommand === null || initCommand === void 0 ? void 0 : initCommand.options.find((opt) => opt.long === '--yes');
307
+ (0, vitest_1.expect)(yesOption).toBeDefined();
308
+ (0, vitest_1.expect)(yesOption === null || yesOption === void 0 ? void 0 : yesOption.description).toContain('automation');
309
+ });
310
+ (0, vitest_1.it)('should support -y flag for automated run', () => {
311
+ const codeCommand = program.commands.find((cmd) => cmd.name() === 'code');
312
+ const runCommand = codeCommand === null || codeCommand === void 0 ? void 0 : codeCommand.commands.find((cmd) => cmd.name() === 'run');
313
+ (0, vitest_1.expect)(runCommand).toBeDefined();
314
+ const yesOption = runCommand === null || runCommand === void 0 ? void 0 : runCommand.options.find((opt) => opt.long === '--yes');
315
+ (0, vitest_1.expect)(yesOption).toBeDefined();
316
+ (0, vitest_1.expect)(yesOption === null || yesOption === void 0 ? void 0 : yesOption.description).toContain('automation');
317
+ });
318
+ (0, vitest_1.it)('should use BERGET_API_KEY environment variable in automation mode', () => {
319
+ // Test that environment variable is used when -y flag is set
320
+ process.env.BERGET_API_KEY = 'test-env-key';
321
+ (0, vitest_1.expect)(process.env.BERGET_API_KEY).toBe('test-env-key');
322
+ // Clean up
323
+ delete process.env.BERGET_API_KEY;
324
+ });
325
+ });
326
+ (0, vitest_1.describe)('.env file handling', () => {
327
+ let mockUpdateEnvFile;
328
+ (0, vitest_1.beforeEach)(() => {
329
+ mockUpdateEnvFile = vitest_1.vi.mocked(env_manager_1.updateEnvFile);
330
+ });
331
+ (0, vitest_1.it)('should call updateEnvFile when creating new project', () => __awaiter(void 0, void 0, void 0, function* () {
332
+ mockUpdateEnvFile.mockResolvedValue(true);
333
+ mockFs.existsSync.mockReturnValue(false); // .env doesn't exist
334
+ mockFsPromises.writeFile.mockResolvedValue(undefined);
335
+ // This would be tested by actually calling the init command
336
+ // For now we verify the mock is properly set up
337
+ (0, vitest_1.expect)(mockUpdateEnvFile).toBeDefined();
338
+ }));
339
+ (0, vitest_1.it)('should not overwrite existing BERGET_API_KEY in .env', () => __awaiter(void 0, void 0, void 0, function* () {
340
+ const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { });
341
+ // Mock existing .env with BERGET_API_KEY
342
+ mockFs.existsSync.mockReturnValue(true);
343
+ mockFs.readFileSync.mockReturnValue('BERGET_API_KEY=existing_key\nOTHER_KEY=value\n');
344
+ // Mock updateEnvFile to simulate the check
345
+ mockUpdateEnvFile.mockImplementation((options) => __awaiter(void 0, void 0, void 0, function* () {
346
+ if (options.key === 'BERGET_API_KEY' && !options.force) {
347
+ console.log(`⚠ ${options.key} already exists in .env - leaving unchanged`);
348
+ return false;
349
+ }
350
+ return true;
351
+ }));
352
+ yield (0, env_manager_1.updateEnvFile)({
353
+ key: 'BERGET_API_KEY',
354
+ value: 'new_key',
355
+ });
356
+ (0, vitest_1.expect)(consoleSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('BERGET_API_KEY already exists in .env - leaving unchanged'));
357
+ consoleSpy.mockRestore();
358
+ }));
359
+ (0, vitest_1.it)('should add new key to existing .env file', () => __awaiter(void 0, void 0, void 0, function* () {
360
+ mockFs.existsSync.mockReturnValue(true);
361
+ mockFs.readFileSync.mockReturnValue('EXISTING_KEY=value\n');
362
+ mockUpdateEnvFile.mockResolvedValue(true);
363
+ yield (0, env_manager_1.updateEnvFile)({
364
+ key: 'BERGET_API_KEY',
365
+ value: 'new_api_key',
366
+ comment: 'Berget AI Configuration',
367
+ });
368
+ (0, vitest_1.expect)(mockUpdateEnvFile).toHaveBeenCalledWith({
369
+ key: 'BERGET_API_KEY',
370
+ value: 'new_api_key',
371
+ comment: 'Berget AI Configuration',
372
+ });
373
+ }));
374
+ (0, vitest_1.it)('should create new .env file when none exists', () => __awaiter(void 0, void 0, void 0, function* () {
375
+ mockFs.existsSync.mockReturnValue(false);
376
+ mockUpdateEnvFile.mockResolvedValue(true);
377
+ yield (0, env_manager_1.updateEnvFile)({
378
+ key: 'BERGET_API_KEY',
379
+ value: 'new_api_key',
380
+ });
381
+ (0, vitest_1.expect)(mockUpdateEnvFile).toHaveBeenCalledWith({
382
+ key: 'BERGET_API_KEY',
383
+ value: 'new_api_key',
384
+ });
385
+ }));
386
+ });
387
+ (0, vitest_1.describe)('error handling', () => {
388
+ (0, vitest_1.it)('should handle API key creation failures', () => {
389
+ // Mock API key service to throw error
390
+ mockApiKeyService.create.mockRejectedValue(new Error('API Error'));
391
+ (0, vitest_1.expect)(mockApiKeyService.create).toBeDefined();
392
+ });
393
+ (0, vitest_1.it)('should handle file system errors', () => {
394
+ // Mock file operations to throw errors
395
+ mockFsPromises.writeFile.mockRejectedValue(new Error('File write error'));
396
+ (0, vitest_1.expect)(mockFsPromises.writeFile).toBeDefined();
397
+ });
398
+ (0, vitest_1.it)('should handle spawn errors', () => {
399
+ // Mock spawn to throw error
400
+ mockSpawn.mockImplementation(() => {
401
+ throw new Error('Command not found');
402
+ });
403
+ (0, vitest_1.expect)(mockSpawn).toBeDefined();
404
+ });
405
+ (0, vitest_1.it)('should handle .env update failures', () => __awaiter(void 0, void 0, void 0, function* () {
406
+ const mockUpdateEnvFile = vitest_1.vi.mocked(env_manager_1.updateEnvFile);
407
+ mockUpdateEnvFile.mockRejectedValue(new Error('Env update failed'));
408
+ yield (0, vitest_1.expect)((0, env_manager_1.updateEnvFile)({
409
+ key: 'TEST_KEY',
410
+ value: 'test_value',
411
+ })).rejects.toThrow('Env update failed');
412
+ }));
413
+ });
414
+ });
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const vitest_1 = require("vitest");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const promises_1 = require("fs/promises");
18
+ const path_1 = __importDefault(require("path"));
19
+ const env_manager_1 = require("../../src/utils/env-manager");
20
+ vitest_1.vi.mock('fs');
21
+ vitest_1.vi.mock('fs/promises');
22
+ vitest_1.vi.mock('path');
23
+ const mockFs = vitest_1.vi.mocked(fs_1.default);
24
+ const mockWriteFile = vitest_1.vi.mocked(promises_1.writeFile);
25
+ const mockPath = vitest_1.vi.mocked(path_1.default);
26
+ (0, vitest_1.describe)('env-manager', () => {
27
+ const testEnvPath = '/test/.env';
28
+ const testCwd = '/test';
29
+ (0, vitest_1.beforeEach)(() => {
30
+ vitest_1.vi.clearAllMocks();
31
+ mockPath.join.mockReturnValue(testEnvPath);
32
+ vitest_1.vi.spyOn(process, 'cwd').mockReturnValue(testCwd);
33
+ });
34
+ (0, vitest_1.afterEach)(() => {
35
+ vitest_1.vi.restoreAllMocks();
36
+ });
37
+ (0, vitest_1.describe)('updateEnvFile', () => {
38
+ (0, vitest_1.it)('should create a new .env file with the key when file does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
39
+ mockFs.existsSync.mockReturnValue(false);
40
+ yield (0, env_manager_1.updateEnvFile)({
41
+ key: 'TEST_KEY',
42
+ value: 'test_value',
43
+ comment: 'Test comment',
44
+ });
45
+ (0, vitest_1.expect)(mockFs.existsSync).toHaveBeenCalledWith(testEnvPath);
46
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(testEnvPath, '# Test comment\nTEST_KEY=test_value\n');
47
+ }));
48
+ (0, vitest_1.it)('should append to existing .env file when key does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
49
+ const existingContent = 'EXISTING_KEY=existing_value\n';
50
+ mockFs.existsSync.mockReturnValue(true);
51
+ mockFs.readFileSync.mockReturnValue(existingContent);
52
+ yield (0, env_manager_1.updateEnvFile)({
53
+ key: 'NEW_KEY',
54
+ value: 'new_value',
55
+ comment: 'Test comment',
56
+ });
57
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(testEnvPath, 'EXISTING_KEY=existing_value\nNEW_KEY=new_value\n');
58
+ }));
59
+ (0, vitest_1.it)('should not update when key already exists and force is false', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ const existingContent = 'EXISTING_KEY=existing_value\nTEST_KEY=old_value\n';
61
+ mockFs.existsSync.mockReturnValue(true);
62
+ mockFs.readFileSync.mockReturnValue(existingContent);
63
+ const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { });
64
+ yield (0, env_manager_1.updateEnvFile)({
65
+ key: 'TEST_KEY',
66
+ value: 'new_value',
67
+ });
68
+ (0, vitest_1.expect)(consoleSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('TEST_KEY already exists in .env - leaving unchanged'));
69
+ (0, vitest_1.expect)(mockWriteFile).not.toHaveBeenCalled();
70
+ consoleSpy.mockRestore();
71
+ }));
72
+ (0, vitest_1.it)('should update existing key when force is true', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ const existingContent = 'EXISTING_KEY=existing_value\nTEST_KEY=old_value\n';
74
+ mockFs.existsSync.mockReturnValue(true);
75
+ mockFs.readFileSync.mockReturnValue(existingContent);
76
+ yield (0, env_manager_1.updateEnvFile)({
77
+ key: 'TEST_KEY',
78
+ value: 'new_value',
79
+ force: true,
80
+ });
81
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(testEnvPath, 'EXISTING_KEY=existing_value\nTEST_KEY=new_value\n');
82
+ }));
83
+ (0, vitest_1.it)('should handle complex values with quotes and special characters', () => __awaiter(void 0, void 0, void 0, function* () {
84
+ mockFs.existsSync.mockReturnValue(false);
85
+ yield (0, env_manager_1.updateEnvFile)({
86
+ key: 'COMPLEX_KEY',
87
+ value: 'value with "quotes" and $special',
88
+ comment: 'Complex test',
89
+ });
90
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(testEnvPath, '# Complex test\nCOMPLEX_KEY=value with "quotes" and $special\n');
91
+ }));
92
+ (0, vitest_1.it)('should use custom env path when provided', () => __awaiter(void 0, void 0, void 0, function* () {
93
+ const customPath = '/custom/.env';
94
+ mockFs.existsSync.mockReturnValue(false);
95
+ yield (0, env_manager_1.updateEnvFile)({
96
+ envPath: customPath,
97
+ key: 'TEST_KEY',
98
+ value: 'test_value',
99
+ });
100
+ (0, vitest_1.expect)(mockFs.existsSync).toHaveBeenCalledWith(customPath);
101
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(customPath, 'TEST_KEY=test_value\n');
102
+ }));
103
+ (0, vitest_1.it)('should throw error when write fails', () => __awaiter(void 0, void 0, void 0, function* () {
104
+ mockFs.existsSync.mockReturnValue(false);
105
+ mockWriteFile.mockRejectedValue(new Error('Write error'));
106
+ yield (0, vitest_1.expect)((0, env_manager_1.updateEnvFile)({
107
+ key: 'TEST_KEY',
108
+ value: 'test_value',
109
+ })).rejects.toThrow('Write error');
110
+ }));
111
+ });
112
+ (0, vitest_1.describe)('hasEnvKey', () => {
113
+ (0, vitest_1.it)('should return false when .env file does not exist', () => {
114
+ mockFs.existsSync.mockReturnValue(false);
115
+ const result = (0, env_manager_1.hasEnvKey)(testEnvPath, 'TEST_KEY');
116
+ (0, vitest_1.expect)(result).toBe(false);
117
+ (0, vitest_1.expect)(mockFs.existsSync).toHaveBeenCalledWith(testEnvPath);
118
+ (0, vitest_1.expect)(mockFs.readFileSync).not.toHaveBeenCalled();
119
+ });
120
+ (0, vitest_1.it)('should return true when key exists in .env file', () => {
121
+ const existingContent = 'KEY1=value1\nTEST_KEY=test_value\nKEY2=value2\n';
122
+ mockFs.existsSync.mockReturnValue(true);
123
+ mockFs.readFileSync.mockReturnValue(existingContent);
124
+ const result = (0, env_manager_1.hasEnvKey)(testEnvPath, 'TEST_KEY');
125
+ (0, vitest_1.expect)(result).toBe(true);
126
+ });
127
+ (0, vitest_1.it)('should return false when key does not exist in .env file', () => {
128
+ const existingContent = 'KEY1=value1\nKEY2=value2\n';
129
+ mockFs.existsSync.mockReturnValue(true);
130
+ mockFs.readFileSync.mockReturnValue(existingContent);
131
+ const result = (0, env_manager_1.hasEnvKey)(testEnvPath, 'TEST_KEY');
132
+ (0, vitest_1.expect)(result).toBe(false);
133
+ });
134
+ (0, vitest_1.it)('should return false when .env file is malformed', () => {
135
+ mockFs.existsSync.mockReturnValue(true);
136
+ mockFs.readFileSync.mockImplementation(() => {
137
+ throw new Error('Read error');
138
+ });
139
+ const result = (0, env_manager_1.hasEnvKey)(testEnvPath, 'TEST_KEY');
140
+ (0, vitest_1.expect)(result).toBe(false);
141
+ });
142
+ (0, vitest_1.it)('should use default path when not provided', () => {
143
+ mockFs.existsSync.mockReturnValue(false);
144
+ (0, env_manager_1.hasEnvKey)(undefined, 'TEST_KEY');
145
+ (0, vitest_1.expect)(mockFs.existsSync).toHaveBeenCalledWith(testEnvPath);
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const opencode_validator_1 = require("../../src/utils/opencode-validator");
5
+ const fs_1 = require("fs");
6
+ (0, vitest_1.describe)('OpenCode Validator', () => {
7
+ (0, vitest_1.it)('should validate a correct OpenCode configuration', () => {
8
+ const validConfig = {
9
+ $schema: 'https://opencode.ai/config.json',
10
+ username: 'test-user',
11
+ model: 'gpt-4',
12
+ agent: {
13
+ test: {
14
+ model: 'gpt-4',
15
+ temperature: 0.7,
16
+ prompt: 'Test agent',
17
+ permission: {
18
+ edit: 'allow',
19
+ bash: 'allow',
20
+ webfetch: 'allow',
21
+ },
22
+ },
23
+ },
24
+ };
25
+ const result = (0, opencode_validator_1.validateOpenCodeConfig)(validConfig);
26
+ (0, vitest_1.expect)(result.valid).toBe(true);
27
+ (0, vitest_1.expect)(result.errors).toBeUndefined();
28
+ });
29
+ (0, vitest_1.it)('should reject invalid configuration', () => {
30
+ const invalidConfig = {
31
+ username: 123, // Should be string
32
+ model: 'gpt-4',
33
+ agent: {
34
+ test: {
35
+ model: 'gpt-4',
36
+ temperature: 'high', // Should be number
37
+ prompt: 'Test agent',
38
+ permission: {
39
+ edit: 'invalid', // Should be enum value
40
+ bash: 'allow',
41
+ webfetch: 'allow',
42
+ },
43
+ },
44
+ },
45
+ };
46
+ const result = (0, opencode_validator_1.validateOpenCodeConfig)(invalidConfig);
47
+ (0, vitest_1.expect)(result.valid).toBe(false);
48
+ (0, vitest_1.expect)(result.errors).toBeDefined();
49
+ (0, vitest_1.expect)(result.errors.length).toBeGreaterThan(0);
50
+ });
51
+ (0, vitest_1.it)('should fix common configuration issues', () => {
52
+ const configWithIssues = {
53
+ username: 'test-user',
54
+ model: 'gpt-4',
55
+ tools: {
56
+ compact: { threshold: 80000 }, // Should be boolean
57
+ },
58
+ maxTokens: 4000, // Invalid property
59
+ provider: {
60
+ berget: {
61
+ models: {
62
+ 'test-model': {
63
+ name: 'Test Model',
64
+ maxTokens: 4000, // Should be moved to limit.context
65
+ contextWindow: 8000, // Should be moved to limit.context
66
+ },
67
+ },
68
+ },
69
+ },
70
+ };
71
+ const fixed = (0, opencode_validator_1.fixOpenCodeConfig)(configWithIssues);
72
+ // tools.compact should be boolean
73
+ (0, vitest_1.expect)(typeof fixed.tools.compact).toBe('boolean');
74
+ // maxTokens should be removed
75
+ (0, vitest_1.expect)(fixed.maxTokens).toBeUndefined();
76
+ // maxTokens and contextWindow should be moved to limit.context
77
+ (0, vitest_1.expect)(fixed.provider.berget.models['test-model'].limit).toBeDefined();
78
+ (0, vitest_1.expect)(fixed.provider.berget.models['test-model'].limit.context).toBe(8000);
79
+ (0, vitest_1.expect)(fixed.provider.berget.models['test-model'].maxTokens).toBeUndefined();
80
+ (0, vitest_1.expect)(fixed.provider.berget.models['test-model'].contextWindow).toBeUndefined();
81
+ });
82
+ (0, vitest_1.it)('should validate the current opencode.json file', () => {
83
+ var _a;
84
+ try {
85
+ const currentConfig = JSON.parse((0, fs_1.readFileSync)('opencode.json', 'utf8'));
86
+ // Apply fixes to handle common issues
87
+ const fixedConfig = (0, opencode_validator_1.fixOpenCodeConfig)(currentConfig);
88
+ // Validate the fixed config
89
+ const result = (0, opencode_validator_1.validateOpenCodeConfig)(fixedConfig);
90
+ // The fixed config should be valid according to the JSON Schema
91
+ (0, vitest_1.expect)(result.valid).toBe(true);
92
+ if (!result.valid) {
93
+ console.log('Fixed opencode.json validation errors:');
94
+ (_a = result.errors) === null || _a === void 0 ? void 0 : _a.forEach((err) => console.log(` - ${err}`));
95
+ }
96
+ }
97
+ catch (error) {
98
+ // If we can't read the file, that's ok for this test
99
+ console.log('Could not read opencode.json for testing:', error);
100
+ vitest_1.expect.fail('Should be able to read opencode.json');
101
+ }
102
+ });
103
+ });