fix(init): require auth upfront and fix prompt/spinner ordering#610
fix(init): require auth upfront and fix prompt/spinner ordering#610
Conversation
Clack's design is prompts-first, then tasks. The previous code called select() inside createSentryProject (a local-op) while the spinner was still running, causing the spinner's setInterval to write output below the active prompt's bottom border. Fix: resolve org and detect existing project before spin.start() in runWizard(). Since options.org is now always set by the time the spinner starts, createSentryProject's interactive prompts are automatically skipped (they already guard on options.org). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛Upgrade
Other
Documentation 📚
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
When a user cancelled the org selection prompt, resolveOrgSlug returned
{ ok: false, error: "Organisation selection cancelled." }, which the
pre-spinner handler treated as a hard failure (exit code 1, "Setup
failed.").
Fix: throw WizardCancelledError on cancel (consistent with
abortIfCancelled) and catch it in the pre-spinner phase for a graceful
exit (code 0, "Setup cancelled.").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-throwing non-WizardCancelledError exceptions from resolveOrgSlug caused unhandled rejections on network or auth failures. Handle them with log.error + cancel + exit 1, consistent with the rest of the wizard error handling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, running `sentry init` while unauthenticated would either: - silently fail with "Connection failed" (HTTP 401 from the Mastra API swallowed by the wizard's catch block), or - show org/project prompts before failing Now `refreshToken()` is called as the first thing in the command, so an unauthenticated user gets the interactive login flow immediately — consistent with every other command that requires auth.
…ection
- Stop spinner before throwing AuthError so the terminal isn't garbled
when the login flow retakes control
- Use a separate 'resolvedOrg' field for orgs auto-resolved in the prompt
phase, keeping options.org as the user-provided value so createSentryProject's
bare-slug existing-project check (options.project && !options.org) stays correct
- Tighten is401() from .includes('401') to .includes('status: 401') to
avoid false positives on error messages containing '4010ms' etc.
…ng-project prompt Two follow-up bugs from moving prompts before the spinner: 1. When running 'sentry init' with no args and an existing DSN is found, the user is prompted 'use existing or create new?'. If they pick 'create new', createSentryProject re-shows the same prompt during the spinner because the check (options.org || options.project) didn't account for resolvedOrg being set. Fix: add resolvedOrg to the guard so the pre-spinner result is respected. 2. Running 'sentry init my-app' (bare slug, no org) still showed the existing- project prompt inside the spinner because the bare-slug check in createSentryProject (options.project && !options.org) ran after the spinner started. Fix: perform the check in the pre-spinner phase after resolving org, set options.org if the user picks 'use existing', and skip the check in createSentryProject when resolvedOrg is set.
All interactive prompts (existing project detection, org resolution, bare-slug existing-project check) are now handled in runWizard's pre-spinner phase. By the time createSentryProject runs, org is always set via options.org or options.resolvedOrg — the previous fallback branches are unreachable. Also removes the now-unused promptForExistingProject helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| if (options.project) { | ||
| const slug = slugify(options.project); | ||
| const existing = slug | ||
| ? await tryGetExistingProject(orgResult, slug).catch(() => null) | ||
| : null; | ||
| if (existing?.ok) { | ||
| if (yes) { | ||
| // --yes: silently use the existing project | ||
| options = { ...options, org: orgResult }; | ||
| } else { |
There was a problem hiding this comment.
Bug: The code makes a redundant API call to tryGetExistingProject when initializing an existing project via a bare slug, fetching the same project details twice.
Severity: MEDIUM
Suggested Fix
Reuse the project data obtained from the first tryGetExistingProject call in wizard-runner.ts. Pass this data down to the createSentryProject function to avoid making the second, redundant API call in local-ops.ts.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/lib/init/wizard-runner.ts#L366-L375
Potential issue: When initializing a project with a bare slug (e.g., `sentry init
<project-name>`) that already exists, the application makes two identical API calls to
`tryGetExistingProject`. The first call occurs in `wizard-runner.ts:369` to check for
the project's existence before prompting the user. After the user confirms their choice,
a second, redundant call is made in `local-ops.ts:831`. The result from the first API
call is discarded, leading to an unnecessary network request and a performance
degradation due to the API N+1 pattern.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| // All interactive prompts (existing project detection, org resolution, bare | ||
| // slug existing-project check) are handled in runWizard's pre-spinner phase. | ||
| // By the time we reach here, options.org or options.resolvedOrg is always set. | ||
| const orgSlug = (options.org ?? options.resolvedOrg) as string; |
There was a problem hiding this comment.
Unsafe as string cast hides potential undefined orgSlug
Medium Severity
(options.org ?? options.resolvedOrg) as string silently casts a potentially undefined value to string. Both fields are optional in WizardOptions, and the invariant that one is always set relies on the prompt phase in runWizard having executed first. If createSentryProject is ever reached without the prompt phase (e.g., future refactoring, or a code path bypassing runWizard), orgSlug becomes undefined at runtime, causing silent failures in downstream API calls like resolveOrCreateTeam and createProject with an undefined org.


Summary
Two bugs in
sentry init:1. No auth flow when unauthenticated
Running
sentry initwithout being logged in would silently fail with "Connection failed" (HTTP 401 from Mastra, swallowed by the wizard's catch block) instead of triggering the login flow. AddedrefreshToken()as the first thing in the command so unauthenticated users get the login prompt immediately — same as every other command that requires auth.2. Prompts appearing while spinner is running
With multiple Sentry orgs, the org
select()prompt appeared while the spinner was still active. Clack's spinner runs onsetInterval, so it kept writing animation frames below the prompt, garbling the display:Clack's design is prompts-first, then tasks. The org resolution and existing-project detection were buried inside
createSentryProject(a local-op that runs during the spinner). Moved them into a prompt phase inrunWizard()that completes beforespin.start().Test plan
sentry initwhile not logged in — login flow triggers before any wizard UI appearssentry initwith multiple orgs — org prompt appears cleanly before the spinner--org— prompt skipped, spinner starts immediately--yes— existing project auto-selected, no prompt shown