berget 2.2.6 → 2.2.7

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 (144) hide show
  1. package/.github/workflows/publish.yml +6 -6
  2. package/.github/workflows/test.yml +11 -5
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +5 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +21 -21
  9. package/dist/package.json +28 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +54 -62
  20. package/dist/src/commands/api-keys.js +132 -140
  21. package/dist/src/commands/auth.js +9 -9
  22. package/dist/src/commands/autocomplete.js +9 -9
  23. package/dist/src/commands/billing.js +7 -9
  24. package/dist/src/commands/chat.js +90 -92
  25. package/dist/src/commands/clusters.js +12 -12
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +348 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +23 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +55 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +5 -7
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +9 -0
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +60 -18
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +374 -107
  33. package/dist/src/commands/code/adapters/clack-prompter.js +10 -0
  34. package/dist/src/commands/code/adapters/fs-file-store.js +8 -3
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +15 -11
  36. package/dist/src/commands/code/auth-sync.js +283 -0
  37. package/dist/src/commands/code/errors.js +4 -4
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +234 -93
  40. package/dist/src/commands/code.js +139 -251
  41. package/dist/src/commands/models.js +13 -15
  42. package/dist/src/commands/users.js +6 -8
  43. package/dist/src/constants/command-structure.js +116 -116
  44. package/dist/src/services/api-key-service.js +43 -48
  45. package/dist/src/services/auth-service.js +60 -299
  46. package/dist/src/services/browser-auth.js +278 -0
  47. package/dist/src/services/chat-service.js +78 -91
  48. package/dist/src/services/cluster-service.js +6 -6
  49. package/dist/src/services/collaborator-service.js +5 -8
  50. package/dist/src/services/flux-service.js +5 -8
  51. package/dist/src/services/helm-service.js +5 -8
  52. package/dist/src/services/kubectl-service.js +7 -10
  53. package/dist/src/utils/config-checker.js +5 -5
  54. package/dist/src/utils/config-loader.js +25 -25
  55. package/dist/src/utils/default-api-key.js +23 -23
  56. package/dist/src/utils/env-manager.js +7 -7
  57. package/dist/src/utils/error-handler.js +60 -61
  58. package/dist/src/utils/logger.js +7 -7
  59. package/dist/src/utils/markdown-renderer.js +2 -2
  60. package/dist/src/utils/opencode-validator.js +17 -20
  61. package/dist/src/utils/token-manager.js +38 -11
  62. package/dist/tests/commands/chat.test.js +24 -24
  63. package/dist/tests/commands/code.test.js +147 -147
  64. package/dist/tests/utils/config-loader.test.js +114 -114
  65. package/dist/tests/utils/env-manager.test.js +57 -57
  66. package/dist/tests/utils/opencode-validator.test.js +33 -33
  67. package/dist/vitest.config.js +1 -1
  68. package/eslint.config.mjs +47 -0
  69. package/index.ts +42 -48
  70. package/package.json +28 -2
  71. package/src/agents/app.ts +27 -0
  72. package/src/agents/backend.ts +24 -0
  73. package/src/agents/devops.ts +33 -0
  74. package/src/agents/frontend.ts +24 -0
  75. package/src/agents/fullstack.ts +24 -0
  76. package/src/agents/index.ts +71 -0
  77. package/src/agents/quality.ts +69 -0
  78. package/src/agents/security.ts +26 -0
  79. package/src/agents/types.ts +17 -0
  80. package/src/client.ts +125 -167
  81. package/src/commands/api-keys.ts +261 -358
  82. package/src/commands/auth.ts +24 -30
  83. package/src/commands/autocomplete.ts +12 -12
  84. package/src/commands/billing.ts +22 -27
  85. package/src/commands/chat.ts +230 -323
  86. package/src/commands/clusters.ts +33 -33
  87. package/src/commands/code/__tests__/auth-sync.test.ts +481 -0
  88. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  89. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  90. package/src/commands/code/__tests__/fake-command-runner.ts +39 -42
  91. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  92. package/src/commands/code/__tests__/fake-prompter.ts +107 -69
  93. package/src/commands/code/__tests__/setup-flow.test.ts +624 -270
  94. package/src/commands/code/adapters/clack-prompter.ts +50 -38
  95. package/src/commands/code/adapters/fs-file-store.ts +31 -27
  96. package/src/commands/code/adapters/spawn-command-runner.ts +33 -29
  97. package/src/commands/code/auth-sync.ts +329 -0
  98. package/src/commands/code/errors.ts +15 -15
  99. package/src/commands/code/ports/auth-services.ts +14 -0
  100. package/src/commands/code/ports/command-runner.ts +8 -4
  101. package/src/commands/code/ports/file-store.ts +5 -4
  102. package/src/commands/code/ports/prompter.ts +24 -18
  103. package/src/commands/code/setup.ts +545 -317
  104. package/src/commands/code.ts +271 -473
  105. package/src/commands/index.ts +19 -19
  106. package/src/commands/models.ts +32 -37
  107. package/src/commands/users.ts +15 -22
  108. package/src/constants/command-structure.ts +119 -142
  109. package/src/services/api-key-service.ts +96 -113
  110. package/src/services/auth-service.ts +92 -339
  111. package/src/services/browser-auth.ts +296 -0
  112. package/src/services/chat-service.ts +246 -279
  113. package/src/services/cluster-service.ts +29 -32
  114. package/src/services/collaborator-service.ts +13 -18
  115. package/src/services/flux-service.ts +16 -18
  116. package/src/services/helm-service.ts +16 -18
  117. package/src/services/kubectl-service.ts +12 -14
  118. package/src/types/api.d.ts +924 -926
  119. package/src/types/json.d.ts +3 -3
  120. package/src/utils/config-checker.ts +10 -10
  121. package/src/utils/config-loader.ts +110 -127
  122. package/src/utils/default-api-key.ts +81 -93
  123. package/src/utils/env-manager.ts +36 -40
  124. package/src/utils/error-handler.ts +83 -78
  125. package/src/utils/logger.ts +41 -41
  126. package/src/utils/markdown-renderer.ts +11 -11
  127. package/src/utils/opencode-validator.ts +51 -56
  128. package/src/utils/token-manager.ts +84 -64
  129. package/templates/agents/app.md +1 -0
  130. package/templates/agents/backend.md +1 -0
  131. package/templates/agents/devops.md +2 -0
  132. package/templates/agents/frontend.md +1 -0
  133. package/templates/agents/fullstack.md +1 -0
  134. package/templates/agents/quality.md +45 -40
  135. package/templates/agents/security.md +1 -0
  136. package/tests/commands/chat.test.ts +60 -70
  137. package/tests/commands/code.test.ts +330 -376
  138. package/tests/utils/config-loader.test.ts +260 -260
  139. package/tests/utils/env-manager.test.ts +127 -134
  140. package/tests/utils/opencode-validator.test.ts +58 -63
  141. package/tsconfig.json +2 -2
  142. package/vitest.config.ts +3 -3
  143. package/AGENTS.md +0 -374
  144. package/TODO.md +0 -19
@@ -1,402 +1,630 @@
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 { CancelledError, CommandFailedError, PrerequisiteError } from './errors'
5
- import { modify, parse, applyEdits } from 'jsonc-parser'
6
-
7
- const OPENCODE_PLUGIN = '@bergetai/opencode-auth@1.0.16'
8
- const PI_PROVIDER = 'npm:@bergetai/pi-provider'
9
- const OPENCODE_PLUGIN_NAME = '@bergetai/opencode-auth'
10
- const PI_PROVIDER_NAME = '@bergetai/pi-provider'
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";
11
20
 
12
21
  export interface WizardDeps {
13
- prompter: Prompter
14
- files: FileStore
15
- commands: CommandRunner
16
- homeDir: string
17
- cwd: string
22
+ prompter: Prompter;
23
+ files: FileStore;
24
+ commands: CommandRunner;
25
+ authService: AuthServicePort;
26
+ apiKeyService: ApiKeyServicePort;
27
+ homeDir: string;
28
+ cwd: string;
18
29
  }
19
30
 
20
31
  export async function runSetup(deps: WizardDeps): Promise<void> {
21
- const { prompter, files, commands, homeDir, cwd } = deps
22
-
23
- prompter.intro('\uD83D\uDD27 Berget Code Setup')
24
-
25
- const ocState = await getOpencodeState(files, homeDir, cwd)
26
- const piState = await getPiState(files, homeDir, cwd)
27
-
28
- const tool = await prompter.select<'opencode' | 'pi'>({
29
- message: 'How do you want to use Berget AI?',
30
- options: [
31
- {
32
- value: 'opencode',
33
- label: `OpenCode${getOpencodeLabel(ocState)}`,
34
- hint: 'Open source AI coding agent',
35
- },
36
- {
37
- value: 'pi',
38
- label: `Pi${getPiLabel(piState)}`,
39
- hint: 'Minimal terminal coding harness',
40
- },
41
- ],
42
- })
43
-
44
- const scope = await prompter.select<'project' | 'global'>({
45
- message: 'Where should the configuration apply?',
46
- options: [
47
- {
48
- value: 'project',
49
- label: 'This project only',
50
- hint: tool === 'opencode'
51
- ? (ocState.project ? 'Already configured' : 'opencode.json in current directory')
52
- : (piState.project ? 'Already configured' : '.pi/settings.json in current directory'),
53
- },
54
- {
55
- value: 'global',
56
- label: 'Globally for all projects',
57
- hint: tool === 'opencode'
58
- ? (ocState.global ? 'Already configured' : '~/.config/opencode/opencode.json')
59
- : (piState.global ? 'Already configured' : '~/.pi/agent/settings.json'),
60
- },
61
- ],
62
- })
63
-
64
- if (tool === 'opencode') {
65
- await setupOpenCode({ prompter, files, commands, homeDir, cwd, scope })
66
- prompter.note(`Next steps:\n\n1. Run: opencode\n2. Type: /connect\n3. Choose your auth method:\n \u2022 "Login with Berget" \u2014 Berget Code plan\n \u2022 "Enter Berget API Key manually"\n \u2022 (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`, 'Successfully configured Berget AI for OpenCode')
32
+ const { prompter, files, commands, authService, apiKeyService, homeDir, cwd } = deps;
33
+
34
+ prompter.intro("\uD83D\uDD27 Berget Code Setup");
35
+
36
+ const ocState = await getOpencodeState(files, homeDir, cwd);
37
+ const piState = await getPiState(files, homeDir, cwd);
38
+
39
+ const tool = await prompter.select<"opencode" | "pi">({
40
+ message: "How do you want to use Berget AI?",
41
+ options: [
42
+ {
43
+ value: "opencode",
44
+ label: `OpenCode${getOpencodeLabel(ocState)}`,
45
+ hint: "Open source AI coding agent",
46
+ },
47
+ {
48
+ value: "pi",
49
+ label: `Pi${getPiLabel(piState)}`,
50
+ hint: "Minimal terminal coding harness",
51
+ },
52
+ ],
53
+ });
54
+
55
+ const scope = await prompter.select<"project" | "global">({
56
+ message: "Where should the configuration apply?",
57
+ options: [
58
+ {
59
+ value: "project",
60
+ label: "This project only",
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
+ },
70
+ {
71
+ value: "global",
72
+ label: "Globally for all projects",
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
+ },
82
+ ],
83
+ });
84
+
85
+ const authResult = await configureAuth(
86
+ { prompter, files, authService, apiKeyService, homeDir },
87
+ tool
88
+ );
89
+
90
+ if (tool === "opencode") {
91
+ await setupOpenCode({ prompter, files, commands, homeDir, cwd, scope });
92
+ await setupOpenCodeAgents({ prompter, files, homeDir, cwd, scope });
93
+
94
+ if (authResult.authenticated) {
95
+ prompter.note(
96
+ `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"
98
+ );
67
99
  } else {
68
- await setupPi({ prompter, files, commands, homeDir, cwd, scope })
69
- prompter.note(`Next steps:\n\n1. Restart Pi or run /reload\n2. Type: /login\n3. Choose your auth method:\n \u2022 "Use a subscription" \u2192 Berget AI\n \u2022 (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`, 'Successfully configured Berget AI for Pi')
100
+ prompter.note(
101
+ `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"
103
+ );
70
104
  }
105
+ } else {
106
+ await setupPi({ prompter, files, commands, homeDir, cwd, scope });
107
+ await setupPiAgent({ prompter, files, homeDir, cwd, scope });
108
+
109
+ if (authResult.authenticated) {
110
+ prompter.note(
111
+ `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"
113
+ );
114
+ } else {
115
+ prompter.note(
116
+ `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"
118
+ );
119
+ }
120
+ }
71
121
 
72
- prompter.outro('Setup complete!')
122
+ prompter.outro("Setup complete!");
73
123
  }
74
124
 
75
125
  // ─── OpenCode ────────────────────────────────────────────────────────────────
76
126
 
77
127
  async function setupOpenCode(deps: {
78
- prompter: Prompter
79
- files: FileStore
80
- commands: CommandRunner
81
- homeDir: string
82
- cwd: string
83
- scope: 'project' | 'global'
128
+ prompter: Prompter;
129
+ files: FileStore;
130
+ commands: CommandRunner;
131
+ homeDir: string;
132
+ cwd: string;
133
+ scope: "project" | "global";
84
134
  }): Promise<void> {
85
- const { prompter, files, commands, homeDir, cwd, scope } = deps
86
-
87
- const installed = await commands.checkInstalled('opencode')
88
- if (!installed) {
89
- throw new PrerequisiteError('opencode')
90
- }
91
-
92
- const configPath = await resolveOpencodeConfigPath(files, homeDir, cwd, scope)
93
- const existingContent = await files.readFile(configPath)
94
- const newContent = generateModifiedContent(existingContent, configPath)
135
+ const { prompter, files, commands, homeDir, cwd, scope } = deps;
136
+
137
+ const installed = await commands.checkInstalled("opencode");
138
+ if (!installed) {
139
+ throw new PrerequisiteError("opencode");
140
+ }
141
+
142
+ const configPath = await resolveOpencodeConfigPath(files, homeDir, cwd, scope);
143
+ const existingContent = await files.readFile(configPath);
144
+ const newContent = generateModifiedContent(existingContent, configPath);
145
+
146
+ if (existingContent && existingContent === newContent) {
147
+ return;
148
+ }
149
+
150
+ if (existingContent) {
151
+ prompter.note(generateDiff(existingContent, newContent, configPath), "Changes to be written");
152
+ } else {
153
+ prompter.note(`New config at ${configPath}:\n\n${newContent}`, "Config preview");
154
+ }
155
+
156
+ const shouldWrite = await prompter.confirm({
157
+ message: existingContent ? `Write these changes to ${configPath}?` : `Create ${configPath}?`,
158
+ initialValue: true,
159
+ });
160
+ if (!shouldWrite) throw new CancelledError();
161
+
162
+ const s = prompter.spinner();
163
+ s.start("Writing OpenCode configuration...");
164
+ await files.writeFile(configPath, newContent);
165
+ s.stop(`Wrote configuration to ${configPath}.`);
166
+ }
95
167
 
96
- if (existingContent && existingContent === newContent) {
97
- return
98
- }
168
+ // ─── Pi ────────────────────────────────────────────────────────────────────────
99
169
 
100
- if (existingContent) {
101
- prompter.note(generateDiff(existingContent, newContent, configPath), 'Changes to be written')
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
+ }
102
221
  } else {
103
- prompter.note(`New config at ${configPath}:\n\n${newContent}`, 'Config preview')
222
+ settings.defaultProvider = "berget";
223
+ await writeJsonFile(files, settingsPath, settings);
224
+ prompter.note("Berget AI is now your default provider.", "Updated default provider");
104
225
  }
226
+ }
227
+ }
105
228
 
