158 lines
5.2 KiB
JavaScript
Executable File
158 lines
5.2 KiB
JavaScript
Executable File
#!/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);
|
|
});
|