mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7157a37a39 | ||
|
|
50f6c254d6 | ||
|
|
b085c17945 | ||
|
|
235afac85e | ||
|
|
5f0c5909fb |
@@ -73,7 +73,7 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
|
||||
#### Scenario: Configuring Cline
|
||||
|
||||
- **WHEN** Cline is selected
|
||||
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
|
||||
- **THEN** create or update `.clinerules/openspec-rules.md` (following Cline's convention for rules files)
|
||||
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
|
||||
|
||||
#### Scenario: Creating new CLAUDE.md
|
||||
@@ -174,7 +174,7 @@ The init command SHALL generate slash command files for supported editors using
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
|
||||
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
@@ -63,11 +63,16 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating Cline rules file
|
||||
- **WHEN** `.clinerules/openspec-rules.md` exists
|
||||
- **THEN** refresh the managed block to point teammates to `@/openspec/AGENTS.md`
|
||||
- **AND** preserve any content outside the managed markers
|
||||
|
||||
#### Scenario: Updating slash commands for Crush
|
||||
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
|
||||
@@ -6,13 +6,13 @@ import { OPENSPEC_MARKERS } from '../config.js';
|
||||
|
||||
export class ClineConfigurator implements ToolConfigurator {
|
||||
name = 'Cline';
|
||||
configFileName = 'CLINE.md';
|
||||
configFileName = '.clinerules/openspec-rules.md';
|
||||
isAvailable = true;
|
||||
|
||||
async configure(projectPath: string, openspecDir: string): Promise<void> {
|
||||
const filePath = path.join(projectPath, this.configFileName);
|
||||
const content = TemplateManager.getClineTemplate();
|
||||
|
||||
const content = TemplateManager.getAgentsStandardTemplate();
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
content,
|
||||
|
||||
@@ -126,6 +126,10 @@ export class FileSystemUtils {
|
||||
return await fs.readFile(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
static async deleteFile(filePath: string): Promise<void> {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
|
||||
static async updateFileWithMarkers(
|
||||
filePath: string,
|
||||
content: string,
|
||||
|
||||
+6
-23
@@ -10,7 +10,7 @@ type SelectionQueue = string[][];
|
||||
|
||||
let selectionQueue: SelectionQueue = [];
|
||||
|
||||
const mockPrompt = vi.fn(async () => {
|
||||
const mockPrompt = vi.fn(async (options: any) => {
|
||||
if (selectionQueue.length === 0) {
|
||||
throw new Error('No queued selections provided to init prompt.');
|
||||
}
|
||||
@@ -136,38 +136,21 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create CLINE.md when Cline is selected', async () => {
|
||||
it('should create .clinerules/openspec-rules.md when Cline is selected', async () => {
|
||||
queueSelections('cline', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
expect(await fileExists(clinePath)).toBe(true);
|
||||
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
|
||||
expect(await fileExists(clineRulesPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(clinePath, 'utf-8');
|
||||
const content = await fs.readFile(clineRulesPath, 'utf-8');
|
||||
expect(content).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(content).toContain("@/openspec/AGENTS.md");
|
||||
expect(content).toContain('OpenSpec Instructions');
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
});
|
||||
|
||||
it('should update existing CLINE.md with markers', async () => {
|
||||
queueSelections('cline', DONE);
|
||||
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
const existingContent =
|
||||
'# My Cline Rules\nCustom Cline instructions here';
|
||||
await fs.writeFile(clinePath, existingContent);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(clinePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
expect(updatedContent).toContain('openspec update');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain('Custom Cline instructions here');
|
||||
});
|
||||
|
||||
it('should create Windsurf workflows when Windsurf is selected', async () => {
|
||||
queueSelections('windsurf', DONE);
|
||||
|
||||
+13
-12
@@ -248,9 +248,10 @@ Old content
|
||||
await expect(FileSystemUtils.fileExists(qwenPath)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should update only existing CLINE.md file', async () => {
|
||||
// Create CLINE.md file with initial content
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
it('should update only existing .clinerules/openspec-rules.md file', async () => {
|
||||
// Create .clinerules/openspec-rules.md file with initial content
|
||||
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
|
||||
await fs.mkdir(path.dirname(clineRulesPath), { recursive: true });
|
||||
const initialContent = `# Cline Rules
|
||||
|
||||
Some existing Cline rules here.
|
||||
@@ -260,15 +261,15 @@ Old OpenSpec content
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
More rules after.`;
|
||||
await fs.writeFile(clinePath, initialContent);
|
||||
await fs.writeFile(clineRulesPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLINE.md was updated
|
||||
const updatedContent = await fs.readFile(clinePath, 'utf-8');
|
||||
// Check that .clinerules/openspec-rules.md was updated
|
||||
const updatedContent = await fs.readFile(clineRulesPath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
@@ -282,19 +283,19 @@ More rules after.`;
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain('Updated AI tool files: CLINE.md');
|
||||
expect(logMessage).toContain('Updated AI tool files: .clinerules/openspec-rules.md');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create CLINE.md if it does not exist', async () => {
|
||||
// Ensure CLINE.md does not exist
|
||||
const clinePath = path.join(testDir, 'CLINE.md');
|
||||
it('should not create .clinerules/openspec-rules.md if it does not exist', async () => {
|
||||
// Ensure .clinerules/openspec-rules.md does not exist
|
||||
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
|
||||
|
||||
// Execute update command
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
// Check that CLINE.md was not created
|
||||
const fileExists = await FileSystemUtils.fileExists(clinePath);
|
||||
// Check that .clinerules/openspec-rules.md was not created
|
||||
const fileExists = await FileSystemUtils.fileExists(clineRulesPath);
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user