feat(signup): prompt for missing fields instead of requiring all flags#220
feat(signup): prompt for missing fields instead of requiring all flags#220bird-m wants to merge 11 commits intofeat/direct-signup-v2from
Conversation
Adds two new Screen enum values and two flow entries in the SUSI pipeline (Flow.Wizard), placed after RegionSelect and before Auth. The `show` predicates gate the screens on `session.signup && <field> === null`, so they only render when --signup was passed without the corresponding flag. `isComplete` advances past them once the session field is written (by the screens themselves in follow-up tasks). This commit intentionally leaves the screens themselves, store setters, and screen-registry wiring for follow-up commits. `pnpm tsc --noEmit` may report missing registry cases until those land — any other type errors are regressions.
Enables the upcoming SignupFullName / SignupEmail TUI screens to write into WizardSession without each touching the nanostore directly. setSignupFullName trims whitespace at the setter so screens don't need to handle it; setSignupEmail expects a pre-trimmed value (the screen does regex validation on the trimmed string). Both setters emitChange so the router re-evaluates the flow pipeline and advances past the signup screens as soon as each field is filled.
Collects the user's full name when --signup is passed without --full-name in interactive TUI mode. Validates non-empty after trim, shows an inline error on bad input, writes the trimmed value to session.signupFullName via the store setter. ink-testing-library is not in the project's deps, so the test file exercises the submit-handler logic and store integration directly rather than rendering the component. All four validation cases are covered: empty input, whitespace-only input, valid input (trimmed), and single-word names. The screen is not yet registered in screen-registry.tsx — that wiring lands in a follow-up commit.
Collects the email address when --signup is passed without --email in interactive TUI mode. Validates using the shared EMAIL_REGEX (the same regex used by yargs coerce and the zod schema in wizard-session), shows an inline error on invalid input, writes the trimmed value via store.setSignupEmail. Mirrors SignupFullNameScreen's structure so the two screens stay symmetric. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vitest's include glob is `src/**/*.test.ts` — .test.tsx files were silently invisible, so the Task 3 + Task 4 tests never ran under `pnpm test`. Neither file uses JSX (the submit handler is exercised via a small helper, not via render), so .ts is the correct extension and matches the project's established convention (no other .test.tsx files exist in src/).
Wires the two new screen components into the TUI screen-registry so the router can resolve them when the flow pipeline produces Screen.SignupFullName or Screen.SignupEmail. Closes out the screen-registry type error that Tasks 1-4 knowingly left open.
Adds four parameterized cases to router.test.ts covering the signup resolution paths: signup=true routes to SignupFullName when name is missing, to SignupEmail when name is set but email is missing, and past both screens when all fields are populated. signup=false skips both screens entirely — protects against a regression where a non-signup user would get trapped in the new collection screens.
Locks in two routing invariants the new SignupFullName/SignupEmail
screens depend on:
1. signup=true with signupEmail=null never resolves to Auth (the
flow must stop at one of the collection screens first).
2. signup=true with all three signup fields populated never resolves
to SignupFullName or SignupEmail (the flow must advance past the
collection screens).
These protect against a future flow-ordering change silently dropping
a collection screen or trapping a fully-configured signup user on one.
Classic-mode equivalent of the TUI SignupFullName/SignupEmail screens. When --signup is passed without --region/--full-name/--email in classic mode, this helper prompts interactively via @inquirer/prompts for each missing field. Validates full name as non-empty after trim, email via the shared EMAIL_REGEX. Trims both string fields before writing to the session. Task 9 wires this into runDirectSignupIfRequested behind a canPrompt gate so agent/CI paths stay non-interactive. Also adds signup-prompt.ts to the zone-resolution invariant allowlist: it legitimately gates on session.region === null before prompting and writes back the user's selection (same pattern as bin.ts).
runDirectSignupIfRequested now takes a canPrompt option controlling whether it may run interactive prompts. Classic mode passes true and calls promptForMissingSignupFields to collect missing region/name/email before the network call; agent and CI pass false and keep the existing hard-fail behavior on missing flags. With canPrompt=true the subsequent tryResolveZone guard is effectively unreachable for missing-region in classic (the prompt already filled it), but the guard stays in place so agent/CI fail fast without regressing their contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three scenarios:
1. --signup with no other flags: prompts for full name, then email,
then lands on Auth
2. --signup --email <...> but no --full-name: prompts for full name
only, then lands on Auth
3. --signup with all flags populated: no prompts, goes straight to
Auth
All scenarios drive the router directly against session state — the same
pattern used by create-project.steps.ts and susi-flow.steps.ts. Classic-mode
prompt coverage is at the unit level in
src/utils/__tests__/signup-prompt.test.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
|
|
||
| const handleSubmit = (value: string) => { | ||
| const trimmed = value.trim(); | ||
| if (!EMAIL_REGEX.test(trimmed)) { |
There was a problem hiding this comment.
let's use zod here instead?
| What email should we use for your new Amplitude account? | ||
| </Text> | ||
| <Text color={Colors.muted}> | ||
| We'll send a verification link here after setup. |
There was a problem hiding this comment.
this is not necessarily true. We should strip it
| What name should we use for your new Amplitude account? | ||
| </Text> | ||
| <Text color={Colors.muted}> | ||
| You can change this later in your account settings. |
There was a problem hiding this comment.
no need for this follow up
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Inconsistent trimming between sibling store setters
- Updated
setSignupEmailto trim its input matchingsetSignupFullName, and updated the corresponding store test to assert trimming behavior.
- Updated
Or push these changes by commenting:
@cursor push 2dce0a449d
Preview (2dce0a449d)
diff --git a/src/ui/tui/__tests__/store.test.ts b/src/ui/tui/__tests__/store.test.ts
--- a/src/ui/tui/__tests__/store.test.ts
+++ b/src/ui/tui/__tests__/store.test.ts
@@ -1036,10 +1036,10 @@
// ── Signup field setters ─────────────────────────────────────────
describe('WizardStore.setSignupEmail', () => {
- it('writes the email onto session.signupEmail', () => {
+ it('writes the trimmed email onto session.signupEmail', () => {
const store = createStore();
expect(store.session.signupEmail).toBeNull();
- store.setSignupEmail('foo@example.com');
+ store.setSignupEmail(' foo@example.com ');
expect(store.session.signupEmail).toBe('foo@example.com');
});
});
diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts
--- a/src/ui/tui/store.ts
+++ b/src/ui/tui/store.ts
@@ -288,7 +288,7 @@
}
setSignupEmail(email: string): void {
- this.$session.setKey('signupEmail', email);
+ this.$session.setKey('signupEmail', email.trim());
this.emitChange();
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 4f47f4c. Configure here.
| setSignupEmail(email: string): void { | ||
| this.$session.setKey('signupEmail', email); | ||
| this.emitChange(); | ||
| } |
There was a problem hiding this comment.
Inconsistent trimming between sibling store setters
Low Severity
setSignupFullName defensively trims its input via name.trim(), but the sibling setSignupEmail stores the value as-is without trimming. Both current callers pre-trim, so this is safe today, but the inconsistency between two otherwise identical signup-field setters sets a misleading precedent — a future caller of setSignupEmail may reasonably assume the store normalizes whitespace (as setSignupFullName does), leading to emails with leading/trailing spaces reaching the signup endpoint.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4f47f4c. Configure here.



Summary
When
--signupis passed without all required fields, interactive TUI and classic modes now prompt for the missing ones instead of hard-failing. Agent and CI modes keep today's contract (hard-fail on missing flags — no human to prompt).SignupFullNameScreen,SignupEmailScreen) slot into the SUSI flow afterRegionSelect. They're shown only when--signupis set and the corresponding flag is missing.promptForMissingSignupFieldsuses@inquirer/promptsto ask for any missingregion,signupFullName, orsignupEmail.runDirectSignupIfRequestedgains acanPromptoption. Classic passestrue; agent and CI passfalseand keep the existing hard-fail behavior on missing flags.tryResolveZoneguard still exits withAUTH_REQUIREDwhen region is missing in non-interactive modes.Test plan
promptForMissingSignupFields— all-missing / partial / no-op / email validator / name validator / trimming--agent --signup→ exits withAUTH_REQUIRED(3) andauth_requiredNDJSON event--ci --signup→ exits non-zero with clear error messagepnpm try --signup(TUI) — verify screens render, submit advancespnpm try --classic --signup— verify inquirer prompts run in orderSpec
~/repos/docs/snippets/signup-prompt-missing-fields-design.mdOut of scope
A "stored user will be cleared — continue?" confirmation before direct signup. Followup captured in memory (
project_signup_wipe_confirmation_followup).Note
Medium Risk
Changes the
--signupflow and routing around authentication, adding new interactive prompts/screens that can affect how users reach Auth and when network signup is attempted. Agent/CI remain non-interactive, but classic/TUI behavior changes could introduce regressions in signup gating and region selection.Overview
Interactive
--signupno longer requires all flags up front: classic mode now prompts for missingregion,signupFullName, andsignupEmailvia a newpromptForMissingSignupFields, and the CLI’srunDirectSignupIfRequestedgains anopts.canPromptswitch (true for classic; false for agent/CI).The TUI wizard flow inserts two new screens,
SignupFullNameandSignupEmail, betweenRegionSelectandAuth, with newWizardStoresetters and validation; routing/tests are updated to ensure Auth is unreachable until required signup fields are collected and that screens are skipped when fields are already provided.Reviewed by Cursor Bugbot for commit 4f47f4c. Bugbot is set up for automated code reviews on this repo. Configure here.