@sentry/wizard 6.10.0 → 6.11.0

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/CHANGELOG.md +20 -0
  2. package/dist/ci-ensure-runtime-loaded.sh +82 -0
  3. package/dist/e2e-tests/tests/angular-17.test.js +72 -82
  4. package/dist/e2e-tests/tests/angular-17.test.js.map +1 -1
  5. package/dist/e2e-tests/tests/angular-19.test.js +71 -80
  6. package/dist/e2e-tests/tests/angular-19.test.js.map +1 -1
  7. package/dist/e2e-tests/tests/cloudflare-worker.test.d.ts +1 -0
  8. package/dist/e2e-tests/tests/cloudflare-worker.test.js +64 -0
  9. package/dist/e2e-tests/tests/cloudflare-worker.test.js.map +1 -0
  10. package/dist/e2e-tests/tests/cloudflare-wrangler-sourcemaps.test.js +2 -5
  11. package/dist/e2e-tests/tests/cloudflare-wrangler-sourcemaps.test.js.map +1 -1
  12. package/dist/e2e-tests/tests/expo.test.js +36 -61
  13. package/dist/e2e-tests/tests/expo.test.js.map +1 -1
  14. package/dist/e2e-tests/tests/flutter.test.js +63 -70
  15. package/dist/e2e-tests/tests/flutter.test.js.map +1 -1
  16. package/dist/e2e-tests/tests/help-message.test.js +2 -2
  17. package/dist/e2e-tests/tests/help-message.test.js.map +1 -1
  18. package/dist/e2e-tests/tests/nextjs-14.test.js +48 -76
  19. package/dist/e2e-tests/tests/nextjs-14.test.js.map +1 -1
  20. package/dist/e2e-tests/tests/nextjs-15.test.js +89 -99
  21. package/dist/e2e-tests/tests/nextjs-15.test.js.map +1 -1
  22. package/dist/e2e-tests/tests/nextjs-16.test.js +48 -45
  23. package/dist/e2e-tests/tests/nextjs-16.test.js.map +1 -1
  24. package/dist/e2e-tests/tests/nuxt-3.test.js +45 -58
  25. package/dist/e2e-tests/tests/nuxt-3.test.js.map +1 -1
  26. package/dist/e2e-tests/tests/nuxt-4.test.js +59 -73
  27. package/dist/e2e-tests/tests/nuxt-4.test.js.map +1 -1
  28. package/dist/e2e-tests/tests/pnpm-workspace.test.js +4 -7
  29. package/dist/e2e-tests/tests/pnpm-workspace.test.js.map +1 -1
  30. package/dist/e2e-tests/tests/react-native.test.js +44 -80
  31. package/dist/e2e-tests/tests/react-native.test.js.map +1 -1
  32. package/dist/e2e-tests/tests/react-router.test.js +163 -145
  33. package/dist/e2e-tests/tests/react-router.test.js.map +1 -1
  34. package/dist/e2e-tests/tests/remix.test.js +162 -132
  35. package/dist/e2e-tests/tests/remix.test.js.map +1 -1
  36. package/dist/e2e-tests/tests/sveltekit-hooks.test.js +48 -36
  37. package/dist/e2e-tests/tests/sveltekit-hooks.test.js.map +1 -1
  38. package/dist/e2e-tests/tests/sveltekit-tracing.test.js +3 -6
  39. package/dist/e2e-tests/tests/sveltekit-tracing.test.js.map +1 -1
  40. package/dist/e2e-tests/utils/index.d.ts +15 -43
  41. package/dist/e2e-tests/utils/index.js +95 -185
  42. package/dist/e2e-tests/utils/index.js.map +1 -1
  43. package/dist/get-e2e-test-matrix.mjs +11 -0
  44. package/dist/lib/Constants.d.ts +1 -0
  45. package/dist/lib/Constants.js +5 -0
  46. package/dist/lib/Constants.js.map +1 -1
  47. package/dist/src/android/android-wizard.js +2 -4
  48. package/dist/src/android/android-wizard.js.map +1 -1
  49. package/dist/src/angular/angular-wizard.js +4 -6
  50. package/dist/src/angular/angular-wizard.js.map +1 -1
  51. package/dist/src/angular/sdk-setup.js +1 -1
  52. package/dist/src/angular/sdk-setup.js.map +1 -1
  53. package/dist/src/apple/apple-wizard.js +2 -4
  54. package/dist/src/apple/apple-wizard.js.map +1 -1
  55. package/dist/src/cloudflare/cloudflare-wizard.d.ts +3 -0
  56. package/dist/src/cloudflare/cloudflare-wizard.js +99 -0
  57. package/dist/src/cloudflare/cloudflare-wizard.js.map +1 -0
  58. package/dist/src/cloudflare/sdk-setup.d.ts +7 -0
  59. package/dist/src/cloudflare/sdk-setup.js +47 -0
  60. package/dist/src/cloudflare/sdk-setup.js.map +1 -0
  61. package/dist/src/cloudflare/templates.d.ts +4 -0
  62. package/dist/src/cloudflare/templates.js +44 -0
  63. package/dist/src/cloudflare/templates.js.map +1 -0
  64. package/dist/src/cloudflare/wrangler/create-wrangler-config.d.ts +4 -0
  65. package/dist/src/cloudflare/wrangler/create-wrangler-config.js +27 -0
  66. package/dist/src/cloudflare/wrangler/create-wrangler-config.js.map +1 -0
  67. package/dist/src/cloudflare/wrangler/ensure-wrangler-config.d.ts +4 -0
  68. package/dist/src/cloudflare/wrangler/ensure-wrangler-config.js +25 -0
  69. package/dist/src/cloudflare/wrangler/ensure-wrangler-config.js.map +1 -0
  70. package/dist/src/cloudflare/wrangler/find-wrangler-config.d.ts +4 -0
  71. package/dist/src/cloudflare/wrangler/find-wrangler-config.js +23 -0
  72. package/dist/src/cloudflare/wrangler/find-wrangler-config.js.map +1 -0
  73. package/dist/src/cloudflare/wrangler/get-entry-point-from-wrangler-config.d.ts +6 -0
  74. package/dist/src/cloudflare/wrangler/get-entry-point-from-wrangler-config.js +52 -0
  75. package/dist/src/cloudflare/wrangler/get-entry-point-from-wrangler-config.js.map +1 -0
  76. package/dist/src/cloudflare/wrangler/update-wrangler-config.d.ts +17 -0
  77. package/dist/src/cloudflare/wrangler/update-wrangler-config.js +173 -0
  78. package/dist/src/cloudflare/wrangler/update-wrangler-config.js.map +1 -0
  79. package/dist/src/cloudflare/wrap-worker.d.ts +32 -0
  80. package/dist/src/cloudflare/wrap-worker.js +109 -0
  81. package/dist/src/cloudflare/wrap-worker.js.map +1 -0
  82. package/dist/src/flutter/flutter-wizard.js +3 -6
  83. package/dist/src/flutter/flutter-wizard.js.map +1 -1
  84. package/dist/src/nextjs/nextjs-wizard.js +0 -2
  85. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  86. package/dist/src/nuxt/nuxt-wizard.js +3 -5
  87. package/dist/src/nuxt/nuxt-wizard.js.map +1 -1
  88. package/dist/src/react-native/react-native-wizard.js +2 -4
  89. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  90. package/dist/src/react-router/react-router-wizard.js +3 -5
  91. package/dist/src/react-router/react-router-wizard.js.map +1 -1
  92. package/dist/src/react-router/sdk-setup.d.ts +1 -1
  93. package/dist/src/react-router/sdk-setup.js +3 -4
  94. package/dist/src/react-router/sdk-setup.js.map +1 -1
  95. package/dist/src/remix/remix-wizard.js +2 -4
  96. package/dist/src/remix/remix-wizard.js.map +1 -1
  97. package/dist/src/run.d.ts +1 -1
  98. package/dist/src/run.js +5 -0
  99. package/dist/src/run.js.map +1 -1
  100. package/dist/src/sveltekit/sveltekit-wizard.js +2 -4
  101. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  102. package/dist/src/utils/abort-if-sportlight-not-supported.d.ts +5 -0
  103. package/dist/src/utils/abort-if-sportlight-not-supported.js +40 -0
  104. package/dist/src/utils/abort-if-sportlight-not-supported.js.map +1 -0
  105. package/dist/src/utils/ast-utils.d.ts +1 -1
  106. package/dist/src/utils/ast-utils.js.map +1 -1
  107. package/dist/src/utils/clack/index.d.ts +2 -2
  108. package/dist/src/utils/clack/index.js.map +1 -1
  109. package/dist/src/utils/clack/mcp-config.js +117 -59
  110. package/dist/src/utils/clack/mcp-config.js.map +1 -1
  111. package/dist/src/version.d.ts +1 -1
  112. package/dist/src/version.js +1 -1
  113. package/dist/src/version.js.map +1 -1
  114. package/dist/test/angular/angular-wizard.test.js +2 -4
  115. package/dist/test/angular/angular-wizard.test.js.map +1 -1
  116. package/dist/test/cloudflare/create-wrangler-config.test.d.ts +1 -0
  117. package/dist/test/cloudflare/create-wrangler-config.test.js +48 -0
  118. package/dist/test/cloudflare/create-wrangler-config.test.js.map +1 -0
  119. package/dist/test/cloudflare/ensure-wrangler-config.test.d.ts +1 -0
  120. package/dist/test/cloudflare/ensure-wrangler-config.test.js +61 -0
  121. package/dist/test/cloudflare/ensure-wrangler-config.test.js.map +1 -0
  122. package/dist/test/cloudflare/find-wrangler-config.test.d.ts +1 -0
  123. package/dist/test/cloudflare/find-wrangler-config.test.js +77 -0
  124. package/dist/test/cloudflare/find-wrangler-config.test.js.map +1 -0
  125. package/dist/test/cloudflare/get-entry-point-from-wrangler-config.test.d.ts +1 -0
  126. package/dist/test/cloudflare/get-entry-point-from-wrangler-config.test.js +81 -0
  127. package/dist/test/cloudflare/get-entry-point-from-wrangler-config.test.js.map +1 -0
  128. package/dist/test/cloudflare/sdk-setup.test.d.ts +1 -0
  129. package/dist/test/cloudflare/sdk-setup.test.js +152 -0
  130. package/dist/test/cloudflare/sdk-setup.test.js.map +1 -0
  131. package/dist/test/cloudflare/templates.test.d.ts +1 -0
  132. package/dist/test/cloudflare/templates.test.js +68 -0
  133. package/dist/test/cloudflare/templates.test.js.map +1 -0
  134. package/dist/test/cloudflare/update-wrangler-config.test.d.ts +1 -0
  135. package/dist/test/cloudflare/update-wrangler-config.test.js +216 -0
  136. package/dist/test/cloudflare/update-wrangler-config.test.js.map +1 -0
  137. package/dist/test/cloudflare/wrap-worker.test.d.ts +1 -0
  138. package/dist/test/cloudflare/wrap-worker.test.js +143 -0
  139. package/dist/test/cloudflare/wrap-worker.test.js.map +1 -0
  140. package/dist/test/react-router/sdk-setup.test.js +2 -2
  141. package/dist/test/react-router/sdk-setup.test.js.map +1 -1
  142. package/dist/test/utils/clack/mcp-config.test.js +176 -51
  143. package/dist/test/utils/clack/mcp-config.test.js.map +1 -1
  144. package/package.json +5 -4
@@ -37,6 +37,7 @@ vitest_1.vi.mock('../../../src/utils/clack', () => ({
37
37
  vitest_1.vi.mock('@clack/prompts', () => ({
38
38
  confirm: vitest_1.vi.fn(),
39
39
  select: vitest_1.vi.fn(),
40
+ multiselect: vitest_1.vi.fn(),
40
41
  isCancel: vitest_1.vi.fn(() => false),
41
42
  cancel: vitest_1.vi.fn(),
42
43
  log: {
@@ -77,9 +78,8 @@ vitest_1.vi.mock('node:child_process');
77
78
  });
78
79
  (0, vitest_1.it)('should configure for Cursor when selected', async () => {
79
80
  const { clack, clackUtils } = await getMocks();
80
- vitest_1.vi.mocked(clack.select)
81
- .mockResolvedValueOnce('yes')
82
- .mockResolvedValueOnce('cursor');
81
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
82
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']);
83
83
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
84
84
  const mockReadFile = vitest_1.vi
85
85
  .fn()
@@ -90,9 +90,8 @@ vitest_1.vi.mock('node:child_process');
90
90
  vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
91
91
  vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
92
92
  await (0, mcp_config_1.offerProjectScopedMcpConfig)();
93
- (0, vitest_1.expect)(clack.select).toHaveBeenCalledTimes(2);
94
- (0, vitest_1.expect)(clack.select).toHaveBeenNthCalledWith(2, vitest_1.expect.objectContaining({
95
- message: 'Which editor do you want to configure?',
93
+ (0, vitest_1.expect)(clack.multiselect).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
94
+ message: 'Which editor(s) do you want to configure?',
96
95
  options: vitest_1.expect.arrayContaining([
97
96
  vitest_1.expect.objectContaining({ value: 'cursor' }),
98
97
  vitest_1.expect.objectContaining({ value: 'vscode' }),
@@ -101,14 +100,13 @@ vitest_1.vi.mock('node:child_process');
101
100
  }));
102
101
  (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8');
103
102
  (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'));
104
- (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration.');
103
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for Cursor.');
105
104
  (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('reload your editor'));
106
105
  });
107
106
  (0, vitest_1.it)('should configure for VS Code when selected', async () => {
108
107
  const { clack, clackUtils } = await getMocks();
109
- vitest_1.vi.mocked(clack.select)
110
- .mockResolvedValueOnce('yes')
111
- .mockResolvedValueOnce('vscode');
108
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
109
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['vscode']);
112
110
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
113
111
  const mockReadFile = vitest_1.vi
114
112
  .fn()
@@ -124,9 +122,8 @@ vitest_1.vi.mock('node:child_process');
124
122
  });
125
123
  (0, vitest_1.it)('should configure for Claude Code when selected', async () => {
126
124
  const { clack, clackUtils } = await getMocks();
127
- vitest_1.vi.mocked(clack.select)
128
- .mockResolvedValueOnce('yes')
129
- .mockResolvedValueOnce('claudeCode');
125
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
126
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['claudeCode']);
130
127
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
131
128
  const mockReadFile = vitest_1.vi
132
129
  .fn()
@@ -142,9 +139,8 @@ vitest_1.vi.mock('node:child_process');
142
139
  });
143
140
  (0, vitest_1.it)('should update existing Cursor config file', async () => {
144
141
  const { clack, clackUtils } = await getMocks();
145
- vitest_1.vi.mocked(clack.select)
146
- .mockResolvedValueOnce('yes')
147
- .mockResolvedValueOnce('cursor');
142
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
143
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']);
148
144
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
149
145
  const existingConfig = JSON.stringify({
150
146
  mcpServers: {
@@ -169,9 +165,8 @@ vitest_1.vi.mock('node:child_process');
169
165
  });
170
166
  (0, vitest_1.it)('should update existing VS Code config file', async () => {
171
167
  const { clack, clackUtils } = await getMocks();
172
- vitest_1.vi.mocked(clack.select)
173
- .mockResolvedValueOnce('yes')
174
- .mockResolvedValueOnce('vscode');
168
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
169
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['vscode']);
175
170
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
176
171
  const existingConfig = JSON.stringify({
177
172
  servers: {
@@ -196,9 +191,8 @@ vitest_1.vi.mock('node:child_process');
196
191
  });
197
192
  (0, vitest_1.it)('should update existing Claude Code config file', async () => {
198
193
  const { clack, clackUtils } = await getMocks();
199
- vitest_1.vi.mocked(clack.select)
200
- .mockResolvedValueOnce('yes')
201
- .mockResolvedValueOnce('claudeCode');
194
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
195
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['claudeCode']);
202
196
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
203
197
  const existingConfig = JSON.stringify({
204
198
  mcpServers: {
@@ -219,11 +213,85 @@ vitest_1.vi.mock('node:child_process');
219
213
  (0, vitest_1.expect)(writtenContent.mcpServers).toHaveProperty('Sentry');
220
214
  (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated .mcp.json');
221
215
  });
216
+ (0, vitest_1.it)('should configure for OpenCode when selected', async () => {
217
+ const { clack, clackUtils } = await getMocks();
218
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
219
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
220
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
221
+ const mockReadFile = vitest_1.vi
222
+ .fn()
223
+ .mockRejectedValue(new Error('File not found'));
224
+ const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined);
225
+ const mockMkdirSync = vitest_1.vi.fn();
226
+ vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile);
227
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
228
+ vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
229
+ await (0, mcp_config_1.offerProjectScopedMcpConfig)();
230
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('opencode.json'), vitest_1.expect.stringContaining('"mcp"'), 'utf8');
231
+ // Verify the written content has the correct structure for OpenCode
232
+ const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]);
233
+ (0, vitest_1.expect)(writtenContent.$schema).toBe('https://opencode.ai/config.json');
234
+ (0, vitest_1.expect)(writtenContent.mcp).toHaveProperty('Sentry');
235
+ (0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('type', 'remote');
236
+ (0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('url');
237
+ (0, vitest_1.expect)(writtenContent.mcp?.Sentry?.oauth).toEqual({});
238
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('opencode.json'));
239
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for OpenCode.');
240
+ (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('restart OpenCode'));
241
+ });
242
+ (0, vitest_1.it)('should update existing OpenCode config file', async () => {
243
+ const { clack, clackUtils } = await getMocks();
244
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
245
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
246
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
247
+ const existingConfig = JSON.stringify({
248
+ mcp: {
249
+ OtherServer: {
250
+ type: 'remote',
251
+ url: 'https://other.example.com',
252
+ },
253
+ },
254
+ });
255
+ const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig);
256
+ const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined);
257
+ const mockMkdirSync = vitest_1.vi.fn();
258
+ vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile);
259
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
260
+ vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
261
+ await (0, mcp_config_1.offerProjectScopedMcpConfig)();
262
+ const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]);
263
+ (0, vitest_1.expect)(writtenContent.mcp).toHaveProperty('OtherServer');
264
+ (0, vitest_1.expect)(writtenContent.mcp).toHaveProperty('Sentry');
265
+ (0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('type', 'remote');
266
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated opencode.json');
267
+ });
268
+ (0, vitest_1.it)('should handle file write errors gracefully for OpenCode', async () => {
269
+ const { clack, clackUtils } = await getMocks();
270
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
271
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
272
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
273
+ const mockReadFile = vitest_1.vi
274
+ .fn()
275
+ .mockRejectedValue(new Error('File not found'));
276
+ const mockWriteFile = vitest_1.vi
277
+ .fn()
278
+ .mockRejectedValue(new Error('Permission denied'));
279
+ const mockMkdirSync = vitest_1.vi.fn();
280
+ vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile);
281
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
282
+ vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
283
+ await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined();
284
+ (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config for openCode'));
285
+ (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
286
+ filename: 'opencode.json',
287
+ codeSnippet: vitest_1.expect.stringContaining('"mcp"'),
288
+ hint: 'create the file if it does not exist',
289
+ }));
290
+ });
222
291
  (0, vitest_1.it)('should handle file write errors gracefully for Cursor', async () => {
223
292
  const { clack, clackUtils } = await getMocks();
224
- vitest_1.vi.mocked(clack.select)
225
- .mockResolvedValueOnce('yes')
226
- .mockResolvedValueOnce('cursor');
293
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
294
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']);
227
295
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
228
296
  const mockReadFile = vitest_1.vi
229
297
  .fn()
@@ -236,7 +304,7 @@ vitest_1.vi.mock('node:child_process');
236
304
  vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
237
305
  vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
238
306
  await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined();
239
- (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically'));
307
+ (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config'));
240
308
  (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
241
309
  filename: path.join('.cursor', 'mcp.json'),
242
310
  codeSnippet: vitest_1.expect.stringContaining('mcpServers'),
@@ -245,9 +313,8 @@ vitest_1.vi.mock('node:child_process');
245
313
  });
246
314
  (0, vitest_1.it)('should handle file write errors gracefully for VS Code', async () => {
247
315
  const { clack, clackUtils } = await getMocks();
248
- vitest_1.vi.mocked(clack.select)
249
- .mockResolvedValueOnce('yes')
250
- .mockResolvedValueOnce('vscode');
316
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
317
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['vscode']);
251
318
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
252
319
  const mockReadFile = vitest_1.vi
253
320
  .fn()
@@ -268,9 +335,8 @@ vitest_1.vi.mock('node:child_process');
268
335
  });
269
336
  (0, vitest_1.it)('should handle file write errors gracefully for Claude Code', async () => {
270
337
  const { clack, clackUtils } = await getMocks();
271
- vitest_1.vi.mocked(clack.select)
272
- .mockResolvedValueOnce('yes')
273
- .mockResolvedValueOnce('claudeCode');
338
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
339
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['claudeCode']);
274
340
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
275
341
  const mockReadFile = vitest_1.vi
276
342
  .fn()
@@ -291,9 +357,8 @@ vitest_1.vi.mock('node:child_process');
291
357
  });
292
358
  (0, vitest_1.it)('should handle update errors and show copy-paste instructions', async () => {
293
359
  const { clack, clackUtils } = await getMocks();
294
- vitest_1.vi.mocked(clack.select)
295
- .mockResolvedValueOnce('yes')
296
- .mockResolvedValueOnce('cursor');
360
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
361
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']);
297
362
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
298
363
  // Mock existing file and simulate write error during update
299
364
  const existingConfig = JSON.stringify({
@@ -312,14 +377,13 @@ vitest_1.vi.mock('node:child_process');
312
377
  vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
313
378
  vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
314
379
  await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined();
315
- (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically'));
380
+ (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config'));
316
381
  (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalled();
317
382
  });
318
383
  (0, vitest_1.it)('should handle mkdirSync errors', async () => {
319
384
  const { clack, clackUtils } = await getMocks();
320
- vitest_1.vi.mocked(clack.select)
321
- .mockResolvedValueOnce('yes')
322
- .mockResolvedValueOnce('cursor');
385
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
386
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']);
323
387
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
324
388
  const mockReadFile = vitest_1.vi
325
389
  .fn()
@@ -332,14 +396,13 @@ vitest_1.vi.mock('node:child_process');
332
396
  vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
333
397
  vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
334
398
  await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined();
335
- (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically'));
399
+ (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config'));
336
400
  (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalled();
337
401
  });
338
402
  (0, vitest_1.it)('should create config with empty servers/mcpServers when existing config lacks them', async () => {
339
403
  const { clack, clackUtils } = await getMocks();
340
- vitest_1.vi.mocked(clack.select)
341
- .mockResolvedValueOnce('yes')
342
- .mockResolvedValueOnce('vscode');
404
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
405
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['vscode']);
343
406
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
344
407
  const existingConfig = JSON.stringify({
345
408
  otherProperty: 'value',
@@ -361,8 +424,8 @@ vitest_1.vi.mock('node:child_process');
361
424
  const { clack, clackUtils } = await getMocks();
362
425
  vitest_1.vi.mocked(clack.select)
363
426
  .mockResolvedValueOnce('yes')
364
- .mockResolvedValueOnce('jetbrains')
365
427
  .mockResolvedValueOnce(true); // For the clipboard copy prompt
428
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
366
429
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
367
430
  // Mock clipboard copy
368
431
  const mockSpawn = vitest_1.vi.fn().mockReturnValue({
@@ -393,8 +456,8 @@ vitest_1.vi.mock('node:child_process');
393
456
  const { clack, clackUtils } = await getMocks();
394
457
  vitest_1.vi.mocked(clack.select)
395
458
  .mockResolvedValueOnce('yes')
396
- .mockResolvedValueOnce('other')
397
459
  .mockResolvedValueOnce(true); // For the clipboard copy prompt
460
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['other']);
398
461
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
399
462
  // Mock clipboard copy failure to test fallback
400
463
  const mockSpawn = vitest_1.vi.fn().mockReturnValue({
@@ -426,8 +489,8 @@ vitest_1.vi.mock('node:child_process');
426
489
  const { clack, clackUtils } = await getMocks();
427
490
  vitest_1.vi.mocked(clack.select)
428
491
  .mockResolvedValueOnce('yes')
429
- .mockResolvedValueOnce('jetbrains')
430
492
  .mockResolvedValueOnce(true); // For clipboard copy prompt
493
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
431
494
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
432
495
  // Mock clipboard copy to throw error
433
496
  const mockSpawn = vitest_1.vi.fn().mockImplementation(() => {
@@ -450,8 +513,8 @@ vitest_1.vi.mock('node:child_process');
450
513
  const { clack, clackUtils } = await getMocks();
451
514
  vitest_1.vi.mocked(clack.select)
452
515
  .mockResolvedValueOnce('explain') // User selects "What is MCP?"
453
- .mockResolvedValueOnce(true) // User selects "Yes" after explanation
454
- .mockResolvedValueOnce('cursor'); // User selects Cursor
516
+ .mockResolvedValueOnce(true); // User selects "Yes" after explanation
517
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor']); // User selects Cursor
455
518
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
456
519
  const mockReadFile = vitest_1.vi
457
520
  .fn()
@@ -477,8 +540,8 @@ vitest_1.vi.mock('node:child_process');
477
540
  const { clack, clackUtils } = await getMocks();
478
541
  vitest_1.vi.mocked(clack.select)
479
542
  .mockResolvedValueOnce('yes')
480
- .mockResolvedValueOnce('jetbrains')
481
543
  .mockResolvedValueOnce(false); // User declines to copy to clipboard
544
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
482
545
  vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
483
546
  const mockSpawn = vitest_1.vi.fn();
484
547
  vitest_1.vi.spyOn(childProcess, 'spawn').mockImplementation(mockSpawn);
@@ -511,8 +574,70 @@ vitest_1.vi.mock('node:child_process');
511
574
  message: 'Would you like to configure MCP for your IDE now?',
512
575
  }));
513
576
  // Should NOT proceed with editor selection
514
- (0, vitest_1.expect)(clack.select).not.toHaveBeenCalledWith(vitest_1.expect.objectContaining({
515
- message: 'Which editor do you want to configure?',
577
+ (0, vitest_1.expect)(clack.multiselect).not.toHaveBeenCalled();
578
+ });
579
+ (0, vitest_1.it)('should configure multiple editors when selected', async () => {
580
+ const { clack, clackUtils } = await getMocks();
581
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
582
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce([
583
+ 'cursor',
584
+ 'vscode',
585
+ 'claudeCode',
586
+ ]);
587
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
588
+ const mockReadFile = vitest_1.vi
589
+ .fn()
590
+ .mockRejectedValue(new Error('File not found'));
591
+ const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined);
592
+ const mockMkdirSync = vitest_1.vi.fn();
593
+ vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile);
594
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
595
+ vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
596
+ await (0, mcp_config_1.offerProjectScopedMcpConfig)();
597
+ // Should write config for all three editors
598
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledTimes(3);
599
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8');
600
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.vscode/mcp.json'), vitest_1.expect.stringContaining('"servers"'), 'utf8');
601
+ (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8');
602
+ // Should show success messages for each (twice per editor: filename + editor-specific message)
603
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for Cursor.');
604
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for VS Code.');
605
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for Claude Code.');
606
+ });
607
+ (0, vitest_1.it)('should return early when no editors are selected', async () => {
608
+ const { clack, clackUtils } = await getMocks();
609
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
610
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce([]);
611
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
612
+ const mockWriteFile = vitest_1.vi.fn();
613
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
614
+ await (0, mcp_config_1.offerProjectScopedMcpConfig)();
615
+ (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith('No editors selected. You can add MCP configuration later.');
616
+ (0, vitest_1.expect)(mockWriteFile).not.toHaveBeenCalled();
617
+ });
618
+ (0, vitest_1.it)('should handle mixed success and failure for multiple editors', async () => {
619
+ const { clack, clackUtils } = await getMocks();
620
+ vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
621
+ vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor', 'vscode']);
622
+ vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
623
+ const mockReadFile = vitest_1.vi
624
+ .fn()
625
+ .mockRejectedValue(new Error('File not found'));
626
+ const mockWriteFile = vitest_1.vi
627
+ .fn()
628
+ .mockResolvedValueOnce(undefined) // Cursor succeeds
629
+ .mockRejectedValueOnce(new Error('Permission denied')); // VS Code fails
630
+ const mockMkdirSync = vitest_1.vi.fn();
631
+ vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile);
632
+ vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
633
+ vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync);
634
+ await (0, mcp_config_1.offerProjectScopedMcpConfig)();
635
+ // Cursor should succeed
636
+ (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'));
637
+ // VS Code should fail and show fallback
638
+ (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config for vscode'));
639
+ (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
640
+ filename: path.join('.vscode', 'mcp.json'),
516
641
  }));
517
642
  });
518
643
  });