mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: correctly identify .roo folder in multi-root workspaces
- Add findWorkspaceWithRoo() utility to detect when .roo is a workspace folder - Update getProjectRooDirectoryForCwd() to return .roo workspace path directly - Update all direct .roo path constructions to use centralized utilities - Add comprehensive tests for multi-root workspace scenarios Fixes #6720
This commit is contained in:
parent
d90bab71ff
commit
f921cdb72c
6 changed files with 171 additions and 19 deletions
|
|
@ -10,7 +10,7 @@ import { type ModeConfig, type PromptComponent, customModesSettingsSchema, modeC
|
|||
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { getGlobalRooDirectory } from "../../services/roo-config"
|
||||
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../../services/roo-config"
|
||||
import { logger } from "../../utils/logging"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
|
|
@ -566,14 +566,15 @@ export class CustomModesManager {
|
|||
if (scope === "project") {
|
||||
const workspacePath = getWorkspacePath()
|
||||
if (workspacePath) {
|
||||
rulesFolderPath = path.join(workspacePath, ".roo", `rules-${slug}`)
|
||||
const rooDir = await getProjectRooDirectoryForCwd(workspacePath)
|
||||
rulesFolderPath = path.join(rooDir, `rules-${slug}`)
|
||||
} else {
|
||||
return // No workspace, can't delete project rules
|
||||
}
|
||||
} else {
|
||||
// Global scope - use OS home directory
|
||||
const homeDir = os.homedir()
|
||||
rulesFolderPath = path.join(homeDir, ".roo", `rules-${slug}`)
|
||||
// Global scope - use global .roo directory
|
||||
const globalRooDir = getGlobalRooDirectory()
|
||||
rulesFolderPath = path.join(globalRooDir, `rules-${slug}`)
|
||||
}
|
||||
|
||||
// Check if the rules folder exists and delete it
|
||||
|
|
@ -665,7 +666,8 @@ export class CustomModesManager {
|
|||
if (!workspacePath) {
|
||||
return false
|
||||
}
|
||||
modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`)
|
||||
const rooDir = await getProjectRooDirectoryForCwd(workspacePath)
|
||||
modeRulesDir = path.join(rooDir, `rules-${slug}`)
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -769,9 +771,13 @@ export class CustomModesManager {
|
|||
}
|
||||
|
||||
// Check for .roo/rules-{slug}/ directory (or rules-{slug}/ for global)
|
||||
const modeRulesDir = isGlobalMode
|
||||
? path.join(baseDir, `rules-${slug}`)
|
||||
: path.join(baseDir, ".roo", `rules-${slug}`)
|
||||
let modeRulesDir: string
|
||||
if (isGlobalMode) {
|
||||
modeRulesDir = path.join(baseDir, `rules-${slug}`)
|
||||
} else {
|
||||
const rooDir = await getProjectRooDirectoryForCwd(baseDir)
|
||||
modeRulesDir = path.join(rooDir, `rules-${slug}`)
|
||||
}
|
||||
|
||||
let rulesFiles: RuleFile[] = []
|
||||
try {
|
||||
|
|
@ -855,7 +861,7 @@ export class CustomModesManager {
|
|||
rulesFolderPath = path.join(baseDir, `rules-${importMode.slug}`)
|
||||
} else {
|
||||
const workspacePath = getWorkspacePath()
|
||||
baseDir = path.join(workspacePath, ".roo")
|
||||
baseDir = await getProjectRooDirectoryForCwd(workspacePath)
|
||||
rulesFolderPath = path.join(baseDir, `rules-${importMode.slug}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
|
|||
import { GetModelsOptions } from "../../shared/api"
|
||||
import { generateSystemPrompt } from "./generateSystemPrompt"
|
||||
import { getCommand } from "../../utils/commands"
|
||||
import { getProjectRooDirectoryForCwd } from "../../services/roo-config"
|
||||
|
||||
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
||||
|
||||
|
|
@ -801,7 +802,7 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders[0]
|
||||
const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
|
||||
const rooDir = await getProjectRooDirectoryForCwd(workspaceFolder.uri.fsPath)
|
||||
const mcpPath = path.join(rooDir, "mcp.json")
|
||||
|
||||
try {
|
||||
|
|
@ -2483,7 +2484,8 @@ export const webviewMessageHandler = async (
|
|||
vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command"))
|
||||
break
|
||||
}
|
||||
commandsDir = path.join(workspaceRoot, ".roo", "commands")
|
||||
const rooDir = await getProjectRooDirectoryForCwd(workspaceRoot)
|
||||
commandsDir = path.join(rooDir, "commands")
|
||||
}
|
||||
|
||||
// Ensure the commands directory exists
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOption
|
|||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
import type { CustomModesManager } from "../../core/config/CustomModesManager"
|
||||
import { getProjectRooDirectoryForCwd } from "../../services/roo-config"
|
||||
|
||||
export interface InstallOptions extends InstallMarketplaceItemOptions {
|
||||
target: "project" | "global"
|
||||
|
|
@ -375,7 +376,8 @@ export class SimpleInstaller {
|
|||
if (!workspaceFolder) {
|
||||
throw new Error("No workspace folder found")
|
||||
}
|
||||
return path.join(workspaceFolder.uri.fsPath, ".roo", "mcp.json")
|
||||
const rooDir = await getProjectRooDirectoryForCwd(workspaceFolder.uri.fsPath)
|
||||
return path.join(rooDir, "mcp.json")
|
||||
} else {
|
||||
const globalSettingsPath = await ensureSettingsDirectoryExists(this.context)
|
||||
return path.join(globalSettingsPath, GlobalFileNames.mcpSettings)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
import { injectVariables } from "../../utils/config"
|
||||
import { getProjectRooDirectoryForCwd } from "../../services/roo-config"
|
||||
|
||||
export type McpConnection = {
|
||||
server: McpServer
|
||||
|
|
@ -536,7 +537,7 @@ export class McpHub {
|
|||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders[0]
|
||||
const projectMcpDir = path.join(workspaceFolder.uri.fsPath, ".roo")
|
||||
const projectMcpDir = await getProjectRooDirectoryForCwd(workspaceFolder.uri.fsPath)
|
||||
const projectMcpPath = path.join(projectMcpDir, "mcp.json")
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
||||
|
||||
// Use vi.hoisted to ensure mocks are available during hoisting
|
||||
const { mockStat, mockReadFile, mockHomedir } = vi.hoisted(() => ({
|
||||
const { mockStat, mockReadFile, mockHomedir, mockWorkspaceFolders } = vi.hoisted(() => ({
|
||||
mockStat: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockHomedir: vi.fn(),
|
||||
mockWorkspaceFolders: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock fs/promises module
|
||||
|
|
@ -21,6 +23,18 @@ vi.mock("os", () => ({
|
|||
homedir: mockHomedir,
|
||||
}))
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
get workspaceFolders() {
|
||||
return mockWorkspaceFolders()
|
||||
},
|
||||
},
|
||||
Uri: {
|
||||
file: (path: string) => ({ fsPath: path }),
|
||||
},
|
||||
}))
|
||||
|
||||
import {
|
||||
getGlobalRooDirectory,
|
||||
getProjectRooDirectoryForCwd,
|
||||
|
|
@ -29,12 +43,14 @@ import {
|
|||
readFileIfExists,
|
||||
getRooDirectoriesForCwd,
|
||||
loadConfiguration,
|
||||
findWorkspaceWithRoo,
|
||||
} from "../index"
|
||||
|
||||
describe("RooConfigService", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHomedir.mockReturnValue("/mock/home")
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -54,10 +70,81 @@ describe("RooConfigService", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("findWorkspaceWithRoo", () => {
|
||||
it("should return undefined when no workspace folders exist", () => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
const result = findWorkspaceWithRoo()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined when workspace folders array is empty", () => {
|
||||
mockWorkspaceFolders.mockReturnValue([])
|
||||
const result = findWorkspaceWithRoo()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return the workspace folder named .roo", () => {
|
||||
const workspaceFolders = [
|
||||
{ uri: { fsPath: "/workspace/project1" }, name: "project1", index: 0 },
|
||||
{ uri: { fsPath: "/workspace/.roo" }, name: ".roo", index: 1 },
|
||||
{ uri: { fsPath: "/workspace/project2" }, name: "project2", index: 2 },
|
||||
]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const result = findWorkspaceWithRoo()
|
||||
expect(result).toBe(workspaceFolders[1])
|
||||
})
|
||||
|
||||
it("should return undefined when no workspace folder is named .roo", () => {
|
||||
const workspaceFolders = [
|
||||
{ uri: { fsPath: "/workspace/project1" }, name: "project1", index: 0 },
|
||||
{ uri: { fsPath: "/workspace/project2" }, name: "project2", index: 1 },
|
||||
]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const result = findWorkspaceWithRoo()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle workspace folders with .roo in path but not as basename", () => {
|
||||
const workspaceFolders = [
|
||||
{ uri: { fsPath: "/workspace/.roo/subproject" }, name: "subproject", index: 0 },
|
||||
{ uri: { fsPath: "/workspace/project.roo" }, name: "project.roo", index: 1 },
|
||||
]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const result = findWorkspaceWithRoo()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getProjectRooDirectoryForCwd", () => {
|
||||
it("should return correct path for given cwd", () => {
|
||||
it("should return .roo workspace folder path when it exists", async () => {
|
||||
const workspaceFolders = [
|
||||
{ uri: { fsPath: "/workspace/project1" }, name: "project1", index: 0 },
|
||||
{ uri: { fsPath: "/workspace/.roo" }, name: ".roo", index: 1 },
|
||||
]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const cwd = "/workspace/project1"
|
||||
const result = await getProjectRooDirectoryForCwd(cwd)
|
||||
expect(result).toBe("/workspace/.roo")
|
||||
})
|
||||
|
||||
it("should return cwd/.roo when no .roo workspace folder exists", async () => {
|
||||
const workspaceFolders = [{ uri: { fsPath: "/workspace/project1" }, name: "project1", index: 0 }]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const cwd = "/custom/project/path"
|
||||
const result = getProjectRooDirectoryForCwd(cwd)
|
||||
const result = await getProjectRooDirectoryForCwd(cwd)
|
||||
expect(result).toBe(path.join(cwd, ".roo"))
|
||||
})
|
||||
|
||||
it("should return cwd/.roo when no workspace folders exist", async () => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
|
||||
const cwd = "/custom/project/path"
|
||||
const result = await getProjectRooDirectoryForCwd(cwd)
|
||||
expect(result).toBe(path.join(cwd, ".roo"))
|
||||
})
|
||||
})
|
||||
|
|
@ -206,13 +293,26 @@ describe("RooConfigService", () => {
|
|||
})
|
||||
|
||||
describe("getRooDirectoriesForCwd", () => {
|
||||
it("should return directories for given cwd", () => {
|
||||
it("should return directories for given cwd", async () => {
|
||||
const cwd = "/custom/project/path"
|
||||
|
||||
const result = getRooDirectoriesForCwd(cwd)
|
||||
const result = await getRooDirectoriesForCwd(cwd)
|
||||
|
||||
expect(result).toEqual([path.join("/mock/home", ".roo"), path.join(cwd, ".roo")])
|
||||
})
|
||||
|
||||
it("should use .roo workspace folder when it exists", async () => {
|
||||
const workspaceFolders = [
|
||||
{ uri: { fsPath: "/workspace/project1" }, name: "project1", index: 0 },
|
||||
{ uri: { fsPath: "/workspace/.roo" }, name: ".roo", index: 1 },
|
||||
]
|
||||
mockWorkspaceFolders.mockReturnValue(workspaceFolders)
|
||||
|
||||
const cwd = "/workspace/project1"
|
||||
const result = await getRooDirectoriesForCwd(cwd)
|
||||
|
||||
expect(result).toEqual([path.join("/mock/home", ".roo"), "/workspace/.roo"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("loadConfiguration", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Finds the workspace folder that contains a .roo directory
|
||||
*
|
||||
* @returns The workspace folder containing .roo, or undefined if not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const workspaceWithRoo = findWorkspaceWithRoo()
|
||||
* if (workspaceWithRoo) {
|
||||
* // .roo folder exists as one of the workspace folders
|
||||
* const rooPath = workspaceWithRoo.uri.fsPath
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function findWorkspaceWithRoo(): vscode.WorkspaceFolder | undefined {
|
||||
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Check if any workspace folder is named .roo
|
||||
for (const folder of vscode.workspace.workspaceFolders) {
|
||||
if (path.basename(folder.uri.fsPath) === ".roo") {
|
||||
return folder
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global .roo directory path based on the current platform
|
||||
|
|
@ -56,8 +86,19 @@ export function getGlobalRooDirectory(): string {
|
|||
* │ └── index.ts
|
||||
* └── package.json
|
||||
* ```
|
||||
*
|
||||
* @note In multi-root workspaces, if .roo is one of the workspace folders,
|
||||
* this function will return that folder's path instead of creating a .roo
|
||||
* subdirectory in the first workspace folder.
|
||||
*/
|
||||
export function getProjectRooDirectoryForCwd(cwd: string): string {
|
||||
// Check if .roo is one of the workspace folders in a multi-root workspace
|
||||
const workspaceWithRoo = findWorkspaceWithRoo()
|
||||
if (workspaceWithRoo) {
|
||||
return workspaceWithRoo.uri.fsPath
|
||||
}
|
||||
|
||||
// Default behavior: create .roo as a subdirectory
|
||||
return path.join(cwd, ".roo")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue