diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index f981ba7bf9..3c30f555a7 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -102,6 +102,9 @@ export const modeConfigSchema = z.object({ customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), + // Marketplace origin metadata (optional) + installedFromMarketplace: z.boolean().optional(), + marketplaceItemId: z.string().optional(), }) export type ModeConfig = z.infer diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index a243a9236b..90fef93801 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -552,6 +552,58 @@ export class CustomModesManager { vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) } } + /** + * Deletes a custom mode only from the specified source (project or global) + * without affecting the other scope. Also handles rules folder cleanup. + * @param slug - The mode slug + * @param source - "project" or "global" + * @param fromMarketplace - Whether this deletion was initiated by marketplace flows + */ + public async deleteCustomModeForSource( + slug: string, + source: "project" | "global", + fromMarketplace = false, + ): Promise { + try { + await this.queueWrite(async () => { + const settingsPath = await this.getCustomModesFilePath() + const roomodesPath = await this.getWorkspaceRoomodes() + + let targetPath: string + let modeToDelete: ModeConfig | undefined + + if (source === "project") { + if (!roomodesPath) { + throw new Error(t("common:customModes.errors.noWorkspaceForProject")) + } + targetPath = roomodesPath + const roomodesModes = await this.loadModesFromFile(roomodesPath) + modeToDelete = roomodesModes.find((m) => m.slug === slug) + } else { + targetPath = settingsPath + const settingsModes = await this.loadModesFromFile(settingsPath) + modeToDelete = settingsModes.find((m) => m.slug === slug) + } + + if (!modeToDelete) { + throw new Error(t("common:customModes.errors.modeNotFound")) + } + + // Delete only from the selected source file + await this.updateModesInFile(targetPath, (modes) => modes.filter((m) => m.slug !== slug)) + + // Delete associated rules folder using the located mode (preserves correct scope) + await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace) + + // Refresh state and clear caches + this.clearCache() + await this.refreshMergedState() + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) + } + } /** * Deletes the rules folder for a specific mode diff --git a/src/i18n/locales/ca/marketplace.json b/src/i18n/locales/ca/marketplace.json index cf0710ac97..ca4303f943 100644 --- a/src/i18n/locales/ca/marketplace.json +++ b/src/i18n/locales/ca/marketplace.json @@ -64,6 +64,7 @@ "installError": "Error en instal·lar \"{{itemName}}\": {{errorMessage}}", "removing": "Eliminant element: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" eliminat correctament", - "removeError": "Error en eliminar \"{{itemName}}\": {{errorMessage}}" + "removeError": "Error en eliminar \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode no trobat" } } diff --git a/src/i18n/locales/de/marketplace.json b/src/i18n/locales/de/marketplace.json index dea82aa44c..2e9e771f60 100644 --- a/src/i18n/locales/de/marketplace.json +++ b/src/i18n/locales/de/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installation von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}", "removing": "Element wird entfernt: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" erfolgreich entfernt", - "removeError": "Entfernung von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}" + "removeError": "Entfernung von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}", + "notInstalledForTarget": "Modus nicht gefunden" } } diff --git a/src/i18n/locales/en/marketplace.json b/src/i18n/locales/en/marketplace.json index 17ae20078a..b911cb8ec5 100644 --- a/src/i18n/locales/en/marketplace.json +++ b/src/i18n/locales/en/marketplace.json @@ -64,6 +64,7 @@ "installError": "Failed to install \"{{itemName}}\": {{errorMessage}}", "removing": "Removing item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removed successfully", - "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}" + "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode not found" } } diff --git a/src/i18n/locales/es/marketplace.json b/src/i18n/locales/es/marketplace.json index ecee7c1645..aecc70f763 100644 --- a/src/i18n/locales/es/marketplace.json +++ b/src/i18n/locales/es/marketplace.json @@ -64,6 +64,7 @@ "installError": "Error al instalar \"{{itemName}}\": {{errorMessage}}", "removing": "Eliminando elemento: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" eliminado correctamente", - "removeError": "Error al eliminar \"{{itemName}}\": {{errorMessage}}" + "removeError": "Error al eliminar \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Modo no encontrado" } } diff --git a/src/i18n/locales/fr/marketplace.json b/src/i18n/locales/fr/marketplace.json index 7a6a3dbd74..bcf5d793b2 100644 --- a/src/i18n/locales/fr/marketplace.json +++ b/src/i18n/locales/fr/marketplace.json @@ -64,6 +64,7 @@ "installError": "Échec de l'installation de \"{{itemName}}\" : {{errorMessage}}", "removing": "Suppression de l'élément : \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" supprimé avec succès", - "removeError": "Échec de la suppression de \"{{itemName}}\" : {{errorMessage}}" + "removeError": "Échec de la suppression de \"{{itemName}}\" : {{errorMessage}}", + "notInstalledForTarget": "Mode non trouvé" } } diff --git a/src/i18n/locales/hi/marketplace.json b/src/i18n/locales/hi/marketplace.json index 3797c50b01..e30f95d6d9 100644 --- a/src/i18n/locales/hi/marketplace.json +++ b/src/i18n/locales/hi/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" इंस्टॉल करने में विफल: {{errorMessage}}", "removing": "आइटम हटा रहे हैं: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" सफलतापूर्वक हटाया गया", - "removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}" + "removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}", + "notInstalledForTarget": "मोड नहीं मिला" } } diff --git a/src/i18n/locales/id/marketplace.json b/src/i18n/locales/id/marketplace.json index 7f31c78cbf..1194d54eb6 100644 --- a/src/i18n/locales/id/marketplace.json +++ b/src/i18n/locales/id/marketplace.json @@ -64,6 +64,7 @@ "installError": "Gagal menginstal \"{{itemName}}\": {{errorMessage}}", "removing": "Menghapus item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" berhasil dihapus", - "removeError": "Gagal menghapus \"{{itemName}}\": {{errorMessage}}" + "removeError": "Gagal menghapus \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode tidak ditemukan" } } diff --git a/src/i18n/locales/it/marketplace.json b/src/i18n/locales/it/marketplace.json index 46c50acc7d..fa80eb78f1 100644 --- a/src/i18n/locales/it/marketplace.json +++ b/src/i18n/locales/it/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installazione di \"{{itemName}}\" fallita: {{errorMessage}}", "removing": "Rimozione elemento: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" rimosso con successo", - "removeError": "Rimozione di \"{{itemName}}\" fallita: {{errorMessage}}" + "removeError": "Rimozione di \"{{itemName}}\" fallita: {{errorMessage}}", + "notInstalledForTarget": "Modalità non trovata" } } diff --git a/src/i18n/locales/ja/marketplace.json b/src/i18n/locales/ja/marketplace.json index 426a5db1fd..a7a7156f0c 100644 --- a/src/i18n/locales/ja/marketplace.json +++ b/src/i18n/locales/ja/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\"のインストールに失敗しました: {{errorMessage}}", "removing": "アイテムを削除中: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\"の削除が完了しました", - "removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}" + "removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}", + "notInstalledForTarget": "モードが見つかりません" } } diff --git a/src/i18n/locales/ko/marketplace.json b/src/i18n/locales/ko/marketplace.json index 349f0c5ad8..61c98e940a 100644 --- a/src/i18n/locales/ko/marketplace.json +++ b/src/i18n/locales/ko/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" 설치 실패: {{errorMessage}}", "removing": "항목 제거 중: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" 제거 완료", - "removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}" + "removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}", + "notInstalledForTarget": "모드를 찾을 수 없습니다" } } diff --git a/src/i18n/locales/nl/marketplace.json b/src/i18n/locales/nl/marketplace.json index 1274666b9c..609ed2d3c5 100644 --- a/src/i18n/locales/nl/marketplace.json +++ b/src/i18n/locales/nl/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installatie van \"{{itemName}}\" mislukt: {{errorMessage}}", "removing": "Item verwijderen: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" succesvol verwijderd", - "removeError": "Verwijdering van \"{{itemName}}\" mislukt: {{errorMessage}}" + "removeError": "Verwijdering van \"{{itemName}}\" mislukt: {{errorMessage}}", + "notInstalledForTarget": "Modus niet gevonden" } } diff --git a/src/i18n/locales/pl/marketplace.json b/src/i18n/locales/pl/marketplace.json index 3ba3c076ad..d2a77a030e 100644 --- a/src/i18n/locales/pl/marketplace.json +++ b/src/i18n/locales/pl/marketplace.json @@ -64,6 +64,7 @@ "installError": "Instalacja \"{{itemName}}\" nie powiodła się: {{errorMessage}}", "removing": "Usuwanie elementu: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" usunięto pomyślnie", - "removeError": "Usunięcie \"{{itemName}}\" nie powiodło się: {{errorMessage}}" + "removeError": "Usunięcie \"{{itemName}}\" nie powiodło się: {{errorMessage}}", + "notInstalledForTarget": "Nie znaleziono trybu" } } diff --git a/src/i18n/locales/pt-BR/marketplace.json b/src/i18n/locales/pt-BR/marketplace.json index b0e763b0ff..b5a0ab887f 100644 --- a/src/i18n/locales/pt-BR/marketplace.json +++ b/src/i18n/locales/pt-BR/marketplace.json @@ -64,6 +64,7 @@ "installError": "Falha ao instalar \"{{itemName}}\": {{errorMessage}}", "removing": "Removendo item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removido com sucesso", - "removeError": "Falha ao remover \"{{itemName}}\": {{errorMessage}}" + "removeError": "Falha ao remover \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Modo não encontrado" } } diff --git a/src/i18n/locales/ru/marketplace.json b/src/i18n/locales/ru/marketplace.json index 1c4ba45df5..3f53914772 100644 --- a/src/i18n/locales/ru/marketplace.json +++ b/src/i18n/locales/ru/marketplace.json @@ -64,6 +64,7 @@ "installError": "Не удалось установить \"{{itemName}}\": {{errorMessage}}", "removing": "Удаление элемента: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" успешно удален", - "removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}" + "removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Режим не найден" } } diff --git a/src/i18n/locales/tr/marketplace.json b/src/i18n/locales/tr/marketplace.json index 628c239a12..f60ec3c6e8 100644 --- a/src/i18n/locales/tr/marketplace.json +++ b/src/i18n/locales/tr/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" yüklenemedi: {{errorMessage}}", "removing": "Öğe kaldırılıyor: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" başarıyla kaldırıldı", - "removeError": "\"{{itemName}}\" kaldırılamadı: {{errorMessage}}" + "removeError": "\"{{itemName}}\" kaldırılamadı: {{errorMessage}}", + "notInstalledForTarget": "Mod bulunamadı" } } diff --git a/src/i18n/locales/vi/marketplace.json b/src/i18n/locales/vi/marketplace.json index 8fbc1a7ad0..f6cda48e35 100644 --- a/src/i18n/locales/vi/marketplace.json +++ b/src/i18n/locales/vi/marketplace.json @@ -64,6 +64,7 @@ "installError": "Cài đặt \"{{itemName}}\" thất bại: {{errorMessage}}", "removing": "Đang xóa mục: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" đã được xóa thành công", - "removeError": "Xóa \"{{itemName}}\" thất bại: {{errorMessage}}" + "removeError": "Xóa \"{{itemName}}\" thất bại: {{errorMessage}}", + "notInstalledForTarget": "Không tìm thấy chế độ" } } diff --git a/src/i18n/locales/zh-CN/marketplace.json b/src/i18n/locales/zh-CN/marketplace.json index a69e9c1e9d..06c1d0463d 100644 --- a/src/i18n/locales/zh-CN/marketplace.json +++ b/src/i18n/locales/zh-CN/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" 安装失败:{{errorMessage}}", "removing": "正在移除项目:\"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" 移除成功", - "removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}" + "removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}", + "notInstalledForTarget": "未找到模式" } } diff --git a/src/i18n/locales/zh-TW/marketplace.json b/src/i18n/locales/zh-TW/marketplace.json index 9270314108..37c4e8f760 100644 --- a/src/i18n/locales/zh-TW/marketplace.json +++ b/src/i18n/locales/zh-TW/marketplace.json @@ -64,6 +64,7 @@ "installError": "「{{itemName}}」安裝失敗:{{errorMessage}}", "removing": "正在移除項目:「{{itemName}}」", "removeSuccess": "「{{itemName}}」移除成功", - "removeError": "「{{itemName}}」移除失敗:{{errorMessage}}" + "removeError": "「{{itemName}}」移除失敗:{{errorMessage}}", + "notInstalledForTarget": "找不到模式" } } diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index dfde5600b9..72ac6b64da 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -4,7 +4,14 @@ import * as path from "path" import * as vscode from "vscode" import * as yaml from "yaml" -import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem } from "@roo-code/types" +import type { + OrganizationSettings, + MarketplaceItem, + MarketplaceItemType, + McpMarketplaceItem, + ModeConfig, +} from "@roo-code/types" +import { customModesSettingsSchema } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -241,6 +248,35 @@ export class MarketplaceManager { return metadata } + // Helper: identify marketplace-installed modes + private isMarketplaceInstalledMode( + mode: ModeConfig, + ): mode is ModeConfig & { installedFromMarketplace: true; marketplaceItemId: string } { + return ( + mode.installedFromMarketplace === true && + typeof mode.marketplaceItemId === "string" && + mode.marketplaceItemId.length > 0 + ) + } + + // Helper: parse YAML and collect installed mode metadata with proper typing + private collectInstalledModesFromYaml(content: string, out: Record): void { + try { + const parsed = yaml.parse(content) + const result = customModesSettingsSchema.safeParse(parsed) + if (!result.success) { + return + } + for (const mode of result.data.customModes) { + if (this.isMarketplaceInstalledMode(mode)) { + out[mode.marketplaceItemId] = { type: "mode" } + } + } + } catch { + // Ignore parse errors here; caller handles file existence/errors + } + } + /** * Check for project-level installed items */ @@ -255,16 +291,7 @@ export class MarketplaceManager { const projectModesPath = path.join(workspaceFolder.uri.fsPath, ".roomodes") try { const content = await fs.readFile(projectModesPath, "utf-8") - const data = yaml.parse(content) - if (data?.customModes && Array.isArray(data.customModes)) { - for (const mode of data.customModes) { - if (mode.slug) { - metadata[mode.slug] = { - type: "mode", - } - } - } - } + this.collectInstalledModesFromYaml(content, metadata) } catch (error) { // File doesn't exist or can't be read, skip } @@ -300,16 +327,7 @@ export class MarketplaceManager { const globalModesPath = path.join(globalSettingsPath, GlobalFileNames.customModes) try { const content = await fs.readFile(globalModesPath, "utf-8") - const data = yaml.parse(content) - if (data?.customModes && Array.isArray(data.customModes)) { - for (const mode of data.customModes) { - if (mode.slug) { - metadata[mode.slug] = { - type: "mode", - } - } - } - } + this.collectInstalledModesFromYaml(content, metadata) } catch (error) { // File doesn't exist or can't be read, skip } diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index be002e2f1d..ac53c14fb8 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -2,14 +2,16 @@ import * as vscode from "vscode" import * as path from "path" import * as fs from "fs/promises" import * as yaml from "yaml" -import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types" +import type { MarketplaceItem, McpParameter, ModeConfig } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import type { CustomModesManager } from "../../core/config/CustomModesManager" +import { t } from "../../i18n" -export interface InstallOptions extends InstallMarketplaceItemOptions { +export interface InstallOptions { target: "project" | "global" selectedIndex?: number // Which installation method to use (for array content) + parameters?: Record } export class SimpleInstaller { @@ -20,6 +22,7 @@ export class SimpleInstaller { async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> { const { target } = options + const itemType = item.type switch (item.type) { case "mode": @@ -27,7 +30,7 @@ export class SimpleInstaller { case "mcp": return await this.installMcp(item, target, options) default: - throw new Error(`Unsupported item type: ${(item as any).type}`) + throw new Error(`Unsupported item type: ${itemType}`) } } @@ -47,8 +50,14 @@ export class SimpleInstaller { // If CustomModesManager is available, use importModeWithRules if (this.customModesManager) { // Transform marketplace content to import format (wrap in customModes array) + const parsedMode = yaml.parse(item.content) + // Annotate marketplace origin to disambiguate from user-created modes + if (parsedMode && typeof parsedMode === "object") { + ;(parsedMode as ModeConfig).installedFromMarketplace = true + ;(parsedMode as ModeConfig).marketplaceItemId = item.id + } const importData = { - customModes: [yaml.parse(item.content)], + customModes: [parsedMode], } const importYaml = yaml.stringify(importData) @@ -72,7 +81,7 @@ export class SimpleInstaller { // Find the line containing the slug of the added mode if (modeData?.slug) { const slugLineIndex = lines.findIndex( - (l) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`), + (l: string) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`), ) if (slugLineIndex >= 0) { line = slugLineIndex + 1 // Convert to 1-based line number @@ -88,6 +97,11 @@ export class SimpleInstaller { // Fallback to original implementation if CustomModesManager is not available const filePath = await this.getModeFilePath(target) const modeData = yaml.parse(item.content) + // Annotate marketplace origin fields for fallback path as well + if (modeData && typeof modeData === "object") { + ;(modeData as ModeConfig).installedFromMarketplace = true + ;(modeData as ModeConfig).marketplaceItemId = item.id + } // Read existing file or create new structure let existingData: any = { customModes: [] } @@ -143,7 +157,7 @@ export class SimpleInstaller { const addedMode = existingData.customModes[addedModeIndex] if (addedMode?.slug) { const slugLineIndex = lines.findIndex( - (l) => l.includes(`slug: ${addedMode.slug}`) || l.includes(`slug: "${addedMode.slug}"`), + (l: string) => l.includes(`slug: ${addedMode.slug}`) || l.includes(`slug: "${addedMode.slug}"`), ) if (slugLineIndex >= 0) { line = slugLineIndex + 1 // Convert to 1-based line number @@ -280,6 +294,7 @@ export class SimpleInstaller { async removeItem(item: MarketplaceItem, options: InstallOptions): Promise { const { target } = options + const itemType = item.type switch (item.type) { case "mode": @@ -289,7 +304,7 @@ export class SimpleInstaller { await this.removeMcp(item, target) break default: - throw new Error(`Unsupported item type: ${(item as any).type}`) + throw new Error(`Unsupported item type: ${itemType}`) } } @@ -319,13 +334,23 @@ export class SimpleInstaller { throw new Error("Mode missing slug identifier") } - // Get the current modes to determine the source + // Get the current modes and locate the exact marketplace-installed mode for the selected target const modes = await this.customModesManager.getCustomModes() - const mode = modes.find((m) => m.slug === modeSlug) + const candidate = modes.find( + (m: ModeConfig) => + m.slug === modeSlug && + m.installedFromMarketplace === true && + m.marketplaceItemId === item.id && + m.source === target, + ) - // Use CustomModesManager to delete the mode configuration - // This also handles rules folder deletion - await this.customModesManager.deleteCustomMode(modeSlug, true) + if (!candidate) { + const msg = t("marketplace:installation.notInstalledForTarget") + throw new Error(msg === "installation.notInstalledForTarget" ? "Mode not found" : msg) + } + + // Delete only from the selected source to avoid unintended removals + await this.customModesManager.deleteCustomModeForSource(modeSlug, target, true) } private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise { diff --git a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts index 94684056d4..061fb81dc9 100644 --- a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts +++ b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts @@ -42,6 +42,7 @@ describe("SimpleInstaller", () => { mockContext = {} as vscode.ExtensionContext mockCustomModesManager = { deleteCustomMode: vi.fn().mockResolvedValue(undefined), + deleteCustomModeForSource: vi.fn().mockResolvedValue(undefined), importModeWithRules: vi.fn().mockResolvedValue({ success: true }), getCustomModes: vi.fn().mockResolvedValue([]), } as any @@ -220,13 +221,13 @@ describe("SimpleInstaller", () => { it("should use CustomModesManager to delete mode and clean up rules folder", async () => { // Mock that the mode exists with project source vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ - { slug: "test", name: "Test Mode", source: "project" } as any, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "project" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -235,13 +236,13 @@ describe("SimpleInstaller", () => { it("should handle global mode removal with rules cleanup", async () => { // Mock that the mode exists with global source vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ - { slug: "test", name: "Test Mode", source: "global" } as any, + { slug: "test", name: "Test Mode", source: "global", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "global" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "global", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -250,13 +251,13 @@ describe("SimpleInstaller", () => { it("should handle case when rules folder does not exist", async () => { // Mock that the mode exists vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ - { slug: "test", name: "Test Mode", source: "project" } as any, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "project" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -265,27 +266,26 @@ describe("SimpleInstaller", () => { it("should throw error if deleteCustomMode fails", async () => { // Mock that the mode exists vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ - { slug: "test", name: "Test Mode", source: "project" } as any, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) - // Mock that deleteCustomMode fails - mockCustomModesManager.deleteCustomMode = vi.fn().mockRejectedValueOnce(new Error("Permission denied")) + // Mock that scoped deletion fails + ;(mockCustomModesManager as any).deleteCustomModeForSource = vi + .fn() + .mockRejectedValueOnce(new Error("Permission denied")) - // Should throw the error from deleteCustomMode + // Should throw the error from scoped deletion await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow("Permission denied") - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) }) - it("should handle mode not found in custom modes list", async () => { + it("should throw when mode is not marketplace-installed for selected target", async () => { // Mock that the mode doesn't exist in the list vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([]) - await installer.removeItem(mockModeItem, { target: "project" }) + await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(/(Mode not found|customModes\.errors\.modeNotFound)/) - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) - // Should not attempt to delete rules folder - expect(fileExistsAtPath).not.toHaveBeenCalled() - expect(mockFs.rm).not.toHaveBeenCalled() + expect((mockCustomModesManager as any).deleteCustomModeForSource).not.toHaveBeenCalled() }) it("should throw error when mode content is invalid YAML", async () => { @@ -333,9 +333,14 @@ describe("SimpleInstaller", () => { ] as any, } + // Mock installed marketplace mode with matching marketplaceItemId and source + vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ + { slug: "test-array", name: "Test Array Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, + ]) + await installer.removeItem(arrayContentItem, { target: "project" }) - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-array", true) + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test-array", "project", true) }) it("should throw error when CustomModesManager is not available", async () => {