From 23a0209369b2e95efb85311ee50835fff3a6c60d Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Wed, 1 Apr 2026 11:45:38 -0500 Subject: [PATCH 1/2] fix: skip device auth when unclaimed environment is active checkStoredAuth only checked for OAuth tokens, so the state machine forced device auth even when a valid unclaimed environment (with apiKey + claimToken) was already active. This caused the CLI to hang on "Waiting for authentication..." after the browser showed success. --- src/lib/run-with-core.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/run-with-core.ts b/src/lib/run-with-core.ts index 79c106e..ef14508 100644 --- a/src/lib/run-with-core.ts +++ b/src/lib/run-with-core.ts @@ -27,7 +27,7 @@ import { getStagingCredentials, saveStagingCredentials, } from './credentials.js'; -import { getConfig, saveConfig, getActiveEnvironment } from './config-store.js'; +import { getConfig, saveConfig, getActiveEnvironment, isUnclaimedEnvironment } from './config-store.js'; import { checkForEnvFiles, discoverCredentials } from './credential-discovery.js'; import { requestDeviceCode, pollForToken } from './device-auth.js'; import { fetchStagingCredentials as fetchStagingCredentialsApi } from './staging-api.js'; @@ -321,6 +321,11 @@ export async function runWithCore(options: InstallerOptions): Promise { }), checkStoredAuth: fromPromise(async () => { + const activeEnv = getActiveEnvironment(); + if (activeEnv?.apiKey && isUnclaimedEnvironment(activeEnv)) { + return true; + } + const token = getAccessToken(); return token !== null; }), From d8fddac885b616c59d77d25cc2cf93b9dd7ebfa2 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Wed, 1 Apr 2026 11:52:51 -0500 Subject: [PATCH 2/2] test: verify unclaimed env skips device auth in state machine --- src/lib/installer-core.spec.ts | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/lib/installer-core.spec.ts b/src/lib/installer-core.spec.ts index 90dfe97..8c62dd7 100644 --- a/src/lib/installer-core.spec.ts +++ b/src/lib/installer-core.spec.ts @@ -10,6 +10,8 @@ import type { InstallerMachineContext, BranchCheckOutput, } from './installer-core.types.js'; +import type { EnvFileInfo } from './credential-discovery.js'; +import type { StagingCredentials } from './staging-api.js'; // Shared mock actors for reuse across tests const baseMockActors = { @@ -378,5 +380,54 @@ describe('InstallerCore State Machine', () => { expect(actor.getSnapshot().value).toBe('complete'); actor.stop(); }); + + it('skips device auth when checkStoredAuth returns true (unclaimed env)', async () => { + const emitter = createInstallerEventEmitter(); + const options: InstallerOptions = { + debug: false, + forceInstall: false, + installDir: '/test/project', + default: false, + local: true, + ci: false, + skipAuth: true, + dashboard: false, + emitter, + // No CLI credentials — forces credential gathering flow + }; + + let deviceAuthStarted = false; + + const machine = installerMachine.provide({ + actors: { + ...baseMockActors, + detectEnvFiles: fromPromise(async () => ({ + found: false, + })), + checkStoredAuth: fromPromise(async () => true), + runDeviceAuth: fromPromise(async () => { + deviceAuthStarted = true; + throw new Error('device auth should not be called'); + }), + fetchStagingCredentials: fromPromise(async () => ({ + clientId: 'client_unclaimed', + apiKey: 'sk_test_unclaimed', + })), + }, + }); + + const actor = createActor(machine, { + input: { emitter, options }, + }); + + actor.start(); + actor.send({ type: 'START' }); + + await new Promise((r) => setTimeout(r, 200)); + + expect(deviceAuthStarted).toBe(false); + expect(actor.getSnapshot().value).toBe('complete'); + actor.stop(); + }); }); });