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