From f547cd013dcb6f123ad17dce6bef9ed234195906 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 4 Apr 2026 02:10:27 +0900 Subject: [PATCH] refactor(shared): consolidate plugin entry migration and detection utilities --- src/hooks/legacy-plugin-toast/auto-migrate.ts | 21 ++------- src/shared/external-plugin-detector.ts | 45 +++---------------- .../log-legacy-plugin-startup-warning.test.ts | 6 ++- .../log-legacy-plugin-startup-warning.ts | 15 +------ src/shared/migrate-legacy-plugin-entry.ts | 19 ++------ src/shared/plugin-entry-migrator.ts | 22 ++++++++- 6 files changed, 40 insertions(+), 88 deletions(-) diff --git a/src/hooks/legacy-plugin-toast/auto-migrate.ts b/src/hooks/legacy-plugin-toast/auto-migrate.ts index a33cf8ac9..424a11b3e 100644 --- a/src/hooks/legacy-plugin-toast/auto-migrate.ts +++ b/src/hooks/legacy-plugin-toast/auto-migrate.ts @@ -3,7 +3,8 @@ import { join } from "node:path" import { parseJsoncSafe } from "../../shared/jsonc-parser" import { getOpenCodeConfigPaths } from "../../shared/opencode-config-dir" -import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../shared/plugin-identity" +import { PLUGIN_NAME } from "../../shared/plugin-identity" +import { isCanonicalEntry, isLegacyEntry, toCanonicalEntry } from "../../shared/plugin-entry-migrator" import { migrateLegacyPluginEntry } from "./plugin-entry-migrator" export interface MigrationResult { @@ -17,22 +18,6 @@ interface OpenCodeConfig { plugin?: string[] } -function isLegacyEntry(entry: string): boolean { - return entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`) -} - -function isCanonicalEntry(entry: string): boolean { - return entry === PLUGIN_NAME || entry.startsWith(`${PLUGIN_NAME}@`) -} - -function toLegacyCanonical(entry: string): string { - if (entry === LEGACY_PLUGIN_NAME) return PLUGIN_NAME - if (entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) { - return `${PLUGIN_NAME}${entry.slice(LEGACY_PLUGIN_NAME.length)}` - } - return entry -} - function detectOpenCodeConfigPath(overrideConfigDir?: string): string | null { if (overrideConfigDir) { const jsoncPath = join(overrideConfigDir, "opencode.jsonc") @@ -63,7 +48,7 @@ export function autoMigrateLegacyPluginEntry(overrideConfigDir?: string): Migrat const hasCanonical = plugins.some(isCanonicalEntry) const from = legacyEntries[0] - const to = toLegacyCanonical(from) + const to = toCanonicalEntry(from) const migrated = migrateLegacyPluginEntry(configPath) if (!migrated) return { migrated: false, from: null, to: null, configPath } diff --git a/src/shared/external-plugin-detector.ts b/src/shared/external-plugin-detector.ts index 1ebc90967..13a8a0d76 100644 --- a/src/shared/external-plugin-detector.ts +++ b/src/shared/external-plugin-detector.ts @@ -27,49 +27,18 @@ const KNOWN_SKILL_PLUGINS = [ "@opencode/skills", ] -/** - * Check if a plugin entry matches a known notification plugin. - * Handles various formats: "name", "name@version", "npm:name", "file://path/name" - */ -function matchesNotificationPlugin(entry: string): string | null { +function matchesKnownPlugin(entry: string, knownPlugins: readonly string[]): string | null { const normalized = entry.toLowerCase() - for (const known of KNOWN_NOTIFICATION_PLUGINS) { - // Exact match + for (const known of knownPlugins) { if (normalized === known) return known - // Version suffix: "opencode-notifier@1.2.3" if (normalized.startsWith(`${known}@`)) return known - // Scoped package: "@mohak34/opencode-notifier" or "@mohak34/opencode-notifier@1.2.3" - if (normalized === `@mohak34/${known}` || normalized.startsWith(`@mohak34/${known}@`)) return known - // npm: prefix if (normalized === `npm:${known}` || normalized.startsWith(`npm:${known}@`)) return known - // file:// path ending exactly with package name if (normalized.startsWith("file://") && ( - normalized.endsWith(`/${known}`) || + normalized.endsWith(`/${known}`) || normalized.endsWith(`\\${known}`) )) return known } - return null -} -/** - * Check if a plugin entry matches a known skill plugin. - * Handles various formats: "name", "name@version", "npm:name", "file://path/name" - */ -function matchesSkillPlugin(entry: string): string | null { - const normalized = entry.toLowerCase() - for (const known of KNOWN_SKILL_PLUGINS) { - // Exact match - if (normalized === known) return known - // Version suffix: "opencode-skills@1.2.3" - if (normalized.startsWith(`${known}@`)) return known - // npm: prefix - if (normalized === `npm:${known}` || normalized.startsWith(`npm:${known}@`)) return known - // file:// path ending exactly with package name - if (normalized.startsWith("file://") && ( - normalized.endsWith(`/${known}`) || - normalized.endsWith(`\\${known}`) - )) return known - } return null } @@ -91,9 +60,9 @@ export interface ExternalSkillPluginResult { */ export function detectExternalNotificationPlugin(directory: string): ExternalNotifierResult { const plugins = loadOpencodePlugins(directory) - + for (const plugin of plugins) { - const match = matchesNotificationPlugin(plugin) + const match = matchesKnownPlugin(plugin, KNOWN_NOTIFICATION_PLUGINS) if (match) { log(`Detected external notification plugin: ${plugin}`) return { @@ -117,9 +86,9 @@ export function detectExternalNotificationPlugin(directory: string): ExternalNot */ export function detectExternalSkillPlugin(directory: string): ExternalSkillPluginResult { const plugins = loadOpencodePlugins(directory) - + for (const plugin of plugins) { - const match = matchesSkillPlugin(plugin) + const match = matchesKnownPlugin(plugin, KNOWN_SKILL_PLUGINS) if (match) { log(`Detected external skill plugin: ${plugin}`) return { diff --git a/src/shared/log-legacy-plugin-startup-warning.test.ts b/src/shared/log-legacy-plugin-startup-warning.test.ts index 0b288a461..d95f48254 100644 --- a/src/shared/log-legacy-plugin-startup-warning.test.ts +++ b/src/shared/log-legacy-plugin-startup-warning.test.ts @@ -1,3 +1,5 @@ +/// + import { afterAll, afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import type { LegacyPluginCheckResult } from "./legacy-plugin-warning" @@ -26,7 +28,7 @@ mock.module("./logger", () => ({ log: mockLog, })) -mock.module("./plugin-entry-migrator", () => ({ +mock.module("./migrate-legacy-plugin-entry", () => ({ migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry, })) @@ -143,7 +145,7 @@ describe("logLegacyPluginStartupWarning", () => { logLegacyPluginStartupWarning() //#then - const calls = consoleWarnSpy.mock.calls.map((c) => c[0] as string) + const calls = consoleWarnSpy.mock.calls.map((call: string[]) => call[0] ?? "") expect(calls.some((c) => c.includes("Auto-migrated"))).toBe(true) }) }) diff --git a/src/shared/log-legacy-plugin-startup-warning.ts b/src/shared/log-legacy-plugin-startup-warning.ts index 1ff3c7c19..5382ac86e 100644 --- a/src/shared/log-legacy-plugin-startup-warning.ts +++ b/src/shared/log-legacy-plugin-startup-warning.ts @@ -1,20 +1,9 @@ import { checkForLegacyPluginEntry } from "./legacy-plugin-warning" import { log } from "./logger" -import { migrateLegacyPluginEntry } from "./plugin-entry-migrator" +import { migrateLegacyPluginEntry } from "./migrate-legacy-plugin-entry" +import { toCanonicalEntry } from "./plugin-entry-migrator" import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity" -function toCanonicalEntry(entry: string): string { - if (entry === LEGACY_PLUGIN_NAME) { - return PLUGIN_NAME - } - - if (entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) { - return `${PLUGIN_NAME}${entry.slice(LEGACY_PLUGIN_NAME.length)}` - } - - return entry -} - export function logLegacyPluginStartupWarning(): void { const result = checkForLegacyPluginEntry() if (!result.hasLegacyEntry) { diff --git a/src/shared/migrate-legacy-plugin-entry.ts b/src/shared/migrate-legacy-plugin-entry.ts index 21dc5f875..80a015c6e 100644 --- a/src/shared/migrate-legacy-plugin-entry.ts +++ b/src/shared/migrate-legacy-plugin-entry.ts @@ -1,30 +1,16 @@ import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs" + import { applyEdits, modify } from "jsonc-parser" import { parseJsoncSafe } from "./jsonc-parser" import { log } from "./logger" import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity" +import { isCanonicalEntry, isLegacyEntry, toCanonicalEntry } from "./plugin-entry-migrator" interface OpenCodeConfig { plugin?: string[] } -function isLegacyEntry(entry: string): boolean { - return entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`) -} - -function isCanonicalEntry(entry: string): boolean { - return entry === PLUGIN_NAME || entry.startsWith(`${PLUGIN_NAME}@`) -} - -function toCanonicalEntry(entry: string): string { - if (entry === LEGACY_PLUGIN_NAME) return PLUGIN_NAME - if (entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) { - return `${PLUGIN_NAME}${entry.slice(LEGACY_PLUGIN_NAME.length)}` - } - return entry -} - function normalizePluginEntries(entries: string[]): string[] { const hasCanonical = entries.some(isCanonicalEntry) @@ -74,6 +60,7 @@ export function migrateLegacyPluginEntry(configPath: string): boolean { } finally { closeSync(tempFileDescriptor) } + renameSync(tempPath, configPath) log("[migrateLegacyPluginEntry] Auto-migrated opencode.json plugin entry", { configPath, diff --git a/src/shared/plugin-entry-migrator.ts b/src/shared/plugin-entry-migrator.ts index 50cc7c1a1..d8b44fcbb 100644 --- a/src/shared/plugin-entry-migrator.ts +++ b/src/shared/plugin-entry-migrator.ts @@ -1 +1,21 @@ -export { migrateLegacyPluginEntry } from "./migrate-legacy-plugin-entry" +import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity" + +export function isLegacyEntry(entry: string): boolean { + return entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`) +} + +export function isCanonicalEntry(entry: string): boolean { + return entry === PLUGIN_NAME || entry.startsWith(`${PLUGIN_NAME}@`) +} + +export function toCanonicalEntry(entry: string): string { + if (entry === LEGACY_PLUGIN_NAME) { + return PLUGIN_NAME + } + + if (entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) { + return `${PLUGIN_NAME}${entry.slice(LEGACY_PLUGIN_NAME.length)}` + } + + return entry +}