Skip to content

Commit e987a5a

Browse files
ajuan-muchenajuanliTabishB
authored
Add Continue support (Fission-AI#402)
* OpenSpec 支持 Continue 插件 * add update * Update spec.md * fix: correct Continue frontmatter format and README ordering - Fix Continue frontmatter to use proper YAML format with opening `---` and required `invokable: true` field for slash command availability - Fix README table ordering: Continue should be after Codex alphabetically - Remove unused TemplateManager import from continue.ts - Fix trailing whitespace in update.test.ts - Fix double blank line in init.test.ts - Fix extra blank line in cli-init/spec.md - Update tests to verify correct frontmatter format --------- Co-authored-by: ajuanli <ajuanli@tencent.com> Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com> Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
1 parent 4971cda commit e987a5a

9 files changed

Lines changed: 198 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686

8787
### Minor Changes
8888

89+
- Add Continue slash command support so `openspec init` can generate `.continue/prompts/openspec-*.prompt` files with MARKDOWN frontmatter and `$ARGUMENTS` placeholder, and refresh them on `openspec update`.
90+
8991
- Add Antigravity slash command support so `openspec init` can generate `.agent/workflows/openspec-*.md` files with description-only frontmatter and `openspec update` refreshes existing workflows alongside Windsurf.
9092

9193
## 0.15.0

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
103103
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
104104
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
105105
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
106+
| **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
106107
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
107108
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
108109
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |

openspec/specs/cli-init/spec.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ The init command SHALL generate slash command files for supported editors using
211211
- **AND** populate each file from shared templates so command text matches other tools
212212
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
213213

214+
#### Scenario: Generating slash commands for Continue
215+
- **WHEN** the user selects Continue during initialization
216+
- **THEN** create `.continue/prompts/openspec-proposal.prompt`, `.continue/prompts/openspec-apply.prompt`, and `.continue/prompts/openspec-archive.prompt`
217+
- **AND** populate each file from shared templates so command text matches other tools
218+
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
219+
214220
#### Scenario: Generating slash commands for Factory Droid
215221
- **WHEN** the user selects Factory Droid during initialization
216222
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`

openspec/specs/cli-update/spec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ The update command SHALL refresh existing slash command files for configured too
7575
- **AND** include Cline-specific Markdown heading frontmatter
7676
- **AND** ensure templates include instructions for the relevant workflow stage
7777

78+
#### Scenario: Updating slash commands for Continue
79+
- **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt`
80+
- **THEN** refresh each file using shared templates
81+
- **AND** ensure templates include instructions for the relevant workflow stage
82+
7883
#### Scenario: Updating slash commands for Crush
7984
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
8085
- **THEN** refresh each file using shared templates

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const AI_TOOLS: AIToolOption[] = [
2424
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
2525
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
2626
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
27+
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)' },
2728
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
2829
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
2930
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.continue/prompts/openspec-proposal.prompt',
6+
apply: '.continue/prompts/openspec-apply.prompt',
7+
archive: '.continue/prompts/openspec-archive.prompt'
8+
};
9+
10+
/*
11+
* Continue .prompt format requires YAML frontmatter:
12+
* ---
13+
* name: commandName
14+
* description: description
15+
* invokable: true
16+
* ---
17+
* Body...
18+
*
19+
* The 'invokable: true' field is required to make the prompt available as a slash command.
20+
* We use 'openspec-proposal' as the name so the command becomes /openspec-proposal.
21+
*/
22+
const FRONTMATTER: Record<SlashCommandId, string> = {
23+
proposal: `---
24+
name: openspec-proposal
25+
description: Scaffold a new OpenSpec change and validate strictly.
26+
invokable: true
27+
---`,
28+
apply: `---
29+
name: openspec-apply
30+
description: Implement an approved OpenSpec change and keep tasks in sync.
31+
invokable: true
32+
---`,
33+
archive: `---
34+
name: openspec-archive
35+
description: Archive a deployed OpenSpec change and update specs.
36+
invokable: true
37+
---`
38+
};
39+
40+
export class ContinueSlashCommandConfigurator extends SlashCommandConfigurator {
41+
readonly toolId = 'continue';
42+
readonly isAvailable = true;
43+
44+
protected getRelativePath(id: SlashCommandId): string {
45+
return FILE_PATHS[id];
46+
}
47+
48+
protected getFrontmatter(id: SlashCommandId): string {
49+
return FRONTMATTER[id];
50+
}
51+
}

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { QwenSlashCommandConfigurator } from './qwen.js';
1919
import { RooCodeSlashCommandConfigurator } from './roocode.js';
2020
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
2121
import { IflowSlashCommandConfigurator } from './iflow.js';
22+
import { ContinueSlashCommandConfigurator } from './continue.js';
2223

