UNPKG

@intellectronica/ruler

Version:

Ruler — apply the same rules to all coding agents

964 lines (963 loc) 37.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.discoverSkills = discoverSkills; exports.getSkillsGitignorePaths = getSkillsGitignorePaths; exports.propagateSkills = propagateSkills; exports.propagateSkillsForClaude = propagateSkillsForClaude; exports.propagateSkillsForCodex = propagateSkillsForCodex; exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode; exports.propagateSkillsForPi = propagateSkillsForPi; exports.propagateSkillsForGoose = propagateSkillsForGoose; exports.propagateSkillsForVibe = propagateSkillsForVibe; exports.propagateSkillsForRoo = propagateSkillsForRoo; exports.propagateSkillsForGemini = propagateSkillsForGemini; exports.propagateSkillsForCursor = propagateSkillsForCursor; exports.propagateSkillsForFactory = propagateSkillsForFactory; exports.propagateSkillsForAntigravity = propagateSkillsForAntigravity; const path = __importStar(require("path")); const fs = __importStar(require("fs/promises")); const constants_1 = require("../constants"); const SkillsUtils_1 = require("./SkillsUtils"); /** * Discovers skills in the project's .ruler/skills directory. * Returns discovered skills and any validation warnings. */ async function discoverSkills(projectRoot) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); // Check if skills directory exists try { await fs.access(skillsDir); } catch { // Skills directory doesn't exist - this is fine, just return empty return { skills: [], warnings: [] }; } // Walk the skills tree return await (0, SkillsUtils_1.walkSkillsTree)(skillsDir); } /** * Gets the paths that skills will generate, for gitignore purposes. * Returns empty array if skills directory doesn't exist. */ async function getSkillsGitignorePaths(projectRoot, agents) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); // Check if skills directory exists try { await fs.access(skillsDir); } catch { return []; } // Import here to avoid circular dependency const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, PI_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, ROO_SKILLS_PATH, GEMINI_SKILLS_PATH, CURSOR_SKILLS_PATH, FACTORY_SKILLS_PATH, ANTIGRAVITY_SKILLS_PATH, } = await Promise.resolve().then(() => __importStar(require('../constants'))); const selectedTargets = getSelectedSkillTargets(agents); const targetPaths = { claude: CLAUDE_SKILLS_PATH, codex: CODEX_SKILLS_PATH, opencode: OPENCODE_SKILLS_PATH, pi: PI_SKILLS_PATH, goose: GOOSE_SKILLS_PATH, vibe: VIBE_SKILLS_PATH, roo: ROO_SKILLS_PATH, gemini: GEMINI_SKILLS_PATH, cursor: CURSOR_SKILLS_PATH, factory: FACTORY_SKILLS_PATH, antigravity: ANTIGRAVITY_SKILLS_PATH, }; return Array.from(selectedTargets).map((target) => path.join(projectRoot, targetPaths[target])); } /** * Module-level state to track if experimental warning has been shown. * This ensures the warning appears once per process (CLI invocation), not once per apply call. * This is intentional: warnings about experimental features should not spam the user * if they run multiple applies in the same process or test suite. */ let hasWarnedExperimental = false; /** * Warns once per process about experimental skills support. * Uses module-level state to prevent duplicate warnings within the same process. */ function warnOnceExperimental(verbose, dryRun) { if (hasWarnedExperimental) { return; } hasWarnedExperimental = true; (0, constants_1.logWarn)('Skills support is experimental and behavior may change in future releases.', dryRun); } const SKILL_TARGET_TO_IDENTIFIERS = new Map([ ['claude', ['claude', 'copilot', 'kilocode']], ['codex', ['codex']], ['opencode', ['opencode']], ['pi', ['pi']], ['goose', ['goose', 'amp']], ['vibe', ['mistral']], ['roo', ['roo']], ['gemini', ['gemini-cli']], ['cursor', ['cursor']], ['factory', ['factory']], ['antigravity', ['antigravity']], ]); function getSelectedSkillTargets(agents) { const selectedIdentifiers = new Set(agents .filter((agent) => agent.supportsNativeSkills?.()) .map((agent) => agent.getIdentifier())); const targets = new Set(); for (const [target, identifiers] of SKILL_TARGET_TO_IDENTIFIERS) { if (identifiers.some((id) => selectedIdentifiers.has(id))) { targets.add(target); } } return targets; } /** * Cleans up skills directories when skills are disabled. * This ensures that stale skills from previous runs don't persist when skills are turned off. */ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) { const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH); const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH); const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH); const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH); const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH); const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH); const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH); const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH); const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH); const factorySkillsPath = path.join(projectRoot, constants_1.FACTORY_SKILLS_PATH); const antigravitySkillsPath = path.join(projectRoot, constants_1.ANTIGRAVITY_SKILLS_PATH); // Clean up .claude/skills try { await fs.access(claudeSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(claudeSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .codex/skills try { await fs.access(codexSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CODEX_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(codexSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CODEX_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .opencode/skills try { await fs.access(opencodeSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(opencodeSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .pi/skills try { await fs.access(piSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.PI_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(piSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.PI_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .agents/skills try { await fs.access(gooseSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(gooseSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .vibe/skills try { await fs.access(vibeSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(vibeSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .roo/skills try { await fs.access(rooSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ROO_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(rooSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ROO_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .gemini/skills try { await fs.access(geminiSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GEMINI_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(geminiSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GEMINI_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .cursor/skills try { await fs.access(cursorSkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CURSOR_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(cursorSkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CURSOR_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .factory/skills try { await fs.access(factorySkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.FACTORY_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(factorySkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.FACTORY_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } // Clean up .agent/skills try { await fs.access(antigravitySkillsPath); if (dryRun) { (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ANTIGRAVITY_SKILLS_PATH}`, verbose, dryRun); } else { await fs.rm(antigravitySkillsPath, { recursive: true, force: true }); (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ANTIGRAVITY_SKILLS_PATH} (skills disabled)`, verbose, dryRun); } } catch { // Directory doesn't exist, nothing to clean } } /** * Propagates skills for agents that need them. */ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun) { if (!skillsEnabled) { (0, constants_1.logVerboseInfo)('Skills support disabled, cleaning up skills directories', verbose, dryRun); // Clean up skills directories when skills are disabled await cleanupSkillsDirectories(projectRoot, dryRun, verbose); return; } const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); // Check if skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - this is fine (0, constants_1.logVerboseInfo)('No .ruler/skills directory found, skipping skills propagation', verbose, dryRun); return; } // Discover skills const { skills, warnings } = await discoverSkills(projectRoot); if (warnings.length > 0) { warnings.forEach((warning) => (0, constants_1.logWarn)(warning, dryRun)); } if (skills.length === 0) { (0, constants_1.logVerboseInfo)('No valid skills found in .ruler/skills', verbose, dryRun); return; } (0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun); const hasNativeSkillsAgent = agents.some((a) => a.supportsNativeSkills?.()); const nonNativeAgents = agents.filter((agent) => !agent.supportsNativeSkills?.()); if (nonNativeAgents.length > 0) { const agentList = nonNativeAgents .map((agent) => agent.getName()) .join(', '); (0, constants_1.logWarn)(`Skills are configured, but the following agents do not support native skills and will be skipped: ${agentList}`, dryRun); } if (!hasNativeSkillsAgent) { (0, constants_1.logVerboseInfo)('No agents support native skills, skipping skills propagation', verbose, dryRun); return; } // Warn about experimental features warnOnceExperimental(verbose, dryRun); const selectedTargets = getSelectedSkillTargets(agents); if (selectedTargets.size === 0) { (0, constants_1.logVerboseInfo)('No selected agents require skills propagation, skipping skills propagation', verbose, dryRun); return; } // Copy to Claude skills directory if needed if (selectedTargets.has('claude')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code, GitHub Copilot, and KiloCode`, verbose, dryRun); await propagateSkillsForClaude(projectRoot, { dryRun }); } if (selectedTargets.has('codex')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CODEX_SKILLS_PATH} for OpenAI Codex CLI`, verbose, dryRun); await propagateSkillsForCodex(projectRoot, { dryRun }); } if (selectedTargets.has('opencode')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.OPENCODE_SKILLS_PATH} for OpenCode`, verbose, dryRun); await propagateSkillsForOpenCode(projectRoot, { dryRun }); } if (selectedTargets.has('pi')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.PI_SKILLS_PATH} for Pi Coding Agent`, verbose, dryRun); await propagateSkillsForPi(projectRoot, { dryRun }); } if (selectedTargets.has('goose')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose and Amp`, verbose, dryRun); await propagateSkillsForGoose(projectRoot, { dryRun }); } if (selectedTargets.has('vibe')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun); await propagateSkillsForVibe(projectRoot, { dryRun }); } if (selectedTargets.has('roo')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.ROO_SKILLS_PATH} for Roo Code`, verbose, dryRun); await propagateSkillsForRoo(projectRoot, { dryRun }); } if (selectedTargets.has('gemini')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GEMINI_SKILLS_PATH} for Gemini CLI`, verbose, dryRun); await propagateSkillsForGemini(projectRoot, { dryRun }); } if (selectedTargets.has('cursor')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CURSOR_SKILLS_PATH} for Cursor`, verbose, dryRun); await propagateSkillsForCursor(projectRoot, { dryRun }); } if (selectedTargets.has('factory')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.FACTORY_SKILLS_PATH} for Factory Droid`, verbose, dryRun); await propagateSkillsForFactory(projectRoot, { dryRun }); } if (selectedTargets.has('antigravity')) { (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.ANTIGRAVITY_SKILLS_PATH} for Antigravity`, verbose, dryRun); await propagateSkillsForAntigravity(projectRoot, { dryRun }); } // No MCP-based propagation; only native skills are supported. } /** * Propagates skills for Claude Code by copying .ruler/skills to .claude/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForClaude(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH); const claudeDir = path.dirname(claudeSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CLAUDE_SKILLS_PATH}`]; } // Ensure .claude directory exists await fs.mkdir(claudeDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(claudeSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, claudeSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .codex/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForCodex(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH); const codexDir = path.dirname(codexSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CODEX_SKILLS_PATH}`]; } // Ensure .codex directory exists await fs.mkdir(codexDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(codexDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(codexSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, codexSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for OpenCode by copying .ruler/skills to .opencode/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForOpenCode(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH); const opencodeDir = path.dirname(opencodeSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.OPENCODE_SKILLS_PATH}`]; } // Ensure .opencode directory exists await fs.mkdir(opencodeDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(opencodeSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, opencodeSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Pi Coding Agent by copying .ruler/skills to .pi/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForPi(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH); const piDir = path.dirname(piSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.PI_SKILLS_PATH}`]; } // Ensure .pi directory exists await fs.mkdir(piDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(piDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(piSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, piSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Goose by copying .ruler/skills to .agents/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForGoose(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH); const gooseDir = path.dirname(gooseSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GOOSE_SKILLS_PATH}`]; } // Ensure .agents directory exists await fs.mkdir(gooseDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(gooseSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, gooseSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Mistral Vibe by copying .ruler/skills to .vibe/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForVibe(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH); const vibeDir = path.dirname(vibeSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.VIBE_SKILLS_PATH}`]; } // Ensure .vibe directory exists await fs.mkdir(vibeDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(vibeSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, vibeSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Roo Code by copying .ruler/skills to .roo/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForRoo(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH); const rooDir = path.dirname(rooSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.ROO_SKILLS_PATH}`]; } // Ensure .roo directory exists await fs.mkdir(rooDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(rooDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(rooSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, rooSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Gemini CLI by copying .ruler/skills to .gemini/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForGemini(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH); const geminiDir = path.dirname(geminiSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GEMINI_SKILLS_PATH}`]; } // Ensure .gemini directory exists await fs.mkdir(geminiDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(geminiDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(geminiSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, geminiSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Cursor by copying .ruler/skills to .cursor/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForCursor(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH); const cursorDir = path.dirname(cursorSkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CURSOR_SKILLS_PATH}`]; } // Ensure .cursor directory exists await fs.mkdir(cursorDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(cursorDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(cursorSkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, cursorSkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Factory Droid by copying .ruler/skills to .factory/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForFactory(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const factorySkillsPath = path.join(projectRoot, constants_1.FACTORY_SKILLS_PATH); const factoryDir = path.dirname(factorySkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.FACTORY_SKILLS_PATH}`]; } // Ensure .factory directory exists await fs.mkdir(factoryDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(factoryDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(factorySkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, factorySkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; } /** * Propagates skills for Antigravity by copying .ruler/skills to .agent/skills. * Uses atomic replace to ensure safe overwriting of existing skills. * Returns dry-run steps if dryRun is true, otherwise returns empty array. */ async function propagateSkillsForAntigravity(projectRoot, options) { const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH); const antigravitySkillsPath = path.join(projectRoot, constants_1.ANTIGRAVITY_SKILLS_PATH); const antigravityDir = path.dirname(antigravitySkillsPath); // Check if source skills directory exists try { await fs.access(skillsDir); } catch { // No skills directory - return empty return []; } if (options.dryRun) { return [ `Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.ANTIGRAVITY_SKILLS_PATH}`, ]; } // Ensure .agent directory exists await fs.mkdir(antigravityDir, { recursive: true }); // Use atomic replace: copy to temp, then rename const tempDir = path.join(antigravityDir, `skills.tmp-${Date.now()}`); try { // Copy to temp directory await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir); // Atomically replace the target // First, remove existing target if it exists try { await fs.rm(antigravitySkillsPath, { recursive: true, force: true }); } catch { // Target didn't exist, that's fine } // Rename temp to target await fs.rename(tempDir, antigravitySkillsPath); } catch (error) { // Clean up temp directory on error try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } throw error; } return []; }