@intellectronica/ruler 0.3.40 → 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.
- package/README.md +59 -46
- package/dist/agents/AbstractAgent.d.ts +53 -0
- package/dist/agents/AgentsMdAgent.d.ts +14 -0
- package/dist/agents/AiderAgent.d.ts +14 -0
- package/dist/agents/AiderAgent.js +3 -1
- package/dist/agents/AmazonQCliAgent.d.ts +13 -0
- package/dist/agents/AmpAgent.d.ts +6 -0
- package/dist/agents/AntigravityAgent.d.ts +10 -0
- package/dist/agents/AugmentCodeAgent.d.ts +13 -0
- package/dist/agents/ClaudeAgent.d.ts +13 -0
- package/dist/agents/ClineAgent.d.ts +9 -0
- package/dist/agents/CodexCliAgent.d.ts +31 -0
- package/dist/agents/CopilotAgent.d.ts +20 -0
- package/dist/agents/CrushAgent.d.ts +14 -0
- package/dist/agents/CrushAgent.js +5 -2
- package/dist/agents/CursorAgent.d.ts +17 -0
- package/dist/agents/FactoryDroidAgent.d.ts +13 -0
- package/dist/agents/FirebaseAgent.d.ts +11 -0
- package/dist/agents/FirebenderAgent.d.ts +36 -0
- package/dist/agents/GeminiCliAgent.d.ts +11 -0
- package/dist/agents/GeminiCliAgent.js +2 -2
- package/dist/agents/GooseAgent.d.ts +12 -0
- package/dist/agents/IAgent.d.ts +72 -0
- package/dist/agents/JetBrainsAiAssistantAgent.d.ts +10 -0
- package/dist/agents/JulesAgent.d.ts +5 -0
- package/dist/agents/JunieAgent.d.ts +12 -0
- package/dist/agents/KiloCodeAgent.d.ts +14 -0
- package/dist/agents/KiroAgent.d.ts +8 -0
- package/dist/agents/MistralVibeAgent.d.ts +31 -0
- package/dist/agents/OpenCodeAgent.d.ts +11 -0
- package/dist/agents/OpenCodeAgent.js +14 -9
- package/dist/agents/OpenHandsAgent.d.ts +8 -0
- package/dist/agents/PiAgent.d.ts +9 -0
- package/dist/agents/QwenCodeAgent.d.ts +10 -0
- package/dist/agents/QwenCodeAgent.js +2 -2
- package/dist/agents/RooCodeAgent.d.ts +16 -0
- package/dist/agents/TraeAgent.d.ts +10 -0
- package/dist/agents/WarpAgent.d.ts +12 -0
- package/dist/agents/WindsurfAgent.d.ts +13 -0
- package/dist/agents/ZedAgent.d.ts +21 -0
- package/dist/agents/ZedAgent.js +5 -2
- package/dist/agents/agent-utils.d.ts +5 -0
- package/dist/agents/agent-utils.js +8 -5
- package/dist/agents/index.d.ts +9 -0
- package/dist/cli/commands.d.ts +4 -0
- package/dist/cli/commands.js +2 -3
- package/dist/cli/handlers.d.ts +41 -0
- package/dist/cli/handlers.js +76 -60
- package/dist/cli/index.d.ts +2 -0
- package/dist/constants.d.ts +35 -0
- package/dist/core/ConfigLoader.d.ts +57 -0
- package/dist/core/ConfigLoader.js +123 -41
- package/dist/core/FileSystemUtils.d.ts +51 -0
- package/dist/core/FileSystemUtils.js +37 -17
- package/dist/core/GitignoreUtils.d.ts +15 -0
- package/dist/core/GitignoreUtils.js +32 -1
- package/dist/core/RuleProcessor.d.ts +8 -0
- package/dist/core/SkillsProcessor.d.ts +127 -0
- package/dist/core/SkillsProcessor.js +104 -218
- package/dist/core/SkillsUtils.d.ts +26 -0
- package/dist/core/SubagentsProcessor.d.ts +38 -0
- package/dist/core/SubagentsProcessor.js +68 -22
- package/dist/core/SubagentsUtils.d.ts +34 -0
- package/dist/core/UnifiedConfigLoader.d.ts +10 -0
- package/dist/core/UnifiedConfigLoader.js +61 -31
- package/dist/core/UnifiedConfigTypes.d.ts +95 -0
- package/dist/core/agent-selection.d.ts +12 -0
- package/dist/core/agent-selection.js +11 -3
- package/dist/core/apply-engine.d.ts +69 -0
- package/dist/core/apply-engine.js +57 -50
- package/dist/core/config-utils.d.ts +14 -0
- package/dist/core/config-utils.js +9 -3
- package/dist/core/hash.d.ts +2 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +42 -0
- package/dist/core/revert-engine.d.ts +36 -0
- package/dist/core/revert-engine.js +70 -9
- package/dist/lib.d.ts +13 -0
- package/dist/lib.js +23 -5
- package/dist/mcp/capabilities.d.ts +20 -0
- package/dist/mcp/merge.d.ts +10 -0
- package/dist/mcp/merge.js +19 -1
- package/dist/mcp/propagateOpenCodeMcp.d.ts +2 -0
- package/dist/mcp/propagateOpenCodeMcp.js +21 -9
- package/dist/mcp/propagateOpenHandsMcp.d.ts +2 -0
- package/dist/mcp/propagateOpenHandsMcp.js +31 -15
- package/dist/mcp/validate.d.ts +7 -0
- package/dist/mcp/validate.js +6 -1
- package/dist/paths/mcp.d.ts +8 -0
- package/dist/paths/mcp.js +33 -4
- package/dist/revert.d.ts +6 -0
- package/dist/revert.js +39 -27
- package/dist/types.d.ts +87 -0
- package/dist/vscode/settings.d.ts +40 -0
- package/package.json +7 -4
package/dist/cli/handlers.js
CHANGED
|
@@ -51,15 +51,41 @@ function assertNotInsideRulerDir(projectRoot) {
|
|
|
51
51
|
process.exit(1);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
function formatCliError(message) {
|
|
55
|
+
return message.startsWith(constants_1.ERROR_PREFIX)
|
|
56
|
+
? message
|
|
57
|
+
: `${constants_1.ERROR_PREFIX} ${message}`;
|
|
58
|
+
}
|
|
59
|
+
function parseCliAgents(agents) {
|
|
60
|
+
if (agents === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const parsedAgents = agents.split(',').map((agent) => agent.trim());
|
|
64
|
+
if (parsedAgents.some((agent) => agent.length === 0)) {
|
|
65
|
+
throw new Error('Empty agent token in --agents. Remove extra commas or provide an agent name.');
|
|
66
|
+
}
|
|
67
|
+
return parsedAgents;
|
|
68
|
+
}
|
|
69
|
+
async function resolveNestedPreference(argv, projectRoot, configPath, localOnly) {
|
|
70
|
+
if (argv.nested !== undefined) {
|
|
71
|
+
// CLI explicitly set nested (either --nested or --no-nested)
|
|
72
|
+
return argv.nested;
|
|
73
|
+
}
|
|
74
|
+
// CLI didn't set nested, check TOML configuration
|
|
75
|
+
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
76
|
+
projectRoot,
|
|
77
|
+
configPath,
|
|
78
|
+
checkGlobal: !localOnly,
|
|
79
|
+
});
|
|
80
|
+
// Use TOML setting if available, otherwise default to false
|
|
81
|
+
return config.nested ?? false;
|
|
82
|
+
}
|
|
54
83
|
/**
|
|
55
84
|
* Handler for the 'apply' command.
|
|
56
85
|
*/
|
|
57
86
|
async function applyHandler(argv) {
|
|
58
87
|
const projectRoot = argv['project-root'];
|
|
59
88
|
assertNotInsideRulerDir(projectRoot);
|
|
60
|
-
const agents = argv.agents
|
|
61
|
-
? argv.agents.split(',').map((a) => a.trim())
|
|
62
|
-
: undefined;
|
|
63
89
|
const configPath = argv.config;
|
|
64
90
|
const mcpEnabled = argv.mcp;
|
|
65
91
|
const mcpStrategy = argv['mcp-overwrite']
|
|
@@ -85,27 +111,6 @@ async function applyHandler(argv) {
|
|
|
85
111
|
else {
|
|
86
112
|
gitignoreLocalPreference = undefined; // Let TOML/default decide
|
|
87
113
|
}
|
|
88
|
-
// Determine nested preference: CLI > TOML > Default (false)
|
|
89
|
-
let nested;
|
|
90
|
-
if (argv.nested !== undefined) {
|
|
91
|
-
// CLI explicitly set nested (either --nested or --no-nested)
|
|
92
|
-
nested = argv.nested;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
// CLI didn't set nested, check TOML configuration
|
|
96
|
-
try {
|
|
97
|
-
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
98
|
-
projectRoot,
|
|
99
|
-
configPath,
|
|
100
|
-
});
|
|
101
|
-
// Use TOML setting if available, otherwise default to false
|
|
102
|
-
nested = config.nested ?? false;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// If config loading fails, use default (false)
|
|
106
|
-
nested = false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
114
|
// Determine skills preference: CLI > TOML > Default (enabled)
|
|
110
115
|
let skillsEnabled;
|
|
111
116
|
if (argv.skills !== undefined) {
|
|
@@ -114,7 +119,7 @@ async function applyHandler(argv) {
|
|
|
114
119
|
else {
|
|
115
120
|
skillsEnabled = undefined; // Let config/default decide
|
|
116
121
|
}
|
|
117
|
-
// Determine subagents preference: CLI > TOML > Default (
|
|
122
|
+
// Determine subagents preference: CLI > TOML > Default (disabled)
|
|
118
123
|
let subagentsEnabled;
|
|
119
124
|
if (argv.subagents !== undefined) {
|
|
120
125
|
subagentsEnabled = argv.subagents;
|
|
@@ -123,12 +128,15 @@ async function applyHandler(argv) {
|
|
|
123
128
|
subagentsEnabled = undefined; // Let config/default decide
|
|
124
129
|
}
|
|
125
130
|
try {
|
|
131
|
+
const agents = parseCliAgents(argv.agents);
|
|
132
|
+
// Determine nested preference: CLI > TOML > Default (false)
|
|
133
|
+
const nested = await resolveNestedPreference(argv, projectRoot, configPath, localOnly);
|
|
126
134
|
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly, nested, backup, skillsEnabled, gitignoreLocalPreference, subagentsEnabled);
|
|
127
135
|
console.log('Ruler apply completed successfully.');
|
|
128
136
|
}
|
|
129
137
|
catch (err) {
|
|
130
138
|
const message = err instanceof Error ? err.message : String(err);
|
|
131
|
-
console.error(
|
|
139
|
+
console.error(formatCliError(message));
|
|
132
140
|
process.exit(1);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -138,23 +146,24 @@ async function applyHandler(argv) {
|
|
|
138
146
|
async function initHandler(argv) {
|
|
139
147
|
const projectRoot = argv['project-root'];
|
|
140
148
|
const isGlobal = argv['global'];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
try {
|
|
150
|
+
const rulerDir = isGlobal
|
|
151
|
+
? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'ruler')
|
|
152
|
+
: path.join(projectRoot, '.ruler');
|
|
153
|
+
await fs.mkdir(rulerDir, { recursive: true });
|
|
154
|
+
const instructionsPath = path.join(rulerDir, constants_1.DEFAULT_RULES_FILENAME); // .ruler/AGENTS.md
|
|
155
|
+
const tomlPath = path.join(rulerDir, 'ruler.toml');
|
|
156
|
+
const exists = async (p) => {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(p);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const DEFAULT_INSTRUCTIONS = `# AGENTS.md\n\nCentralised AI agent instructions. Add coding guidelines, style guides, and project context here.\n\nRuler concatenates all .md files in this directory (and subdirectories), starting with AGENTS.md (if present), then remaining files in sorted order.\n`;
|
|
166
|
+
const DEFAULT_TOML = `# Ruler Configuration File
|
|
158
167
|
# See https://ai.intellectronica.net/ruler for documentation.
|
|
159
168
|
|
|
160
169
|
# To specify which agents are active by default when --agents is not used,
|
|
@@ -169,6 +178,9 @@ async function initHandler(argv) {
|
|
|
169
178
|
# enabled = true
|
|
170
179
|
# local = false # set true to write generated ignores to .git/info/exclude instead
|
|
171
180
|
|
|
181
|
+
# [backup]
|
|
182
|
+
# enabled = true # set false to disable .bak backup files
|
|
183
|
+
|
|
172
184
|
# --- Agent Specific Configurations ---
|
|
173
185
|
# You can enable/disable agents and override their default output paths here.
|
|
174
186
|
# Use lowercase agent identifiers: aider, amp, claude, cline, codex, copilot, cursor, jetbrains-ai, kilocode, pi, windsurf
|
|
@@ -199,20 +211,26 @@ async function initHandler(argv) {
|
|
|
199
211
|
# url = "https://api.example.com/mcp"
|
|
200
212
|
# headers = { Authorization = "Bearer REPLACE_ME" }
|
|
201
213
|
`;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
if (!(await exists(instructionsPath))) {
|
|
215
|
+
// Create new AGENTS.md regardless of legacy presence.
|
|
216
|
+
await fs.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS);
|
|
217
|
+
console.log(`[ruler] Created ${instructionsPath}`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.log(`[ruler] ${constants_1.DEFAULT_RULES_FILENAME} already exists, skipping`);
|
|
221
|
+
}
|
|
222
|
+
if (!(await exists(tomlPath))) {
|
|
223
|
+
await fs.writeFile(tomlPath, DEFAULT_TOML);
|
|
224
|
+
console.log(`[ruler] Created ${tomlPath}`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(`[ruler] ruler.toml already exists, skipping`);
|
|
228
|
+
}
|
|
213
229
|
}
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
catch (err) {
|
|
231
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
232
|
+
console.error(formatCliError(message));
|
|
233
|
+
process.exit(1);
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
/**
|
|
@@ -221,20 +239,18 @@ async function initHandler(argv) {
|
|
|
221
239
|
async function revertHandler(argv) {
|
|
222
240
|
const projectRoot = argv['project-root'];
|
|
223
241
|
assertNotInsideRulerDir(projectRoot);
|
|
224
|
-
const agents = argv.agents
|
|
225
|
-
? argv.agents.split(',').map((a) => a.trim())
|
|
226
|
-
: undefined;
|
|
227
242
|
const configPath = argv.config;
|
|
228
243
|
const keepBackups = argv['keep-backups'];
|
|
229
244
|
const verbose = argv.verbose;
|
|
230
245
|
const dryRun = argv['dry-run'];
|
|
231
246
|
const localOnly = argv['local-only'];
|
|
232
247
|
try {
|
|
248
|
+
const agents = parseCliAgents(argv.agents);
|
|
233
249
|
await (0, revert_1.revertAllAgentConfigs)(projectRoot, agents, configPath, keepBackups, verbose, dryRun, localOnly);
|
|
234
250
|
}
|
|
235
251
|
catch (err) {
|
|
236
252
|
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
-
console.error(
|
|
253
|
+
console.error(formatCliError(message));
|
|
238
254
|
process.exit(1);
|
|
239
255
|
}
|
|
240
256
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const ERROR_PREFIX = "[ruler]";
|
|
2
|
+
export declare const DEFAULT_RULES_FILENAME = "AGENTS.md";
|
|
3
|
+
export declare function actionPrefix(dry: boolean): string;
|
|
4
|
+
export declare function createRulerError(message: string, context?: string): Error;
|
|
5
|
+
export declare function logVerbose(message: string, isVerbose: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* Centralized logging functions with consistent output streams and prefixing.
|
|
8
|
+
* - info/verbose go to stdout (user-visible progress)
|
|
9
|
+
* - warn/error go to stderr (problems)
|
|
10
|
+
*/
|
|
11
|
+
export declare function logInfo(message: string, dryRun?: boolean): void;
|
|
12
|
+
export declare function logWarn(message: string, dryRun?: boolean): void;
|
|
13
|
+
export declare function logError(message: string, dryRun?: boolean): void;
|
|
14
|
+
export declare function logVerboseInfo(message: string, isVerbose: boolean, dryRun?: boolean): void;
|
|
15
|
+
export declare const SKILLS_DIR = "skills";
|
|
16
|
+
export declare const RULER_SKILLS_PATH = ".ruler/skills";
|
|
17
|
+
export declare const CLAUDE_SKILLS_PATH = ".claude/skills";
|
|
18
|
+
export declare const CODEX_SKILLS_PATH = ".codex/skills";
|
|
19
|
+
export declare const OPENCODE_SKILLS_PATH = ".opencode/skills";
|
|
20
|
+
export declare const PI_SKILLS_PATH = ".pi/skills";
|
|
21
|
+
export declare const GOOSE_SKILLS_PATH = ".agents/skills";
|
|
22
|
+
export declare const VIBE_SKILLS_PATH = ".vibe/skills";
|
|
23
|
+
export declare const ROO_SKILLS_PATH = ".roo/skills";
|
|
24
|
+
export declare const GEMINI_SKILLS_PATH = ".gemini/skills";
|
|
25
|
+
export declare const JUNIE_SKILLS_PATH = ".junie/skills";
|
|
26
|
+
export declare const CURSOR_SKILLS_PATH = ".cursor/skills";
|
|
27
|
+
export declare const WINDSURF_SKILLS_PATH = ".windsurf/skills";
|
|
28
|
+
export declare const FACTORY_SKILLS_PATH = ".factory/skills";
|
|
29
|
+
export declare const ANTIGRAVITY_SKILLS_PATH = ".agent/skills";
|
|
30
|
+
export declare const SKILL_MD_FILENAME = "SKILL.md";
|
|
31
|
+
export declare const RULER_SUBAGENTS_PATH = ".ruler/agents";
|
|
32
|
+
export declare const CLAUDE_SUBAGENTS_PATH = ".claude/agents";
|
|
33
|
+
export declare const CURSOR_SUBAGENTS_PATH = ".cursor/agents";
|
|
34
|
+
export declare const CODEX_SUBAGENTS_PATH = ".codex/agents";
|
|
35
|
+
export declare const COPILOT_SUBAGENTS_PATH = ".github/agents";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { McpConfig, GlobalMcpConfig, GitignoreConfig, BackupConfig, SkillsConfig, SubagentsConfig } from '../types';
|
|
2
|
+
/** Test helper — re-arms the deprecation guard so suites can assert it fires. */
|
|
3
|
+
export declare function _resetLegacySubagentsWarningForTests(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for a specific agent as defined in ruler.toml.
|
|
6
|
+
*/
|
|
7
|
+
export interface IAgentConfig {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
outputPath?: string;
|
|
10
|
+
outputPathInstructions?: string;
|
|
11
|
+
outputPathConfig?: string;
|
|
12
|
+
/** MCP propagation config for this agent. */
|
|
13
|
+
mcp?: McpConfig;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parsed ruler configuration values.
|
|
17
|
+
*/
|
|
18
|
+
export interface LoadedConfig {
|
|
19
|
+
/** Agents to run by default, as specified by default_agents. */
|
|
20
|
+
defaultAgents?: string[];
|
|
21
|
+
/** Per-agent configuration overrides. */
|
|
22
|
+
agentConfigs: Record<string, IAgentConfig>;
|
|
23
|
+
/** Command-line agent filters (--agents), if provided. */
|
|
24
|
+
cliAgents?: string[];
|
|
25
|
+
/** Global MCP servers configuration section. */
|
|
26
|
+
mcp?: GlobalMcpConfig;
|
|
27
|
+
/** Gitignore configuration section. */
|
|
28
|
+
gitignore?: GitignoreConfig;
|
|
29
|
+
/** Backup configuration section. */
|
|
30
|
+
backup?: BackupConfig;
|
|
31
|
+
/** Skills configuration section. */
|
|
32
|
+
skills?: SkillsConfig;
|
|
33
|
+
/** Subagents configuration section. */
|
|
34
|
+
subagents?: SubagentsConfig;
|
|
35
|
+
/** Whether to enable nested rule loading from nested .ruler directories. */
|
|
36
|
+
nested?: boolean;
|
|
37
|
+
/** Whether the nested option was explicitly provided in the config. */
|
|
38
|
+
nestedDefined?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Options for loading the ruler configuration.
|
|
42
|
+
*/
|
|
43
|
+
export interface ConfigOptions {
|
|
44
|
+
projectRoot: string;
|
|
45
|
+
/** Path to a custom TOML config file. */
|
|
46
|
+
configPath?: string;
|
|
47
|
+
/** CLI filters from --agents option. */
|
|
48
|
+
cliAgents?: string[];
|
|
49
|
+
/** Whether implicit config discovery may fall back to XDG_CONFIG_HOME/ruler. */
|
|
50
|
+
checkGlobal?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Loads and parses the ruler TOML configuration file, applying defaults.
|
|
54
|
+
* Missing implicit configs return defaults. Explicit configs and existing
|
|
55
|
+
* implicit configs fail fast when missing, unreadable, or invalid.
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadConfig(options: ConfigOptions): Promise<LoadedConfig>;
|
|
@@ -76,13 +76,18 @@ const agentConfigSchema = zod_1.z
|
|
|
76
76
|
// - one nested table per coding-agent integration (`[agents.claude]`, etc.)
|
|
77
77
|
// Reserved keys are validated by the object shape; everything else falls
|
|
78
78
|
// through `catchall` and is treated as a per-agent config record.
|
|
79
|
-
const SUBAGENT_RESERVED_KEYS = new Set([
|
|
79
|
+
const SUBAGENT_RESERVED_KEYS = new Set([
|
|
80
|
+
'enabled',
|
|
81
|
+
'include_in_rules',
|
|
82
|
+
'cleanup_orphaned',
|
|
83
|
+
]);
|
|
80
84
|
const rulerConfigSchema = zod_1.z.object({
|
|
81
85
|
default_agents: zod_1.z.array(zod_1.z.string()).optional(),
|
|
82
86
|
agents: zod_1.z
|
|
83
87
|
.object({
|
|
84
88
|
enabled: zod_1.z.boolean().optional(),
|
|
85
89
|
include_in_rules: zod_1.z.boolean().optional(),
|
|
90
|
+
cleanup_orphaned: zod_1.z.boolean().optional(),
|
|
86
91
|
})
|
|
87
92
|
.catchall(agentConfigSchema)
|
|
88
93
|
.optional(),
|
|
@@ -98,6 +103,11 @@ const rulerConfigSchema = zod_1.z.object({
|
|
|
98
103
|
local: zod_1.z.boolean().optional(),
|
|
99
104
|
})
|
|
100
105
|
.optional(),
|
|
106
|
+
backup: zod_1.z
|
|
107
|
+
.object({
|
|
108
|
+
enabled: zod_1.z.boolean().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
101
111
|
skills: zod_1.z
|
|
102
112
|
.object({
|
|
103
113
|
enabled: zod_1.z.boolean().optional(),
|
|
@@ -111,6 +121,7 @@ const rulerConfigSchema = zod_1.z.object({
|
|
|
111
121
|
.object({
|
|
112
122
|
enabled: zod_1.z.boolean().optional(),
|
|
113
123
|
include_in_rules: zod_1.z.boolean().optional(),
|
|
124
|
+
cleanup_orphaned: zod_1.z.boolean().optional(),
|
|
114
125
|
})
|
|
115
126
|
.optional(),
|
|
116
127
|
nested: zod_1.z.boolean().optional(),
|
|
@@ -138,48 +149,16 @@ function stripSymbols(obj) {
|
|
|
138
149
|
}
|
|
139
150
|
/**
|
|
140
151
|
* Loads and parses the ruler TOML configuration file, applying defaults.
|
|
141
|
-
*
|
|
152
|
+
* Missing implicit configs return defaults. Explicit configs and existing
|
|
153
|
+
* implicit configs fail fast when missing, unreadable, or invalid.
|
|
142
154
|
*/
|
|
143
155
|
async function loadConfig(options) {
|
|
144
156
|
const { projectRoot, configPath, cliAgents } = options;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Try local .ruler/ruler.toml first
|
|
151
|
-
const localConfigFile = path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
152
|
-
try {
|
|
153
|
-
await fs_1.promises.access(localConfigFile);
|
|
154
|
-
configFile = localConfigFile;
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// If local config doesn't exist, try global config
|
|
158
|
-
const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
159
|
-
configFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
let raw = {};
|
|
163
|
-
try {
|
|
164
|
-
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
165
|
-
const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
166
|
-
// Strip Symbol properties added by @iarna/toml (required for Zod v4+)
|
|
167
|
-
raw = stripSymbols(parsed);
|
|
168
|
-
// Validate the configuration with zod
|
|
169
|
-
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
170
|
-
if (!validationResult.success) {
|
|
171
|
-
throw (0, constants_1.createRulerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
if (err instanceof Error && err.code !== 'ENOENT') {
|
|
176
|
-
if (err.message.includes('[ruler]')) {
|
|
177
|
-
throw err; // Re-throw validation errors
|
|
178
|
-
}
|
|
179
|
-
console.warn(`[ruler] Warning: could not read config file at ${configFile}: ${err.message}`);
|
|
180
|
-
}
|
|
181
|
-
raw = {};
|
|
182
|
-
}
|
|
157
|
+
const checkGlobal = options.checkGlobal ?? true;
|
|
158
|
+
const configFile = configPath
|
|
159
|
+
? path.resolve(configPath)
|
|
160
|
+
: await resolveImplicitConfigFile(projectRoot, checkGlobal);
|
|
161
|
+
const raw = configFile ? await readConfigFile(configFile) : {};
|
|
183
162
|
const defaultAgents = Array.isArray(raw.default_agents)
|
|
184
163
|
? raw.default_agents.map((a) => String(a))
|
|
185
164
|
: undefined;
|
|
@@ -250,6 +229,13 @@ async function loadConfig(options) {
|
|
|
250
229
|
if (typeof rawGitignoreSection.local === 'boolean') {
|
|
251
230
|
gitignoreConfig.local = rawGitignoreSection.local;
|
|
252
231
|
}
|
|
232
|
+
const rawBackupSection = raw.backup && typeof raw.backup === 'object' && !Array.isArray(raw.backup)
|
|
233
|
+
? raw.backup
|
|
234
|
+
: {};
|
|
235
|
+
const backupConfig = {};
|
|
236
|
+
if (typeof rawBackupSection.enabled === 'boolean') {
|
|
237
|
+
backupConfig.enabled = rawBackupSection.enabled;
|
|
238
|
+
}
|
|
253
239
|
const rawSkillsSection = raw.skills && typeof raw.skills === 'object' && !Array.isArray(raw.skills)
|
|
254
240
|
? raw.skills
|
|
255
241
|
: {};
|
|
@@ -272,7 +258,8 @@ async function loadConfig(options) {
|
|
|
272
258
|
? raw.subagents
|
|
273
259
|
: {};
|
|
274
260
|
const legacyHasContent = typeof rawLegacySubagentsSection.enabled === 'boolean' ||
|
|
275
|
-
typeof rawLegacySubagentsSection.include_in_rules === 'boolean'
|
|
261
|
+
typeof rawLegacySubagentsSection.include_in_rules === 'boolean' ||
|
|
262
|
+
typeof rawLegacySubagentsSection.cleanup_orphaned === 'boolean';
|
|
276
263
|
if (legacyHasContent) {
|
|
277
264
|
warnLegacySubagentsSection();
|
|
278
265
|
}
|
|
@@ -291,6 +278,14 @@ async function loadConfig(options) {
|
|
|
291
278
|
subagentsConfig.include_in_rules =
|
|
292
279
|
rawLegacySubagentsSection.include_in_rules;
|
|
293
280
|
}
|
|
281
|
+
if (typeof agentsSection.cleanup_orphaned === 'boolean') {
|
|
282
|
+
subagentsConfig.cleanup_orphaned =
|
|
283
|
+
agentsSection.cleanup_orphaned;
|
|
284
|
+
}
|
|
285
|
+
else if (typeof rawLegacySubagentsSection.cleanup_orphaned === 'boolean') {
|
|
286
|
+
subagentsConfig.cleanup_orphaned =
|
|
287
|
+
rawLegacySubagentsSection.cleanup_orphaned;
|
|
288
|
+
}
|
|
294
289
|
const nestedDefined = typeof raw.nested === 'boolean';
|
|
295
290
|
const nested = nestedDefined ? raw.nested : false;
|
|
296
291
|
return {
|
|
@@ -299,9 +294,96 @@ async function loadConfig(options) {
|
|
|
299
294
|
cliAgents,
|
|
300
295
|
mcp: globalMcpConfig,
|
|
301
296
|
gitignore: gitignoreConfig,
|
|
297
|
+
backup: backupConfig,
|
|
302
298
|
skills: skillsConfig,
|
|
303
299
|
subagents: subagentsConfig,
|
|
304
300
|
nested,
|
|
305
301
|
nestedDefined,
|
|
306
302
|
};
|
|
307
303
|
}
|
|
304
|
+
async function resolveImplicitConfigFile(projectRoot, checkGlobal) {
|
|
305
|
+
const localRulerDir = await findNearestLocalRulerDir(projectRoot);
|
|
306
|
+
const localConfigFile = localRulerDir
|
|
307
|
+
? path.join(localRulerDir, 'ruler.toml')
|
|
308
|
+
: path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
309
|
+
if (await configFileExists(localConfigFile)) {
|
|
310
|
+
return localConfigFile;
|
|
311
|
+
}
|
|
312
|
+
if (!checkGlobal) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
316
|
+
const globalConfigFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
|
|
317
|
+
if (await configFileExists(globalConfigFile)) {
|
|
318
|
+
return globalConfigFile;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
async function findNearestLocalRulerDir(startPath) {
|
|
323
|
+
let current = path.resolve(startPath);
|
|
324
|
+
while (current) {
|
|
325
|
+
const candidate = path.join(current, '.ruler');
|
|
326
|
+
try {
|
|
327
|
+
const stat = await fs_1.promises.stat(candidate);
|
|
328
|
+
if (stat.isDirectory()) {
|
|
329
|
+
return candidate;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Keep walking; missing or inaccessible candidates simply do not match.
|
|
334
|
+
}
|
|
335
|
+
const parent = path.dirname(current);
|
|
336
|
+
if (parent === current) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
current = parent;
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
async function configFileExists(configFile) {
|
|
344
|
+
try {
|
|
345
|
+
await fs_1.promises.access(configFile);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
if (err.code === 'ENOENT') {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
throw (0, constants_1.createRulerError)('Could not access configuration file', `File: ${configFile}, Error: ${errorMessage(err)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function readConfigFile(configFile) {
|
|
356
|
+
const text = await readConfigText(configFile);
|
|
357
|
+
const parsed = parseConfigText(text, configFile);
|
|
358
|
+
const raw = stripSymbols(parsed);
|
|
359
|
+
validateConfig(raw, configFile);
|
|
360
|
+
return raw;
|
|
361
|
+
}
|
|
362
|
+
async function readConfigText(configFile) {
|
|
363
|
+
try {
|
|
364
|
+
return await fs_1.promises.readFile(configFile, 'utf8');
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
if (err.code === 'ENOENT') {
|
|
368
|
+
throw (0, constants_1.createRulerError)('Configuration file not found', `File: ${configFile}`);
|
|
369
|
+
}
|
|
370
|
+
throw (0, constants_1.createRulerError)('Could not read configuration file', `File: ${configFile}, Error: ${errorMessage(err)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function parseConfigText(text, configFile) {
|
|
374
|
+
try {
|
|
375
|
+
return text.trim() ? (0, toml_1.parse)(text) : {};
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
throw (0, constants_1.createRulerError)('Invalid configuration file', `File: ${configFile}, Error: ${errorMessage(err)}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function validateConfig(raw, configFile) {
|
|
382
|
+
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
383
|
+
if (!validationResult.success) {
|
|
384
|
+
throw (0, constants_1.createRulerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function errorMessage(err) {
|
|
388
|
+
return err instanceof Error ? err.message : String(err);
|
|
389
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searches upwards from startPath to find a directory named .ruler.
|
|
3
|
+
* If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/ruler.
|
|
4
|
+
* Returns the path to the .ruler directory, or null if not found.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findRulerDir(startPath: string, checkGlobal?: boolean): Promise<string | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Options for {@link readMarkdownFiles}.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReadMarkdownFilesOptions {
|
|
11
|
+
/**
|
|
12
|
+
* When true, include `.ruler/agents/*.md` in the returned set so they are
|
|
13
|
+
* concatenated into the top-level generated rule files. When false or
|
|
14
|
+
* omitted, `.ruler/agents/` is skipped, mirroring `.ruler/skills/`.
|
|
15
|
+
*/
|
|
16
|
+
includeAgents?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Recursively reads all Markdown (.md) files in rulerDir, returning their paths and contents.
|
|
20
|
+
* Files are sorted alphabetically by path.
|
|
21
|
+
*
|
|
22
|
+
* `.ruler/skills/` is always skipped (skills are propagated separately).
|
|
23
|
+
* `.ruler/agents/` is skipped unless `options.includeAgents` is `true`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readMarkdownFiles(rulerDir: string, options?: ReadMarkdownFilesOptions): Promise<{
|
|
26
|
+
path: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Writes content to filePath, creating parent directories if necessary.
|
|
31
|
+
*/
|
|
32
|
+
export declare function writeGeneratedFile(filePath: string, content: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a backup of the given filePath by copying it to filePath.bak if it exists.
|
|
35
|
+
* Keeps an existing backup intact so repeated applies preserve the original file.
|
|
36
|
+
*/
|
|
37
|
+
export declare function backupFile(filePath: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Ensures that the given directory exists by creating it recursively.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ensureDirExists(dirPath: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Finds the global ruler configuration directory at XDG_CONFIG_HOME/ruler.
|
|
44
|
+
* Returns the path if it exists, null otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export declare function findGlobalRulerDir(): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Searches the entire directory tree from startPath to find all .ruler directories.
|
|
49
|
+
* Returns an array of .ruler directory paths from most specific to least specific.
|
|
50
|
+
*/
|
|
51
|
+
export declare function findAllRulerDirs(startPath: string): Promise<string[]>;
|
|
@@ -45,12 +45,27 @@ const path = __importStar(require("path"));
|
|
|
45
45
|
const os = __importStar(require("os"));
|
|
46
46
|
const constants_1 = require("../constants");
|
|
47
47
|
const SUBAGENTS_DIR_NAME = path.basename(constants_1.RULER_SUBAGENTS_PATH);
|
|
48
|
+
const DEFAULT_NESTED_DISCOVERY_IGNORES = new Set([
|
|
49
|
+
'__fixtures__',
|
|
50
|
+
'__generated__',
|
|
51
|
+
'build',
|
|
52
|
+
'coverage',
|
|
53
|
+
'dist',
|
|
54
|
+
'fixtures',
|
|
55
|
+
'generated',
|
|
56
|
+
'node_modules',
|
|
57
|
+
'temp',
|
|
58
|
+
'tmp',
|
|
59
|
+
]);
|
|
48
60
|
/**
|
|
49
61
|
* Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
|
|
50
62
|
*/
|
|
51
63
|
function getXdgConfigDir() {
|
|
52
64
|
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
53
65
|
}
|
|
66
|
+
function shouldSkipNestedDiscoveryDir(dirName) {
|
|
67
|
+
return (dirName.startsWith('.') || DEFAULT_NESTED_DISCOVERY_IGNORES.has(dirName));
|
|
68
|
+
}
|
|
54
69
|
/**
|
|
55
70
|
* Searches upwards from startPath to find a directory named .ruler.
|
|
56
71
|
* If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/ruler.
|
|
@@ -222,11 +237,19 @@ async function writeGeneratedFile(filePath, content) {
|
|
|
222
237
|
}
|
|
223
238
|
/**
|
|
224
239
|
* Creates a backup of the given filePath by copying it to filePath.bak if it exists.
|
|
240
|
+
* Keeps an existing backup intact so repeated applies preserve the original file.
|
|
225
241
|
*/
|
|
226
242
|
async function backupFile(filePath) {
|
|
243
|
+
const backupPath = `${filePath}.bak`;
|
|
227
244
|
try {
|
|
228
|
-
await fs_1.promises.access(
|
|
229
|
-
|
|
245
|
+
await fs_1.promises.access(backupPath);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// continue if no backup exists yet
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await fs_1.promises.copyFile(filePath, backupPath);
|
|
230
253
|
}
|
|
231
254
|
catch {
|
|
232
255
|
// ignore if file does not exist
|
|
@@ -272,23 +295,20 @@ async function findAllRulerDirs(startPath) {
|
|
|
272
295
|
if (entry.name === '.ruler') {
|
|
273
296
|
rulerDirs.push(fullPath);
|
|
274
297
|
}
|
|
275
|
-
else {
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
path.resolve(fullPath) !== rootPath) {
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// no .git boundary, continue traversal
|
|
298
|
+
else if (!shouldSkipNestedDiscoveryDir(entry.name)) {
|
|
299
|
+
// Do not cross git repository boundaries (except the starting root)
|
|
300
|
+
const gitDir = path.join(fullPath, '.git');
|
|
301
|
+
try {
|
|
302
|
+
const gitStat = await fs_1.promises.stat(gitDir);
|
|
303
|
+
if (gitStat.isDirectory() &&
|
|
304
|
+
path.resolve(fullPath) !== rootPath) {
|
|
305
|
+
continue;
|
|
289
306
|
}
|
|
290
|
-
await findRulerDirs(fullPath);
|
|
291
307
|
}
|
|
308
|
+
catch {
|
|
309
|
+
// no .git boundary, continue traversal
|
|
310
|
+
}
|
|
311
|
+
await findRulerDirs(fullPath);
|
|
292
312
|
}
|
|
293
313
|
}
|
|
294
314
|
}
|