"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultCommand = void 0;
const helpers_1 = require("./helpers");
const context_1 = require("./context");
const environment_1 = require("../utils/environment");
const exit_codes_1 = require("../lib/exit-codes");
const orchestrator_context_1 = require("../utils/orchestrator-context");
const wizard_session_1 = require("../lib/wizard-session");
/**
 * Load `--context-file` (or `AMPLITUDE_WIZARD_CONTEXT`) and stamp the
 * resulting string onto `session.orchestratorContext`. Centralized here
 * so all four mode branches (agent / CI / classic / TUI) share one
 * read+validate path and emit one consistent error envelope.
 *
 * On failure: emits a structured NDJSON event in agent mode, plain
 * stderr in every other mode, then exits with INVALID_ARGS so the
 * orchestrator can distinguish "your config was bad" from "the wizard
 * crashed mid-run". Returns void so callers can `return` immediately.
 */
function applyOrchestratorContext(session, options, emitJson) {
    const path = (0, orchestrator_context_1.resolveOrchestratorContextPath)(options['context-file']);
    if (!path)
        return;
    const result = (0, orchestrator_context_1.loadOrchestratorContext)(path, session.installDir);
    if (!result.ok) {
        if (emitJson) {
            process.stdout.write(JSON.stringify({
                v: 1,
                '@timestamp': new Date().toISOString(),
                type: 'error',
                message: result.message,
                data: {
                    event: 'context_file_failed',
                    reason: result.reason,
                    sourcePath: result.sourcePath,
                },
            }) + '\n');
        }
        else {
            process.stderr.write(`[wizard] --context-file: ${result.message} (${result.sourcePath})\n`);
        }
        process.exit(exit_codes_1.ExitCode.INVALID_ARGS);
    }
    session.orchestratorContext = result.content;
}
/**
 * If `--resume` was passed, look up the per-project checkpoint and
 * restore its fields onto the session BEFORE credentials are
 * resolved. Silently no-ops when no fresh checkpoint exists so
 * orchestrators can pass `--resume` unconditionally on every retry
 * without branching on disk state. Used by the agent / CI mode
 * branches; TUI mode has its own restore path inside the auth task.
 *
 * Telemetry: `loadCheckpoint` emits `progress: checkpoint_loaded` on
 * success — no extra plumbing needed here.
 */
async function maybeResumeFromCheckpoint(session, options) {
    if (!options.resume)
        return;
    const { loadCheckpoint } = await import('../lib/session-checkpoint.js');
    const restored = await loadCheckpoint(session.installDir);
    if (!restored)
        return;
    Object.assign(session, restored);
    // Treat resume as an explicit user action — the intro / region pick
    // screens shouldn't replay just because the checkpoint had stale
    // intro state. (TUI mode handles this with its own
    // `_restoredFromCheckpoint` flag; for headless mode the equivalent
    // is "we skipped past the screens, don't second-guess.")
    session.introConcluded = true;
}
/**
 * Wipe per-project caches that survived a `git reset` (or manual deletion
 * of `<installDir>/ampli.json`). Without this, a stale checkpoint +
 * stale stored API key silently steers the wizard back into the prior
 * project — the user reaches a "Continue with this Amplitude project?"
 * prompt against state they thought they wiped, with no obvious way to
 * re-auth. See `src/lib/self-heal.ts` for the full rationale.
 *
 * Runs synchronously and BEFORE both `maybeResumeFromCheckpoint` and
 * `resolveCredentials` so neither sees the stale data. Safe to call
 * unconditionally — no-ops on fresh and on healthy projects.
 */
