Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 9f27f60a87 update gitignore 2025-11-03 00:14:15 +11:00
Tabish Bidiwale 937bdd6c42 test: cover qwen configurators 2025-11-03 00:07:47 +11:00
andershsuehandQwen-Coder 9ec2440d90 fix: remove duplicate JSDoc comment in QwenConfigurator
- Remove duplicate documentation comment for configure method
- Keep only the updated comment that correctly documents the unused parameter

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:29 +11:00
andershsuehandQwen-Coder 2d4b30f923 chore: remove personal notes files from git tracking
- Remove README_CN.md and supportQwen.md from git tracking
- These files are now ignored via .gitignore
- Keep only necessary files for the OpenSpec project

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:26 +11:00
andershsuehandQwen-Coder 9f91b4157e chore: add personal notes files to .gitignore
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:23 +11:00
andershsuehandQwen-Coder dbb7d4b39f fix: address CodeRabbit review comments - parameter naming fix
- Fix unused parameter naming convention (_openspecDir)
- Keep only necessary changes for Qwen Code support

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:20 +11:00
andershsuehandQwen-Coder a22d0925d4 fix: add language identifier to markdown code block in supportQwen.md
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:15 +11:00
andershsuehandQwen-Coder 4569daa620 docs: add docstrings and fix review comments for Qwen Code support
- Add comprehensive JSDoc comments to Qwen configurator files
- Add Qwen Code entry to README_CN.md support table
- Fix file ending newline in src/core/configurators/qwen.ts
- Address CodeRabbit review suggestions for improved documentation coverage

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:52:11 +11:00
andershsuehandQwen-Coder d0edbd5590 feat: add Qwen Code support with slash command integration
- Add QwenSlashCommandConfigurator for .qwen/commands/ structure
- Add QwenConfigurator to main registry
- Update README.md to include Qwen Code in supported tools list
- Register Qwen in both slash command and main tool registries
- Implement proper YAML frontmatter for Qwen command files
- Follow OpenSpec's established patterns for AI tool integration

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-11-02 23:51:59 +11:00
17 changed files with 55 additions and 346 deletions
+1 -1
View File
@@ -149,4 +149,4 @@ CLAUDE.md
.DS_Store
# Pnpm
.pnpm-store/
.pnpm-store/
-16
View File
@@ -1,21 +1,5 @@
# @fission-ai/openspec
## 0.14.0
### Minor Changes
- 8386b91: Add support for new AI assistants and configuration improvements
- feat: add Qwen Code support with slash command integration
- feat: add $ARGUMENTS support to apply slash command for dynamic variable passing
- feat: add Qoder CLI support to configuration and documentation
- feat: add CoStrict AI assistant support
- fix: recreate missing openspec template files in extend mode
- fix: prevent false 'already configured' detection for tools
- fix: use change-id as fallback title instead of "Untitled Change"
- docs: add guidance for populating project-level context
- docs: add Crush to supported AI tools in README
## 0.13.0
### Minor Changes
+2 -3
View File
@@ -94,10 +94,9 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
| **Qoder (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com/cli) |
@@ -116,7 +115,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
| Tools |
|-------|
| Amp • Jules • Others |
| Amp • Jules • Gemini CLI • Others |
### Install & Initialize
@@ -1,13 +0,0 @@
## Why
The Cline implementation was architecturally incorrect. According to Cline's official documentation, Cline uses workflows for on-demand automation and rules for behavioral guidelines. The OpenSpec slash commands are procedural workflows (scaffold → implement → archive), not behavioral rules, so they should be placed in `.clinerules/workflows/` instead of `.clinerules/`.
## What Changes
- Update ClineSlashCommandConfigurator to use `.clinerules/workflows/` paths instead of `.clinerules/` paths
- Update all tests to expect the correct workflow file locations
- Update README.md documentation to reflect workflows instead of rules
- **BREAKING**: Existing Cline users will need to re-run `openspec init` to get the corrected workflow files
## Impact
- Affected specs: cli-init (corrected Cline workflow paths)
- Affected code: `src/core/configurators/slash/cline.ts`, test files, README.md
- Modified files: `.clinerules/workflows/openspec-*.md` (moved from `.clinerules/openspec-*.md`)
@@ -1,11 +0,0 @@
# Delta for CLI Init
## MODIFIED Requirements
### Requirement: Slash Command Configuration
#### Scenario: Generating slash commands for Cline
- **WHEN** the user selects Cline during initialization
- **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
@@ -1,13 +0,0 @@
## 1. Update ClineSlashCommandConfigurator
- [x] Change FILE_PATHS in `src/core/configurators/slash/cline.ts` from `.clinerules/openspec-*.md` to `.clinerules/workflows/openspec-*.md`
## 2. Update Tests
- [x] Update "should refresh existing Cline rule files" test in `test/core/update.test.ts` to use workflow paths
- [x] Update "should create Cline rule files with templates" test in `test/core/init.test.ts` to use workflow paths
## 3. Update Documentation
- [x] Update README.md table to show "Workflows in `.clinerules/workflows/` directory" for Cline
## 4. Validate Changes
- [x] Ensure all tests pass with the new paths
- [x] Verify the change follows OpenSpec conventions
+2 -9
View File
@@ -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 `.clinerules/openspec-rules.md` (following Cline's convention for rules files)
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
- **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/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/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
@@ -231,13 +231,6 @@ The init command SHALL generate slash command files for supported editors using
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Gemini CLI
- **WHEN** the user selects Gemini CLI during initialization
- **THEN** create `.gemini/commands/openspec/proposal.toml`, `.gemini/commands/openspec/apply.toml`, and `.gemini/commands/openspec/archive.toml`
- **AND** populate each file as TOML that sets a stage-specific `description = "<summary>"` and a multi-line `prompt = """` block with the shared OpenSpec template
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
### Requirement: Non-Interactive Mode
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
+1 -12
View File
@@ -63,16 +63,11 @@ 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/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** `.clinerules/` 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
@@ -122,12 +117,6 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Gemini CLI
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
#### Scenario: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.14.0",
"version": "0.13.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
-1
View File
@@ -25,7 +25,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
+3 -3
View File
@@ -6,13 +6,13 @@ import { OPENSPEC_MARKERS } from '../config.js';
export class ClineConfigurator implements ToolConfigurator {
name = 'Cline';
configFileName = '.clinerules/openspec-rules.md';
configFileName = 'CLINE.md';
isAvailable = true;
async configure(projectPath: string, openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getAgentsStandardTemplate();
const content = TemplateManager.getClineTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
+3 -3
View File
@@ -2,9 +2,9 @@ import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.clinerules/workflows/openspec-proposal.md',
apply: '.clinerules/workflows/openspec-apply.md',
archive: '.clinerules/workflows/openspec-archive.md'
proposal: '.clinerules/openspec-proposal.md',
apply: '.clinerules/openspec-apply.md',
archive: '.clinerules/openspec-archive.md'
};
export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
-83
View File
@@ -1,83 +0,0 @@
import { FileSystemUtils } from '../../../utils/file-system.js';
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId, TemplateManager } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.gemini/commands/openspec/proposal.toml',
apply: '.gemini/commands/openspec/apply.toml',
archive: '.gemini/commands/openspec/archive.toml'
};
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.'
};
export class GeminiSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'gemini';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(_id: SlashCommandId): string | undefined {
// TOML doesn't use separate frontmatter - it's all in one structure
return undefined;
}
// Override to generate TOML format with markers inside the prompt field
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
const createdOrUpdated: string[] = [];
for (const target of this.getTargets()) {
const body = this.getBody(target.id);
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
} else {
const tomlContent = this.generateTOML(target.id, body);
await FileSystemUtils.writeFile(filePath, tomlContent);
}
createdOrUpdated.push(target.path);
}
return createdOrUpdated;
}
private generateTOML(id: SlashCommandId, body: string): string {
const description = DESCRIPTIONS[id];
// TOML format with triple-quoted string for multi-line prompt
// Markers are inside the prompt value
return `description = "${description}"
prompt = """
${OPENSPEC_MARKERS.start}
${body}
${OPENSPEC_MARKERS.end}
"""
`;
}
// Override updateBody to handle TOML format
protected async updateBody(filePath: string, body: string): Promise<void> {
const content = await FileSystemUtils.readFile(filePath);
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error(`Missing OpenSpec markers in ${filePath}`);
}
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
const after = content.slice(endIndex);
const updatedContent = `${before}\n${body}\n${after}`;
await FileSystemUtils.writeFile(filePath, updatedContent);
}
}
-3
View File
@@ -10,7 +10,6 @@ import { CodexSlashCommandConfigurator } from './codex.js';
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
import { FactorySlashCommandConfigurator } from './factory.js';
import { GeminiSlashCommandConfigurator } from './gemini.js';
import { AuggieSlashCommandConfigurator } from './auggie.js';
import { ClineSlashCommandConfigurator } from './cline.js';
import { CrushSlashCommandConfigurator } from './crush.js';
@@ -32,7 +31,6 @@ export class SlashCommandRegistry {
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
const amazonQ = new AmazonQSlashCommandConfigurator();
const factory = new FactorySlashCommandConfigurator();
const gemini = new GeminiSlashCommandConfigurator();
const auggie = new AuggieSlashCommandConfigurator();
const cline = new ClineSlashCommandConfigurator();
const crush = new CrushSlashCommandConfigurator();
@@ -50,7 +48,6 @@ export class SlashCommandRegistry {
this.configurators.set(githubCopilot.toolId, githubCopilot);
this.configurators.set(amazonQ.toolId, amazonQ);
this.configurators.set(factory.toolId, factory);
this.configurators.set(gemini.toolId, gemini);
this.configurators.set(auggie.toolId, auggie);
this.configurators.set(cline.toolId, cline);
this.configurators.set(crush.toolId, crush);
-4
View File
@@ -126,10 +126,6 @@ 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,
+27 -106
View File
@@ -10,7 +10,7 @@ type SelectionQueue = string[][];
let selectionQueue: SelectionQueue = [];
const mockPrompt = vi.fn(async (options: any) => {
const mockPrompt = vi.fn(async () => {
if (selectionQueue.length === 0) {
throw new Error('No queued selections provided to init prompt.');
}
@@ -136,21 +136,38 @@ describe('InitCommand', () => {
expect(updatedContent).toContain('Custom instructions here');
});
it('should create .clinerules/openspec-rules.md when Cline is selected', async () => {
it('should create CLINE.md when Cline is selected', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
expect(await fileExists(clineRulesPath)).toBe(true);
const clinePath = path.join(testDir, 'CLINE.md');
expect(await fileExists(clinePath)).toBe(true);
const content = await fs.readFile(clineRulesPath, 'utf-8');
const content = await fs.readFile(clinePath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain('OpenSpec Instructions');
expect(content).toContain("@/openspec/AGENTS.md");
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);
@@ -288,73 +305,6 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec list --specs');
});
it('should create Gemini CLI TOML files when selected', async () => {
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
const geminiApply = path.join(
testDir,
'.gemini/commands/openspec/apply.toml'
);
const geminiArchive = path.join(
testDir,
'.gemini/commands/openspec/archive.toml'
);
expect(await fileExists(geminiProposal)).toBe(true);
expect(await fileExists(geminiApply)).toBe(true);
expect(await fileExists(geminiArchive)).toBe(true);
const proposalContent = await fs.readFile(geminiProposal, 'utf-8');
expect(proposalContent).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
expect(proposalContent).toContain('prompt = """');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
const applyContent = await fs.readFile(geminiApply, 'utf-8');
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(geminiArchive, 'utf-8');
expect(archiveContent).toContain('description = "Archive a deployed OpenSpec change and update specs."');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should update existing Gemini CLI TOML files with refreshed content', async () => {
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
// Modify the file to simulate user customization
const originalContent = await fs.readFile(geminiProposal, 'utf-8');
const modifiedContent = originalContent.replace(
'<!-- OPENSPEC:START -->',
'<!-- OPENSPEC:START -->\nCustom instruction added by user\n'
);
await fs.writeFile(geminiProposal, modifiedContent);
// Run init again to test update/refresh path
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(geminiProposal, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('**Guardrails**');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).not.toContain('Custom instruction added by user');
});
it('should create OpenCode slash command files with templates', async () => {
queueSelections('opencode', DONE);
@@ -447,39 +397,22 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec archive <id>');
});
it('should update existing QWEN.md with markers', async () => {
queueSelections('qwen', DONE);
const qwenPath = path.join(testDir, 'QWEN.md');
const existingContent = '# My Qwen Instructions\nCustom instructions here';
await fs.writeFile(qwenPath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(qwenPath, '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 instructions here');
});
it('should create Cline workflow files with templates', async () => {
it('should create Cline rule files with templates', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clineProposal = path.join(
testDir,
'.clinerules/workflows/openspec-proposal.md'
'.clinerules/openspec-proposal.md'
);
const clineApply = path.join(
testDir,
'.clinerules/workflows/openspec-apply.md'
'.clinerules/openspec-apply.md'
);
const clineArchive = path.join(
testDir,
'.clinerules/workflows/openspec-archive.md'
'.clinerules/openspec-archive.md'
);
expect(await fileExists(clineProposal)).toBe(true);
@@ -803,18 +736,6 @@ describe('InitCommand', () => {
expect(claudeChoice.configured).toBe(true);
});
it('should mark Qwen as already configured during extend mode', async () => {
queueSelections('qwen', DONE, 'qwen', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const qwenChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'qwen'
);
expect(qwenChoice.configured).toBe(true);
});
it('should preselect Kilo Code when workflows already exist', async () => {
queueSelections('kilocode', DONE, 'kilocode', DONE);
await initCommand.execute(testDir);
+15 -64
View File
@@ -248,10 +248,9 @@ Old content
await expect(FileSystemUtils.fileExists(qwenPath)).resolves.toBe(false);
});
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 });
it('should update only existing CLINE.md file', async () => {
// Create CLINE.md file with initial content
const clinePath = path.join(testDir, 'CLINE.md');
const initialContent = `# Cline Rules
Some existing Cline rules here.
@@ -261,15 +260,15 @@ Old OpenSpec content
<!-- OPENSPEC:END -->
More rules after.`;
await fs.writeFile(clineRulesPath, initialContent);
await fs.writeFile(clinePath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
// Execute update command
await updateCommand.execute(testDir);
// Check that .clinerules/openspec-rules.md was updated
const updatedContent = await fs.readFile(clineRulesPath, 'utf-8');
// Check that CLINE.md was updated
const updatedContent = await fs.readFile(clinePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
@@ -283,26 +282,26 @@ More rules after.`;
'Updated OpenSpec instructions (openspec/AGENTS.md'
);
expect(logMessage).toContain('AGENTS.md (created)');
expect(logMessage).toContain('Updated AI tool files: .clinerules/openspec-rules.md');
expect(logMessage).toContain('Updated AI tool files: CLINE.md');
consoleSpy.mockRestore();
});
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');
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');
// Execute update command
await updateCommand.execute(testDir);
// Check that .clinerules/openspec-rules.md was not created
const fileExists = await FileSystemUtils.fileExists(clineRulesPath);
// Check that CLINE.md was not created
const fileExists = await FileSystemUtils.fileExists(clinePath);
expect(fileExists).toBe(false);
});
it('should refresh existing Cline workflow files', async () => {
it('should refresh existing Cline rule files', async () => {
const proposalPath = path.join(
testDir,
'.clinerules/workflows/openspec-proposal.md'
'.clinerules/openspec-proposal.md'
);
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
const initialContent = `# OpenSpec: Proposal
@@ -332,7 +331,7 @@ Old slash content
);
expect(logMessage).toContain('AGENTS.md (created)');
expect(logMessage).toContain(
'Updated slash commands: .clinerules/workflows/openspec-proposal.md'
'Updated slash commands: .clinerules/openspec-proposal.md'
);
consoleSpy.mockRestore();
@@ -589,54 +588,6 @@ Old body
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
});
it('should refresh existing Gemini CLI TOML files without creating new ones', async () => {
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
await fs.mkdir(path.dirname(geminiProposal), { recursive: true });
const initialContent = `description = "Scaffold a new OpenSpec change and validate strictly."
prompt = """
<!-- OPENSPEC:START -->
Old Gemini body
<!-- OPENSPEC:END -->
"""
`;
await fs.writeFile(geminiProposal, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(geminiProposal, 'utf-8');
expect(updated).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
expect(updated).toContain('prompt = """');
expect(updated).toContain('<!-- OPENSPEC:START -->');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain('<!-- OPENSPEC:END -->');
expect(updated).not.toContain('Old Gemini body');
const geminiApply = path.join(
testDir,
'.gemini/commands/openspec/apply.toml'
);
const geminiArchive = path.join(
testDir,
'.gemini/commands/openspec/archive.toml'
);
await expect(FileSystemUtils.fileExists(geminiApply)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(geminiArchive)).resolves.toBe(false);
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .gemini/commands/openspec/proposal.toml'
);
consoleSpy.mockRestore();
});
it('should refresh existing Factory slash commands', async () => {
const factoryPath = path.join(
testDir,