From 612e9b3e03c3ca7dc8e0cfa8fd4cb242aa950fe0 Mon Sep 17 00:00:00 2001 From: sisyphus-dev-ai Date: Sun, 11 Jan 2026 09:56:09 +0000 Subject: [PATCH] fix(auto-update): implement channel-based version fetching Add support for npm dist-tag channels (@beta, @next, @canary) in auto-update mechanism. Users pinned to oh-my-opencode@beta now correctly fetch and compare against beta channel instead of stable latest. - Add extractChannel() to detect channel from version string - Modify getLatestVersion() to accept channel parameter - Update auto-update flow to use channel-aware fetching - Add comprehensive tests for channel detection and fetching - Resolves #687 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/hooks/auto-update-checker/checker.test.ts | 24 ++++ src/hooks/auto-update-checker/checker.ts | 23 ++-- src/hooks/auto-update-checker/index.test.ts | 103 +++++++++++++++++- src/hooks/auto-update-checker/index.ts | 40 ++++--- 4 files changed, 161 insertions(+), 29 deletions(-) create mode 100644 src/hooks/auto-update-checker/checker.test.ts diff --git a/src/hooks/auto-update-checker/checker.test.ts b/src/hooks/auto-update-checker/checker.test.ts new file mode 100644 index 000000000..a10676350 --- /dev/null +++ b/src/hooks/auto-update-checker/checker.test.ts @@ -0,0 +1,24 @@ +import { describe, test, expect } from "bun:test" +import { getLatestVersion } from "./checker" + +describe("auto-update-checker/checker", () => { + describe("getLatestVersion", () => { + test("accepts channel parameter", async () => { + const result = await getLatestVersion("beta") + + expect(typeof result === "string" || result === null).toBe(true) + }) + + test("accepts latest channel", async () => { + const result = await getLatestVersion("latest") + + expect(typeof result === "string" || result === null).toBe(true) + }) + + test("works without channel (defaults to latest)", async () => { + const result = await getLatestVersion() + + expect(typeof result === "string" || result === null).toBe(true) + }) + }) +}) diff --git a/src/hooks/auto-update-checker/checker.ts b/src/hooks/auto-update-checker/checker.ts index 29919963e..2d35453f9 100644 --- a/src/hooks/auto-update-checker/checker.ts +++ b/src/hooks/auto-update-checker/checker.ts @@ -231,7 +231,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer } } -export async function getLatestVersion(): Promise { +export async function getLatestVersion(channel: string = "latest"): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT) @@ -244,7 +244,7 @@ export async function getLatestVersion(): Promise { if (!response.ok) return null const data = (await response.json()) as NpmDistTags - return data.latest ?? null + return data[channel] ?? data.latest ?? null } catch { return null } finally { @@ -264,24 +264,21 @@ export async function checkForUpdate(directory: string): Promise { describe("isPrereleaseVersion", () => { @@ -150,4 +150,105 @@ describe("auto-update-checker", () => { expect(result).toBe(false) }) }) + + describe("extractChannel", () => { + test("extracts beta from dist-tag", () => { + // #given beta dist-tag + const version = "beta" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns beta + expect(result).toBe("beta") + }) + + test("extracts next from dist-tag", () => { + // #given next dist-tag + const version = "next" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns next + expect(result).toBe("next") + }) + + test("extracts canary from dist-tag", () => { + // #given canary dist-tag + const version = "canary" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns canary + expect(result).toBe("canary") + }) + + test("extracts beta from prerelease version", () => { + // #given beta prerelease version + const version = "3.0.0-beta.1" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns beta + expect(result).toBe("beta") + }) + + test("extracts alpha from prerelease version", () => { + // #given alpha prerelease version + const version = "1.0.0-alpha" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns alpha + expect(result).toBe("alpha") + }) + + test("extracts rc from prerelease version", () => { + // #given rc prerelease version + const version = "2.0.0-rc.1" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns rc + expect(result).toBe("rc") + }) + + test("returns latest for stable version", () => { + // #given stable version + const version = "2.14.0" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns latest + expect(result).toBe("latest") + }) + + test("returns latest for null", () => { + // #given null version + const version = null + + // #when extracting channel + const result = extractChannel(version) + + // #then returns latest + expect(result).toBe("latest") + }) + + test("handles complex prerelease identifiers", () => { + // #given complex prerelease + const version = "3.0.0-beta.1.experimental" + + // #when extracting channel + const result = extractChannel(version) + + // #then returns beta + expect(result).toBe("beta") + }) + }) }) diff --git a/src/hooks/auto-update-checker/index.ts b/src/hooks/auto-update-checker/index.ts index bf2a13847..08cbd64c5 100644 --- a/src/hooks/auto-update-checker/index.ts +++ b/src/hooks/auto-update-checker/index.ts @@ -23,6 +23,26 @@ export function isPrereleaseOrDistTag(pinnedVersion: string | null): boolean { return isPrereleaseVersion(pinnedVersion) || isDistTag(pinnedVersion) } +export function extractChannel(version: string | null): string { + if (!version) return "latest" + + if (isDistTag(version)) { + return version + } + + if (isPrereleaseVersion(version)) { + const prereleasePart = version.split("-")[1] + if (prereleasePart) { + const channelMatch = prereleasePart.match(/^(alpha|beta|rc|canary|next)/) + if (channelMatch) { + return channelMatch[1] + } + } + } + + return "latest" +} + export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) { const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options @@ -94,18 +114,19 @@ async function runBackgroundUpdateCheck( return } - const latestVersion = await getLatestVersion() + const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion) + const latestVersion = await getLatestVersion(channel) if (!latestVersion) { - log("[auto-update-checker] Failed to fetch latest version") + log("[auto-update-checker] Failed to fetch latest version for channel:", channel) return } if (currentVersion === latestVersion) { - log("[auto-update-checker] Already on latest version") + log("[auto-update-checker] Already on latest version for channel:", channel) return } - log(`[auto-update-checker] Update available: ${currentVersion} → ${latestVersion}`) + log(`[auto-update-checker] Update available (${channel}): ${currentVersion} → ${latestVersion}`) if (!autoUpdate) { await showUpdateAvailableToast(ctx, latestVersion, getToastMessage) @@ -113,18 +134,7 @@ async function runBackgroundUpdateCheck( return } - // Check if current version is a prerelease - don't auto-downgrade prerelease to stable - if (isPrereleaseVersion(currentVersion)) { - log(`[auto-update-checker] Skipping auto-update for prerelease version: ${currentVersion}`) - return - } - if (pluginInfo.isPinned) { - if (isPrereleaseOrDistTag(pluginInfo.pinnedVersion)) { - log(`[auto-update-checker] Skipping auto-update for prerelease/dist-tag: ${pluginInfo.pinnedVersion}`) - return - } - const updated = updatePinnedVersion(pluginInfo.configPath, pluginInfo.entry, latestVersion) if (!updated) { await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)