Skip to content

Commit 6ff70d2

Browse files
authored
Merge branch 'RooCodeInc:main' into fix/website-logo-theme-persistence
2 parents 13ca4d7 + 2eb586b commit 6ff70d2

File tree

3 files changed

+285
-158
lines changed

3 files changed

+285
-158
lines changed

src/core/config/CustomModesManager.ts

Lines changed: 150 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -558,49 +558,55 @@ export class CustomModesManager {
558558
*/
559559
public async checkRulesDirectoryHasContent(slug: string): Promise<boolean> {
560560
try {
561-
// Get workspace path
562-
const workspacePath = getWorkspacePath()
563-
if (!workspacePath) {
564-
return false
565-
}
561+
// First, find the mode to determine its source
562+
const allModes = await this.getCustomModes()
563+
const mode = allModes.find((m) => m.slug === slug)
566564

567-
// Check if .roomodes file exists and contains this mode
568-
// This ensures we can only consolidate rules for modes that have been customized
569-
const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME)
570-
try {
571-
const roomodesExists = await fileExistsAtPath(roomodesPath)
572-
if (roomodesExists) {
573-
const roomodesContent = await fs.readFile(roomodesPath, "utf-8")
574-
const roomodesData = yaml.parse(roomodesContent)
575-
const roomodesModes = roomodesData?.customModes || []
576-
577-
// Check if this specific mode exists in .roomodes
578-
const modeInRoomodes = roomodesModes.find((m: any) => m.slug === slug)
579-
if (!modeInRoomodes) {
580-
return false // Mode not customized in .roomodes, cannot consolidate
581-
}
582-
} else {
583-
// If no .roomodes file exists, check if it's in global custom modes
584-
const allModes = await this.getCustomModes()
585-
const mode = allModes.find((m) => m.slug === slug)
565+
if (!mode) {
566+
// If not in custom modes, check if it's in .roomodes (project-specific)
567+
const workspacePath = getWorkspacePath()
568+
if (!workspacePath) {
569+
return false
570+
}
586571

587-
if (!mode) {
588-
return false // Not a custom mode, cannot consolidate
572+
const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME)
573+
try {
574+
const roomodesExists = await fileExistsAtPath(roomodesPath)
575+
if (roomodesExists) {
576+
const roomodesContent = await fs.readFile(roomodesPath, "utf-8")
577+
const roomodesData = yaml.parse(roomodesContent)
578+
const roomodesModes = roomodesData?.customModes || []
579+
580+
// Check if this specific mode exists in .roomodes
581+
const modeInRoomodes = roomodesModes.find((m: any) => m.slug === slug)
582+
if (!modeInRoomodes) {
583+
return false // Mode not found anywhere
584+
}
585+
} else {
586+
return false // No .roomodes file and not in custom modes
589587
}
588+
} catch (error) {
589+
return false // Cannot read .roomodes and not in custom modes
590590
}
591-
} catch (error) {
592-
// If we can't read .roomodes, fall back to checking custom modes
593-
const allModes = await this.getCustomModes()
594-
const mode = allModes.find((m) => m.slug === slug)
591+
}
595592

596-
if (!mode) {
597-
return false // Not a custom mode, cannot consolidate
593+
// Determine the correct rules directory based on mode source
594+
let modeRulesDir: string
595+
const isGlobalMode = mode?.source === "global"
596+
597+
if (isGlobalMode) {
598+
// For global modes, check in global .roo directory
599+
const globalRooDir = getGlobalRooDirectory()
600+
modeRulesDir = path.join(globalRooDir, `rules-${slug}`)
601+
} else {
602+
// For project modes, check in workspace .roo directory
603+
const workspacePath = getWorkspacePath()
604+
if (!workspacePath) {
605+
return false
598606
}
607+
modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`)
599608
}
600609

601-
// Check for .roo/rules-{slug}/ directory
602-
const modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`)
603-
604610
try {
605611
const stats = await fs.stat(modeRulesDir)
606612
if (!stats.isDirectory()) {
@@ -655,24 +661,23 @@ export class CustomModesManager {
655661

656662
// If mode not found in custom modes, check if it's a built-in mode that has been customized
657663
if (!mode) {
664+
// Only check workspace-based modes if workspace is available
658665
const workspacePath = getWorkspacePath()
659-
if (!workspacePath) {
660-
return { success: false, error: "No workspace found" }
661-
}
662-
663-
const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME)
664-
try {
665-
const roomodesExists = await fileExistsAtPath(roomodesPath)
666-
if (roomodesExists) {
667-
const roomodesContent = await fs.readFile(roomodesPath, "utf-8")
668-
const roomodesData = yaml.parse(roomodesContent)
669-
const roomodesModes = roomodesData?.customModes || []
670-
671-
// Find the mode in .roomodes
672-
mode = roomodesModes.find((m: any) => m.slug === slug)
666+
if (workspacePath) {
667+
const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME)
668+
try {
669+
const roomodesExists = await fileExistsAtPath(roomodesPath)
670+
if (roomodesExists) {
671+
const roomodesContent = await fs.readFile(roomodesPath, "utf-8")
672+
const roomodesData = yaml.parse(roomodesContent)
673+
const roomodesModes = roomodesData?.customModes || []
674+
675+
// Find the mode in .roomodes
676+
mode = roomodesModes.find((m: any) => m.slug === slug)
677+
}
678+
} catch (error) {
679+
// Continue to check built-in modes
673680
}
674-
} catch (error) {
675-
// Continue to check built-in modes
676681
}
677682

678683
// If still not found, check if it's a built-in mode
@@ -687,14 +692,25 @@ export class CustomModesManager {
687692
}
688693
}
689694

690-
// Get workspace path
691-
const workspacePath = getWorkspacePath()
692-
if (!workspacePath) {
693-
return { success: false, error: "No workspace found" }
695+
// Determine the base directory based on mode source
696+
const isGlobalMode = mode.source === "global"
697+
let baseDir: string
698+
if (isGlobalMode) {
699+
// For global modes, use the global .roo directory
700+
baseDir = getGlobalRooDirectory()
701+
} else {
702+
// For project modes, use the workspace directory
703+
const workspacePath = getWorkspacePath()
704+
if (!workspacePath) {
705+
return { success: false, error: "No workspace found" }
706+
}
707+
baseDir = workspacePath
694708
}
695709

696-
// Check for .roo/rules-{slug}/ directory
697-
const modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`)
710+
// Check for .roo/rules-{slug}/ directory (or rules-{slug}/ for global)
711+
const modeRulesDir = isGlobalMode
712+
? path.join(baseDir, `rules-${slug}`)
713+
: path.join(baseDir, ".roo", `rules-${slug}`)
698714

699715
let rulesFiles: RuleFile[] = []
700716
try {
@@ -709,8 +725,10 @@ export class CustomModesManager {
709725
const filePath = path.join(modeRulesDir, entry.name)
710726
const content = await fs.readFile(filePath, "utf-8")
711727
if (content.trim()) {
712-
// Calculate relative path from .roo directory
713-
const relativePath = path.relative(path.join(workspacePath, ".roo"), filePath)
728+
// Calculate relative path based on mode source
729+
const relativePath = isGlobalMode
730+
? path.relative(baseDir, filePath)
731+
: path.relative(path.join(baseDir, ".roo"), filePath)
714732
rulesFiles.push({ relativePath, content: content.trim() })
715733
}
716734
}
@@ -755,6 +773,77 @@ export class CustomModesManager {
755773
}
756774
}
757775

776+
/**
777+
* Helper method to import rules files for a mode
778+
* @param importMode - The mode being imported
779+
* @param rulesFiles - The rules files to import
780+
* @param source - The import source ("global" or "project")
781+
*/
782+
private async importRulesFiles(
783+
importMode: ExportedModeConfig,
784+
rulesFiles: RuleFile[],
785+
source: "global" | "project",
786+
): Promise<void> {
787+
// Determine base directory and rules folder path based on source
788+
let baseDir: string
789+
let rulesFolderPath: string
790+
791+
if (source === "global") {
792+
baseDir = getGlobalRooDirectory()
793+
rulesFolderPath = path.join(baseDir, `rules-${importMode.slug}`)
794+
} else {
795+
const workspacePath = getWorkspacePath()
796+
baseDir = path.join(workspacePath, ".roo")
797+
rulesFolderPath = path.join(baseDir, `rules-${importMode.slug}`)
798+
}
799+
800+
// Always remove the existing rules folder for this mode if it exists
801+
// This ensures that if the imported mode has no rules, the folder is cleaned up
802+
try {
803+
await fs.rm(rulesFolderPath, { recursive: true, force: true })
804+
logger.info(`Removed existing ${source} rules folder for mode ${importMode.slug}`)
805+
} catch (error) {
806+
// It's okay if the folder doesn't exist
807+
logger.debug(`No existing ${source} rules folder to remove for mode ${importMode.slug}`)
808+
}
809+
810+
// Only proceed with file creation if there are rules files to import
811+
if (!rulesFiles || !Array.isArray(rulesFiles) || rulesFiles.length === 0) {
812+
return
813+
}
814+
815+
// Import the new rules files with path validation
816+
for (const ruleFile of rulesFiles) {
817+
if (ruleFile.relativePath && ruleFile.content) {
818+
// Validate the relative path to prevent path traversal attacks
819+
const normalizedRelativePath = path.normalize(ruleFile.relativePath)
820+
821+
// Ensure the path doesn't contain traversal sequences
822+
if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) {
823+
logger.error(`Invalid file path detected: ${ruleFile.relativePath}`)
824+
continue // Skip this file but continue with others
825+
}
826+
827+
const targetPath = path.join(baseDir, normalizedRelativePath)
828+
const normalizedTargetPath = path.normalize(targetPath)
829+
const expectedBasePath = path.normalize(baseDir)
830+
831+
// Ensure the resolved path stays within the base directory
832+
if (!normalizedTargetPath.startsWith(expectedBasePath)) {
833+
logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`)
834+
continue // Skip this file but continue with others
835+
}
836+
837+
// Ensure directory exists
838+
const targetDir = path.dirname(targetPath)
839+
await fs.mkdir(targetDir, { recursive: true })
840+
841+
// Write the file
842+
await fs.writeFile(targetPath, ruleFile.content, "utf-8")
843+
}
844+
}
845+
}
846+
758847
/**
759848
* Imports modes from YAML content, including their associated rules files
760849
* @param yamlContent - The YAML content containing mode configurations
@@ -821,100 +910,8 @@ export class CustomModesManager {
821910
source: source, // Use the provided source parameter
822911
})
823912

824-
// Handle project-level imports
825-
if (source === "project") {
826-
const workspacePath = getWorkspacePath()
827-
828-
// Always remove the existing rules folder for this mode if it exists
829-
// This ensures that if the imported mode has no rules, the folder is cleaned up
830-
const rulesFolderPath = path.join(workspacePath, ".roo", `rules-${importMode.slug}`)
831-
try {
832-
await fs.rm(rulesFolderPath, { recursive: true, force: true })
833-
logger.info(`Removed existing rules folder for mode ${importMode.slug}`)
834-
} catch (error) {
835-
// It's okay if the folder doesn't exist
836-
logger.debug(`No existing rules folder to remove for mode ${importMode.slug}`)
837-
}
838-
839-
// Only create new rules files if they exist in the import
840-
if (rulesFiles && Array.isArray(rulesFiles) && rulesFiles.length > 0) {
841-
// Import the new rules files with path validation
842-
for (const ruleFile of rulesFiles) {
843-
if (ruleFile.relativePath && ruleFile.content) {
844-
// Validate the relative path to prevent path traversal attacks
845-
const normalizedRelativePath = path.normalize(ruleFile.relativePath)
846-
847-
// Ensure the path doesn't contain traversal sequences
848-
if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) {
849-
logger.error(`Invalid file path detected: ${ruleFile.relativePath}`)
850-
continue // Skip this file but continue with others
851-
}
852-
853-
const targetPath = path.join(workspacePath, ".roo", normalizedRelativePath)
854-
const normalizedTargetPath = path.normalize(targetPath)
855-
const expectedBasePath = path.normalize(path.join(workspacePath, ".roo"))
856-
857-
// Ensure the resolved path stays within the .roo directory
858-
if (!normalizedTargetPath.startsWith(expectedBasePath)) {
859-
logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`)
860-
continue // Skip this file but continue with others
861-
}
862-
863-
// Ensure directory exists
864-
const targetDir = path.dirname(targetPath)
865-
await fs.mkdir(targetDir, { recursive: true })
866-
867-
// Write the file
868-
await fs.writeFile(targetPath, ruleFile.content, "utf-8")
869-
}
870-
}
871-
}
872-
} else if (source === "global" && rulesFiles && Array.isArray(rulesFiles)) {
873-
// For global imports, preserve the rules files structure in the global .roo directory
874-
const globalRooDir = getGlobalRooDirectory()
875-
876-
// Always remove the existing rules folder for this mode if it exists
877-
// This ensures that if the imported mode has no rules, the folder is cleaned up
878-
const rulesFolderPath = path.join(globalRooDir, `rules-${importMode.slug}`)
879-
try {
880-
await fs.rm(rulesFolderPath, { recursive: true, force: true })
881-
logger.info(`Removed existing global rules folder for mode ${importMode.slug}`)
882-
} catch (error) {
883-
// It's okay if the folder doesn't exist
884-
logger.debug(`No existing global rules folder to remove for mode ${importMode.slug}`)
885-
}
886-
887-
// Import the new rules files with path validation
888-
for (const ruleFile of rulesFiles) {
889-
if (ruleFile.relativePath && ruleFile.content) {
890-
// Validate the relative path to prevent path traversal attacks
891-
const normalizedRelativePath = path.normalize(ruleFile.relativePath)
892-
893-
// Ensure the path doesn't contain traversal sequences
894-
if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) {
895-
logger.error(`Invalid file path detected: ${ruleFile.relativePath}`)
896-
continue // Skip this file but continue with others
897-
}
898-
899-
const targetPath = path.join(globalRooDir, normalizedRelativePath)
900-
const normalizedTargetPath = path.normalize(targetPath)
901-
const expectedBasePath = path.normalize(globalRooDir)
902-
903-
// Ensure the resolved path stays within the global .roo directory
904-
if (!normalizedTargetPath.startsWith(expectedBasePath)) {
905-
logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`)
906-
continue // Skip this file but continue with others
907-
}
908-
909-
// Ensure directory exists
910-
const targetDir = path.dirname(targetPath)
911-
await fs.mkdir(targetDir, { recursive: true })
912-
913-
// Write the file
914-
await fs.writeFile(targetPath, ruleFile.content, "utf-8")
915-
}
916-
}
917-
}
913+
// Import rules files (this also handles cleanup of existing rules folders)
914+
await this.importRulesFiles(importMode, rulesFiles || [], source)
918915
}
919916

920917
// Refresh the modes after import

0 commit comments

Comments
 (0)