Skip to content

Commit a3a78af

Browse files
authored
feat(commands): add support for project spec commands from .cospec directory (#583)
* feat(commands): add support for project spec commands from .cospec directory * test: update tests to reflect project spec command fallback behavior
1 parent 5b96a46 commit a3a78af

File tree

5 files changed

+37
-13
lines changed

5 files changed

+37
-13
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ import { ErrorCodeManager } from "../costrict/error-code"
7272
import { writeCostrictAccessToken } from "../costrict/codebase-index/utils"
7373
import { workspaceEventMonitor } from "../costrict/codebase-index/workspace-event-monitor"
7474
import { fetchZgsmQuotaInfo, fetchZgsmInviteCode } from "../../api/providers/fetchers/zgsm"
75-
import { ensureProjectWikiSubtasksExists } from "../costrict/wiki/projectWikiHelpers"
75+
// import { ensureProjectWikiSubtasksExists } from "../costrict/wiki/projectWikiHelpers"
7676

7777
export const webviewMessageHandler = async (
7878
provider: ClineProvider,

src/services/command/__tests__/frontmatter-commands.spec.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ vi.mock("fs/promises")
88
vi.mock("../roo-config", () => ({
99
getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"),
1010
getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"),
11+
getProjectCostrictSpecDirectoryForCwd: vi.fn(() => "/mock/project/.cospec"),
1112
}))
1213
vi.mock("../built-in-commands", () => ({
1314
getBuiltInCommands: vi.fn(() => Promise.resolve([])),
@@ -154,29 +155,29 @@ Global setup instructions.`
154155
})
155156
})
156157

157-
it("should fall back to global command if project command doesn't exist", async () => {
158-
const globalCommandContent = `---
159-
description: Global setup command
158+
it("should fall back to project spec command if project command doesn't exist", async () => {
159+
const projectSpecCommandContent = `---
160+
description: Project spec setup command
160161
---
161162
162-
# Global Setup
163+
# Project Spec Setup
163164
164-
Global setup instructions.`
165+
Project spec setup instructions.`
165166

166167
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
167168
mockFs.readFile = vi
168169
.fn()
169170
.mockRejectedValueOnce(new Error("File not found")) // Project command doesn't exist
170-
.mockResolvedValueOnce(globalCommandContent) // Global command exists
171+
.mockResolvedValueOnce(projectSpecCommandContent) // Project spec command exists
171172

172173
const result = await getCommand("/test/cwd", "setup")
173174

174175
expect(result).toEqual({
175176
name: "setup",
176-
content: "# Global Setup\n\nGlobal setup instructions.",
177-
source: "global",
178-
filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")),
179-
description: "Global setup command",
177+
content: "# Project Spec Setup\n\nProject spec setup instructions.",
178+
source: "project",
179+
filePath: path.join("/test/cwd/.cospec/openspec/commands/setup.md"),
180+
description: "Project spec setup command",
180181
argumentHint: undefined,
181182
})
182183
})

src/services/command/commands.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import fs from "fs/promises"
22
import * as path from "path"
33
import matter from "gray-matter"
4-
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config"
4+
import {
5+
getGlobalRooDirectory,
6+
getProjectRooDirectoryForCwd,
7+
getProjectCostrictSpecDirectoryForCwd,
8+
} from "../roo-config"
59
import { getBuiltInCommands, getBuiltInCommand } from "./built-in-commands"
610

711
export interface Command {
@@ -34,6 +38,9 @@ export async function getCommands(cwd: string): Promise<Command[]> {
3438
const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands")
3539
await scanCommandDirectory(projectDir, "project", commands)
3640

41+
const projectSpecCommandsDir = path.join(getProjectCostrictSpecDirectoryForCwd(cwd), "openspec", "commands")
42+
await scanCommandDirectory(projectSpecCommandsDir, "project", commands)
43+
3744
return Array.from(commands.values())
3845
}
3946

@@ -43,6 +50,7 @@ export async function getCommands(cwd: string): Promise<Command[]> {
4350
*/
4451
export async function getCommand(cwd: string, name: string): Promise<Command | undefined> {
4552
// Try to find the command directly without scanning all commands
53+
const projectSpecCommandsDir = path.join(getProjectCostrictSpecDirectoryForCwd(cwd), "openspec", "commands")
4654
const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands")
4755
const globalDir = path.join(getGlobalRooDirectory(), "commands")
4856

@@ -52,6 +60,12 @@ export async function getCommand(cwd: string, name: string): Promise<Command | u
5260
return projectCommand
5361
}
5462

63+
// Check project directory first (highest priority)
64+
const projectSpecCommand = await tryLoadCommand(projectSpecCommandsDir, name, "project")
65+
if (projectSpecCommand) {
66+
return projectSpecCommand
67+
}
68+
5569
// Check global directory if not found in project
5670
const globalCommand = await tryLoadCommand(globalDir, name, "global")
5771
if (globalCommand) {

src/services/roo-config/__tests__/index.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,11 @@ describe("RooConfigService", () => {
210210

211211
const result = getRooDirectoriesForCwd(cwd)
212212

213-
expect(result).toEqual([path.join("/mock/home", ".roo"), path.join(cwd, ".roo")])
213+
expect(result).toEqual([
214+
path.join("/mock/home", ".roo"),
215+
path.join(cwd, ".roo"),
216+
path.join(cwd, ".cospec", "openspec"),
217+
])
214218
})
215219
})
216220

src/services/roo-config/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export function getProjectRooDirectoryForCwd(cwd: string): string {
6161
return path.join(cwd, ".roo")
6262
}
6363

64+
export function getProjectCostrictSpecDirectoryForCwd(cwd: string): string {
65+
return path.join(cwd, ".cospec")
66+
}
67+
6468
/**
6569
* Checks if a directory exists
6670
*/
@@ -152,6 +156,7 @@ export function getRooDirectoriesForCwd(cwd: string): string[] {
152156

153157
// Add project-local directory second
154158
directories.push(getProjectRooDirectoryForCwd(cwd))
159+
directories.push(path.join(getProjectCostrictSpecDirectoryForCwd(cwd), "openspec"))
155160

156161
return directories
157162
}

0 commit comments

Comments
 (0)