177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
// Regenerates the OpenCode artifacts from the pristine upstream snapshot:
|
|
//
|
|
// node scripts/generate.ts
|
|
//
|
|
// Inputs: upstream/skills/** + .opencode/state/classification.json
|
|
// Outputs: skills/<name>/** (verbatim, minus agents/openai.yaml; only skills whose
|
|
// classification artifacts include "skill:verbatim")
|
|
// manifest.json (plugin-injection manifest: id/name/description/autoinvoke/path)
|
|
// docs/skill-mapping.md matrix section (between <!-- matrix:start/end --> markers)
|
|
//
|
|
// Deterministic: same inputs produce byte-identical outputs, so the nightly git diff is the
|
|
// affected-set report. Never hand-edit skills/ or the matrix section.
|
|
import {
|
|
cpSync,
|
|
existsSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import { isUserInvoked, parseFrontmatter } from "../src/frontmatter.ts"
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
|
|
export type ClassificationEntry = {
|
|
name: string
|
|
bucket: string
|
|
classification: string
|
|
user_invoked: boolean
|
|
artifacts: string[]
|
|
depends_on: string[]
|
|
status: string
|
|
rationale: string
|
|
}
|
|
|
|
export type ManifestEntry = {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
autoinvoke: boolean
|
|
path: string
|
|
}
|
|
|
|
export function loadClassification(file: string): ClassificationEntry[] {
|
|
const doc = JSON.parse(readFileSync(file, "utf8"))
|
|
return doc.skills as ClassificationEntry[]
|
|
}
|
|
|
|
function copySkillDir(from: string, to: string) {
|
|
rmSync(to, { recursive: true, force: true })
|
|
cpSync(from, to, {
|
|
recursive: true,
|
|
filter: (src) => {
|
|
// strip Codex UI metadata dir; everything else ships verbatim
|
|
const rel = path.relative(from, src)
|
|
return rel !== "agents" && !rel.startsWith("agents" + path.sep)
|
|
},
|
|
})
|
|
}
|
|
|
|
export function buildSkills(opts: {
|
|
upstreamSkills: string
|
|
entries: ClassificationEntry[]
|
|
skillsOut: string
|
|
}): ManifestEntry[] {
|
|
const vendored = opts.entries.filter((e) => e.artifacts.includes("skill:verbatim"))
|
|
const seen = new Set<string>()
|
|
const manifest: ManifestEntry[] = []
|
|
rmSync(opts.skillsOut, { recursive: true, force: true })
|
|
|
|
for (const entry of vendored) {
|
|
if (seen.has(entry.name)) throw new Error(`duplicate skill name across buckets: ${entry.name}`)
|
|
seen.add(entry.name)
|
|
const from = path.join(opts.upstreamSkills, entry.bucket, entry.name)
|
|
const skillMd = path.join(from, "SKILL.md")
|
|
if (!existsSync(skillMd)) {
|
|
throw new Error(`classified skill missing upstream: ${entry.bucket}/${entry.name}`)
|
|
}
|
|
const to = path.join(opts.skillsOut, entry.name)
|
|
copySkillDir(from, to)
|
|
|
|
const { frontmatter } = parseFrontmatter(readFileSync(skillMd, "utf8"))
|
|
const name = frontmatter.name || entry.name
|
|
const description = frontmatter.description
|
|
if (!frontmatter.name || !description) {
|
|
throw new Error(`${entry.name}: SKILL.md frontmatter needs name and description`)
|
|
}
|
|
manifest.push({
|
|
id: entry.name,
|
|
name,
|
|
description,
|
|
autoinvoke: !isUserInvoked(frontmatter),
|
|
path: `skills/${entry.name}/SKILL.md`,
|
|
})
|
|
}
|
|
|
|
// fail on upstream skills nobody classified (sync-upstream normally adds needs-review entries)
|
|
for (const bucket of existsSync(opts.upstreamSkills) ? readdirSync(opts.upstreamSkills) : []) {
|
|
const bucketPath = path.join(opts.upstreamSkills, bucket)
|
|
if (!statSync(bucketPath).isDirectory()) continue
|
|
for (const name of readdirSync(bucketPath)) {
|
|
if (!existsSync(path.join(bucketPath, name, "SKILL.md"))) continue
|
|
if (!opts.entries.some((e) => e.name === name)) {
|
|
throw new Error(`unclassified upstream skill: ${bucket}/${name} — run sync + review`)
|
|
}
|
|
}
|
|
}
|
|
|
|
manifest.sort((a, b) => a.id.localeCompare(b.id))
|
|
return manifest
|
|
}
|
|
|
|
const BUCKET_SHORT: Record<string, string> = {
|
|
engineering: "eng",
|
|
productivity: "prod",
|
|
misc: "misc",
|
|
"in-progress": "in-prog",
|
|
deprecated: "dep",
|
|
}
|
|
|
|
const cell = (value: string) => value.replace(/\|/g, "\\|")
|
|
|
|
export function renderMatrix(entries: ClassificationEntry[]): string {
|
|
const lines = [
|
|
"| Upstream skill | Bucket | DMI | Classification | Artifacts | Depends on | Status | Rationale |",
|
|
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
]
|
|
for (const e of entries) {
|
|
lines.push(
|
|
`| ${e.name} | ${BUCKET_SHORT[e.bucket] ?? e.bucket} | ${e.user_invoked ? "✓" : ""} | ` +
|
|
`${e.classification} | ${cell(e.artifacts.join(", "))} | ${cell(e.depends_on.join(", "))} | ` +
|
|
`${e.status} | ${cell(e.rationale)} |`,
|
|
)
|
|
}
|
|
return lines.join("\n")
|
|
}
|
|
|
|
export function updateDocMatrix(docPath: string, matrix: string): void {
|
|
const doc = readFileSync(docPath, "utf8")
|
|
const start = "<!-- matrix:start -->"
|
|
const end = "<!-- matrix:end -->"
|
|
const i = doc.indexOf(start)
|
|
const j = doc.indexOf(end)
|
|
if (i === -1 || j === -1 || j < i) throw new Error(`${docPath}: missing matrix markers`)
|
|
writeFileSync(docPath, `${doc.slice(0, i + start.length)}\n\n${matrix}\n\n${doc.slice(j)}`)
|
|
}
|
|
|
|
function main() {
|
|
const classificationPath = path.join(root, ".opencode/state/classification.json")
|
|
const docPath = path.join(root, "docs/skill-mapping.md")
|
|
const entries = loadClassification(classificationPath)
|
|
const manifest = buildSkills({
|
|
upstreamSkills: path.join(root, "upstream/skills"),
|
|
entries,
|
|
skillsOut: path.join(root, "skills"),
|
|
})
|
|
writeFileSync(path.join(root, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n")
|
|
updateDocMatrix(docPath, renderMatrix(entries))
|
|
const needsReview = entries.filter((e) => e.status === "needs-review").map((e) => e.name)
|
|
console.log(
|
|
`generated ${manifest.length} skills (${manifest.filter((m) => !m.autoinvoke).length} hidden from model), manifest.json, matrix in docs/skill-mapping.md` +
|
|
(needsReview.length ? `\nWARNING needs-review: ${needsReview.join(", ")}` : ""),
|
|
)
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
main()
|
|
} catch (err) {
|
|
console.error((err as Error).message)
|
|
process.exit(1)
|
|
}
|
|
}
|