async function selfHealIfNeeded(session) {
    const { selfHealStaleProjectState } = await import('../lib/self-heal.js');
    selfHealStaleProjectState(session.installDir);
}
exports.defaultCommand = {
    command: ['$0'],
    describe: 'Run the Amplitude setup wizard',
    builder: (yargs) => yargs.options({
        region: {
            // Required for --auth-onboarding create-account in non-TUI modes: the backend does
            // not route across regions, so the client must POST to the
            // correct provisioning endpoint (us or eu). In the TUI this
            // is covered by the RegionSelect screen; agent/CI/classic
            // have no prompt, so this flag is the only way to signal
            // regional intent on a first-time signup. When provided in
            // TUI mode, pre-populates the region and skips RegionSelect.
            // `--zone` is accepted as an alias for consistency with the
            // `wizard login` subcommand.
            describe: 'data center region for --auth-onboarding create-account in non-interactive modes',
            choices: ['us', 'eu'],
            type: 'string',
            alias: 'zone',
        },
        'force-install': {
            default: false,
            describe: 'install packages even if dependency checks fail',
            type: 'boolean',
        },
        'install-dir': {
            describe: 'project directory to instrument',
            type: 'string',
        },
        integration: {
            describe: 'framework to set up (skips auto-detection)',
            choices: [
                'nextjs',
                'vue',
                'react-router',
                'django',
                'flask',
                'fastapi',
                'javascript_web',
                'javascript_node',
                'python',
            ],
            type: 'string',
        },
        menu: {
            default: false,
            describe: 'show a framework picker instead of auto-detecting',
            type: 'boolean',
        },
        benchmark: {
            default: false,
            describe: 'collect performance metrics during the run',
            type: 'boolean',
        },
        // `--mode` is intentionally hidden. See `docs/internal/agent-mode-flag.md`
        // for the full rationale and the model mapping. Briefly: this is an
        // internal performance / capability knob — not advertised in --help,
        // README, CLAUDE.md, or any agent-facing skill, and not for casual
        // recommendation. The default ('standard') is the only tier most
        // users should ever see.
        mode: {
            default: 'standard',
            choices: ['fast', 'standard', 'thorough'],
            describe: 'internal — see docs/internal/agent-mode-flag.md',
            type: 'string',
            hidden: true,
        },
        agent: {
            default: false,
            describe: 'emit structured NDJSON output for automation',
            type: 'boolean',
        },
        classic: {
            default: false,
            describe: 'use the classic prompt-based UI',
            type: 'boolean',
        },
        'context-file': {
            // Lets an outer agent / CI pipeline inject team conventions
            // ("we use snake_case for events", existing taxonomy snippets,
            // project-specific guidance) WITHOUT modifying any skill content.
            // The file is read once at startup and prepended to every agent
            // turn's system prompt. 64 KB cap; UTF-8; falls back to
            // AMPLITUDE_WIZARD_CONTEXT env var when the flag is omitted.
            describe: 'path to a file whose contents are injected into the inner-agent system prompt',
            type: 'string',
        },
        resume: {
            // In agent / CI mode: load the per-project crash-recovery
            // checkpoint at startup so a rerun skips already-completed
            // setup steps (region pick, framework detection, org/project
            // selection). TUI mode auto-restores; this flag is the
            // headless equivalent. Silently no-ops when no checkpoint
            // exists so orchestrators can pass `--resume` unconditionally
            // without branching on disk state.
            default: false,
            describe: 'load the saved checkpoint and skip already-completed setup steps',
            type: 'boolean',
        },
    }),
    handler: (argv) => {
        const options = { ...argv };
        // --env is redundant with --app-id (each Amplitude env has its own
        // app.id, so the numeric app-id already identifies the env). Keep the
        // flag parseable for legacy scripts, but nudge callers toward --app-id.
        // Surfaced via stderr for interactive/CI; agent mode re-emits it as a
        // structured NDJSON log event once AgentUI exists.
        const envDeprecationWarning = options.env
            ? '[deprecation] --env is redundant with --app-id — prefer ' +
                '--app-id <id> (globally unique, identifies the env directly). ' +
                '--env will be removed in a future release.'
            : null;
        const willRunAsAgent = options.agent || process.env.AMPLITUDE_WIZARD_AGENT === '1';
        if (envDeprecationWarning && !willRunAsAgent) {
            process.stderr.write(`${envDeprecationWarning}\n`);
        }
        // CI mode validation and TTY check
        if (options.agent ||
            process.env.AMPLITUDE_WIZARD_AGENT === '1' ||
            (!options.ci &&
                !options.yes &&
                !options.force &&
                // --auto-approve grants ONLY auto-approve, not writes — so a
                // user who passes only --auto-approve in a non-TTY env should
                // NOT be auto-promoted to agent mode (which would otherwise
                // route through resolveMode's --agent back-compat path).
                !options['auto-approve'] &&
                !options.classic &&
                process.env.AMPLITUDE_WIZARD_CLASSIC !== '1' &&
                (0, environment_1.isNonInteractiveEnvironment)())) {
            // Agent mode (explicit --agent or auto-detected non-TTY)
            if (!options.agent)
                options.agent = true;
            void (async () => {
                const { AgentUI } = await import('../ui/agent-ui.js');
                const agentUI = new AgentUI();
                (0, helpers_1.setUI)(agentUI);
                if (!options.installDir)
                    options.installDir = process.cwd();
                // Surface the --env deprecation warning as a structured log event
                // so orchestrators can parse it (raw stderr would mix with NDJSON).
                if (envDeprecationWarning) {
                    agentUI.log.warn(envDeprecationWarning);
                }
                const session = await (0, helpers_1.buildSessionFromOptions)(options);
                session.agent = true;
                applyOrchestratorContext(session, options, true);
                await selfHealIfNeeded(session);
                await maybeResumeFromCheckpoint(session, options);
                if (!(0, helpers_1.gateAgentSignupArguments)(session, agentUI)) {
                    return;
                }
                // Attempt direct signup before falling through to cached-token
                // resolution. Agent mode has no browser, so a null result continues
                // to resolveNonInteractiveCredentials, which handles cached tokens
                // or exits cleanly with AUTH_REQUIRED.
                await (0, helpers_1.runDirectSignupIfRequested)(session, 'cached-token resolution');
                await (0, helpers_1.resolveNonInteractiveCredentials)(session, options, 'agent', agentUI);
                await (0, helpers_1.lazyRunWizard)(options, session, () => session.additionalFeatureQueue);
            })();
        }
        else if (options.ci || options.yes || options.force) {
            // CI mode: no prompts, auto-select first environment
            (0, helpers_1.setUI)(new helpers_1.LoggingUI());
            if (!options.installDir)
                options.installDir = process.cwd();
            void (async () => {
                const session = await (0, helpers_1.buildSessionFromOptions)(options, { ci: true });
                applyOrchestratorContext(session, options, false);
                await selfHealIfNeeded(session);
                await maybeResumeFromCheckpoint(session, options);
                if (!(0, helpers_1.gateCiSignupAcceptToS)(session)) {
                    return;
                }
                // Attempt direct signup before falling through to cached-token
                // resolution. CI mode has no browser, so a null result continues to
                // resolveNonInteractiveCredentials, which handles cached tokens or
                // exits cleanly with AUTH_REQUIRED.
                await (0, helpers_1.runDirectSignupIfRequested)(session, 'cached-token resolution');
                await (0, helpers_1.resolveNonInteractiveCredentials)(session, options, 'ci');
                await (0, helpers_1.lazyRunWizard)(options, session, () => session.additionalFeatureQueue);
            })();
        }
        else if (options.classic ||
            process.env.AMPLITUDE_WIZARD_CLASSIC === '1') {
            // Classic mode: interactive prompts without the rich TUI
            void (async () => {
                const session = await (0, helpers_1.buildSessionFromOptions)(options);
                applyOrchestratorContext(session, options, false);
                await selfHealIfNeeded(session);
                // Attempt direct signup before falling through to OAuth browser
                // flow. On success, run resolveCredentials so agent-runner's
                // !session.credentials guard skips the OAuth call. On null/failure,
                // classic mode proceeds normally — getOrAskForProjectData calls
                // performAmplitudeAuth, which opens a browser (valid for classic).
                //
                // requireOrgId: false — classic has no AuthScreen to recover from
                // the TUI-only safety check that clears credentials when no org is
                // selected. Without this, a successful signup would get silently
                // cleared and the browser would open anyway, defeating the point.
                await (0, helpers_1.runDirectSignupIfRequested)(session, 'OAuth', async () => {
                    const { resolveCredentials } = await import('../lib/credential-resolution.js');
                    await resolveCredentials(session, { requireOrgId: false });
                });
                await (0, helpers_1.lazyRunWizard)(options, session);
            })();
        }
        else {
            // Interactive TTY: launch the Ink TUI
            void (async () => {
                try {
                    const { startTUI } = await import('../ui/tui/start-tui.js');
                    // Build the session from CLI args BEFORE starting the TUI so the
                    // initial render reflects --install-dir (and any other flag-driven
                    // session fields) instead of the WizardStore's default
                    // `buildSession({})` which falls back to process.cwd(). Without
                    // this, a user who runs `pnpm try --install-dir=/some/app` sees
                    // the wizard's own cwd in the IntroScreen Target line for the
                    // ~seconds it takes to resolve OAuth credentials, until the
                    // `tui.store.session = session` swap below runs.
                    const session = await (0, helpers_1.buildSessionFromOptions)(options);
                    // Loading the orchestrator context BEFORE startTUI so a bad
                    // path fails fast on stderr (no half-rendered TUI to tear
                    // down). The TUI branch keeps NDJSON off — interactive users
                    // get a plain error message they can read.
                    applyOrchestratorContext(session, options, false);
                    const tui = startTUI(context_1.WIZARD_VERSION, undefined, session);
                    // Install the SIGINT handler IMMEDIATELY after starting the TUI.
                    // This handler covers external `kill -INT <pid>` signals. When
                    // the user presses Ctrl+C in the TUI, CtrlCHandler (Ink useInput)
                    // owns that flow instead — Ink puts stdin in raw mode, so Ctrl+C
                    // is delivered as a keypress, not SIGINT.
                    //
                    // Import the shared helper eagerly so it's available when SIGINT
                    // fires — placing this after startTUI but before registering the
                    // handler avoids the TDZ issue that would occur if we referenced
                    // a `const` binding from a later dynamic import.
                    const { performGracefulExit } = await import('../lib/graceful-exit.js');
                    let sigintReceived = false;
                    process.on('SIGINT', () => {
                        if (sigintReceived) {
                            process.exit(130);
                        }
                        sigintReceived = true;
                        performGracefulExit({
                            session: tui.store.session,
                            setCommandFeedback: (msg, ms) => tui.store.setCommandFeedback(msg, ms),
                        });
                    });
                    // Session is already built above (and attached to the store via
                    // startTUI's initialSession arg). Continue mutating it for
                    // checkpoint restoration / credential resolution; the final
                    // `tui.store.session = session` below re-emits the change.
                    // If --api-key was provided, skip the OAuth/TUI auth flow entirely.
                    if (session.apiKey) {
                        const { DEFAULT_HOST_URL } = await import('../lib/constants.js');
                        // See AMPLITUDE_WIZARD_PROXY_BEARER comment in the agent/ci
                        // branch above — same separation applies here.
                        const proxyBearer = process.env.AMPLITUDE_WIZARD_PROXY_BEARER?.trim() ||
                            session.apiKey;
                        session.credentials = {
                            accessToken: proxyBearer,
                            projectApiKey: session.apiKey,
                            host: DEFAULT_HOST_URL,
                            appId: session.appId ?? 0,
                        };
                        session.projectHasData = false;
                    }
                    else {
                        // Pre-populate region + credentials from stored OAuth tokens.
                        const { logToFile } = await import('../utils/debug.js');
                        // Self-heal stale per-project caches BEFORE loading the
                        // checkpoint or resolving credentials. After `git reset`,
                        // `<installDir>/ampli.json` is gone but the per-project
                        // checkpoint + stored API key still point at the prior
                        // project — without this, the wizard silently steers the
                        // user past auth into stale state. See `src/lib/self-heal.ts`.
                        await selfHealIfNeeded(session);
                        // Check for crash-recovery checkpoint
                        const { loadCheckpoint } = await import('../lib/session-checkpoint.js');
                        const checkpoint = await loadCheckpoint(session.installDir);
                        if (checkpoint) {
                            Object.assign(session, checkpoint);
                            session.introConcluded = false;
                            session._restoredFromCheckpoint = true;
                            logToFile('[bin] restored session from crash-recovery checkpoint');
                        }
                        // Resolve credentials using shared logic (token refresh,
                        // env auto-select, pendingOrgs population)
                        const { resolveCredentials } = await import('../lib/credential-resolution.js');
                        await resolveCredentials(session);
                        // If resolveCredentials silently picked an org/project from
                        // disk (returning-user path), require an explicit confirmation
                        // before routing the run against it. The Auth flow gate reads
                        // this flag — without it we could silently target a project
                        // the user didn't intend (especially after upgrading from an
                        // old wizard version where project IDs may have rolled over).
                        // Skipped in --ci / --agent mode where there is no interactive
                        // confirm step and the user has already opted into automation.
                        if (session.credentials !== null &&
                            !session.ci &&
                            !session.agent &&
                            (session.selectedOrgId || session.selectedProjectId)) {
                            session.requiresAccountConfirmation = true;
                        }
                        // Resolve org/project display names so /whoami shows them.
                        // Also extracts the numeric analytics project ID for MCP event detection.
                        // Fire-and-forget so it doesn't block startup.
                        // Hydrate org/project display names after credential
                        // resolution succeeds. Gate on credentials (not region) because
                        // resolveCredentials no longer cache-writes session.region;
                        // gating on region would silently skip hydration for returning
                        // agent-mode users whose zone comes from storedUser, not an
                        // explicit flag.
                        if (session.credentials && session.selectedOrgId) {
                            const { getStoredUser, getStoredToken } = await import('../utils/ampli-settings.js');
                            const { fetchAmplitudeUser, extractAppId } = await import('../lib/api.js');
                            const { resolveZone } = await import('../lib/zone-resolution.js');
                            const { DEFAULT_AMPLITUDE_ZONE } = await import('../lib/constants.js');
                            const storedUser = getStoredUser();
                            const realUser = storedUser && storedUser.id !== 'pending' ? storedUser : null;
                            // Fire-and-forget user refresh during CLI startup: session may
                            // not yet have region set, so fall back to disk tiers.
                            const zone = resolveZone(session, DEFAULT_AMPLITUDE_ZONE, {
                                readDisk: true,
                            });
                            const storedToken = realUser
                                ? getStoredToken(realUser.id, realUser.zone)
                                : getStoredToken(undefined, zone);
                            logToFile(`[bin] fire-and-forget: storedToken=${storedToken ? 'found' : 'null'}`);
                            if (storedToken) {
                                fetchAmplitudeUser(storedToken.idToken, zone)
                                    .then((userInfo) => {
                                    let changed = false;
                                    if (userInfo.email && !session.userEmail) {
                                        session.userEmail = userInfo.email;
                                        changed = true;
                                    }
                                    if (userInfo.email) {
                                        helpers_1.analytics.setDistinctId(userInfo.email);
                                        helpers_1.analytics.identifyUser({
                                            email: userInfo.email,
                                            org_id: session.selectedOrgId ?? undefined,
                                            org_name: session.selectedOrgName ?? undefined,
                                            project_id: session.selectedProjectId ?? undefined,
                                            project_name: session.selectedProjectName ?? undefined,
                                            app_id: session.selectedAppId,
                                            env_name: session.selectedEnvName,
                                            region: zone,
                                            integration: session.integration,
                                        });
                                    }
                                    if (session.selectedOrgId) {
                                        // Fall back to the first org if the stored ID is stale
                                        // (e.g. session checkpoint from a different account).
                                        const org = userInfo.orgs.find((o) => o.id === session.selectedOrgId) ?? userInfo.orgs[0];
                                        logToFile(`[bin] fire-and-forget: orgs=${userInfo.orgs
                                            .map((o) => o.id)
                                            .join(',')}, looking for ${session.selectedOrgId}, using=${org?.id ?? 'none'}`);
                                        if (org) {
                                            session.selectedOrgName = org.name;
                                            changed = true;
                                            // Fall back to the first project if the stored ID is stale.
                                            const project = session.selectedProjectId
                                                ? org.projects.find((p) => p.id === session.selectedProjectId) ?? org.projects[0]
                                                : org.projects[0];
                                            if (project) {
                                                session.selectedProjectName = project.name;
                                                // Extract the Amplitude app ID from the lowest-rank environment.
                                                const appId = extractAppId(project);
                                                logToFile(`[bin] app ID resolution: environments=${project.environments?.length ?? 'null'}, appId=${appId}`);
                                                if (appId)
                                                    session.selectedAppId = appId;
                                            }
                                            else {
                                                logToFile(`[bin] app ID resolution: no projects in org ${org.id}`);
                                            }
                                        }
                                    }
                                    if (changed && tui.store.session === session) {
                                        tui.store.emitChange();
                                    }
                                })
                                    .catch((err) => {
                                    logToFile(`[bin] fire-and-forget fetchAmplitudeUser failed: ${err instanceof Error ? err.message : String(err)}`);
                                });
                            }
                        }
                    }
                    tui.store.session = session;
                    // Load event plan from a previous run (if it exists) so the
                    // Events tab is available immediately on returning runs.
                    // Import from the lightweight parser module (not agent-interface)
                    // so we don't pull in the Claude Agent SDK / UI singleton here.
                    try {
                        const { readLocalEventPlan } = await import('../lib/event-plan-parser.js');
                        const events = readLocalEventPlan(session.installDir);
                        if (events.length > 0) {
                            tui.store.setEventPlan(events.filter((e) => e.name.trim().length > 0));
                        }
                    }
                    catch {
                        // No event plan file yet — that's fine
                    }
                    // Initialize Amplitude Experiment feature flags (non-blocking).
                    const { initFeatureFlags } = await import('../lib/feature-flags.js');
                    await initFeatureFlags().catch(() => {
                        // Flag init failure is non-fatal — all flags default to off
                    });
                    // Apply SDK-level opt-out based on feature flags
                    helpers_1.analytics.applyOptOut();
                    const installDir = session.installDir ?? process.cwd();
                    // Verbose startup diagnostics — always written to the log file;
                    // visible in the RunScreen "Logs" tab.
                    if (session.verbose || session.debug) {
                        const { enableDebugLogs, logToFile } = await import('../utils/debug.js');
                        enableDebugLogs();
                        logToFile('[verbose] Amplitude Wizard starting');
                        logToFile(`[verbose] node          : ${process.version}`);
                        logToFile(`[verbose] process.cwd() : ${process.cwd()}`);
                        logToFile(`[verbose] installDir    : ${installDir}`);
                        logToFile(`[verbose] platform      : ${process.platform}`);
                        logToFile(`[verbose] argv          : ${process.argv.join(' ')}`);
                    }
                    // ── OAuth + account setup ──────────────────────────────
                    // Runs concurrently with framework detection while AuthScreen shows.
                    // When OAuth completes, store.setOAuthComplete() triggers the
                    // AuthScreen SUSI pickers (org → workspace → API key).
                    // AuthScreen calls store.setCredentials() when done, advancing the
                    // router past Auth → RegionSelect → DataSetup → to IntroScreen.
                    const authTask = (async () => {
                        // If we silently resolved credentials from disk on a returning
                        // run, the user is currently looking at the account-confirm
                        // step inside AuthScreen. Wait for them to either accept (the
                        // task can short-circuit and exit) or reject (credentials get
                        // cleared so we proceed into the OAuth pipeline below). Without
                        // this wait the task would early-return and leave the user
                        // stranded if they pressed [C] to change project.
                        if (tui.store.session.requiresAccountConfirmation) {
                            await new Promise((resolve) => {
                                if (!tui.store.session.requiresAccountConfirmation) {
                                    resolve();
                                    return;
                                }
                                const unsub = tui.store.subscribe(() => {
                                    if (!tui.store.session.requiresAccountConfirmation) {
                                        unsub();
                                        resolve();
                                    }
                                });
                            });
                        }
                        // Skip the full OAuth + SUSI flow when credentials were pre-populated
                        // from ~/.ampli.json + the saved API key (returning user).
                        if (tui.store.session.credentials !== null)
                            return;
                        try {
                            const { ampliConfigExists } = await import('../lib/ampli-config.js');
                            const { performAmplitudeAuth } = await import('../utils/oauth.js');
                            const { fetchAmplitudeUser } = await import('../lib/api.js');
                            const { DEFAULT_AMPLITUDE_ZONE } = await import('../lib/constants.js');
                            const { storeToken, getStoredToken } = await import('../utils/ampli-settings.js');
                            // Wait for the user to dismiss the welcome screen AND pick a
                            // region before opening the OAuth URL. This ensures the logo
                            // and intro are visible before the browser opens.
                            //
                            // The `!regionForced` guard handles the IntroScreen "Change
                            // region" path: a returning user already has `region` set
                            // from disk, so picking "Change region" calls
                            // setRegionForced() (regionForced=true) + concludeIntro()
                            // (introConcluded=true) — both fields the original wait
                            // condition watched. Without the guard the auth task would
                            // wake up and open the browser against the OLD region while
                            // the user is still picking the new one on RegionSelect.
                            // setRegion() flips regionForced back to false once the new
                            // region is chosen, releasing the wait against the right zone.
                            //
                            // For create-account onboarding, also wait for ToS acceptance. Otherwise the
                            // auth task races past EmailCapture / ToS and opens the OAuth
                            // browser before the user has filled in their email or
                            // accepted the terms — which is the entire point of the
                            // signup flow. See `isAuthTaskGateReady` for the full
                            // predicate (kept testable, since the inline version
                            // silently regressed when its conditions drifted).
                            await new Promise((resolve) => {
                                if ((0, helpers_1.isAuthTaskGateReady)(tui.store.session)) {
                                    resolve();
                                    return;
                                }
                                const unsub = tui.store.subscribe(() => {
                                    if ((0, helpers_1.isAuthTaskGateReady)(tui.store.session)) {
                                        unsub();
                                        resolve();
                                    }
                                });
                            });
                            // Read the live installDir from the store after the intro
                            // wait — the user may have switched directories on the
                            // welcome screen, and `forceFresh` should reflect the
                            // ampli.json status of the directory we're actually
                            // about to instrument.
                            const forceFresh = !ampliConfigExists(tui.store.session.installDir);
                            const { resolveZone } = await import('../lib/zone-resolution.js');
                            const zone = resolveZone(tui.store.session, DEFAULT_AMPLITUDE_ZONE, { readDisk: false });
                            // Try direct signup first when create-account + email + fullName are provided
                            // and the feature flag is enabled. performSignupOrAuth returns null when
                            // any of those gates are missing, or when the server returns a non-success
                            // response — in which case we fall through to the existing OAuth flow
                            // (TUI has a browser; this fallback is valid).
                            //
                            // On signup success, the wrapper already fetched the real user
                            // profile (with provisioning retry) and persisted tokens to
                            // ~/.ampli.json — so we carry its userInfo through and skip the
                            // redundant fetch + storeToken below.
                            let auth = null;
                            let signupUserInfo = null;
                            // True iff direct signup produced fresh tokens in this run.
                            // Used by the downstream fetchAmplitudeUser catch to
                            // distinguish a provisioning-lag recovery (signup succeeded,
                            // but user data not yet available) from the normal
                            // expired-token case.
                            let signupTokensObtained = false;
                            const { trackSignupAttempt } = await import('../utils/signup-or-auth.js');
                            const s = tui.store.session;
                            if ((0, wizard_session_1.isCreateAccountOnboarding)(s) &&
                                s.signupEmail &&
                                s.signupFullName &&
                                !s.signupTokensObtained) {
                                const { performSignupOrAuth } = await import('../utils/signup-or-auth.js');
                                try {
                                    const signupResult = await performSignupOrAuth({
                                        email: s.signupEmail,
                                        fullName: s.signupFullName,
                                        zone,
                                    });
                                    if (signupResult !== null) {
                                        auth = signupResult;
                                        signupUserInfo = signupResult.userInfo;
                                        signupTokensObtained = true;
                                        tui.store.setSignupMagicLinkUrl(signupResult.dashboardUrl ?? null);
                                        (0, helpers_1.getUI)().log.info('Direct signup succeeded; using newly created account.');
                                    }
                                }
                                catch (err) {
                                    trackSignupAttempt({ status: 'wrapper_exception', zone });
                                    (0, helpers_1.getUI)().log.warn(`Direct signup errored: ${err instanceof Error ? err.message : String(err)}. Falling back to OAuth.`);
                                    auth = null;
                                }
                            }
                            else if (s.signupTokensObtained) {
                                // EmailCaptureScreen already called replaceStoredUser + set
                                // signupTokensObtained. Without hydrating `auth` here,
                                // performAmplitudeAuth({ forceFresh }) runs on a fresh install
                                // dir and skips ~/.ampli.json — spurious browser OAuth.
                                signupTokensObtained = true;
                                const fromDisk = getStoredToken(undefined, zone);
                                if (fromDisk) {
                                    auth = {
                                        idToken: fromDisk.idToken,
                                        accessToken: fromDisk.accessToken,
                                        refreshToken: fromDisk.refreshToken,
                                        zone,
                                    };
                                    (0, helpers_1.getUI)().log.info('Using signup tokens obtained during email capture.');
                                }
                                else {
                                    (0, helpers_1.getUI)().log.warn('Signup tokens were recorded but none found on disk; opening OAuth.');
                                }
                            }
                            if (auth === null) {
                                auth = await performAmplitudeAuth({ zone, forceFresh });
                            }
                            // Update login URL (clears the "copy this URL" hint)
                            tui.store.setLoginUrl(null);
                            // Zone was already selected by the user before OAuth started.
                            const cloudRegion = zone;
                            let userInfo;
                            if (signupUserInfo) {
                                // Wrapper already fetched userInfo and stored tokens — no
                                // redundant network call, no browser fallback needed.
                                userInfo = signupUserInfo;
                            }
                            else {
                                // Probe the cached token with a bounded timeout. On a
                                // returning user from an older wizard version,
                                // performAmplitudeAuth can silently reuse a stored token
                                // whose idToken is rejected by the API (or whose call
                                // hangs). Without this bound the user sits on AuthScreen
                                // Step 1 with no URL until the network stack gives up —
                                // that's the "blank auth screen" symptom. Treat a timeout
                                // as a fetch failure and force a fresh OAuth so the
                                // browser opens and setLoginUrl fires immediately.
                                const STALE_TOKEN_PROBE_MS = 8_000;
                                // Capture the current (non-null) auth reference for the
                                // closure — the outer `auth` is typed `T | null` and the
                                // narrowing from the `if (auth === null)` branch above
                                // doesn't propagate into the inner async function. The
                                // catch block below may reassign auth on probe failure.
                                const probeAuth = auth;
                                const probeFetchUser = async () => {
                                    let timer;
                                    try {
                                        return await Promise.race([
                                            fetchAmplitudeUser(probeAuth.idToken, cloudRegion),
                                            new Promise((_, reject) => {
                                                timer = setTimeout(() => {
                                                    const err = new Error(`cached-token probe timed out after ${STALE_TOKEN_PROBE_MS}ms`);
                                                    err.name = 'TimeoutError';
                                                    reject(err);
                                                }, STALE_TOKEN_PROBE_MS);
                                            }),
                                        ]);
                                    }
                                    finally {
                                        if (timer !== undefined)
                                            clearTimeout(timer);
                                    }
                                };
                                try {
                                    userInfo = await probeFetchUser();
                                }
                                catch (probeErr) {
                                    const { logToFile: log } = await import('../utils/debug.js');
                                    log('[bin] cached-token probe failed; forcing fresh OAuth', probeErr instanceof Error
                                        ? probeErr.message
                                        : String(probeErr));
                                    if (signupTokensObtained) {
                                        // Signup succeeded moments ago so the tokens can't be
                                        // expired — the fetch failure is almost certainly
                                        // backend provisioning lag for a brand-new account.
                                        // Surface the transition so the user isn't confused
                                        // when a browser opens after "signup succeeded", and
                                        // emit telemetry so we can measure how often the rare
                                        // edge case actually hits production.
                                        (0, helpers_1.getUI)().log.info('Account created, but user data is still being provisioned. ' +
                                            'Opening browser to complete sign-in…');
                                        trackSignupAttempt({
                                            status: 'browser_fallback_after_signup',
                                            zone,
                                        });
                                    }
                                    // Token may be expired — re-open the browser for a fresh login
                                    tui.store.setLoginUrl(null);
                                    auth = await performAmplitudeAuth({
                                        zone,
                                        forceFresh: true,
                                    });
                                    userInfo = await fetchAmplitudeUser(auth.idToken, cloudRegion);
                                }
                                // Persist to ~/.ampli.json (signup path already did this)
                                storeToken({
                                    id: userInfo.id,
                                    firstName: userInfo.firstName,
                                    lastName: userInfo.lastName,
                                    email: userInfo.email,
                                    zone: auth.zone,
                                }, {
                                    accessToken: auth.accessToken,
                                    idToken: auth.idToken,
                                    refreshToken: auth.refreshToken,
                                    expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
                                });
                            }
                            // Populate user email for /whoami display
                            session.userEmail = userInfo.email;
                            helpers_1.analytics.setDistinctId(userInfo.email);
                            helpers_1.analytics.identifyUser({ email: userInfo.email });
                            // Signal AuthScreen — triggers org/project/API key pickers
                            tui.store.setOAuthComplete({
                                accessToken: auth.accessToken,
                                idToken: auth.idToken,
                                cloudRegion,
                                orgs: userInfo.orgs,
                            });
                        }
                        catch (err) {
                            // Auth failure is non-fatal here — agent-runner will retry/handle it
                            if (process.env.DEBUG || process.env.AMPLITUDE_WIZARD_DEBUG) {
                                (0, helpers_1.getUI)().log.error(`OAuth setup error: ${err instanceof Error ? err.message : String(err)}`);
                            }
                        }
                    })();
                    // ── Mid-session re-auth watcher (handles /region) ──────────
                    // setRegionForced() clears credentials and pendingOrgs so the
                    // RegionSelect screen re-appears. Once the user picks a new
                    // region, the Auth flow gate would otherwise sit on a blank
                    // AuthScreen because `authTask` is single-shot — it's already
                    // resolved against the OLD region. This watcher re-fires the
                    // OAuth pipeline against the new region so AuthScreen actually
                    // makes progress.
                    //
                    // PR #296 (the bin.ts split) silently dropped this watcher —
                    // /region mid-session has been broken on main since that
                    // refactor. Restoring it here. The probe + bounded fetchUser
                    // (same as authTask) catches stale tokens that would otherwise
                    // leave the new-region AuthScreen blank after a region switch.
                    //
                    // Deferred until authTask resolves so the watcher doesn't add
                    // a second subscribe during the initial-auth window.
                    void (async () => {
                        await authTask;
                        const { Overlay } = await import('../ui/tui/router.js');
                        const { performAmplitudeAuth } = await import('../utils/oauth.js');
                        const { fetchAmplitudeUser } = await import('../lib/api.js');
                        const { storeToken } = await import('../utils/ampli-settings.js');
                        const { resolveZone } = await import('../lib/zone-resolution.js');
                        const { DEFAULT_AMPLITUDE_ZONE } = await import('../lib/constants.js');
                        const waitForSessionState = (predicate) => new Promise((resolve) => {
                            if (predicate()) {
                                resolve();
                                return;
                            }
                            const unsub = tui.store.subscribe(() => {
                                if (predicate()) {
                                    unsub();
                                    resolve();
                                }
                            });
                        });
                        // Single OAuth + fetchUser + setOAuthComplete cycle against
                        // the currently-selected region. Includes the same
                        // stale-token probe as authTask so a returning user who
                        // switches regions doesn't get stuck on a blank AuthScreen
                        // when their old per-zone token in ~/.ampli.json works for
                        // performAmplitudeAuth's cache check but fails the
                        // fetchAmplitudeUser call against the new zone.
                        const runReauthCycle = async () => {
                            const zone = resolveZone(tui.store.session, DEFAULT_AMPLITUDE_ZONE, { readDisk: false });
                            let auth = await performAmplitudeAuth({
                                zone,
                                forceFresh: false,
                            });
                            tui.store.setLoginUrl(null);
                            const STALE_TOKEN_PROBE_MS = 8_000;
                            const probeAuth = auth;
                            const probeFetchUser = async () => {
                                let timer;
                                try {
                                    return await Promise.race([
                                        fetchAmplitudeUser(probeAuth.idToken, zone),
                                        new Promise((_, reject) => {
                                            timer = setTimeout(() => {
                                                const err = new Error(`cached-token probe timed out after ${STALE_TOKEN_PROBE_MS}ms`);
                                                err.name = 'TimeoutError';
                                                reject(err);
                                            }, STALE_TOKEN_PROBE_MS);
                                        }),
                                    ]);
                                }
                                finally {
                                    if (timer !== undefined)
                                        clearTimeout(timer);
                                }
                            };
                            let userInfo;
                            try {
                                userInfo = await probeFetchUser();
                            }
                            catch (probeErr) {
                                const { logToFile } = await import('../utils/debug.js');
                                logToFile('[reauth] cached-token probe failed; forcing fresh OAuth', probeErr instanceof Error
                                    ? probeErr.message
                                    : String(probeErr));
                                tui.store.setLoginUrl(null);
                                auth = await performAmplitudeAuth({ zone, forceFresh: true });
                                userInfo = await fetchAmplitudeUser(auth.idToken, zone);
                            }
                            storeToken({
                                id: userInfo.id,
                                firstName: userInfo.firstName,
                                lastName: userInfo.lastName,
                                email: userInfo.email,
                                zone: auth.zone,
                            }, {
                                accessToken: auth.accessToken,
                                idToken: auth.idToken,
                                refreshToken: auth.refreshToken,
                                expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
                            });
                            tui.store.setUserEmail(userInfo.email);
                            helpers_1.analytics.setDistinctId(userInfo.email);
                            helpers_1.analytics.identifyUser({ email: userInfo.email });
                            tui.store.setOAuthComplete({
                                accessToken: auth.accessToken,
                                idToken: auth.idToken,
                                cloudRegion: zone,
                                orgs: userInfo.orgs,
                            });
                        };
                        // Bounded retry loop. Three consecutive failures and we
                        // back off until the user takes manual action (/login or
                        // /region) — prevents hot-spinning OAuth attempts when the
                        // new zone keeps rejecting.
                        //
                        // The outer loop waits for the next /region trigger; the inner
                        // loop retries OAuth itself when runReauthCycle throws. The
                        // earlier shape collapsed both into a single loop, which
                        // deadlocked: failures 1 and 2 hit the linear-backoff branch
                        // and looped back to `waitForSessionState(credentials !== null)`,
                        // but a failed runReauthCycle never sets credentials — so the
                        // wait blocked forever and the toast's "Retrying" promise was
                        // a lie. Splitting the loops makes retry a real retry and
                        // keeps the 3-strike circuit-breaker as the only place that
                        // hands control back to the user.
                        while (true) {
                            // Wait for /region to be invoked. setRegionForced sets
                            // regionForced=true (and clears credentials + pendingOrgs).
                            //
                            // Gating on regionForced rather than `credentials !== null`
                            // matters when /region is fired DURING the AuthScreen SUSI
                            // step — initial OAuth completed (pendingOrgs set) but the
                            // user hasn't picked an org/project yet, so credentials is
                            // still null. The old `credentials !== null` gate parked
                            // forever in that window and never reacted to setRegionForced.
                            // `await authTask` above already prevents racing with the
                            // initial OAuth, so we don't need the credentials sentinel.
                            await waitForSessionState(() => tui.store.session.regionForced);
                            // Then wait for the user to pick a new region. setRegion
                            // clears regionForced.
                            //
                            // Skip when /logout is active (loggingOut flag) or an
                            // outro is queued — /logout clears credentials and exits
                            // 1.5s later; without this guard the watcher would race
                            // the exit and pop a browser during "Logged out".
                            await waitForSessionState(() => !tui.store.session.loggingOut &&
                                tui.store.session.credentials === null &&
                                tui.store.session.region !== null &&
                                !tui.store.session.regionForced &&
                                tui.store.session.introConcluded &&
                                tui.store.currentScreen !== Overlay.Logout &&
                                tui.store.session.outroData === null);
                            // OAuth retry loop. Runs at least once; retries on transient
                            // failures up to the 3-strike circuit-breaker; success or
                            // manual /login both break out so the outer loop can wait
                            // for the next /region trigger.
                            let consecutiveFailures = 0;
                            while (true) {
                                try {
                                    await runReauthCycle();
                                    break;
                                }
                                catch (err) {
                                    consecutiveFailures += 1;
                                    if (process.env.DEBUG || process.env.AMPLITUDE_WIZARD_DEBUG) {
                                        console.error('Re-auth error:', err);
                                    }
                                    tui.store.setCommandFeedback(consecutiveFailures === 1
                                        ? "Authentication didn't complete. Retrying — use /login to retry manually."
                                        : `Authentication failed (attempt ${consecutiveFailures}). Use /login to retry manually.`, consecutiveFailures >= 3 ? 8000 : 4000);
                                    if (consecutiveFailures >= 3) {
                                        // After 3 strikes, stop auto-retrying. The user can
                                        // resume via /login or /region; both clear credentials
                                        // and unblock the watcher.
                                        await waitForSessionState(() => tui.store.currentScreen === Overlay.Login ||
                                            tui.store.session.regionForced);
                                        // /login takes over the auth flow itself; /region
                                        // restarts the outer loop. Either way reset the
                                        // failure count and exit the inner retry — the
                                        // outer loop's wait guards will pick up the next
                                        // signal.
                                        break;
                                    }
                                    // Linear backoff before retrying runReauthCycle directly.
                                    await new Promise((r) => setTimeout(r, 1500 * consecutiveFailures));
                                }
                            }
                        }
                    })();
                    // ── Framework detection ────────────────────────────────
                    // Runs concurrently with auth while AuthScreen shows. Each
                    // detector has its own per-framework timeout internally, so
                    // no outer timeout is needed. The shared `runFrameworkDetection`
                    // helper handles detect → gatherContext → setFrameworkConfig →
                    // discoverFeatures → autoEnableInlineAddons → setDetectionComplete
                    // in the right order, including the abort-aware short-circuits
                    // that let a directory change mid-detection cancel the
                    // in-flight run cleanly.
                    const { runFrameworkDetection } = await import('../lib/framework-detection.js');
                    // 1) Wire the redetector so the IntroScreen's "Change directory"
                    //    flow can re-run detection against a new tree. Without
                    //    this, `store.changeInstallDir(newDir)` resets
                    //    `detectionComplete=false` but the spinner never advances —
                    //    it sat there forever before this hookup.
                    tui.store.setFrameworkRedetector((newDir, signal) => runFrameworkDetection(tui.store, newDir, { signal }));
                    // 2) Run the INITIAL detection with an abort controller the
                    //    store can reach. If the user picks "Change directory"
                    //    while this first run is still scanning, the store cancels
                    //    it through this controller so a stale
                    //    `setDetectionComplete()` can't fire after the directory
                    //    swap.
                    const detectionController = new AbortController();
                    tui.store.registerActiveDetection(detectionController);
                    const detectionTask = runFrameworkDetection(tui.store, installDir, {
                        signal: detectionController.signal,
                    });
                    // Gate runWizard on the user reaching RunScreen — at that point
                    // auth, data check, and any setup questions are all complete.
                    const { Screen } = await import('../ui/tui/router.js');
                    // RunPhase is also exported from `wizard-session.ts`, but the TUI
                    // layer keeps its own copy in session-constants.ts to dodge the
                    // tsx ESM/CJS dual-loading bug documented at the top of that
                    // file — and importing from there avoids tripping the cli.test
                    // module mock for `../lib/wizard-session` which only exposes
                    // `buildSession` + `DiscoveredFeature`.
                    const { RunPhase: RunPhaseEnum } = await import('../ui/tui/session-constants.js');
                    tui.store.onEnterScreen(Screen.Run, () => {
                        // Eagerly transition into the Running phase the instant the
                        // user lands on RunScreen. Without this, the screen renders
                        // with `runPhase === Idle` while runAgentWizard does its
                        // async setup (skill staging, gateway liveness probe, agent
                        // SDK init, MCP connect) — anywhere from a few seconds on a
                        // warm cache to ~30s on a cold one. During that window the
                        // header shows a misleading "Agent running · 0s" with the
                        // empty-tasks placeholder ("Analyzing project…"), the timer
                        // never advances (runStartedAt is null so it falls back to
                        // Date.now() each render), and the Logs tab sits at
                        // "Waiting for the agent to start writing logs…". From the
                        // user's perspective the wizard looks hung. setRunPhase
                        // populates the canonical 5 tasks, stamps runStartedAt, and
                        // is idempotent against agent-runner's later
                        // getUI().startRun() call.
                        tui.store.setRunPhase(RunPhaseEnum.Running);
                        tui.store.completeSetup();
                    });
                    // Session checkpointing — save at key transitions so crash
                    // recovery can skip already-completed steps.
                    const { saveCheckpoint, clearCheckpoint } = await import('../lib/session-checkpoint.js');
                    // After auth completes (most expensive step to repeat)
                    tui.store.onEnterScreen(Screen.DataSetup, () => {
                        saveCheckpoint(tui.store.session, 'screen_data_setup');
                    });
                    // Before agent starts (captures all setup state)
                    tui.store.onEnterScreen(Screen.Run, () => {
                        saveCheckpoint(tui.store.session, 'screen_run');
                    });
                    // Clear checkpoint only on successful completion — error/cancel
                    // should preserve the checkpoint so users can resume next run.
                    tui.store.onEnterScreen(Screen.Outro, () => {
                        if (tui.store.session.outroData?.kind === 'success') {
                            clearCheckpoint(tui.store.session.installDir, 'success');
                        }
                    });
                    // (The SIGINT handler is now installed earlier, right after
                    // startTUI(), to close a race window where early Ctrl+C would
                    // bypass the handler and terminate immediately.)
                    // Wait for auth and framework detection to finish concurrently.
                    await Promise.all([authTask, detectionTask]);
                    if (session.verbose || session.debug) {
                        const { logToFile } = await import('../utils/debug.js');
                        logToFile(`[verbose] detection    : ${tui.store.session.integration ?? 'none'}`);
                        logToFile(`[verbose] framework    : ${tui.store.session.detectedFrameworkLabel ?? 'unknown'}`);
                        logToFile(`[verbose] region       : ${tui.store.session.region ?? 'not set'}`);
                    }
                    // Blocks until onEnterScreen(Screen.Run) fires completeSetup().
                    await tui.waitForSetup();
                    // Before calling the AI agent, do a quick static check to see if
                    // Amplitude is already installed in the project. If so, skip the
                    // agent entirely and advance directly to MCP setup.
                    //
                    // Read the live installDir from the store — the user may have
                    // changed it via the IntroScreen "Change directory" picker
                    // since the local `installDir` was captured at startup.
                    const { detectAmplitudeInProject } = await import('../lib/detect-amplitude.js');
                    const localDetection = detectAmplitudeInProject(tui.store.session.installDir);
                    if (localDetection.confidence !== 'none') {
                        const { logToFile: log } = await import('../utils/debug.js');
                        log(`[bin] Amplitude already detected (${localDetection.reason ?? 'unknown'}) — prompting on MCP screen (continue vs run wizard)`);
                        const { RunPhase, OutroKind } = await import('../lib/wizard-session.js');
                        tui.store.setAmplitudePreDetected();
                        tui.store.setRunPhase(RunPhase.Completed);
                        const runWizardAnyway = await tui.store.waitForPreDetectedChoice();
                        if (runWizardAnyway) {
                            log('[bin] user chose to run setup wizard despite pre-detection');
                            tui.store.resetForAgentAfterPreDetected();
                            await (0, helpers_1.lazyRunWizard)(options, tui.store.session, () => tui.store.session.additionalFeatureQueue, {
                                onFeatureStart: (f) => tui.store.setCurrentFeature(f),
                                onFeatureComplete: (f) => tui.store.markFeatureComplete(f),
                            });
                        }
                        else {
                            tui.store.setOutroData({ kind: OutroKind.Success });
                        }
                    }
                    else {
                        await (0, helpers_1.lazyRunWizard)(options, tui.store.session, () => tui.store.session.additionalFeatureQueue, {
                            onFeatureStart: (f) => tui.store.setCurrentFeature(f),
                            onFeatureComplete: (f) => tui.store.markFeatureComplete(f),
                        });
                    }
                    // Keep the outro screen visible — let process.exit() handle cleanup
                }
                catch (err) {
                    // TUI unavailable (e.g., in test environment) — continue with default UI.
                    // Use console.error directly: startTUI() calls setUI(inkUI) before
                    // render(), so if render() throws, getUI() returns an InkUI whose
                    // renderer never started — messages would vanish into the store.
                    if (process.env.DEBUG || process.env.AMPLITUDE_WIZARD_DEBUG) {
                        console.error(`TUI init failed: ${err instanceof Error ? err.message : String(err)}`);
                    }
                    await (0, helpers_1.lazyRunWizard)(options);
                }
            })();
        }
    },
};
