#!/usr/bin/env node import { createHash } from "node:crypto"; import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { createReadStream } from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; const root = path.resolve(new URL("..", import.meta.url).pathname); const skillsDir = path.join(root, "skills"); const apisDir = path.join(root, "apis"); const packagesDir = path.join(root, "packages"); const registryDir = path.join(root, "registry"); const repository = { name: "zsb/omniclaw-skills", git_url: "https://git.omniclaw.store/zsb/omniclaw-skills.git", raw_base_url: "https://git.omniclaw.store/zsb/omniclaw-skills/raw/branch/main", branch: "main", }; async function exists(file) { try { await stat(file); return true; } catch { return false; } } async function sha256(file) { return new Promise((resolve, reject) => { const hash = createHash("sha256"); const stream = createReadStream(file); stream.on("data", (chunk) => hash.update(chunk)); stream.on("error", reject); stream.on("end", () => resolve(hash.digest("hex"))); }); } async function collectApiDocs() { const docs = []; if (!(await exists(apisDir))) { return docs; } const products = await readdir(apisDir, { withFileTypes: true }); for (const productEntry of products) { if (!productEntry.isDirectory()) continue; const product = productEntry.name; const productDir = path.join(apisDir, product); const files = await readdir(productDir, { withFileTypes: true }); for (const file of files) { if (!file.isFile() || !file.name.endsWith(".md")) continue; const fullPath = path.join(productDir, file.name); const relPath = path.relative(root, fullPath); const text = await readFile(fullPath, "utf8"); const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() || file.name; const language = file.name.endsWith(".zh.md") ? "zh" : file.name.endsWith(".en.md") ? "en" : "unknown"; const id = `${product}/${file.name.replace(/\.md$/, "")}`; const sourceUrls = [...text.matchAll(/https:\/\/[^\s)]+/g)].map((m) => m[0].replace(/[",)}>]+$/g, ""), ); docs.push({ id, title, product, language, path: relPath, url: `${repository.raw_base_url}/${relPath}`, tags: Array.from(new Set([product, ...id.split(/[/.]/).filter(Boolean)])), source_urls: Array.from(new Set(sourceUrls)), }); } } return docs.sort((a, b) => a.id.localeCompare(b.id)); } async function buildSkillPackage(skillName, version) { await mkdir(packagesDir, { recursive: true }); const packageName = `${skillName}-${version}.tar.gz`; const packagePath = path.join(packagesDir, packageName); await rm(packagePath, { force: true }); const result = spawnSync("tar", ["-czf", packagePath, "-C", skillsDir, skillName], { cwd: root, stdio: "inherit", }); if (result.status !== 0) { throw new Error(`tar failed for ${skillName}`); } const fileStat = await stat(packagePath); return { packageName, packagePath, sizeBytes: fileStat.size, digest: await sha256(packagePath), }; } async function collectSkills(apiDocs) { const skills = []; if (!(await exists(skillsDir))) { return skills; } const entries = await readdir(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const skillName = entry.name; const skillDir = path.join(skillsDir, skillName); const metadataPath = path.join(skillDir, "skill.registry.json"); const skillPath = path.join(skillDir, "SKILL.md"); if (!(await exists(metadataPath)) || !(await exists(skillPath))) { continue; } const metadata = JSON.parse(await readFile(metadataPath, "utf8")); const pkg = await buildSkillPackage(skillName, metadata.version); const relatedDocs = (metadata.api_docs || []).map((docId) => { const doc = apiDocs.find((item) => item.id === docId); return doc || { id: docId }; }); skills.push({ name: skillName, version: metadata.version, title: metadata.title, description: metadata.description, path: `skills/${skillName}`, raw_url: `${repository.raw_base_url}/skills/${skillName}/SKILL.md`, package: `packages/${pkg.packageName}`, package_url: `${repository.raw_base_url}/packages/${pkg.packageName}`, sha256: pkg.digest, size_bytes: pkg.sizeBytes, api_docs: relatedDocs, tags: metadata.tags || [], agents: metadata.agents || ["codex"], updated_at: metadata.updated_at, }); } return skills.sort((a, b) => a.name.localeCompare(b.name)); } async function main() { await mkdir(registryDir, { recursive: true }); const apis = await collectApiDocs(); const skills = await collectSkills(apis); const manifest = { schema_version: "1.0.0", generated_at: new Date().toISOString(), repository, skills, apis, }; await writeFile(path.join(registryDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); console.log(`Built registry: ${skills.length} skills, ${apis.length} API docs`); } main().catch((error) => { console.error(error); process.exit(1); });