mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Use yaml as default custom modes format (#3749)
This commit is contained in:
parent
452ec95465
commit
4b974f623c
7 changed files with 312 additions and 104 deletions
5
.changeset/gold-meals-tell.md
Normal file
5
.changeset/gold-meals-tell.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Use YAML as default custom modes format
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue