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,60 +1,62 @@
1
- import { describe, expect, it } from "vitest";
2
- import { runSetup } from "../setup";
3
- import { CancelledError, CommandFailedError, PrerequisiteError } from "../errors";
4
- import { FakePrompter, CANCEL, select, confirm, multiselect } from "./fake-prompter";
5
- import { FakeFileStore } from "./fake-file-store";
6
- import { FakeCommandRunner } from "./fake-command-runner";
7
- import { FakeAuthService } from "./fake-auth-service";
8
- import { FakeApiKeyService } from "./fake-api-key-service";
9
- import type { AuthServicePort, ApiKeyServicePort } from "../ports/auth-services";
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { ApiKeyServicePort, AuthServicePort } from '../ports/auth-services';
4
+
5
+ import { CancelledError, CommandFailedError, PrerequisiteError } from '../errors';
6
+ import { runSetup } from '../setup';
7
+ import { FakeApiKeyService } from './fake-api-key-service';
8
+ import { FakeAuthService } from './fake-auth-service';
9
+ import { FakeCommandRunner } from './fake-command-runner';
10
+ import { FakeFileStore } from './fake-file-store';
11
+ import { CANCEL, confirm, FakePrompter, multiselect, select } from './fake-prompter';
10
12
 
11
13
  const makeDeps = (
12
- overrides: Partial<Parameters<typeof runSetup>[0]> = {}
14
+ overrides: Partial<Parameters<typeof runSetup>[0]> = {},
13
15
  ): Parameters<typeof runSetup>[0] => {
14
16
  return {
15
- prompter: overrides.prompter ?? new FakePrompter([]),
16
- files: overrides.files ?? new FakeFileStore(),
17
+ apiKeyService:
18
+ (overrides.apiKeyService as ApiKeyServicePort) ?? new FakeApiKeyService('sk_ber_test'),
19
+ authService: (overrides.authService as AuthServicePort) ?? new FakeAuthService(false),
17
20
  commands:
18
21
  overrides.commands ??
19
22
  new FakeCommandRunner()
20
- .handle("opencode --version", "mocked")
21
- .handle("pi --version", "mocked"),
22
- authService: (overrides.authService as AuthServicePort) ?? new FakeAuthService(false),
23
- apiKeyService:
24
- (overrides.apiKeyService as ApiKeyServicePort) ?? new FakeApiKeyService("sk_ber_test"),
25
- homeDir: "/home/user",
26
- cwd: "/home/user/project",
23
+ .handle('opencode --version', 'mocked')
24
+ .handle('pi --version', 'mocked'),
25
+ cwd: '/home/user/project',
26
+ files: overrides.files ?? new FakeFileStore(),
27
+ homeDir: '/home/user',
28
+ prompter: overrides.prompter ?? new FakePrompter([]),
27
29
  ...Object.fromEntries(
28
30
  Object.entries(overrides).filter(
29
31
  ([k]) =>
30
- k !== "prompter" &&
31
- k !== "files" &&
32
- k !== "commands" &&
33
- k !== "authService" &&
34
- k !== "apiKeyService"
35
- )
32
+ k !== 'prompter' &&
33
+ k !== 'files' &&
34
+ k !== 'commands' &&
35
+ k !== 'authService' &&
36
+ k !== 'apiKeyService',
37
+ ),
36
38
  ),
37
39
  };
38
40
  };
39
41
 
40
42
  function base64urlEncode(data: string): string {
41
- return Buffer.from(data).toString("base64url");
43
+ return Buffer.from(data).toString('base64url');
42
44
  }
43
45
 
44
46
  function makeJwt(payload: Record<string, unknown>): string {
45
- const header = base64urlEncode(JSON.stringify({ alg: "none", typ: "JWT" }));
47
+ const header = base64urlEncode(JSON.stringify({ alg: 'none', typ: 'JWT' }));
46
48
  const body = base64urlEncode(JSON.stringify(payload));
47
49
  return `${header}.${body}.signature`;
48
50
  }
49
51
 
