Skip to content

feat(signup): prompt for missing fields instead of requiring all flags#220

Open
bird-m wants to merge 11 commits intofeat/direct-signup-v2from
followup/signup-missing-fields
Open

feat(signup): prompt for missing fields instead of requiring all flags#220
bird-m wants to merge 11 commits intofeat/direct-signup-v2from
followup/signup-missing-fields

Conversation

@bird-m
Copy link
Copy Markdown
Collaborator

@bird-m bird-m commented Apr 24, 2026

Summary

When --signup is 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).

  • TUI: two new screens (SignupFullNameScreen, SignupEmailScreen) slot into the SUSI flow after RegionSelect. They're shown only when --signup is set and the corresponding flag is missing.
  • Classic: new helper promptForMissingSignupFields uses @inquirer/prompts to ask for any missing region, signupFullName, or signupEmail.
  • runDirectSignupIfRequested gains a canPrompt option. Classic passes true; agent and CI pass false and keep the existing hard-fail behavior on missing flags.
  • Agent/CI behavior unchanged: the tryResolveZone guard still exits with AUTH_REQUIRED when region is missing in non-interactive modes.

Test plan

  • Unit tests for both TUI screens (validation + session writes)
  • Unit tests for promptForMissingSignupFields — all-missing / partial / no-op / email validator / name validator / trimming
  • Router parameterized cases for each signup field combination
  • Flow-invariants property tests (fast-check) — signup gate + Auth unreachable until fields filled
  • BDD scenarios for interactive TUI path (no flags / partial flags / all flags)
  • Automated smoke of --agent --signup → exits with AUTH_REQUIRED (3) and auth_required NDJSON event
  • Automated smoke of --ci --signup → exits non-zero with clear error message
  • Manual smoke of pnpm try --signup (TUI) — verify screens render, submit advances
  • Manual smoke of pnpm try --classic --signup — verify inquirer prompts run in order

Spec

~/repos/docs/snippets/signup-prompt-missing-fields-design.md

Out 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 --signup flow 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 --signup no longer requires all flags up front: classic mode now prompts for missing region, signupFullName, and signupEmail via a new promptForMissingSignupFields, and the CLI’s runDirectSignupIfRequested gains an opts.canPrompt switch (true for classic; false for agent/CI).

The TUI wizard flow inserts two new screens, SignupFullName and SignupEmail, between RegionSelect and Auth, with new WizardStore setters 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.

bird-m and others added 11 commits April 23, 2026 18:55
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>
@bird-m bird-m requested a review from a team as a code owner April 24, 2026 03:35
@github-actions
Copy link
Copy Markdown
Contributor

🧙 Wizard CI

Run 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:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci django
  • /wizard-ci fastapi
  • /wizard-ci flask
  • /wizard-ci javascript-node
  • /wizard-ci javascript-web
  • /wizard-ci next-js
  • /wizard-ci python
  • /wizard-ci react-router
  • /wizard-ci vue

Test an individual app:

  • /wizard-ci django/django3-saas
  • /wizard-ci fastapi/fastapi3-ai-saas
  • /wizard-ci flask/flask3-social-media
Show more apps
  • /wizard-ci javascript-node/express-todo
  • /wizard-ci javascript-node/fastify-blog
  • /wizard-ci javascript-node/hono-links
  • /wizard-ci javascript-node/koa-notes
  • /wizard-ci javascript-node/native-http-contacts
  • /wizard-ci javascript-web/saas-dashboard
  • /wizard-ci next-js/15-app-router-saas
  • /wizard-ci next-js/15-app-router-todo
  • /wizard-ci next-js/15-pages-router-saas
  • /wizard-ci next-js/15-pages-router-todo
  • /wizard-ci python/meeting-summarizer
  • /wizard-ci react-router/react-router-v7-project
  • /wizard-ci react-router/rrv7-starter
  • /wizard-ci react-router/saas-template
  • /wizard-ci react-router/shopper
  • /wizard-ci vue/movies

Results will be posted here when complete.


const handleSubmit = (value: string) => {
const trimmed = value.trim();
if (!EMAIL_REGEX.test(trimmed)) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this follow up

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Inconsistent trimming between sibling store setters
    • Updated setSignupEmail to trim its input matching setSignupFullName, and updated the corresponding store test to assert trimming behavior.

Create PR

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.

Comment thread src/ui/tui/store.ts
setSignupEmail(email: string): void {
this.$session.setKey('signupEmail', email);
this.emitChange();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f47f4c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant