skiller 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +989 -0
- package/dist/agents/AbstractAgent.js +92 -0
- package/dist/agents/AgentsMdAgent.js +85 -0
- package/dist/agents/AiderAgent.js +108 -0
- package/dist/agents/AmazonQCliAgent.js +103 -0
- package/dist/agents/AmpAgent.js +13 -0
- package/dist/agents/AugmentCodeAgent.js +70 -0
- package/dist/agents/ClaudeAgent.js +95 -0
- package/dist/agents/ClineAgent.js +53 -0
- package/dist/agents/CodexCliAgent.js +143 -0
- package/dist/agents/CopilotAgent.js +43 -0
- package/dist/agents/CrushAgent.js +128 -0
- package/dist/agents/CursorAgent.js +93 -0
- package/dist/agents/FirebaseAgent.js +61 -0
- package/dist/agents/FirebenderAgent.js +205 -0
- package/dist/agents/GeminiCliAgent.js +99 -0
- package/dist/agents/GooseAgent.js +58 -0
- package/dist/agents/IAgent.js +2 -0
- package/dist/agents/JulesAgent.js +14 -0
- package/dist/agents/JunieAgent.js +53 -0
- package/dist/agents/KiloCodeAgent.js +63 -0
- package/dist/agents/KiroAgent.js +50 -0
- package/dist/agents/OpenCodeAgent.js +99 -0
- package/dist/agents/OpenHandsAgent.js +56 -0
- package/dist/agents/QwenCodeAgent.js +82 -0
- package/dist/agents/RooCodeAgent.js +139 -0
- package/dist/agents/TraeAgent.js +54 -0
- package/dist/agents/WarpAgent.js +61 -0
- package/dist/agents/WindsurfAgent.js +27 -0
- package/dist/agents/ZedAgent.js +132 -0
- package/dist/agents/agent-utils.js +37 -0
- package/dist/agents/index.js +77 -0
- package/dist/cli/commands.js +136 -0
- package/dist/cli/handlers.js +221 -0
- package/dist/cli/index.js +5 -0
- package/dist/constants.js +58 -0
- package/dist/core/ConfigLoader.js +274 -0
- package/dist/core/FileSystemUtils.js +421 -0
- package/dist/core/FrontmatterParser.js +142 -0
- package/dist/core/GitignoreUtils.js +171 -0
- package/dist/core/RuleProcessor.js +60 -0
- package/dist/core/SkillsProcessor.js +528 -0
- package/dist/core/SkillsUtils.js +230 -0
- package/dist/core/UnifiedConfigLoader.js +432 -0
- package/dist/core/UnifiedConfigTypes.js +2 -0
- package/dist/core/agent-selection.js +52 -0
- package/dist/core/apply-engine.js +668 -0
- package/dist/core/config-utils.js +30 -0
- package/dist/core/hash.js +24 -0
- package/dist/core/revert-engine.js +413 -0
- package/dist/lib.js +196 -0
- package/dist/mcp/capabilities.js +65 -0
- package/dist/mcp/merge.js +39 -0
- package/dist/mcp/propagateOpenCodeMcp.js +116 -0
- package/dist/mcp/propagateOpenHandsMcp.js +169 -0
- package/dist/mcp/validate.js +17 -0
- package/dist/paths/mcp.js +120 -0
- package/dist/revert.js +186 -0
- package/dist/types.js +2 -0
- package/dist/vscode/settings.js +117 -0
- package/package.json +77 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.findRulerDir = findRulerDir;
|
|
37
|
+
exports.normalizePattern = normalizePattern;
|
|
38
|
+
exports.matchesPattern = matchesPattern;
|
|
39
|
+
exports.readMarkdownFiles = readMarkdownFiles;
|
|
40
|
+
exports.writeGeneratedFile = writeGeneratedFile;
|
|
41
|
+
exports.backupFile = backupFile;
|
|
42
|
+
exports.ensureDirExists = ensureDirExists;
|
|
43
|
+
exports.findGlobalRulerDir = findGlobalRulerDir;
|
|
44
|
+
exports.findAllRulerDirs = findAllRulerDirs;
|
|
45
|
+
const fs_1 = require("fs");
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
49
|
+
/**
|
|
50
|
+
* Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
|
|
51
|
+
*/
|
|
52
|
+
function getXdgConfigDir() {
|
|
53
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Searches upwards from startPath to find a directory named .ruler or .claude.
|
|
57
|
+
* Priority: .ruler first, then .claude (to check .ruler/ruler.toml before .claude/ruler.toml)
|
|
58
|
+
* If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/ruler.
|
|
59
|
+
* Returns the path to the found directory, or null if not found.
|
|
60
|
+
*/
|
|
61
|
+
async function findRulerDir(startPath, checkGlobal = true) {
|
|
62
|
+
// First, search upwards from startPath for local .ruler directory
|
|
63
|
+
let current = startPath;
|
|
64
|
+
while (current) {
|
|
65
|
+
const rulerCandidate = path.join(current, '.ruler');
|
|
66
|
+
try {
|
|
67
|
+
const stat = await fs_1.promises.stat(rulerCandidate);
|
|
68
|
+
if (stat.isDirectory()) {
|
|
69
|
+
return rulerCandidate;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore errors when checking for .ruler directory
|
|
74
|
+
}
|
|
75
|
+
const parent = path.dirname(current);
|
|
76
|
+
if (parent === current) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
current = parent;
|
|
80
|
+
}
|
|
81
|
+
// If no .ruler found, search for .claude directory
|
|
82
|
+
current = startPath;
|
|
83
|
+
while (current) {
|
|
84
|
+
const claudeCandidate = path.join(current, '.claude');
|
|
85
|
+
try {
|
|
86
|
+
const stat = await fs_1.promises.stat(claudeCandidate);
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
// Check if this .claude directory has ruler.toml
|
|
89
|
+
const tomlPath = path.join(claudeCandidate, 'ruler.toml');
|
|
90
|
+
try {
|
|
91
|
+
await fs_1.promises.stat(tomlPath);
|
|
92
|
+
return claudeCandidate;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// .claude exists but no ruler.toml, continue searching
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore errors when checking for .claude directory
|
|
101
|
+
}
|
|
102
|
+
const parent = path.dirname(current);
|
|
103
|
+
if (parent === current) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
current = parent;
|
|
107
|
+
}
|
|
108
|
+
// If no local .ruler or .claude found and checkGlobal is true, check global config directory
|
|
109
|
+
if (checkGlobal) {
|
|
110
|
+
const globalConfigDir = path.join(getXdgConfigDir(), 'ruler');
|
|
111
|
+
try {
|
|
112
|
+
const stat = await fs_1.promises.stat(globalConfigDir);
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
return globalConfigDir;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[ruler] Error checking global config directory ${globalConfigDir}:`, err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Normalizes a pattern by expanding directory patterns to glob patterns.
|
|
125
|
+
* Directory patterns (no wildcards, no .md/.mdc extension) are expanded to match all markdown files.
|
|
126
|
+
* @example "rules-global" becomes a pattern matching both .md and .mdc files
|
|
127
|
+
* @example "rules-global/star.md" stays unchanged
|
|
128
|
+
* @example "AGENTS.md" stays unchanged
|
|
129
|
+
*/
|
|
130
|
+
function normalizePattern(pattern) {
|
|
131
|
+
// If pattern already contains wildcards or is a specific markdown file, return as-is
|
|
132
|
+
if (pattern.includes('*') ||
|
|
133
|
+
pattern.endsWith('.md') ||
|
|
134
|
+
pattern.endsWith('.mdc')) {
|
|
135
|
+
return pattern;
|
|
136
|
+
}
|
|
137
|
+
// Otherwise, treat as directory and expand to include all .md/.mdc files recursively
|
|
138
|
+
// We'll return the .md pattern, but the file walker will pick up both .md and .mdc
|
|
139
|
+
return `${pattern}/**/*.{md,mdc}`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Simple glob pattern matcher supporting basic patterns:
|
|
143
|
+
* - `*` matches any characters except `/`
|
|
144
|
+
* - `**` matches any characters including `/` (zero or more path segments)
|
|
145
|
+
* - `{md,mdc}` matches either md or mdc
|
|
146
|
+
* - Exact string matches
|
|
147
|
+
*/
|
|
148
|
+
function matchesPattern(filePath, pattern) {
|
|
149
|
+
// Handle brace expansion {md,mdc} -> (md|mdc)
|
|
150
|
+
let expandedPattern = pattern;
|
|
151
|
+
const braceMatch = pattern.match(/\{([^}]+)\}/);
|
|
152
|
+
if (braceMatch) {
|
|
153
|
+
const options = braceMatch[1].split(',');
|
|
154
|
+
expandedPattern = pattern.replace(braceMatch[0], `(${options.join('|')})`);
|
|
155
|
+
}
|
|
156
|
+
// Convert glob pattern to regex
|
|
157
|
+
// Escape special regex characters except * / ( ) |
|
|
158
|
+
let regexPattern = expandedPattern
|
|
159
|
+
.replace(/[.+?^${}[\]\\]/g, '\\$&')
|
|
160
|
+
// Replace **/ with a special placeholder (matches zero or more path segments)
|
|
161
|
+
.replace(/\*\*\//g, '__DOUBLESTAR_SLASH__')
|
|
162
|
+
// Replace /** with another placeholder
|
|
163
|
+
.replace(/\/\*\*/g, '__SLASH_DOUBLESTAR__')
|
|
164
|
+
// Replace remaining ** with yet another placeholder
|
|
165
|
+
.replace(/\*\*/g, '__DOUBLESTAR__')
|
|
166
|
+
// Replace single * with regex (matches anything except /)
|
|
167
|
+
.replace(/\*/g, '[^/]*')
|
|
168
|
+
// Replace **/ with regex that matches zero or more path segments
|
|
169
|
+
.replace(/__DOUBLESTAR_SLASH__/g, '(?:.*?/)?')
|
|
170
|
+
// Replace /** with regex
|
|
171
|
+
.replace(/__SLASH_DOUBLESTAR__/g, '(?:/.*)?')
|
|
172
|
+
// Replace standalone ** with regex
|
|
173
|
+
.replace(/__DOUBLESTAR__/g, '.*');
|
|
174
|
+
// Anchor the pattern to match the full path
|
|
175
|
+
regexPattern = '^' + regexPattern + '$';
|
|
176
|
+
const regex = new RegExp(regexPattern);
|
|
177
|
+
return regex.test(filePath);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Recursively reads all Markdown (.md and .mdc) files in rulerDir, returning their paths and contents.
|
|
181
|
+
* Files are sorted alphabetically by path.
|
|
182
|
+
*
|
|
183
|
+
* @param rulerDir The directory to scan for markdown files
|
|
184
|
+
* @param options Optional filtering configuration
|
|
185
|
+
* @param options.include Glob patterns to include (if specified, only matching files are included)
|
|
186
|
+
* @param options.exclude Glob patterns to exclude (takes precedence over include)
|
|
187
|
+
* @param options.merge_strategy Merge strategy: 'all' (default) or 'cursor' (uses MDC frontmatter)
|
|
188
|
+
*/
|
|
189
|
+
async function readMarkdownFiles(rulerDir, options) {
|
|
190
|
+
const mdFiles = [];
|
|
191
|
+
// Gather all markdown files (recursive) first
|
|
192
|
+
async function walk(dir) {
|
|
193
|
+
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const fullPath = path.join(dir, entry.name);
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
await walk(fullPath);
|
|
198
|
+
}
|
|
199
|
+
else if (entry.isFile() &&
|
|
200
|
+
(entry.name.endsWith('.md') || entry.name.endsWith('.mdc'))) {
|
|
201
|
+
const content = await fs_1.promises.readFile(fullPath, 'utf8');
|
|
202
|
+
mdFiles.push({ path: fullPath, content });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await walk(rulerDir);
|
|
207
|
+
// Apply include/exclude filters
|
|
208
|
+
let filteredFiles = mdFiles;
|
|
209
|
+
if (options?.include || options?.exclude) {
|
|
210
|
+
// Normalize patterns (expand directory patterns to globs)
|
|
211
|
+
const normalizedInclude = options.include?.map(normalizePattern);
|
|
212
|
+
const normalizedExclude = options.exclude?.map(normalizePattern);
|
|
213
|
+
filteredFiles = mdFiles.filter((file) => {
|
|
214
|
+
// Get relative path from rulerDir for pattern matching
|
|
215
|
+
const relativePath = path.relative(rulerDir, file.path);
|
|
216
|
+
// Normalize to forward slashes for consistent pattern matching
|
|
217
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
218
|
+
// Check exclude patterns first (they take precedence)
|
|
219
|
+
if (normalizedExclude) {
|
|
220
|
+
for (const pattern of normalizedExclude) {
|
|
221
|
+
if (matchesPattern(normalizedPath, pattern)) {
|
|
222
|
+
return false; // Exclude this file
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// If include patterns are specified, file must match at least one
|
|
227
|
+
if (normalizedInclude && normalizedInclude.length > 0) {
|
|
228
|
+
for (const pattern of normalizedInclude) {
|
|
229
|
+
if (matchesPattern(normalizedPath, pattern)) {
|
|
230
|
+
return true; // Include this file
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false; // No include pattern matched
|
|
234
|
+
}
|
|
235
|
+
// No include patterns specified, file passed exclude check
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Apply cursor mode filtering if enabled
|
|
240
|
+
let processedFiles = filteredFiles;
|
|
241
|
+
if (options?.merge_strategy === 'cursor') {
|
|
242
|
+
const cursorFiles = [];
|
|
243
|
+
for (const file of filteredFiles) {
|
|
244
|
+
const relativePath = path.relative(rulerDir, file.path);
|
|
245
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
246
|
+
// Always include AGENTS.md for backward compatibility
|
|
247
|
+
if (/^AGENTS\.md$/i.test(normalizedPath)) {
|
|
248
|
+
cursorFiles.push(file);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// Check if file is in rules/ folder and is .mdc
|
|
252
|
+
if (normalizedPath.startsWith('rules/') && file.path.endsWith('.mdc')) {
|
|
253
|
+
// Parse frontmatter
|
|
254
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(file.content);
|
|
255
|
+
// Only include if alwaysApply is true
|
|
256
|
+
if (parsed.frontmatter?.alwaysApply === true) {
|
|
257
|
+
// Strip frontmatter from content
|
|
258
|
+
cursorFiles.push({
|
|
259
|
+
path: file.path,
|
|
260
|
+
content: parsed.body,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
processedFiles = cursorFiles;
|
|
266
|
+
}
|
|
267
|
+
// Prioritisation logic:
|
|
268
|
+
// 1. Prefer top-level AGENTS.md if present.
|
|
269
|
+
// 2. If AGENTS.md absent but legacy instructions.md present, use it (no longer emits a warning; legacy accepted silently).
|
|
270
|
+
// 3. Include any remaining .md files (excluding whichever of the above was used if present) in
|
|
271
|
+
// sorted order AFTER the preferred primary file so that new concatenation priority starts with AGENTS.md.
|
|
272
|
+
const topLevelAgents = path.join(rulerDir, 'AGENTS.md');
|
|
273
|
+
const topLevelLegacy = path.join(rulerDir, 'instructions.md');
|
|
274
|
+
// Separate primary candidates from others
|
|
275
|
+
let primaryFile = null;
|
|
276
|
+
const others = [];
|
|
277
|
+
for (const f of processedFiles) {
|
|
278
|
+
if (f.path === topLevelAgents) {
|
|
279
|
+
primaryFile = f; // Highest priority
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!primaryFile) {
|
|
283
|
+
for (const f of processedFiles) {
|
|
284
|
+
if (f.path === topLevelLegacy) {
|
|
285
|
+
primaryFile = f;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const f of processedFiles) {
|
|
291
|
+
if (primaryFile && f.path === primaryFile.path)
|
|
292
|
+
continue;
|
|
293
|
+
others.push(f);
|
|
294
|
+
}
|
|
295
|
+
// Sort the remaining others for stable deterministic concatenation order.
|
|
296
|
+
others.sort((a, b) => a.path.localeCompare(b.path));
|
|
297
|
+
let ordered = primaryFile ? [primaryFile, ...others] : others;
|
|
298
|
+
// NEW: Prepend repository root AGENTS.md (outside .ruler) if it exists and is not identical path.
|
|
299
|
+
try {
|
|
300
|
+
const repoRoot = path.dirname(rulerDir); // .ruler parent
|
|
301
|
+
const rootAgentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
302
|
+
if (path.resolve(rootAgentsPath) !== path.resolve(topLevelAgents)) {
|
|
303
|
+
const stat = await fs_1.promises.stat(rootAgentsPath);
|
|
304
|
+
if (stat.isFile()) {
|
|
305
|
+
const content = await fs_1.promises.readFile(rootAgentsPath, 'utf8');
|
|
306
|
+
// Check if this is a generated file and we have other .ruler files
|
|
307
|
+
const isGenerated = content.startsWith('<!-- Generated by Ruler -->');
|
|
308
|
+
const hasRulerFiles = others.length > 0 || primaryFile !== null;
|
|
309
|
+
// Additional check: if AGENTS.md contains ruler source comments and we have ruler files,
|
|
310
|
+
// it's likely a corrupted generated file that should be skipped
|
|
311
|
+
const containsRulerSources = content.includes('<!-- Source: .ruler/') ||
|
|
312
|
+
content.includes('<!-- Source: ruler/');
|
|
313
|
+
const isProbablyGenerated = isGenerated || (containsRulerSources && hasRulerFiles);
|
|
314
|
+
// Skip generated AGENTS.md if we have other files in .ruler
|
|
315
|
+
if (!isProbablyGenerated || !hasRulerFiles) {
|
|
316
|
+
// Prepend so it has highest precedence
|
|
317
|
+
ordered = [{ path: rootAgentsPath, content }, ...ordered];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// ignore if root AGENTS.md not present
|
|
324
|
+
}
|
|
325
|
+
return ordered;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Writes content to filePath, creating parent directories if necessary.
|
|
329
|
+
*/
|
|
330
|
+
async function writeGeneratedFile(filePath, content) {
|
|
331
|
+
await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
332
|
+
await fs_1.promises.writeFile(filePath, content, 'utf8');
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Creates a backup of the given filePath by copying it to filePath.bak if it exists.
|
|
336
|
+
*/
|
|
337
|
+
async function backupFile(filePath) {
|
|
338
|
+
try {
|
|
339
|
+
await fs_1.promises.access(filePath);
|
|
340
|
+
await fs_1.promises.copyFile(filePath, `${filePath}.bak`);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// ignore if file does not exist
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Ensures that the given directory exists by creating it recursively.
|
|
348
|
+
*/
|
|
349
|
+
async function ensureDirExists(dirPath) {
|
|
350
|
+
await fs_1.promises.mkdir(dirPath, { recursive: true });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Finds the global ruler configuration directory at XDG_CONFIG_HOME/ruler.
|
|
354
|
+
* Returns the path if it exists, null otherwise.
|
|
355
|
+
*/
|
|
356
|
+
async function findGlobalRulerDir() {
|
|
357
|
+
const globalConfigDir = path.join(getXdgConfigDir(), 'ruler');
|
|
358
|
+
try {
|
|
359
|
+
const stat = await fs_1.promises.stat(globalConfigDir);
|
|
360
|
+
if (stat.isDirectory()) {
|
|
361
|
+
return globalConfigDir;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// ignore if global config doesn't exist
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Searches the entire directory tree from startPath to find all .ruler and .claude directories with ruler.toml.
|
|
371
|
+
* Returns an array of directory paths from most specific to least specific.
|
|
372
|
+
*/
|
|
373
|
+
async function findAllRulerDirs(startPath) {
|
|
374
|
+
const rulerDirs = [];
|
|
375
|
+
// Search the entire directory tree downwards from startPath
|
|
376
|
+
async function findRulerDirs(dir) {
|
|
377
|
+
try {
|
|
378
|
+
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
const fullPath = path.join(dir, entry.name);
|
|
381
|
+
if (entry.isDirectory()) {
|
|
382
|
+
if (entry.name === '.ruler') {
|
|
383
|
+
rulerDirs.push(fullPath);
|
|
384
|
+
}
|
|
385
|
+
else if (entry.name === '.claude') {
|
|
386
|
+
// Check if .claude has ruler.toml
|
|
387
|
+
const tomlPath = path.join(fullPath, 'ruler.toml');
|
|
388
|
+
try {
|
|
389
|
+
await fs_1.promises.stat(tomlPath);
|
|
390
|
+
rulerDirs.push(fullPath);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// .claude exists but no ruler.toml
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Recursively search subdirectories (but skip hidden directories like .git)
|
|
398
|
+
if (!entry.name.startsWith('.')) {
|
|
399
|
+
await findRulerDirs(fullPath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// ignore errors when reading directories
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Start searching from the startPath
|
|
410
|
+
await findRulerDirs(startPath);
|
|
411
|
+
// Sort by depth (most specific first) - deeper paths come first
|
|
412
|
+
rulerDirs.sort((a, b) => {
|
|
413
|
+
const depthA = a.split(path.sep).length;
|
|
414
|
+
const depthB = b.split(path.sep).length;
|
|
415
|
+
if (depthA !== depthB) {
|
|
416
|
+
return depthB - depthA; // Deeper paths first
|
|
417
|
+
}
|
|
418
|
+
return a.localeCompare(b); // Alphabetical for same depth
|
|
419
|
+
});
|
|
420
|
+
return rulerDirs;
|
|
421
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
37
|
+
const yaml = __importStar(require("js-yaml"));
|
|
38
|
+
/**
|
|
39
|
+
* Parses YAML frontmatter from MDC file content.
|
|
40
|
+
* Frontmatter must be at the start of the file, between --- delimiters.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```
|
|
44
|
+
* ---
|
|
45
|
+
* description: My rule
|
|
46
|
+
* alwaysApply: true
|
|
47
|
+
* ---
|
|
48
|
+
* # Rule content here
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @param content The full file content
|
|
52
|
+
* @returns Object with parsed frontmatter and body (frontmatter stripped)
|
|
53
|
+
*/
|
|
54
|
+
function parseFrontmatter(content) {
|
|
55
|
+
// Match YAML frontmatter at the start of the file
|
|
56
|
+
// Pattern: start of string, ---, content, ---, rest
|
|
57
|
+
// Allow for empty content between delimiters (consecutive ---\n--- lines)
|
|
58
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)---\s*\n([\s\S]*)$/;
|
|
59
|
+
const match = content.match(frontmatterRegex);
|
|
60
|
+
if (!match) {
|
|
61
|
+
// No frontmatter found
|
|
62
|
+
return {
|
|
63
|
+
frontmatter: null,
|
|
64
|
+
body: content,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const [, yamlContent, body] = match;
|
|
68
|
+
try {
|
|
69
|
+
// Try parsing YAML as-is first
|
|
70
|
+
const parsed = yaml.load(yamlContent);
|
|
71
|
+
return extractFrontmatter(parsed, body);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// YAML parsing failed - try to fix common issues
|
|
75
|
+
try {
|
|
76
|
+
// Fix common issue: globs as comma-separated unquoted strings
|
|
77
|
+
// Pattern: globs: *.tsx,**/path -> globs: ["*.tsx", "**/path"]
|
|
78
|
+
const fixedYaml = yamlContent.replace(/^(\s*globs\s*:\s*)([^\n[{]+)$/gm, (match, prefix, value) => {
|
|
79
|
+
// Check if value looks like comma-separated patterns (contains * or commas)
|
|
80
|
+
if (value.includes('*') || value.includes(',')) {
|
|
81
|
+
// Split by comma and quote each part
|
|
82
|
+
const patterns = value
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((p) => p.trim())
|
|
85
|
+
.filter((p) => p.length > 0)
|
|
86
|
+
.map((p) => `"${p}"`)
|
|
87
|
+
.join(', ');
|
|
88
|
+
return `${prefix}[${patterns}]`;
|
|
89
|
+
}
|
|
90
|
+
return match;
|
|
91
|
+
});
|
|
92
|
+
// Try parsing again with fixed YAML
|
|
93
|
+
if (fixedYaml !== yamlContent) {
|
|
94
|
+
const parsed = yaml.load(fixedYaml);
|
|
95
|
+
return extractFrontmatter(parsed, body);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Fixed version also failed, fall through to warning
|
|
100
|
+
}
|
|
101
|
+
// YAML parsing failed - treat as no frontmatter
|
|
102
|
+
return {
|
|
103
|
+
frontmatter: null,
|
|
104
|
+
body: content,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extracts and validates frontmatter fields from parsed YAML.
|
|
110
|
+
*/
|
|
111
|
+
function extractFrontmatter(parsed, body) {
|
|
112
|
+
// Extract and validate frontmatter fields
|
|
113
|
+
const frontmatter = {};
|
|
114
|
+
// Handle null or undefined parsed YAML (empty frontmatter)
|
|
115
|
+
if (!parsed) {
|
|
116
|
+
return {
|
|
117
|
+
frontmatter: {},
|
|
118
|
+
body: body.trim(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (typeof parsed === 'object') {
|
|
122
|
+
// Extract description
|
|
123
|
+
if (typeof parsed.description === 'string') {
|
|
124
|
+
frontmatter.description = parsed.description;
|
|
125
|
+
}
|
|
126
|
+
// Extract globs (can be string or array)
|
|
127
|
+
if (typeof parsed.globs === 'string') {
|
|
128
|
+
frontmatter.globs = [parsed.globs];
|
|
129
|
+
}
|
|
130
|
+
else if (Array.isArray(parsed.globs)) {
|
|
131
|
+
frontmatter.globs = parsed.globs.filter((g) => typeof g === 'string');
|
|
132
|
+
}
|
|
133
|
+
// Extract alwaysApply
|
|
134
|
+
if (typeof parsed.alwaysApply === 'boolean') {
|
|
135
|
+
frontmatter.alwaysApply = parsed.alwaysApply;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
frontmatter,
|
|
140
|
+
body: body.trim(),
|
|
141
|
+
};
|
|
142
|
+
}
|