From 67f963c78a5566ac7b15d687adde727293f1534e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 7 Apr 2026 13:40:09 +0200 Subject: [PATCH 1/2] refactor(init): reuse resolveOrCreateTeam for wizard team resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The init wizard had ~55 lines of inline team resolution that duplicated the shared resolveOrCreateTeam logic. Both did: list teams → 0 (create) → 1 (auto-select) → N (filter by membership → pick). Adds an onAmbiguous callback to resolveOrCreateTeam so the wizard can present a clack select() prompt when multiple teams match, instead of throwing ContextError. Also fixes a silent error-swallowing catch that let team resolution failures pass through unnoticed, causing confusing errors later when project creation needed a team. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/init/wizard-runner.ts | 81 ++++++++++++----------------------- src/lib/resolve-team.ts | 12 +++++- 2 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index f0f823975..d7e6d44b9 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -20,13 +20,12 @@ import { import { MastraClient } from "@mastra/client-js"; import { captureException, getTraceData } from "@sentry/node-core/light"; import type { SentryTeam } from "../../types/index.js"; -import { createTeam, listTeams } from "../api-client.js"; import { formatBanner } from "../banner.js"; import { CLI_VERSION } from "../constants.js"; import { getAuthToken } from "../db/auth.js"; import { WizardError } from "../errors.js"; import { terminalLink } from "../formatters/colors.js"; -import { getSentryBaseUrl } from "../sentry-urls.js"; +import { resolveOrCreateTeam } from "../resolve-team.js"; import { slugify } from "../utils.js"; import { abortIfCancelled, @@ -506,58 +505,34 @@ async function resolvePreSpinnerOptions( // Resolve team upfront so failures surface before the AI workflow starts. if (!opts.team && opts.org) { try { - const teams = await listTeams(opts.org); - - if (teams.length === 0) { - // New org with no teams — auto-create one - const teamSlug = deriveTeamSlug(); - try { - const created = await createTeam(opts.org, teamSlug); - opts = { ...opts, team: created.slug }; - } catch (err) { - captureException(err, { - extra: { orgSlug: opts.org, teamSlug, context: "auto-create team" }, - }); - const teamsUrl = `${getSentryBaseUrl()}/settings/${opts.org}/teams/`; - log.error( - "No teams in your organization.\n" + - `Create one at ${terminalLink(teamsUrl)} and run sentry init again.` - ); - cancel("Setup failed."); - throw new WizardError("No teams in your organization."); - } - } else if (teams.length === 1) { - opts = { ...opts, team: (teams[0] as SentryTeam).slug }; - } else { - // Multiple teams — prefer teams the user belongs to - const memberTeams = teams.filter((t) => t.isMember === true); - const candidates = memberTeams.length > 0 ? memberTeams : teams; - - if (candidates.length === 1) { - opts = { ...opts, team: (candidates[0] as SentryTeam).slug }; - } else if (yes) { - opts = { ...opts, team: (candidates[0] as SentryTeam).slug }; - } else { - const selected = await select({ - message: "Which team should own this project?", - options: candidates.map((t) => ({ - value: t.slug, - label: t.slug, - hint: t.name !== t.slug ? t.name : undefined, - })), - }); - if (isCancel(selected)) { - cancel("Setup cancelled."); - process.exitCode = 0; - return null; - } - opts = { ...opts, team: selected }; - } - } - } catch (err) { - captureException(err, { - extra: { orgSlug: opts.org, context: "early team resolution" }, + const result = await resolveOrCreateTeam(opts.org, { + autoCreateSlug: deriveTeamSlug(), + usageHint: "sentry init", + onAmbiguous: yes + ? async (candidates) => (candidates[0] as SentryTeam).slug + : async (candidates) => { + const selected = await select({ + message: "Which team should own this project?", + options: candidates.map((t) => ({ + value: t.slug, + label: t.slug, + hint: t.name !== t.slug ? t.name : undefined, + })), + }); + if (isCancel(selected)) { + cancel("Setup cancelled."); + process.exitCode = 0; + throw new WizardCancelledError(); + } + return selected; + }, }); + opts = { ...opts, team: result.slug }; + } catch (err) { + if (err instanceof WizardCancelledError) { + return null; + } + throw new WizardError(errorMessage(err)); } } diff --git a/src/lib/resolve-team.ts b/src/lib/resolve-team.ts index 7c3628334..632991836 100644 --- a/src/lib/resolve-team.ts +++ b/src/lib/resolve-team.ts @@ -71,6 +71,11 @@ export type ResolveTeamOptions = { * with the autoCreateSlug value. */ dryRun?: boolean; + /** + * Called when multiple candidate teams remain after membership filtering. + * Return the selected team slug. If not provided, a ContextError is thrown. + */ + onAmbiguous?: (candidates: SentryTeam[]) => Promise; }; /** Result of team resolution, including how the team was determined */ @@ -142,7 +147,12 @@ export async function resolveOrCreateTeam( }; } - // Multiple candidates — user must specify + // Multiple candidates — let caller choose or throw + if (options.onAmbiguous) { + const slug = await options.onAmbiguous(candidates); + return { slug, source: "auto-selected" }; + } + const label = memberTeams.length > 0 ? `You belong to ${candidates.length} teams in ${orgSlug}` From d0872c8c48141aaa0ca46f5a175af168e0c479b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 7 Apr 2026 15:13:01 +0200 Subject: [PATCH 2/2] fix(init): display error before throwing WizardError in team resolution WizardError defaults rendered=true, so the framework error handler suppresses output. Every other throw site calls log.error() + cancel() first to display the message. The new catch block was missing this, which would silently swallow the error. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/init/wizard-runner.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index d7e6d44b9..5ef660a53 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -532,6 +532,8 @@ async function resolvePreSpinnerOptions( if (err instanceof WizardCancelledError) { return null; } + log.error(errorMessage(err)); + cancel("Setup failed."); throw new WizardError(errorMessage(err)); } }