From e85f09bc4d1319f8ed3332135731c5c0fb9aea06 Mon Sep 17 00:00:00 2001 From: wei chen Date: Fri, 6 Feb 2026 18:02:22 +0800 Subject: [PATCH 1/3] feat: add support for the Trae tool adapter --- .gitignore | 3 ++ docs/supported-tools.md | 2 +- src/core/command-generation/adapters/index.ts | 1 + src/core/command-generation/adapters/trae.ts | 48 +++++++++++++++++++ src/core/command-generation/registry.ts | 2 + test/core/command-generation/adapters.test.ts | 42 +++++++++++++++- 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/core/command-generation/adapters/trae.ts diff --git a/.gitignore b/.gitignore index abea4d16e..42295fbd8 100644 --- a/.gitignore +++ b/.gitignore @@ -146,6 +146,9 @@ vite.config.ts.timestamp-* CLAUDE.md .DS_Store +#Trae +.trae/ + # Pnpm .pnpm-store/ result diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 2f9b61526..42a843f5c 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -33,7 +33,7 @@ For each tool you select, OpenSpec installs: | Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` | | Qwen Code | `.qwen/skills/` | `.qwen/commands/` | | RooCode | `.roo/skills/` | `.roo/commands/` | -| Trae | `.trae/skills/` | `.trae/skills/` (via `/openspec-*`) | +| Trae | `.trae/skills/` | `.trae/commands/opsx/` (via `/opsx:*`) | | Windsurf | `.windsurf/skills/` | `.windsurf/workflows/` | \* Codex commands are installed to the global home directory (`~/.codex/prompts/` or `$CODEX_HOME/prompts/`), not the project directory. diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 83de1f0d2..ad20eab57 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -24,4 +24,5 @@ export { opencodeAdapter } from './opencode.js'; export { qoderAdapter } from './qoder.js'; export { qwenAdapter } from './qwen.js'; export { roocodeAdapter } from './roocode.js'; +export { traeAdapter } from './trae.js'; export { windsurfAdapter } from './windsurf.js'; diff --git a/src/core/command-generation/adapters/trae.ts b/src/core/command-generation/adapters/trae.ts new file mode 100644 index 000000000..8a1191e80 --- /dev/null +++ b/src/core/command-generation/adapters/trae.ts @@ -0,0 +1,48 @@ +/** + * Trae Command Adapter + * + * Formats commands for Trae following its command frontmatter specification. + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +function escapeYamlValue(value: string): string { + const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); + if (needsQuoting) { + const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); + return `"${escaped}"`; + } + return value; +} + +function formatTagsArray(tags: string[]): string { + const escapedTags = tags.map((tag) => escapeYamlValue(tag)); + return `[${escapedTags.join(', ')}]`; +} + +/** + * Trae adapter for command generation. + * File path: .trae/commands/opsx/.md + * Frontmatter: name, description, category, tags + */ +export const traeAdapter: ToolCommandAdapter = { + toolId: 'trae', + + getFilePath(commandId: string): string { + return path.join('.trae', 'commands', 'opsx', `${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + return `--- +name: ${escapeYamlValue(content.name)} +description: ${escapeYamlValue(content.description)} +category: ${escapeYamlValue(content.category)} +tags: ${formatTagsArray(content.tags)} +--- + +${content.body} +`; + }, +}; + diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index f99edac61..0a7bd179f 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -26,6 +26,7 @@ import { opencodeAdapter } from './adapters/opencode.js'; import { qoderAdapter } from './adapters/qoder.js'; import { qwenAdapter } from './adapters/qwen.js'; import { roocodeAdapter } from './adapters/roocode.js'; +import { traeAdapter } from './adapters/trae.js'; import { windsurfAdapter } from './adapters/windsurf.js'; /** @@ -56,6 +57,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(qoderAdapter); CommandAdapterRegistry.register(qwenAdapter); CommandAdapterRegistry.register(roocodeAdapter); + CommandAdapterRegistry.register(traeAdapter); CommandAdapterRegistry.register(windsurfAdapter); } diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index 5341f6a25..835749c08 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -21,6 +21,7 @@ import { opencodeAdapter } from '../../../src/core/command-generation/adapters/o import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js'; import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js'; import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js'; +import { traeAdapter } from '../../../src/core/command-generation/adapters/trae.js'; import { windsurfAdapter } from '../../../src/core/command-generation/adapters/windsurf.js'; import type { CommandContent } from '../../../src/core/command-generation/types.js'; @@ -124,6 +125,40 @@ describe('command-generation/adapters', () => { }); }); + describe('traeAdapter', () => { + it('should have correct toolId', () => { + expect(traeAdapter.toolId).toBe('trae'); + }); + + it('should generate correct file path', () => { + const filePath = traeAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.trae', 'commands', 'opsx', 'explore.md')); + }); + + it('should generate correct file path for different command IDs', () => { + expect(traeAdapter.getFilePath('new')).toBe(path.join('.trae', 'commands', 'opsx', 'new.md')); + expect(traeAdapter.getFilePath('bulk-archive')).toBe(path.join('.trae', 'commands', 'opsx', 'bulk-archive.md')); + }); + + it('should format file with correct YAML frontmatter', () => { + const output = traeAdapter.formatFile(sampleContent); + + expect(output).toContain('---\n'); + expect(output).toContain('name: OpenSpec Explore'); + expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('category: Workflow'); + expect(output).toContain('tags: [workflow, explore, experimental]'); + expect(output).toContain('---\n\n'); + expect(output).toContain('This is the command body.\n\nWith multiple lines.'); + }); + + it('should handle empty tags', () => { + const contentNoTags: CommandContent = { ...sampleContent, tags: [] }; + const output = traeAdapter.formatFile(contentNoTags); + expect(output).toContain('tags: []'); + }); + }); + describe('amazonQAdapter', () => { it('should have correct toolId', () => { expect(amazonQAdapter.toolId).toBe('amazon-q'); @@ -560,6 +595,11 @@ describe('command-generation/adapters', () => { expect(filePath.split(path.sep)).toEqual(['.windsurf', 'workflows', 'opsx-test.md']); }); + it('Trae adapter uses path.join for paths', () => { + const filePath = traeAdapter.getFilePath('test'); + expect(filePath.split(path.sep)).toEqual(['.trae', 'commands', 'opsx', 'test.md']); + }); + it('All adapters use path.join for paths', () => { // Verify all adapters produce valid paths const adapters = [ @@ -567,7 +607,7 @@ describe('command-generation/adapters', () => { codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter, crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter, iflowAdapter, kilocodeAdapter, opencodeAdapter, qoderAdapter, - qwenAdapter, roocodeAdapter + qwenAdapter, roocodeAdapter, traeAdapter ]; for (const adapter of adapters) { const filePath = adapter.getFilePath('test'); From 5b12d063e58c84a0ab626c5f7780878e3d6c47fb Mon Sep 17 00:00:00 2001 From: wei chen Date: Sat, 7 Feb 2026 16:10:32 +0800 Subject: [PATCH 2/3] style: Standardize the comment format in the .gitignore file --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 42295fbd8..976ad4854 100644 --- a/.gitignore +++ b/.gitignore @@ -146,7 +146,7 @@ vite.config.ts.timestamp-* CLAUDE.md .DS_Store -#Trae +# Trae .trae/ # Pnpm From c9e3b5bf2e076becc716676fd44a87c3593034cd Mon Sep 17 00:00:00 2001 From: wei chen Date: Sat, 7 Feb 2026 16:33:51 +0800 Subject: [PATCH 3/3] docs(command-generation): Add escape function comments for YAML strings and arrays of tags. --- src/core/command-generation/adapters/trae.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/command-generation/adapters/trae.ts b/src/core/command-generation/adapters/trae.ts index 8a1191e80..7c923861c 100644 --- a/src/core/command-generation/adapters/trae.ts +++ b/src/core/command-generation/adapters/trae.ts @@ -7,6 +7,11 @@ import path from 'path'; import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +/** + * Escapes a string value for safe YAML output. + * Quotes the string if it contains special YAML characters. + */ function escapeYamlValue(value: string): string { const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); if (needsQuoting) { @@ -16,6 +21,9 @@ function escapeYamlValue(value: string): string { return value; } +/** + * Formats a tags array as a YAML array with proper escaping. + */ function formatTagsArray(tags: string[]): string { const escapedTags = tags.map((tag) => escapeYamlValue(tag)); return `[${escapedTags.join(', ')}]`;