2324
export class SlashCommandRegistry {
2425
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -44,6 +45,7 @@ export class SlashCommandRegistry {
4445
const roocode = new RooCodeSlashCommandConfigurator();
4546
const antigravity = new AntigravitySlashCommandConfigurator();
4647
const iflow = new IflowSlashCommandConfigurator();
48+
const continueTool = new ContinueSlashCommandConfigurator();
4749

4850
this.configurators.set(claude.toolId, claude);
4951
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -65,6 +67,7 @@ export class SlashCommandRegistry {
6567
this.configurators.set(roocode.toolId, roocode);
6668
this.configurators.set(antigravity.toolId, antigravity);
6769
this.configurators.set(iflow.toolId, iflow);
70+
this.configurators.set(continueTool.toolId, continueTool);
6871
}
6972

7073
static register(configurator: SlashCommandConfigurator): void {

test/core/init.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,61 @@ describe('InitCommand', () => {
11511151
expect(codeBuddyChoice.configured).toBe(true);
11521152
});
11531153

1154+
it('should create Continue slash command files with templates', async () => {
1155+
queueSelections('continue', DONE);
1156+
1157+
await initCommand.execute(testDir);
1158+
1159+
const continueProposal = path.join(
1160+
testDir,
1161+
'.continue/prompts/openspec-proposal.prompt'
1162+
);
1163+
const continueApply = path.join(
1164+
testDir,
1165+
'.continue/prompts/openspec-apply.prompt'
1166+
);
1167+
const continueArchive = path.join(
1168+
testDir,
1169+
'.continue/prompts/openspec-archive.prompt'
1170+
);
1171+
1172+
expect(await fileExists(continueProposal)).toBe(true);
1173+
expect(await fileExists(continueApply)).toBe(true);
1174+
expect(await fileExists(continueArchive)).toBe(true);
1175+
1176+
const proposalContent = await fs.readFile(continueProposal, 'utf-8');
1177+
expect(proposalContent).toContain('---');
1178+
expect(proposalContent).toContain('name: openspec-proposal');
1179+
expect(proposalContent).toContain('invokable: true');
1180+
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
1181+
1182+
const applyContent = await fs.readFile(continueApply, 'utf-8');
1183+
expect(applyContent).toContain('---');
1184+
expect(applyContent).toContain('name: openspec-apply');
1185+
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
1186+
expect(applyContent).toContain('invokable: true');
1187+
expect(applyContent).toContain('Work through tasks sequentially');
1188+
1189+
const archiveContent = await fs.readFile(continueArchive, 'utf-8');
1190+
expect(archiveContent).toContain('---');
1191+
expect(archiveContent).toContain('name: openspec-archive');
1192+
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
1193+
expect(archiveContent).toContain('invokable: true');
1194+
expect(archiveContent).toContain('openspec archive <id> --yes');
1195+
});
1196+
1197+
it('should mark Continue as already configured during extend mode', async () => {
1198+
queueSelections('continue', DONE, 'continue', DONE);
1199+
await initCommand.execute(testDir);
1200+
await initCommand.execute(testDir);
1201+
1202+
const secondRunArgs = mockPrompt.mock.calls[1][0];
1203+
const continueChoice = secondRunArgs.choices.find(
1204+
(choice: any) => choice.value === 'continue'
1205+
);
1206+
expect(continueChoice.configured).toBe(true);
1207+
});
1208+
11541209
it('should create CODEBUDDY.md when CodeBuddy is selected', async () => {
11551210
queueSelections('codebuddy', DONE);
11561211

test/core/update.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,80 @@ Old body
368368
consoleSpy.mockRestore();
369369
});
370370

371+
it('should refresh existing Continue prompt files', async () => {
372+
const continuePath = path.join(
373+
testDir,
374+
'.continue/prompts/openspec-apply.prompt'
375+
);
376+
await fs.mkdir(path.dirname(continuePath), { recursive: true });
377+
const initialContent = `---
378+
name: openspec-apply
379+
description: Old description
380+
invokable: true
381+
---
382+
<!-- OPENSPEC:START -->
383+
Old body
384+
<!-- OPENSPEC:END -->`;
385+
await fs.writeFile(continuePath, initialContent);
386+
387+
const consoleSpy = vi.spyOn(console, 'log');
388+
389+
await updateCommand.execute(testDir);
390+
391+
const updated = await fs.readFile(continuePath, 'utf-8');
392+
expect(updated).toContain('name: openspec-apply');
393+
expect(updated).toContain('invokable: true');
394+
expect(updated).toContain('Work through tasks sequentially');
395+
expect(updated).not.toContain('Old body');
396+
397+
const [logMessage] = consoleSpy.mock.calls[0];
398+
expect(logMessage).toContain(
399+
'Updated OpenSpec instructions (openspec/AGENTS.md'
400+
);
401+
expect(logMessage).toContain('AGENTS.md (created)');
402+
expect(logMessage).toContain(
403+
'Updated slash commands: .continue/prompts/openspec-apply.prompt'
404+
);
405+
406+
consoleSpy.mockRestore();
407+
});
408+
409+
it('should not create missing Continue prompt files on update', async () => {
410+
const continueApply = path.join(
411+
testDir,
412+
'.continue/prompts/openspec-apply.prompt'
413+
);
414+
415+
// Only create apply; leave proposal and archive missing
416+
await fs.mkdir(path.dirname(continueApply), { recursive: true });
417+
await fs.writeFile(
418+
continueApply,
419+
`---
420+
name: openspec-apply
421+
description: Old description
422+
invokable: true
423+
---
424+
<!-- OPENSPEC:START -->
425+
Old body
426+
<!-- OPENSPEC:END -->`
427+
);
428+
429+
await updateCommand.execute(testDir);
430+
431+
const continueProposal = path.join(
432+
testDir,
433+
'.continue/prompts/openspec-proposal.prompt'
434+
);
435+
const continueArchive = path.join(
436+
testDir,
437+
'.continue/prompts/openspec-archive.prompt'
438+
);
439+
440+
// Confirm they weren't created by update
441+
await expect(FileSystemUtils.fileExists(continueProposal)).resolves.toBe(false);
442+
await expect(FileSystemUtils.fileExists(continueArchive)).resolves.toBe(false);
443+
});
444+
371445
it('should refresh existing OpenCode slash command files', async () => {
372446
const openCodePath = path.join(
373447
testDir,

0 commit comments

Comments
 (0)