@intellectronica/ruler 0.3.10 → 0.3.12
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/README.md +196 -35
- package/dist/agents/AbstractAgent.js +8 -2
- package/dist/agents/AgentsMdAgent.js +1 -2
- package/dist/agents/AugmentCodeAgent.js +1 -2
- package/dist/agents/ClaudeAgent.js +3 -0
- package/dist/agents/CursorAgent.js +1 -2
- package/dist/agents/QwenCodeAgent.js +1 -2
- package/dist/agents/WindsurfAgent.js +6 -137
- package/dist/cli/commands.js +5 -2
- package/dist/cli/handlers.js +31 -2
- package/dist/constants.js +8 -1
- package/dist/core/ConfigLoader.js +40 -2
- package/dist/core/SkillsProcessor.js +301 -0
- package/dist/core/SkillsUtils.js +161 -0
- package/dist/core/UnifiedConfigLoader.js +12 -0
- package/dist/core/apply-engine.js +195 -32
- package/dist/lib.js +105 -7
- package/package.json +17 -13
|
@@ -69,8 +69,34 @@ const rulerConfigSchema = zod_1.z.object({
|
|
|
69
69
|
enabled: zod_1.z.boolean().optional(),
|
|
70
70
|
})
|
|
71
71
|
.optional(),
|
|
72
|
+
skills: zod_1.z
|
|
73
|
+
.object({
|
|
74
|
+
enabled: zod_1.z.boolean().optional(),
|
|
75
|
+
})
|
|
76
|
+
.optional(),
|
|
72
77
|
nested: zod_1.z.boolean().optional(),
|
|
73
78
|
});
|
|
79
|
+
/**
|
|
80
|
+
* Recursively creates a new object with only enumerable string keys,
|
|
81
|
+
* effectively excluding Symbol properties.
|
|
82
|
+
* The @iarna/toml parser adds Symbol properties (Symbol(type), Symbol(declared))
|
|
83
|
+
* for metadata, which Zod v4+ validates and rejects as invalid record keys.
|
|
84
|
+
* By rebuilding the object structure using Object.keys(), we create clean objects
|
|
85
|
+
* that only contain the actual data without Symbol metadata.
|
|
86
|
+
*/
|
|
87
|
+
function stripSymbols(obj) {
|
|
88
|
+
if (obj === null || typeof obj !== 'object') {
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(obj)) {
|
|
92
|
+
return obj.map(stripSymbols);
|
|
93
|
+
}
|
|
94
|
+
const result = {};
|
|
95
|
+
for (const key of Object.keys(obj)) {
|
|
96
|
+
result[key] = stripSymbols(obj[key]);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
74
100
|
/**
|
|
75
101
|
* Loads and parses the ruler TOML configuration file, applying defaults.
|
|
76
102
|
* If the file is missing or invalid, returns empty/default config.
|
|
@@ -97,7 +123,9 @@ async function loadConfig(options) {
|
|
|
97
123
|
let raw = {};
|
|
98
124
|
try {
|
|
99
125
|
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
100
|
-
|
|
126
|
+
const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
127
|
+
// Strip Symbol properties added by @iarna/toml (required for Zod v4+)
|
|
128
|
+
raw = stripSymbols(parsed);
|
|
101
129
|
// Validate the configuration with zod
|
|
102
130
|
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
103
131
|
if (!validationResult.success) {
|
|
@@ -175,13 +203,23 @@ async function loadConfig(options) {
|
|
|
175
203
|
if (typeof rawGitignoreSection.enabled === 'boolean') {
|
|
176
204
|
gitignoreConfig.enabled = rawGitignoreSection.enabled;
|
|
177
205
|
}
|
|
178
|
-
const
|
|
206
|
+
const rawSkillsSection = raw.skills && typeof raw.skills === 'object' && !Array.isArray(raw.skills)
|
|
207
|
+
? raw.skills
|
|
208
|
+
: {};
|
|
209
|
+
const skillsConfig = {};
|
|
210
|
+
if (typeof rawSkillsSection.enabled === 'boolean') {
|
|
211
|
+
skillsConfig.enabled = rawSkillsSection.enabled;
|
|
212
|
+
}
|
|
213
|
+
const nestedDefined = typeof raw.nested === 'boolean';
|
|
214
|
+
const nested = nestedDefined ? raw.nested : false;
|
|
179
215
|
return {
|
|
180
216
|
defaultAgents,
|
|
181
217
|
agentConfigs,
|
|
182
218
|
cliAgents,
|
|
183
219
|
mcp: globalMcpConfig,
|
|
184
220
|
gitignore: gitignoreConfig,
|
|
221
|
+
skills: skillsConfig,
|
|
185
222
|
nested,
|
|
223
|
+
nestedDefined,
|
|
186
224
|
};
|
|
187
225
|
}
|
|
@@ -0,0 +1,301 @@
|
|
|
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.discoverSkills = discoverSkills;
|
|
37
|
+
exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
|
|
38
|
+
exports.propagateSkills = propagateSkills;
|
|
39
|
+
exports.propagateSkillsForClaude = propagateSkillsForClaude;
|
|
40
|
+
exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
41
|
+
exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const fs = __importStar(require("fs/promises"));
|
|
44
|
+
const constants_1 = require("../constants");
|
|
45
|
+
const SkillsUtils_1 = require("./SkillsUtils");
|
|
46
|
+
/**
|
|
47
|
+
* Discovers skills in the project's .ruler/skills directory.
|
|
48
|
+
* Returns discovered skills and any validation warnings.
|
|
49
|
+
*/
|
|
50
|
+
async function discoverSkills(projectRoot) {
|
|
51
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
52
|
+
// Check if skills directory exists
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(skillsDir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Skills directory doesn't exist - this is fine, just return empty
|
|
58
|
+
return { skills: [], warnings: [] };
|
|
59
|
+
}
|
|
60
|
+
// Walk the skills tree
|
|
61
|
+
return await (0, SkillsUtils_1.walkSkillsTree)(skillsDir);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Gets the paths that skills will generate, for gitignore purposes.
|
|
65
|
+
* Returns empty array if skills directory doesn't exist.
|
|
66
|
+
*/
|
|
67
|
+
async function getSkillsGitignorePaths(projectRoot) {
|
|
68
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
69
|
+
// Check if skills directory exists
|
|
70
|
+
try {
|
|
71
|
+
await fs.access(skillsDir);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
// Import here to avoid circular dependency
|
|
77
|
+
const { CLAUDE_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
78
|
+
return [
|
|
79
|
+
path.join(projectRoot, CLAUDE_SKILLS_PATH),
|
|
80
|
+
path.join(projectRoot, SKILLZ_DIR),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Module-level state to track if experimental warning has been shown.
|
|
85
|
+
* This ensures the warning appears once per process (CLI invocation), not once per apply call.
|
|
86
|
+
* This is intentional: warnings about experimental features should not spam the user
|
|
87
|
+
* if they run multiple applies in the same process or test suite.
|
|
88
|
+
*/
|
|
89
|
+
let hasWarnedExperimental = false;
|
|
90
|
+
/**
|
|
91
|
+
* Warns once per process about experimental skills features and uv requirement.
|
|
92
|
+
* Uses module-level state to prevent duplicate warnings within the same process.
|
|
93
|
+
*/
|
|
94
|
+
function warnOnceExperimentalAndUv(verbose, dryRun) {
|
|
95
|
+
if (hasWarnedExperimental) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
hasWarnedExperimental = true;
|
|
99
|
+
(0, constants_1.logWarn)('Skills support is experimental and behavior may change in future releases.', dryRun);
|
|
100
|
+
(0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Cleans up skills directories (.claude/skills and .skillz) when skills are disabled.
|
|
104
|
+
* This ensures that stale skills from previous runs don't persist when skills are turned off.
|
|
105
|
+
*/
|
|
106
|
+
async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
107
|
+
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
108
|
+
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
109
|
+
// Clean up .claude/skills
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(claudeSkillsPath);
|
|
112
|
+
if (dryRun) {
|
|
113
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await fs.rm(claudeSkillsPath, { recursive: true, force: true });
|
|
117
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Directory doesn't exist, nothing to clean
|
|
122
|
+
}
|
|
123
|
+
// Clean up .skillz
|
|
124
|
+
try {
|
|
125
|
+
await fs.access(skillzPath);
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.SKILLZ_DIR}`, verbose, dryRun);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await fs.rm(skillzPath, { recursive: true, force: true });
|
|
131
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.SKILLZ_DIR} (skills disabled)`, verbose, dryRun);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Directory doesn't exist, nothing to clean
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Propagates skills for agents that need them.
|
|
140
|
+
*/
|
|
141
|
+
async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun) {
|
|
142
|
+
if (!skillsEnabled) {
|
|
143
|
+
(0, constants_1.logVerboseInfo)('Skills support disabled, cleaning up skills directories', verbose, dryRun);
|
|
144
|
+
// Clean up skills directories when skills are disabled
|
|
145
|
+
await cleanupSkillsDirectories(projectRoot, dryRun, verbose);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
149
|
+
// Check if skills directory exists
|
|
150
|
+
try {
|
|
151
|
+
await fs.access(skillsDir);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// No skills directory - this is fine
|
|
155
|
+
(0, constants_1.logVerboseInfo)('No .ruler/skills directory found, skipping skills propagation', verbose, dryRun);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Discover skills
|
|
159
|
+
const { skills, warnings } = await discoverSkills(projectRoot);
|
|
160
|
+
if (warnings.length > 0) {
|
|
161
|
+
warnings.forEach((warning) => (0, constants_1.logWarn)(warning, dryRun));
|
|
162
|
+
}
|
|
163
|
+
if (skills.length === 0) {
|
|
164
|
+
(0, constants_1.logVerboseInfo)('No valid skills found in .ruler/skills', verbose, dryRun);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
(0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun);
|
|
168
|
+
// Check if any agents need skills
|
|
169
|
+
const hasNativeSkillsAgent = agents.some((a) => a.supportsNativeSkills?.());
|
|
170
|
+
const hasMcpAgent = agents.some((a) => a.supportsMcpStdio?.() && !a.supportsNativeSkills?.());
|
|
171
|
+
if (!hasNativeSkillsAgent && !hasMcpAgent) {
|
|
172
|
+
(0, constants_1.logVerboseInfo)('No agents require skills support', verbose, dryRun);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Warn about experimental features
|
|
176
|
+
if (hasMcpAgent) {
|
|
177
|
+
warnOnceExperimentalAndUv(verbose, dryRun);
|
|
178
|
+
}
|
|
179
|
+
// Copy to Claude skills directory if needed
|
|
180
|
+
if (hasNativeSkillsAgent) {
|
|
181
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code`, verbose, dryRun);
|
|
182
|
+
await propagateSkillsForClaude(projectRoot, { dryRun });
|
|
183
|
+
}
|
|
184
|
+
// Copy to .skillz directory if needed
|
|
185
|
+
if (hasMcpAgent) {
|
|
186
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.SKILLZ_DIR} for MCP agents`, verbose, dryRun);
|
|
187
|
+
await propagateSkillsForSkillz(projectRoot, { dryRun });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Propagates skills for Claude Code by copying .ruler/skills to .claude/skills.
|
|
192
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
193
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
194
|
+
*/
|
|
195
|
+
async function propagateSkillsForClaude(projectRoot, options) {
|
|
196
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
197
|
+
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
198
|
+
const claudeDir = path.dirname(claudeSkillsPath);
|
|
199
|
+
// Check if source skills directory exists
|
|
200
|
+
try {
|
|
201
|
+
await fs.access(skillsDir);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// No skills directory - return empty
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
if (options.dryRun) {
|
|
208
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CLAUDE_SKILLS_PATH}`];
|
|
209
|
+
}
|
|
210
|
+
// Ensure .claude directory exists
|
|
211
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
212
|
+
// Use atomic replace: copy to temp, then rename
|
|
213
|
+
const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`);
|
|
214
|
+
try {
|
|
215
|
+
// Copy to temp directory
|
|
216
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
217
|
+
// Atomically replace the target
|
|
218
|
+
// First, remove existing target if it exists
|
|
219
|
+
try {
|
|
220
|
+
await fs.rm(claudeSkillsPath, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Target didn't exist, that's fine
|
|
224
|
+
}
|
|
225
|
+
// Rename temp to target
|
|
226
|
+
await fs.rename(tempDir, claudeSkillsPath);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Clean up temp directory on error
|
|
230
|
+
try {
|
|
231
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Ignore cleanup errors
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Propagates skills for MCP agents by copying .ruler/skills to .skillz.
|
|
242
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
243
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
244
|
+
*/
|
|
245
|
+
async function propagateSkillsForSkillz(projectRoot, options) {
|
|
246
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
247
|
+
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
248
|
+
// Check if source skills directory exists
|
|
249
|
+
try {
|
|
250
|
+
await fs.access(skillsDir);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// No skills directory - return empty
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
if (options.dryRun) {
|
|
257
|
+
return [
|
|
258
|
+
`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.SKILLZ_DIR}`,
|
|
259
|
+
`Configure Skillz MCP server with absolute path to ${constants_1.SKILLZ_DIR}`,
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
// Use atomic replace: copy to temp, then rename
|
|
263
|
+
const tempDir = path.join(projectRoot, `${constants_1.SKILLZ_DIR}.tmp-${Date.now()}`);
|
|
264
|
+
try {
|
|
265
|
+
// Copy to temp directory
|
|
266
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
267
|
+
// Atomically replace the target
|
|
268
|
+
// First, remove existing target if it exists
|
|
269
|
+
try {
|
|
270
|
+
await fs.rm(skillzPath, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Target didn't exist, that's fine
|
|
274
|
+
}
|
|
275
|
+
// Rename temp to target
|
|
276
|
+
await fs.rename(tempDir, skillzPath);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
// Clean up temp directory on error
|
|
280
|
+
try {
|
|
281
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Ignore cleanup errors
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Builds MCP config for Skillz server.
|
|
292
|
+
*/
|
|
293
|
+
function buildSkillzMcpConfig(projectRoot) {
|
|
294
|
+
const skillzAbsPath = path.resolve(projectRoot, constants_1.SKILLZ_DIR);
|
|
295
|
+
return {
|
|
296
|
+
[constants_1.SKILLZ_MCP_SERVER_NAME]: {
|
|
297
|
+
command: 'uvx',
|
|
298
|
+
args: ['skillz@latest', skillzAbsPath],
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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.hasSkillMd = hasSkillMd;
|
|
37
|
+
exports.isGroupingDir = isGroupingDir;
|
|
38
|
+
exports.walkSkillsTree = walkSkillsTree;
|
|
39
|
+
exports.formatValidationWarnings = formatValidationWarnings;
|
|
40
|
+
exports.copySkillsDirectory = copySkillsDirectory;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs/promises"));
|
|
43
|
+
const constants_1 = require("../constants");
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a directory contains a SKILL.md file.
|
|
46
|
+
*/
|
|
47
|
+
async function hasSkillMd(dirPath) {
|
|
48
|
+
try {
|
|
49
|
+
const skillMdPath = path.join(dirPath, constants_1.SKILL_MD_FILENAME);
|
|
50
|
+
await fs.access(skillMdPath);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
|
|
59
|
+
*/
|
|
60
|
+
async function isGroupingDir(dirPath) {
|
|
61
|
+
try {
|
|
62
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
63
|
+
const subdirs = entries.filter((e) => e.isDirectory());
|
|
64
|
+
for (const subdir of subdirs) {
|
|
65
|
+
const subdirPath = path.join(dirPath, subdir.name);
|
|
66
|
+
if (await hasSkillMd(subdirPath)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// Check recursively for nested grouping
|
|
70
|
+
if (await isGroupingDir(subdirPath)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Walks the skills tree and discovers all skills.
|
|
82
|
+
* Returns skills and any validation warnings.
|
|
83
|
+
*/
|
|
84
|
+
async function walkSkillsTree(root) {
|
|
85
|
+
const skills = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
async function walk(currentPath, relativePath) {
|
|
88
|
+
try {
|
|
89
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (!entry.isDirectory()) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
95
|
+
const entryRelativePath = relativePath
|
|
96
|
+
? path.join(relativePath, entry.name)
|
|
97
|
+
: entry.name;
|
|
98
|
+
const hasSkill = await hasSkillMd(entryPath);
|
|
99
|
+
const isGrouping = !hasSkill && (await isGroupingDir(entryPath));
|
|
100
|
+
if (hasSkill) {
|
|
101
|
+
// This is a valid skill directory
|
|
102
|
+
skills.push({
|
|
103
|
+
name: entry.name,
|
|
104
|
+
path: entryPath,
|
|
105
|
+
hasSkillMd: true,
|
|
106
|
+
valid: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else if (isGrouping) {
|
|
110
|
+
// This is a grouping directory, recurse into it
|
|
111
|
+
await walk(entryPath, entryRelativePath);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// This is neither a skill nor a grouping directory - warn about it
|
|
115
|
+
warnings.push(`Directory '${entryRelativePath}' in .ruler/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// If we can't read the directory, just return what we have
|
|
121
|
+
warnings.push(`Failed to read directory ${relativePath || 'root'}: ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
await walk(root, '');
|
|
125
|
+
return { skills, warnings };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Formats validation warnings for display.
|
|
129
|
+
*/
|
|
130
|
+
function formatValidationWarnings(warnings) {
|
|
131
|
+
if (warnings.length === 0) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
return warnings.map((w) => ` - ${w}`).join('\n');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Recursively copies a directory and all its contents.
|
|
138
|
+
*/
|
|
139
|
+
async function copyRecursive(src, dest) {
|
|
140
|
+
const stat = await fs.stat(src);
|
|
141
|
+
if (stat.isDirectory()) {
|
|
142
|
+
await fs.mkdir(dest, { recursive: true });
|
|
143
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const srcPath = path.join(src, entry.name);
|
|
146
|
+
const destPath = path.join(dest, entry.name);
|
|
147
|
+
await copyRecursive(srcPath, destPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await fs.copyFile(src, dest);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Copies the skills directory to the destination, preserving structure.
|
|
156
|
+
* Creates the destination directory if it doesn't exist.
|
|
157
|
+
*/
|
|
158
|
+
async function copySkillsDirectory(srcDir, destDir) {
|
|
159
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
160
|
+
await copyRecursive(srcDir, destDir);
|
|
161
|
+
}
|
|
@@ -85,12 +85,24 @@ async function loadUnifiedConfig(options) {
|
|
|
85
85
|
typeof tomlRaw.nested === 'boolean') {
|
|
86
86
|
nested = tomlRaw.nested;
|
|
87
87
|
}
|
|
88
|
+
// Parse skills configuration
|
|
89
|
+
let skillsConfig;
|
|
90
|
+
if (tomlRaw && typeof tomlRaw === 'object') {
|
|
91
|
+
const skillsSection = tomlRaw.skills;
|
|
92
|
+
if (skillsSection && typeof skillsSection === 'object') {
|
|
93
|
+
const skillsObj = skillsSection;
|
|
94
|
+
if (typeof skillsObj.enabled === 'boolean') {
|
|
95
|
+
skillsConfig = { enabled: skillsObj.enabled };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
88
99
|
const toml = {
|
|
89
100
|
raw: tomlRaw,
|
|
90
101
|
schemaVersion: 1,
|
|
91
102
|
agents: {},
|
|
92
103
|
defaultAgents,
|
|
93
104
|
nested,
|
|
105
|
+
skills: skillsConfig,
|
|
94
106
|
};
|
|
95
107
|
// Collect rule markdown files
|
|
96
108
|
let ruleFiles = [];
|