mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-31 22:32:28 +00:00
241 lines
7.2 KiB
TypeScript
241 lines
7.2 KiB
TypeScript
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(process.cwd(), async () => {
|
|
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. git@github.com:sst/opencode.git
|
|
// ie. git@github.com:sst/opencode
|
|
// ie. ssh://git@github.com/sst/opencode.git
|
|
// ie. ssh://git@github.com/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}"`)
|
|
}
|
|
})
|
|
},
|
|
})
|