source dump of claude code
at main 235 lines 7.3 kB view raw
1import axios from 'axios' 2import { getOauthConfig } from 'src/constants/oauth.js' 3import { getOrganizationUUID } from 'src/services/oauth/client.js' 4import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js' 5import { 6 checkAndRefreshOAuthTokenIfNeeded, 7 getClaudeAIOAuthTokens, 8 isClaudeAISubscriber, 9} from '../../auth.js' 10import { getCwd } from '../../cwd.js' 11import { logForDebugging } from '../../debug.js' 12import { detectCurrentRepository } from '../../detectRepository.js' 13import { errorMessage } from '../../errors.js' 14import { findGitRoot, getIsClean } from '../../git.js' 15import { getOAuthHeaders } from '../../teleport/api.js' 16import { fetchEnvironments } from '../../teleport/environments.js' 17 18/** 19 * Checks if user needs to log in with Claude.ai 20 * Extracted from getTeleportErrors() in TeleportError.tsx 21 * @returns true if login is required, false otherwise 22 */ 23export async function checkNeedsClaudeAiLogin(): Promise<boolean> { 24 if (!isClaudeAISubscriber()) { 25 return false 26 } 27 return checkAndRefreshOAuthTokenIfNeeded() 28} 29 30/** 31 * Checks if git working directory is clean (no uncommitted changes) 32 * Ignores untracked files since they won't be lost during branch switching 33 * Extracted from getTeleportErrors() in TeleportError.tsx 34 * @returns true if git is clean, false otherwise 35 */ 36export async function checkIsGitClean(): Promise<boolean> { 37 const isClean = await getIsClean({ ignoreUntracked: true }) 38 return isClean 39} 40 41/** 42 * Checks if user has access to at least one remote environment 43 * @returns true if user has remote environments, false otherwise 44 */ 45export async function checkHasRemoteEnvironment(): Promise<boolean> { 46 try { 47 const environments = await fetchEnvironments() 48 return environments.length > 0 49 } catch (error) { 50 logForDebugging(`checkHasRemoteEnvironment failed: ${errorMessage(error)}`) 51 return false 52 } 53} 54 55/** 56 * Checks if current directory is inside a git repository (has .git/). 57 * Distinct from checkHasGitRemote — a local-only repo passes this but not that. 58 */ 59export function checkIsInGitRepo(): boolean { 60 return findGitRoot(getCwd()) !== null 61} 62 63/** 64 * Checks if current repository has a GitHub remote configured. 65 * Returns false for local-only repos (git init with no `origin`). 66 */ 67export async function checkHasGitRemote(): Promise<boolean> { 68 const repository = await detectCurrentRepository() 69 return repository !== null 70} 71 72/** 73 * Checks if GitHub app is installed on a specific repository 74 * @param owner The repository owner (e.g., "anthropics") 75 * @param repo The repository name (e.g., "claude-cli-internal") 76 * @returns true if GitHub app is installed, false otherwise 77 */ 78export async function checkGithubAppInstalled( 79 owner: string, 80 repo: string, 81 signal?: AbortSignal, 82): Promise<boolean> { 83 try { 84 const accessToken = getClaudeAIOAuthTokens()?.accessToken 85 if (!accessToken) { 86 logForDebugging( 87 'checkGithubAppInstalled: No access token found, assuming app not installed', 88 ) 89 return false 90 } 91 92 const orgUUID = await getOrganizationUUID() 93 if (!orgUUID) { 94 logForDebugging( 95 'checkGithubAppInstalled: No org UUID found, assuming app not installed', 96 ) 97 return false 98 } 99 100 const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/code/repos/${owner}/${repo}` 101 const headers = { 102 ...getOAuthHeaders(accessToken), 103 'x-organization-uuid': orgUUID, 104 } 105 106 logForDebugging(`Checking GitHub app installation for ${owner}/${repo}`) 107 108 const response = await axios.get<{ 109 repo: { 110 name: string 111 owner: { login: string } 112 default_branch: string 113 } 114 status: { 115 app_installed: boolean 116 relay_enabled: boolean 117 } | null 118 }>(url, { 119 headers, 120 timeout: 15000, 121 signal, 122 }) 123 124 if (response.status === 200) { 125 if (response.data.status) { 126 const installed = response.data.status.app_installed 127 logForDebugging( 128 `GitHub app ${installed ? 'is' : 'is not'} installed on ${owner}/${repo}`, 129 ) 130 return installed 131 } 132 // status is null - app is not installed on this repo 133 logForDebugging( 134 `GitHub app is not installed on ${owner}/${repo} (status is null)`, 135 ) 136 return false 137 } 138 139 logForDebugging( 140 `checkGithubAppInstalled: Unexpected response status ${response.status}`, 141 ) 142 return false 143 } catch (error) { 144 // 4XX errors typically mean app is not installed or repo not accessible 145 if (axios.isAxiosError(error)) { 146 const status = error.response?.status 147 if (status && status >= 400 && status < 500) { 148 logForDebugging( 149 `checkGithubAppInstalled: Got ${status} error, app likely not installed on ${owner}/${repo}`, 150 ) 151 return false 152 } 153 } 154 155 logForDebugging(`checkGithubAppInstalled error: ${errorMessage(error)}`) 156 return false 157 } 158} 159 160/** 161 * Checks if the user has synced their GitHub credentials via /web-setup 162 * @returns true if GitHub token is synced, false otherwise 163 */ 164export async function checkGithubTokenSynced(): Promise<boolean> { 165 try { 166 const accessToken = getClaudeAIOAuthTokens()?.accessToken 167 if (!accessToken) { 168 logForDebugging('checkGithubTokenSynced: No access token found') 169 return false 170 } 171 172 const orgUUID = await getOrganizationUUID() 173 if (!orgUUID) { 174 logForDebugging('checkGithubTokenSynced: No org UUID found') 175 return false 176 } 177 178 const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/sync/github/auth` 179 const headers = { 180 ...getOAuthHeaders(accessToken), 181 'x-organization-uuid': orgUUID, 182 } 183 184 logForDebugging('Checking if GitHub token is synced via web-setup') 185 186 const response = await axios.get(url, { 187 headers, 188 timeout: 15000, 189 }) 190 191 const synced = 192 response.status === 200 && response.data?.is_authenticated === true 193 logForDebugging( 194 `GitHub token synced: ${synced} (status=${response.status}, data=${JSON.stringify(response.data)})`, 195 ) 196 return synced 197 } catch (error) { 198 if (axios.isAxiosError(error)) { 199 const status = error.response?.status 200 if (status && status >= 400 && status < 500) { 201 logForDebugging( 202 `checkGithubTokenSynced: Got ${status}, token not synced`, 203 ) 204 return false 205 } 206 } 207 208 logForDebugging(`checkGithubTokenSynced error: ${errorMessage(error)}`) 209 return false 210 } 211} 212 213type RepoAccessMethod = 'github-app' | 'token-sync' | 'none' 214 215/** 216 * Tiered check for whether a GitHub repo is accessible for remote operations. 217 * 1. GitHub App installed on the repo 218 * 2. GitHub token synced via /web-setup 219 * 3. Neither — caller should prompt user to set up access 220 */ 221export async function checkRepoForRemoteAccess( 222 owner: string, 223 repo: string, 224): Promise<{ hasAccess: boolean; method: RepoAccessMethod }> { 225 if (await checkGithubAppInstalled(owner, repo)) { 226 return { hasAccess: true, method: 'github-app' } 227 } 228 if ( 229 getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_lantern', false) && 230 (await checkGithubTokenSynced()) 231 ) { 232 return { hasAccess: true, method: 'token-sync' } 233 } 234 return { hasAccess: false, method: 'none' } 235}