Skip to content

Commit 917cd7e

Browse files
committed
Fixes #4792: Add auto-import config on extension load
- Add new VSCode setting 'roo-cline.autoImportConfigPath' for specifying config file path - Implement autoImportConfig utility function with path resolution and error handling - Integrate auto-import functionality into extension activation - Add comprehensive test coverage for auto-import scenarios - Support absolute paths, relative paths, and home directory expansion (~/) - Graceful error handling - extension continues to work if config import fails - User notifications for successful imports and warnings for failures
1 parent 2e2f83b commit 917cd7e

File tree

6 files changed

+1391
-1
lines changed

6 files changed

+1391
-1
lines changed

roo-code-messages.log

Lines changed: 932 additions & 0 deletions
Large diffs are not rendered by default.

src/extension.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
2727
import { McpServerManager } from "./services/mcp/McpServerManager"
2828
import { CodeIndexManager } from "./services/code-index/manager"
2929
import { migrateSettings } from "./utils/migrateSettings"
30+
import { autoImportConfig } from "./utils/autoImportConfig"
3031
import { API } from "./extension/api"
3132

3233
import {
@@ -110,6 +111,20 @@ export async function activate(context: vscode.ExtensionContext) {
110111
context.subscriptions.push(codeIndexManager)
111112
}
112113

114+
// Auto-import configuration if specified in settings
115+
try {
116+
await autoImportConfig({
117+
providerSettingsManager: provider.providerSettingsManager,
118+
contextProxy,
119+
customModesManager: provider.customModesManager,
120+
outputChannel,
121+
})
122+
} catch (error) {
123+
outputChannel.appendLine(
124+
`[AutoImport] Error during auto-import: ${error instanceof Error ? error.message : String(error)}`,
125+
)
126+
}
127+
113128
context.subscriptions.push(
114129
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, {
115130
webviewOptions: { retainContextWhenHidden: true },

src/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@
344344
"type": "boolean",
345345
"default": false,
346346
"description": "%settings.rooCodeCloudEnabled.description%"
347+
},
348+
"roo-cline.autoImportConfigPath": {
349+
"type": "string",
350+
"default": "",
351+
"description": "%settings.autoImportConfigPath.description%"
347352
}
348353
}
349354
}

