| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import path from "path"
- import { $ } from "bun"
- import { exec } from "child_process"
- import * as prompts from "@clack/prompts"
- import { map, pipe, sortBy, values } from "remeda"
- import { UI } from "../ui"
- import { cmd } from "./cmd"
- import { ModelsDev } from "../../provider/models"
- import { Instance } from "../../project/instance"
- const WORKFLOW_FILE = ".github/workflows/opencode.yml"
- export const GithubCommand = cmd({
- command: "github",
- describe: "manage GitHub agent",
- builder: (yargs) => yargs.command(GithubInstallCommand).demandCommand(),
- async handler() {},
- })
- export const GithubInstallCommand = cmd({
- command: "install",
- describe: "install the GitHub agent",
- async handler() {
- await Instance.provide({
- directory: process.cwd(),
- async fn() {
- UI.empty()
- prompts.intro("Install GitHub agent")
- const app = await getAppInfo()
- await installGitHubApp()
- const providers = await ModelsDev.get()
- const provider = await promptProvider()
- const model = await promptModel()
- //const key = await promptKey()
- await addWorkflowFiles()
- printNextSteps()
- function printNextSteps() {
- let step2
- if (provider === "amazon-bedrock") {
- step2 =
- "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"
- } else {
- step2 = [
- ` 2. Add the following secrets in org or repo (${app.owner}/${app.repo}) settings`,
- "",
- ...providers[provider].env.map((e) => ` - ${e}`),
- ].join("\n")
- }
- prompts.outro(
- [
- "Next steps:",
- "",
- ` 1. Commit the \`${WORKFLOW_FILE}\` file and push`,
- step2,
- "",
- " 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action",
- "",
- " Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples",
- ].join("\n"),
- )
- }
- async function getAppInfo() {
- const project = Instance.project
- if (project.vcs !== "git") {
- prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
- throw new UI.CancelledError()
- }
- // Get repo info
- const info = await $`git remote get-url origin`
- .quiet()
- .nothrow()
- .text()
- .then((text) => text.trim())
- // match https or git pattern
- // ie. https://github.com/sst/opencode.git
- // ie. https://github.com/sst/opencode
- // ie. [email protected]:sst/opencode.git
- // ie. [email protected]:sst/opencode
- // ie. ssh://[email protected]/sst/opencode.git
- // ie. ssh://[email protected]/sst/opencode
- const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
- if (!parsed) {
- prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
- throw new UI.CancelledError()
- }
- const [, owner, repo] = parsed
- return { owner, repo, root: Instance.worktree }
- }
- async function promptProvider() {
- const priority: Record<string, number> = {
- opencode: 0,
- anthropic: 1,
- "github-copilot": 2,
- openai: 3,
- google: 4,
- openrouter: 5,
- vercel: 6,
- }
- let provider = await prompts.select({
- message: "Select provider",
- maxItems: 8,
- options: pipe(
- providers,
- values(),
- sortBy(
- (x) => priority[x.id] ?? 99,
- (x) => x.name ?? x.id,
- ),
- map((x) => ({
- label: x.name,
- value: x.id,
- hint: priority[x.id] <= 1 ? "recommended" : undefined,
- })),
- ),
- })
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
- return provider
- }
- async function promptModel() {
- const providerData = providers[provider]!
- const model = await prompts.select({
- message: "Select model",
- maxItems: 8,
- options: pipe(
- providerData.models,
- values(),
- sortBy((x) => x.name ?? x.id),
- map((x) => ({
- label: x.name ?? x.id,
- value: x.id,
- })),
- ),
- })
- if (prompts.isCancel(model)) throw new UI.CancelledError()
- return model
- }
- async function installGitHubApp() {
- const s = prompts.spinner()
- s.start("Installing GitHub app")
- // Get installation
- const installation = await getInstallation()
- if (installation) return s.stop("GitHub app already installed")
- // Open browser
- const url = "https://github.com/apps/opencode-agent"
- const command =
- process.platform === "darwin"
- ? `open "${url}"`
- : process.platform === "win32"
- ? `start "${url}"`
- : `xdg-open "${url}"`
- exec(command, (error) => {
- if (error) {
- prompts.log.warn(`Could not open browser. Please visit: ${url}`)
- }
- })
- // Wait for installation
- s.message("Waiting for GitHub app to be installed")
- const MAX_RETRIES = 120
- let retries = 0
- do {
- const installation = await getInstallation()
- if (installation) break
- if (retries > MAX_RETRIES) {
- s.stop(
- `Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
- )
- throw new UI.CancelledError()
- }
- retries++
- await new Promise((resolve) => setTimeout(resolve, 1000))
- } while (true)
- s.stop("Installed GitHub app")
- async function getInstallation() {
- return await fetch(
- `https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`,
- )
- .then((res) => res.json())
- .then((data) => data.installation)
- }
- }
- async function addWorkflowFiles() {
- const envStr =
- provider === "amazon-bedrock"
- ? ""
- : `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
- await Bun.write(
- path.join(app.root, WORKFLOW_FILE),
- `
- name: opencode
- on:
- issue_comment:
- types: [created]
- jobs:
- opencode:
- if: |
- contains(github.event.comment.body, ' /oc') ||
- startsWith(github.event.comment.body, '/oc') ||
- contains(github.event.comment.body, ' /opencode') ||
- startsWith(github.event.comment.body, '/opencode')
- runs-on: ubuntu-latest
- permissions:
- contents: read
- id-token: write
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- - name: Run opencode
- uses: sst/opencode/github@latest${envStr}
- with:
- model: ${provider}/${model}
- `.trim(),
- )
- prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
- }
- },
- })
- },
- })
|