mirror of
https://github.com/ConardLi/garden-skills.git
synced 2026-09-14 15:56:41 +08:00
3e9654bb26
- Updated README.md and README.zh-CN.md to reflect the new skill repository name and added download links for skill releases. - Enhanced installation instructions with additional options for using the `skills` CLI and clarified the process for obtaining pinned versions. - Added a new entry to .gitignore to exclude release artifacts generated during the CI process, ensuring a cleaner repository.
197 lines
5.5 KiB
JavaScript
197 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Build a release zip for one or all skills.
|
|
//
|
|
// Usage:
|
|
// node scripts/release/pack-skill.mjs --skill web-design-engineer
|
|
// node scripts/release/pack-skill.mjs --skill web-design-engineer --version 1.2.0
|
|
// node scripts/release/pack-skill.mjs --all
|
|
//
|
|
// Output (default): dist/release/<skill>-<version>.zip + .sha256
|
|
// Override out dir with --out <dir>.
|
|
//
|
|
// The zip's top-level entry is always "<skill>/" so users can extract straight
|
|
// into .claude/skills/, .agents/skills/, etc.
|
|
|
|
import { mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
import { existsSync, createReadStream } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import {
|
|
REPO_ROOT,
|
|
SKILLS_DIR,
|
|
listSkills,
|
|
readManifest,
|
|
validateManifest,
|
|
validateSkillStructure,
|
|
dirSize,
|
|
formatBytes,
|
|
zipName,
|
|
} from "./lib/skills.mjs";
|
|
|
|
const EXCLUDE = [
|
|
".DS_Store",
|
|
"Thumbs.db",
|
|
"node_modules",
|
|
"*.log",
|
|
"*.swp",
|
|
"*.swo",
|
|
".idea",
|
|
".vscode",
|
|
];
|
|
|
|
function parseArgs(argv) {
|
|
const args = { all: false };
|
|
for (let i = 2; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === "--all") args.all = true;
|
|
else if (a === "--skill") args.skill = argv[++i];
|
|
else if (a === "--version") args.version = argv[++i];
|
|
else if (a === "--out") args.out = argv[++i];
|
|
else if (a === "--help" || a === "-h") args.help = true;
|
|
else throw new Error(`Unknown arg: ${a}`);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function help() {
|
|
console.log(
|
|
"Usage: pack-skill.mjs (--skill <name> [--version <semver>] | --all) [--out <dir>]",
|
|
);
|
|
}
|
|
|
|
async function sha256File(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const hash = createHash("sha256");
|
|
const s = createReadStream(file);
|
|
s.on("data", (c) => hash.update(c));
|
|
s.on("end", () => resolve(hash.digest("hex")));
|
|
s.on("error", reject);
|
|
});
|
|
}
|
|
|
|
async function packOne({ name, dir, manifest, outDir, expectVersion }) {
|
|
const errs = [
|
|
...validateManifest(manifest, name),
|
|
...(await validateSkillStructure(dir, name)),
|
|
];
|
|
if (errs.length) {
|
|
throw new Error(`Validation failed for ${name}:\n - ${errs.join("\n - ")}`);
|
|
}
|
|
|
|
const version = expectVersion ?? manifest.version;
|
|
if (expectVersion && manifest.version !== expectVersion) {
|
|
throw new Error(
|
|
`Version drift: tag asks for ${expectVersion} but manifest is ${manifest.version}. ` +
|
|
`Bump skills/${name}/manifest.json#version first.`,
|
|
);
|
|
}
|
|
|
|
const sizeBefore = await dirSize(dir, { exclude: EXCLUDE });
|
|
console.log(`[pack] ${name}@${version} source=${formatBytes(sizeBefore)}`);
|
|
|
|
await mkdir(outDir, { recursive: true });
|
|
const zipFile = path.join(outDir, zipName(name, version));
|
|
if (existsSync(zipFile)) await rm(zipFile);
|
|
|
|
// Use system `zip` for portability + deterministic-ish output.
|
|
// Run inside SKILLS_DIR so the archive's top-level dir is "<name>/".
|
|
const excludeArgs = EXCLUDE.flatMap((p) => [
|
|
"-x",
|
|
`${name}/${p}`,
|
|
"-x",
|
|
`${name}/**/${p}`,
|
|
]);
|
|
const result = spawnSync(
|
|
"zip",
|
|
["-r", "-q", "-X", zipFile, name, ...excludeArgs],
|
|
{ cwd: SKILLS_DIR, stdio: ["ignore", "inherit", "inherit"] },
|
|
);
|
|
if (result.status !== 0) {
|
|
throw new Error(`zip exited with status ${result.status}`);
|
|
}
|
|
|
|
const digest = await sha256File(zipFile);
|
|
const shaFile = `${zipFile}.sha256`;
|
|
await writeFile(shaFile, `${digest} ${path.basename(zipFile)}\n`, "utf8");
|
|
const stat = await import("node:fs/promises").then((m) => m.stat(zipFile));
|
|
console.log(
|
|
`[pack] -> ${path.relative(REPO_ROOT, zipFile)} (${formatBytes(stat.size)})`,
|
|
);
|
|
console.log(`[pack] -> ${path.relative(REPO_ROOT, shaFile)} (sha256=${digest.slice(0, 12)}…)`);
|
|
|
|
// Soft size gate — warn (don't fail) for skills > 5 MB.
|
|
if (stat.size > 5 * 1024 * 1024) {
|
|
console.warn(
|
|
`[pack] WARNING: ${name}-${version}.zip is ${formatBytes(stat.size)} — consider trimming bundled assets.`,
|
|
);
|
|
}
|
|
|
|
return { name, version, zipFile, shaFile, digest, size: stat.size };
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv);
|
|
if (args.help) return help();
|
|
if (!args.all && !args.skill) {
|
|
help();
|
|
process.exitCode = 2;
|
|
return;
|
|
}
|
|
|
|
const outDir = path.resolve(REPO_ROOT, args.out ?? "dist/release");
|
|
|
|
let targets;
|
|
if (args.all) {
|
|
targets = await listSkills();
|
|
} else {
|
|
const all = await listSkills();
|
|
const t = all.find((s) => s.name === args.skill);
|
|
if (!t) throw new Error(`No such skill: ${args.skill}`);
|
|
targets = [t];
|
|
}
|
|
|
|
const results = [];
|
|
for (const t of targets) {
|
|
const manifest = await readManifest(t.dir);
|
|
results.push(
|
|
await packOne({
|
|
name: t.name,
|
|
dir: t.dir,
|
|
manifest,
|
|
outDir,
|
|
expectVersion: args.skill ? args.version : undefined,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Emit a summary index.json so CI / README sync can consume it.
|
|
const indexPath = path.join(outDir, "index.json");
|
|
await writeFile(
|
|
indexPath,
|
|
JSON.stringify(
|
|
{
|
|
builtAt: new Date().toISOString(),
|
|
artifacts: results.map((r) => ({
|
|
name: r.name,
|
|
version: r.version,
|
|
file: path.basename(r.zipFile),
|
|
sha256File: path.basename(r.shaFile),
|
|
sha256: r.digest,
|
|
size: r.size,
|
|
})),
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
"utf8",
|
|
);
|
|
console.log(`[pack] -> ${path.relative(REPO_ROOT, indexPath)}`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`[pack] ERROR: ${err.message}`);
|
|
process.exit(1);
|
|
});
|