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.
Files changed (130) hide show
  1. package/.github/workflows/publish.yml +6 -6
  2. package/.github/workflows/test.yml +1 -1
  3. package/.prettierrc +5 -3
  4. package/dist/index.js +24 -25
  5. package/dist/package.json +5 -3
  6. package/dist/src/agents/app.js +8 -8
  7. package/dist/src/agents/backend.js +3 -3
  8. package/dist/src/agents/devops.js +8 -8
  9. package/dist/src/agents/frontend.js +3 -3
  10. package/dist/src/agents/fullstack.js +3 -3
  11. package/dist/src/agents/index.js +18 -18
  12. package/dist/src/agents/quality.js +8 -8
  13. package/dist/src/agents/security.js +8 -8
  14. package/dist/src/client.js +115 -127
  15. package/dist/src/commands/api-keys.js +195 -202
  16. package/dist/src/commands/auth.js +16 -25
  17. package/dist/src/commands/autocomplete.js +8 -8
  18. package/dist/src/commands/billing.js +10 -19
  19. package/dist/src/commands/chat.js +139 -170
  20. package/dist/src/commands/clusters.js +21 -30
  21. package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
  22. package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
  23. package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
  24. package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
  25. package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
  26. package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
  27. package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
  28. package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
  29. package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
  30. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
  31. package/dist/src/commands/code/auth-sync.js +215 -228
  32. package/dist/src/commands/code/errors.js +15 -12
  33. package/dist/src/commands/code/setup.js +390 -425
  34. package/dist/src/commands/code.js +279 -294
  35. package/dist/src/commands/index.js +5 -5
  36. package/dist/src/commands/models.js +16 -25
  37. package/dist/src/commands/users.js +9 -18
  38. package/dist/src/constants/command-structure.js +138 -138
  39. package/dist/src/services/api-key-service.js +132 -152
  40. package/dist/src/services/auth-service.js +81 -95
  41. package/dist/src/services/browser-auth.js +121 -131
  42. package/dist/src/services/chat-service.js +369 -386
  43. package/dist/src/services/cluster-service.js +47 -62
  44. package/dist/src/services/collaborator-service.js +9 -21
  45. package/dist/src/services/flux-service.js +13 -25
  46. package/dist/src/services/helm-service.js +9 -21
  47. package/dist/src/services/kubectl-service.js +15 -29
  48. package/dist/src/utils/config-checker.js +7 -7
  49. package/dist/src/utils/config-loader.js +109 -109
  50. package/dist/src/utils/default-api-key.js +129 -139
  51. package/dist/src/utils/env-manager.js +55 -66
  52. package/dist/src/utils/error-handler.js +62 -62
  53. package/dist/src/utils/logger.js +74 -67
  54. package/dist/src/utils/markdown-renderer.js +28 -28
  55. package/dist/src/utils/opencode-validator.js +67 -69
  56. package/dist/src/utils/token-manager.js +67 -65
  57. package/dist/tests/commands/chat.test.js +30 -39
  58. package/dist/tests/commands/code.test.js +186 -195
  59. package/dist/tests/utils/config-loader.test.js +107 -107
  60. package/dist/tests/utils/env-manager.test.js +81 -90
  61. package/dist/tests/utils/opencode-validator.test.js +42 -41
  62. package/dist/vitest.config.js +1 -1
  63. package/eslint.config.mjs +50 -30
  64. package/index.ts +30 -31
  65. package/package.json +5 -3
  66. package/src/agents/app.ts +9 -9
  67. package/src/agents/backend.ts +4 -4
  68. package/src/agents/devops.ts +9 -9
  69. package/src/agents/frontend.ts +4 -4
  70. package/src/agents/fullstack.ts +4 -4
  71. package/src/agents/index.ts +27 -25
  72. package/src/agents/quality.ts +9 -9
  73. package/src/agents/security.ts +9 -9
  74. package/src/agents/types.ts +10 -10
  75. package/src/client.ts +85 -77
  76. package/src/commands/api-keys.ts +190 -185
  77. package/src/commands/auth.ts +15 -14
  78. package/src/commands/autocomplete.ts +10 -10
  79. package/src/commands/billing.ts +13 -12
  80. package/src/commands/chat.ts +145 -142
  81. package/src/commands/clusters.ts +20 -19
  82. package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
  83. package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
  84. package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
  85. package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
  86. package/src/commands/code/__tests__/fake-file-store.ts +15 -15
  87. package/src/commands/code/__tests__/fake-prompter.ts +86 -85
  88. package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
  89. package/src/commands/code/adapters/clack-prompter.ts +32 -30
  90. package/src/commands/code/adapters/fs-file-store.ts +18 -17
  91. package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
  92. package/src/commands/code/auth-sync.ts +210 -210
  93. package/src/commands/code/errors.ts +11 -11
  94. package/src/commands/code/ports/auth-services.ts +7 -7
  95. package/src/commands/code/ports/command-runner.ts +2 -2
  96. package/src/commands/code/ports/file-store.ts +3 -3
  97. package/src/commands/code/ports/prompter.ts +13 -13
  98. package/src/commands/code/setup.ts +408 -406
  99. package/src/commands/code.ts +288 -287
  100. package/src/commands/index.ts +11 -10
  101. package/src/commands/models.ts +19 -18
  102. package/src/commands/users.ts +11 -10
  103. package/src/constants/command-structure.ts +159 -159
  104. package/src/services/api-key-service.ts +85 -85
  105. package/src/services/auth-service.ts +55 -54
  106. package/src/services/browser-auth.ts +62 -62
  107. package/src/services/chat-service.ts +169 -170
  108. package/src/services/cluster-service.ts +28 -28
  109. package/src/services/collaborator-service.ts +6 -6
  110. package/src/services/flux-service.ts +17 -17
  111. package/src/services/helm-service.ts +11 -11
  112. package/src/services/kubectl-service.ts +12 -12
  113. package/src/types/api.d.ts +1933 -1933
  114. package/src/types/json.d.ts +1 -1
  115. package/src/utils/config-checker.ts +6 -6
  116. package/src/utils/config-loader.ts +130 -129
  117. package/src/utils/default-api-key.ts +81 -80
  118. package/src/utils/env-manager.ts +37 -37
  119. package/src/utils/error-handler.ts +64 -64
  120. package/src/utils/logger.ts +72 -66
  121. package/src/utils/markdown-renderer.ts +28 -28
  122. package/src/utils/opencode-validator.ts +72 -71
  123. package/src/utils/token-manager.ts +69 -68
  124. package/tests/commands/chat.test.ts +32 -31
  125. package/tests/commands/code.test.ts +182 -181
  126. package/tests/utils/config-loader.test.ts +111 -110
  127. package/tests/utils/env-manager.test.ts +83 -79
  128. package/tests/utils/opencode-validator.test.ts +43 -42
  129. package/tsconfig.json +2 -1
  130. package/vitest.config.ts +2 -2
