Skip to content

Commit 6794330

Browse files
betegonclaudegithub-actions[bot]
authored
fix(init): add --app flag and actionable errors for non-interactive monorepo runs (#977)
Running `sentry init --yes --features errors` against a monorepo would hit the `select-target-app` step, fail to resolve an app non-interactively, and send `{ cancelled: true }` as resume data to the server. The server validated it against a schema requiring `selectedApp` and returned HTTP 500. Users saw: ``` WizardError: HTTP error! status: 500 - {"error":"Invalid resume data: \n- selectedApp: Required"} ``` Three root causes fixed here: **1. No non-interactive escape hatch.** Added `--app <name>` flag so CI/agent runs can specify which monorepo app to initialize. Case-insensitive match against the server-provided app list. **2. `handleSelect` returned instead of throwing.** When `--yes` was set with multiple apps, it returned `{ cancelled: true }` which got forwarded to the server as resume data. It now throws with a formatted app list and the exact re-run command: ``` This monorepo has 3 apps. Use --app to specify which one to initialize: sentry init --yes --features <features> --app web Available apps: web (Next.js) /repo/apps/web api (Express) /repo/apps/api admin (React) /repo/packages/admin Or run without --yes to pick interactively: sentry init ``` **3. Safety net.** Added a guard in `handleSuspendedStep` that throws before any `{ cancelled: true }` result can reach `resumeWithRetry`, so a future regression fails on the CLI side rather than producing an opaque server error. Also fixed the `default` branch in `handleInteractive` (same `{ cancelled: true }` bug for unknown prompt kinds) and rewrote `formatAppList` to iterate over `items` rather than `apps` so the list stays correct if `payload.options` and `payload.apps` ever arrive at different lengths. Closes CLI-17A (77 events, all `is_tty: False` + `flag.yes: True`). --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent e630bf9 commit 6794330

7 files changed

Lines changed: 262 additions & 39 deletions

File tree

plugins/sentry-cli/skills/sentry-cli/references/init.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Initialize Sentry in your project (experimental)
2020
- `-n, --dry-run - Show what would happen without making changes`
2121
- `--features <value>... - Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,ai-monitoring,user-feedback`
2222
- `-t, --team <value> - Team slug to create the project under`
23+
- `--app <value> - App to initialize in a monorepo (required with --yes when multiple apps are detected)`
2324
- `--tui - Use the Ink-based interactive UI (default). Pass --no-tui to fall back to plain log output.`
2425

2526
**Examples:**

src/commands/init.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type InitFlags = {
8282
readonly "dry-run": boolean;
8383
readonly features?: string[];
8484
readonly team?: string;
85+
readonly app?: string;
8586
/**
8687
* Default `true` — Ink is the default UI on both the Bun binary
8788
* and the npm/Node distribution. Stricli auto-generates a negated
@@ -336,6 +337,13 @@ export const initCommand = buildCommand<
336337
brief: "Team slug to create the project under",
337338
optional: true,
338339
},
340+
app: {
341+
kind: "parsed",
342+
parse: String,
343+
brief:
344+
"App to initialize in a monorepo (required with --yes when multiple apps are detected)",
345+
optional: true,
346+
},
339347
tui: {
340348
kind: "boolean",
341349
brief:
@@ -406,6 +414,7 @@ export const initCommand = buildCommand<
406414
dryRun: flags["dry-run"],
407415
features: featuresList,
408416
team: flags.team,
417+
app: flags.app,
409418
org: explicitOrg,
410419
project: explicitProject,
411420
// `flags.tui` defaults to `true`. `--no-tui` (auto-generated

src/lib/init/interactive.ts

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import chalk from "chalk";
14+
import { WizardError } from "../errors.js";
1415
import {
1516
abortIfCancelled,
1617
featureHint,
@@ -50,10 +51,59 @@ export async function handleInteractive(
5051
case "confirm":
5152
return await handleConfirm(payload, options, ui);
5253
default:
53-
return { cancelled: true };
54+
throw new WizardError(
55+
`Unsupported interactive prompt kind: "${(payload as { kind: string }).kind}"`,
56+
{ rendered: false }
57+
);
5458
}
5559
}
5660

61+
type AppEntry = { name: string; path: string; framework?: string };
62+
63+
function formatAppList(apps: AppEntry[], items: string[]): string[] {
64+
// Name-based lookup keeps this correct even when payload.options and
65+
// payload.apps arrive with different lengths.
66+
const nameWidth = Math.max(1, ...items.map((n) => n.length));
67+
return items.map((name) => {
68+
const meta = apps.find((a) => a.name === name);
69+
const fw = meta?.framework ? ` (${meta.framework})` : "";
70+
const path = meta?.path ? ` ${meta.path}` : "";
71+
return ` ${name.padEnd(nameWidth)}${fw}${path}`;
72+
});
73+
}
74+
75+
function buildMultiAppMessage(apps: AppEntry[], items: string[]): string {
76+
const exampleApp = items[0] ?? "<app>";
77+
return [
78+
`This monorepo has ${items.length} apps. Use --app to specify which one to initialize:`,
79+
"",
80+
` sentry init --yes --features <features> --app ${exampleApp}`,
81+
"",
82+
"Available apps:",
83+
...formatAppList(apps, items),
84+
"",
85+
"Or run without --yes to pick interactively:",
86+
" sentry init",
87+
].join("\n");
88+
}
89+
90+
function buildAppNotFoundMessage(
91+
requested: string,
92+
apps: AppEntry[],
93+
items: string[]
94+
): string {
95+
const exampleApp = items[0] ?? "<app>";
96+
return [
97+
`App "${requested}" not found in this monorepo.`,
98+
"",
99+
"Available apps:",
100+
...formatAppList(apps, items),
101+
"",
102+
"Re-run with --app <name>, for example:",
103+
` sentry init --yes --features <features> --app ${exampleApp}`,
104+
].join("\n");
105+
}
106+
57107
async function handleSelect(
58108
payload: SelectPayload,
59109
options: InteractiveContext,
@@ -63,24 +113,39 @@ async function handleSelect(
63113
const items = payload.options ?? apps.map((a) => a.name);
64114

65115
if (items.length === 0) {
66-
return { cancelled: true };
116+
throw new WizardError("No options available for this selection.", {
117+
rendered: false,
118+
});
67119
}
68120

69-
if (options.yes) {
70-
if (items.length === 1) {
71-
ui.log.info(`Auto-selected: ${items[0]}`);
72-
return { selectedApp: items[0] };
73-
}
74-
ui.log.error(
75-
`--yes requires exactly one option for selection, but found ${items.length}. Run interactively to choose.`
121+
if (options.app && payload.apps && payload.apps.length > 0) {
122+
const match = items.find(
123+
(item) => item.toLowerCase() === options.app?.toLowerCase()
76124
);
77-
return { cancelled: true };
125+
if (!match) {
126+
const message = buildAppNotFoundMessage(options.app, apps, items);
127+
ui.log.error(message);
128+
throw new WizardError(message, { rendered: true });
129+
}
130+
ui.log.info(`Using app: ${match}`);
131+
return { selectedApp: match };
132+
}
133+
134+
if (options.yes && items.length === 1) {
135+
ui.log.info(`Auto-selected: ${items[0]}`);
136+
return { selectedApp: items[0] };
137+
}
138+
139+
if (options.yes && payload.apps && payload.apps.length > 0) {
140+
const message = buildMultiAppMessage(apps, items);
141+
ui.log.error(message);
142+
throw new WizardError(message, { rendered: true });
78143
}
79144

80145
const selected = await ui.select<string>({
81146
message: payload.prompt,
82-
options: items.map((item, i) => {
83-
const app = apps[i];
147+
options: items.map((item) => {
148+
const app = apps.find((a) => a.name === item);
84149
return {
85150
value: item,
86151
label: item,

src/lib/init/preflight.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function buildResolvedInitContext(
9999
org,
100100
team,
101101
project: selection.project,
102+
app: initial.app,
102103
authToken: getAuthToken(),
103104
existingProject: selection.existingProject,
104105
};

src/lib/init/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export type WizardOptions = {
2121
team?: string;
2222
org?: string;
2323
project?: string;
24+
/** Pre-selected app name for monorepo runs. When set, skips the interactive
25+
* app-selection prompt and uses this value directly. Required when `--yes`
26+
* is passed against a monorepo with more than one detected app. */
27+
app?: string;
2428
/**
2529
* Force the non-Ink fallback (`LoggingUI`). Mapped from
2630
* `--no-tui`. Acts as an escape hatch when the Ink TUI
@@ -44,11 +48,16 @@ export type ResolvedInitContext = {
4448
*/
4549
team?: string;
4650
project?: string;
51+
/** Pre-selected app name for monorepo runs. Passed through from `--app`. */
52+
app?: string;
4753
authToken?: string;
4854
existingProject?: ExistingProjectData;
4955
};
5056

51-
export type InteractiveContext = Pick<ResolvedInitContext, "yes" | "dryRun">;
57+
export type InteractiveContext = Pick<
58+
ResolvedInitContext,
59+
"yes" | "dryRun" | "app"
60+
>;
5261

5362
// Tool suspend payloads
5463
export type ToolPayload =

src/lib/init/wizard-runner.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,17 @@ async function handleSuspendedStep(
227227

228228
const interactiveResult = await handleInteractive(payload, context, ui);
229229

230+
// Safety net: { cancelled: true } would send malformed resume data to the
231+
// server and produce a cryptic HTTP 500. All interactive handlers should
232+
// throw on unresolvable prompts instead of returning this sentinel, but
233+
// guard here as well so any future regression fails loudly on the CLI side.
234+
if (interactiveResult.cancelled === true) {
235+
throw new WizardError(
236+
"Setup could not complete: interactive step was not resolved.",
237+
{ rendered: false }
238+
);
239+
}
240+
230241
spin.start("Processing...");
231242
spinState.running = true;
232243

0 commit comments

Comments
 (0)