Spaces:
Running
Running
File size: 5,085 Bytes
780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 98c9143 780d695 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import consola from "consola"
import fs from "node:fs/promises"
import { setTimeout as delay } from "node:timers/promises"
import { isOpencodeOauthApp } from "~/lib/api-config"
import { PATHS } from "~/lib/paths"
import { getCopilotToken } from "~/services/github/get-copilot-token"
import { getCopilotUsage } from "~/services/github/get-copilot-usage"
import { getDeviceCode } from "~/services/github/get-device-code"
import { getGitHubUser } from "~/services/github/get-user"
import { pollAccessToken } from "~/services/github/poll-access-token"
import { HTTPError } from "./error"
import { state } from "./state"
let copilotRefreshLoopController: AbortController | null = null
export const stopCopilotRefreshLoop = () => {
if (!copilotRefreshLoopController) {
return
}
copilotRefreshLoopController.abort()
copilotRefreshLoopController = null
}
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")
const writeGithubToken = (token: string) =>
fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token)
export const setupCopilotToken = async () => {
if (isOpencodeOauthApp()) {
if (!state.githubToken) throw new Error(`opencode token not found`)
state.copilotToken = state.githubToken
consola.debug("GitHub Copilot token set from opencode auth token")
if (state.showToken) {
consola.info("Copilot token:", state.copilotToken)
}
stopCopilotRefreshLoop()
return
}
const { token, refresh_in } = await getCopilotToken()
state.copilotToken = token
// Display the Copilot token to the screen
consola.debug("GitHub Copilot Token fetched successfully!")
if (state.showToken) {
consola.info("Copilot token:", token)
}
stopCopilotRefreshLoop()
const controller = new AbortController()
copilotRefreshLoopController = controller
runCopilotRefreshLoop(refresh_in, controller.signal)
.catch(() => {
consola.warn("Copilot token refresh loop stopped")
})
.finally(() => {
if (copilotRefreshLoopController === controller) {
copilotRefreshLoopController = null
}
})
}
const REFRESH_POLL_INTERVAL_MS = 15_000
const EARLY_REFRESH_BUFFER_MS = 60_000
const RETRY_REFRESH_DELAY_MS = 15_000
const MIN_REFRESH_DELAY_MS = 1_000
export const getRefreshDeadlineMs = (
refreshIn: number,
nowMs: number = Date.now(),
) =>
nowMs
+ Math.max(refreshIn * 1000 - EARLY_REFRESH_BUFFER_MS, MIN_REFRESH_DELAY_MS)
// Use short wall-clock chunks so the next wake after sleep notices elapsed time
// quickly, without relying on the server's absolute expires_at matching local time.
export const getRefreshPollDelayMs = (
refreshAtMs: number,
nowMs: number = Date.now(),
) => Math.min(Math.max(refreshAtMs - nowMs, 0), REFRESH_POLL_INTERVAL_MS)
const runCopilotRefreshLoop = async (
refreshIn: number,
signal: AbortSignal,
) => {
let refreshAtMs = getRefreshDeadlineMs(refreshIn)
while (!signal.aborted) {
const nextDelayMs = getRefreshPollDelayMs(refreshAtMs)
if (nextDelayMs > 0) {
await delay(nextDelayMs, undefined, { signal })
continue
}
consola.debug("Refreshing Copilot token")
try {
const { token, refresh_in } = await getCopilotToken()
state.copilotToken = token
refreshAtMs = getRefreshDeadlineMs(refresh_in)
consola.debug("Copilot token refreshed")
if (state.showToken) {
consola.info("Refreshed Copilot token:", token)
}
} catch (error) {
consola.error("Failed to refresh Copilot token:", error)
refreshAtMs = Date.now() + RETRY_REFRESH_DELAY_MS
consola.warn(
`Retrying Copilot token refresh in ${RETRY_REFRESH_DELAY_MS / 1000}s`,
)
}
}
}
interface SetupGitHubTokenOptions {
force?: boolean
}
export async function setupGitHubToken(
options?: SetupGitHubTokenOptions,
): Promise<void> {
try {
const githubToken = await readGithubToken()
if (githubToken && !options?.force) {
state.githubToken = githubToken
if (state.showToken) {
consola.info("GitHub token:", githubToken)
}
await logUser()
return
}
consola.info("Not logged in, getting new access token")
const response = await getDeviceCode()
consola.debug("Device code response:", response)
consola.info(
`Please enter the code "${response.user_code}" in ${response.verification_uri}`,
)
const token = await pollAccessToken(response)
await writeGithubToken(token)
state.githubToken = token
if (state.showToken) {
consola.info("GitHub token:", token)
}
await logUser()
} catch (error) {
if (error instanceof HTTPError) {
consola.error("Failed to get GitHub token:", await error.response.json())
throw error
}
consola.error("Failed to get GitHub token:", error)
throw error
}
}
export async function logUser() {
const user = await getGitHubUser()
state.userName = user.login
consola.info(`Logged in as ${user.login}`)
const copilotUser = await getCopilotUsage()
state.copilotApiUrl = copilotUser.endpoints.api
}
|