@@ -5,7 +5,7 @@ import * as fs from "fs/promises"
55import * as yaml from "yaml"
66import stripBom from "strip-bom"
77
8- import { type ModeConfig , customModesSettingsSchema } from "@roo-code/types"
8+ import { type ModeConfig , customModesSettingsSchema , modeConfigSchema } from "@roo-code/types"
99
1010import { fileExistsAtPath } from "../../utils/fs"
1111import { getWorkspacePath } from "../../utils/path"
@@ -17,6 +17,31 @@ import { loadRuleFiles } from "../prompts/sections/custom-instructions"
1717
1818const ROOMODES_FILENAME = ".roomodes"
1919
20+ // Type definitions for import/export functionality
21+ interface RuleFile {
22+ relativePath : string
23+ content : string
24+ }
25+
26+ interface ExportedModeConfig extends ModeConfig {
27+ rulesFiles ?: RuleFile [ ]
28+ }
29+
30+ interface ImportData {
31+ customModes : ExportedModeConfig [ ]
32+ }
33+
34+ interface ExportResult {
35+ success : boolean
36+ yaml ?: string
37+ error ?: string
38+ }
39+
40+ interface ImportResult {
41+ success : boolean
42+ error ?: string
43+ }
44+
2045export class CustomModesManager {
2146 private static readonly cacheTTL = 10_000
2247
@@ -499,6 +524,11 @@ export class CustomModesManager {
499524 }
500525 }
501526
527+ /**
528+ * Checks if a mode has associated rules files in the .roo/rules-{slug}/ directory
529+ * @param slug - The mode identifier to check
530+ * @returns True if the mode has rules files with content, false otherwise
531+ */
502532 public async checkRulesDirectoryHasContent ( slug : string ) : Promise < boolean > {
503533 try {
504534 // Get workspace path
@@ -580,7 +610,12 @@ export class CustomModesManager {
580610 }
581611 }
582612
583- public async exportModeWithRules ( slug : string ) : Promise < { success : boolean ; yaml ?: string ; error ?: string } > {
613+ /**
614+ * Exports a mode configuration with its associated rules files into a shareable YAML format
615+ * @param slug - The mode identifier to export
616+ * @returns Success status with YAML content or error message
617+ */
618+ public async exportModeWithRules ( slug : string ) : Promise < ExportResult > {
584619 try {
585620 // Import modes from shared to check built-in modes
586621 const { modes : builtInModes } = await import ( "../../shared/modes" )
@@ -632,7 +667,7 @@ export class CustomModesManager {
632667 // Check for .roo/rules-{slug}/ directory
633668 const modeRulesDir = path . join ( workspacePath , ".roo" , `rules-${ slug } ` )
634669
635- let rulesFiles : Array < { relativePath : string ; content : string } > = [ ]
670+ let rulesFiles : RuleFile [ ] = [ ]
636671 try {
637672 const stats = await fs . stat ( modeRulesDir )
638673 if ( stats . isDirectory ( ) ) {
@@ -656,7 +691,7 @@ export class CustomModesManager {
656691 }
657692
658693 // Create an export mode with rules files preserved
659- const exportMode : ModeConfig & { rulesFiles ?: Array < { relativePath : string ; content : string } > } = {
694+ const exportMode : ExportedModeConfig = {
660695 ...mode ,
661696 // Remove source property for export
662697 source : undefined as any ,
@@ -682,20 +717,33 @@ export class CustomModesManager {
682717 }
683718 }
684719
720+ /**
721+ * Imports modes from YAML content, including their associated rules files
722+ * @param yamlContent - The YAML content containing mode configurations
723+ * @param source - Target level for import: "global" (all projects) or "project" (current workspace only)
724+ * @returns Success status with optional error message
725+ */
685726 public async importModeWithRules (
686727 yamlContent : string ,
687728 source : "global" | "project" = "project" ,
688- ) : Promise < { success : boolean ; error ?: string } > {
729+ ) : Promise < ImportResult > {
689730 try {
690- // Parse the YAML content
691- const importData = yaml . parse ( yamlContent )
692-
693- if (
694- ! importData ?. customModes ||
695- ! Array . isArray ( importData . customModes ) ||
696- importData . customModes . length === 0
697- ) {
698- return { success : false , error : "Invalid import format: no custom modes found" }
731+ // Parse the YAML content with proper type validation
732+ let importData : ImportData
733+ try {
734+ const parsed = yaml . parse ( yamlContent )
735+
736+ // Validate the structure
737+ if ( ! parsed ?. customModes || ! Array . isArray ( parsed . customModes ) || parsed . customModes . length === 0 ) {
738+ return { success : false , error : "Invalid import format: Expected 'customModes' array in YAML" }
739+ }
740+
741+ importData = parsed as ImportData
742+ } catch ( parseError ) {
743+ return {
744+ success : false ,
745+ error : `Invalid YAML format: ${ parseError instanceof Error ? parseError . message : "Failed to parse YAML" } ` ,
746+ }
699747 }
700748
701749 // Check workspace availability early if importing at project level
@@ -710,6 +758,25 @@ export class CustomModesManager {
710758 for ( const importMode of importData . customModes ) {
711759 const { rulesFiles, ...modeConfig } = importMode
712760
761+ // Validate the mode configuration
762+ const validationResult = modeConfigSchema . safeParse ( modeConfig )
763+ if ( ! validationResult . success ) {
764+ logger . error ( `Invalid mode configuration for ${ modeConfig . slug } ` , {
765+ errors : validationResult . error . errors ,
766+ } )
767+ return {
768+ success : false ,
769+ error : `Invalid mode configuration for ${ modeConfig . slug } : ${ validationResult . error . errors . map ( ( e ) => e . message ) . join ( ", " ) } ` ,
770+ }
771+ }
772+
773+ // Check for existing mode conflicts
774+ const existingModes = await this . getCustomModes ( )
775+ const existingMode = existingModes . find ( ( m ) => m . slug === importMode . slug )
776+ if ( existingMode ) {
777+ logger . info ( `Overwriting existing mode: ${ importMode . slug } ` )
778+ }
779+
713780 // Import the mode configuration with the specified source
714781 await this . updateCustomMode ( importMode . slug , {
715782 ...modeConfig ,
@@ -733,10 +800,27 @@ export class CustomModesManager {
733800
734801 // Only create new rules files if they exist in the import
735802 if ( rulesFiles && Array . isArray ( rulesFiles ) && rulesFiles . length > 0 ) {
736- // Import the new rules files
803+ // Import the new rules files with path validation
737804 for ( const ruleFile of rulesFiles ) {
738805 if ( ruleFile . relativePath && ruleFile . content ) {
739- const targetPath = path . join ( workspacePath , ".roo" , ruleFile . relativePath )
806+ // Validate the relative path to prevent path traversal attacks
807+ const normalizedRelativePath = path . normalize ( ruleFile . relativePath )
808+
809+ // Ensure the path doesn't contain traversal sequences
810+ if ( normalizedRelativePath . includes ( ".." ) || path . isAbsolute ( normalizedRelativePath ) ) {
811+ logger . error ( `Invalid file path detected: ${ ruleFile . relativePath } ` )
812+ continue // Skip this file but continue with others
813+ }
814+
815+ const targetPath = path . join ( workspacePath , ".roo" , normalizedRelativePath )
816+ const normalizedTargetPath = path . normalize ( targetPath )
817+ const expectedBasePath = path . normalize ( path . join ( workspacePath , ".roo" ) )
818+
819+ // Ensure the resolved path stays within the .roo directory
820+ if ( ! normalizedTargetPath . startsWith ( expectedBasePath ) ) {
821+ logger . error ( `Path traversal attempt detected: ${ ruleFile . relativePath } ` )
822+ continue // Skip this file but continue with others
823+ }
740824
741825 // Ensure directory exists
742826 const targetDir = path . dirname ( targetPath )
0 commit comments