diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml deleted file mode 100644 index e93d5fbdb26..00000000000 --- a/.github/workflows/beta.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: beta - -on: - workflow_dispatch: - schedule: - - cron: "0 * * * *" - -jobs: - sync: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup Git Committer - id: setup-git-committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Install OpenCode - run: bun i -g opencode-ai - - - name: Sync beta branch - env: - GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - run: bun script/beta.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c903426458c..c035a2e3fb5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: - ci - dev - beta + - v2 - fix/npm-native-binary-install - snapshot-* workflow_dispatch: @@ -32,7 +33,7 @@ permissions: packages: write env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }} + OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }} jobs: version: @@ -45,6 +46,13 @@ jobs: - uses: ./.github/actions/setup-bun + - name: Deploy update service + if: github.ref_name == 'v2' || github.ref_name == 'beta' + working-directory: packages/updates + run: bun run deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + - name: Setup git committer id: committer uses: ./.github/actions/setup-git-committer @@ -74,7 +82,7 @@ jobs: build-cli: needs: version runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' + if: github.repository == 'anomalyco/opencode' steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: @@ -188,7 +196,7 @@ jobs: build-node-cli: needs: version - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' + if: github.repository == 'anomalyco/opencode' strategy: fail-fast: false matrix: @@ -338,6 +346,7 @@ jobs: build-electron: needs: - version + - sign-cli-macos if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' continue-on-error: false env: @@ -376,6 +385,12 @@ jobs: steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + if: github.ref_name == 'beta' + with: + name: opencode-preview-cli + path: packages/cli/dist + - uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0 if: runner.os == 'macOS' with: @@ -434,6 +449,7 @@ jobs: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CLI_TARGET: ${{ matrix.settings.target }} + OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }} - name: Build run: bun run build @@ -572,13 +588,11 @@ jobs: path: packages/opencode/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'beta' with: name: opencode-preview-cli path: packages/cli/dist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name != 'beta' with: pattern: opencode-node-cli-* path: packages/cli/dist/node diff --git a/AGENTS.md b/AGENTS.md index 583ca5a9026..8cbc83706dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ - Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions. - Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`. -- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `next` TUI storage channel so tabs and other client-local state match the installed client. +- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client. - Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it. ## V2 TUI Stories diff --git a/install b/install index 70b22afb037..c9098cc15e9 100755 --- a/install +++ b/install @@ -15,13 +15,13 @@ Usage: install.sh [options] Options: -h, --help Display this help message - -v, --version Install a specific version (e.g., 0.0.0-next-17236) + -v, --version Install a specific version (e.g., 0.0.0-beta-17236) -b, --binary Install from a local binary instead of downloading --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.) Examples: curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash - curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash -s -- --version 0.0.0-next-17236 + curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash -s -- --version 0.0.0-beta-17236 ./install --binary /path/to/opencode2 EOF } @@ -166,7 +166,7 @@ else fi if [ -z "$requested_version" ]; then - metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/next || true) + metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true) specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') if [ -z "$specific_version" ]; then diff --git a/package.json b/package.json index be9f6d99df4..7a20291b08a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "packageManager": "bun@1.3.14", "scripts": { "dev": "bun run --cwd packages/cli --conditions=browser src/index.ts", - "dev:live": "OPENCODE_TUI_CHANNEL=next OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"", + "dev:live": "OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"", "dev:desktop": "bun --cwd packages/desktop dev", "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index 7312483dc9a..ec7895e7e46 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -14,9 +14,13 @@ async function published(name: string, version: string) { async function publish(dir: string, name: string, version: string) { if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) - if (await published(name, version)) return console.log(`already published ${name}@${version}`) - await $`bun pm pack`.cwd(dir) - await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) + const exists = await published(name, version) + if (exists) console.log(`already published ${name}@${version}`) + if (!exists) { + await $`bun pm pack`.cwd(dir) + await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) + } + if (Script.channel === "beta") await $`npm dist-tag add ${`${name}@${version}`} next` } async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) { diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts index 5f6031257da..e99cfeff2aa 100644 --- a/packages/cli/src/server-process.ts +++ b/packages/cli/src/server-process.ts @@ -84,7 +84,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) { database: { path: process.env.OPENCODE_DB ?? - (["latest", "beta", "next", "prod"].includes(OPENCODE_CHANNEL) || + (["latest", "dev", "beta", "next", "prod"].includes(OPENCODE_CHANNEL) || process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" || process.env.OPENCODE_DISABLE_CHANNEL_DB === "true" ? "opencode.db" diff --git a/packages/cli/src/services/service-config.ts b/packages/cli/src/services/service-config.ts index d348f390c2d..3c8dbba2260 100644 --- a/packages/cli/src/services/service-config.ts +++ b/packages/cli/src/services/service-config.ts @@ -25,12 +25,12 @@ const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info)) const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Service.Info)) export function filename(channel = OPENCODE_CHANNEL) { - if (channel === "latest" || channel === "next") return "service.json" + if (channel === "latest" || channel === "dev" || channel === "beta" || channel === "next") return "service.json" return `service-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.json` } export function defaultPort(channel = OPENCODE_CHANNEL) { - if (channel === "latest" || channel === "next") return 0xc0de + if (channel === "latest" || channel === "dev" || channel === "beta" || channel === "next") return 0xc0de if (channel === "local") return 0xc0df return 10_000 + (Number.parseInt(Hash.fast(channel).slice(0, 8), 16) % 50_000) } diff --git a/packages/cli/src/services/updater.test.ts b/packages/cli/src/services/updater.test.ts index 3977205c641..94a44aa803d 100644 --- a/packages/cli/src/services/updater.test.ts +++ b/packages/cli/src/services/updater.test.ts @@ -35,7 +35,8 @@ describe("updater", () => { test("accepts strict release version variants", () => { expect(action("v1.2.3", " 1.2.4\n", true)).toBe("upgrade") expect(action("1.2.3-alpha.1", "1.2.3-alpha.2", true)).toBe("upgrade") - expect(action("0.0.0-next-17403", "0.0.0-next-17403.2", true)).toBe("upgrade") + expect(action("0.0.0-dev-17403", "0.0.0-dev-17403.2", true)).toBe("upgrade") + expect(action("0.0.0-next-17403", "0.0.0-beta-17404", true)).toBe("upgrade") expect(action("1.2.3+old", "1.2.3+new", true)).toBe("none") expect(action("v1.2.3+old", "1.2.3", true)).toBe("none") }) diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts index 3e29d6e4430..62b92bdd8f9 100644 --- a/packages/cli/test/service.test.ts +++ b/packages/cli/test/service.test.ts @@ -11,6 +11,8 @@ import { ServiceConfig } from "../src/services/service-config" test("managed service ports are stable per installation channel", () => { expect(ServiceConfig.defaultPort("latest")).toBe(0xc0de) + expect(ServiceConfig.defaultPort("dev")).toBe(0xc0de) + expect(ServiceConfig.defaultPort("beta")).toBe(0xc0de) expect(ServiceConfig.defaultPort("next")).toBe(0xc0de) expect(ServiceConfig.defaultPort("local")).toBe(0xc0df) expect(ServiceConfig.defaultPort("preview-a")).toBe(ServiceConfig.defaultPort("preview-a")) @@ -37,6 +39,8 @@ test("local channel stores service config with the local service filename", asyn test("service filenames share release channels and identify preview channels", () => { expect(ServiceConfig.filename("latest")).toBe("service.json") + expect(ServiceConfig.filename("dev")).toBe("service.json") + expect(ServiceConfig.filename("beta")).toBe("service.json") expect(ServiceConfig.filename("next")).toBe("service.json") expect(ServiceConfig.filename("local")).toBe("service-local.json") expect(ServiceConfig.filename("preview-a")).toBe("service-preview-a.json") diff --git a/packages/desktop/scripts/prebuild.ts b/packages/desktop/scripts/prebuild.ts index 72f3b5a30a0..a7a3793a522 100644 --- a/packages/desktop/scripts/prebuild.ts +++ b/packages/desktop/scripts/prebuild.ts @@ -1,11 +1,12 @@ #!/usr/bin/env bun import { $ } from "bun" -import { downloadCliToResources, resolveChannel } from "./utils" +import { copyBuiltCliToResources, downloadCliToResources, resolveChannel } from "./utils" const channel = resolveChannel() await $`bun ./scripts/copy-icons.ts ${channel}` await $`bun ./scripts/copy-metainfo.ts ${channel}` if (channel === "dev") await downloadCliToResources() -if (channel === "beta") await downloadCliToResources("next") +if (channel === "beta" && Bun.env.OPENCODE_CLI_DIST) await copyBuiltCliToResources(Bun.env.OPENCODE_CLI_DIST) +if (channel === "beta" && !Bun.env.OPENCODE_CLI_DIST) await downloadCliToResources("beta") diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index a5f85e11f5f..ad356fce50d 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -3,7 +3,7 @@ import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" -const CLI_VERSION = "0.0.0-next-16365" +const CLI_VERSION = "dev" export type Channel = "dev" | "beta" | "prod" @@ -74,18 +74,28 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo const directory = await mkdtemp(join(tmpdir(), "opencode-cli-")) try { await $`bun install --no-save --cwd ${directory} ${`${cli.package}@${version}`} ${`--os=${cli.os}`} ${`--cpu=${cli.cpu}`}` - await copyFile( + await copyCliToResources( join(directory, "node_modules", cli.package, "bin", cli.os === "win32" ? "opencode2.exe" : "opencode2"), dest, ) } finally { await rm(directory, { recursive: true, force: true }) } - await prepareCli(dest) console.log(`Copied ${cli.package}@${version} to ${dest}`) } +export async function copyBuiltCliToResources(root: string, dest = windowsify("resources/opencode-cli")) { + const cli = getCurrentCli() + const directory = cli.package.replace("@opencode-ai/", "") + await copyCliToResources(join(root, directory, "bin", cli.os === "win32" ? "opencode2.exe" : "opencode2"), dest) +} + +async function copyCliToResources(source: string, dest: string) { + await copyFile(source, dest) + await prepareCli(dest) +} + async function prepareCli(dest: string) { if (process.platform !== "win32") await chmod(dest, 0o755) if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") { diff --git a/packages/desktop/src/main/wsl/servers.test.ts b/packages/desktop/src/main/wsl/servers.test.ts index ba6c6a01bf9..458d82f78fb 100644 --- a/packages/desktop/src/main/wsl/servers.test.ts +++ b/packages/desktop/src/main/wsl/servers.test.ts @@ -27,7 +27,7 @@ test("installs and verifies the bundled CLI version", async () => { await controller.installOpencode("Debian") - expect(installs).toEqual([["Debian", "0.0.0-next-16365"]]) + expect(installs).toEqual([["Debian", "0.0.0-dev-16365"]]) expect(controller.getState().opencodeChecks.Debian?.matchesDesktop).toBe(true) }) @@ -37,12 +37,12 @@ test("rejects a WSL CLI version that differs from the bundled version", async () testControllerOptions({ installCli: async () => undefined, resolveCli: async () => "/home/me/.opencode/bin/opencode2", - readCliVersion: async () => "0.0.0-next-older", + readCliVersion: async () => "0.0.0-dev-older", }), ) await expect(controller.installOpencode("Debian")).rejects.toThrow( - "OpenCode update finished but Debian still reports 0.0.0-next-older; expected 0.0.0-next-16365", + "OpenCode update finished but Debian still reports 0.0.0-dev-older; expected 0.0.0-dev-16365", ) }) @@ -147,7 +147,7 @@ async function waitFor(check: () => boolean) { function testControllerOptions(overrides: Partial = {}): ControllerOptions { return { - cli: { version: "0.0.0-next-16365" }, + cli: { version: "0.0.0-dev-16365" }, spawnSidecar: async () => ({ stop: async () => undefined, onExit: () => undefined, @@ -159,7 +159,7 @@ function testControllerOptions(overrides: Partial = {}): Cont writeServers: (servers: WslServerConfig[]) => { persistedServers = servers }, - readCliVersion: async () => "0.0.0-next-16365", + readCliVersion: async () => "0.0.0-dev-16365", resolveCli: async () => "/home/me/.opencode/bin/opencode2", ...overrides, } diff --git a/packages/script/src/index.ts b/packages/script/src/index.ts index 8c91c088421..55c89ddf7a0 100644 --- a/packages/script/src/index.ts +++ b/packages/script/src/index.ts @@ -34,7 +34,7 @@ const IS_PREVIEW = CHANNEL !== "latest" const VERSION = await (async () => { if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${previewBuildNumber()}` - const version = await fetch("https://registry.npmjs.org/opencode-ai/latest") + const version = await fetch("https://registry.npmjs.org/@opencode-ai%2fcli/latest") .then((res) => { if (!res.ok) throw new Error(res.statusText) return res.json() diff --git a/packages/updates/src/index.test.ts b/packages/updates/src/index.test.ts index a999f865dee..ae6af0d26d8 100644 --- a/packages/updates/src/index.test.ts +++ b/packages/updates/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { channelsForRef, validGitHubClaims } from "./index" +import { channelsForRef, resolveChannel, validGitHubClaims } from "./index" const claims = { repository: "anomalyco/opencode", @@ -19,6 +19,10 @@ describe("GitHub publish authorization", () => { expect(channelsForRef(claims.ref)).toEqual(["dev", "latest"]) }) + test("maps V2 development to the dev channel", () => { + expect(channelsForRef("refs/heads/v2")).toEqual(["dev"]) + }) + test("rejects another repository or workflow", () => { expect(validGitHubClaims({ ...claims, repository_id: "1" })).toBe(false) expect( @@ -33,3 +37,8 @@ describe("GitHub publish authorization", () => { ).toBe(false) }) }) + +test("routes the retired next channel to beta", () => { + expect(resolveChannel("next")).toBe("beta") + expect(resolveChannel("dev")).toBe("dev") +}) diff --git a/packages/updates/src/index.ts b/packages/updates/src/index.ts index f0043b39832..e166c75ab39 100644 --- a/packages/updates/src/index.ts +++ b/packages/updates/src/index.ts @@ -42,7 +42,7 @@ export default { const segments = url.pathname.split("/").filter(Boolean) if (segments.length === 2 && segments[0] === "api" && validIdentifier(segments[1])) { - return channel(env.DB, segments[1]) + return channel(env.DB, resolveChannel(segments[1])) } if ( segments.length === 3 && @@ -50,7 +50,7 @@ export default { validIdentifier(segments[1]) && validIdentifier(segments[2]) ) { - return artifactName(env.DB, segments[1], segments[2]) + return artifactName(env.DB, resolveChannel(segments[1]), segments[2]) } if ( segments.length === 4 && @@ -59,7 +59,7 @@ export default { validIdentifier(segments[2]) && validIdentifier(segments[3]) ) { - return artifactDistribution(env.DB, segments[1], segments[2], segments[3]) + return artifactDistribution(env.DB, resolveChannel(segments[1]), segments[2], segments[3]) } return new Response("Not found", { status: 404 }) }, @@ -344,7 +344,7 @@ export function validGitHubClaims(claims: JWTPayload): claims is GitHubClaims { export function channelsForRef(ref: string) { if (ref === "refs/heads/dev") return ["dev", "latest"] - if (ref === "refs/heads/v2") return ["next"] + if (ref === "refs/heads/v2") return ["dev"] if (ref === "refs/heads/beta") return ["beta"] if (ref === "refs/heads/ci") return ["ci"] if (ref === "refs/heads/fix/npm-native-binary-install") return ["fix/npm-native-binary-install"] @@ -352,6 +352,10 @@ export function channelsForRef(ref: string) { return snapshot ? [snapshot] : [] } +export function resolveChannel(channel: string) { + return channel === "next" ? "beta" : channel +} + function validMutation(request: Request) { const origin = request.headers.get("Origin") if (origin && origin !== new URL(request.url).origin) return json({ error: "Invalid origin" }, 403) diff --git a/packages/www/content/docs/build/client.mdx b/packages/www/content/docs/build/client.mdx index deaf29bb793..1422f2c74f8 100644 --- a/packages/www/content/docs/build/client.mdx +++ b/packages/www/content/docs/build/client.mdx @@ -15,7 +15,7 @@ network. Its types and methods are generated from the same contract as the ## Install ```sh -bun add @opencode-ai/client@next +bun add @opencode-ai/client@beta ``` ## Create a client @@ -119,7 +119,7 @@ OpenCode provides a first-class Effect client through the and decodes responses into OpenCode schema values. ```sh -bun add @opencode-ai/client@next effect +bun add @opencode-ai/client@beta effect ``` ### Create a client diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index c53066bb1ca..cdfe3cb3598 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -106,7 +106,7 @@ visible from the plugin file, for example: ```sh cd .opencode -bun add @opencode-ai/plugin@next +bun add @opencode-ai/plugin@beta ``` Match the plugin package version to the OpenCode release you target. @@ -400,7 +400,7 @@ manifest is: "type": "module", "exports": "./src/index.ts", "dependencies": { - "@opencode-ai/plugin": "next" + "@opencode-ai/plugin": "beta" } } ``` @@ -430,7 +430,7 @@ OpenCode provides a first-class Effect API for plugins through the plugin package and export an `effect` function instead of `setup`: ```sh -bun add @opencode-ai/plugin@next effect +bun add @opencode-ai/plugin@beta effect ``` ```ts title=".opencode/plugins/reviewer-effect.ts" diff --git a/packages/www/content/docs/index.mdx b/packages/www/content/docs/index.mdx index 11b4714b0db..39bf788935e 100644 --- a/packages/www/content/docs/index.mdx +++ b/packages/www/content/docs/index.mdx @@ -18,19 +18,19 @@ description: "Get started with OpenCode." ```bash npm -npm install -g @opencode-ai/cli@next +npm install -g @opencode-ai/cli@beta ``` ```bash bun -bun install -g --trust @opencode-ai/cli@next +bun install -g --trust @opencode-ai/cli@beta ``` ```bash pnpm -pnpm add -g --allow-build=@opencode-ai/cli @opencode-ai/cli@next +pnpm add -g --allow-build=@opencode-ai/cli @opencode-ai/cli@beta ``` ```bash yarn -yarn global add @opencode-ai/cli@next +yarn global add @opencode-ai/cli@beta ``` ```bash curl diff --git a/packages/www/content/docs/migrate-v1.mdx b/packages/www/content/docs/migrate-v1.mdx index 3e1ed4bc2b8..970272e04bc 100644 --- a/packages/www/content/docs/migrate-v1.mdx +++ b/packages/www/content/docs/migrate-v1.mdx @@ -35,10 +35,10 @@ beta compatibility bug rather than an expected migration requirement. ## Install the beta -Install the beta from the `next` distribution tag: +Install the beta from the `beta` distribution tag: ```bash -npm install -g @opencode-ai/cli@next +npm install -g @opencode-ai/cli@beta ``` Start it in your project with: diff --git a/packages/www/content/docs/troubleshooting.mdx b/packages/www/content/docs/troubleshooting.mdx index e5fae398187..59bc5087609 100644 --- a/packages/www/content/docs/troubleshooting.mdx +++ b/packages/www/content/docs/troubleshooting.mdx @@ -119,7 +119,7 @@ Its private service configuration is stored separately at: The database normally lives at: ```text -~/.local/share/opencode/opencode-next.db +~/.local/share/opencode/opencode.db ``` `OPENCODE_DB` can override the database location. diff --git a/script/beta.ts b/script/beta.ts deleted file mode 100755 index d738c36ff01..00000000000 --- a/script/beta.ts +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env bun - -import { $ } from "bun" -import fs from "fs/promises" - -const model = "opencode/gpt-5.3-codex" - -interface PR { - number: number - title: string - author: { login: string } - labels: Array<{ name: string }> -} - -interface FailedPR { - number: number - title: string - reason: string -} - -async function commentOnPR(prNumber: number, reason: string) { - const body = `⚠️ **Blocking Beta Release** - -This PR cannot be merged into the beta branch due to: **${reason}** - -Please resolve this issue to include this PR in the next beta release.` - - try { - await $`gh pr comment ${prNumber} --body ${body}` - console.log(` Posted comment on PR #${prNumber}`) - } catch (err) { - console.log(` Failed to post comment on PR #${prNumber}: ${err}`) - } -} - -async function conflicts() { - const out = await $`git diff --name-only --diff-filter=U`.text().catch(() => "") - return out - .split("\n") - .map((x) => x.trim()) - .filter(Boolean) -} - -async function cleanup() { - try { - await $`git merge --abort` - } catch {} - try { - await $`git checkout -- .` - } catch {} - try { - await $`git clean -fd` - } catch {} -} - -function lines(prs: PR[]) { - return prs.map((x) => `- #${x.number}: ${x.title}`).join("\n") || "(none)" -} - -function group(title: string) { - if (process.env.GITHUB_ACTIONS !== "true") { - console.log(title) - return { [Symbol.dispose]() {} } - } - console.log(`::group::${title}`) - return { - [Symbol.dispose]() { - console.log("::endgroup::") - }, - } -} - -async function typecheck() { - console.log(" Running typecheck...") - - try { - await $`bun typecheck` - return true - } catch (err) { - console.log(`Typecheck failed: ${err}`) - return false - } -} - -async function build() { - console.log(" Running final build smoke check...") - - try { - await $`./script/build.ts --single`.cwd("packages/opencode") - return true - } catch (err) { - console.log(`Build failed: ${err}`) - return false - } -} - -async function validate() { - if (!(await typecheck())) return false - if (!(await build())) return false - return true -} - -async function commitSmokeChanges() { - const out = await $`git status --porcelain`.text() - if (!out.trim()) { - console.log("Smoke check passed") - return true - } - - try { - await $`git add -A` - await $`git commit -m "Fix beta integration"` - } catch (err) { - console.log(`Failed to commit smoke fixes: ${err}`) - return false - } - - if (!(await validate())) return false - - const left = await $`git status --porcelain`.text() - if (!left.trim()) { - console.log("Smoke check passed") - return true - } - - console.log(`Smoke check left uncommitted changes:\n${left}`) - return false -} - -async function install() { - console.log(" Regenerating bun.lock...") - - try { - await fs.rm("bun.lock", { force: true }) - await $`bun install` - await $`git add bun.lock` - return true - } catch (err) { - console.log(`Install failed: ${err}`) - return false - } -} - -async function fix(pr: PR, files: string[], prs: PR[], applied: number[], idx: number) { - console.log(` Trying to auto-resolve ${files.length} conflict(s) with opencode...`) - - const done = lines(prs.filter((x) => applied.includes(x.number))) - const next = lines(prs.slice(idx + 1)) - - const prompt = [ - `Resolve the current git merge conflicts while merging PR #${pr.number} into the beta branch.`, - `PR #${pr.number}: ${pr.title}`, - `Start with these conflicted files: ${files.join(", ")}.`, - `Merged PRs on HEAD:\n${done}`, - `Pending PRs after this one (context only):\n${next}`, - "IMPORTANT: The conflict resolution must be consistent with already-merged PRs.", - "Pending PRs are context only; do not introduce their changes unless they are already present on HEAD.", - "Prefer already-merged PRs over the base branch when resolving stacked conflicts.", - "If bun.lock is conflicted, do not hand-merge it. Delete bun.lock and run bun install after the code conflicts are resolved.", - "If a PR already deleted a file/directory, do not re-add it, instead apply changes in the new semantic location.", - "If a PR already changed an import, keep that change.", - "After resolving the conflicts, run `bun typecheck` at the repo root.", - "If typecheck fails, you may also update any files reported by typecheck.", - "Keep any non-conflict edits narrowly scoped to restoring a valid merged state for the current PR batch.", - "Fix any merge-caused typecheck errors before finishing.", - "Keep the merge in progress, do not abort the merge, and do not create a commit.", - "When done, leave the working tree with no unmerged files and a passing typecheck.", - ].join("\n") - - try { - await $`opencode run -m ${model} ${prompt}` - } catch (err) { - console.log(` opencode failed: ${err}`) - return false - } - - const left = await conflicts() - if (left.length > 0) { - console.log(` Conflicts remain: ${left.join(", ")}`) - return false - } - - if (files.includes("bun.lock") && !(await install())) return false - - if (!(await typecheck())) return false - - console.log(" Conflicts resolved with opencode") - return true -} - -async function smoke(prs: PR[], applied: number[]) { - console.log("\nRunning final smoke check...") - - if (await validate()) return commitSmokeChanges() - - console.log("\nTrying to fix final smoke check with opencode...") - - const done = lines(prs.filter((x) => applied.includes(x.number))) - const prompt = [ - "The beta merge batch is complete, but the deterministic final smoke check failed.", - `Merged PRs on HEAD:\n${done}`, - "Run `bun typecheck` at the repo root.", - "Run `./script/build.ts --single` in `packages/opencode`.", - "Fix any merge-caused issues until both commands pass.", - "Do not create a commit.", - ].join("\n") - - try { - await $`opencode run -m ${model} ${prompt}` - } catch (err) { - console.log(`Smoke fix failed: ${err}`) - return false - } - - if (!(await validate())) return false - return commitSmokeChanges() -} - -async function main() { - console.log("Fetching open PRs with beta label...") - - const stdout = - await $`gh pr list --state open --draft=false --label beta --json number,title,author,labels --limit 100`.text() - const prs: PR[] = JSON.parse(stdout).sort((a: PR, b: PR) => a.number - b.number) - - console.log(`Found ${prs.length} open PRs with beta label`) - - if (prs.length === 0) { - console.log("No team PRs to merge") - return - } - - console.log("Fetching latest dev branch...") - await $`git fetch origin dev` - - console.log("Checking out beta branch...") - await $`git checkout -B beta origin/dev` - - const applied: number[] = [] - const failed: FailedPR[] = [] - - for (const [idx, pr] of prs.entries()) { - console.log() - using _ = group(`Processing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`) - console.log(" Fetching PR head...") - try { - await $`git fetch origin pull/${pr.number}/head:pr/${pr.number}` - } catch (err) { - console.log(` Failed to fetch: ${err}`) - failed.push({ number: pr.number, title: pr.title, reason: "Fetch failed" }) - await commentOnPR(pr.number, "Fetch failed") - continue - } - - console.log(" Merging...") - try { - await $`git merge --no-commit --no-ff pr/${pr.number}` - } catch { - const files = await conflicts() - if (files.length > 0) { - console.log(" Failed to merge (conflicts)") - if (!(await fix(pr, files, prs, applied, idx))) { - await cleanup() - failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) - await commentOnPR(pr.number, "Merge conflicts with dev branch") - continue - } - } else { - console.log(" Failed to merge") - await cleanup() - failed.push({ number: pr.number, title: pr.title, reason: "Merge failed" }) - await commentOnPR(pr.number, "Merge failed") - continue - } - } - - try { - await $`git rev-parse -q --verify MERGE_HEAD`.text() - } catch { - console.log(" No changes, skipping") - continue - } - - try { - await $`git add -A` - } catch { - console.log(" Failed to stage changes") - failed.push({ number: pr.number, title: pr.title, reason: "Staging failed" }) - await commentOnPR(pr.number, "Failed to stage changes") - continue - } - - const commitMsg = `Apply PR #${pr.number}: ${pr.title}` - try { - await $`git commit -m ${commitMsg}` - } catch (err) { - console.log(` Failed to commit: ${err}`) - failed.push({ number: pr.number, title: pr.title, reason: "Commit failed" }) - await commentOnPR(pr.number, "Failed to commit changes") - continue - } - - console.log(" Applied successfully") - applied.push(pr.number) - } - - console.log("\n--- Summary ---") - console.log(`Applied: ${applied.length} PRs`) - applied.forEach((num) => console.log(` - PR #${num}`)) - - if (failed.length > 0) { - console.log(`Failed: ${failed.length} PRs`) - failed.forEach((f) => console.log(` - PR #${f.number}: ${f.reason}`)) - throw new Error(`${failed.length} PR(s) failed to merge`) - } - - console.log("\nChecking if beta branch has changes...") - await $`git fetch origin beta` - - const localTree = (await $`git rev-parse beta^{tree}`.text()).trim() - const remoteTrees = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") - - const matchIdx = remoteTrees.indexOf(localTree) - if (matchIdx !== -1) { - if (matchIdx !== 0) { - console.log(`Beta branch contains this sync, but additional commits exist after it. Leaving beta branch as is.`) - } else { - console.log("Beta branch has identical contents, no push needed") - } - return - } - - if (!(await smoke(prs, applied))) throw new Error("Final smoke check failed") - - await $`git fetch origin beta` - - const validatedTree = (await $`git rev-parse beta^{tree}`.text()).trim() - const remoteTreesAfterSmoke = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") - const matchIdxAfterSmoke = remoteTreesAfterSmoke.indexOf(validatedTree) - if (matchIdxAfterSmoke !== -1) { - if (matchIdxAfterSmoke !== 0) { - console.log( - `Beta branch contains this validated sync, but additional commits exist after it. Leaving beta branch as is.`, - ) - } else { - console.log("Validated beta branch now matches remote contents, no push needed") - } - return - } - - console.log("Force pushing validated beta branch...") - await $`git push origin beta --force --no-verify` - - console.log("Successfully synced beta branch") -} - -main().catch((err) => { - console.error("Error:", err) - process.exit(1) -}) diff --git a/script/publish.ts b/script/publish.ts index 48fc95c2578..54d336bfc53 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -35,39 +35,53 @@ if (Script.release && !Script.preview) { await prepareReleaseFiles() -if (Script.channel !== "beta") { - console.log("\n=== schema ===\n") - await $`bun ./packages/schema/script/publish.ts` +console.log("\n=== schema ===\n") +await $`bun ./packages/schema/script/publish.ts` - console.log("\n=== codemode ===\n") - await $`bun ./packages/codemode/script/publish.ts` +console.log("\n=== codemode ===\n") +await $`bun ./packages/codemode/script/publish.ts` - console.log("\n=== theme ===\n") - await $`bun ./packages/theme/script/publish.ts` +console.log("\n=== theme ===\n") +await $`bun ./packages/theme/script/publish.ts` - console.log("\n=== ai ===\n") - await $`bun ./packages/ai/script/publish.ts` +console.log("\n=== ai ===\n") +await $`bun ./packages/ai/script/publish.ts` - console.log("\n=== util ===\n") - await $`bun ./packages/util/script/publish.ts` +console.log("\n=== util ===\n") +await $`bun ./packages/util/script/publish.ts` - console.log("\n=== protocol ===\n") - await $`bun ./packages/protocol/script/publish.ts` +console.log("\n=== protocol ===\n") +await $`bun ./packages/protocol/script/publish.ts` - console.log("\n=== client ===\n") - await $`bun ./packages/client/script/publish.ts` +console.log("\n=== client ===\n") +await $`bun ./packages/client/script/publish.ts` - console.log("\n=== cli ===\n") - await $`bun ./packages/cli/script/publish.ts` +console.log("\n=== cli ===\n") +await $`bun ./packages/cli/script/publish.ts` - console.log("\n=== plugin ===\n") - await $`bun ./packages/plugin/script/publish.ts` +console.log("\n=== plugin ===\n") +await $`bun ./packages/plugin/script/publish.ts` - console.log("\n=== core ===\n") - await $`bun ./packages/core/script/publish.ts` +console.log("\n=== core ===\n") +await $`bun ./packages/core/script/publish.ts` - console.log("\n=== ui ===\n") - await $`bun ./packages/ui/script/publish.ts` +console.log("\n=== ui ===\n") +await $`bun ./packages/ui/script/publish.ts` + +if (Script.channel === "beta") { + const packages = [ + "@opencode-ai/schema", + "@opencode-ai/codemode", + "@opencode-ai/theme", + "@opencode-ai/ai", + "@opencode-ai/util", + "@opencode-ai/protocol", + "@opencode-ai/client", + "@opencode-ai/plugin", + "@opencode-ai/core", + "@opencode-ai/ui", + ] + await Promise.all(packages.map((name) => $`npm dist-tag add ${`${name}@${Script.version}`} next`)) } if (Script.release) {