@intellectronica/ruler 0.3.41 → 0.3.42

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 (94) hide show
  1. package/README.md +40 -28
  2. package/dist/agents/AbstractAgent.d.ts +53 -0
  3. package/dist/agents/AgentsMdAgent.d.ts +14 -0
  4. package/dist/agents/AiderAgent.d.ts +14 -0
  5. package/dist/agents/AiderAgent.js +3 -1
  6. package/dist/agents/AmazonQCliAgent.d.ts +13 -0
  7. package/dist/agents/AmpAgent.d.ts +6 -0
  8. package/dist/agents/AntigravityAgent.d.ts +10 -0
  9. package/dist/agents/AugmentCodeAgent.d.ts +13 -0
  10. package/dist/agents/ClaudeAgent.d.ts +13 -0
  11. package/dist/agents/ClineAgent.d.ts +9 -0
  12. package/dist/agents/CodexCliAgent.d.ts +31 -0
  13. package/dist/agents/CopilotAgent.d.ts +20 -0
  14. package/dist/agents/CrushAgent.d.ts +14 -0
  15. package/dist/agents/CrushAgent.js +5 -2
  16. package/dist/agents/CursorAgent.d.ts +17 -0
  17. package/dist/agents/FactoryDroidAgent.d.ts +13 -0
  18. package/dist/agents/FirebaseAgent.d.ts +11 -0
  19. package/dist/agents/FirebenderAgent.d.ts +36 -0
  20. package/dist/agents/GeminiCliAgent.d.ts +11 -0
  21. package/dist/agents/GeminiCliAgent.js +2 -2
  22. package/dist/agents/GooseAgent.d.ts +12 -0
  23. package/dist/agents/IAgent.d.ts +72 -0
  24. package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
  25. package/dist/agents/JulesAgent.d.ts +5 -0
  26. package/dist/agents/JunieAgent.d.ts +12 -0
  27. package/dist/agents/KiloCodeAgent.d.ts +14 -0
  28. package/dist/agents/KiroAgent.d.ts +8 -0
  29. package/dist/agents/MistralVibeAgent.d.ts +31 -0
  30. package/dist/agents/OpenCodeAgent.d.ts +11 -0
  31. package/dist/agents/OpenCodeAgent.js +14 -9
  32. package/dist/agents/OpenHandsAgent.d.ts +8 -0
  33. package/dist/agents/PiAgent.d.ts +9 -0
  34. package/dist/agents/QwenCodeAgent.d.ts +10 -0
  35. package/dist/agents/QwenCodeAgent.js +2 -2
  36. package/dist/agents/RooCodeAgent.d.ts +16 -0
  37. package/dist/agents/TraeAgent.d.ts +10 -0
  38. package/dist/agents/WarpAgent.d.ts +12 -0
  39. package/dist/agents/WindsurfAgent.d.ts +13 -0
  40. package/dist/agents/ZedAgent.d.ts +21 -0
  41. package/dist/agents/ZedAgent.js +5 -2
  42. package/dist/agents/agent-utils.d.ts +5 -0
  43. package/dist/agents/agent-utils.js +8 -5
  44. package/dist/agents/index.d.ts +9 -0
  45. package/dist/cli/commands.d.ts +4 -0
  46. package/dist/cli/commands.js +1 -2
  47. package/dist/cli/handlers.d.ts +41 -0
  48. package/dist/cli/handlers.js +75 -59
  49. package/dist/cli/index.d.ts +2 -0
  50. package/dist/constants.d.ts +35 -0
  51. package/dist/core/ConfigLoader.d.ts +57 -0
  52. package/dist/core/ConfigLoader.js +106 -39
  53. package/dist/core/FileSystemUtils.d.ts +51 -0
  54. package/dist/core/FileSystemUtils.js +37 -17
  55. package/dist/core/GitignoreUtils.d.ts +15 -0
  56. package/dist/core/GitignoreUtils.js +32 -1
  57. package/dist/core/RuleProcessor.d.ts +8 -0
  58. package/dist/core/SkillsProcessor.d.ts +127 -0
  59. package/dist/core/SkillsProcessor.js +104 -218
  60. package/dist/core/SkillsUtils.d.ts +26 -0
  61. package/dist/core/SubagentsProcessor.d.ts +38 -0
  62. package/dist/core/SubagentsUtils.d.ts +34 -0
  63. package/dist/core/UnifiedConfigLoader.d.ts +10 -0
  64. package/dist/core/UnifiedConfigLoader.js +61 -31
  65. package/dist/core/UnifiedConfigTypes.d.ts +95 -0
  66. package/dist/core/agent-selection.d.ts +12 -0
  67. package/dist/core/agent-selection.js +11 -3
  68. package/dist/core/apply-engine.d.ts +69 -0
  69. package/dist/core/apply-engine.js +57 -50
  70. package/dist/core/config-utils.d.ts +14 -0
  71. package/dist/core/config-utils.js +9 -3
  72. package/dist/core/hash.d.ts +2 -0
  73. package/dist/core/path-utils.d.ts +1 -0
  74. package/dist/core/path-utils.js +42 -0
  75. package/dist/core/revert-engine.d.ts +36 -0
  76. package/dist/core/revert-engine.js +70 -9
  77. package/dist/lib.d.ts +13 -0
  78. package/dist/lib.js +16 -3
  79. package/dist/mcp/capabilities.d.ts +20 -0
  80. package/dist/mcp/merge.d.ts +10 -0
  81. package/dist/mcp/merge.js +19 -1
  82. package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
  83. package/dist/mcp/propagateOpenCodeMcp.js +21 -9
  84. package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
  85. package/dist/mcp/propagateOpenHandsMcp.js +31 -15
  86. package/dist/mcp/validate.d.ts +7 -0
  87. package/dist/mcp/validate.js +6 -1
  88. package/dist/paths/mcp.d.ts +8 -0
  89. package/dist/paths/mcp.js +33 -4
  90. package/dist/revert.d.ts +6 -0
  91. package/dist/revert.js +39 -27
  92. package/dist/types.d.ts +87 -0
  93. package/dist/vscode/settings.d.ts +40 -0
  94. package/package.json +6 -4
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.discoverSkills = discoverSkills;
37
37
  exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