50
- describe("runSetup", () => {
51
- describe("happy path", () => {
52
- it("sets up opencode project without existing config", async () => {
52
+ describe('runSetup', () => {
53
+ describe('happy path', () => {
54
+ it('sets up opencode project without existing config', async () => {
53
55
  const deps = makeDeps({
54
56
  prompter: new FakePrompter([
55
- select("opencode"),
56
- select("project"),
57
- confirm(true, "Create"), // Config write
57
+ select('opencode'),
58
+ select('project'),
59
+ confirm(true, 'Create'), // Config write
58
60
  multiselect([]), // No agents selected
59
61
  ]),
60
62
  });
@@ -63,17 +65,17 @@ describe("runSetup", () => {
63
65
 
64
66
  const files = deps.files as FakeFileStore;
65
67
  const written = files.getWrittenFiles();
66
- expect(written.has("/home/user/project/opencode.json")).toBe(true);
67
- const config = JSON.parse(written.get("/home/user/project/opencode.json")!);
68
- expect(config.plugin).toContain("@bergetai/opencode-auth@1.0.16");
68
+ expect(written.has('/home/user/project/opencode.json')).toBe(true);
69
+ const config = JSON.parse(written.get('/home/user/project/opencode.json')!);
70
+ expect(config.plugin).toContain('@bergetai/opencode-auth');
69
71
  });
70
72
 
71
- it("sets up opencode globally without existing config", async () => {
73
+ it('sets up opencode globally without existing config', async () => {
72
74
  const deps = makeDeps({
73
75
  prompter: new FakePrompter([
74
- select("opencode"),
75
- select("global"),
76
- confirm(true, "Create"), // Config write
76
+ select('opencode'),
77
+ select('global'),
78
+ confirm(true, 'Create'), // Config write
77
79
  multiselect([]), // No agents selected
78
80
  ]),
79
81
  });
@@ -82,40 +84,40 @@ describe("runSetup", () => {
82
84
 
83
85
  const files = deps.files as FakeFileStore;
84
86
  const written = files.getWrittenFiles();
85
- expect(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
87
+ expect(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
86
88
  });
87
89
 
88
- it("sets up pi project with fresh install", async () => {
90
+ it('sets up pi project with fresh install', async () => {
89
91
  const deps = makeDeps({
92
+ commands: new FakeCommandRunner()
93
+ .handle('pi --version', 'mocked') // For checkInstalled
94
+ .handle('pi install', ''), // For actual install
90
95
  prompter: new FakePrompter([
91
- select("pi"),
92
- select("project"),
93
- select("fullstack"), // Agent selection
94
- confirm(true, "Create"),
96
+ select('pi'),
97
+ select('project'),
98
+ select('fullstack'), // Agent selection
99
+ confirm(true, 'Create'),
95
100
  ]),
96
- commands: new FakeCommandRunner()
97
- .handle("pi --version", "mocked") // For checkInstalled
98
- .handle("pi install", ""), // For actual install
99
101
  });
100
102
 
101
103
  await runSetup(deps);
102
104
 
103
105
  const commands = deps.commands as FakeCommandRunner;
104
106
  expect(commands.calls.length).toBeGreaterThan(0);
105
- const installCall = commands.calls.find(c => c.command === "pi");
106
- expect(installCall?.args).toContain("npm:@bergetai/pi-provider");
107
+ const installCall = commands.calls.find((c) => c.command === 'pi');
108
+ expect(installCall?.args).toContain('npm:@bergetai/pi-provider');
107
109
  });
108
110
 
109
- it("skips agent selection for pi project", async () => {
111
+ it('skips agent selection for pi project', async () => {
110
112
  const deps = makeDeps({
113
+ commands: new FakeCommandRunner()
114
+ .handle('pi --version', 'mocked') // For checkInstalled
115
+ .handle('pi install', ''), // For actual install
111
116
  prompter: new FakePrompter([
112
- select("pi"),
113
- select("project"),
114
- select("__skip__"), // Skip agent selection
117
+ select('pi'),
118
+ select('project'),
119
+ select('__skip__'), // Skip agent selection
115
120
  ]),
116
- commands: new FakeCommandRunner()
117
- .handle("pi --version", "mocked") // For checkInstalled
118
- .handle("pi install", ""), // For actual install
119
121
  });
120
122
 
121
123
  await runSetup(deps);
@@ -124,16 +126,16 @@ describe("runSetup", () => {
124
126
  const written = files.getWrittenFiles();
125
127
  // Should not create any agent files
126
128
  for (const path of written.keys()) {
127
- expect(path).not.toContain("SYSTEM.md");
129
+ expect(path).not.toContain('SYSTEM.md');
128
130
  }
129
131
  });
130
132
  });
131
133
 
132
- describe("prerequisites", () => {
133
- it("throws PrerequisiteError when opencode is not installed", async () => {
134
+ describe('prerequisites', () => {
135
+ it('throws PrerequisiteError when opencode is not installed', async () => {
134
136
  const deps = makeDeps({
135
- prompter: new FakePrompter([select("opencode"), select("project")]),
136
137
  commands: new FakeCommandRunner(),
138
+ prompter: new FakePrompter([select('opencode'), select('project')]),
137
139
  });
138
140
 
139
141
  // Simulate opencode not being installed
@@ -141,8 +143,8 @@ describe("runSetup", () => {
141
143
  });
142
144
  });
143
145
 
144
- describe("cancellation", () => {
145
- it("throws CancelledError when user cancels at tool selection", async () => {
146
+ describe('cancellation', () => {
147
+ it('throws CancelledError when user cancels at tool selection', async () => {
146
148
  const deps = makeDeps({
147
149
  prompter: new FakePrompter([select(CANCEL)]),
148
150
  });
@@ -150,171 +152,171 @@ describe("runSetup", () => {
150
152
  await expect(runSetup(deps)).rejects.toBeInstanceOf(CancelledError);
151
153
  });
152
154
 
153
- it("throws CancelledError when user cancels at write confirmation", async () => {
155
+ it('throws CancelledError when user cancels at write confirmation', async () => {
154
156
  const deps = makeDeps({
155
157
  prompter: new FakePrompter([
156
- select("opencode"),
157
- select("project"),
158
- confirm(false, "Create"),
158
+ select('opencode'),
159
+ select('project'),
160
+ confirm(false, 'Create'),
159
161
  ]),
160
162
  });
161
163
 
162
164
  await expect(runSetup(deps)).rejects.toBeInstanceOf(CancelledError);
163
165
  });
164
166
 
165
- it("throws CancelledError when user cancels at agent write confirmation (opencode)", async () => {
167
+ it('throws CancelledError when user cancels at agent write confirmation (opencode)', async () => {
166
168
  const deps = makeDeps({
167
169
  prompter: new FakePrompter([
168
- select("opencode"),
169
- select("project"),
170
- confirm(true, "Create"),
171
- multiselect(["backend", "frontend"]),
172
- confirm(false, "agent"),
170
+ select('opencode'),
171
+ select('project'),
172
+ confirm(true, 'Create'),
173
+ multiselect(['backend', 'frontend']),
174
+ confirm(false, 'agent'),
173
175
  ]),
174
176
  });
175
177
 
176
178
  await expect(runSetup(deps)).rejects.toBeInstanceOf(CancelledError);
177
179
  });
178
180
 
179
- it("throws CancelledError when user cancels at agent write confirmation (pi)", async () => {
181
+ it('throws CancelledError when user cancels at agent write confirmation (pi)', async () => {
180
182
  const deps = makeDeps({
183
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
181
184
  prompter: new FakePrompter([
182
- select("pi"),
183
- select("project"),
184
- select("fullstack"),
185
+ select('pi'),
186
+ select('project'),
187
+ select('fullstack'),
185
188
  confirm(false, /Create|Overwrite/),
186
189
  ]),
187
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
188
190
  });
189
191
 
190
192
  await expect(runSetup(deps)).rejects.toBeInstanceOf(CancelledError);
191
193
  });
192
194
  });
193
195
 
194
- describe("file operations", () => {
195
- it("preserves existing configuration keys when updating", async () => {
196
+ describe('file operations', () => {
197
+ it('preserves existing configuration keys when updating', async () => {
196
198
  const deps = makeDeps({
197
199
  prompter: new FakePrompter([
198
- select("opencode"),
199
- select("project"),
200
- confirm(true, "Write"),
200
+ select('opencode'),
201
+ select('project'),
202
+ confirm(true, 'Write'),
201
203
  multiselect([]),
202
204
  ]),
203
205
  });
204
206
 
205
207
  const files = deps.files as FakeFileStore;
206
208
  files.seed(
207
- "/home/user/project/opencode.json",
209
+ '/home/user/project/opencode.json',
208
210
  JSON.stringify({
209
- customField: "should-preserve",
210
- plugin: ["other-plugin"],
211
- })
211
+ customField: 'should-preserve',
212
+ plugin: ['other-plugin'],
213
+ }),
212
214
  );
213
215
 
214
216
  await runSetup(deps);
215
217
 
216
218
  const written = files.getWrittenFiles();
217
- const config = JSON.parse(written.get("/home/user/project/opencode.json")!);
218
- expect(config.customField).toBe("should-preserve");
219
- expect(config.plugin).toContain("other-plugin");
220
- expect(config.plugin).toContain("@bergetai/opencode-auth@1.0.16");
219
+ const config = JSON.parse(written.get('/home/user/project/opencode.json')!);
220
+ expect(config.customField).toBe('should-preserve');
221
+ expect(config.plugin).toContain('other-plugin');
222
+ expect(config.plugin).toContain('@bergetai/opencode-auth');
221
223
  });
222
224
 
223
- it("preserves jsonc comments when updating", async () => {
225
+ it('preserves jsonc comments when updating', async () => {
224
226
  const deps = makeDeps({
225
227
  prompter: new FakePrompter([
226
- select("opencode"),
227
- select("project"),
228
- confirm(true, "Write"),
228
+ select('opencode'),
229
+ select('project'),
230
+ confirm(true, 'Write'),
229
231
  multiselect([]),
230
232
  ]),
231
233
  });
232
234
 
233
235
  const files = deps.files as FakeFileStore;
234
236
  files.seed(
235
- "/home/user/project/opencode.jsonc",
237
+ '/home/user/project/opencode.jsonc',
236
238
  `{
237
239
  // This is my custom config
238
240
  "customField": "should-preserve",
239
241
  /* block comment explaining plugin */
240
242
  "plugin": ["other-plugin"]
241
- }`
243
+ }`,
242
244
  );
243
245
 
244
246
  await runSetup(deps);
245
247
 
246
248
  const written = files.getWrittenFiles();
247
- const content = written.get("/home/user/project/opencode.jsonc")!;
248
- expect(content).toContain("// This is my custom config");
249
- expect(content).toContain("/* block comment explaining plugin */");
249
+ const content = written.get('/home/user/project/opencode.jsonc')!;
250
+ expect(content).toContain('// This is my custom config');
251
+ expect(content).toContain('/* block comment explaining plugin */');
250
252
  expect(content).toContain('"customField": "should-preserve"');
251
- expect(content).toContain("@bergetai/opencode-auth@1.0.16");
253
+ expect(content).toContain('@bergetai/opencode-auth');
252
254
  });
253
255
 
254
- it("shows no changes needed when config is already up to date", async () => {
256
+ it('shows no changes needed when config is already up to date', async () => {
255
257
  const deps = makeDeps({
256
- prompter: new FakePrompter([select("opencode"), select("project"), multiselect([])]),
258
+ prompter: new FakePrompter([select('opencode'), select('project'), multiselect([])]),
257
259
  });
258
260
 
259
261
  const files = deps.files as FakeFileStore;
260
262
  // Already has the exact same plugin version
261
263
  files.seed(
262
- "/home/user/project/opencode.json",
264
+ '/home/user/project/opencode.json',
263
265
  JSON.stringify(
264
266
  {
265
- $schema: "https://opencode.ai/config.json",
266
- plugin: ["@bergetai/opencode-auth@1.0.16"],
267
+ $schema: 'https://opencode.ai/config.json',
268
+ plugin: ['@bergetai/opencode-auth'],
267
269
  },
268
270
  null,
269
- 2
270
- ) + "\n"
271
+ 2,
272
+ ) + '\n',
271
273
  );
272
274
 
273
275
  await runSetup(deps);
274
276
 
275
277
  // Check that no write happened — content should be unchanged
276
278
  const written = files.getWrittenFiles();
277
- const content = written.get("/home/user/project/opencode.json")!;
279
+ const content = written.get('/home/user/project/opencode.json')!;
278
280
  const config = JSON.parse(content);
279
- expect(config.plugin).toEqual(["@bergetai/opencode-auth@1.0.16"]);
280
- expect(content).toContain("$schema");
281
+ expect(config.plugin).toEqual(['@bergetai/opencode-auth']);
282
+ expect(content).toContain('$schema');
281
283
  });
282
284
 
283
- it("preserves existing Pi settings when setting defaultProvider", async () => {
285
+ it('preserves existing Pi settings when setting defaultProvider', async () => {
284
286
  const deps = makeDeps({
287
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
285
288
  prompter: new FakePrompter([
286
- select("pi"),
287
- select("project"),
288
- select("fullstack"),
289
- confirm(true, "Create"),
289
+ select('pi'),
290
+ select('project'),
291
+ select('fullstack'),
292
+ confirm(true, 'Create'),
290
293
  ]),
291
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
292
294
  });
293
295
 
294
296
  const files = deps.files as FakeFileStore;
295
297
  files.seed(
296
- "/home/user/project/.pi/settings.json",
298
+ '/home/user/project/.pi/settings.json',
297
299
  JSON.stringify({
298
- existingKey: "should-preserve",
299
300
  anotherSetting: true,
300
- })
301
+ existingKey: 'should-preserve',
302
+ }),
301
303
  );
302
304
 
303
305
  await runSetup(deps);
304
306
 
305
307
  const written = files.getWrittenFiles();
306
- const settings = JSON.parse(written.get("/home/user/project/.pi/settings.json")!);
307
- expect(settings.existingKey).toBe("should-preserve");
308
+ const settings = JSON.parse(written.get('/home/user/project/.pi/settings.json')!);
309
+ expect(settings.existingKey).toBe('should-preserve');
308
310
  expect(settings.anotherSetting).toBe(true);
309
- expect(settings.defaultProvider).toBe("berget");
311
+ expect(settings.defaultProvider).toBe('berget');
310
312
  });
311
313
 
312
- it("creates parent directories when writing files", async () => {
314
+ it('creates parent directories when writing files', async () => {
313
315
  const deps = makeDeps({
314
316
  prompter: new FakePrompter([
315
- select("opencode"),
316
- select("global"),
317
- confirm(true, "Create"),
317
+ select('opencode'),
318
+ select('global'),
319
+ confirm(true, 'Create'),
318
320
  multiselect([]),
319
321
  ]),
320
322
  });
@@ -323,157 +325,157 @@ describe("runSetup", () => {
323
325
 
324
326
  const files = deps.files as FakeFileStore;
325
327
  const written = files.getWrittenFiles();
326
- expect(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
328
+ expect(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
327
329
  });
328
330
  });
329
331
 
330
- describe("command execution", () => {
331
- it("passes arguments as array (no shell injection)", async () => {
332
+ describe('command execution', () => {
333
+ it('passes arguments as array (no shell injection)', async () => {
332
334
  const deps = makeDeps({
335
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
333
336
  prompter: new FakePrompter([
334
- select("pi"),
335
- select("project"),
336
- select("fullstack"),
337
- confirm(true, "Create"),
337
+ select('pi'),
338
+ select('project'),
339
+ select('fullstack'),
340
+ confirm(true, 'Create'),
338
341
  ]),
339
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
340
342
  });
341
343
 
342
344
  await runSetup(deps);
343
345
 
344
346
  const commands = deps.commands as FakeCommandRunner;
345
- const installCall = commands.calls.find(c => c.command === "pi");
346
- expect(installCall?.args).toContain("npm:@bergetai/pi-provider");
347
- expect(installCall?.args).toContain("-l");
347
+ const installCall = commands.calls.find((c) => c.command === 'pi');
348
+ expect(installCall?.args).toContain('npm:@bergetai/pi-provider');
349
+ expect(installCall?.args).toContain('-l');
348
350
  });
349
351
  });
350
352
 
351
- describe("error handling", () => {
352
- it("throws CommandFailedError when pi install fails", async () => {
353
+ describe('error handling', () => {
354
+ it('throws CommandFailedError when pi install fails', async () => {
353
355
  const deps = makeDeps({
354
- prompter: new FakePrompter([select("pi"), select("project")]),
355
356
  commands: new FakeCommandRunner()
356
- .handle("pi --version", "mocked")
357
- .handle("pi install", new Error("npm error")),
357
+ .handle('pi --version', 'mocked')
358
+ .handle('pi install', new Error('npm error')),
359
+ prompter: new FakePrompter([select('pi'), select('project')]),
358
360
  });
359
361
 
360
362
  await expect(runSetup(deps)).rejects.toBeInstanceOf(CommandFailedError);
361
363
  });
362
364
  });
363
365
 
364
- describe("auth integration", () => {
365
- it("already authenticated shows simplified message", async () => {
366
+ describe('auth integration', () => {
367
+ it('already authenticated shows simplified message', async () => {
366
368
  const files = new FakeFileStore();
367
369
  files.seed(
368
- "/home/user/.local/share/opencode/auth.json",
369
- JSON.stringify({ berget: { type: "oauth" } })
370
+ '/home/user/.local/share/opencode/auth.json',
371
+ JSON.stringify({ berget: { type: 'oauth' } }),
370
372
  );
371
373
 
372
374
  const deps = makeDeps({
375
+ files,
373
376
  prompter: new FakePrompter([
374
- select("opencode"),
375
- select("project"),
376
- select("keep"), // New: keep existing auth
377
- confirm(true, "Create"), // Config write
377
+ select('opencode'),
378
+ select('project'),
379
+ select('keep'), // New: keep existing auth
380
+ confirm(true, 'Create'), // Config write
378
381
  multiselect([]),
379
382
  ]),
380
- files,
381
383
  });
382
384
 
383
385
  await runSetup(deps);
384
386
 
385
387
  const prompter = deps.prompter as FakePrompter;
386
- const notes = prompter.calls.filter(c => c.method === "note");
387
- const lastNote = notes[notes.length - 1];
388
- expect(JSON.stringify(lastNote)).toContain("Run: opencode");
389
- expect(JSON.stringify(lastNote)).not.toContain("/connect");
388
+ const notes = prompter.calls.filter((c) => c.method === 'note');
389
+ const lastNote = notes.at(-1);
390
+ expect(JSON.stringify(lastNote)).toContain('Run: opencode');
391
+ expect(JSON.stringify(lastNote)).not.toContain('/connect');
390
392
  });
391
393
 
392
- it("login failure shows manual auth instructions", async () => {
394
+ it('login failure shows manual auth instructions', async () => {
393
395
  const deps = makeDeps({
394
- prompter: new FakePrompter([
395
- select("pi"),
396
- select("project"),
397
- select("fullstack"),
398
- confirm(true, "Create"),
399
- ]),
400
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
401
396
  authService: new FakeAuthService(false),
397
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
402
398
  files: new FakeFileStore(), // No pre-seeded auth → auth flow runs
399
+ prompter: new FakePrompter([
400
+ select('pi'),
401
+ select('project'),
402
+ select('fullstack'),
403
+ confirm(true, 'Create'),
404
+ ]),
403
405
  });
404
406
 
405
407
  await runSetup(deps);
406
408
 
407
409
  const prompter = deps.prompter as FakePrompter;
408
- const notes = prompter.calls.filter(c => c.method === "note");
409
- const lastNote = notes[notes.length - 1];
410
- expect(JSON.stringify(lastNote)).toContain("/login");
410
+ const notes = prompter.calls.filter((c) => c.method === 'note');
411
+ const lastNote = notes.at(-1);
412
+ expect(JSON.stringify(lastNote)).toContain('/login');
411
413
  });
412
414
 
413
- it("creates api key for pi when no seat", async () => {
415
+ it('creates api key for pi when no seat', async () => {
414
416
  const files = new FakeFileStore();
415
417
 
416
418
  const deps = makeDeps({
419
+ authService: new FakeAuthService(true, false), // succeed, no seat
420
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
421
+ files,
417
422
  prompter: new FakePrompter([
418
- select("pi"),
419
- select("project"),
423
+ select('pi'),
424
+ select('project'),
420
425
  confirm(true), // API key creation prompt
421
- select("fullstack"),
422
- confirm(true, "Create"),
426
+ select('fullstack'),
427
+ confirm(true, 'Create'),
423
428
  ]),
424
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
425
- authService: new FakeAuthService(true, false), // succeed, no seat
426
- files,
427
429
  });
428
430
 
429
431
  await runSetup(deps);
430
432
 
431
433
  const written = files.getWrittenFiles();
432
- expect(written.has("/home/user/.pi/agent/auth.json")).toBe(true);
433
- const parsed = JSON.parse(written.get("/home/user/.pi/agent/auth.json")!);
434
- expect(parsed.berget.type).toBe("api_key");
434
+ expect(written.has('/home/user/.pi/agent/auth.json')).toBe(true);
435
+ const parsed = JSON.parse(written.get('/home/user/.pi/agent/auth.json')!);
436
+ expect(parsed.berget.type).toBe('api_key');
435
437
  });
436
438
 
437
- it("uses subscription when berget_code_seat present", async () => {
439
+ it('uses subscription when berget_code_seat present', async () => {
438
440
  const files = new FakeFileStore();
439
441
  const farFuture = Math.floor(Date.now() / 1000) + 3600 * 24 * 365; // 1 year from now in seconds
440
442
  files.seed(
441
- "/home/user/.berget/auth.json",
443
+ '/home/user/.berget/auth.json',
442
444
  JSON.stringify({
443
- access_token: makeJwt({ realm_access: { roles: ["berget_code_seat"] }, exp: farFuture }),
444
- refresh_token: "ref",
445
+ access_token: makeJwt({ exp: farFuture, realm_access: { roles: ['berget_code_seat'] } }),
445
446
  expires_at: farFuture * 1000,
446
- })
447
+ refresh_token: 'ref',
448
+ }),
447
449
  );
448
450
 
449
451
  const deps = makeDeps({
452
+ files,
450
453
  prompter: new FakePrompter([
451
- select("opencode"),
452
- select("project"),
453
- select("subscription"),
454
- confirm(true, "Create"),
454
+ select('opencode'),
455
+ select('project'),
456
+ select('subscription'),
457
+ confirm(true, 'Create'),
455
458
  multiselect([]),
456
459
  ]),
457
- files,
458
460
  });
459
461
 
460
462
  await runSetup(deps);
461
463
 
462
464
  const written = files.getWrittenFiles();
463
- const parsed = JSON.parse(written.get("/home/user/.local/share/opencode/auth.json")!);
464
- expect(parsed.berget.type).toBe("oauth");
465
+ const parsed = JSON.parse(written.get('/home/user/.local/share/opencode/auth.json')!);
466
+ expect(parsed.berget.type).toBe('oauth');
465
467
  });
466
468
  });
467
469
 
468
- describe("agent configuration", () => {
469
- it("sets up multiple agents for opencode project", async () => {
470
+ describe('agent configuration', () => {
471
+ it('sets up multiple agents for opencode project', async () => {
470
472
  const deps = makeDeps({
471
473
  prompter: new FakePrompter([
472
- select("opencode"),
473
- select("project"),
474
- confirm(true, "Create"),
475
- multiselect(["backend", "frontend"]),
476
- confirm(true, "agent"),
474
+ select('opencode'),
475
+ select('project'),
476
+ confirm(true, 'Create'),
477
+ multiselect(['backend', 'frontend']),
478
+ confirm(true, 'agent'),
477
479
  ]),
478
480
  });
479
481
 
@@ -481,16 +483,16 @@ describe("runSetup", () => {
481
483
 
482
484
  const files = deps.files as FakeFileStore;
483
485
  const written = files.getWrittenFiles();
484
- expect(written.has("/home/user/project/.opencode/agents/backend.md")).toBe(true);
485
- expect(written.has("/home/user/project/.opencode/agents/frontend.md")).toBe(true);
486
+ expect(written.has('/home/user/project/.opencode/agents/backend.md')).toBe(true);
487
+ expect(written.has('/home/user/project/.opencode/agents/frontend.md')).toBe(true);
486
488
  });
487
489
 
488
- it("sets up no agents for opencode when none selected", async () => {
490
+ it('sets up no agents for opencode when none selected', async () => {
489
491
  const deps = makeDeps({
490
492
  prompter: new FakePrompter([
491
- select("opencode"),
492
- select("project"),
493
- confirm(true, "Create"),
493
+ select('opencode'),
494
+ select('project'),
495
+ confirm(true, 'Create'),
494
496
  multiselect([]),
495
497
  ]),
496
498
  });
@@ -504,14 +506,14 @@ describe("runSetup", () => {
504
506
  }
505
507
  });
506
508
 
507
- it("sets up agent globally for opencode", async () => {
509
+ it('sets up agent globally for opencode', async () => {
508
510
  const deps = makeDeps({
509
511
  prompter: new FakePrompter([
510
- select("opencode"),
511
- select("global"),
512
- confirm(true, "Create"),
513
- multiselect(["fullstack"]),
514
- confirm(true, "agent"),
512
+ select('opencode'),
513
+ select('global'),
514
+ confirm(true, 'Create'),
515
+ multiselect(['fullstack']),
516
+ confirm(true, 'agent'),
515
517
  ]),
516
518
  });
517
519
 
@@ -519,53 +521,53 @@ describe("runSetup", () => {
519
521
 
520
522
  const files = deps.files as FakeFileStore;
521
523
  const written = files.getWrittenFiles();
522
- expect(written.has("/home/user/.config/opencode/agents/fullstack.md")).toBe(true);
524
+ expect(written.has('/home/user/.config/opencode/agents/fullstack.md')).toBe(true);
523
525
  });
524
526
 
525
- it("sets up agent for pi project", async () => {
527
+ it('sets up agent for pi project', async () => {
526
528
  const deps = makeDeps({
529
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
527
530
  prompter: new FakePrompter([
528
- select("pi"),
529
- select("project"),
530
- select("fullstack"),
531
- confirm(true, "Create"),
531
+ select('pi'),
532
+ select('project'),
533
+ select('fullstack'),
534
+ confirm(true, 'Create'),
532
535
  ]),
533
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
534
536
  });
535
537
 
536
538
  await runSetup(deps);
537
539
 
538
540
  const files = deps.files as FakeFileStore;
539
541
  const written = files.getWrittenFiles();
540
- expect(written.has("/home/user/project/.pi/SYSTEM.md")).toBe(true);
542
+ expect(written.has('/home/user/project/.pi/SYSTEM.md')).toBe(true);
541
543
  });
542
544
 
543
- it("sets up agent for pi globally", async () => {
545
+ it('sets up agent for pi globally', async () => {
544
546
  const deps = makeDeps({
547
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
545
548
  prompter: new FakePrompter([
546
- select("pi"),
547
- select("global"),
548
- select("backend"),
549
- confirm(true, "Create"),
549
+ select('pi'),
550
+ select('global'),
551
+ select('backend'),
552
+ confirm(true, 'Create'),
550
553
  ]),
551
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
552
554
  });
553
555
 
554
556
  await runSetup(deps);
555
557
 
556
558
  const files = deps.files as FakeFileStore;
557
559
  const written = files.getWrittenFiles();
558
- expect(written.has("/home/user/.pi/agent/SYSTEM.md")).toBe(true);
560
+ expect(written.has('/home/user/.pi/agent/SYSTEM.md')).toBe(true);
559
561
  });
560
562
 
561
- it("skips writing identical opencode agent files", async () => {
563
+ it('skips writing identical opencode agent files', async () => {
562
564
  const deps = makeDeps({
563
565
  prompter: new FakePrompter([
564
- select("opencode"),
565
- select("project"),
566
- confirm(true, "Create"),
567
- multiselect(["backend", "frontend"]),
568
- confirm(true, "agent"),
566
+ select('opencode'),
567
+ select('project'),
568
+ confirm(true, 'Create'),
569
+ multiselect(['backend', 'frontend']),
570
+ confirm(true, 'agent'),
569
571
  ]),
570
572
  });
571
573
 
@@ -575,54 +577,54 @@ describe("runSetup", () => {
575
577
  const files = deps.files as FakeFileStore;
576
578
  const firstBackend = files
577
579
  .getWrittenFiles()
578
- .get("/home/user/project/.opencode/agents/backend.md");
580
+ .get('/home/user/project/.opencode/agents/backend.md');
579
581
  const firstFrontend = files
580
582
  .getWrittenFiles()
581
- .get("/home/user/project/.opencode/agents/frontend.md");
583
+ .get('/home/user/project/.opencode/agents/frontend.md');
582
584
 
583
585
  // Second run with exact same content should not prompt for overwrite
584
586
  const deps2 = makeDeps({
585
587
  files,
586
588
  prompter: new FakePrompter([
587
- select("opencode"),
588
- select("project"),
589
- multiselect(["backend", "frontend"]),
589
+ select('opencode'),
590
+ select('project'),
591
+ multiselect(['backend', 'frontend']),
590
592
  ]),
591
593
  });
592
594
 
593
595
  await runSetup(deps2);
594
596
 
595
597
  // Content should be unchanged
596
- expect(files.getWrittenFiles().get("/home/user/project/.opencode/agents/backend.md")).toBe(
597
- firstBackend
598
+ expect(files.getWrittenFiles().get('/home/user/project/.opencode/agents/backend.md')).toBe(
599
+ firstBackend,
598
600
  );
599
- expect(files.getWrittenFiles().get("/home/user/project/.opencode/agents/frontend.md")).toBe(
600
- firstFrontend
601
+ expect(files.getWrittenFiles().get('/home/user/project/.opencode/agents/frontend.md')).toBe(
602
+ firstFrontend,
601
603
  );
602
604
  });
603
605
 
604
- it("overwrites pi SYSTEM.md when content differs", async () => {
606
+ it('overwrites pi SYSTEM.md when content differs', async () => {
605
607
  const files = new FakeFileStore();
606
- files.seed("/home/user/project/.pi/SYSTEM.md", "old agent content");
608
+ files.seed('/home/user/project/.pi/SYSTEM.md', 'old agent content');
607
609
 
608
610
  const deps = makeDeps({
611
+ commands: new FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
612
+ files,
609
613
  prompter: new FakePrompter([
610
- select("pi"),
611
- select("project"),
612
- select("fullstack"),
613
- confirm(true, "Overwrite"),
614
+ select('pi'),
615
+ select('project'),
616
+ select('fullstack'),
617
+ confirm(true, 'Overwrite'),
614
618
  ]),
615
- files,
616
- commands: new FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
617
619
  });
618
620
 
619
621
  await runSetup(deps);
620
622
 
621
623
  const written = files.getWrittenFiles();
622
- const content = written.get("/home/user/project/.pi/SYSTEM.md");
623
- expect(content).not.toBe("old agent content");
624
+ const content = written.get('/home/user/project/.pi/SYSTEM.md');
625
+ expect(content).not.toBe('old agent content');
624
626
  // Pi doesn't use front matter, so check for system prompt content
625
- expect(content).toContain("Fullstack Agent");
627
+ expect(content).toContain('Fullstack Agent');
626
628
  });
627
629
  });
628
630
  });