Inline base64 source maps had been checked into tracked src files. This strips those comments from the repository without changing runtime behavior or adding ongoing guardrails, per the requested one-time cleanup scope. Constraint: Keep this change limited to tracked source cleanup only Rejected: Add CI/source verification guard | user requested one-time cleanup only Confidence: high Scope-risk: narrow Reversibility: clean Directive: If these directives reappear, fix the producing transform instead of reintroducing repo-side cleanup code Tested: rg -n "sourceMappingURL" ., bun run smoke, bun run verify:privacy, bun run test:provider, npm run test:provider-recommendation Not-tested: bun run typecheck (repository has many pre-existing unrelated failures) Co-authored-by: anandh8x <test@example.com>
587 lines
24 KiB
TypeScript
587 lines
24 KiB
TypeScript
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} />;
|
|
}
|