Use yaml as default custom modes format (#3749)

This commit is contained in:
Matt Rubens 2025-05-20 16:44:29 -04:00 committed by hannesrudolph
parent 452ec95465
commit 4b974f623c
7 changed files with 312 additions and 104 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Use YAML as default custom modes format

View file

@ -7,7 +7,13 @@ import { migrateSettings } from "../utils/migrateSettings"
// Mock dependencies
jest.mock("vscode")
jest.mock("fs/promises")
jest.mock("fs/promises", () => ({
mkdir: jest.fn().mockResolvedValue(undefined),
readFile: jest.fn(),
writeFile: jest.fn().mockResolvedValue(undefined),
rename: jest.fn().mockResolvedValue(undefined),
unlink: jest.fn().mockResolvedValue(undefined),
}))
jest.mock("fs")
jest.mock("../utils/fs")
@ -18,11 +24,12 @@ describe("Settings Migration", () => {
const mockSettingsDir = path.join(mockStoragePath, "settings")
// Legacy file names
const legacyCustomModesPath = path.join(mockSettingsDir, "cline_custom_modes.json")
const legacyCustomModesJson = path.join(mockSettingsDir, "custom_modes.json")
const legacyClineCustomModesPath = path.join(mockSettingsDir, "cline_custom_modes.json")
const legacyMcpSettingsPath = path.join(mockSettingsDir, "cline_mcp_settings.json")
// New file names
const newCustomModesPath = path.join(mockSettingsDir, GlobalFileNames.customModes)
const newCustomModesYaml = path.join(mockSettingsDir, GlobalFileNames.customModes)
const newMcpSettingsPath = path.join(mockSettingsDir, GlobalFileNames.mcpSettings)
beforeEach(() => {
@ -43,49 +50,66 @@ describe("Settings Migration", () => {
globalStorageUri: { fsPath: mockStoragePath },
} as unknown as vscode.ExtensionContext
// The fs/promises mock is already set up in src/__mocks__/fs/promises.ts
// We don't need to manually mock these methods
// Set global outputChannel for all tests
;(global as any).outputChannel = mockOutputChannel
})
it("should migrate custom modes file if old file exists and new file doesn't", async () => {
// Mock file existence checks
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
// Setup mock for rename function
const mockRename = (fs.rename as jest.Mock).mockResolvedValue(undefined)
// Mock file existence checks - only return true for paths we want to exist
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyCustomModesPath) return true
if (path === newCustomModesPath) return false
return false
if (path === legacyClineCustomModesPath) return true
return false // All other paths don't exist, including destination files
})
// Run the migration
await migrateSettings(mockContext, mockOutputChannel)
// Verify file was renamed
expect(fs.rename).toHaveBeenCalledWith(legacyCustomModesPath, newCustomModesPath)
// Verify expected rename call - cline_custom_modes.json should be renamed to custom_modes.json
expect(mockRename).toHaveBeenCalledWith(legacyClineCustomModesPath, legacyCustomModesJson)
})
it("should migrate MCP settings file if old file exists and new file doesn't", async () => {
// Mock file existence checks
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
// Setup mock for rename function
const mockRename = (fs.rename as jest.Mock).mockResolvedValue(undefined)
// Ensure the other files don't interfere with this test
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyMcpSettingsPath) return true
if (path === newMcpSettingsPath) return false
return false
if (path === legacyClineCustomModesPath) return false // Ensure this file doesn't exist
if (path === legacyCustomModesJson) return false // Ensure this file doesn't exist
return false // All other paths don't exist, including destination files
})
// Run the migration
await migrateSettings(mockContext, mockOutputChannel)
// Verify file was renamed
expect(fs.rename).toHaveBeenCalledWith(legacyMcpSettingsPath, newMcpSettingsPath)
// Verify expected rename call
expect(mockRename).toHaveBeenCalledWith(legacyMcpSettingsPath, newMcpSettingsPath)
})
it("should not migrate if new file already exists", async () => {
// Mock file existence checks
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
// Setup mock for rename function
const mockRename = (fs.rename as jest.Mock).mockResolvedValue(undefined)
// Mock file existence checks - both source and destination exist
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyCustomModesPath) return true
if (path === newCustomModesPath) return true
if (path === legacyClineCustomModesPath) return true
if (path === legacyCustomModesJson) return true // Destination already exists
if (path === legacyMcpSettingsPath) return true
if (path === newMcpSettingsPath) return true
return false
@ -93,16 +117,16 @@ describe("Settings Migration", () => {
await migrateSettings(mockContext, mockOutputChannel)
// Verify no files were renamed
expect(fs.rename).not.toHaveBeenCalled()
// Verify rename was not called since destination files exist
expect(mockRename).not.toHaveBeenCalled()
})
it("should handle errors gracefully", async () => {
// Mock file existence checks to throw an error
;(fileExistsAtPath as jest.Mock).mockRejectedValue(new Error("Test error"))
// Clear mocks
jest.clearAllMocks()
// Set the global outputChannel for the test
;(global as any).outputChannel = mockOutputChannel
// Mock file existence to throw error
;(fileExistsAtPath as jest.Mock).mockRejectedValue(new Error("Test error"))
await migrateSettings(mockContext, mockOutputChannel)
@ -111,4 +135,119 @@ describe("Settings Migration", () => {
expect.stringContaining("Error migrating settings files"),
)
})
it("should convert custom_modes.json to YAML format", async () => {
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
const testJsonContent = JSON.stringify({ customModes: [{ slug: "test-mode", name: "Test Mode" }] })
// Setup mock functions
const mockWrite = (fs.writeFile as jest.Mock).mockResolvedValue(undefined)
const mockUnlink = (fs.unlink as jest.Mock).mockResolvedValue(undefined)
// Mock file read to return JSON content
;(fs.readFile as jest.Mock).mockImplementation(async (path: any) => {
if (path === legacyCustomModesJson) {
return testJsonContent
}
throw new Error("File not found: " + path)
})
// Isolate this test by making sure only the specific JSON file exists
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyCustomModesJson) return true
if (path === legacyClineCustomModesPath) return false
if (path === legacyMcpSettingsPath) return false
return false
})
await migrateSettings(mockContext, mockOutputChannel)
// Verify file operations
expect(mockWrite).toHaveBeenCalledWith(newCustomModesYaml, expect.any(String), "utf-8")
// We don't delete the original JSON file to allow for rollback
expect(mockUnlink).not.toHaveBeenCalled()
// Verify log message mentions preservation of original file
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
expect.stringContaining("original JSON file preserved for rollback purposes"),
)
})
it("should handle corrupt JSON gracefully", async () => {
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
// Setup mock functions
const mockWrite = (fs.writeFile as jest.Mock).mockResolvedValue(undefined)
const mockUnlink = (fs.unlink as jest.Mock).mockResolvedValue(undefined)
// Mock file read to return corrupt JSON
;(fs.readFile as jest.Mock).mockImplementation(async (path: any) => {
if (path === legacyCustomModesJson) {
return "{ invalid json content" // This will cause an error when parsed
}
throw new Error("File not found: " + path)
})
// Isolate this test
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyCustomModesJson) return true
if (path === legacyClineCustomModesPath) return false
if (path === legacyMcpSettingsPath) return false
return false
})
await migrateSettings(mockContext, mockOutputChannel)
// Verify error was logged
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
expect.stringContaining("Error parsing custom_modes.json"),
)
// Verify no write/unlink operations were performed
expect(mockWrite).not.toHaveBeenCalled()
expect(mockUnlink).not.toHaveBeenCalled()
})
it("should skip migration when YAML file already exists", async () => {
// Clear all previous mocks to ensure clean test environment
jest.clearAllMocks()
// Setup mock functions
const mockWrite = (fs.writeFile as jest.Mock).mockResolvedValue(undefined)
const mockUnlink = (fs.unlink as jest.Mock).mockResolvedValue(undefined)
// Mock file read
;(fs.readFile as jest.Mock).mockImplementation(async (path: any) => {
if (path === legacyCustomModesJson) {
return JSON.stringify({ customModes: [] })
}
throw new Error("File not found: " + path)
})
// Mock file existence checks - both source and yaml destination exist
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsDir) return true
if (path === legacyCustomModesJson) return true
if (path === newCustomModesYaml) return true // YAML already exists
if (path === legacyClineCustomModesPath) return false
if (path === legacyMcpSettingsPath) return false
return false
})
await migrateSettings(mockContext, mockOutputChannel)
// Verify skip message was logged
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
"custom_modes.yaml already exists, skipping migration",
)
// Verify no file operations occurred
expect(mockWrite).not.toHaveBeenCalled()
expect(mockUnlink).not.toHaveBeenCalled()
})
})

