diff --git a/src/core/init.ts b/src/core/init.ts index 95728dc7e..301f6054a 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -11,7 +11,7 @@ import ora from 'ora'; import * as fs from 'fs'; import { createRequire } from 'module'; import { FileSystemUtils } from '../utils/file-system.js'; -import { transformToHyphenCommands } from '../utils/command-references.js'; +import { transformToHyphenCommands, transformToSkillReferences } from '../utils/command-references.js'; import { AI_TOOLS, OPENSPEC_DIR_NAME, @@ -537,8 +537,11 @@ export class InitCommand { const skillFile = path.join(skillDir, 'SKILL.md'); // Generate SKILL.md content with YAML frontmatter including generatedBy - // Use hyphen-based command references for OpenCode - const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; + const transformer = tool.value === 'opencode' + ? transformToHyphenCommands + : delivery === 'skills' + ? transformToSkillReferences + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); // Write the skill file diff --git a/src/core/update.ts b/src/core/update.ts index 62db8a08f..e3cbea827 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -11,7 +11,7 @@ import ora from 'ora'; import * as fs from 'fs'; import { createRequire } from 'module'; import { FileSystemUtils } from '../utils/file-system.js'; -import { transformToHyphenCommands } from '../utils/command-references.js'; +import { transformToHyphenCommands, transformToSkillReferences } from '../utils/command-references.js'; import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js'; import { generateCommands, @@ -194,8 +194,11 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; + const transformer = tool.value === 'opencode' + ? transformToHyphenCommands + : delivery === 'skills' + ? transformToSkillReferences + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } @@ -665,8 +668,11 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined; + const transformer = tool.value === 'opencode' + ? transformToHyphenCommands + : delivery === 'skills' + ? transformToSkillReferences + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } diff --git a/src/utils/command-references.ts b/src/utils/command-references.ts index bfa49b9ff..5d9db6218 100644 --- a/src/utils/command-references.ts +++ b/src/utils/command-references.ts @@ -4,6 +4,20 @@ * Utilities for transforming command references to tool-specific formats. */ +const OPSX_TO_SKILL_REFERENCE: Record = { + 'apply': '/openspec-apply-change', + 'archive': '/openspec-archive-change', + 'verify': '/openspec-verify-change', + 'continue': '/openspec-continue-change', + 'propose': '/openspec-propose', + 'explore': '/openspec-explore', + 'ff': '/openspec-ff-change', + 'new': '/openspec-new-change', + 'sync': '/openspec-sync-specs', + 'bulk-archive': '/openspec-bulk-archive-change', + 'onboard': '/openspec-onboard', +}; + /** * Transforms colon-based command references to hyphen-based format. * Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax. @@ -18,3 +32,15 @@ export function transformToHyphenCommands(text: string): string { return text.replace(/\/opsx:/g, '/opsx-'); } + +/** + * Transforms `/opsx:*` command references to skill references for skills-only delivery. + * + * @param text - The text containing command references + * @returns Text with known command references replaced by skill references + */ +export function transformToSkillReferences(text: string): string { + return text.replace(/\/opsx:([a-z-]+)/g, (match, workflowId: string) => { + return OPSX_TO_SKILL_REFERENCE[workflowId] ?? match; + }); +} diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6af92aed2..b194bd037 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -685,8 +685,11 @@ describe('InitCommand - profile and detection features', () => { await initCommand.execute(testDir); // Skills should exist - const skillFile = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'); + const skillFile = path.join(testDir, '.claude', 'skills', 'openspec-propose', 'SKILL.md'); expect(await fileExists(skillFile)).toBe(true); + const skillContent = await fs.readFile(skillFile, 'utf-8'); + expect(skillContent).toContain('/openspec-apply-change'); + expect(skillContent).not.toContain('/opsx:apply'); // Commands should NOT exist const cmdFile = path.join(testDir, '.claude', 'commands', 'opsx', 'explore.md'); diff --git a/test/utils/command-references.test.ts b/test/utils/command-references.test.ts index c7ff2ed85..fd8c2920b 100644 --- a/test/utils/command-references.test.ts +++ b/test/utils/command-references.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { transformToHyphenCommands } from '../../src/utils/command-references.js'; +import { transformToHyphenCommands, transformToSkillReferences } from '../../src/utils/command-references.js'; describe('transformToHyphenCommands', () => { describe('basic transformations', () => { @@ -81,3 +81,41 @@ Finally /opsx-apply to implement`; } }); }); + +describe('transformToSkillReferences', () => { + it('should transform known workflow references to skill references', () => { + const input = 'Use /opsx:apply then /opsx:archive'; + const expected = 'Use /openspec-apply-change then /openspec-archive-change'; + expect(transformToSkillReferences(input)).toBe(expected); + }); + + it('should transform all known workflow references', () => { + const mappings = { + '/opsx:apply': '/openspec-apply-change', + '/opsx:archive': '/openspec-archive-change', + '/opsx:verify': '/openspec-verify-change', + '/opsx:continue': '/openspec-continue-change', + '/opsx:propose': '/openspec-propose', + '/opsx:explore': '/openspec-explore', + '/opsx:ff': '/openspec-ff-change', + '/opsx:new': '/openspec-new-change', + '/opsx:sync': '/openspec-sync-specs', + '/opsx:bulk-archive': '/openspec-bulk-archive-change', + '/opsx:onboard': '/openspec-onboard', + }; + + for (const [input, expected] of Object.entries(mappings)) { + expect(transformToSkillReferences(input)).toBe(expected); + } + }); + + it('should leave unknown workflow references unchanged', () => { + const input = 'Unknown /opsx:unknown should stay untouched'; + expect(transformToSkillReferences(input)).toBe(input); + }); + + it('should leave non-command text unchanged', () => { + const input = 'This is plain text without command references'; + expect(transformToSkillReferences(input)).toBe(input); + }); +});