From d37f0ed16f94aaec2a7b435a0aaa018de5631ed3 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Wed, 8 Apr 2026 00:09:02 -0700 Subject: [PATCH] feat(ce-update): add plugin version check skill and ce_platforms filtering (#532) Co-authored-by: Claude Opus 4.6 (1M context) --- plugins/compound-engineering/AGENTS.md | 2 +- plugins/compound-engineering/README.md | 1 + .../skills/ce-update/SKILL.md | 69 +++++++++++++++++++ src/converters/claude-to-codex.ts | 11 +-- src/converters/claude-to-copilot.ts | 4 +- src/converters/claude-to-droid.ts | 4 +- src/converters/claude-to-gemini.ts | 5 +- src/converters/claude-to-kiro.ts | 4 +- src/converters/claude-to-openclaw.ts | 16 +++-- src/converters/claude-to-opencode.ts | 15 ++-- src/converters/claude-to-pi.ts | 7 +- src/converters/claude-to-qwen.ts | 10 +-- src/converters/claude-to-windsurf.ts | 4 +- src/parsers/claude.ts | 2 + src/types/claude.ts | 9 +++ tests/claude-parser.test.ts | 31 ++++++++- .../skills/claude-only-skill/SKILL.md | 7 ++ 17 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 plugins/compound-engineering/skills/ce-update/SKILL.md create mode 100644 tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md diff --git a/plugins/compound-engineering/AGENTS.md b/plugins/compound-engineering/AGENTS.md index 78c749b..da5309e 100644 --- a/plugins/compound-engineering/AGENTS.md +++ b/plugins/compound-engineering/AGENTS.md @@ -136,7 +136,7 @@ Why: shell-heavy exploration causes avoidable permission prompts in sub-agent wo - [ ] Never instruct agents to use `find`, `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `wc`, or `tree` through a shell for routine file discovery, content search, or file reading - [ ] Describe tools by capability class with platform hints — e.g., "Use the native file-search/glob tool (e.g., Glob in Claude Code)" — not by Claude Code-specific tool names alone -- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no chaining (`&&`, `||`, `;`) and no error suppression (`2>/dev/null`, `|| true`). Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures +- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no chaining (`&&`, `||`, `;`) and no error suppression (`2>/dev/null`, `|| true`). Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures. **Exception:** `!` backtick pre-resolution commands may use error suppression and `||` fallbacks to produce clean sentinel values (e.g., `|| echo '__FAILED__'`), since these run at load time and the model needs a parseable signal, not raw stderr - [ ] Do not encode shell recipes for routine exploration when native tools can do the job; encode intent and preferred tool classes instead - [ ] For shell-only workflows (e.g., `gh`, `git`, `bundle show`, project CLIs), explicit command examples are acceptable when they are simple, task-scoped, and not chained together diff --git a/plugins/compound-engineering/README.md b/plugins/compound-engineering/README.md index a2511a6..b53a16f 100644 --- a/plugins/compound-engineering/README.md +++ b/plugins/compound-engineering/README.md @@ -47,6 +47,7 @@ The primary entry points for engineering work, invoked as slash commands: | `/test-browser` | Run browser tests on PR-affected pages | | `/test-xcode` | Build and test iOS apps on simulator using XcodeBuildMCP | | `/onboarding` | Generate `ONBOARDING.md` to help new contributors understand the codebase | +| `/ce-update` | Check compound-engineering plugin version and fix stale cache (Claude Code only) | | `/todo-resolve` | Resolve todos in parallel | | `/todo-triage` | Triage and prioritize pending todos | diff --git a/plugins/compound-engineering/skills/ce-update/SKILL.md b/plugins/compound-engineering/skills/ce-update/SKILL.md new file mode 100644 index 0000000..05746af --- /dev/null +++ b/plugins/compound-engineering/skills/ce-update/SKILL.md @@ -0,0 +1,69 @@ +--- +name: ce-update +description: | + Check if the compound-engineering plugin is up to date and fix stale cache if not. + Use when the user says "update compound engineering", "check compound engineering version", + "ce update", "is compound engineering up to date", "update ce plugin", or reports issues + that might stem from a stale compound-engineering plugin version. This skill only works + in Claude Code — it relies on the plugin harness cache layout. +disable-model-invocation: true +ce_platforms: [claude] +--- + +# Check & Fix Plugin Version + +Verify the installed compound-engineering plugin version matches the latest released +version, and fix stale marketplace/cache state if it doesn't. Claude Code only. + +## Pre-resolved context + +The three sections below contain pre-resolved data. Only the **Plugin root +path** determines whether this session is Claude Code — if it contains an error +sentinel, an empty value, or a literal `${CLAUDE_PLUGIN_ROOT}` string, tell the +user this skill only works in Claude Code and stop. The other two sections may +contain error sentinels even in valid Claude Code sessions; the decision logic +below handles those cases. + +**Plugin root path:** +!`echo "${CLAUDE_PLUGIN_ROOT}" 2>/dev/null || echo '__CE_UPDATE_ROOT_FAILED__'` + +**Latest released version:** +!`gh release list --repo Everyinc/compound-engineering-plugin --limit 30 --json tagName --jq '[.[] | select(.tagName | startswith("compound-engineering-v"))][0].tagName | sub("compound-engineering-v";"")' 2>/dev/null || echo '__CE_UPDATE_VERSION_FAILED__'` + +**Cached version folder(s):** +!`ls "${CLAUDE_PLUGIN_ROOT}/cache/every-marketplace/compound-engineering/" 2>/dev/null || echo '__CE_UPDATE_CACHE_FAILED__'` + +## Decision logic + +### 1. Platform gate + +If **Plugin root path** contains `__CE_UPDATE_ROOT_FAILED__`, a literal +`${CLAUDE_PLUGIN_ROOT}` string, or is empty: tell the user this skill requires Claude Code +and stop. No further action. + +### 2. Compare versions + +If **Latest released version** contains `__CE_UPDATE_VERSION_FAILED__`: tell the user the +latest release could not be fetched (gh may be unavailable or rate-limited) and stop. + +If **Cached version folder(s)** contains `__CE_UPDATE_CACHE_FAILED__`: no marketplace cache +exists. Tell the user: "No marketplace cache found — this appears to be a local dev checkout +or fresh install." and stop. + +Take the **latest released version** and the **cached folder list**. + +**Up to date** — exactly one cached folder exists AND its name matches the latest version: +- Tell the user: "compound-engineering **v{version}** is installed and up to date." + +**Out of date or corrupted** — multiple cached folders exist, OR the single folder name +does not match the latest version. Use the **Plugin root path** value from above to +construct the delete path. + +**Clear the stale cache:** +```bash +rm -rf "/cache/every-marketplace/compound-engineering" +``` + +Tell the user: +- "compound-engineering was on **v{old}** but **v{latest}** is available." +- "Cleared the plugin cache. Now run `/plugin marketplace update` in this session, then restart Claude Code to pick up v{latest}." diff --git a/src/converters/claude-to-codex.ts b/src/converters/claude-to-codex.ts index 238ca19..5f7d90c 100644 --- a/src/converters/claude-to-codex.ts +++ b/src/converters/claude-to-codex.ts @@ -1,5 +1,5 @@ import { formatFrontmatter } from "../utils/frontmatter" -import type { ClaudeAgent, ClaudeCommand, ClaudePlugin, ClaudeSkill } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, type ClaudeSkill, filterSkillsByPlatform } from "../types/claude" import type { CodexBundle, CodexGeneratedSkill } from "../types/codex" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" import { @@ -16,17 +16,18 @@ export function convertClaudeToCodex( plugin: ClaudePlugin, _options: ClaudeToCodexOptions, ): CodexBundle { + const platformSkills = filterSkillsByPlatform(plugin.skills, "codex") const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation) const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin) const canonicalWorkflowSkills = applyCompoundWorkflowModel - ? plugin.skills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name)) + ? platformSkills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name)) : [] const deprecatedWorkflowAliases = applyCompoundWorkflowModel - ? plugin.skills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name)) + ? platformSkills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name)) : [] const copiedSkills = applyCompoundWorkflowModel - ? plugin.skills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name)) - : plugin.skills + ? platformSkills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name)) + : platformSkills const skillDirs = copiedSkills.map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, diff --git a/src/converters/claude-to-copilot.ts b/src/converters/claude-to-copilot.ts index 594f3a2..4631415 100644 --- a/src/converters/claude-to-copilot.ts +++ b/src/converters/claude-to-copilot.ts @@ -1,6 +1,6 @@ import { formatFrontmatter } from "../utils/frontmatter" import { sanitizePathName } from "../utils/files" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { CopilotAgent, CopilotBundle, @@ -23,7 +23,7 @@ export function convertClaudeToCopilot( const agents = plugin.agents.map((agent) => convertAgent(agent, usedAgentNames)) // Reserve sanitized skill names so generated skills (from commands) don't collide on disk - const skillDirs = plugin.skills.map((skill) => { + const skillDirs = filterSkillsByPlatform(plugin.skills, "copilot").map((skill) => { usedSkillNames.add(sanitizePathName(skill.name)) return { name: skill.name, diff --git a/src/converters/claude-to-droid.ts b/src/converters/claude-to-droid.ts index 43fd41f..a912a9c 100644 --- a/src/converters/claude-to-droid.ts +++ b/src/converters/claude-to-droid.ts @@ -1,5 +1,5 @@ import { formatFrontmatter } from "../utils/frontmatter" -import type { ClaudeAgent, ClaudeCommand, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { DroidBundle, DroidCommandFile, DroidAgentFile } from "../types/droid" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" @@ -45,7 +45,7 @@ export function convertClaudeToDroid( ): DroidBundle { const commands = plugin.commands.map((command) => convertCommand(command)) const droids = plugin.agents.map((agent) => convertAgent(agent)) - const skillDirs = plugin.skills.map((skill) => ({ + const skillDirs = filterSkillsByPlatform(plugin.skills, "droid").map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, })) diff --git a/src/converters/claude-to-gemini.ts b/src/converters/claude-to-gemini.ts index 561cfd4..9e933d1 100644 --- a/src/converters/claude-to-gemini.ts +++ b/src/converters/claude-to-gemini.ts @@ -1,5 +1,5 @@ import { formatFrontmatter } from "../utils/frontmatter" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { GeminiBundle, GeminiCommand, GeminiMcpServer, GeminiSkill } from "../types/gemini" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" @@ -14,7 +14,8 @@ export function convertClaudeToGemini( const usedSkillNames = new Set() const usedCommandNames = new Set() - const skillDirs = plugin.skills.map((skill) => ({ + const platformSkills = filterSkillsByPlatform(plugin.skills, "gemini") + const skillDirs = platformSkills.map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, })) diff --git a/src/converters/claude-to-kiro.ts b/src/converters/claude-to-kiro.ts index 3e8d622..8c160cd 100644 --- a/src/converters/claude-to-kiro.ts +++ b/src/converters/claude-to-kiro.ts @@ -1,7 +1,7 @@ import { readFileSync, existsSync } from "fs" import path from "path" import { formatFrontmatter } from "../utils/frontmatter" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { KiroAgent, KiroAgentConfig, @@ -36,7 +36,7 @@ export function convertClaudeToKiro( const usedSkillNames = new Set() // Pass-through skills are processed first — they're the source of truth - const skillDirs = plugin.skills.map((skill) => ({ + const skillDirs = filterSkillsByPlatform(plugin.skills, "kiro").map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, })) diff --git a/src/converters/claude-to-openclaw.ts b/src/converters/claude-to-openclaw.ts index 0143564..a74ddf3 100644 --- a/src/converters/claude-to-openclaw.ts +++ b/src/converters/claude-to-openclaw.ts @@ -1,11 +1,12 @@ import { formatFrontmatter } from "../utils/frontmatter" import { normalizeModelWithProvider } from "../utils/model" import { sanitizePathName } from "../utils/files" -import type { - ClaudeAgent, - ClaudeCommand, - ClaudePlugin, - ClaudeMcpServer, +import { + type ClaudeAgent, + type ClaudeCommand, + type ClaudePlugin, + type ClaudeMcpServer, + filterSkillsByPlatform, } from "../types/claude" import type { OpenClawBundle, @@ -29,7 +30,8 @@ export function convertClaudeToOpenClaw( const skills: OpenClawSkillFile[] = [...agentSkills, ...commandSkills] - const skillDirCopies = plugin.skills.map((skill) => ({ + const platformSkills = filterSkillsByPlatform(plugin.skills, "openclaw") + const skillDirCopies = platformSkills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name, })) @@ -37,7 +39,7 @@ export function convertClaudeToOpenClaw( const allSkillDirs = [ ...agentSkills.map((s) => sanitizePathName(s.dir)), ...commandSkills.map((s) => sanitizePathName(s.dir)), - ...plugin.skills.map((s) => sanitizePathName(s.name)), + ...platformSkills.map((s) => sanitizePathName(s.name)), ] const manifest = buildManifest(plugin, allSkillDirs) diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index e8aa61a..09646ae 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -1,11 +1,12 @@ import { formatFrontmatter } from "../utils/frontmatter" import { normalizeModelWithProvider } from "../utils/model" -import type { - ClaudeAgent, - ClaudeCommand, - ClaudeHooks, - ClaudePlugin, - ClaudeMcpServer, +import { + type ClaudeAgent, + type ClaudeCommand, + type ClaudeHooks, + type ClaudePlugin, + type ClaudeMcpServer, + filterSkillsByPlatform, } from "../types/claude" import type { OpenCodeBundle, @@ -83,7 +84,7 @@ export function convertClaudeToOpenCode( agents: agentFiles, commandFiles: cmdFiles, plugins, - skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })), + skillDirs: filterSkillsByPlatform(plugin.skills, "opencode").map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })), } } diff --git a/src/converters/claude-to-pi.ts b/src/converters/claude-to-pi.ts index 9225990..fa02da3 100644 --- a/src/converters/claude-to-pi.ts +++ b/src/converters/claude-to-pi.ts @@ -1,5 +1,5 @@ import { formatFrontmatter } from "../utils/frontmatter" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { PiBundle, PiGeneratedSkill, @@ -17,8 +17,9 @@ export function convertClaudeToPi( plugin: ClaudePlugin, _options: ClaudeToPiOptions, ): PiBundle { + const platformSkills = filterSkillsByPlatform(plugin.skills, "pi") const promptNames = new Set() - const usedSkillNames = new Set(plugin.skills.map((skill) => normalizeName(skill.name))) + const usedSkillNames = new Set(platformSkills.map((skill) => normalizeName(skill.name))) const prompts = plugin.commands .filter((command) => !command.disableModelInvocation) @@ -35,7 +36,7 @@ export function convertClaudeToPi( return { prompts, - skillDirs: plugin.skills.map((skill) => ({ + skillDirs: platformSkills.map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, })), diff --git a/src/converters/claude-to-qwen.ts b/src/converters/claude-to-qwen.ts index 204e424..3723468 100644 --- a/src/converters/claude-to-qwen.ts +++ b/src/converters/claude-to-qwen.ts @@ -1,6 +1,6 @@ import { formatFrontmatter } from "../utils/frontmatter" import { normalizeModelWithProvider } from "../utils/model" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { QwenAgentFile, QwenBundle, @@ -16,6 +16,7 @@ export type ClaudeToQwenOptions = { } export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenOptions): QwenBundle { + const platformSkills = filterSkillsByPlatform(plugin.skills, "qwen") const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options)) const cmdFiles = convertCommands(plugin.commands) const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined @@ -43,7 +44,7 @@ export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenO config, agents: agentFiles, commandFiles: cmdFiles, - skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })), + skillDirs: platformSkills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })), contextFile, } } @@ -181,10 +182,11 @@ function generateContextFile(plugin: ClaudePlugin): string { } // Skills section - if (plugin.skills.length > 0) { + const qwenSkills = filterSkillsByPlatform(plugin.skills, "qwen") + if (qwenSkills.length > 0) { sections.push("## Skills") sections.push("") - for (const skill of plugin.skills) { + for (const skill of qwenSkills) { sections.push(`- ${skill.name}`) } sections.push("") diff --git a/src/converters/claude-to-windsurf.ts b/src/converters/claude-to-windsurf.ts index 347b010..7bba313 100644 --- a/src/converters/claude-to-windsurf.ts +++ b/src/converters/claude-to-windsurf.ts @@ -1,7 +1,7 @@ import { formatFrontmatter } from "../utils/frontmatter" import { sanitizePathName } from "../utils/files" import { findServersWithPotentialSecrets } from "../utils/secrets" -import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { WindsurfBundle, WindsurfGeneratedSkill, WindsurfMcpConfig, WindsurfMcpServerEntry, WindsurfWorkflow } from "../types/windsurf" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" @@ -16,7 +16,7 @@ export function convertClaudeToWindsurf( const knownAgentNames = plugin.agents.map((a) => normalizeName(a.name)) // Pass-through skills (collected first so agent skill names can deduplicate against them) - const skillDirs = plugin.skills.map((skill) => ({ + const skillDirs = filterSkillsByPlatform(plugin.skills, "windsurf").map((skill) => ({ name: skill.name, sourceDir: skill.sourceDir, })) diff --git a/src/parsers/claude.ts b/src/parsers/claude.ts index a28a394..fbe15f3 100644 --- a/src/parsers/claude.ts +++ b/src/parsers/claude.ts @@ -107,11 +107,13 @@ async function loadSkills(skillsDirs: string[]): Promise { const { data } = parseFrontmatter(raw, file) const name = (data.name as string) ?? path.basename(path.dirname(file)) const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined + const ce_platforms = Array.isArray(data.ce_platforms) ? (data.ce_platforms as string[]) : undefined skills.push({ name, description: data.description as string | undefined, argumentHint: data["argument-hint"] as string | undefined, disableModelInvocation, + ce_platforms, sourceDir: path.dirname(file), skillPath: file, }) diff --git a/src/types/claude.ts b/src/types/claude.ts index 9e00f7f..c982041 100644 --- a/src/types/claude.ts +++ b/src/types/claude.ts @@ -49,10 +49,19 @@ export type ClaudeSkill = { description?: string argumentHint?: string disableModelInvocation?: boolean + ce_platforms?: string[] sourceDir: string skillPath: string } +/** + * Filter skills to those available on a given platform. + * Skills without a `platforms` field are available everywhere. + */ +export function filterSkillsByPlatform(skills: ClaudeSkill[], platform: string): ClaudeSkill[] { + return skills.filter((skill) => !skill.ce_platforms || skill.ce_platforms.includes(platform)) +} + export type ClaudePlugin = { root: string manifest: ClaudeManifest diff --git a/tests/claude-parser.test.ts b/tests/claude-parser.test.ts index fe2f348..d89ebcd 100644 --- a/tests/claude-parser.test.ts +++ b/tests/claude-parser.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { loadClaudePlugin } from "../src/parsers/claude" +import { filterSkillsByPlatform } from "../src/types/claude" const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin") const mcpFixtureRoot = path.join(import.meta.dir, "fixtures", "mcp-file") @@ -16,7 +17,7 @@ describe("loadClaudePlugin", () => { expect(plugin.manifest.name).toBe("compound-engineering") expect(plugin.agents.length).toBe(2) expect(plugin.commands.length).toBe(7) - expect(plugin.skills.length).toBe(2) + expect(plugin.skills.length).toBe(3) expect(plugin.hooks).toBeDefined() expect(plugin.mcpServers).toBeDefined() @@ -66,6 +67,34 @@ describe("loadClaudePlugin", () => { expect(normalCommand?.disableModelInvocation).toBeUndefined() }) + test("parses ce_platforms from skills", async () => { + const plugin = await loadClaudePlugin(fixtureRoot) + + const claudeOnly = plugin.skills.find((skill) => skill.name === "claude-only-skill") + expect(claudeOnly).toBeDefined() + expect(claudeOnly?.ce_platforms).toEqual(["claude"]) + + const normalSkill = plugin.skills.find((skill) => skill.name === "skill-one") + expect(normalSkill?.ce_platforms).toBeUndefined() + }) + + test("filterSkillsByPlatform includes skills without platforms field", async () => { + const plugin = await loadClaudePlugin(fixtureRoot) + const codexSkills = filterSkillsByPlatform(plugin.skills, "codex") + + expect(codexSkills.find((s) => s.name === "skill-one")).toBeDefined() + expect(codexSkills.find((s) => s.name === "disabled-skill")).toBeDefined() + expect(codexSkills.find((s) => s.name === "claude-only-skill")).toBeUndefined() + }) + + test("filterSkillsByPlatform includes skills matching the platform", async () => { + const plugin = await loadClaudePlugin(fixtureRoot) + const claudeSkills = filterSkillsByPlatform(plugin.skills, "claude") + + expect(claudeSkills.find((s) => s.name === "skill-one")).toBeDefined() + expect(claudeSkills.find((s) => s.name === "claude-only-skill")).toBeDefined() + }) + test("parses disable-model-invocation from skills", async () => { const plugin = await loadClaudePlugin(fixtureRoot) diff --git a/tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md b/tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md new file mode 100644 index 0000000..016bb92 --- /dev/null +++ b/tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md @@ -0,0 +1,7 @@ +--- +name: claude-only-skill +description: A skill restricted to Claude Code only +ce_platforms: [claude] +--- + +Claude-only skill body.