berget 2.2.7 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -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 +195 -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 +7 -7
- 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 +50 -30
- package/index.ts +30 -31
- package/package.json +5 -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 +190 -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 +169 -170
- 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 +6 -6
- 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,142 +1,375 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { AuthService } from
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
1
|
+
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import type { ApiKeyServicePort, AuthServicePort } from './ports/auth-services';
|
|
5
|
+
import type { CommandRunner } from './ports/command-runner';
|
|
6
|
+
import type { FileStore } from './ports/file-store';
|
|
7
|
+
import type { Prompter } from './ports/prompter';
|
|
8
|
+
|
|
9
|
+
import { getAllAgents, toMarkdown, toPiPrompt } from '../../agents/index.js';
|
|
10
|
+
import { ApiKeyService } from '../../services/api-key-service.js';
|
|
11
|
+
import { AuthService } from '../../services/auth-service.js';
|
|
12
|
+
import { ClackPrompter } from './adapters/clack-prompter.js';
|
|
13
|
+
import { FsFileStore } from './adapters/fs-file-store.js';
|
|
14
|
+
import { SpawnCommandRunner } from './adapters/spawn-command-runner.js';
|
|
15
|
+
import { configureAuth } from './auth-sync.js';
|
|
16
|
+
import { CancelledError, CommandFailedError, PrerequisiteError } from './errors';
|
|
17
|
+
|
|
18
|
+
const OPENCODE_PLUGIN = '@bergetai/opencode-auth';
|
|
19
|
+
const PI_PROVIDER = 'npm:@bergetai/pi-provider';
|
|
20
|
+
const OPENCODE_PLUGIN_NAME = '@bergetai/opencode-auth';
|
|
21
|
+
const PI_PROVIDER_NAME = '@bergetai/pi-provider';
|
|
20
22
|
|
|
21
23
|
export interface WizardDeps {
|
|
22
|
-
prompter: Prompter;
|
|
23
|
-
files: FileStore;
|
|
24
|
-
commands: CommandRunner;
|
|
25
|
-
authService: AuthServicePort;
|
|
26
24
|
apiKeyService: ApiKeyServicePort;
|
|
27
|
-
|
|
25
|
+
authService: AuthServicePort;
|
|
26
|
+
commands: CommandRunner;
|
|
28
27
|
cwd: string;
|
|
28
|
+
files: FileStore;
|
|
29
|
+
homeDir: string;
|
|
30
|
+
prompter: Prompter;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function runSetup(deps: WizardDeps): Promise<void> {
|
|
32
|
-
const {
|
|
34
|
+
const { apiKeyService, authService, commands, cwd, files, homeDir, prompter } = deps;
|
|
33
35
|
|
|
34
|
-
prompter.intro(
|
|
36
|
+
prompter.intro('\uD83D\uDD27 Berget Code Setup');
|
|
35
37
|
|
|
36
38
|
const ocState = await getOpencodeState(files, homeDir, cwd);
|
|
37
39
|
const piState = await getPiState(files, homeDir, cwd);
|
|
38
40
|
|
|
39
|
-
const tool = await prompter.select<
|
|
40
|
-
message:
|
|
41
|
+
const tool = await prompter.select<'opencode' | 'pi'>({
|
|
42
|
+
message: 'How do you want to use Berget AI?',
|
|
41
43
|
options: [
|
|
42
44
|
{
|
|
43
|
-
|
|
45
|
+
hint: 'Open source AI coding agent',
|
|
44
46
|
label: `OpenCode${getOpencodeLabel(ocState)}`,
|
|
45
|
-
|
|
47
|
+
value: 'opencode',
|
|
46
48
|
},
|
|
47
49
|
{
|
|
48
|
-
|
|
50
|
+
hint: 'Minimal terminal coding harness',
|
|
49
51
|
label: `Pi${getPiLabel(piState)}`,
|
|
50
|
-
|
|
52
|
+
value: 'pi',
|
|
51
53
|
},
|
|
52
54
|
],
|
|
53
55
|
});
|
|
54
56
|
|
|
55
|
-
const scope = await prompter.select<
|
|
56
|
-
message:
|
|
57
|
+
const scope = await prompter.select<'global' | 'project'>({
|
|
58
|
+
message: 'Where should the configuration apply?',
|
|
57
59
|
options: [
|
|
58
60
|
{
|
|
59
|
-
value: "project",
|
|
60
|
-
label: "This project only",
|
|
61
61
|
hint:
|
|
62
|
-
tool ===
|
|
62
|
+
tool === 'opencode'
|
|
63
63
|
? ocState.project
|
|
64
|
-
?
|
|
65
|
-
:
|
|
64
|
+
? 'Already configured'
|
|
65
|
+
: 'opencode.json in current directory'
|
|
66
66
|
: piState.project
|
|
67
|
-
?
|
|
68
|
-
:
|
|
67
|
+
? 'Already configured'
|
|
68
|
+
: '.pi/settings.json in current directory',
|
|
69
|
+
label: 'This project only',
|
|
70
|
+
value: 'project',
|
|
69
71
|
},
|
|
70
72
|
{
|
|
71
|
-
value: "global",
|
|
72
|
-
label: "Globally for all projects",
|
|
73
73
|
hint:
|
|
74
|
-
tool ===
|
|
74
|
+
tool === 'opencode'
|
|
75
75
|
? ocState.global
|
|
76
|
-
?
|
|
77
|
-
:
|
|
76
|
+
? 'Already configured'
|
|
77
|
+
: '~/.config/opencode/opencode.json'
|
|
78
78
|
: piState.global
|
|
79
|
-
?
|
|
80
|
-
:
|
|
79
|
+
? 'Already configured'
|
|
80
|
+
: '~/.pi/agent/settings.json',
|
|
81
|
+
label: 'Globally for all projects',
|
|
82
|
+
value: 'global',
|
|
81
83
|
},
|
|
82
84
|
],
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
const authResult = await configureAuth(
|
|
86
|
-
{
|
|
87
|
-
tool
|
|
88
|
+
{ apiKeyService, authService, files, homeDir, prompter },
|
|
89
|
+
tool,
|
|
88
90
|
);
|
|
89
91
|
|
|
90
|
-
if (tool ===
|
|
91
|
-
await setupOpenCode({
|
|
92
|
-
await setupOpenCodeAgents({
|
|
92
|
+
if (tool === 'opencode') {
|
|
93
|
+
await setupOpenCode({ commands, cwd, files, homeDir, prompter, scope });
|
|
94
|
+
await setupOpenCodeAgents({ cwd, files, homeDir, prompter, scope });
|
|
93
95
|
|
|
94
96
|
if (authResult.authenticated) {
|
|
95
97
|
prompter.note(
|
|
96
98
|
`You're all set!\n\n1. Run: opencode\n2. Select model: /models\n\nFor more information, see official docs:\n\nhttps://github.com/berget-ai/opencode-berget-auth`,
|
|
97
|
-
|
|
99
|
+
'Successfully configured Berget AI for OpenCode',
|
|
98
100
|
);
|
|
99
101
|
} else {
|
|
100
102
|
prompter.note(
|
|
101
103
|
`Next steps:\n\n1. Run: opencode\n2. Type: /connect\n3. Choose your auth method:\n • "Login with Berget" — Berget Code plan\n • "Enter Berget API Key manually"\n • (or set BERGET_API_KEY env var)\n4. Select model: /models\n\nFor more information, see official docs:\n\nhttps://github.com/berget-ai/opencode-berget-auth`,
|
|
102
|
-
|
|
104
|
+
'Successfully configured Berget AI for OpenCode',
|
|
103
105
|
);
|
|
104
106
|
}
|
|
105
107
|
} else {
|
|
106
|
-
await setupPi({
|
|
107
|
-
await setupPiAgent({
|
|
108
|
+
await setupPi({ commands, cwd, files, homeDir, prompter, scope });
|
|
109
|
+
await setupPiAgent({ cwd, files, homeDir, prompter, scope });
|
|
108
110
|
|
|
109
111
|
if (authResult.authenticated) {
|
|
110
112
|
prompter.note(
|
|
111
113
|
`You're all set!\n\n1. Restart Pi or run /reload\n2. Select model: /model\n\nFor more information, see official docs:\n\nhttps://github.com/berget-ai/pi-provider`,
|
|
112
|
-
|
|
114
|
+
'Successfully configured Berget AI for Pi',
|
|
113
115
|
);
|
|
114
116
|
} else {
|
|
115
117
|
prompter.note(
|
|
116
118
|
`Next steps:\n\n1. Restart Pi or run /reload\n2. Type: /login\n3. Choose your auth method:\n • "Use a subscription" → Berget AI\n • (or set BERGET_API_KEY env var)\n4. Select model: /model\n\nFor more information, see official docs:\n\nhttps://github.com/berget-ai/pi-provider`,
|
|
117
|
-
|
|
119
|
+
'Successfully configured Berget AI for Pi',
|
|
118
120
|
);
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
prompter.outro(
|
|
124
|
+
prompter.outro('Setup complete!');
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
// ─── OpenCode ────────────────────────────────────────────────────────────────
|
|
126
128
|
|
|
129
|
+
export async function runSetupCommand(): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
await runSetup({
|
|
132
|
+
apiKeyService: ApiKeyService.getInstance(),
|
|
133
|
+
authService: AuthService.getInstance(),
|
|
134
|
+
commands: new SpawnCommandRunner(),
|
|
135
|
+
cwd: process.cwd(),
|
|
136
|
+
files: new FsFileStore(),
|
|
137
|
+
homeDir: os.homedir(),
|
|
138
|
+
prompter: new ClackPrompter(),
|
|
139
|
+
});
|
|
140
|
+
process.exit(0);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof CancelledError) {
|
|
143
|
+
process.exit(130);
|
|
144
|
+
}
|
|
145
|
+
if (error instanceof PrerequisiteError) {
|
|
146
|
+
console.error(`Missing required binary: ${error.binary}`);
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
if (error instanceof CommandFailedError) {
|
|
150
|
+
console.error(error.message);
|
|
151
|
+
process.exit(5);
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Pi ────────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function generateDiff(oldText: string, newText: string, filePath: string): string {
|
|
160
|
+
const oldLines = oldText.split('\n');
|
|
161
|
+
const newLines = newText.split('\n');
|
|
162
|
+
let result = `--- ${filePath}\n+++ ${filePath}\n`;
|
|
163
|
+
|
|
164
|
+
const maxLength = Math.max(oldLines.length, newLines.length);
|
|
165
|
+
for (let index = 0; index < maxLength; index++) {
|
|
166
|
+
const oldLine = oldLines[index];
|
|
167
|
+
const newLine = newLines[index];
|
|
168
|
+
if (oldLine !== newLine) {
|
|
169
|
+
if (oldLine !== undefined) result += `- ${oldLine}\n`;
|
|
170
|
+
if (newLine !== undefined) result += `+ ${newLine}\n`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result.trimEnd();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function generateModifiedContent(existingContent: null | string, configPath: string): string {
|
|
177
|
+
if (configPath.endsWith('.jsonc')) {
|
|
178
|
+
const content = existingContent || '{}';
|
|
179
|
+
const parseErrors: any[] = [];
|
|
180
|
+
const parsed = parse(content, parseErrors, {
|
|
181
|
+
allowTrailingComma: true,
|
|
182
|
+
disallowComments: false,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
let jsConfig: Record<string, any> = {};
|
|
186
|
+
const canModifyText =
|
|
187
|
+
parsed !== undefined &&
|
|
188
|
+
typeof parsed === 'object' &&
|
|
189
|
+
parsed !== null &&
|
|
190
|
+
!Array.isArray(parsed);
|
|
191
|
+
|
|
192
|
+
if (canModifyText) {
|
|
193
|
+
jsConfig = parsed as Record<string, any>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pluginsKey = jsConfig.plugins === undefined ? 'plugin' : 'plugins';
|
|
197
|
+
const existing: string[] = jsConfig[pluginsKey] || [];
|
|
198
|
+
const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME));
|
|
199
|
+
filtered.push(OPENCODE_PLUGIN);
|
|
200
|
+
|
|
201
|
+
if (canModifyText) {
|
|
202
|
+
let modifiedContent = content;
|
|
203
|
+
const pluginEdits = modify(modifiedContent, [pluginsKey], filtered, {
|
|
204
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
205
|
+
});
|
|
206
|
+
modifiedContent = applyEdits(modifiedContent, pluginEdits);
|
|
207
|
+
|
|
208
|
+
if (!jsConfig.$schema) {
|
|
209
|
+
const schemaEdits = modify(
|
|
210
|
+
modifiedContent,
|
|
211
|
+
['$schema'],
|
|
212
|
+
'https://opencode.ai/config.json',
|
|
213
|
+
{
|
|
214
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
modifiedContent = applyEdits(modifiedContent, schemaEdits);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return modifiedContent;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Malformed, empty, or non-object JSONC — write a clean config
|
|
224
|
+
const config: Record<string, any> = {
|
|
225
|
+
$schema: 'https://opencode.ai/config.json',
|
|
226
|
+
[pluginsKey]: filtered,
|
|
227
|
+
};
|
|
228
|
+
return JSON.stringify(config, null, 2) + '\n';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Plain JSON
|
|
232
|
+
let config: Record<string, any> = {};
|
|
233
|
+
if (existingContent) {
|
|
234
|
+
try {
|
|
235
|
+
config = JSON.parse(existingContent);
|
|
236
|
+
} catch {
|
|
237
|
+
// ignore malformed, overwrite
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pluginsKey = config.plugins === undefined ? 'plugin' : 'plugins';
|
|
242
|
+
const existing: string[] = config[pluginsKey] || [];
|
|
243
|
+
const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME));
|
|
244
|
+
filtered.push(OPENCODE_PLUGIN);
|
|
245
|
+
config[pluginsKey] = filtered;
|
|
246
|
+
config.$schema = config.$schema || 'https://opencode.ai/config.json';
|
|
247
|
+
|
|
248
|
+
return JSON.stringify(config, null, 2) + '\n';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getOpencodeLabel(state: { global: boolean; project: boolean }): string {
|
|
252
|
+
if (state.project || state.global) return ' (already configured)';
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
async function getOpencodeState(
|
|
259
|
+
files: FileStore,
|
|
260
|
+
homeDir: string,
|
|
261
|
+
cwd: string,
|
|
262
|
+
): Promise<{ global: boolean; project: boolean }> {
|
|
263
|
+
const projectJsonc = await readJsonMaybe(files, pathJoin(cwd, 'opencode.jsonc'));
|
|
264
|
+
const projectJson = await readJsonMaybe(files, pathJoin(cwd, 'opencode.json'));
|
|
265
|
+
const globalJsonc = await readJsonMaybe(
|
|
266
|
+
files,
|
|
267
|
+
pathJoin(homeDir, '.config', 'opencode', 'opencode.jsonc'),
|
|
268
|
+
);
|
|
269
|
+
const globalJson = await readJsonMaybe(
|
|
270
|
+
files,
|
|
271
|
+
pathJoin(homeDir, '.config', 'opencode', 'opencode.json'),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
global: (await hasPluginInConfig(globalJsonc)) || (await hasPluginInConfig(globalJson)),
|
|
276
|
+
project: (await hasPluginInConfig(projectJsonc)) || (await hasPluginInConfig(projectJson)),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getPiLabel(state: { global: boolean; project: boolean }): string {
|
|
281
|
+
if (state.project || state.global) return ' (already configured)';
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function getPiState(
|
|
286
|
+
files: FileStore,
|
|
287
|
+
homeDir: string,
|
|
288
|
+
cwd: string,
|
|
289
|
+
): Promise<{ global: boolean; project: boolean }> {
|
|
290
|
+
const projectSettings = await readJsonMaybe(files, pathJoin(cwd, '.pi', 'settings.json'));
|
|
291
|
+
const globalSettings = await readJsonMaybe(
|
|
292
|
+
files,
|
|
293
|
+
pathJoin(homeDir, '.pi', 'agent', 'settings.json'),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
global: await hasPiProviderInSettings(globalSettings),
|
|
298
|
+
project: await hasPiProviderInSettings(projectSettings),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function hasPiProviderInSettings(settings: any): Promise<boolean> {
|
|
303
|
+
if (!settings) return false;
|
|
304
|
+
const packages = settings.packages || [];
|
|
305
|
+
return packages.some((p: any) => {
|
|
306
|
+
if (typeof p === 'string') return p.includes(PI_PROVIDER_NAME);
|
|
307
|
+
if (typeof p === 'object' && p.source) return p.source.includes(PI_PROVIDER_NAME);
|
|
308
|
+
return false;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function hasPluginInConfig(config: any): Promise<boolean> {
|
|
313
|
+
if (!config) return false;
|
|
314
|
+
const plugins = config.plugin || config.plugins || [];
|
|
315
|
+
return plugins.some((p: string) => p.includes(OPENCODE_PLUGIN_NAME));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function pathJoin(...parts: string[]): string {
|
|
319
|
+
// Simple path join that avoids importing 'path' module
|
|
320
|
+
// This is good enough for cross-platform testing since tests control the path format
|
|
321
|
+
return parts.join('/');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function readJsonMaybe(files: FileStore, filePath: string): Promise<any | null> {
|
|
325
|
+
const content = await files.readFile(filePath);
|
|
326
|
+
if (!content) return null;
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(content);
|
|
329
|
+
} catch {
|
|
330
|
+
try {
|
|
331
|
+
return JSON.parse(stripJsoncComments(content));
|
|
332
|
+
} catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function resolveOpencodeConfigPath(
|
|
339
|
+
files: FileStore,
|
|
340
|
+
homeDir: string,
|
|
341
|
+
cwd: string,
|
|
342
|
+
scope: 'global' | 'project',
|
|
343
|
+
): Promise<string> {
|
|
344
|
+
if (scope === 'project') {
|
|
345
|
+
const jsoncPath = pathJoin(cwd, 'opencode.jsonc');
|
|
346
|
+
const jsonPath = pathJoin(cwd, 'opencode.json');
|
|
347
|
+
if (await files.exists(jsoncPath)) return jsoncPath;
|
|
348
|
+
if (await files.exists(jsonPath)) return jsonPath;
|
|
349
|
+
return jsonPath;
|
|
350
|
+
} else {
|
|
351
|
+
const globalDir = pathJoin(homeDir, '.config', 'opencode');
|
|
352
|
+
const jsoncPath = pathJoin(globalDir, 'opencode.jsonc');
|
|
353
|
+
const jsonPath = pathJoin(globalDir, 'opencode.json');
|
|
354
|
+
if (await files.exists(jsoncPath)) return jsoncPath;
|
|
355
|
+
if (await files.exists(jsonPath)) return jsonPath;
|
|
356
|
+
return jsonPath;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
127
360
|
async function setupOpenCode(deps: {
|
|
128
|
-
prompter: Prompter;
|
|
129
|
-
files: FileStore;
|
|
130
361
|
commands: CommandRunner;
|
|
131
|
-
homeDir: string;
|
|
132
362
|
cwd: string;
|
|
133
|
-
|
|
363
|
+
files: FileStore;
|
|
364
|
+
homeDir: string;
|
|
365
|
+
prompter: Prompter;
|
|
366
|
+
scope: 'global' | 'project';
|
|
134
367
|
}): Promise<void> {
|
|
135
|
-
const {
|
|
368
|
+
const { commands, cwd, files, homeDir, prompter, scope } = deps;
|
|
136
369
|
|
|
137
|
-
const installed = await commands.checkInstalled(
|
|
370
|
+
const installed = await commands.checkInstalled('opencode');
|
|
138
371
|
if (!installed) {
|
|
139
|
-
throw new PrerequisiteError(
|
|
372
|
+
throw new PrerequisiteError('opencode');
|
|
140
373
|
}
|
|
141
374
|
|
|
142
375
|
const configPath = await resolveOpencodeConfigPath(files, homeDir, cwd, scope);
|
|
@@ -148,105 +381,44 @@ async function setupOpenCode(deps: {
|
|
|
148
381
|
}
|
|
149
382
|
|
|
150
383
|
if (existingContent) {
|
|
151
|
-
prompter.note(generateDiff(existingContent, newContent, configPath),
|
|
384
|
+
prompter.note(generateDiff(existingContent, newContent, configPath), 'Changes to be written');
|
|
152
385
|
} else {
|
|
153
|
-
prompter.note(`New config at ${configPath}:\n\n${newContent}`,
|
|
386
|
+
prompter.note(`New config at ${configPath}:\n\n${newContent}`, 'Config preview');
|
|
154
387
|
}
|
|
155
388
|
|
|
156
389
|
const shouldWrite = await prompter.confirm({
|
|
157
|
-
message: existingContent ? `Write these changes to ${configPath}?` : `Create ${configPath}?`,
|
|
158
390
|
initialValue: true,
|
|
391
|
+
message: existingContent ? `Write these changes to ${configPath}?` : `Create ${configPath}?`,
|
|
159
392
|
});
|
|
160
393
|
if (!shouldWrite) throw new CancelledError();
|
|
161
394
|
|
|
162
395
|
const s = prompter.spinner();
|
|
163
|
-
s.start(
|
|
396
|
+
s.start('Writing OpenCode configuration...');
|
|
164
397
|
await files.writeFile(configPath, newContent);
|
|
165
398
|
s.stop(`Wrote configuration to ${configPath}.`);
|
|
166
399
|
}
|
|
167
400
|
|
|
168
|
-
// ─── Pi ────────────────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
async function setupPi(deps: {
|
|
171
|
-
prompter: Prompter;
|
|
172
|
-
files: FileStore;
|
|
173
|
-
commands: CommandRunner;
|
|
174
|
-
homeDir: string;
|
|
175
|
-
cwd: string;
|
|
176
|
-
scope: "project" | "global";
|
|
177
|
-
}): Promise<void> {
|
|
178
|
-
const { prompter, files, commands, homeDir, cwd, scope } = deps;
|
|
179
|
-
const s = prompter.spinner();
|
|
180
|
-
|
|
181
|
-
const installed = await commands.checkInstalled("pi");
|
|
182
|
-
if (!installed) {
|
|
183
|
-
throw new PrerequisiteError("pi");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const installArgs =
|
|
187
|
-
scope === "project" ? ["install", "-l", PI_PROVIDER] : ["install", PI_PROVIDER];
|
|
188
|
-
|
|
189
|
-
s.start(`Installing Berget AI provider for Pi...`);
|
|
190
|
-
try {
|
|
191
|
-
await commands.run("pi", installArgs);
|
|
192
|
-
s.stop("Installed Pi provider.");
|
|
193
|
-
} catch {
|
|
194
|
-
s.stop("Pi provider installation failed. Please try again or install manually.");
|
|
195
|
-
throw new CommandFailedError(`pi ${installArgs.join(" ")}`, 1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const settingsPath =
|
|
199
|
-
scope === "project"
|
|
200
|
-
? pathJoin(cwd, ".pi", "settings.json")
|
|
201
|
-
: pathJoin(homeDir, ".pi", "agent", "settings.json");
|
|
202
|
-
|
|
203
|
-
let settings = (await readJsonMaybe(files, settingsPath)) || {};
|
|
204
|
-
|
|
205
|
-
if (settings.defaultProvider === "berget") {
|
|
206
|
-
prompter.note(
|
|
207
|
-
"Berget AI is already set as your default provider.",
|
|
208
|
-
"Default provider already set"
|
|
209
|
-
);
|
|
210
|
-
} else {
|
|
211
|
-
if (settings.defaultProvider) {
|
|
212
|
-
const makeDefault = await prompter.confirm({
|
|
213
|
-
message: `Your default provider is ${settings.defaultProvider}. Switch to Berget AI instead?`,
|
|
214
|
-
initialValue: false,
|
|
215
|
-
});
|
|
216
|
-
if (makeDefault) {
|
|
217
|
-
settings.defaultProvider = "berget";
|
|
218
|
-
await writeJsonFile(files, settingsPath, settings);
|
|
219
|
-
prompter.note("Berget AI is now your default provider.", "Updated default provider");
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
settings.defaultProvider = "berget";
|
|
223
|
-
await writeJsonFile(files, settingsPath, settings);
|
|
224
|
-
prompter.note("Berget AI is now your default provider.", "Updated default provider");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
401
|
async function setupOpenCodeAgents(deps: {
|
|
230
|
-
|
|
402
|
+
cwd: string;
|
|
231
403
|
files: FileStore;
|
|
232
404
|
homeDir: string;
|
|
233
|
-
|
|
234
|
-
scope:
|
|
405
|
+
prompter: Prompter;
|
|
406
|
+
scope: 'global' | 'project';
|
|
235
407
|
}): Promise<void> {
|
|
236
|
-
const {
|
|
408
|
+
const { cwd, files, homeDir, prompter, scope } = deps;
|
|
237
409
|
|
|
238
|
-
const agents = getAllAgents().filter(a => a.config.mode ===
|
|
410
|
+
const agents = getAllAgents().filter((a) => a.config.mode === 'primary');
|
|
239
411
|
|
|
240
412
|
if (agents.length === 0) {
|
|
241
413
|
return;
|
|
242
414
|
}
|
|
243
415
|
|
|
244
416
|
const selectedAgents = await prompter.multiselect({
|
|
245
|
-
message:
|
|
246
|
-
options: agents.map(agent => ({
|
|
247
|
-
value: agent.config.name,
|
|
248
|
-
label: agent.config.name,
|
|
417
|
+
message: 'Select agents to set up (optional - press enter to skip):',
|
|
418
|
+
options: agents.map((agent) => ({
|
|
249
419
|
hint: agent.config.description,
|
|
420
|
+
label: agent.config.name,
|
|
421
|
+
value: agent.config.name,
|
|
250
422
|
})),
|
|
251
423
|
});
|
|
252
424
|
|
|
@@ -255,15 +427,15 @@ async function setupOpenCodeAgents(deps: {
|
|
|
255
427
|
}
|
|
256
428
|
|
|
257
429
|
const agentsDir =
|
|
258
|
-
scope ===
|
|
259
|
-
? pathJoin(cwd,
|
|
260
|
-
: pathJoin(homeDir,
|
|
430
|
+
scope === 'project'
|
|
431
|
+
? pathJoin(cwd, '.opencode', 'agents')
|
|
432
|
+
: pathJoin(homeDir, '.config', 'opencode', 'agents');
|
|
261
433
|
|
|
262
434
|
await files.mkdir(agentsDir);
|
|
263
435
|
|
|
264
436
|
const hasChanges = await Promise.all(
|
|
265
|
-
selectedAgents.map(async agentName => {
|
|
266
|
-
const agent = agents.find(a => a.config.name === agentName);
|
|
437
|
+
selectedAgents.map(async (agentName) => {
|
|
438
|
+
const agent = agents.find((a) => a.config.name === agentName);
|
|
267
439
|
if (!agent) return false;
|
|
268
440
|
|
|
269
441
|
const agentPath = pathJoin(agentsDir, `${agentName}.md`);
|
|
@@ -277,22 +449,22 @@ async function setupOpenCodeAgents(deps: {
|
|
|
277
449
|
if (existing) {
|
|
278
450
|
prompter.note(
|
|
279
451
|
generateDiff(existing, newContent, agentPath),
|
|
280
|
-
`Changes to ${agentName} agent
|
|
452
|
+
`Changes to ${agentName} agent`,
|
|
281
453
|
);
|
|
282
454
|
}
|
|
283
455
|
|
|
284
456
|
return true;
|
|
285
|
-
})
|
|
457
|
+
}),
|
|
286
458
|
);
|
|
287
459
|
|
|
288
460
|
if (!hasChanges.some(Boolean)) {
|
|
289
|
-
prompter.note(
|
|
461
|
+
prompter.note('Agent files are already up to date.', 'No changes needed');
|
|
290
462
|
return;
|
|
291
463
|
}
|
|
292
464
|
|
|
293
465
|
const shouldWrite = await prompter.confirm({
|
|
294
|
-
message: "Write agent configuration files?",
|
|
295
466
|
initialValue: true,
|
|
467
|
+
message: 'Write agent configuration files?',
|
|
296
468
|
});
|
|
297
469
|
|
|
298
470
|
if (!shouldWrite) {
|
|
@@ -300,10 +472,10 @@ async function setupOpenCodeAgents(deps: {
|
|
|
300
472
|
}
|
|
301
473
|
|
|
302
474
|
const s = prompter.spinner();
|
|
303
|
-
s.start(
|
|
475
|
+
s.start('Writing agent configurations...');
|
|
304
476
|
|
|
305
477
|
for (const agentName of selectedAgents) {
|
|
306
|
-
const agent = agents.find(a => a.config.name === agentName);
|
|
478
|
+
const agent = agents.find((a) => a.config.name === agentName);
|
|
307
479
|
if (!agent) continue;
|
|
308
480
|
|
|
309
481
|
const agentPath = pathJoin(agentsDir, `${agentName}.md`);
|
|
@@ -314,62 +486,121 @@ async function setupOpenCodeAgents(deps: {
|
|
|
314
486
|
s.stop(`Wrote ${selectedAgents.length} agent(s) to ${agentsDir}`);
|
|
315
487
|
}
|
|
316
488
|
|
|
317
|
-
async function
|
|
318
|
-
|
|
489
|
+
async function setupPi(deps: {
|
|
490
|
+
commands: CommandRunner;
|
|
491
|
+
cwd: string;
|
|
319
492
|
files: FileStore;
|
|
320
493
|
homeDir: string;
|
|
494
|
+
prompter: Prompter;
|
|
495
|
+
scope: 'global' | 'project';
|
|
496
|
+
}): Promise<void> {
|
|
497
|
+
const { commands, cwd, files, homeDir, prompter, scope } = deps;
|
|
498
|
+
const s = prompter.spinner();
|
|
499
|
+
|
|
500
|
+
const installed = await commands.checkInstalled('pi');
|
|
501
|
+
if (!installed) {
|
|
502
|
+
throw new PrerequisiteError('pi');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const installArguments =
|
|
506
|
+
scope === 'project' ? ['install', '-l', PI_PROVIDER] : ['install', PI_PROVIDER];
|
|
507
|
+
|
|
508
|
+
s.start(`Installing Berget AI provider for Pi...`);
|
|
509
|
+
try {
|
|
510
|
+
await commands.run('pi', installArguments);
|
|
511
|
+
s.stop('Installed Pi provider.');
|
|
512
|
+
} catch {
|
|
513
|
+
s.stop('Pi provider installation failed. Please try again or install manually.');
|
|
514
|
+
throw new CommandFailedError(`pi ${installArguments.join(' ')}`, 1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const settingsPath =
|
|
518
|
+
scope === 'project'
|
|
519
|
+
? pathJoin(cwd, '.pi', 'settings.json')
|
|
520
|
+
: pathJoin(homeDir, '.pi', 'agent', 'settings.json');
|
|
521
|
+
|
|
522
|
+
const settings = (await readJsonMaybe(files, settingsPath)) || {};
|
|
523
|
+
|
|
524
|
+
if (settings.defaultProvider === 'berget') {
|
|
525
|
+
prompter.note(
|
|
526
|
+
'Berget AI is already set as your default provider.',
|
|
527
|
+
'Default provider already set',
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
if (settings.defaultProvider) {
|
|
531
|
+
const makeDefault = await prompter.confirm({
|
|
532
|
+
initialValue: false,
|
|
533
|
+
message: `Your default provider is ${settings.defaultProvider}. Switch to Berget AI instead?`,
|
|
534
|
+
});
|
|
535
|
+
if (makeDefault) {
|
|
536
|
+
settings.defaultProvider = 'berget';
|
|
537
|
+
await writeJsonFile(files, settingsPath, settings);
|
|
538
|
+
prompter.note('Berget AI is now your default provider.', 'Updated default provider');
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
settings.defaultProvider = 'berget';
|
|
542
|
+
await writeJsonFile(files, settingsPath, settings);
|
|
543
|
+
prompter.note('Berget AI is now your default provider.', 'Updated default provider');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function setupPiAgent(deps: {
|
|
321
549
|
cwd: string;
|
|
322
|
-
|
|
550
|
+
files: FileStore;
|
|
551
|
+
homeDir: string;
|
|
552
|
+
prompter: Prompter;
|
|
553
|
+
scope: 'global' | 'project';
|
|
323
554
|
}): Promise<void> {
|
|
324
|
-
const {
|
|
555
|
+
const { cwd, files, homeDir, prompter, scope } = deps;
|
|
325
556
|
|
|
326
|
-
const agents = getAllAgents().filter(a => a.config.mode ===
|
|
557
|
+
const agents = getAllAgents().filter((a) => a.config.mode === 'primary');
|
|
327
558
|
|
|
328
559
|
if (agents.length === 0) {
|
|
329
560
|
return;
|
|
330
561
|
}
|
|
331
562
|
|
|
332
563
|
const selectedAgentName = await prompter.select({
|
|
333
|
-
message:
|
|
564
|
+
message: 'Select an agent (optional - press enter to skip):',
|
|
334
565
|
options: [
|
|
335
|
-
{
|
|
336
|
-
...agents.map(agent => ({
|
|
337
|
-
value: agent.config.name,
|
|
338
|
-
label: agent.config.name,
|
|
566
|
+
{ label: 'Skip agent setup', value: '__skip__' },
|
|
567
|
+
...agents.map((agent) => ({
|
|
339
568
|
hint: agent.config.description,
|
|
569
|
+
label: agent.config.name,
|
|
570
|
+
value: agent.config.name,
|
|
340
571
|
})),
|
|
341
572
|
],
|
|
342
573
|
});
|
|
343
574
|
|
|
344
|
-
if (selectedAgentName ===
|
|
575
|
+
if (selectedAgentName === '__skip__') {
|
|
345
576
|
return;
|
|
346
577
|
}
|
|
347
578
|
|
|
348
|
-
const agent = agents.find(a => a.config.name === selectedAgentName);
|
|
579
|
+
const agent = agents.find((a) => a.config.name === selectedAgentName);
|
|
349
580
|
if (!agent) return;
|
|
350
581
|
|
|
351
582
|
const systemPath =
|
|
352
|
-
scope ===
|
|
353
|
-
? pathJoin(cwd,
|
|
354
|
-
: pathJoin(homeDir,
|
|
583
|
+
scope === 'project'
|
|
584
|
+
? pathJoin(cwd, '.pi', 'SYSTEM.md')
|
|
585
|
+
: pathJoin(homeDir, '.pi', 'agent', 'SYSTEM.md');
|
|
355
586
|
|
|
356
587
|
const existing = await files.readFile(systemPath);
|
|
357
588
|
const newContent = toPiPrompt(agent);
|
|
358
589
|
|
|
359
590
|
if (existing === newContent) {
|
|
360
|
-
prompter.note(
|
|
591
|
+
prompter.note('Agent configuration is already up to date.', 'No changes needed');
|
|
361
592
|
return;
|
|
362
593
|
}
|
|
363
594
|
|
|
364
595
|
if (existing) {
|
|
365
|
-
prompter.note(generateDiff(existing, newContent, systemPath),
|
|
596
|
+
prompter.note(generateDiff(existing, newContent, systemPath), 'Changes to agent configuration');
|
|
366
597
|
} else {
|
|
367
|
-
prompter.note(newContent,
|
|
598
|
+
prompter.note(newContent, 'New agent configuration');
|
|
368
599
|
}
|
|
369
600
|
|
|
370
601
|
const shouldWrite = await prompter.confirm({
|
|
371
|
-
message: existing ? "Overwrite existing agent configuration?" : "Create agent configuration?",
|
|
372
602
|
initialValue: true,
|
|
603
|
+
message: existing ? 'Overwrite existing agent configuration?' : 'Create agent configuration?',
|
|
373
604
|
});
|
|
374
605
|
|
|
375
606
|
if (!shouldWrite) {
|
|
@@ -377,254 +608,25 @@ async function setupPiAgent(deps: {
|
|
|
377
608
|
}
|
|
378
609
|
|
|
379
610
|
const s = prompter.spinner();
|
|
380
|
-
s.start(
|
|
611
|
+
s.start('Writing agent configuration...');
|
|
381
612
|
|
|
382
|
-
const systemDir = scope ===
|
|
613
|
+
const systemDir = scope === 'project' ? pathJoin(cwd, '.pi') : pathJoin(homeDir, '.pi', 'agent');
|
|
383
614
|
await files.mkdir(systemDir);
|
|
384
615
|
await files.writeFile(systemPath, newContent);
|
|
385
616
|
|
|
386
617
|
s.stop(`Wrote agent configuration to ${systemPath}`);
|
|
387
618
|
}
|
|
388
619
|
|
|
389
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
function pathJoin(...parts: string[]): string {
|
|
392
|
-
// Simple path join that avoids importing 'path' module
|
|
393
|
-
// This is good enough for cross-platform testing since tests control the path format
|
|
394
|
-
return parts.join("/");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
620
|
function stripJsoncComments(content: string): string {
|
|
398
|
-
content = content.
|
|
399
|
-
content = content.
|
|
621
|
+
content = content.replaceAll(/\/\/.*$/gm, '');
|
|
622
|
+
content = content.replaceAll(/\/\*[\s\S]*?\*\//g, '');
|
|
400
623
|
return content;
|
|
401
624
|
}
|
|
402
625
|
|
|
403
|
-
function generateDiff(oldText: string, newText: string, filePath: string): string {
|
|
404
|
-
const oldLines = oldText.split("\n");
|
|
405
|
-
const newLines = newText.split("\n");
|
|
406
|
-
let result = `--- ${filePath}\n+++ ${filePath}\n`;
|
|
407
|
-
|
|
408
|
-
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
409
|
-
for (let i = 0; i < maxLen; i++) {
|
|
410
|
-
const oldLine = oldLines[i];
|
|
411
|
-
const newLine = newLines[i];
|
|
412
|
-
if (oldLine !== newLine) {
|
|
413
|
-
if (oldLine !== undefined) result += `- ${oldLine}\n`;
|
|
414
|
-
if (newLine !== undefined) result += `+ ${newLine}\n`;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return result.trimEnd();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function readJsonMaybe(files: FileStore, filePath: string): Promise<any | null> {
|
|
421
|
-
const content = await files.readFile(filePath);
|
|
422
|
-
if (!content) return null;
|
|
423
|
-
try {
|
|
424
|
-
return JSON.parse(content);
|
|
425
|
-
} catch {
|
|
426
|
-
try {
|
|
427
|
-
return JSON.parse(stripJsoncComments(content));
|
|
428
|
-
} catch {
|
|
429
|
-
return null;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
626
|
async function writeJsonFile(
|
|
435
627
|
files: FileStore,
|
|
436
628
|
filePath: string,
|
|
437
|
-
data: Record<string, unknown
|
|
629
|
+
data: Record<string, unknown>,
|
|
438
630
|
): Promise<void> {
|
|
439
|
-
await files.writeFile(filePath, JSON.stringify(data, null, 2) +
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function hasPluginInConfig(config: any): Promise<boolean> {
|
|
443
|
-
if (!config) return false;
|
|
444
|
-
const plugins = config.plugin || config.plugins || [];
|
|
445
|
-
return plugins.some((p: string) => p.includes(OPENCODE_PLUGIN_NAME));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async function hasPiProviderInSettings(settings: any): Promise<boolean> {
|
|
449
|
-
if (!settings) return false;
|
|
450
|
-
const packages = settings.packages || [];
|
|
451
|
-
return packages.some((p: any) => {
|
|
452
|
-
if (typeof p === "string") return p.includes(PI_PROVIDER_NAME);
|
|
453
|
-
if (typeof p === "object" && p.source) return p.source.includes(PI_PROVIDER_NAME);
|
|
454
|
-
return false;
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function getOpencodeState(
|
|
459
|
-
files: FileStore,
|
|
460
|
-
homeDir: string,
|
|
461
|
-
cwd: string
|
|
462
|
-
): Promise<{ project: boolean; global: boolean }> {
|
|
463
|
-
const projectJsonc = await readJsonMaybe(files, pathJoin(cwd, "opencode.jsonc"));
|
|
464
|
-
const projectJson = await readJsonMaybe(files, pathJoin(cwd, "opencode.json"));
|
|
465
|
-
const globalJsonc = await readJsonMaybe(
|
|
466
|
-
files,
|
|
467
|
-
pathJoin(homeDir, ".config", "opencode", "opencode.jsonc")
|
|
468
|
-
);
|
|
469
|
-
const globalJson = await readJsonMaybe(
|
|
470
|
-
files,
|
|
471
|
-
pathJoin(homeDir, ".config", "opencode", "opencode.json")
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
project: (await hasPluginInConfig(projectJsonc)) || (await hasPluginInConfig(projectJson)),
|
|
476
|
-
global: (await hasPluginInConfig(globalJsonc)) || (await hasPluginInConfig(globalJson)),
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function getPiState(
|
|
481
|
-
files: FileStore,
|
|
482
|
-
homeDir: string,
|
|
483
|
-
cwd: string
|
|
484
|
-
): Promise<{ project: boolean; global: boolean }> {
|
|
485
|
-
const projectSettings = await readJsonMaybe(files, pathJoin(cwd, ".pi", "settings.json"));
|
|
486
|
-
const globalSettings = await readJsonMaybe(
|
|
487
|
-
files,
|
|
488
|
-
pathJoin(homeDir, ".pi", "agent", "settings.json")
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
project: await hasPiProviderInSettings(projectSettings),
|
|
493
|
-
global: await hasPiProviderInSettings(globalSettings),
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function getOpencodeLabel(state: { project: boolean; global: boolean }): string {
|
|
498
|
-
if (state.project || state.global) return " (already configured)";
|
|
499
|
-
return "";
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function getPiLabel(state: { project: boolean; global: boolean }): string {
|
|
503
|
-
if (state.project || state.global) return " (already configured)";
|
|
504
|
-
return "";
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async function resolveOpencodeConfigPath(
|
|
508
|
-
files: FileStore,
|
|
509
|
-
homeDir: string,
|
|
510
|
-
cwd: string,
|
|
511
|
-
scope: "project" | "global"
|
|
512
|
-
): Promise<string> {
|
|
513
|
-
if (scope === "project") {
|
|
514
|
-
const jsoncPath = pathJoin(cwd, "opencode.jsonc");
|
|
515
|
-
const jsonPath = pathJoin(cwd, "opencode.json");
|
|
516
|
-
if (await files.exists(jsoncPath)) return jsoncPath;
|
|
517
|
-
if (await files.exists(jsonPath)) return jsonPath;
|
|
518
|
-
return jsonPath;
|
|
519
|
-
} else {
|
|
520
|
-
const globalDir = pathJoin(homeDir, ".config", "opencode");
|
|
521
|
-
const jsoncPath = pathJoin(globalDir, "opencode.jsonc");
|
|
522
|
-
const jsonPath = pathJoin(globalDir, "opencode.json");
|
|
523
|
-
if (await files.exists(jsoncPath)) return jsoncPath;
|
|
524
|
-
if (await files.exists(jsonPath)) return jsonPath;
|
|
525
|
-
return jsonPath;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function generateModifiedContent(existingContent: string | null, configPath: string): string {
|
|
530
|
-
if (configPath.endsWith(".jsonc")) {
|
|
531
|
-
const content = existingContent || "{}";
|
|
532
|
-
const parseErrors: any[] = [];
|
|
533
|
-
const parsed = parse(content, parseErrors, {
|
|
534
|
-
allowTrailingComma: true,
|
|
535
|
-
disallowComments: false,
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
let jsConfig: Record<string, any> = {};
|
|
539
|
-
const canModifyText =
|
|
540
|
-
parsed !== undefined &&
|
|
541
|
-
typeof parsed === "object" &&
|
|
542
|
-
parsed !== null &&
|
|
543
|
-
!Array.isArray(parsed);
|
|
544
|
-
|
|
545
|
-
if (canModifyText) {
|
|
546
|
-
jsConfig = parsed as Record<string, any>;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const pluginsKey = jsConfig.plugins !== undefined ? "plugins" : "plugin";
|
|
550
|
-
const existing: string[] = jsConfig[pluginsKey] || [];
|
|
551
|
-
const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME));
|
|
552
|
-
filtered.push(OPENCODE_PLUGIN);
|
|
553
|
-
|
|
554
|
-
if (canModifyText) {
|
|
555
|
-
let modifiedContent = content;
|
|
556
|
-
const pluginEdits = modify(modifiedContent, [pluginsKey], filtered, {
|
|
557
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
558
|
-
});
|
|
559
|
-
modifiedContent = applyEdits(modifiedContent, pluginEdits);
|
|
560
|
-
|
|
561
|
-
if (!jsConfig.$schema) {
|
|
562
|
-
const schemaEdits = modify(
|
|
563
|
-
modifiedContent,
|
|
564
|
-
["$schema"],
|
|
565
|
-
"https://opencode.ai/config.json",
|
|
566
|
-
{
|
|
567
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
568
|
-
}
|
|
569
|
-
);
|
|
570
|
-
modifiedContent = applyEdits(modifiedContent, schemaEdits);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return modifiedContent;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Malformed, empty, or non-object JSONC — write a clean config
|
|
577
|
-
const config: Record<string, any> = {
|
|
578
|
-
[pluginsKey]: filtered,
|
|
579
|
-
$schema: "https://opencode.ai/config.json",
|
|
580
|
-
};
|
|
581
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Plain JSON
|
|
585
|
-
let config: Record<string, any> = {};
|
|
586
|
-
if (existingContent) {
|
|
587
|
-
try {
|
|
588
|
-
config = JSON.parse(existingContent);
|
|
589
|
-
} catch {
|
|
590
|
-
// ignore malformed, overwrite
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const pluginsKey = config.plugins !== undefined ? "plugins" : "plugin";
|
|
595
|
-
const existing: string[] = config[pluginsKey] || [];
|
|
596
|
-
const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME));
|
|
597
|
-
filtered.push(OPENCODE_PLUGIN);
|
|
598
|
-
config[pluginsKey] = filtered;
|
|
599
|
-
config.$schema = config.$schema || "https://opencode.ai/config.json";
|
|
600
|
-
|
|
601
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
export async function runSetupCommand(): Promise<void> {
|
|
605
|
-
try {
|
|
606
|
-
await runSetup({
|
|
607
|
-
prompter: new ClackPrompter(),
|
|
608
|
-
files: new FsFileStore(),
|
|
609
|
-
commands: new SpawnCommandRunner(),
|
|
610
|
-
authService: AuthService.getInstance(),
|
|
611
|
-
apiKeyService: ApiKeyService.getInstance(),
|
|
612
|
-
homeDir: os.homedir(),
|
|
613
|
-
cwd: process.cwd(),
|
|
614
|
-
});
|
|
615
|
-
process.exit(0);
|
|
616
|
-
} catch (err) {
|
|
617
|
-
if (err instanceof CancelledError) {
|
|
618
|
-
process.exit(130);
|
|
619
|
-
}
|
|
620
|
-
if (err instanceof PrerequisiteError) {
|
|
621
|
-
console.error(`Missing required binary: ${err.binary}`);
|
|
622
|
-
process.exit(2);
|
|
623
|
-
}
|
|
624
|
-
if (err instanceof CommandFailedError) {
|
|
625
|
-
console.error(err.message);
|
|
626
|
-
process.exit(5);
|
|
627
|
-
}
|
|
628
|
-
throw err;
|
|
629
|
-
}
|
|
631
|
+
await files.writeFile(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
630
632
|
}
|