From 1d68eec4bdd0912054ccb9c3f101fe08d600b293 Mon Sep 17 00:00:00 2001 From: "zelin.cai" Date: Sat, 11 Apr 2026 23:34:43 +0800 Subject: [PATCH 1/2] fix(config): support legacy skills array --- packages/opencode/src/config/config.ts | 16 +++++- packages/opencode/src/skill/index.ts | 55 +++++++++++++++----- packages/opencode/test/config/config.test.ts | 35 +++++++++++++ packages/opencode/test/skill/skill.test.ts | 33 ++++++++++++ 4 files changed, 126 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ff79b739fe10..823ec0af9ae6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -522,6 +522,17 @@ export namespace Config { }) export type Skills = z.infer + export const LegacySkill = z + .object({ + name: z.string(), + description: z.string(), + command: z.string(), + }) + .catchall(z.unknown()) + export const LegacySkills = z.array(LegacySkill) + export type LegacySkill = z.infer + export type LegacySkills = z.infer + export const Agent = z .object({ model: ModelId.optional(), @@ -916,7 +927,10 @@ export namespace Config { .record(z.string(), Command) .optional() .describe("Command configuration, see https://opencode.ai/docs/commands"), - skills: Skills.optional().describe("Additional skill folder paths"), + skills: z + .union([Skills, LegacySkills]) + .optional() + .describe("Additional skill folder paths, remote skill URLs, or legacy inline skills"), watcher: z .object({ ignore: z.array(z.string()).optional(), diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index cde36dd52d07..6742b913a531 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -165,25 +165,56 @@ export namespace Skill { } const cfg = yield* config.get() - for (const item of cfg.skills?.paths ?? []) { - const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item - const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded) - if (!(yield* fsys.isDir(dir))) { - log.warn("skill path not found", { path: dir }) - continue + if (Array.isArray(cfg.skills)) { + for (const item of cfg.skills) { + if (state.skills[item.name]) { + log.warn("duplicate skill name", { + name: item.name, + existing: state.skills[item.name].location, + duplicate: path.join(directory, "opencode.json"), + }) + } + + state.skills[item.name] = { + name: item.name, + description: item.description, + location: path.join(directory, "opencode.json"), + content: [ + "# Legacy Inline Skill", + "", + "This skill was loaded from the legacy `skills` array in `opencode.json`.", + "", + "```bash", + item.command, + "```", + ].join("\n"), + } } - - yield* scan(state, bus, dir, SKILL_PATTERN) } - for (const url of cfg.skills?.urls ?? []) { - const pulledDirs = yield* discovery.pull(url) - for (const dir of pulledDirs) { - state.dirs.add(dir) + if (!Array.isArray(cfg.skills)) { + for (const item of cfg.skills?.paths ?? []) { + const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item + const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded) + if (!(yield* fsys.isDir(dir))) { + log.warn("skill path not found", { path: dir }) + continue + } + yield* scan(state, bus, dir, SKILL_PATTERN) } } + if (!Array.isArray(cfg.skills)) { + for (const url of cfg.skills?.urls ?? []) { + const pulledDirs = yield* discovery.pull(url) + for (const dir of pulledDirs) { + state.dirs.add(dir) + yield* scan(state, bus, dir, SKILL_PATTERN) + } + } + } + log.info("init", { count: Object.keys(state.skills).length }) }) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 0ac61aee7172..3c4357131efa 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -491,6 +491,41 @@ test("handles command configuration", async () => { }) }) +test("accepts legacy skills array in opencode.json", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Filesystem.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: [ + { + name: "test", + description: "Test skill", + command: "echo test", + }, + ], + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(Array.isArray(config.skills)).toBe(true) + expect(config.skills).toEqual([ + { + name: "test", + description: "Test skill", + command: "echo test", + }, + ]) + }, + }) +}) + test("migrates autoshare to share field", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 12e16f86a1a3..7fb2800ead96 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -224,6 +224,39 @@ test("returns empty array when no skills exist", async () => { }) }) +test("loads legacy inline skills from opencode.json", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + skills: [ + { + name: "test", + description: "Test skill", + command: "echo test", + }, + ], + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const skills = await Skill.all() + expect(skills.length).toBe(1) + expect(skills[0].name).toBe("test") + expect(skills[0].description).toBe("Test skill") + expect(skills[0].location).toBe(path.join(tmp.path, "opencode.json")) + expect(skills[0].content).toContain("echo test") + }, + }) +}) + test("discovers skills from .agents/skills/ directory", async () => { await using tmp = await tmpdir({ git: true, From 046e438e6a9bc0874eaca8cff09d8cee4ba0af51 Mon Sep 17 00:00:00 2001 From: "zelin.cai" Date: Tue, 14 Apr 2026 15:49:16 +0800 Subject: [PATCH 2/2] Fix config test after config service migration --- packages/opencode/test/config/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 795e6511fd5b..4669caf17c10 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -536,7 +536,7 @@ test("accepts legacy skills array in opencode.json", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const config = await Config.get() + const config = await load() expect(Array.isArray(config.skills)).toBe(true) expect(config.skills).toEqual([ {