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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
24
src/hooks/auto-update-checker/checker.test.ts
Normal file
24
src/hooks/auto-update-checker/checker.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -231,7 +231,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLatestVersion(): Promise<string | null> {
|
||||
export async function getLatestVersion(channel: string = "latest"): Promise<string | null> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT)
|
||||
|
||||
@@ -244,7 +244,7 @@ export async function getLatestVersion(): Promise<string | null> {
|
||||
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<UpdateCheckResu
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
if (pluginInfo.isPinned) {
|
||||
log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`)
|
||||
return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true }
|
||||
}
|
||||
|
||||
const currentVersion = getCachedVersion()
|
||||
const currentVersion = getCachedVersion() ?? pluginInfo.pinnedVersion
|
||||
if (!currentVersion) {
|
||||
log("[auto-update-checker] No cached version found")
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestVersion()
|
||||
const { extractChannel } = await import("./index")
|
||||
const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion)
|
||||
const latestVersion = await getLatestVersion(channel)
|
||||
if (!latestVersion) {
|
||||
log("[auto-update-checker] Failed to fetch latest version")
|
||||
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
log("[auto-update-checker] Failed to fetch latest version for channel:", channel)
|
||||
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: pluginInfo.isPinned }
|
||||
}
|
||||
|
||||
const needsUpdate = currentVersion !== latestVersion
|
||||
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false }
|
||||
log(`[auto-update-checker] Current: ${currentVersion}, Latest (${channel}): ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: pluginInfo.isPinned }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { isPrereleaseVersion, isDistTag, isPrereleaseOrDistTag } from "./index"
|
||||
import { isPrereleaseVersion, isDistTag, isPrereleaseOrDistTag, extractChannel } from "./index"
|
||||
|
||||
describe("auto-update-checker", () => {
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user