Compare commits

..
Author SHA1 Message Date
DaxandGitHub 23786505b2 Merge branch 'dev' into trigger-publish-on-beta 2026-01-29 16:30:26 -05:00
FrankandGitHub 6cc739701b zen: kimi k2.5 free (#11199) 2026-01-29 21:28:54 +00:00
DaxandGitHub 2125dc11c7 fix: show all provider models when no providers connected (#11198) 2026-01-29 21:19:50 +00:00
Dax Raad e2c6ab3a45 ci: trigger publish workflow after beta sync 2026-01-29 16:16:36 -05:00
Dax Raad 29d8cc6a1c fix: show all provider models when no providers connected
When no providers were connected, the model dialog would hide models
that were in the recent list, but the recent section wasn't being
displayed. This caused models to completely disappear.

The filter was checking recentList (which only contained non-favorite
recent items) instead of recents (all recent items), causing models
to be incorrectly filtered out of the main provider list when the
recent section wasn't shown.
2026-01-29 16:14:03 -05:00
FilipandGitHub aa1d0f6167 fix(app): better header item wrapping (#10831) 2026-01-29 14:51:56 -06:00
fdd484d2c1 feat: expose acp thinking variants (#9064)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-29 20:15:36 +00:00
DaxandGitHub cd4075faf6 feat: add beta branch sync workflow for contributor PRs (#11190) 2026-01-29 20:02:36 +00:00
DaxandGitHub 33311e9950 ci: remove push triggers from workflow files (#11186) 2026-01-29 19:25:05 +00:00
DaxandGitHub a92b7923c2 ci: disable nix-desktop workflow (#11188) 2026-01-29 19:16:26 +00:00
14 changed files with 778 additions and 563 deletions
+40
View File
@@ -0,0 +1,40 @@
name: beta
on:
push:
branches: [dev]
pull_request:
types: [opened, synchronize, labeled, unlabeled]
jobs:
sync:
if: |
github.event_name == 'push' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'contributor'))
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: read
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Sync beta branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bun script/beta.ts
- name: Trigger publish workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run publish.yml --ref beta
+1
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
@@ -0,0 +1,46 @@
name: nix-desktop
on:
push:
branches: [dev]
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
pull_request:
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
workflow_dispatch:
jobs:
nix-desktop:
strategy:
fail-fast: false
matrix:
os:
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-ubuntu-2404-arm
- macos-15-intel
- macos-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Build desktop via flake
run: |
set -euo pipefail
nix --version
nix build .#desktop -L
+1
View File
@@ -6,6 +6,7 @@ on:
branches:
- ci
- dev
- beta
- snapshot-*
workflow_dispatch:
inputs:
+329 -487
View File
File diff suppressed because it is too large Load Diff
@@ -130,7 +130,7 @@ export function SessionHeader() {
<Portal mount={mount()}>
<button
type="button"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
class="hidden md:flex w-[320px] max-w-full min-w-0 p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
aria-label={language.t("session.header.searchFiles")}
>
+23 -12
View File
@@ -132,12 +132,14 @@ export function Titlebar() {
}
return (
<header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
data-tauri-drag-region
>
<div
classList={{
"flex items-center w-full min-w-0": true,
"flex items-center min-w-0": true,
"pl-2": !mac(),
"pr-6": !windows(),
}}
onMouseDown={drag}
data-tauri-drag-region
@@ -218,20 +220,29 @@ export function Titlebar() {
</div>
</div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
<div class="flex-1 h-full" data-tauri-drag-region />
<div
id="opencode-titlebar-right"
class="flex items-center gap-3 shrink-0 flex-1 justify-end"
data-tauri-drag-region
/>
</div>
<div
class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center"
data-tauri-drag-region
>
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
</div>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-6": !windows(),
}}
onMouseDown={drag}
data-tauri-drag-region
>
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" data-tauri-drag-region />
<Show when={windows()}>
<div class="w-6 shrink-0" />
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto" />
</div>
</header>
)
}
+1 -1
View File
@@ -49,7 +49,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.12.0",
"@agentclientprotocol/sdk": "0.13.0",
"@ai-sdk/amazon-bedrock": "3.0.73",
"@ai-sdk/anthropic": "2.0.57",
"@ai-sdk/azure": "2.0.91",
+186 -56
View File
@@ -26,6 +26,7 @@ import {
type ToolCallContent,
type ToolKind,
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
@@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai"
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { applyPatch } from "diff"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
const DEFAULT_VARIANT_VALUE = "default"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
@@ -476,7 +482,7 @@ export namespace ACP {
sessionId,
models: load.models,
modes: load.modes,
_meta: {},
_meta: load._meta,
}
} catch (e) {
const error = MessageV2.fromError(e, {
@@ -529,7 +535,7 @@ export namespace ACP {
providerID: lastUser.model.providerID,
modelID: lastUser.model.modelID,
})
if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) {
if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
result.modes.currentModeId = lastUser.agent
this.sessionManager.setMode(sessionId, lastUser.agent)
}
@@ -956,27 +962,7 @@ export namespace ACP {
}
}
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = providers.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
const availableModels = entries.flatMap((provider) => {
const models = Provider.sort(Object.values(provider.models))
return models.map((model) => ({
modelId: `${provider.id}/${model.id}`,
name: `${provider.name}/${model.name}`,
}))
})
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
const agents = await this.config.sdk.app
.agents(
{
@@ -986,6 +972,56 @@ export namespace ACP {
)
.then((resp) => resp.data!)
return agents
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
}
private async resolveModeState(
directory: string,
sessionId: string,
): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
const availableModes = await this.loadAvailableModes(directory)
const currentModeId =
this.sessionManager.get(sessionId).modeId ||
(await (async () => {
if (!availableModes.length) return undefined
const defaultAgentName = await AgentModule.defaultAgent()
const resolvedModeId =
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, resolvedModeId)
return resolvedModeId
})())
return { availableModes, currentModeId }
}
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, model)
const currentVariant = this.sessionManager.getVariant(sessionId)
if (currentVariant && !availableVariants.includes(currentVariant)) {
this.sessionManager.setVariant(sessionId, undefined)
}
const availableModels = buildAvailableModels(entries, { includeVariants: true })
const modeState = await this.resolveModeState(directory, sessionId)
const currentModeId = modeState.currentModeId
const modes = currentModeId
? {
availableModes: modeState.availableModes,
currentModeId,
}
: undefined
const commands = await this.config.sdk.command
.list(
{
@@ -1006,20 +1042,6 @@ export namespace ACP {
description: "compact the session",
})
const availableModes = agents
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
const defaultAgentName = await AgentModule.defaultAgent()
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
// Persist the default mode so prompt() uses it immediately
this.sessionManager.setMode(sessionId, currentModeId)
const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
if ("type" in server) {
@@ -1073,40 +1095,46 @@ export namespace ACP {
return {
sessionId,
models: {
currentModelId: `${model.providerID}/${model.modelID}`,
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
availableModels,
},
modes: {
availableModes,
currentModeId,
},
_meta: {},
modes,
_meta: buildVariantMeta({
model,
variant: this.sessionManager.getVariant(sessionId),
availableVariants,
}),
}
}
async unstable_setSessionModel(params: SetSessionModelRequest) {
const session = this.sessionManager.get(params.sessionId)
const providers = await this.sdk.config
.providers({ directory: session.cwd }, { throwOnError: true })
.then((x) => x.data!.providers)
const model = Provider.parseModel(params.modelId)
const selection = parseModelSelection(params.modelId, providers)
this.sessionManager.setModel(session.id, selection.model)
this.sessionManager.setVariant(session.id, selection.variant)
this.sessionManager.setModel(session.id, {
providerID: model.providerID,
modelID: model.modelID,
})
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, selection.model)
return {
_meta: {},
_meta: buildVariantMeta({
model: selection.model,
variant: selection.variant,
availableVariants,
}),
}
}
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
this.sessionManager.get(params.sessionId)
await this.config.sdk.app
.agents({}, { throwOnError: true })
.then((x) => x.data)
.then((agent) => {
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
})
const session = this.sessionManager.get(params.sessionId)
const availableModes = await this.loadAvailableModes(session.cwd)
if (!availableModes.some((mode) => mode.id === params.modeId)) {
throw new Error(`Agent not found: ${params.modeId}`)
}
this.sessionManager.setMode(params.sessionId, params.modeId)
}
@@ -1223,6 +1251,7 @@ export namespace ACP {
providerID: model.providerID,
modelID: model.modelID,
},
variant: this.sessionManager.getVariant(sessionID),
parts,
agent,
directory,
@@ -1434,4 +1463,105 @@ export namespace ACP {
}
return result
}
function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
return [...providers].sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
}
function modelVariantsFromProviders(
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
model: { providerID: string; modelID: string },
): string[] {
const provider = providers.find((entry) => entry.id === model.providerID)
if (!provider) return []
const modelInfo = provider.models[model.modelID]
if (!modelInfo?.variants) return []
return Object.keys(modelInfo.variants)
}
function buildAvailableModels(
providers: Array<{ id: string; name: string; models: Record<string, any> }>,
options: { includeVariants?: boolean } = {},
): ModelOption[] {
const includeVariants = options.includeVariants ?? false
return providers.flatMap((provider) => {
const models = Provider.sort(Object.values(provider.models) as any)
return models.flatMap((model) => {
const base: ModelOption = {
modelId: `${provider.id}/${model.id}`,
name: `${provider.name}/${model.name}`,
}
if (!includeVariants || !model.variants) return [base]
const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
const variantOptions = variants.map((variant) => ({
modelId: `${provider.id}/${model.id}/${variant}`,
name: `${provider.name}/${model.name} (${variant})`,
}))
return [base, ...variantOptions]
})
})
}
function formatModelIdWithVariant(
model: { providerID: string; modelID: string },
variant: string | undefined,
availableVariants: string[],
includeVariant: boolean,
) {
const base = `${model.providerID}/${model.modelID}`
if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
return `${base}/${variant}`
}
function buildVariantMeta(input: {
model: { providerID: string; modelID: string }
variant?: string
availableVariants: string[]
}) {
return {
opencode: {
modelId: `${input.model.providerID}/${input.model.modelID}`,
variant: input.variant ?? null,
availableVariants: input.availableVariants,
},
}
}
function parseModelSelection(
modelId: string,
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
): { model: { providerID: string; modelID: string }; variant?: string } {
const parsed = Provider.parseModel(modelId)
const provider = providers.find((p) => p.id === parsed.providerID)
if (!provider) {
return { model: parsed, variant: undefined }
}
// Check if modelID exists directly
if (provider.models[parsed.modelID]) {
return { model: parsed, variant: undefined }
}
// Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
const segments = parsed.modelID.split("/")
if (segments.length > 1) {
const candidateVariant = segments[segments.length - 1]
const baseModelId = segments.slice(0, -1).join("/")
const baseModelInfo = provider.models[baseModelId]
if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
return {
model: { providerID: parsed.providerID, modelID: baseModelId },
variant: candidateVariant,
}
}
}
return { model: parsed, variant: undefined }
}
}
+12
View File
@@ -96,6 +96,18 @@ export class ACPSessionManager {
return session
}
getVariant(sessionId: string) {
const session = this.get(sessionId)
return session.variant
}
setVariant(sessionId: string, variant?: string) {
const session = this.get(sessionId)
session.variant = variant
this.sessions.set(sessionId, session)
return session
}
setMode(sessionId: string, modeId: string) {
const session = this.get(sessionId)
session.modeId = modeId
+1
View File
@@ -10,6 +10,7 @@ export interface ACPSessionState {
providerID: string
modelID: string
}
variant?: string
modeId?: string
}
@@ -158,7 +158,7 @@ export function DialogModel(props: { providerID?: string }) {
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inFavorites) return false
const inRecents = recentList.some(
const inRecents = recents.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
)
if (inRecents) return false
+9 -5
View File
@@ -87,6 +87,7 @@ You can also access our models through the following API endpoints.
| GLM 4.7 Free | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -120,7 +121,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| GLM 4.7 Free | Free | Free | Free | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2.5 Free | Free | Free | Free | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
@@ -153,8 +155,9 @@ Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don
The free models:
- GLM 4.7 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- MiniMax M2.1 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- GLM 4.7 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Kimi M2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- MiniMax M2.1 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
<a href={email}>Contact us</a> if you have any questions.
@@ -184,9 +187,10 @@ charging you more than $20 if your balance goes below $5.
All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
- GLM 4.7: During its free period, collected data may be used to improve the model.
- MiniMax M2.1: During its free period, collected data may be used to improve the model.
- Big Pickle: During its free period, collected data may be used to improve the model.
- GLM 4.7 Free: During its free period, collected data may be used to improve the model.
- Kimi K2.5 Free: During its free period, collected data may be used to improve the model.
- MiniMax M2.1 Free: During its free period, collected data may be used to improve the model.
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
Executable
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bun
interface PR {
number: number
headRefName: string
headRefOid: string
createdAt: string
isDraft: boolean
title: string
}
async function main() {
console.log("Fetching open contributor PRs...")
const prsResult =
await $`gh pr list --label contributor --state open --json number,headRefName,headRefOid,createdAt,isDraft,title --limit 100`.nothrow()
if (prsResult.exitCode !== 0) {
throw new Error(`Failed to fetch PRs: ${prsResult.stderr}`)
}
const allPRs: PR[] = JSON.parse(prsResult.stdout)
const prs = allPRs.filter((pr) => !pr.isDraft)
console.log(`Found ${prs.length} open non-draft contributor PRs`)
console.log("Fetching latest dev branch...")
const fetchDev = await $`git fetch origin dev`.nothrow()
if (fetchDev.exitCode !== 0) {
throw new Error(`Failed to fetch dev branch: ${fetchDev.stderr}`)
}
console.log("Checking out beta branch...")
const checkoutBeta = await $`git checkout -B beta origin/dev`.nothrow()
if (checkoutBeta.exitCode !== 0) {
throw new Error(`Failed to checkout beta branch: ${checkoutBeta.stderr}`)
}
const applied: number[] = []
const skipped: Array<{ number: number; reason: string }> = []
for (const pr of prs) {
console.log(`\nProcessing PR #${pr.number}: ${pr.title}`)
const fetchPR = await $`git fetch origin pull/${pr.number}/head:pr-${pr.number}`.nothrow()
if (fetchPR.exitCode !== 0) {
console.log(` Failed to fetch PR #${pr.number}, skipping`)
skipped.push({ number: pr.number, reason: "Failed to fetch" })
continue
}
const merge = await $`git merge --squash pr-${pr.number}`.nothrow()
if (merge.exitCode !== 0) {
console.log(` Squash merge failed for PR #${pr.number}`)
console.log(` Error: ${merge.stderr}`)
await $`git reset --hard HEAD`.nothrow()
skipped.push({ number: pr.number, reason: `Squash merge failed: ${merge.stderr}` })
continue
}
const add = await $`git add -A`.nothrow()
if (add.exitCode !== 0) {
console.log(` Failed to stage changes for PR #${pr.number}`)
await $`git reset --hard HEAD`.nothrow()
skipped.push({ number: pr.number, reason: "Failed to stage" })
continue
}
const status = await $`git status --porcelain`.nothrow()
if (status.exitCode !== 0 || !status.stdout.trim()) {
console.log(` No changes to commit for PR #${pr.number}, skipping`)
await $`git reset --hard HEAD`.nothrow()
skipped.push({ number: pr.number, reason: "No changes to commit" })
continue
}
const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
const commit = await Bun.spawn(["git", "commit", "-m", commitMsg], { stdout: "pipe", stderr: "pipe" })
const commitExit = await commit.exited
const commitStderr = await Bun.readableStreamToText(commit.stderr)
if (commitExit !== 0) {
console.log(` Failed to commit PR #${pr.number}`)
console.log(` Error: ${commitStderr}`)
await $`git reset --hard HEAD`.nothrow()
skipped.push({ number: pr.number, reason: `Commit failed: ${commitStderr}` })
continue
}
console.log(` Successfully applied PR #${pr.number}`)
applied.push(pr.number)
}
console.log("\n--- Summary ---")
console.log(`Applied: ${applied.length} PRs`)
applied.forEach((num) => console.log(` - PR #${num}`))
console.log(`Skipped: ${skipped.length} PRs`)
skipped.forEach((x) => console.log(` - PR #${x.number}: ${x.reason}`))
console.log("\nForce pushing beta branch...")
const push = await $`git push origin beta --force`.nothrow()
if (push.exitCode !== 0) {
throw new Error(`Failed to push beta branch: ${push.stderr}`)
}
console.log("Successfully synced beta branch")
}
main().catch((err) => {
console.error("Error:", err)
process.exit(1)
})
function $(strings: TemplateStringsArray, ...values: unknown[]) {
const cmd = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
return {
async nothrow() {
const proc = Bun.spawn(cmd.split(" "), {
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
return { exitCode, stdout, stderr }
},
}
}