@intellectronica/ruler
Version:
Ruler — apply the same rules to all coding agents
162 lines (161 loc) • 5.81 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.hasSkillMd = hasSkillMd;
exports.isGroupingDir = isGroupingDir;
exports.walkSkillsTree = walkSkillsTree;
exports.formatValidationWarnings = formatValidationWarnings;
exports.copySkillsDirectory = copySkillsDirectory;
const path = __importStar(require("path"));
const fs = __importStar(require("fs/promises"));
const constants_1 = require("../constants");
/**
* Checks if a directory contains a SKILL.md file.
*/
async function hasSkillMd(dirPath) {
try {
const skillMdPath = path.join(dirPath, constants_1.SKILL_MD_FILENAME);
await fs.access(skillMdPath);
return true;
}
catch {
return false;
}
}
/**
* Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
*/
async function isGroupingDir(dirPath) {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const subdirs = entries.filter((e) => e.isDirectory());
for (const subdir of subdirs) {
const subdirPath = path.join(dirPath, subdir.name);
if (await hasSkillMd(subdirPath)) {
return true;
}
// Check recursively for nested grouping
if (await isGroupingDir(subdirPath)) {
return true;
}
}
return false;
}
catch {
return false;
}
}
/**
* Walks the skills tree and discovers all skills.
* Returns skills and any validation warnings.
*/
async function walkSkillsTree(root) {
const skills = [];
const warnings = [];
async function walk(currentPath, relativePath) {
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const entryPath = path.join(currentPath, entry.name);
const entryRelativePath = relativePath
? path.join(relativePath, entry.name)
: entry.name;
const hasSkill = await hasSkillMd(entryPath);
const isGrouping = !hasSkill && (await isGroupingDir(entryPath));
if (hasSkill) {
// This is a valid skill directory
skills.push({
name: entry.name,
path: entryPath,
hasSkillMd: true,
valid: true,
});
}
else if (isGrouping) {
// This is a grouping directory, recurse into it
await walk(entryPath, entryRelativePath);
}
else {
// This is neither a skill nor a grouping directory - warn about it
warnings.push(`Directory '${entryRelativePath}' in .ruler/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
}
}
}
catch (err) {
// If we can't read the directory, just return what we have
warnings.push(`Failed to read directory ${relativePath || 'root'}: ${err.message}`);
}
}
await walk(root, '');
return { skills, warnings };
}
/**
* Formats validation warnings for display.
*/
function formatValidationWarnings(warnings) {
if (warnings.length === 0) {
return '';
}
return warnings.map((w) => ` - ${w}`).join('\n');
}
/**
* Recursively copies a directory and all its contents.
*/
async function copyRecursive(src, dest) {
const stat = await fs.stat(src);
if (stat.isDirectory()) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
await copyRecursive(srcPath, destPath);
}
}
else {
await fs.copyFile(src, dest);
}
}
/**
* Copies the skills directory to the destination, preserving structure.
* Creates the destination directory if it doesn't exist.
*/
async function copySkillsDirectory(srcDir, destDir) {
await fs.mkdir(destDir, { recursive: true });
await copyRecursive(srcDir, destDir);
}