This commit is contained in:
matt-rudolph 2026-04-23 04:39:10 +00:00 committed by GitHub
commit 341e237e4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 193 additions and 72 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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é"
}
}

View file

@ -64,6 +64,7 @@
"installError": "\"{{itemName}}\" इंस्टॉल करने में विफल: {{errorMessage}}",
"removing": "आइटम हटा रहे हैं: \"{{itemName}}\"",
"removeSuccess": "\"{{itemName}}\" सफलतापूर्वक हटाया गया",
"removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}"
"removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}",
"notInstalledForTarget": "मोड नहीं मिला"
}
}

View file

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

View file

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

View file

@ -64,6 +64,7 @@
"installError": "\"{{itemName}}\"のインストールに失敗しました: {{errorMessage}}",
"removing": "アイテムを削除中: \"{{itemName}}\"",
"removeSuccess": "\"{{itemName}}\"の削除が完了しました",
"removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}"
"removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}",
"notInstalledForTarget": "モードが見つかりません"
}
}

View file

@ -64,6 +64,7 @@
"installError": "\"{{itemName}}\" 설치 실패: {{errorMessage}}",
"removing": "항목 제거 중: \"{{itemName}}\"",
"removeSuccess": "\"{{itemName}}\" 제거 완료",
"removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}"
"removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}",
"notInstalledForTarget": "모드를 찾을 수 없습니다"
}
}

View file

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

View file

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

View file

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

View file

@ -64,6 +64,7 @@
"installError": "Не удалось установить \"{{itemName}}\": {{errorMessage}}",
"removing": "Удаление элемента: \"{{itemName}}\"",
"removeSuccess": "\"{{itemName}}\" успешно удален",
"removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}"
"removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}",
"notInstalledForTarget": "Режим не найден"
}
}

View file

@ -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ı"
}
}

View file

@ -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ế độ"
}
}

View file

@ -64,6 +64,7 @@
"installError": "\"{{itemName}}\" 安装失败:{{errorMessage}}",
"removing": "正在移除项目:\"{{itemName}}\"",
"removeSuccess": "\"{{itemName}}\" 移除成功",
"removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}"
"removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}",
"notInstalledForTarget": "未找到模式"
}
}

View file

@ -64,6 +64,7 @@
"installError": "「{{itemName}}」安裝失敗:{{errorMessage}}",
"removing": "正在移除項目:「{{itemName}}」",
"removeSuccess": "「{{itemName}}」移除成功",
"removeError": "「{{itemName}}」移除失敗:{{errorMessage}}"
"removeError": "「{{itemName}}」移除失敗:{{errorMessage}}",
"notInstalledForTarget": "找不到模式"
}
}

View file

@ -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<string, { type: string }>): 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
}

View file

@ -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<string, any>
}
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<void> {
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<void> {

View file

@ -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 () => {