fix(install): register bundled skill paths
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { readFileSync, writeFileSync } from "node:fs"
|
import { readFileSync, writeFileSync } from "node:fs"
|
||||||
|
import { applyEdits, modify } from "jsonc-parser"
|
||||||
import type { ConfigMergeResult } from "../types"
|
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 { 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"
|
||||||
@@ -10,14 +11,85 @@ import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-o
|
|||||||
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
||||||
import { checkVersionCompatibility, extractVersionFromPluginEntry } from "./version-compatibility"
|
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> {
|
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||||
try {
|
try {
|
||||||
ensureConfigDirectoryExists()
|
ensureConfigDirectoryExists()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error(String(err))
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
configPath: getConfigDir(),
|
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 {
|
try {
|
||||||
if (format === "none") {
|
if (format === "none") {
|
||||||
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||||
|
ensureBundledSkillPaths(config)
|
||||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||||
return { success: true, configPath: path }
|
return { success: true, configPath: path }
|
||||||
}
|
}
|
||||||
@@ -82,30 +155,22 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
|||||||
normalizedPlugins.push(pluginEntry)
|
normalizedPlugins.push(pluginEntry)
|
||||||
|
|
||||||
config.plugin = normalizedPlugins
|
config.plugin = normalizedPlugins
|
||||||
|
ensureBundledSkillPaths(config)
|
||||||
|
|
||||||
if (format === "jsonc") {
|
if (format === "jsonc") {
|
||||||
const content = readFileSync(path, "utf-8")
|
const content = readFileSync(path, "utf-8")
|
||||||
const pluginArrayRegex = /((?:"plugin"|plugin)\s*:\s*)\[([\s\S]*?)\]/
|
writeFileSync(path, updateJsoncConfig(content, normalizedPlugins, getConfiguredSkillPaths(config)))
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, configPath: path }
|
return { success: true, configPath: path }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const error = err instanceof Error ? err : new Error(String(err))
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
configPath: path,
|
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 { join } from "node:path"
|
||||||
|
|
||||||
import { resetConfigContext } from "./config-context"
|
import { resetConfigContext } from "./config-context"
|
||||||
|
import { parseJsonc } from "../../shared"
|
||||||
import { detectCurrentConfig } from "./detect-current-config"
|
import { detectCurrentConfig } from "./detect-current-config"
|
||||||
import { addPluginToOpenCodeConfig } from "./add-plugin-to-opencode-config"
|
import { addPluginToOpenCodeConfig } from "./add-plugin-to-opencode-config"
|
||||||
import * as pluginNameWithVersion from "./plugin-name-with-version"
|
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", () => {
|
describe("detectCurrentConfig - single package detection", () => {
|
||||||
let testConfigDir = ""
|
let testConfigDir = ""
|
||||||
let testConfigPath = ""
|
let testConfigPath = ""
|
||||||
@@ -95,6 +101,54 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
|
|||||||
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).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 () => {
|
it("upgrades a bare legacy plugin entry to canonical", async () => {
|
||||||
@@ -181,7 +235,9 @@ 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")
|
||||||
|
const savedConfig = parseJsonc<{ plugin?: string[]; skills?: { paths?: string[] } }>(savedContent)
|
||||||
expect(savedContent.includes('"plugin": [\n "oh-my-openagent"\n ]')).toBe(true)
|
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
|
// Mock console methods to capture output
|
||||||
const mockConsoleLog = mock(() => {})
|
const mockConsoleLog = mock(() => {})
|
||||||
const mockConsoleError = 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", () => {
|
describe("install CLI - binary check behavior", () => {
|
||||||
let tempDir: string
|
let tempDir: string
|
||||||
@@ -121,6 +125,7 @@ describe("install CLI - binary check behavior", () => {
|
|||||||
expect(config.plugin).toBeDefined()
|
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-openagent"))).toBe(true)
|
||||||
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(false)
|
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)
|
// then exit code should be 0 (success)
|
||||||
expect(exitCode).toBe(0)
|
expect(exitCode).toBe(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user