berget 2.2.6 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.github/workflows/publish.yml +6 -6
  2. package/.github/workflows/test.yml +11 -5
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +5 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +21 -21
  9. package/dist/package.json +28 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +54 -62
  20. package/dist/src/commands/api-keys.js +132 -140
  21. package/dist/src/commands/auth.js +9 -9
  22. package/dist/src/commands/autocomplete.js +9 -9
  23. package/dist/src/commands/billing.js +7 -9
  24. package/dist/src/commands/chat.js +90 -92
  25. package/dist/src/commands/clusters.js +12 -12
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +348 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +23 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +55 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +5 -7
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +9 -0
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +60 -18
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +374 -107
  33. package/dist/src/commands/code/adapters/clack-prompter.js +10 -0
  34. package/dist/src/commands/code/adapters/fs-file-store.js +8 -3
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +15 -11
  36. package/dist/src/commands/code/auth-sync.js +283 -0
  37. package/dist/src/commands/code/errors.js +4 -4
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +234 -93
  40. package/dist/src/commands/code.js +139 -251
  41. package/dist/src/commands/models.js +13 -15
  42. package/dist/src/commands/users.js +6 -8
  43. package/dist/src/constants/command-structure.js +116 -116
  44. package/dist/src/services/api-key-service.js +43 -48
  45. package/dist/src/services/auth-service.js +60 -299
  46. package/dist/src/services/browser-auth.js +278 -0
  47. package/dist/src/services/chat-service.js +78 -91
  48. package/dist/src/services/cluster-service.js +6 -6
  49. package/dist/src/services/collaborator-service.js +5 -8
  50. package/dist/src/services/flux-service.js +5 -8
  51. package/dist/src/services/helm-service.js +5 -8
  52. package/dist/src/services/kubectl-service.js +7 -10
  53. package/dist/src/utils/config-checker.js +5 -5
  54. package/dist/src/utils/config-loader.js +25 -25
  55. package/dist/src/utils/default-api-key.js +23 -23
  56. package/dist/src/utils/env-manager.js +7 -7
  57. package/dist/src/utils/error-handler.js +60 -61
  58. package/dist/src/utils/logger.js +7 -7
  59. package/dist/src/utils/markdown-renderer.js +2 -2
  60. package/dist/src/utils/opencode-validator.js +17 -20
  61. package/dist/src/utils/token-manager.js +38 -11
  62. package/dist/tests/commands/chat.test.js +24 -24
  63. package/dist/tests/commands/code.test.js +147 -147
  64. package/dist/tests/utils/config-loader.test.js +114 -114
  65. package/dist/tests/utils/env-manager.test.js +57 -57
  66. package/dist/tests/utils/opencode-validator.test.js +33 -33
  67. package/dist/vitest.config.js +1 -1
  68. package/eslint.config.mjs +47 -0
  69. package/index.ts +42 -48
  70. package/package.json +28 -2
  71. package/src/agents/app.ts +27 -0
  72. package/src/agents/backend.ts +24 -0
  73. package/src/agents/devops.ts +33 -0
  74. package/src/agents/frontend.ts +24 -0
  75. package/src/agents/fullstack.ts +24 -0
  76. package/src/agents/index.ts +71 -0
  77. package/src/agents/quality.ts +69 -0
  78. package/src/agents/security.ts +26 -0
  79. package/src/agents/types.ts +17 -0
  80. package/src/client.ts +125 -167
  81. package/src/commands/api-keys.ts +261 -358
  82. package/src/commands/auth.ts +24 -30
  83. package/src/commands/autocomplete.ts +12 -12
  84. package/src/commands/billing.ts +22 -27
  85. package/src/commands/chat.ts +230 -323
  86. package/src/commands/clusters.ts +33 -33
  87. package/src/commands/code/__tests__/auth-sync.test.ts +481 -0
  88. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  89. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  90. package/src/commands/code/__tests__/fake-command-runner.ts +39 -42
  91. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  92. package/src/commands/code/__tests__/fake-prompter.ts +107 -69
  93. package/src/commands/code/__tests__/setup-flow.test.ts +624 -270
  94. package/src/commands/code/adapters/clack-prompter.ts +50 -38
  95. package/src/commands/code/adapters/fs-file-store.ts +31 -27
  96. package/src/commands/code/adapters/spawn-command-runner.ts +33 -29
  97. package/src/commands/code/auth-sync.ts +329 -0
  98. package/src/commands/code/errors.ts +15 -15
  99. package/src/commands/code/ports/auth-services.ts +14 -0
  100. package/src/commands/code/ports/command-runner.ts +8 -4
  101. package/src/commands/code/ports/file-store.ts +5 -4
  102. package/src/commands/code/ports/prompter.ts +24 -18
  103. package/src/commands/code/setup.ts +545 -317
  104. package/src/commands/code.ts +271 -473
  105. package/src/commands/index.ts +19 -19
  106. package/src/commands/models.ts +32 -37
  107. package/src/commands/users.ts +15 -22
  108. package/src/constants/command-structure.ts +119 -142
  109. package/src/services/api-key-service.ts +96 -113
  110. package/src/services/auth-service.ts +92 -339
  111. package/src/services/browser-auth.ts +296 -0
  112. package/src/services/chat-service.ts +246 -279
  113. package/src/services/cluster-service.ts +29 -32
  114. package/src/services/collaborator-service.ts +13 -18
  115. package/src/services/flux-service.ts +16 -18
  116. package/src/services/helm-service.ts +16 -18
  117. package/src/services/kubectl-service.ts +12 -14
  118. package/src/types/api.d.ts +924 -926
  119. package/src/types/json.d.ts +3 -3
  120. package/src/utils/config-checker.ts +10 -10
  121. package/src/utils/config-loader.ts +110 -127
  122. package/src/utils/default-api-key.ts +81 -93
  123. package/src/utils/env-manager.ts +36 -40
  124. package/src/utils/error-handler.ts +83 -78
  125. package/src/utils/logger.ts +41 -41
  126. package/src/utils/markdown-renderer.ts +11 -11
  127. package/src/utils/opencode-validator.ts +51 -56
  128. package/src/utils/token-manager.ts +84 -64
  129. package/templates/agents/app.md +1 -0
  130. package/templates/agents/backend.md +1 -0
  131. package/templates/agents/devops.md +2 -0
  132. package/templates/agents/frontend.md +1 -0
  133. package/templates/agents/fullstack.md +1 -0
  134. package/templates/agents/quality.md +45 -40
  135. package/templates/agents/security.md +1 -0
  136. package/tests/commands/chat.test.ts +60 -70
  137. package/tests/commands/code.test.ts +330 -376
  138. package/tests/utils/config-loader.test.ts +260 -260
  139. package/tests/utils/env-manager.test.ts +127 -134
  140. package/tests/utils/opencode-validator.test.ts +58 -63
  141. package/tsconfig.json +2 -2
  142. package/vitest.config.ts +3 -3
  143. package/AGENTS.md +0 -374
  144. package/TODO.md +0 -19
@@ -15,121 +15,181 @@ const errors_1 = require("../errors");
15
15
  const fake_prompter_1 = require("./fake-prompter");
16
16
  const fake_file_store_1 = require("./fake-file-store");
17
17
  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));
21
- (0, vitest_1.describe)('runSetup', () => {
22
- (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* () {
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* () {
24
41
  const deps = makeDeps({
25
42
  prompter: new fake_prompter_1.FakePrompter([
26
- (0, fake_prompter_1.select)('opencode'),
27
- (0, fake_prompter_1.select)('project'),
28
- (0, fake_prompter_1.confirm)(true, 'Create'),
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
29
47
  ]),
30
48
  });
31
49
  yield (0, setup_1.runSetup)(deps);
32
50
  const files = deps.files;
33
51
  const written = files.getWrittenFiles();
34
- (0, vitest_1.expect)(written.has('/home/user/project/opencode.json')).toBe(true);
35
- 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');
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");
37
55
  }));
38
- (0, vitest_1.it)('sets up opencode globally without existing config', () => __awaiter(void 0, void 0, void 0, function* () {
56
+ (0, vitest_1.it)("sets up opencode globally without existing config", () => __awaiter(void 0, void 0, void 0, function* () {
39
57
  const deps = makeDeps({
40
58
  prompter: new fake_prompter_1.FakePrompter([
41
- (0, fake_prompter_1.select)('opencode'),
42
- (0, fake_prompter_1.select)('global'),
43
- (0, fake_prompter_1.confirm)(true, 'Create'),
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
44
63
  ]),
45
64
  });
46
65
  yield (0, setup_1.runSetup)(deps);
47
66
  const files = deps.files;
48
67
  const written = files.getWrittenFiles();
49
- (0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
68
+ (0, vitest_1.expect)(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
50
69
  }));
51
- (0, vitest_1.it)('sets up pi project with fresh install', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ (0, vitest_1.it)("sets up pi project with fresh install", () => __awaiter(void 0, void 0, void 0, function* () {
52
71
  const deps = makeDeps({
53
72
  prompter: new fake_prompter_1.FakePrompter([
54
- (0, fake_prompter_1.select)('pi'),
55
- (0, fake_prompter_1.select)('project'),
56
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
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"),
57
77
  ]),
58
78
  commands: new fake_command_runner_1.FakeCommandRunner()
59
- .handle('pi --version', 'mocked') // For checkInstalled
60
- .handle('pi install', ''), // For actual install
79
+ .handle("pi --version", "mocked") // For checkInstalled
80
+ .handle("pi install", ""), // For actual install
61
81
  });
62
82
  yield (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');
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");
67
87
  }));
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* () {
88
+ (0, vitest_1.it)("skips agent selection for pi project", () => __awaiter(void 0, void 0, void 0, function* () {
71
89
  const deps = makeDeps({
72
90
  prompter: new fake_prompter_1.FakePrompter([
73
- (0, fake_prompter_1.select)('opencode'),
74
- (0, fake_prompter_1.select)('project'),
91
+ (0, fake_prompter_1.select)("pi"),
92
+ (0, fake_prompter_1.select)("project"),
93
+ (0, fake_prompter_1.select)("__skip__"), // Skip agent selection
75
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")]),
76
112
  commands: new fake_command_runner_1.FakeCommandRunner(),
77
113
  });
78
114
  // Simulate opencode not being installed
79
115
  yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
80
116
  }));
81
117
  });
82
- (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* () {
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* () {
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
133
  yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
90
134
  }));
91
- (0, vitest_1.it)('throws CancelledError when user cancels at write confirmation', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ (0, vitest_1.it)("throws CancelledError when user cancels at agent write confirmation (opencode)", () => __awaiter(void 0, void 0, void 0, function* () {
92
136
  const deps = makeDeps({
93
137
  prompter: new fake_prompter_1.FakePrompter([
94
- (0, fake_prompter_1.select)('opencode'),
95
- (0, fake_prompter_1.select)('project'),
96
- (0, fake_prompter_1.confirm)(false, 'Create'),
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"),
97
143
  ]),
98
144
  });
99
145
  yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
100
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
+ }));
101
159
  });
102
- (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* () {
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* () {
104
162
  const deps = makeDeps({
105
163
  prompter: new fake_prompter_1.FakePrompter([
106
- (0, fake_prompter_1.select)('opencode'),
107
- (0, fake_prompter_1.select)('project'),
108
- (0, fake_prompter_1.confirm)(true, 'Write'),
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)([]),
109
168
  ]),
110
169
  });
111
170
  const files = deps.files;
112
- files.seed('/home/user/project/opencode.json', JSON.stringify({
113
- customField: 'should-preserve',
114
- plugin: ['other-plugin'],
171
+ files.seed("/home/user/project/opencode.json", JSON.stringify({
172
+ customField: "should-preserve",
173
+ plugin: ["other-plugin"],
115
174
  }));
116
175
  yield (0, setup_1.runSetup)(deps);
117
176
  const written = files.getWrittenFiles();
118
- const config = JSON.parse(written.get('/home/user/project/opencode.json'));
119
- (0, vitest_1.expect)(config.customField).toBe('should-preserve');
120
- (0, vitest_1.expect)(config.plugin).toContain('other-plugin');
121
- (0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth@1.0.16');
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");
122
181
  }));
123
- (0, vitest_1.it)('preserves jsonc comments when updating', () => __awaiter(void 0, void 0, void 0, function* () {
182
+ (0, vitest_1.it)("preserves jsonc comments when updating", () => __awaiter(void 0, void 0, void 0, function* () {
124
183
  const deps = makeDeps({
125
184
  prompter: new fake_prompter_1.FakePrompter([
126
- (0, fake_prompter_1.select)('opencode'),
127
- (0, fake_prompter_1.select)('project'),
128
- (0, fake_prompter_1.confirm)(true, 'Write'),
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)([]),
129
189
  ]),
130
190
  });
131
191
  const files = deps.files;
132
- files.seed('/home/user/project/opencode.jsonc', `{
192
+ files.seed("/home/user/project/opencode.jsonc", `{
133
193
  // This is my custom config
134
194
  "customField": "should-preserve",
135
195
  /* block comment explaining plugin */
@@ -137,102 +197,309 @@ const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompte
137
197
  }`);
138
198
  yield (0, setup_1.runSetup)(deps);
139
199
  const written = files.getWrittenFiles();
140
- const content = written.get('/home/user/project/opencode.jsonc');
141
- (0, vitest_1.expect)(content).toContain('// This is my custom config');
142
- (0, vitest_1.expect)(content).toContain('/* block comment explaining plugin */');
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 */");
143
203
  (0, vitest_1.expect)(content).toContain('"customField": "should-preserve"');
144
- (0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth@1.0.16');
204
+ (0, vitest_1.expect)(content).toContain("@bergetai/opencode-auth@1.0.16");
145
205
  }));
146
- (0, vitest_1.it)('shows no changes needed when config is already up to date', () => __awaiter(void 0, void 0, void 0, function* () {
206
+ (0, vitest_1.it)("shows no changes needed when config is already up to date", () => __awaiter(void 0, void 0, void 0, function* () {
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
- files.seed('/home/user/project/opencode.json', JSON.stringify({
156
- $schema: 'https://opencode.ai/config.json',
157
- plugin: ['@bergetai/opencode-auth@1.0.16'],
158
- }, null, 2) + '\n');
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");
159
216
  yield (0, setup_1.runSetup)(deps);
160
217
  // Check that no write happened — content should be unchanged
161
218
  const written = files.getWrittenFiles();
162
- const content = written.get('/home/user/project/opencode.json');
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']);
165
- (0, vitest_1.expect)(content).toContain('$schema');
221
+ (0, vitest_1.expect)(config.plugin).toEqual(["@bergetai/opencode-auth@1.0.16"]);
222
+ (0, vitest_1.expect)(content).toContain("$schema");
166
223
  }));
167
- (0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', () => __awaiter(void 0, void 0, void 0, function* () {
224
+ (0, vitest_1.it)("preserves existing Pi settings when setting defaultProvider", () => __awaiter(void 0, void 0, void 0, function* () {
168
225
  const deps = makeDeps({
169
226
  prompter: new fake_prompter_1.FakePrompter([
170
- (0, fake_prompter_1.select)('pi'),
171
- (0, fake_prompter_1.select)('project'),
172
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
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"),
173
231
  ]),
174
- commands: new fake_command_runner_1.FakeCommandRunner()
175
- .handle('pi --version', 'mocked')
176
- .handle('pi install', ''),
232
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
177
233
  });
178
234
  const files = deps.files;
179
- files.seed('/home/user/project/.pi/settings.json', JSON.stringify({
180
- existingKey: 'should-preserve',
235
+ files.seed("/home/user/project/.pi/settings.json", JSON.stringify({
236
+ existingKey: "should-preserve",
181
237
  anotherSetting: true,
182
238
  }));
183
239
  yield (0, setup_1.runSetup)(deps);
184
240
  const written = files.getWrittenFiles();
185
- const settings = JSON.parse(written.get('/home/user/project/.pi/settings.json'));
186
- (0, vitest_1.expect)(settings.existingKey).toBe('should-preserve');
241
+ const settings = JSON.parse(written.get("/home/user/project/.pi/settings.json"));
242
+ (0, vitest_1.expect)(settings.existingKey).toBe("should-preserve");
187
243
  (0, vitest_1.expect)(settings.anotherSetting).toBe(true);
188
- (0, vitest_1.expect)(settings.defaultProvider).toBe('berget');
244
+ (0, vitest_1.expect)(settings.defaultProvider).toBe("berget");
189
245
  }));
190
- (0, vitest_1.it)('creates parent directories when writing files', () => __awaiter(void 0, void 0, void 0, function* () {
246
+ (0, vitest_1.it)("creates parent directories when writing files", () => __awaiter(void 0, void 0, void 0, function* () {
191
247
  const deps = makeDeps({
192
248
  prompter: new fake_prompter_1.FakePrompter([
193
- (0, fake_prompter_1.select)('opencode'),
194
- (0, fake_prompter_1.select)('global'),
195
- (0, fake_prompter_1.confirm)(true, 'Create'),
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)([]),
196
253
  ]),
197
254
  });
198
255
  yield (0, setup_1.runSetup)(deps);
199
256
  const files = deps.files;
200
257
  const written = files.getWrittenFiles();
201
- (0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
258
+ (0, vitest_1.expect)(written.has("/home/user/.config/opencode/opencode.json")).toBe(true);
202
259
  }));
203
260
  });
204
- (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* () {
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* () {
206
263
  const deps = makeDeps({
207
264
  prompter: new fake_prompter_1.FakePrompter([
208
- (0, fake_prompter_1.select)('pi'),
209
- (0, fake_prompter_1.select)('project'),
210
- (0, fake_prompter_1.confirm)(true, 'Proceed'),
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"),
211
269
  ]),
212
- commands: new fake_command_runner_1.FakeCommandRunner()
213
- .handle('pi --version', 'mocked')
214
- .handle('pi install', ''),
270
+ commands: new fake_command_runner_1.FakeCommandRunner().handle("pi --version", "mocked").handle("pi install", ""),
215
271
  });
216
272
  yield (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');
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");
221
277
  }));
222
278
  });
223
- (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* () {
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* () {
225
281
  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.confirm)(true, 'Proceed'),
230
- ]),
282
+ prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)("pi"), (0, fake_prompter_1.select)("project")]),
231
283
  commands: new fake_command_runner_1.FakeCommandRunner()
232
- .handle('pi --version', 'mocked')
233
- .handle('pi install', new Error('npm error')),
284
+ .handle("pi --version", "mocked")
285
+ .handle("pi install", new Error("npm error")),
234
286
  });
235
287
  yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CommandFailedError);
236
288
  }));
237
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
+ });
238
505
  });
@@ -67,5 +67,15 @@ class ClackPrompter {
67
67
  return unwrap(yield p.confirm(opts));
68
68
  });
69
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
+ }
70
80
  }
71
81
  exports.ClackPrompter = ClackPrompter;