Add support for slash command frontmatter descriptions (#6314)

This commit is contained in:
Matt Rubens 2025-07-28 17:03:10 -04:00 • committed by GitHub
parent 03ce1e58a3
commit 182679d34c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 347 additions and 21 deletions

43
pnpm-lock.yaml generated
View file

@ -654,6 +654,9 @@ importers:
google-auth-library:
specifier: ^9.15.1
version: 9.15.1
gray-matter:
specifier: ^4.0.3
version: 4.0.3
i18next:
specifier: ^25.0.0
version: 25.2.1(typescript@5.8.3)
@ -5644,6 +5647,10 @@ packages:
exsolve@1.0.5:
resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@ -6026,6 +6033,10 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
gray-matter@4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
gtoken@7.1.0:
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
engines: {node: '>=14.0.0'}
@ -6340,6 +6351,10 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -8432,6 +8447,10 @@ packages:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
seed-random@2.2.0:
resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==}
@ -8744,6 +8763,10 @@ packages:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
strip-bom-string@1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@ -15174,6 +15197,10 @@ snapshots:
exsolve@1.0.5: {}
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
extend@3.0.2: {}
extendable-error@0.1.7: {}
@ -15597,6 +15624,13 @@ snapshots:
graphemer@1.4.0: {}
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.1
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
gtoken@7.1.0:
dependencies:
gaxios: 6.7.1
@ -15959,6 +15993,8 @@ snapshots:
is-docker@3.0.0: {}
is-extendable@0.1.1: {}
is-extglob@2.1.1: {}
is-finalizationregistry@1.1.1:
@ -18479,6 +18515,11 @@ snapshots:
screenfull@5.2.0: {}
section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
seed-random@2.2.0: {}
semver@5.7.2: {}
@ -18873,6 +18914,8 @@ snapshots:
dependencies:
ansi-regex: 6.1.0
strip-bom-string@1.0.0: {}
strip-bom@3.0.0: {}
strip-bom@5.0.0: {}

View file

