From 26dd80e021117abce32124403419e31c009e267d Mon Sep 17 00:00:00 2001 From: Wendy Smoak Date: Sat, 14 Feb 2026 19:31:04 -0500 Subject: [PATCH] Fix description regex to handle quotes in transformToSkillFormat The regex that extracts descriptions from YAML frontmatter did not properly parse quoted values. Descriptions containing apostrophes (e.g. "can't") produced invalid YAML when re-wrapped in single quotes. Replace the naive regex with proper handling of single-quoted, double-quoted, and unquoted YAML values, and escape inner single quotes using YAML '' syntax before re-wrapping. Add tests for transformToSkillFormat covering plain, double-quoted, and single-quoted descriptions with embedded apostrophes. --- test/test-codex-transform.js | 90 +++++++++++++++++++++++++++ tools/cli/installers/lib/ide/codex.js | 24 +++++-- 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 test/test-codex-transform.js diff --git a/test/test-codex-transform.js b/test/test-codex-transform.js new file mode 100644 index 000000000..ff8c5c936 --- /dev/null +++ b/test/test-codex-transform.js @@ -0,0 +1,90 @@ +/** + * Tests for CodexSetup.transformToSkillFormat + * + * Demonstrates that the description regex mangles descriptions containing quotes. + * + * Usage: node test/test-codex-transform.js + */ + +const path = require('node:path'); + +// ANSI colors +const colors = { + reset: '\u001B[0m', + green: '\u001B[32m', + red: '\u001B[31m', + cyan: '\u001B[36m', + dim: '\u001B[2m', +}; + +let passed = 0; +let failed = 0; + +function assert(condition, testName, detail) { + if (condition) { + console.log(` ${colors.green}PASS${colors.reset} ${testName}`); + passed++; + } else { + console.log(` ${colors.red}FAIL${colors.reset} ${testName}`); + if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`); + failed++; + } +} + +// Import the class under test +const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js')); + +const setup = new CodexSetup(); + +console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`); + +// --- Passing case: simple description, no quotes --- +{ + const input = `---\ndescription: A simple description\n---\n\nBody content here.`; + const result = setup.transformToSkillFormat(input, 'my-skill'); + const expected = `---\nname: my-skill\ndescription: 'A simple description'\n---\n\nBody content here.`; + assert(result === expected, 'simple description without quotes', `got: ${JSON.stringify(result)}`); +} + +// --- Description with embedded single quotes (from double-quoted YAML input) --- +{ + const input = `---\ndescription: "can't stop won't stop"\n---\n\nBody content here.`; + const result = setup.transformToSkillFormat(input, 'my-skill'); + + // Output should have properly escaped YAML single-quoted scalar: '' for each ' + const expected = `---\nname: my-skill\ndescription: 'can''t stop won''t stop'\n---\n\nBody content here.`; + assert(result === expected, 'description with embedded single quotes produces valid escaped YAML', `got: ${JSON.stringify(result)}`); +} + +// --- Description with embedded single quote produces valid YAML --- +{ + const input = `---\ndescription: "it's a test"\n---\n\nBody.`; + const result = setup.transformToSkillFormat(input, 'test-skill'); + + // Verify the inner quote is escaped: description: 'it''s a test' + const descLine = result.split('\n').find((l) => l.startsWith('description:')); + const value = descLine.replace('description: ', ''); + + // Check that single quotes inside the value are properly escaped + // In YAML single-quoted scalars, a literal ' must be written as '' + const innerContent = value.slice(1, -1); // strip outer quotes + const hasUnescapedQuote = innerContent.match(/[^']'[^']/); + assert( + !hasUnescapedQuote, + 'description with apostrophe produces valid YAML (no unescaped inner quotes)', + `description line: ${descLine}`, + ); +} + +// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) --- +{ + const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`; + const result = setup.transformToSkillFormat(input, 'test-skill'); + // Input has don''t (YAML-escaped). Should round-trip to don''t in output. + const expected = `---\nname: test-skill\ndescription: 'don''t panic'\n---\n\nBody.`; + assert(result === expected, 'single-quoted description with escaped apostrophe round-trips correctly', `got: ${JSON.stringify(result)}`); +} + +// --- Summary --- +console.log(`\n${passed} passed, ${failed} failed\n`); +process.exit(failed > 0 ? 1 : 0); diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index a81af0e0d..01988758e 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -205,12 +205,28 @@ class CodexSetup extends BaseIdeSetup { const frontmatter = fmMatch[1]; const body = fmMatch[2]; - // Extract description from existing frontmatter - const descMatch = frontmatter.match(/^description:\s*['"]?(.*?)['"]?\s*$/m); - const description = descMatch ? descMatch[1] : `${skillName} skill`; + // Extract description from existing frontmatter, handling quoted and unquoted values + const descMatch = frontmatter.match(/^description:\s*(?:'((?:[^']|'')*)'|"((?:[^"\\]|\\.)*)"|(.*))\s*$/m); + let description; + if (descMatch) { + if (descMatch[1] != null) { + // Single-quoted YAML: unescape '' to ' + description = descMatch[1].replaceAll("''", "'"); + } else if (descMatch[2] == null) { + description = descMatch[3]; + } else { + // Double-quoted YAML: unescape \" to " + description = descMatch[2].replaceAll(String.raw`\"`, '"'); + } + } else { + description = `${skillName} skill`; + } + + // Escape single quotes for YAML single-quoted scalar (a literal ' becomes '') + const safeDescription = description.replaceAll("'", "''"); // Build new frontmatter with only skills-spec fields - return `---\nname: ${skillName}\ndescription: '${description}'\n---\n${body}`; + return `---\nname: ${skillName}\ndescription: '${safeDescription}'\n---\n${body}`; } /**