@intellectronica/ruler
Version:
Ruler — apply the same rules to all coding agents
964 lines (963 loc) • 37.8 kB
JavaScript
;
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 [];
}