mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0e96688b8 | ||
|
|
260de7bfa7 | ||
|
|
6f03cfe12f | ||
|
|
1fcdfd1a7d | ||
|
|
428545a799 | ||
|
|
36a975153f | ||
|
|
728243d2c2 | ||
|
|
19f7357ecb |
@@ -85,6 +85,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -21,5 +21,6 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
||||
import { CursorSlashCommandConfigurator } from './cursor.js';
|
||||
import { WindsurfSlashCommandConfigurator } from './windsurf.js';
|
||||
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
||||
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
||||
|
||||
@@ -10,11 +11,13 @@ export class SlashCommandRegistry {
|
||||
static {
|
||||
const claude = new ClaudeSlashCommandConfigurator();
|
||||
const cursor = new CursorSlashCommandConfigurator();
|
||||
const windsurf = new WindsurfSlashCommandConfigurator();
|
||||
const kilocode = new KiloCodeSlashCommandConfigurator();
|
||||
const opencode = new OpenCodeSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
this.configurators.set(windsurf.toolId, windsurf);
|
||||
this.configurators.set(kilocode.toolId, kilocode);
|
||||
this.configurators.set(opencode.toolId, opencode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.windsurf/workflows/openspec-proposal.md',
|
||||
apply: '.windsurf/workflows/openspec-apply.md',
|
||||
archive: '.windsurf/workflows/openspec-archive.md'
|
||||
};
|
||||
|
||||
export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'windsurf';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
const descriptions: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
const description = descriptions[id];
|
||||
return `---\ndescription: ${description}\nauto_execution_mode: 3\n---`;
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,50 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create Windsurf workflows when Windsurf is selected', async () => {
|
||||
queueSelections('windsurf', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const wsProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
const wsApply = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
const wsArchive = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(wsProposal)).toBe(true);
|
||||
expect(await fileExists(wsApply)).toBe(true);
|
||||
expect(await fileExists(wsArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(wsProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('auto_execution_mode: 3');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(wsApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('auto_execution_mode: 3');
|
||||
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(wsArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('auto_execution_mode: 3');
|
||||
expect(archiveContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
|
||||
});
|
||||
|
||||
it('should always create AGENTS.md in project root', async () => {
|
||||
queueSelections(DONE);
|
||||
|
||||
@@ -399,6 +443,18 @@ describe('InitCommand', () => {
|
||||
const preselected = secondRunArgs.initialSelected ?? [];
|
||||
expect(preselected).toContain('kilocode');
|
||||
});
|
||||
|
||||
it('should mark Windsurf as already configured during extend mode', async () => {
|
||||
queueSelections('windsurf', DONE, 'windsurf', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const wsChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'windsurf'
|
||||
);
|
||||
expect(wsChoice.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
||||
@@ -220,6 +220,81 @@ Old body
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Windsurf workflows', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
||||
const initialContent = `## OpenSpec: Apply (Windsurf)
|
||||
Intro
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(wsPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(wsPath, 'utf-8');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).toContain('## OpenSpec: Apply (Windsurf)');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should preserve Windsurf content outside markers during update', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
||||
const initialContent = `## Custom Intro Title\nSome intro text\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->\n\nFooter stays`;
|
||||
await fs.writeFile(wsPath, initialContent);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(wsPath, 'utf-8');
|
||||
expect(updated).toContain('## Custom Intro Title');
|
||||
expect(updated).toContain('Footer stays');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
|
||||
});
|
||||
|
||||
it('should not create missing Windsurf workflows on update', async () => {
|
||||
const wsApply = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(wsApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
wsApply,
|
||||
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const wsProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
const wsArchive = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(wsProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(wsArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should handle no AI tool files present', async () => {
|
||||
// Execute update command with no AI tool files
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
Reference in New Issue
Block a user