k-skill/scripts/test_build_manus_bundle.js
Jeffrey (Dongkyu) Kim f348cb4f85
feat: Manus.ai 호환 import 경로 추가 (GitHub URL + rolling .skill 번들) (#227)
* docs: add Manus.ai GitHub skill import guide

Manus.ai의 'GitHub에서 프로젝트 스킬 가져오기' 기능은 폴더 루트에 SKILL.md(YAML frontmatter name/description 필수)가 있는 디렉토리 URL을 받는다. k-skill의 모든 스킬은 이미 이 포맷을 만족하므로 코드 변경 없이 문서만 추가한다.

- 사용자는 저장소 루트 URL(https://github.com/NomaDamas/k-skill) 대신 개별 스킬 폴더 URL(https://github.com/NomaDamas/k-skill/tree/main/<skill-name>)을 붙여 넣어야 한다.

- 기존 frontmatter(license, metadata.*)는 Manus가 무시하지만 다른 코딩 에이전트와의 호환을 위해 그대로 유지한다.

* feat: add build:manus-bundle for batch .skill upload to Manus.ai

Per-folder GitHub URL import is tedious for 61 skills, so add 'npm run build:manus-bundle' which emits one .skill (ZIP) per skill into dist/manus/, plus a single k-skill-manus-all.zip convenience bundle and an INDEX.md listing. Each archive nests its content under <skill-name>/ to match the public Anthropic skill-creator packager layout.

Manus does NOT support multi-skill bulk import in a single archive (verified against help.manus.im, manus.im/docs, and open.manus.ai API docs). The combined zip is purely a download convenience: users still drag-drop individual .skill files into Manus, but the file picker accepts multiple selections so it's still much faster than pasting 61 GitHub URLs.

- scripts/build-manus-bundle.js: discovers root-level skills (mirrors validate-skills.sh exclusions), shells out to system zip with -X for reproducible archives, excludes node_modules/__pycache__/.DS_Store.

- scripts/test_build_manus_bundle.js: validates discovery, frontmatter parsing, lockstep with validate-skills.sh, and docs coverage.

- scripts/validate-skills.sh: also skip dist/ and .sisyphus/ so the validator stays clean after a build.

- .gitignore: ignore dist/ and .sisyphus/.

- docs/install-manus.md: document both Method A (GitHub URL) and Method B (.skill bundle).

* ci: auto-publish Manus .skill bundle as rolling release on main push

Every push to main that touches a skill folder or the bundler now builds the .skill bundle and publishes it to the GitHub Releases tag 'manus-bundle-latest' (marked prerelease so it does not pollute the Latest release pointer used by the npm release flow).

Users get stable download URLs that always point to the latest build:

  - https://github.com/NomaDamas/k-skill/releases/download/manus-bundle-latest/k-skill-manus-all.zip

  - https://github.com/NomaDamas/k-skill/releases/download/manus-bundle-latest/INDEX.md

This removes the 'clone the repo and run npm' step for non-developers. The direct-build path remains documented as the developer fallback.

- .github/workflows/manus-bundle.yml: workflow_dispatch + push-to-main with paths filter, uses preinstalled gh CLI (no third-party release action), concurrency-grouped so overlapping pushes do not race on the same tag, --clobber upload to keep asset URLs stable.

- docs/install-manus.md: new 'quick path' section with the rolling-release URLs; existing local-build section reframed as a developer fallback.

- scripts/test_build_manus_bundle.js: 2 new tests pinning the doc URLs and key workflow invariants (trigger branch, build invocation, tag, asset name, prerelease flag, write permission).
2026-05-11 12:12:44 +09:00

104 lines
4.1 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const repoRoot = path.resolve(__dirname, "..");
const buildScript = path.join(__dirname, "build-manus-bundle.js");
test("build-manus-bundle script exists and is executable as a Node module", () => {
assert.ok(fs.existsSync(buildScript), "build-manus-bundle.js must exist");
const checked = spawnSync(process.execPath, ["--check", buildScript], { encoding: "utf8" });
assert.equal(checked.status, 0, `node --check failed: ${checked.stderr}`);
});
test("discoverSkills finds every root-level skill with a SKILL.md and matches validate-skills.sh", () => {
const { discoverSkills, EXCLUDED_DIRS } = require("./build-manus-bundle.js");
const skills = discoverSkills();
assert.ok(skills.length >= 50, `expected at least 50 skills, got ${skills.length}`);
for (const name of skills) {
assert.ok(
fs.existsSync(path.join(repoRoot, name, "SKILL.md")),
`discovered skill ${name} must have a SKILL.md`,
);
assert.ok(!EXCLUDED_DIRS.has(name), `${name} must not be an excluded tooling dir`);
}
const validatorOutput = spawnSync(path.join(__dirname, "validate-skills.sh"), [], {
cwd: repoRoot,
encoding: "utf8",
});
assert.equal(validatorOutput.status, 0, `validate-skills.sh failed: ${validatorOutput.stderr}`);
});
test("readSkillMeta extracts name and description from YAML frontmatter", () => {
const { readSkillMeta } = require("./build-manus-bundle.js");
const sample = readSkillMeta("mfds-food-safety");
assert.equal(sample.name, "mfds-food-safety");
assert.ok(sample.description.length > 0, "description must be non-empty");
});
test("EXCLUDED_DIRS stays in lockstep with validate-skills.sh exclusions", () => {
const { EXCLUDED_DIRS } = require("./build-manus-bundle.js");
const validator = fs.readFileSync(path.join(__dirname, "validate-skills.sh"), "utf8");
const required = [
".git",
".github",
".codex",
".claude",
".changeset",
"docs",
"node_modules",
"packages",
"python-packages",
"scripts",
"examples",
];
for (const dir of required) {
assert.ok(
validator.includes(`! -name ${dir}`),
`validate-skills.sh must exclude ${dir} (or this list needs updating)`,
);
assert.ok(EXCLUDED_DIRS.has(dir), `EXCLUDED_DIRS must also skip ${dir}`);
}
});
test("docs/install-manus.md documents both the GitHub URL path and the .skill bundle path", () => {
const doc = fs.readFileSync(path.join(repoRoot, "docs", "install-manus.md"), "utf8");
assert.match(doc, /tree\/main\//, "must explain per-skill folder URL pattern");
assert.match(doc, /\.skill/, "must document the .skill file flow");
assert.match(doc, /build:manus-bundle/, "must reference the npm build script");
});
test("docs/install-manus.md advertises the rolling release download URL", () => {
const doc = fs.readFileSync(path.join(repoRoot, "docs", "install-manus.md"), "utf8");
assert.match(
doc,
/releases\/download\/manus-bundle-latest\/k-skill-manus-all\.zip/,
"must link to the stable rolling-release download URL",
);
assert.match(
doc,
/releases\/tag\/manus-bundle-latest/,
"must link to the rolling-release page",
);
});
test("manus-bundle workflow exists, targets main, and publishes the expected assets", () => {
const wfPath = path.join(repoRoot, ".github", "workflows", "manus-bundle.yml");
assert.ok(fs.existsSync(wfPath), "manus-bundle.yml workflow must exist");
const wf = fs.readFileSync(wfPath, "utf8");
assert.match(wf, /branches:\s*\n\s*-\s*main/, "workflow must trigger on push to main");
assert.match(wf, /npm run build:manus-bundle/, "workflow must invoke the build script");
assert.match(wf, /manus-bundle-latest/, "workflow must use the stable rolling tag");
assert.match(wf, /k-skill-manus-all\.zip/, "workflow must upload the combined archive");
assert.match(wf, /--prerelease/, "rolling release must be marked as prerelease");
assert.match(wf, /contents:\s*write/, "workflow needs write permission to publish releases");
});