Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/core/src/installation/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,17 @@ declare global {
export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const InstallationLocal = InstallationChannel === "local"

/**
* Version pin for installing `@opencode-ai/plugin` on non-local builds.
*
* npm-package-arg treats any string containing `/` as a github `owner/repo`
* remote. Preview builds stamped from slash-containing branch names
* (`0.0.0-agent/m4-…`) therefore hang on `git ls-remote` during config
* dependency install. Skip the pin when the version is not npm-safe.
*/
export function pluginDependencyVersion(version: string | undefined = InstallationVersion) {
if (!version) return
if (version.includes("/")) return
return version
}
32 changes: 32 additions & 0 deletions packages/core/test/installation-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, test } from "bun:test"
import npa from "npm-package-arg"
import { pluginDependencyVersion } from "../src/installation/version"

describe("pluginDependencyVersion", () => {
test("pins ordinary semver and preview versions", () => {
expect(pluginDependencyVersion("1.2.3")).toBe("1.2.3")
expect(pluginDependencyVersion("0.0.0-agent-m4-verdict-202607080429")).toBe(
"0.0.0-agent-m4-verdict-202607080429",
)
})

test("skips slash-containing versions that npm-package-arg treats as github remotes", () => {
const bad = "0.0.0-agent/m4-verdict-opencode-runtime-202607080429"
// Documents the hang class: npa rewrites the pin into github:user/project
// and arborist then blocks on git ls-remote.
const parsed = npa(`@opencode-ai/plugin@${bad}`)
expect(parsed.type).toBe("git")
expect(parsed.hosted?.user).toBe("0.0.0-agent")
expect(parsed.hosted?.project).toBe("m4-verdict-opencode-runtime-202607080429")

expect(pluginDependencyVersion(bad)).toBeUndefined()
expect(pluginDependencyVersion("")).toBeUndefined()
})

test("sanitized branch-style versions stay npm version pins", () => {
const good = "0.0.0-agent-m4-verdict-opencode-runtime-202607080429"
const parsed = npa(`@opencode-ai/plugin@${good}`)
expect(parsed.type).toBe("version")
expect(pluginDependencyVersion(good)).toBe(good)
})
})
22 changes: 22 additions & 0 deletions packages/core/test/plugin-dependency-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, test } from "bun:test"
import { pluginDependencyVersion } from "../src/installation/version"

describe("pluginDependencyVersion", () => {
test("returns npm-safe versions unchanged", () => {
expect(pluginDependencyVersion("1.2.3")).toBe("1.2.3")
expect(pluginDependencyVersion("0.0.0-main-202607050502")).toBe("0.0.0-main-202607050502")
expect(pluginDependencyVersion("local")).toBe("local")
})

test("skips slash-containing versions that npm treats as github owner/repo", () => {
// Branch-stamped preview builds like 0.0.0-agent/m4-… hang on git ls-remote
// when npm-package-arg parses the version as a remote.
expect(pluginDependencyVersion("0.0.0-agent/m4-verdict-opencode-runtime-202607080429")).toBeUndefined()
expect(pluginDependencyVersion("agent/m4-feature")).toBeUndefined()
})

test("skips empty string versions", () => {
expect(pluginDependencyVersion("")).toBeUndefined()
})
}
)
4 changes: 2 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
import { applyEdits, modify } from "jsonc-parser"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { InstallationLocal, pluginDependencyVersion } from "@opencode-ai/core/installation/version"
import { existsSync } from "fs"
import { Account } from "@/account/account"
import { isRecord } from "@/util/record"
Expand Down Expand Up @@ -439,7 +439,7 @@ const layer = Layer.effect(
add: [
{
name: "@opencode-ai/plugin",
version: InstallationLocal ? undefined : InstallationVersion,
version: InstallationLocal ? undefined : pluginDependencyVersion(),
},
],
})
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/config/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { CurrentWorkingDirectory } from "./tui-cwd"
import { ConfigPlugin } from "@/config/plugin"
import { TuiKeybind } from "@opencode-ai/tui/config/keybind"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { InstallationLocal, pluginDependencyVersion } from "@opencode-ai/core/installation/version"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { Filesystem } from "@/util/filesystem"
import { ConfigVariable } from "@/config/variable"
Expand Down Expand Up @@ -239,7 +239,7 @@ const layer = Layer.effect(
add: [
{
name: "@opencode-ai/plugin",
version: InstallationLocal ? undefined : InstallationVersion,
version: InstallationLocal ? undefined : pluginDependencyVersion(),
},
],
})
Expand Down
52 changes: 31 additions & 21 deletions packages/script/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,39 @@ const env = {
OPENCODE_VERSION: process.env["OPENCODE_VERSION"],
OPENCODE_RELEASE: process.env["OPENCODE_RELEASE"],
}
const CHANNEL = await (async () => {
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL
if (env.OPENCODE_BUMP) return "latest"
if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.startsWith("0.0.0-")) return "latest"
return await $`git branch --show-current`.text().then((x) => x.trim())
})()
// npm-package-arg treats "owner/repo" (and version strings containing "/") as
// github remotes and spawns blocking `git ls-remote`. Branch names like
// agent/m4-… must not be embedded raw into OPENCODE_CHANNEL / OPENCODE_VERSION.
function sanitizeNpmLabel(value: string) {
return value.replaceAll("/", "-")
}
const CHANNEL = sanitizeNpmLabel(
await (async () => {
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL
if (env.OPENCODE_BUMP) return "latest"
if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.startsWith("0.0.0-")) return "latest"
return await $`git branch --show-current`.text().then((x) => x.trim())
})(),
)
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}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
const t = env.OPENCODE_BUMP?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
})()
const VERSION = sanitizeNpmLabel(
await (async () => {
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
const t = env.OPENCODE_BUMP?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
})(),
)

const bot = ["actions-user", "opencode", "opencode-agent[bot]"]
const teamPath = path.resolve(import.meta.dir, "../../../.github/TEAM_MEMBERS")
Expand Down
Loading