@@ -1,142 +1,375 @@
1
- import type { Prompter } from "./ports/prompter";
2
- import type { FileStore } from "./ports/file-store";
3
- import type { CommandRunner } from "./ports/command-runner";
4
- import type { AuthServicePort, ApiKeyServicePort } from "./ports/auth-services";
5
- import { CancelledError, CommandFailedError, PrerequisiteError } from "./errors";
6
- import { modify, parse, applyEdits } from "jsonc-parser";
7
- import { configureAuth } from "./auth-sync.js";
8
- import { ClackPrompter } from "./adapters/clack-prompter.js";
9
- import { FsFileStore } from "./adapters/fs-file-store.js";
10
- import { SpawnCommandRunner } from "./adapters/spawn-command-runner.js";
11
- import { AuthService } from "../../services/auth-service.js";
12
- import { ApiKeyService } from "../../services/api-key-service.js";
13
- import { getAllAgents, toMarkdown, toPiPrompt } from "../../agents/index.js";
14
- import * as os from "os";
15
-
16
- const OPENCODE_PLUGIN = "@bergetai/opencode-auth@1.0.16";
17
- const PI_PROVIDER = "npm:@bergetai/pi-provider";
18
- const OPENCODE_PLUGIN_NAME = "@bergetai/opencode-auth";
19
- const PI_PROVIDER_NAME = "@bergetai/pi-provider";
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
- homeDir: string;
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 { prompter, files, commands, authService, apiKeyService, homeDir, cwd } = deps;
34
+ const { apiKeyService, authService, commands, cwd, files, homeDir, prompter } = deps;
33
35
 
