import type { PluginInput } from "@opencode-ai/plugin" import { getCachedVersion, getLocalDevVersion, findPluginEntry, getLatestVersion, updatePinnedVersion } from "./checker" import { invalidatePackage } from "./cache" import { PACKAGE_NAME } from "./constants" import { log } from "../../shared/logger" import { getConfigLoadErrors, clearConfigLoadErrors } from "../../shared/config-errors" import type { AutoUpdateCheckerOptions } from "./types" export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) { const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options const getToastMessage = (isUpdate: boolean, latestVersion?: string): string => { if (isSisyphusEnabled) { return isUpdate ? `Sisyphus on steroids is steering OpenCode.\nv${latestVersion} available. Restart to apply.` : `Sisyphus on steroids is steering OpenCode.` } return isUpdate ? `OpenCode is now on Steroids. oMoMoMoMo...\nv${latestVersion} available. Restart OpenCode to apply.` : `OpenCode is now on Steroids. oMoMoMoMo...` } let hasChecked = false return { event: async ({ event }: { event: { type: string; properties?: unknown } }) => { if (event.type !== "session.created") return if (hasChecked) return const props = event.properties as { info?: { parentID?: string } } | undefined if (props?.info?.parentID) return hasChecked = true const cachedVersion = getCachedVersion() const localDevVersion = getLocalDevVersion(ctx.directory) const displayVersion = localDevVersion ?? cachedVersion if (showStartupToast) { showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {}) } showConfigErrorsIfAny(ctx).catch(() => {}) if (localDevVersion) { log("[auto-update-checker] Skipped: local development mode") return } 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 { 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 { const errors = getConfigLoadErrors() if (errors.length === 0) return const errorMessages = errors.map(e => `${e.path}: ${e.error}`).join("\n") await ctx.client.tui .showToast({ body: { title: "Config Load Error", message: `Failed to load config:\n${errorMessages}`, variant: "error" as const, duration: 10000, }, }) .catch(() => {}) log(`[auto-update-checker] Config load errors shown: ${errors.length} error(s)`) clearConfigLoadErrors() } async function showVersionToast(ctx: PluginInput, version: string | null, message: string): Promise { 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 { 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 { 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"