src/package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
3131
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
3232
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
33-
"settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud."
33+
"settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud.",
34+
"settings.autoImportConfigPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-config.json'). Leave empty to disable auto-import."
3435
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
2+
3+
// Mock dependencies
4+
vi.mock("vscode", () => ({
5+
workspace: {
6+
getConfiguration: vi.fn(),
7+
},
8+
window: {
9+
showInformationMessage: vi.fn(),
10+
showWarningMessage: vi.fn(),
11+
},
12+
}))
13+
14+
vi.mock("fs/promises", () => ({
15+
default: {
16+
readFile: vi.fn(),
17+
},
18+
readFile: vi.fn(),
19+
}))
20+
21+
vi.mock("path", () => ({
22+
join: vi.fn((...args: string[]) => args.join("/")),
23+
isAbsolute: vi.fn((p: string) => p.startsWith("/")),
24+
basename: vi.fn((p: string) => p.split("/").pop() || ""),
25+
}))
26+
27+
vi.mock("os", () => ({
28+
homedir: vi.fn(() => "/home/user"),
29+
}))
30+
31+
vi.mock("../fs", () => ({
32+
fileExistsAtPath: vi.fn(),
33+
}))
34+
35+
vi.mock("../../core/config/ProviderSettingsManager")
36+
vi.mock("../../core/config/ContextProxy")
37+
vi.mock("../../core/config/CustomModesManager")
38+
39+
import { autoImportConfig } from "../autoImportConfig"
40+
import * as vscode from "vscode"
41+
import * as fs from "fs/promises"
42+
import { fileExistsAtPath } from "../fs"
43+
44+
describe("autoImportConfig", () => {
45+
let mockProviderSettingsManager: any
46+
let mockContextProxy: any
47+
let mockCustomModesManager: any
48+
let mockOutputChannel: any
49+
50+
beforeEach(() => {
51+
// Reset all mocks
52+
vi.clearAllMocks()
53+
54+
// Mock output channel
55+
mockOutputChannel = {
56+
appendLine: vi.fn(),
57+
}
58+
59+
// Mock provider settings manager
60+
mockProviderSettingsManager = {
61+
export: vi.fn().mockResolvedValue({
62+
apiConfigs: {},
63+
modeApiConfigs: {},
64+
currentApiConfigName: "default",
65+
}),
66+
import: vi.fn().mockResolvedValue(undefined),
67+
listConfig: vi.fn().mockResolvedValue([]),
68+
}
69+
70+
// Mock context proxy
71+
mockContextProxy = {
72+
setValues: vi.fn().mockResolvedValue(undefined),
73+
setValue: vi.fn().mockResolvedValue(undefined),
74+
setProviderSettings: vi.fn().mockResolvedValue(undefined),
75+
}
76+
77+
// Mock custom modes manager
78+
mockCustomModesManager = {
79+
updateCustomMode: vi.fn().mockResolvedValue(undefined),
80+
}
81+
82+
// Reset fs mock
83+
vi.mocked(fs.readFile).mockReset()
84+
vi.mocked(fileExistsAtPath).mockReset()
85+
vi.mocked(vscode.workspace.getConfiguration).mockReset()
86+
vi.mocked(vscode.window.showInformationMessage).mockReset()
87+
vi.mocked(vscode.window.showWarningMessage).mockReset()
88+
})
89+
90+
afterEach(() => {
91+
vi.restoreAllMocks()
92+
})
93+
94+
it("should skip auto-import when no config path is specified", async () => {
95+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
96+
get: vi.fn().mockReturnValue(""),
97+
} as any)
98+
99+
await autoImportConfig({
100+
providerSettingsManager: mockProviderSettingsManager,
101+
contextProxy: mockContextProxy,
102+
customModesManager: mockCustomModesManager,
103+
outputChannel: mockOutputChannel,
104+
})
105+
106+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
107+
"[AutoImport] No auto-import config path specified, skipping auto-import",
108+
)
109+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
110+
})
111+
112+
it("should skip auto-import when config file does not exist", async () => {
113+
const configPath = "~/Documents/roo-config.json"
114+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
115+
get: vi.fn().mockReturnValue(configPath),
116+
} as any)
117+
118+
// Mock fileExistsAtPath to return false
119+
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
120+
121+
await autoImportConfig({
122+
providerSettingsManager: mockProviderSettingsManager,
123+
contextProxy: mockContextProxy,
124+
customModesManager: mockCustomModesManager,
125+
outputChannel: mockOutputChannel,
126+
})
127+
128+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
129+
"[AutoImport] Checking for config file at: /home/user/Documents/roo-config.json",
130+
)
131+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
132+
"[AutoImport] Config file not found at /home/user/Documents/roo-config.json, skipping auto-import",
133+
)
134+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
135+
})
136+
137+
it("should successfully import config when file exists and is valid", async () => {
138+
const configPath = "/absolute/path/to/config.json"
139+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
140+
get: vi.fn().mockReturnValue(configPath),
141+
} as any)
142+
143+
// Mock fileExistsAtPath to return true
144+
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
145+
146+
// Mock fs.readFile to return valid config
147+
const mockConfig = {
148+
providerProfiles: {
149+
currentApiConfigName: "test-config",
150+
apiConfigs: {
151+
"test-config": {
152+
apiProvider: "anthropic",
153+
anthropicApiKey: "test-key",
154+
},
155+
},
156+
modeApiConfigs: {},
157+
},
158+
globalSettings: {
159+
customInstructions: "Test instructions",
160+
},
161+
}
162+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig) as any)
163+
164+
await autoImportConfig({
165+
providerSettingsManager: mockProviderSettingsManager,
166+
contextProxy: mockContextProxy,
167+
customModesManager: mockCustomModesManager,
168+
outputChannel: mockOutputChannel,
169+
})
170+
171+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
172+
"[AutoImport] Checking for config file at: /absolute/path/to/config.json",
173+
)
174+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
175+
"[AutoImport] Successfully imported configuration from /absolute/path/to/config.json",
176+
)
177+
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith(
178+
"RooCode configuration automatically imported from config.json",
179+
)
180+
expect(mockProviderSettingsManager.import).toHaveBeenCalled()
181+
expect(mockContextProxy.setValues).toHaveBeenCalled()
182+
})
183+
184+
it("should handle invalid JSON gracefully", async () => {
185+
const configPath = "~/config.json"
186+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
187+
get: vi.fn().mockReturnValue(configPath),
188+
} as any)
189+
190+
// Mock fileExistsAtPath to return true
191+
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
192+
193+
// Mock fs.readFile to return invalid JSON
194+
vi.mocked(fs.readFile).mockResolvedValue("invalid json" as any)
195+
196+
await autoImportConfig({
197+
providerSettingsManager: mockProviderSettingsManager,
198+
contextProxy: mockContextProxy,
199+
customModesManager: mockCustomModesManager,
200+
outputChannel: mockOutputChannel,
201+
})
202+
203+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
204+
expect.stringContaining("[AutoImport] Failed to import configuration:"),
205+
)
206+
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(
207+
expect.stringContaining("Failed to auto-import RooCode configuration:"),
208+
)
209+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
210+
})
211+
212+
it("should resolve home directory paths correctly", async () => {
213+
const configPath = "~/Documents/config.json"
214+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
215+
get: vi.fn().mockReturnValue(configPath),
216+
} as any)
217+
218+
// Mock fileExistsAtPath to return false (so we can check the resolved path)
219+
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
220+
221+
await autoImportConfig({
222+
providerSettingsManager: mockProviderSettingsManager,
223+
contextProxy: mockContextProxy,
224+
customModesManager: mockCustomModesManager,
225+
outputChannel: mockOutputChannel,
226+
})
227+
228+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
229+
"[AutoImport] Checking for config file at: /home/user/Documents/config.json",
230+
)
231+
})
232+
233+
it("should handle relative paths by resolving them to home directory", async () => {
234+
const configPath = "Documents/config.json"
235+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
236+
get: vi.fn().mockReturnValue(configPath),
237+
} as any)
238+
239+
// Mock fileExistsAtPath to return false (so we can check the resolved path)
240+
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
241+
242+
await autoImportConfig({
243+
providerSettingsManager: mockProviderSettingsManager,
244+
contextProxy: mockContextProxy,
245+
customModesManager: mockCustomModesManager,
246+
outputChannel: mockOutputChannel,
247+
})
248+
249+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
250+
"[AutoImport] Checking for config file at: /home/user/Documents/config.json",
251+
)
252+
})
253+
254+
it("should handle file system errors gracefully", async () => {
255+
const configPath = "~/config.json"
256+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
257+
get: vi.fn().mockReturnValue(configPath),
258+
} as any)
259+
260+
// Mock fileExistsAtPath to throw an error
261+
vi.mocked(fileExistsAtPath).mockRejectedValue(new Error("File system error"))
262+
263+
await autoImportConfig({
264+
providerSettingsManager: mockProviderSettingsManager,
265+
contextProxy: mockContextProxy,
266+
customModesManager: mockCustomModesManager,
267+
outputChannel: mockOutputChannel,
268+
})
269+
270+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
271+
expect.stringContaining("[AutoImport] Unexpected error during auto-import:"),
272+
)
273+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
274+
})
275+
})

0 commit comments

Comments
 (0)