From 18fa503d44ed683b7130b3b569c91e158a04815d Mon Sep 17 00:00:00 2001 From: Ryanba <92616678+Gujiassh@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:19:50 +0800 Subject: [PATCH] fix: normalize quoted user-invocable frontmatter (#87) * fix: normalize quoted user-invokable frontmatter Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus * fix: preserve quoted non-boolean frontmatter values Only normalize quoted booleans for the user-invocable frontmatter flag so other quoted fields like argument-hint and description continue to round-trip as plain strings. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --------- Co-authored-by: Sisyphus --- scripts/lib/utils.js | 16 +++++++++++----- tests/lib/utils.test.js | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index a277f6b73..7f14a8c3e 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -101,13 +101,19 @@ export function parseFrontmatter(content) { if (colonIndex > 0) { const key = trimmed.slice(0, colonIndex).trim(); const value = trimmed.slice(colonIndex + 1).trim(); + const isQuoted = /^(".*"|'.*')$/.test(value); + const unquotedValue = isQuoted ? value.slice(1, -1) : value; + const shouldCoerceBoolean = + key === 'user-invocable' || key === 'user-invokable' || !isQuoted; if (value) { - // Strip YAML quotes - const unquoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) - ? value.slice(1, -1) - : value; - frontmatter[key] = unquoted === 'true' ? true : unquoted === 'false' ? false : unquoted; + frontmatter[key] = shouldCoerceBoolean + ? unquotedValue === 'true' + ? true + : unquotedValue === 'false' + ? false + : unquotedValue + : unquotedValue; currentKey = key; currentArray = null; } else { diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 38bfe9f0c..b0b6473ca 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -89,7 +89,7 @@ Body.`; expect(result.frontmatter['user-invocable']).toBe(true); }); - test('should parse user-invocable as string true (code behavior)', () => { + test('should parse quoted user-invocable boolean as true', () => { const content = `--- name: test-skill user-invocable: 'true' @@ -98,10 +98,21 @@ user-invocable: 'true' Body.`; const result = parseFrontmatter(content); - // parseFrontmatter strips YAML quotes, so 'true' becomes boolean true expect(result.frontmatter['user-invocable']).toBe(true); }); + test('should keep quoted non-user-invocable booleans as plain strings', () => { + const content = `--- +name: test-skill +description: 'true' +--- + +Body.`; + + const result = parseFrontmatter(content); + expect(result.frontmatter.description).toBe('true'); + }); + test('should parse allowed-tools field', () => { const content = `--- name: test-skill @@ -393,6 +404,25 @@ description: Run technical quality checks user-invocable: true --- +Audit the code.`; + + const skillDir = path.join(testRootDir, 'source/skills/audit'); + ensureDir(skillDir); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + + const { skills } = readSourceFiles(testRootDir); + + expect(skills).toHaveLength(1); + expect(skills[0].userInvocable).toBe(true); + }); + + test('should read skill with quoted user-invocable flag', () => { + const skillContent = `--- +name: audit +description: Run technical quality checks +user-invocable: 'true' +--- + Audit the code.`; const skillDir = path.join(testRootDir, 'source/skills/audit');