@ -218,7 +218,12 @@ export async function parseMentions(
try {
const command = await getCommand(cwd, commandName)
if (command) {
parsedText += `\n\n<command name="${commandName}">\n${command.content}\n</command>`
let commandOutput = ""
if (command.description) {
commandOutput += `Description: ${command.description}\n\n`
}
commandOutput += command.content
parsedText += `\n\n<command name="${commandName}">\n${commandOutput}\n</command>`
} else {
parsedText += `\n\n<command name="${commandName}">\nCommand '${commandName}' not found. Available commands can be found in .roo/commands/ or ~/.roo/commands/\n</command>`
}

View file

@ -2366,6 +2366,7 @@ export const webviewMessageHandler = async (
name: command.name,
source: command.source,
filePath: command.filePath,
description: command.description,
}))
await provider.postMessageToWebview({
@ -2524,6 +2525,7 @@ export const webviewMessageHandler = async (
name: command.name,
source: command.source,
filePath: command.filePath,
description: command.description,
}))
await provider.postMessageToWebview({
type: "commands",

View file

@ -80,7 +80,7 @@
"no_workspace_for_project_command": "No s'ha trobat cap carpeta d'espai de treball per a l'ordre del projecte",
"command_already_exists": "L'ordre \"{{commandName}}\" ja existeix",
"create_command_failed": "Error en crear l'ordre",
"command_template_content": "Aquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.",
"command_template_content": "---\ndescription: \"Breu descripció del que fa aquesta ordre\"\n---\n\nAquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.",
"claudeCode": {
"processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.",
"errorOutput": "Sortida d'error: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Kein Arbeitsbereich-Ordner für Projektbefehl gefunden",
"command_already_exists": "Befehl \"{{commandName}}\" existiert bereits",
"create_command_failed": "Fehler beim Erstellen des Befehls",
"command_template_content": "Dies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.",
"command_template_content": "---\ndescription: \"Kurze Beschreibung dessen, was dieser Befehl macht\"\n---\n\nDies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.",
"claudeCode": {
"processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.",
"errorOutput": "Fehlerausgabe: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "No workspace folder found for project command",
"command_already_exists": "Command \"{{commandName}}\" already exists",
"create_command_failed": "Failed to create command",
"command_template_content": "This is a new slash command. Edit this file to customize the command behavior.",
"command_template_content": "---\ndescription: \"Brief description of what this command does\"\n---\n\nThis is a new slash command. Edit this file to customize the command behavior.",
"claudeCode": {
"processExited": "Claude Code process exited with code {{exitCode}}.",
"errorOutput": "Error output: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "No se encontró carpeta de espacio de trabajo para comando de proyecto",
"command_already_exists": "El comando \"{{commandName}}\" ya existe",
"create_command_failed": "Error al crear comando",
"command_template_content": "Este es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.",
"command_template_content": "---\ndescription: \"Breve descripción de lo que hace este comando\"\n---\n\nEste es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.",
"claudeCode": {
"processExited": "El proceso de Claude Code terminó con código {{exitCode}}.",
"errorOutput": "Salida de error: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Aucun dossier d'espace de travail trouvé pour la commande de projet",
"command_already_exists": "La commande \"{{commandName}}\" existe déjà",
"create_command_failed": "Échec de la création de la commande",
"command_template_content": "Ceci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.",
"command_template_content": "---\ndescription: \"Brève description de ce que fait cette commande\"\n---\n\nCeci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.",
"claudeCode": {
"processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.",
"errorOutput": "Sortie d'erreur : {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "प्रोजेक्ट कमांड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला",
"command_already_exists": "कमांड \"{{commandName}}\" पहले से मौजूद है",
"create_command_failed": "कमांड बनाने में विफल",
"command_template_content": "यह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।",
"command_template_content": "---\ndescription: \"इस कमांड के कार्य का संक्षिप्त विवरण\"\n---\n\nयह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।",
"claudeCode": {
"processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।",
"errorOutput": "त्रुटि आउटपुट: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Tidak ditemukan folder workspace untuk perintah proyek",
"command_already_exists": "Perintah \"{{commandName}}\" sudah ada",
"create_command_failed": "Gagal membuat perintah",
"command_template_content": "Ini adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.",
"command_template_content": "---\ndescription: \"Deskripsi singkat tentang fungsi perintah ini\"\n---\n\nIni adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.",
"claudeCode": {
"processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.",
"errorOutput": "Output error: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Nessuna cartella workspace trovata per il comando di progetto",
"command_already_exists": "Il comando \"{{commandName}}\" esiste già",
"create_command_failed": "Errore nella creazione del comando",
"command_template_content": "Questo è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.",
"command_template_content": "---\ndescription: \"Breve descrizione di cosa fa questo comando\"\n---\n\nQuesto è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.",
"claudeCode": {
"processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.",
"errorOutput": "Output di errore: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "プロジェクトコマンド用のワークスペースフォルダが見つかりません",
"command_already_exists": "コマンド \"{{commandName}}\" は既に存在します",
"create_command_failed": "コマンドの作成に失敗しました",
"command_template_content": "これは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。",
"command_template_content": "---\ndescription: \"このコマンドが何をするかの簡潔な説明\"\n---\n\nこれは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。",
"claudeCode": {
"processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。",
"errorOutput": "エラー出力:{{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "프로젝트 명령용 워크스페이스 폴더를 찾을 수 없습니다",
"command_already_exists": "명령 \"{{commandName}}\"이(가) 이미 존재합니다",
"create_command_failed": "명령 생성에 실패했습니다",
"command_template_content": "이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.",
"command_template_content": "---\ndescription: \"이 명령이 수행하는 작업에 대한 간단한 설명\"\n---\n\n이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.",
"claudeCode": {
"processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.",
"errorOutput": "오류 출력: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Geen werkruimtemap gevonden voor projectopdracht",
"command_already_exists": "Opdracht \"{{commandName}}\" bestaat al",
"create_command_failed": "Kan opdracht niet aanmaken",
"command_template_content": "Dit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.",
"command_template_content": "---\ndescription: \"Korte beschrijving van wat deze opdracht doet\"\n---\n\nDit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.",
"claudeCode": {
"processExited": "Claude Code proces beëindigd met code {{exitCode}}.",
"errorOutput": "Foutuitvoer: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Nie znaleziono folderu obszaru roboczego dla polecenia projektu",
"command_already_exists": "Polecenie \"{{commandName}}\" już istnieje",
"create_command_failed": "Nie udało się utworzyć polecenia",
"command_template_content": "To jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.",
"command_template_content": "---\ndescription: \"Krótki opis tego, co robi to polecenie\"\n---\n\nTo jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.",
"claudeCode": {
"processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.",
"errorOutput": "Wyjście błędu: {{output}}",

View file

@ -81,7 +81,7 @@
"no_workspace_for_project_command": "Nenhuma pasta de workspace encontrada para comando de projeto",
"command_already_exists": "Comando \"{{commandName}}\" já existe",
"create_command_failed": "Falha ao criar comando",
"command_template_content": "Este é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.",
"command_template_content": "---\ndescription: \"Breve descrição do que este comando faz\"\n---\n\nEste é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.",
"claudeCode": {
"processExited": "O processo Claude Code saiu com código {{exitCode}}.",
"errorOutput": "Saída de erro: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Не найдена папка рабочего пространства для команды проекта",
"command_already_exists": "Команда \"{{commandName}}\" уже существует",
"create_command_failed": "Не удалось создать команду",
"command_template_content": "Это новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.",
"command_template_content": "---\ndescription: \"Краткое описание того, что делает эта команда\"\n---\n\nЭто новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.",
"claudeCode": {
"processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.",
"errorOutput": "Вывод ошибки: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Proje komutu için çalışma alanı klasörü bulunamadı",
"command_already_exists": "\"{{commandName}}\" komutu zaten mevcut",
"create_command_failed": "Komut oluşturulamadı",
"command_template_content": "Bu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.",
"command_template_content": "---\ndescription: \"Bu komutun ne yaptığının kısa açıklaması\"\n---\n\nBu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.",
"claudeCode": {
"processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.",
"errorOutput": "Hata çıktısı: {{output}}",

View file

@ -77,7 +77,7 @@
"no_workspace_for_project_command": "Không tìm thấy thư mục workspace cho lệnh dự án",
"command_already_exists": "Lệnh \"{{commandName}}\" đã tồn tại",
"create_command_failed": "Không thể tạo lệnh",
"command_template_content": "Đây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.",
"command_template_content": "---\ndescription: \"Mô tả ngắn gọn về chức năng của lệnh này\"\n---\n\nĐây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.",
"claudeCode": {
"processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.",
"errorOutput": "Đầu ra lỗi: {{output}}",

View file

@ -82,7 +82,7 @@
"no_workspace_for_project_command": "未找到项目命令的工作区文件夹",
"command_already_exists": "命令 \"{{commandName}}\" 已存在",
"create_command_failed": "创建命令失败",
"command_template_content": "这是一个新的斜杠命令。编辑此文件以自定义命令行为。",
"command_template_content": "---\ndescription: \"此命令功能的简要描述\"\n---\n\n这是一个新的斜杠命令。编辑此文件以自定义命令行为。",
"claudeCode": {
"processExited": "Claude Code 进程退出,退出码:{{exitCode}}。",
"errorOutput": "错误输出:{{output}}",

View file

@ -76,7 +76,7 @@
"no_workspace_for_project_command": "找不到專案指令的工作區資料夾",
"command_already_exists": "指令 \"{{commandName}}\" 已存在",
"create_command_failed": "建立指令失敗",
"command_template_content": "這是一個新的斜線指令。編輯此檔案以自訂指令行為。",
"command_template_content": "---\ndescription: \"此指令功能的簡要描述\"\n---\n\n這是一個新的斜線指令。編輯此檔案以自訂指令行為。",
"claudeCode": {
"processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。",
"errorOutput": "錯誤輸出:{{output}}",

View file

@ -442,6 +442,7 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"google-auth-library": "^9.15.1",
"gray-matter": "^4.0.3",
"i18next": "^25.0.0",
"ignore": "^7.0.3",
"isbinaryfile": "^5.0.2",

View file

@ -0,0 +1,231 @@
import { describe, it, expect, beforeEach, vi } from "vitest"
import fs from "fs/promises"
import * as path from "path"
import { getCommand, getCommands } from "../commands"
// Mock fs and path modules
vi.mock("fs/promises")
vi.mock("../roo-config", () => ({
getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"),
getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"),
}))
const mockFs = vi.mocked(fs)
describe("Command loading with frontmatter", () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe("getCommand with frontmatter", () => {
it("should load command with description from frontmatter", async () => {
const commandContent = `---
description: Sets up the development environment
author: John Doe
---
# Setup Command
Run the following commands:
\`\`\`bash
npm install
npm run build
\`\`\``
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
const result = await getCommand("/test/cwd", "setup")
expect(result).toEqual({
name: "setup",
content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```",
source: "project",
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: "Sets up the development environment",
})
})
it("should load command without frontmatter", async () => {
const commandContent = `# Setup Command
Run the following commands:
\`\`\`bash
npm install
npm run build
\`\`\``
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
const result = await getCommand("/test/cwd", "setup")
expect(result).toEqual({
name: "setup",
content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```",
source: "project",
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: undefined,
})
})
it("should handle empty description in frontmatter", async () => {
const commandContent = `---
description: ""
author: John Doe
---
# Setup Command
Command content here.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
const result = await getCommand("/test/cwd", "setup")
expect(result?.description).toBeUndefined()
})
it("should handle malformed frontmatter gracefully", async () => {
const commandContent = `---
description: Test
invalid: yaml: [
---
# Setup Command
Command content here.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi.fn().mockResolvedValue(commandContent)
const result = await getCommand("/test/cwd", "setup")
expect(result).toEqual({
name: "setup",
content: commandContent.trim(),
source: "project",
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: undefined,
})
})
it("should prioritize project commands over global commands", async () => {
const projectCommandContent = `---
description: Project-specific setup
---
# Project Setup
Project-specific setup instructions.`
const globalCommandContent = `---
description: Global setup
---
# Global Setup
Global setup instructions.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi
.fn()
.mockResolvedValueOnce(projectCommandContent) // First call for project
.mockResolvedValueOnce(globalCommandContent) // Second call for global (shouldn't be used)
const result = await getCommand("/test/cwd", "setup")
expect(result).toEqual({
name: "setup",
content: "# Project Setup\n\nProject-specific setup instructions.",
source: "project",
filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"),
description: "Project-specific setup",
})
})
it("should fall back to global command if project command doesn't exist", async () => {
const globalCommandContent = `---
description: Global setup command
---
# Global Setup
Global setup instructions.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readFile = vi
.fn()
.mockRejectedValueOnce(new Error("File not found")) // Project command doesn't exist
.mockResolvedValueOnce(globalCommandContent) // Global command exists
const result = await getCommand("/test/cwd", "setup")
expect(result).toEqual({
name: "setup",
content: "# Global Setup\n\nGlobal setup instructions.",
source: "global",
filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")),
description: "Global setup command",
})
})
})
describe("getCommands with frontmatter", () => {
it("should load multiple commands with descriptions", async () => {
const setupContent = `---
description: Sets up the development environment
---
# Setup Command
Setup instructions.`
const deployContent = `---
description: Deploys the application to production
---
# Deploy Command
Deploy instructions.`
const buildContent = `# Build Command
Build instructions without frontmatter.`
mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true })
mockFs.readdir = vi.fn().mockResolvedValue([
{ name: "setup.md", isFile: () => true },
{ name: "deploy.md", isFile: () => true },
{ name: "build.md", isFile: () => true },
{ name: "not-markdown.txt", isFile: () => true }, // Should be ignored
])
mockFs.readFile = vi
.fn()
.mockResolvedValueOnce(setupContent)
.mockResolvedValueOnce(deployContent)
.mockResolvedValueOnce(buildContent)
const result = await getCommands("/test/cwd")
expect(result).toHaveLength(3)
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "setup",
description: "Sets up the development environment",
}),
expect.objectContaining({
name: "deploy",
description: "Deploys the application to production",
}),
expect.objectContaining({
name: "build",
description: undefined,
}),
]),
)
})
})
})

View file

@ -1,5 +1,6 @@
import fs from "fs/promises"
import * as path from "path"
import matter from "gray-matter"
import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config"
export interface Command {
@ -7,6 +8,7 @@ export interface Command {
content: string
source: "global" | "project"
filePath: string
description?: string
}
/**
@ -65,11 +67,31 @@ async function tryLoadCommand(
try {
const content = await fs.readFile(filePath, "utf-8")
let parsed
let description: string | undefined
let commandContent: string
try {
// Try to parse frontmatter with gray-matter
parsed = matter(content)
description =
typeof parsed.data.description === "string" && parsed.data.description.trim()
? parsed.data.description.trim()
: undefined
commandContent = parsed.content.trim()
} catch (frontmatterError) {
// If frontmatter parsing fails, treat the entire content as command content
description = undefined
commandContent = content.trim()
}
return {
name,
content: content.trim(),
content: commandContent,
source,
filePath,
description,
}
} catch (error) {
// File doesn't exist or can't be read
@ -113,13 +135,32 @@ async function scanCommandDirectory(
try {
const content = await fs.readFile(filePath, "utf-8")
let parsed
let description: string | undefined
let commandContent: string
try {
// Try to parse frontmatter with gray-matter
parsed = matter(content)
description =
typeof parsed.data.description === "string" && parsed.data.description.trim()
? parsed.data.description.trim()
: undefined
commandContent = parsed.content.trim()
} catch (frontmatterError) {
// If frontmatter parsing fails, treat the entire content as command content
description = undefined
commandContent = content.trim()
}
// Project commands override global ones
if (source === "project" || !commands.has(commandName)) {
commands.set(commandName, {
name: commandName,
content: content.trim(),
content: commandContent,
source,
filePath,
description,
})
}
} catch (error) {

View file

@ -24,6 +24,7 @@ export interface Command {
name: string
source: "global" | "project"
filePath?: string
description?: string
}
// Type for marketplace installed metadata

View file

@ -153,11 +153,13 @@ export function getContextMenuOptions(
type: ContextMenuOptionType.Command,
value: result.item.original.name,
slashCommand: `/${result.item.original.name}`,
description: result.item.original.description,
}))
: commands.map((command) => ({
type: ContextMenuOptionType.Command,
value: command.name,
slashCommand: `/${command.name}`,
description: command.description,
}))
if (matchingCommands.length > 0) {