From e0d01c16def757b779e0a7a8dd648452ee739710 Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Thu, 4 Jun 2026 11:56:58 +0200 Subject: [PATCH 1/6] proposal: add devin desktop support --- .../add-devin-desktop-support/.openspec.yaml | 2 + .../add-devin-desktop-support/proposal.md | 33 +++++++ .../specs/cli-init/spec.md | 34 +++++++ .../specs/cli-update/spec.md | 19 ++++ .../add-devin-desktop-support/tasks.md | 92 +++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 openspec/changes/add-devin-desktop-support/.openspec.yaml create mode 100644 openspec/changes/add-devin-desktop-support/proposal.md create mode 100644 openspec/changes/add-devin-desktop-support/specs/cli-init/spec.md create mode 100644 openspec/changes/add-devin-desktop-support/specs/cli-update/spec.md create mode 100644 openspec/changes/add-devin-desktop-support/tasks.md diff --git a/openspec/changes/add-devin-desktop-support/.openspec.yaml b/openspec/changes/add-devin-desktop-support/.openspec.yaml new file mode 100644 index 000000000..f617bd186 --- /dev/null +++ b/openspec/changes/add-devin-desktop-support/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-04 diff --git a/openspec/changes/add-devin-desktop-support/proposal.md b/openspec/changes/add-devin-desktop-support/proposal.md new file mode 100644 index 000000000..139a77cb8 --- /dev/null +++ b/openspec/changes/add-devin-desktop-support/proposal.md @@ -0,0 +1,33 @@ +## Why + +- Windsurf has been rebranded to **Devin Desktop**, the new flagship AI coding assistant from Cognition. Users who previously used Windsurf are now transitioning to Devin Desktop. +- Devin Desktop uses the same workflow system as Windsurf (Cascade workflows stored in `.devin/workflows/`), making it a natural migration path for existing OpenSpec users. +- OpenSpec currently supports Windsurf but not Devin Desktop. Adding Devin Desktop support ensures users can continue using OpenSpec with their new tool without manual migration. +- The adapter pattern is already established and proven with Windsurf; extending it to Devin Desktop is straightforward and maintains consistency across the tool ecosystem. + +## What Changes + +- Add **Devin Desktop** (`devin`) to the CLI tool picker (`openspec init`) so users can select it during setup. +- Create a new **Devin adapter** (`src/core/command-generation/adapters/devin.ts`) that generates commands in `.devin/workflows/opsx-.md` with the same frontmatter structure as Windsurf. +- Register the Devin adapter in the command adapter registry (`src/core/command-generation/registry.ts`) and export it from the adapters index. +- Update `docs/supported-tools.md` to include Devin Desktop in the tool reference table. +- Ensure `openspec update` refreshes existing Devin workflows in-place, mirroring current behavior for other editors. +- Extend unit tests for init/update to cover Devin Desktop generation and updates. +- Update CLI prompts and documentation to advertise Devin Desktop support. + +## Impact + +- **Specs:** `cli-init`, `cli-update`, `command-generation` +- **Code:** + - `src/core/command-generation/adapters/devin.ts` (new adapter) + - `src/core/command-generation/registry.ts` (register adapter) + - `src/core/command-generation/adapters/index.ts` (export adapter) + - CLI tool selection logic +- **Docs:** `docs/supported-tools.md` +- **Tests:** init/update integration coverage for Devin Desktop workflows + +## Notes + +- This is a **migration enabler** for existing Windsurf users transitioning to Devin Desktop. +- Windsurf support can remain in place for backward compatibility with users still on Windsurf. +- The implementation closely mirrors the existing Windsurf adapter, reducing complexity and risk. diff --git a/openspec/changes/add-devin-desktop-support/specs/cli-init/spec.md b/openspec/changes/add-devin-desktop-support/specs/cli-init/spec.md new file mode 100644 index 000000000..3bc56d294 --- /dev/null +++ b/openspec/changes/add-devin-desktop-support/specs/cli-init/spec.md @@ -0,0 +1,34 @@ +## MODIFIED Requirements + +### Requirement: AI Tool Configuration +The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system. + +#### Scenario: Prompting for AI tool selection +- **WHEN** run interactively +- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu +- **AND** list every available tool with a checkbox: + - Claude Code (creates or refreshes CLAUDE.md and slash commands) + - Cursor (creates or refreshes `.cursor/commands/*` slash commands) + - OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands) + - Devin Desktop (creates or refreshes `.devin/workflows/opsx-*.md` workflows) + - Windsurf (creates or refreshes `.windsurf/workflows/opsx-*.md` workflows) + - AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers) +- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content +- **AND** treat disabled tools as "coming soon" and keep them unselectable +- **AND** allow confirming with Enter after selecting one or more tools + +### Requirement: Slash Command Configuration +The init command SHALL generate slash command files for supported editors using shared templates. + +#### Scenario: Generating workflows for Devin Desktop +- **WHEN** the user selects Devin Desktop during initialization +- **THEN** create `.devin/workflows/opsx-propose.md`, `.devin/workflows/opsx-apply.md`, and `.devin/workflows/opsx-archive.md` +- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage +- **AND** use the same frontmatter structure as Windsurf (name, description, category, tags) + +#### Scenario: Generating workflows for Windsurf +- **WHEN** the user selects Windsurf during initialization +- **THEN** create `.windsurf/workflows/opsx-propose.md`, `.windsurf/workflows/opsx-apply.md`, and `.windsurf/workflows/opsx-archive.md` +- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage diff --git a/openspec/changes/add-devin-desktop-support/specs/cli-update/spec.md b/openspec/changes/add-devin-desktop-support/specs/cli-update/spec.md new file mode 100644 index 000000000..83fb4686f --- /dev/null +++ b/openspec/changes/add-devin-desktop-support/specs/cli-update/spec.md @@ -0,0 +1,19 @@ +## MODIFIED Requirements + +### Requirement: Slash Command Updates +The update command SHALL refresh existing slash command files for configured tools without creating new ones. + +#### Scenario: Updating workflows for Devin Desktop +- **WHEN** `.devin/workflows/` contains `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md` +- **THEN** refresh each file using shared templates wrapped in OpenSpec markers +- **AND** ensure templates include instructions for the relevant workflow stage +- **AND** preserve the frontmatter structure (name, description, category, tags) + +#### Scenario: Updating workflows for Windsurf +- **WHEN** `.windsurf/workflows/` contains `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md` +- **THEN** refresh each file using shared templates wrapped in OpenSpec markers +- **AND** ensure templates include instructions for the relevant workflow stage + +#### Scenario: Missing workflow file +- **WHEN** a tool lacks a workflow file +- **THEN** do not create a new file during update diff --git a/openspec/changes/add-devin-desktop-support/tasks.md b/openspec/changes/add-devin-desktop-support/tasks.md new file mode 100644 index 000000000..72b954e18 --- /dev/null +++ b/openspec/changes/add-devin-desktop-support/tasks.md @@ -0,0 +1,92 @@ +# Implementation Tasks + +## 1. Create Devin Desktop Adapter + +### 1.1 Create adapter file +- Create `src/core/command-generation/adapters/devin.ts` +- Base implementation on the existing Windsurf adapter (`src/core/command-generation/adapters/windsurf.ts`) +- Use `.devin/workflows/` as the target directory +- Use `opsx-.md` as the filename pattern +- Include frontmatter with: name, description, category, tags + +### 1.2 Implement adapter interface +- Export `devinAdapter` object implementing `ToolCommandAdapter` +- Set `toolId` to `'devin'` +- Implement `getFilePath()` to return `.devin/workflows/opsx-.md` +- Implement `formatFile()` to generate YAML frontmatter + body content + +## 2. Register Adapter + +### 2.1 Update registry +- Edit `src/core/command-generation/registry.ts` +- Import the new `devinAdapter` +- Register it in the static initializer: `CommandAdapterRegistry.register(devinAdapter)` + +### 2.2 Export adapter +- Edit `src/core/command-generation/adapters/index.ts` +- Add export: `export { devinAdapter } from './devin.js'` +- Update main index if needed: `src/core/command-generation/index.ts` + +## 3. Update CLI Tool Selection + +### 3.1 Add Devin Desktop to tool picker +- Locate CLI initialization code that prompts for tool selection +- Add "Devin Desktop" option to the multi-select menu +- Ensure it appears alongside Windsurf and other tools +- Map selection to `devin` tool ID + +## 4. Update Documentation + +### 4.1 Update supported tools reference +- Edit `docs/supported-tools.md` +- Add Devin Desktop row to the tool directory reference table +- Include: + - Tool name and ID: `Devin Desktop (devin)` + - Skills path: `.devin/skills/openspec-*/SKILL.md` + - Command path: `.devin/workflows/opsx-.md` +- Add `devin` to the available tool IDs list in the "Non-Interactive Setup" section + +### 4.2 Update README if needed +- Check if README mentions tool count or lists specific tools +- Update any references to reflect Devin Desktop support + +## 5. Add Tests + +### 5.1 Test adapter functionality +- Create or update tests for the Devin adapter +- Test `getFilePath()` returns correct path +- Test `formatFile()` generates valid YAML frontmatter +- Test cross-platform path handling (Windows, macOS, Linux) + +### 5.2 Test CLI integration +- Test `openspec init --tools devin` generates `.devin/workflows/` files +- Test `openspec update` refreshes existing Devin workflows +- Test that Devin Desktop appears in interactive tool selection +- Verify files are created with correct structure and content + +### 5.3 Test backward compatibility +- Ensure Windsurf adapter still works +- Verify both Devin and Windsurf can be selected together +- Test that existing Windsurf installations are not affected + +## 6. Verify and Polish + +### 6.1 Manual testing +- Run `openspec init` and select Devin Desktop +- Verify `.devin/workflows/` directory is created +- Check that workflow files have correct frontmatter and content +- Run `openspec update` and verify files are refreshed +- Test on Windows, macOS, and Linux if possible + +### 6.2 Code review checklist +- Adapter follows existing patterns (Windsurf, Cursor, Claude) +- No hardcoded paths (use `path.join()`) +- YAML escaping handles special characters +- Error handling is consistent with other adapters +- Comments are clear and helpful + +### 6.3 Documentation review +- Supported tools table is accurate and complete +- Tool IDs are consistent across docs +- Examples show Devin Desktop usage +- Links and references are correct From a0a3fb6e670980ed586e9db8f41d25fe8d40d542 Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Thu, 4 Jun 2026 12:08:31 +0200 Subject: [PATCH 2/6] feat(adapters): add devin desktop command adapter - Create new Devin Desktop adapter for .devin/workflows/opsx-.md - Register adapter in CommandAdapterRegistry - Export adapter from adapters index - Update docs/supported-tools.md with Devin Desktop entry - Add 'devin' to available tool IDs list Devin Desktop uses the same Cascade workflow system as Windsurf, making it a natural migration path for existing users. --- docs/supported-tools.md | 3 +- src/core/command-generation/adapters/devin.ts | 57 +++++++++++++++++++ src/core/command-generation/adapters/index.ts | 1 + src/core/command-generation/registry.ts | 2 + 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/core/command-generation/adapters/devin.ts diff --git a/docs/supported-tools.md b/docs/supported-tools.md index b2ee30fb4..667f15049 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -30,6 +30,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch | Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-.md` | | CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/.md` | | Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-.md`\* | +| Devin Desktop (`devin`) | `.devin/skills/openspec-*/SKILL.md` | `.devin/workflows/opsx-.md` | | ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | Continue (`continue`) | `.continue/skills/openspec-*/SKILL.md` | `.continue/prompts/opsx-.prompt` | | CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/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`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf` ## Workflow-Dependent Installation diff --git a/src/core/command-generation/adapters/devin.ts b/src/core/command-generation/adapters/devin.ts new file mode 100644 index 000000000..a16d18a08 --- /dev/null +++ b/src/core/command-generation/adapters/devin.ts @@ -0,0 +1,57 @@ +/** + * Devin Desktop Command Adapter + * + * Formats commands for Devin Desktop following its frontmatter specification. + * Devin Desktop uses the same Cascade workflow system as Windsurf. + */ + +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 { + // 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; +} + +/** + * 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(', ')}]`; +} + +/** + * Devin Desktop adapter for command generation. + * File path: .devin/workflows/opsx-.md + * Frontmatter: name, description, category, tags + */ +export const devinAdapter: ToolCommandAdapter = { + toolId: 'devin', + + getFilePath(commandId: string): string { + return path.join('.devin', 'workflows', `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/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 00fc75d5d..97015d3db 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -11,6 +11,7 @@ export { bobAdapter } from './bob.js'; export { claudeAdapter } from './claude.js'; export { clineAdapter } from './cline.js'; export { codexAdapter } from './codex.js'; +export { devinAdapter } from './devin.js'; export { codebuddyAdapter } from './codebuddy.js'; export { continueAdapter } from './continue.js'; export { costrictAdapter } from './costrict.js'; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index 3b726d707..69a12f0fb 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -13,6 +13,7 @@ import { bobAdapter } from './adapters/bob.js'; import { claudeAdapter } from './adapters/claude.js'; import { clineAdapter } from './adapters/cline.js'; import { codexAdapter } from './adapters/codex.js'; +import { devinAdapter } from './adapters/devin.js'; import { codebuddyAdapter } from './adapters/codebuddy.js'; import { continueAdapter } from './adapters/continue.js'; import { costrictAdapter } from './adapters/costrict.js'; @@ -48,6 +49,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(claudeAdapter); CommandAdapterRegistry.register(clineAdapter); CommandAdapterRegistry.register(codexAdapter); + CommandAdapterRegistry.register(devinAdapter); CommandAdapterRegistry.register(codebuddyAdapter); CommandAdapterRegistry.register(continueAdapter); CommandAdapterRegistry.register(costrictAdapter); From 514699e569d2e98b76340e091d16119dc2fbe3a4 Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Thu, 4 Jun 2026 12:35:53 +0200 Subject: [PATCH 3/6] fix(config): add devin desktop to AI_TOOLS Add Devin Desktop entry to AI_TOOLS configuration so that: - getToolsWithSkillsDir() includes 'devin' as a valid tool ID - getWorkspaceSkillToolIds() returns 'devin' in the list - parseWorkspaceSkillToolsValue() accepts 'devin' as valid input - openspec init --tools devin works correctly This fixes validation failures where 'devin' was documented in docs/supported-tools.md but not recognized by validation functions that derive valid IDs from AI_TOOLS. --- src/core/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/config.ts b/src/core/config.ts index 3be428b26..29182655c 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -26,6 +26,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' }, { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' }, { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' }, + { name: 'Devin Desktop', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin' }, { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' }, { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' }, { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' }, From fd974e18192cae9af2eee8a079ece985d624596c Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Thu, 4 Jun 2026 12:38:15 +0200 Subject: [PATCH 4/6] fix(devin-adapter): escape implicit YAML scalars in frontmatter Update escapeYamlValue to detect and quote implicit YAML scalars that would be coerced by parsers: - Booleans: true, false, yes, no, on, off - Null variants: null, ~ - Numbers: integers, floats, exponentials, hex (0x), octal (0o) - Edge cases: standalone dash (-) and dot (.) This ensures values like 'true', '123', 'null' remain strings in YAML frontmatter instead of being interpreted as booleans, numbers, or nulls. Preserves existing escaping logic for special characters and newlines. --- src/core/command-generation/adapters/devin.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/core/command-generation/adapters/devin.ts b/src/core/command-generation/adapters/devin.ts index a16d18a08..33ceba6d6 100644 --- a/src/core/command-generation/adapters/devin.ts +++ b/src/core/command-generation/adapters/devin.ts @@ -10,12 +10,18 @@ import type { CommandContent, ToolCommandAdapter } from '../types.js'; /** * Escapes a string value for safe YAML output. - * Quotes the string if it contains special YAML characters. + * Quotes the string if it contains special YAML characters or would be + * interpreted as an implicit YAML scalar (boolean, null, number, etc). */ 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) { + // Check if value needs quoting due to special YAML characters or whitespace + const hasSpecialChars = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value); + + // Check if value would be interpreted as an implicit YAML scalar + // Matches: booleans (true/false/yes/no/on/off), null variants, numbers, hex/octal + const isImplicitScalar = /^(true|false|yes|no|on|off|null|~|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|0x[0-9a-fA-F]+|0o[0-7]+|-|\.?)$/.test(value); + + if (hasSpecialChars || isImplicitScalar) { // Use double quotes and escape internal double quotes and backslashes const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); return `"${escaped}"`; From 9a8befa2025066c7860bf0ccb1bfdc696dc75672 Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Sat, 6 Jun 2026 18:31:41 +0200 Subject: [PATCH 5/6] test(devin-adapter): add comprehensive tests for Devin Desktop adapter Add test coverage for the Devin Desktop adapter including: - Command reference transformation from colon to hyphen syntax - YAML frontmatter escaping for special characters and implicit scalars - File path generation for workflows - Integration with available tools detection - Init and update command workflows --- src/core/command-generation/adapters/devin.ts | 8 ++- test/core/available-tools.test.ts | 13 ++++ test/core/command-generation/adapters.test.ts | 71 +++++++++++++++++++ test/core/command-generation/registry.test.ts | 12 +++- test/core/init.test.ts | 15 ++++ test/core/update.test.ts | 27 +++++++ 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/core/command-generation/adapters/devin.ts b/src/core/command-generation/adapters/devin.ts index 33ceba6d6..17b86600e 100644 --- a/src/core/command-generation/adapters/devin.ts +++ b/src/core/command-generation/adapters/devin.ts @@ -6,6 +6,7 @@ */ import path from 'path'; +import { transformToHyphenCommands } from '../../../utils/command-references.js'; import type { CommandContent, ToolCommandAdapter } from '../types.js'; /** @@ -41,6 +42,8 @@ function formatTagsArray(tags: string[]): string { * Devin Desktop adapter for command generation. * File path: .devin/workflows/opsx-.md * Frontmatter: name, description, category, tags + * + * Devin Desktop uses slash-hyphen syntax (/opsx-apply) instead of colon syntax (/opsx:apply). */ export const devinAdapter: ToolCommandAdapter = { toolId: 'devin', @@ -50,6 +53,9 @@ export const devinAdapter: ToolCommandAdapter = { }, formatFile(content: CommandContent): string { + // Transform command references from colon to hyphen syntax + const transformedBody = transformToHyphenCommands(content.body); + return `--- name: ${escapeYamlValue(content.name)} description: ${escapeYamlValue(content.description)} @@ -57,7 +63,7 @@ category: ${escapeYamlValue(content.category)} tags: ${formatTagsArray(content.tags)} --- -${content.body} +${transformedBody} `; }, }; diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index 50d758070..b23121a42 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -46,6 +46,19 @@ describe('available-tools', () => { expect(tools).toHaveLength(3); }); + it('should detect Devin Desktop when .devin directory exists', async () => { + await fs.mkdir(path.join(testDir, '.devin'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const toolValues = tools.map((t) => t.value); + expect(toolValues).toContain('devin'); + + const devinTool = tools.find((t) => t.value === 'devin'); + expect(devinTool).toBeDefined(); + expect(devinTool?.name).toBe('Devin Desktop'); + expect(devinTool?.skillsDir).toBe('.devin'); + }); + it('should ignore files that are not directories', async () => { // Create a file named .claude instead of a directory await fs.writeFile(path.join(testDir, '.claude'), 'not a directory'); diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index b91dc024f..09684f910 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -13,6 +13,7 @@ import { continueAdapter } from '../../../src/core/command-generation/adapters/c import { costrictAdapter } from '../../../src/core/command-generation/adapters/costrict.js'; import { crushAdapter } from '../../../src/core/command-generation/adapters/crush.js'; import { cursorAdapter } from '../../../src/core/command-generation/adapters/cursor.js'; +import { devinAdapter } from '../../../src/core/command-generation/adapters/devin.js'; import { factoryAdapter } from '../../../src/core/command-generation/adapters/factory.js'; import { geminiAdapter } from '../../../src/core/command-generation/adapters/gemini.js'; import { githubCopilotAdapter } from '../../../src/core/command-generation/adapters/github-copilot.js'; @@ -126,6 +127,76 @@ describe('command-generation/adapters', () => { }); }); + describe('devinAdapter', () => { + it('should have correct toolId', () => { + expect(devinAdapter.toolId).toBe('devin'); + }); + + it('should generate correct file path', () => { + const filePath = devinAdapter.getFilePath('explore'); + expect(filePath).toBe(path.join('.devin', 'workflows', 'opsx-explore.md')); + }); + + it('should generate correct file paths for different commands', () => { + expect(devinAdapter.getFilePath('new')).toBe(path.join('.devin', 'workflows', 'opsx-new.md')); + expect(devinAdapter.getFilePath('bulk-archive')).toBe(path.join('.devin', 'workflows', 'opsx-bulk-archive.md')); + }); + + it('should format file with YAML frontmatter', () => { + const output = devinAdapter.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.'); + }); + + it('should transform colon command references to hyphen format', () => { + const contentWithRefs: CommandContent = { + ...sampleContent, + body: 'Run /opsx:apply to implement. Then use /opsx:verify.', + }; + const output = devinAdapter.formatFile(contentWithRefs); + expect(output).toContain('/opsx-apply'); + expect(output).toContain('/opsx-verify'); + expect(output).not.toContain('/opsx:apply'); + expect(output).not.toContain('/opsx:verify'); + }); + + it('should escape YAML special characters in frontmatter', () => { + const contentWithSpecialChars: CommandContent = { + ...sampleContent, + name: 'Test: Command', + description: 'Fix "auth" feature', + }; + const output = devinAdapter.formatFile(contentWithSpecialChars); + expect(output).toContain('name: "Test: Command"'); + expect(output).toContain('description: "Fix \\"auth\\" feature"'); + }); + + it('should escape implicit YAML scalars in frontmatter', () => { + const contentWithImplicitScalar: CommandContent = { + ...sampleContent, + name: 'true', + description: 'null', + category: 'on', + }; + const output = devinAdapter.formatFile(contentWithImplicitScalar); + expect(output).toContain('name: "true"'); + expect(output).toContain('description: "null"'); + expect(output).toContain('category: "on"'); + }); + + it('should handle empty tags', () => { + const contentNoTags: CommandContent = { ...sampleContent, tags: [] }; + const output = devinAdapter.formatFile(contentNoTags); + expect(output).toContain('tags: []'); + }); + }); + describe('amazonQAdapter', () => { it('should have correct toolId', () => { expect(amazonQAdapter.toolId).toBe('amazon-q'); diff --git a/test/core/command-generation/registry.test.ts b/test/core/command-generation/registry.test.ts index 14165ff51..6a665a005 100644 --- a/test/core/command-generation/registry.test.ts +++ b/test/core/command-generation/registry.test.ts @@ -21,6 +21,12 @@ describe('command-generation/registry', () => { expect(adapter?.toolId).toBe('windsurf'); }); + it('should return Devin adapter for "devin"', () => { + const adapter = CommandAdapterRegistry.get('devin'); + expect(adapter).toBeDefined(); + expect(adapter?.toolId).toBe('devin'); + }); + it('should return Junie adapter for "junie"', () => { const adapter = CommandAdapterRegistry.get('junie'); expect(adapter).toBeDefined(); @@ -45,13 +51,14 @@ describe('command-generation/registry', () => { expect(adapters.length).toBeGreaterThanOrEqual(3); // At least Claude, Cursor, Windsurf }); - it('should include Claude, Cursor, and Windsurf adapters', () => { + it('should include Claude, Cursor, Windsurf, and Devin adapters', () => { const adapters = CommandAdapterRegistry.getAll(); const toolIds = adapters.map((a) => a.toolId); expect(toolIds).toContain('claude'); expect(toolIds).toContain('cursor'); expect(toolIds).toContain('windsurf'); + expect(toolIds).toContain('devin'); }); }); @@ -60,6 +67,7 @@ describe('command-generation/registry', () => { expect(CommandAdapterRegistry.has('claude')).toBe(true); expect(CommandAdapterRegistry.has('cursor')).toBe(true); expect(CommandAdapterRegistry.has('windsurf')).toBe(true); + expect(CommandAdapterRegistry.has('devin')).toBe(true); expect(CommandAdapterRegistry.has('junie')).toBe(true); }); @@ -74,10 +82,12 @@ describe('command-generation/registry', () => { const claudeAdapter = CommandAdapterRegistry.get('claude'); const cursorAdapter = CommandAdapterRegistry.get('cursor'); const windsurfAdapter = CommandAdapterRegistry.get('windsurf'); + const devinAdapter = CommandAdapterRegistry.get('devin'); expect(claudeAdapter?.getFilePath('test')).toContain('.claude'); expect(cursorAdapter?.getFilePath('test')).toContain('.cursor'); expect(windsurfAdapter?.getFilePath('test')).toContain('.windsurf'); + expect(devinAdapter?.getFilePath('test')).toContain('.devin'); }); it('registered adapters should have working formatFile', () => { diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6a436eaed..1f3b8af57 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -457,6 +457,21 @@ describe('InitCommand', () => { expect(await fileExists(cmdFile)).toBe(true); }); + it('should generate Devin Desktop workflows', async () => { + const initCommand = new InitCommand({ tools: 'devin', force: true }); + await initCommand.execute(testDir); + + const cmdFile = path.join(testDir, '.devin', 'workflows', 'opsx-explore.md'); + expect(await fileExists(cmdFile)).toBe(true); + + const content = await fs.readFile(cmdFile, 'utf-8'); + expect(content).toContain('---'); + expect(content).toContain('name:'); + expect(content).toContain('description:'); + // Verify command references are transformed to hyphen syntax + expect(content).not.toContain('/opsx:'); + }); + it('should generate Continue prompt files', async () => { const initCommand = new InitCommand({ tools: 'continue', force: true }); await initCommand.execute(testDir); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index ea7f66a7e..4e450b8c3 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -251,6 +251,33 @@ Old instructions content } }); + it('should update Devin Desktop workflows with hyphen command references', async () => { + // Set up Devin Desktop directory with a skill to indicate it's configured + const skillsDir = path.join(testDir, '.devin', 'skills'); + await fs.mkdir(path.join(skillsDir, 'openspec-explore'), { + recursive: true, + }); + await fs.writeFile( + path.join(skillsDir, 'openspec-explore', 'SKILL.md'), + 'old content' + ); + + await updateCommand.execute(testDir); + + // Verify workflows were created + const workflowsDir = path.join(testDir, '.devin', 'workflows'); + const exploreWorkflow = path.join(workflowsDir, 'opsx-explore.md'); + const exists = await FileSystemUtils.fileExists(exploreWorkflow); + expect(exists).toBe(true); + + const content = await fs.readFile(exploreWorkflow, 'utf-8'); + expect(content).toContain('---'); + expect(content).toContain('name:'); + expect(content).toContain('description:'); + // Verify command references are transformed to hyphen syntax + expect(content).not.toContain('/opsx:'); + }); + }); describe('multi-tool support', () => { From cbb8301dae942077c6ee73cd17b9c82b5da3bd9b Mon Sep 17 00:00:00 2001 From: Mehdi Shahdoost Date: Sat, 6 Jun 2026 18:47:47 +0200 Subject: [PATCH 6/6] Add cross-platform testcase. --- test/core/command-generation/adapters.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index 09684f910..e963f739c 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -762,6 +762,11 @@ describe('command-generation/adapters', () => { expect(filePath.split(path.sep)).toEqual(['.windsurf', 'workflows', 'opsx-test.md']); }); + it('Devin adapter uses path.join for paths', () => { + const filePath = devinAdapter.getFilePath('test'); + expect(filePath.split(path.sep)).toEqual(['.devin', 'workflows', 'opsx-test.md']); + }); + it('All adapters use path.join for paths', () => { // Verify all adapters produce valid paths const adapters = [