fix(auto-update): treat only explicit semver pins as user-pinned

Fixes #1920

Installer-written exact versions (e.g., oh-my-opencode@3.5.2) were incorrectly treated as user-pinned, blocking auto-updates for all installer users.

Fix isPinned to only block auto-update when pinnedVersion is an explicit semver string (user's intent). Channel tags (latest, beta, next) and bare package name all allow auto-update.

Fix installer fallback to return bare PACKAGE_NAME for stable versions and PACKAGE_NAME@{channel} for prerelease versions, preserving channel tracking.
This commit is contained in:
YeonGyu-Kim
2026-02-21 02:24:43 +09:00
parent 032d7fd139
commit 88148fe248
6 changed files with 112 additions and 20 deletions
+10 -10
View File
@@ -1,4 +1,4 @@
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test"
import { describe, expect, test, mock, afterEach } from "bun:test"
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
import type { InstallConfig } from "./types"
@@ -58,7 +58,7 @@ describe("getPluginNameWithVersion", () => {
expect(result).toBe("oh-my-opencode@next")
})
test("returns pinned version when no tag matches", async () => {
test("returns prerelease channel tag when no dist-tag matches prerelease version", async () => {
// #given npm dist-tags with beta=3.0.0-beta.3
globalThis.fetch = mock(() =>
Promise.resolve({
@@ -70,22 +70,22 @@ describe("getPluginNameWithVersion", () => {
// #when current version is old beta 3.0.0-beta.2
const result = await getPluginNameWithVersion("3.0.0-beta.2")
// #then should pin to specific version
expect(result).toBe("oh-my-opencode@3.0.0-beta.2")
// #then should preserve prerelease channel
expect(result).toBe("oh-my-opencode@beta")
})
test("returns pinned version when fetch fails", async () => {
test("returns prerelease channel tag when fetch fails", async () => {
// #given network failure
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
// #when current version is 3.0.0-beta.3
const result = await getPluginNameWithVersion("3.0.0-beta.3")
// #then should fall back to pinned version
expect(result).toBe("oh-my-opencode@3.0.0-beta.3")
// #then should preserve prerelease channel
expect(result).toBe("oh-my-opencode@beta")
})
test("returns pinned version when npm returns non-ok response", async () => {
test("returns bare package name when npm returns non-ok response for stable version", async () => {
// #given npm returns 404
globalThis.fetch = mock(() =>
Promise.resolve({
@@ -97,8 +97,8 @@ describe("getPluginNameWithVersion", () => {
// #when current version is 2.14.0
const result = await getPluginNameWithVersion("2.14.0")
// #then should fall back to pinned version
expect(result).toBe("oh-my-opencode@2.14.0")
// #then should fall back to bare package entry
expect(result).toBe("oh-my-opencode")
})
test("prioritizes latest over other tags when version matches multiple", async () => {
@@ -3,6 +3,15 @@ import { fetchNpmDistTags } from "./npm-dist-tags"
const PACKAGE_NAME = "oh-my-opencode"
const PRIORITIZED_TAGS = ["latest", "beta", "next"] as const
function getFallbackEntry(version: string): string {
const prereleaseMatch = version.match(/-([a-zA-Z][a-zA-Z0-9]*)\./)
if (prereleaseMatch) {
return `${PACKAGE_NAME}@${prereleaseMatch[1]}`
}
return PACKAGE_NAME
}
export async function getPluginNameWithVersion(currentVersion: string): Promise<string> {
const distTags = await fetchNpmDistTags(PACKAGE_NAME)
@@ -15,5 +24,5 @@ export async function getPluginNameWithVersion(currentVersion: string): Promise<
}
}
return `${PACKAGE_NAME}@${currentVersion}`
return getFallbackEntry(currentVersion)
}