Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/__tests__/command-integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("Command Integration Tests", () => {
commands.forEach((command) => {
expect(command.name).toBeDefined()
expect(typeof command.name).toBe("string")
expect(command.source).toMatch(/^(project|global)$/)
expect(command.source).toMatch(/^(project|global|built-in)$/)
expect(command.content).toBeDefined()
expect(typeof command.content).toBe("string")
})
Expand Down Expand Up @@ -43,7 +43,7 @@ describe("Command Integration Tests", () => {

expect(loadedCommand).toBeDefined()
expect(loadedCommand?.name).toBe(firstCommand.name)
expect(loadedCommand?.source).toMatch(/^(project|global)$/)
expect(loadedCommand?.source).toMatch(/^(project|global|built-in)$/)
expect(loadedCommand?.content).toBeDefined()
expect(typeof loadedCommand?.content).toBe("string")
}
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2619,6 +2619,7 @@ export const webviewMessageHandler = async (
source: command.source,
filePath: command.filePath,
description: command.description,
argumentHint: command.argumentHint,
}))
await provider.postMessageToWebview({
type: "commands",
Expand Down
98 changes: 98 additions & 0 deletions src/services/command/__tests__/built-in-commands.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it, expect } from "vitest"
import { getBuiltInCommands, getBuiltInCommand, getBuiltInCommandNames } from "../built-in-commands"

describe("Built-in Commands", () => {
describe("getBuiltInCommands", () => {
it("should return all built-in commands", async () => {
const commands = await getBuiltInCommands()

expect(commands).toHaveLength(1)
expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init"]))

// Verify all commands have required properties
commands.forEach((command) => {
expect(command.name).toBeDefined()
expect(typeof command.name).toBe("string")
expect(command.content).toBeDefined()
expect(typeof command.content).toBe("string")
expect(command.source).toBe("built-in")
expect(command.filePath).toMatch(/^<built-in:.+>$/)
expect(command.description).toBeDefined()
expect(typeof command.description).toBe("string")
})
})

it("should return commands with proper content", async () => {
const commands = await getBuiltInCommands()

const initCommand = commands.find((cmd) => cmd.name === "init")
expect(initCommand).toBeDefined()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests will fail because they expect content that doesn't exist in the actual command files. The test expectations don't match the current 'TBD' placeholders. Should these tests be marked as pending/skipped until the actual content is implemented?

expect(initCommand!.content).toContain("AGENTS.md")
expect(initCommand!.content).toContain(".roo/rules-")
expect(initCommand!.description).toBe("Initialize a project with recommended rules and configuration")
})
})

describe("getBuiltInCommand", () => {
it("should return specific built-in command by name", async () => {
const initCommand = await getBuiltInCommand("init")

expect(initCommand).toBeDefined()
expect(initCommand!.name).toBe("init")
expect(initCommand!.source).toBe("built-in")
expect(initCommand!.filePath).toBe("<built-in:init>")
expect(initCommand!.content).toContain("AGENTS.md")
expect(initCommand!.description).toBe("Initialize a project with recommended rules and configuration")
})

it("should return undefined for non-existent command", async () => {
const nonExistentCommand = await getBuiltInCommand("non-existent")
expect(nonExistentCommand).toBeUndefined()
})

it("should handle empty string command name", async () => {
const emptyCommand = await getBuiltInCommand("")
expect(emptyCommand).toBeUndefined()
})
})

describe("getBuiltInCommandNames", () => {
it("should return all built-in command names", async () => {
const names = await getBuiltInCommandNames()

expect(names).toHaveLength(1)
expect(names).toEqual(expect.arrayContaining(["init"]))
// Order doesn't matter since it's based on filesystem order
expect(names.sort()).toEqual(["init"])
})

it("should return array of strings", async () => {
const names = await getBuiltInCommandNames()

names.forEach((name) => {
expect(typeof name).toBe("string")
expect(name.length).toBeGreaterThan(0)
})
})
})

describe("Command Content Validation", () => {
it("init command should have comprehensive content", async () => {
const command = await getBuiltInCommand("init")
const content = command!.content

// Should contain key sections
expect(content).toContain("Please analyze this codebase")
expect(content).toContain("Build/lint/test commands")
expect(content).toContain("Code style guidelines")
expect(content).toContain("mode-specific rule directories")
expect(content).toContain("refer to the system prompt")

// Should mention important concepts
expect(content).toContain("AGENTS.md")
expect(content).toContain(".roo/rules-")
expect(content).toContain("four core modes")
expect(content).toContain("mode-specific AGENTS.md files")
})
})
})
5 changes: 5 additions & 0 deletions src/services/command/__tests__/frontmatter-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ vi.mock("../roo-config", () => ({
getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"),
getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"),
}))
vi.mock("../built-in-commands", () => ({
getBuiltInCommands: vi.fn(() => Promise.resolve([])),
getBuiltInCommand: vi.fn(() => Promise.resolve(undefined)),
getBuiltInCommandNames: vi.fn(() => Promise.resolve([])),
}))

const mockFs = vi.mocked(fs)

Expand Down
140 changes: 140 additions & 0 deletions src/services/command/built-in-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Command } from "./commands"

interface BuiltInCommandDefinition {
name: string
description: string
argumentHint?: string
content: string
}

const BUILT_IN_COMMANDS: Record<string, BuiltInCommandDefinition> = {
init: {
name: "init",
description: "Initialize a project with recommended rules and configuration",
content: `Please analyze this codebase and create an AGENTS.md file containing:
1. Build/lint/test commands - especially for running a single test
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.

Usage notes:
- The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
- If there's already an AGENTS.md, improve it.
- If there are Claude Code rules (in CLAUDE.md), Cursor rules (in .cursor/rules/ or .cursorrules), or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
- Be sure to prefix the file with the following text:

# AGENTS.md

This file provides guidance to agents when working with code in this repository.

Additionally, please:
1. **Create mode-specific rule directories** - Create directory structures for the four core modes: \`.roo/rules-code/\`, \`.roo/rules-ask/\`, \`.roo/rules-architect/\`, and \`.roo/rules-debug/\`
2. **Create mode-specific AGENTS.md files** - Within each of these four mode directories, research and then create an AGENTS.md file with rules specific to that mode's purpose and capabilities. These rules should provide additive context and not just repeat the mode definitions. Only include rules that you have high confidence are accurate, valuable, and non-obvious.

**For the complete list of available modes with detailed descriptions, refer to the system prompt.** The system prompt contains comprehensive information about each mode's purpose, when to use it, and its specific capabilities.

Example structure with specific instructions:

\\\`\\\`\\\`
AGENTS.md # General project guidance
.roo/
├── rules-code/
│ └── AGENTS.md # Code mode specific instructions
├── rules-debug/
│ └── AGENTS.md # Debug mode specific instructions
├── rules-ask/
│ └── AGENTS.md # Ask mode specific instructions
└── rules-architect/
└── AGENTS.md # Architect mode specific instructions
\\\`\\\`\\\`

**Example project-specific instructions:**

**\`.roo/rules-code/AGENTS.md\`** - Project-specific coding rules:
\\\`\\\`\\\`
# Project Coding Rules

- All API calls must use the retry mechanism in src/api/providers/utils/
- UI components should use Tailwind CSS classes, not inline styles
- New providers must implement the Provider interface in packages/types/src/
- Database queries must use the query builder in packages/evals/src/db/queries/
- Always use safeWriteJson() from src/utils/ instead of JSON.stringify for file writes
- Test coverage required for all new features in src/ and webview-ui/
\\\`\\\`\\\`

**\`.roo/rules-debug/AGENTS.md\`** - Project-specific debugging approaches:
\\\`\\\`\\\`
# Project Debug Rules

- Check VSCode extension logs in the Debug Console
- For webview issues, inspect the webview dev tools via Command Palette
- Provider issues: check src/api/providers/__tests__/ for similar test patterns
- Database issues: run migrations in packages/evals/src/db/migrations/
- IPC communication issues: review packages/ipc/src/ message patterns
- Always reproduce in both development and production extension builds
\\\`\\\`\\\`

**\`.roo/rules-ask/AGENTS.md\`** - Project-specific explanation context:
\\\`\\\`\\\`
# Project Documentation Rules

- Reference the monorepo structure: src/ (VSCode extension), apps/ (web apps), packages/ (shared)
- Explain provider patterns by referencing existing ones in src/api/providers/
- For UI questions, reference webview-ui/ React components and their patterns
- Point to package.json scripts for build/test commands
- Reference locales/ for i18n patterns when discussing translations
- Always mention the VSCode webview architecture when discussing UI
\\\`\\\`\\\`

**\`.roo/rules-architect/AGENTS.md\`** - Project-specific architectural considerations:
\\\`\\\`\\\`
# Project Architecture Rules

- New features must work within VSCode extension + webview architecture
- Provider implementations must be stateless and cacheable
- UI state management uses React hooks, not external state libraries
- Database schema changes require migrations in packages/evals/src/db/migrations/
- New packages must follow the existing monorepo structure in packages/
- API changes must maintain backward compatibility with existing provider contracts
\\\`\\\`\\\`

This structure provides both general project guidance and specialized instructions for the four core modes' specific domains and workflows.
`,
},
}

/**
* Get all built-in commands as Command objects
*/
export async function getBuiltInCommands(): Promise<Command[]> {
return Object.values(BUILT_IN_COMMANDS).map((cmd) => ({
name: cmd.name,
content: cmd.content,
source: "built-in" as const,
filePath: `<built-in:${cmd.name}>`,
description: cmd.description,
argumentHint: cmd.argumentHint,
}))
}

/**
* Get a specific built-in command by name
*/
export async function getBuiltInCommand(name: string): Promise<Command | undefined> {
const cmd = BUILT_IN_COMMANDS[name]
if (!cmd) return undefined

return {
name: cmd.name,
content: cmd.content,
source: "built-in" as const,
filePath: `<built-in:${name}>`,
description: cmd.description,
argumentHint: cmd.argumentHint,
}
}

/**
* Get names of all built-in commands
*/
export async function getBuiltInCommandNames(): Promise<string[]> {
return Object.keys(BUILT_IN_COMMANDS)
}
26 changes: 20 additions & 6 deletions src/services/command/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@ import fs from "fs/promises"
import * as path from "path"
import matter from "gray-matter"
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config"
import { getBuiltInCommands, getBuiltInCommand } from "./built-in-commands"

export interface Command {
name: string
content: string
source: "global" | "project"
source: "global" | "project" | "built-in"
filePath: string
description?: string
argumentHint?: string
}

/**
* Get all available commands from both global and project directories
* Get all available commands from built-in, global, and project directories
* Priority order: project > global > built-in (later sources override earlier ones)
*/
export async function getCommands(cwd: string): Promise<Command[]> {
const commands = new Map<string, Command>()

// Scan global commands first
// Add built-in commands first (lowest priority)
const builtInCommands = await getBuiltInCommands()
for (const command of builtInCommands) {
commands.set(command.name, command)
}

// Scan global commands (override built-in)
const globalDir = path.join(getGlobalRooDirectory(), "commands")
await scanCommandDirectory(globalDir, "global", commands)

// Scan project commands (these override global ones)
// Scan project commands (highest priority - override both global and built-in)
const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands")
await scanCommandDirectory(projectDir, "project", commands)

Expand All @@ -31,21 +39,27 @@ export async function getCommands(cwd: string): Promise<Command[]> {

/**
* Get a specific command by name (optimized to avoid scanning all commands)
* Priority order: project > global > built-in
*/
export async function getCommand(cwd: string, name: string): Promise<Command | undefined> {
// Try to find the command directly without scanning all commands
const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands")
const globalDir = path.join(getGlobalRooDirectory(), "commands")

// Check project directory first (project commands override global ones)
// Check project directory first (highest priority)
const projectCommand = await tryLoadCommand(projectDir, name, "project")
if (projectCommand) {
return projectCommand
}

// Check global directory if not found in project
const globalCommand = await tryLoadCommand(globalDir, name, "global")
return globalCommand
if (globalCommand) {
return globalCommand
}

// Check built-in commands if not found in project or global (lowest priority)
return await getBuiltInCommand(name)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ModelRecord, RouterModels } from "./api"
// Command interface for frontend/backend communication
export interface Command {
name: string
source: "global" | "project"
source: "global" | "project" | "built-in"
filePath?: string
description?: string
argumentHint?: string
Expand Down
Loading
Loading