Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 16 additions & 10 deletions src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}`)
}

Expand Down
6 changes: 4 additions & 2 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/services/marketplace/SimpleInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
110 changes: 105 additions & 5 deletions src/services/roo-config/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -29,12 +43,14 @@ import {
readFileIfExists,
getRooDirectoriesForCwd,
loadConfiguration,
findWorkspaceWithRoo,
} from "../index"

describe("RooConfigService", () => {
beforeEach(() => {
vi.clearAllMocks()
mockHomedir.mockReturnValue("/mock/home")
mockWorkspaceFolders.mockReturnValue(undefined)
})

afterEach(() => {
Expand All @@ -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"))
})
})
Expand Down Expand Up @@ -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", () => {
Expand Down
41 changes: 41 additions & 0 deletions src/services/roo-config/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
}

Expand Down
Loading