berget 2.2.6 → 2.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/.github/workflows/test.yml +10 -4
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +7 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +10 -11
  9. package/dist/package.json +30 -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 +97 -117
  20. package/dist/src/commands/api-keys.js +75 -90
  21. package/dist/src/commands/auth.js +7 -16
  22. package/dist/src/commands/autocomplete.js +1 -1
  23. package/dist/src/commands/billing.js +6 -17
  24. package/dist/src/commands/chat.js +68 -101
  25. package/dist/src/commands/clusters.js +9 -18
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
  33. package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
  34. package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
  36. package/dist/src/commands/code/auth-sync.js +270 -0
  37. package/dist/src/commands/code/errors.js +12 -9
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +387 -281
  40. package/dist/src/commands/code.js +205 -332
  41. package/dist/src/commands/index.js +5 -5
  42. package/dist/src/commands/models.js +6 -17
  43. package/dist/src/commands/users.js +5 -16
  44. package/dist/src/constants/command-structure.js +104 -104
  45. package/dist/src/services/api-key-service.js +132 -157
  46. package/dist/src/services/auth-service.js +89 -342
  47. package/dist/src/services/browser-auth.js +268 -0
  48. package/dist/src/services/chat-service.js +371 -401
  49. package/dist/src/services/cluster-service.js +47 -62
  50. package/dist/src/services/collaborator-service.js +10 -25
  51. package/dist/src/services/flux-service.js +14 -29
  52. package/dist/src/services/helm-service.js +10 -25
  53. package/dist/src/services/kubectl-service.js +16 -33
  54. package/dist/src/utils/config-checker.js +3 -3
  55. package/dist/src/utils/config-loader.js +95 -95
  56. package/dist/src/utils/default-api-key.js +124 -134
  57. package/dist/src/utils/env-manager.js +55 -66
  58. package/dist/src/utils/error-handler.js +20 -21
  59. package/dist/src/utils/logger.js +72 -65
  60. package/dist/src/utils/markdown-renderer.js +27 -27
  61. package/dist/src/utils/opencode-validator.js +63 -68
  62. package/dist/src/utils/token-manager.js +74 -45
  63. package/dist/tests/commands/chat.test.js +16 -25
  64. package/dist/tests/commands/code.test.js +95 -104
  65. package/dist/tests/utils/config-loader.test.js +48 -48
  66. package/dist/tests/utils/env-manager.test.js +43 -52
  67. package/dist/tests/utils/opencode-validator.test.js +22 -21
  68. package/dist/vitest.config.js +1 -1
  69. package/eslint.config.mjs +67 -0
  70. package/index.ts +35 -42
  71. package/package.json +30 -2
  72. package/src/agents/app.ts +27 -0
  73. package/src/agents/backend.ts +24 -0
  74. package/src/agents/devops.ts +33 -0
  75. package/src/agents/frontend.ts +24 -0
  76. package/src/agents/fullstack.ts +24 -0
  77. package/src/agents/index.ts +73 -0
  78. package/src/agents/quality.ts +69 -0
  79. package/src/agents/security.ts +26 -0
  80. package/src/agents/types.ts +17 -0
  81. package/src/client.ts +118 -152
  82. package/src/commands/api-keys.ts +241 -333
  83. package/src/commands/auth.ts +22 -27
  84. package/src/commands/autocomplete.ts +9 -9
  85. package/src/commands/billing.ts +20 -24
  86. package/src/commands/chat.ts +248 -338
  87. package/src/commands/clusters.ts +27 -26
  88. package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
  89. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  90. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  91. package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
  92. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  93. package/src/commands/code/__tests__/fake-prompter.ts +116 -77
  94. package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
  95. package/src/commands/code/adapters/clack-prompter.ts +53 -39
  96. package/src/commands/code/adapters/fs-file-store.ts +32 -27
  97. package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
  98. package/src/commands/code/auth-sync.ts +329 -0
  99. package/src/commands/code/errors.ts +18 -18
  100. package/src/commands/code/ports/auth-services.ts +14 -0
  101. package/src/commands/code/ports/command-runner.ts +8 -4
  102. package/src/commands/code/ports/file-store.ts +5 -4
  103. package/src/commands/code/ports/prompter.ts +24 -18
  104. package/src/commands/code/setup.ts +570 -340
  105. package/src/commands/code.ts +338 -539
  106. package/src/commands/index.ts +20 -19
  107. package/src/commands/models.ts +28 -32
  108. package/src/commands/users.ts +15 -21
  109. package/src/constants/command-structure.ts +134 -157
  110. package/src/services/api-key-service.ts +105 -122
  111. package/src/services/auth-service.ts +99 -345
  112. package/src/services/browser-auth.ts +296 -0
  113. package/src/services/chat-service.ts +265 -299
  114. package/src/services/cluster-service.ts +42 -45
  115. package/src/services/collaborator-service.ts +14 -19
  116. package/src/services/flux-service.ts +23 -25
  117. package/src/services/helm-service.ts +19 -21
  118. package/src/services/kubectl-service.ts +17 -19
  119. package/src/types/api.d.ts +1905 -1907
  120. package/src/types/json.d.ts +2 -2
  121. package/src/utils/config-checker.ts +10 -10
  122. package/src/utils/config-loader.ts +162 -178
  123. package/src/utils/default-api-key.ts +114 -125
  124. package/src/utils/env-manager.ts +53 -57
  125. package/src/utils/error-handler.ts +61 -56
  126. package/src/utils/logger.ts +79 -73
  127. package/src/utils/markdown-renderer.ts +31 -31
  128. package/src/utils/opencode-validator.ts +85 -89
  129. package/src/utils/token-manager.ts +108 -87
  130. package/templates/agents/app.md +1 -0
  131. package/templates/agents/backend.md +1 -0
  132. package/templates/agents/devops.md +2 -0
  133. package/templates/agents/frontend.md +1 -0
  134. package/templates/agents/fullstack.md +1 -0
  135. package/templates/agents/quality.md +45 -40
  136. package/templates/agents/security.md +1 -0
  137. package/tests/commands/chat.test.ts +53 -62
  138. package/tests/commands/code.test.ts +265 -310
  139. package/tests/utils/config-loader.test.ts +189 -188
  140. package/tests/utils/env-manager.test.ts +110 -113
  141. package/tests/utils/opencode-validator.test.ts +52 -56
  142. package/tsconfig.json +4 -3
  143. package/vitest.config.ts +3 -3
  144. package/AGENTS.md +0 -374
  145. package/TODO.md +0 -19
