mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Merge commit 'b6b0c70' into ulw-merge-seven-prs
# Conflicts: # scripts/validate-skills.sh
This commit is contained in:
commit
51388a539e
6 changed files with 413 additions and 2 deletions
13
.claude-plugin/marketplace.json
Normal file
13
.claude-plugin/marketplace.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "k-skill",
|
||||
"owner": {
|
||||
"name": "NomaDamas"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "k-skill",
|
||||
"source": "./",
|
||||
"description": "한국인을 위한 90+ Agent Skill 번들 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화"
|
||||
}
|
||||
]
|
||||
}
|
||||
102
.claude-plugin/plugin.json
Normal file
102
.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"name": "k-skill",
|
||||
"description": "한국인을 위한 90+ Agent Skill 모음 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "NomaDamas"
|
||||
},
|
||||
"homepage": "https://github.com/NomaDamas/k-skill",
|
||||
"repository": "https://github.com/NomaDamas/k-skill",
|
||||
"license": "MIT",
|
||||
"skills": [
|
||||
"./bunjang-search",
|
||||
"./catchtable-sniper",
|
||||
"./cheap-gas-nearby",
|
||||
"./corporate-registration-consulting",
|
||||
"./coupang-product-search",
|
||||
"./court-auction-notice-search",
|
||||
"./daangn-cars-search",
|
||||
"./daangn-jobs-search",
|
||||
"./daangn-realty-search",
|
||||
"./daangn-used-goods-search",
|
||||
"./daishin-report-search",
|
||||
"./daiso-product-search",
|
||||
"./danawa-price-search",
|
||||
"./delivery-tracking",
|
||||
"./donation-place-search",
|
||||
"./emergency-room-beds",
|
||||
"./express-bus-booking",
|
||||
"./fine-dust-location",
|
||||
"./flight-ticket-search",
|
||||
"./foresttrip-vacancy",
|
||||
"./gangnamunni-clinic-search",
|
||||
"./geeknews-search",
|
||||
"./gongsijiga-search",
|
||||
"./han-river-water-level",
|
||||
"./hipass-receipt",
|
||||
"./hola-poke-yeoksam",
|
||||
"./household-waste-info",
|
||||
"./hwp",
|
||||
"./intercity-bus-booking",
|
||||
"./iros-registry-automation",
|
||||
"./joseon-sillok-search",
|
||||
"./k-dart",
|
||||
"./k-schoollunch-menu",
|
||||
"./k-skill-cleaner",
|
||||
"./k-skill-setup",
|
||||
"./kakao-bar-nearby",
|
||||
"./kakao-map",
|
||||
"./kakaotalk-mac",
|
||||
"./kbl-results",
|
||||
"./kbo-results",
|
||||
"./kleague-results",
|
||||
"./korea-weather",
|
||||
"./korean-character-count",
|
||||
"./korean-cinema-search",
|
||||
"./korean-jangbu-for",
|
||||
"./korean-law-search",
|
||||
"./korean-marathon-schedule",
|
||||
"./korean-middle-korean",
|
||||
"./korean-patent-search",
|
||||
"./korean-privacy-terms",
|
||||
"./korean-scholarship-search",
|
||||
"./korean-slang-writing",
|
||||
"./korean-spell-check",
|
||||
"./korean-stock-search",
|
||||
"./korean-transit-route",
|
||||
"./kosis-stats",
|
||||
"./kstartup-search",
|
||||
"./ktx-booking",
|
||||
"./lck-analytics",
|
||||
"./lh-notice-search",
|
||||
"./library-book-search",
|
||||
"./local-election-candidate-search",
|
||||
"./lotto-results",
|
||||
"./market-kurly-search",
|
||||
"./mfds-drug-safety",
|
||||
"./mfds-food-safety",
|
||||
"./myrealtrip-search",
|
||||
"./naver-blog-research",
|
||||
"./naver-map-route",
|
||||
"./naver-news-search",
|
||||
"./naver-shopping-search",
|
||||
"./nts-business-registration",
|
||||
"./ohou-today-deal",
|
||||
"./olive-young-search",
|
||||
"./parking-lot-search",
|
||||
"./public-restroom-nearby",
|
||||
"./real-estate-search",
|
||||
"./rhwp-advanced",
|
||||
"./rhwp-edit",
|
||||
"./seoul-bike",
|
||||
"./seoul-density",
|
||||
"./seoul-subway-arrival",
|
||||
"./sh-notice-search",
|
||||
"./srt-booking",
|
||||
"./subway-lost-property",
|
||||
"./ticket-availability",
|
||||
"./toss-securities",
|
||||
"./used-car-price-search",
|
||||
"./zipcode-search"
|
||||
]
|
||||
}
|
||||
11
README.md
11
README.md
|
|
@ -120,6 +120,17 @@ Claude Code, Codex, OpenCode, OpenClaw/ClawHub 등 각종 코딩 에이전트
|
|||
> - 유료 회원권 보유자도 접근이 막히는 사례가 확인되었습니다. 복구 여부와 일정은 블루리본 측 정책에 전적으로 달려 있어 이 레포에서 대응할 수 있는 범위를 벗어났습니다.
|
||||
> - 해당 스킬 디렉토리(`blue-ribbon-nearby/`)와 관련 프록시 라우트는 히스토리 보존을 위해 당분간 남겨두지만, **새 프로젝트에서는 해당 스킬을 사용하지 마세요.** 차단이 해제되는 날이 오면 이 안내를 제거하고 재검증하겠습니다.
|
||||
|
||||
## Claude Code 플러그인으로 설치
|
||||
|
||||
[Claude Code](https://claude.com/claude-code)에서는 마켓플레이스로 전체 스킬을 한 번에 설치할 수 있습니다.
|
||||
|
||||
```
|
||||
/plugin marketplace add NomaDamas/k-skill
|
||||
/plugin install k-skill@k-skill
|
||||
```
|
||||
|
||||
설치하면 스킬이 `/k-skill:<스킬 이름>` 네임스페이스로 호출됩니다 (예: `/k-skill:lotto-results`). 개별 디렉토리를 직접 복사하는 수동 설치나 다른 에이전트 설치는 [설치 방법](docs/install.md)을 참고하세요.
|
||||
|
||||
## 처음 시작하는 순서
|
||||
|
||||
1. [설치 방법](docs/install.md)을 따라 `k-skill` 전체 스킬을 먼저 설치합니다.
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@
|
|||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"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/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.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/nts_business_registration.py scripts/test_nts_business_registration.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 nts-business-registration/scripts/nts_business_registration.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 scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.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 seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"generate:plugin-manifest": "node scripts/generate-plugin-manifest.js",
|
||||
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/generate-plugin-manifest.js scripts/test_generate_plugin_manifest.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/nts_business_registration.py scripts/test_nts_business_registration.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 nts-business-registration/scripts/nts_business_registration.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 scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.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 seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh && node scripts/generate-plugin-manifest.js --check",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.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_nts_business_registration 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 scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/test_generate_plugin_manifest.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_nts_business_registration 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 scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.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 court-auction-notice-search --dry-run && npm pack --workspace donation-place-search --dry-run && npm pack --workspace gongsijiga-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 && npm pack --workspace korean-marathon-schedule --dry-run && npm pack --workspace gangnamunni-clinic-search --dry-run && npm pack --workspace daishin-report-search --dry-run && npm pack --workspace sh-notice-search --dry-run && npm pack --workspace emergency-room-beds --dry-run && npm pack --workspace local-election-candidate-search --dry-run",
|
||||
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
|
||||
"version-packages": "changeset version",
|
||||
|
|
|
|||
179
scripts/generate-plugin-manifest.js
Normal file
179
scripts/generate-plugin-manifest.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate / refresh the Claude Code plugin manifest's `skills` list.
|
||||
*
|
||||
* This repo is a flat collection of `<skill-name>/SKILL.md` directories at the
|
||||
* repo root (NOT under a `skills/` folder), because the npm workspaces +
|
||||
* changesets release pipeline depends on that layout. A Claude Code plugin can
|
||||
* still expose them by listing each skill directory in the `skills` array of
|
||||
* `.claude-plugin/plugin.json` (the field accepts custom directory paths in
|
||||
* addition to the default `skills/` dir).
|
||||
*
|
||||
* Skill discovery mirrors scripts/validate-skills.sh and
|
||||
* scripts/build-manus-bundle.js. This script writes the sorted `skills` array
|
||||
* into `.claude-plugin/plugin.json` while preserving every other field.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-plugin-manifest.js # write/update plugin.json
|
||||
* node scripts/generate-plugin-manifest.js --check # exit 1 if out of date
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
// Root-level directories that are never skills. Superset of the exclusion
|
||||
// lists in scripts/validate-skills.sh and scripts/build-manus-bundle.js so
|
||||
// that test fixtures under tools/ never leak in. Dot-directories are excluded
|
||||
// unconditionally below; they are listed here only for documentation.
|
||||
const EXCLUDED_DIRS = new Set([
|
||||
".git",
|
||||
".github",
|
||||
".codex",
|
||||
".claude",
|
||||
".omc",
|
||||
".omx",
|
||||
".ouroboros",
|
||||
".changeset",
|
||||
".cursor",
|
||||
".vscode",
|
||||
".sisyphus",
|
||||
".idea",
|
||||
"docs",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"packages",
|
||||
"python-packages",
|
||||
"scripts",
|
||||
"examples",
|
||||
"tools",
|
||||
]);
|
||||
|
||||
// Skills that exist on disk but must not ship in the plugin (e.g. upstream
|
||||
// blocked automation and the skill no longer works).
|
||||
const EXCLUDED_SKILLS = new Set(["blue-ribbon-nearby"]);
|
||||
|
||||
// Identity fields used when the manifest does not exist yet. Existing values
|
||||
// are never overwritten; only missing keys are backfilled.
|
||||
const DEFAULT_MANIFEST = {
|
||||
name: "k-skill",
|
||||
description:
|
||||
"한국인을 위한 90+ Agent Skill 모음 — SRT/KTX/당근/쿠팡/카톡/정부24 등 한국 일상·업무 자동화",
|
||||
version: "1.0.0",
|
||||
author: { name: "NomaDamas" },
|
||||
homepage: "https://github.com/NomaDamas/k-skill",
|
||||
repository: "https://github.com/NomaDamas/k-skill",
|
||||
license: "MIT",
|
||||
skills: [],
|
||||
};
|
||||
|
||||
function manifestPathFor(root) {
|
||||
return path.join(root, ".claude-plugin", "plugin.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover skill directories (those containing a SKILL.md) directly under
|
||||
* `root`, returning sorted plugin-relative paths like `./lotto-results`.
|
||||
*/
|
||||
function discoverSkillPaths(root) {
|
||||
const entries = fs.readdirSync(root, { withFileTypes: true });
|
||||
const skills = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
||||
if (EXCLUDED_SKILLS.has(entry.name)) continue;
|
||||
const skillMd = path.join(root, entry.name, "SKILL.md");
|
||||
if (fs.existsSync(skillMd)) {
|
||||
skills.push(`./${entry.name}`);
|
||||
}
|
||||
}
|
||||
skills.sort();
|
||||
return skills;
|
||||
}
|
||||
|
||||
/** Build the manifest object, preserving existing fields and refreshing skills. */
|
||||
function buildManifest(root) {
|
||||
const manifestPath = manifestPathFor(root);
|
||||
let manifest = { ...DEFAULT_MANIFEST };
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
for (const [key, value] of Object.entries(DEFAULT_MANIFEST)) {
|
||||
if (key === "skills") continue;
|
||||
if (manifest[key] === undefined) manifest[key] = value;
|
||||
}
|
||||
}
|
||||
manifest.skills = discoverSkillPaths(root);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function serialize(manifest) {
|
||||
return `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core entry point usable from tests.
|
||||
* @returns {{ ok: boolean, manifest: object, current: string, next: string, written?: boolean }}
|
||||
*/
|
||||
function run({ root = repoRoot, check = false } = {}) {
|
||||
const manifestPath = manifestPathFor(root);
|
||||
const manifest = buildManifest(root);
|
||||
const next = serialize(manifest);
|
||||
const current = fs.existsSync(manifestPath) ? fs.readFileSync(manifestPath, "utf8") : "";
|
||||
|
||||
if (check) {
|
||||
return { ok: current === next, manifest, current, next };
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
||||
fs.writeFileSync(manifestPath, next);
|
||||
return { ok: true, manifest, current, next, written: true };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const check = process.argv.includes("--check");
|
||||
const result = run({ check });
|
||||
const count = result.manifest.skills.length;
|
||||
|
||||
if (check) {
|
||||
if (!result.ok) {
|
||||
console.error(
|
||||
"plugin.json is out of date. Run `node scripts/generate-plugin-manifest.js` and commit the result.",
|
||||
);
|
||||
let currentSkills = [];
|
||||
try {
|
||||
currentSkills = result.current ? JSON.parse(result.current).skills || [] : [];
|
||||
} catch {
|
||||
/* malformed current manifest; treat as empty for the diff */
|
||||
}
|
||||
const nextSkills = result.manifest.skills;
|
||||
const added = nextSkills.filter((s) => !currentSkills.includes(s));
|
||||
const removed = currentSkills.filter((s) => !nextSkills.includes(s));
|
||||
if (added.length) console.error(` + ${added.join(", ")}`);
|
||||
if (removed.length) console.error(` - ${removed.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`plugin.json is up to date (${count} skills).`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Wrote .claude-plugin/plugin.json with ${count} skills.`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EXCLUDED_DIRS,
|
||||
EXCLUDED_SKILLS,
|
||||
DEFAULT_MANIFEST,
|
||||
discoverSkillPaths,
|
||||
buildManifest,
|
||||
serialize,
|
||||
run,
|
||||
manifestPathFor,
|
||||
};
|
||||
105
scripts/test_generate_plugin_manifest.js
Normal file
105
scripts/test_generate_plugin_manifest.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const {
|
||||
discoverSkillPaths,
|
||||
buildManifest,
|
||||
serialize,
|
||||
run,
|
||||
manifestPathFor,
|
||||
EXCLUDED_SKILLS,
|
||||
} = require("./generate-plugin-manifest.js");
|
||||
|
||||
/** Create a throwaway repo-like tree and return its root path. */
|
||||
function makeFixtureRoot(layout) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-manifest-"));
|
||||
for (const [relPath, contents] of Object.entries(layout)) {
|
||||
const full = path.join(root, relPath);
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
fs.writeFileSync(full, contents);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const SKILL_FM = "---\nname: x\ndescription: y\n---\n";
|
||||
|
||||
test("discoverSkillPaths returns sorted ./-prefixed dirs that contain SKILL.md", () => {
|
||||
const root = makeFixtureRoot({
|
||||
"lotto-results/SKILL.md": SKILL_FM,
|
||||
"ktx-booking/SKILL.md": SKILL_FM,
|
||||
"not-a-skill/README.md": "no skill here",
|
||||
"top-level-file.md": "ignored",
|
||||
});
|
||||
assert.deepEqual(discoverSkillPaths(root), ["./ktx-booking", "./lotto-results"]);
|
||||
});
|
||||
|
||||
test("discoverSkillPaths excludes infrastructure dirs and nested fixtures", () => {
|
||||
const root = makeFixtureRoot({
|
||||
"lotto-results/SKILL.md": SKILL_FM,
|
||||
// Excluded root dirs that happen to contain a SKILL.md somewhere.
|
||||
"packages/k-lotto/SKILL.md": SKILL_FM,
|
||||
"scripts/SKILL.md": SKILL_FM,
|
||||
"tools/k-skill-qa-bot/test/fixtures/skills/kbo-results/SKILL.md": SKILL_FM,
|
||||
"docs/SKILL.md": SKILL_FM,
|
||||
// Dot-directory must be skipped regardless of contents.
|
||||
".github/SKILL.md": SKILL_FM,
|
||||
});
|
||||
assert.deepEqual(discoverSkillPaths(root), ["./lotto-results"]);
|
||||
});
|
||||
|
||||
test("discoverSkillPaths drops deprecated EXCLUDED_SKILLS", () => {
|
||||
assert.ok(EXCLUDED_SKILLS.has("blue-ribbon-nearby"));
|
||||
const root = makeFixtureRoot({
|
||||
"blue-ribbon-nearby/SKILL.md": SKILL_FM,
|
||||
"lotto-results/SKILL.md": SKILL_FM,
|
||||
});
|
||||
assert.deepEqual(discoverSkillPaths(root), ["./lotto-results"]);
|
||||
});
|
||||
|
||||
test("buildManifest backfills identity fields and preserves author overrides", () => {
|
||||
const root = makeFixtureRoot({ "lotto-results/SKILL.md": SKILL_FM });
|
||||
// Pre-seed a manifest with a custom description that must survive.
|
||||
fs.mkdirSync(path.join(root, ".claude-plugin"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
manifestPathFor(root),
|
||||
serialize({ name: "k-skill", description: "custom desc", skills: [] }),
|
||||
);
|
||||
|
||||
const manifest = buildManifest(root);
|
||||
assert.equal(manifest.description, "custom desc"); // not clobbered
|
||||
assert.equal(manifest.license, "MIT"); // backfilled from default
|
||||
assert.deepEqual(manifest.skills, ["./lotto-results"]); // always refreshed
|
||||
});
|
||||
|
||||
test("run --check passes when manifest matches, fails after drift", () => {
|
||||
const root = makeFixtureRoot({ "lotto-results/SKILL.md": SKILL_FM });
|
||||
|
||||
// First write, then a check should agree.
|
||||
const written = run({ root });
|
||||
assert.equal(written.written, true);
|
||||
assert.equal(run({ root, check: true }).ok, true);
|
||||
|
||||
// Add a new skill on disk -> check must now report drift.
|
||||
fs.mkdirSync(path.join(root, "ktx-booking"));
|
||||
fs.writeFileSync(path.join(root, "ktx-booking", "SKILL.md"), SKILL_FM);
|
||||
assert.equal(run({ root, check: true }).ok, false);
|
||||
});
|
||||
|
||||
test("run writes deterministic, trailing-newline JSON", () => {
|
||||
const root = makeFixtureRoot({ "lotto-results/SKILL.md": SKILL_FM });
|
||||
run({ root });
|
||||
const raw = fs.readFileSync(manifestPathFor(root), "utf8");
|
||||
assert.ok(raw.endsWith("\n"));
|
||||
assert.equal(raw, serialize(buildManifest(root)));
|
||||
});
|
||||
|
||||
test("marketplace manifest uses Claude validator-supported top-level keys", () => {
|
||||
const marketplacePath = path.join(__dirname, "..", ".claude-plugin", "marketplace.json");
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
|
||||
assert.deepEqual(Object.keys(marketplace).sort(), ["name", "owner", "plugins"]);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue