refactor(shared): consolidate plugin entry migration and detection utilities
This commit is contained in:
@@ -3,7 +3,8 @@ import { join } from "node:path"
|
|||||||
|
|
||||||
import { parseJsoncSafe } from "../../shared/jsonc-parser"
|
import { parseJsoncSafe } from "../../shared/jsonc-parser"
|
||||||
import { getOpenCodeConfigPaths } from "../../shared/opencode-config-dir"
|
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"
|
import { migrateLegacyPluginEntry } from "./plugin-entry-migrator"
|
||||||
|
|
||||||
export interface MigrationResult {
|
export interface MigrationResult {
|
||||||
@@ -17,22 +18,6 @@ interface OpenCodeConfig {
|
|||||||
plugin?: string[]
|
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 {
|
function detectOpenCodeConfigPath(overrideConfigDir?: string): string | null {
|
||||||
if (overrideConfigDir) {
|
if (overrideConfigDir) {
|
||||||
const jsoncPath = join(overrideConfigDir, "opencode.jsonc")
|
const jsoncPath = join(overrideConfigDir, "opencode.jsonc")
|
||||||
@@ -63,7 +48,7 @@ export function autoMigrateLegacyPluginEntry(overrideConfigDir?: string): Migrat
|
|||||||
|
|
||||||
const hasCanonical = plugins.some(isCanonicalEntry)
|
const hasCanonical = plugins.some(isCanonicalEntry)
|
||||||
const from = legacyEntries[0]
|
const from = legacyEntries[0]
|
||||||
const to = toLegacyCanonical(from)
|
const to = toCanonicalEntry(from)
|
||||||
const migrated = migrateLegacyPluginEntry(configPath)
|
const migrated = migrateLegacyPluginEntry(configPath)
|
||||||
if (!migrated) return { migrated: false, from: null, to: null, configPath }
|
if (!migrated) return { migrated: false, from: null, to: null, configPath }
|
||||||
|
|
||||||
|
|||||||
@@ -27,49 +27,18 @@ const KNOWN_SKILL_PLUGINS = [
|
|||||||
"@opencode/skills",
|
"@opencode/skills",
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
function matchesKnownPlugin(entry: string, knownPlugins: readonly string[]): string | null {
|
||||||
* 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 {
|
|
||||||
const normalized = entry.toLowerCase()
|
const normalized = entry.toLowerCase()
|
||||||
for (const known of KNOWN_NOTIFICATION_PLUGINS) {
|
for (const known of knownPlugins) {
|
||||||
// Exact match
|
|
||||||
if (normalized === known) return known
|
if (normalized === known) return known
|
||||||
// Version suffix: "opencode-notifier@1.2.3"
|
|
||||||
if (normalized.startsWith(`${known}@`)) return known
|
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
|
if (normalized === `npm:${known}` || normalized.startsWith(`npm:${known}@`)) return known
|
||||||
// file:// path ending exactly with package name
|
|
||||||
if (normalized.startsWith("file://") && (
|
if (normalized.startsWith("file://") && (
|
||||||
normalized.endsWith(`/${known}`) ||
|
normalized.endsWith(`/${known}`) ||
|
||||||
normalized.endsWith(`\\${known}`)
|
normalized.endsWith(`\\${known}`)
|
||||||
)) return 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +60,9 @@ export interface ExternalSkillPluginResult {
|
|||||||
*/
|
*/
|
||||||
export function detectExternalNotificationPlugin(directory: string): ExternalNotifierResult {
|
export function detectExternalNotificationPlugin(directory: string): ExternalNotifierResult {
|
||||||
const plugins = loadOpencodePlugins(directory)
|
const plugins = loadOpencodePlugins(directory)
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const match = matchesNotificationPlugin(plugin)
|
const match = matchesKnownPlugin(plugin, KNOWN_NOTIFICATION_PLUGINS)
|
||||||
if (match) {
|
if (match) {
|
||||||
log(`Detected external notification plugin: ${plugin}`)
|
log(`Detected external notification plugin: ${plugin}`)
|
||||||
return {
|
return {
|
||||||
@@ -117,9 +86,9 @@ export function detectExternalNotificationPlugin(directory: string): ExternalNot
|
|||||||
*/
|
*/
|
||||||
export function detectExternalSkillPlugin(directory: string): ExternalSkillPluginResult {
|
export function detectExternalSkillPlugin(directory: string): ExternalSkillPluginResult {
|
||||||
const plugins = loadOpencodePlugins(directory)
|
const plugins = loadOpencodePlugins(directory)
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const match = matchesSkillPlugin(plugin)
|
const match = matchesKnownPlugin(plugin, KNOWN_SKILL_PLUGINS)
|
||||||
if (match) {
|
if (match) {
|
||||||
log(`Detected external skill plugin: ${plugin}`)
|
log(`Detected external skill plugin: ${plugin}`)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference path="../../bun-test.d.ts" />
|
||||||
|
|
||||||
import { afterAll, afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
|
import { afterAll, afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
|
||||||
import type { LegacyPluginCheckResult } from "./legacy-plugin-warning"
|
import type { LegacyPluginCheckResult } from "./legacy-plugin-warning"
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ mock.module("./logger", () => ({
|
|||||||
log: mockLog,
|
log: mockLog,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("./plugin-entry-migrator", () => ({
|
mock.module("./migrate-legacy-plugin-entry", () => ({
|
||||||
migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry,
|
migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ describe("logLegacyPluginStartupWarning", () => {
|
|||||||
logLegacyPluginStartupWarning()
|
logLegacyPluginStartupWarning()
|
||||||
|
|
||||||
//#then
|
//#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)
|
expect(calls.some((c) => c.includes("Auto-migrated"))).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
import { checkForLegacyPluginEntry } from "./legacy-plugin-warning"
|
import { checkForLegacyPluginEntry } from "./legacy-plugin-warning"
|
||||||
import { log } from "./logger"
|
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"
|
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 {
|
export function logLegacyPluginStartupWarning(): void {
|
||||||
const result = checkForLegacyPluginEntry()
|
const result = checkForLegacyPluginEntry()
|
||||||
if (!result.hasLegacyEntry) {
|
if (!result.hasLegacyEntry) {
|
||||||
|
|||||||
@@ -1,30 +1,16 @@
|
|||||||
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
||||||
|
|
||||||
import { applyEdits, modify } from "jsonc-parser"
|
import { applyEdits, modify } from "jsonc-parser"
|
||||||
|
|
||||||
import { parseJsoncSafe } from "./jsonc-parser"
|
import { parseJsoncSafe } from "./jsonc-parser"
|
||||||
import { log } from "./logger"
|
import { log } from "./logger"
|
||||||
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity"
|
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity"
|
||||||
|
import { isCanonicalEntry, isLegacyEntry, toCanonicalEntry } from "./plugin-entry-migrator"
|
||||||
|
|
||||||
interface OpenCodeConfig {
|
interface OpenCodeConfig {
|
||||||
plugin?: string[]
|
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[] {
|
function normalizePluginEntries(entries: string[]): string[] {
|
||||||
const hasCanonical = entries.some(isCanonicalEntry)
|
const hasCanonical = entries.some(isCanonicalEntry)
|
||||||
|
|
||||||
@@ -74,6 +60,7 @@ export function migrateLegacyPluginEntry(configPath: string): boolean {
|
|||||||
} finally {
|
} finally {
|
||||||
closeSync(tempFileDescriptor)
|
closeSync(tempFileDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
renameSync(tempPath, configPath)
|
renameSync(tempPath, configPath)
|
||||||
log("[migrateLegacyPluginEntry] Auto-migrated opencode.json plugin entry", {
|
log("[migrateLegacyPluginEntry] Auto-migrated opencode.json plugin entry", {
|
||||||
configPath,
|
configPath,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user