diff --git a/package-lock.json b/package-lock.json index f002a87..fb453be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@inquirer/prompts": "^7.9.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "gray-matter": "^4.0.3", "ora": "^9.0.0" }, "bin": { @@ -1448,6 +1449,15 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1722,6 +1732,19 @@ "@esbuild/win32-x64": "0.25.11" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1742,6 +1765,18 @@ "node": ">=12.0.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1837,6 +1872,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", @@ -1853,6 +1903,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1919,6 +1978,28 @@ "node": ">=10" } }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2406,6 +2487,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2472,6 +2566,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2601,6 +2701,15 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/package.json b/package.json index 97ad8e9..5f33b97 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@inquirer/prompts": "^7.9.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "gray-matter": "^4.0.3", "ora": "^9.0.0" }, "devDependencies": { diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts index dffc2bc..15cf857 100644 --- a/src/utils/yaml.ts +++ b/src/utils/yaml.ts @@ -1,9 +1,25 @@ +import matter from 'gray-matter'; + /** * Extract field from YAML frontmatter + * Uses gray-matter to properly parse multiline YAML descriptions (using > or | syntax) */ export function extractYamlField(content: string, field: string): string { - const match = content.match(new RegExp(`^${field}:\\s*(.+?)$`, 'm')); - return match ? match[1].trim() : ''; + try { + const result = matter(content); + const value = result.data[field]; + + // Handle different value types + if (value === undefined || value === null) { + return ''; + } + + // Convert to string and trim + return String(value).trim(); + } catch { + // Fallback to empty string if parsing fails + return ''; + } } /** diff --git a/tests/utils/yaml.test.ts b/tests/utils/yaml.test.ts index 35e9574..01c088f 100644 --- a/tests/utils/yaml.test.ts +++ b/tests/utils/yaml.test.ts @@ -22,7 +22,7 @@ name: test-skill expect(extractYamlField(content, 'missing')).toBe(''); }); - it('should handle multiline descriptions', () => { + it('should handle single-line descriptions', () => { const content = `--- name: test description: First line @@ -31,6 +31,57 @@ description: First line expect(extractYamlField(content, 'description')).toBe('First line'); }); + it('should parse multiline YAML description with folded scalar (>)', () => { + // This tests the exact bug from issue #69 + const content = `--- +name: test-multiline-skill +description: > + This is a comprehensive guide for using this skill effectively. Use this skill when: + (1) setting up your development environment, + (2) configuring core services and dependencies, + (3) implementing authentication and caching strategies, + (4) writing queries, mutations, or other operations, +---`; + + const desc = extractYamlField(content, 'description'); + expect(desc).toContain('This is a comprehensive guide'); + expect(desc).toContain('setting up your development environment'); + expect(desc).toContain('configuring core services'); + expect(desc).not.toBe('>'); // Should not just capture the '>' character + }); + + it('should parse multiline YAML description with literal scalar (|)', () => { + const content = `--- +name: test-skill +description: | + Line 1 + Line 2 + Line 3 +---`; + + const desc = extractYamlField(content, 'description'); + expect(desc).toContain('Line 1'); + expect(desc).toContain('Line 2'); + expect(desc).toContain('Line 3'); + }); + + it('should handle complex multiline with indentation', () => { + const content = `--- +name: complex-skill +description: > + This is a very long description + that spans multiple lines + with consistent indentation. + + It even has a blank line above. +---`; + + const desc = extractYamlField(content, 'description'); + expect(desc).toContain('This is a very long description'); + expect(desc).toContain('that spans multiple lines'); + expect(desc.length).toBeGreaterThan(50); // Should be much longer than just '>' + }); + // Security tests for non-greedy regex it('should use non-greedy matching (security)', () => { // With greedy regex (.+), this could match across lines incorrectly @@ -72,12 +123,13 @@ description: Contains "quotes" and 'apostrophes' it('should handle colons in values', () => { const content = `--- name: my-skill -description: URL: https://example.com +description: "URL: https://example.com" ---`; // Should capture the full value including the colon const desc = extractYamlField(content, 'description'); expect(desc).toContain('URL:'); + expect(desc).toContain('https://example.com'); }); });