init claude-code
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import axios from 'axios'
|
||||
import { getOauthConfig } from 'src/constants/oauth.js'
|
||||
import { getOrganizationUUID } from 'src/services/oauth/client.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'
|
||||
import {
|
||||
checkAndRefreshOAuthTokenIfNeeded,
|
||||
getClaudeAIOAuthTokens,
|
||||
isClaudeAISubscriber,
|
||||
} from '../../auth.js'
|
||||
import { getCwd } from '../../cwd.js'
|
||||
import { logForDebugging } from '../../debug.js'
|
||||
import { detectCurrentRepository } from '../../detectRepository.js'
|
||||
import { errorMessage } from '../../errors.js'
|
||||
import { findGitRoot, getIsClean } from '../../git.js'
|
||||
import { getOAuthHeaders } from '../../teleport/api.js'
|
||||
import { fetchEnvironments } from '../../teleport/environments.js'
|
||||
|
||||
/**
|
||||
* Checks if user needs to log in with Claude.ai
|
||||
* Extracted from getTeleportErrors() in TeleportError.tsx
|
||||
* @returns true if login is required, false otherwise
|
||||
*/
|
||||
export async function checkNeedsClaudeAiLogin(): Promise<boolean> {
|
||||
if (!isClaudeAISubscriber()) {
|
||||
return false
|
||||
}
|
||||
return checkAndRefreshOAuthTokenIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if git working directory is clean (no uncommitted changes)
|
||||
* Ignores untracked files since they won't be lost during branch switching
|
||||
* Extracted from getTeleportErrors() in TeleportError.tsx
|
||||
* @returns true if git is clean, false otherwise
|
||||
*/
|
||||
export async function checkIsGitClean(): Promise<boolean> {
|
||||
const isClean = await getIsClean({ ignoreUntracked: true })
|
||||
return isClean
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user has access to at least one remote environment
|
||||
* @returns true if user has remote environments, false otherwise
|
||||
*/
|
||||
export async function checkHasRemoteEnvironment(): Promise<boolean> {
|
||||
try {
|
||||
const environments = await fetchEnvironments()
|
||||
return environments.length > 0
|
||||
} catch (error) {
|
||||
logForDebugging(`checkHasRemoteEnvironment failed: ${errorMessage(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current directory is inside a git repository (has .git/).
|
||||
* Distinct from checkHasGitRemote — a local-only repo passes this but not that.
|
||||
*/
|
||||
export function checkIsInGitRepo(): boolean {
|
||||
return findGitRoot(getCwd()) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current repository has a GitHub remote configured.
|
||||
* Returns false for local-only repos (git init with no `origin`).
|
||||
*/
|
||||
export async function checkHasGitRemote(): Promise<boolean> {
|
||||
const repository = await detectCurrentRepository()
|
||||
return repository !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if GitHub app is installed on a specific repository
|
||||
* @param owner The repository owner (e.g., "anthropics")
|
||||
* @param repo The repository name (e.g., "claude-cli-internal")
|
||||
* @returns true if GitHub app is installed, false otherwise
|
||||
*/
|
||||
export async function checkGithubAppInstalled(
|
||||
owner: string,
|
||||
repo: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const accessToken = getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging(
|
||||
'checkGithubAppInstalled: No access token found, assuming app not installed',
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging(
|
||||
'checkGithubAppInstalled: No org UUID found, assuming app not installed',
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/code/repos/${owner}/${repo}`
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
logForDebugging(`Checking GitHub app installation for ${owner}/${repo}`)
|
||||
|
||||
const response = await axios.get<{
|
||||
repo: {
|
||||
name: string
|
||||
owner: { login: string }
|
||||
default_branch: string
|
||||
}
|
||||
status: {
|
||||
app_installed: boolean
|
||||
relay_enabled: boolean
|
||||
} | null
|
||||
}>(url, {
|
||||
headers,
|
||||
timeout: 15000,
|
||||
signal,
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.data.status) {
|
||||
const installed = response.data.status.app_installed
|
||||
logForDebugging(
|
||||
`GitHub app ${installed ? 'is' : 'is not'} installed on ${owner}/${repo}`,
|
||||
)
|
||||
return installed
|
||||
}
|
||||
// status is null - app is not installed on this repo
|
||||
logForDebugging(
|
||||
`GitHub app is not installed on ${owner}/${repo} (status is null)`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
logForDebugging(
|
||||
`checkGithubAppInstalled: Unexpected response status ${response.status}`,
|
||||
)
|
||||
return false
|
||||
} catch (error) {
|
||||
// 4XX errors typically mean app is not installed or repo not accessible
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
if (status && status >= 400 && status < 500) {
|
||||
logForDebugging(
|
||||
`checkGithubAppInstalled: Got ${status} error, app likely not installed on ${owner}/${repo}`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
logForDebugging(`checkGithubAppInstalled error: ${errorMessage(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has synced their GitHub credentials via /web-setup
|
||||
* @returns true if GitHub token is synced, false otherwise
|
||||
*/
|
||||
export async function checkGithubTokenSynced(): Promise<boolean> {
|
||||
try {
|
||||
const accessToken = getClaudeAIOAuthTokens()?.accessToken
|
||||
if (!accessToken) {
|
||||
logForDebugging('checkGithubTokenSynced: No access token found')
|
||||
return false
|
||||
}
|
||||
|
||||
const orgUUID = await getOrganizationUUID()
|
||||
if (!orgUUID) {
|
||||
logForDebugging('checkGithubTokenSynced: No org UUID found')
|
||||
return false
|
||||
}
|
||||
|
||||
const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/sync/github/auth`
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
logForDebugging('Checking if GitHub token is synced via web-setup')
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers,
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
const synced =
|
||||
response.status === 200 && response.data?.is_authenticated === true
|
||||
logForDebugging(
|
||||
`GitHub token synced: ${synced} (status=${response.status}, data=${JSON.stringify(response.data)})`,
|
||||
)
|
||||
return synced
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
if (status && status >= 400 && status < 500) {
|
||||
logForDebugging(
|
||||
`checkGithubTokenSynced: Got ${status}, token not synced`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
logForDebugging(`checkGithubTokenSynced error: ${errorMessage(error)}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type RepoAccessMethod = 'github-app' | 'token-sync' | 'none'
|
||||
|
||||
/**
|
||||
* Tiered check for whether a GitHub repo is accessible for remote operations.
|
||||
* 1. GitHub App installed on the repo
|
||||
* 2. GitHub token synced via /web-setup
|
||||
* 3. Neither — caller should prompt user to set up access
|
||||
*/
|
||||
export async function checkRepoForRemoteAccess(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<{ hasAccess: boolean; method: RepoAccessMethod }> {
|
||||
if (await checkGithubAppInstalled(owner, repo)) {
|
||||
return { hasAccess: true, method: 'github-app' }
|
||||
}
|
||||
if (
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_lantern', false) &&
|
||||
(await checkGithubTokenSynced())
|
||||
) {
|
||||
return { hasAccess: true, method: 'token-sync' }
|
||||
}
|
||||
return { hasAccess: false, method: 'none' }
|
||||
}
|
||||
Reference in New Issue
Block a user