UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

209 lines 7.01 kB
/** * Platform-specific skill deployment path resolution * * @module smiths/skillsmith/platform-resolver */ import * as os from 'os'; import * as path from 'path'; /** * Platform-specific skill configurations */ const PLATFORM_CONFIGS = { claude: { baseDir: '.claude/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, factory: { baseDir: '.factory/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, cursor: { baseDir: '.cursor/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, codex: { baseDir: '~/.codex/skills', // Home-dir deployment; path.join(os.homedir(), ...) at call site extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, opencode: { baseDir: '.opencode/skill', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, warp: { baseDir: '.warp/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, windsurf: { baseDir: '.windsurf/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: false, // 1-level discovery only — native since v1.13.6 }, copilot: { baseDir: '.github/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, generic: { baseDir: 'skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, hermes: { baseDir: '.hermes/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, openclaw: { baseDir: '.openclaw/skills', extension: '.md', supportsSkills: true, supportsSubdirectory: true, }, }; /** * Resolve platform-specific skill paths */ export class PlatformSkillResolver { /** * Get platform configuration for skills */ static getConfig(platform) { return PLATFORM_CONFIGS[platform]; } /** * Get base directory for skills on a platform. * Supports home-dir paths (baseDir starting with `~/`) for platforms like Codex and OpenClaw. */ static getBaseDir(platform, projectPath) { const config = this.getConfig(platform); if (config.baseDir.startsWith('~/')) { return path.join(os.homedir(), config.baseDir.slice(2)); } return path.join(projectPath, config.baseDir); } /** * Get path for a specific skill */ static getSkillPath(platform, projectPath, skillName) { const baseDir = this.getBaseDir(platform, projectPath); return path.join(baseDir, skillName); } /** * Get path for SKILL.md file */ static getSkillFilePath(platform, projectPath, skillName) { const skillPath = this.getSkillPath(platform, projectPath, skillName); const config = this.getConfig(platform); return path.join(skillPath, `SKILL${config.extension}`); } /** * Get path for references directory */ static getReferencesPath(platform, projectPath, skillName) { const skillPath = this.getSkillPath(platform, projectPath, skillName); return path.join(skillPath, 'references'); } /** * Check if platform natively supports skills */ static supportsSkills(platform) { return this.getConfig(platform).supportsSkills; } /** * Get alternative deployment strategy for platforms without skill support */ static getAlternativeStrategy(platform) { return this.getConfig(platform).alternativeStrategy; } /** * Compute the canonical namespaced slug for a skill. * * Idempotent: if `name` already starts with `{namespace}-`, returns `name` unchanged. * This prevents double-prefixing existing `aiwg-*` skills (e.g. `aiwg-sync` → `aiwg-sync`, * not `aiwg-aiwg-sync`). * * @param name - Skill folder name (e.g. 'sync', 'aiwg-sync') * @param namespace - Namespace prefix (e.g. 'aiwg') * @returns Canonical slug (e.g. 'aiwg-sync') */ static computeCanonicalSlug(name, namespace) { const prefix = `${namespace}-`; return name.startsWith(prefix) ? name : `${prefix}${name}`; } /** * Get path for a namespaced skill deployment. * * Platforms with subdirectory support deploy under `{baseDir}/{namespace}/{slug}/`. * Windsurf (1-level discovery) deploys flat at `{baseDir}/{slug}/`. * * @param platform - Target platform * @param projectPath - Project root directory * @param skillName - Skill folder name (e.g. 'sync') * @param namespace - Namespace prefix (e.g. 'aiwg') * @returns Full deployment directory path */ static getNamespacedSkillPath(platform, projectPath, skillName, namespace) { const config = this.getConfig(platform); const slug = this.computeCanonicalSlug(skillName, namespace); const baseDir = path.join(projectPath, config.baseDir); if (config.supportsSubdirectory) { return path.join(baseDir, namespace, slug); } // Windsurf and other flat-discovery platforms: slug only, no namespace subdir return path.join(baseDir, slug); } /** * Get path for the SKILL.md file under namespaced deployment layout. * * @param platform - Target platform * @param projectPath - Project root directory * @param skillName - Skill folder name * @param namespace - Namespace prefix (e.g. 'aiwg') * @returns Full path to SKILL.md */ static getNamespacedSkillFilePath(platform, projectPath, skillName, namespace) { const skillDir = this.getNamespacedSkillPath(platform, projectPath, skillName, namespace); const config = this.getConfig(platform); return path.join(skillDir, `SKILL${config.extension}`); } /** * Validate skill name format */ static validateSkillName(name) { // kebab-case: lowercase letters, numbers, hyphens const kebabRegex = /^[a-z][a-z0-9-]*[a-z0-9]$/; if (!name) { return { valid: false, error: 'Skill name cannot be empty' }; } if (name.length < 2) { return { valid: false, error: 'Skill name must be at least 2 characters' }; } if (name.startsWith('-') || name.endsWith('-')) { return { valid: false, error: 'Skill name cannot start or end with hyphen' }; } if (!kebabRegex.test(name)) { return { valid: false, error: 'Skill name must be kebab-case (lowercase, alphanumeric, hyphens)', }; } return { valid: true }; } } //# sourceMappingURL=platform-resolver.js.map