@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.
- package/README.md +40 -28
- 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 +1 -2
- package/dist/cli/handlers.d.ts +41 -0
- package/dist/cli/handlers.js +75 -59
- 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 +106 -39
- 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/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 +16 -3
- 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 +6 -4
|
@@ -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>;
|
|
@@ -103,6 +103,11 @@ const rulerConfigSchema = zod_1.z.object({
|
|
|
103
103
|
local: zod_1.z.boolean().optional(),
|
|
104
104
|
})
|
|
105
105
|
.optional(),
|
|
106
|
+
backup: zod_1.z
|
|
107
|
+
.object({
|
|
108
|
+
enabled: zod_1.z.boolean().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
106
111
|
skills: zod_1.z
|
|
107
112
|
.object({
|
|
108
113
|
enabled: zod_1.z.boolean().optional(),
|
|
@@ -144,48 +149,16 @@ function stripSymbols(obj) {
|
|
|
144
149
|
}
|
|
145
150
|
/**
|
|
146
151
|
* Loads and parses the ruler TOML configuration file, applying defaults.
|
|
147
|
-
*
|
|
152
|
+
* Missing implicit configs return defaults. Explicit configs and existing
|
|
153
|
+
* implicit configs fail fast when missing, unreadable, or invalid.
|
|
148
154
|
*/
|
|
149
155
|
async function loadConfig(options) {
|
|
150
156
|
const { projectRoot, configPath, cliAgents } = options;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Try local .ruler/ruler.toml first
|
|
157
|
-
const localConfigFile = path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
158
|
-
try {
|
|
159
|
-
await fs_1.promises.access(localConfigFile);
|
|
160
|
-
configFile = localConfigFile;
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// If local config doesn't exist, try global config
|
|
164
|
-
const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
165
|
-
configFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
let raw = {};
|
|
169
|
-
try {
|
|
170
|
-
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
171
|
-
const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
172
|
-
// Strip Symbol properties added by @iarna/toml (required for Zod v4+)
|
|
173
|
-
raw = stripSymbols(parsed);
|
|
174
|
-
// Validate the configuration with zod
|
|
175
|
-
const validationResult = rulerConfigSchema.safeParse(raw);
|
|
176
|
-
if (!validationResult.success) {
|
|
177
|
-
throw (0, constants_1.createRulerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
if (err instanceof Error && err.code !== 'ENOENT') {
|
|
182
|
-
if (err.message.includes('[ruler]')) {
|
|
183
|
-
throw err; // Re-throw validation errors
|
|
184
|
-
}
|
|
185
|
-
console.warn(`[ruler] Warning: could not read config file at ${configFile}: ${err.message}`);
|
|
186
|
-
}
|
|
187
|
-
raw = {};
|
|
188
|
-
}
|
|
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) : {};
|
|
189
162
|
const defaultAgents = Array.isArray(raw.default_agents)
|
|
190
163
|
? raw.default_agents.map((a) => String(a))
|
|
191
164
|
: undefined;
|
|
@@ -256,6 +229,13 @@ async function loadConfig(options) {
|
|
|
256
229
|
if (typeof rawGitignoreSection.local === 'boolean') {
|
|
257
230
|
gitignoreConfig.local = rawGitignoreSection.local;
|
|
258
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
|
+
}
|
|
259
239
|
const rawSkillsSection = raw.skills && typeof raw.skills === 'object' && !Array.isArray(raw.skills)
|
|
260
240
|
? raw.skills
|
|
261
241
|
: {};
|
|
@@ -314,9 +294,96 @@ async function loadConfig(options) {
|
|
|
314
294
|
cliAgents,
|
|
315
295
|
mcp: globalMcpConfig,
|
|
316
296
|
gitignore: gitignoreConfig,
|
|
297
|
+
backup: backupConfig,
|
|
317
298
|
skills: skillsConfig,
|
|
318
299
|
subagents: subagentsConfig,
|
|
319
300
|
nested,
|
|
320
301
|
nestedDefined,
|
|
321
302
|
};
|
|
322
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
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Updates an ignore file in the project root with paths in a managed Ruler block.
|
|
3
|
+
* Creates the file if it doesn't exist, and creates or updates the Ruler-managed block.
|
|
4
|
+
*
|
|
5
|
+
* @param projectRoot The project root directory
|
|
6
|
+
* @param paths Array of file paths to add to the ignore file (can be absolute or relative)
|
|
7
|
+
* @param ignoreFile Relative path to the ignore file from project root (defaults to .gitignore)
|
|
8
|
+
*/
|
|
9
|
+
export declare function updateGitignore(projectRoot: string, paths: string[], ignoreFile?: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves ignore files Ruler manages. Linked worktrees store `.git` as a
|
|
12
|
+
* file containing a `gitdir:` pointer, so `.git/info/exclude` must be resolved
|
|
13
|
+
* through that pointer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveIgnoreFilePath(projectRoot: string, ignoreFile: string): Promise<string>;
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.updateGitignore = updateGitignore;
|
|
37
|
+
exports.resolveIgnoreFilePath = resolveIgnoreFilePath;
|
|
37
38
|
const fs_1 = require("fs");
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const RULER_START_MARKER = '# START Ruler Generated Files';
|
|
@@ -47,7 +48,7 @@ const RULER_END_MARKER = '# END Ruler Generated Files';
|
|
|
47
48
|
* @param ignoreFile Relative path to the ignore file from project root (defaults to .gitignore)
|
|
48
49
|
*/
|
|
49
50
|
async function updateGitignore(projectRoot, paths, ignoreFile = '.gitignore') {
|
|
50
|
-
const gitignorePath =
|
|
51
|
+
const gitignorePath = await resolveIgnoreFilePath(projectRoot, ignoreFile);
|
|
51
52
|
// Read existing .gitignore or start with empty content
|
|
52
53
|
let existingContent = '';
|
|
53
54
|
try {
|
|
@@ -101,6 +102,36 @@ async function updateGitignore(projectRoot, paths, ignoreFile = '.gitignore') {
|
|
|
101
102
|
await fs_1.promises.mkdir(path.dirname(gitignorePath), { recursive: true });
|
|
102
103
|
await fs_1.promises.writeFile(gitignorePath, newContent);
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolves ignore files Ruler manages. Linked worktrees store `.git` as a
|
|
107
|
+
* file containing a `gitdir:` pointer, so `.git/info/exclude` must be resolved
|
|
108
|
+
* through that pointer.
|
|
109
|
+
*/
|
|
110
|
+
async function resolveIgnoreFilePath(projectRoot, ignoreFile) {
|
|
111
|
+
if (ignoreFile !== '.git/info/exclude') {
|
|
112
|
+
return path.join(projectRoot, ignoreFile);
|
|
113
|
+
}
|
|
114
|
+
const dotGitPath = path.join(projectRoot, '.git');
|
|
115
|
+
try {
|
|
116
|
+
const dotGitStat = await fs_1.promises.lstat(dotGitPath);
|
|
117
|
+
if (dotGitStat.isFile()) {
|
|
118
|
+
const dotGitContent = await fs_1.promises.readFile(dotGitPath, 'utf8');
|
|
119
|
+
const gitDirMatch = dotGitContent.match(/^gitdir:\s*(.+)\s*$/m);
|
|
120
|
+
if (gitDirMatch) {
|
|
121
|
+
const gitDir = gitDirMatch[1];
|
|
122
|
+
const resolvedGitDir = path.isAbsolute(gitDir)
|
|
123
|
+
? gitDir
|
|
124
|
+
: path.resolve(projectRoot, gitDir);
|
|
125
|
+
return path.join(resolvedGitDir, 'info', 'exclude');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Fall back to the historical project-root path for non-git test fixtures
|
|
131
|
+
// and unusual repositories where `.git` cannot be inspected.
|
|
132
|
+
}
|
|
133
|
+
return path.join(projectRoot, ignoreFile);
|
|
134
|
+
}
|
|
104
135
|
/**
|
|
105
136
|
* Gets all paths from .gitignore content excluding those in the Ruler block.
|
|
106
137
|
*/
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import { SkillInfo } from '../types';
|
|
3
|
+
import type { IAgent } from '../agents/IAgent';
|
|
4
|
+
/**
|
|
5
|
+
* Discovers skills in the project's .ruler/skills directory.
|
|
6
|
+
* Returns discovered skills and any validation warnings.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discoverSkills(projectRoot: string): Promise<{
|
|
9
|
+
skills: SkillInfo[];
|
|
10
|
+
warnings: string[];
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Gets the paths that skills will generate, for gitignore purposes.
|
|
14
|
+
* Returns empty array if skills directory doesn't exist.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getSkillsGitignorePaths(projectRoot: string, agents: IAgent[]): Promise<string[]>;
|
|
17
|
+
type ReplaceSkillsFsOps = Pick<typeof fs, 'rename' | 'cp' | 'rm'>;
|
|
18
|
+
export declare function replaceSkillsDirectory(tempDir: string, targetDir: string, fsOps?: ReplaceSkillsFsOps): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Propagates skills for agents that need them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function propagateSkills(projectRoot: string, agents: IAgent[], skillsEnabled: boolean, verbose: boolean, dryRun: boolean): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Propagates skills for Claude Code by copying .ruler/skills to .claude/skills.
|
|
25
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
26
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
27
|
+
*/
|
|
28
|
+
export declare function propagateSkillsForClaude(projectRoot: string, options: {
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
}): Promise<string[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .codex/skills.
|
|
33
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
34
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
35
|
+
*/
|
|
36
|
+
export declare function propagateSkillsForCodex(projectRoot: string, options: {
|
|
37
|
+
dryRun: boolean;
|
|
38
|
+
}): Promise<string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Propagates skills for OpenCode by copying .ruler/skills to .opencode/skills.
|
|
41
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
42
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
43
|
+
*/
|
|
44
|
+
export declare function propagateSkillsForOpenCode(projectRoot: string, options: {
|
|
45
|
+
dryRun: boolean;
|
|
46
|
+
}): Promise<string[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Propagates skills for Pi Coding Agent by copying .ruler/skills to .pi/skills.
|
|
49
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
50
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
51
|
+
*/
|
|
52
|
+
export declare function propagateSkillsForPi(projectRoot: string, options: {
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
}): Promise<string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Propagates skills for Goose by copying .ruler/skills to .agents/skills.
|
|
57
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
58
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
59
|
+
*/
|
|
60
|
+
export declare function propagateSkillsForGoose(projectRoot: string, options: {
|
|
61
|
+
dryRun: boolean;
|
|
62
|
+
}): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Propagates skills for Mistral Vibe by copying .ruler/skills to .vibe/skills.
|
|
65
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
66
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
67
|
+
*/
|
|
68
|
+
export declare function propagateSkillsForVibe(projectRoot: string, options: {
|
|
69
|
+
dryRun: boolean;
|
|
70
|
+
}): Promise<string[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Propagates skills for Roo Code by copying .ruler/skills to .roo/skills.
|
|
73
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
74
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
75
|
+
*/
|
|
76
|
+
export declare function propagateSkillsForRoo(projectRoot: string, options: {
|
|
77
|
+
dryRun: boolean;
|
|
78
|
+
}): Promise<string[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Propagates skills for Gemini CLI by copying .ruler/skills to .gemini/skills.
|
|
81
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
82
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
83
|
+
*/
|
|
84
|
+
export declare function propagateSkillsForGemini(projectRoot: string, options: {
|
|
85
|
+
dryRun: boolean;
|
|
86
|
+
}): Promise<string[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Propagates skills for Junie by copying .ruler/skills to .junie/skills.
|
|
89
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
90
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
91
|
+
*/
|
|
92
|
+
export declare function propagateSkillsForJunie(projectRoot: string, options: {
|
|
93
|
+
dryRun: boolean;
|
|
94
|
+
}): Promise<string[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Propagates skills for Cursor by copying .ruler/skills to .cursor/skills.
|
|
97
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
98
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
99
|
+
*/
|
|
100
|
+
export declare function propagateSkillsForCursor(projectRoot: string, options: {
|
|
101
|
+
dryRun: boolean;
|
|
102
|
+
}): Promise<string[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Propagates skills for Windsurf by copying .ruler/skills to .windsurf/skills.
|
|
105
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
106
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
107
|
+
*/
|
|
108
|
+
export declare function propagateSkillsForWindsurf(projectRoot: string, options: {
|
|
109
|
+
dryRun: boolean;
|
|
110
|
+
}): Promise<string[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Propagates skills for Factory Droid by copying .ruler/skills to .factory/skills.
|
|
113
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
114
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
115
|
+
*/
|
|
116
|
+
export declare function propagateSkillsForFactory(projectRoot: string, options: {
|
|
117
|
+
dryRun: boolean;
|
|
118
|
+
}): Promise<string[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Propagates skills for Antigravity by copying .ruler/skills to .agent/skills.
|
|
121
|
+
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
122
|
+
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
123
|
+
*/
|
|
124
|
+
export declare function propagateSkillsForAntigravity(projectRoot: string, options: {
|
|
125
|
+
dryRun: boolean;
|
|
126
|
+
}): Promise<string[]>;
|
|
127
|
+
export {};
|