mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '@anthropic/ink'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '@anthropic/ink';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
|
||||
interface ApiKeyStepProps {
|
||||
existingApiKey: string | null
|
||||
useExistingKey: boolean
|
||||
apiKeyOrOAuthToken: string
|
||||
onApiKeyChange: (value: string) => void
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void
|
||||
onSubmit: () => void
|
||||
onCreateOAuthToken?: () => void
|
||||
selectedOption?: 'existing' | 'new' | 'oauth'
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void
|
||||
existingApiKey: string | null;
|
||||
useExistingKey: boolean;
|
||||
apiKeyOrOAuthToken: string;
|
||||
onApiKeyChange: (value: string) => void;
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
onCreateOAuthToken?: () => void;
|
||||
selectedOption?: 'existing' | 'new' | 'oauth';
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void;
|
||||
}
|
||||
|
||||
export function ApiKeyStep({
|
||||
@@ -23,62 +23,47 @@ export function ApiKeyStep({
|
||||
onSubmit,
|
||||
onToggleUseExistingKey,
|
||||
onCreateOAuthToken,
|
||||
selectedOption = existingApiKey
|
||||
? 'existing'
|
||||
: onCreateOAuthToken
|
||||
? 'oauth'
|
||||
: 'new',
|
||||
selectedOption = existingApiKey ? 'existing' : onCreateOAuthToken ? 'oauth' : 'new',
|
||||
onSelectOption,
|
||||
}: ApiKeyStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
if (selectedOption === 'new' && onCreateOAuthToken) {
|
||||
// From 'new' go up to 'oauth'
|
||||
onSelectOption?.('oauth')
|
||||
onSelectOption?.('oauth');
|
||||
} else if (selectedOption === 'oauth' && existingApiKey) {
|
||||
// From 'oauth' go up to 'existing' (only if it exists)
|
||||
onSelectOption?.('existing')
|
||||
onToggleUseExistingKey(true)
|
||||
onSelectOption?.('existing');
|
||||
onToggleUseExistingKey(true);
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
existingApiKey,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
}, [selectedOption, onCreateOAuthToken, existingApiKey, onSelectOption, onToggleUseExistingKey]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (selectedOption === 'existing') {
|
||||
// From 'existing' go down to 'oauth' (if available) or 'new'
|
||||
onSelectOption?.(onCreateOAuthToken ? 'oauth' : 'new')
|
||||
onToggleUseExistingKey(false)
|
||||
onSelectOption?.(onCreateOAuthToken ? 'oauth' : 'new');
|
||||
onToggleUseExistingKey(false);
|
||||
} else if (selectedOption === 'oauth') {
|
||||
// From 'oauth' go down to 'new'
|
||||
onSelectOption?.('new')
|
||||
onSelectOption?.('new');
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
}, [selectedOption, onCreateOAuthToken, onSelectOption, onToggleUseExistingKey]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (selectedOption === 'oauth' && onCreateOAuthToken) {
|
||||
onCreateOAuthToken()
|
||||
onCreateOAuthToken();
|
||||
} else {
|
||||
onSubmit()
|
||||
onSubmit();
|
||||
}
|
||||
}, [selectedOption, onCreateOAuthToken, onSubmit])
|
||||
}, [selectedOption, onCreateOAuthToken, onSubmit]);
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = selectedOption === 'new'
|
||||
const isTextInputVisible = selectedOption === 'new';
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
@@ -86,14 +71,14 @@ export function ApiKeyStep({
|
||||
'confirm:yes': handleConfirm,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
);
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,9 +90,7 @@ export function ApiKeyStep({
|
||||
{existingApiKey && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'existing'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
{selectedOption === 'existing' ? color('success', theme)('> ') : ' '}
|
||||
Use your existing Claude Code API key
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -115,9 +98,7 @@ export function ApiKeyStep({
|
||||
{onCreateOAuthToken && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'oauth'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
{selectedOption === 'oauth' ? color('success', theme)('> ') : ' '}
|
||||
Create a long-lived token with your Claude subscription
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -148,5 +129,5 @@ export function ApiKeyStep({
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '@anthropic/ink'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '@anthropic/ink';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
|
||||
interface CheckExistingSecretStepProps {
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void
|
||||
onSecretNameChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void;
|
||||
onSecretNameChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export function CheckExistingSecretStep({
|
||||
@@ -19,21 +19,15 @@ export function CheckExistingSecretStep({
|
||||
onSecretNameChange,
|
||||
onSubmit,
|
||||
}: CheckExistingSecretStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const handlePrevious = useCallback(
|
||||
() => onToggleUseExistingSecret(true),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
const handleNext = useCallback(
|
||||
() => onToggleUseExistingSecret(false),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
const handlePrevious = useCallback(() => onToggleUseExistingSecret(true), [onToggleUseExistingSecret]);
|
||||
const handleNext = useCallback(() => onToggleUseExistingSecret(false), [onToggleUseExistingSecret]);
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
@@ -41,14 +35,14 @@ export function CheckExistingSecretStep({
|
||||
'confirm:yes': onSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: useExistingSecret },
|
||||
)
|
||||
);
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !useExistingSecret },
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -58,9 +52,7 @@ export function CheckExistingSecretStep({
|
||||
<Text dimColor>Setup API key secret</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
ANTHROPIC_API_KEY already exists in repository secrets!
|
||||
</Text>
|
||||
<Text color="warning">ANTHROPIC_API_KEY already exists in repository secrets!</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Would you like to:</Text>
|
||||
@@ -80,9 +72,7 @@ export function CheckExistingSecretStep({
|
||||
{!useExistingSecret && (
|
||||
<>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Enter new secret name (alphanumeric with underscores):
|
||||
</Text>
|
||||
<Text>Enter new secret name (alphanumeric with underscores):</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={secretName}
|
||||
@@ -102,5 +92,5 @@ export function CheckExistingSecretStep({
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@anthropic/ink'
|
||||
import React from 'react';
|
||||
import { Text } from '@anthropic/ink';
|
||||
|
||||
export function CheckGitHubStep() {
|
||||
return <Text>Checking GitHub CLI installation…</Text>
|
||||
return <Text>Checking GitHub CLI installation…</Text>;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
|
||||
interface ChooseRepoStepProps {
|
||||
currentRepo: string | null
|
||||
useCurrentRepo: boolean
|
||||
repoUrl: string
|
||||
onRepoUrlChange: (value: string) => void
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void
|
||||
onSubmit: () => void
|
||||
currentRepo: string | null;
|
||||
useCurrentRepo: boolean;
|
||||
repoUrl: string;
|
||||
onRepoUrlChange: (value: string) => void;
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export function ChooseRepoStep({
|
||||
@@ -21,32 +21,32 @@ export function ChooseRepoStep({
|
||||
onSubmit,
|
||||
onToggleUseCurrentRepo,
|
||||
}: ChooseRepoStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const [showEmptyError, setShowEmptyError] = useState(false)
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = terminalSize.columns
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [showEmptyError, setShowEmptyError] = useState(false);
|
||||
const terminalSize = useTerminalSize();
|
||||
const textInputColumns = terminalSize.columns;
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl;
|
||||
if (!repoName?.trim()) {
|
||||
setShowEmptyError(true)
|
||||
return
|
||||
setShowEmptyError(true);
|
||||
return;
|
||||
}
|
||||
onSubmit()
|
||||
}, [useCurrentRepo, currentRepo, repoUrl, onSubmit])
|
||||
onSubmit();
|
||||
}, [useCurrentRepo, currentRepo, repoUrl, onSubmit]);
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo;
|
||||
const handlePrevious = useCallback(() => {
|
||||
onToggleUseCurrentRepo(true)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
onToggleUseCurrentRepo(true);
|
||||
setShowEmptyError(false);
|
||||
}, [onToggleUseCurrentRepo]);
|
||||
const handleNext = useCallback(() => {
|
||||
onToggleUseCurrentRepo(false)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
onToggleUseCurrentRepo(false);
|
||||
setShowEmptyError(false);
|
||||
}, [onToggleUseCurrentRepo]);
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
@@ -55,14 +55,14 @@ export function ChooseRepoStep({
|
||||
'confirm:yes': handleSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
);
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -73,10 +73,7 @@ export function ChooseRepoStep({
|
||||
</Box>
|
||||
{currentRepo && (
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
bold={useCurrentRepo}
|
||||
color={useCurrentRepo ? 'permission' : undefined}
|
||||
>
|
||||
<Text bold={useCurrentRepo} color={useCurrentRepo ? 'permission' : undefined}>
|
||||
{useCurrentRepo ? '> ' : ' '}
|
||||
Use current repository: {currentRepo}
|
||||
</Text>
|
||||
@@ -96,8 +93,8 @@ export function ChooseRepoStep({
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
onChange={value => {
|
||||
onRepoUrlChange(value)
|
||||
setShowEmptyError(false)
|
||||
onRepoUrlChange(value);
|
||||
setShowEmptyError(false);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
focus={true}
|
||||
@@ -116,10 +113,8 @@ export function ChooseRepoStep({
|
||||
</Box>
|
||||
)}
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>
|
||||
{currentRepo ? '↑/↓ to select · ' : ''}Enter to continue
|
||||
</Text>
|
||||
<Text dimColor>{currentRepo ? '↑/↓ to select · ' : ''}Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { Workflow } from './types.js'
|
||||
import React from 'react';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import type { Workflow } from './types.js';
|
||||
|
||||
interface CreatingStepProps {
|
||||
currentWorkflowInstallStep: number
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
selectedWorkflows: Workflow[]
|
||||
currentWorkflowInstallStep: number;
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
selectedWorkflows: Workflow[];
|
||||
}
|
||||
|
||||
export function CreatingStep({
|
||||
@@ -22,21 +22,15 @@ export function CreatingStep({
|
||||
const progressSteps = skipWorkflow
|
||||
? [
|
||||
'Getting repository information',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
secretExists && useExistingSecret ? 'Using existing API key secret' : `Setting up ${secretName} secret`,
|
||||
]
|
||||
: [
|
||||
'Getting repository information',
|
||||
'Creating branch',
|
||||
selectedWorkflows.length > 1
|
||||
? 'Creating workflow files'
|
||||
: 'Creating workflow file',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
selectedWorkflows.length > 1 ? 'Creating workflow files' : 'Creating workflow file',
|
||||
secretExists && useExistingSecret ? 'Using existing API key secret' : `Setting up ${secretName} secret`,
|
||||
'Opening pull request page',
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,33 +40,25 @@ export function CreatingStep({
|
||||
<Text dimColor>Create GitHub Actions workflow</Text>
|
||||
</Box>
|
||||
{progressSteps.map((stepText, index) => {
|
||||
let status: 'completed' | 'in-progress' | 'pending' = 'pending'
|
||||
let status: 'completed' | 'in-progress' | 'pending' = 'pending';
|
||||
|
||||
if (index < currentWorkflowInstallStep) {
|
||||
status = 'completed'
|
||||
status = 'completed';
|
||||
} else if (index === currentWorkflowInstallStep) {
|
||||
status = 'in-progress'
|
||||
status = 'in-progress';
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text
|
||||
color={
|
||||
status === 'completed'
|
||||
? 'success'
|
||||
: status === 'in-progress'
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Text color={status === 'completed' ? 'success' : status === 'in-progress' ? 'warning' : undefined}>
|
||||
{status === 'completed' ? '✓ ' : ''}
|
||||
{stepText}
|
||||
{status === 'in-progress' ? '…' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
|
||||
interface ErrorStepProps {
|
||||
error: string | undefined
|
||||
errorReason?: string
|
||||
errorInstructions?: string[]
|
||||
error: string | undefined;
|
||||
errorReason?: string;
|
||||
errorInstructions?: string[];
|
||||
}
|
||||
|
||||
export function ErrorStep({
|
||||
error,
|
||||
errorReason,
|
||||
errorInstructions,
|
||||
}: ErrorStepProps) {
|
||||
export function ErrorStep({ error, errorReason, errorInstructions }: ErrorStepProps) {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
@@ -38,8 +34,7 @@ export function ErrorStep({
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
For manual setup instructions, see:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
For manual setup instructions, see: <Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -47,5 +42,5 @@ export function ErrorStep({
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Select } from 'src/components/CustomSelect/index.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import React from 'react';
|
||||
import { Select } from 'src/components/CustomSelect/index.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
|
||||
interface ExistingWorkflowStepProps {
|
||||
repoName: string
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void
|
||||
repoName: string;
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void;
|
||||
}
|
||||
|
||||
export function ExistingWorkflowStep({
|
||||
repoName,
|
||||
onSelectAction,
|
||||
}: ExistingWorkflowStepProps) {
|
||||
export function ExistingWorkflowStep({ repoName, onSelectAction }: ExistingWorkflowStepProps) {
|
||||
const options = [
|
||||
{
|
||||
label: 'Update workflow file with latest version',
|
||||
@@ -24,15 +21,15 @@ export function ExistingWorkflowStep({
|
||||
label: 'Exit without making changes',
|
||||
value: 'exit',
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit')
|
||||
}
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onSelectAction('exit')
|
||||
}
|
||||
onSelectAction('exit');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
@@ -43,28 +40,21 @@ export function ExistingWorkflowStep({
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
A Claude workflow file already exists at{' '}
|
||||
<Text color="claude">.github/workflows/claude.yml</Text>
|
||||
A Claude workflow file already exists at <Text color="claude">.github/workflows/claude.yml</Text>
|
||||
</Text>
|
||||
<Text dimColor>What would you like to do?</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
<Select options={options} onChange={handleSelect} onCancel={handleCancel} />
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
View the latest workflow template at:{' '}
|
||||
<Text color="claude">
|
||||
https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml
|
||||
</Text>
|
||||
<Text color="claude">https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
|
||||
interface InstallAppStepProps {
|
||||
repoUrl: string
|
||||
onSubmit: () => void
|
||||
repoUrl: string;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export function InstallAppStep({ repoUrl, onSubmit }: InstallAppStepProps) {
|
||||
// Enter to submit
|
||||
useKeybinding('confirm:yes', onSubmit, { context: 'Confirmation' })
|
||||
useKeybinding('confirm:yes', onSubmit, { context: 'Confirmation' });
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
@@ -33,9 +33,7 @@ export function InstallAppStep({ repoUrl, onSubmit }: InstallAppStepProps) {
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Important: Make sure to grant access to this specific repository
|
||||
</Text>
|
||||
<Text dimColor>Important: Make sure to grant access to this specific repository</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold color="permission">
|
||||
@@ -44,10 +42,9 @@ export function InstallAppStep({ repoUrl, onSubmit }: InstallAppStepProps) {
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Having trouble? See manual setup instructions at:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
Having trouble? See manual setup instructions at: <Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
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 '@anthropic/ink'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { type KeyboardEvent, setClipboard, Box, Link, Text } from '@anthropic/ink'
|
||||
import { OAuthService } from '../../services/oauth/index.js'
|
||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
} from 'src/services/analytics/index.js';
|
||||
import { KeyboardShortcutHint } from '@anthropic/ink';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { type KeyboardEvent, setClipboard, Box, Link, Text } from '@anthropic/ink';
|
||||
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
|
||||
onSuccess: (token: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type OAuthStatus =
|
||||
@@ -23,139 +23,132 @@ type OAuthStatus =
|
||||
| { state: 'processing' }
|
||||
| { state: 'success'; token: string }
|
||||
| { state: 'error'; message: string; toRetry?: OAuthStatus }
|
||||
| { state: 'about_to_retry'; nextState: OAuthStatus }
|
||||
| { state: 'about_to_retry'; nextState: OAuthStatus };
|
||||
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > '
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > ';
|
||||
|
||||
export function OAuthFlowStep({
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}: OAuthFlowStepProps): React.ReactNode {
|
||||
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())
|
||||
});
|
||||
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 urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = Math.max(
|
||||
50,
|
||||
terminalSize.columns - PASTE_HERE_MSG.length - 4,
|
||||
)
|
||||
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 (oauthStatus.state !== 'error') return;
|
||||
e.preventDefault();
|
||||
if (e.key === 'return' && oauthStatus.toRetry) {
|
||||
setPastedCode('')
|
||||
setCursorOffset(0)
|
||||
setPastedCode('');
|
||||
setCursorOffset(0);
|
||||
setOAuthStatus({
|
||||
state: 'about_to_retry',
|
||||
nextState: oauthStatus.toRetry,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
onCancel()
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitCode(value: string, url: string) {
|
||||
try {
|
||||
// Expecting format "authorizationCode#state" from the authorization callback URL
|
||||
const [authorizationCode, state] = value.split('#')
|
||||
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
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Track which path the user is taking (manual code entry)
|
||||
logEvent('tengu_oauth_manual_entry', {})
|
||||
logEvent('tengu_oauth_manual_entry', {});
|
||||
oauthService.handleManualAuthCodeInput({
|
||||
authorizationCode,
|
||||
state,
|
||||
})
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
logError(err)
|
||||
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()
|
||||
timersRef.current.forEach(timer => clearTimeout(timer));
|
||||
timersRef.current.clear();
|
||||
|
||||
try {
|
||||
const result = await oauthService.startOAuthFlow(
|
||||
async url => {
|
||||
setOAuthStatus({ state: 'waiting_for_login', url })
|
||||
const timer = setTimeout(setShowPastePrompt, 3000, true)
|
||||
timersRef.current.add(timer)
|
||||
setOAuthStatus({ state: 'waiting_for_login', url });
|
||||
const timer = setTimeout(setShowPastePrompt, 3000, true);
|
||||
timersRef.current.add(timer);
|
||||
},
|
||||
{
|
||||
loginWithClaudeAi: true, // Always use Claude AI for subscription tokens
|
||||
inferenceOnly: true,
|
||||
expiresIn: 365 * 24 * 60 * 60, // 1 year
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Show processing state
|
||||
setOAuthStatus({ state: 'processing' })
|
||||
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)
|
||||
saveOAuthTokensIfNeeded(result);
|
||||
|
||||
// For OAuth flow, the access token can be used as an API key
|
||||
const timer1 = setTimeout(
|
||||
(setOAuthStatus, accessToken, onSuccess, timersRef) => {
|
||||
setOAuthStatus({ state: 'success', token: accessToken })
|
||||
setOAuthStatus({ state: 'success', token: accessToken });
|
||||
// Auto-continue after brief delay to show success
|
||||
const timer2 = setTimeout(onSuccess, 1000, accessToken)
|
||||
timersRef.current.add(timer2 as unknown as NodeJS.Timeout)
|
||||
const timer2 = setTimeout(onSuccess, 1000, accessToken);
|
||||
timersRef.current.add(timer2 as unknown as NodeJS.Timeout);
|
||||
},
|
||||
100,
|
||||
setOAuthStatus,
|
||||
result.accessToken,
|
||||
onSuccess,
|
||||
timersRef,
|
||||
)
|
||||
timersRef.current.add(timer1)
|
||||
);
|
||||
timersRef.current.add(timer1);
|
||||
} catch (err) {
|
||||
const errorMessage = (err as Error).message
|
||||
const errorMessage = (err as Error).message;
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: errorMessage,
|
||||
toRetry: { state: 'starting' }, // Allow retry by starting fresh OAuth flow
|
||||
})
|
||||
logError(err)
|
||||
});
|
||||
logError(err);
|
||||
logEvent('tengu_oauth_error', {
|
||||
error:
|
||||
errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
}
|
||||
}, [oauthService, onSuccess])
|
||||
}, [oauthService, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (oauthStatus.state === 'starting') {
|
||||
void startOAuth()
|
||||
void startOAuth();
|
||||
}
|
||||
}, [oauthStatus.state, startOAuth])
|
||||
}, [oauthStatus.state, startOAuth]);
|
||||
|
||||
// Retry logic
|
||||
useEffect(() => {
|
||||
@@ -163,46 +156,41 @@ export function OAuthFlowStep({
|
||||
const timer = setTimeout(
|
||||
(nextState, setShowPastePrompt, setOAuthStatus) => {
|
||||
// Only show paste prompt when retrying to waiting_for_login
|
||||
setShowPastePrompt(nextState.state === 'waiting_for_login')
|
||||
setOAuthStatus(nextState)
|
||||
setShowPastePrompt(nextState.state === 'waiting_for_login');
|
||||
setOAuthStatus(nextState);
|
||||
},
|
||||
500,
|
||||
oauthStatus.nextState,
|
||||
setShowPastePrompt,
|
||||
setOAuthStatus,
|
||||
)
|
||||
timersRef.current.add(timer)
|
||||
);
|
||||
timersRef.current.add(timer);
|
||||
}
|
||||
}, [oauthStatus])
|
||||
}, [oauthStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
pastedCode === 'c' &&
|
||||
oauthStatus.state === 'waiting_for_login' &&
|
||||
showPastePrompt &&
|
||||
!urlCopied
|
||||
) {
|
||||
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('')
|
||||
if (raw) process.stdout.write(raw);
|
||||
setUrlCopied(true);
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false);
|
||||
});
|
||||
setPastedCode('');
|
||||
}
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied])
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied]);
|
||||
|
||||
// Cleanup OAuth service and timers when component unmounts
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current
|
||||
const timers = timersRef.current;
|
||||
return () => {
|
||||
oauthService.cleanup()
|
||||
oauthService.cleanup();
|
||||
// Clear all timers
|
||||
timers.forEach(timer => clearTimeout(timer))
|
||||
timers.clear()
|
||||
clearTimeout(urlCopiedTimerRef.current)
|
||||
}
|
||||
}, [oauthService])
|
||||
timers.forEach(timer => clearTimeout(timer));
|
||||
timers.clear();
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
};
|
||||
}, [oauthService]);
|
||||
|
||||
// Helper function to render the appropriate status message
|
||||
function renderStatusMessage(): React.ReactNode {
|
||||
@@ -213,7 +201,7 @@ export function OAuthFlowStep({
|
||||
<Spinner />
|
||||
<Text>Starting authentication…</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
case 'waiting_for_login':
|
||||
return (
|
||||
@@ -221,9 +209,7 @@ export function OAuthFlowStep({
|
||||
{!showPastePrompt && (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>
|
||||
Opening browser to sign in with your Claude account…
|
||||
</Text>
|
||||
<Text>Opening browser to sign in with your Claude account…</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -233,9 +219,7 @@ export function OAuthFlowStep({
|
||||
<TextInput
|
||||
value={pastedCode}
|
||||
onChange={setPastedCode}
|
||||
onSubmit={(value: string) =>
|
||||
handleSubmitCode(value, oauthStatus.url)
|
||||
}
|
||||
onSubmit={(value: string) => handleSubmitCode(value, oauthStatus.url)}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
columns={textInputColumns}
|
||||
@@ -243,7 +227,7 @@ export function OAuthFlowStep({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
case 'processing':
|
||||
return (
|
||||
@@ -251,52 +235,42 @@ export function OAuthFlowStep({
|
||||
<Spinner />
|
||||
<Text>Processing authentication…</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
case 'success':
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="success">
|
||||
✓ Authentication token created successfully!
|
||||
</Text>
|
||||
<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 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 null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<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}>
|
||||
@@ -305,21 +279,17 @@ export function OAuthFlowStep({
|
||||
</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>
|
||||
)}
|
||||
{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't open? Use the url below to sign in{' '}
|
||||
</Text>
|
||||
<Text dimColor>Browser didn't open? Use the url below to sign in </Text>
|
||||
{urlCopied ? (
|
||||
<Text color="success">(Copied!)</Text>
|
||||
) : (
|
||||
@@ -337,5 +307,5 @@ export function OAuthFlowStep({
|
||||
{renderStatusMessage()}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import React from 'react';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
|
||||
type SuccessStepProps = {
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
}
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
};
|
||||
|
||||
export function SuccessStep({
|
||||
secretExists,
|
||||
@@ -21,14 +21,10 @@ export function SuccessStep({
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Success</Text>
|
||||
</Box>
|
||||
{!skipWorkflow && (
|
||||
<Text color="success">✓ GitHub Actions workflow created!</Text>
|
||||
)}
|
||||
{!skipWorkflow && <Text color="success">✓ GitHub Actions workflow created!</Text>}
|
||||
{secretExists && useExistingSecret && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">
|
||||
✓ Using existing ANTHROPIC_API_KEY secret
|
||||
</Text>
|
||||
<Text color="success">✓ Using existing ANTHROPIC_API_KEY secret</Text>
|
||||
</Box>
|
||||
)}
|
||||
{(!secretExists || !useExistingSecret) && (
|
||||
@@ -41,18 +37,14 @@ export function SuccessStep({
|
||||
</Box>
|
||||
{skipWorkflow ? (
|
||||
<>
|
||||
<Text>
|
||||
1. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>1. Install the Claude GitHub App if you haven't already</Text>
|
||||
<Text>2. Your workflow file was kept unchanged</Text>
|
||||
<Text>3. API key is configured and ready to use</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>1. A pre-filled PR page has been created</Text>
|
||||
<Text>
|
||||
2. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>2. Install the Claude GitHub App if you haven't already</Text>
|
||||
<Text>3. Merge the PR to enable Claude PR assistance</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -61,5 +53,5 @@ export function SuccessStep({
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Warning } from './types.js'
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Warning } from './types.js';
|
||||
|
||||
interface WarningsStepProps {
|
||||
warnings: Warning[]
|
||||
onContinue: () => void
|
||||
warnings: Warning[];
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
export function WarningsStep({ warnings, onContinue }: WarningsStepProps) {
|
||||
// Enter to continue
|
||||
useKeybinding('confirm:yes', onContinue, { context: 'Confirmation' })
|
||||
useKeybinding('confirm:yes', onContinue, { context: 'Confirmation' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>{figures.warning} Setup Warnings</Text>
|
||||
<Text dimColor>
|
||||
We found some potential issues, but you can continue anyway
|
||||
</Text>
|
||||
<Text dimColor>We found some potential issues, but you can continue anyway</Text>
|
||||
</Box>
|
||||
|
||||
{warnings.map((warning, index) => (
|
||||
@@ -55,5 +53,5 @@ export function WarningsStep({ warnings, onContinue }: WarningsStepProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { execa } from 'execa'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { execa } from 'execa';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { type KeyboardEvent, Box } from '@anthropic/ink'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { getGithubRepo } from '../../utils/git.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { ApiKeyStep } from './ApiKeyStep.js'
|
||||
import { CheckExistingSecretStep } from './CheckExistingSecretStep.js'
|
||||
import { CheckGitHubStep } from './CheckGitHubStep.js'
|
||||
import { ChooseRepoStep } from './ChooseRepoStep.js'
|
||||
import { CreatingStep } from './CreatingStep.js'
|
||||
import { ErrorStep } from './ErrorStep.js'
|
||||
import { ExistingWorkflowStep } from './ExistingWorkflowStep.js'
|
||||
import { InstallAppStep } from './InstallAppStep.js'
|
||||
import { OAuthFlowStep } from './OAuthFlowStep.js'
|
||||
import { SuccessStep } from './SuccessStep.js'
|
||||
import { setupGitHubActions } from './setupGitHubActions.js'
|
||||
import type { State, Warning, Workflow } from './types.js'
|
||||
import { WarningsStep } from './WarningsStep.js'
|
||||
} from 'src/services/analytics/index.js';
|
||||
import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { type KeyboardEvent, Box } from '@anthropic/ink';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
|
||||
import { getGithubRepo } from '../../utils/git.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { ApiKeyStep } from './ApiKeyStep.js';
|
||||
import { CheckExistingSecretStep } from './CheckExistingSecretStep.js';
|
||||
import { CheckGitHubStep } from './CheckGitHubStep.js';
|
||||
import { ChooseRepoStep } from './ChooseRepoStep.js';
|
||||
import { CreatingStep } from './CreatingStep.js';
|
||||
import { ErrorStep } from './ErrorStep.js';
|
||||
import { ExistingWorkflowStep } from './ExistingWorkflowStep.js';
|
||||
import { InstallAppStep } from './InstallAppStep.js';
|
||||
import { OAuthFlowStep } from './OAuthFlowStep.js';
|
||||
import { SuccessStep } from './SuccessStep.js';
|
||||
import { setupGitHubActions } from './setupGitHubActions.js';
|
||||
import type { State, Warning, Workflow } from './types.js';
|
||||
import { WarningsStep } from './WarningsStep.js';
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
step: 'check-gh',
|
||||
@@ -44,54 +44,50 @@ const INITIAL_STATE: State = {
|
||||
selectedWorkflows: ['claude', 'claude-review'] as Workflow[],
|
||||
selectedApiKeyOption: 'new' as 'existing' | 'new' | 'oauth',
|
||||
authType: 'api_key',
|
||||
}
|
||||
};
|
||||
|
||||
function InstallGitHubApp(props: {
|
||||
onDone: (message: string) => void
|
||||
}): React.ReactNode {
|
||||
const [existingApiKey] = useState(() => getAnthropicApiKey())
|
||||
function InstallGitHubApp(props: { onDone: (message: string) => void }): React.ReactNode {
|
||||
const [existingApiKey] = useState(() => getAnthropicApiKey());
|
||||
const [state, setState] = useState({
|
||||
...INITIAL_STATE,
|
||||
useExistingKey: !!existingApiKey,
|
||||
selectedApiKeyOption: (existingApiKey
|
||||
? 'existing'
|
||||
: isAnthropicAuthEnabled()
|
||||
? 'oauth'
|
||||
: 'new') as 'existing' | 'new' | 'oauth',
|
||||
})
|
||||
useExitOnCtrlCDWithKeybindings()
|
||||
selectedApiKeyOption: (existingApiKey ? 'existing' : isAnthropicAuthEnabled() ? 'oauth' : 'new') as
|
||||
| 'existing'
|
||||
| 'new'
|
||||
| 'oauth',
|
||||
});
|
||||
useExitOnCtrlCDWithKeybindings();
|
||||
|
||||
React.useEffect(() => {
|
||||
logEvent('tengu_install_github_app_started', {})
|
||||
}, [])
|
||||
logEvent('tengu_install_github_app_started', {});
|
||||
}, []);
|
||||
|
||||
const checkGitHubCLI = useCallback(async () => {
|
||||
const warnings: Warning[] = []
|
||||
const warnings: Warning[] = [];
|
||||
|
||||
// Check if gh is installed
|
||||
const ghVersionResult = await execa('gh --version', {
|
||||
shell: true,
|
||||
reject: false,
|
||||
})
|
||||
});
|
||||
if (ghVersionResult.exitCode !== 0) {
|
||||
warnings.push({
|
||||
title: 'GitHub CLI not found',
|
||||
message:
|
||||
'GitHub CLI (gh) does not appear to be installed or accessible.',
|
||||
message: 'GitHub CLI (gh) does not appear to be installed or accessible.',
|
||||
instructions: [
|
||||
'Install GitHub CLI from https://cli.github.com/',
|
||||
'macOS: brew install gh',
|
||||
'Windows: winget install --id GitHub.cli',
|
||||
'Linux: See installation instructions at https://github.com/cli/cli#installation',
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Check auth status
|
||||
const authResult = await execa('gh auth status -a', {
|
||||
shell: true,
|
||||
reject: false,
|
||||
})
|
||||
});
|
||||
if (authResult.exitCode !== 0) {
|
||||
warnings.push({
|
||||
title: 'GitHub CLI not authenticated',
|
||||
@@ -101,19 +97,19 @@ function InstallGitHubApp(props: {
|
||||
'Follow the prompts to authenticate with GitHub',
|
||||
'Or set up authentication using environment variables or other methods',
|
||||
],
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// Check if required scopes are present in the Token scopes line
|
||||
const tokenScopesMatch = authResult.stdout.match(/Token scopes:.*$/m)
|
||||
const tokenScopesMatch = authResult.stdout.match(/Token scopes:.*$/m);
|
||||
if (tokenScopesMatch) {
|
||||
const scopes = tokenScopesMatch[0]
|
||||
const missingScopes: string[] = []
|
||||
const scopes = tokenScopesMatch[0];
|
||||
const missingScopes: string[] = [];
|
||||
|
||||
if (!scopes.includes('repo')) {
|
||||
missingScopes.push('repo')
|
||||
missingScopes.push('repo');
|
||||
}
|
||||
if (!scopes.includes('workflow')) {
|
||||
missingScopes.push('workflow')
|
||||
missingScopes.push('workflow');
|
||||
}
|
||||
|
||||
if (missingScopes.length > 0) {
|
||||
@@ -131,18 +127,18 @@ function InstallGitHubApp(props: {
|
||||
'',
|
||||
'This will add the necessary permissions to manage workflows and secrets.',
|
||||
],
|
||||
}))
|
||||
return
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if in a git repo and get remote URL
|
||||
const currentRepo = (await getGithubRepo()) ?? ''
|
||||
const currentRepo = (await getGithubRepo()) ?? '';
|
||||
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'check-gh' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
@@ -151,14 +147,14 @@ function InstallGitHubApp(props: {
|
||||
selectedRepoName: currentRepo,
|
||||
useCurrentRepo: !!currentRepo, // Set to false if no repo detected
|
||||
step: warnings.length > 0 ? 'warnings' : 'choose-repo',
|
||||
}))
|
||||
}, [])
|
||||
}));
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.step === 'check-gh') {
|
||||
void checkGitHubCLI()
|
||||
void checkGitHubCLI();
|
||||
}
|
||||
}, [state.step, checkGitHubCLI])
|
||||
}, [state.step, checkGitHubCLI]);
|
||||
|
||||
const runSetupGitHubActions = useCallback(
|
||||
async (apiKeyOrOAuthToken: string | null, secretName: string) => {
|
||||
@@ -166,7 +162,7 @@ function InstallGitHubApp(props: {
|
||||
...prev,
|
||||
step: 'creating',
|
||||
currentWorkflowInstallStep: 0,
|
||||
}))
|
||||
}));
|
||||
|
||||
try {
|
||||
await setupGitHubActions(
|
||||
@@ -177,7 +173,7 @@ function InstallGitHubApp(props: {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflowInstallStep: prev.currentWorkflowInstallStep + 1,
|
||||
}))
|
||||
}));
|
||||
},
|
||||
state.workflowAction === 'skip',
|
||||
state.selectedWorkflows,
|
||||
@@ -187,22 +183,18 @@ function InstallGitHubApp(props: {
|
||||
workflowExists: state.workflowExists,
|
||||
secretExists: state.secretExists,
|
||||
},
|
||||
)
|
||||
);
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'creating' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setState(prev => ({ ...prev, step: 'success' }))
|
||||
});
|
||||
setState(prev => ({ ...prev, step: 'success' }));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to set up GitHub Actions'
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to set up GitHub Actions';
|
||||
|
||||
if (errorMessage.includes('workflow file already exists')) {
|
||||
logEvent('tengu_install_github_app_error', {
|
||||
reason:
|
||||
'workflow_file_exists' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
reason: 'workflow_file_exists' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
step: 'error',
|
||||
@@ -215,12 +207,11 @@ function InstallGitHubApp(props: {
|
||||
' 2. Update the existing file manually using the template from:',
|
||||
` ${GITHUB_ACTION_SETUP_DOCS_URL}`,
|
||||
],
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
logEvent('tengu_install_github_app_error', {
|
||||
reason:
|
||||
'setup_github_actions_failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
reason: 'setup_github_actions_failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
@@ -228,7 +219,7 @@ function InstallGitHubApp(props: {
|
||||
error: errorMessage,
|
||||
errorReason: 'GitHub Actions setup failed',
|
||||
errorInstructions: [],
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -241,42 +232,32 @@ function InstallGitHubApp(props: {
|
||||
state.secretExists,
|
||||
state.authType,
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
async function openGitHubAppInstallation() {
|
||||
const installUrl = 'https://github.com/apps/claude'
|
||||
await openBrowser(installUrl)
|
||||
const installUrl = 'https://github.com/apps/claude';
|
||||
await openBrowser(installUrl);
|
||||
}
|
||||
|
||||
async function checkRepositoryPermissions(
|
||||
repoName: string,
|
||||
): Promise<{ hasAccess: boolean; error?: string }> {
|
||||
async function checkRepositoryPermissions(repoName: string): Promise<{ hasAccess: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
`repos/${repoName}`,
|
||||
'--jq',
|
||||
'.permissions.admin',
|
||||
])
|
||||
const result = await execFileNoThrow('gh', ['api', `repos/${repoName}`, '--jq', '.permissions.admin']);
|
||||
|
||||
if (result.code === 0) {
|
||||
const hasAdmin = result.stdout.trim() === 'true'
|
||||
return { hasAccess: hasAdmin }
|
||||
const hasAdmin = result.stdout.trim() === 'true';
|
||||
return { hasAccess: hasAdmin };
|
||||
}
|
||||
|
||||
if (
|
||||
result.stderr.includes('404') ||
|
||||
result.stderr.includes('Not Found')
|
||||
) {
|
||||
if (result.stderr.includes('404') || result.stderr.includes('Not Found')) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
error: 'repository_not_found',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { hasAccess: false }
|
||||
return { hasAccess: false };
|
||||
} catch {
|
||||
return { hasAccess: false }
|
||||
return { hasAccess: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +267,9 @@ function InstallGitHubApp(props: {
|
||||
`repos/${repoName}/contents/.github/workflows/claude.yml`,
|
||||
'--jq',
|
||||
'.sha',
|
||||
])
|
||||
]);
|
||||
|
||||
return checkFileResult.code === 0
|
||||
return checkFileResult.code === 0;
|
||||
}
|
||||
|
||||
async function checkExistingSecret() {
|
||||
@@ -299,20 +280,20 @@ function InstallGitHubApp(props: {
|
||||
'actions',
|
||||
'--repo',
|
||||
state.selectedRepoName,
|
||||
])
|
||||
]);
|
||||
|
||||
if (checkSecretsResult.code === 0) {
|
||||
const lines = checkSecretsResult.stdout.split('\n')
|
||||
const lines = checkSecretsResult.stdout.split('\n');
|
||||
const hasAnthropicKey = lines.some((line: string) => {
|
||||
return /^ANTHROPIC_API_KEY\s+/.test(line)
|
||||
})
|
||||
return /^ANTHROPIC_API_KEY\s+/.test(line);
|
||||
});
|
||||
|
||||
if (hasAnthropicKey) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
secretExists: true,
|
||||
step: 'check-existing-secret',
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
// No existing secret found
|
||||
if (existingApiKey) {
|
||||
@@ -321,11 +302,11 @@ function InstallGitHubApp(props: {
|
||||
...prev,
|
||||
apiKeyOrOAuthToken: existingApiKey,
|
||||
useExistingKey: true,
|
||||
}))
|
||||
await runSetupGitHubActions(existingApiKey, state.secretName)
|
||||
}));
|
||||
await runSetupGitHubActions(existingApiKey, state.secretName);
|
||||
} else {
|
||||
// No local key, go to API key step
|
||||
setState(prev => ({ ...prev, step: 'api-key' }))
|
||||
setState(prev => ({ ...prev, step: 'api-key' }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -336,11 +317,11 @@ function InstallGitHubApp(props: {
|
||||
...prev,
|
||||
apiKeyOrOAuthToken: existingApiKey,
|
||||
useExistingKey: true,
|
||||
}))
|
||||
await runSetupGitHubActions(existingApiKey, state.secretName)
|
||||
}));
|
||||
await runSetupGitHubActions(existingApiKey, state.secretName);
|
||||
} else {
|
||||
// No local key, go to API key step
|
||||
setState(prev => ({ ...prev, step: 'api-key' }))
|
||||
setState(prev => ({ ...prev, step: 'api-key' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,33 +330,28 @@ function InstallGitHubApp(props: {
|
||||
if (state.step === 'warnings') {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'warnings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setState(prev => ({ ...prev, step: 'install-app' }))
|
||||
setTimeout(openGitHubAppInstallation, 0)
|
||||
});
|
||||
setState(prev => ({ ...prev, step: 'install-app' }));
|
||||
setTimeout(openGitHubAppInstallation, 0);
|
||||
} else if (state.step === 'choose-repo') {
|
||||
let repoName = state.useCurrentRepo
|
||||
? state.currentRepo
|
||||
: state.selectedRepoName
|
||||
let repoName = state.useCurrentRepo ? state.currentRepo : state.selectedRepoName;
|
||||
|
||||
if (!repoName.trim()) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const repoWarnings: Warning[] = []
|
||||
const repoWarnings: Warning[] = [];
|
||||
|
||||
if (repoName.includes('github.com')) {
|
||||
const match = repoName.match(/github\.com[:/]([^/]+\/[^/]+)(\.git)?$/)
|
||||
const match = repoName.match(/github\.com[:/]([^/]+\/[^/]+)(\.git)?$/);
|
||||
if (!match) {
|
||||
repoWarnings.push({
|
||||
title: 'Invalid GitHub URL format',
|
||||
message: 'The repository URL format appears to be invalid.',
|
||||
instructions: [
|
||||
'Use format: owner/repo or https://github.com/owner/repo',
|
||||
'Example: anthropics/claude-cli',
|
||||
],
|
||||
})
|
||||
instructions: ['Use format: owner/repo or https://github.com/owner/repo', 'Example: anthropics/claude-cli'],
|
||||
});
|
||||
} else {
|
||||
repoName = match[1]?.replace(/\.git$/, '') || ''
|
||||
repoName = match[1]?.replace(/\.git$/, '') || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,14 +359,11 @@ function InstallGitHubApp(props: {
|
||||
repoWarnings.push({
|
||||
title: 'Repository format warning',
|
||||
message: 'Repository should be in format "owner/repo"',
|
||||
instructions: [
|
||||
'Use format: owner/repo',
|
||||
'Example: anthropics/claude-cli',
|
||||
],
|
||||
})
|
||||
instructions: ['Use format: owner/repo', 'Example: anthropics/claude-cli'],
|
||||
});
|
||||
}
|
||||
|
||||
const permissionCheck = await checkRepositoryPermissions(repoName)
|
||||
const permissionCheck = await checkRepositoryPermissions(repoName);
|
||||
|
||||
if (permissionCheck.error === 'repository_not_found') {
|
||||
repoWarnings.push({
|
||||
@@ -402,7 +375,7 @@ function InstallGitHubApp(props: {
|
||||
'For private repositories, make sure your GitHub token has the "repo" scope',
|
||||
'You can add the repo scope with: gh auth refresh -h github.com -s repo,workflow',
|
||||
],
|
||||
})
|
||||
});
|
||||
} else if (!permissionCheck.hasAccess) {
|
||||
repoWarnings.push({
|
||||
title: 'Admin permissions required',
|
||||
@@ -412,81 +385,77 @@ function InstallGitHubApp(props: {
|
||||
'Ask a repository admin to run this command if setup fails',
|
||||
'Alternatively, you can use the manual setup instructions',
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const workflowExists = await checkExistingWorkflowFile(repoName)
|
||||
const workflowExists = await checkExistingWorkflowFile(repoName);
|
||||
|
||||
if (repoWarnings.length > 0) {
|
||||
const allWarnings = [...state.warnings, ...repoWarnings]
|
||||
const allWarnings = [...state.warnings, ...repoWarnings];
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selectedRepoName: repoName,
|
||||
workflowExists,
|
||||
warnings: allWarnings,
|
||||
step: 'warnings',
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'choose-repo' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selectedRepoName: repoName,
|
||||
workflowExists,
|
||||
step: 'install-app',
|
||||
}))
|
||||
setTimeout(openGitHubAppInstallation, 0)
|
||||
}));
|
||||
setTimeout(openGitHubAppInstallation, 0);
|
||||
}
|
||||
} else if (state.step === 'install-app') {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'install-app' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
if (state.workflowExists) {
|
||||
setState(prev => ({ ...prev, step: 'check-existing-workflow' }))
|
||||
setState(prev => ({ ...prev, step: 'check-existing-workflow' }));
|
||||
} else {
|
||||
setState(prev => ({ ...prev, step: 'select-workflows' }))
|
||||
setState(prev => ({ ...prev, step: 'select-workflows' }));
|
||||
}
|
||||
} else if (state.step === 'check-existing-workflow') {
|
||||
return
|
||||
return;
|
||||
} else if (state.step === 'select-workflows') {
|
||||
// Handled by the WorkflowMultiselectDialog component
|
||||
return
|
||||
return;
|
||||
} else if (state.step === 'check-existing-secret') {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'check-existing-secret' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
if (state.useExistingSecret) {
|
||||
await runSetupGitHubActions(null, state.secretName)
|
||||
await runSetupGitHubActions(null, state.secretName);
|
||||
} else {
|
||||
// User wants to use a new secret name with their API key
|
||||
await runSetupGitHubActions(state.apiKeyOrOAuthToken, state.secretName)
|
||||
await runSetupGitHubActions(state.apiKeyOrOAuthToken, state.secretName);
|
||||
}
|
||||
} else if (state.step === 'api-key') {
|
||||
// In the new flow, api-key step only appears when user has no existing key
|
||||
// They either entered a new key or will create OAuth token
|
||||
if (state.selectedApiKeyOption === 'oauth') {
|
||||
// OAuth flow already handled by handleCreateOAuthToken
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// If user selected 'existing' option, use the existing API key
|
||||
const apiKeyToUse =
|
||||
state.selectedApiKeyOption === 'existing'
|
||||
? existingApiKey
|
||||
: state.apiKeyOrOAuthToken
|
||||
const apiKeyToUse = state.selectedApiKeyOption === 'existing' ? existingApiKey : state.apiKeyOrOAuthToken;
|
||||
|
||||
if (!apiKeyToUse) {
|
||||
logEvent('tengu_install_github_app_error', {
|
||||
reason:
|
||||
'api_key_missing' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
reason: 'api_key_missing' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
step: 'error',
|
||||
error: 'API key is required',
|
||||
}))
|
||||
return
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the API key being used (either existing or newly entered)
|
||||
@@ -494,7 +463,7 @@ function InstallGitHubApp(props: {
|
||||
...prev,
|
||||
apiKeyOrOAuthToken: apiKeyToUse,
|
||||
useExistingKey: state.selectedApiKeyOption === 'existing',
|
||||
}))
|
||||
}));
|
||||
|
||||
// Check if ANTHROPIC_API_KEY secret already exists
|
||||
const checkSecretsResult = await execFileNoThrow('gh', [
|
||||
@@ -504,132 +473,132 @@ function InstallGitHubApp(props: {
|
||||
'actions',
|
||||
'--repo',
|
||||
state.selectedRepoName,
|
||||
])
|
||||
]);
|
||||
|
||||
if (checkSecretsResult.code === 0) {
|
||||
const lines = checkSecretsResult.stdout.split('\n')
|
||||
const lines = checkSecretsResult.stdout.split('\n');
|
||||
const hasAnthropicKey = lines.some((line: string) => {
|
||||
return /^ANTHROPIC_API_KEY\s+/.test(line)
|
||||
})
|
||||
return /^ANTHROPIC_API_KEY\s+/.test(line);
|
||||
});
|
||||
|
||||
if (hasAnthropicKey) {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'api-key' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
secretExists: true,
|
||||
step: 'check-existing-secret',
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'api-key' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
// No existing secret, proceed to creating
|
||||
await runSetupGitHubActions(apiKeyToUse, state.secretName)
|
||||
await runSetupGitHubActions(apiKeyToUse, state.secretName);
|
||||
}
|
||||
} else {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'api-key' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
// Error checking secrets, proceed anyway
|
||||
await runSetupGitHubActions(apiKeyToUse, state.secretName)
|
||||
await runSetupGitHubActions(apiKeyToUse, state.secretName);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepoUrlChange = (value: string) => {
|
||||
setState(prev => ({ ...prev, selectedRepoName: value }))
|
||||
}
|
||||
setState(prev => ({ ...prev, selectedRepoName: value }));
|
||||
};
|
||||
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setState(prev => ({ ...prev, apiKeyOrOAuthToken: value }))
|
||||
}
|
||||
setState(prev => ({ ...prev, apiKeyOrOAuthToken: value }));
|
||||
};
|
||||
|
||||
const handleApiKeyOptionChange = (option: 'existing' | 'new' | 'oauth') => {
|
||||
setState(prev => ({ ...prev, selectedApiKeyOption: option }))
|
||||
}
|
||||
setState(prev => ({ ...prev, selectedApiKeyOption: option }));
|
||||
};
|
||||
|
||||
const handleCreateOAuthToken = useCallback(() => {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'api-key' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setState(prev => ({ ...prev, step: 'oauth-flow' }))
|
||||
}, [])
|
||||
});
|
||||
setState(prev => ({ ...prev, step: 'oauth-flow' }));
|
||||
}, []);
|
||||
|
||||
const handleOAuthSuccess = useCallback(
|
||||
(token: string) => {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'oauth-flow' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
apiKeyOrOAuthToken: token,
|
||||
useExistingKey: false,
|
||||
secretName: 'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
authType: 'oauth_token',
|
||||
}))
|
||||
void runSetupGitHubActions(token, 'CLAUDE_CODE_OAUTH_TOKEN')
|
||||
}));
|
||||
void runSetupGitHubActions(token, 'CLAUDE_CODE_OAUTH_TOKEN');
|
||||
},
|
||||
[runSetupGitHubActions],
|
||||
)
|
||||
);
|
||||
|
||||
const handleOAuthCancel = useCallback(() => {
|
||||
setState(prev => ({ ...prev, step: 'api-key' }))
|
||||
}, [])
|
||||
setState(prev => ({ ...prev, step: 'api-key' }));
|
||||
}, []);
|
||||
|
||||
const handleSecretNameChange = (value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9_]+$/.test(value)) return
|
||||
setState(prev => ({ ...prev, secretName: value }))
|
||||
}
|
||||
if (value && !/^[a-zA-Z0-9_]+$/.test(value)) return;
|
||||
setState(prev => ({ ...prev, secretName: value }));
|
||||
};
|
||||
|
||||
const handleToggleUseCurrentRepo = (useCurrentRepo: boolean) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
useCurrentRepo,
|
||||
selectedRepoName: useCurrentRepo ? prev.currentRepo : '',
|
||||
}))
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleToggleUseExistingKey = (useExistingKey: boolean) => {
|
||||
setState(prev => ({ ...prev, useExistingKey }))
|
||||
}
|
||||
setState(prev => ({ ...prev, useExistingKey }));
|
||||
};
|
||||
|
||||
const handleToggleUseExistingSecret = (useExistingSecret: boolean) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
useExistingSecret,
|
||||
secretName: useExistingSecret ? 'ANTHROPIC_API_KEY' : '',
|
||||
}))
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleWorkflowAction = async (action: 'update' | 'skip' | 'exit') => {
|
||||
if (action === 'exit') {
|
||||
props.onDone('Installation cancelled by user')
|
||||
return
|
||||
props.onDone('Installation cancelled by user');
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'check-existing-workflow' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, workflowAction: action }))
|
||||
setState(prev => ({ ...prev, workflowAction: action }));
|
||||
|
||||
if (action === 'skip' || action === 'update') {
|
||||
// Check if user has existing local API key
|
||||
if (existingApiKey) {
|
||||
await checkExistingSecret()
|
||||
await checkExistingSecret();
|
||||
} else {
|
||||
// No local key, go straight to API key step
|
||||
setState(prev => ({ ...prev, step: 'api-key' }))
|
||||
setState(prev => ({ ...prev, step: 'api-key' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleDismissKeyDown(e: KeyboardEvent): void {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
if (state.step === 'success') {
|
||||
logEvent('tengu_install_github_app_completed', {})
|
||||
logEvent('tengu_install_github_app_completed', {});
|
||||
}
|
||||
props.onDone(
|
||||
state.step === 'success'
|
||||
@@ -637,16 +606,14 @@ function InstallGitHubApp(props: {
|
||||
: state.error
|
||||
? `Couldn't install GitHub App: ${state.error}\nFor manual setup instructions, see: ${GITHUB_ACTION_SETUP_DOCS_URL}`
|
||||
: `GitHub App installation failed\nFor manual setup instructions, see: ${GITHUB_ACTION_SETUP_DOCS_URL}`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
switch (state.step) {
|
||||
case 'check-gh':
|
||||
return <CheckGitHubStep />
|
||||
return <CheckGitHubStep />;
|
||||
case 'warnings':
|
||||
return (
|
||||
<WarningsStep warnings={state.warnings} onContinue={handleSubmit} />
|
||||
)
|
||||
return <WarningsStep warnings={state.warnings} onContinue={handleSubmit} />;
|
||||
case 'choose-repo':
|
||||
return (
|
||||
<ChooseRepoStep
|
||||
@@ -657,21 +624,11 @@ function InstallGitHubApp(props: {
|
||||
onToggleUseCurrentRepo={handleToggleUseCurrentRepo}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'install-app':
|
||||
return (
|
||||
<InstallAppStep
|
||||
repoUrl={state.selectedRepoName}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)
|
||||
return <InstallAppStep repoUrl={state.selectedRepoName} onSubmit={handleSubmit} />;
|
||||
case 'check-existing-workflow':
|
||||
return (
|
||||
<ExistingWorkflowStep
|
||||
repoName={state.selectedRepoName}
|
||||
onSelectAction={handleWorkflowAction}
|
||||
/>
|
||||
)
|
||||
return <ExistingWorkflowStep repoName={state.selectedRepoName} onSelectAction={handleWorkflowAction} />;
|
||||
case 'check-existing-secret':
|
||||
return (
|
||||
<CheckExistingSecretStep
|
||||
@@ -681,7 +638,7 @@ function InstallGitHubApp(props: {
|
||||
onSecretNameChange={handleSecretNameChange}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'api-key':
|
||||
return (
|
||||
<ApiKeyStep
|
||||
@@ -691,13 +648,11 @@ function InstallGitHubApp(props: {
|
||||
onApiKeyChange={handleApiKeyChange}
|
||||
onToggleUseExistingKey={handleToggleUseExistingKey}
|
||||
onSubmit={handleSubmit}
|
||||
onCreateOAuthToken={
|
||||
isAnthropicAuthEnabled() ? handleCreateOAuthToken : undefined
|
||||
}
|
||||
onCreateOAuthToken={isAnthropicAuthEnabled() ? handleCreateOAuthToken : undefined}
|
||||
selectedOption={state.selectedApiKeyOption}
|
||||
onSelectOption={handleApiKeyOptionChange}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'creating':
|
||||
return (
|
||||
<CreatingStep
|
||||
@@ -708,7 +663,7 @@ function InstallGitHubApp(props: {
|
||||
skipWorkflow={state.workflowAction === 'skip'}
|
||||
selectedWorkflows={state.selectedWorkflows}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'success':
|
||||
return (
|
||||
<Box tabIndex={0} autoFocus onKeyDown={handleDismissKeyDown}>
|
||||
@@ -719,17 +674,13 @@ function InstallGitHubApp(props: {
|
||||
skipWorkflow={state.workflowAction === 'skip'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Box tabIndex={0} autoFocus onKeyDown={handleDismissKeyDown}>
|
||||
<ErrorStep
|
||||
error={state.error}
|
||||
errorReason={state.errorReason}
|
||||
errorInstructions={state.errorInstructions}
|
||||
/>
|
||||
<ErrorStep error={state.error} errorReason={state.errorReason} errorInstructions={state.errorInstructions} />
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
case 'select-workflows':
|
||||
return (
|
||||
<WorkflowMultiselectDialog
|
||||
@@ -737,33 +688,26 @@ function InstallGitHubApp(props: {
|
||||
onSubmit={selectedWorkflows => {
|
||||
logEvent('tengu_install_github_app_step_completed', {
|
||||
step: 'select-workflows' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selectedWorkflows,
|
||||
}))
|
||||
}));
|
||||
// Check if user has existing local API key
|
||||
if (existingApiKey) {
|
||||
void checkExistingSecret()
|
||||
void checkExistingSecret();
|
||||
} else {
|
||||
// No local key, go straight to API key step
|
||||
setState(prev => ({ ...prev, step: 'api-key' }))
|
||||
setState(prev => ({ ...prev, step: 'api-key' }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
case 'oauth-flow':
|
||||
return (
|
||||
<OAuthFlowStep
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onCancel={handleOAuthCancel}
|
||||
/>
|
||||
)
|
||||
return <OAuthFlowStep onSuccess={handleOAuthSuccess} onCancel={handleOAuthCancel} />;
|
||||
}
|
||||
}
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <InstallGitHubApp onDone={onDone} />
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <InstallGitHubApp onDone={onDone} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user