34
- prompter.intro("\uD83D\uDD27 Berget Code Setup");
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<"opencode" | "pi">({
40
- message: "How do you want to use Berget AI?",
41
+ const tool = await prompter.select<'opencode' | 'pi'>({
42
+ message: 'How do you want to use Berget AI?',
41
43
  options: [
42
44
  {
43
- value: "opencode",
45
+ hint: 'Open source AI coding agent',
44
46
  label: `OpenCode${getOpencodeLabel(ocState)}`,
45
- hint: "Open source AI coding agent",
47
+ value: 'opencode',
46
48
  },
47
49
  {
48
- value: "pi",
50
+ hint: 'Minimal terminal coding harness',
49
51
  label: `Pi${getPiLabel(piState)}`,
50
- hint: "Minimal terminal coding harness",
52
+ value: 'pi',
51
53
  },
52
54
  ],
53
55
  });
54
56
 
55
- const scope = await prompter.select<"project" | "global">({
56
- message: "Where should the configuration apply?",
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 === "opencode"
62
+ tool === 'opencode'
63
63
  ? ocState.project
64
- ? "Already configured"
65
- : "opencode.json in current directory"
64
+ ? 'Already configured'
65
+ : 'opencode.json in current directory'
66
66
  : piState.project
67
- ? "Already configured"
68
- : ".pi/settings.json in current directory",
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 === "opencode"
74
+ tool === 'opencode'
75
75
  ? ocState.global
76
- ? "Already configured"
77
- : "~/.config/opencode/opencode.json"
76
+ ? 'Already configured'
77
+ : '~/.config/opencode/opencode.json'
78
78
  : piState.global
79
- ? "Already configured"
80
- : "~/.pi/agent/settings.json",
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
- { prompter, files, authService, apiKeyService, homeDir },
87
- tool
88
+ { apiKeyService, authService, files, homeDir, prompter },
89
+ tool,
88
90
  );
89
91
 
90
- if (tool === "opencode") {
91
- await setupOpenCode({ prompter, files, commands, homeDir, cwd, scope });
92
- await setupOpenCodeAgents({ prompter, files, homeDir, cwd, scope });
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
- "Successfully configured Berget AI for OpenCode"
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
- "Successfully configured Berget AI for OpenCode"
104
+ 'Successfully configured Berget AI for OpenCode',
103
105
  );
104
106
  }
105
107
  } else {
106
- await setupPi({ prompter, files, commands, homeDir, cwd, scope });
107
- await setupPiAgent({ prompter, files, homeDir, cwd, scope });
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
- "Successfully configured Berget AI for Pi"
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
- "Successfully configured Berget AI for Pi"
119
+ 'Successfully configured Berget AI for Pi',
118
120
  );
119
121
  }
120
122
  }
121
123
 
122
- prompter.outro("Setup complete!");
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
- scope: "project" | "global";
363
+ files: FileStore;
364
+ homeDir: string;
365
+ prompter: Prompter;
366
+ scope: 'global' | 'project';
134
367
  }): Promise<void> {
135
- const { prompter, files, commands, homeDir, cwd, scope } = deps;
368
+ const { commands, cwd, files, homeDir, prompter, scope } = deps;
136
369
 
137
- const installed = await commands.checkInstalled("opencode");
370
+ const installed = await commands.checkInstalled('opencode');
138
371
  if (!installed) {
139
- throw new PrerequisiteError("opencode");
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), "Changes to be written");
384
+ prompter.note(generateDiff(existingContent, newContent, configPath), 'Changes to be written');
152
385
  } else {
153
- prompter.note(`New config at ${configPath}:\n\n${newContent}`, "Config preview");
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("Writing OpenCode configuration...");
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
- prompter: Prompter;
402
+ cwd: string;
231
403
  files: FileStore;
232
404
  homeDir: string;
233
- cwd: string;
234
- scope: "project" | "global";
405
+ prompter: Prompter;
406
+ scope: 'global' | 'project';
235
407
  }): Promise<void> {
236
- const { prompter, files, homeDir, cwd, scope } = deps;
408
+ const { cwd, files, homeDir, prompter, scope } = deps;
237
409
 
238
- const agents = getAllAgents().filter(a => a.config.mode === "primary");
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: "Select agents to set up (optional - press enter to skip):",
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 === "project"
259
- ? pathJoin(cwd, ".opencode", "agents")
260
- : pathJoin(homeDir, ".config", "opencode", "agents");
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("Agent files are already up to date.", "No changes needed");
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("Writing agent configurations...");
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 setupPiAgent(deps: {
318
- prompter: Prompter;
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
- scope: "project" | "global";
550
+ files: FileStore;
551
+ homeDir: string;
552
+ prompter: Prompter;
553
+ scope: 'global' | 'project';
323
554
  }): Promise<void> {
324
- const { prompter, files, homeDir, cwd, scope } = deps;
555
+ const { cwd, files, homeDir, prompter, scope } = deps;
325
556
 
326
- const agents = getAllAgents().filter(a => a.config.mode === "primary");
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: "Select an agent (optional - press enter to skip):",
564
+ message: 'Select an agent (optional - press enter to skip):',
334
565
  options: [
335
- { value: "__skip__", label: "Skip agent setup" },
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 === "__skip__") {
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 === "project"
353
- ? pathJoin(cwd, ".pi", "SYSTEM.md")
354
- : pathJoin(homeDir, ".pi", "agent", "SYSTEM.md");
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("Agent configuration is already up to date.", "No changes needed");
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), "Changes to agent configuration");
596
+ prompter.note(generateDiff(existing, newContent, systemPath), 'Changes to agent configuration');
366
597
  } else {
367
- prompter.note(newContent, "New agent configuration");
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("Writing agent configuration...");
611
+ s.start('Writing agent configuration...');
381
612
 
382
- const systemDir = scope === "project" ? pathJoin(cwd, ".pi") : pathJoin(homeDir, ".pi", "agent");
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.replace(/\/\/.*$/gm, "");
399
- content = content.replace(/\/\*[\s\S]*?\*\//g, "");
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) + "\n");
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
  }