106
- const shouldWrite = await prompter.confirm({
107
- message: existingContent
108
- ? `Write these changes to ${configPath}?`
109
- : `Create ${configPath}?`,
110
- initialValue: true,
229
+ async function setupOpenCodeAgents(deps: {
230
+ prompter: Prompter;
231
+ files: FileStore;
232
+ homeDir: string;
233
+ cwd: string;
234
+ scope: "project" | "global";
235
+ }): Promise<void> {
236
+ const { prompter, files, homeDir, cwd, scope } = deps;
237
+
238
+ const agents = getAllAgents().filter(a => a.config.mode === "primary");
239
+
240
+ if (agents.length === 0) {
241
+ return;
242
+ }
243
+
244
+ 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,
249
+ hint: agent.config.description,
250
+ })),
251
+ });
252
+
253
+ if (selectedAgents.length === 0) {
254
+ return;
255
+ }
256
+
257
+ const agentsDir =
258
+ scope === "project"
259
+ ? pathJoin(cwd, ".opencode", "agents")
260
+ : pathJoin(homeDir, ".config", "opencode", "agents");
261
+
262
+ await files.mkdir(agentsDir);
263
+
264
+ const hasChanges = await Promise.all(
265
+ selectedAgents.map(async agentName => {
266
+ const agent = agents.find(a => a.config.name === agentName);
267
+ if (!agent) return false;
268
+
269
+ const agentPath = pathJoin(agentsDir, `${agentName}.md`);
270
+ const existing = await files.readFile(agentPath);
271
+ const newContent = toMarkdown(agent);
272
+
273
+ if (existing === newContent) {
274
+ return false;
275
+ }
276
+
277
+ if (existing) {
278
+ prompter.note(
279
+ generateDiff(existing, newContent, agentPath),
280
+ `Changes to ${agentName} agent`
281
+ );
282
+ }
283
+
284
+ return true;
111
285
  })
112
- if (!shouldWrite) throw new CancelledError()
286
+ );
113
287
 
