Fix local plugin registration for OpenCode 1.4
This commit is contained in:
@@ -1,15 +1,27 @@
|
|||||||
import { readFileSync, writeFileSync } from "node:fs"
|
import { readFileSync, writeFileSync } from "node:fs"
|
||||||
import type { ConfigMergeResult } from "../types"
|
import type { ConfigMergeResult } from "../types"
|
||||||
import { PLUGIN_NAME, LEGACY_PLUGIN_NAME } from "../../shared"
|
import {
|
||||||
|
PLUGIN_NAME,
|
||||||
|
LEGACY_PLUGIN_NAME,
|
||||||
|
isAcceptedLocalPluginEntry,
|
||||||
|
} from "../../shared"
|
||||||
import { backupConfigFile } from "./backup-config"
|
import { backupConfigFile } from "./backup-config"
|
||||||
import { getConfigDir } from "./config-context"
|
import { getConfigDir } from "./config-context"
|
||||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||||
import { detectConfigFormat } from "./opencode-config-format"
|
import { detectConfigFormat } from "./opencode-config-format"
|
||||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||||
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
import { getPreferredPluginEntry } from "./preferred-plugin-entry"
|
||||||
import { checkVersionCompatibility, extractVersionFromPluginEntry } from "./version-compatibility"
|
import { checkVersionCompatibility, extractVersionFromPluginEntry } from "./version-compatibility"
|
||||||
|
|
||||||
|
function isOurPluginEntry(entry: string): boolean {
|
||||||
|
return entry === PLUGIN_NAME ||
|
||||||
|
entry.startsWith(`${PLUGIN_NAME}@`) ||
|
||||||
|
entry === LEGACY_PLUGIN_NAME ||
|
||||||
|
entry.startsWith(`${LEGACY_PLUGIN_NAME}@`) ||
|
||||||
|
isAcceptedLocalPluginEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||||
try {
|
try {
|
||||||
ensureConfigDirectoryExists()
|
ensureConfigDirectoryExists()
|
||||||
@@ -22,7 +34,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { format, path } = detectConfigFormat()
|
const { format, path } = detectConfigFormat()
|
||||||
const pluginEntry = await getPluginNameWithVersion(currentVersion, PLUGIN_NAME)
|
const pluginEntry = await getPreferredPluginEntry(currentVersion)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (format === "none") {
|
if (format === "none") {
|
||||||
@@ -43,18 +55,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
|||||||
const config = parseResult.config
|
const config = parseResult.config
|
||||||
const plugins = config.plugin ?? []
|
const plugins = config.plugin ?? []
|
||||||
|
|
||||||
const canonicalEntries = plugins.filter(
|
const existingEntry = plugins.find(isOurPluginEntry)
|
||||||
(plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`)
|
|
||||||
)
|
|
||||||
const legacyEntries = plugins.filter(
|
|
||||||
(plugin) => plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`)
|
|
||||||
)
|
|
||||||
const otherPlugins = plugins.filter(
|
|
||||||
(plugin) => !(plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`))
|
|
||||||
&& !(plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`))
|
|
||||||
)
|
|
||||||
|
|
||||||
const existingEntry = canonicalEntries[0] ?? legacyEntries[0]
|
|
||||||
if (existingEntry) {
|
if (existingEntry) {
|
||||||
const installedVersion = extractVersionFromPluginEntry(existingEntry)
|
const installedVersion = extractVersionFromPluginEntry(existingEntry)
|
||||||
const compatibility = checkVersionCompatibility(installedVersion, currentVersion)
|
const compatibility = checkVersionCompatibility(installedVersion, currentVersion)
|
||||||
@@ -77,7 +78,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedPlugins = [...otherPlugins]
|
const normalizedPlugins = plugins.filter((plugin) => !isOurPluginEntry(plugin))
|
||||||
|
|
||||||
normalizedPlugins.push(pluginEntry)
|
normalizedPlugins.push(pluginEntry)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
import { parseJsonc, LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../shared"
|
import {
|
||||||
|
parseJsonc,
|
||||||
|
LEGACY_PLUGIN_NAME,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
isAcceptedLocalPluginEntry,
|
||||||
|
} from "../../shared"
|
||||||
import type { DetectedConfig } from "../types"
|
import type { DetectedConfig } from "../types"
|
||||||
import { getOmoConfigPath } from "./config-context"
|
import { getOmoConfigPath } from "./config-context"
|
||||||
import { detectConfigFormat } from "./opencode-config-format"
|
import { detectConfigFormat } from "./opencode-config-format"
|
||||||
@@ -58,7 +63,8 @@ function detectProvidersFromOmoConfig(): {
|
|||||||
|
|
||||||
function isOurPlugin(plugin: string): boolean {
|
function isOurPlugin(plugin: string): boolean {
|
||||||
return plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) ||
|
return plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) ||
|
||||||
plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`)
|
plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`) ||
|
||||||
|
isAcceptedLocalPluginEntry(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOurPluginEntry(plugins: string[]): string | null {
|
function findOurPluginEntry(plugins: string[]): string | null {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
|||||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
|
||||||
import { resetConfigContext } from "./config-context"
|
import { resetConfigContext } from "./config-context"
|
||||||
import { detectCurrentConfig } from "./detect-current-config"
|
import { detectCurrentConfig } from "./detect-current-config"
|
||||||
@@ -40,6 +41,25 @@ describe("detectCurrentConfig - single package detection", () => {
|
|||||||
expect(result.isInstalled).toBe(true)
|
expect(result.isInstalled).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("detects a local file URL plugin entry", () => {
|
||||||
|
// given
|
||||||
|
const pluginDir = join(testConfigDir, "plugin-root")
|
||||||
|
mkdirSync(pluginDir, { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(pluginDir, "package.json"),
|
||||||
|
JSON.stringify({ name: "oh-my-opencode" }, null, 2) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
const localPluginEntry = pathToFileURL(pluginDir).href
|
||||||
|
writeFileSync(testConfigPath, JSON.stringify({ plugin: [localPluginEntry] }, null, 2) + "\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = detectCurrentConfig()
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.isInstalled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it("returns false when plugin not present with similar name", () => {
|
it("returns false when plugin not present with similar name", () => {
|
||||||
// given
|
// given
|
||||||
writeFileSync(testConfigPath, JSON.stringify({ plugin: ["oh-my-openagent-extra"] }, null, 2) + "\n", "utf-8")
|
writeFileSync(testConfigPath, JSON.stringify({ plugin: ["oh-my-openagent-extra"] }, null, 2) + "\n", "utf-8")
|
||||||
@@ -94,7 +114,8 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent"])
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("upgrades a bare legacy plugin entry to canonical", async () => {
|
it("upgrades a bare legacy plugin entry to canonical", async () => {
|
||||||
@@ -107,7 +128,8 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent"])
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a version-pinned legacy entry to the requested version", async () => {
|
it("updates a version-pinned legacy entry to the requested version", async () => {
|
||||||
@@ -121,7 +143,8 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent@3.16.0"])
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
getPluginNameWithVersionSpy.mockRestore()
|
getPluginNameWithVersionSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,7 +158,8 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent"])
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("preserves a canonical entry when the same version is re-installed", async () => {
|
it("preserves a canonical entry when the same version is re-installed", async () => {
|
||||||
@@ -149,10 +173,33 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent@3.10.0"])
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
getPluginNameWithVersionSpy.mockRestore()
|
getPluginNameWithVersionSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("replaces an existing local file URL entry without duplicating it", async () => {
|
||||||
|
// given
|
||||||
|
const pluginDir = join(testConfigDir, "plugin-root")
|
||||||
|
mkdirSync(pluginDir, { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(pluginDir, "package.json"),
|
||||||
|
JSON.stringify({ name: "oh-my-opencode" }, null, 2) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
const existingLocalEntry = pathToFileURL(pluginDir).href
|
||||||
|
writeFileSync(testConfigPath, JSON.stringify({ plugin: [existingLocalEntry] }, null, 2) + "\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await addPluginToOpenCodeConfig("3.10.0")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||||
|
expect(savedConfig.plugin).toHaveLength(1)
|
||||||
|
expect(savedConfig.plugin[0]).toMatch(/^file:\/\//)
|
||||||
|
})
|
||||||
|
|
||||||
it("blocks a downgrade for a version-pinned canonical entry", async () => {
|
it("blocks a downgrade for a version-pinned canonical entry", async () => {
|
||||||
// given
|
// given
|
||||||
const getPluginNameWithVersionSpy = spyOn(pluginNameWithVersion, "getPluginNameWithVersion").mockResolvedValue("oh-my-openagent@3.15.0")
|
const getPluginNameWithVersionSpy = spyOn(pluginNameWithVersion, "getPluginNameWithVersion").mockResolvedValue("oh-my-openagent@3.15.0")
|
||||||
@@ -181,7 +228,7 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
// then
|
// then
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
const savedContent = readFileSync(testConfigPath, "utf-8")
|
const savedContent = readFileSync(testConfigPath, "utf-8")
|
||||||
expect(savedContent.includes('"plugin": [\n "oh-my-openagent"\n ]')).toBe(true)
|
expect(savedContent.includes('"plugin": [\n "file://')).toBe(true)
|
||||||
expect(savedContent.includes("oh-my-opencode")).toBe(false)
|
expect(savedContent.includes("oh-my-opencode")).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
|
import { dirname, join } from "node:path"
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||||
|
|
||||||
|
import { ACCEPTED_PACKAGE_NAMES, PLUGIN_NAME } from "../../shared"
|
||||||
|
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
||||||
|
|
||||||
|
const ACCEPTED_PACKAGE_NAME_SET = new Set<string>(ACCEPTED_PACKAGE_NAMES)
|
||||||
|
const PACKAGE_JSON_SEARCH_DEPTH = 10
|
||||||
|
|
||||||
|
type PackageJsonShape = {
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInstalledPluginRoot(startPath: string): string | null {
|
||||||
|
let directory = dirname(startPath)
|
||||||
|
|
||||||
|
for (let depth = 0; depth < PACKAGE_JSON_SEARCH_DEPTH; depth += 1) {
|
||||||
|
const packageJsonPath = join(directory, "package.json")
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(packageJsonPath, "utf-8")
|
||||||
|
const packageJson = JSON.parse(content) as PackageJsonShape
|
||||||
|
if (packageJson.name && ACCEPTED_PACKAGE_NAME_SET.has(packageJson.name)) {
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed package.json files while searching upward.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDirectory = dirname(directory)
|
||||||
|
if (parentDirectory === directory) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
directory = parentDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPreferredPluginEntry(currentVersion: string): Promise<string> {
|
||||||
|
const installedPluginRoot = findInstalledPluginRoot(fileURLToPath(import.meta.url))
|
||||||
|
if (installedPluginRoot) {
|
||||||
|
return pathToFileURL(installedPluginRoot).href
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPluginNameWithVersion(currentVersion, PLUGIN_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { findInstalledPluginRoot }
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
||||||
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
|
||||||
|
import { findPluginEntry, getPluginInfo } from "./system-plugin"
|
||||||
|
|
||||||
|
describe("system-plugin", () => {
|
||||||
|
let testConfigDir = ""
|
||||||
|
let testConfigPath = ""
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testConfigDir = join(tmpdir(), `omo-system-plugin-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||||
|
testConfigPath = join(testConfigDir, "opencode.json")
|
||||||
|
|
||||||
|
mkdirSync(testConfigDir, { recursive: true })
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = testConfigDir
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(testConfigDir, { recursive: true, force: true })
|
||||||
|
delete process.env.OPENCODE_CONFIG_DIR
|
||||||
|
})
|
||||||
|
|
||||||
|
it("treats file URL plugin entries as local-dev installs", () => {
|
||||||
|
// given
|
||||||
|
const entry = pathToFileURL(join(testConfigDir, "node_modules", "oh-my-openagent")).href
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = findPluginEntry([entry])
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ entry, isLocalDev: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("treats absolute plugin paths as local-dev installs", () => {
|
||||||
|
// given
|
||||||
|
const entry = "/home/test/.config/opencode/node_modules/oh-my-opencode"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = findPluginEntry([entry])
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({ entry, isLocalDev: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("reports local path plugin entries as registered", () => {
|
||||||
|
// given
|
||||||
|
const pluginDir = join(testConfigDir, "plugin-root")
|
||||||
|
mkdirSync(pluginDir, { recursive: true })
|
||||||
|
writeFileSync(
|
||||||
|
join(pluginDir, "package.json"),
|
||||||
|
JSON.stringify({ name: "oh-my-opencode" }, null, 2) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
const entry = pathToFileURL(pluginDir).href
|
||||||
|
writeFileSync(testConfigPath, JSON.stringify({ plugin: [entry] }, null, 2) + "\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const pluginInfo = getPluginInfo()
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(pluginInfo.registered).toBe(true)
|
||||||
|
expect(pluginInfo.entry).toBe(entry)
|
||||||
|
expect(pluginInfo.isLocalDev).toBe(true)
|
||||||
|
expect(pluginInfo.isPinned).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
|
|
||||||
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
|
import {
|
||||||
|
LEGACY_PLUGIN_NAME,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
getOpenCodeConfigPaths,
|
||||||
|
parseJsonc,
|
||||||
|
isAcceptedLocalPluginEntry,
|
||||||
|
} from "../../../shared"
|
||||||
|
|
||||||
export interface PluginInfo {
|
export interface PluginInfo {
|
||||||
registered: boolean
|
registered: boolean
|
||||||
@@ -44,7 +50,7 @@ function findPluginEntry(entries: string[]): { entry: string; isLocalDev: boolea
|
|||||||
if (entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) {
|
if (entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) {
|
||||||
return { entry, isLocalDev: false }
|
return { entry, isLocalDev: false }
|
||||||
}
|
}
|
||||||
if (entry.startsWith("file://") && (entry.includes(PLUGIN_NAME) || entry.includes(LEGACY_PLUGIN_NAME))) {
|
if (isAcceptedLocalPluginEntry(entry)) {
|
||||||
return { entry, isLocalDev: true }
|
return { entry, isLocalDev: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,5 +73,6 @@ export * from "./internal-initiator-marker"
|
|||||||
export * from "./plugin-command-discovery"
|
export * from "./plugin-command-discovery"
|
||||||
export { SessionCategoryRegistry } from "./session-category-registry"
|
export { SessionCategoryRegistry } from "./session-category-registry"
|
||||||
export * from "./plugin-identity"
|
export * from "./plugin-identity"
|
||||||
|
export * from "./local-plugin-entry"
|
||||||
export * from "./log-legacy-plugin-startup-warning"
|
export * from "./log-legacy-plugin-startup-warning"
|
||||||
export * from "./task-system-enabled"
|
export * from "./task-system-enabled"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { existsSync, readFileSync, statSync } from "node:fs"
|
||||||
|
import { dirname, join } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
import { ACCEPTED_PACKAGE_NAMES } from "./plugin-identity"
|
||||||
|
|
||||||
|
const ACCEPTED_PACKAGE_NAME_SET = new Set<string>(ACCEPTED_PACKAGE_NAMES)
|
||||||
|
const PACKAGE_JSON_SEARCH_DEPTH = 10
|
||||||
|
|
||||||
|
type PackageJsonShape = {
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalPluginPath(entry: string): boolean {
|
||||||
|
return entry.startsWith("file://") || entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFilePath(entry: string): string | null {
|
||||||
|
if (entry.startsWith("file://")) {
|
||||||
|
try {
|
||||||
|
return fileURLToPath(entry)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.startsWith("/") || /^[A-Za-z]:[\\/]/.test(entry)) {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLocalPluginPackageName(entry: string): string | null {
|
||||||
|
const filePath = toFilePath(entry)
|
||||||
|
if (!filePath || !existsSync(filePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let directory = filePath
|
||||||
|
try {
|
||||||
|
const stat = statSync(filePath)
|
||||||
|
directory = stat.isDirectory() ? filePath : dirname(filePath)
|
||||||
|
} catch {
|
||||||
|
directory = dirname(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let depth = 0; depth < PACKAGE_JSON_SEARCH_DEPTH; depth += 1) {
|
||||||
|
const packageJsonPath = join(directory, "package.json")
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(packageJsonPath, "utf-8")
|
||||||
|
const packageJson = JSON.parse(content) as PackageJsonShape
|
||||||
|
if (typeof packageJson.name === "string" && packageJson.name.length > 0) {
|
||||||
|
return packageJson.name
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed package.json files while searching upward.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDirectory = dirname(directory)
|
||||||
|
if (parentDirectory === directory) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
directory = parentDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAcceptedLocalPluginEntry(entry: string): boolean {
|
||||||
|
if (!isLocalPluginPath(entry)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageName = resolveLocalPluginPackageName(entry)
|
||||||
|
if (packageName && ACCEPTED_PACKAGE_NAME_SET.has(packageName)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ACCEPTED_PACKAGE_NAMES.some((name) => entry.includes(name))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user