Skip to content
Open
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
1 change: 1 addition & 0 deletions src/core/command-generation/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export { lingmaAdapter } from './lingma.js';
export { qwenAdapter } from './qwen.js';
export { roocodeAdapter } from './roocode.js';
export { windsurfAdapter } from './windsurf.js';
export { zcodeAdapter } from './zcode.js';
34 changes: 34 additions & 0 deletions src/core/command-generation/adapters/zcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* ZCode Command Adapter
*
* Formats commands for ZCode under its project-local command directory.
*/

import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';

/**
* ZCode adapter for command generation.
* File path: .zcode/commands/opsx/<id>.md
* Frontmatter: name, description, category, tags
*/
export const zcodeAdapter: ToolCommandAdapter = {
toolId: 'zcode',

getFilePath(commandId: string): string {
return path.join('.zcode', 'commands', 'opsx', `${commandId}.md`);
},

formatFile(content: CommandContent): string {
const tagsStr = content.tags.join(', ');
return `---
name: ${content.name}
description: ${content.description}
category: ${content.category}
tags: [${tagsStr}]
---
${content.body}
`;
},
};
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { lingmaAdapter } from './adapters/lingma.js';
import { qwenAdapter } from './adapters/qwen.js';
import { roocodeAdapter } from './adapters/roocode.js';
import { windsurfAdapter } from './adapters/windsurf.js';
import { zcodeAdapter } from './adapters/zcode.js';

/**
* Registry for looking up tool command adapters.
Expand Down Expand Up @@ -67,6 +68,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(qwenAdapter);
CommandAdapterRegistry.register(roocodeAdapter);
CommandAdapterRegistry.register(windsurfAdapter);
CommandAdapterRegistry.register(zcodeAdapter);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' },
{ name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
11 changes: 11 additions & 0 deletions test/core/available-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,16 @@ describe('available-tools', () => {
expect(vibeTool?.name).toBe('Mistral Vibe');
expect(vibeTool?.skillsDir).toBe('.vibe');
});

it('should detect ZCode when .zcode directory exists', async () => {
await fs.mkdir(path.join(testDir, '.zcode'), { recursive: true });

const tools = getAvailableTools(testDir);
const zcodeTool = tools.find((t) => t.value === 'zcode');

expect(zcodeTool).toBeDefined();
expect(zcodeTool?.name).toBe('ZCode');
expect(zcodeTool?.skillsDir).toBe('.zcode');
});
});
});
25 changes: 24 additions & 1 deletion test/core/command-generation/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { qoderAdapter } from '../../../src/core/command-generation/adapters/qode
import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js';
import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js';
import { windsurfAdapter } from '../../../src/core/command-generation/adapters/windsurf.js';
import { zcodeAdapter } from '../../../src/core/command-generation/adapters/zcode.js';
import type { CommandContent } from '../../../src/core/command-generation/types.js';

describe('command-generation/adapters', () => {
Expand Down Expand Up @@ -126,6 +127,28 @@ describe('command-generation/adapters', () => {
});
});

describe('zcodeAdapter', () => {
it('should have correct toolId', () => {
expect(zcodeAdapter.toolId).toBe('zcode');
});

it('should generate correct file path with nested opsx folder', () => {
const filePath = zcodeAdapter.getFilePath('explore');
expect(filePath).toBe(path.join('.zcode', 'commands', 'opsx', 'explore.md'));
});

it('should format file with name, description, category, and tags', () => {
const output = zcodeAdapter.formatFile(sampleContent);
expect(output).toContain('---\n');
expect(output).toContain('name: OpenSpec Explore');
expect(output).toContain('description: Enter explore mode for thinking');
expect(output).toContain('category: Workflow');
expect(output).toContain('tags: [workflow, explore, experimental]');
expect(output).toContain('---\n\n');
expect(output).toContain('This is the command body.');
});
});

describe('amazonQAdapter', () => {
it('should have correct toolId', () => {
expect(amazonQAdapter.toolId).toBe('amazon-q');
Expand Down Expand Up @@ -698,7 +721,7 @@ describe('command-generation/adapters', () => {
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
qwenAdapter, roocodeAdapter
qwenAdapter, roocodeAdapter, zcodeAdapter
];
for (const adapter of adapters) {
const filePath = adapter.getFilePath('test');
Expand Down
9 changes: 9 additions & 0 deletions test/core/command-generation/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ describe('command-generation/registry', () => {
expect(adapter?.toolId).toBe('junie');
});

it('should return ZCode adapter for "zcode"', () => {
const adapter = CommandAdapterRegistry.get('zcode');
expect(adapter).toBeDefined();
expect(adapter?.toolId).toBe('zcode');
});

it('should return undefined for unregistered tool', () => {
const adapter = CommandAdapterRegistry.get('unknown-tool');
expect(adapter).toBeUndefined();
Expand All @@ -52,6 +58,7 @@ describe('command-generation/registry', () => {
expect(toolIds).toContain('claude');
expect(toolIds).toContain('cursor');
expect(toolIds).toContain('windsurf');
expect(toolIds).toContain('zcode');
});
});

Expand All @@ -61,6 +68,7 @@ describe('command-generation/registry', () => {
expect(CommandAdapterRegistry.has('cursor')).toBe(true);
expect(CommandAdapterRegistry.has('windsurf')).toBe(true);
expect(CommandAdapterRegistry.has('junie')).toBe(true);
expect(CommandAdapterRegistry.has('zcode')).toBe(true);
});

it('should return false for unregistered tools', () => {
Expand All @@ -78,6 +86,7 @@ describe('command-generation/registry', () => {
expect(claudeAdapter?.getFilePath('test')).toContain('.claude');
expect(cursorAdapter?.getFilePath('test')).toContain('.cursor');
expect(windsurfAdapter?.getFilePath('test')).toContain('.windsurf');
expect(CommandAdapterRegistry.get('zcode')?.getFilePath('test')).toContain('.zcode');
});

it('registered adapters should have working formatFile', () => {
Expand Down
16 changes: 16 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ describe('InitCommand', () => {
expect(await fileExists(skillFile)).toBe(true);
});

it('should create ZCode skills and commands', async () => {
const initCommand = new InitCommand({ tools: 'zcode', force: true });

await initCommand.execute(testDir);

const skillFile = path.join(testDir, '.zcode', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillFile)).toBe(true);

const commandFile = path.join(testDir, '.zcode', 'commands', 'opsx', 'explore.md');
expect(await fileExists(commandFile)).toBe(true);

const commandContent = await fs.readFile(commandFile, 'utf-8');
expect(commandContent).toContain('name: OPSX: Explore');
expect(commandContent).toContain('description: Enter explore mode');
});

it('should support Kimi CLI as an adapterless skills-only tool', async () => {
saveGlobalConfig({
featureFlags: {},
Expand Down