Skip to content

Commit d61d7f0

Browse files
betegonclaude
andcommitted
refactor(init): route wizard errors through framework error pipeline
Replace direct `process.exitCode = 1` in wizard-runner.ts with thrown WizardError instances so errors flow through Stricli's error handler, gaining Sentry telemetry capture and consistent formatting. WizardError carries a `rendered` flag so clack-displayed errors are not duplicated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ca14e7c commit d61d7f0

File tree

3 files changed

+32
-17
lines changed

3 files changed

+32
-17
lines changed

src/app.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
getExitCode,
5252
OutputError,
5353
stringifyUnknown,
54+
WizardError,
5455
} from "./lib/errors.js";
5556
import { error as errorColor, warning } from "./lib/formatters/colors.js";
5657
import { isRouteMap, type RouteMap } from "./lib/introspect.js";
@@ -324,6 +325,11 @@ const customText: ApplicationText = {
324325
Sentry.captureException(exc);
325326

326327
if (exc instanceof CliError) {
328+
// WizardError with rendered=true: clack already displayed the error.
329+
// Return empty string to avoid double output, exit code flows through.
330+
if (exc instanceof WizardError && exc.rendered) {
331+
return "";
332+
}
327333
const prefix = ansiColor ? errorColor("Error:") : "Error:";
328334
return `${prefix} ${exc.format()}`;
329335
}

src/lib/errors.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,21 @@ export class TimeoutError extends CliError {
476476
}
477477
}
478478

479+
/**
480+
* Error thrown by the init wizard when it has already displayed
481+
* the error via clack UI. The `rendered` flag tells the framework
482+
* error handler to skip its own formatting.
483+
*/
484+
export class WizardError extends CliError {
485+
readonly rendered: boolean;
486+
487+
constructor(message: string, options?: { rendered?: boolean }) {
488+
super(message);
489+
this.name = "WizardError";
490+
this.rendered = options?.rendered ?? true;
491+
}
492+
}
493+
479494
// Error Utilities
480495

481496
/**

src/lib/init/wizard-runner.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { SentryTeam } from "../../types/index.js";
2323
import { createTeam, listTeams } from "../api-client.js";
2424
import { formatBanner } from "../banner.js";
2525
import { CLI_VERSION } from "../constants.js";
26+
import { WizardError } from "../errors.js";
2627
import { getAuthToken } from "../db/auth.js";
2728
import { terminalLink } from "../formatters/colors.js";
2829
import { getSentryBaseUrl } from "../sentry-urls.js";
@@ -326,11 +327,10 @@ async function preamble(
326327
dryRun: boolean
327328
): Promise<boolean> {
328329
if (!(yes || process.stdin.isTTY)) {
329-
process.stderr.write(
330-
"Error: Interactive mode requires a terminal. Use --yes for non-interactive mode.\n"
330+
throw new WizardError(
331+
"Interactive mode requires a terminal. Use --yes for non-interactive mode.",
332+
{ rendered: false }
331333
);
332-
process.exitCode = 1;
333-
return false;
334334
}
335335

336336
process.stderr.write(`\n${formatBanner()}\n\n`);
@@ -450,14 +450,12 @@ async function resolvePreSpinnerOptions(
450450
}
451451
log.error(errorMessage(err));
452452
cancel("Setup failed.");
453-
process.exitCode = 1;
454-
return null;
453+
throw new WizardError(errorMessage(err));
455454
}
456455
if (typeof orgResult !== "string") {
457456
log.error(orgResult.error ?? "Failed to resolve organization.");
458457
cancel("Setup failed.");
459-
process.exitCode = 1;
460-
return null;
458+
throw new WizardError(orgResult.error ?? "Failed to resolve organization.");
461459
}
462460
opts = { ...opts, org: orgResult };
463461
}
@@ -524,8 +522,7 @@ async function resolvePreSpinnerOptions(
524522
`Create one at ${terminalLink(teamsUrl)} and run sentry init again.`
525523
);
526524
cancel("Setup failed.");
527-
process.exitCode = 1;
528-
return null;
525+
throw new WizardError("No teams in your organization.");
529526
}
530527
} else if (teams.length === 1) {
531528
opts = { ...opts, team: (teams[0] as SentryTeam).slug };
@@ -641,8 +638,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
641638
spinState.running = false;
642639
log.error(errorMessage(err));
643640
cancel("Setup failed");
644-
process.exitCode = 1;
645-
return;
641+
throw new WizardError(errorMessage(err));
646642
}
647643

648644
const stepPhases = new Map<string, number>();
@@ -659,8 +655,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
659655
spinState.running = false;
660656
log.error(`No suspend payload found for step "${stepId}"`);
661657
cancel("Setup failed");
662-
process.exitCode = 1;
663-
return;
658+
throw new WizardError(`No suspend payload found for step "${stepId}"`);
664659
}
665660

666661
const resumeData = await handleSuspendedStep(
@@ -701,8 +696,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
701696
}
702697
log.error(errorMessage(err));
703698
cancel("Setup failed");
704-
process.exitCode = 1;
705-
return;
699+
throw new WizardError(errorMessage(err));
706700
}
707701

708702
handleFinalResult(result, spin, spinState);
@@ -721,7 +715,7 @@ function handleFinalResult(
721715
spinState.running = false;
722716
}
723717
formatError(result);
724-
process.exitCode = 1;
718+
throw new WizardError("Workflow returned an error");
725719
} else {
726720
if (spinState.running) {
727721
spin.stop("Done");

0 commit comments

Comments
 (0)