Merge commit 'e261853451addb9d3d5d5d0fb7aae830ab492470'
This commit is contained in:
@@ -1222,6 +1222,9 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"auto_update": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +122,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
google_auth: z.boolean().optional(),
|
google_auth: z.boolean().optional(),
|
||||||
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
|
sisyphus_agent: SisyphusAgentConfigSchema.optional(),
|
||||||
experimental: ExperimentalConfigSchema.optional(),
|
experimental: ExperimentalConfigSchema.optional(),
|
||||||
|
auto_update: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface PluginEntryInfo {
|
|||||||
entry: string
|
entry: string
|
||||||
isPinned: boolean
|
isPinned: boolean
|
||||||
pinnedVersion: string | null
|
pinnedVersion: string | null
|
||||||
|
configPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findPluginEntry(directory: string): PluginEntryInfo | null {
|
export function findPluginEntry(directory: string): PluginEntryInfo | null {
|
||||||
@@ -109,12 +110,12 @@ export function findPluginEntry(directory: string): PluginEntryInfo | null {
|
|||||||
|
|
||||||
for (const entry of plugins) {
|
for (const entry of plugins) {
|
||||||
if (entry === PACKAGE_NAME) {
|
if (entry === PACKAGE_NAME) {
|
||||||
return { entry, isPinned: false, pinnedVersion: null }
|
return { entry, isPinned: false, pinnedVersion: null, configPath }
|
||||||
}
|
}
|
||||||
if (entry.startsWith(`${PACKAGE_NAME}@`)) {
|
if (entry.startsWith(`${PACKAGE_NAME}@`)) {
|
||||||
const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1)
|
const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1)
|
||||||
const isPinned = pinnedVersion !== "latest"
|
const isPinned = pinnedVersion !== "latest"
|
||||||
return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null }
|
return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null, configPath }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -149,6 +150,64 @@ export function getCachedVersion(): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a pinned version entry in the config file.
|
||||||
|
* Only replaces within the "plugin" array to avoid unintended edits.
|
||||||
|
* Preserves JSONC comments and formatting via string replacement.
|
||||||
|
*/
|
||||||
|
export function updatePinnedVersion(configPath: string, oldEntry: string, newVersion: string): boolean {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(configPath, "utf-8")
|
||||||
|
const newEntry = `${PACKAGE_NAME}@${newVersion}`
|
||||||
|
|
||||||
|
// Find the "plugin" array region to scope replacement
|
||||||
|
const pluginMatch = content.match(/"plugin"\s*:\s*\[/)
|
||||||
|
if (!pluginMatch || pluginMatch.index === undefined) {
|
||||||
|
log(`[auto-update-checker] No "plugin" array found in ${configPath}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the closing bracket of the plugin array
|
||||||
|
const startIdx = pluginMatch.index + pluginMatch[0].length
|
||||||
|
let bracketCount = 1
|
||||||
|
let endIdx = startIdx
|
||||||
|
|
||||||
|
for (let i = startIdx; i < content.length && bracketCount > 0; i++) {
|
||||||
|
if (content[i] === "[") bracketCount++
|
||||||
|
else if (content[i] === "]") bracketCount--
|
||||||
|
endIdx = i
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = content.slice(0, startIdx)
|
||||||
|
const pluginArrayContent = content.slice(startIdx, endIdx)
|
||||||
|
const after = content.slice(endIdx)
|
||||||
|
|
||||||
|
// Only replace first occurrence within plugin array
|
||||||
|
const escapedOldEntry = oldEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
const regex = new RegExp(`["']${escapedOldEntry}["']`)
|
||||||
|
|
||||||
|
if (!regex.test(pluginArrayContent)) {
|
||||||
|
log(`[auto-update-checker] Entry "${oldEntry}" not found in plugin array of ${configPath}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPluginArray = pluginArrayContent.replace(regex, `"${newEntry}"`)
|
||||||
|
const updatedContent = before + updatedPluginArray + after
|
||||||
|
|
||||||
|
if (updatedContent === content) {
|
||||||
|
log(`[auto-update-checker] No changes made to ${configPath}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(configPath, updatedContent, "utf-8")
|
||||||
|
log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} → ${newEntry}`)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
log(`[auto-update-checker] Failed to update config file ${configPath}:`, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLatestVersion(): Promise<string | null> {
|
export async function getLatestVersion(): Promise<string | null> {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT)
|
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
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 { invalidatePackage } from "./cache"
|
||||||
import { PACKAGE_NAME } from "./constants"
|
import { PACKAGE_NAME } from "./constants"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
@@ -7,7 +7,7 @@ import { getConfigLoadErrors, clearConfigLoadErrors } from "../../shared/config-
|
|||||||
import type { AutoUpdateCheckerOptions } from "./types"
|
import type { AutoUpdateCheckerOptions } from "./types"
|
||||||
|
|
||||||
export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) {
|
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 => {
|
const getToastMessage = (isUpdate: boolean, latestVersion?: string): string => {
|
||||||
if (isSisyphusEnabled) {
|
if (isSisyphusEnabled) {
|
||||||
@@ -20,21 +20,6 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdat
|
|||||||
: `OpenCode is now on Steroids. oMoMoMoMo...`
|
: `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
|
let hasChecked = false
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -47,54 +32,76 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdat
|
|||||||
|
|
||||||
hasChecked = true
|
hasChecked = true
|
||||||
|
|
||||||
try {
|
const cachedVersion = getCachedVersion()
|
||||||
const result = await checkForUpdate(ctx.directory)
|
const localDevVersion = getLocalDevVersion(ctx.directory)
|
||||||
|
const displayVersion = localDevVersion ?? cachedVersion
|
||||||
|
|
||||||
if (result.isLocalDev) {
|
if (showStartupToast) {
|
||||||
|
showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {})
|
||||||
|
}
|
||||||
|
showConfigErrorsIfAny(ctx).catch(() => {})
|
||||||
|
|
||||||
|
if (localDevVersion) {
|
||||||
log("[auto-update-checker] Skipped: local development mode")
|
log("[auto-update-checker] Skipped: local development mode")
|
||||||
if (showStartupToast) {
|
|
||||||
const version = getLocalDevVersion(ctx.directory) ?? getCachedVersion()
|
|
||||||
await showVersionToast(version)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isPinned) {
|
runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch(err => {
|
||||||
log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`)
|
log("[auto-update-checker] Background update check failed:", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await showConfigErrorsIfAny(ctx)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +125,53 @@ async function showConfigErrorsIfAny(ctx: PluginInput): Promise<void> {
|
|||||||
clearConfigLoadErrors()
|
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 type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types"
|
||||||
export { checkForUpdate } from "./checker"
|
export { checkForUpdate } from "./checker"
|
||||||
export { invalidatePackage, invalidateCache } from "./cache"
|
export { invalidatePackage, invalidateCache } from "./cache"
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ export interface UpdateCheckResult {
|
|||||||
export interface AutoUpdateCheckerOptions {
|
export interface AutoUpdateCheckerOptions {
|
||||||
showStartupToast?: boolean
|
showStartupToast?: boolean
|
||||||
isSisyphusEnabled?: boolean
|
isSisyphusEnabled?: boolean
|
||||||
|
autoUpdate?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createAutoUpdateCheckerHook(ctx, {
|
? createAutoUpdateCheckerHook(ctx, {
|
||||||
showStartupToast: isHookEnabled("startup-toast"),
|
showStartupToast: isHookEnabled("startup-toast"),
|
||||||
isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
|
isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
|
||||||
|
autoUpdate: pluginConfig.auto_update ?? true,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const keywordDetector = isHookEnabled("keyword-detector")
|
const keywordDetector = isHookEnabled("keyword-detector")
|
||||||
|
|||||||
Reference in New Issue
Block a user