feat(init): rewrite wizard client for Vercel Workflow + Sandbox server#850
feat(init): rewrite wizard client for Vercel Workflow + Sandbox server#850
Conversation
Replaces the Mastra-suspend-resume client with a thin NDJSON-stream client that talks to the new Nitro+Hono server in `cli-init-api`. - Drop `wizard-runner.ts` + `workflow-inputs.ts` + `@mastra/client-js`. - New `init-runner.ts`: preflight (banner, git, org/team/project, features) -> POST /api/init -> consume stream with reconnect + startIndex (handles Bun fetch idle-body timeouts). - New `transport.ts`, `stream-parser.ts`, `interactive.ts` ported from the bailing-cli prototype. - New `ensure-project.ts` and `select-features.ts` so the workflow receives a complete `{org, team, project, dsn, features}` upfront. - Existing tool registry (`lib/init/tools/`) is reused as-is; the server addresses operations by the same canonical names. - `formatters.ts`: take `WizardOutput` / `InitErrorEvent` directly (no `WorkflowRunResult` indirection). - Rename `MASTRA_API_URL` -> `INIT_API_URL` (still reads `SENTRY_INIT_API_URL` for back-compat). Test: `bun test test/commands/init.test.ts` updated to spy on `init-runner.runInit` instead of the deleted `wizard-runner`. Made-with: Cursor
The CLI no longer prompts the user with a hardcoded list of 8 Sentry features before the workflow even starts. The sandboxed agent analyses the project + docs, calls a new propose_features MCP tool on the server with only the relevant subset, and the CLI renders that tailored multiselect via the existing prompt_request bridge. - init-runner: drop the upfront selectFeatures() call. --features flag still works as a non-interactive override (CI / --yes); when provided it's normalised via normaliseFromFlag and sent on InitStartInput so the agent skips its own proposal. - select-features: keep FEATURE_LABELS, sortFeatures, and normaliseFromFlag as a labels-and-flag module (no more upfront prompt). Drop selectFeatures(). Add a few alternate aliases and the metrics label. - interactive: multi-select handler now looks up FEATURE_LABELS for label + hint, sorts the agent-proposed IDs into canonical display order, and renders the agent's prompt body verbatim. - clack-utils: STEP_LABELS keys on propose-features (matches the new bridge action name). - docs: describe the analyze-then-pick flow + --features override. - tests: new select-features.test.ts covering normaliseFromFlag / sortFeatures / FEATURE_LABELS; extended interactive.test.ts with rendering + sorting cases for the propose-features flow. Made-with: Cursor
|
| EXIT_VERIFICATION_FAILED, | ||
| } from "./constants.js"; | ||
| import type { WizardOutput, WorkflowRunResult } from "./types.js"; | ||
| import type { InitErrorEvent, WizardOutput } from "./types.js"; |
There was a problem hiding this comment.
Summary formatter uses stale feature label map
Medium Severity
formatters.ts imports featureLabel from clack-utils.ts, which uses the old FEATURE_INFO map. That map lacks the new canonical tracing ID introduced by select-features.ts. If the workflow summary includes tracing in its features list, the final output renders the raw string "tracing" instead of "Performance Monitoring (Tracing)". The interactive prompt in interactive.ts already switched to FEATURE_LABELS from select-features.ts, creating an inconsistency between the picker and the summary.
Reviewed by Cursor Bugbot for commit 33a8892. Configure here.
| message: text || response.statusText, | ||
| retryable: false, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Response body consumed twice in error reader
Low Severity
readErrorPayload calls response.json() when the content type is JSON. If that succeeds but the parsed payload lacks an error string field, the code falls through to response.text(). Since the body stream was already consumed by json(), text() returns an empty string, causing the error message to degrade to the generic response.statusText. This loses any useful error details the server may have sent in a different JSON shape.
Reviewed by Cursor Bugbot for commit 33a8892. Configure here.
Two issues kept biting long wizard runs: 1. The agent's first turn always burned a bridge round-trip on list_dir + read_files of common config files, which was visible to the user as "Listing ." pinning the spinner for ~3 minutes when the agent's first read happened to coincide with a workflow replay. 2. Idle reconnects after a Cloudflare 524 / dropped stream would either retry forever with no backoff or die on the first 404, depending on which path tripped first. Project-context preflight (workflow-inputs.ts): - Local snapshot of the working directory listing, common config files (package.json, tsconfig.json, next.config.*, etc.), and Sentry presence detection. - Sent on InitStartInput.projectContext and inlined into the agent user prompt so phase 1 starts with everything it needs. - Capped to MAX_FILE_BYTES per file to keep the start payload small. Status-aware reconnection (init-runner.ts, transport.ts, constants.ts): - consumeStream / handleStreamClosure / resumeRun mirror the birthday-card-generator example: fetch run status before reopening the stream, count failures against MAX_STATUS_FAILURES, cap exponential backoff at MAX_RECONNECT_DELAY_MS. - openInitStream failures fall back into handleStreamClosure rather than throwing immediately, so a transient 5xx during the agent's long-running tool calls no longer kills the whole run. - stream-parser accepts heartbeat events from the new server-side NDJSON heartbeat. Drops the upfront features multiselect — the agent now drives that through the propose_features tool — and threads --features through as an override instead. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7499355. Configure here.
| // Stream errored mid-read (idle timeout / network blip). Same | ||
| // recovery as a clean close: ask the run for its status and let | ||
| // `handleStreamClosure` decide. | ||
| } |
There was a problem hiding this comment.
Stream catch block swallows user cancellation errors
High Severity
The bare catch {} in consumeStream swallows all errors from readNdjsonStream, including WizardCancelledError. When a user presses Ctrl+C during an interactive prompt, the cancellation error is correctly re-thrown through performActionRequest and handleEvent, but then silently caught here. Execution falls through to handleStreamClosure, which sees no terminal state and reconnects — effectively ignoring the user's cancellation. The catch needs to re-throw WizardCancelledError before falling through to reconnect logic.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7499355. Configure here.
| const ALIASES: Record<string, SelectableFeatureId> = { | ||
| errors: "tracing", // backward-compat | ||
| performance: "tracing", |
There was a problem hiding this comment.
Bug: The --features errors flag incorrectly enables tracing (performance monitoring) due to a backward-compatibility alias, instead of being a no-op for the already-implicit errorMonitoring.
Severity: MEDIUM
Suggested Fix
Remove the alias mapping errors to tracing. The normaliseFromFlag function should be updated to either silently ignore the errors feature flag or warn the user that the flag is redundant. This would prevent the silent, unintended activation of the performance tracing feature.
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/select-features.ts#L109-L111
Potential issue: The `ALIASES` constant in `select-features.ts` incorrectly maps the
`errors` feature to `tracing`. When a user provides the `--features errors` flag,
intending to configure error monitoring, the system silently enables performance tracing
instead. This occurs because the `normaliseFromFlag` function resolves `errors` to
`tracing`. Since error monitoring is always implicitly enabled, the `errors` flag should
ideally be a no-op or trigger a warning, not enable an unrelated feature like
performance tracing.


Companion to getsentry/cli-init-api#114 — the server-side rewrite. Read the server PR first; this one focuses on the client side.
TL;DR
Replaces the old Mastra-driven CLI flow (
wizard-runner.ts,workflow-inputs.ts) with a thin NDJSON consumer that talks to the new Nitro server. The CLI is now:GET /api/init/{runId}/stream),All AI work happens server-side. Feature selection moves from a hardcoded upfront prompt to an agent-driven
propose_featurescall.Glossary (reviewers new to this stack)
/api/init/{runId}/streamand dispatches events bytype.list-dir,apply-patchset, …), prompt the user (multi-select, confirm), etc. Each action has a uniqueactionId; the CLI POSTs the result back to/api/init/actions/{actionId}.action_requestevent on the stream means "run this locally and post the result back".Architecture (CLI viewpoint)
sequenceDiagram participant User participant CLI as init-runner.ts participant Server as Nitro server participant Loop as stream + dispatch loop User->>CLI: sentry init [./dir] [--features ...] CLI->>CLI: preflight (auth, org, team, project, DSN) CLI->>Server: POST /api/init {input} Server-->>CLI: 202 {runId} CLI->>Server: GET /api/init/{runId}/stream loop NDJSON events Server-->>Loop: status / action_request / summary / done alt status Loop->>User: spinner.message(...) else action_request kind=tool Loop->>Loop: executeTool(name, params, cwd) Loop->>Server: POST /api/init/actions/{actionId} {ok, output} else action_request kind=prompt Loop->>User: clack multiselect / confirm Loop->>Server: POST /api/init/actions/{actionId} {features} else summary Loop->>Loop: state.finalOutput = output else done Loop->>User: formatResult / formatError end endKey files
src/lib/init/init-runner.tswizard-runner.ts.src/lib/init/transport.tsfetchhelpers:startInit,openInitStream,resumeInitAction.src/lib/init/stream-parser.tsreadNdjsonStream+ zod-validatedInitEventtypes.src/lib/init/interactive.tsprompt_requestevents to clack multiselect / select / confirm.src/lib/init/select-features.tsFEATURE_LABELS+normaliseFromFlagfor the--featuresoverride. No upfront prompt anymore.src/lib/init/ensure-project.tssrc/lib/init/types.tsInitEvent,InitStartInput,InitActionRequestEvent, etc. — wire-format types shared with the server.Code snippets
1. Entry: starting the run
There is no upfront feature prompt. The agent decides which features apply after inspecting the repo.
--featuresremains an override for CI / non-interactive use.2. The stream loop with reconnect
Bun/undici's
fetchbody has an idle timeout that kills long-lived streams. We accept that and reconnect at the samestartIndexso we resume mid-run without dropping events.3. Dispatching an action
executeToolreuses the existing.cli/tools (read, glob, run-command, apply-patchset, etc.) so the agent's MCP calls map 1:1 to functions we already trust.4. The agent-driven feature picker (no upfront prompt)
The
availableFeatureslist comes from the server'spropose_featuresMCP call — the agent has already filtered features that don't apply (e.g. no Session Replay on a server-only Node app). The CLI just renders labels.UX before / after
Before (PR base)
The agent then has to ignore the user's irrelevant picks.
After (this PR)
Only the features that actually apply to a Next.js app appear. AI Monitoring and Cron Monitoring are absent because the agent didn't see signals for them.
Removed
src/lib/init/wizard-runner.ts(623 lines) — the old Mastra/D1-aware runner.src/lib/init/workflow-inputs.ts(152 lines) — Mastra-specific input shaping.test/lib/init/wizard-runner.test.ts(634 lines) — covered the old runner.Added
src/lib/init/init-runner.ts(512 lines) — new entry runner.src/lib/init/transport.ts(224 lines) — fetch + NDJSON helpers.src/lib/init/stream-parser.ts(111 lines) —readNdjsonStream+ zod-validated events.src/lib/init/select-features.ts(151 lines) — labels + flag parser.src/lib/init/ensure-project.ts(135 lines) — preflight project creation.test/lib/init/select-features.test.ts(76 lines) — flag parsing + label rendering.test/lib/init/interactive.test.tsfor the agent-driven multi-select.How to run locally
Requires a running server (see getsentry/cli-init-api#114 for setup).
Test plan
bunx tsc --noEmitclean.bun test test/lib/init/ test/commands/init.test.tsgreen (63 tests).--features tracing,logsand confirm the multiselect is skipped.Made with Cursor