feat(ce-update): add plugin version check skill and ce_platforms filtering (#532)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trevin Chow
2026-04-08 00:09:02 -07:00
committed by GitHub
parent 0ae91dcc29
commit d37f0ed16f
17 changed files with 163 additions and 38 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
}))

View File

@@ -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<string>()
const usedCommandNames = new Set<string>()
const skillDirs = plugin.skills.map((skill) => ({
const platformSkills = filterSkillsByPlatform(plugin.skills, "gemini")
const skillDirs = platformSkills.map((skill) => ({
name: skill.name,
sourceDir: skill.sourceDir,
}))

View File

@@ -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<string>()
// 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,
}))

View File

@@ -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)

View File

@@ -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 })),
}
}

View File

@@ -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<string>()
const usedSkillNames = new Set<string>(plugin.skills.map((skill) => normalizeName(skill.name)))
const usedSkillNames = new Set<string>(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,
})),

View File

@@ -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("")

View File

@@ -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,
}))

View File

@@ -107,11 +107,13 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
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,
})

View File

@@ -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