Skip to content

Commit f4e98b7

Browse files
Merge pull request #396 from intellectronica/copilot/fix-install-skills-functionality
Add Junie native skills propagation to `.junie/skills` Fixes #395
2 parents 07b20a1 + b668bdf commit f4e98b7

File tree

8 files changed

+432
-72
lines changed

8 files changed

+432
-72
lines changed

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
7373
| Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - |
7474
| Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - |
7575
| Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` |
76-
| Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | - |
76+
| Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | `.junie/skills/` |
7777
| AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - |
7878
| Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` |
7979
| OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` |
@@ -325,15 +325,15 @@ ruler revert [options]
325325

326326
### Options
327327

328-
| Option | Description |
329-
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330-
| `--project-root <path>` | Path to your project's root (default: current directory) |
328+
| Option | Description |
329+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
330+
| `--project-root <path>` | Path to your project's root (default: current directory) |
331331
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, factory, firebase, firebender, gemini-cli, goose, jetbrains-ai, jules, junie, kilocode, kiro, mistral, opencode, openhands, pi, qwen, roo, trae, warp, windsurf, zed) |
332-
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
333-
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
334-
| `--dry-run` | Preview changes without actually reverting files |
335-
| `--verbose` / `-v` | Display detailed output during execution |
336-
| `--local-only` | Only search for local .ruler directories, ignore global config |
332+
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
333+
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
334+
| `--dry-run` | Preview changes without actually reverting files |
335+
| `--verbose` / `-v` | Display detailed output during execution |
336+
| `--local-only` | Only search for local .ruler directories, ignore global config |
337337

338338
### Common Examples
339339

@@ -594,6 +594,7 @@ Skills are specialized knowledge packages that extend AI agent capabilities with
594594
- **Mistral Vibe**: `.vibe/skills/`
595595
- **Roo Code**: `.roo/skills/`
596596
- **Gemini CLI**: `.gemini/skills/`
597+
- **Junie**: `.junie/skills/`
597598
- **Cursor**: `.cursor/skills/`
598599

599600
### Skills Directory Structure
@@ -659,13 +660,14 @@ When skills support is enabled and gitignore integration is active, Ruler automa
659660
- `.vibe/skills/` (for Mistral Vibe)
660661
- `.roo/skills/` (for Roo Code)
661662
- `.gemini/skills/` (for Gemini CLI)
663+
- `.junie/skills/` (for Junie)
662664
- `.cursor/skills/` (for Cursor)
663665

664666
to your `.gitignore` file within the managed Ruler block.
665667

666668
### Requirements
667669

668-
- **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Cursor): No additional requirements.
670+
- **For agents with native skills support** (Claude Code, GitHub Copilot, Kilo Code, OpenAI Codex CLI, OpenCode, Pi Coding Agent, Goose, Amp, Antigravity, Factory Droid, Mistral Vibe, Roo Code, Gemini CLI, Junie, Cursor): No additional requirements.
669671

670672
### Validation
671673

@@ -718,6 +720,7 @@ ruler apply
718720
# - Mistral Vibe: .vibe/skills/my-skill/
719721
# - Roo Code: .roo/skills/my-skill/
720722
# - Gemini CLI: .gemini/skills/my-skill/
723+
# - Junie: .junie/skills/my-skill/
721724
# - Cursor: .cursor/skills/my-skill/
722725
```
723726

src/agents/JunieAgent.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ export class JunieAgent extends AbstractAgent {
2424
supportsMcpRemote(): boolean {
2525
return true;
2626
}
27+
28+
supportsNativeSkills(): boolean {
29+
return true;
30+
}
2731
}

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const GOOSE_SKILLS_PATH = '.agents/skills';
6363
export const VIBE_SKILLS_PATH = '.vibe/skills';
6464
export const ROO_SKILLS_PATH = '.roo/skills';
6565
export const GEMINI_SKILLS_PATH = '.gemini/skills';
66+
export const JUNIE_SKILLS_PATH = '.junie/skills';
6667
export const CURSOR_SKILLS_PATH = '.cursor/skills';
6768
export const FACTORY_SKILLS_PATH = '.factory/skills';
6869
export const ANTIGRAVITY_SKILLS_PATH = '.agent/skills';

