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
JavaScript
/**
* 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