View file

@ -120,7 +120,7 @@ export class CustomModesManager {
const fileExists = await fileExistsAtPath(filePath)
if (!fileExists) {
await this.queueWrite(() => fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2)))
await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] })))
}
return filePath
@ -136,7 +136,7 @@ export class CustomModesManager {
const content = await fs.readFile(settingsPath, "utf-8")
const errorMessage =
"Invalid custom modes format. Please ensure your settings follow the correct JSON format."
"Invalid custom modes format. Please ensure your settings follow the correct YAML format."
let config: any
@ -291,7 +291,7 @@ export class CustomModesManager {
content = await fs.readFile(filePath, "utf-8")
} catch (error) {
// File might not exist yet.
content = JSON.stringify({ customModes: [] })
content = yaml.stringify({ customModes: [] })
}
let settings
@ -299,12 +299,12 @@ export class CustomModesManager {
try {
settings = yaml.parse(content)
} catch (error) {
console.error(`[CustomModesManager] Failed to parse JSON from ${filePath}:`, error)
console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, error)
settings = { customModes: [] }
}
settings.customModes = operation(settings.customModes || [])
await fs.writeFile(filePath, JSON.stringify(settings, null, 2), "utf-8")
await fs.writeFile(filePath, yaml.stringify(settings), "utf-8")
}
private async refreshMergedState(): Promise<void> {
@ -369,7 +369,7 @@ export class CustomModesManager {
public async resetCustomModes(): Promise<void> {
try {
const filePath = await this.getCustomModesFilePath()
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
await fs.writeFile(filePath, yaml.stringify({ customModes: [] }))
await this.context.globalState.update("customModes", [])
this.clearCache()
await this.onUpdate()

View file

@ -48,7 +48,7 @@ describe("CustomModesManager", () => {
;(fs.mkdir as jest.Mock).mockResolvedValue(undefined)
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: [] })
return yaml.stringify({ customModes: [] })
}
throw new Error("File not found")
})
@ -68,7 +68,7 @@ describe("CustomModesManager", () => {
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
if (path === mockRoomodes) {
return yaml.stringify({ customModes: roomodesModes })
@ -94,10 +94,10 @@ describe("CustomModesManager", () => {
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
if (path === mockRoomodes) {
return JSON.stringify({ customModes: roomodesModes })
return yaml.stringify({ customModes: roomodesModes })
}
throw new Error("File not found")
})
@ -122,7 +122,7 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -133,15 +133,15 @@ describe("CustomModesManager", () => {
expect(modes[0].slug).toBe("mode1")
})
it("should handle invalid JSON in .roomodes", async () => {
it("should handle invalid YAML in .roomodes", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
if (path === mockRoomodes) {
return "invalid json"
return "invalid yaml content"
}
throw new Error("File not found")
})
@ -158,7 +158,7 @@ describe("CustomModesManager", () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -180,7 +180,7 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -200,7 +200,7 @@ describe("CustomModesManager", () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -225,7 +225,7 @@ describe("CustomModesManager", () => {
const updatedSettingsModes = [updatedMode]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: updatedSettingsModes })
return yaml.stringify({ customModes: updatedSettingsModes })
}
throw new Error("File not found")
})
@ -246,7 +246,7 @@ describe("CustomModesManager", () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -264,7 +264,7 @@ describe("CustomModesManager", () => {
// Mock the updated file content (empty)
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: [] })
return yaml.stringify({ customModes: [] })
}
throw new Error("File not found")
})
@ -282,7 +282,7 @@ describe("CustomModesManager", () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -310,7 +310,7 @@ describe("CustomModesManager", () => {
const updatedSettingsModes = [updatedMode]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: updatedSettingsModes })
return yaml.stringify({ customModes: updatedSettingsModes })
}
throw new Error("File not found")
})
@ -328,7 +328,7 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: updatedSettingsModes })
return yaml.stringify({ customModes: updatedSettingsModes })
}
throw new Error("File not found")
})
@ -343,7 +343,7 @@ describe("CustomModesManager", () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -369,7 +369,7 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -390,7 +390,7 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: settingsModes })
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
@ -434,10 +434,10 @@ describe("CustomModesManager", () => {
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockRoomodes) {
return JSON.stringify(roomodesContent)
return yaml.stringify(roomodesContent)
}
if (path === mockSettingsPath) {
return JSON.stringify(settingsContent)
return yaml.stringify(settingsContent)
}
throw new Error("File not found")
})
@ -502,13 +502,13 @@ describe("CustomModesManager", () => {
})
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify({ customModes: [] })
return yaml.stringify({ customModes: [] })
}
if (path === mockRoomodes) {
if (!roomodesContent) {
throw new Error("File not found")
}
return JSON.stringify(roomodesContent)
return yaml.stringify(roomodesContent)
}
throw new Error("File not found")
})
@ -564,7 +564,7 @@ describe("CustomModesManager", () => {
let settingsContent = { customModes: [] }
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify(settingsContent)
return yaml.stringify(settingsContent)
}
throw new Error("File not found")
})
@ -629,16 +629,13 @@ describe("CustomModesManager", () => {
await manager.getCustomModesFilePath()
expect(fs.writeFile).toHaveBeenCalledWith(
settingsPath,
expect.stringMatching(/^\{\s+"customModes":\s+\[\s*\]\s*\}$/),
)
expect(fs.writeFile).toHaveBeenCalledWith(settingsPath, expect.stringMatching(/^customModes: \[\]/))
})
it("watches file for changes", async () => {
const configPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
;(fs.readFile as jest.Mock).mockResolvedValue(yaml.stringify({ customModes: [] }))
;(arePathsEqual as jest.Mock).mockImplementation((path1: string, path2: string) => {
return path.normalize(path1) === path.normalize(path2)
})
@ -673,7 +670,7 @@ describe("CustomModesManager", () => {
let settingsContent = { customModes: [existingMode] }
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return JSON.stringify(settingsContent)
return yaml.stringify(settingsContent)
}
throw new Error("File not found")
})
@ -718,9 +715,9 @@ describe("CustomModesManager", () => {
})
describe("updateModesInFile", () => {
it("handles corrupted JSON content gracefully", async () => {
const corruptedJson = "{ invalid json content"
;(fs.readFile as jest.Mock).mockResolvedValue(corruptedJson)
it("handles corrupted YAML content gracefully", async () => {
const corruptedYaml = "customModes: [invalid yaml content"
;(fs.readFile as jest.Mock).mockResolvedValue(corruptedYaml)
const newMode: ModeConfig = {
slug: "test-mode",
@ -732,7 +729,7 @@ describe("CustomModesManager", () => {
await manager.updateCustomMode("test-mode", newMode)
// Verify that a valid JSON structure was written
// Verify that a valid YAML structure was written
const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
const writtenContent = yaml.parse(writeCall[1])
expect(writtenContent).toEqual({

View file

@ -31,25 +31,30 @@ If asked to create a project mode, create it in .roomodes in the workspace root.
- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break."
Both files should follow this structure:
{
"customModes": [
{
"slug": "designer", // Required: unique slug with lowercase letters, numbers, and hyphens
"name": "Designer", // Required: mode display name
"roleDefinition": "You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:\\n- Creating and maintaining design systems\\n- Implementing responsive and accessible web interfaces\\n- Working with CSS, HTML, and modern frontend frameworks\\n- Ensuring consistent user experiences across platforms", // Required: non-empty
"whenToUse": "Use this mode when creating or modifying UI components, implementing design systems, or ensuring responsive web interfaces. This mode is especially effective with CSS, HTML, and modern frontend frameworks.", // Optional but recommended
"groups": [ // Required: array of tool groups (can be empty)
"read", // Read files group (read_file, fetch_instructions, search_files, list_files, list_code_definition_names)
"edit", // Edit files group (apply_diff, write_to_file) - allows editing any file
// Or with file restrictions:
// ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], // Edit group that only allows editing markdown files
"browser", // Browser group (browser_action)
"command", // Command group (execute_command)
"mcp" // MCP group (use_mcp_tool, access_mcp_resource)
],
"customInstructions": "Additional instructions for the Designer mode" // Optional
}
]
}`
Both files should follow this structure (in YAML format):
customModes:
- slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
name: Designer # Required: mode display name
roleDefinition: >-
You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
- Creating and maintaining design systems
- Implementing responsive and accessible web interfaces
- Working with CSS, HTML, and modern frontend frameworks
- Ensuring consistent user experiences across platforms # Required: non-empty
whenToUse: >-
Use this mode when creating or modifying UI components, implementing design systems,
or ensuring responsive web interfaces. This mode is especially effective with CSS,
HTML, and modern frontend frameworks. # Optional but recommended
groups: # Required: array of tool groups (can be empty)
- read # Read files group (read_file, fetch_instructions, search_files, list_files, list_code_definition_names)
- edit # Edit files group (apply_diff, write_to_file) - allows editing any file
# Or with file restrictions:
# - - edit
# - fileRegex: \\.md$
# description: Markdown files only # Edit group that only allows editing markdown files
- browser # Browser group (browser_action)
- command # Command group (execute_command)
- mcp # MCP group (use_mcp_tool, access_mcp_resource)
customInstructions: Additional instructions for the Designer mode # Optional`
}

View file

@ -2,6 +2,6 @@ export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
mcpSettings: "mcp_settings.json",
customModes: "custom_modes.json",
customModes: "custom_modes.yaml",
taskMetadata: "task_metadata.json",
}

View file

@ -3,6 +3,9 @@ import * as path from "path"
import * as fs from "fs/promises"
import { fileExistsAtPath } from "./fs"
import { GlobalFileNames } from "../shared/globalFileNames"
import * as yaml from "yaml"
const deprecatedCustomModesJSONFilename = "custom_modes.json"
/**
* Migrates old settings files to new file names
@ -15,7 +18,8 @@ export async function migrateSettings(
): Promise<void> {
// Legacy file names that need to be migrated to the new names in GlobalFileNames
const fileMigrations = [
{ oldName: "cline_custom_modes.json", newName: GlobalFileNames.customModes },
// custom_modes.json to custom_modes.yaml is handled separately below
{ oldName: "cline_custom_modes.json", newName: deprecatedCustomModesJSONFilename },
{ oldName: "cline_mcp_settings.json", newName: GlobalFileNames.mcpSettings },
]
@ -29,25 +33,83 @@ export async function migrateSettings(
}
// Process each file migration
for (const migration of fileMigrations) {
const oldPath = path.join(settingsDir, migration.oldName)
const newPath = path.join(settingsDir, migration.newName)
try {
for (const migration of fileMigrations) {
const oldPath = path.join(settingsDir, migration.oldName)
const newPath = path.join(settingsDir, migration.newName)
// Only migrate if old file exists and new file doesn't exist yet
// This ensures we don't overwrite any existing new files
const oldFileExists = await fileExistsAtPath(oldPath)
const newFileExists = await fileExistsAtPath(newPath)
// Only migrate if old file exists and new file doesn't exist yet
// This ensures we don't overwrite any existing new files
const oldFileExists = await fileExistsAtPath(oldPath)
const newFileExists = await fileExistsAtPath(newPath)
if (oldFileExists && !newFileExists) {
await fs.rename(oldPath, newPath)
outputChannel.appendLine(`Renamed ${migration.oldName} to ${migration.newName}`)
} else {
outputChannel.appendLine(
`Skipping migration of ${migration.oldName} to ${migration.newName}: ${oldFileExists ? "new file already exists" : "old file not found"}`,
)
if (oldFileExists && !newFileExists) {
await fs.rename(oldPath, newPath)
outputChannel.appendLine(`Renamed ${migration.oldName} to ${migration.newName}`)
} else {
outputChannel.appendLine(
`Skipping migration of ${migration.oldName} to ${migration.newName}: ${oldFileExists ? "new file already exists" : "old file not found"}`,
)
}
}
// Special migration for custom_modes.json to custom_modes.yaml with content transformation
await migrateCustomModesToYaml(settingsDir, outputChannel)
} catch (error) {
outputChannel.appendLine(`Error in file migrations: ${error}`)
}
} catch (error) {
outputChannel.appendLine(`Error migrating settings files: ${error}`)
}
}
/**
* Special migration function to convert custom_modes.json to YAML format
*/
async function migrateCustomModesToYaml(settingsDir: string, outputChannel: vscode.OutputChannel): Promise<void> {
const oldJsonPath = path.join(settingsDir, deprecatedCustomModesJSONFilename)
const newYamlPath = path.join(settingsDir, GlobalFileNames.customModes)
// Only proceed if JSON exists and YAML doesn't
const jsonExists = await fileExistsAtPath(oldJsonPath)
const yamlExists = await fileExistsAtPath(newYamlPath)
if (!jsonExists) {
outputChannel.appendLine("No custom_modes.json found, skipping YAML migration")
return
}
if (yamlExists) {
outputChannel.appendLine("custom_modes.yaml already exists, skipping migration")
return
}
try {
// Read JSON content
const jsonContent = await fs.readFile(oldJsonPath, "utf-8")
try {
// Parse JSON to object (using the yaml library just to be safe/consistent)
const customModesData = yaml.parse(jsonContent)
// Convert to YAML
const yamlContent = yaml.stringify(customModesData)
// Write YAML file
await fs.writeFile(newYamlPath, yamlContent, "utf-8")
// Keeping the old JSON file for backward compatibility
// This allows users to roll back if needed
outputChannel.appendLine(
"Successfully migrated custom_modes.json to YAML format (original JSON file preserved for rollback purposes)",
)
} catch (parseError) {
// Handle corrupt JSON file
outputChannel.appendLine(
`Error parsing custom_modes.json: ${parseError}. File might be corrupted. Skipping migration.`,
)
}
} catch (fileError) {
outputChannel.appendLine(`Error reading custom_modes.json: ${fileError}. Skipping migration.`)
}
}