114
- const s = prompter.spinner()
115
- s.start('Writing OpenCode configuration...')
116
- await files.writeFile(configPath, newContent)
117
- s.stop(`Wrote configuration to ${configPath}.`)
118
- }
119
-
120
- // ─── Pi ────────────────────────────────────────────────────────────────────────
288
+ if (!hasChanges.some(Boolean)) {
289
+ prompter.note("Agent files are already up to date.", "No changes needed");
290
+ return;
291
+ }
121
292
 
122
- async function setupPi(deps: {
123
- prompter: Prompter
124
- files: FileStore
125
- commands: CommandRunner
126
- homeDir: string
127
- cwd: string
128
- scope: 'project' | 'global'
129
- }): Promise<void> {
130
- const { prompter, files, commands, homeDir, cwd, scope } = deps
131
- const s = prompter.spinner()
293
+ const shouldWrite = await prompter.confirm({
294
+ message: "Write agent configuration files?",
295
+ initialValue: true,
296
+ });
132
297
 
133
- const installed = await commands.checkInstalled('pi')
134
- if (!installed) {
135
- throw new PrerequisiteError('pi')
136
- }
298
+ if (!shouldWrite) {
299
+ throw new CancelledError();
300
+ }
137
301
 
138
- const installArgs = scope === 'project'
139
- ? ['install', '-l', PI_PROVIDER]
140
- : ['install', PI_PROVIDER]
302
+ const s = prompter.spinner();
303
+ s.start("Writing agent configurations...");
141
304
 
142
- s.start(`Installing Berget AI provider for Pi...`)
143
- try {
144
- await commands.run('pi', installArgs)
145
- s.stop('Installed Pi provider.')
146
- } catch (err: any) {
147
- s.stop('Pi provider installation failed. Please try again or install manually.')
148
- throw new CommandFailedError(`pi ${installArgs.join(' ')}`, 1)
149
- }
305
+ for (const agentName of selectedAgents) {
306
+ const agent = agents.find(a => a.config.name === agentName);
307
+ if (!agent) continue;
150
308
 
151
- const settingsPath = scope === 'project'
152
- ? pathJoin(cwd, '.pi', 'settings.json')
153
- : pathJoin(homeDir, '.pi', 'agent', 'settings.json')
309
+ const agentPath = pathJoin(agentsDir, `${agentName}.md`);
310
+ const content = toMarkdown(agent);
311
+ await files.writeFile(agentPath, content);
312
+ }
154
313
 
155
- let settings = await readJsonMaybe(files, settingsPath) || {}
314
+ s.stop(`Wrote ${selectedAgents.length} agent(s) to ${agentsDir}`);
315
+ }
156
316
 
