@@ -2,17 +2,23 @@ import * as vscode from "vscode"
22import * as path from "path"
33import * as fs from "fs/promises"
44import * as yaml from "yaml"
5+ import * as os from "os"
56import type { MarketplaceItem , MarketplaceItemType , InstallMarketplaceItemOptions , McpParameter } from "@roo-code/types"
67import { GlobalFileNames } from "../../shared/globalFileNames"
78import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
9+ import { fileExistsAtPath } from "../../utils/fs"
10+ import type { CustomModesManager } from "../../core/config/CustomModesManager"
811
912export interface InstallOptions extends InstallMarketplaceItemOptions {
1013 target : "project" | "global"
1114 selectedIndex ?: number // Which installation method to use (for array content)
1215}
1316
1417export class SimpleInstaller {
15- constructor ( private readonly context : vscode . ExtensionContext ) { }
18+ constructor (
19+ private readonly context : vscode . ExtensionContext ,
20+ private readonly customModesManager ?: CustomModesManager ,
21+ ) { }
1622
1723 async installItem ( item : MarketplaceItem , options : InstallOptions ) : Promise < { filePath : string ; line ?: number } > {
1824 const { target } = options
@@ -40,6 +46,48 @@ export class SimpleInstaller {
4046 throw new Error ( "Mode content should not be an array" )
4147 }
4248
49+ // If CustomModesManager is available, use importModeWithRules
50+ if ( this . customModesManager ) {
51+ // Transform marketplace content to import format (wrap in customModes array)
52+ const importData = {
53+ customModes : [ yaml . parse ( item . content ) ] ,
54+ }
55+ const importYaml = yaml . stringify ( importData )
56+
57+ // Call customModesManager.importModeWithRules
58+ const result = await this . customModesManager . importModeWithRules ( importYaml , target )
59+
60+ if ( ! result . success ) {
61+ throw new Error ( result . error || "Failed to import mode" )
62+ }
63+
64+ // Return the file path and line number for VS Code to open
65+ const filePath = await this . getModeFilePath ( target )
66+
67+ // Try to find the line number where the mode was added
68+ let line : number | undefined
69+ try {
70+ const fileContent = await fs . readFile ( filePath , "utf-8" )
71+ const lines = fileContent . split ( "\n" )
72+ const modeData = yaml . parse ( item . content )
73+
74+ // Find the line containing the slug of the added mode
75+ if ( modeData ?. slug ) {
76+ const slugLineIndex = lines . findIndex (
77+ ( l ) => l . includes ( `slug: ${ modeData . slug } ` ) || l . includes ( `slug: "${ modeData . slug } "` ) ,
78+ )
79+ if ( slugLineIndex >= 0 ) {
80+ line = slugLineIndex + 1 // Convert to 1-based line number
81+ }
82+ }
83+ } catch ( error ) {
84+ // If we can't find the line number, that's okay
85+ }
86+
87+ return { filePath, line }
88+ }
89+
90+ // Fallback to original implementation if CustomModesManager is not available
4391 const filePath = await this . getModeFilePath ( target )
4492 const modeData = yaml . parse ( item . content )
4593
@@ -248,55 +296,69 @@ export class SimpleInstaller {
248296 }
249297
250298 private async removeMode ( item : MarketplaceItem , target : "project" | "global" ) : Promise < void > {
251- const filePath = await this . getModeFilePath ( target )
299+ if ( ! this . customModesManager ) {
300+ throw new Error ( "CustomModesManager is not available" )
301+ }
252302
303+ // Parse the item content to get the slug
304+ let content : string
305+ if ( Array . isArray ( item . content ) ) {
306+ // Array of McpInstallationMethod objects - use first method
307+ content = item . content [ 0 ] . content
308+ } else {
309+ content = item . content || ""
310+ }
311+
312+ let modeSlug : string
253313 try {
254- const existing = await fs . readFile ( filePath , "utf-8" )
255- let existingData : any
314+ const modeData = yaml . parse ( content )
315+ modeSlug = modeData . slug
316+ } catch ( error ) {
317+ throw new Error ( "Invalid mode content: unable to parse YAML" )
318+ }
256319
257- try {
258- const parsed = yaml . parse ( existing )
259- // Ensure we have a valid object
260- existingData = parsed && typeof parsed === "object" ? parsed : { }
261- } catch ( parseError ) {
262- // If we can't parse the file, we can't safely remove a mode
263- const fileName = target === "project" ? ".roomodes" : "custom-modes.yaml"
264- throw new Error (
265- `Cannot remove mode: The ${ fileName } file contains invalid YAML. ` +
266- `Please fix the syntax errors before removing modes.` ,
267- )
268- }
320+ if ( ! modeSlug ) {
321+ throw new Error ( "Mode missing slug identifier" )
322+ }
269323
270- // Ensure customModes array exists
271- if ( ! existingData . customModes ) {
272- existingData . customModes = [ ]
273- }
324+ // Get the mode details before deletion to determine source and rules folder path
325+ const customModes = await this . customModesManager . getCustomModes ( )
326+ const modeToDelete = customModes . find ( ( mode ) => mode . slug === modeSlug )
274327
275- // Parse the item content to get the slug
276- let content : string
277- if ( Array . isArray ( item . content ) ) {
278- // Array of McpInstallationMethod objects - use first method
279- content = item . content [ 0 ] . content
280- } else {
281- content = item . content
282- }
283- const modeData = yaml . parse ( content || "" )
328+ // Use CustomModesManager to delete the mode configuration
329+ await this . customModesManager . deleteCustomMode ( modeSlug )
284330
285- if ( ! modeData . slug ) {
286- return // Nothing to remove if no slug
287- }
331+ // Handle rules folder deletion separately (similar to webviewMessageHandler)
332+ if ( modeToDelete ) {
333+ // Determine the scope based on source (project or global)
334+ const scope = modeToDelete . source || "global"
288335
289- // Remove mode with matching slug
290- existingData . customModes = existingData . customModes . filter ( ( mode : any ) => mode . slug !== modeData . slug )
336+ // Determine the rules folder path
337+ let rulesFolderPath : string
338+ if ( scope === "project" ) {
339+ const workspaceFolder = vscode . workspace . workspaceFolders ?. [ 0 ]
340+ if ( workspaceFolder ) {
341+ rulesFolderPath = path . join ( workspaceFolder . uri . fsPath , ".roo" , `rules-${ modeSlug } ` )
342+ } else {
343+ return // No workspace, can't delete project rules
344+ }
345+ } else {
346+ // Global scope - use OS home directory
347+ const homeDir = os . homedir ( )
348+ rulesFolderPath = path . join ( homeDir , ".roo" , `rules-${ modeSlug } ` )
349+ }
291350
292- // Always write back the file, even if empty
293- await fs . writeFile ( filePath , yaml . stringify ( existingData , { lineWidth : 0 } ) , "utf-8" )
294- } catch ( error : any ) {
295- if ( error . code === "ENOENT" ) {
296- // File doesn't exist, nothing to remove
297- return
351+ // Check if the rules folder exists and delete it
352+ const rulesFolderExists = await fileExistsAtPath ( rulesFolderPath )
353+ if ( rulesFolderExists ) {
354+ try {
355+ await fs . rm ( rulesFolderPath , { recursive : true , force : true } )
356+ console . log ( `Deleted rules folder for mode ${ modeSlug } : ${ rulesFolderPath } ` )
357+ } catch ( error ) {
358+ console . error ( `Failed to delete rules folder for mode ${ modeSlug } : ${ error } ` )
359+ // Continue even if folder deletion fails
360+ }
298361 }
299- throw error
300362 }
301363 }
302364
0 commit comments