Skip to content

Commit 6f46e16

Browse files
betegonclaude
andauthored
refactor(init): reuse resolveOrCreateTeam for wizard team resolution (#679)
## Summary The init wizard had ~55 lines of inline team resolution that duplicated `resolveOrCreateTeam` from `resolve-team.ts`. Both implementations did the same thing: list teams → 0 teams (auto-create) → 1 team (auto-select) → N teams (filter by membership → pick). Adds an `onAmbiguous` callback to `ResolveTeamOptions` so the wizard can present a clack `select()` prompt when multiple teams match, instead of the shared function throwing `ContextError`. Also fixes a silent error-swallowing catch (`captureException` + continue) that let team resolution failures pass through unnoticed — the wizard would continue without a team and fail later when project creation needed one. ## Changes - `resolve-team.ts`: Added optional `onAmbiguous` callback to `ResolveTeamOptions`. When provided, it's called with the candidate teams instead of throwing `ContextError`. Non-breaking — existing callers are unaffected. - `wizard-runner.ts`: Replaced inline team resolution with a call to `resolveOrCreateTeam`. Errors now surface properly instead of being silently swallowed. Removed unused imports (`createTeam`, `listTeams`, `getSentryBaseUrl`). ## Test plan - [ ] `npx tsc --noEmit` passes - [ ] `npx biome check` passes on changed files - [ ] Run `sentry init` against an org with 0, 1, and multiple teams - [ ] Run `sentry init --yes` against an org with multiple teams (should auto-pick first) - [ ] Cancel team selection mid-prompt — should exit cleanly with code 0 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2009674 commit 6f46e16

File tree

2 files changed

+41
-54
lines changed

2 files changed

+41
-54
lines changed

src/lib/init/wizard-runner.ts

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ import {
2020
import { MastraClient } from "@mastra/client-js";
2121
import { captureException, getTraceData } from "@sentry/node-core/light";
2222
import type { SentryTeam } from "../../types/index.js";
23-
import { createTeam, listTeams } from "../api-client.js";
2423
import { formatBanner } from "../banner.js";
2524
import { CLI_VERSION } from "../constants.js";
2625
import { getAuthToken } from "../db/auth.js";
2726
import { WizardError } from "../errors.js";
2827
import { terminalLink } from "../formatters/colors.js";
29-
import { getSentryBaseUrl } from "../sentry-urls.js";
28+
import { resolveOrCreateTeam } from "../resolve-team.js";
3029
import { slugify } from "../utils.js";
3130
import {
3231
abortIfCancelled,
@@ -506,58 +505,36 @@ async function resolvePreSpinnerOptions(
506505
// Resolve team upfront so failures surface before the AI workflow starts.
507506
if (!opts.team && opts.org) {
508507
try {
509-
const teams = await listTeams(opts.org);
510-
511-
if (teams.length === 0) {
512-
// New org with no teams — auto-create one
513-
const teamSlug = deriveTeamSlug();
514-
try {
515-
const created = await createTeam(opts.org, teamSlug);
516-
opts = { ...opts, team: created.slug };
517-
} catch (err) {
518-
captureException(err, {
519-
extra: { orgSlug: opts.org, teamSlug, context: "auto-create team" },
520-
});
521-
const teamsUrl = `${getSentryBaseUrl()}/settings/${opts.org}/teams/`;
522-
log.error(
523-
"No teams in your organization.\n" +
524-
`Create one at ${terminalLink(teamsUrl)} and run sentry init again.`
525-
);
526-
cancel("Setup failed.");
527-
throw new WizardError("No teams in your organization.");
528-
}
529-
} else if (teams.length === 1) {
530-
opts = { ...opts, team: (teams[0] as SentryTeam).slug };
531-
} else {
532-
// Multiple teams — prefer teams the user belongs to
533-
const memberTeams = teams.filter((t) => t.isMember === true);
534-
const candidates = memberTeams.length > 0 ? memberTeams : teams;
535-
536-
if (candidates.length === 1) {
537-
opts = { ...opts, team: (candidates[0] as SentryTeam).slug };
538-
} else if (yes) {
539-
opts = { ...opts, team: (candidates[0] as SentryTeam).slug };
540-
} else {
541-
const selected = await select({
542-
message: "Which team should own this project?",
543-
options: candidates.map((t) => ({
544-
value: t.slug,
545-
label: t.slug,
546-
hint: t.name !== t.slug ? t.name : undefined,
547-
})),
548-
});
549-
if (isCancel(selected)) {
550-
cancel("Setup cancelled.");
551-
process.exitCode = 0;
552-
return null;
553-
}
554-
opts = { ...opts, team: selected };
555-
}
556-
}
557-
} catch (err) {
558-
captureException(err, {
559-
extra: { orgSlug: opts.org, context: "early team resolution" },
508+
const result = await resolveOrCreateTeam(opts.org, {
509+
autoCreateSlug: deriveTeamSlug(),
510+
usageHint: "sentry init",
511+
onAmbiguous: yes
512+
? async (candidates) => (candidates[0] as SentryTeam).slug
513+
: async (candidates) => {
514+
const selected = await select({
515+
message: "Which team should own this project?",
516+
options: candidates.map((t) => ({
517+
value: t.slug,
518+
label: t.slug,
519+
hint: t.name !== t.slug ? t.name : undefined,
520+
})),
521+
});
522+
if (isCancel(selected)) {
523+
cancel("Setup cancelled.");
524+
process.exitCode = 0;
525+
throw new WizardCancelledError();
526+
}
527+
return selected;
528+
},
560529
});
530+
opts = { ...opts, team: result.slug };
531+
} catch (err) {
532+
if (err instanceof WizardCancelledError) {
533+
return null;
534+
}
535+
log.error(errorMessage(err));
536+
cancel("Setup failed.");
537+
throw new WizardError(errorMessage(err));
561538
}
562539
}
563540

src/lib/resolve-team.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export type ResolveTeamOptions = {
7171
* with the autoCreateSlug value.
7272
*/
7373
dryRun?: boolean;
74+
/**
75+
* Called when multiple candidate teams remain after membership filtering.
76+
* Return the selected team slug. If not provided, a ContextError is thrown.
77+
*/
78+
onAmbiguous?: (candidates: SentryTeam[]) => Promise<string>;
7479
};
7580

7681
/** Result of team resolution, including how the team was determined */
@@ -142,7 +147,12 @@ export async function resolveOrCreateTeam(
142147
};
143148
}
144149

145-
// Multiple candidates — user must specify
150+
// Multiple candidates — let caller choose or throw
151+
if (options.onAmbiguous) {
152+
const slug = await options.onAmbiguous(candidates);
153+
return { slug, source: "auto-selected" };
154+
}
155+
146156
const label =
147157
memberTeams.length > 0
148158
? `You belong to ${candidates.length} teams in ${orgSlug}`

0 commit comments

Comments
 (0)