mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
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).
This commit is contained in:
parent
c58adebdd3
commit
f348cb4f85
8 changed files with 549 additions and 2 deletions
91
.github/workflows/manus-bundle.yml
vendored
Normal file
91
.github/workflows/manus-bundle.yml
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
name: Publish Manus bundle
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "*/SKILL.md"
|
||||
- "*/scripts/**"
|
||||
- "*/references/**"
|
||||
- "*/templates/**"
|
||||
- "scripts/build-manus-bundle.js"
|
||||
- "scripts/validate-skills.sh"
|
||||
- ".github/workflows/manus-bundle.yml"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: manus-bundle-latest
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_TAG: manus-bundle-latest
|
||||
RELEASE_TITLE: "Manus bundle (rolling)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Build .skill bundle
|
||||
run: npm run build:manus-bundle
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f dist/manus/k-skill-manus-all.zip
|
||||
test -f dist/manus/INDEX.md
|
||||
count=$(ls dist/manus/*.skill | wc -l)
|
||||
if [ "$count" -lt 1 ]; then
|
||||
echo "no .skill files produced" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "built $count .skill files"
|
||||
|
||||
- name: Publish rolling release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes=$(cat <<EOF
|
||||
Auto-built Manus.ai-compatible \`.skill\` bundle for every skill in k-skill.
|
||||
|
||||
- **\`k-skill-manus-all.zip\`** — combined download containing every \`.skill\` file plus \`INDEX.md\`. Unzip, then drag-drop individual \`.skill\` files into Manus.
|
||||
- **\`INDEX.md\`** — human-readable listing of every bundled skill.
|
||||
|
||||
Manus accepts one skill per upload (\`.skill\`/\`.zip\`/folder). See [\`docs/install-manus.md\`](https://github.com/${GITHUB_REPOSITORY}/blob/main/docs/install-manus.md) for the full flow.
|
||||
|
||||
This is a rolling pre-release that is overwritten on every push to \`main\`. Built from commit \`${GITHUB_SHA}\`.
|
||||
EOF
|
||||
)
|
||||
|
||||
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release edit "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$RELEASE_TITLE" \
|
||||
--notes "$notes" \
|
||||
--prerelease
|
||||
else
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$RELEASE_TITLE" \
|
||||
--notes "$notes" \
|
||||
--prerelease \
|
||||
--target "$GITHUB_SHA"
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber \
|
||||
dist/manus/k-skill-manus-all.zip \
|
||||
dist/manus/INDEX.md
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ node_modules/
|
|||
*.plaintext
|
||||
.venv/
|
||||
__pycache__/
|
||||
dist/
|
||||
.sisyphus/
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
|
|||
| 문서 | 설명 |
|
||||
| --- | --- |
|
||||
| [설치 방법](docs/install.md) | 패키지 설치, 선택 설치, 로컬 테스트 방법 |
|
||||
| [Manus.ai 에서 가져오기](docs/install-manus.md) | Manus.ai 에서 개별 스킬 폴더 URL 가져오기 또는 `npm run build:manus-bundle` 로 빌드한 `.skill` 파일을 드래그-드롭으로 업로드하는 방법 |
|
||||
| [공통 설정 가이드](docs/setup.md) | credential resolution order, 기본 secrets 파일 준비 |
|
||||
| [보안/시크릿 정책](docs/security-and-secrets.md) | 인증 정보 저장 원칙, 금지 패턴, 표준 환경변수 이름 |
|
||||
| [k-skill 프록시 서버 가이드](docs/features/k-skill-proxy.md) | 무료 API를 프록시 서버로 바로 호출하는 방법 |
|
||||
|
|
|
|||
125
docs/install-manus.md
Normal file
125
docs/install-manus.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Manus.ai 에서 k-skill 사용하기
|
||||
|
||||
Manus.ai 는 스킬을 가져오는 두 가지 공식 경로를 제공한다. k-skill 의 모든 스킬은 이미 Manus 가 요구하는 포맷(루트 디렉토리 + `SKILL.md` + YAML frontmatter `name` / `description`)을 만족하므로, 변환 없이 둘 다 사용할 수 있다.
|
||||
|
||||
| 방법 | 언제 쓰면 좋은가 | 한 번에 등록되는 스킬 수 |
|
||||
| --- | --- | --- |
|
||||
| **A. GitHub URL 가져오기** | 원하는 스킬이 1~3 개 정도일 때 | 1 |
|
||||
| **B. `.skill` 파일 업로드** | 여러 스킬을 한꺼번에 받아두고 골라서 올리고 싶을 때 | 1 (드래그-드롭은 동시에 여러 개 선택 가능) |
|
||||
|
||||
> Manus 는 **하나의 아카이브로 여러 스킬을 한꺼번에 등록하는 기능은 공식 지원하지 않는다.** 어느 경로든 "스킬 한 개 = 업로드 한 번" 이다. 다만 방법 B 의 드래그-드롭 업로드는 여러 `.skill` 파일을 한 번에 선택해 빠르게 반복할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 방법 A — GitHub URL 가져오기
|
||||
|
||||
### TL;DR
|
||||
|
||||
❌ 저장소 루트 URL 은 동작하지 않는다 (루트에는 `SKILL.md` 가 없다).
|
||||
|
||||
```
|
||||
https://github.com/NomaDamas/k-skill
|
||||
```
|
||||
|
||||
✅ 가져오려는 **개별 스킬 폴더** URL 을 붙여 넣는다.
|
||||
|
||||
```
|
||||
https://github.com/NomaDamas/k-skill/tree/main/<skill-name>
|
||||
```
|
||||
|
||||
예시:
|
||||
|
||||
```
|
||||
https://github.com/NomaDamas/k-skill/tree/main/mfds-food-safety
|
||||
https://github.com/NomaDamas/k-skill/tree/main/srt-booking
|
||||
https://github.com/NomaDamas/k-skill/tree/main/korea-weather
|
||||
https://github.com/NomaDamas/k-skill/tree/main/real-estate-search
|
||||
```
|
||||
|
||||
각 스킬 폴더에는 Manus 가 요구하는 `SKILL.md` 가 루트에 존재하고, 필요하면 `scripts/`, `references/`, `templates/` 같은 부속 리소스가 같이 들어 있다.
|
||||
|
||||
### 절차
|
||||
|
||||
1. Manus 에서 **"+ 추가"** 또는 **스킬 가져오기** 화면을 연다.
|
||||
2. **GitHub 탭**을 선택한다.
|
||||
3. URL 입력란에 위 형식의 **스킬 폴더 URL** 을 붙여 넣는다.
|
||||
4. **가져오기** 버튼을 누른다.
|
||||
5. 추가로 쓰고 싶은 스킬은 폴더 단위로 같은 절차를 반복한다.
|
||||
|
||||
---
|
||||
|
||||
## 방법 B — `.skill` 번들 업로드 (여러 스킬을 빠르게)
|
||||
|
||||
GitHub URL 을 한 번에 하나씩 붙여 넣는 게 귀찮다면, 미리 빌드된 `.skill` 파일들을 한꺼번에 받아 두고 Manus 의 파일 업로드로 드래그-드롭하는 게 더 빠르다.
|
||||
|
||||
### 빠른 경로 — 미리 빌드된 번들 다운로드 (권장)
|
||||
|
||||
`main` 에 변경이 들어올 때마다 GitHub Actions 가 자동으로 모든 스킬을 패키징해서 rolling pre-release `manus-bundle-latest` 에 올린다. 클론도 빌드도 필요 없다.
|
||||
|
||||
- **합본 (권장)**: <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>
|
||||
- **릴리스 페이지**: <https://github.com/NomaDamas/k-skill/releases/tag/manus-bundle-latest>
|
||||
|
||||
> 위 URL 은 매 `main` 푸시마다 같은 자리에서 새 번들로 교체된다. 항상 최신 상태가 보장된다.
|
||||
|
||||
업로드 절차:
|
||||
|
||||
1. `k-skill-manus-all.zip` 을 받아 압축을 푼다. 한 폴더에 `<skill-name>.skill` 파일들이 펼쳐진다.
|
||||
2. Manus 에서 **스킬 업로드 / 파일 추가** 화면을 연다.
|
||||
3. 원하는 `<skill-name>.skill` 파일을 드래그-드롭하거나 파일 선택으로 업로드한다. 파일 선택 다이얼로그에서 여러 파일을 한꺼번에 골라도 된다.
|
||||
4. Manus 가 파일 하나당 스킬 하나씩 등록한다.
|
||||
|
||||
### 직접 빌드 (개발자용)
|
||||
|
||||
저장소를 수정 중이거나 main 에 아직 머지되지 않은 변경을 테스트하고 싶다면 로컬에서 직접 빌드한다.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NomaDamas/k-skill.git
|
||||
cd k-skill
|
||||
npm install
|
||||
npm run build:manus-bundle
|
||||
```
|
||||
|
||||
빌드가 끝나면 다음 산출물이 생긴다.
|
||||
|
||||
```
|
||||
dist/manus/
|
||||
├── <skill-name>.skill # 스킬 1개당 .skill 파일 1개 (총 60+ 개)
|
||||
├── k-skill-manus-all.zip # 위 .skill 파일들을 한 번에 받기 위한 편의 번들
|
||||
└── INDEX.md # 포함된 스킬 목록과 설명
|
||||
```
|
||||
|
||||
> `.skill` 파일은 사실상 ZIP 아카이브이며, 내부에는 단일 최상위 폴더 `<skill-name>/`(SKILL.md + 보조 리소스)가 들어 있다. 이 레이아웃은 Anthropic 의 공식 [skill-creator packager](https://github.com/anthropics/skills/blob/main/skills/skill-creator/scripts/package_skill.py) 와 동일하다.
|
||||
|
||||
직접 빌드에 필요한 것:
|
||||
|
||||
- Node.js 18+
|
||||
- 시스템 `zip` 명령 (macOS 와 대부분의 Linux 배포판은 기본 설치, Ubuntu 에서 누락 시 `sudo apt-get install -y zip`)
|
||||
|
||||
---
|
||||
|
||||
## 호환성 메모
|
||||
|
||||
- k-skill 의 모든 스킬은 `name`, `description` 을 YAML frontmatter 최상위에 두고 있다. 이 두 필드는 Manus 가 요구하는 **유일한 필수 필드**이므로 호환성을 위해 추가로 수정할 항목이 없다.
|
||||
- 기존 `license`, `metadata.category`, `metadata.locale`, `metadata.phase` 같은 필드는 Manus 가 인식하지 않더라도 무시되며, Claude Code / Codex / OpenCode 등 다른 코딩 에이전트에서는 그대로 사용된다.
|
||||
- `scripts/`, `references/`, `templates/` 같은 보조 디렉토리는 Manus 의 progressive disclosure 규칙과 동일하게 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 사용자 인증과 프록시
|
||||
|
||||
Manus 환경에서 k-skill 을 쓸 때도 본 저장소의 **사용자 로그인 / 시크릿 정책**을 그대로 따른다.
|
||||
|
||||
- "사용자 로그인 필요" 로 표시된 스킬(예: `srt-booking`, `ktx-booking`, `toss-securities`)은 Manus 세션 안에서 사용자가 직접 자격 증명을 제공해야 한다.
|
||||
- "불필요" 로 표시된 스킬은 공개 API 또는 운영자가 관리하는 `k-skill-proxy` 를 그대로 사용한다. Manus 측에서 별도 키를 받지 않는다.
|
||||
- 자세한 정책은 [`docs/security-and-secrets.md`](security-and-secrets.md) 와 [`docs/features/k-skill-proxy.md`](features/k-skill-proxy.md) 참고.
|
||||
|
||||
---
|
||||
|
||||
## 출처
|
||||
|
||||
- Manus 공식 도움말 (업로드/공유 방법): <https://help.manus.im/en/articles/14753565-how-to-share-and-use-skills-in-manus>
|
||||
- Manus 스킬 문서: <https://manus.im/docs/features/skills>
|
||||
- Manus 공개 API (스킬 목록): <https://open.manus.ai/docs/v2/list-skills>
|
||||
- `.skill` 패키징 레퍼런스 (Anthropic skill-creator): <https://github.com/anthropics/skills/blob/main/skills/skill-creator/scripts/package_skill.py>
|
||||
- 폴더별 import 모노레포 예시: <https://github.com/WebWakaHub/manus-agency-skills>
|
||||
|
|
@ -9,9 +9,10 @@
|
|||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py coupang-product-search/scripts/coupang_partners_mcp.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"build:manus-bundle": "node scripts/build-manus-bundle.js",
|
||||
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py coupang-product-search/scripts/coupang_partners_mcp.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"test": "node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_build_manus_bundle.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run",
|
||||
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
|
||||
"version-packages": "changeset version",
|
||||
|
|
|
|||
220
scripts/build-manus-bundle.js
Normal file
220
scripts/build-manus-bundle.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build Manus.ai-compatible bundles for k-skill.
|
||||
*
|
||||
* Manus accepts ONE skill per upload (`.skill`/`.zip`/folder) and offers no
|
||||
* multi-skill bulk import path, so this script emits one `.skill` per skill
|
||||
* plus a single combined download archive.
|
||||
*
|
||||
* Each `.skill` archive contains a single top-level `<skill-name>/` folder
|
||||
* that matches the layout produced by the public Anthropic skill-creator
|
||||
* packager (https://github.com/anthropics/skills/blob/main/skills/skill-creator/scripts/package_skill.py).
|
||||
* That nested layout is load-bearing: flattening it breaks Manus import.
|
||||
*
|
||||
* Skill discovery mirrors `scripts/validate-skills.sh`. Requires the system
|
||||
* `zip` command (preinstalled on macOS and GitHub Actions ubuntu-latest).
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const distDir = path.join(repoRoot, "dist", "manus");
|
||||
|
||||
// Directories at the repo root that are NEVER skills, mirroring
|
||||
// scripts/validate-skills.sh's exclusion list.
|
||||
const EXCLUDED_DIRS = new Set([
|
||||
".git",
|
||||
".github",
|
||||
".codex",
|
||||
".claude",
|
||||
".omx",
|
||||
".ouroboros",
|
||||
".changeset",
|
||||
".cursor",
|
||||
".vscode",
|
||||
".sisyphus",
|
||||
".idea",
|
||||
"docs",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"packages",
|
||||
"python-packages",
|
||||
"scripts",
|
||||
"examples",
|
||||
]);
|
||||
|
||||
function ensureZipAvailable() {
|
||||
const probe = spawnSync("zip", ["-v"], { stdio: "ignore" });
|
||||
if (probe.error || probe.status !== 0) {
|
||||
console.error(
|
||||
"ERROR: the `zip` command is required to build Manus bundles.\n" +
|
||||
" - macOS: preinstalled.\n" +
|
||||
" - Debian/Ubuntu: sudo apt-get install -y zip\n" +
|
||||
" - Windows: install via WSL or Git Bash, or use 7-Zip and zip the folders manually.",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
function discoverSkills() {
|
||||
const entries = fs.readdirSync(repoRoot, { withFileTypes: true });
|
||||
const skills = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
const skillMd = path.join(repoRoot, entry.name, "SKILL.md");
|
||||
if (fs.existsSync(skillMd)) {
|
||||
skills.push(entry.name);
|
||||
}
|
||||
}
|
||||
skills.sort();
|
||||
return skills;
|
||||
}
|
||||
|
||||
function readSkillMeta(skillName) {
|
||||
const skillMd = path.join(repoRoot, skillName, "SKILL.md");
|
||||
const raw = fs.readFileSync(skillMd, "utf8");
|
||||
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return { name: skillName, description: "" };
|
||||
const fm = match[1];
|
||||
const grab = (key) => {
|
||||
const m = fm.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
||||
return m ? m[1].trim().replace(/^["']|["']$/g, "") : "";
|
||||
};
|
||||
return {
|
||||
name: grab("name") || skillName,
|
||||
description: grab("description"),
|
||||
};
|
||||
}
|
||||
|
||||
function rimrafSync(target) {
|
||||
if (!fs.existsSync(target)) return;
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function buildSkillArchive(skillName) {
|
||||
const archivePath = path.join(distDir, `${skillName}.skill`);
|
||||
rimrafSync(archivePath);
|
||||
// zip is run from the repo root and asked to add the whole `<skillName>/`
|
||||
// folder; the resulting archive therefore has `<skillName>/SKILL.md` etc. at
|
||||
// its root, which matches the public Anthropic packager layout.
|
||||
const result = spawnSync(
|
||||
"zip",
|
||||
[
|
||||
"-r",
|
||||
"-q",
|
||||
"-X", // strip extra file attributes for reproducible archives
|
||||
archivePath,
|
||||
skillName,
|
||||
"-x",
|
||||
`${skillName}/node_modules/*`,
|
||||
"-x",
|
||||
`${skillName}/__pycache__/*`,
|
||||
"-x",
|
||||
`${skillName}/*/__pycache__/*`,
|
||||
"-x",
|
||||
`${skillName}/.DS_Store`,
|
||||
"-x",
|
||||
`${skillName}/*/.DS_Store`,
|
||||
],
|
||||
{ cwd: repoRoot, stdio: ["ignore", "inherit", "inherit"] },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`zip failed for ${skillName} (exit ${result.status})`);
|
||||
}
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
function buildAllInOneArchive(skillNames) {
|
||||
// Bundle all the .skill files together so users can download a single
|
||||
// release asset and then drag-drop the individual .skill files into Manus.
|
||||
const allInOne = path.join(distDir, "k-skill-manus-all.zip");
|
||||
rimrafSync(allInOne);
|
||||
const relativeNames = skillNames.map((s) => `${s}.skill`);
|
||||
relativeNames.push("INDEX.md");
|
||||
const result = spawnSync("zip", ["-q", "-X", "-j", allInOne, ...relativeNames.map((n) => path.join(distDir, n))], {
|
||||
cwd: distDir,
|
||||
stdio: ["ignore", "inherit", "inherit"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`zip failed for k-skill-manus-all.zip (exit ${result.status})`);
|
||||
}
|
||||
return allInOne;
|
||||
}
|
||||
|
||||
function writeIndex(skillMetas) {
|
||||
const lines = [];
|
||||
lines.push("# k-skill — Manus.ai 가져오기용 번들");
|
||||
lines.push("");
|
||||
lines.push("이 폴더에는 NomaDamas/k-skill 의 모든 스킬이 Manus.ai 호환 `.skill` 아카이브로 빌드되어 있다.");
|
||||
lines.push("");
|
||||
lines.push("## 사용 방법");
|
||||
lines.push("");
|
||||
lines.push("1. Manus.ai 에서 **스킬 업로드** 화면을 연다.");
|
||||
lines.push("2. 원하는 `<skill-name>.skill` 파일을 드래그-드롭하거나 파일 선택으로 업로드한다.");
|
||||
lines.push("3. 한 번의 업로드는 한 개의 스킬을 등록한다. 필요한 스킬만큼 반복한다.");
|
||||
lines.push("");
|
||||
lines.push("`.skill` 파일은 사실상 ZIP 아카이브이며, 내부에는 단일 최상위 폴더 `<skill-name>/`(SKILL.md + 보조 리소스)가 들어 있다.");
|
||||
lines.push("");
|
||||
lines.push("Manus.ai 는 **하나의 아카이브로 여러 스킬을 한꺼번에 등록하는 기능을 공식 지원하지 않는다.** `k-skill-manus-all.zip` 은 단순히 모든 `.skill` 파일을 한 번에 받기 위한 편의 번들이다. 압축을 풀면 N개의 `.skill` 파일이 나오며 그 파일들을 Manus 에 하나씩 업로드해야 한다.");
|
||||
lines.push("");
|
||||
lines.push("## 포함된 스킬");
|
||||
lines.push("");
|
||||
lines.push("| 스킬 이름 | 설명 | 파일 |");
|
||||
lines.push("| --- | --- | --- |");
|
||||
for (const meta of skillMetas) {
|
||||
const desc = (meta.description || "").replace(/\|/g, "\\|");
|
||||
lines.push(`| \`${meta.name}\` | ${desc} | \`${meta.name}.skill\` |`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(`총 ${skillMetas.length}개 스킬.`);
|
||||
lines.push("");
|
||||
fs.writeFileSync(path.join(distDir, "INDEX.md"), lines.join("\n"));
|
||||
}
|
||||
|
||||
function main() {
|
||||
ensureZipAvailable();
|
||||
rimrafSync(distDir);
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
const skills = discoverSkills();
|
||||
if (skills.length === 0) {
|
||||
console.error("ERROR: no skills with SKILL.md found at repo root.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const metas = [];
|
||||
for (const skill of skills) {
|
||||
process.stdout.write(`packing ${skill}.skill ... `);
|
||||
buildSkillArchive(skill);
|
||||
metas.push(readSkillMeta(skill));
|
||||
process.stdout.write("ok\n");
|
||||
}
|
||||
|
||||
writeIndex(metas);
|
||||
buildAllInOneArchive(skills);
|
||||
|
||||
console.log("");
|
||||
console.log(`built ${skills.length} .skill files in ${path.relative(repoRoot, distDir)}/`);
|
||||
console.log(`combined download: ${path.relative(repoRoot, path.join(distDir, "k-skill-manus-all.zip"))}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(err.message || err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EXCLUDED_DIRS,
|
||||
discoverSkills,
|
||||
readSkillMeta,
|
||||
};
|
||||
104
scripts/test_build_manus_bundle.js
Normal file
104
scripts/test_build_manus_bundle.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"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");
|
||||
});
|
||||
|
|
@ -45,6 +45,9 @@ done < <(
|
|||
! -name .changeset \
|
||||
! -name .cursor \
|
||||
! -name .vscode \
|
||||
! -name .sisyphus \
|
||||
! -name .idea \
|
||||
! -name dist \
|
||||
! -name docs \
|
||||
! -name node_modules \
|
||||
! -name packages \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue