@sentry/wizard 3.10.0 → 3.12.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 (150) hide show
  1. package/CHANGELOG.md +54 -7
  2. package/dist/lib/Constants.d.ts +1 -0
  3. package/dist/lib/Constants.js +5 -0
  4. package/dist/lib/Constants.js.map +1 -1
  5. package/dist/lib/Steps/ChooseIntegration.js +8 -4
  6. package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
  7. package/dist/lib/Steps/Integrations/Android.d.ts +9 -0
  8. package/dist/lib/Steps/Integrations/Android.js +86 -0
  9. package/dist/lib/Steps/Integrations/Android.js.map +1 -0
  10. package/dist/lib/Steps/Integrations/ReactNative.js +3 -3
  11. package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
  12. package/dist/lib/Steps/PromptForParameters.js +36 -3
  13. package/dist/lib/Steps/PromptForParameters.js.map +1 -1
  14. package/dist/lib/Steps/SentryProjectSelector.js +1 -1
  15. package/dist/lib/Steps/SentryProjectSelector.js.map +1 -1
  16. package/dist/package.json +4 -3
  17. package/dist/src/android/android-wizard.d.ts +2 -0
  18. package/dist/src/android/android-wizard.js +225 -0
  19. package/dist/src/android/android-wizard.js.map +1 -0
  20. package/dist/src/android/code-tools.d.ts +47 -0
  21. package/dist/src/android/code-tools.js +173 -0
  22. package/dist/src/android/code-tools.js.map +1 -0
  23. package/dist/src/android/gradle.d.ts +62 -0
  24. package/dist/src/android/gradle.js +286 -0
  25. package/dist/src/android/gradle.js.map +1 -0
  26. package/dist/src/android/manifest.d.ts +57 -0
  27. package/dist/src/android/manifest.js +183 -0
  28. package/dist/src/android/manifest.js.map +1 -0
  29. package/dist/src/android/templates.d.ts +11 -0
  30. package/dist/src/android/templates.js +34 -0
  31. package/dist/src/android/templates.js.map +1 -0
  32. package/dist/src/apple/apple-wizard.js +123 -64
  33. package/dist/src/apple/apple-wizard.js.map +1 -1
  34. package/dist/src/apple/cocoapod.js +4 -3
  35. package/dist/src/apple/cocoapod.js.map +1 -1
  36. package/dist/src/apple/code-tools.d.ts +1 -1
  37. package/dist/src/apple/code-tools.js +43 -19
  38. package/dist/src/apple/code-tools.js.map +1 -1
  39. package/dist/src/apple/fastlane.d.ts +1 -1
  40. package/dist/src/apple/fastlane.js +12 -6
  41. package/dist/src/apple/fastlane.js.map +1 -1
  42. package/dist/src/apple/templates.d.ts +2 -2
  43. package/dist/src/apple/templates.js +4 -4
  44. package/dist/src/apple/templates.js.map +1 -1
  45. package/dist/src/apple/xcode-manager.d.ts +19 -3
  46. package/dist/src/apple/xcode-manager.js +126 -24
  47. package/dist/src/apple/xcode-manager.js.map +1 -1
  48. package/dist/src/nextjs/nextjs-wizard.js +49 -11
  49. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  50. package/dist/src/nextjs/templates.d.ts +2 -0
  51. package/dist/src/nextjs/templates.js +6 -2
  52. package/dist/src/nextjs/templates.js.map +1 -1
  53. package/dist/src/remix/remix-wizard.js +10 -20
  54. package/dist/src/remix/remix-wizard.js.map +1 -1
  55. package/dist/src/sourcemaps/sourcemaps-wizard.js +26 -13
  56. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  57. package/dist/src/sourcemaps/tools/nextjs.js +1 -1
  58. package/dist/src/sourcemaps/tools/nextjs.js.map +1 -1
  59. package/dist/src/sourcemaps/tools/sentry-cli.js +19 -16
  60. package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
  61. package/dist/src/sourcemaps/tools/vite.d.ts +2 -1
  62. package/dist/src/sourcemaps/tools/vite.js +123 -111
  63. package/dist/src/sourcemaps/tools/vite.js.map +1 -1
  64. package/dist/src/sourcemaps/tools/webpack.d.ts +6 -1
  65. package/dist/src/sourcemaps/tools/webpack.js +290 -25
  66. package/dist/src/sourcemaps/tools/webpack.js.map +1 -1
  67. package/dist/src/sourcemaps/utils/detect-tool.d.ts +1 -1
  68. package/dist/src/sourcemaps/utils/detect-tool.js.map +1 -1
  69. package/dist/src/sveltekit/sdk-setup.js +5 -5
  70. package/dist/src/sveltekit/sdk-setup.js.map +1 -1
  71. package/dist/src/sveltekit/sveltekit-wizard.js +34 -44
  72. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  73. package/dist/src/telemetry.js +1 -0
  74. package/dist/src/telemetry.js.map +1 -1
  75. package/dist/src/utils/ast-utils.d.ts +9 -5
  76. package/dist/src/utils/ast-utils.js +26 -11
  77. package/dist/src/utils/ast-utils.js.map +1 -1
  78. package/dist/src/utils/clack-utils.d.ts +74 -28
  79. package/dist/src/utils/clack-utils.js +427 -264
  80. package/dist/src/utils/clack-utils.js.map +1 -1
  81. package/dist/src/utils/package-manager.d.ts +10 -0
  82. package/dist/{lib/Helper/PackageManager.js → src/utils/package-manager.js} +42 -74
  83. package/dist/src/utils/package-manager.js.map +1 -0
  84. package/dist/src/utils/release-registry.d.ts +1 -0
  85. package/dist/src/utils/release-registry.js +68 -0
  86. package/dist/src/utils/release-registry.js.map +1 -0
  87. package/dist/src/utils/sentrycli-utils.d.ts +4 -0
  88. package/dist/src/utils/sentrycli-utils.js +41 -0
  89. package/dist/src/utils/sentrycli-utils.js.map +1 -0
  90. package/dist/test/android/code-tools.test.d.ts +1 -0
  91. package/dist/test/android/code-tools.test.js +34 -0
  92. package/dist/test/android/code-tools.test.js.map +1 -0
  93. package/dist/test/sourcemaps/tools/vite.test.d.ts +1 -0
  94. package/dist/test/sourcemaps/tools/vite.test.js +132 -0
  95. package/dist/test/sourcemaps/tools/vite.test.js.map +1 -0
  96. package/dist/test/sourcemaps/tools/webpack.test.d.ts +1 -0
  97. package/dist/test/sourcemaps/tools/webpack.test.js +179 -0
  98. package/dist/test/sourcemaps/tools/webpack.test.js.map +1 -0
  99. package/dist/test/utils/ast-utils.test.js +42 -7
  100. package/dist/test/utils/ast-utils.test.js.map +1 -1
  101. package/dist/test/utils/clack-utils.test.d.ts +1 -0
  102. package/dist/test/utils/clack-utils.test.js +200 -0
  103. package/dist/test/utils/clack-utils.test.js.map +1 -0
  104. package/lib/Constants.ts +5 -0
  105. package/lib/Steps/ChooseIntegration.ts +7 -3
  106. package/lib/Steps/Integrations/Android.ts +23 -0
  107. package/lib/Steps/Integrations/ReactNative.ts +9 -3
  108. package/lib/Steps/PromptForParameters.ts +48 -3
  109. package/lib/Steps/SentryProjectSelector.ts +3 -1
  110. package/package.json +4 -3
  111. package/src/android/android-wizard.ts +204 -0
  112. package/src/android/code-tools.ts +170 -0
  113. package/src/android/gradle.ts +250 -0
  114. package/src/android/manifest.ts +180 -0
  115. package/src/android/templates.ts +88 -0
  116. package/src/apple/apple-wizard.ts +113 -35
  117. package/src/apple/cocoapod.ts +6 -3
  118. package/src/apple/code-tools.ts +46 -18
  119. package/src/apple/fastlane.ts +6 -12
  120. package/src/apple/templates.ts +2 -8
  121. package/src/apple/xcode-manager.ts +167 -25
  122. package/src/nextjs/nextjs-wizard.ts +72 -8
  123. package/src/nextjs/templates.ts +16 -2
  124. package/src/remix/remix-wizard.ts +10 -15
  125. package/src/sourcemaps/sourcemaps-wizard.ts +19 -5
  126. package/src/sourcemaps/tools/nextjs.ts +2 -2
  127. package/src/sourcemaps/tools/sentry-cli.ts +8 -7
  128. package/src/sourcemaps/tools/vite.ts +143 -79
  129. package/src/sourcemaps/tools/webpack.ts +369 -30
  130. package/src/sourcemaps/utils/detect-tool.ts +2 -1
  131. package/src/sveltekit/sdk-setup.ts +10 -6
  132. package/src/sveltekit/sveltekit-wizard.ts +5 -14
  133. package/src/telemetry.ts +2 -0
  134. package/src/utils/ast-utils.ts +29 -11
  135. package/src/utils/clack-utils.ts +485 -283
  136. package/src/utils/package-manager.ts +61 -0
  137. package/src/utils/release-registry.ts +19 -0
  138. package/src/utils/sentrycli-utils.ts +22 -0
  139. package/test/android/code-tools.test.ts +49 -0
  140. package/test/sourcemaps/tools/vite.test.ts +149 -0
  141. package/test/sourcemaps/tools/webpack.test.ts +303 -0
  142. package/test/utils/ast-utils.test.ts +28 -9
  143. package/test/utils/clack-utils.test.ts +142 -0
  144. package/dist/lib/Helper/PackageManager.d.ts +0 -22
  145. package/dist/lib/Helper/PackageManager.js.map +0 -1
  146. package/dist/src/utils/vendor/clack-custom-select.d.ts +0 -21
  147. package/dist/src/utils/vendor/clack-custom-select.js +0 -137
  148. package/dist/src/utils/vendor/clack-custom-select.js.map +0 -1
  149. package/lib/Helper/PackageManager.ts +0 -59
  150. package/src/utils/vendor/clack-custom-select.ts +0 -160
