From 2f7d2b5a817cd166aafdb44e4aba01dbe65a7bd3 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Thu, 4 Dec 2025 21:48:49 +0800 Subject: [PATCH 1/3] feat: add Neovate Code support with slash command integration --- README.md | 1 + src/core/config.ts | 1 + src/core/configurators/slash/neovate.ts | 78 ++++++++++++++++++++++++ src/core/configurators/slash/registry.ts | 3 + 4 files changed, 83 insertions(+) create mode 100644 src/core/configurators/slash/neovate.ts diff --git a/README.md b/README.md index 7b6c7354..759de4ea 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe | **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) | | **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) | | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) | +| **Neovate Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`..neovate/commands/openspec/`) — see [docs](https://neovateai.dev/en) | Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`. diff --git a/src/core/config.ts b/src/core/config.ts index c0c3da9e..e1312e5d 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -37,5 +37,6 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' }, { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' }, { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' }, + { name: 'Neovate Code', value: 'neovate', available: true, successLabel: 'Neovate Code' }, { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/configurators/slash/neovate.ts b/src/core/configurators/slash/neovate.ts new file mode 100644 index 00000000..810fa4a7 --- /dev/null +++ b/src/core/configurators/slash/neovate.ts @@ -0,0 +1,78 @@ +import { SlashCommandConfigurator } from './base.js'; +import { SlashCommandId } from '../../templates/index.js'; + +/** + * File paths for Neovate slash commands + * Maps each OpenSpec workflow stage to its command file location + * Commands are stored in .neovate/commands/openspec/ directory + */ +const FILE_PATHS: Record = { + // Create and validate new change proposals + proposal: '.neovate/commands/openspec/proposal.md', + + // Implement approved changes with task tracking + apply: '.neovate/commands/openspec/apply.md', + + // Archive completed changes and update specs + archive: '.neovate/commands/openspec/archive.md' +}; + +/** + * YAML frontmatter for Neovate slash commands + * Defines metadata displayed in Neovate's command palette + * Each command is categorized and tagged for easy discovery + */ +const FRONTMATTER: Record = { + proposal: `--- +name: Proposal +description: Scaffold a new OpenSpec change and validate strictly. +---`, + apply: `--- +name: Apply +description: Implement an approved OpenSpec change and keep tasks in sync. +---`, + archive: `--- +name: Archive +description: Archive a deployed OpenSpec change and update specs. +---` +}; + +/** + * Neovate Slash Command Configurator + * + * Manages OpenSpec slash commands for Neovate Code AI assistant. + * Creates three workflow commands: proposal, apply, and archive. + * Uses colon-separated command format (/openspec:proposal). + * + * @extends {SlashCommandConfigurator} + */ +export class NeovateSlashCommandConfigurator extends SlashCommandConfigurator { + /** Unique identifier for Neovate tool */ + readonly toolId = 'neovate'; + + /** Indicates slash commands are available for this tool */ + readonly isAvailable = true; + + /** + * Get relative file path for a slash command + * + * @param {SlashCommandId} id - Command identifier (proposal, apply, or archive) + * @returns {string} Relative path from project root to command file + */ + protected getRelativePath(id: SlashCommandId): string { + return FILE_PATHS[id]; + } + + /** + * Get YAML frontmatter for a slash command + * + * Frontmatter defines how the command appears in Neovate's UI, + * including display name, description, and categorization. + * + * @param {SlashCommandId} id - Command identifier (proposal, apply, or archive) + * @returns {string} YAML frontmatter block with command metadata + */ + protected getFrontmatter(id: SlashCommandId): string { + return FRONTMATTER[id]; + } +} diff --git a/src/core/configurators/slash/registry.ts b/src/core/configurators/slash/registry.ts index 8020940e..570033d2 100644 --- a/src/core/configurators/slash/registry.ts +++ b/src/core/configurators/slash/registry.ts @@ -2,6 +2,7 @@ import { SlashCommandConfigurator } from './base.js'; import { ClaudeSlashCommandConfigurator } from './claude.js'; import { CodeBuddySlashCommandConfigurator } from './codebuddy.js'; import { QoderSlashCommandConfigurator } from './qoder.js'; +import { NeovateSlashCommandConfigurator } from './neovate.js'; import { CursorSlashCommandConfigurator } from './cursor.js'; import { WindsurfSlashCommandConfigurator } from './windsurf.js'; import { KiloCodeSlashCommandConfigurator } from './kilocode.js'; @@ -27,6 +28,7 @@ export class SlashCommandRegistry { const claude = new ClaudeSlashCommandConfigurator(); const codeBuddy = new CodeBuddySlashCommandConfigurator(); const qoder = new QoderSlashCommandConfigurator(); + const neovate = new NeovateSlashCommandConfigurator(); const cursor = new CursorSlashCommandConfigurator(); const windsurf = new WindsurfSlashCommandConfigurator(); const kilocode = new KiloCodeSlashCommandConfigurator(); @@ -48,6 +50,7 @@ export class SlashCommandRegistry { this.configurators.set(claude.toolId, claude); this.configurators.set(codeBuddy.toolId, codeBuddy); this.configurators.set(qoder.toolId, qoder); + this.configurators.set(neovate.toolId, neovate); this.configurators.set(cursor.toolId, cursor); this.configurators.set(windsurf.toolId, windsurf); this.configurators.set(kilocode.toolId, kilocode); From f030ff86a304ac0da8333f84a47733ba780d2ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E5=B9=B3?= Date: Thu, 4 Dec 2025 22:00:13 +0800 Subject: [PATCH 2/3] chore: README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 759de4ea..1787d0e8 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe | **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) | | **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) | | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) | -| **Neovate Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`..neovate/commands/openspec/`) — see [docs](https://neovateai.dev/en) | +| **Neovate Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.neovate/commands/openspec/`) — see [docs](https://neovateai.dev/en) | Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`. From 9cb9e1a9a969b36945ca2da0a0efda2776436280 Mon Sep 17 00:00:00 2001 From: xierenhong Date: Thu, 4 Dec 2025 22:16:41 +0800 Subject: [PATCH 3/3] chore(test): add Neovate slash command file creation and refresh functionality --- test/core/init.test.ts | 54 +++++++++++++++++++++++++++++ test/core/update.test.ts | 74 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 7fdd5c4f..445132ef 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -1400,6 +1400,60 @@ describe('InitCommand', () => { expect(qoderChoice.configured).toBe(true); }); + it('should create Neovate slash command files with templates', async () => { + queueSelections('neovate', DONE); + + await initCommand.execute(testDir); + + const neovateProposal = path.join( + testDir, + '.neovate/commands/openspec/proposal.md' + ); + const neovateApply = path.join( + testDir, + '.neovate/commands/openspec/apply.md' + ); + const neovateArchive = path.join( + testDir, + '.neovate/commands/openspec/archive.md' + ); + + expect(await fileExists(neovateProposal)).toBe(true); + expect(await fileExists(neovateApply)).toBe(true); + expect(await fileExists(neovateArchive)).toBe(true); + + const proposalContent = await fs.readFile(neovateProposal, 'utf-8'); + expect(proposalContent).toContain('---'); + expect(proposalContent).toContain('name: Proposal'); + expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.'); + expect(proposalContent).toContain(''); + expect(proposalContent).toContain('**Guardrails**'); + + const applyContent = await fs.readFile(neovateApply, 'utf-8'); + expect(applyContent).toContain('---'); + expect(applyContent).toContain('name: Apply'); + expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.'); + expect(applyContent).toContain('Work through tasks sequentially'); + + const archiveContent = await fs.readFile(neovateArchive, 'utf-8'); + expect(archiveContent).toContain('---'); + expect(archiveContent).toContain('name: Archive'); + expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.'); + expect(archiveContent).toContain('openspec archive --yes'); + }); + + it('should mark Neovate as already configured during extend mode', async () => { + queueSelections('neovate', DONE, 'neovate', DONE); + await initCommand.execute(testDir); + await initCommand.execute(testDir); + + const secondRunArgs = mockPrompt.mock.calls[1][0]; + const neovateChoice = secondRunArgs.choices.find( + (choice: any) => choice.value === 'neovate' + ); + expect(neovateChoice.configured).toBe(true); + }); + it('should create COSTRICT.md when CoStrict is selected', async () => { queueSelections('costrict', DONE); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index b6fe974c..1ca159c1 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -1360,6 +1360,80 @@ More instructions after.`; expect(updated).toContain('Validate with `openspec validate --strict`'); }); + it('should refresh existing Neovate slash command files', async () => { + const neovatePath = path.join( + testDir, + '.neovate/commands/openspec/proposal.md' + ); + await fs.mkdir(path.dirname(neovatePath), { recursive: true }); + const initialContent = `--- +name: Proposal +description: Old description +--- + +Old slash content +`; + await fs.writeFile(neovatePath, initialContent); + + const consoleSpy = vi.spyOn(console, 'log'); + + await updateCommand.execute(testDir); + + const updated = await fs.readFile(neovatePath, 'utf-8'); + expect(updated).toContain('name: Proposal'); + expect(updated).toContain('**Guardrails**'); + expect(updated).toContain( + 'Validate with `openspec validate --strict`' + ); + expect(updated).not.toContain('Old slash content'); + + const [logMessage] = consoleSpy.mock.calls[0]; + expect(logMessage).toContain( + 'Updated OpenSpec instructions (openspec/AGENTS.md' + ); + expect(logMessage).toContain('AGENTS.md (created)'); + expect(logMessage).toContain( + 'Updated slash commands: .neovate/commands/openspec/proposal.md' + ); + + consoleSpy.mockRestore(); + }); + + it('should not create missing Neovate slash command files on update', async () => { + const neovateApply = path.join( + testDir, + '.neovate/commands/openspec/apply.md' + ); + + // Only create apply; leave proposal and archive missing + await fs.mkdir(path.dirname(neovateApply), { recursive: true }); + await fs.writeFile( + neovateApply, + `--- +name: Apply +description: Old description +--- + +Old body +` + ); + + await updateCommand.execute(testDir); + + const neovateProposal = path.join( + testDir, + '.neovate/commands/openspec/proposal.md' + ); + const neovateArchive = path.join( + testDir, + '.neovate/commands/openspec/archive.md' + ); + + // Confirm they weren't created by update + await expect(FileSystemUtils.fileExists(neovateProposal)).resolves.toBe(false); + await expect(FileSystemUtils.fileExists(neovateArchive)).resolves.toBe(false); + }); + it('should handle configurator errors gracefully for CoStrict', async () => { // Create COSTRICT.md file but make it read-only to cause an error const costrictPath = path.join(testDir, 'COSTRICT.md');