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