berget 2.2.5 → 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 (148) hide show
  1. package/.github/workflows/publish.yml +8 -8
  2. package/.github/workflows/test.yml +12 -6
  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 +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 +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 +50 -0
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +55 -0
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +133 -0
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +505 -0
  33. package/dist/src/commands/code/adapters/clack-prompter.js +81 -0
  34. package/dist/src/commands/code/adapters/fs-file-store.js +80 -0
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +53 -0
  36. package/dist/src/commands/code/auth-sync.js +283 -0
  37. package/dist/src/commands/code/errors.js +27 -0
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/ports/command-runner.js +2 -0
  40. package/dist/src/commands/code/ports/file-store.js +2 -0
  41. package/dist/src/commands/code/ports/prompter.js +2 -0
  42. package/dist/src/commands/code/setup.js +533 -0
  43. package/dist/src/commands/code.js +223 -779
  44. package/dist/src/commands/models.js +13 -15
  45. package/dist/src/commands/users.js +6 -8
  46. package/dist/src/constants/command-structure.js +116 -114
  47. package/dist/src/services/api-key-service.js +43 -48
  48. package/dist/src/services/auth-service.js +60 -299
  49. package/dist/src/services/browser-auth.js +278 -0
  50. package/dist/src/services/chat-service.js +78 -91
  51. package/dist/src/services/cluster-service.js +6 -6
  52. package/dist/src/services/collaborator-service.js +5 -8
  53. package/dist/src/services/flux-service.js +5 -8
  54. package/dist/src/services/helm-service.js +5 -8
  55. package/dist/src/services/kubectl-service.js +7 -10
  56. package/dist/src/utils/config-checker.js +5 -5
  57. package/dist/src/utils/config-loader.js +25 -25
  58. package/dist/src/utils/default-api-key.js +23 -23
  59. package/dist/src/utils/env-manager.js +7 -7
  60. package/dist/src/utils/error-handler.js +60 -61
  61. package/dist/src/utils/logger.js +7 -7
  62. package/dist/src/utils/markdown-renderer.js +2 -2
  63. package/dist/src/utils/opencode-validator.js +17 -20
  64. package/dist/src/utils/token-manager.js +38 -11
  65. package/dist/tests/commands/chat.test.js +24 -24
  66. package/dist/tests/commands/code.test.js +169 -138
  67. package/dist/tests/utils/config-loader.test.js +114 -114
  68. package/dist/tests/utils/env-manager.test.js +57 -57
  69. package/dist/tests/utils/opencode-validator.test.js +44 -43
  70. package/dist/vitest.config.js +1 -1
  71. package/eslint.config.mjs +47 -0
  72. package/index.ts +42 -48
  73. package/package.json +30 -2
  74. package/src/agents/app.ts +27 -0
  75. package/src/agents/backend.ts +24 -0
  76. package/src/agents/devops.ts +33 -0
  77. package/src/agents/frontend.ts +24 -0
  78. package/src/agents/fullstack.ts +24 -0
  79. package/src/agents/index.ts +71 -0
  80. package/src/agents/quality.ts +69 -0
  81. package/src/agents/security.ts +26 -0
  82. package/src/agents/types.ts +17 -0
  83. package/src/client.ts +125 -167
  84. package/src/commands/api-keys.ts +261 -358
  85. package/src/commands/auth.ts +24 -30
  86. package/src/commands/autocomplete.ts +12 -12
  87. package/src/commands/billing.ts +22 -27
  88. package/src/commands/chat.ts +230 -323
  89. package/src/commands/clusters.ts +33 -33
  90. package/src/commands/code/__tests__/auth-sync.test.ts +481 -0
  91. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  92. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  93. package/src/commands/code/__tests__/fake-command-runner.ts +44 -0
  94. package/src/commands/code/__tests__/fake-file-store.ts +44 -0
  95. package/src/commands/code/__tests__/fake-prompter.ts +121 -0
  96. package/src/commands/code/__tests__/setup-flow.test.ts +628 -0
  97. package/src/commands/code/adapters/clack-prompter.ts +55 -0
  98. package/src/commands/code/adapters/fs-file-store.ts +37 -0
  99. package/src/commands/code/adapters/spawn-command-runner.ts +40 -0
  100. package/src/commands/code/auth-sync.ts +329 -0
  101. package/src/commands/code/errors.ts +23 -0
  102. package/src/commands/code/ports/auth-services.ts +14 -0
  103. package/src/commands/code/ports/command-runner.ts +10 -0
  104. package/src/commands/code/ports/file-store.ts +7 -0
  105. package/src/commands/code/ports/prompter.ts +29 -0
  106. package/src/commands/code/setup.ts +630 -0
  107. package/src/commands/code.ts +335 -1074
  108. package/src/commands/index.ts +19 -19
  109. package/src/commands/models.ts +32 -37
  110. package/src/commands/users.ts +15 -22
  111. package/src/constants/command-structure.ts +120 -140
  112. package/src/services/api-key-service.ts +96 -113
  113. package/src/services/auth-service.ts +92 -339
  114. package/src/services/browser-auth.ts +296 -0
  115. package/src/services/chat-service.ts +246 -279
  116. package/src/services/cluster-service.ts +29 -32
  117. package/src/services/collaborator-service.ts +13 -18
  118. package/src/services/flux-service.ts +16 -18
  119. package/src/services/helm-service.ts +16 -18
  120. package/src/services/kubectl-service.ts +12 -14
  121. package/src/types/api.d.ts +924 -926
  122. package/src/types/json.d.ts +3 -3
  123. package/src/utils/config-checker.ts +10 -10
  124. package/src/utils/config-loader.ts +110 -127
  125. package/src/utils/default-api-key.ts +81 -93
  126. package/src/utils/env-manager.ts +36 -40
  127. package/src/utils/error-handler.ts +83 -78
  128. package/src/utils/logger.ts +41 -41
  129. package/src/utils/markdown-renderer.ts +11 -11
  130. package/src/utils/opencode-validator.ts +51 -56
  131. package/src/utils/token-manager.ts +84 -64
  132. package/templates/agents/app.md +23 -0
  133. package/templates/agents/backend.md +23 -0
  134. package/templates/agents/devops.md +30 -0
  135. package/templates/agents/frontend.md +25 -0
  136. package/templates/agents/fullstack.md +23 -0
  137. package/templates/agents/quality.md +69 -0
  138. package/templates/agents/security.md +21 -0
  139. package/tests/commands/chat.test.ts +60 -70
  140. package/tests/commands/code.test.ts +346 -345
  141. package/tests/utils/config-loader.test.ts +260 -260
  142. package/tests/utils/env-manager.test.ts +127 -134
  143. package/tests/utils/opencode-validator.test.ts +65 -69
  144. package/tsconfig.json +2 -2
  145. package/vitest.config.ts +3 -3
  146. package/AGENTS.md +0 -374
  147. package/TODO.md +0 -19
  148. package/opencode.json +0 -146
@@ -0,0 +1,505 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const vitest_1 = require("vitest");
13
+ const setup_1 = require("../setup");
14
+ const errors_1 = require("../errors");
15
+ const fake_prompter_1 = require("./fake-prompter");
16
+ const fake_file_store_1 = require("./fake-file-store");
17
+ const fake_command_runner_1 = require("./fake-command-runner");
18
+ const fake_auth_service_1 = require("./fake-auth-service");
19
+ const fake_api_key_service_1 = require("./fake-api-key-service");
20
+ const makeDeps = (overrides = {}) => {
21
+ var _a, _b, _c, _d, _e;
22
+ return Object.assign({ prompter: (_a = overrides.prompter) !== null && _a !== void 0 ? _a : new fake_prompter_1.FakePrompter([]), files: (_b = overrides.files) !== null && _b !== void 0 ? _b : new fake_file_store_1.FakeFileStore(), commands: (_c = overrides.commands) !== null && _c !== void 0 ? _c : new fake_command_runner_1.FakeCommandRunner()
23
+ .handle("opencode --version", "mocked")
24
+ .handle("pi --version", "mocked"), authService: (_d = overrides.authService) !== null && _d !== void 0 ? _d : new fake_auth_service_1.FakeAuthService(false), apiKeyService: (_e = overrides.apiKeyService) !== null && _e !== void 0 ? _e : new fake_api_key_service_1.FakeApiKeyService("sk_ber_test"), homeDir: "/home/user", cwd: "/home/user/project" }, Object.fromEntries(Object.entries(overrides).filter(([k]) => k !== "prompter" &&
25
+ k !== "files" &&
26
+ k !== "commands" &&
27
+ k !== "authService" &&
28
+ k !== "apiKeyService")));
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
+ }
38
+ (0, vitest_1.describe)("runSetup", () => {
39
+ (0, vitest_1.describe)("happy path", () => {
40
+ (0, vitest_1.it)("sets up opencode project without existing config", () => __awaiter(void 0, void 0, void 0, function* () {
41
+ const deps = makeDeps({
42
+ prompter: new fake_prompter_1.FakePrompter([
43
+ (0, fake_prompter_1.select)("opencode"),
44
+ (0, fake_prompter_1.select)("project"),
45
+ (0, fake_prompter_1.confirm)(true, "Create"), // Config write
46
+ (0, fake_prompter_1.multiselect)([]), // No agents selected
47
+ ]),
48
+ });
49
+ yield (0, setup_1.runSetup)(deps);
50
+ const files = deps.files;
51
+ const written = files.getWrittenFiles();
52
+ (0, vitest_1.expect)(written.has("/home/user/project/opencode.json")).toBe(true);
53
+ const config = JSON.parse(written.get("/home/user/project/opencode.json"));
54
+ (0, vitest_1.expect)(config.plugin).toContain("@bergetai/opencode-auth@1.0.16");
55
+ }));
56
+ (0, vitest_1.it)("sets up opencode globally without existing config", () => __awaiter(void 0, void 0, void 0, function* () {
57
+ const deps = makeDeps({
58
+ prompter: new fake_prompter_1.FakePrompter([
59
+ (0, fake_prompter_1.select)("opencode"),
60
+ (0, fake_prompter_1.select)("global"),
61
+ (0, fake_prompter_1.confirm)(true, "Create"), // Config write
62
+ (0, fake_prompter_1.multiselect)([]), // No agents selected
63
+ ]),
64
+ });
65
+ yield (0, setup_1.runSetup)(deps);
66
+ const files = deps.files;
67
+ const written = files.getWrittenFiles();
68
+ (0, vitest_1.expect)(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
69
+ }));
70
+ (0, vitest_1.it)("sets up pi project with fresh install", () => __awaiter(void 0, void 0, void 0, function* () {
71
+ const deps = makeDeps({
72
+ prompter: new fake_prompter_1.FakePrompter([
73
+ (0, fake_prompter_1.select)("pi"),
74
+ (0, fake_prompter_1.select)("project"),
75
+ (0, fake_prompter_1.select)("fullstack"), // Agent selection
76
+ (0, fake_prompter_1.confirm)(true, "Create"),
77
+ ]),
78
+ commands: new fake_command_runner_1.FakeCommandRunner()
79
+ .handle("pi --version", "mocked") // For checkInstalled
80
+ .handle("pi install", ""), // For actual install
81
+ });
82
+ yield (0, setup_1.runSetup)(deps);
83
+ const commands = deps.commands;
84
+ (0, vitest_1.expect)(commands.calls.length).toBeGreaterThan(0);
85
+ const installCall = commands.calls.find(c => c.command === "pi");
86
+ (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain("npm:@bergetai/pi-provider");
87
+ }));
88
+ (0, vitest_1.it)("skips agent selection for pi project", () => __awaiter(void 0, void 0, void 0, function* () {
89
+ const deps = makeDeps({
90
+ prompter: new fake_prompter_1.FakePrompter([
91
+ (0, fake_prompter_1.select)("pi"),
92
+ (0, fake_prompter_1.select)("project"),
93
+ (0, fake_prompter_1.select)("__skip__"), // Skip agent selection
94
+ ]),
95
+ commands: new fake_command_runner_1.FakeCommandRunner()
96
+ .handle("pi --version", "mocked") // For checkInstalled
97
+ .handle("pi install", ""), // For actual install
98
+ });
99
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
110
+ const deps = makeDeps({
111
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)("opencode"), (0, fake_prompter_1.select)("project")]),
112
+ commands: new fake_command_runner_1.FakeCommandRunner(),
113
+ });
114
+ // Simulate opencode not being installed
115
+ yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
116
+ }));
117
+ });
118
+ (0, vitest_1.describe)("cancellation", () => {
119
+ (0, vitest_1.it)("throws CancelledError when user cancels at tool selection", () => __awaiter(void 0, void 0, void 0, function* () {
120
+ const deps = makeDeps({
121
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(fake_prompter_1.CANCEL)]),
122
+ });
123
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
126
+ const deps = makeDeps({
127
+ prompter: new fake_prompter_1.FakePrompter([
128
+ (0, fake_prompter_1.select)("opencode"),
129
+ (0, fake_prompter_1.select)("project"),
130
+ (0, fake_prompter_1.confirm)(false, "Create"),
131
+ ]),
132
+ });
133
+ yield (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)", () => __awaiter(void 0, void 0, void 0, function* () {
136
+ const deps = makeDeps({
137
+ prompter: new fake_prompter_1.FakePrompter([
138
+ (0, fake_prompter_1.select)("opencode"),
139
+ (0, fake_prompter_1.select)("project"),
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
+ yield (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)", () => __awaiter(void 0, void 0, void 0, function* () {
148
+ const deps = makeDeps({
149
+ prompter: new fake_prompter_1.FakePrompter([
150
+ (0, fake_prompter_1.select)("pi"),
151
+ (0, fake_prompter_1.select)("project"),
152
+ (0, fake_prompter_1.select)("fullstack"),
153
+ (0, fake_prompter_1.confirm)(false, /Create|Overwrite/),
154
+ ]),
155
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
156
+ });
157
+ yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
158
+ }));
159
+ });
160
+ (0, vitest_1.describe)("file operations", () => {
161
+ (0, vitest_1.it)("preserves existing configuration keys when updating", () => __awaiter(void 0, void 0, void 0, function* () {
162
+ const deps = makeDeps({
163
+ prompter: new fake_prompter_1.FakePrompter([
164
+ (0, fake_prompter_1.select)("opencode"),
165
+ (0, fake_prompter_1.select)("project"),
166
+ (0, fake_prompter_1.confirm)(true, "Write"),
167
+ (0, fake_prompter_1.multiselect)([]),
168
+ ]),
169
+ });
170
+ const files = deps.files;
171
+ files.seed("/home/user/project/opencode.json", JSON.stringify({
172
+ customField: "should-preserve",
173
+ plugin: ["other-plugin"],
174
+ }));
175
+ yield (0, setup_1.runSetup)(deps);
176
+ const written = files.getWrittenFiles();
177
+ const config = JSON.parse(written.get("/home/user/project/opencode.json"));
178
+ (0, vitest_1.expect)(config.customField).toBe("should-preserve");
179
+ (0, vitest_1.expect)(config.plugin).toContain("other-plugin");
180
+ (0, vitest_1.expect)(config.plugin).toContain("@bergetai/opencode-auth@1.0.16");
181
+ }));
182
+ (0, vitest_1.it)("preserves jsonc comments when updating", () => __awaiter(void 0, void 0, void 0, function* () {
183
+ const deps = makeDeps({
184
+ prompter: new fake_prompter_1.FakePrompter([
185
+ (0, fake_prompter_1.select)("opencode"),
186
+ (0, fake_prompter_1.select)("project"),
187
+ (0, fake_prompter_1.confirm)(true, "Write"),
188
+ (0, fake_prompter_1.multiselect)([]),
189
+ ]),
190
+ });
191
+ const files = deps.files;
192
+ files.seed("/home/user/project/opencode.jsonc", `{
193
+ // This is my custom config
194
+ "customField": "should-preserve",
195
+ /* block comment explaining plugin */
196
+ "plugin": ["other-plugin"]
197
+ }`);
198
+ yield (0, setup_1.runSetup)(deps);
199
+ const written = files.getWrittenFiles();
200
+ const content = written.get("/home/user/project/opencode.jsonc");
201
+ (0, vitest_1.expect)(content).toContain("// This is my custom config");
202
+ (0, vitest_1.expect)(content).toContain("/* block comment explaining plugin */");
203
+ (0, vitest_1.expect)(content).toContain('"customField": "should-preserve"');
204
+ (0, vitest_1.expect)(content).toContain("@bergetai/opencode-auth@1.0.16");
205
+ }));
206
+ (0, vitest_1.it)("shows no changes needed when config is already up to date", () => __awaiter(void 0, void 0, void 0, function* () {
207
+ const deps = makeDeps({
208
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)("opencode"), (0, fake_prompter_1.select)("project"), (0, fake_prompter_1.multiselect)([])]),
209
+ });
210
+ const files = deps.files;
211
+ // Already has the exact same plugin version
212
+ files.seed("/home/user/project/opencode.json", JSON.stringify({
213
+ $schema: "https://opencode.ai/config.json",
214
+ plugin: ["@bergetai/opencode-auth@1.0.16"],
215
+ }, null, 2) + "\n");
216
+ yield (0, setup_1.runSetup)(deps);
217
+ // Check that no write happened — content should be unchanged
218
+ const written = files.getWrittenFiles();
219
+ const content = written.get("/home/user/project/opencode.json");
220
+ const config = JSON.parse(content);
221
+ (0, vitest_1.expect)(config.plugin).toEqual(["@bergetai/opencode-auth@1.0.16"]);
222
+ (0, vitest_1.expect)(content).toContain("$schema");
223
+ }));
224
+ (0, vitest_1.it)("preserves existing Pi settings when setting defaultProvider", () => __awaiter(void 0, void 0, void 0, function* () {
225
+ const deps = makeDeps({
226
+ prompter: new fake_prompter_1.FakePrompter([
227
+ (0, fake_prompter_1.select)("pi"),
228
+ (0, fake_prompter_1.select)("project"),
229
+ (0, fake_prompter_1.select)("fullstack"),
230
+ (0, fake_prompter_1.confirm)(true, "Create"),
231
+ ]),
232
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
233
+ });
234
+ const files = deps.files;
235
+ files.seed("/home/user/project/.pi/settings.json", JSON.stringify({
236
+ existingKey: "should-preserve",
237
+ anotherSetting: true,
238
+ }));
239
+ yield (0, setup_1.runSetup)(deps);
240
+ const written = files.getWrittenFiles();
241
+ const settings = JSON.parse(written.get("/home/user/project/.pi/settings.json"));
242
+ (0, vitest_1.expect)(settings.existingKey).toBe("should-preserve");
243
+ (0, vitest_1.expect)(settings.anotherSetting).toBe(true);
244
+ (0, vitest_1.expect)(settings.defaultProvider).toBe("berget");
245
+ }));
246
+ (0, vitest_1.it)("creates parent directories when writing files", () => __awaiter(void 0, void 0, void 0, function* () {
247
+ const deps = makeDeps({
248
+ prompter: new fake_prompter_1.FakePrompter([
249
+ (0, fake_prompter_1.select)("opencode"),
250
+ (0, fake_prompter_1.select)("global"),
251
+ (0, fake_prompter_1.confirm)(true, "Create"),
252
+ (0, fake_prompter_1.multiselect)([]),
253
+ ]),
254
+ });
255
+ yield (0, setup_1.runSetup)(deps);
256
+ const files = deps.files;
257
+ const written = files.getWrittenFiles();
258
+ (0, vitest_1.expect)(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
259
+ }));
260
+ });
261
+ (0, vitest_1.describe)("command execution", () => {
262
+ (0, vitest_1.it)("passes arguments as array (no shell injection)", () => __awaiter(void 0, void 0, void 0, function* () {
263
+ const deps = makeDeps({
264
+ prompter: new fake_prompter_1.FakePrompter([
265
+ (0, fake_prompter_1.select)("pi"),
266
+ (0, fake_prompter_1.select)("project"),
267
+ (0, fake_prompter_1.select)("fullstack"),
268
+ (0, fake_prompter_1.confirm)(true, "Create"),
269
+ ]),
270
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
271
+ });
272
+ yield (0, setup_1.runSetup)(deps);
273
+ const commands = deps.commands;
274
+ const installCall = commands.calls.find(c => c.command === "pi");
275
+ (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain("npm:@bergetai/pi-provider");
276
+ (0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain("-l");
277
+ }));
278
+ });
279
+ (0, vitest_1.describe)("error handling", () => {
280
+ (0, vitest_1.it)("throws CommandFailedError when pi install fails", () => __awaiter(void 0, void 0, void 0, function* () {
281
+ const deps = makeDeps({
282
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)("pi"), (0, fake_prompter_1.select)("project")]),
283
+ commands: new fake_command_runner_1.FakeCommandRunner()
284
+ .handle("pi --version", "mocked")
285
+ .handle("pi install", new Error("npm error")),
286
+ });
287
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ prompter: new fake_prompter_1.FakePrompter([
296
+ (0, fake_prompter_1.select)("opencode"),
297
+ (0, fake_prompter_1.select)("project"),
298
+ (0, fake_prompter_1.select)("keep"), // New: keep existing auth
299
+ (0, fake_prompter_1.confirm)(true, "Create"), // Config write
300
+ (0, fake_prompter_1.multiselect)([]),
301
+ ]),
302
+ files,
303
+ });
304
+ yield (0, setup_1.runSetup)(deps);
305
+ const prompter = deps.prompter;
306
+ const notes = prompter.calls.filter(c => c.method === "note");
307
+ const lastNote = notes[notes.length - 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", () => __awaiter(void 0, void 0, void 0, function* () {
312
+ const deps = makeDeps({
313
+ prompter: new fake_prompter_1.FakePrompter([
314
+ (0, fake_prompter_1.select)("pi"),
315
+ (0, fake_prompter_1.select)("project"),
316
+ (0, fake_prompter_1.select)("fullstack"),
317
+ (0, fake_prompter_1.confirm)(true, "Create"),
318
+ ]),
319
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
320
+ authService: new fake_auth_service_1.FakeAuthService(false),
321
+ files: new fake_file_store_1.FakeFileStore(), // No pre-seeded auth → auth flow runs
322
+ });
323
+ yield (0, setup_1.runSetup)(deps);
324
+ const prompter = deps.prompter;
325
+ const notes = prompter.calls.filter(c => c.method === "note");
326
+ const lastNote = notes[notes.length - 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", () => __awaiter(void 0, void 0, void 0, function* () {
330
+ const files = new fake_file_store_1.FakeFileStore();
331
+ const deps = makeDeps({
332
+ prompter: new fake_prompter_1.FakePrompter([
333
+ (0, fake_prompter_1.select)("pi"),
334
+ (0, fake_prompter_1.select)("project"),
335
+ (0, fake_prompter_1.confirm)(true), // API key creation prompt
336
+ (0, fake_prompter_1.select)("fullstack"),
337
+ (0, fake_prompter_1.confirm)(true, "Create"),
338
+ ]),
339
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
340
+ authService: new fake_auth_service_1.FakeAuthService(true, false), // succeed, no seat
341
+ files,
342
+ });
343
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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({ realm_access: { roles: ["berget_code_seat"] }, exp: farFuture }),
354
+ refresh_token: "ref",
355
+ expires_at: farFuture * 1000,
356
+ }));
357
+ const deps = makeDeps({
358
+ prompter: new fake_prompter_1.FakePrompter([
359
+ (0, fake_prompter_1.select)("opencode"),
360
+ (0, fake_prompter_1.select)("project"),
361
+ (0, fake_prompter_1.select)("subscription"),
362
+ (0, fake_prompter_1.confirm)(true, "Create"),
363
+ (0, fake_prompter_1.multiselect)([]),
364
+ ]),
365
+ files,
366
+ });
367
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
422
+ const deps = makeDeps({
423
+ prompter: new fake_prompter_1.FakePrompter([
424
+ (0, fake_prompter_1.select)("pi"),
425
+ (0, fake_prompter_1.select)("project"),
426
+ (0, fake_prompter_1.select)("fullstack"),
427
+ (0, fake_prompter_1.confirm)(true, "Create"),
428
+ ]),
429
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
430
+ });
431
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
437
+ const deps = makeDeps({
438
+ prompter: new fake_prompter_1.FakePrompter([
439
+ (0, fake_prompter_1.select)("pi"),
440
+ (0, fake_prompter_1.select)("global"),
441
+ (0, fake_prompter_1.select)("backend"),
442
+ (0, fake_prompter_1.confirm)(true, "Create"),
443
+ ]),
444
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
445
+ });
446
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ yield (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
+ yield (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", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ prompter: new fake_prompter_1.FakePrompter([
489
+ (0, fake_prompter_1.select)("pi"),
490
+ (0, fake_prompter_1.select)("project"),
491
+ (0, fake_prompter_1.select)("fullstack"),
492
+ (0, fake_prompter_1.confirm)(true, "Overwrite"),
493
+ ]),
494
+ files,
495
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
496
+ });
497
+ yield (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
+ }));
504
+ });
505
+ });
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.ClackPrompter = void 0;
36
+ const p = __importStar(require("@clack/prompts"));
37
+ const errors_1 = require("../errors");
38
+ const unwrap = (v) => {
39
+ if (p.isCancel(v))
40
+ throw new errors_1.CancelledError();
41
+ return v;
42
+ };
43
+ class ClackPrompter {
44
+ intro(message) {
45
+ p.intro(message);
46
+ }
47
+ outro(message) {
48
+ p.outro(message);
49
+ }
50
+ note(message, title) {
51
+ p.note(message, title);
52
+ }
53
+ spinner() {
54
+ const s = p.spinner();
55
+ return {
56
+ start: (msg) => s.start(msg),
57
+ stop: (msg) => s.stop(msg),
58
+ };
59
+ }
60
+ select(opts) {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ return unwrap(yield p.select(opts));
63
+ });
64
+ }
65
+ confirm(opts) {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ return unwrap(yield p.confirm(opts));
68
+ });
69
+ }
70
+ text(opts) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ return unwrap(yield p.text(opts));
73
+ });
74
+ }
75
+ multiselect(opts) {
76
+ return __awaiter(this, void 0, void 0, function* () {
77
+ return unwrap(yield p.multiselect(opts));
78
+ });
79
+ }
80
+ }
81
+ exports.ClackPrompter = ClackPrompter;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.FsFileStore = void 0;
36
+ const node_fs_1 = require("node:fs");
37
+ const path = __importStar(require("node:path"));
38
+ class FsFileStore {
39
+ exists(filePath) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ try {
42
+ yield node_fs_1.promises.access(filePath);
43
+ return true;
44
+ }
45
+ catch (_a) {
46
+ return false;
47
+ }
48
+ });
49
+ }
50
+ readFile(filePath) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ try {
53
+ return yield node_fs_1.promises.readFile(filePath, "utf8");
54
+ }
55
+ catch (err) {
56
+ if (err.code === "ENOENT")
57
+ return null;
58
+ throw err;
59
+ }
60
+ });
61
+ }
62
+ writeFile(filePath, content) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ const dir = path.dirname(filePath);
65
+ yield node_fs_1.promises.mkdir(dir, { recursive: true });
66
+ yield node_fs_1.promises.writeFile(filePath, content, "utf8");
67
+ });
68
+ }
69
+ mkdir(dir) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ yield node_fs_1.promises.mkdir(dir, { recursive: true });
72
+ });
73
+ }
74
+ chmod(filePath, mode) {
75
+ return __awaiter(this, void 0, void 0, function* () {
76
+ yield node_fs_1.promises.chmod(filePath, mode);
77
+ });
78
+ }
79
+ }
80
+ exports.FsFileStore = FsFileStore;