mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f3e57cc8 | ||
|
|
35ce49ff1c | ||
|
|
b770fdc7e0 | ||
|
|
5c2e8f4465 | ||
|
|
18df13b9d1 | ||
|
|
fe99418783 | ||
|
|
8f379e4ef8 | ||
|
|
dbd11f9a6e |
@@ -31,6 +31,7 @@ For each tool you select, OpenSpec installs:
|
||||
| Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` |
|
||||
| Kiro | `.kiro/skills/` | `.kiro/prompts/` |
|
||||
| OpenCode | `.opencode/skills/` | `.opencode/command/` |
|
||||
| Pi | `.pi/skills/` | `.pi/prompts/` |
|
||||
| Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` |
|
||||
| Qwen Code | `.qwen/skills/` | `.qwen/commands/` |
|
||||
| RooCode | `.roo/skills/` | `.roo/commands/` |
|
||||
@@ -56,7 +57,7 @@ openspec init --tools all
|
||||
openspec init --tools none
|
||||
```
|
||||
|
||||
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
|
||||
Generated
+4967
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ export { iflowAdapter } from './iflow.js';
|
||||
export { kilocodeAdapter } from './kilocode.js';
|
||||
export { kiroAdapter } from './kiro.js';
|
||||
export { opencodeAdapter } from './opencode.js';
|
||||
export { piAdapter } from './pi.js';
|
||||
export { qoderAdapter } from './qoder.js';
|
||||
export { qwenAdapter } from './qwen.js';
|
||||
export { roocodeAdapter } from './roocode.js';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Pi Command Adapter
|
||||
*
|
||||
* Formats commands for Pi (pi.dev) following its prompt template specification.
|
||||
* Pi prompt templates live in .pi/prompts/*.md with description frontmatter.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
|
||||
/**
|
||||
* Escapes a string value for safe YAML output.
|
||||
* Quotes the string if it contains special YAML characters.
|
||||
*/
|
||||
function escapeYamlValue(value: string): string {
|
||||
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
||||
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
||||
if (needsQuoting) {
|
||||
// Use double quotes and escape internal double quotes and backslashes
|
||||
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pi adapter for prompt template generation.
|
||||
* File path: .pi/prompts/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*/
|
||||
export const piAdapter: ToolCommandAdapter = {
|
||||
toolId: 'pi',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.pi', 'prompts', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
return `---
|
||||
description: ${escapeYamlValue(content.description)}
|
||||
---
|
||||
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -24,6 +24,7 @@ import { iflowAdapter } from './adapters/iflow.js';
|
||||
import { kilocodeAdapter } from './adapters/kilocode.js';
|
||||
import { kiroAdapter } from './adapters/kiro.js';
|
||||
import { opencodeAdapter } from './adapters/opencode.js';
|
||||
import { piAdapter } from './adapters/pi.js';
|
||||
import { qoderAdapter } from './adapters/qoder.js';
|
||||
import { qwenAdapter } from './adapters/qwen.js';
|
||||
import { roocodeAdapter } from './adapters/roocode.js';
|
||||
@@ -55,6 +56,7 @@ export class CommandAdapterRegistry {
|
||||
CommandAdapterRegistry.register(kilocodeAdapter);
|
||||
CommandAdapterRegistry.register(kiroAdapter);
|
||||
CommandAdapterRegistry.register(opencodeAdapter);
|
||||
CommandAdapterRegistry.register(piAdapter);
|
||||
CommandAdapterRegistry.register(qoderAdapter);
|
||||
CommandAdapterRegistry.register(qwenAdapter);
|
||||
CommandAdapterRegistry.register(roocodeAdapter);
|
||||
|
||||
@@ -36,6 +36,7 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
|
||||
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
||||
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
||||
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
|
||||
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
||||
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
|
||||
|
||||
@@ -18,6 +18,7 @@ import { githubCopilotAdapter } from '../../../src/core/command-generation/adapt
|
||||
import { iflowAdapter } from '../../../src/core/command-generation/adapters/iflow.js';
|
||||
import { kilocodeAdapter } from '../../../src/core/command-generation/adapters/kilocode.js';
|
||||
import { opencodeAdapter } from '../../../src/core/command-generation/adapters/opencode.js';
|
||||
import { piAdapter } from '../../../src/core/command-generation/adapters/pi.js';
|
||||
import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js';
|
||||
import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js';
|
||||
import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js';
|
||||
@@ -523,6 +524,48 @@ describe('command-generation/adapters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('piAdapter', () => {
|
||||
it('should have correct toolId', () => {
|
||||
expect(piAdapter.toolId).toBe('pi');
|
||||
});
|
||||
|
||||
it('should generate correct file path', () => {
|
||||
const filePath = piAdapter.getFilePath('explore');
|
||||
expect(filePath).toBe(path.join('.pi', 'prompts', 'opsx-explore.md'));
|
||||
});
|
||||
|
||||
it('should generate correct file paths for different commands', () => {
|
||||
expect(piAdapter.getFilePath('new')).toBe(path.join('.pi', 'prompts', 'opsx-new.md'));
|
||||
expect(piAdapter.getFilePath('bulk-archive')).toBe(path.join('.pi', 'prompts', 'opsx-bulk-archive.md'));
|
||||
});
|
||||
|
||||
it('should format file with description frontmatter', () => {
|
||||
const output = piAdapter.formatFile(sampleContent);
|
||||
expect(output).toContain('---\n');
|
||||
expect(output).toContain('description: Enter explore mode for thinking');
|
||||
expect(output).toContain('---\n\n');
|
||||
expect(output).toContain('This is the command body.');
|
||||
});
|
||||
|
||||
it('should escape YAML special characters in description', () => {
|
||||
const contentWithSpecialChars: CommandContent = {
|
||||
...sampleContent,
|
||||
description: 'Fix: regression in "auth" feature',
|
||||
};
|
||||
const output = piAdapter.formatFile(contentWithSpecialChars);
|
||||
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
|
||||
});
|
||||
|
||||
it('should escape newlines in description', () => {
|
||||
const contentWithNewline: CommandContent = {
|
||||
...sampleContent,
|
||||
description: 'Line 1\nLine 2',
|
||||
};
|
||||
const output = piAdapter.formatFile(contentWithNewline);
|
||||
expect(output).toContain('description: "Line 1\\nLine 2"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('roocodeAdapter', () => {
|
||||
it('should have correct toolId', () => {
|
||||
expect(roocodeAdapter.toolId).toBe('roocode');
|
||||
@@ -566,7 +609,7 @@ describe('command-generation/adapters', () => {
|
||||
amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter,
|
||||
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
|
||||
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
|
||||
iflowAdapter, kilocodeAdapter, opencodeAdapter, qoderAdapter,
|
||||
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
|
||||
qwenAdapter, roocodeAdapter
|
||||
];
|
||||
for (const adapter of adapters) {
|
||||
|
||||
@@ -902,16 +902,16 @@ ${OPENSPEC_MARKERS.end}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover all tools from the CommandAdapterRegistry', () => {
|
||||
const expectedTools = CommandAdapterRegistry.getAll().map(adapter => adapter.toolId);
|
||||
it('should only include legacy tool IDs that are present in the CommandAdapterRegistry', () => {
|
||||
const registeredTools = new Set(CommandAdapterRegistry.getAll().map(adapter => adapter.toolId));
|
||||
|
||||
// Verify all adapters have legacy paths
|
||||
for (const tool of expectedTools) {
|
||||
expect(LEGACY_SLASH_COMMAND_PATHS).toHaveProperty(tool);
|
||||
// Verify all legacy map entries correspond to known adapters
|
||||
for (const tool of Object.keys(LEGACY_SLASH_COMMAND_PATHS)) {
|
||||
expect(registeredTools.has(tool)).toBe(true);
|
||||
}
|
||||
|
||||
// Verify counts match
|
||||
expect(expectedTools.length).toBe(Object.keys(LEGACY_SLASH_COMMAND_PATHS).length);
|
||||
// Pi was never a pre-1.0 legacy tool
|
||||
expect(LEGACY_SLASH_COMMAND_PATHS).not.toHaveProperty('pi');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user