Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-eyes-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Add settings migration to support renaming legacy settings files to new format
2 changes: 1 addition & 1 deletion src/__mocks__/fs/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const mockFs = {
_setInitialMockData: () => {
// Set up default MCP settings
mockFiles.set(
"/mock/settings/path/cline_mcp_settings.json",
"/mock/settings/path/mcp_settings.json",
JSON.stringify({
mcpServers: {
"test-server": {
Expand Down
3 changes: 2 additions & 1 deletion src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ModeConfig } from "../../shared/modes"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
import { logger } from "../../utils/logging"
import { GlobalFileNames } from "../../shared/globalFileNames"

const ROOMODES_FILENAME = ".roomodes"

Expand Down Expand Up @@ -113,7 +114,7 @@ export class CustomModesManager {

async getCustomModesFilePath(): Promise<string> {
const settingsDir = await this.ensureSettingsDirectoryExists()
const filePath = path.join(settingsDir, "cline_custom_modes.json")
const filePath = path.join(settingsDir, GlobalFileNames.customModes)
const fileExists = await fileExistsAtPath(filePath)
if (!fileExists) {
await this.queueWrite(async () => {
Expand Down
14 changes: 7 additions & 7 deletions src/core/config/__tests__/CustomModesManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CustomModesManager } from "../CustomModesManager"
import { ModeConfig } from "../../../shared/modes"
import { fileExistsAtPath } from "../../../utils/fs"
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
import { GlobalFileNames } from "../../../shared/globalFileNames"

jest.mock("vscode")
jest.mock("fs/promises")
Expand All @@ -21,7 +22,7 @@ describe("CustomModesManager", () => {

// Use path.sep to ensure correct path separators for the current platform
const mockStoragePath = `${path.sep}mock${path.sep}settings`
const mockSettingsPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const mockSettingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)
const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes`

beforeEach(() => {
Expand Down Expand Up @@ -333,17 +334,16 @@ describe("CustomModesManager", () => {
expect(mockOnUpdate).toHaveBeenCalled()
})
})

describe("File Operations", () => {
it("creates settings directory if it doesn't exist", async () => {
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const settingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)
await manager.getCustomModesFilePath()

expect(fs.mkdir).toHaveBeenCalledWith(path.dirname(configPath), { recursive: true })
expect(fs.mkdir).toHaveBeenCalledWith(path.dirname(settingsPath), { recursive: true })
})

it("creates default config if file doesn't exist", async () => {
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const settingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)

// Mock fileExists to return false first time, then true
let firstCall = true
Expand All @@ -358,13 +358,13 @@ describe("CustomModesManager", () => {
await manager.getCustomModesFilePath()

expect(fs.writeFile).toHaveBeenCalledWith(
configPath,
settingsPath,
expect.stringMatching(/^\{\s+"customModes":\s+\[\s*\]\s*\}$/),
)
})

it("watches file for changes", async () => {
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const configPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)

;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
;(arePathsEqual as jest.Mock).mockImplementation((path1: string, path2: string) => {
Expand Down
3 changes: 2 additions & 1 deletion src/core/prompts/sections/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import * as path from "path"
import * as vscode from "vscode"
import { promises as fs } from "fs"
import { ModeConfig, getAllModesWithPrompts } from "../../../shared/modes"
import { GlobalFileNames } from "../../../shared/globalFileNames"

export async function getModesSection(context: vscode.ExtensionContext): Promise<string> {
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
const customModesPath = path.join(settingsDir, GlobalFileNames.customModes)

// Get all modes with their overrides from extension state
const allModes = await getAllModesWithPrompts(context)
Expand Down
55 changes: 54 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as vscode from "vscode"
import * as dotenvx from "@dotenvx/dotenvx"
import * as path from "path"
import * as fs from "fs/promises"
import * as fsSync from "fs"

// Load environment variables from .env file
try {
Expand All @@ -21,6 +24,8 @@ import { McpServerManager } from "./services/mcp/McpServerManager"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { API } from "./exports/api"
import { fileExistsAtPath } from "./utils/fs"
import { GlobalFileNames } from "./shared/globalFileNames"

import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
import { formatLanguage } from "./shared/language"
Expand All @@ -36,14 +41,62 @@ import { formatLanguage } from "./shared/language"
let outputChannel: vscode.OutputChannel
let extensionContext: vscode.ExtensionContext

/**
* Migrates old settings files to new file names
*
* TODO: Remove this migration code in September 2025 (6 months after implementation)
*/
export async function migrateSettings(context: vscode.ExtensionContext): 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 },
{ oldName: "cline_mcp_settings.json", newName: GlobalFileNames.mcpSettings },
]

try {
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")

// Check if settings directory exists first
if (!(await fileExistsAtPath(settingsDir))) {
outputChannel.appendLine("No settings directory found, no migrations necessary")
return
}

// Process each file migration
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)

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"}`,
)
}
}
} catch (error) {
outputChannel.appendLine(`Error migrating settings files: ${error}`)
}
}

// This method is called when your extension is activated.
// Your extension is activated the very first time the command is executed.
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
extensionContext = context
outputChannel = vscode.window.createOutputChannel("Roo-Code")
context.subscriptions.push(outputChannel)
outputChannel.appendLine("Roo-Code extension activated")

// Migrate old settings to new
await migrateSettings(context)

// Initialize telemetry service after environment variables are loaded.
telemetryService.initialize()

Expand Down
2 changes: 1 addition & 1 deletion src/services/mcp/__tests__/McpHub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jest.mock("../../../core/webview/ClineProvider")
describe("McpHub", () => {
let mcpHub: McpHubType
let mockProvider: Partial<ClineProvider>
const mockSettingsPath = "/mock/settings/path/cline_mcp_settings.json"
const mockSettingsPath = "/mock/settings/path/mcp_settings.json"

beforeEach(() => {
jest.clearAllMocks()
Expand Down
3 changes: 2 additions & 1 deletion src/shared/globalFileNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const GlobalFileNames = {
glamaModels: "glama_models.json",
openRouterModels: "openrouter_models.json",
requestyModels: "requesty_models.json",
mcpSettings: "cline_mcp_settings.json",
mcpSettings: "mcp_settings.json",
unboundModels: "unbound_models.json",
customModes: "custom_modes.json",
}