src/core/SkillsProcessor.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
VIBE_SKILLS_PATH,
1212
ROO_SKILLS_PATH,
1313
GEMINI_SKILLS_PATH,
14+
JUNIE_SKILLS_PATH,
1415
CURSOR_SKILLS_PATH,
1516
FACTORY_SKILLS_PATH,
1617
ANTIGRAVITY_SKILLS_PATH,
@@ -68,6 +69,7 @@ export async function getSkillsGitignorePaths(
6869
VIBE_SKILLS_PATH,
6970
ROO_SKILLS_PATH,
7071
GEMINI_SKILLS_PATH,
72+
JUNIE_SKILLS_PATH,
7173
CURSOR_SKILLS_PATH,
7274
FACTORY_SKILLS_PATH,
7375
ANTIGRAVITY_SKILLS_PATH,
@@ -83,6 +85,7 @@ export async function getSkillsGitignorePaths(
8385
vibe: VIBE_SKILLS_PATH,
8486
roo: ROO_SKILLS_PATH,
8587
gemini: GEMINI_SKILLS_PATH,
88+
junie: JUNIE_SKILLS_PATH,
8689
cursor: CURSOR_SKILLS_PATH,
8790
factory: FACTORY_SKILLS_PATH,
8891
antigravity: ANTIGRAVITY_SKILLS_PATH,
@@ -125,6 +128,7 @@ type SkillTarget =
125128
| 'vibe'
126129
| 'roo'
127130
| 'gemini'
131+
| 'junie'
128132
| 'cursor'
129133
| 'factory'
130134
| 'antigravity';
@@ -138,6 +142,7 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map<SkillTarget, readonly string[]>([
138142
['vibe', ['mistral']],
139143
['roo', ['roo']],
140144
['gemini', ['gemini-cli']],
145+
['junie', ['junie']],
141146
['cursor', ['cursor']],
142147
['factory', ['factory']],
143148
['antigravity', ['antigravity']],
@@ -177,6 +182,7 @@ async function cleanupSkillsDirectories(
177182
const vibeSkillsPath = path.join(projectRoot, VIBE_SKILLS_PATH);
178183
const rooSkillsPath = path.join(projectRoot, ROO_SKILLS_PATH);
179184
const geminiSkillsPath = path.join(projectRoot, GEMINI_SKILLS_PATH);
185+
const junieSkillsPath = path.join(projectRoot, JUNIE_SKILLS_PATH);
180186
const cursorSkillsPath = path.join(projectRoot, CURSOR_SKILLS_PATH);
181187
const factorySkillsPath = path.join(projectRoot, FACTORY_SKILLS_PATH);
182188
const antigravitySkillsPath = path.join(projectRoot, ANTIGRAVITY_SKILLS_PATH);
@@ -349,6 +355,27 @@ async function cleanupSkillsDirectories(
349355
// Directory doesn't exist, nothing to clean
350356
}
351357

358+
// Clean up .junie/skills
359+
try {
360+
await fs.access(junieSkillsPath);
361+
if (dryRun) {
362+
logVerboseInfo(
363+
`DRY RUN: Would remove ${JUNIE_SKILLS_PATH}`,
364+
verbose,
365+
dryRun,
366+
);
367+
} else {
368+
await fs.rm(junieSkillsPath, { recursive: true, force: true });
369+
logVerboseInfo(
370+
`Removed ${JUNIE_SKILLS_PATH} (skills disabled)`,
371+
verbose,
372+
dryRun,
373+
);
374+
}
375+
} catch {
376+
// Directory doesn't exist, nothing to clean
377+
}
378+
352379
// Clean up .cursor/skills
353380
try {
354381
await fs.access(cursorSkillsPath);
@@ -573,6 +600,15 @@ export async function propagateSkills(
573600
await propagateSkillsForGemini(projectRoot, { dryRun });
574601
}
575602

603+
if (selectedTargets.has('junie')) {
604+
logVerboseInfo(
605+
`Copying skills to ${JUNIE_SKILLS_PATH} for Junie`,
606+
verbose,
607+
dryRun,
608+
);
609+
await propagateSkillsForJunie(projectRoot, { dryRun });
610+
}
611+
576612
if (selectedTargets.has('cursor')) {
577613
logVerboseInfo(
578614
`Copying skills to ${CURSOR_SKILLS_PATH} for Cursor`,
@@ -1067,6 +1103,55 @@ export async function propagateSkillsForGemini(
10671103
return [];
10681104
}
10691105

1106+
/**
1107+
* Propagates skills for Junie by copying .ruler/skills to .junie/skills.
1108+
* Uses atomic replace to ensure safe overwriting of existing skills.
1109+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
1110+
*/
1111+
export async function propagateSkillsForJunie(
1112+
projectRoot: string,
1113+
options: { dryRun: boolean },
1114+
): Promise<string[]> {
1115+
const skillsDir = path.join(projectRoot, RULER_SKILLS_PATH);
1116+
const junieSkillsPath = path.join(projectRoot, JUNIE_SKILLS_PATH);
1117+
const junieDir = path.dirname(junieSkillsPath);
1118+
1119+
try {
1120+
await fs.access(skillsDir);
1121+
} catch {
1122+
return [];
1123+
}
1124+
1125+
if (options.dryRun) {
1126+
return [`Copy skills from ${RULER_SKILLS_PATH} to ${JUNIE_SKILLS_PATH}`];
1127+
}
1128+
1129+
await fs.mkdir(junieDir, { recursive: true });
1130+
1131+
const tempDir = path.join(junieDir, `skills.tmp-${Date.now()}`);
1132+
1133+
try {
1134+
await copySkillsDirectory(skillsDir, tempDir);
1135+
1136+
try {
1137+
await fs.rm(junieSkillsPath, { recursive: true, force: true });
1138+
} catch {
1139+
// Target didn't exist, that's fine
1140+
}
1141+
1142+
await fs.rename(tempDir, junieSkillsPath);
1143+
} catch (error) {
1144+
try {
1145+
await fs.rm(tempDir, { recursive: true, force: true });
1146+
} catch {
1147+
// Ignore cleanup errors
1148+
}
1149+
throw error;
1150+
}
1151+
1152+
return [];
1153+
}
1154+
10701155
/**
10711156
* Propagates skills for Cursor by copying .ruler/skills to .cursor/skills.
10721157
* Uses atomic replace to ensure safe overwriting of existing skills.

0 commit comments

Comments
 (0)