157
- if (settings.defaultProvider === 'berget') {
158
- prompter.note('Berget AI is already set as your default provider.', 'Default provider already set')
159
- } else {
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
- }
317
+ async function setupPiAgent(deps: {
318
+ prompter: Prompter;
319
+ files: FileStore;
320
+ homeDir: string;
321
+ cwd: string;
322
+ scope: "project" | "global";
323
+ }): Promise<void> {
324
+ const { prompter, files, homeDir, cwd, scope } = deps;
325
+
326
+ const agents = getAllAgents().filter(a => a.config.mode === "primary");
327
+
328
+ if (agents.length === 0) {
329
+ return;
330
+ }
331
+
332
+ const selectedAgentName = await prompter.select({
333
+ message: "Select an agent (optional - press enter to skip):",
334
+ options: [
335
+ { value: "__skip__", label: "Skip agent setup" },
336
+ ...agents.map(agent => ({
337
+ value: agent.config.name,
338
+ label: agent.config.name,
339
+ hint: agent.config.description,
340
+ })),
341
+ ],
342
+ });
343
+
344
+ if (selectedAgentName === "__skip__") {
345
+ return;
346
+ }
347
+
348
+ const agent = agents.find(a => a.config.name === selectedAgentName);
349
+ if (!agent) return;
350
+
351
+ const systemPath =
352
+ scope === "project"
353
+ ? pathJoin(cwd, ".pi", "SYSTEM.md")
354
+ : pathJoin(homeDir, ".pi", "agent", "SYSTEM.md");
355
+
356
+ const existing = await files.readFile(systemPath);
357
+ const newContent = toPiPrompt(agent);
358
+
359
+ if (existing === newContent) {
360
+ prompter.note("Agent configuration is already up to date.", "No changes needed");
361
+ return;
362
+ }
363
+
364
+ if (existing) {
365
+ prompter.note(generateDiff(existing, newContent, systemPath), "Changes to agent configuration");
366
+ } else {
367
+ prompter.note(newContent, "New agent configuration");
368
+ }
369
+
370
+ const shouldWrite = await prompter.confirm({
371
+ message: existing ? "Overwrite existing agent configuration?" : "Create agent configuration?",
372
+ initialValue: true,
373
+ });
374
+
375
+ if (!shouldWrite) {
376
+ throw new CancelledError();
377
+ }
378
+
379
+ const s = prompter.spinner();
380
+ s.start("Writing agent configuration...");
381
+
382
+ const systemDir = scope === "project" ? pathJoin(cwd, ".pi") : pathJoin(homeDir, ".pi", "agent");
383
+ await files.mkdir(systemDir);
384
+ await files.writeFile(systemPath, newContent);
385
+
386
+ s.stop(`Wrote agent configuration to ${systemPath}`);
176
387
  }
