github.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import path from "path"
  2. import { $ } from "bun"
  3. import { exec } from "child_process"
  4. import * as prompts from "@clack/prompts"
  5. import { map, pipe, sortBy, values } from "remeda"
  6. import { UI } from "../ui"
  7. import { cmd } from "./cmd"
  8. import { ModelsDev } from "../../provider/models"
  9. import { Instance } from "../../project/instance"
  10. const WORKFLOW_FILE = ".github/workflows/opencode.yml"
  11. export const GithubCommand = cmd({
  12. command: "github",
  13. describe: "manage GitHub agent",
  14. builder: (yargs) => yargs.command(GithubInstallCommand).demandCommand(),
  15. async handler() {},
  16. })
  17. export const GithubInstallCommand = cmd({
  18. command: "install",
  19. describe: "install the GitHub agent",
  20. async handler() {
  21. await Instance.provide({
  22. directory: process.cwd(),
  23. async fn() {
  24. UI.empty()
  25. prompts.intro("Install GitHub agent")
  26. const app = await getAppInfo()
  27. await installGitHubApp()
  28. const providers = await ModelsDev.get()
  29. const provider = await promptProvider()
  30. const model = await promptModel()
  31. //const key = await promptKey()
  32. await addWorkflowFiles()
  33. printNextSteps()
  34. function printNextSteps() {
  35. let step2
  36. if (provider === "amazon-bedrock") {
  37. step2 =
  38. "Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
  39. } else {
  40. step2 = [
  41. ` 2. Add the following secrets in org or repo (${app.owner}/${app.repo}) settings`,
  42. "",
  43. ...providers[provider].env.map((e) => ` - ${e}`),
  44. ].join("\n")
  45. }
  46. prompts.outro(
  47. [
  48. "Next steps:",
  49. "",
  50. ` 1. Commit the \`${WORKFLOW_FILE}\` file and push`,
  51. step2,
  52. "",
  53. " 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action",
  54. "",
  55. " Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples",
  56. ].join("\n"),
  57. )
  58. }
  59. async function getAppInfo() {
  60. const project = Instance.project
  61. if (project.vcs !== "git") {
  62. prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
  63. throw new UI.CancelledError()
  64. }
  65. // Get repo info
  66. const info = await $`git remote get-url origin`
  67. .quiet()
  68. .nothrow()
  69. .text()
  70. .then((text) => text.trim())
  71. // match https or git pattern
  72. // ie. https://github.com/sst/opencode.git
  73. // ie. https://github.com/sst/opencode
  74. // ie. [email protected]:sst/opencode.git
  75. // ie. [email protected]:sst/opencode
  76. // ie. ssh://[email protected]/sst/opencode.git
  77. // ie. ssh://[email protected]/sst/opencode
  78. const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
  79. if (!parsed) {
  80. prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
  81. throw new UI.CancelledError()
  82. }
  83. const [, owner, repo] = parsed
  84. return { owner, repo, root: Instance.worktree }
  85. }
  86. async function promptProvider() {
  87. const priority: Record<string, number> = {
  88. opencode: 0,
  89. anthropic: 1,
  90. "github-copilot": 2,
  91. openai: 3,
  92. google: 4,
  93. openrouter: 5,
  94. vercel: 6,
  95. }
  96. let provider = await prompts.select({
  97. message: "Select provider",
  98. maxItems: 8,
  99. options: pipe(
  100. providers,
  101. values(),
  102. sortBy(
  103. (x) => priority[x.id] ?? 99,
  104. (x) => x.name ?? x.id,
  105. ),
  106. map((x) => ({
  107. label: x.name,
  108. value: x.id,
  109. hint: priority[x.id] <= 1 ? "recommended" : undefined,
  110. })),
  111. ),
  112. })
  113. if (prompts.isCancel(provider)) throw new UI.CancelledError()
  114. return provider
  115. }
  116. async function promptModel() {
  117. const providerData = providers[provider]!
  118. const model = await prompts.select({
  119. message: "Select model",
  120. maxItems: 8,
  121. options: pipe(
  122. providerData.models,
  123. values(),
  124. sortBy((x) => x.name ?? x.id),
  125. map((x) => ({
  126. label: x.name ?? x.id,
  127. value: x.id,
  128. })),
  129. ),
  130. })
  131. if (prompts.isCancel(model)) throw new UI.CancelledError()
  132. return model
  133. }
  134. async function installGitHubApp() {
  135. const s = prompts.spinner()
  136. s.start("Installing GitHub app")
  137. // Get installation
  138. const installation = await getInstallation()
  139. if (installation) return s.stop("GitHub app already installed")
  140. // Open browser
  141. const url = "https://github.com/apps/opencode-agent"
  142. const command =
  143. process.platform === "darwin"
  144. ? `open "${url}"`
  145. : process.platform === "win32"
  146. ? `start "${url}"`
  147. : `xdg-open "${url}"`
  148. exec(command, (error) => {
  149. if (error) {
  150. prompts.log.warn(`Could not open browser. Please visit: ${url}`)
  151. }
  152. })
  153. // Wait for installation
  154. s.message("Waiting for GitHub app to be installed")
  155. const MAX_RETRIES = 120
  156. let retries = 0
  157. do {
  158. const installation = await getInstallation()
  159. if (installation) break
  160. if (retries > MAX_RETRIES) {
  161. s.stop(
  162. `Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
  163. )
  164. throw new UI.CancelledError()
  165. }
  166. retries++
  167. await new Promise((resolve) => setTimeout(resolve, 1000))
  168. } while (true)
  169. s.stop("Installed GitHub app")
  170. async function getInstallation() {
  171. return await fetch(
  172. `https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`,
  173. )
  174. .then((res) => res.json())
  175. .then((data) => data.installation)
  176. }
  177. }
  178. async function addWorkflowFiles() {
  179. const envStr =
  180. provider === "amazon-bedrock"
  181. ? ""
  182. : `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
  183. await Bun.write(
  184. path.join(app.root, WORKFLOW_FILE),
  185. `
  186. name: opencode
  187. on:
  188. issue_comment:
  189. types: [created]
  190. jobs:
  191. opencode:
  192. if: |
  193. contains(github.event.comment.body, ' /oc') ||
  194. startsWith(github.event.comment.body, '/oc') ||
  195. contains(github.event.comment.body, ' /opencode') ||
  196. startsWith(github.event.comment.body, '/opencode')
  197. runs-on: ubuntu-latest
  198. permissions:
  199. contents: read
  200. id-token: write
  201. steps:
  202. - name: Checkout repository
  203. uses: actions/checkout@v4
  204. - name: Run opencode
  205. uses: sst/opencode/github@latest${envStr}
  206. with:
  207. model: ${provider}/${model}
  208. `.trim(),
  209. )
  210. prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
  211. }
  212. },
  213. })
  214. },
  215. })