rhodawk-ai-devops-engine / vendor /openclaude /src /commands /install-github-app /install-github-app.tsx
| 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 } from '../../ink/events/keyboard-event.js'; | |
| import { Box } from '../../ink.js'; | |
| 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 { extractGitHubRepoSlug } from './repoSlug.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', | |
| selectedRepoName: '', | |
| currentRepo: '', | |
| useCurrentRepo: false, | |
| // Default to false, will be set to true if repo detected | |
| apiKeyOrOAuthToken: '', | |
| useExistingKey: true, | |
| currentWorkflowInstallStep: 0, | |
| warnings: [], | |
| secretExists: false, | |
| secretName: 'ANTHROPIC_API_KEY', | |
| useExistingSecret: true, | |
| workflowExists: false, | |
| 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()); | |
| const [state, setState] = useState({ | |
| ...INITIAL_STATE, | |
| useExistingKey: !!existingApiKey, | |
| selectedApiKeyOption: (existingApiKey ? 'existing' : isAnthropicAuthEnabled() ? 'oauth' : 'new') as 'existing' | 'new' | 'oauth' | |
| }); | |
| useExitOnCtrlCDWithKeybindings(); | |
| React.useEffect(() => { | |
| logEvent('tengu_install_github_app_started', {}); | |
| }, []); | |
| const checkGitHubCLI = useCallback(async () => { | |
| 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.', | |
| 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', | |
| message: 'GitHub CLI does not appear to be authenticated.', | |
| instructions: ['Run: gh auth login', '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); | |
| if (tokenScopesMatch) { | |
| const scopes = tokenScopesMatch[0]; | |
| const missingScopes: string[] = []; | |
| if (!scopes.includes('repo')) { | |
| missingScopes.push('repo'); | |
| } | |
| if (!scopes.includes('workflow')) { | |
| missingScopes.push('workflow'); | |
| } | |
| if (missingScopes.length > 0) { | |
| // Missing required scopes - exit immediately | |
| setState(prev => ({ | |
| ...prev, | |
| step: 'error', | |
| error: `GitHub CLI is missing required permissions: ${missingScopes.join(', ')}.`, | |
| errorReason: 'Missing required scopes', | |
| errorInstructions: [`Your GitHub CLI authentication is missing the "${missingScopes.join('" and "')}" ${plural(missingScopes.length, 'scope')} needed to manage GitHub Actions and secrets.`, '', 'To fix this, run:', ' gh auth refresh -h github.com -s repo,workflow', '', 'This will add the necessary permissions to manage workflows and secrets.'] | |
| })); | |
| return; | |
| } | |
| } | |
| } | |
| // Check if in a git repo and get remote URL | |
| 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_0 => ({ | |
| ...prev_0, | |
| warnings, | |
| currentRepo, | |
| 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(); | |
| } | |
| }, [state.step, checkGitHubCLI]); | |
| const runSetupGitHubActions = useCallback(async (apiKeyOrOAuthToken: string | null, secretName: string) => { | |
| setState(prev_1 => ({ | |
| ...prev_1, | |
| step: 'creating', | |
| currentWorkflowInstallStep: 0 | |
| })); | |
| try { | |
| await setupGitHubActions(state.selectedRepoName, apiKeyOrOAuthToken, secretName, () => { | |
| setState(prev_4 => ({ | |
| ...prev_4, | |
| currentWorkflowInstallStep: prev_4.currentWorkflowInstallStep + 1 | |
| })); | |
| }, state.workflowAction === 'skip', state.selectedWorkflows, state.authType, { | |
| useCurrentRepo: state.useCurrentRepo, | |
| 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_5 => ({ | |
| ...prev_5, | |
| step: 'success' | |
| })); | |
| } catch (error) { | |
| 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 | |
| }); | |
| setState(prev_2 => ({ | |
| ...prev_2, | |
| step: 'error', | |
| error: 'A Claude workflow file already exists in this repository.', | |
| errorReason: 'Workflow file conflict', | |
| errorInstructions: ['The file .github/workflows/claude.yml already exists', 'You can either:', ' 1. Delete the existing file and run this command again', ' 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 | |
| }); | |
| setState(prev_3 => ({ | |
| ...prev_3, | |
| step: 'error', | |
| error: errorMessage, | |
| errorReason: 'GitHub Actions setup failed', | |
| errorInstructions: [] | |
| })); | |
| } | |
| } | |
| }, [state.selectedRepoName, state.workflowAction, state.selectedWorkflows, state.useCurrentRepo, state.workflowExists, state.secretExists, state.authType]); | |
| async function openGitHubAppInstallation() { | |
| const installUrl = 'https://github.com/apps/claude'; | |
| await openBrowser(installUrl); | |
| } | |
| async function checkRepositoryPermissions(repoName: string): Promise<{ | |
| hasAccess: boolean; | |
| error?: string; | |
| }> { | |
| try { | |
| const result = await execFileNoThrow('gh', ['api', `repos/${repoName}`, '--jq', '.permissions.admin']); | |
| if (result.code === 0) { | |
| const hasAdmin = result.stdout.trim() === 'true'; | |
| return { | |
| hasAccess: hasAdmin | |
| }; | |
| } | |
| if (result.stderr.includes('404') || result.stderr.includes('Not Found')) { | |
| return { | |
| hasAccess: false, | |
| error: 'repository_not_found' | |
| }; | |
| } | |
| return { | |
| hasAccess: false | |
| }; | |
| } catch { | |
| return { | |
| hasAccess: false | |
| }; | |
| } | |
| } | |
| async function checkExistingWorkflowFile(repoName_0: string): Promise<boolean> { | |
| const checkFileResult = await execFileNoThrow('gh', ['api', `repos/${repoName_0}/contents/.github/workflows/claude.yml`, '--jq', '.sha']); | |
| return checkFileResult.code === 0; | |
| } | |
| async function checkExistingSecret() { | |
| const checkSecretsResult = await execFileNoThrow('gh', ['secret', 'list', '--app', 'actions', '--repo', state.selectedRepoName]); | |
| if (checkSecretsResult.code === 0) { | |
| const lines = checkSecretsResult.stdout.split('\n'); | |
| const hasAnthropicKey = lines.some((line: string) => { | |
| return /^ANTHROPIC_API_KEY\s+/.test(line); | |
| }); | |
| if (hasAnthropicKey) { | |
| setState(prev_6 => ({ | |
| ...prev_6, | |
| secretExists: true, | |
| step: 'check-existing-secret' | |
| })); | |
| } else { | |
| // No existing secret found | |
| if (existingApiKey) { | |
| // User has local key, skip to creating with it | |
| setState(prev_7 => ({ | |
| ...prev_7, | |
| apiKeyOrOAuthToken: existingApiKey, | |
| useExistingKey: true | |
| })); | |
| await runSetupGitHubActions(existingApiKey, state.secretName); | |
| } else { | |
| // No local key, go to API key step | |
| setState(prev_8 => ({ | |
| ...prev_8, | |
| step: 'api-key' | |
| })); | |
| } | |
| } | |
| } else { | |
| // Error checking secrets | |
| if (existingApiKey) { | |
| // User has local key, skip to creating with it | |
| setState(prev_9 => ({ | |
| ...prev_9, | |
| apiKeyOrOAuthToken: existingApiKey, | |
| useExistingKey: true | |
| })); | |
| await runSetupGitHubActions(existingApiKey, state.secretName); | |
| } else { | |
| // No local key, go to API key step | |
| setState(prev_10 => ({ | |
| ...prev_10, | |
| step: 'api-key' | |
| })); | |
| } | |
| } | |
| } | |
| const handleSubmit = async () => { | |
| 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_11 => ({ | |
| ...prev_11, | |
| step: 'install-app' | |
| })); | |
| setTimeout(openGitHubAppInstallation, 0); | |
| } else if (state.step === 'choose-repo') { | |
| let repoName_1 = state.useCurrentRepo ? state.currentRepo : state.selectedRepoName; | |
| if (!repoName_1.trim()) { | |
| return; | |
| } | |
| const repoWarnings: Warning[] = []; | |
| const slug = extractGitHubRepoSlug(repoName_1); | |
| const isUrlLike = /^[a-z][a-z0-9+.-]*:\/\//i.test(repoName_1) || repoName_1.startsWith('www.'); | |
| if (slug) { | |
| repoName_1 = slug; | |
| } else if (isUrlLike) { | |
| 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'] | |
| }); | |
| } | |
| if (!repoName_1.includes('/')) { | |
| repoWarnings.push({ | |
| title: 'Repository format warning', | |
| message: 'Repository should be in format "owner/repo"', | |
| instructions: ['Use format: owner/repo', 'Example: anthropics/claude-cli'] | |
| }); | |
| } | |
| const permissionCheck = await checkRepositoryPermissions(repoName_1); | |
| if (permissionCheck.error === 'repository_not_found') { | |
| repoWarnings.push({ | |
| title: 'Repository not found', | |
| message: `Repository ${repoName_1} was not found or you don't have access.`, | |
| instructions: [`Check that the repository name is correct: ${repoName_1}`, 'Ensure you have access to this repository', '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', | |
| message: `You might need admin permissions on ${repoName_1} to set up GitHub Actions.`, | |
| instructions: ['Repository admins can install GitHub Apps and set secrets', 'Ask a repository admin to run this command if setup fails', 'Alternatively, you can use the manual setup instructions'] | |
| }); | |
| } | |
| const workflowExists = await checkExistingWorkflowFile(repoName_1); | |
| if (repoWarnings.length > 0) { | |
| const allWarnings = [...state.warnings, ...repoWarnings]; | |
| setState(prev_12 => ({ | |
| ...prev_12, | |
| selectedRepoName: repoName_1, | |
| 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_13 => ({ | |
| ...prev_13, | |
| selectedRepoName: repoName_1, | |
| workflowExists, | |
| step: 'install-app' | |
| })); | |
| 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_14 => ({ | |
| ...prev_14, | |
| step: 'check-existing-workflow' | |
| })); | |
| } else { | |
| setState(prev_15 => ({ | |
| ...prev_15, | |
| step: 'select-workflows' | |
| })); | |
| } | |
| } else if (state.step === 'check-existing-workflow') { | |
| return; | |
| } else if (state.step === 'select-workflows') { | |
| // Handled by the WorkflowMultiselectDialog component | |
| 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); | |
| } else { | |
| // User wants to use a new secret name with their API key | |
| 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; | |
| } | |
| // If user selected 'existing' option, use the existing API key | |
| 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 | |
| }); | |
| setState(prev_16 => ({ | |
| ...prev_16, | |
| step: 'error', | |
| error: 'API key is required' | |
| })); | |
| return; | |
| } | |
| // Store the API key being used (either existing or newly entered) | |
| setState(prev_17 => ({ | |
| ...prev_17, | |
| apiKeyOrOAuthToken: apiKeyToUse, | |
| useExistingKey: state.selectedApiKeyOption === 'existing' | |
| })); | |
| // Check if ANTHROPIC_API_KEY secret already exists | |
| const checkSecretsResult_0 = await execFileNoThrow('gh', ['secret', 'list', '--app', 'actions', '--repo', state.selectedRepoName]); | |
| if (checkSecretsResult_0.code === 0) { | |
| const lines_0 = checkSecretsResult_0.stdout.split('\n'); | |
| const hasAnthropicKey_0 = lines_0.some((line_0: string) => { | |
| return /^ANTHROPIC_API_KEY\s+/.test(line_0); | |
| }); | |
| if (hasAnthropicKey_0) { | |
| logEvent('tengu_install_github_app_step_completed', { | |
| step: 'api-key' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| }); | |
| setState(prev_18 => ({ | |
| ...prev_18, | |
| 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); | |
| } | |
| } 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); | |
| } | |
| } | |
| }; | |
| const handleRepoUrlChange = (value: string) => { | |
| setState(prev_19 => ({ | |
| ...prev_19, | |
| selectedRepoName: value | |
| })); | |
| }; | |
| const handleApiKeyChange = (value_0: string) => { | |
| setState(prev_20 => ({ | |
| ...prev_20, | |
| apiKeyOrOAuthToken: value_0 | |
| })); | |
| }; | |
| const handleApiKeyOptionChange = (option: 'existing' | 'new' | 'oauth') => { | |
| setState(prev_21 => ({ | |
| ...prev_21, | |
| 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_22 => ({ | |
| ...prev_22, | |
| 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_23 => ({ | |
| ...prev_23, | |
| apiKeyOrOAuthToken: token, | |
| useExistingKey: false, | |
| secretName: 'CLAUDE_CODE_OAUTH_TOKEN', | |
| authType: 'oauth_token' | |
| })); | |
| void runSetupGitHubActions(token, 'CLAUDE_CODE_OAUTH_TOKEN'); | |
| }, [runSetupGitHubActions]); | |
| const handleOAuthCancel = useCallback(() => { | |
| setState(prev_24 => ({ | |
| ...prev_24, | |
| step: 'api-key' | |
| })); | |
| }, []); | |
| const handleSecretNameChange = (value_1: string) => { | |
| if (value_1 && !/^[a-zA-Z0-9_]+$/.test(value_1)) return; | |
| setState(prev_25 => ({ | |
| ...prev_25, | |
| secretName: value_1 | |
| })); | |
| }; | |
| const handleToggleUseCurrentRepo = (useCurrentRepo: boolean) => { | |
| setState(prev_26 => ({ | |
| ...prev_26, | |
| useCurrentRepo, | |
| selectedRepoName: useCurrentRepo ? prev_26.currentRepo : '' | |
| })); | |
| }; | |
| const handleToggleUseExistingKey = (useExistingKey: boolean) => { | |
| setState(prev_27 => ({ | |
| ...prev_27, | |
| useExistingKey | |
| })); | |
| }; | |
| const handleToggleUseExistingSecret = (useExistingSecret: boolean) => { | |
| setState(prev_28 => ({ | |
| ...prev_28, | |
| useExistingSecret, | |
| secretName: useExistingSecret ? 'ANTHROPIC_API_KEY' : '' | |
| })); | |
| }; | |
| const handleWorkflowAction = async (action: 'update' | 'skip' | 'exit') => { | |
| if (action === 'exit') { | |
| 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_29 => ({ | |
| ...prev_29, | |
| workflowAction: action | |
| })); | |
| if (action === 'skip' || action === 'update') { | |
| // Check if user has existing local API key | |
| if (existingApiKey) { | |
| await checkExistingSecret(); | |
| } else { | |
| // No local key, go straight to API key step | |
| setState(prev_30 => ({ | |
| ...prev_30, | |
| step: 'api-key' | |
| })); | |
| } | |
| } | |
| }; | |
| function handleDismissKeyDown(e: KeyboardEvent): void { | |
| e.preventDefault(); | |
| if (state.step === 'success') { | |
| logEvent('tengu_install_github_app_completed', {}); | |
| } | |
| props.onDone(state.step === 'success' ? 'GitHub Actions setup complete!' : 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 />; | |
| case 'warnings': | |
| return <WarningsStep warnings={state.warnings} onContinue={handleSubmit} />; | |
| case 'choose-repo': | |
| return <ChooseRepoStep currentRepo={state.currentRepo} useCurrentRepo={state.useCurrentRepo} repoUrl={state.selectedRepoName} onRepoUrlChange={handleRepoUrlChange} onToggleUseCurrentRepo={handleToggleUseCurrentRepo} onSubmit={handleSubmit} />; | |
| case 'install-app': | |
| return <InstallAppStep repoUrl={state.selectedRepoName} onSubmit={handleSubmit} />; | |
| case 'check-existing-workflow': | |
| return <ExistingWorkflowStep repoName={state.selectedRepoName} onSelectAction={handleWorkflowAction} />; | |
| case 'check-existing-secret': | |
| return <CheckExistingSecretStep useExistingSecret={state.useExistingSecret} secretName={state.secretName} onToggleUseExistingSecret={handleToggleUseExistingSecret} onSecretNameChange={handleSecretNameChange} onSubmit={handleSubmit} />; | |
| case 'api-key': | |
| return <ApiKeyStep existingApiKey={existingApiKey} useExistingKey={state.useExistingKey} apiKeyOrOAuthToken={state.apiKeyOrOAuthToken} onApiKeyChange={handleApiKeyChange} onToggleUseExistingKey={handleToggleUseExistingKey} onSubmit={handleSubmit} onCreateOAuthToken={isAnthropicAuthEnabled() ? handleCreateOAuthToken : undefined} selectedOption={state.selectedApiKeyOption} onSelectOption={handleApiKeyOptionChange} />; | |
| case 'creating': | |
| return <CreatingStep currentWorkflowInstallStep={state.currentWorkflowInstallStep} secretExists={state.secretExists} useExistingSecret={state.useExistingSecret} secretName={state.secretName} skipWorkflow={state.workflowAction === 'skip'} selectedWorkflows={state.selectedWorkflows} />; | |
| case 'success': | |
| return <Box tabIndex={0} autoFocus onKeyDown={handleDismissKeyDown}> | |
| <SuccessStep secretExists={state.secretExists} useExistingSecret={state.useExistingSecret} secretName={state.secretName} skipWorkflow={state.workflowAction === 'skip'} /> | |
| </Box>; | |
| case 'error': | |
| return <Box tabIndex={0} autoFocus onKeyDown={handleDismissKeyDown}> | |
| <ErrorStep error={state.error} errorReason={state.errorReason} errorInstructions={state.errorInstructions} /> | |
| </Box>; | |
| case 'select-workflows': | |
| return <WorkflowMultiselectDialog defaultSelections={state.selectedWorkflows} onSubmit={selectedWorkflows => { | |
| logEvent('tengu_install_github_app_step_completed', { | |
| step: 'select-workflows' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS | |
| }); | |
| setState(prev_31 => ({ | |
| ...prev_31, | |
| selectedWorkflows | |
| })); | |
| // Check if user has existing local API key | |
| if (existingApiKey) { | |
| void checkExistingSecret(); | |
| } else { | |
| // No local key, go straight to API key step | |
| setState(prev_32 => ({ | |
| ...prev_32, | |
| step: 'api-key' | |
| })); | |
| } | |
| }} />; | |
| case 'oauth-flow': | |
| return <OAuthFlowStep onSuccess={handleOAuthSuccess} onCancel={handleOAuthCancel} />; | |
| } | |
| } | |
| export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> { | |
| return <InstallGitHubApp onDone={onDone} />; | |
| } | |