Skip to content

Commit c97e3dd

Browse files
committed
Add a built-in /init slash command
1 parent 0c481a3 commit c97e3dd

File tree

28 files changed

+360
-53
lines changed

28 files changed

+360
-53
lines changed

src/__tests__/command-integration.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("Command Integration Tests", () => {
1515
commands.forEach((command) => {
1616
expect(command.name).toBeDefined()
1717
expect(typeof command.name).toBe("string")
18-
expect(command.source).toMatch(/^(project|global)$/)
18+
expect(command.source).toMatch(/^(project|global|built-in)$/)
1919
expect(command.content).toBeDefined()
2020
expect(typeof command.content).toBe("string")
2121
})
@@ -43,7 +43,7 @@ describe("Command Integration Tests", () => {
4343

4444
expect(loadedCommand).toBeDefined()
4545
expect(loadedCommand?.name).toBe(firstCommand.name)
46-
expect(loadedCommand?.source).toMatch(/^(project|global)$/)
46+
expect(loadedCommand?.source).toMatch(/^(project|global|built-in)$/)
4747
expect(loadedCommand?.content).toBeDefined()
4848
expect(typeof loadedCommand?.content).toBe("string")
4949
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2619,6 +2619,7 @@ export const webviewMessageHandler = async (
26192619
source: command.source,
26202620
filePath: command.filePath,
26212621
description: command.description,
2622+
argumentHint: command.argumentHint,
26222623
}))
26232624
await provider.postMessageToWebview({
26242625
type: "commands",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, it, expect } from "vitest"
2+
import { getBuiltInCommands, getBuiltInCommand, getBuiltInCommandNames } from "../built-in-commands"
3+
4+
describe("Built-in Commands", () => {
5+
describe("getBuiltInCommands", () => {
6+
it("should return all built-in commands", async () => {
7+
const commands = await getBuiltInCommands()
8+
9+
expect(commands).toHaveLength(1)
10+
expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init"]))
11+
12+
// Verify all commands have required properties
13+
commands.forEach((command) => {
14+
expect(command.name).toBeDefined()
15+
expect(typeof command.name).toBe("string")
16+
expect(command.content).toBeDefined()
17+
expect(typeof command.content).toBe("string")
18+
expect(command.source).toBe("built-in")
19+
expect(command.filePath).toMatch(/^<built-in:.+>$/)
20+
expect(command.description).toBeDefined()
21+
expect(typeof command.description).toBe("string")
22+
})
23+
})
24+
25+
it("should return commands with proper content", async () => {
26+
const commands = await getBuiltInCommands()
27+
28+
const initCommand = commands.find((cmd) => cmd.name === "init")
29+
expect(initCommand).toBeDefined()
30+
expect(initCommand!.content).toContain("AGENTS.md")
31+
expect(initCommand!.content).toContain(".roo/rules-")
32+
expect(initCommand!.description).toBe("Initialize a project with recommended rules and configuration")
33+
})
34+
})
35+
36+
describe("getBuiltInCommand", () => {
37+
it("should return specific built-in command by name", async () => {
38+
const initCommand = await getBuiltInCommand("init")
39+
40+
expect(initCommand).toBeDefined()
41+
expect(initCommand!.name).toBe("init")
42+
expect(initCommand!.source).toBe("built-in")
43+
expect(initCommand!.filePath).toBe("<built-in:init>")
44+
expect(initCommand!.content).toContain("AGENTS.md")
45+
expect(initCommand!.description).toBe("Initialize a project with recommended rules and configuration")
46+
})
47+
48+
it("should return undefined for non-existent command", async () => {
49+
const nonExistentCommand = await getBuiltInCommand("non-existent")
50+
expect(nonExistentCommand).toBeUndefined()
51+
})
52+
53+
it("should handle empty string command name", async () => {
54+
const emptyCommand = await getBuiltInCommand("")
55+
expect(emptyCommand).toBeUndefined()
56+
})
57+
})
58+
59+
describe("getBuiltInCommandNames", () => {
60+
it("should return all built-in command names", async () => {
61+
const names = await getBuiltInCommandNames()
62+
63+
expect(names).toHaveLength(1)
64+
expect(names).toEqual(expect.arrayContaining(["init"]))
65+
// Order doesn't matter since it's based on filesystem order
66+
expect(names.sort()).toEqual(["init"])
67+
})
68+
69+
it("should return array of strings", async () => {
70+
const names = await getBuiltInCommandNames()
71+
72+
names.forEach((name) => {
73+
expect(typeof name).toBe("string")
74+
expect(name.length).toBeGreaterThan(0)
75+
})
76+
})
77+
})
78+
79+
describe("Command Content Validation", () => {
80+
it("init command should have comprehensive content", async () => {
81+
const command = await getBuiltInCommand("init")
82+
const content = command!.content
83+
84+
// Should contain key sections
85+
expect(content).toContain("Please analyze this codebase")
86+
expect(content).toContain("Build/lint/test commands")
87+
expect(content).toContain("Code style guidelines")
88+
expect(content).toContain("mode-specific rule directories")
89+
expect(content).toContain("refer to the system prompt")
90+
91+
// Should mention important concepts
92+
expect(content).toContain("AGENTS.md")
93+
expect(content).toContain(".roo/rules-")
94+
expect(content).toContain("four core modes")
95+
expect(content).toContain("mode-specific AGENTS.md files")
96+
})
97+
})
98+
})

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ vi.mock("../roo-config", () => ({
99
getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"),
1010
getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"),
1111
}))
12+
vi.mock("../built-in-commands", () => ({
13+
getBuiltInCommands: vi.fn(() => Promise.resolve([])),
14+
getBuiltInCommand: vi.fn(() => Promise.resolve(undefined)),
15+
getBuiltInCommandNames: vi.fn(() => Promise.resolve([])),
16+
}))
1217

1318
const mockFs = vi.mocked(fs)
1419

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Command } from "./commands"
2+
3+
interface BuiltInCommandDefinition {
4+
name: string
5+
description: string
6+
argumentHint?: string
7+
content: string
8+
}
9+
10+
const BUILT_IN_COMMANDS: Record<string, BuiltInCommandDefinition> = {
11+
init: {
12+
name: "init",
13+
description: "Initialize a project with recommended rules and configuration",
14+
content: `Please analyze this codebase and create an AGENTS.md file containing:
15+
1. Build/lint/test commands - especially for running a single test
16+
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
17+
18+
Usage notes:
19+
- 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.
20+
- If there's already an AGENTS.md, improve it.
21+
- 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.
22+
- Be sure to prefix the file with the following text:
23+
24+
# AGENTS.md
25+
26+
This file provides guidance to agents when working with code in this repository.
27+
28+
Additionally, please:
29+
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/\`
30+
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.
31+
32+
**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.
33+
34+
Example structure with specific instructions:
35+
36+
\\\`\\\`\\\`
37+
AGENTS.md # General project guidance
38+
.roo/
39+
├── rules-code/
40+
│ └── AGENTS.md # Code mode specific instructions
41+
├── rules-debug/
42+
│ └── AGENTS.md # Debug mode specific instructions
43+
├── rules-ask/
44+
│ └── AGENTS.md # Ask mode specific instructions
45+
└── rules-architect/
46+
└── AGENTS.md # Architect mode specific instructions
47+
\\\`\\\`\\\`
48+
49+
**Example project-specific instructions:**
50+
51+
**\`.roo/rules-code/AGENTS.md\`** - Project-specific coding rules:
52+
\\\`\\\`\\\`
53+
# Project Coding Rules
54+
55+
- All API calls must use the retry mechanism in src/api/providers/utils/
56+
- UI components should use Tailwind CSS classes, not inline styles
57+
- New providers must implement the Provider interface in packages/types/src/
58+
- Database queries must use the query builder in packages/evals/src/db/queries/
59+
- Always use safeWriteJson() from src/utils/ instead of JSON.stringify for file writes
60+
- Test coverage required for all new features in src/ and webview-ui/
61+
\\\`\\\`\\\`
62+
63+
**\`.roo/rules-debug/AGENTS.md\`** - Project-specific debugging approaches:
64+
\\\`\\\`\\\`
65+
# Project Debug Rules
66+
67+
- Check VSCode extension logs in the Debug Console
68+
- For webview issues, inspect the webview dev tools via Command Palette
69+
- Provider issues: check src/api/providers/__tests__/ for similar test patterns
70+
- Database issues: run migrations in packages/evals/src/db/migrations/
71+
- IPC communication issues: review packages/ipc/src/ message patterns
72+
- Always reproduce in both development and production extension builds
73+
\\\`\\\`\\\`
74+
75+
**\`.roo/rules-ask/AGENTS.md\`** - Project-specific explanation context:
76+
\\\`\\\`\\\`
77+
# Project Documentation Rules
78+
79+
- Reference the monorepo structure: src/ (VSCode extension), apps/ (web apps), packages/ (shared)
80+
- Explain provider patterns by referencing existing ones in src/api/providers/
81+
- For UI questions, reference webview-ui/ React components and their patterns
82+
- Point to package.json scripts for build/test commands
83+
- Reference locales/ for i18n patterns when discussing translations
84+
- Always mention the VSCode webview architecture when discussing UI
85+
\\\`\\\`\\\`
86+
87+
**\`.roo/rules-architect/AGENTS.md\`** - Project-specific architectural considerations:
88+
\\\`\\\`\\\`
89+
# Project Architecture Rules
90+
91+
- New features must work within VSCode extension + webview architecture
92+
- Provider implementations must be stateless and cacheable
93+
- UI state management uses React hooks, not external state libraries
94+
- Database schema changes require migrations in packages/evals/src/db/migrations/
95+
- New packages must follow the existing monorepo structure in packages/
96+
- API changes must maintain backward compatibility with existing provider contracts
97+
\\\`\\\`\\\`
98+
99+
This structure provides both general project guidance and specialized instructions for the four core modes' specific domains and workflows.
100+
`,
101+
},
102+
}
103+
104+
/**
105+
* Get all built-in commands as Command objects
106+
*/
107+
export async function getBuiltInCommands(): Promise<Command[]> {
108+
return Object.values(BUILT_IN_COMMANDS).map((cmd) => ({
109+
name: cmd.name,
110+
content: cmd.content,
111+
source: "built-in" as const,
112+
filePath: `<built-in:${cmd.name}>`,
113+
description: cmd.description,
114+
argumentHint: cmd.argumentHint,
115+
}))
116+
}
117+
118+
/**
119+
* Get a specific built-in command by name
120+
*/
121+
export async function getBuiltInCommand(name: string): Promise<Command | undefined> {
122+
const cmd = BUILT_IN_COMMANDS[name]
123+
if (!cmd) return undefined
124+
125+
return {
126+
name: cmd.name,
127+
content: cmd.content,
128+
source: "built-in" as const,
129+
filePath: `<built-in:${name}>`,
130+
description: cmd.description,
131+
argumentHint: cmd.argumentHint,
132+
}
133+
}
134+
135+
/**
136+
* Get names of all built-in commands
137+
*/
138+
export async function getBuiltInCommandNames(): Promise<string[]> {
139+
return Object.keys(BUILT_IN_COMMANDS)
140+
}

src/services/command/commands.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,35 @@ import fs from "fs/promises"
22
import * as path from "path"
33
import matter from "gray-matter"
44
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config"
5+
import { getBuiltInCommands, getBuiltInCommand } from "./built-in-commands"
56

67
export interface Command {
78
name: string
89
content: string
9-
source: "global" | "project"
10+
source: "global" | "project" | "built-in"
1011
filePath: string
1112
description?: string
1213
argumentHint?: string
1314
}
1415

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

21-
// Scan global commands first
23+
// Add built-in commands first (lowest priority)
24+
const builtInCommands = await getBuiltInCommands()
25+
for (const command of builtInCommands) {
26+
commands.set(command.name, command)
27+
}
28+
29+
// Scan global commands (override built-in)
2230
const globalDir = path.join(getGlobalRooDirectory(), "commands")
2331
await scanCommandDirectory(globalDir, "global", commands)
2432

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

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

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

40-
// Check project directory first (project commands override global ones)
49+
// Check project directory first (highest priority)
4150
const projectCommand = await tryLoadCommand(projectDir, name, "project")
4251
if (projectCommand) {
4352
return projectCommand
4453
}
4554

4655
// Check global directory if not found in project
4756
const globalCommand = await tryLoadCommand(globalDir, name, "global")
48-
return globalCommand
57+
if (globalCommand) {
58+
return globalCommand
59+
}
60+
61+
// Check built-in commands if not found in project or global (lowest priority)
62+
return await getBuiltInCommand(name)
4963
}
5064

5165
/**

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ModelRecord, RouterModels } from "./api"
2121
// Command interface for frontend/backend communication
2222
export interface Command {
2323
name: string
24-
source: "global" | "project"
24+
source: "global" | "project" | "built-in"
2525
filePath?: string
2626
description?: string
2727
argumentHint?: string

0 commit comments

Comments
 (0)