chore: initialize recovered claude workspace
This commit is contained in:
231
src/commands/install-github-app/ApiKeyStep.tsx
Normal file
231
src/commands/install-github-app/ApiKeyStep.tsx
Normal file
File diff suppressed because one or more lines are too long
190
src/commands/install-github-app/CheckExistingSecretStep.tsx
Normal file
190
src/commands/install-github-app/CheckExistingSecretStep.tsx
Normal file
File diff suppressed because one or more lines are too long
15
src/commands/install-github-app/CheckGitHubStep.tsx
Normal file
15
src/commands/install-github-app/CheckGitHubStep.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
export function CheckGitHubStep() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text>Checking GitHub CLI installation…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJDaGVja0dpdEh1YlN0ZXAiLCIkIiwiX2MiLCJ0MCIsIlN5bWJvbCIsImZvciJdLCJzb3VyY2VzIjpbIkNoZWNrR2l0SHViU3RlcC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIENoZWNrR2l0SHViU3RlcCgpIHtcbiAgcmV0dXJuIDxUZXh0PkNoZWNraW5nIEdpdEh1YiBDTEkgaW5zdGFsbGF0aW9u4oCmPC9UZXh0PlxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsSUFBSSxRQUFRLGNBQWM7QUFFbkMsT0FBTyxTQUFBQyxnQkFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFBLElBQUFDLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFHLE1BQUEsQ0FBQUMsR0FBQTtJQUNFRixFQUFBLElBQUMsSUFBSSxDQUFDLGlDQUFpQyxFQUF0QyxJQUFJLENBQXlDO0lBQUFGLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQUEsT0FBOUNFLEVBQThDO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
||||
211
src/commands/install-github-app/ChooseRepoStep.tsx
Normal file
211
src/commands/install-github-app/ChooseRepoStep.tsx
Normal file
File diff suppressed because one or more lines are too long
65
src/commands/install-github-app/CreatingStep.tsx
Normal file
65
src/commands/install-github-app/CreatingStep.tsx
Normal file
File diff suppressed because one or more lines are too long
85
src/commands/install-github-app/ErrorStep.tsx
Normal file
85
src/commands/install-github-app/ErrorStep.tsx
Normal file
File diff suppressed because one or more lines are too long
103
src/commands/install-github-app/ExistingWorkflowStep.tsx
Normal file
103
src/commands/install-github-app/ExistingWorkflowStep.tsx
Normal file
File diff suppressed because one or more lines are too long
94
src/commands/install-github-app/InstallAppStep.tsx
Normal file
94
src/commands/install-github-app/InstallAppStep.tsx
Normal file
File diff suppressed because one or more lines are too long
276
src/commands/install-github-app/OAuthFlowStep.tsx
Normal file
276
src/commands/install-github-app/OAuthFlowStep.tsx
Normal file
File diff suppressed because one or more lines are too long
96
src/commands/install-github-app/SuccessStep.tsx
Normal file
96
src/commands/install-github-app/SuccessStep.tsx
Normal file
File diff suppressed because one or more lines are too long
73
src/commands/install-github-app/WarningsStep.tsx
Normal file
73
src/commands/install-github-app/WarningsStep.tsx
Normal file
File diff suppressed because one or more lines are too long
13
src/commands/install-github-app/index.ts
Normal file
13
src/commands/install-github-app/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
|
||||
const installGitHubApp = {
|
||||
type: 'local-jsx',
|
||||
name: 'install-github-app',
|
||||
description: 'Set up Claude GitHub Actions for a repository',
|
||||
availability: ['claude-ai', 'console'],
|
||||
isEnabled: () => !isEnvTruthy(process.env.DISABLE_INSTALL_GITHUB_APP_COMMAND),
|
||||
load: () => import('./install-github-app.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default installGitHubApp
|
||||
587
src/commands/install-github-app/install-github-app.tsx
Normal file
587
src/commands/install-github-app/install-github-app.tsx
Normal file
File diff suppressed because one or more lines are too long
325
src/commands/install-github-app/setupGitHubActions.ts
Normal file
325
src/commands/install-github-app/setupGitHubActions.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { saveGlobalConfig } from 'src/utils/config.js'
|
||||
import {
|
||||
CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT,
|
||||
PR_BODY,
|
||||
PR_TITLE,
|
||||
WORKFLOW_CONTENT,
|
||||
} from '../../constants/github-app.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import type { Workflow } from './types.js'
|
||||
|
||||
async function createWorkflowFile(
|
||||
repoName: string,
|
||||
branchName: string,
|
||||
workflowPath: string,
|
||||
workflowContent: string,
|
||||
secretName: string,
|
||||
message: string,
|
||||
context?: {
|
||||
useCurrentRepo?: boolean
|
||||
workflowExists?: boolean
|
||||
secretExists?: boolean
|
||||
},
|
||||
): Promise<void> {
|
||||
// Check if workflow file already exists
|
||||
const checkFileResult = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
`repos/${repoName}/contents/${workflowPath}`,
|
||||
'--jq',
|
||||
'.sha',
|
||||
])
|
||||
|
||||
let fileSha: string | null = null
|
||||
if (checkFileResult.code === 0) {
|
||||
fileSha = checkFileResult.stdout.trim()
|
||||
}
|
||||
|
||||
let content = workflowContent
|
||||
if (secretName === 'CLAUDE_CODE_OAUTH_TOKEN') {
|
||||
// For OAuth tokens, use the claude_code_oauth_token parameter
|
||||
content = workflowContent.replace(
|
||||
/anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g,
|
||||
`claude_code_oauth_token: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}`,
|
||||
)
|
||||
} else if (secretName !== 'ANTHROPIC_API_KEY') {
|
||||
// For other custom secret names, keep using anthropic_api_key parameter
|
||||
content = workflowContent.replace(
|
||||
/anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g,
|
||||
`anthropic_api_key: \${{ secrets.${secretName} }}`,
|
||||
)
|
||||
}
|
||||
const base64Content = Buffer.from(content).toString('base64')
|
||||
|
||||
const apiParams = [
|
||||
'api',
|
||||
'--method',
|
||||
'PUT',
|
||||
`repos/${repoName}/contents/${workflowPath}`,
|
||||
'-f',
|
||||
`message=${fileSha ? `"Update ${message}"` : `"${message}"`}`,
|
||||
'-f',
|
||||
`content=${base64Content}`,
|
||||
'-f',
|
||||
`branch=${branchName}`,
|
||||
]
|
||||
|
||||
if (fileSha) {
|
||||
apiParams.push('-f', `sha=${fileSha}`)
|
||||
}
|
||||
|
||||
const createFileResult = await execFileNoThrow('gh', apiParams)
|
||||
if (createFileResult.code !== 0) {
|
||||
if (
|
||||
createFileResult.stderr.includes('422') &&
|
||||
createFileResult.stderr.includes('sha')
|
||||
) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: createFileResult.code,
|
||||
...context,
|
||||
})
|
||||
throw new Error(
|
||||
`Failed to create workflow file ${workflowPath}: A Claude workflow file already exists in this repository. Please remove it first or update it manually.`,
|
||||
)
|
||||
}
|
||||
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: createFileResult.code,
|
||||
...context,
|
||||
})
|
||||
|
||||
const helpText =
|
||||
'\n\nNeed help? Common issues:\n' +
|
||||
'· Permission denied → Run: gh auth refresh -h github.com -s repo,workflow\n' +
|
||||
'· Not authorized → Ensure you have admin access to the repository\n' +
|
||||
'· For manual setup → Visit: https://github.com/anthropics/claude-code-action'
|
||||
|
||||
throw new Error(
|
||||
`Failed to create workflow file ${workflowPath}: ${createFileResult.stderr}${helpText}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupGitHubActions(
|
||||
repoName: string,
|
||||
apiKeyOrOAuthToken: string | null,
|
||||
secretName: string,
|
||||
updateProgress: () => void,
|
||||
skipWorkflow = false,
|
||||
selectedWorkflows: Workflow[],
|
||||
authType: 'api_key' | 'oauth_token',
|
||||
context?: {
|
||||
useCurrentRepo?: boolean
|
||||
workflowExists?: boolean
|
||||
secretExists?: boolean
|
||||
},
|
||||
) {
|
||||
try {
|
||||
logEvent('tengu_setup_github_actions_started', {
|
||||
skip_workflow: skipWorkflow,
|
||||
has_api_key: !!apiKeyOrOAuthToken,
|
||||
using_default_secret_name: secretName === 'ANTHROPIC_API_KEY',
|
||||
selected_claude_workflow: selectedWorkflows.includes('claude'),
|
||||
selected_claude_review_workflow:
|
||||
selectedWorkflows.includes('claude-review'),
|
||||
...context,
|
||||
})
|
||||
|
||||
// Check if repository exists
|
||||
const repoCheckResult = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
`repos/${repoName}`,
|
||||
'--jq',
|
||||
'.id',
|
||||
])
|
||||
if (repoCheckResult.code !== 0) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'repo_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: repoCheckResult.code,
|
||||
...context,
|
||||
})
|
||||
throw new Error(
|
||||
`Failed to access repository ${repoName}: ${repoCheckResult.stderr}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Get default branch
|
||||
const defaultBranchResult = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
`repos/${repoName}`,
|
||||
'--jq',
|
||||
'.default_branch',
|
||||
])
|
||||
if (defaultBranchResult.code !== 0) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_get_default_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: defaultBranchResult.code,
|
||||
...context,
|
||||
})
|
||||
throw new Error(
|
||||
`Failed to get default branch: ${defaultBranchResult.stderr}`,
|
||||
)
|
||||
}
|
||||
const defaultBranch = defaultBranchResult.stdout.trim()
|
||||
|
||||
// Get SHA of default branch
|
||||
const shaResult = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
`repos/${repoName}/git/ref/heads/${defaultBranch}`,
|
||||
'--jq',
|
||||
'.object.sha',
|
||||
])
|
||||
if (shaResult.code !== 0) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_get_branch_sha' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: shaResult.code,
|
||||
...context,
|
||||
})
|
||||
throw new Error(`Failed to get branch SHA: ${shaResult.stderr}`)
|
||||
}
|
||||
const sha = shaResult.stdout.trim()
|
||||
|
||||
let branchName: string | null = null
|
||||
|
||||
if (!skipWorkflow) {
|
||||
updateProgress()
|
||||
// Create new branch
|
||||
branchName = `add-claude-github-actions-${Date.now()}`
|
||||
const createBranchResult = await execFileNoThrow('gh', [
|
||||
'api',
|
||||
'--method',
|
||||
'POST',
|
||||
`repos/${repoName}/git/refs`,
|
||||
'-f',
|
||||
`ref=refs/heads/${branchName}`,
|
||||
'-f',
|
||||
`sha=${sha}`,
|
||||
])
|
||||
if (createBranchResult.code !== 0) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_create_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: createBranchResult.code,
|
||||
...context,
|
||||
})
|
||||
throw new Error(`Failed to create branch: ${createBranchResult.stderr}`)
|
||||
}
|
||||
|
||||
updateProgress()
|
||||
// Create selected workflow files
|
||||
const workflows = []
|
||||
|
||||
if (selectedWorkflows.includes('claude')) {
|
||||
workflows.push({
|
||||
path: '.github/workflows/claude.yml',
|
||||
content: WORKFLOW_CONTENT,
|
||||
message: 'Claude PR Assistant workflow',
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedWorkflows.includes('claude-review')) {
|
||||
workflows.push({
|
||||
path: '.github/workflows/claude-code-review.yml',
|
||||
content: CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT,
|
||||
message: 'Claude Code Review workflow',
|
||||
})
|
||||
}
|
||||
|
||||
for (const workflow of workflows) {
|
||||
await createWorkflowFile(
|
||||
repoName,
|
||||
branchName,
|
||||
workflow.path,
|
||||
workflow.content,
|
||||
secretName,
|
||||
workflow.message,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress()
|
||||
// Set the API key as a secret if provided
|
||||
if (apiKeyOrOAuthToken) {
|
||||
const setSecretResult = await execFileNoThrow('gh', [
|
||||
'secret',
|
||||
'set',
|
||||
secretName,
|
||||
'--body',
|
||||
apiKeyOrOAuthToken,
|
||||
'--repo',
|
||||
repoName,
|
||||
])
|
||||
if (setSecretResult.code !== 0) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'failed_to_set_api_key_secret' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
exit_code: setSecretResult.code,
|
||||
...context,
|
||||
})
|
||||
|
||||
const helpText =
|
||||
'\n\nNeed help? Common issues:\n' +
|
||||
'· Permission denied → Run: gh auth refresh -h github.com -s repo\n' +
|
||||
'· Not authorized → Ensure you have admin access to the repository\n' +
|
||||
'· For manual setup → Visit: https://github.com/anthropics/claude-code-action'
|
||||
|
||||
throw new Error(
|
||||
`Failed to set API key secret: ${setSecretResult.stderr || 'Unknown error'}${helpText}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipWorkflow && branchName) {
|
||||
updateProgress()
|
||||
// Create PR template URL instead of creating PR directly
|
||||
const compareUrl = `https://github.com/${repoName}/compare/${defaultBranch}...${branchName}?quick_pull=1&title=${encodeURIComponent(PR_TITLE)}&body=${encodeURIComponent(PR_BODY)}`
|
||||
|
||||
await openBrowser(compareUrl)
|
||||
}
|
||||
|
||||
logEvent('tengu_setup_github_actions_completed', {
|
||||
skip_workflow: skipWorkflow,
|
||||
has_api_key: !!apiKeyOrOAuthToken,
|
||||
auth_type:
|
||||
authType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
using_default_secret_name: secretName === 'ANTHROPIC_API_KEY',
|
||||
selected_claude_workflow: selectedWorkflows.includes('claude'),
|
||||
selected_claude_review_workflow:
|
||||
selectedWorkflows.includes('claude-review'),
|
||||
...context,
|
||||
})
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
githubActionSetupCount: (current.githubActionSetupCount ?? 0) + 1,
|
||||
}))
|
||||
} catch (error) {
|
||||
if (
|
||||
!error ||
|
||||
!(error instanceof Error) ||
|
||||
!error.message.includes('Failed to')
|
||||
) {
|
||||
logEvent('tengu_setup_github_actions_failed', {
|
||||
reason:
|
||||
'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
...context,
|
||||
})
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
logError(error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user