From a70283affb5d492cd25c56ce2bbcec5ded938293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Thu, 4 Jun 2026 16:21:16 +0300 Subject: [PATCH 1/7] feat(core): add codeassistant support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- docs/supported-tools.md | 3 +- .../adapters/codeassistant.ts | 30 +++++++++++++++++++ src/core/command-generation/adapters/index.ts | 1 + src/core/command-generation/registry.ts | 2 ++ src/core/config.ts | 1 + 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/core/command-generation/adapters/codeassistant.ts diff --git a/docs/supported-tools.md b/docs/supported-tools.md index b2ee30fb4..05e24fda6 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -47,6 +47,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch | Mistral Vibe (`vibe`) | `.vibe/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-.md` | | Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-.md` | +| SourceCraft Code Assistant (`codeassistant`) | `.codeassistant/skills/openspec-*/SKILL.md` | `.codeassistant/commands/opsx-.md` | | Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/.md` | | Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-.toml` | | RooCode (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-.md` | @@ -75,7 +76,7 @@ openspec init --tools none openspec init --profile core ``` -**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf` +**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `codeassistant`, `trae`, `vibe`, `windsurf` ## Workflow-Dependent Installation diff --git a/src/core/command-generation/adapters/codeassistant.ts b/src/core/command-generation/adapters/codeassistant.ts new file mode 100644 index 000000000..e490f9d94 --- /dev/null +++ b/src/core/command-generation/adapters/codeassistant.ts @@ -0,0 +1,30 @@ +/** + * SourceCraft Code Assistant Command Adapter + * + * Formats commands for SourceCraft Code Assistant following its frontmatter specification. + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +/** + * SourceCraft Code Assistant adapter for command generation. + * File path: .codeassistant/commands/opsx-.md + * Format: Markdown header with description + */ +export const codeassistantAdapter: ToolCommandAdapter = { + toolId: 'codeassistant', + + getFilePath(commandId: string): string { + return path.join('.codeassistant', 'commands', `opsx-${commandId}.md`); + }, + + formatFile(content: CommandContent): string { + return `# ${content.name} + +${content.description} + +${content.body} +`; + }, +}; diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 00fc75d5d..8c97471d6 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -25,6 +25,7 @@ export { kilocodeAdapter } from './kilocode.js'; export { kiroAdapter } from './kiro.js'; export { opencodeAdapter } from './opencode.js'; export { piAdapter } from './pi.js'; +export { codeassistantAdapter } from './codeassistant.js'; export { qoderAdapter } from './qoder.js'; export { lingmaAdapter } from './lingma.js'; export { qwenAdapter } from './qwen.js'; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index 3b726d707..53d800f8f 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -27,6 +27,7 @@ import { kilocodeAdapter } from './adapters/kilocode.js'; import { kiroAdapter } from './adapters/kiro.js'; import { opencodeAdapter } from './adapters/opencode.js'; import { piAdapter } from './adapters/pi.js'; +import { codeassistantAdapter } from './adapters/codeassistant.js'; import { qoderAdapter } from './adapters/qoder.js'; import { lingmaAdapter } from './adapters/lingma.js'; import { qwenAdapter } from './adapters/qwen.js'; @@ -62,6 +63,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(kiroAdapter); CommandAdapterRegistry.register(opencodeAdapter); CommandAdapterRegistry.register(piAdapter); + CommandAdapterRegistry.register(codeassistantAdapter); CommandAdapterRegistry.register(qoderAdapter); CommandAdapterRegistry.register(lingmaAdapter); CommandAdapterRegistry.register(qwenAdapter); diff --git a/src/core/config.ts b/src/core/config.ts index 3be428b26..d063a7940 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -44,6 +44,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' }, + { name: 'SourceCraft Code Assistant', value: 'codeassistant', available: true, successLabel: 'SourceCraft Code Assistant', skillsDir: '.codeassistant' }, { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' }, From fbe9b7eabfa63202d13e74946a744cb686a3fed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Mon, 8 Jun 2026 10:35:14 +0300 Subject: [PATCH 2/7] chore: change format file and add test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- .../command-generation/adapters/codeassistant.ts | 9 ++++++--- test/core/available-tools.test.ts | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/core/command-generation/adapters/codeassistant.ts b/src/core/command-generation/adapters/codeassistant.ts index e490f9d94..fbd87bc7b 100644 --- a/src/core/command-generation/adapters/codeassistant.ts +++ b/src/core/command-generation/adapters/codeassistant.ts @@ -6,6 +6,7 @@ import path from 'path'; import type { CommandContent, ToolCommandAdapter } from '../types.js'; +import { transformToHyphenCommands } from '../../../utils/command-references.js'; /** * SourceCraft Code Assistant adapter for command generation. @@ -20,11 +21,13 @@ export const codeassistantAdapter: ToolCommandAdapter = { }, formatFile(content: CommandContent): string { - return `# ${content.name} + const transformedBody = transformToHyphenCommands(content.body); -${content.description} + return `--- +description: ${content.description} +--- -${content.body} +${transformedBody} `; }, }; diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index 50d758070..de07a335c 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -157,11 +157,24 @@ describe('available-tools', () => { const tools = getAvailableTools(testDir); const toolValues = tools.map((t) => t.value); expect(toolValues).toContain('vibe'); - + const vibeTool = tools.find((t) => t.value === 'vibe'); expect(vibeTool).toBeDefined(); expect(vibeTool?.name).toBe('Mistral Vibe'); expect(vibeTool?.skillsDir).toBe('.vibe'); }); + + it('should detect SourceCraft Code Assistant when .codeassistant directory exists', async () => { + await fs.mkdir(path.join(testDir, '.codeassistant'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const toolValues = tools.map((t) => t.value); + expect(toolValues).toContain('codeassistant'); + + const vibeTool = tools.find((t) => t.value === 'codeassistant'); + expect(vibeTool).toBeDefined(); + expect(vibeTool?.name).toBe('SourceCraft Code Assistant'); + expect(vibeTool?.skillsDir).toBe('.codeassistant'); + }); }); }); From 2d363052da381b2c52180d70c7cbf7682efa0ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Wed, 10 Jun 2026 10:32:07 +0300 Subject: [PATCH 3/7] chore: add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- test/core/command-generation/adapters.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index b91dc024f..8603d7f2b 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -24,6 +24,7 @@ import { qoderAdapter } from '../../../src/core/command-generation/adapters/qode import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js'; import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js'; import { windsurfAdapter } from '../../../src/core/command-generation/adapters/windsurf.js'; +import { codeassistantAdapter } from '../../../src/core/command-generation/adapters/codeassistant.js'; import type { CommandContent } from '../../../src/core/command-generation/types.js'; describe('command-generation/adapters', () => { @@ -707,4 +708,29 @@ describe('command-generation/adapters', () => { } }); }); + + describe('codeassistantAdapter', () => { + it('should have correct toolId', () => { + expect(codeassistantAdapter.toolId).toBe('codeassistant'); + }); + + it('should generate correct file path', () => { + const filePath = codeassistantAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.codeassistant', 'commands', 'opsx-explore.md')); + }); + + it('should generate correct file path for different command IDs', () => { + expect(codeassistantAdapter.getFilePath('new')).toBe(path.join('.codeassistant', 'commands', 'opsx-new.md')); + expect(codeassistantAdapter.getFilePath('bulk-archive')).toBe(path.join('.codeassistant', 'commands', 'opsx-bulk-archive.md')); + }); + + it('should format file with correct YAML frontmatter', () => { + const output = codeassistantAdapter.formatFile(sampleContent); + + expect(output).toContain('---\n'); + expect(output).toContain('description: Enter explore mode for thinking'); + expect(output).toContain('---\n\n'); + expect(output).toContain('This is the command body.\n\nWith multiple lines.'); + }); + }); }); From 3fe9a033a3b4d46e7313a2a8f4b760242db276e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Fri, 12 Jun 2026 08:20:28 +0300 Subject: [PATCH 4/7] fix: escaped description yaml values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- .../adapters/codeassistant.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/command-generation/adapters/codeassistant.ts b/src/core/command-generation/adapters/codeassistant.ts index fbd87bc7b..5baf41604 100644 --- a/src/core/command-generation/adapters/codeassistant.ts +++ b/src/core/command-generation/adapters/codeassistant.ts @@ -8,6 +8,21 @@ import path from 'path'; import type { CommandContent, ToolCommandAdapter } from '../types.js'; import { transformToHyphenCommands } from '../../../utils/command-references.js'; +/** + * Escapes a string value for safe YAML output. + * Quotes the string if it contains special YAML characters. + */ +function escapeYamlValue(value: string): string { + // Check if value needs quoting (contains special YAML characters or starts/ends with whitespace) + const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); + if (needsQuoting) { + // Use double quotes and escape internal double quotes and backslashes + const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); + return `"${escaped}"`; + } + return value; +} + /** * SourceCraft Code Assistant adapter for command generation. * File path: .codeassistant/commands/opsx-.md @@ -24,7 +39,7 @@ export const codeassistantAdapter: ToolCommandAdapter = { const transformedBody = transformToHyphenCommands(content.body); return `--- -description: ${content.description} +description: ${escapeYamlValue(content.description)} --- ${transformedBody} From 6ecac19817dc7803a037fe9e4e6e1c0e66f7255c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Fri, 12 Jun 2026 08:23:06 +0300 Subject: [PATCH 5/7] chore: add test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- test/core/command-generation/adapters.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index 8603d7f2b..d4e729aa9 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -732,5 +732,14 @@ describe('command-generation/adapters', () => { expect(output).toContain('---\n\n'); expect(output).toContain('This is the command body.\n\nWith multiple lines.'); }); + + it('should escape YAML special characters in description', () => { + const contentWithSpecialChars: CommandContent = { + ...sampleContent, + description: 'Fix: regression in "auth" feature', + }; + const output = bobAdapter.formatFile(contentWithSpecialChars); + expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"'); + }); }); }); From 39fe298fd559039e49b551fdd92d360142d7ab71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Fri, 12 Jun 2026 22:53:11 +0300 Subject: [PATCH 6/7] chore: change adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- test/core/command-generation/adapters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index d4e729aa9..56acdbead 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -738,7 +738,7 @@ describe('command-generation/adapters', () => { ...sampleContent, description: 'Fix: regression in "auth" feature', }; - const output = bobAdapter.formatFile(contentWithSpecialChars); + const output = codeassistantAdapter.formatFile(contentWithSpecialChars); expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"'); }); }); From 18bb5c49839b194fb69cabd49533a6544565f78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=B5=D0=BD=D1=82=D1=8C=D0=B5=D0=B2?= Date: Fri, 12 Jun 2026 22:57:04 +0300 Subject: [PATCH 7/7] chore: handle \r in description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Александр Мелентьев --- src/core/command-generation/adapters/codeassistant.ts | 4 ++-- test/core/command-generation/adapters.test.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/command-generation/adapters/codeassistant.ts b/src/core/command-generation/adapters/codeassistant.ts index 5baf41604..b41c8a759 100644 --- a/src/core/command-generation/adapters/codeassistant.ts +++ b/src/core/command-generation/adapters/codeassistant.ts @@ -17,7 +17,7 @@ function escapeYamlValue(value: string): string { const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); if (needsQuoting) { // Use double quotes and escape internal double quotes and backslashes - const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); + const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r'); return `"${escaped}"`; } return value; @@ -26,7 +26,7 @@ function escapeYamlValue(value: string): string { /** * SourceCraft Code Assistant adapter for command generation. * File path: .codeassistant/commands/opsx-.md - * Format: Markdown header with description + * Format: YAML frontmatter with description */ export const codeassistantAdapter: ToolCommandAdapter = { toolId: 'codeassistant', diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index 56acdbead..84dc0dd7e 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -741,5 +741,14 @@ describe('command-generation/adapters', () => { const output = codeassistantAdapter.formatFile(contentWithSpecialChars); expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"'); }); + + it('should escape carriage return in description', () => { + const contentWithCR: CommandContent = { + ...sampleContent, + description: 'Line 1\rLine 2', + }; + const output = codeassistantAdapter.formatFile(contentWithCR); + expect(output).toContain('description: "Line 1\\rLine 2"'); + }); }); });