Files
longbridge__skills/scripts/build-skills-json.ts
Jason Lee 4ce7a1998a fix(build): handle YAML folded block scalar (>) in frontmatter parser (#32)
## Summary

- `build-skills-json.ts` 的自定义 YAML 解析器只处理了 `|`(literal block scalar),未处理
`>` (folded block scalar)
-
`longbridge-catalyst-radar`、`longbridge-earnings`、`longbridge-earnings-preview`
三个 skill 的 `description` 字段使用了 `>` 语法,导致生成的 `skills.json` 中这些 skill 的
`description` 被输出为字面量 `">"`
- 修复后,`>` folded scalar 的内容会正确折叠(段内换行合并为空格,空行保留为段落分隔符)

## Test plan

- [ ] 运行 `bun run scripts/build-skills-json.ts --output
/tmp/test.json`,确认三个 skill 的 description 内容正常
- [ ] 合并后 CI 触发,确认 developers repo 收到正确的 PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:32:19 +08:00

125 lines
3.7 KiB
TypeScript

#!/usr/bin/env bun
/**
* Generate skills.json from all skills/<slug>/SKILL.md frontmatter.
* Usage: bun run scripts/build-skills-json.ts [--output PATH]
*/
import { readdirSync } from "fs"
import { join } from "path"
const REPO_ROOT = join(import.meta.dir, "..")
const SKILLS_DIR = join(REPO_ROOT, "skills")
interface SkillEntry {
slug: string
name: string
description: string
metadata: Record<string, unknown>
}
function parseFrontmatter(content: string): Record<string, unknown> | null {
if (!content.startsWith("---\n")) return null
const end = content.indexOf("\n---\n", 4)
if (end === -1) return null
return parseYaml(content.slice(4, end))
}
function parseYaml(text: string): Record<string, unknown> {
const result: Record<string, unknown> = {}
const lines = text.split("\n")
let i = 0
while (i < lines.length) {
const line = lines[i]
if (!line.trim() || line.startsWith("#")) { i++; continue }
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/)
if (!m) { i++; continue }
const [, key, rest] = m
i++
if (rest.trimEnd() === "|") {
const buf: string[] = []
while (i < lines.length && (lines[i].startsWith(" ") || lines[i] === "")) {
buf.push(lines[i].startsWith(" ") ? lines[i].slice(2) : "")
i++
}
result[key] = buf.join("\n").trimEnd()
} else if (rest.trimEnd() === ">") {
const buf: string[] = []
while (i < lines.length && (lines[i].startsWith(" ") || lines[i] === "")) {
buf.push(lines[i].startsWith(" ") ? lines[i].slice(2) : "")
i++
}
// Folded scalar: join lines with spaces, blank lines become newlines
const folded: string[] = []
let chunk: string[] = []
for (const ln of buf) {
if (ln === "") {
if (chunk.length) { folded.push(chunk.join(" ")); chunk = [] }
folded.push("")
} else {
chunk.push(ln)
}
}
if (chunk.length) folded.push(chunk.join(" "))
result[key] = folded.join("\n").trimEnd()
} else if (rest === "") {
const obj: Record<string, unknown> = {}
while (i < lines.length && lines[i].startsWith(" ")) {
const nm = lines[i].trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.+)$/)
if (nm) {
const v = nm[2].trim().replace(/^["']|["']$/g, "")
obj[nm[1]] = v === "true" ? true : v === "false" ? false : v
}
i++
}
result[key] = obj
} else {
result[key] = rest.trim().replace(/^["']|["']$/g, "")
}
}
return result
}
const outputFlag = process.argv.indexOf("--output")
const outputPath =
outputFlag !== -1 && outputFlag + 1 < process.argv.length
? process.argv[outputFlag + 1]
: join(REPO_ROOT, "skills.json")
const skills: SkillEntry[] = []
let errors = 0
for (const entry of readdirSync(SKILLS_DIR, { withFileTypes: true }).sort((a, b) =>
a.name.localeCompare(b.name),
)) {
if (!entry.isDirectory()) continue
const skillMdPath = join(SKILLS_DIR, entry.name, "SKILL.md")
const file = Bun.file(skillMdPath)
if (!(await file.exists())) continue
try {
const fm = parseFrontmatter(await file.text())
if (!fm) continue
skills.push({
slug: entry.name,
name: (fm.name as string) ?? entry.name,
description: ((fm.description as string) ?? "").trim(),
metadata: (fm.metadata as Record<string, unknown>) ?? {},
})
} catch (err) {
console.error(`Warning: failed to parse ${skillMdPath}:`, err)
errors++
}
}
const output = {
version: "1.0",
skills,
}
await Bun.write(outputPath, JSON.stringify(output, null, 2) + "\n")
console.log(`Generated ${outputPath} with ${skills.length} skills (${errors} errors)`)