38
+ exports.replaceSkillsDirectory = replaceSkillsDirectory;
38
39
  exports.propagateSkills = propagateSkills;
39
40
  exports.propagateSkillsForClaude = propagateSkillsForClaude;
40
41
  exports.propagateSkillsForCodex = propagateSkillsForCodex;
@@ -126,7 +127,7 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
126
127
  ['codex', ['codex']],
127
128
  ['opencode', ['opencode']],
128
129
  ['pi', ['pi']],
129
- ['goose', ['goose', 'amp']],
130
+ ['goose', ['goose', 'amp', 'zed']],
130
131
  ['vibe', ['mistral']],
131
132
  ['roo', ['roo']],
132
133
  ['gemini', ['gemini-cli']],
@@ -136,6 +137,21 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
136
137
  ['factory', ['factory']],
137
138
  ['antigravity', ['antigravity']],
138
139
  ]);
140
+ const SKILL_TARGET_PATHS = [
141
+ constants_1.CLAUDE_SKILLS_PATH,
142
+ constants_1.CODEX_SKILLS_PATH,
143
+ constants_1.OPENCODE_SKILLS_PATH,
144
+ constants_1.PI_SKILLS_PATH,
145
+ constants_1.GOOSE_SKILLS_PATH,
146
+ constants_1.VIBE_SKILLS_PATH,
147
+ constants_1.ROO_SKILLS_PATH,
148
+ constants_1.GEMINI_SKILLS_PATH,
149
+ constants_1.JUNIE_SKILLS_PATH,
150
+ constants_1.CURSOR_SKILLS_PATH,
151
+ constants_1.WINDSURF_SKILLS_PATH,
152
+ constants_1.FACTORY_SKILLS_PATH,
153
+ constants_1.ANTIGRAVITY_SKILLS_PATH,
154
+ ];
139
155
  function getSelectedSkillTargets(agents) {
140
156
  const selectedIdentifiers = new Set(agents
141
157
  .filter((agent) => agent.supportsNativeSkills?.())
@@ -148,205 +164,75 @@ function getSelectedSkillTargets(agents) {
148
164
  }
149
165
  return targets;
150
166
  }
151
- /**
152
- * Cleans up skills directories when skills are disabled.
153
- * This ensures that stale skills from previous runs don't persist when skills are turned off.
154
- */
155
- async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
156
- const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
157
- const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
158
- const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
159
- const piSkillsPath = path.join(projectRoot, constants_1.PI_SKILLS_PATH);
160
- const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
161
- const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
162
- const rooSkillsPath = path.join(projectRoot, constants_1.ROO_SKILLS_PATH);
163
- const geminiSkillsPath = path.join(projectRoot, constants_1.GEMINI_SKILLS_PATH);
164
- const junieSkillsPath = path.join(projectRoot, constants_1.JUNIE_SKILLS_PATH);
165
- const cursorSkillsPath = path.join(projectRoot, constants_1.CURSOR_SKILLS_PATH);
166
- const windsurfSkillsPath = path.join(projectRoot, constants_1.WINDSURF_SKILLS_PATH);
167
- const factorySkillsPath = path.join(projectRoot, constants_1.FACTORY_SKILLS_PATH);
168
- const antigravitySkillsPath = path.join(projectRoot, constants_1.ANTIGRAVITY_SKILLS_PATH);
169
- // Clean up .claude/skills
170
- try {
171
- await fs.access(claudeSkillsPath);
172
- if (dryRun) {
173
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CLAUDE_SKILLS_PATH}`, verbose, dryRun);
174
- }
175
- else {
176
- await fs.rm(claudeSkillsPath, { recursive: true, force: true });
177
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CLAUDE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
178
- }
179
- }
180
- catch {
181
- // Directory doesn't exist, nothing to clean
182
- }
183
- // Clean up .codex/skills
184
- try {
185
- await fs.access(codexSkillsPath);
186
- if (dryRun) {
187
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CODEX_SKILLS_PATH}`, verbose, dryRun);
188
- }
189
- else {
190
- await fs.rm(codexSkillsPath, { recursive: true, force: true });
191
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CODEX_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
192
- }
193
- }
194
- catch {
195
- // Directory doesn't exist, nothing to clean
196
- }
197
- // Clean up .opencode/skills
198
- try {
199
- await fs.access(opencodeSkillsPath);
200
- if (dryRun) {
201
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun);
202
- }
203
- else {
204
- await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
205
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
206
- }
207
- }
208
- catch {
209
- // Directory doesn't exist, nothing to clean
210
- }
211
- // Clean up .pi/skills
212
- try {
213
- await fs.access(piSkillsPath);
214
- if (dryRun) {
215
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.PI_SKILLS_PATH}`, verbose, dryRun);
216
- }
217
- else {
218
- await fs.rm(piSkillsPath, { recursive: true, force: true });
219
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.PI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
220
- }
221
- }
222
- catch {
223
- // Directory doesn't exist, nothing to clean
224
- }
225
- // Clean up .agents/skills
226
- try {
227
- await fs.access(gooseSkillsPath);
228
- if (dryRun) {
229
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun);
230
- }
231
- else {
232
- await fs.rm(gooseSkillsPath, { recursive: true, force: true });
233
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
234
- }
235
- }
236
- catch {
237
- // Directory doesn't exist, nothing to clean
238
- }
239
- // Clean up .vibe/skills
240
- try {
241
- await fs.access(vibeSkillsPath);
242
- if (dryRun) {
243
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun);
244
- }
245
- else {
246
- await fs.rm(vibeSkillsPath, { recursive: true, force: true });
247
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
248
- }
249
- }
250
- catch {
251
- // Directory doesn't exist, nothing to clean
252
- }
253
- // Clean up .roo/skills
254
- try {
255
- await fs.access(rooSkillsPath);
256
- if (dryRun) {
257
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ROO_SKILLS_PATH}`, verbose, dryRun);
258
- }
259
- else {
260
- await fs.rm(rooSkillsPath, { recursive: true, force: true });
261
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ROO_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
262
- }
263
- }
264
- catch {
265
- // Directory doesn't exist, nothing to clean
266
- }
267
- // Clean up .gemini/skills
268
- try {
269
- await fs.access(geminiSkillsPath);
270
- if (dryRun) {
271
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GEMINI_SKILLS_PATH}`, verbose, dryRun);
272
- }
273
- else {
274
- await fs.rm(geminiSkillsPath, { recursive: true, force: true });
275
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.GEMINI_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
276
- }
277
- }
278
- catch {
279
- // Directory doesn't exist, nothing to clean
280
- }
281
- // Clean up .junie/skills
282
- try {
283
- await fs.access(junieSkillsPath);
284
- if (dryRun) {
285
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.JUNIE_SKILLS_PATH}`, verbose, dryRun);
286
- }
287
- else {
288
- await fs.rm(junieSkillsPath, { recursive: true, force: true });
289
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.JUNIE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
290
- }
291
- }
292
- catch {
293
- // Directory doesn't exist, nothing to clean
294
- }
295
- // Clean up .cursor/skills
167
+ function isMissingPathError(error) {
168
+ return (typeof error === 'object' &&
169
+ error !== null &&
170
+ 'code' in error &&
171
+ error.code === 'ENOENT');
172
+ }
173
+ async function cleanupSkillsDirectory(projectRoot, relativePath, dryRun, verbose) {
174
+ const targetPath = path.join(projectRoot, relativePath);
296
175
  try {
297
- await fs.access(cursorSkillsPath);
298
- if (dryRun) {
299
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.CURSOR_SKILLS_PATH}`, verbose, dryRun);
300
- }
301
- else {
302
- await fs.rm(cursorSkillsPath, { recursive: true, force: true });
303
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.CURSOR_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
304
- }
305
- }
306
- catch {
307
- // Directory doesn't exist, nothing to clean
176
+ await fs.access(targetPath);
308
177
  }
309
- // Clean up .windsurf/skills
310
- try {
311
- await fs.access(windsurfSkillsPath);
312
- if (dryRun) {
313
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.WINDSURF_SKILLS_PATH}`, verbose, dryRun);
314
- }
315
- else {
316
- await fs.rm(windsurfSkillsPath, { recursive: true, force: true });
317
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.WINDSURF_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
178
+ catch (error) {
179
+ if (isMissingPathError(error)) {
180
+ return;
318
181
  }
182
+ throw error;
319
183
  }
320
- catch {
321
- // Directory doesn't exist, nothing to clean
184
+ if (dryRun) {
185
+ (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${relativePath}`, verbose, dryRun);
186
+ return;
322
187
  }
323
- // Clean up .factory/skills
324
- try {
325
- await fs.access(factorySkillsPath);
326
- if (dryRun) {
327
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.FACTORY_SKILLS_PATH}`, verbose, dryRun);
188
+ await fs.rm(targetPath, { recursive: true, force: true });
189
+ (0, constants_1.logVerboseInfo)(`Removed ${relativePath} (skills disabled)`, verbose, dryRun);
190
+ }
191
+ async function createTempSkillsDir(parentDir) {
192
+ return fs.mkdtemp(path.join(parentDir, 'skills.tmp-'));
193
+ }
194
+ const TRANSIENT_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'ENOTEMPTY']);
195
+ const RENAME_RETRY_ATTEMPTS = 5;
196
+ const RENAME_RETRY_DELAY_MS = 50;
197
+ function isTransientRenameError(error) {
198
+ return (typeof error === 'object' &&
199
+ error !== null &&
200
+ 'code' in error &&
201
+ typeof error.code === 'string' &&
202
+ TRANSIENT_RENAME_ERROR_CODES.has(error.code));
203
+ }
204
+ function wait(ms) {
205
+ return new Promise((resolve) => setTimeout(resolve, ms));
206
+ }
207
+ async function replaceSkillsDirectory(tempDir, targetDir, fsOps = fs) {
208
+ let lastError;
209
+ for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt += 1) {
210
+ try {
211
+ await fsOps.rename(tempDir, targetDir);
212
+ return;
328
213
  }
329
- else {
330
- await fs.rm(factorySkillsPath, { recursive: true, force: true });
331
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.FACTORY_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
214
+ catch (error) {
215
+ lastError = error;
216
+ if (!isTransientRenameError(error) || attempt === RENAME_RETRY_ATTEMPTS) {
217
+ break;
218
+ }
219
+ await wait(RENAME_RETRY_DELAY_MS * attempt);
332
220
  }
333
221
  }
334
- catch {
335
- // Directory doesn't exist, nothing to clean
336
- }
337
- // Clean up .agent/skills
338
- try {
339
- await fs.access(antigravitySkillsPath);
340
- if (dryRun) {
341
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.ANTIGRAVITY_SKILLS_PATH}`, verbose, dryRun);
342
- }
343
- else {
344
- await fs.rm(antigravitySkillsPath, { recursive: true, force: true });
345
- (0, constants_1.logVerboseInfo)(`Removed ${constants_1.ANTIGRAVITY_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
346
- }
222
+ if (isTransientRenameError(lastError)) {
223
+ await fsOps.cp(tempDir, targetDir, { recursive: true, force: true });
224
+ await fsOps.rm(tempDir, { recursive: true, force: true });
225
+ return;
347
226
  }
348
- catch {
349
- // Directory doesn't exist, nothing to clean
227
+ throw lastError;
228
+ }
229
+ /**
230
+ * Cleans up skills directories when skills are disabled.
231
+ * This ensures that stale skills from previous runs don't persist when skills are turned off.
232
+ */
233
+ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
234
+ for (const skillPath of SKILL_TARGET_PATHS) {
235
+ await cleanupSkillsDirectory(projectRoot, skillPath, dryRun, verbose);
350
236
  }
351
237
  }
352
238
  /**
@@ -416,7 +302,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
416
302
  await propagateSkillsForPi(projectRoot, { dryRun });
417
303
  }
418
304
  if (selectedTargets.has('goose')) {
419
- (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose and Amp`, verbose, dryRun);
305
+ (0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose, Amp and Zed`, verbose, dryRun);
420
306
  await propagateSkillsForGoose(projectRoot, { dryRun });
421
307
  }
422
308
  if (selectedTargets.has('vibe')) {
@@ -476,7 +362,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
476
362
  // Ensure .claude directory exists
477
363
  await fs.mkdir(claudeDir, { recursive: true });
478
364
  // Use atomic replace: copy to temp, then rename
479
- const tempDir = path.join(claudeDir, `skills.tmp-${Date.now()}`);
365
+ const tempDir = await createTempSkillsDir(claudeDir);
480
366
  try {
481
367
  // Copy to temp directory
482
368
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -489,7 +375,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
489
375
  // Target didn't exist, that's fine
490
376
  }
491
377
  // Rename temp to target
492
- await fs.rename(tempDir, claudeSkillsPath);
378
+ await replaceSkillsDirectory(tempDir, claudeSkillsPath);
493
379
  }
494
380
  catch (error) {
495
381
  // Clean up temp directory on error
@@ -526,7 +412,7 @@ async function propagateSkillsForCodex(projectRoot, options) {
526
412
  // Ensure .codex directory exists
527
413
  await fs.mkdir(codexDir, { recursive: true });
528
414
  // Use atomic replace: copy to temp, then rename
529
- const tempDir = path.join(codexDir, `skills.tmp-${Date.now()}`);
415
+ const tempDir = await createTempSkillsDir(codexDir);
530
416
  try {
531
417
  // Copy to temp directory
532
418
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -539,7 +425,7 @@ async function propagateSkillsForCodex(projectRoot, options) {
539
425
  // Target didn't exist, that's fine
540
426
  }
541
427
  // Rename temp to target
542
- await fs.rename(tempDir, codexSkillsPath);
428
+ await replaceSkillsDirectory(tempDir, codexSkillsPath);
543
429
  }
544
430
  catch (error) {
545
431
  // Clean up temp directory on error
@@ -576,7 +462,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
576
462
  // Ensure .opencode directory exists
577
463
  await fs.mkdir(opencodeDir, { recursive: true });
578
464
  // Use atomic replace: copy to temp, then rename
579
- const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`);
465
+ const tempDir = await createTempSkillsDir(opencodeDir);
580
466
  try {
581
467
  // Copy to temp directory
582
468
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -589,7 +475,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
589
475
  // Target didn't exist, that's fine
590
476
  }
591
477
  // Rename temp to target
592
- await fs.rename(tempDir, opencodeSkillsPath);
478
+ await replaceSkillsDirectory(tempDir, opencodeSkillsPath);
593
479
  }
594
480
  catch (error) {
595
481
  // Clean up temp directory on error
@@ -626,7 +512,7 @@ async function propagateSkillsForPi(projectRoot, options) {
626
512
  // Ensure .pi directory exists
627
513
  await fs.mkdir(piDir, { recursive: true });
628
514
  // Use atomic replace: copy to temp, then rename
629
- const tempDir = path.join(piDir, `skills.tmp-${Date.now()}`);
515
+ const tempDir = await createTempSkillsDir(piDir);
630
516
  try {
631
517
  // Copy to temp directory
632
518
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -639,7 +525,7 @@ async function propagateSkillsForPi(projectRoot, options) {
639
525
  // Target didn't exist, that's fine
640
526
  }
641
527
  // Rename temp to target
642
- await fs.rename(tempDir, piSkillsPath);
528
+ await replaceSkillsDirectory(tempDir, piSkillsPath);
643
529
  }
644
530
  catch (error) {
645
531
  // Clean up temp directory on error
@@ -676,7 +562,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
676
562
  // Ensure .agents directory exists
677
563
  await fs.mkdir(gooseDir, { recursive: true });
678
564
  // Use atomic replace: copy to temp, then rename
679
- const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`);
565
+ const tempDir = await createTempSkillsDir(gooseDir);
680
566
  try {
681
567
  // Copy to temp directory
682
568
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -689,7 +575,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
689
575
  // Target didn't exist, that's fine
690
576
  }
691
577
  // Rename temp to target
692
- await fs.rename(tempDir, gooseSkillsPath);
578
+ await replaceSkillsDirectory(tempDir, gooseSkillsPath);
693
579
  }
694
580
  catch (error) {
695
581
  // Clean up temp directory on error
@@ -726,7 +612,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
726
612
  // Ensure .vibe directory exists
727
613
  await fs.mkdir(vibeDir, { recursive: true });
728
614
  // Use atomic replace: copy to temp, then rename
729
- const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`);
615
+ const tempDir = await createTempSkillsDir(vibeDir);
730
616
  try {
731
617
  // Copy to temp directory
732
618
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -739,7 +625,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
739
625
  // Target didn't exist, that's fine
740
626
  }
741
627
  // Rename temp to target
742
- await fs.rename(tempDir, vibeSkillsPath);
628
+ await replaceSkillsDirectory(tempDir, vibeSkillsPath);
743
629
  }
744
630
  catch (error) {
745
631
  // Clean up temp directory on error
@@ -776,7 +662,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
776
662
  // Ensure .roo directory exists
777
663
  await fs.mkdir(rooDir, { recursive: true });
778
664
  // Use atomic replace: copy to temp, then rename
779
- const tempDir = path.join(rooDir, `skills.tmp-${Date.now()}`);
665
+ const tempDir = await createTempSkillsDir(rooDir);
780
666
  try {
781
667
  // Copy to temp directory
782
668
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -789,7 +675,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
789
675
  // Target didn't exist, that's fine
790
676
  }
791
677
  // Rename temp to target
792
- await fs.rename(tempDir, rooSkillsPath);
678
+ await replaceSkillsDirectory(tempDir, rooSkillsPath);
793
679
  }
794
680
  catch (error) {
795
681
  // Clean up temp directory on error
@@ -826,7 +712,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
826
712
  // Ensure .gemini directory exists
827
713
  await fs.mkdir(geminiDir, { recursive: true });
828
714
  // Use atomic replace: copy to temp, then rename
829
- const tempDir = path.join(geminiDir, `skills.tmp-${Date.now()}`);
715
+ const tempDir = await createTempSkillsDir(geminiDir);
830
716
  try {
831
717
  // Copy to temp directory
832
718
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -839,7 +725,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
839
725
  // Target didn't exist, that's fine
840
726
  }
841
727
  // Rename temp to target
842
- await fs.rename(tempDir, geminiSkillsPath);
728
+ await replaceSkillsDirectory(tempDir, geminiSkillsPath);
843
729
  }
844
730
  catch (error) {
845
731
  // Clean up temp directory on error
@@ -872,7 +758,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
872
758
  return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.JUNIE_SKILLS_PATH}`];
873
759
  }
874
760
  await fs.mkdir(junieDir, { recursive: true });
875
- const tempDir = path.join(junieDir, `skills.tmp-${Date.now()}`);
761
+ const tempDir = await createTempSkillsDir(junieDir);
876
762
  try {
877
763
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
878
764
  try {
@@ -881,7 +767,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
881
767
  catch {
882
768
  // Target didn't exist, that's fine
883
769
  }
884
- await fs.rename(tempDir, junieSkillsPath);
770
+ await replaceSkillsDirectory(tempDir, junieSkillsPath);
885
771
  }
886
772
  catch (error) {
887
773
  try {
@@ -917,7 +803,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
917
803
  // Ensure .cursor directory exists
918
804
  await fs.mkdir(cursorDir, { recursive: true });
919
805
  // Use atomic replace: copy to temp, then rename
920
- const tempDir = path.join(cursorDir, `skills.tmp-${Date.now()}`);
806
+ const tempDir = await createTempSkillsDir(cursorDir);
921
807
  try {
922
808
  // Copy to temp directory
923
809
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -930,7 +816,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
930
816
  // Target didn't exist, that's fine
931
817
  }
932
818
  // Rename temp to target
933
- await fs.rename(tempDir, cursorSkillsPath);
819
+ await replaceSkillsDirectory(tempDir, cursorSkillsPath);
934
820
  }
935
821
  catch (error) {
936
822
  // Clean up temp directory on error
@@ -967,7 +853,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
967
853
  // Ensure .windsurf directory exists
968
854
  await fs.mkdir(windsurfDir, { recursive: true });
969
855
  // Use atomic replace: copy to temp, then rename
970
- const tempDir = path.join(windsurfDir, `skills.tmp-${Date.now()}`);
856
+ const tempDir = await createTempSkillsDir(windsurfDir);
971
857
  try {
972
858
  // Copy to temp directory
973
859
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -980,7 +866,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
980
866
  // Target didn't exist, that's fine
981
867
  }
982
868
  // Rename temp to target
983
- await fs.rename(tempDir, windsurfSkillsPath);
869
+ await replaceSkillsDirectory(tempDir, windsurfSkillsPath);
984
870
  }
985
871
  catch (error) {
986
872
  // Clean up temp directory on error
@@ -1017,7 +903,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
1017
903
  // Ensure .factory directory exists
1018
904
  await fs.mkdir(factoryDir, { recursive: true });
1019
905
  // Use atomic replace: copy to temp, then rename
1020
- const tempDir = path.join(factoryDir, `skills.tmp-${Date.now()}`);
906
+ const tempDir = await createTempSkillsDir(factoryDir);
1021
907
  try {
1022
908
  // Copy to temp directory
1023
909
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -1030,7 +916,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
1030
916
  // Target didn't exist, that's fine
1031
917
  }
1032
918
  // Rename temp to target
1033
- await fs.rename(tempDir, factorySkillsPath);
919
+ await replaceSkillsDirectory(tempDir, factorySkillsPath);
1034
920
  }
1035
921
  catch (error) {
1036
922
  // Clean up temp directory on error
@@ -1069,7 +955,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
1069
955
  // Ensure .agent directory exists
1070
956
  await fs.mkdir(antigravityDir, { recursive: true });
1071
957
  // Use atomic replace: copy to temp, then rename
1072
- const tempDir = path.join(antigravityDir, `skills.tmp-${Date.now()}`);
958
+ const tempDir = await createTempSkillsDir(antigravityDir);
1073
959
  try {
1074
960
  // Copy to temp directory
1075
961
  await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
@@ -1082,7 +968,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
1082
968
  // Target didn't exist, that's fine
1083
969
  }
1084
970
  // Rename temp to target
1085
- await fs.rename(tempDir, antigravitySkillsPath);
971
+ await replaceSkillsDirectory(tempDir, antigravitySkillsPath);
1086
972
  }
1087
973
  catch (error) {
1088
974
  // Clean up temp directory on error
@@ -0,0 +1,26 @@
1
+ import { SkillInfo } from '../types';
2
+ /**
3
+ * Checks if a directory contains a SKILL.md file.
4
+ */
5
+ export declare function hasSkillMd(dirPath: string): Promise<boolean>;
6
+ /**
7
+ * Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
8
+ */
9
+ export declare function isGroupingDir(dirPath: string): Promise<boolean>;
10
+ /**
11
+ * Walks the skills tree and discovers all skills.
12
+ * Returns skills and any validation warnings.
13
+ */
14
+ export declare function walkSkillsTree(root: string): Promise<{
15
+ skills: SkillInfo[];
16
+ warnings: string[];
17
+ }>;
18
+ /**
19
+ * Formats validation warnings for display.
20
+ */
21
+ export declare function formatValidationWarnings(warnings: string[]): string;
22
+ /**
23
+ * Copies the skills directory to the destination, preserving structure.
24
+ * Creates the destination directory if it doesn't exist.
25
+ */
26
+ export declare function copySkillsDirectory(srcDir: string, destDir: string): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import type { SubagentInfo } from '../types';
2
+ import type { IAgent } from '../agents/IAgent';
3
+ /**
4
+ * Discovers subagent definitions in `.ruler/agents/`.
5
+ * Each `.md` file is parsed for YAML frontmatter (name, description, …).
6
+ * Files that fail validation are dropped from the returned list and
7
+ * reported via warnings.
8
+ */
9
+ export declare function discoverSubagents(projectRoot: string): Promise<{
10
+ subagents: SubagentInfo[];
11
+ warnings: string[];
12
+ }>;
13
+ type SubagentTarget = 'claude' | 'cursor' | 'codex' | 'copilot';
14
+ /**
15
+ * Returns which native subagent targets are reachable through the supplied
16
+ * agent list. An agent only contributes to a target when it implements
17
+ * `supportsNativeSubagents()` returning true.
18
+ */
19
+ export declare function getSelectedSubagentTargets(agents: IAgent[]): Set<SubagentTarget>;
20
+ /**
21
+ * Returns absolute paths that subagent propagation may generate, for the
22
+ * supplied agents, used for `.gitignore` integration.
23
+ */
24
+ export declare function getSubagentsGitignorePaths(projectRoot: string, agents: IAgent[]): Promise<string[]>;
25
+ /**
26
+ * Test-only hook to reset the once-per-process experimental warning state.
27
+ */
28
+ export declare function _resetExperimentalWarningForTests(): void;
29
+ interface PropagateOptions {
30
+ dryRun: boolean;
31
+ verbose?: boolean;
32
+ }
33
+ export declare function propagateSubagentsForClaude(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
34
+ export declare function propagateSubagentsForCursor(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
35
+ export declare function propagateSubagentsForCodex(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
36
+ export declare function propagateSubagentsForCopilot(projectRoot: string, subagents: SubagentInfo[], options: PropagateOptions): Promise<string[]>;
37
+ export declare function propagateSubagents(projectRoot: string, agents: IAgent[], subagentsEnabled: boolean, cleanupOrphaned: boolean, verbose: boolean, dryRun: boolean): Promise<void>;
38
+ export {};
@@ -0,0 +1,34 @@
1
+ import type { SubagentFrontmatter, SubagentInfo } from '../types';
2
+ export interface ParsedFrontmatter {
3
+ meta: Record<string, unknown>;
4
+ body: string;
5
+ }
6
+ /**
7
+ * Extracts YAML frontmatter and body from a Markdown file's contents.
8
+ * Returns null if no frontmatter delimiter pair is present at the head of the file.
9
+ */
10
+ export declare function parseFrontmatter(content: string): ParsedFrontmatter | null;
11
+ /**
12
+ * Validates a parsed frontmatter object and reads the required and optional
13
+ * fields into a typed SubagentFrontmatter. Returns the typed value on success
14
+ * or an error message on failure.
15
+ */
16
+ export declare function validateFrontmatter(meta: Record<string, unknown>, expectedName: string): {
17
+ value: SubagentFrontmatter;
18
+ } | {
19
+ error: string;
20
+ };
21
+ /**
22
+ * Loads a single subagent file and produces a SubagentInfo.
23
+ * Invalid files produce a SubagentInfo with valid=false and an error string.
24
+ */
25
+ export declare function loadSubagentFile(filePath: string): Promise<SubagentInfo>;
26
+ export interface CopilotToolMapping {
27
+ tools: string[];
28
+ unknown: string[];
29
+ }
30
+ /**
31
+ * Translates Claude tool names to Copilot aliases. Deduplicates results.
32
+ * Unknown source tools are reported separately so callers can surface a warning.
33
+ */
34
+ export declare function mapToolsForCopilot(sourceTools: string[]): CopilotToolMapping;
@@ -0,0 +1,10 @@
1
+ import { RulerUnifiedConfig } from './UnifiedConfigTypes';
2
+ export interface UnifiedLoadOptions {
3
+ projectRoot: string;
4
+ configPath?: string;
5
+ cliAgents?: string[];
6
+ cliMcpEnabled?: boolean;
7
+ cliMcpStrategy?: string;
8
+ checkGlobal?: boolean;
9
+ }
10
+ export declare function loadUnifiedConfig(options: UnifiedLoadOptions): Promise<RulerUnifiedConfig>;