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/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 diff --git a/src/core/command-generation/adapters/devin.ts b/src/core/command-generation/adapters/devin.ts new file mode 100644 index 000000000..17b86600e --- /dev/null +++ b/src/core/command-generation/adapters/devin.ts @@ -0,0 +1,69 @@ +/** + * 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 { transformToHyphenCommands } from '../../../utils/command-references.js'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; + +/** + * Escapes a string value for safe YAML output. + * 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 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}"`; + } + 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 + * + * Devin Desktop uses slash-hyphen syntax (/opsx-apply) instead of colon syntax (/opsx:apply). + */ +export const devinAdapter: ToolCommandAdapter = { + toolId: 'devin', + + getFilePath(commandId: string): string { + return path.join('.devin', 'workflows', `opsx-${commandId}.md`); + }, + + 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)} +category: ${escapeYamlValue(content.category)} +tags: ${formatTagsArray(content.tags)} +--- + +${transformedBody} +`; + }, +}; 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); 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' }, 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..e963f739c 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'); @@ -691,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 = [ 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', () => {