@@ -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