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
10 changed files with 311 additions and 6 deletions
+1
View File
@@ -105,6 +105,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
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`.
+1 -1
View File
@@ -59,7 +59,7 @@
"@changesets/cli": "^2.27.7",
"@types/node": "^24.2.0",
"@vitest/ui": "^3.2.4",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"dependencies": {
+5 -5
View File
@@ -37,8 +37,8 @@ importers:
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4)
typescript:
specifier: ^5.9.2
version: 5.9.2
specifier: ^5.9.3
version: 5.9.3
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/node@24.2.0)(@vitest/ui@3.2.4)
@@ -1115,8 +1115,8 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -2223,7 +2223,7 @@ snapshots:
type-fest@0.21.3: {}
typescript@5.9.2: {}
typescript@5.9.3: {}
undici-types@7.10.0: {}
+1
View File
@@ -32,5 +32,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
+47
View File
@@ -0,0 +1,47 @@
/**
* Qwen Code configurator for OpenSpec integration.
* This class handles the configuration of Qwen Code as an AI tool within OpenSpec.
*
* @implements {ToolConfigurator}
*/
import path from 'path';
import { ToolConfigurator } from './base.js';
import { FileSystemUtils } from '../../utils/file-system.js';
import { TemplateManager } from '../templates/index.js';
import { OPENSPEC_MARKERS } from '../config.js';
/**
* QwenConfigurator class provides integration with Qwen Code
* by creating and managing the necessary configuration files.
* Currently configures the QWEN.md file with OpenSpec instructions.
*/
export class QwenConfigurator implements ToolConfigurator {
/** Display name for the Qwen Code tool */
name = 'Qwen Code';
/** Configuration file name for Qwen Code */
configFileName = 'QWEN.md';
/** Availability status for the Qwen Code tool */
isAvailable = true;
/**
* Configures the Qwen Code integration by creating or updating the QWEN.md file
* with OpenSpec instructions and markers.
*
* @param {string} projectPath - The path to the project root
* @param {string} _openspecDir - The path to the openspec directory (unused)
* @returns {Promise<void>} A promise that resolves when configuration is complete
*/
async configure(projectPath: string, _openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getAgentsStandardTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}
+3
View File
@@ -5,6 +5,7 @@ import { CodeBuddyConfigurator } from './codebuddy.js';
import { CostrictConfigurator } from './costrict.js';
import { QoderConfigurator } from './qoder.js';
import { AgentsStandardConfigurator } from './agents.js';
import { QwenConfigurator } from './qwen.js';
export class ToolRegistry {
private static tools: Map<string, ToolConfigurator> = new Map();
@@ -16,6 +17,7 @@ export class ToolRegistry {
const costrictConfigurator = new CostrictConfigurator();
const qoderConfigurator = new QoderConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
const qwenConfigurator = new QwenConfigurator();
// Register with the ID that matches the checkbox value
this.tools.set('claude', claudeConfigurator);
this.tools.set('cline', clineConfigurator);
@@ -23,6 +25,7 @@ export class ToolRegistry {
this.tools.set('costrict', costrictConfigurator);
this.tools.set('qoder', qoderConfigurator);
this.tools.set('agents', agentsConfigurator);
this.tools.set('qwen', qwenConfigurator);
}
static register(tool: ToolConfigurator): void {
+80
View File
@@ -0,0 +1,80 @@
/**
* Qwen slash command configurator for OpenSpec integration.
* This class handles the generation of Qwen-specific slash command files
* in the .qwen/commands directory structure.
*
* @implements {SlashCommandConfigurator}
*/
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
/**
* Mapping of slash command IDs to their corresponding file paths in .qwen/commands directory.
* @type {Record<SlashCommandId, string>}
*/
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.qwen/commands/openspec-proposal.md',
apply: '.qwen/commands/openspec-apply.md',
archive: '.qwen/commands/openspec-archive.md'
};
/**
* YAML frontmatter definitions for Qwen command files.
* These provide metadata for each slash command to ensure proper recognition by Qwen Code.
* @type {Record<SlashCommandId, string>}
*/
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---`
};
/**
* QwenSlashCommandConfigurator class provides integration with Qwen Code
* by creating the necessary slash command files in the .qwen/commands directory.
*
* The slash commands include:
* - /openspec-proposal: Create an OpenSpec change proposal
* - /openspec-apply: Apply an approved OpenSpec change
* - /openspec-archive: Archive a deployed OpenSpec change
*/
export class QwenSlashCommandConfigurator extends SlashCommandConfigurator {
/** Unique identifier for the Qwen tool */
readonly toolId = 'qwen';
/** Availability status for the Qwen tool */
readonly isAvailable = true;
/**
* Returns the relative file path for a given slash command ID.
* @param {SlashCommandId} id - The slash command identifier
* @returns {string} The relative path to the command file
*/
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
/**
* Returns the YAML frontmatter for a given slash command ID.
* @param {SlashCommandId} id - The slash command identifier
* @returns {string} The YAML frontmatter string
*/
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+3
View File
@@ -14,6 +14,7 @@ import { AuggieSlashCommandConfigurator } from './auggie.js';
import { ClineSlashCommandConfigurator } from './cline.js';
import { CrushSlashCommandConfigurator } from './crush.js';
import { CostrictSlashCommandConfigurator } from './costrict.js';
import { QwenSlashCommandConfigurator } from './qwen.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -34,6 +35,7 @@ export class SlashCommandRegistry {
const cline = new ClineSlashCommandConfigurator();
const crush = new CrushSlashCommandConfigurator();
const costrict = new CostrictSlashCommandConfigurator();
const qwen = new QwenSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -50,6 +52,7 @@ export class SlashCommandRegistry {
this.configurators.set(cline.toolId, cline);
this.configurators.set(crush.toolId, crush);
this.configurators.set(costrict.toolId, costrict);
this.configurators.set(qwen.toolId, qwen);
}
static register(configurator: SlashCommandConfigurator): void {
+48
View File
@@ -349,6 +349,54 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec list --specs');
});
it('should create Qwen configuration and slash command files with templates', async () => {
queueSelections('qwen', DONE);
await initCommand.execute(testDir);
const qwenConfigPath = path.join(testDir, 'QWEN.md');
const proposalPath = path.join(
testDir,
'.qwen/commands/openspec-proposal.md'
);
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
const archivePath = path.join(
testDir,
'.qwen/commands/openspec-archive.md'
);
expect(await fileExists(qwenConfigPath)).toBe(true);
expect(await fileExists(proposalPath)).toBe(true);
expect(await fileExists(applyPath)).toBe(true);
expect(await fileExists(archivePath)).toBe(true);
const qwenConfigContent = await fs.readFile(qwenConfigPath, 'utf-8');
expect(qwenConfigContent).toContain('<!-- OPENSPEC:START -->');
expect(qwenConfigContent).toContain("@/openspec/AGENTS.md");
expect(qwenConfigContent).toContain('<!-- OPENSPEC:END -->');
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
expect(proposalContent).toContain('name: /openspec-proposal');
expect(proposalContent).toContain('category: OpenSpec');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
const applyContent = await fs.readFile(applyPath, 'utf-8');
expect(applyContent).toContain('name: /openspec-apply');
expect(applyContent).toContain('category: OpenSpec');
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(archivePath, 'utf-8');
expect(archiveContent).toContain('name: /openspec-archive');
expect(archiveContent).toContain('category: OpenSpec');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should create Cline rule files with templates', async () => {
queueSelections('cline', DONE);
+122
View File
@@ -73,6 +73,41 @@ More content after.`;
consoleSpy.mockRestore();
});
it('should update only existing QWEN.md file', async () => {
const qwenPath = path.join(testDir, 'QWEN.md');
const initialContent = `# Qwen Instructions
Some existing content.
<!-- OPENSPEC:START -->
Old OpenSpec content
<!-- OPENSPEC:END -->
More notes here.`;
await fs.writeFile(qwenPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updatedContent = await fs.readFile(qwenPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('Some existing content.');
expect(updatedContent).toContain('More notes here.');
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 AI tool files: QWEN.md');
consoleSpy.mockRestore();
});
it('should refresh existing Claude slash command files', async () => {
const proposalPath = path.join(
testDir,
@@ -114,6 +149,87 @@ Old slash content
consoleSpy.mockRestore();
});
it('should refresh existing Qwen slash command files', async () => {
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
await fs.mkdir(path.dirname(applyPath), { recursive: true });
const initialContent = `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Old description
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(applyPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(applyPath, 'utf-8');
expect(updated).toContain('name: /openspec-apply');
expect(updated).toContain('category: OpenSpec');
expect(updated).toContain('<!-- OPENSPEC:START -->');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
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: .qwen/commands/openspec-apply.md'
);
consoleSpy.mockRestore();
});
it('should not create missing Qwen slash command files on update', async () => {
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
await fs.mkdir(path.dirname(applyPath), { recursive: true });
await fs.writeFile(
applyPath,
`---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Old description
---
<!-- OPENSPEC:START -->
Old content
<!-- OPENSPEC:END -->`
);
await updateCommand.execute(testDir);
const updatedApply = await fs.readFile(applyPath, 'utf-8');
expect(updatedApply).toContain('Work through tasks sequentially');
expect(updatedApply).not.toContain('Old content');
const proposalPath = path.join(
testDir,
'.qwen/commands/openspec-proposal.md'
);
const archivePath = path.join(
testDir,
'.qwen/commands/openspec-archive.md'
);
await expect(FileSystemUtils.fileExists(proposalPath)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(archivePath)).resolves.toBe(false);
});
it('should not create CLAUDE.md if it does not exist', async () => {
// Ensure CLAUDE.md does not exist
const claudePath = path.join(testDir, 'CLAUDE.md');
@@ -126,6 +242,12 @@ Old slash content
expect(fileExists).toBe(false);
});
it('should not create QWEN.md if it does not exist', async () => {
const qwenPath = path.join(testDir, 'QWEN.md');
await updateCommand.execute(testDir);
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');