feat(auto-update-checker): implement background auto-update with configurable pinning

- Run update check in background after startup (non-blocking)
- Auto-update pinned versions in config file when newer version available
- Add auto_update config option to disable auto-updating
- Properly invalidate package cache after config update
- Scoped regex replacement to avoid editing outside plugin array

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-19 14:05:09 +09:00
parent e3ff34c76e
commit e261853451
6 changed files with 181 additions and 62 deletions

View File

@@ -1,5 +1,5 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { checkForUpdate, getCachedVersion, getLocalDevVersion } from "./checker"
import { getCachedVersion, getLocalDevVersion, findPluginEntry, getLatestVersion, updatePinnedVersion } from "./checker"
import { invalidatePackage } from "./cache"
import { PACKAGE_NAME } from "./constants"
import { log } from "../../shared/logger"
@@ -7,7 +7,7 @@ import { getConfigLoadErrors, clearConfigLoadErrors } from "../../shared/config-
import type { AutoUpdateCheckerOptions } from "./types"
export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) {
const { showStartupToast = true, isSisyphusEnabled = false } = options
const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options
const getToastMessage = (isUpdate: boolean, latestVersion?: string): string => {
if (isSisyphusEnabled) {
@@ -20,21 +20,6 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdat
: `OpenCode is now on Steroids. oMoMoMoMo...`
}
const showVersionToast = async (version: string | null): Promise<void> => {
const displayVersion = version ?? "unknown"
await ctx.client.tui
.showToast({
body: {
title: `OhMyOpenCode ${displayVersion}`,
message: getToastMessage(false),
variant: "info" as const,
duration: 5000,
},
})
.catch(() => {})
log(`[auto-update-checker] Startup toast shown: v${displayVersion}`)
}
let hasChecked = false
return {
@@ -47,57 +32,79 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdat
hasChecked = true
try {
const result = await checkForUpdate(ctx.directory)
const cachedVersion = getCachedVersion()
const localDevVersion = getLocalDevVersion(ctx.directory)
const displayVersion = localDevVersion ?? cachedVersion
if (result.isLocalDev) {
log("[auto-update-checker] Skipped: local development mode")
if (showStartupToast) {
const version = getLocalDevVersion(ctx.directory) ?? getCachedVersion()
await showVersionToast(version)
}
return
}
if (showStartupToast) {
showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {})
}
showConfigErrorsIfAny(ctx).catch(() => {})
if (result.isPinned) {
log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`)
if (showStartupToast) {
await showVersionToast(result.currentVersion)
}
return
}
if (!result.needsUpdate) {
log("[auto-update-checker] No update needed")
if (showStartupToast) {
await showVersionToast(result.currentVersion)
}
return
}
invalidatePackage(PACKAGE_NAME)
await ctx.client.tui
.showToast({
body: {
title: `OhMyOpenCode ${result.latestVersion}`,
message: getToastMessage(true, result.latestVersion ?? undefined),
variant: "info" as const,
duration: 8000,
},
})
.catch(() => {})
log(`[auto-update-checker] Update notification sent: v${result.currentVersion} → v${result.latestVersion}`)
} catch (err) {
log("[auto-update-checker] Error during update check:", err)
if (localDevVersion) {
log("[auto-update-checker] Skipped: local development mode")
return
}
await showConfigErrorsIfAny(ctx)
runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch(err => {
log("[auto-update-checker] Background update check failed:", err)
})
},
}
}
async function runBackgroundUpdateCheck(
ctx: PluginInput,
autoUpdate: boolean,
getToastMessage: (isUpdate: boolean, latestVersion?: string) => string
): Promise<void> {
const pluginInfo = findPluginEntry(ctx.directory)
if (!pluginInfo) {
log("[auto-update-checker] Plugin not found in config")
return
}
const cachedVersion = getCachedVersion()
const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion
if (!currentVersion) {
log("[auto-update-checker] No version found (cached or pinned)")
return
}
const latestVersion = await getLatestVersion()
if (!latestVersion) {
log("[auto-update-checker] Failed to fetch latest version")
return
}
if (currentVersion === latestVersion) {
log("[auto-update-checker] Already on latest version")
return
}
log(`[auto-update-checker] Update available: ${currentVersion}${latestVersion}`)
if (!autoUpdate) {
await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
log("[auto-update-checker] Auto-update disabled, notification only")
return
}
if (pluginInfo.isPinned) {
const updated = updatePinnedVersion(pluginInfo.configPath, pluginInfo.entry, latestVersion)
if (updated) {
invalidatePackage(PACKAGE_NAME)
await showAutoUpdatedToast(ctx, currentVersion, latestVersion)
log(`[auto-update-checker] Config updated: ${pluginInfo.entry}${PACKAGE_NAME}@${latestVersion}`)
} else {
await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
}
} else {
invalidatePackage(PACKAGE_NAME)
await showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
}
}
async function showConfigErrorsIfAny(ctx: PluginInput): Promise<void> {
const errors = getConfigLoadErrors()
if (errors.length === 0) return
@@ -118,6 +125,53 @@ async function showConfigErrorsIfAny(ctx: PluginInput): Promise<void> {
clearConfigLoadErrors()
}
async function showVersionToast(ctx: PluginInput, version: string | null, message: string): Promise<void> {
const displayVersion = version ?? "unknown"
await ctx.client.tui
.showToast({
body: {
title: `OhMyOpenCode ${displayVersion}`,
message,
variant: "info" as const,
duration: 5000,
},
})
.catch(() => {})
log(`[auto-update-checker] Startup toast shown: v${displayVersion}`)
}
async function showUpdateAvailableToast(
ctx: PluginInput,
latestVersion: string,
getToastMessage: (isUpdate: boolean, latestVersion?: string) => string
): Promise<void> {
await ctx.client.tui
.showToast({
body: {
title: `OhMyOpenCode ${latestVersion}`,
message: getToastMessage(true, latestVersion),
variant: "info" as const,
duration: 8000,
},
})
.catch(() => {})
log(`[auto-update-checker] Update available toast shown: v${latestVersion}`)
}
async function showAutoUpdatedToast(ctx: PluginInput, oldVersion: string, newVersion: string): Promise<void> {
await ctx.client.tui
.showToast({
body: {
title: `OhMyOpenCode Updated!`,
message: `v${oldVersion} → v${newVersion}\nRestart OpenCode to apply.`,
variant: "success" as const,
duration: 8000,
},
})
.catch(() => {})
log(`[auto-update-checker] Auto-updated toast shown: v${oldVersion} → v${newVersion}`)
}
export type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types"
export { checkForUpdate } from "./checker"
export { invalidatePackage, invalidateCache } from "./cache"