@intellectronica/ruler 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/dist/agents/GooseAgent.js +3 -0
- package/dist/agents/MistralVibeAgent.js +171 -0
- package/dist/agents/OpenCodeAgent.js +3 -0
- package/dist/agents/index.js +2 -0
- package/dist/constants.js +4 -1
- package/dist/core/SkillsProcessor.js +209 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
84
84
|
| Warp | `WARP.md` | - |
|
|
85
85
|
| Kiro | `.kiro/steering/ruler_kiro_instructions.md` | `.kiro/settings/mcp.json` |
|
|
86
86
|
| Firebender | `firebender.json` | `firebender.json` (rules and MCP in same file) |
|
|
87
|
+
| Mistral Vibe | `AGENTS.md` | `.vibe/config.toml` |
|
|
87
88
|
|
|
88
89
|
## Getting Started
|
|
89
90
|
|
|
@@ -319,7 +320,7 @@ ruler revert [options]
|
|
|
319
320
|
| Option | Description |
|
|
320
321
|
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
321
322
|
| `--project-root <path>` | Path to your project's root (default: current directory) |
|
|
322
|
-
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
|
|
323
|
+
| `--agents <agent1,agent2,...>` | Comma-separated list of agent names to revert (agentsmd, aider, amazonqcli, amp, antigravity, augmentcode, claude, cline, codex, copilot, crush, cursor, firebase, firebender, gemini-cli, goose, jules, junie, kilocode, kiro, mistral, opencode, openhands, qwen, roo, trae, warp, windsurf, zed) |
|
|
323
324
|
| `--config <path>` | Path to a custom `ruler.toml` configuration file |
|
|
324
325
|
| `--keep-backups` | Keep backup files (.bak) after restoration (default: false) |
|
|
325
326
|
| `--dry-run` | Preview changes without actually reverting files |
|
|
@@ -557,15 +558,20 @@ export CODEX_HOME="$(pwd)/.codex"
|
|
|
557
558
|
|
|
558
559
|
## Skills Support (Experimental)
|
|
559
560
|
|
|
560
|
-
**⚠️ Experimental Feature**: Skills support is currently experimental and requires `uv` (the Python package manager) to be installed on your system for MCP-based agent integration.
|
|
561
|
+
**⚠️ Experimental Feature**: Skills support is currently experimental and requires `uv` (the Python package manager) to be installed on your system for MCP-based agent integration (agents without native skills support).
|
|
561
562
|
|
|
562
|
-
Ruler can manage and propagate
|
|
563
|
+
Ruler can manage and propagate skills to supported AI agents. Skills are stored in `.ruler/skills/` and are automatically distributed to compatible agents when you run `ruler apply`.
|
|
563
564
|
|
|
564
565
|
### How It Works
|
|
565
566
|
|
|
566
567
|
Skills are specialized knowledge packages that extend AI agent capabilities with domain-specific expertise, workflows, or tool integrations. Ruler discovers skills in your `.ruler/skills/` directory and propagates them to compatible agents:
|
|
567
568
|
|
|
568
|
-
- **
|
|
569
|
+
- **Agents with native skills support**: Skills are copied directly to each agent's native skills directory:
|
|
570
|
+
- **Claude Code**: `.claude/skills/`
|
|
571
|
+
- **GitHub Copilot**: `.claude/skills/` (shared with Claude Code)
|
|
572
|
+
- **OpenAI Codex CLI**: `.codex/skills/`
|
|
573
|
+
- **OpenCode**: `.opencode/skill/`
|
|
574
|
+
- **Goose**: `.agents/skills/`
|
|
569
575
|
- **Other MCP-compatible agents**: Skills are copied to `.skillz/` and a Skillz MCP server is automatically configured via `uvx`
|
|
570
576
|
|
|
571
577
|
### Skills Directory Structure
|
|
@@ -615,12 +621,14 @@ enabled = true # or false to disable
|
|
|
615
621
|
|
|
616
622
|
### Skillz MCP Server
|
|
617
623
|
|
|
618
|
-
For agents that support MCP but don't have native skills support
|
|
624
|
+
For agents that support MCP but don't have native skills support, Ruler automatically:
|
|
619
625
|
|
|
620
626
|
1. Copies skills to `.skillz/` directory
|
|
621
627
|
2. Configures a Skillz MCP server in the agent's configuration
|
|
622
628
|
3. Uses `uvx` to launch the server with the absolute path to `.skillz`
|
|
623
629
|
|
|
630
|
+
Agents using native skills support (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, and Goose) **do not** use the Skillz MCP server and instead use their own native skills directories.
|
|
631
|
+
|
|
624
632
|
Example auto-generated MCP server configuration:
|
|
625
633
|
|
|
626
634
|
```toml
|
|
@@ -633,15 +641,18 @@ args = ["skillz@latest", "/absolute/path/to/project/.skillz"]
|
|
|
633
641
|
|
|
634
642
|
When skills support is enabled and gitignore integration is active, Ruler automatically adds:
|
|
635
643
|
|
|
636
|
-
- `.claude/skills/` (for Claude Code
|
|
637
|
-
- `.
|
|
644
|
+
- `.claude/skills/` (for Claude Code and GitHub Copilot)
|
|
645
|
+
- `.codex/skills/` (for OpenAI Codex CLI)
|
|
646
|
+
- `.opencode/skill/` (for OpenCode)
|
|
647
|
+
- `.agents/skills/` (for Goose)
|
|
648
|
+
- `.skillz/` (for other MCP-based agents)
|
|
638
649
|
|
|
639
650
|
to your `.gitignore` file within the managed Ruler block.
|
|
640
651
|
|
|
641
652
|
### Requirements
|
|
642
653
|
|
|
643
|
-
- **For Claude Code
|
|
644
|
-
- **For MCP agents**: `uv` must be installed and available in your PATH
|
|
654
|
+
- **For agents with native skills support** (Claude Code, GitHub Copilot, OpenAI Codex CLI, OpenCode, Goose): No additional requirements
|
|
655
|
+
- **For other MCP agents**: `uv` must be installed and available in your PATH
|
|
645
656
|
```bash
|
|
646
657
|
# Install uv if needed
|
|
647
658
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
@@ -688,7 +699,10 @@ EOF
|
|
|
688
699
|
ruler apply
|
|
689
700
|
|
|
690
701
|
# 3. Skills are now available to compatible agents:
|
|
691
|
-
# - Claude Code: .claude/skills/my-skill/
|
|
702
|
+
# - Claude Code & GitHub Copilot: .claude/skills/my-skill/
|
|
703
|
+
# - OpenAI Codex CLI: .codex/skills/my-skill/
|
|
704
|
+
# - OpenCode: .opencode/skill/my-skill/
|
|
705
|
+
# - Goose: .agents/skills/my-skill/
|
|
692
706
|
# - Other MCP agents: .skillz/my-skill/ + Skillz MCP server configured
|
|
693
707
|
```
|
|
694
708
|
|
|
@@ -0,0 +1,171 @@
|
|
|
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.MistralVibeAgent = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const toml_1 = require("@iarna/toml");
|
|
40
|
+
const AgentsMdAgent_1 = require("./AgentsMdAgent");
|
|
41
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
42
|
+
const constants_1 = require("../constants");
|
|
43
|
+
/**
|
|
44
|
+
* Mistral Vibe CLI agent adapter.
|
|
45
|
+
* Propagates rules to AGENTS.md and MCP servers to .vibe/config.toml.
|
|
46
|
+
*/
|
|
47
|
+
class MistralVibeAgent {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.agentsMdAgent = new AgentsMdAgent_1.AgentsMdAgent();
|
|
50
|
+
}
|
|
51
|
+
getIdentifier() {
|
|
52
|
+
return 'mistral';
|
|
53
|
+
}
|
|
54
|
+
getName() {
|
|
55
|
+
return 'Mistral';
|
|
56
|
+
}
|
|
57
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig, backup = true) {
|
|
58
|
+
// First perform idempotent AGENTS.md write via composed AgentsMdAgent
|
|
59
|
+
await this.agentsMdAgent.applyRulerConfig(concatenatedRules, projectRoot, null, {
|
|
60
|
+
outputPath: agentConfig?.outputPath ||
|
|
61
|
+
agentConfig?.outputPathInstructions ||
|
|
62
|
+
undefined,
|
|
63
|
+
}, backup);
|
|
64
|
+
// Handle MCP configuration
|
|
65
|
+
const defaults = this.getDefaultOutputPath(projectRoot);
|
|
66
|
+
const mcpEnabled = agentConfig?.mcp?.enabled ?? true;
|
|
67
|
+
if (mcpEnabled && rulerMcpJson) {
|
|
68
|
+
// Apply MCP server filtering and transformation
|
|
69
|
+
const { filterMcpConfigForAgent } = await Promise.resolve().then(() => __importStar(require('../mcp/capabilities')));
|
|
70
|
+
const filteredMcpConfig = filterMcpConfigForAgent(rulerMcpJson, this);
|
|
71
|
+
if (!filteredMcpConfig) {
|
|
72
|
+
return; // No compatible servers found
|
|
73
|
+
}
|
|
74
|
+
const filteredRulerMcpJson = filteredMcpConfig;
|
|
75
|
+
// Determine the config file path
|
|
76
|
+
const configPath = agentConfig?.outputPathConfig ?? defaults.config;
|
|
77
|
+
// Ensure the parent directory exists
|
|
78
|
+
await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
79
|
+
// Get the merge strategy
|
|
80
|
+
const strategy = agentConfig?.mcp?.strategy ?? 'merge';
|
|
81
|
+
// Transform ruler MCP servers to Vibe format
|
|
82
|
+
const rulerServers = filteredRulerMcpJson.mcpServers || {};
|
|
83
|
+
const vibeServers = [];
|
|
84
|
+
for (const [serverName, serverConfig] of Object.entries(rulerServers)) {
|
|
85
|
+
const vibeServer = {
|
|
86
|
+
name: serverName,
|
|
87
|
+
transport: this.determineTransport(serverConfig),
|
|
88
|
+
};
|
|
89
|
+
// Handle stdio servers
|
|
90
|
+
if (serverConfig.command) {
|
|
91
|
+
vibeServer.command = serverConfig.command;
|
|
92
|
+
if (serverConfig.args) {
|
|
93
|
+
vibeServer.args = serverConfig.args;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Handle remote servers
|
|
97
|
+
if (serverConfig.url) {
|
|
98
|
+
vibeServer.url = serverConfig.url;
|
|
99
|
+
}
|
|
100
|
+
// Handle headers
|
|
101
|
+
if (serverConfig.headers) {
|
|
102
|
+
vibeServer.headers = serverConfig.headers;
|
|
103
|
+
}
|
|
104
|
+
// Handle env
|
|
105
|
+
if (serverConfig.env) {
|
|
106
|
+
vibeServer.env = serverConfig.env;
|
|
107
|
+
}
|
|
108
|
+
vibeServers.push(vibeServer);
|
|
109
|
+
}
|
|
110
|
+
// Read existing TOML config if it exists
|
|
111
|
+
let existingConfig = {};
|
|
112
|
+
try {
|
|
113
|
+
const existingContent = await fs_1.promises.readFile(configPath, 'utf8');
|
|
114
|
+
existingConfig = (0, toml_1.parse)(existingContent);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// File doesn't exist or can't be parsed, use empty config
|
|
118
|
+
}
|
|
119
|
+
// Create the updated config
|
|
120
|
+
const updatedConfig = { ...existingConfig };
|
|
121
|
+
if (strategy === 'overwrite') {
|
|
122
|
+
// For overwrite strategy, replace the entire mcp_servers array
|
|
123
|
+
updatedConfig.mcp_servers = vibeServers;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// For merge strategy, merge by server name
|
|
127
|
+
const existingServers = updatedConfig.mcp_servers || [];
|
|
128
|
+
// Keep existing servers that aren't being overwritten by ruler
|
|
129
|
+
const mergedServers = existingServers.filter((s) => !rulerServers[s.name]);
|
|
130
|
+
// Add all ruler servers
|
|
131
|
+
mergedServers.push(...vibeServers);
|
|
132
|
+
updatedConfig.mcp_servers = mergedServers;
|
|
133
|
+
}
|
|
134
|
+
// Convert to TOML and write
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
const tomlContent = (0, toml_1.stringify)(updatedConfig);
|
|
137
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(configPath, tomlContent);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Determines the transport type based on server configuration.
|
|
142
|
+
*/
|
|
143
|
+
determineTransport(server) {
|
|
144
|
+
if (server.command) {
|
|
145
|
+
return 'stdio';
|
|
146
|
+
}
|
|
147
|
+
if (server.url) {
|
|
148
|
+
// Default to http for remote servers
|
|
149
|
+
// Could potentially detect streamable-http based on URL patterns if needed
|
|
150
|
+
return 'http';
|
|
151
|
+
}
|
|
152
|
+
return 'stdio';
|
|
153
|
+
}
|
|
154
|
+
getDefaultOutputPath(projectRoot) {
|
|
155
|
+
return {
|
|
156
|
+
instructions: path.join(projectRoot, constants_1.DEFAULT_RULES_FILENAME),
|
|
157
|
+
config: path.join(projectRoot, '.vibe', 'config.toml'),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
supportsMcpStdio() {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
supportsMcpRemote() {
|
|
164
|
+
return true; // Mistral Vibe supports http and streamable-http transports
|
|
165
|
+
}
|
|
166
|
+
supportsNativeSkills() {
|
|
167
|
+
// Mistral Vibe supports native skills in .vibe/skills/
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
exports.MistralVibeAgent = MistralVibeAgent;
|
package/dist/agents/index.js
CHANGED
|
@@ -32,6 +32,7 @@ const TraeAgent_1 = require("./TraeAgent");
|
|
|
32
32
|
const AmazonQCliAgent_1 = require("./AmazonQCliAgent");
|
|
33
33
|
const FirebenderAgent_1 = require("./FirebenderAgent");
|
|
34
34
|
const AntigravityAgent_1 = require("./AntigravityAgent");
|
|
35
|
+
const MistralVibeAgent_1 = require("./MistralVibeAgent");
|
|
35
36
|
exports.allAgents = [
|
|
36
37
|
new CopilotAgent_1.CopilotAgent(),
|
|
37
38
|
new ClaudeAgent_1.ClaudeAgent(),
|
|
@@ -61,6 +62,7 @@ exports.allAgents = [
|
|
|
61
62
|
new AmazonQCliAgent_1.AmazonQCliAgent(),
|
|
62
63
|
new FirebenderAgent_1.FirebenderAgent(),
|
|
63
64
|
new AntigravityAgent_1.AntigravityAgent(),
|
|
65
|
+
new MistralVibeAgent_1.MistralVibeAgent(),
|
|
64
66
|
];
|
|
65
67
|
/**
|
|
66
68
|
* Generates a comma-separated list of agent identifiers for CLI help text.
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
|
|
3
|
+
exports.SKILLZ_MCP_SERVER_NAME = exports.SKILL_MD_FILENAME = exports.SKILLZ_DIR = exports.VIBE_SKILLS_PATH = exports.GOOSE_SKILLS_PATH = exports.OPENCODE_SKILLS_PATH = exports.CODEX_SKILLS_PATH = exports.CLAUDE_SKILLS_PATH = exports.RULER_SKILLS_PATH = exports.SKILLS_DIR = exports.DEFAULT_RULES_FILENAME = exports.ERROR_PREFIX = void 0;
|
|
4
4
|
exports.actionPrefix = actionPrefix;
|
|
5
5
|
exports.createRulerError = createRulerError;
|
|
6
6
|
exports.logVerbose = logVerbose;
|
|
@@ -54,6 +54,9 @@ exports.SKILLS_DIR = 'skills';
|
|
|
54
54
|
exports.RULER_SKILLS_PATH = '.ruler/skills';
|
|
55
55
|
exports.CLAUDE_SKILLS_PATH = '.claude/skills';
|
|
56
56
|
exports.CODEX_SKILLS_PATH = '.codex/skills';
|
|
57
|
+
exports.OPENCODE_SKILLS_PATH = '.opencode/skill';
|
|
58
|
+
exports.GOOSE_SKILLS_PATH = '.agents/skills';
|
|
59
|
+
exports.VIBE_SKILLS_PATH = '.vibe/skills';
|
|
57
60
|
exports.SKILLZ_DIR = '.skillz';
|
|
58
61
|
exports.SKILL_MD_FILENAME = 'SKILL.md';
|
|
59
62
|
exports.SKILLZ_MCP_SERVER_NAME = 'skillz';
|
|
@@ -38,6 +38,9 @@ exports.getSkillsGitignorePaths = getSkillsGitignorePaths;
|
|
|
38
38
|
exports.propagateSkills = propagateSkills;
|
|
39
39
|
exports.propagateSkillsForClaude = propagateSkillsForClaude;
|
|
40
40
|
exports.propagateSkillsForCodex = propagateSkillsForCodex;
|
|
41
|
+
exports.propagateSkillsForOpenCode = propagateSkillsForOpenCode;
|
|
42
|
+
exports.propagateSkillsForGoose = propagateSkillsForGoose;
|
|
43
|
+
exports.propagateSkillsForVibe = propagateSkillsForVibe;
|
|
41
44
|
exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
42
45
|
exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
|
|
43
46
|
const path = __importStar(require("path"));
|
|
@@ -75,10 +78,13 @@ async function getSkillsGitignorePaths(projectRoot) {
|
|
|
75
78
|
return [];
|
|
76
79
|
}
|
|
77
80
|
// Import here to avoid circular dependency
|
|
78
|
-
const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
81
|
+
const { CLAUDE_SKILLS_PATH, CODEX_SKILLS_PATH, OPENCODE_SKILLS_PATH, GOOSE_SKILLS_PATH, VIBE_SKILLS_PATH, SKILLZ_DIR, } = await Promise.resolve().then(() => __importStar(require('../constants')));
|
|
79
82
|
return [
|
|
80
83
|
path.join(projectRoot, CLAUDE_SKILLS_PATH),
|
|
81
84
|
path.join(projectRoot, CODEX_SKILLS_PATH),
|
|
85
|
+
path.join(projectRoot, OPENCODE_SKILLS_PATH),
|
|
86
|
+
path.join(projectRoot, GOOSE_SKILLS_PATH),
|
|
87
|
+
path.join(projectRoot, VIBE_SKILLS_PATH),
|
|
82
88
|
path.join(projectRoot, SKILLZ_DIR),
|
|
83
89
|
];
|
|
84
90
|
}
|
|
@@ -102,12 +108,15 @@ function warnOnceExperimentalAndUv(verbose, dryRun) {
|
|
|
102
108
|
(0, constants_1.logWarn)('Skills MCP server (Skillz) requires uv. Install: https://github.com/astral-sh/uv', dryRun);
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
105
|
-
* Cleans up skills directories
|
|
111
|
+
* Cleans up skills directories when skills are disabled.
|
|
106
112
|
* This ensures that stale skills from previous runs don't persist when skills are turned off.
|
|
107
113
|
*/
|
|
108
114
|
async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
109
115
|
const claudeSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
110
116
|
const codexSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
|
|
117
|
+
const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
|
|
118
|
+
const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
|
|
119
|
+
const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
|
|
111
120
|
const skillzPath = path.join(projectRoot, constants_1.SKILLZ_DIR);
|
|
112
121
|
// Clean up .claude/skills
|
|
113
122
|
try {
|
|
@@ -137,6 +146,48 @@ async function cleanupSkillsDirectories(projectRoot, dryRun, verbose) {
|
|
|
137
146
|
catch {
|
|
138
147
|
// Directory doesn't exist, nothing to clean
|
|
139
148
|
}
|
|
149
|
+
// Clean up .opencode/skill
|
|
150
|
+
try {
|
|
151
|
+
await fs.access(opencodeSkillsPath);
|
|
152
|
+
if (dryRun) {
|
|
153
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.OPENCODE_SKILLS_PATH}`, verbose, dryRun);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
|
|
157
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.OPENCODE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Directory doesn't exist, nothing to clean
|
|
162
|
+
}
|
|
163
|
+
// Clean up .agents/skills
|
|
164
|
+
try {
|
|
165
|
+
await fs.access(gooseSkillsPath);
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.GOOSE_SKILLS_PATH}`, verbose, dryRun);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await fs.rm(gooseSkillsPath, { recursive: true, force: true });
|
|
171
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.GOOSE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Directory doesn't exist, nothing to clean
|
|
176
|
+
}
|
|
177
|
+
// Clean up .vibe/skills
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(vibeSkillsPath);
|
|
180
|
+
if (dryRun) {
|
|
181
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${constants_1.VIBE_SKILLS_PATH}`, verbose, dryRun);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
await fs.rm(vibeSkillsPath, { recursive: true, force: true });
|
|
185
|
+
(0, constants_1.logVerboseInfo)(`Removed ${constants_1.VIBE_SKILLS_PATH} (skills disabled)`, verbose, dryRun);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Directory doesn't exist, nothing to clean
|
|
190
|
+
}
|
|
140
191
|
// Clean up .skillz
|
|
141
192
|
try {
|
|
142
193
|
await fs.access(skillzPath);
|
|
@@ -199,6 +250,12 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
199
250
|
await propagateSkillsForClaude(projectRoot, { dryRun });
|
|
200
251
|
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.CODEX_SKILLS_PATH} for OpenAI Codex CLI`, verbose, dryRun);
|
|
201
252
|
await propagateSkillsForCodex(projectRoot, { dryRun });
|
|
253
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.OPENCODE_SKILLS_PATH} for OpenCode`, verbose, dryRun);
|
|
254
|
+
await propagateSkillsForOpenCode(projectRoot, { dryRun });
|
|
255
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.GOOSE_SKILLS_PATH} for Goose`, verbose, dryRun);
|
|
256
|
+
await propagateSkillsForGoose(projectRoot, { dryRun });
|
|
257
|
+
(0, constants_1.logVerboseInfo)(`Copying skills to ${constants_1.VIBE_SKILLS_PATH} for Mistral Vibe`, verbose, dryRun);
|
|
258
|
+
await propagateSkillsForVibe(projectRoot, { dryRun });
|
|
202
259
|
}
|
|
203
260
|
// Copy to .skillz directory if needed
|
|
204
261
|
if (hasMcpAgent) {
|
|
@@ -306,6 +363,156 @@ async function propagateSkillsForCodex(projectRoot, options) {
|
|
|
306
363
|
}
|
|
307
364
|
return [];
|
|
308
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Propagates skills for OpenCode by copying .ruler/skills to .opencode/skill.
|
|
368
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
369
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
370
|
+
*/
|
|
371
|
+
async function propagateSkillsForOpenCode(projectRoot, options) {
|
|
372
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
373
|
+
const opencodeSkillsPath = path.join(projectRoot, constants_1.OPENCODE_SKILLS_PATH);
|
|
374
|
+
const opencodeDir = path.dirname(opencodeSkillsPath);
|
|
375
|
+
// Check if source skills directory exists
|
|
376
|
+
try {
|
|
377
|
+
await fs.access(skillsDir);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// No skills directory - return empty
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
if (options.dryRun) {
|
|
384
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.OPENCODE_SKILLS_PATH}`];
|
|
385
|
+
}
|
|
386
|
+
// Ensure .opencode directory exists
|
|
387
|
+
await fs.mkdir(opencodeDir, { recursive: true });
|
|
388
|
+
// Use atomic replace: copy to temp, then rename
|
|
389
|
+
const tempDir = path.join(opencodeDir, `skill.tmp-${Date.now()}`);
|
|
390
|
+
try {
|
|
391
|
+
// Copy to temp directory
|
|
392
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
393
|
+
// Atomically replace the target
|
|
394
|
+
// First, remove existing target if it exists
|
|
395
|
+
try {
|
|
396
|
+
await fs.rm(opencodeSkillsPath, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Target didn't exist, that's fine
|
|
400
|
+
}
|
|
401
|
+
// Rename temp to target
|
|
402
|
+
await fs.rename(tempDir, opencodeSkillsPath);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
// Clean up temp directory on error
|
|
406
|
+
try {
|
|
407
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Ignore cleanup errors
|
|
411
|
+
}
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Propagates skills for Goose by copying .ruler/skills to .agents/skills.
|
|
418
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
419
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
420
|
+
*/
|
|
421
|
+
async function propagateSkillsForGoose(projectRoot, options) {
|
|
422
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
423
|
+
const gooseSkillsPath = path.join(projectRoot, constants_1.GOOSE_SKILLS_PATH);
|
|
424
|
+
const gooseDir = path.dirname(gooseSkillsPath);
|
|
425
|
+
// Check if source skills directory exists
|
|
426
|
+
try {
|
|
427
|
+
await fs.access(skillsDir);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// No skills directory - return empty
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
if (options.dryRun) {
|
|
434
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.GOOSE_SKILLS_PATH}`];
|
|
435
|
+
}
|
|
436
|
+
// Ensure .agents directory exists
|
|
437
|
+
await fs.mkdir(gooseDir, { recursive: true });
|
|
438
|
+
// Use atomic replace: copy to temp, then rename
|
|
439
|
+
const tempDir = path.join(gooseDir, `skills.tmp-${Date.now()}`);
|
|
440
|
+
try {
|
|
441
|
+
// Copy to temp directory
|
|
442
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
443
|
+
// Atomically replace the target
|
|
444
|
+
// First, remove existing target if it exists
|
|
445
|
+
try {
|
|
446
|
+
await fs.rm(gooseSkillsPath, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Target didn't exist, that's fine
|
|
450
|
+
}
|
|
451
|
+
// Rename temp to target
|
|
452
|
+
await fs.rename(tempDir, gooseSkillsPath);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
// Clean up temp directory on error
|
|
456
|
+
try {
|
|
457
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Ignore cleanup errors
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Propagates skills for Mistral Vibe by copying .ruler/skills to .vibe/skills.
|
|
468
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
469
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
470
|
+
*/
|
|
471
|
+
async function propagateSkillsForVibe(projectRoot, options) {
|
|
472
|
+
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
473
|
+
const vibeSkillsPath = path.join(projectRoot, constants_1.VIBE_SKILLS_PATH);
|
|
474
|
+
const vibeDir = path.dirname(vibeSkillsPath);
|
|
475
|
+
// Check if source skills directory exists
|
|
476
|
+
try {
|
|
477
|
+
await fs.access(skillsDir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// No skills directory - return empty
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
if (options.dryRun) {
|
|
484
|
+
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.VIBE_SKILLS_PATH}`];
|
|
485
|
+
}
|
|
486
|
+
// Ensure .vibe directory exists
|
|
487
|
+
await fs.mkdir(vibeDir, { recursive: true });
|
|
488
|
+
// Use atomic replace: copy to temp, then rename
|
|
489
|
+
const tempDir = path.join(vibeDir, `skills.tmp-${Date.now()}`);
|
|
490
|
+
try {
|
|
491
|
+
// Copy to temp directory
|
|
492
|
+
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
493
|
+
// Atomically replace the target
|
|
494
|
+
// First, remove existing target if it exists
|
|
495
|
+
try {
|
|
496
|
+
await fs.rm(vibeSkillsPath, { recursive: true, force: true });
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// Target didn't exist, that's fine
|
|
500
|
+
}
|
|
501
|
+
// Rename temp to target
|
|
502
|
+
await fs.rename(tempDir, vibeSkillsPath);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
// Clean up temp directory on error
|
|
506
|
+
try {
|
|
507
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Ignore cleanup errors
|
|
511
|
+
}
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
309
516
|
/**
|
|
310
517
|
* Propagates skills for MCP agents by copying .ruler/skills to .skillz.
|
|
311
518
|
* Uses atomic replace to ensure safe overwriting of existing skills.
|