@@ -7,12 +7,17 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { setInterval } from 'timers';
9
9
  import { URL } from 'url';
10
- import { promisify } from 'util';
11
10
  import * as Sentry from '@sentry/node';
12
- import { windowedSelect } from './vendor/clack-custom-select';
13
11
  import { hasPackageInstalled, PackageDotJson } from './package-json';
14
12
  import { SentryProjectData, WizardOptions } from './types';
15
13
  import { traceStep } from '../telemetry';
14
+ import {
15
+ detectPackageManger,
16
+ PackageManager,
17
+ installPackageWithPackageManager,
18
+ packageManagers,
19
+ } from './package-manager';
20
+ import { debug } from './debug';
16
21
 
17
22
  const opn = require('opn') as (
18
23
  url: string,
@@ -20,6 +25,7 @@ const opn = require('opn') as (
20
25
 
21
26
  export const SENTRY_DOT_ENV_FILE = '.env.sentry-build-plugin';
22
27
  export const SENTRY_CLI_RC_FILE = '.sentryclirc';
28
+ export const SENTRY_PROPERTIES_FILE = 'sentry.properties';
23
29
 
24
30
  const SAAS_URL = 'https://sentry.io/';
25
31
 
@@ -30,6 +36,38 @@ interface WizardProjectData {
30
36
  projects: SentryProjectData[];
31
37
  }
32
38
 
39
+ export interface CliSetupConfig {
40
+ filename: string;
41
+ name: string;
42
+
43
+ likelyAlreadyHasAuthToken(contents: string): boolean;
44
+ tokenContent(authToken: string): string;
45
+
46
+ likelyAlreadyHasOrgAndProject(contents: string): boolean;
47
+ orgAndProjContent(org: string, project: string): string;
48
+ }
49
+
50
+ export const sourceMapsCliSetupConfig: CliSetupConfig = {
51
+ filename: SENTRY_CLI_RC_FILE,
52
+ name: 'source maps',
53
+ likelyAlreadyHasAuthToken: function (contents: string): boolean {
54
+ return !!(contents.includes('[auth]') && contents.match(/token=./g));
55
+ },
56
+ tokenContent: function (authToken: string): string {
57
+ return `[auth]\ntoken=${authToken}`;
58
+ },
59
+ likelyAlreadyHasOrgAndProject: function (contents: string): boolean {
60
+ return !!(
61
+ contents.includes('[defaults]') &&
62
+ contents.match(/org=./g) &&
63
+ contents.match(/project=./g)
64
+ );
65
+ },
66
+ orgAndProjContent: function (org: string, project: string): string {
67
+ return `[defaults]\norg=${org}\nproject=${project}`;
68
+ },
69
+ };
70
+
33
71
  export async function abort(message?: string, status?: number): Promise<never> {
34
72
  clack.outro(message ?? 'Wizard setup cancelled.');
35
73
  const sentryHub = Sentry.getCurrentHub();
@@ -134,119 +172,13 @@ export async function askToInstallSentryCLI(): Promise<boolean> {
134
172
  );
135
173
  }
136
174
 
137
- export async function askForWizardLogin(options: {
138
- url: string;
139
- promoCode?: string;
140
- platform?:
141
- | 'javascript-nextjs'
142
- | 'javascript-remix'
143
- | 'javascript-sveltekit'
144
- | 'apple-ios';
145
- }): Promise<WizardProjectData> {
146
- Sentry.setTag('has-promo-code', !!options.promoCode);
147
-
148
- let hasSentryAccount = await clack.confirm({
149
- message: 'Do you already have a Sentry account?',
150
- });
151
-
152
- hasSentryAccount = await abortIfCancelled(hasSentryAccount);
153
-
154
- Sentry.setTag('already-has-sentry-account', hasSentryAccount);
155
-
156
- let wizardHash: string;
157
- try {
158
- wizardHash = (
159
- await axios.get<{ hash: string }>(`${options.url}api/0/wizard/`)
160
- ).data.hash;
161
- } catch {
162
- if (options.url !== SAAS_URL) {
163
- clack.log.error('Loading Wizard failed. Did you provide the right URL?');
164
- await abort(
165
- chalk.red(
166
- 'Please check your configuration and try again.\n\n Let us know if you think this is an issue with the wizard or Sentry: https://github.com/getsentry/sentry-wizard/issues',
167
- ),
168
- );
169
- } else {
170
- clack.log.error('Loading Wizard failed.');
171
- await abort(
172
- chalk.red(
173
- 'Please try again in a few minutes and let us know if this issue persists: https://github.com/getsentry/sentry-wizard/issues',
174
- ),
175
- );
176
- }
177
- }
178
-
179
- const loginUrl = new URL(
180
- `${options.url}account/settings/wizard/${wizardHash!}/`,
181
- );
182
-
183
- if (!hasSentryAccount) {
184
- loginUrl.searchParams.set('signup', '1');
185
- if (options.platform) {
186
- loginUrl.searchParams.set('project_platform', options.platform);
187
- }
188
- }
189
-
190
- if (options.promoCode) {
191
- loginUrl.searchParams.set('code', options.promoCode);
192
- }
193
-
194
- const urlToOpen = loginUrl.toString();
195
- clack.log.info(
196
- `${chalk.bold(
197
- `If the browser window didn't open automatically, please open the following link to ${
198
- hasSentryAccount ? 'log' : 'sign'
199
- } into Sentry:`,
200
- )}\n\n${chalk.cyan(urlToOpen)}`,
201
- );
202
-
203
- opn(urlToOpen).catch(() => {
204
- // opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
205
- });
206
-
207
- const loginSpinner = clack.spinner();
208
-
209
- loginSpinner.start('Waiting for you to log in using the link above');
210
-
211
- const data = await new Promise<WizardProjectData>((resolve) => {
212
- const pollingInterval = setInterval(() => {
213
- axios
214
- .get<WizardProjectData>(`${options.url}api/0/wizard/${wizardHash}/`)
215
- .then((result) => {
216
- resolve(result.data);
217
- clearTimeout(timeout);
218
- clearInterval(pollingInterval);
219
- void axios.delete(`${options.url}api/0/wizard/${wizardHash}/`);
220
- })
221
- .catch(() => {
222
- // noop - just try again
223
- });
224
- }, 500);
225
-
226
- const timeout = setTimeout(() => {
227
- clearInterval(pollingInterval);
228
- loginSpinner.stop(
229
- 'Login timed out. No worries - it happens to the best of us.',
230
- );
231
-
232
- Sentry.setTag('opened-wizard-link', false);
233
- void abort('Please restart the Wizard and log in to complete the setup.');
234
- }, 180_000);
235
- });
236
-
237
- loginSpinner.stop('Login complete.');
238
- Sentry.setTag('opened-wizard-link', true);
239
-
240
- return data;
241
- }
242
-
243
175
  export async function askForItemSelection(
244
176
  items: string[],
245
177
  message: string,
246
178
  ): Promise<{ value: string; index: number }> {
247
179
  const selection: { value: string; index: number } | symbol =
248
180
  await abortIfCancelled(
249
- windowedSelect({
181
+ clack.select({
250
182
  maxItems: 12,
251
183
  message: message,
252
184
  options: items.map((item, index) => {
@@ -261,29 +193,6 @@ export async function askForItemSelection(
261
193
  return selection;
262
194
  }
263
195
 
264
- export async function askForProjectSelection(
265
- projects: SentryProjectData[],
266
- ): Promise<SentryProjectData> {
267
- const selection: SentryProjectData | symbol = await abortIfCancelled(
268
- windowedSelect({
269
- maxItems: 12,
270
- message: 'Select your Sentry project.',
271
- options: projects.map((project) => {
272
- return {
273
- value: project,
274
- label: `${project.organization.slug}/${project.slug}`,
275
- };
276
- }),
277
- }),
278
- );
279
-
280
- Sentry.setTag('project', selection.slug);
281
- Sentry.setTag('project-platform', selection.platform);
282
- Sentry.setUser({ id: selection.organization.slug });
283
-
284
- return selection;
285
- }
286
-
287
196
  export async function installPackage({
288
197
  packageName,
289
198
  alreadyInstalled,
@@ -314,17 +223,11 @@ export async function installPackage({
314
223
  sdkInstallSpinner.start(
315
224
  `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
316
225
  packageName,
317
- )} with ${chalk.bold(packageManager)}.`,
226
+ )} with ${chalk.bold(packageManager.label)}.`,
318
227
  );
319
228
 
320
229
  try {
321
- if (packageManager === 'yarn') {
322
- await promisify(childProcess.exec)(`yarn add ${packageName}@latest`);
323
- } else if (packageManager === 'pnpm') {
324
- await promisify(childProcess.exec)(`pnpm add ${packageName}@latest`);
325
- } else if (packageManager === 'npm') {
326
- await promisify(childProcess.exec)(`npm install ${packageName}@latest`);
327
- }
230
+ await installPackageWithPackageManager(packageManager, packageName);
328
231
  } catch (e) {
329
232
  sdkInstallSpinner.stop('Installation failed.');
330
233
  clack.log.error(
@@ -341,189 +244,119 @@ export async function installPackage({
341
244
  sdkInstallSpinner.stop(
342
245
  `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
343
246
  packageName,
344
- )} with ${chalk.bold(packageManager)}.`,
247
+ )} with ${chalk.bold(packageManager.label)}.`,
345
248
  );
346
249
  }
347
250
 
348
- /**
349
- * Asks users if they are using SaaS or self-hosted Sentry and returns the validated URL.
350
- *
351
- * If users started the wizard with a --url arg, that URL is used as the default and we skip
352
- * the self-hosted question. However, the passed url is still validated and in case it's
353
- * invalid, users are asked to enter a new one until it is valid.
354
- *
355
- * @param urlFromArgs the url passed via the --url arg
356
- */
357
- export async function askForSelfHosted(urlFromArgs?: string): Promise<{
358
- url: string;
359
- selfHosted: boolean;
360
- }> {
361
- if (!urlFromArgs) {
362
- const choice: 'saas' | 'self-hosted' | symbol = await abortIfCancelled(
363
- clack.select({
364
- message: 'Are you using Sentry SaaS or self-hosted Sentry?',
365
- options: [
366
- { value: 'saas', label: 'Sentry SaaS (sentry.io)' },
367
- {
368
- value: 'self-hosted',
369
- label: 'Self-hosted/on-premise/single-tenant',
370
- },
371
- ],
372
- }),
373
- );
374
-
375
- if (choice === 'saas') {
376
- Sentry.setTag('url', SAAS_URL);
377
- Sentry.setTag('self-hosted', false);
378
- return { url: SAAS_URL, selfHosted: false };
379
- }
380
- }
381
-
382
- let validUrl: string | undefined;
383
- let tmpUrlFromArgs = urlFromArgs;
384
-
385
- while (validUrl === undefined) {
386
- const url =
387
- tmpUrlFromArgs ||
388
- (await abortIfCancelled(
389
- clack.text({
390
- message: `Please enter the URL of your ${
391
- urlFromArgs ? '' : 'self-hosted '
392
- }Sentry instance.`,
393
- placeholder: 'https://sentry.io/',
394
- }),
395
- ));
396
- tmpUrlFromArgs = undefined;
397
-
398
- try {
399
- validUrl = new URL(url).toString();
400
-
401
- // We assume everywhere else that the URL ends in a slash
402
- if (!validUrl.endsWith('/')) {
403
- validUrl += '/';
404
- }
405
- } catch {
406
- clack.log.error(
407
- 'Please enter a valid URL. (It should look something like "https://sentry.mydomain.com/")',
408
- );
409
- }
410
- }
411
-
412
- const isSelfHostedUrl = new URL(validUrl).host !== new URL(SAAS_URL).host;
413
-
414
- Sentry.setTag('url', validUrl);
415
- Sentry.setTag('self-hosted', isSelfHostedUrl);
416
-
417
- return { url: validUrl, selfHosted: true };
418
- }
419
-
420
251
  async function addOrgAndProjectToSentryCliRc(
421
252
  org: string,
422
253
  project: string,
254
+ setupConfig: CliSetupConfig,
423
255
  ): Promise<void> {
424
- const clircContents = fs.readFileSync(
425
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
256
+ const configContents = fs.readFileSync(
257
+ path.join(process.cwd(), setupConfig.filename),
426
258
  'utf8',
427
259
  );
428
260
 
429
- const likelyAlreadyHasOrgAndProject = !!(
430
- clircContents.includes('[defaults]') &&
431
- clircContents.match(/org=./g) &&
432
- clircContents.match(/project=./g)
433
- );
434
-
435
- if (likelyAlreadyHasOrgAndProject) {
261
+ if (setupConfig.likelyAlreadyHasOrgAndProject(configContents)) {
436
262
  clack.log.warn(
437
263
  `${chalk.bold(
438
- SENTRY_CLI_RC_FILE,
264
+ setupConfig.filename,
439
265
  )} already has org and project. Will not add them.`,
440
266
  );
441
267
  } else {
442
268
  try {
443
269
  await fs.promises.appendFile(
444
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
445
- `\n[defaults]\norg=${org}\nproject=${project}\n`,
270
+ path.join(process.cwd(), setupConfig.filename),
271
+ `\n${setupConfig.orgAndProjContent(org, project)}\n`,
446
272
  );
447
273
  } catch (e) {
448
274
  clack.log.warn(
449
275
  `${chalk.bold(
450
- SENTRY_CLI_RC_FILE,
276
+ setupConfig.filename,
451
277
  )} could not be updated with org and project.`,
452
278
  );
453
279
  }
454
280
  }
455
281
  }
456
282
 
457
- export async function addSentryCliRc(
283
+ export async function addSentryCliConfig(
458
284
  authToken: string,
285
+ setupConfig: CliSetupConfig = sourceMapsCliSetupConfig,
459
286
  orgSlug?: string,
460
287
  projectSlug?: string,
461
288
  ): Promise<void> {
462
- const clircExists = fs.existsSync(
463
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
289
+ const configExists = fs.existsSync(
290
+ path.join(process.cwd(), setupConfig.filename),
464
291
  );
465
- if (clircExists) {
466
- const clircContents = fs.readFileSync(
467
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
292
+ if (configExists) {
293
+ const configContents = fs.readFileSync(
294
+ path.join(process.cwd(), setupConfig.filename),
468
295
  'utf8',
469
296
  );
470
297
 
471
- const likelyAlreadyHasAuthToken = !!(
472
- clircContents.includes('[auth]') && clircContents.match(/token=./g)
473
- );
474
-
475
- if (likelyAlreadyHasAuthToken) {
298
+ if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
476
299
  clack.log.warn(
477
300
  `${chalk.bold(
478
- SENTRY_CLI_RC_FILE,
301
+ setupConfig.filename,
479
302
  )} already has auth token. Will not add one.`,
480
303
  );
481
304
  } else {
482
305
  try {
483
306
  await fs.promises.writeFile(
484
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
485
- `${clircContents}\n[auth]\ntoken=${authToken}\n`,
307
+ path.join(process.cwd(), setupConfig.filename),
308
+ `${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
486
309
  { encoding: 'utf8', flag: 'w' },
487
310
  );
488
311
  clack.log.success(
489
- `Added auth token to ${chalk.bold(
490
- SENTRY_CLI_RC_FILE,
491
- )} for you to test uploading source maps locally.`,
312
+ chalk.greenBright(
313
+ `Added auth token to ${chalk.bold(
314
+ setupConfig.filename,
315
+ )} for you to test uploading ${setupConfig.name} locally.`,
316
+ ),
492
317
  );
493
318
  } catch {
494
319
  clack.log.warning(
495
320
  `Failed to add auth token to ${chalk.bold(
496
- SENTRY_CLI_RC_FILE,
497
- )}. Uploading source maps during build will likely not work locally.`,
321
+ setupConfig.filename,
322
+ )}. Uploading ${
323
+ setupConfig.name
324
+ } during build will likely not work locally.`,
498
325
  );
499
326
  }
500
327
  }
501
328
  } else {
502
329
  try {
503
330
  await fs.promises.writeFile(
504
- path.join(process.cwd(), SENTRY_CLI_RC_FILE),
505
- `[auth]\ntoken=${authToken}\n`,
331
+ path.join(process.cwd(), setupConfig.filename),
332
+ `${setupConfig.tokenContent(authToken)}\n`,
506
333
  { encoding: 'utf8', flag: 'w' },
507
334
  );
508
335
  clack.log.success(
509
- `Created ${chalk.bold(
510
- SENTRY_CLI_RC_FILE,
511
- )} with auth token for you to test uploading source maps locally.`,
336
+ chalk.greenBright(
337
+ `Created ${chalk.bold(
338
+ setupConfig.filename,
339
+ )} with auth token for you to test uploading ${
340
+ setupConfig.name
341
+ } locally.`,
342
+ ),
512
343
  );
513
344
  } catch {
514
345
  clack.log.warning(
515
346
  `Failed to create ${chalk.bold(
516
- SENTRY_CLI_RC_FILE,
517
- )} with auth token. Uploading source maps during build will likely not work locally.`,
347
+ setupConfig.filename,
348
+ )} with auth token. Uploading ${
349
+ setupConfig.name
350
+ } during build will likely not work locally.`,
518
351
  );
519
352
  }
520
353
  }
521
354
 
522
355
  if (orgSlug && projectSlug) {
523
- await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug);
356
+ await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug, setupConfig);
524
357
  }
525
358
 
526
- await addAuthTokenFileToGitIgnore(SENTRY_CLI_RC_FILE);
359
+ await addAuthTokenFileToGitIgnore(setupConfig.filename);
527
360
  }
528
361
 
529
362
  export async function addDotEnvSentryBuildPluginFile(
@@ -605,7 +438,9 @@ async function addAuthTokenFileToGitIgnore(filename: string): Promise<void> {
605
438
  { encoding: 'utf8' },
606
439
  );
607
440
  clack.log.success(
608
- `Added ${chalk.bold(filename)} to ${chalk.bold('.gitignore')}.`,
441
+ chalk.greenBright(
442
+ `Added ${chalk.bold(filename)} to ${chalk.bold('.gitignore')}.`,
443
+ ),
609
444
  );
610
445
  } catch {
611
446
  clack.log.error(
@@ -661,42 +496,29 @@ export async function getPackageDotJson(): Promise<PackageDotJson> {
661
496
  return packageJson || {};
662
497
  }
663
498
 
664
- async function getPackageManager(): Promise<string> {
665
- const detectedPackageManager = detectPackageManager();
499
+ async function getPackageManager(): Promise<PackageManager> {
500
+ const detectedPackageManager = detectPackageManger();
666
501
 
667
502
  if (detectedPackageManager) {
668
503
  return detectedPackageManager;
669
504
  }
670
505
 
671
- const selectedPackageManager: string | symbol = await abortIfCancelled(
672
- clack.select({
673
- message: 'Please select your package manager.',
674
- options: [
675
- { value: 'npm', label: 'Npm' },
676
- { value: 'yarn', label: 'Yarn' },
677
- { value: 'pnpm', label: 'Pnpm' },
678
- ],
679
- }),
680
- );
506
+ const selectedPackageManager: PackageManager | symbol =
507
+ await abortIfCancelled(
508
+ clack.select({
509
+ message: 'Please select your package manager.',
510
+ options: packageManagers.map((packageManager) => ({
511
+ value: packageManager,
512
+ label: packageManager.label,
513
+ })),
514
+ }),
515
+ );
681
516
 
682
- Sentry.setTag('package-manager', selectedPackageManager);
517
+ Sentry.setTag('package-manager', selectedPackageManager.name);
683
518
 
684
519
  return selectedPackageManager;
685
520
  }
686
521
 
687
- export function detectPackageManager(): 'yarn' | 'npm' | 'pnpm' | undefined {
688
- if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) {
689
- return 'yarn';
690
- }
691
- if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) {
692
- return 'npm';
693
- }
694
- if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) {
695
- return 'pnpm';
696
- }
697
- return undefined;
698
- }
699
-
700
522
  export function isUsingTypeScript() {
701
523
  try {
702
524
  return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
@@ -705,7 +527,26 @@ export function isUsingTypeScript() {
705
527
  }
706
528
  }
707
529
 
708
- export async function getOrAskForProjectData(options: WizardOptions): Promise<{
530
+ /**
531
+ * Checks if we already got project data from a previous wizard invocation.
532
+ * If yes, this data is returned.
533
+ * Otherwise, we start the login flow and ask the user to select a project.
534
+ *
535
+ * Use this function to get project data for the wizard.
536
+ *
537
+ * @param options wizard options
538
+ * @param platform the platform of the wizard
539
+ * @returns project data (org, project, token, url)
540
+ */
541
+ export async function getOrAskForProjectData(
542
+ options: WizardOptions,
543
+ platform?:
544
+ | 'javascript-nextjs'
545
+ | 'javascript-remix'
546
+ | 'javascript-sveltekit'
547
+ | 'apple-ios'
548
+ | 'android',
549
+ ): Promise<{
709
550
  sentryUrl: string;
710
551
  selfHosted: boolean;
711
552
  selectedProject: SentryProjectData;
@@ -728,10 +569,18 @@ export async function getOrAskForProjectData(options: WizardOptions): Promise<{
728
569
  askForWizardLogin({
729
570
  promoCode: options.promoCode,
730
571
  url: sentryUrl,
731
- platform: 'javascript-nextjs',
572
+ platform: platform,
732
573
  }),
733
574
  );
734
575
 
576
+ if (!projects || !projects.length) {
577
+ clack.log.error(
578
+ 'No projects found. Please create a project in Sentry and try again.',
579
+ );
580
+ Sentry.setTag('no-projects-found', true);
581
+ await abort();
582
+ }
583
+
735
584
  const selectedProject = await traceStep('select-project', () =>
736
585
  askForProjectSelection(projects),
737
586
  );
@@ -743,3 +592,356 @@ export async function getOrAskForProjectData(options: WizardOptions): Promise<{
743
592
  selectedProject,
744
593
  };
745
594
  }
595
+
596
+ /**
597
+ * Asks users if they are using SaaS or self-hosted Sentry and returns the validated URL.
598
+ *
599
+ * If users started the wizard with a --url arg, that URL is used as the default and we skip
600
+ * the self-hosted question. However, the passed url is still validated and in case it's
601
+ * invalid, users are asked to enter a new one until it is valid.
602
+ *
603
+ * @param urlFromArgs the url passed via the --url arg
604
+ */
605
+ async function askForSelfHosted(urlFromArgs?: string): Promise<{
606
+ url: string;
607
+ selfHosted: boolean;
608
+ }> {
609
+ if (!urlFromArgs) {
610
+ const choice: 'saas' | 'self-hosted' | symbol = await abortIfCancelled(
611
+ clack.select({
612
+ message: 'Are you using Sentry SaaS or self-hosted Sentry?',
613
+ options: [
614
+ { value: 'saas', label: 'Sentry SaaS (sentry.io)' },
615
+ {
616
+ value: 'self-hosted',
617
+ label: 'Self-hosted/on-premise/single-tenant',
618
+ },
619
+ ],
620
+ }),
621
+ );
622
+
623
+ if (choice === 'saas') {
624
+ Sentry.setTag('url', SAAS_URL);
625
+ Sentry.setTag('self-hosted', false);
626
+ return { url: SAAS_URL, selfHosted: false };
627
+ }
628
+ }
629
+
630
+ let validUrl: string | undefined;
631
+ let tmpUrlFromArgs = urlFromArgs;
632
+
633
+ while (validUrl === undefined) {
634
+ const url =
635
+ tmpUrlFromArgs ||
636
+ (await abortIfCancelled(
637
+ clack.text({
638
+ message: `Please enter the URL of your ${
639
+ urlFromArgs ? '' : 'self-hosted '
640
+ }Sentry instance.`,
641
+ placeholder: 'https://sentry.io/',
642
+ }),
643
+ ));
644
+ tmpUrlFromArgs = undefined;
645
+
646
+ try {
647
+ validUrl = new URL(url).toString();
648
+
649
+ // We assume everywhere else that the URL ends in a slash
650
+ if (!validUrl.endsWith('/')) {
651
+ validUrl += '/';
652
+ }
653
+ } catch {
654
+ clack.log.error(
655
+ 'Please enter a valid URL. (It should look something like "https://sentry.mydomain.com/")',
656
+ );
657
+ }
658
+ }
659
+
660
+ const isSelfHostedUrl = new URL(validUrl).host !== new URL(SAAS_URL).host;
661
+
662
+ Sentry.setTag('url', validUrl);
663
+ Sentry.setTag('self-hosted', isSelfHostedUrl);
664
+
665
+ return { url: validUrl, selfHosted: true };
666
+ }
667
+
668
+ async function askForWizardLogin(options: {
669
+ url: string;
670
+ promoCode?: string;
671
+ platform?:
672
+ | 'javascript-nextjs'
673
+ | 'javascript-remix'
674
+ | 'javascript-sveltekit'
675
+ | 'apple-ios'
676
+ | 'android';
677
+ }): Promise<WizardProjectData> {
678
+ Sentry.setTag('has-promo-code', !!options.promoCode);
679
+
680
+ let hasSentryAccount = await clack.confirm({
681
+ message: 'Do you already have a Sentry account?',
682
+ });
683
+
684
+ hasSentryAccount = await abortIfCancelled(hasSentryAccount);
685
+
686
+ Sentry.setTag('already-has-sentry-account', hasSentryAccount);
687
+
688
+ let wizardHash: string;
689
+ try {
690
+ wizardHash = (
691
+ await axios.get<{ hash: string }>(`${options.url}api/0/wizard/`)
692
+ ).data.hash;
693
+ } catch {
694
+ if (options.url !== SAAS_URL) {
695
+ clack.log.error('Loading Wizard failed. Did you provide the right URL?');
696
+ await abort(
697
+ chalk.red(
698
+ 'Please check your configuration and try again.\n\n Let us know if you think this is an issue with the wizard or Sentry: https://github.com/getsentry/sentry-wizard/issues',
699
+ ),
700
+ );
701
+ } else {
702
+ clack.log.error('Loading Wizard failed.');
703
+ await abort(
704
+ chalk.red(
705
+ 'Please try again in a few minutes and let us know if this issue persists: https://github.com/getsentry/sentry-wizard/issues',
706
+ ),
707
+ );
708
+ }
709
+ }
710
+
711
+ const loginUrl = new URL(
712
+ `${options.url}account/settings/wizard/${wizardHash!}/`,
713
+ );
714
+
715
+ if (!hasSentryAccount) {
716
+ loginUrl.searchParams.set('signup', '1');
717
+ if (options.platform) {
718
+ loginUrl.searchParams.set('project_platform', options.platform);
719
+ }
720
+ }
721
+
722
+ if (options.promoCode) {
723
+ loginUrl.searchParams.set('code', options.promoCode);
724
+ }
725
+
726
+ const urlToOpen = loginUrl.toString();
727
+ clack.log.info(
728
+ `${chalk.bold(
729
+ `If the browser window didn't open automatically, please open the following link to ${
730
+ hasSentryAccount ? 'log' : 'sign'
731
+ } into Sentry:`,
732
+ )}\n\n${chalk.cyan(urlToOpen)}`,
733
+ );
734
+
735
+ opn(urlToOpen).catch(() => {
736
+ // opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
737
+ });
738
+
739
+ const loginSpinner = clack.spinner();
740
+
741
+ loginSpinner.start('Waiting for you to log in using the link above');
742
+
743
+ const data = await new Promise<WizardProjectData>((resolve) => {
744
+ const pollingInterval = setInterval(() => {
745
+ axios
746
+ .get<WizardProjectData>(`${options.url}api/0/wizard/${wizardHash}/`, {
747
+ headers: {
748
+ 'Accept-Encoding': 'deflate',
749
+ },
750
+ })
751
+ .then((result) => {
752
+ resolve(result.data);
753
+ clearTimeout(timeout);
754
+ clearInterval(pollingInterval);
755
+ void axios.delete(`${options.url}api/0/wizard/${wizardHash}/`);
756
+ })
757
+ .catch(() => {
758
+ // noop - just try again
759
+ });
760
+ }, 500);
761
+
762
+ const timeout = setTimeout(() => {
763
+ clearInterval(pollingInterval);
764
+ loginSpinner.stop(
765
+ 'Login timed out. No worries - it happens to the best of us.',
766
+ );
767
+
768
+ Sentry.setTag('opened-wizard-link', false);
769
+ void abort('Please restart the Wizard and log in to complete the setup.');
770
+ }, 180_000);
771
+ });
772
+
773
+ loginSpinner.stop('Login complete.');
774
+ Sentry.setTag('opened-wizard-link', true);
775
+
776
+ return data;
777
+ }
778
+
779
+ async function askForProjectSelection(
780
+ projects: SentryProjectData[],
781
+ ): Promise<SentryProjectData> {
782
+ const label = (project: SentryProjectData): string => {
783
+ return `${project.organization.slug}/${project.slug}`;
784
+ };
785
+ const sortedProjects = [...projects];
786
+ sortedProjects.sort((a: SentryProjectData, b: SentryProjectData) => {
787
+ return label(a).localeCompare(label(b));
788
+ });
789
+ const selection: SentryProjectData | symbol = await abortIfCancelled(
790
+ clack.select({
791
+ maxItems: 12,
792
+ message: 'Select your Sentry project.',
793
+ options: sortedProjects.map((project) => {
794
+ return {
795
+ value: project,
796
+ label: label(project),
797
+ };
798
+ }),
799
+ }),
800
+ );
801
+
802
+ Sentry.setTag('project', selection.slug);
803
+ Sentry.setTag('project-platform', selection.platform);
804
+ Sentry.setUser({ id: selection.organization.slug });
805
+
806
+ return selection;
807
+ }
808
+
809
+ /**
810
+ * Asks users if they have a config file for @param tool (e.g. Vite).
811
+ * If yes, asks users to specify the path to their config file.
812
+ *
813
+ * Use this helper function as a fallback mechanism if the lookup for
814
+ * a config file with its most usual location/name fails.
815
+ *
816
+ * @param toolName Name of the tool for which we're looking for the config file
817
+ * @param configFileName Name of the most common config file name (e.g. vite.config.js)
818
+ *
819
+ * @returns a user path to the config file or undefined if the user doesn't have a config file
820
+ */
821
+ export async function askForToolConfigPath(
822
+ toolName: string,
823
+ configFileName: string,
824
+ ): Promise<string | undefined> {
825
+ const hasConfig = await abortIfCancelled(
826
+ clack.confirm({
827
+ message: `Do you have a ${toolName} config file (e.g. ${chalk.cyan(
828
+ configFileName,
829
+ )}?`,
830
+ initialValue: true,
831
+ }),
832
+ );
833
+
834
+ if (!hasConfig) {
835
+ return undefined;
836
+ }
837
+
838
+ return await abortIfCancelled(
839
+ clack.text({
840
+ message: `Please enter the path to your ${toolName} config file:`,
841
+ placeholder: path.join('.', configFileName),
842
+ validate: (value) => {
843
+ if (!value) {
844
+ return 'Please enter a path.';
845
+ }
846
+
847
+ try {
848
+ fs.accessSync(value);
849
+ } catch {
850
+ return 'Could not access the file at this path.';
851
+ }
852
+ },
853
+ }),
854
+ );
855
+ }
856
+
857
+ /**
858
+ * Prints copy/paste-able instructions to the console.
859
+ * Afterwards asks the user if they added the code snippet to their file.
860
+ *
861
+ * While there's no point in providing a "no" answer here, it gives users time to fulfill the
862
+ * task before the wizard continues with additional steps.
863
+ *
864
+ * Use this function if you want to show users instructions on how to add/modify
865
+ * code in their file. This is helpful if automatic insertion failed or is not possible/feasible.
866
+ *
867
+ * @param filename the name of the file to which the code snippet should be applied.
868
+ * If a path is provided, only the filename will be used.
869
+ * @param codeSnippet the snippet to be printed.
870
+ * Make sure to follow the diff-like format of highlighting lines that require changes
871
+ * and showing unchanged lines in gray.
872
+ *
873
+ * TODO: Link to wizard spec (develop) once it is live
874
+ * TODO: refactor copy paste instructions across different wizards to use this function.
875
+ * this might require adding a custom message parameter to the function
876
+ */
877
+ export async function showCopyPasteInstructions(
878
+ filename: string,
879
+ codeSnippet: string,
880
+ ): Promise<void> {
881
+ clack.log.step(
882
+ `Add the following code to your ${chalk.cyan(
883
+ path.basename(filename),
884
+ )} file:`,
885
+ );
886
+
887
+ // Intentionally logging directly to console here so that the code can be copied/pasted directly
888
+ // eslint-disable-next-line no-console
889
+ console.log(`\n${codeSnippet}`);
890
+
891
+ await abortIfCancelled(
892
+ clack.select({
893
+ message: 'Did you apply the snippet above?',
894
+ options: [{ label: 'Yes, continue!', value: true }],
895
+ initialValue: true,
896
+ }),
897
+ );
898
+ }
899
+
900
+ /**
901
+ * Creates a new config file with the given @param filepath and @param codeSnippet.
902
+ *
903
+ * Use this function to create a new config file for users. This is useful
904
+ * when users answered that they don't yet have a config file for a tool.
905
+ *
906
+ * (This doesn't mean that they don't yet have some other way of configuring
907
+ * their tool but we can leave it up to them to figure out how to merge configs
908
+ * here.)
909
+ *
910
+ * @param filepath absolute path to the new config file
911
+ * @param codeSnippet the snippet to be inserted into the file
912
+ * @param moreInformation (optional) the message to be printed after the file was created
913
+ * For example, this can be a link to more information about configuring the tool.
914
+ *
915
+ * @returns true on sucess, false otherwise
916
+ */
917
+ export async function createNewConfigFile(
918
+ filepath: string,
919
+ codeSnippet: string,
920
+ moreInformation?: string,
921
+ ): Promise<boolean> {
922
+ if (!path.isAbsolute(filepath)) {
923
+ debug(`createNewConfigFile: filepath is not absolute: ${filepath}`);
924
+ return false;
925
+ }
926
+
927
+ const prettyFilename = chalk.cyan(path.relative(process.cwd(), filepath));
928
+
929
+ try {
930
+ await fs.promises.writeFile(filepath, codeSnippet);
931
+
932
+ clack.log.success(`Added new ${prettyFilename} file.`);
933
+
934
+ if (moreInformation) {
935
+ clack.log.info(chalk.gray(moreInformation));
936
+ }
937
+
938
+ return true;
939
+ } catch (e) {
940
+ debug(e);
941
+ clack.log.warn(
942
+ `Could not create a new ${prettyFilename} file. Please create one manually and follow the instructions below.`,
943
+ );
944
+ }
945
+
946
+ return false;
947
+ }