diff --git a/bin/install.js b/bin/install.js index ebbaa4c68..869821dd0 100755 --- a/bin/install.js +++ b/bin/install.js @@ -710,6 +710,14 @@ function yamlQuote(value) { return JSON.stringify(value); } +function yamlIdentifier(value) { + const text = String(value).trim(); + if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) { + return text; + } + return yamlQuote(text); +} + function extractFrontmatterAndBody(content) { if (!content.startsWith('---')) { return { frontmatter: null, body: content }; @@ -828,7 +836,7 @@ function convertClaudeCommandToCursorSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); - return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** @@ -845,7 +853,7 @@ function convertClaudeAgentToCursorAgent(content) { const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; - const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; + const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } @@ -3257,7 +3265,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { // Test-only exports — skip main logic when loaded as a module for testing if (process.env.GSD_TEST_MODE) { module.exports = { + yamlIdentifier, getCodexSkillAdapterHeader, + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, convertClaudeToGeminiAgent, convertClaudeAgentToCodexAgent, generateCodexAgentToml, diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs new file mode 100644 index 000000000..87652ab96 --- /dev/null +++ b/tests/cursor-conversion.test.cjs @@ -0,0 +1,61 @@ +/** + * Cursor conversion regression tests. + * + * Ensures Cursor frontmatter names are emitted as plain identifiers + * (without surrounding quotes), so Cursor does not treat quotes as + * literal parts of skill/subagent names. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +const { + convertClaudeCommandToCursorSkill, + convertClaudeAgentToCursorAgent, +} = require('../bin/install.js'); + +describe('convertClaudeCommandToCursorSkill', () => { + test('writes unquoted Cursor skill name in frontmatter', () => { + const input = `--- +name: quick +description: Execute a quick task +--- + + +Test body + +`; + + const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick'); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar'); + assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted'); + }); +}); + +describe('convertClaudeAgentToCursorAgent', () => { + test('writes unquoted Cursor agent name in frontmatter', () => { + const input = `--- +name: gsd-planner +description: Planner agent +tools: Read, Write +color: green +--- + + +Planner body + +`; + + const result = convertClaudeAgentToCursorAgent(input); + const nameMatch = result.match(/^name:\s*(.+)$/m); + + assert.ok(nameMatch, 'frontmatter contains name field'); + assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar'); + assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted'); + }); +});