import { existsSync } from "node:fs"; import { join } from "node:path"; import { homedir, platform as osPlatform } from "node:os"; /** * Platform registry for SKILL.md install paths across AI coding agents. * * Each registry entry describes how to install a SKILL.md for one platform: * - paths: candidate directories where the skill will be written. The first * entry is the primary path; later entries are fallbacks for legacy / per- * workspace setups. * - format: file format we emit (currently 'md' for all, kept for forward * compatibility with platforms that prefer JSON/YAML rule manifests). * - detect: returns true if this platform is "installed" on the current * machine, typically by probing for its config dir. * * Where a path is best-effort (the platform's docs do not authoritatively * specify a skills directory), it's marked with a "best-effort" comment. */ export type KnownTarget = | "claude-code" | "cursor" | "windsurf" | "cline" | "codex" | "copilot" | "opencode" | "gemini" | "roo" | "aide" | "augment" | "zed" | "continue" | "kiro" | "junie"; export type Target = KnownTarget | "unknown"; export type SkillFileFormat = "md" | "json" | "yaml"; export interface PlatformRegistryEntry { /** Stable platform identifier, use this for --target=. */ id: KnownTarget; /** Human-readable display name. */ label: string; /** Candidate skill directories, in priority order. */ paths: string[]; /** Output file format for the skill. */ format: SkillFileFormat; /** Returns true if the platform appears to be installed on this machine. */ detect: () => boolean; } export interface InstallTarget { type: Target; path: string; } const home = () => homedir(); /** Helper: directory exists. */ const has = (...segments: string[]) => existsSync(join(...segments)); /** * The single source of truth for supported install targets. * * Adding a new platform: append an entry below. Tests, the install command, * `list-platforms`, and the docs page all read from this registry. */ export const PLATFORM_REGISTRY: PlatformRegistryEntry[] = [ { id: "claude-code", label: "Claude Code", paths: [join(home(), ".claude", "skills")], format: "md", detect: () => has(home(), ".claude"), }, { id: "cursor", label: "Cursor", paths: [join(home(), ".cursor", "skills")], format: "md", detect: () => has(home(), ".cursor"), }, { id: "codex", label: "Codex CLI", paths: [join(home(), ".codex", "skills")], format: "md", detect: () => has(home(), ".codex"), }, { id: "gemini", label: "Gemini CLI", // TODO: verify path with platform docs, Gemini CLI does not yet document // a canonical "skills" directory; ~/.gemini is the published config root. paths: [join(home(), ".gemini", "skills")], format: "md", detect: () => has(home(), ".gemini"), }, { id: "copilot", label: "GitHub Copilot", // Copilot supports workspace-level instructions at .github/copilot-instructions.md //, we install under .github/copilot/skills//SKILL.md so users can // include them via custom instructions without overwriting the single // documented file. paths: [join(process.cwd(), ".github", "copilot", "skills")], format: "md", detect: () => has(process.cwd(), ".github", "copilot") || has(process.cwd(), ".github", "copilot-instructions.md"), }, { id: "windsurf", label: "Windsurf", // Windsurf uses .windsurfrules at the workspace root. We install per-skill // SKILL.md files under ~/.windsurf/skills so users can opt-in to multiple. paths: [join(home(), ".windsurf", "skills")], format: "md", detect: () => has(home(), ".windsurf") || has(process.cwd(), ".windsurfrules"), }, { id: "cline", label: "Cline", // Cline reads .clinerules at workspace root; ~/.cline is best-effort for // global skills bundles. paths: [join(home(), ".cline", "skills")], format: "md", detect: () => has(home(), ".cline") || has(process.cwd(), ".clinerules"), }, { id: "roo", label: "Roo Code", // TODO: verify path with platform docs, Roo Code is a fork of Cline; it // historically reads .roorules and ~/.roo. Treating ~/.roo/skills as primary. paths: [join(home(), ".roo", "skills")], format: "md", detect: () => has(home(), ".roo") || has(process.cwd(), ".roorules"), }, { id: "aide", label: "Aide", // TODO: verify path with platform docs, Aide (codestoryai) does not // publish a stable skills directory. ~/.aide/skills is best-effort. paths: [join(home(), ".aide", "skills")], format: "md", detect: () => has(home(), ".aide"), }, { id: "augment", label: "Augment", // TODO: verify path with platform docs, Augment Code stores config under // ~/.augment on macOS/Linux; skills subdir is best-effort. paths: [join(home(), ".augment", "skills")], format: "md", detect: () => has(home(), ".augment"), }, { id: "zed", label: "Zed", // Zed config lives at ~/.config/zed on Linux and ~/Library/Application // Support/Zed on macOS. The "skills" subdirectory is best-effort, Zed // does not yet have a documented Agent Skill loader. paths: zedPaths(), format: "md", detect: () => zedPaths().some((p) => existsSync(dirOf(p))), }, { id: "continue", label: "Continue", // Continue uses ~/.continue/config.json; skills subdir is best-effort // until Continue ships an Agent Skills loader. paths: [join(home(), ".continue", "skills")], format: "md", detect: () => has(home(), ".continue"), }, { id: "opencode", label: "OpenCode", paths: [join(home(), ".opencode", "skills")], format: "md", detect: () => has(home(), ".opencode"), }, { id: "kiro", label: "Amazon Kiro", // Kiro uses a project-level .kiro/ directory for steering, hooks, and // agent-hooks. Skills install under .kiro/skills/ per project, similar // to how Copilot uses .github/copilot/. paths: [join(process.cwd(), ".kiro", "skills")], format: "md", detect: () => has(process.cwd(), ".kiro"), }, { id: "junie", label: "JetBrains Junie", // Junie stores global agent instructions under ~/.junie/. Skills install // under ~/.junie/skills/ so they persist across all JetBrains projects. paths: [join(home(), ".junie", "skills")], format: "md", detect: () => has(home(), ".junie"), }, ]; function zedPaths(): string[] { const h = home(); if (osPlatform() === "darwin") { return [ // TODO: verify path with platform docs join(h, "Library", "Application Support", "Zed", "skills"), join(h, ".config", "zed", "skills"), ]; } return [join(h, ".config", "zed", "skills")]; } function dirOf(p: string): string { // The detect() probes the parent of the "skills" subdir (i.e. the // platform config dir). We strip the trailing "/skills" segment. return p.replace(/[\\/]skills$/, ""); } export const ALL_TARGETS: KnownTarget[] = PLATFORM_REGISTRY.map((p) => p.id); export function getPlatform(id: string): PlatformRegistryEntry | undefined { return PLATFORM_REGISTRY.find((p) => p.id === id); } /** Map a target name to its primary skills path. */ export function targetToPath(target: Target): string { const entry = PLATFORM_REGISTRY.find((p) => p.id === target); if (entry) return entry.paths[0]; // Default fallback for "unknown" return join(home(), ".claude", "skills"); } /** * Auto-detect the highest-priority installed platform. Order matches * registry order, so Claude Code wins ties (matches historical behavior). */ export function detectInstallTarget(): InstallTarget { for (const entry of PLATFORM_REGISTRY) { if (entry.detect()) { return { type: entry.id, path: entry.paths[0] }; } } return { type: "claude-code", path: join(home(), ".claude", "skills") }; } /** Detect every installed platform on this machine. */ export function detectAllTargets(): InstallTarget[] { const found: InstallTarget[] = []; for (const entry of PLATFORM_REGISTRY) { if (entry.detect()) { found.push({ type: entry.id, path: entry.paths[0] }); } } return found; } /** * Resolve the install path for a given target option, falling back to * auto-detection when the target is missing or unknown. */ export function resolveInstallPath(target?: string): InstallTarget { if (target) { const entry = getPlatform(target); if (entry) { return { type: entry.id, path: entry.paths[0] }; } } return detectInstallTarget(); } /** * Parse a comma-separated --target value into known target ids. * Unknown ids are filtered out (caller can inspect what was dropped). */ export function parseTargets(value?: string): { known: KnownTarget[]; unknown: string[]; } { if (!value) return { known: [], unknown: [] }; const known: KnownTarget[] = []; const unknown: string[] = []; for (const raw of value.split(",")) { const id = raw.trim(); if (!id) continue; if (ALL_TARGETS.includes(id as KnownTarget)) { known.push(id as KnownTarget); } else { unknown.push(id); } } return { known, unknown }; }