Route third-party first-run setup into the provider wizard (#261)

The login picker previously sent third-party users to a dead-end info screen
that only mentioned env vars. This change reuses the existing provider wizard
from the login flow so first-run setup can continue without requiring slash
command access first.

Constraint: The existing provider setup logic must remain the single source of truth
Rejected: Build a separate third-party auth wizard in ConsoleOAuthFlow | would duplicate provider setup behavior and drift over time
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep third-party onboarding routed through ProviderWizard unless the provider command flow is intentionally redesigned
Tested: bun test src/components/ConsoleOAuthFlow.test.tsx src/commands/provider/provider.test.tsx
Tested: tsc --noEmit via project diagnostics
Not-tested: Live gh-authenticated push and PR creation path

Co-authored-by: anandh8x <test@example.com>
This commit is contained in:
Anandan
2026-04-03 18:48:00 +05:30
committed by GitHub
parent 19c00e67ed
commit 116cc8e6bd
4 changed files with 470 additions and 394 deletions

File diff suppressed because one or more lines are too long

View File

@@ -903,7 +903,11 @@ function resolveCodexCredentials(processEnv: NodeJS.ProcessEnv):
}
}
function ProviderWizard({ onDone }: { onDone: LocalJSXCommandOnDone }): React.ReactNode {
export function ProviderWizard({
onDone,
}: {
onDone: LocalJSXCommandOnDone
}): React.ReactNode {
const defaults = getProviderWizardDefaults()
const [step, setStep] = React.useState<Step>({ name: 'choose' })

View File

@@ -0,0 +1,117 @@
import { PassThrough } from 'node:stream'
import { expect, test } from 'bun:test'
import React from 'react'
import stripAnsi from 'strip-ansi'
import { AppStateProvider } from '../state/AppState.js'
import { createRoot } from '../ink.js'
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'
const SYNC_START = '\x1B[?2026h'
const SYNC_END = '\x1B[?2026l'
function extractLastFrame(output: string): string {
let lastFrame: string | null = null
let cursor = 0
while (cursor < output.length) {
const start = output.indexOf(SYNC_START, cursor)
if (start === -1) {
break
}
const contentStart = start + SYNC_START.length
const end = output.indexOf(SYNC_END, contentStart)
if (end === -1) {
break
}
const frame = output.slice(contentStart, end)
if (frame.trim().length > 0) {
lastFrame = frame
}
cursor = end + SYNC_END.length
}
return lastFrame ?? output
}
function createTestStreams(): {
stdout: PassThrough
stdin: PassThrough & {
isTTY: boolean
setRawMode: (mode: boolean) => void
ref: () => void
unref: () => void
}
getOutput: () => string
} {
let output = ''
const stdout = new PassThrough()
const stdin = new PassThrough() as PassThrough & {
isTTY: boolean
setRawMode: (mode: boolean) => void
ref: () => void
unref: () => void
}
stdin.isTTY = true
stdin.setRawMode = () => {}
stdin.ref = () => {}
stdin.unref = () => {}
;(stdout as unknown as { columns: number }).columns = 120
stdout.on('data', chunk => {
output += chunk.toString()
})
return {
stdout,
stdin,
getOutput: () => output,
}
}
async function renderFrame(node: React.ReactNode): Promise<string> {
const { stdout, stdin, getOutput } = createTestStreams()
const root = await createRoot({
stdout: stdout as unknown as NodeJS.WriteStream,
stdin: stdin as unknown as NodeJS.ReadStream,
patchConsole: false,
})
root.render(
<AppStateProvider>
<KeybindingSetup>{node}</KeybindingSetup>
</AppStateProvider>,
)
await Bun.sleep(50)
root.unmount()
stdin.end()
stdout.end()
await Bun.sleep(25)
return stripAnsi(extractLastFrame(getOutput()))
}
test('login picker shows the third-party platform option', async () => {
const output = await renderFrame(<ConsoleOAuthFlow onDone={() => {}} />)
expect(output).toContain('Select login method:')
expect(output).toContain('3rd-party platform')
})
test('third-party provider branch opens the provider wizard', async () => {
const output = await renderFrame(
<ConsoleOAuthFlow
initialStatus={{ state: 'platform_setup' }}
onDone={() => {}}
/>,
)
expect(output).toContain('Set up a provider profile')
expect(output).toContain('OpenAI-compatible')
expect(output).toContain('Ollama')
})

File diff suppressed because one or more lines are too long