@@ -1,111 +1,170 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  const vitest_1 = require("vitest");
13
- const setup_1 = require("../setup");
14
4
  const errors_1 = require("../errors");
15
- const fake_prompter_1 = require("./fake-prompter");
16
- const fake_file_store_1 = require("./fake-file-store");
5
+ const setup_1 = require("../setup");
6
+ const fake_api_key_service_1 = require("./fake-api-key-service");
7
+ const fake_auth_service_1 = require("./fake-auth-service");
17
8
  const fake_command_runner_1 = require("./fake-command-runner");
18
- const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompter_1.FakePrompter([]), files: new fake_file_store_1.FakeFileStore(), commands: new fake_command_runner_1.FakeCommandRunner()
19
- .handle('opencode --version', 'mocked')
20
- .handle('pi --version', 'mocked'), homeDir: '/home/user', cwd: '/home/user/project' }, overrides));
9
+ const fake_file_store_1 = require("./fake-file-store");
10
+ const fake_prompter_1 = require("./fake-prompter");
11
+ const makeDeps = (overrides = {}) => {
12
+ return {
13
+ apiKeyService: overrides.apiKeyService ?? new fake_api_key_service_1.FakeApiKeyService('sk_ber_test'),
14
+ authService: overrides.authService ?? new fake_auth_service_1.FakeAuthService(false),
15
+ commands: overrides.commands ??
16
+ new fake_command_runner_1.FakeCommandRunner()
17
+ .handle('opencode --version', 'mocked')
18
+ .handle('pi --version', 'mocked'),
19
+ cwd: '/home/user/project',
20
+ files: overrides.files ?? new fake_file_store_1.FakeFileStore(),
21
+ homeDir: '/home/user',
22
+ prompter: overrides.prompter ?? new fake_prompter_1.FakePrompter([]),
23
+ ...Object.fromEntries(Object.entries(overrides).filter(([k]) => k !== 'prompter' &&
24
+ k !== 'files' &&
25
+ k !== 'commands' &&
26
+ k !== 'authService' &&
27
+ k !== 'apiKeyService')),
28
+ };
29
+ };
30
+ function base64urlEncode(data) {
31
+ return Buffer.from(data).toString('base64url');
32
+ }
33
+ function makeJwt(payload) {
34
+ const header = base64urlEncode(JSON.stringify({ alg: 'none', typ: 'JWT' }));
35
+ const body = base64urlEncode(JSON.stringify(payload));
36
+ return `${header}.${body}.signature`;
37
+ }
21
38
  (0, vitest_1.describe)('runSetup', () => {
22
39
  (0, vitest_1.describe)('happy path', () => {
23
- (0, vitest_1.it)('sets up opencode project without existing config', () => __awaiter(void 0, void 0, void 0, function* () {
40
+ (0, vitest_1.it)('sets up opencode project without existing config', async () => {
24
41
  const deps = makeDeps({
25
42
  prompter: new fake_prompter_1.FakePrompter([
26
43
  (0, fake_prompter_1.select)('opencode'),
27
44
  (0, fake_prompter_1.select)('project'),
28
- (0, fake_prompter_1.confirm)(true, 'Create'),
45
+ (0, fake_prompter_1.confirm)(true, 'Create'), // Config write
46
+ (0, fake_prompter_1.multiselect)([]), // No agents selected
29
47
  ]),
30
48
  });
31
- yield (0, setup_1.runSetup)(deps);
49
+ await (0, setup_1.runSetup)(deps);
32
50
  const files = deps.files;
33
51
  const written = files.getWrittenFiles();
34
52
  (0, vitest_1.expect)(written.has('/home/user/project/opencode.json')).toBe(true);
35
53
  const config = JSON.parse(written.get('/home/user/project/opencode.json'));
36
- (0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth@1.0.16');
37
- }));
38
- (0, vitest_1.it)('sets up opencode globally without existing config', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ (0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth');
55
+ });
56
+ (0, vitest_1.it)('sets up opencode globally without existing config', async () => {
39
57
  const deps = makeDeps({
40
58
  prompter: new fake_prompter_1.FakePrompter([
41
59
  (0, fake_prompter_1.select)('opencode'),
42
60
  (0, fake_prompter_1.select)('global'),
43
- (0, fake_prompter_1.confirm)(true, 'Create'),
61
+ (0, fake_prompter_1.confirm)(true, 'Create'), // Config write
62
+ (0, fake_prompter_1.multiselect)([]), // No agents selected
44
63
  ]),
45
64
  });
46
- yield (0, setup_1.runSetup)(deps);
65
+ await (0, setup_1.runSetup)(deps);
47
66
  const files = deps.files;
48
67
  const written = files.getWrittenFiles();
49
68
  (0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
50
- }));
51
- (0, vitest_1.it)('sets up pi project with fresh install', () => __awaiter(void 0, void 0, void 0, function* () {
69
+ });
70
+ (0, vitest_1.it)('sets up pi project with fresh install', async () => {
52
71
  const deps = makeDeps({
72
+ commands: new fake_command_runner_1.FakeCommandRunner()
73
+ .handle('pi --version', 'mocked') // For checkInstalled
74
+ .handle('pi install', ''), // For actual install
53
75
  prompter: new fake_prompter_1.FakePrompter([
54
76
  (0, fake_prompter_1.select)('pi'),
55
77
  (0, fake_prompter_1.select)('project'),
56
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
78
+ (0, fake_prompter_1.select)('fullstack'), // Agent selection
79
+ (0, fake_prompter_1.confirm)(true, 'Create'),
57
80
  ]),
58
- commands: new fake_command_runner_1.FakeCommandRunner()
59
- .handle('pi --version', 'mocked') // For checkInstalled
60
- .handle('pi install', ''), // For actual install
61
81
  });
62
- yield (0, setup_1.runSetup)(deps);
82
+ await (0, setup_1.runSetup)(deps);
63
83
  const commands = deps.commands;
64
84
  (0, vitest_1.expect)(commands.calls.length).toBeGreaterThan(0);
65
- const installCall = commands.calls.find(c => c.command === 'pi');
66
- (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('npm:@bergetai/pi-provider');
67
- }));
68
- });
69
- (0, vitest_1.describe)('prerequisites', () => {
70
- (0, vitest_1.it)('throws PrerequisiteError when opencode is not installed', () => __awaiter(void 0, void 0, void 0, function* () {
85
+ const installCall = commands.calls.find((c) => c.command === 'pi');
86
+ (0, vitest_1.expect)(installCall?.args).toContain('npm:@bergetai/pi-provider');
87
+ });
88
+ (0, vitest_1.it)('skips agent selection for pi project', async () => {
71
89
  const deps = makeDeps({
90
+ commands: new fake_command_runner_1.FakeCommandRunner()
91
+ .handle('pi --version', 'mocked') // For checkInstalled
92
+ .handle('pi install', ''), // For actual install
72
93
  prompter: new fake_prompter_1.FakePrompter([
73
- (0, fake_prompter_1.select)('opencode'),
94
+ (0, fake_prompter_1.select)('pi'),
74
95
  (0, fake_prompter_1.select)('project'),
96
+ (0, fake_prompter_1.select)('__skip__'), // Skip agent selection
75
97
  ]),
98
+ });
99
+ await (0, setup_1.runSetup)(deps);
100
+ const files = deps.files;
101
+ const written = files.getWrittenFiles();
102
+ // Should not create any agent files
103
+ for (const path of written.keys()) {
104
+ (0, vitest_1.expect)(path).not.toContain('SYSTEM.md');
105
+ }
106
+ });
107
+ });
108
+ (0, vitest_1.describe)('prerequisites', () => {
109
+ (0, vitest_1.it)('throws PrerequisiteError when opencode is not installed', async () => {
110
+ const deps = makeDeps({
76
111
  commands: new fake_command_runner_1.FakeCommandRunner(),
112
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('opencode'), (0, fake_prompter_1.select)('project')]),
77
113
  });
78
114
  // Simulate opencode not being installed
79
- yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
80
- }));
115
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
116
+ });
81
117
  });
82
118
  (0, vitest_1.describe)('cancellation', () => {
83
- (0, vitest_1.it)('throws CancelledError when user cancels at tool selection', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ (0, vitest_1.it)('throws CancelledError when user cancels at tool selection', async () => {
120
+ const deps = makeDeps({
121
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(fake_prompter_1.CANCEL)]),
122
+ });
123
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
124
+ });
125
+ (0, vitest_1.it)('throws CancelledError when user cancels at write confirmation', async () => {
84
126
  const deps = makeDeps({
85
127
  prompter: new fake_prompter_1.FakePrompter([
86
- (0, fake_prompter_1.select)(fake_prompter_1.CANCEL),
128
+ (0, fake_prompter_1.select)('opencode'),
129
+ (0, fake_prompter_1.select)('project'),
130
+ (0, fake_prompter_1.confirm)(false, 'Create'),
87
131
  ]),
88
132
  });
89
- yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
90
- }));
91
- (0, vitest_1.it)('throws CancelledError when user cancels at write confirmation', () => __awaiter(void 0, void 0, void 0, function* () {
133
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
134
+ });
135
+ (0, vitest_1.it)('throws CancelledError when user cancels at agent write confirmation (opencode)', async () => {
92
136
  const deps = makeDeps({
93
137
  prompter: new fake_prompter_1.FakePrompter([
94
138
  (0, fake_prompter_1.select)('opencode'),
95
139
  (0, fake_prompter_1.select)('project'),
96
- (0, fake_prompter_1.confirm)(false, 'Create'),
140
+ (0, fake_prompter_1.confirm)(true, 'Create'),
141
+ (0, fake_prompter_1.multiselect)(['backend', 'frontend']),
142
+ (0, fake_prompter_1.confirm)(false, 'agent'),
143
+ ]),
144
+ });
145
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
146
+ });
147
+ (0, vitest_1.it)('throws CancelledError when user cancels at agent write confirmation (pi)', async () => {
148
+ const deps = makeDeps({
149
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
150
+ prompter: new fake_prompter_1.FakePrompter([
151
+ (0, fake_prompter_1.select)('pi'),
152
+ (0, fake_prompter_1.select)('project'),
153
+ (0, fake_prompter_1.select)('fullstack'),
154
+ (0, fake_prompter_1.confirm)(false, /Create|Overwrite/),
97
155
  ]),
98
156
  });
99
- yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
100
- }));
157
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
158
+ });
101
159
  });
102
160
  (0, vitest_1.describe)('file operations', () => {
103
- (0, vitest_1.it)('preserves existing configuration keys when updating', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ (0, vitest_1.it)('preserves existing configuration keys when updating', async () => {
104
162
  const deps = makeDeps({
105
163
  prompter: new fake_prompter_1.FakePrompter([
106
164
  (0, fake_prompter_1.select)('opencode'),
107
165
  (0, fake_prompter_1.select)('project'),
108
166
  (0, fake_prompter_1.confirm)(true, 'Write'),
167
+ (0, fake_prompter_1.multiselect)([]),
109
168
  ]),
110
169
  });
111
170
  const files = deps.files;
@@ -113,19 +172,20 @@ const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompte
113
172
  customField: 'should-preserve',
114
173
  plugin: ['other-plugin'],
115
174
  }));
116
- yield (0, setup_1.runSetup)(deps);
175
+ await (0, setup_1.runSetup)(deps);
117
176
  const written = files.getWrittenFiles();
118
177
  const config = JSON.parse(written.get('/home/user/project/opencode.json'));
119
178
  (0, vitest_1.expect)(config.customField).toBe('should-preserve');
120
179
  (0, vitest_1.expect)(config.plugin).toContain('other-plugin');
121
- (0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth@1.0.16');
122
- }));
123
- (0, vitest_1.it)('preserves jsonc comments when updating', () => __awaiter(void 0, void 0, void 0, function* () {
180
+ (0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth');
181
+ });
182
+ (0, vitest_1.it)('preserves jsonc comments when updating', async () => {
124
183
  const deps = makeDeps({
125
184
  prompter: new fake_prompter_1.FakePrompter([
126
185
  (0, fake_prompter_1.select)('opencode'),
127
186
  (0, fake_prompter_1.select)('project'),
128
187
  (0, fake_prompter_1.confirm)(true, 'Write'),
188
+ (0, fake_prompter_1.multiselect)([]),
129
189
  ]),
130
190
  });
131
191
  const files = deps.files;
@@ -135,104 +195,311 @@ const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompte
135
195
  /* block comment explaining plugin */
136
196
  "plugin": ["other-plugin"]
137
197
  }`);
138
- yield (0, setup_1.runSetup)(deps);
198
+ await (0, setup_1.runSetup)(deps);
139
199
  const written = files.getWrittenFiles();
140
200
  const content = written.get('/home/user/project/opencode.jsonc');
141
201
  (0, vitest_1.expect)(content).toContain('// This is my custom config');
142
202
  (0, vitest_1.expect)(content).toContain('/* block comment explaining plugin */');
143
203
  (0, vitest_1.expect)(content).toContain('"customField": "should-preserve"');
144
- (0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth@1.0.16');
145
- }));
146
- (0, vitest_1.it)('shows no changes needed when config is already up to date', () => __awaiter(void 0, void 0, void 0, function* () {
204
+ (0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth');
205
+ });
206
+ (0, vitest_1.it)('shows no changes needed when config is already up to date', async () => {
147
207
  const deps = makeDeps({
148
- prompter: new fake_prompter_1.FakePrompter([
149
- (0, fake_prompter_1.select)('opencode'),
150
- (0, fake_prompter_1.select)('project'),
151
- ]),
208
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('opencode'), (0, fake_prompter_1.select)('project'), (0, fake_prompter_1.multiselect)([])]),
152
209
  });
153
210
  const files = deps.files;
154
211
  // Already has the exact same plugin version
155
212
  files.seed('/home/user/project/opencode.json', JSON.stringify({
156
213
  $schema: 'https://opencode.ai/config.json',
157
- plugin: ['@bergetai/opencode-auth@1.0.16'],
214
+ plugin: ['@bergetai/opencode-auth'],
158
215
  }, null, 2) + '\n');
159
- yield (0, setup_1.runSetup)(deps);
216
+ await (0, setup_1.runSetup)(deps);
160
217
  // Check that no write happened — content should be unchanged
161
218
  const written = files.getWrittenFiles();
162
219
  const content = written.get('/home/user/project/opencode.json');
163
220
  const config = JSON.parse(content);
164
- (0, vitest_1.expect)(config.plugin).toEqual(['@bergetai/opencode-auth@1.0.16']);
221
+ (0, vitest_1.expect)(config.plugin).toEqual(['@bergetai/opencode-auth']);
165
222
  (0, vitest_1.expect)(content).toContain('$schema');
166
- }));
167
- (0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', () => __awaiter(void 0, void 0, void 0, function* () {
223
+ });
224
+ (0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', async () => {
168
225
  const deps = makeDeps({
226
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
169
227
  prompter: new fake_prompter_1.FakePrompter([
170
228
  (0, fake_prompter_1.select)('pi'),
171
229
  (0, fake_prompter_1.select)('project'),
172
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
230
+ (0, fake_prompter_1.select)('fullstack'),
231
+ (0, fake_prompter_1.confirm)(true, 'Create'),
173
232
  ]),
174
- commands: new fake_command_runner_1.FakeCommandRunner()
175
- .handle('pi --version', 'mocked')
176
- .handle('pi install', ''),
177
233
  });
178
234
  const files = deps.files;
179
235
  files.seed('/home/user/project/.pi/settings.json', JSON.stringify({
180
- existingKey: 'should-preserve',
181
236
  anotherSetting: true,
237
+ existingKey: 'should-preserve',
182
238
  }));
183
- yield (0, setup_1.runSetup)(deps);
239
+ await (0, setup_1.runSetup)(deps);
184
240
  const written = files.getWrittenFiles();
185
241
  const settings = JSON.parse(written.get('/home/user/project/.pi/settings.json'));
186
242
  (0, vitest_1.expect)(settings.existingKey).toBe('should-preserve');
187
243
  (0, vitest_1.expect)(settings.anotherSetting).toBe(true);
188
244
  (0, vitest_1.expect)(settings.defaultProvider).toBe('berget');
189
- }));
190
- (0, vitest_1.it)('creates parent directories when writing files', () => __awaiter(void 0, void 0, void 0, function* () {
245
+ });
246
+ (0, vitest_1.it)('creates parent directories when writing files', async () => {
191
247
  const deps = makeDeps({
192
248
  prompter: new fake_prompter_1.FakePrompter([
193
249
  (0, fake_prompter_1.select)('opencode'),
194
250
  (0, fake_prompter_1.select)('global'),
195
251
  (0, fake_prompter_1.confirm)(true, 'Create'),
252
+ (0, fake_prompter_1.multiselect)([]),
196
253
  ]),
197
254
  });
198
- yield (0, setup_1.runSetup)(deps);
255
+ await (0, setup_1.runSetup)(deps);
199
256
  const files = deps.files;
200
257
  const written = files.getWrittenFiles();
201
258
  (0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
202
- }));
259
+ });
203
260
  });
204
261
  (0, vitest_1.describe)('command execution', () => {
205
- (0, vitest_1.it)('passes arguments as array (no shell injection)', () => __awaiter(void 0, void 0, void 0, function* () {
262
+ (0, vitest_1.it)('passes arguments as array (no shell injection)', async () => {
206
263
  const deps = makeDeps({
264
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
207
265
  prompter: new fake_prompter_1.FakePrompter([
208
266
  (0, fake_prompter_1.select)('pi'),
209
267
  (0, fake_prompter_1.select)('project'),
210
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
268
+ (0, fake_prompter_1.select)('fullstack'),
269
+ (0, fake_prompter_1.confirm)(true, 'Create'),
211
270
  ]),
212
- commands: new fake_command_runner_1.FakeCommandRunner()
213
- .handle('pi --version', 'mocked')
214
- .handle('pi install', ''),
215
271
  });
216
- yield (0, setup_1.runSetup)(deps);
272
+ await (0, setup_1.runSetup)(deps);
217
273
  const commands = deps.commands;
218
- const installCall = commands.calls.find(c => c.command === 'pi');
219
- (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('npm:@bergetai/pi-provider');
220
- (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('-l');
221
- }));
274
+ const installCall = commands.calls.find((c) => c.command === 'pi');
275
+ (0, vitest_1.expect)(installCall?.args).toContain('npm:@bergetai/pi-provider');
276
+ (0, vitest_1.expect)(installCall?.args).toContain('-l');
277
+ });
222
278
  });
223
279
  (0, vitest_1.describe)('error handling', () => {
224
- (0, vitest_1.it)('throws CommandFailedError when pi install fails', () => __awaiter(void 0, void 0, void 0, function* () {
280
+ (0, vitest_1.it)('throws CommandFailedError when pi install fails', async () => {
225
281
  const deps = makeDeps({
282
+ commands: new fake_command_runner_1.FakeCommandRunner()
283
+ .handle('pi --version', 'mocked')
284
+ .handle('pi install', new Error('npm error')),
285
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('pi'), (0, fake_prompter_1.select)('project')]),
286
+ });
287
+ await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CommandFailedError);
288
+ });
289
+ });
290
+ (0, vitest_1.describe)('auth integration', () => {
291
+ (0, vitest_1.it)('already authenticated shows simplified message', async () => {
292
+ const files = new fake_file_store_1.FakeFileStore();
293
+ files.seed('/home/user/.local/share/opencode/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
294
+ const deps = makeDeps({
295
+ files,
296
+ prompter: new fake_prompter_1.FakePrompter([
297
+ (0, fake_prompter_1.select)('opencode'),
298
+ (0, fake_prompter_1.select)('project'),
299
+ (0, fake_prompter_1.select)('keep'), // New: keep existing auth
300
+ (0, fake_prompter_1.confirm)(true, 'Create'), // Config write
301
+ (0, fake_prompter_1.multiselect)([]),
302
+ ]),
303
+ });
304
+ await (0, setup_1.runSetup)(deps);
305
+ const prompter = deps.prompter;
306
+ const notes = prompter.calls.filter((c) => c.method === 'note');
307
+ const lastNote = notes.at(-1);
308
+ (0, vitest_1.expect)(JSON.stringify(lastNote)).toContain('Run: opencode');
309
+ (0, vitest_1.expect)(JSON.stringify(lastNote)).not.toContain('/connect');
310
+ });
311
+ (0, vitest_1.it)('login failure shows manual auth instructions', async () => {
312
+ const deps = makeDeps({
313
+ authService: new fake_auth_service_1.FakeAuthService(false),
314
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
315
+ files: new fake_file_store_1.FakeFileStore(), // No pre-seeded auth → auth flow runs
226
316
  prompter: new fake_prompter_1.FakePrompter([
227
317
  (0, fake_prompter_1.select)('pi'),
228
318
  (0, fake_prompter_1.select)('project'),
229
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
319
+ (0, fake_prompter_1.select)('fullstack'),
320
+ (0, fake_prompter_1.confirm)(true, 'Create'),
230
321
  ]),
231
- commands: new fake_command_runner_1.FakeCommandRunner()
232
- .handle('pi --version', 'mocked')
233
- .handle('pi install', new Error('npm error')),
234
322
  });
235
- yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CommandFailedError);
236
- }));
323
+ await (0, setup_1.runSetup)(deps);
324
+ const prompter = deps.prompter;
325
+ const notes = prompter.calls.filter((c) => c.method === 'note');
326
+ const lastNote = notes.at(-1);
327
+ (0, vitest_1.expect)(JSON.stringify(lastNote)).toContain('/login');
328
+ });
329
+ (0, vitest_1.it)('creates api key for pi when no seat', async () => {
330
+ const files = new fake_file_store_1.FakeFileStore();
331
+ const deps = makeDeps({
332
+ authService: new fake_auth_service_1.FakeAuthService(true, false), // succeed, no seat
333
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
334
+ files,
335
+ prompter: new fake_prompter_1.FakePrompter([
336
+ (0, fake_prompter_1.select)('pi'),
337
+ (0, fake_prompter_1.select)('project'),
338
+ (0, fake_prompter_1.confirm)(true), // API key creation prompt
339
+ (0, fake_prompter_1.select)('fullstack'),
340
+ (0, fake_prompter_1.confirm)(true, 'Create'),
341
+ ]),
342
+ });
343
+ await (0, setup_1.runSetup)(deps);
344
+ const written = files.getWrittenFiles();
345
+ (0, vitest_1.expect)(written.has('/home/user/.pi/agent/auth.json')).toBe(true);
346
+ const parsed = JSON.parse(written.get('/home/user/.pi/agent/auth.json'));
347
+ (0, vitest_1.expect)(parsed.berget.type).toBe('api_key');
348
+ });
349
+ (0, vitest_1.it)('uses subscription when berget_code_seat present', async () => {
350
+ const files = new fake_file_store_1.FakeFileStore();
351
+ const farFuture = Math.floor(Date.now() / 1000) + 3600 * 24 * 365; // 1 year from now in seconds
352
+ files.seed('/home/user/.berget/auth.json', JSON.stringify({
353
+ access_token: makeJwt({ exp: farFuture, realm_access: { roles: ['berget_code_seat'] } }),
354
+ expires_at: farFuture * 1000,
355
+ refresh_token: 'ref',
356
+ }));
357
+ const deps = makeDeps({
358
+ files,
359
+ prompter: new fake_prompter_1.FakePrompter([
360
+ (0, fake_prompter_1.select)('opencode'),
361
+ (0, fake_prompter_1.select)('project'),
362
+ (0, fake_prompter_1.select)('subscription'),
363
+ (0, fake_prompter_1.confirm)(true, 'Create'),
364
+ (0, fake_prompter_1.multiselect)([]),
365
+ ]),
366
+ });
367
+ await (0, setup_1.runSetup)(deps);
368
+ const written = files.getWrittenFiles();
369
+ const parsed = JSON.parse(written.get('/home/user/.local/share/opencode/auth.json'));
370
+ (0, vitest_1.expect)(parsed.berget.type).toBe('oauth');
371
+ });
372
+ });
373
+ (0, vitest_1.describe)('agent configuration', () => {
374
+ (0, vitest_1.it)('sets up multiple agents for opencode project', async () => {
375
+ const deps = makeDeps({
376
+ prompter: new fake_prompter_1.FakePrompter([
377
+ (0, fake_prompter_1.select)('opencode'),
378
+ (0, fake_prompter_1.select)('project'),
379
+ (0, fake_prompter_1.confirm)(true, 'Create'),
380
+ (0, fake_prompter_1.multiselect)(['backend', 'frontend']),
381
+ (0, fake_prompter_1.confirm)(true, 'agent'),
382
+ ]),
383
+ });
384
+ await (0, setup_1.runSetup)(deps);
385
+ const files = deps.files;
386
+ const written = files.getWrittenFiles();
387
+ (0, vitest_1.expect)(written.has('/home/user/project/.opencode/agents/backend.md')).toBe(true);
388
+ (0, vitest_1.expect)(written.has('/home/user/project/.opencode/agents/frontend.md')).toBe(true);
389
+ });
390
+ (0, vitest_1.it)('sets up no agents for opencode when none selected', async () => {
391
+ const deps = makeDeps({
392
+ prompter: new fake_prompter_1.FakePrompter([
393
+ (0, fake_prompter_1.select)('opencode'),
394
+ (0, fake_prompter_1.select)('project'),
395
+ (0, fake_prompter_1.confirm)(true, 'Create'),
396
+ (0, fake_prompter_1.multiselect)([]),
397
+ ]),
398
+ });
399
+ await (0, setup_1.runSetup)(deps);
400
+ const files = deps.files;
401
+ const written = files.getWrittenFiles();
402
+ for (const path of written.keys()) {
403
+ (0, vitest_1.expect)(path).not.toMatch(/agents\/\w+\.md$/);
404
+ }
405
+ });
406
+ (0, vitest_1.it)('sets up agent globally for opencode', async () => {
407
+ const deps = makeDeps({
408
+ prompter: new fake_prompter_1.FakePrompter([
409
+ (0, fake_prompter_1.select)('opencode'),
410
+ (0, fake_prompter_1.select)('global'),
411
+ (0, fake_prompter_1.confirm)(true, 'Create'),
412
+ (0, fake_prompter_1.multiselect)(['fullstack']),
413
+ (0, fake_prompter_1.confirm)(true, 'agent'),
414
+ ]),
415
+ });
416
+ await (0, setup_1.runSetup)(deps);
417
+ const files = deps.files;
418
+ const written = files.getWrittenFiles();
419
+ (0, vitest_1.expect)(written.has('/home/user/.config/opencode/agents/fullstack.md')).toBe(true);
420
+ });
421
+ (0, vitest_1.it)('sets up agent for pi project', async () => {
422
+ const deps = makeDeps({
423
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
424
+ prompter: new fake_prompter_1.FakePrompter([
425
+ (0, fake_prompter_1.select)('pi'),
426
+ (0, fake_prompter_1.select)('project'),
427
+ (0, fake_prompter_1.select)('fullstack'),
428
+ (0, fake_prompter_1.confirm)(true, 'Create'),
429
+ ]),
430
+ });
431
+ await (0, setup_1.runSetup)(deps);
432
+ const files = deps.files;
433
+ const written = files.getWrittenFiles();
434
+ (0, vitest_1.expect)(written.has('/home/user/project/.pi/SYSTEM.md')).toBe(true);
435
+ });
436
+ (0, vitest_1.it)('sets up agent for pi globally', async () => {
437
+ const deps = makeDeps({
438
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
439
+ prompter: new fake_prompter_1.FakePrompter([
440
+ (0, fake_prompter_1.select)('pi'),
441
+ (0, fake_prompter_1.select)('global'),
442
+ (0, fake_prompter_1.select)('backend'),
443
+ (0, fake_prompter_1.confirm)(true, 'Create'),
444
+ ]),
445
+ });
446
+ await (0, setup_1.runSetup)(deps);
447
+ const files = deps.files;
448
+ const written = files.getWrittenFiles();
449
+ (0, vitest_1.expect)(written.has('/home/user/.pi/agent/SYSTEM.md')).toBe(true);
450
+ });
451
+ (0, vitest_1.it)('skips writing identical opencode agent files', async () => {
452
+ const deps = makeDeps({
453
+ prompter: new fake_prompter_1.FakePrompter([
454
+ (0, fake_prompter_1.select)('opencode'),
455
+ (0, fake_prompter_1.select)('project'),
456
+ (0, fake_prompter_1.confirm)(true, 'Create'),
457
+ (0, fake_prompter_1.multiselect)(['backend', 'frontend']),
458
+ (0, fake_prompter_1.confirm)(true, 'agent'),
459
+ ]),
460
+ });
461
+ // First run writes the files
462
+ await (0, setup_1.runSetup)(deps);
463
+ const files = deps.files;
464
+ const firstBackend = files
465
+ .getWrittenFiles()
466
+ .get('/home/user/project/.opencode/agents/backend.md');
467
+ const firstFrontend = files
468
+ .getWrittenFiles()
469
+ .get('/home/user/project/.opencode/agents/frontend.md');
470
+ // Second run with exact same content should not prompt for overwrite
471
+ const deps2 = makeDeps({
472
+ files,
473
+ prompter: new fake_prompter_1.FakePrompter([
474
+ (0, fake_prompter_1.select)('opencode'),
475
+ (0, fake_prompter_1.select)('project'),
476
+ (0, fake_prompter_1.multiselect)(['backend', 'frontend']),
477
+ ]),
478
+ });
479
+ await (0, setup_1.runSetup)(deps2);
480
+ // Content should be unchanged
481
+ (0, vitest_1.expect)(files.getWrittenFiles().get('/home/user/project/.opencode/agents/backend.md')).toBe(firstBackend);
482
+ (0, vitest_1.expect)(files.getWrittenFiles().get('/home/user/project/.opencode/agents/frontend.md')).toBe(firstFrontend);
483
+ });
484
+ (0, vitest_1.it)('overwrites pi SYSTEM.md when content differs', async () => {
485
+ const files = new fake_file_store_1.FakeFileStore();
486
+ files.seed('/home/user/project/.pi/SYSTEM.md', 'old agent content');
487
+ const deps = makeDeps({
488
+ commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
489
+ files,
490
+ prompter: new fake_prompter_1.FakePrompter([
491
+ (0, fake_prompter_1.select)('pi'),
492
+ (0, fake_prompter_1.select)('project'),
493
+ (0, fake_prompter_1.select)('fullstack'),
494
+ (0, fake_prompter_1.confirm)(true, 'Overwrite'),
495
+ ]),
496
+ });
497
+ await (0, setup_1.runSetup)(deps);
498
+ const written = files.getWrittenFiles();
499
+ const content = written.get('/home/user/project/.pi/SYSTEM.md');
500
+ (0, vitest_1.expect)(content).not.toBe('old agent content');
501
+ // Pi doesn't use front matter, so check for system prompt content
502
+ (0, vitest_1.expect)(content).toContain('Fullstack Agent');
503
+ });
237
504
  });
238
505
  });