Skip to content

Commit ac9393d

Browse files
committed
feat: enhance rule file loading with .roo/rules directory support
- Introduced functions to safely read files and check for directory existence. - Added capability to read all text files from a specified directory in alphabetical order. - Updated `loadRuleFiles` to prioritize loading rules from a `.roo/rules/` directory, falling back to existing rule files if necessary. - Enhanced `addCustomInstructions` to support loading mode-specific rules from a `.roo/rules-{mode}/` directory, improving flexibility in rule management. This change improves the organization and retrieval of rule files, allowing for better modularity and maintainability.
1 parent 57d9731 commit ac9393d

File tree

1 file changed

+104
-9
lines changed

1 file changed

+104
-9
lines changed

src/core/prompts/sections/custom-instructions.ts

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import path from "path"
33

44
import { LANGUAGES, isLanguage } from "../../../shared/language"
55

6+
/**
7+
* Safely read a file and return its trimmed content
8+
*/
69
async function safeReadFile(filePath: string): Promise<string> {
710
try {
811
const content = await fs.readFile(filePath, "utf-8")
@@ -16,7 +19,81 @@ async function safeReadFile(filePath: string): Promise<string> {
1619
}
1720
}
1821

22+
/**
23+
* Check if a directory exists
24+
*/
25+
async function directoryExists(dirPath: string): Promise<boolean> {
26+
try {
27+
const stats = await fs.stat(dirPath)
28+
return stats.isDirectory()
29+
} catch (err) {
30+
return false
31+
}
32+
}
33+
34+
/**
35+
* Read all text files from a directory in alphabetical order
36+
*/
37+
async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ filename: string; content: string }>> {
38+
try {
39+
const files = await fs
40+
.readdir(dirPath, { withFileTypes: true, recursive: true })
41+
.then((files) => files.filter((file) => file.isFile()))
42+
.then((files) => files.map((file) => path.resolve(dirPath, file.name)))
43+
44+
const fileContents = await Promise.all(
45+
files.map(async (file) => {
46+
const filePath = path.join(dirPath, file)
47+
try {
48+
// Check if it's a file (not a directory)
49+
const stats = await fs.stat(filePath)
50+
if (stats.isFile()) {
51+
const content = await safeReadFile(filePath)
52+
return { filename: file, content }
53+
}
54+
return null
55+
} catch (err) {
56+
return null
57+
}
58+
}),
59+
)
60+
61+
// Filter out null values (directories or failed reads)
62+
return fileContents.filter((item): item is { filename: string; content: string } => item !== null)
63+
} catch (err) {
64+
return []
65+
}
66+
}
67+
68+
/**
69+
* Format content from multiple files with filenames as headers
70+
*/
71+
function formatDirectoryContent(dirPath: string, files: Array<{ filename: string; content: string }>): string {
72+
if (files.length === 0) return ""
73+
74+
const formattedContent = files
75+
.map((file) => {
76+
return `# Filename: ${file.filename}\n${file.content}`
77+
})
78+
.join("\n\n")
79+
80+
return `\n# Rules from ${path.relative(process.cwd(), dirPath)}:\n${formattedContent}\n`
81+
}
82+
83+
/**
84+
* Load rule files from the specified directory
85+
*/
1986
export async function loadRuleFiles(cwd: string): Promise<string> {
87+
// Check for .roo/rules/ directory
88+
const rooRulesDir = path.join(cwd, ".roo", "rules")
89+
if (await directoryExists(rooRulesDir)) {
90+
const files = await readTextFilesFromDirectory(rooRulesDir)
91+
if (files.length > 0) {
92+
return formatDirectoryContent(rooRulesDir, files)
93+
}
94+
}
95+
96+
// Fall back to existing behavior
2097
const ruleFiles = [".roorules", ".clinerules"]
2198

2299
for (const file of ruleFiles) {
@@ -41,16 +118,30 @@ export async function addCustomInstructions(
41118
// Load mode-specific rules if mode is provided
42119
let modeRuleContent = ""
43120
let usedRuleFile = ""
121+
44122
if (mode) {
45-
const rooModeRuleFile = `.roorules-${mode}`
46-
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
47-
if (modeRuleContent) {
48-
usedRuleFile = rooModeRuleFile
49-
} else {
50-
const clineModeRuleFile = `.clinerules-${mode}`
51-
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
123+
// Check for .roo/rules-${mode}/ directory
124+
const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`)
125+
if (await directoryExists(modeRulesDir)) {
126+
const files = await readTextFilesFromDirectory(modeRulesDir)
127+
if (files.length > 0) {
128+
modeRuleContent = formatDirectoryContent(modeRulesDir, files)
129+
usedRuleFile = modeRulesDir
130+
}
131+
}
132+
133+
// If no directory exists, fall back to existing behavior
134+
if (!modeRuleContent) {
135+
const rooModeRuleFile = `.roorules-${mode}`
136+
modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile))
52137
if (modeRuleContent) {
53-
usedRuleFile = clineModeRuleFile
138+
usedRuleFile = rooModeRuleFile
139+
} else {
140+
const clineModeRuleFile = `.clinerules-${mode}`
141+
modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile))
142+
if (modeRuleContent) {
143+
usedRuleFile = clineModeRuleFile
144+
}
54145
}
55146
}
56147
}
@@ -78,7 +169,11 @@ export async function addCustomInstructions(
78169

79170
// Add mode-specific rules first if they exist
80171
if (modeRuleContent && modeRuleContent.trim()) {
81-
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
172+
if (usedRuleFile.includes(path.join(".roo", `rules-${mode}`))) {
173+
rules.push(modeRuleContent.trim())
174+
} else {
175+
rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`)
176+
}
82177
}
83178

84179
if (options.rooIgnoreInstructions) {

0 commit comments

Comments
 (0)