Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,4 @@ CLAUDE.md
.DS_Store

# Pnpm
.pnpm-store/
.pnpm-store/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 47 additions & 0 deletions src/core/configurators/qwen.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/core/configurators/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -16,13 +17,15 @@ 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);
this.tools.set('codebuddy', codeBuddyConfigurator);
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 {
Expand Down
80 changes: 80 additions & 0 deletions src/core/configurators/slash/qwen.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/core/configurators/slash/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
77 changes: 77 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,71 @@ 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 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 rule files with templates', async () => {
queueSelections('cline', DONE);

Expand Down Expand Up @@ -688,6 +753,18 @@ 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);
Expand Down
Loading
Loading