auth.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { AuthAnthropic } from "../../auth/anthropic"
  2. import { AuthCopilot } from "../../auth/copilot"
  3. import { Auth } from "../../auth"
  4. import { cmd } from "./cmd"
  5. import * as prompts from "@clack/prompts"
  6. import open from "open"
  7. import { UI } from "../ui"
  8. import { ModelsDev } from "../../provider/models"
  9. import { map, pipe, sortBy, values } from "remeda"
  10. import path from "path"
  11. import os from "os"
  12. import { Global } from "../../global"
  13. export const AuthCommand = cmd({
  14. command: "auth",
  15. describe: "manage credentials",
  16. builder: (yargs) =>
  17. yargs
  18. .command(AuthLoginCommand)
  19. .command(AuthLogoutCommand)
  20. .command(AuthListCommand)
  21. .demandCommand(),
  22. async handler() {},
  23. })
  24. export const AuthListCommand = cmd({
  25. command: "list",
  26. aliases: ["ls"],
  27. describe: "list providers",
  28. async handler() {
  29. UI.empty()
  30. const authPath = path.join(Global.Path.data, "auth.json")
  31. const homedir = os.homedir()
  32. const displayPath = authPath.startsWith(homedir)
  33. ? authPath.replace(homedir, "~")
  34. : authPath
  35. prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
  36. const results = await Auth.all().then((x) => Object.entries(x))
  37. const database = await ModelsDev.get()
  38. for (const [providerID, result] of results) {
  39. const name = database[providerID]?.name || providerID
  40. prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
  41. }
  42. prompts.outro(`${results.length} credentials`)
  43. // Environment variables section
  44. const activeEnvVars: Array<{ provider: string; envVar: string }> = []
  45. for (const [providerID, provider] of Object.entries(database)) {
  46. for (const envVar of provider.env) {
  47. if (process.env[envVar]) {
  48. activeEnvVars.push({
  49. provider: provider.name || providerID,
  50. envVar,
  51. })
  52. }
  53. }
  54. }
  55. if (activeEnvVars.length > 0) {
  56. UI.empty()
  57. prompts.intro("Environment")
  58. for (const { provider, envVar } of activeEnvVars) {
  59. prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
  60. }
  61. prompts.outro(`${activeEnvVars.length} environment variables`)
  62. }
  63. },
  64. })
  65. export const AuthLoginCommand = cmd({
  66. command: "login",
  67. describe: "log in to a provider",
  68. async handler() {
  69. UI.empty()
  70. prompts.intro("Add credential")
  71. const providers = await ModelsDev.get()
  72. const priority: Record<string, number> = {
  73. anthropic: 0,
  74. "github-copilot": 1,
  75. openai: 2,
  76. google: 3,
  77. }
  78. let provider = await prompts.select({
  79. message: "Select provider",
  80. maxItems: 8,
  81. options: [
  82. ...pipe(
  83. providers,
  84. values(),
  85. sortBy(
  86. (x) => priority[x.id] ?? 99,
  87. (x) => x.name ?? x.id,
  88. ),
  89. map((x) => ({
  90. label: x.name,
  91. value: x.id,
  92. hint: priority[x.id] === 0 ? "recommended" : undefined,
  93. })),
  94. ),
  95. {
  96. value: "other",
  97. label: "Other",
  98. },
  99. ],
  100. })
  101. if (prompts.isCancel(provider)) throw new UI.CancelledError()
  102. if (provider === "other") {
  103. provider = await prompts.text({
  104. message: "Enter provider id",
  105. validate: (x) =>
  106. x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
  107. })
  108. if (prompts.isCancel(provider)) throw new UI.CancelledError()
  109. provider = provider.replace(/^@ai-sdk\//, "")
  110. if (prompts.isCancel(provider)) throw new UI.CancelledError()
  111. prompts.log.warn(
  112. `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
  113. )
  114. }
  115. if (provider === "amazon-bedrock") {
  116. prompts.log.info(
  117. "Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
  118. )
  119. prompts.outro("Done")
  120. return
  121. }
  122. if (provider === "anthropic") {
  123. const method = await prompts.select({
  124. message: "Login method",
  125. options: [
  126. {
  127. label: "Claude Pro/Max",
  128. value: "oauth",
  129. },
  130. {
  131. label: "API Key",
  132. value: "api",
  133. },
  134. ],
  135. })
  136. if (prompts.isCancel(method)) throw new UI.CancelledError()
  137. if (method === "oauth") {
  138. // some weird bug where program exits without this
  139. await new Promise((resolve) => setTimeout(resolve, 10))
  140. const { url, verifier } = await AuthAnthropic.authorize()
  141. prompts.note("Trying to open browser...")
  142. try {
  143. await open(url)
  144. } catch (e) {
  145. prompts.log.error(
  146. "Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
  147. )
  148. }
  149. prompts.log.info(url)
  150. const code = await prompts.text({
  151. message: "Paste the authorization code here: ",
  152. validate: (x) => (x.length > 0 ? undefined : "Required"),
  153. })
  154. if (prompts.isCancel(code)) throw new UI.CancelledError()
  155. await AuthAnthropic.exchange(code, verifier)
  156. .then(() => {
  157. prompts.log.success("Login successful")
  158. })
  159. .catch(() => {
  160. prompts.log.error("Invalid code")
  161. })
  162. prompts.outro("Done")
  163. return
  164. }
  165. }
  166. const copilot = await AuthCopilot()
  167. if (provider === "github-copilot" && copilot) {
  168. await new Promise((resolve) => setTimeout(resolve, 10))
  169. const deviceInfo = await copilot.authorize()
  170. prompts.note(
  171. `Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
  172. )
  173. const spinner = prompts.spinner()
  174. spinner.start("Waiting for authorization...")
  175. while (true) {
  176. await new Promise((resolve) =>
  177. setTimeout(resolve, deviceInfo.interval * 1000),
  178. )
  179. const response = await copilot.poll(deviceInfo.device)
  180. if (response.status === "pending") continue
  181. if (response.status === "success") {
  182. await Auth.set("github-copilot", {
  183. type: "oauth",
  184. refresh: response.refresh,
  185. access: response.access,
  186. expires: response.expires,
  187. })
  188. spinner.stop("Login successful")
  189. break
  190. }
  191. if (response.status === "failed") {
  192. spinner.stop("Failed to authorize", 1)
  193. break
  194. }
  195. }
  196. prompts.outro("Done")
  197. return
  198. }
  199. const key = await prompts.password({
  200. message: "Enter your API key",
  201. validate: (x) => (x.length > 0 ? undefined : "Required"),
  202. })
  203. if (prompts.isCancel(key)) throw new UI.CancelledError()
  204. await Auth.set(provider, {
  205. type: "api",
  206. key,
  207. })
  208. prompts.outro("Done")
  209. },
  210. })
  211. export const AuthLogoutCommand = cmd({
  212. command: "logout",
  213. describe: "log out from a configured provider",
  214. async handler() {
  215. UI.empty()
  216. const credentials = await Auth.all().then((x) => Object.entries(x))
  217. prompts.intro("Remove credential")
  218. if (credentials.length === 0) {
  219. prompts.log.error("No credentials found")
  220. return
  221. }
  222. const database = await ModelsDev.get()
  223. const providerID = await prompts.select({
  224. message: "Select provider",
  225. options: credentials.map(([key, value]) => ({
  226. label:
  227. (database[key]?.name || key) +
  228. UI.Style.TEXT_DIM +
  229. " (" +
  230. value.type +
  231. ")",
  232. value: key,
  233. })),
  234. })
  235. if (prompts.isCancel(providerID)) throw new UI.CancelledError()
  236. await Auth.remove(providerID)
  237. prompts.outro("Logout successful")
  238. },
  239. })