fix(install): register bundled skill paths
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs"
|
||||
import { applyEdits, modify } from "jsonc-parser"
|
||||
import type { ConfigMergeResult } from "../types"
|
||||
import { PLUGIN_NAME, LEGACY_PLUGIN_NAME } from "../../shared"
|
||||
import { PLUGIN_NAME, LEGACY_PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "../../shared"
|
||||
import { backupConfigFile } from "./backup-config"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
@@ -10,14 +11,85 @@ import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-o
|
||||
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
||||
import { checkVersionCompatibility, extractVersionFromPluginEntry } from "./version-compatibility"
|
||||
|
||||
const BUNDLED_SKILL_PATHS = [
|
||||
`./node_modules/${PUBLISHED_PACKAGE_NAME}/.agents/skills`,
|
||||
`./node_modules/${PLUGIN_NAME}/.agents/skills`,
|
||||
] as const
|
||||
|
||||
interface OpenCodeSkillsConfig {
|
||||
paths?: string[]
|
||||
urls?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function toStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.filter((item): item is string => typeof item === "string")
|
||||
}
|
||||
|
||||
function toSkillsConfig(value: unknown): OpenCodeSkillsConfig {
|
||||
if (!isRecord(value)) return {}
|
||||
|
||||
return {
|
||||
...value,
|
||||
paths: toStringArray(value.paths),
|
||||
urls: toStringArray(value.urls),
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBundledSkillPaths(config: OpenCodeConfig): void {
|
||||
const skills = toSkillsConfig(config.skills)
|
||||
const existingPaths = skills.paths ?? []
|
||||
const pathSet = new Set(existingPaths)
|
||||
const mergedPaths = [...existingPaths]
|
||||
|
||||
for (const skillPath of BUNDLED_SKILL_PATHS) {
|
||||
if (!pathSet.has(skillPath)) {
|
||||
mergedPaths.push(skillPath)
|
||||
pathSet.add(skillPath)
|
||||
}
|
||||
}
|
||||
|
||||
config.skills = {
|
||||
...skills,
|
||||
paths: mergedPaths,
|
||||
}
|
||||
}
|
||||
|
||||
function getConfiguredSkillPaths(config: OpenCodeConfig): string[] {
|
||||
return toSkillsConfig(config.skills).paths ?? [...BUNDLED_SKILL_PATHS]
|
||||
}
|
||||
|
||||
function updateJsoncField(content: string, path: (string | number)[], value: unknown): string {
|
||||
const edits = modify(content, path, value, {
|
||||
formattingOptions: {
|
||||
insertSpaces: true,
|
||||
tabSize: 2,
|
||||
eol: "\n",
|
||||
},
|
||||
})
|
||||
|
||||
return edits.length === 0 ? content : applyEdits(content, edits)
|
||||
}
|
||||
|
||||
function updateJsoncConfig(content: string, pluginEntries: string[], skillPaths: string[]): string {
|
||||
const withPlugin = updateJsoncField(content, ["plugin"], pluginEntries)
|
||||
return updateJsoncField(withPlugin, ["skills", "paths"], skillPaths)
|
||||
}
|
||||
|
||||
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
error: formatErrorWithSuggestion(error, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +99,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
||||
try {
|
||||
if (format === "none") {
|
||||
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||
ensureBundledSkillPaths(config)
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
@@ -82,30 +155,22 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
||||
normalizedPlugins.push(pluginEntry)
|
||||
|
||||
config.plugin = normalizedPlugins
|
||||
ensureBundledSkillPaths(config)
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
const pluginArrayRegex = /((?:"plugin"|plugin)\s*:\s*)\[([\s\S]*?)\]/
|
||||
const match = content.match(pluginArrayRegex)
|
||||
|
||||
if (match) {
|
||||
const formattedPlugins = normalizedPlugins.map((p) => `"${p}"`).join(",\n ")
|
||||
const newContent = content.replace(pluginArrayRegex, `$1[\n ${formattedPlugins}\n ]`)
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
const newContent = content.replace(/(\{)/, `$1\n "plugin": ["${pluginEntry}"],`)
|
||||
writeFileSync(path, newContent)
|
||||
}
|
||||
writeFileSync(path, updateJsoncConfig(content, normalizedPlugins, getConfiguredSkillPaths(config)))
|
||||
} else {
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
}
|
||||
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "update opencode config"),
|
||||
error: formatErrorWithSuggestion(error, "update opencode config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@ import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
import { resetConfigContext } from "./config-context"
|
||||
import { parseJsonc } from "../../shared"
|
||||
import { detectCurrentConfig } from "./detect-current-config"
|
||||
import { addPluginToOpenCodeConfig } from "./add-plugin-to-opencode-config"
|
||||
import * as pluginNameWithVersion from "./plugin-name-with-version"
|
||||
|
||||
const bundledSkillPaths = [
|
||||
"./node_modules/oh-my-opencode/.agents/skills",
|
||||
"./node_modules/oh-my-openagent/.agents/skills",
|
||||
] as const
|
||||
|
||||
describe("detectCurrentConfig - single package detection", () => {
|
||||
let testConfigDir = ""
|
||||
let testConfigPath = ""
|
||||
@@ -95,6 +101,54 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
||||
expect(result.success).toBe(true)
|
||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent"])
|
||||
expect(savedConfig.skills?.paths).toEqual([...bundledSkillPaths])
|
||||
})
|
||||
|
||||
it("preserves existing skill paths while registering bundled skills", async () => {
|
||||
// given
|
||||
writeFileSync(
|
||||
testConfigPath,
|
||||
JSON.stringify({
|
||||
plugin: ["other-plugin"],
|
||||
skills: {
|
||||
paths: ["./custom-skills"],
|
||||
urls: ["https://example.com/skill.md"],
|
||||
},
|
||||
}, null, 2) + "\n",
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
// when
|
||||
const result = await addPluginToOpenCodeConfig("3.11.0")
|
||||
|
||||
// then
|
||||
expect(result.success).toBe(true)
|
||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||
expect(savedConfig.plugin).toEqual(["other-plugin", "oh-my-openagent"])
|
||||
expect(savedConfig.skills.paths).toEqual(["./custom-skills", ...bundledSkillPaths])
|
||||
expect(savedConfig.skills.urls).toEqual(["https://example.com/skill.md"])
|
||||
})
|
||||
|
||||
it("does not duplicate bundled skill paths on reinstall", async () => {
|
||||
// given
|
||||
writeFileSync(
|
||||
testConfigPath,
|
||||
JSON.stringify({
|
||||
plugin: ["oh-my-openagent"],
|
||||
skills: {
|
||||
paths: [...bundledSkillPaths],
|
||||
},
|
||||
}, null, 2) + "\n",
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
// when
|
||||
const result = await addPluginToOpenCodeConfig("3.11.0")
|
||||
|
||||
// then
|
||||
expect(result.success).toBe(true)
|
||||
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
|
||||
expect(savedConfig.skills.paths).toEqual([...bundledSkillPaths])
|
||||
})
|
||||
|
||||
it("upgrades a bare legacy plugin entry to canonical", async () => {
|
||||
@@ -181,7 +235,9 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
||||
// then
|
||||
expect(result.success).toBe(true)
|
||||
const savedContent = readFileSync(testConfigPath, "utf-8")
|
||||
const savedConfig = parseJsonc<{ plugin?: string[]; skills?: { paths?: string[] } }>(savedContent)
|
||||
expect(savedContent.includes('"plugin": [\n "oh-my-openagent"\n ]')).toBe(true)
|
||||
expect(savedContent.includes("oh-my-opencode")).toBe(false)
|
||||
expect(savedConfig.plugin).toEqual(["oh-my-openagent"])
|
||||
expect(savedConfig.skills?.paths).toEqual([...bundledSkillPaths])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,10 @@ import { unsafeTestValue } from "../../test-support/unsafe-test-value"
|
||||
// Mock console methods to capture output
|
||||
const mockConsoleLog = mock(() => {})
|
||||
const mockConsoleError = mock(() => {})
|
||||
const bundledSkillPaths = [
|
||||
"./node_modules/oh-my-opencode/.agents/skills",
|
||||
"./node_modules/oh-my-openagent/.agents/skills",
|
||||
] as const
|
||||
|
||||
describe("install CLI - binary check behavior", () => {
|
||||
let tempDir: string
|
||||
@@ -121,6 +125,7 @@ describe("install CLI - binary check behavior", () => {
|
||||
expect(config.plugin).toBeDefined()
|
||||
expect(config.plugin.some((p: string) => p.includes("oh-my-openagent"))).toBe(true)
|
||||
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(false)
|
||||
expect(config.skills?.paths).toEqual([...bundledSkillPaths])
|
||||
|
||||
// then exit code should be 0 (success)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
Reference in New Issue
Block a user