Merge commit 'b6b0c70' into ulw-merge-seven-prs

# Conflicts:
#	scripts/validate-skills.sh
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-05-31 17:24:25 +09:00
commit 51388a539e
6 changed files with 413 additions and 2 deletions

View 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
View 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"
]
}

View file

@ -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` 전체 스킬을 먼저 설치합니다.

View file

@ -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",

View 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,
};

View 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"]);
});