From 7f815188967de12fad159e475f16af4691544594 Mon Sep 17 00:00:00 2001 From: Wendy Smoak Date: Sun, 15 Feb 2026 07:13:30 -0500 Subject: [PATCH] Replace description regex with yaml.parse/stringify in codex.js Use the yaml library (already a project dependency) instead of a hand-rolled regex to parse and re-serialize frontmatter descriptions, matching the pattern used in manifest-generator.js. Update tests to validate round-trip correctness rather than exact quoting style. Co-Authored-By: Claude Opus 4.6 --- test/test-codex-transform.js | 58 ++++++++++++++++++++------- tools/cli/installers/lib/ide/codex.js | 27 +++++-------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/test/test-codex-transform.js b/test/test-codex-transform.js index 897ff2196..63db245bf 100644 --- a/test/test-codex-transform.js +++ b/test/test-codex-transform.js @@ -1,12 +1,14 @@ /** * Tests for CodexSetup.transformToSkillFormat * - * Demonstrates that the description regex mangles descriptions containing quotes. + * Validates that descriptions round-trip correctly through parse/stringify, + * producing valid YAML regardless of input quoting style. * * Usage: node test/test-codex-transform.js */ const path = require('node:path'); +const yaml = require('yaml'); // ANSI colors const colors = { @@ -31,6 +33,17 @@ function assert(condition, testName, detail) { } } +/** + * Parse the output frontmatter and return the description value. + * Validates the output is well-formed YAML that parses back correctly. + */ +function parseOutputDescription(output) { + const match = output.match(/^---\n([\s\S]*?)\n---/); + if (!match) return null; + const parsed = yaml.parse(match[1]); + return parsed?.description; +} + // Import the class under test const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js')); @@ -38,39 +51,56 @@ const setup = new CodexSetup(); console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`); -// --- Passing case: simple description, no quotes --- +// --- 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)}`); + const desc = parseOutputDescription(result); + assert(desc === 'A simple description', 'simple description round-trips', `got description: ${JSON.stringify(desc)}`); + assert(result.includes('\nBody content here.'), 'body preserved for simple description'); } // --- 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)}`); + const desc = parseOutputDescription(result); + assert(desc === "can't stop won't stop", 'description with apostrophes round-trips', `got description: ${JSON.stringify(desc)}`); + assert(result.includes('\nBody content here.'), 'body preserved for quoted description'); } -// --- Description with embedded single quote produces valid YAML --- +// --- Description with embedded single quote --- { const input = `---\ndescription: "it's a test"\n---\n\nBody.`; const result = setup.transformToSkillFormat(input, 'test-skill'); - const expected = `---\nname: test-skill\ndescription: 'it''s a test'\n---\n\nBody.`; - assert(result === expected, 'description with apostrophe produces valid YAML', `got: ${JSON.stringify(result)}`); + const desc = parseOutputDescription(result); + assert(desc === "it's a test", 'description with apostrophe round-trips', `got description: ${JSON.stringify(desc)}`); } // --- 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)}`); + const desc = parseOutputDescription(result); + assert(desc === "don't panic", 'single-quoted escaped apostrophe round-trips', `got description: ${JSON.stringify(desc)}`); +} + +// --- Verify name is set correctly --- +{ + const input = `---\ndescription: test\n---\n\nBody.`; + const result = setup.transformToSkillFormat(input, 'my-custom-skill'); + const match = result.match(/^---\n([\s\S]*?)\n---/); + const parsed = yaml.parse(match[1]); + assert(parsed.name === 'my-custom-skill', 'name field matches skillName argument', `got name: ${JSON.stringify(parsed.name)}`); +} + +// --- No frontmatter wraps content --- +{ + const input = 'Just some content without frontmatter.'; + const result = setup.transformToSkillFormat(input, 'bare-skill'); + const desc = parseOutputDescription(result); + assert(desc === 'bare-skill', 'no-frontmatter fallback uses skillName as description', `got description: ${JSON.stringify(desc)}`); + assert(result.includes('Just some content without frontmatter.'), 'body preserved when no frontmatter'); } // --- Summary --- diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 01988758e..07be97741 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -7,6 +7,7 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { getTasksFromBmad } = require('./shared/bmad-artifacts'); const { toDashPath, customAgentDashName } = require('./shared/path-utils'); +const yaml = require('yaml'); const prompts = require('../../../lib/prompts'); /** @@ -205,28 +206,18 @@ class CodexSetup extends BaseIdeSetup { const frontmatter = fmMatch[1]; const body = fmMatch[2]; - // Extract description from existing frontmatter, handling quoted and unquoted values - const descMatch = frontmatter.match(/^description:\s*(?:'((?:[^']|'')*)'|"((?:[^"\\]|\\.)*)"|(.*))\s*$/m); + // Parse frontmatter with yaml library to handle all quoting variants 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 { + try { + const parsed = yaml.parse(frontmatter); + description = parsed?.description || `${skillName} skill`; + } catch { 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: '${safeDescription}'\n---\n${body}`; + // Build new frontmatter with only skills-spec fields, let yaml handle quoting + const newFrontmatter = yaml.stringify({ name: skillName, description }).trimEnd(); + return `---\n${newFrontmatter}\n---\n${body}`; } /**