177
388
 
178
389
  // ─── Helpers ─────────────────────────────────────────────────────────────────
179
390
 
180
391
  function pathJoin(...parts: string[]): string {
181
- // Simple path join that avoids importing 'path' module
182
- // This is good enough for cross-platform testing since tests control the path format
183
- return parts.join('/')
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("/");
184
395
  }
185
396
 
186
397
  function stripJsoncComments(content: string): string {
187
- content = content.replace(/\/\/.*$/gm, '')
188
- content = content.replace(/\/\*[\s\S]*?\*\//g, '')
189
- return content
398
+ content = content.replace(/\/\/.*$/gm, "");
399
+ content = content.replace(/\/\*[\s\S]*?\*\//g, "");
400
+ return content;
190
401
  }
191
402
 
192
403
  function generateDiff(oldText: string, newText: string, filePath: string): string {
193
- const oldLines = oldText.split('\n')
194
- const newLines = newText.split('\n')
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
- }
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`;
205
415
  }
206
- return result.trimEnd()
416
+ }
417
+ return result.trimEnd();
207
418
  }
208
419
 
209
420
  async function readJsonMaybe(files: FileStore, filePath: string): Promise<any | null> {
210
- const content = await files.readFile(filePath)
211
- if (!content) return null
421
+ const content = await files.readFile(filePath);
422
+ if (!content) return null;
423
+ try {
424
+ return JSON.parse(content);
425
+ } catch {
212
426
  try {
213
- return JSON.parse(content)
427
+ return JSON.parse(stripJsoncComments(content));
214
428
  } catch {
215
- try {
216
- return JSON.parse(stripJsoncComments(content))
217
- } catch {
218
- return null
219
- }
429
+ return null;
220
430
  }
431
+ }
221
432
  }
222
433
 
223
- async function writeJsonFile(files: FileStore, filePath: string, data: Record<string, unknown>): Promise<void> {
224
- await files.writeFile(filePath, JSON.stringify(data, null, 2) + '\n')
434
+ async function writeJsonFile(
435
+ files: FileStore,
436
+ filePath: string,
437
+ data: Record<string, unknown>
438
+ ): Promise<void> {
439
+ await files.writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
225
440
  }
226
441
 
227
442
  async function hasPluginInConfig(config: any): Promise<boolean> {
228
- if (!config) return false
229
- const plugins = config.plugin || config.plugins || []
230
- return plugins.some((p: string) => p.includes(OPENCODE_PLUGIN_NAME))
443
+ if (!config) return false;
444
+ const plugins = config.plugin || config.plugins || [];
445
+ return plugins.some((p: string) => p.includes(OPENCODE_PLUGIN_NAME));
231
446
  }
232
447
 
233
448
  async function hasPiProviderInSettings(settings: any): Promise<boolean> {
234
- if (!settings) return false
235
- const packages = settings.packages || []
236
- return packages.some((p: any) => {
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
- })
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
+ });
241
456
  }
242
457
 
243
458
  async function getOpencodeState(
244
- files: FileStore,
245
- homeDir: string,
246
- cwd: string
459
+ files: FileStore,
460
+ homeDir: string,
461
+ cwd: string
247
462
  ): Promise<{ project: boolean; global: boolean }> {
248
- const projectJsonc = await readJsonMaybe(files, pathJoin(cwd, 'opencode.jsonc'))
249
- const projectJson = await readJsonMaybe(files, pathJoin(cwd, 'opencode.json'))
250
- const globalJsonc = await readJsonMaybe(files, pathJoin(homeDir, '.config', 'opencode', 'opencode.jsonc'))
251
- const globalJson = await readJsonMaybe(files, pathJoin(homeDir, '.config', 'opencode', 'opencode.json'))
252
-
253
- return {
254
- project: await hasPluginInConfig(projectJsonc) || await hasPluginInConfig(projectJson),
255
- global: await hasPluginInConfig(globalJsonc) || await hasPluginInConfig(globalJson),
256
- }
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
+ };
257
478
  }
258
479
 
259
480
  async function getPiState(
260
- files: FileStore,
261
- homeDir: string,
262
- cwd: string
481
+ files: FileStore,
482
+ homeDir: string,
483
+ cwd: string
263
484
  ): Promise<{ project: boolean; global: boolean }> {
264
- const projectSettings = await readJsonMaybe(files, pathJoin(cwd, '.pi', 'settings.json'))
265
- const globalSettings = await readJsonMaybe(files, pathJoin(homeDir, '.pi', 'agent', 'settings.json'))
266
-
267
- return {
268
- project: await hasPiProviderInSettings(projectSettings),
269
- global: await hasPiProviderInSettings(globalSettings),
270
- }
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
+ };
271
495
  }
272
496
 
273
497
  function getOpencodeLabel(state: { project: boolean; global: boolean }): string {
274
- if (state.project || state.global) return ' (already configured)'
275
- return ''
498
+ if (state.project || state.global) return " (already configured)";
499
+ return "";
276
500
  }
277
501
 
278
502
  function getPiLabel(state: { project: boolean; global: boolean }): string {
279
- if (state.project || state.global) return ' (already configured)'
280
- return ''
503
+ if (state.project || state.global) return " (already configured)";
504
+ return "";
281
505
  }
282
506
 
283
507
  async function resolveOpencodeConfigPath(
284
- files: FileStore,
285
- homeDir: string,
286
- cwd: string,
287
- scope: 'project' | 'global'
508
+ files: FileStore,
509
+ homeDir: string,
510
+ cwd: string,
511
+ scope: "project" | "global"
288
512
  ): Promise<string> {
289
- if (scope === 'project') {
290
- const jsoncPath = pathJoin(cwd, 'opencode.jsonc')
291
- const jsonPath = pathJoin(cwd, 'opencode.json')
292
- if (await files.exists(jsoncPath)) return jsoncPath
293
- if (await files.exists(jsonPath)) return jsonPath
294
- return jsonPath
295
- } else {
296
- const globalDir = pathJoin(homeDir, '.config', 'opencode')
297
- const jsoncPath = pathJoin(globalDir, 'opencode.jsonc')
298
- const jsonPath = pathJoin(globalDir, 'opencode.json')
299
- if (await files.exists(jsoncPath)) return jsoncPath
300
- if (await files.exists(jsonPath)) return jsonPath
301
- return jsonPath
302
- }
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
+ }
303
527
  }
304
528
 
305
529
  function generateModifiedContent(existingContent: string | null, configPath: string): string {
306
- if (configPath.endsWith('.jsonc')) {
307
- const content = existingContent || '{}'
308
- const parseErrors: any[] = []
309
- const parsed = parse(content, parseErrors, { allowTrailingComma: true, disallowComments: false })
310
-
311
- let jsConfig: Record<string, any> = {}
312
- const canModifyText =
313
- parsed !== undefined &&
314
- typeof parsed === 'object' &&
315
- parsed !== null &&
316
- !Array.isArray(parsed)
317
-
318
- if (canModifyText) {
319
- jsConfig = parsed as Record<string, any>
320
- }
321
-
322
- const pluginsKey = jsConfig.plugins !== undefined ? 'plugins' : 'plugin'
323
- const existing: string[] = jsConfig[pluginsKey] || []
324
- const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME))
325
- filtered.push(OPENCODE_PLUGIN)
326
-
327
- if (canModifyText) {
328
- let modifiedContent = content
329
- const pluginEdits = modify(modifiedContent, [pluginsKey], filtered, {
330
- formattingOptions: { insertSpaces: true, tabSize: 2 },
331
- })
332
- modifiedContent = applyEdits(modifiedContent, pluginEdits)
333
-
334
- if (!jsConfig.$schema) {
335
- const schemaEdits = modify(modifiedContent, ['$schema'], 'https://opencode.ai/config.json', {
336
- formattingOptions: { insertSpaces: true, tabSize: 2 },
337
- })
338
- modifiedContent = applyEdits(modifiedContent, schemaEdits)
339
- }
340
-
341
- return modifiedContent
342
- }
343
-
344
- // Malformed, empty, or non-object JSONC — write a clean config
345
- const config: Record<string, any> = {
346
- [pluginsKey]: filtered,
347
- $schema: 'https://opencode.ai/config.json',
348
- }
349
- return JSON.stringify(config, null, 2) + '\n'
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>;
350
547
  }
351
548
 
352
- // Plain JSON
353
- let config: Record<string, any> = {}
354
- if (existingContent) {
355
- try {
356
- config = JSON.parse(existingContent)
357
- } catch {
358
- // ignore malformed, overwrite
359
- }
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;
360
574
  }
361
575
 
362
- const pluginsKey = config.plugins !== undefined ? 'plugins' : 'plugin'
363
- const existing: string[] = config[pluginsKey] || []
364
- const filtered = existing.filter((p: string) => !p.includes(OPENCODE_PLUGIN_NAME))
365
- filtered.push(OPENCODE_PLUGIN)
366
- config[pluginsKey] = filtered
367
- config.$schema = config.$schema || 'https://opencode.ai/config.json'
368
-
369
- return JSON.stringify(config, null, 2) + '\n'
370
- }
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
+ }
371
593
 
372
- // ─── Production CLI entry point ──────────────────────────────────────────────
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";
373
600
 
374
- import { ClackPrompter } from './adapters/clack-prompter.js'
375
- import { FsFileStore } from './adapters/fs-file-store.js'
376
- import { SpawnCommandRunner } from './adapters/spawn-command-runner.js'
377
- import * as os from 'os'
601
+ return JSON.stringify(config, null, 2) + "\n";
602
+ }
378
603
 
379
604
  export async function runSetupCommand(): Promise<void> {
380
- try {
381
- await runSetup({
382
- prompter: new ClackPrompter(),
383
- files: new FsFileStore(),
384
- commands: new SpawnCommandRunner(),
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
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);
401
627
  }
628
+ throw err;
629
+ }
402
630
  }