Skip to content

Commit d8590c5

Browse files
committed
fix: marketplace mode removal now deletes associated rules files (#3)
- Updated SimpleInstaller.removeMode to use CustomModesManager.deleteCustomMode - Added confirmation dialog with rules deletion warning for mode removal - Implemented marketplaceRemoveResult message for proper UI refresh - Added comprehensive tests for rules directory cleanup - Updated all locale files with new confirmation dialog strings This ensures complete cleanup when removing marketplace modes, preventing orphaned rules directories in .roo/rules-{slug}/ folders.
1 parent c47de36 commit d8590c5

25 files changed

+620
-208
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class ClineProvider
158158
this.log(`Failed to initialize MCP Hub: ${error}`)
159159
})
160160

161-
this.marketplaceManager = new MarketplaceManager(this.context)
161+
this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
162162
}
163163

164164
// Adds a new Cline instance to clineStack, marking the start of a new task.

src/core/webview/webviewMessageHandler.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,8 +2229,22 @@ export const webviewMessageHandler = async (
22292229
try {
22302230
await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
22312231
await provider.postStateToWebview()
2232+
2233+
// Send success message to webview
2234+
provider.postMessageToWebview({
2235+
type: "marketplaceRemoveResult",
2236+
success: true,
2237+
slug: message.mpItem.id,
2238+
})
22322239
} catch (error) {
22332240
console.error(`Error removing marketplace item: ${error}`)
2241+
// Send error message to webview
2242+
provider.postMessageToWebview({
2243+
type: "marketplaceRemoveResult",
2244+
success: false,
2245+
error: error instanceof Error ? error.message : String(error),
2246+
slug: message.mpItem.id,
2247+
})
22342248
}
22352249
}
22362250
break

src/services/marketplace/MarketplaceManager.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import { GlobalFileNames } from "../../shared/globalFileNames"
99
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
1010
import { t } from "../../i18n"
1111
import { TelemetryService } from "@roo-code/telemetry"
12+
import type { CustomModesManager } from "../../core/config/CustomModesManager"
1213

1314
export class MarketplaceManager {
1415
private configLoader: RemoteConfigLoader
1516
private installer: SimpleInstaller
1617

17-
constructor(private readonly context: vscode.ExtensionContext) {
18+
constructor(
19+
private readonly context: vscode.ExtensionContext,
20+
private readonly customModesManager?: CustomModesManager,
21+
) {
1822
this.configLoader = new RemoteConfigLoader()
19-
this.installer = new SimpleInstaller(context)
23+
this.installer = new SimpleInstaller(context, customModesManager)
2024
}
2125

2226
async getMarketplaceItems(): Promise<{ items: MarketplaceItem[]; errors?: string[] }> {

src/services/marketplace/SimpleInstaller.ts

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ import * as vscode from "vscode"
22
import * as path from "path"
33
import * as fs from "fs/promises"
44
import * as yaml from "yaml"
5+
import * as os from "os"
56
import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types"
67
import { GlobalFileNames } from "../../shared/globalFileNames"
78
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
9+
import { fileExistsAtPath } from "../../utils/fs"
10+
import type { CustomModesManager } from "../../core/config/CustomModesManager"
811

912
export interface InstallOptions extends InstallMarketplaceItemOptions {
1013
target: "project" | "global"
1114
selectedIndex?: number // Which installation method to use (for array content)
1215
}
1316

1417
export 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

Comments
 (0)