feat: build

This commit is contained in:
claude-code-best committed 2026-03-31 19:22:47 +08:00
commit f90eee85d8
1906 files changed
+514339

No files matched your search

@@ -0,0 +1,276 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
import { Spinner } from '../../components/Spinner.js';
import TextInput from '../../components/TextInput.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { setClipboard } from '../../ink/termio/osc.js';
import { Box, Link, Text } from '../../ink.js';
import { OAuthService } from '../../services/oauth/index.js';
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js';
import { logError } from '../../utils/log.js';
interface OAuthFlowStepProps {
onSuccess: (token: string) => void;
onCancel: () => void;
}
type OAuthStatus = {
state: 'starting';
} | {
state: 'waiting_for_login';
url: string;
} | {
state: 'processing';
} | {
state: 'success';
token: string;
} | {
state: 'error';
message: string;
toRetry?: OAuthStatus;
} | {
state: 'about_to_retry';
nextState: OAuthStatus;
};
const PASTE_HERE_MSG = 'Paste code here if prompted > ';
export function OAuthFlowStep({
onSuccess,
onCancel
}: OAuthFlowStepProps): React.ReactNode {
const [oauthStatus, setOAuthStatus] = useState<OAuthStatus>({
state: 'starting'
});
const [oauthService] = useState(() => new OAuthService());
const [pastedCode, setPastedCode] = useState('');
const [cursorOffset, setCursorOffset] = useState(0);
const [showPastePrompt, setShowPastePrompt] = useState(false);
const [urlCopied, setUrlCopied] = useState(false);
const timersRef = useRef<Set<NodeJS.Timeout>>(new Set());
// Separate ref so startOAuth's timer clear doesn't cancel the urlCopied reset
const urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const terminalSize = useTerminalSize();
const textInputColumns = Math.max(50, terminalSize.columns - PASTE_HERE_MSG.length - 4);
function handleKeyDown(e: KeyboardEvent): void {
if (oauthStatus.state !== 'error') return;
e.preventDefault();
if (e.key === 'return' && oauthStatus.toRetry) {
setPastedCode('');
setCursorOffset(0);
setOAuthStatus({
state: 'about_to_retry',
nextState: oauthStatus.toRetry
});
} else {
onCancel();
}
}
async function handleSubmitCode(value: string, url: string) {
try {
// Expecting format "authorizationCode#state" from the authorization callback URL
const [authorizationCode, state] = value.split('#');
if (!authorizationCode || !state) {
setOAuthStatus({
state: 'error',
message: 'Invalid code. Please make sure the full code was copied',
toRetry: {
state: 'waiting_for_login',
url
}
});
return;
}
// Track which path the user is taking (manual code entry)
logEvent('tengu_oauth_manual_entry', {});
oauthService.handleManualAuthCodeInput({
authorizationCode,
state
});
} catch (err: unknown) {
logError(err);
setOAuthStatus({
state: 'error',
message: (err as Error).message,
toRetry: {
state: 'waiting_for_login',
url
}
});
}
}
const startOAuth = useCallback(async () => {
// Clear any existing timers when starting new OAuth flow
timersRef.current.forEach(timer => clearTimeout(timer));
timersRef.current.clear();
try {
const result = await oauthService.startOAuthFlow(async url_0 => {
setOAuthStatus({
state: 'waiting_for_login',
url: url_0
});
const timer_0 = setTimeout(setShowPastePrompt, 3000, true);
timersRef.current.add(timer_0);
}, {
loginWithClaudeAi: true,
// Always use Claude AI for subscription tokens
inferenceOnly: true,
expiresIn: 365 * 24 * 60 * 60 // 1 year
});
// Show processing state
setOAuthStatus({
state: 'processing'
});
// OAuthFlowStep creates inference-only tokens for GitHub Actions, not a
// replacement login. Use saveOAuthTokensIfNeeded directly to avoid
// performLogout which would destroy the user's existing auth session.
saveOAuthTokensIfNeeded(result);
// For OAuth flow, the access token can be used as an API key
const timer1 = setTimeout((setOAuthStatus_0, accessToken, onSuccess_0, timersRef_0) => {
setOAuthStatus_0({
state: 'success',
token: accessToken
});
// Auto-continue after brief delay to show success
const timer2 = setTimeout(onSuccess_0, 1000, accessToken);
timersRef_0.current.add(timer2);
}, 100, setOAuthStatus, result.accessToken, onSuccess, timersRef);
timersRef.current.add(timer1);
} catch (err_0) {
const errorMessage = (err_0 as Error).message;
setOAuthStatus({
state: 'error',
message: errorMessage,
toRetry: {
state: 'starting'
} // Allow retry by starting fresh OAuth flow
});
logError(err_0);
logEvent('tengu_oauth_error', {
error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
}
}, [oauthService, onSuccess]);
useEffect(() => {
if (oauthStatus.state === 'starting') {
void startOAuth();
}
}, [oauthStatus.state, startOAuth]);
// Retry logic
useEffect(() => {
if (oauthStatus.state === 'about_to_retry') {
const timer_1 = setTimeout((nextState, setShowPastePrompt_0, setOAuthStatus_1) => {
// Only show paste prompt when retrying to waiting_for_login
setShowPastePrompt_0(nextState.state === 'waiting_for_login');
setOAuthStatus_1(nextState);
}, 500, oauthStatus.nextState, setShowPastePrompt, setOAuthStatus);
timersRef.current.add(timer_1);
}
}, [oauthStatus]);
useEffect(() => {
if (pastedCode === 'c' && oauthStatus.state === 'waiting_for_login' && showPastePrompt && !urlCopied) {
void setClipboard(oauthStatus.url).then(raw => {
if (raw) process.stdout.write(raw);
setUrlCopied(true);
clearTimeout(urlCopiedTimerRef.current);
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false);
});
setPastedCode('');
}
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied]);
// Cleanup OAuth service and timers when component unmounts
useEffect(() => {
const timers = timersRef.current;
return () => {
oauthService.cleanup();
// Clear all timers
timers.forEach(timer_2 => clearTimeout(timer_2));
timers.clear();
clearTimeout(urlCopiedTimerRef.current);
};
}, [oauthService]);
// Helper function to render the appropriate status message
function renderStatusMessage(): React.ReactNode {
switch (oauthStatus.state) {
case 'starting':
return <Box>
<Spinner />
<Text>Starting authentication…</Text>
</Box>;
case 'waiting_for_login':
return <Box flexDirection="column" gap={1}>
{!showPastePrompt && <Box>
<Spinner />
<Text>
Opening browser to sign in with your Claude account…
</Text>
</Box>}
{showPastePrompt && <Box>
<Text>{PASTE_HERE_MSG}</Text>
<TextInput value={pastedCode} onChange={setPastedCode} onSubmit={(value_0: string) => handleSubmitCode(value_0, oauthStatus.url)} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={textInputColumns} />
</Box>}
</Box>;
case 'processing':
return <Box>
<Spinner />
<Text>Processing authentication…</Text>
</Box>;
case 'success':
return <Box flexDirection="column" gap={1}>
<Text color="success">
✓ Authentication token created successfully!
</Text>
<Text dimColor>Using token for GitHub Actions setup…</Text>
</Box>;
case 'error':
return <Box flexDirection="column" gap={1}>
<Text color="error">OAuth error: {oauthStatus.message}</Text>
{oauthStatus.toRetry ? <Text dimColor>
Press Enter to try again, or any other key to cancel
</Text> : <Text dimColor>Press any key to return to API key selection</Text>}
</Box>;
case 'about_to_retry':
return <Box flexDirection="column" gap={1}>
<Text color="permission">Retrying…</Text>
</Box>;
default:
return null;
}
}
return <Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
{/* Show header inline only for initial starting state */}
{oauthStatus.state === 'starting' && <Box flexDirection="column" gap={1} paddingBottom={1}>
<Text bold>Create Authentication Token</Text>
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
</Box>}
{/* Show header for non-starting states (to avoid duplicate with inline header)*/}
{oauthStatus.state !== 'success' && oauthStatus.state !== 'starting' && oauthStatus.state !== 'processing' && <Box key="header" flexDirection="column" gap={1} paddingBottom={1}>
<Text bold>Create Authentication Token</Text>
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
</Box>}
{/* Show URL when paste prompt is visible */}
{oauthStatus.state === 'waiting_for_login' && showPastePrompt && <Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
<Box paddingX={1}>
<Text dimColor>
Browser didn&apos;t open? Use the url below to sign in{' '}
</Text>
{urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor>
<KeyboardShortcutHint shortcut="c" action="copy" parens />
</Text>}
</Box>
<Link url={oauthStatus.url}>
<Text dimColor>{oauthStatus.url}</Text>
</Link>
</Box>}
<Box paddingLeft={1} flexDirection="column" gap={1}>
{renderStatusMessage()}
</Box>
</Box>;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUNhbGxiYWNrIiwidXNlRWZmZWN0IiwidXNlUmVmIiwidXNlU3RhdGUiLCJBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTIiwibG9nRXZlbnQiLCJLZXlib2FyZFNob3J0Y3V0SGludCIsIlNwaW5uZXIiLCJUZXh0SW5wdXQiLCJ1c2VUZXJtaW5hbFNpemUiLCJLZXlib2FyZEV2ZW50Iiwic2V0Q2xpcGJvYXJkIiwiQm94IiwiTGluayIsIlRleHQiLCJPQXV0aFNlcnZpY2UiLCJzYXZlT0F1dGhUb2tlbnNJZk5lZWRlZCIsImxvZ0Vycm9yIiwiT0F1dGhGbG93U3RlcFByb3BzIiwib25TdWNjZXNzIiwidG9rZW4iLCJvbkNhbmNlbCIsIk9BdXRoU3RhdHVzIiwic3RhdGUiLCJ1cmwiLCJtZXNzYWdlIiwidG9SZXRyeSIsIm5leHRTdGF0ZSIsIlBBU1RFX0hFUkVfTVNHIiwiT0F1dGhGbG93U3RlcCIsIlJlYWN0Tm9kZSIsIm9hdXRoU3RhdHVzIiwic2V0T0F1dGhTdGF0dXMiLCJvYXV0aFNlcnZpY2UiLCJwYXN0ZWRDb2RlIiwic2V0UGFzdGVkQ29kZSIsImN1cnNvck9mZnNldCIsInNldEN1cnNvck9mZnNldCIsInNob3dQYXN0ZVByb21wdCIsInNldFNob3dQYXN0ZVByb21wdCIsInVybENvcGllZCIsInNldFVybENvcGllZCIsInRpbWVyc1JlZiIsIlNldCIsIk5vZGVKUyIsIlRpbWVvdXQiLCJ1cmxDb3BpZWRUaW1lclJlZiIsInVuZGVmaW5lZCIsInRlcm1pbmFsU2l6ZSIsInRleHRJbnB1dENvbHVtbnMiLCJNYXRoIiwibWF4IiwiY29sdW1ucyIsImxlbmd0aCIsImhhbmRsZUtleURvd24iLCJlIiwicHJldmVudERlZmF1bHQiLCJrZXkiLCJoYW5kbGVTdWJtaXRDb2RlIiwidmFsdWUiLCJhdXRob3JpemF0aW9uQ29kZSIsInNwbGl0IiwiaGFuZGxlTWFudWFsQXV0aENvZGVJbnB1dCIsImVyciIsIkVycm9yIiwic3RhcnRPQXV0aCIsImN1cnJlbnQiLCJmb3JFYWNoIiwidGltZXIiLCJjbGVhclRpbWVvdXQiLCJjbGVhciIsInJlc3VsdCIsInN0YXJ0T0F1dGhGbG93Iiwic2V0VGltZW91dCIsImFkZCIsImxvZ2luV2l0aENsYXVkZUFpIiwiaW5mZXJlbmNlT25seSIsImV4cGlyZXNJbiIsInRpbWVyMSIsImFjY2Vzc1Rva2VuIiwidGltZXIyIiwiZXJyb3JNZXNzYWdlIiwiZXJyb3IiLCJ0aGVuIiwicmF3IiwicHJvY2VzcyIsInN0ZG91dCIsIndyaXRlIiwidGltZXJzIiwiY2xlYW51cCIsInJlbmRlclN0YXR1c01lc3NhZ2UiXSwic291cmNlcyI6WyJPQXV0aEZsb3dTdGVwLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QsIHsgdXNlQ2FsbGJhY2ssIHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IEtleWJvYXJkU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgU3Bpbm5lciB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvU3Bpbm5lci5qcydcbmltcG9ydCBUZXh0SW5wdXQgZnJvbSAnLi4vLi4vY29tcG9uZW50cy9UZXh0SW5wdXQuanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi8uLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgdHlwZSB7IEtleWJvYXJkRXZlbnQgfSBmcm9tICcuLi8uLi9pbmsvZXZlbnRzL2tleWJvYXJkLWV2ZW50LmpzJ1xuaW1wb3J0IHsgc2V0Q2xpcGJvYXJkIH0gZnJvbSAnLi4vLi4vaW5rL3Rlcm1pby9vc2MuanMnXG5pbXBvcnQgeyBCb3gsIExpbmssIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBPQXV0aFNlcnZpY2UgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcy9vYXV0aC9pbmRleC5qcydcbmltcG9ydCB7IHNhdmVPQXV0aFRva2Vuc0lmTmVlZGVkIH0gZnJvbSAnLi4vLi4vdXRpbHMvYXV0aC5qcydcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJ1xuXG5pbnRlcmZhY2UgT0F1dGhGbG93U3RlcFByb3BzIHtcbiAgb25TdWNjZXNzOiAodG9rZW46IHN0cmluZykgPT4gdm9pZFxuICBvbkNhbmNlbDogKCkgPT4gdm9pZFxufVxuXG50eXBlIE9BdXRoU3RhdHVzID1cbiAgfCB7IHN0YXRlOiAnc3RhcnRpbmcnIH1cbiAgfCB7IHN0YXRlOiAnd2FpdGluZ19mb3JfbG9naW4nOyB1cmw6IHN0cmluZyB9XG4gIHwgeyBzdGF0ZTogJ3Byb2Nlc3NpbmcnIH1cbiAgfCB7IHN0YXRlOiAnc3VjY2Vzcyc7IHRva2VuOiBzdHJpbmcgfVxuICB8IHsgc3RhdGU6ICdlcnJvcic7IG1lc3NhZ2U6IHN0cmluZzsgdG9SZXRyeT86IE9BdXRoU3RhdHVzIH1cbiAgfCB7IHN0YXRlOiAnYWJvdXRfdG9fcmV0cnknOyBuZXh0U3RhdGU6IE9BdXRoU3RhdHVzIH1cblxuY29uc3QgUEFTVEVfSEVSRV9NU0cgPSAnUGFzdGUgY29kZSBoZXJlIGlmIHByb21wdGVkID4gJ1xuXG5leHBvcnQgZnVuY3Rpb24gT0F1dGhGbG93U3RlcCh7XG4gIG9uU3VjY2VzcyxcbiAgb25DYW5jZWwsXG59OiBPQXV0aEZsb3dTdGVwUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbb2F1dGhTdGF0dXMsIHNldE9BdXRoU3RhdHVzXSA9IHVzZVN0YXRlPE9BdXRoU3RhdHVzPih7XG4gICAgc3RhdGU6ICdzdGFydGluZycsXG4gIH0pXG4gIGNvbnN0IFtvYXV0aFNlcnZpY2VdID0gdXNlU3RhdGUoKCkgPT4gbmV3IE9BdXRoU2VydmljZSgpKVxuICBjb25zdCBbcGFzdGVkQ29kZSwgc2V0UGFzdGVkQ29kZV0gPSB1c2VTdGF0ZSgnJylcbiAgY29uc3QgW2N1cnNvck9mZnNldCwgc2V0Q3Vyc29yT2Zmc2V0XSA9IHVzZVN0YXRlKDApXG4gIGNvbnN0IFtzaG93UGFzdGVQcm9tcHQsIHNldFNob3dQYXN0ZVByb21wdF0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW3VybENvcGllZCwgc2V0VXJsQ29waWVkXSA9IHVzZVN0YXRlKGZhbHNlKVxuICBjb25zdCB0aW1lcnNSZWYgPSB1c2VSZWY8U2V0PE5vZGVKUy5UaW1lb3V0Pj4obmV3IFNldCgpKVxuICAvLyBTZXBhcmF0ZSByZWYgc28gc3RhcnRPQXV0aCdzIHRpbWVyIGNsZWFyIGRvZXNuJ3QgY2FuY2VsIHRoZSB1cmxDb3BpZWQgcmVzZXRcbiAgY29uc3QgdXJsQ29waWVkVGltZXJSZWYgPSB1c2VSZWY8Tm9kZUpTLlRpbWVvdXQgfCB1bmRlZmluZWQ+KHVuZGVmaW5lZClcblxuICBjb25zdCB0ZXJtaW5hbFNpemUgPSB1c2VUZXJtaW5hbFNpemUoKVxuICBjb25zdCB0ZXh0SW5wdXRDb2x1bW5zID0gTWF0aC5tYXgoXG4gICAgNTAsXG4gICAgdGVybWluYWxTaXplLmNvbHVtbnMgLSBQQVNURV9IRVJFX01TRy5sZW5ndGggLSA0LFxuICApXG5cbiAgZnVuY3Rpb24gaGFuZGxlS2V5RG93bihlOiBLZXlib2FyZEV2ZW50KTogdm9pZCB7XG4gICAgaWYgKG9hdXRoU3RhdHVzLnN0YXRlICE9PSAnZXJyb3InKSByZXR1cm5cbiAgICBlLnByZXZlbnREZWZhdWx0KClcbiAgICBpZiAoZS5rZXkgPT09ICdyZXR1cm4nICYmIG9hdXRoU3RhdHVzLnRvUmV0cnkpIHtcbiAgICAgIHNldFBhc3RlZENvZGUoJycpXG4gICAgICBzZXRDdXJzb3JPZmZzZXQoMClcbiAgICAgIHNldE9BdXRoU3RhdHVzKHtcbiAgICAgICAgc3RhdGU6ICdhYm91dF90b19yZXRyeScsXG4gICAgICAgIG5leHRTdGF0ZTogb2F Line truncated