import type { CodexBundle } from "../types/codex" import type { CopilotBundle } from "../types/copilot" import type { DroidBundle } from "../types/droid" import type { ClaudePlugin } from "../types/claude" import type { GeminiBundle } from "../types/gemini" import type { KiroBundle } from "../types/kiro" import type { OpenCodeBundle } from "../types/opencode" import type { PiBundle } from "../types/pi" import { sanitizePathName } from "../utils/files" import { normalizeCodexName } from "../utils/codex-content" type LegacyPluginArtifacts = { skills?: string[] agents?: string[] commands?: string[] } const EXTRA_LEGACY_ARTIFACTS_BY_PLUGIN: Record = { "compound-engineering": { // Historical CE artifacts derived from git history. Keep these explicit so // cleanup can remove stale flat installs without touching unrelated skills. skills: [ "agent-browser", "agent-native-architecture", "agent-native-audit", "andrew-kane-gem-writer", "brainstorming", "ce-andrew-kane-gem-writer", "ce-changelog", "ce-deploy-docs", "ce-dspy-ruby", "ce-every-style-editor", "ce-onboarding", "ce:brainstorm", "ce:compound", "ce:compound-refresh", "ce:ideate", "ce:plan", "ce:plan-beta", "ce:polish-beta", "ce:release-notes", "ce:review", "ce:review-beta", "ce:work", "ce:work-beta", "ce-audit", "ce-claude-permissions-optimizer", "ce-design", "ce-doctor", "ce-document-review", "ce-feature-video", "ce-orchestrating-swarms", "ce-plan-beta", "ce-pr-stack", "ce-reproduce-bug", "ce-review", "ce-review-beta", "ce-update", "changelog", "claude-permissions-optimizer", "compound-docs", "compound-foundations", "create-agent-skill", "create-agent-skills", "creating-agent-skills", "deepen-plan", "deepen-plan-beta", "demo-reel", "deploy-docs", "dhh-rails-style", "dhh-ruby-style", "doctor", "document-review", "dspy-ruby", "every-style-editor", "evidence-capture", "feature-video", "file-todos", "frontend-design", "gemini-imagegen", "generate_command", "git-clean-gone-branches", "git-commit", "git-commit-push-pr", "git-stack", "git-worktree", "heal-skill", "onboarding", "orchestrating-swarms", "pr-resolve-feedback", "proof", "proofread", "rclone", "report-bug", "report-bug-ce", "reproduce-bug", "resolve-pr-feedback", "resolve-pr-parallel", "resolve-todo-parallel", "resolve_parallel", "resolve_pr_parallel", "resolve_todo_parallel", "setup", "skill-creator", "slfg", "test-browser", "test-xcode", "todo-create", "todo-resolve", "todo-triage", "triage", "workflows-brainstorm", "workflows:brainstorm", "workflows-compound", "workflows:compound", "workflows-plan", "workflows:plan", "workflows-review", "workflows:review", "workflows-work", "workflows:work", ], agents: [ "adversarial-document-reviewer", "adversarial-reviewer", "agent-native-reviewer", "ankane-readme-writer", "api-contract-reviewer", "architecture-strategist", "best-practices-researcher", "bug-reproduction-validator", "ce-bug-reproduction-validator", "ce-lint", "cli-agent-readiness-reviewer", "cli-readiness-reviewer", "code-simplicity-reviewer", "coherence-reviewer", "correctness-reviewer", "data-integrity-guardian", "data-migration-expert", "data-migrations-reviewer", "deployment-verification-agent", "design-implementation-reviewer", "design-iterator", "design-lens-reviewer", "dhh-rails-reviewer", "every-style-editor", "feasibility-reviewer", "figma-design-sync", "framework-docs-researcher", "git-history-analyzer", "issue-intelligence-analyst", "julik-frontend-races-reviewer", "kieran-python-reviewer", "kieran-rails-reviewer", "kieran-typescript-reviewer", "learnings-researcher", "lint", "maintainability-reviewer", "pattern-recognition-specialist", "performance-oracle", "performance-reviewer", "pr-comment-resolver", "pr-reviewability-analyst", "previous-comments-reviewer", "product-lens-reviewer", "project-standards-reviewer", "reliability-reviewer", "repo-research-analyst", "schema-drift-detector", "scope-guardian-reviewer", "security-lens-reviewer", "security-reviewer", "security-sentinel", "session-historian", "session-history-researcher", "slack-researcher", "spec-flow-analyzer", "testing-reviewer", "web-researcher", ], commands: [ "agent-native-audit", "build-website", "ce:brainstorm", "ce:compound", "ce:plan", "ce:review", "ce:work", "changelog", "codify", "compound", "compound:codify", "compound:plan", "compound:review", "compound:work", "create-agent-skill", "deepen-plan", "deprecated:deepen-plan", "deprecated:plan-review", "deprecated:workflows-plan", "deploy-docs", "feature-video", "generate_command", "heal-skill", "lfg", "plan", "plan_review", "playwright-test", "prime", "release-docs", "report-bug", "reproduce-bug", "review", "resolve_parallel", "resolve_pr_parallel", "resolve_todo_parallel", "setup", "slfg", "swarm-status", "technical_review", "test-browser", "test-xcode", "triage", "work", "workflows:brainstorm", "workflows:codify", "workflows:compound", "workflows:plan", "workflows:review", "workflows:work", "xcode-test", ], }, } export type LegacyTargetArtifacts = { skills: string[] prompts: string[] } export type LegacyTargetFileArtifacts = { skills: string[] agents: string[] commands: string[] } export type LegacyDroidArtifacts = { skills: string[] commands: string[] droids: string[] } export type LegacyOpenCodeArtifacts = { skills: string[] commands: string[] agents: string[] } export type LegacyKiroArtifacts = { skills: string[] agents: string[] } export type LegacyCopilotArtifacts = { skills: string[] agents: string[] } export type LegacyWindsurfArtifacts = { skills: string[] workflows: string[] } export function getLegacyPluginArtifacts(pluginName?: string): LegacyPluginArtifacts { if (!pluginName) return {} return EXTRA_LEGACY_ARTIFACTS_BY_PLUGIN[pluginName] ?? {} } export function getLegacyCodexArtifacts(bundle: CodexBundle): LegacyTargetArtifacts { // IMPORTANT: legacy detection for the flat `~/.codex/skills/` and // `~/.codex/prompts/.md` paths must be driven exclusively by the // explicit historical allow-list in `EXTRA_LEGACY_ARTIFACTS_BY_PLUGIN`. // // Earlier versions of this function also seeded candidates from the current // plugin bundle (`bundle.skillDirs`, `bundle.generatedSkills`, `bundle.agents`). // That was unsafe: on a first install, any user-authored skill at a flat // `~/.codex/skills/` path that happened to share a name with a current // CE skill or agent would be swept into `compound-engineering/legacy-backup` // even though it was never part of CE. // // The historical allow-list already enumerates every skill/agent/command name // CE has ever shipped (including names that are still current), so restricting // detection to that list still cleans up real legacy installs without // touching unrelated user skills. const skills = new Set() const prompts = new Set() const currentPromptFiles = new Set() for (const prompt of bundle.prompts) { currentPromptFiles.add(`${sanitizePathName(prompt.name)}.md`) } const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { includeRawColon: true }) } for (const name of extras.agents ?? []) { skills.add(normalizeCodexName(name)) } for (const name of extras.commands ?? []) { const normalized = normalizeCodexName(name) skills.add(normalized) const promptFile = `${normalized}.md` if (!currentPromptFiles.has(promptFile)) { prompts.add(promptFile) } } return { skills: [...skills].sort(), prompts: [...prompts].sort(), } } export function getLegacyPiArtifacts(bundle: PiBundle): LegacyTargetArtifacts { const skills = new Set() const prompts = new Set() const currentSkills = new Set([ ...bundle.generatedSkills.map((skill) => normalizePiName(skill.name)), ...bundle.skillDirs.map((skill) => normalizePiName(skill.name)), ]) const currentPromptFiles = new Set() for (const prompt of bundle.prompts) { currentPromptFiles.add(`${sanitizePathName(prompt.name)}.md`) } const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const skillName = normalizePiName(name) if (!currentSkills.has(skillName)) { skills.add(skillName) } } for (const name of extras.commands ?? []) { const promptFile = `${normalizePiName(name)}.md` if (!currentPromptFiles.has(promptFile)) { prompts.add(promptFile) } } return { skills: [...skills].sort(), prompts: [...prompts].sort(), } } export function getLegacyGeminiArtifacts(bundle: GeminiBundle): LegacyTargetFileArtifacts { const skills = new Set() const agents = new Set() const commands = new Set() const currentSkills = new Set([ ...bundle.generatedSkills.map((skill) => sanitizePathName(skill.name)), ...bundle.skillDirs.map((skill) => sanitizePathName(skill.name)), ]) const currentAgents = new Set((bundle.agents ?? []).map((agent) => `${sanitizePathName(agent.name)}.md`)) const currentCommands = new Set(bundle.commands.map((command) => `${command.name}.toml`)) const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const skillName = normalizeLegacyName(name) if (!currentSkills.has(skillName)) { skills.add(skillName) } const agentPath = `${skillName}.md` if (!currentAgents.has(agentPath)) { agents.add(agentPath) } } for (const name of extras.commands ?? []) { const commandPath = toNestedCommandRelativePath(name, ".toml") if (!currentCommands.has(commandPath)) { commands.add(commandPath) } } return { skills: [...skills].sort(), agents: [...agents].sort(), commands: [...commands].sort(), } } export function getLegacyDroidArtifacts(bundle: DroidBundle): LegacyDroidArtifacts { const skills = new Set() const commands = new Set() const droids = new Set() const currentSkills = new Set(bundle.skillDirs.map((skill) => sanitizePathName(skill.name))) const currentCommands = new Set(bundle.commands.map((command) => `${command.name}.md`)) const currentDroids = new Set(bundle.droids.map((droid) => `${sanitizePathName(droid.name)}.md`)) const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const droidPath = `${normalizeLegacyName(name)}.md` if (!currentDroids.has(droidPath)) { droids.add(droidPath) } } for (const name of extras.commands ?? []) { const commandPath = `${flattenLegacyCommandName(name)}.md` if (!currentCommands.has(commandPath)) { commands.add(commandPath) } } return { skills: [...skills].sort(), commands: [...commands].sort(), droids: [...droids].sort(), } } export function getLegacyOpenCodeArtifacts(bundle: OpenCodeBundle): LegacyOpenCodeArtifacts { const skills = new Set() const commands = new Set() const agents = new Set() const currentSkills = new Set(bundle.skillDirs.map((skill) => sanitizePathName(skill.name))) const currentCommands = new Set(bundle.commandFiles.map((command) => toRawCommandRelativePath(command.name, ".md"))) const currentAgents = new Set(bundle.agents.map((agent) => `${sanitizePathName(agent.name)}.md`)) const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const agentPath = `${sanitizePathName(name)}.md` if (!currentAgents.has(agentPath)) { agents.add(agentPath) } } for (const name of extras.commands ?? []) { const commandPath = toRawCommandRelativePath(name, ".md") if (!currentCommands.has(commandPath)) { commands.add(commandPath) } } return { skills: [...skills].sort(), commands: [...commands].sort(), agents: [...agents].sort(), } } export function getLegacyKiroArtifacts(bundle: KiroBundle): LegacyKiroArtifacts { const skills = new Set() const agents = new Set() const currentSkills = new Set([ ...bundle.generatedSkills.map((skill) => sanitizePathName(skill.name)), ...bundle.skillDirs.map((skill) => sanitizePathName(skill.name)), ]) const currentAgents = new Set(bundle.agents.map((agent) => sanitizePathName(agent.name))) const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const skillName = normalizeLegacyName(name) if (!currentSkills.has(skillName)) { skills.add(skillName) } const agentName = normalizeLegacyName(name) if (!currentAgents.has(agentName)) { agents.add(agentName) } } for (const name of extras.commands ?? []) { for (const skillName of legacyCommandSkillNames(name)) { if (!currentSkills.has(skillName)) { skills.add(skillName) } } } return { skills: [...skills].sort(), agents: [...agents].sort(), } } export function getLegacyCopilotArtifacts(bundle: CopilotBundle): LegacyCopilotArtifacts { const skills = new Set() const agents = new Set() const currentSkills = new Set([ ...bundle.generatedSkills.map((skill) => sanitizePathName(skill.name)), ...bundle.skillDirs.map((skill) => sanitizePathName(skill.name)), ]) const currentAgents = new Set(bundle.agents.map((agent) => `${sanitizePathName(agent.name)}.agent.md`)) const extras = getLegacyPluginArtifacts(bundle.pluginName) for (const name of extras.skills ?? []) { addLegacySkillVariants(skills, name, { currentSkills }) } for (const name of extras.agents ?? []) { const agentPath = `${normalizeLegacyName(name)}.agent.md` if (!currentAgents.has(agentPath)) { agents.add(agentPath) } } for (const name of extras.commands ?? []) { for (const skillName of legacyCommandSkillNames(name)) { if (!currentSkills.has(skillName)) { skills.add(skillName) } } } return { skills: [...skills].sort(), agents: [...agents].sort(), } } export function getLegacyWindsurfArtifacts(plugin: ClaudePlugin): LegacyWindsurfArtifacts { // IMPORTANT: legacy detection for Windsurf roots must be driven exclusively // by the explicit historical allow-list in `EXTRA_LEGACY_ARTIFACTS_BY_PLUGIN`. // // Earlier versions of this function also seeded candidates from the current // plugin bundle (`plugin.skills`, `plugin.agents`, `plugin.commands`). That // was unsafe: the Windsurf writer has since been removed, so the only // purpose of this cleanup is backing up stale files from past installs. // Any user-authored skill/workflow at a flat Windsurf path that happened to // share a name with a current CE skill/agent/command (e.g. // `skills/ce-debug` or `global_workflows/ce-plan.md`) would otherwise be // swept into `compound-engineering/legacy-backup` even though it was never // installed by CE. // // The historical allow-list already enumerates every skill/agent/command // name CE has ever shipped (including names that are still current), so // restricting detection to that list still cleans up real legacy installs // without touching unrelated user content. If the allow-list is empty for // this plugin, Windsurf cleanup is a no-op — the correct safety default. const skills = new Set() const workflows = new Set() const extras = getLegacyPluginArtifacts(plugin.manifest.name) for (const name of extras.skills ?? []) { skills.add(sanitizePathName(name)) } for (const name of extras.agents ?? []) { skills.add(normalizeLegacyName(name)) } for (const name of extras.commands ?? []) { workflows.add(`${normalizeLegacyName(name)}.md`) } return { skills: [...skills].sort(), workflows: [...workflows].sort(), } } function normalizePiName(value: string): string { return normalizeLegacyName(value) } function addLegacySkillVariants( skills: Set, name: string, options: { currentSkills?: Set; includeRawColon?: boolean } = {}, ): void { const { currentSkills, includeRawColon = false } = options const sanitized = sanitizePathName(name) if (!currentSkills?.has(sanitized)) { skills.add(sanitized) } // Codex historically accepted raw colon directory names on macOS // (for example ~/.codex/skills/ce:plan). Other targets generally sanitized // these names, so raw-colon probing is target-specific. if (includeRawColon && name.includes(":") && !currentSkills?.has(name)) { skills.add(name) } } function normalizeLegacyName(value: string): string { const trimmed = value.trim() if (!trimmed) return "item" const normalized = trimmed .toLowerCase() .replace(/[\\/]+/g, "-") .replace(/[:\s]+/g, "-") .replace(/[^a-z0-9_-]+/g, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, "") return normalized || "item" } function flattenLegacyCommandName(value: string): string { const finalSegment = value.includes(":") ? value.split(":").pop()! : value return normalizeLegacyName(finalSegment) } function legacyCommandSkillNames(value: string): string[] { return [...new Set([normalizeLegacyName(value), flattenLegacyCommandName(value)])] } function toNestedCommandRelativePath(value: string, ext: string): string { return `${value.split(":").map((segment) => normalizeLegacyName(segment)).join("/")}${ext}` } function toRawCommandRelativePath(value: string, ext: string): string { const parts = value.split(":").map((segment) => sanitizePathName(segment)) return `${parts.join("/")}${ext}` }