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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +989 -0
  3. package/dist/agents/AbstractAgent.js +92 -0
  4. package/dist/agents/AgentsMdAgent.js +85 -0
  5. package/dist/agents/AiderAgent.js +108 -0
  6. package/dist/agents/AmazonQCliAgent.js +103 -0
  7. package/dist/agents/AmpAgent.js +13 -0
  8. package/dist/agents/AugmentCodeAgent.js +70 -0
  9. package/dist/agents/ClaudeAgent.js +95 -0
  10. package/dist/agents/ClineAgent.js +53 -0
  11. package/dist/agents/CodexCliAgent.js +143 -0
  12. package/dist/agents/CopilotAgent.js +43 -0
  13. package/dist/agents/CrushAgent.js +128 -0
  14. package/dist/agents/CursorAgent.js +93 -0
  15. package/dist/agents/FirebaseAgent.js +61 -0
  16. package/dist/agents/FirebenderAgent.js +205 -0
  17. package/dist/agents/GeminiCliAgent.js +99 -0
  18. package/dist/agents/GooseAgent.js +58 -0
  19. package/dist/agents/IAgent.js +2 -0
  20. package/dist/agents/JulesAgent.js +14 -0
  21. package/dist/agents/JunieAgent.js +53 -0
  22. package/dist/agents/KiloCodeAgent.js +63 -0
  23. package/dist/agents/KiroAgent.js +50 -0
  24. package/dist/agents/OpenCodeAgent.js +99 -0
  25. package/dist/agents/OpenHandsAgent.js +56 -0
  26. package/dist/agents/QwenCodeAgent.js +82 -0
  27. package/dist/agents/RooCodeAgent.js +139 -0
  28. package/dist/agents/TraeAgent.js +54 -0
  29. package/dist/agents/WarpAgent.js +61 -0
  30. package/dist/agents/WindsurfAgent.js +27 -0
  31. package/dist/agents/ZedAgent.js +132 -0
  32. package/dist/agents/agent-utils.js +37 -0
  33. package/dist/agents/index.js +77 -0
  34. package/dist/cli/commands.js +136 -0
  35. package/dist/cli/handlers.js +221 -0
  36. package/dist/cli/index.js +5 -0
  37. package/dist/constants.js +58 -0
  38. package/dist/core/ConfigLoader.js +274 -0
  39. package/dist/core/FileSystemUtils.js +421 -0
  40. package/dist/core/FrontmatterParser.js +142 -0
  41. package/dist/core/GitignoreUtils.js +171 -0
  42. package/dist/core/RuleProcessor.js +60 -0
  43. package/dist/core/SkillsProcessor.js +528 -0
  44. package/dist/core/SkillsUtils.js +230 -0
  45. package/dist/core/UnifiedConfigLoader.js +432 -0
  46. package/dist/core/UnifiedConfigTypes.js +2 -0
  47. package/dist/core/agent-selection.js +52 -0
  48. package/dist/core/apply-engine.js +668 -0
  49. package/dist/core/config-utils.js +30 -0
  50. package/dist/core/hash.js +24 -0
  51. package/dist/core/revert-engine.js +413 -0
  52. package/dist/lib.js +196 -0
  53. package/dist/mcp/capabilities.js +65 -0
  54. package/dist/mcp/merge.js +39 -0
  55. package/dist/mcp/propagateOpenCodeMcp.js +116 -0
  56. package/dist/mcp/propagateOpenHandsMcp.js +169 -0
  57. package/dist/mcp/validate.js +17 -0
  58. package/dist/paths/mcp.js +120 -0
  59. package/dist/revert.js +186 -0
  60. package/dist/types.js +2 -0
  61. package/dist/vscode/settings.js +117 -0
  62. 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
+ }