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,528 @@
|
|
|
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.generateSkillsFromRules = generateSkillsFromRules;
|
|
39
|
+
exports.propagateSkills = propagateSkills;
|
|
40
|
+
exports.propagateSkillsForClaude = propagateSkillsForClaude;
|
|
41
|
+
exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
42
|
+
exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const fs = __importStar(require("fs/promises"));
|
|
45
|
+
const constants_1 = require("../constants");
|
|
46
|
+
const SkillsUtils_1 = require("./SkillsUtils");
|
|
47
|
+
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
48
|
+
/**
|
|
49
|
+
* Discovers skills in the project's skills directory (.ruler/skills or .claude/skills).
|
|
50
|
+
* Returns discovered skills and any validation warnings.
|
|
51
|
+
*/
|
|
52
|
+
async function discoverSkills(projectRoot, rulerDir) {
|
|
53
|
+
// Determine skills directory based on rulerDir
|
|
54
|
+
let skillsPath;
|
|
55
|
+
if (rulerDir && path.basename(rulerDir) === '.claude') {
|
|
56
|
+
// Use .claude/skills
|
|
57
|
+
skillsPath = path.join(rulerDir, 'skills');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Use .ruler/skills (default)
|
|
61
|
+
skillsPath = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
62
|
+
}
|
|
63
|
+
// Check if skills directory exists
|
|
64
|
+
try {
|
|
65
|
+
await fs.access(skillsPath);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Skills directory doesn't exist - this is fine, just return empty
|
|
69
|
+
return { skills: [], warnings: [] };
|
|
70
|
+
}
|
|
71
|
+
// Walk the skills tree
|
|
72
|
+
return await (0, SkillsUtils_1.walkSkillsTree)(skillsPath);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Gets the paths that skills will generate, for gitignore purposes.
|
|
76
|
+
* Returns empty array if skills directory doesn't exist.
|
|
77
|
+
*/
|
|
78
|
+
async function getSkillsGitignorePaths(projectRoot) {
|
|
79
|
+
const rulerSkillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
80
|
+
const claudeSkillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
81
|
+
// Check if skills directory exists in either location
|
|
82
|
+
let isClaudeMode = false;
|
|
83
|
+
let skillsExist = false;
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(rulerSkillsDir);
|
|
86
|
+
skillsExist = true;
|
|
87
|
+
isClaudeMode = false;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Try .claude/skills
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(claudeSkillsDir);
|
|
93
|
+
skillsExist = true;
|
|
94
|
+
isClaudeMode = true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!skillsExist) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
// Import here to avoid circular dependency
|
|
104
|
+
const { CLAUDE_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
105
|
+
const paths = [];
|
|
106
|
+
// When using .claude/skills, check if it's generated from .claude/rules
|
|
107
|
+
if (isClaudeMode) {
|
|
108
|
+
// If .claude/rules exists, then .claude/skills is generated and should be gitignored
|
|
109
|
+
const claudeRulesDir = path.join(projectRoot, '.claude', 'rules');
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(claudeRulesDir);
|
|
112
|
+
// .claude/rules exists, so .claude/skills is generated
|
|
113
|
+
paths.push(path.join(projectRoot, CLAUDE_SKILLS_PATH));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// .claude/rules doesn't exist, so .claude/skills is versioned (don't gitignore)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Using .ruler/skills - gitignore .claude/skills (generated copy)
|
|
121
|
+
paths.push(path.join(projectRoot, CLAUDE_SKILLS_PATH));
|
|
122
|
+
}
|
|
123
|
+
// Always gitignore .skillz (for MCP agents)
|
|
124
|
+
paths.push(path.join(projectRoot, SKILLZ_DIR));
|
|
125
|
+
return paths;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Module-level state to track if experimental warning has been shown.
|
|
129
|
+
* This ensures the warning appears once per process (CLI invocation), not once per apply call.
|
|
130
|
+
* This is intentional: warnings about experimental features should not spam the user
|
|
131
|
+
* if they run multiple applies in the same process or test suite.
|
|
132
|
+
*/
|
|
133
|
+
let hasWarnedExperimental = false;
|
|
134
|
+
/**
|
|
135
|
+
* Warns once per process about experimental skills features and uv requirement.
|
|
136
|
+
* Uses module-level state to prevent duplicate warnings within the same process.
|
|
137
|
+
*/
|
|
138
|
+
// Currently unused but kept for potential future use
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
140
|
+
function warnOnceExperimentalAndUv(verbose, dryRun) {
|
|
141
|
+
if (hasWarnedExperimental) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
hasWarnedExperimental = true;
|
|
145
|
+
(0, constants_1.logWarn)('Skills support is experimental and behavior may change in future releases.', dryRun);
|
|
146
|
+
(0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Recursively finds all .mdc files in a directory.
|
|
150
|
+
*/
|
|
151
|
+
async function findMdcFiles(dir) {
|
|
152
|
+
const mdcFiles = [];
|
|
153
|
+
try {
|
|
154
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const fullPath = path.join(dir, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
// Skip skills directory to avoid processing generated files
|
|
159
|
+
if (entry.name !== 'skills') {
|
|
160
|
+
const subFiles = await findMdcFiles(fullPath);
|
|
161
|
+
mdcFiles.push(...subFiles);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (entry.isFile() && entry.name.endsWith('.mdc')) {
|
|
165
|
+
mdcFiles.push(fullPath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Directory doesn't exist or can't be read, return empty
|
|
171
|
+
}
|
|
172
|
+
return mdcFiles;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Generates skills from .mdc rule files with frontmatter.
|
|
176
|
+
* Creates skill files in the skills directory with @filename references to the original .mdc files.
|
|
177
|
+
*/
|
|
178
|
+
async function generateSkillsFromRules(projectRoot, rulerDir, verbose, dryRun) {
|
|
179
|
+
// Determine skills directory based on rulerDir
|
|
180
|
+
const skillsDir = path.join(rulerDir, 'skills');
|
|
181
|
+
// Find all .mdc files in the ruler directory
|
|
182
|
+
const mdcFiles = await findMdcFiles(rulerDir);
|
|
183
|
+
if (mdcFiles.length === 0) {
|
|
184
|
+
(0, constants_1.logVerboseInfo)('No .mdc files found for skill generation', verbose, dryRun);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let generatedCount = 0;
|
|
188
|
+
const skillsToRemove = [];
|
|
189
|
+
for (const mdcFile of mdcFiles) {
|
|
190
|
+
// Read file content
|
|
191
|
+
const content = await fs.readFile(mdcFile, 'utf8');
|
|
192
|
+
// Parse frontmatter
|
|
193
|
+
const { frontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(content);
|
|
194
|
+
// Skip files without frontmatter
|
|
195
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Derive skill name from filename (without extension)
|
|
199
|
+
const fileName = path.basename(mdcFile, '.mdc');
|
|
200
|
+
// If alwaysApply: true, mark any existing skill for removal
|
|
201
|
+
if (frontmatter.alwaysApply === true) {
|
|
202
|
+
skillsToRemove.push(fileName);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Build description with globs sentence
|
|
206
|
+
let description = frontmatter.description || `Generated from ${fileName}.mdc`;
|
|
207
|
+
if (frontmatter.globs && frontmatter.globs.length > 0) {
|
|
208
|
+
const globsText = frontmatter.globs.join(', ');
|
|
209
|
+
description += ` Applies to files matching: ${globsText}.`;
|
|
210
|
+
}
|
|
211
|
+
// Create skill directory
|
|
212
|
+
const skillDir = path.join(skillsDir, fileName);
|
|
213
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
214
|
+
// Generate @filename reference to the original .mdc file (relative to projectRoot)
|
|
215
|
+
const relativeToProject = path
|
|
216
|
+
.relative(projectRoot, mdcFile)
|
|
217
|
+
.replace(/\\/g, '/');
|
|
218
|
+
const fileReference = `@${relativeToProject}`;
|
|
219
|
+
// Generate skill content with frontmatter
|
|
220
|
+
const skillContent = `---
|
|
221
|
+
name: ${fileName}
|
|
222
|
+
description: ${description}
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
${fileReference}
|
|
226
|
+
`;
|
|
227
|
+
if (dryRun) {
|
|
228
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would generate skill ${fileName} from ${path.relative(projectRoot, mdcFile)}`, verbose, dryRun);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Create skill directory and write file
|
|
232
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
233
|
+
await fs.writeFile(skillFile, skillContent, 'utf8');
|
|
234
|
+
(0, constants_1.logVerboseInfo)(`Generated skill ${fileName} from ${path.relative(projectRoot, mdcFile)}`, verbose, dryRun);
|
|
235
|
+
// Check if .mdc file is in a folder with the same name
|
|
236
|
+
// e.g., rules/docx/docx.mdc -> copy all files from rules/docx/ to skills/docx/
|
|
237
|
+
const mdcDir = path.dirname(mdcFile);
|
|
238
|
+
const mdcDirName = path.basename(mdcDir);
|
|
239
|
+
if (mdcDirName === fileName) {
|
|
240
|
+
// Parent folder matches the file name, copy additional files
|
|
241
|
+
try {
|
|
242
|
+
const entries = await fs.readdir(mdcDir, { withFileTypes: true });
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
// Skip the .mdc file itself
|
|
245
|
+
if (entry.name === `${fileName}.mdc`) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const sourcePath = path.join(mdcDir, entry.name);
|
|
249
|
+
const targetPath = path.join(skillDir, entry.name);
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
// Recursively copy subdirectories
|
|
252
|
+
const { copySkillsDirectory } = await Promise.resolve().then(() => __importStar(require('./SkillsUtils')));
|
|
253
|
+
await copySkillsDirectory(sourcePath, targetPath);
|
|
254
|
+
(0, constants_1.logVerboseInfo)(`Copied directory ${entry.name} to skill ${fileName}`, verbose, dryRun);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// Copy file
|
|
258
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
259
|
+
(0, constants_1.logVerboseInfo)(`Copied file ${entry.name} to skill ${fileName}`, verbose, dryRun);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
// If we can't read the directory, just skip copying additional files
|
|
265
|
+
(0, constants_1.logVerboseInfo)(`Could not copy additional files for skill ${fileName}: ${error}`, verbose, dryRun);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
generatedCount++;
|
|
270
|
+
}
|
|
271
|
+
if (generatedCount > 0) {
|
|
272
|
+
(0, constants_1.logVerboseInfo)(`Generated ${generatedCount} skill(s) from .mdc files`, verbose, dryRun);
|
|
273
|
+
}
|
|
274
|
+
// Remove skills for .mdc files that now have alwaysApply: true
|
|
275
|
+
if (skillsToRemove.length > 0) {
|
|
276
|
+
for (const skillName of skillsToRemove) {
|
|
277
|
+
const skillPath = path.join(skillsDir, skillName);
|
|
278
|
+
try {
|
|
279
|
+
const exists = await fs
|
|
280
|
+
.access(skillPath)
|
|
281
|
+
.then(() => true)
|
|
282
|
+
.catch(() => false);
|
|
283
|
+
if (exists) {
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove skill ${skillName} (alwaysApply changed to true)`, verbose, dryRun);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
289
|
+
(0, constants_1.logVerboseInfo)(`Removed skill ${skillName} (alwaysApply changed to true)`, verbose, dryRun);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Ignore errors - skill might not exist
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Cleans up skills directories (.claude/skills and .skillz) when skills are disabled.
|
|
301
|
+
* This ensures that stale skills from previous runs don't persist when skills are turned off.
|
|
302
|
+
*/
|
|
303
|
+
async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
304
|
+
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
305
|
+
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
306
|
+
// Clean up .claude/skills
|
|
307
|
+
try {
|
|
308
|
+
await fs.access(claudeSkillsPath);
|
|
309
|
+
if (dryRun) {
|
|
310
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
await fs.rm(claudeSkillsPath, { recursive: true, force: true });
|
|
314
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Directory doesn't exist, nothing to clean
|
|
319
|
+
}
|
|
320
|
+
// Clean up .skillz
|
|
321
|
+
try {
|
|
322
|
+
await fs.access(skillzPath);
|
|
323
|
+
if (dryRun) {
|
|
324
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.SKILLZ_DIR}`, verbose, dryRun);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
await fs.rm(skillzPath, { recursive: true, force: true });
|
|
328
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.SKILLZ_DIR} (skills disabled)`, verbose, dryRun);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Directory doesn't exist, nothing to clean
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Propagates skills for agents that need them.
|
|
337
|
+
*/
|
|
338
|
+
async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun, rulerDir) {
|
|
339
|
+
if (!skillsEnabled) {
|
|
340
|
+
(0, constants_1.logVerboseInfo)('Skills support disabled, cleaning up skills directories', verbose, dryRun);
|
|
341
|
+
// Clean up skills directories when skills are disabled
|
|
342
|
+
await cleanupSkillsDirectories(projectRoot, dryRun, verbose);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Determine skills directory based on rulerDir
|
|
346
|
+
let skillsDir;
|
|
347
|
+
if (rulerDir && path.basename(rulerDir) === '.claude') {
|
|
348
|
+
skillsDir = path.join(rulerDir, 'skills');
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
352
|
+
}
|
|
353
|
+
// Check if skills directory exists
|
|
354
|
+
try {
|
|
355
|
+
await fs.access(skillsDir);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// No skills directory - this is fine
|
|
359
|
+
const dirName = rulerDir && path.basename(rulerDir) === '.claude'
|
|
360
|
+
? '.claude/skills'
|
|
361
|
+
: '.ruler/skills';
|
|
362
|
+
(0, constants_1.logVerboseInfo)(`No ${dirName} directory found, skipping skills propagation`, verbose, dryRun);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Discover skills
|
|
366
|
+
const { skills, warnings } = await discoverSkills(projectRoot, rulerDir);
|
|
367
|
+
if (warnings.length > 0) {
|
|
368
|
+
warnings.forEach((warning) => (0, constants_1.logWarn)(warning, dryRun));
|
|
369
|
+
}
|
|
370
|
+
if (skills.length === 0) {
|
|
371
|
+
(0, constants_1.logVerboseInfo)('No valid skills found in .ruler/skills', verbose, dryRun);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
(0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun);
|
|
375
|
+
// Check if any agents need skills
|
|
376
|
+
const hasNativeSkillsAgent = agents.some((a) => a.supportsNativeSkills?.());
|
|
377
|
+
// Only add skillz for agents that support MCP stdio but not native skills
|
|
378
|
+
// Claude Code and Cursor are excluded because they have native skills support
|
|
379
|
+
const hasMcpAgent = agents.some((a) => a.supportsMcpStdio?.() && !a.supportsNativeSkills?.());
|
|
380
|
+
if (!hasNativeSkillsAgent && !hasMcpAgent) {
|
|
381
|
+
(0, constants_1.logVerboseInfo)('No agents require skills support', verbose, dryRun);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Warn about experimental features
|
|
385
|
+
if (hasMcpAgent) {
|
|
386
|
+
// warnOnceExperimentalAndUv(verbose, dryRun);
|
|
387
|
+
}
|
|
388
|
+
// Copy to Claude skills directory if needed
|
|
389
|
+
if (hasNativeSkillsAgent) {
|
|
390
|
+
const isClaudeRoot = rulerDir && path.basename(rulerDir) === '.claude';
|
|
391
|
+
if (!isClaudeRoot) {
|
|
392
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CLAUDE_SKILLS_PATH} for Claude Code`, verbose, dryRun);
|
|
393
|
+
}
|
|
394
|
+
await propagateSkillsForClaude(projectRoot, { dryRun, rulerDir });
|
|
395
|
+
}
|
|
396
|
+
// Copy to .skillz directory if needed
|
|
397
|
+
if (hasMcpAgent) {
|
|
398
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.SKILLZ_DIR} for MCP agents without native skills support`, verbose, dryRun);
|
|
399
|
+
await propagateSkillsForSkillz(projectRoot, { dryRun, rulerDir });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Propagates skills for Claude Code by copying .ruler/skills to .claude/skills.
|
|
404
|
+
* If rulerDir is .claude, skills are already in the right place, so no copy is needed.
|
|
405
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
406
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
407
|
+
*/
|
|
408
|
+
async function propagateSkillsForClaude(projectRoot, options) {
|
|
409
|
+
// If using .claude as the root folder, skills are already in .claude/skills
|
|
410
|
+
const isClaudeRoot = options.rulerDir && path.basename(options.rulerDir) === '.claude';
|
|
411
|
+
if (isClaudeRoot) {
|
|
412
|
+
// No need to copy, skills are already in .claude/skills
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
416
|
+
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
417
|
+
const claudeDir = path.dirname(claudeSkillsPath);
|
|
418
|
+
// Check if source skills directory exists
|
|
419
|
+
try {
|
|
420
|
+
await fs.access(skillsDir);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// No skills directory - return empty
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
if (options.dryRun) {
|
|
427
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CLAUDE_SKILLS_PATH}`];
|
|
428
|
+
}
|
|
429
|
+
// Ensure .claude directory exists
|
|
430
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
431
|
+
// Use atomic replace: copy to temp, then rename
|
|
432
|
+
const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`);
|
|
433
|
+
try {
|
|
434
|
+
// Copy to temp directory
|
|
435
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
436
|
+
// Atomically replace the target
|
|
437
|
+
// First, remove existing target if it exists
|
|
438
|
+
try {
|
|
439
|
+
await fs.rm(claudeSkillsPath, { recursive: true, force: true });
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Target didn't exist, that's fine
|
|
443
|
+
}
|
|
444
|
+
// Rename temp to target
|
|
445
|
+
await fs.rename(tempDir, claudeSkillsPath);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
// Clean up temp directory on error
|
|
449
|
+
try {
|
|
450
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Ignore cleanup errors
|
|
454
|
+
}
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Propagates skills for MCP agents by copying skills to .skillz.
|
|
461
|
+
* Supports both .ruler/skills and .claude/skills as source.
|
|
462
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
463
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
464
|
+
*/
|
|
465
|
+
async function propagateSkillsForSkillz(projectRoot, options) {
|
|
466
|
+
// Determine source skills directory based on rulerDir
|
|
467
|
+
const isClaudeRoot = options.rulerDir && path.basename(options.rulerDir) === '.claude';
|
|
468
|
+
const skillsDir = isClaudeRoot
|
|
469
|
+
? path.join(projectRoot, '.claude', 'skills')
|
|
470
|
+
: path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
471
|
+
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
472
|
+
// Check if source skills directory exists
|
|
473
|
+
try {
|
|
474
|
+
await fs.access(skillsDir);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// No skills directory - return empty
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
if (options.dryRun) {
|
|
481
|
+
const relativeSkillsPath = path.relative(projectRoot, skillsDir);
|
|
482
|
+
return [
|
|
483
|
+
`Copy skills from ${relativeSkillsPath} to ${constants_1.SKILLZ_DIR}`,
|
|
484
|
+
`Configure Skillz MCP server with absolute path to ${constants_1.SKILLZ_DIR}`,
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
// Use atomic replace: copy to temp, then rename
|
|
488
|
+
const tempDir = path.join(projectRoot, `${constants_1.SKILLZ_DIR}.tmp-${Date.now()}`);
|
|
489
|
+
try {
|
|
490
|
+
// Copy and transform to temp directory
|
|
491
|
+
// Transform @filename references to actual content for MCP agents
|
|
492
|
+
const { copySkillsDirectoryWithTransform } = await Promise.resolve().then(() => __importStar(require('./SkillsUtils')));
|
|
493
|
+
await copySkillsDirectoryWithTransform(skillsDir, tempDir, projectRoot);
|
|
494
|
+
// Atomically replace the target
|
|
495
|
+
// First, remove existing target if it exists
|
|
496
|
+
try {
|
|
497
|
+
await fs.rm(skillzPath, { recursive: true, force: true });
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Target didn't exist, that's fine
|
|
501
|
+
}
|
|
502
|
+
// Rename temp to target
|
|
503
|
+
await fs.rename(tempDir, skillzPath);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
// Clean up temp directory on error
|
|
507
|
+
try {
|
|
508
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Ignore cleanup errors
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Builds MCP config for Skillz server.
|
|
519
|
+
*/
|
|
520
|
+
function buildSkillzMcpConfig(projectRoot) {
|
|
521
|
+
const skillzAbsPath = path.resolve(projectRoot, constants_1.SKILLZ_DIR);
|
|
522
|
+
return {
|
|
523
|
+
[constants_1.SKILLZ_MCP_SERVER_NAME]: {
|
|
524
|
+
command: 'uvx',
|
|
525
|
+
args: ['skillz@latest', skillzAbsPath],
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|