Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
4416b95
feat: add direct signup via headless provisioning endpoint
bird-m Apr 20, 2026
3fd891a
fix: rename extractProjectId to extractAppId in credential-resolution…
cursoragent Apr 20, 2026
2709de7
fix: resolve zone from stored state in runDirectSignupIfRequested for…
cursoragent Apr 20, 2026
1cf0c5f
feat(telemetry): agentic signup attempted event [follow-up to #165] (…
bird-m Apr 22, 2026
77537db
chore: log direct signup success in TUI path
bird-m Apr 22, 2026
88947fe
chore: warn when signup userInfo lacks env with apiKey after retries
bird-m Apr 22, 2026
157e39b
refactor: centralize zone resolution (follow-up to #165) (#208)
bird-m Apr 23, 2026
845633c
refactor: remove wizard-direct-signup feature flag gate
bird-m Apr 23, 2026
205f24d
feat(telemetry): emit browser_fallback_after_signup for TUI post-sign…
bird-m Apr 23, 2026
d1774fc
fix(telemetry): persist tokens before emitting signup attempt event
bird-m Apr 23, 2026
a222198
fix(signup): require explicit --zone for non-TUI direct signup
bird-m Apr 23, 2026
7023c92
refactor(signup): use canonical AmplitudeZone type, drop dead conditi…
bird-m Apr 23, 2026
d5d1dee
refactor(cli): align on --region, keep --zone as alias
bird-m Apr 23, 2026
28cbca2
fix(telemetry): scope signup catch so onSuccess throws don't double-c…
bird-m Apr 23, 2026
b9f79a8
test(cli): exercise real control flow in wrapper_exception test
bird-m Apr 23, 2026
7bab7c7
perf(tui): memoize resolveZone in SlackScreen to avoid per-render dis…
bird-m Apr 23, 2026
9f8b030
perf(tui): extract useResolvedZone hook, apply to CreateProjectScreen
bird-m Apr 23, 2026
ee5f3c1
fix(tui): eliminate render-path disk I/O via readDisk option on resol…
bird-m Apr 23, 2026
9ad482a
Merge remote-tracking branch 'origin/main' into feat/direct-signup-v2
bird-m Apr 23, 2026
0e5f09b
fix(signup): share email regex between yargs coerce and zod schema
bird-m Apr 23, 2026
6e638d1
fix(run): include signup fields + region in runWizard Args type
bird-m Apr 23, 2026
6fe698e
fix(session): guard region against zod parse failure like signup fields
bird-m Apr 23, 2026
c8aa9b7
test: use AGENTIC_SIGNUP_ATTEMPTED_EVENT constant instead of hardcode…
bird-m Apr 23, 2026
26c47fd
fix(signup): wipe prior account entries on signup success (#217)
bird-m Apr 23, 2026
66594c0
Merge remote-tracking branch 'origin/main' into feat/direct-signup-v2
bird-m Apr 23, 2026
0bbb537
perf(zone): skip disk I/O in hot-path zone resolution
bird-m Apr 23, 2026
64258cc
refactor(zone): make resolveZone readDisk option required
bird-m Apr 23, 2026
e771a17
fix(direct-signup): surface 4xx client errors with HTTP meaning
bird-m Apr 23, 2026
02d173b
fix(zone): drop redundant readDisk at TUI auth call site
bird-m Apr 24, 2026
16d86a1
feat(signup): add SignupFullName and SignupEmail screens to flow
bird-m Apr 24, 2026
b38eaf6
feat(signup): add setSignupEmail and setSignupFullName store setters
bird-m Apr 24, 2026
3f4065c
feat(signup): add SignupFullNameScreen for interactive --signup
bird-m Apr 24, 2026
ce79def
feat(signup): add SignupEmailScreen for interactive --signup
bird-m Apr 24, 2026
99bf578
fix(signup): rename screen tests to .ts so vitest picks them up
bird-m Apr 24, 2026
df7d3b3
feat(signup): register SignupFullName and SignupEmail screens
bird-m Apr 24, 2026
0fe5add
test(signup): add router cases for SignupFullName and SignupEmail
bird-m Apr 24, 2026
8d6d8bb
test(signup): add flow invariants for signup field collection
bird-m Apr 24, 2026
7d89348
feat(signup): add promptForMissingSignupFields helper for classic mode
bird-m Apr 24, 2026
4910c4c
feat(signup): prompt for missing fields in classic mode
bird-m Apr 24, 2026
4f47f4c
test(signup): BDD coverage for --signup missing-field prompts
bird-m Apr 24, 2026
5db1d23
feat(direct-signup): parse needs_information response arm
bird-m Apr 24, 2026
2eff0a3
feat(signup-or-auth): return discriminated union, add needs_informati…
bird-m Apr 24, 2026
265299b
feat(session): add signupRequiredFields, signupAuth, signupAbandoned
bird-m Apr 24, 2026
a88bec2
feat(tui): add signup store setters
bird-m Apr 24, 2026
2872219
feat(signup): add SignupEmailScreen
bird-m Apr 24, 2026
cb6b01f
feat(signup): add SignupFullNameScreen + shared screen test helper
bird-m Apr 24, 2026
1cf9975
feat(signup): add SigningUpScreen — response-driven POST coordinator
bird-m Apr 24, 2026
5117abb
test(signup): cover SigningUpScreen cleanup on unmount
bird-m Apr 24, 2026
0f2260e
feat(signup): wire signup flow screens into router
bird-m Apr 24, 2026
4778564
test(signup): router cases for signup flow states
bird-m Apr 24, 2026
41889e0
test(signup): flow invariants for signup ceremony
bird-m Apr 24, 2026
de913e6
refactor(bin): read signupAuth from session instead of inline POST
bird-m Apr 24, 2026
12b5a98
refactor(signup): runDirectSignupIfRequested switches on result.kind
bird-m Apr 24, 2026
c7c725c
test(signup): BDD coverage for server-driven signup collection
bird-m Apr 24, 2026
0a22e7b
fix(bin): wait for signup ceremony before OAuth handoff
bird-m Apr 24, 2026
ebce2b5
refactor(signup): collapse signup ceremony into one visual step
bird-m Apr 24, 2026
dadae44
fix(signup): address review feedback on PR #234
bird-m Apr 24, 2026
465859f
refactor(signup): use useResolvedZone in SigningUpScreen
bird-m Apr 24, 2026
a0dc78d
fix(signup): catch performSignupOrAuth throws in SigningUpScreen
bird-m Apr 24, 2026
fc0bc5a
fix(signup): compile-time exhaustiveness on PerformSignupOrAuthResult…
bird-m Apr 25, 2026
5fe5063
feat(direct-signup): parse needs_information response arm
bird-m Apr 24, 2026
4c7c3b8
feat(signup-or-auth): return discriminated union, add needs_informati…
bird-m Apr 24, 2026
05cf6a7
feat(session): add signupRequiredFields, signupAuth, signupAbandoned
bird-m Apr 24, 2026
1649ff7
feat(tui): add signup store setters
bird-m Apr 24, 2026
59c66db
feat(signup): add SignupEmailScreen
bird-m Apr 24, 2026
318d340
feat(signup): add SignupFullNameScreen + shared screen test helper
bird-m Apr 24, 2026
e2803a1
feat(signup): add SigningUpScreen — response-driven POST coordinator
bird-m Apr 24, 2026
13aaab8
test(signup): cover SigningUpScreen cleanup on unmount
bird-m Apr 24, 2026
4517a36
feat(signup): wire signup flow screens into router
bird-m Apr 24, 2026
de4e79f
test(signup): router cases for signup flow states
bird-m Apr 24, 2026
66bbd5f
test(signup): flow invariants for signup ceremony
bird-m Apr 24, 2026
9f29b92
refactor(bin): read signupAuth from session instead of inline POST
bird-m Apr 24, 2026
ddd65a8
refactor(signup): runDirectSignupIfRequested switches on result.kind
bird-m Apr 24, 2026
922a7dd
test(signup): BDD coverage for server-driven signup collection
bird-m Apr 24, 2026
821d3ed
fix(bin): wait for signup ceremony before OAuth handoff
bird-m Apr 24, 2026
5e8ed5a
refactor(signup): collapse signup ceremony into one visual step
bird-m Apr 24, 2026
567f97f
fix(signup): address review feedback on PR #234
bird-m Apr 24, 2026
3448035
refactor(signup): use useResolvedZone in SigningUpScreen
bird-m Apr 24, 2026
b7c1a2f
fix(signup): catch performSignupOrAuth throws in SigningUpScreen
bird-m Apr 24, 2026
f380c1a
fix(signup): compile-time exhaustiveness on PerformSignupOrAuthResult…
bird-m Apr 25, 2026
e4e782a
fix(signup): add flow reverts for signup steps and dedupe store setters
kelsonpw May 1, 2026
dc10f53
fix(signup): wipe stale per-project state on successful direct signup
kelsonpw May 1, 2026
921af9d
refactor(logout): use clearStaleProjectState helper
bird-m Apr 25, 2026
fffe6b6
test(signup): mock execFileSync in clear-stale-project-state.test
bird-m Apr 25, 2026
f91cd2c
test(port-detection): unlink symlink directly to fix Node 23 rmSync
bird-m Apr 25, 2026
751948c
fix(oauth): wipe stale per-project state on fresh-OAuth completion
bird-m Apr 25, 2026
aa5fd03
test: align clear-stale-project-state tests with cache paths
kelsonpw May 1, 2026
6bca966
chore: merge origin/main into signup state-wipe branch
kelsonpw May 1, 2026
c13bc87
chore: merge origin/main
kelsonpw May 1, 2026
6723ff9
chore: merge origin/main and resolve conflicts (signup UX + stale sta…
kelsonpw May 1, 2026
e34c4d6
chore: merge rebase/pr-237 and resolve needs_information stack
kelsonpw May 1, 2026
49a569a
chore: merge consolidated signup stack (favor PR #234/#237)
kelsonpw May 1, 2026
fe3ea1b
fix(signup): classic prompts + remove dead bin merge artifact
kelsonpw May 1, 2026
e41c24a
Merge branch 'main' into followup/signup-missing-fields
kelsonpw May 1, 2026
b2830e1
test(router): use accountCreationFlow for --signup flow resolution tests
kelsonpw May 1, 2026
7ec8356
chore(eslint): lint __tests__/**/*.tsx and *.test.tsx without type-ch…
kelsonpw May 1, 2026
7eac6a0
fix(test): stabilize signup flows and zone-resolution guard
kelsonpw May 1, 2026
4677a2e
fix(flow): route --signup past EmailCapture and collect name before e…
kelsonpw May 1, 2026
8f8370c
Merge branch 'main' into followup/signup-missing-fields
kelsonpw May 1, 2026
84cc822
fix(signup): clear submitted field when server re-requests it
cursoragent May 1, 2026
4517cad
Merge branch 'main' into followup/signup-missing-fields
kelsonpw May 1, 2026
c3479d9
fix: guard against re-prompt loop when server rejects already-sent si…
cursoragent May 1, 2026
46d4087
fix(signup): hydrate TUI auth task from signupAuth and skip duplicate…
kelsonpw May 1, 2026
5bdadbe
Merge branch 'main' into followup/signup-missing-fields
kelsonpw May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export default tseslint.config(
'e2e-tests/**/*.ts',
'**/*.test.js',
'**/*.test.ts',
'**/*.test.tsx',
'**/__tests__/**/*.ts',
'**/__tests__/**/*.tsx',
'**/__tests__/**/*.js',
'features/step-definitions/**/*.ts',
],
Expand Down
84 changes: 84 additions & 0 deletions features/13-direct-signup-needs-information.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Feature: --signup with server-driven field collection
As a user running `npx @amplitude/wizard --signup`
I want the wizard to send what it has and let the server decide what else
is required
So that I only get asked for fields the backend actually needs

# Coverage model. These scenarios exercise the router + session state
# transitions that `SigningUpScreen` drives via the store. We don't boot
# the TUI or mock HTTP — that layer is covered by router/unit tests. The
# BDD here asserts the four user-journey shapes the flow must support:
#
# 1. --signup only: name → email → signing-up → (needs full_name) → name → signing-up
# 2. --signup --email: name → signing-up → (needs full_name) → name → signing-up
# 3. --signup + both fields: straight to signing-up (no collection screens)
# 4. requires_redirect: signing-up → abandoned → Auth
#
# "First POST" / "second POST" are represented by the SigningUpScreen
# being resolved by the router; the screen's useEffect is what actually
# fires `performSignupOrAuth`. We simulate the server response by writing
# its effect back onto the session (setSignupRequiredFields / setSignupAuth
# / setSignupAbandoned), then re-resolve the router.

Scenario: --signup with no other flags — two POSTs, two collection screens
Given the wizard is started with --signup and no email or full name
And the intro is concluded and region is "us"
When the router resolves
Then it should land on the SignupFullName screen
When the user submits the full name "Jane Doe"
And the router resolves
Then it should land on the SignupEmail screen
When the user submits the email "jane@example.com"
And the router resolves
Then it should land on the SigningUp screen
When the first signup POST returns needs_information for "full_name"
And the router resolves
Then it should land on the SignupFullName screen
When the user submits the full name "Jane Doe"
And the router resolves
Then it should land on the SigningUp screen
When the second signup POST returns a success payload
And the router resolves
Then it should advance past SigningUp to Auth

Scenario: --signup --email only — one collection screen, two POSTs
Given the wizard is started with --signup and email "jane@example.com"
And the intro is concluded and region is "us"
When the router resolves
Then it should land on the SignupFullName screen
When the user submits the full name "Jane Doe"
And the router resolves
Then it should land on the SigningUp screen
When the first signup POST returns needs_information for "full_name"
And the router resolves
Then it should land on the SignupFullName screen
When the user submits the full name "Jane Doe"
And the router resolves
Then it should land on the SigningUp screen
When the second signup POST returns a success payload
And the router resolves
Then it should advance past SigningUp to Auth

Scenario: --signup with both email and full name — one POST, no collection screens
Given the wizard is started with --signup, email "jane@example.com", and full name "Jane Doe"
And the intro is concluded and region is "us"
When the router resolves
Then it should land on the SigningUp screen
And the SignupEmail screen should not have been resolved
And the SignupFullName screen should not have been resolved
When the first signup POST returns a success payload
And the router resolves
Then it should advance past SigningUp to Auth

Scenario: requires_redirect — fall through to browser OAuth
Given the wizard is started with --signup and email "existing@acme.com"
And the intro is concluded and region is "us"
When the router resolves
Then it should land on the SignupFullName screen
When the user submits the full name "Existing User"
And the router resolves
Then it should land on the SigningUp screen
When the first signup POST returns requires_redirect
Then session.signupAbandoned becomes true
When the router resolves
Then it should advance past SigningUp to Auth
37 changes: 37 additions & 0 deletions features/13-direct-signup-prompts.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Feature: --signup missing-field prompts in the TUI
As a user invoking the wizard with --signup
I want the TUI to collect any missing fields (full name, email) before Auth
So that I can complete signup without re-running the CLI with extra flags

# The SignupFullName and SignupEmail screens are injected into the Wizard
# flow between RegionSelect and SigningUp (after ToS when required).
# They appear only when --signup is set and the corresponding flag was not
# supplied on the command line. Once both fields are populated and ToS is
# accepted, the router skips collection screens and proceeds to SigningUp.

Scenario: --signup with no email or full-name prompts for full name first, then email
Given the wizard is launched with --signup and no email or full-name
And the intro is concluded and region is selected
Then the router should resolve to the SignupFullName screen
When the user enters their full name
Then the router should resolve to the SignupEmail screen
When the user enters their email
Then the router should resolve to the ToS screen
When the user accepts the Terms of Service for direct signup
Then the router should resolve to the SigningUp screen

Scenario: --signup with email supplied but no full-name prompts for full name only
Given the wizard is launched with --signup and only an email supplied
And the intro is concluded and region is selected
Then the router should resolve to the SignupFullName screen
When the user enters their full name
Then the router should resolve to the ToS screen
When the user accepts the Terms of Service for direct signup
Then the router should resolve to the SigningUp screen

Scenario: --signup with all fields supplied goes straight to SigningUp
Given the wizard is launched with --signup, email, and full-name all supplied
And the intro is concluded and region is selected
Then the router should resolve to the SigningUp screen
And the SignupFullName screen should be skipped
And the SignupEmail screen should be skipped
190 changes: 190 additions & 0 deletions features/step-definitions/direct-signup-needs-information.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
import assert from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { WizardRouter } from '../../src/ui/tui/router.js';
import { Screen, Flow } from '../../src/ui/tui/flows.js';
import {
buildSession,
type WizardSession,
} from '../../src/lib/wizard-session.js';

// ── Shared state ──────────────────────────────────────────────────────────
//
// We mirror the store/router relationship by:
// 1. Building a session with the flag shape `buildSession` produces for
// the given CLI args (signup / signupEmail / signupFullName).
// 2. Calling router.resolve(session) at each "And the router resolves"
// step. Every resolve is recorded in `screensSeen` so we can assert
// things like "SignupEmail was never resolved".
// 3. Simulating the server response by writing its side-effects onto
// the session the same way SigningUpScreen does via the store
// (setSignupRequiredFields / setSignupAuth / setSignupAbandoned).
//
// HTTP is not mocked. The router/session transitions *are* the flow — the
// SigningUpScreen's POST is triggered by the router mounting that screen,
// and the flow predicates in `flows.ts` decide what renders next based on
// what the response wrote back to the session.

let projectDir: string;
let router: WizardRouter;
let session: WizardSession;
let screensSeen: Screen[];

Before(function () {
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ampli-signup-test-'));
router = new WizardRouter(Flow.Wizard);
session = buildSession({ installDir: projectDir });
screensSeen = [];
});

After(function () {
fs.rmSync(projectDir, { recursive: true, force: true });
});

// ── Given ─────────────────────────────────────────────────────────────────

Given(
'the wizard is started with --signup and no email or full name',
function () {
session.accountCreationFlow = true;
session.signupEmail = null;
session.signupFullName = null;
},
);

Given(
'the wizard is started with --signup and email {string}',
function (email: string) {
session.accountCreationFlow = true;
session.signupEmail = email;
session.signupFullName = null;
},
);

Given(
'the wizard is started with --signup, email {string}, and full name {string}',
function (email: string, fullName: string) {
session.accountCreationFlow = true;
session.signupEmail = email;
session.signupFullName = fullName;
},
);

Given(
'the intro is concluded and region is {string}',
function (region: string) {
session.introConcluded = true;
session.region = region as WizardSession['region'];
},
);

// ── When ──────────────────────────────────────────────────────────────────

When('the router resolves', function () {
const resolved = router.resolve(session);
screensSeen.push(resolved);
});

When('the user submits the email {string}', function (email: string) {
// Mirrors store.setSignupEmail — what SignupEmailScreen calls on submit.
session.signupEmail = email;
});

When('the user submits the full name {string}', function (fullName: string) {
// Mirrors store.setSignupFullName (trimmed).
session.signupFullName = fullName.trim();
});

When(
'the first signup POST returns needs_information for {string}',
function (field: string) {
// Mirrors what SigningUpScreen does on a needs_information response
// with a known, unmet field: store.setSignupRequiredFields([...]),
// which both records the requirement and nulls the matching session
// value so the flow re-resolves back to the collection screen.
session.signupRequiredFields = [field];
if (field === 'full_name') {
session.signupFullName = null;
}
},
);

When('the second signup POST returns a success payload', function () {
applySuccess();
});

When('the first signup POST returns a success payload', function () {
applySuccess();
});

When('the first signup POST returns requires_redirect', function () {
// Mirrors SigningUpScreen's response handler on the requires_redirect
// arm: setSignupAbandoned(true) so the router falls through to
// AuthScreen.
session.signupAbandoned = true;
});

// ── Then ──────────────────────────────────────────────────────────────────

Then('it should land on the SignupEmail screen', function () {
const last = screensSeen[screensSeen.length - 1];
assert.strictEqual(
last,
Screen.SignupEmail,
`Expected SignupEmail, got ${last}`,
);
});

Then('it should land on the SigningUp screen', function () {
const last = screensSeen[screensSeen.length - 1];
assert.strictEqual(last, Screen.SigningUp, `Expected SigningUp, got ${last}`);
});

Then('it should land on the SignupFullName screen', function () {
const last = screensSeen[screensSeen.length - 1];
assert.strictEqual(
last,
Screen.SignupFullName,
`Expected SignupFullName, got ${last}`,
);
});

Then('it should advance past SigningUp to Auth', function () {
const last = screensSeen[screensSeen.length - 1];
assert.strictEqual(last, Screen.Auth, `Expected Auth, got ${last}`);
});

Then('the SignupEmail screen should not have been resolved', function () {
assert.ok(
!screensSeen.includes(Screen.SignupEmail),
`SignupEmail was unexpectedly resolved; saw: ${screensSeen.join(', ')}`,
);
});

Then('the SignupFullName screen should not have been resolved', function () {
assert.ok(
!screensSeen.includes(Screen.SignupFullName),
`SignupFullName was unexpectedly resolved; saw: ${screensSeen.join(', ')}`,
);
});

Then('session.signupAbandoned becomes true', function () {
assert.strictEqual(session.signupAbandoned, true);
});

// ── Helpers ───────────────────────────────────────────────────────────────

function applySuccess(): void {
// Mirrors SigningUpScreen's success arm: store.setSignupAuth(result).
session.signupAuth = {
kind: 'success',
idToken: 'id-token',
accessToken: 'access-token',
refreshToken: 'refresh-token',
zone: 'us',
userInfo: null,
dashboardUrl: null,
};
}
Loading
Loading