From 15e3b14ccab1d47173e2a3ce5d68beb15a867595 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 17:17:41 +0900 Subject: [PATCH] fix(auto-update): match both canonical and legacy plugin names in entry finder Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../checker/plugin-entry.test.ts | 59 +++++++++++++++++++ .../checker/plugin-entry.ts | 18 +++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/hooks/auto-update-checker/checker/plugin-entry.test.ts b/src/hooks/auto-update-checker/checker/plugin-entry.test.ts index c621099b6..341839af0 100644 --- a/src/hooks/auto-update-checker/checker/plugin-entry.test.ts +++ b/src/hooks/auto-update-checker/checker/plugin-entry.test.ts @@ -4,6 +4,7 @@ import * as fs from "node:fs" import * as os from "node:os" import * as path from "node:path" import { PACKAGE_NAME } from "../constants" +import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../../shared/plugin-identity" type PluginEntryResult = { entry: string @@ -120,6 +121,64 @@ describe("findPluginEntry", () => { expect(pluginInfo?.pinnedVersion).toBe("3.5.2") }) + test("finds preferred plugin entry", async () => { + // #given preferred plugin entry is configured + fs.writeFileSync(configPath, JSON.stringify({ plugin: [PLUGIN_NAME] })) + + // #when plugin entry is detected + const execution = runFindPluginEntry(temporaryDirectory) + + // #then preferred entry is returned + expect(execution.status).toBe(0) + const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult + expect(pluginInfo?.entry).toBe(PLUGIN_NAME) + expect(pluginInfo?.isPinned).toBe(false) + expect(pluginInfo?.pinnedVersion).toBeNull() + }) + + test("finds legacy plugin entry", async () => { + // #given legacy plugin entry is configured + fs.writeFileSync(configPath, JSON.stringify({ plugin: [LEGACY_PLUGIN_NAME] })) + + // #when plugin entry is detected + const execution = runFindPluginEntry(temporaryDirectory) + + // #then legacy entry is returned + expect(execution.status).toBe(0) + const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult + expect(pluginInfo?.entry).toBe(LEGACY_PLUGIN_NAME) + expect(pluginInfo?.isPinned).toBe(false) + expect(pluginInfo?.pinnedVersion).toBeNull() + }) + + test("finds preferred plugin entry with pinned version", async () => { + // #given preferred plugin entry includes semver version + fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PLUGIN_NAME}@3.15.0`] })) + + // #when plugin entry is detected + const execution = runFindPluginEntry(temporaryDirectory) + + // #then preferred versioned entry is returned + expect(execution.status).toBe(0) + const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult + expect(pluginInfo?.entry).toBe(`${PLUGIN_NAME}@3.15.0`) + expect(pluginInfo?.isPinned).toBe(true) + expect(pluginInfo?.pinnedVersion).toBe("3.15.0") + }) + + test("returns null for unrelated plugin entry", async () => { + // #given unrelated plugin entry is configured + fs.writeFileSync(configPath, JSON.stringify({ plugin: ["some-other-plugin"] })) + + // #when plugin entry is detected + const execution = runFindPluginEntry(temporaryDirectory) + + // #then no matching entry is returned + expect(execution.status).toBe(0) + const pluginInfo = JSON.parse(execution.stdout.trim()) as PluginEntryResult + expect(pluginInfo).toBeNull() + }) + test("reads user config from profile dir even when OPENCODE_CONFIG_DIR changes after import", async () => { // #given profile-specific user config after module import const profileConfigDir = path.join(temporaryDirectory, "profiles", "today") diff --git a/src/hooks/auto-update-checker/checker/plugin-entry.ts b/src/hooks/auto-update-checker/checker/plugin-entry.ts index f204d61f1..55260c94e 100644 --- a/src/hooks/auto-update-checker/checker/plugin-entry.ts +++ b/src/hooks/auto-update-checker/checker/plugin-entry.ts @@ -3,6 +3,7 @@ import type { OpencodeConfig } from "../types" import { PACKAGE_NAME } from "../constants" import { getConfigPaths } from "./config-paths" import { stripJsonComments } from "./jsonc-strip" +import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../../shared/plugin-identity" export interface PluginEntryInfo { entry: string @@ -12,6 +13,7 @@ export interface PluginEntryInfo { } const EXACT_SEMVER_REGEX = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/ +const MATCH_PLUGIN_NAMES = [PACKAGE_NAME, PLUGIN_NAME, LEGACY_PLUGIN_NAME] export function findPluginEntry(directory: string): PluginEntryInfo | null { for (const configPath of getConfigPaths(directory)) { @@ -22,13 +24,15 @@ export function findPluginEntry(directory: string): PluginEntryInfo | null { const plugins = config.plugin ?? [] for (const entry of plugins) { - if (entry === PACKAGE_NAME) { - return { entry, isPinned: false, pinnedVersion: null, configPath } - } - if (entry.startsWith(`${PACKAGE_NAME}@`)) { - const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1) - const isPinned = EXACT_SEMVER_REGEX.test(pinnedVersion.trim()) - return { entry, isPinned, pinnedVersion, configPath } + for (const pluginName of MATCH_PLUGIN_NAMES) { + if (entry === pluginName) { + return { entry, isPinned: false, pinnedVersion: null, configPath } + } + if (entry.startsWith(`${pluginName}@`)) { + const pinnedVersion = entry.slice(pluginName.length + 1) + const isPinned = EXACT_SEMVER_REGEX.test(pinnedVersion.trim()) + return { entry, isPinned, pinnedVersion, configPath } + } } } } catch {