200 lines
6.7 KiB
Bash
200 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Builds dist/cursor/ from skills/: flat skill copies, mcp snippets, one plugin per skill.
|
|
# Usage: scripts/build-cursor.sh [skill-name]
|
|
set -euo pipefail
|
|
source "$(dirname "$0")/lib.sh"
|
|
|
|
name_filter="${1:-}"
|
|
CURSOR_DIST="$DIST_DIR/cursor"
|
|
|
|
python3 - "$ROOT" "$SKILLS_DIR" "$CURSOR_DIST" "$name_filter" <<'PY'
|
|
import json, os, re, shutil, sys
|
|
from pathlib import Path
|
|
|
|
root, skills_dir, cursor_dist, name_filter = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]), sys.argv[4]
|
|
SERVER_KEYS = {"wordcraft-mcp": "wordcraft", "gridcraft-mcp": "gridcraft"}
|
|
|
|
def skill_names():
|
|
names = sorted([p.name for p in skills_dir.iterdir() if p.is_dir()], key=lambda s: s)
|
|
if name_filter:
|
|
names = [n for n in names if n == name_filter]
|
|
return names
|
|
|
|
def skill_version(d: Path) -> str:
|
|
p = d / "CHANGELOG.md"
|
|
if not p.exists():
|
|
return ""
|
|
for line in p.read_text(encoding="utf-8").splitlines():
|
|
m = re.match(r"^## (\d+\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)\s*$", line)
|
|
if m:
|
|
return m.group(1)
|
|
return ""
|
|
|
|
def frontmatter(path: Path):
|
|
text = path.read_text(encoding="utf-8").replace("\r", "")
|
|
lines = text.split("\n")
|
|
if not lines or lines[0] != "---":
|
|
return None
|
|
out = []
|
|
for line in lines[1:]:
|
|
if line == "---":
|
|
return out
|
|
out.append(line)
|
|
return None
|
|
|
|
def fm_value(lines, key):
|
|
for line in lines:
|
|
m = re.match(rf"^{re.escape(key)}\s*:(.*)$", line)
|
|
if m:
|
|
v = m.group(1).strip()
|
|
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
|
v = v[1:-1]
|
|
return v
|
|
return None
|
|
|
|
def server_key(name):
|
|
if name in SERVER_KEYS:
|
|
return SERVER_KEYS[name]
|
|
m = re.match(r"^(.+)-mcp$", name)
|
|
return m.group(1) if m else name
|
|
|
|
def map_args_plugin(args):
|
|
out = []
|
|
for a in args:
|
|
s = str(a)
|
|
if s == "${user_config.author}":
|
|
out.append("${AUTHOR}")
|
|
elif s == "${user_config.read_root}":
|
|
out.append("${READ_ROOT}")
|
|
elif s == "${user_config.write_root}":
|
|
out.append("${WRITE_ROOT}")
|
|
else:
|
|
out.append(s)
|
|
return out
|
|
|
|
def map_args_home(args):
|
|
out = []
|
|
for a in args:
|
|
s = str(a)
|
|
if s in ("${user_config.author}", "${AUTHOR}"):
|
|
out.append("Claude")
|
|
elif s in ("${user_config.read_root}", "${READ_ROOT}"):
|
|
out.append("${userHome}/Documents")
|
|
elif s in ("${user_config.write_root}", "${WRITE_ROOT}"):
|
|
out.append("${userHome}/Documents/Claude")
|
|
else:
|
|
out.append(s)
|
|
return out
|
|
|
|
ok = True
|
|
cursor_dist.mkdir(parents=True, exist_ok=True)
|
|
for name in skill_names():
|
|
d = skills_dir / name
|
|
ver = skill_version(d)
|
|
skill = d / "SKILL.md"
|
|
fm = frontmatter(skill)
|
|
if not ver or fm is None:
|
|
print(f"FEHLER {name}: Version/Frontmatter", file=sys.stderr)
|
|
ok = False
|
|
continue
|
|
if fm_value(fm, "name") != name:
|
|
print(f"FEHLER {name}: name mismatch", file=sys.stderr)
|
|
ok = False
|
|
continue
|
|
desc = fm_value(fm, "description") or ""
|
|
if not desc or len(desc) > 1024:
|
|
print(f"FEHLER {name}: description", file=sys.stderr)
|
|
ok = False
|
|
continue
|
|
|
|
flat = cursor_dist / "skills" / name
|
|
flat.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(skill, flat / "SKILL.md")
|
|
|
|
plugin = cursor_dist / "plugins" / name
|
|
if plugin.exists():
|
|
shutil.rmtree(plugin)
|
|
(plugin / ".cursor-plugin").mkdir(parents=True)
|
|
(plugin / "skills" / name).mkdir(parents=True)
|
|
shutil.copy2(skill, plugin / "skills" / name / "SKILL.md")
|
|
|
|
display_desc = desc
|
|
author = "Dominic Gundel"
|
|
variables = None
|
|
man_path = d / "extension" / "manifest.json"
|
|
sk = server_key(name)
|
|
|
|
if man_path.exists():
|
|
man = json.loads(man_path.read_text(encoding="utf-8"))
|
|
if man.get("version") != ver:
|
|
print(f"FEHLER {name}: manifest version != CHANGELOG", file=sys.stderr)
|
|
ok = False
|
|
continue
|
|
dn = man.get("display_name") or ""
|
|
if dn.endswith(" for Claude"):
|
|
display_desc = dn.replace(" for Claude", " for Cursor") + ". " + desc
|
|
elif man.get("description"):
|
|
display_desc = man["description"]
|
|
if man.get("author", {}).get("name"):
|
|
author = man["author"]["name"]
|
|
cmd = man["server"]["mcp_config"]["command"]
|
|
args = list(man["server"]["mcp_config"].get("args") or [])
|
|
mcp_plugin = {"mcpServers": {sk: {"command": cmd, "args": map_args_plugin(args)}}}
|
|
mcp_home = {"mcpServers": {sk: {"command": cmd, "args": map_args_home(args)}}}
|
|
(cursor_dist / "mcp").mkdir(parents=True, exist_ok=True)
|
|
(cursor_dist / "mcp" / f"{name}.json").write_text(
|
|
json.dumps(mcp_home, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
)
|
|
(plugin / "mcp.json").write_text(
|
|
json.dumps(mcp_plugin, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
)
|
|
uc = man.get("user_config") or {}
|
|
props = {}
|
|
required = []
|
|
if "read_root" in uc:
|
|
props["READ_ROOT"] = {
|
|
"type": "string",
|
|
"title": "Lese-Ordner",
|
|
"description": "Weit fassen (Standard: Dokumente).",
|
|
"default": "${userHome}/Documents",
|
|
}
|
|
required.append("READ_ROOT")
|
|
if "write_root" in uc:
|
|
props["WRITE_ROOT"] = {
|
|
"type": "string",
|
|
"title": "Schreib-Ordner",
|
|
"description": "Eng halten (Standard: Dokumente/Claude).",
|
|
"default": "${userHome}/Documents/Claude",
|
|
}
|
|
required.append("WRITE_ROOT")
|
|
if "author" in uc:
|
|
props["AUTHOR"] = {
|
|
"type": "string",
|
|
"title": "Autorname",
|
|
"description": "Name fuer nachverfolgte Aenderungen.",
|
|
"default": "Claude",
|
|
}
|
|
if props:
|
|
variables = {"type": "object", "properties": props}
|
|
if required:
|
|
variables["required"] = required
|
|
|
|
plugin_json = {
|
|
"name": name,
|
|
"version": ver,
|
|
"description": display_desc,
|
|
"author": {"name": author},
|
|
"license": "MIT",
|
|
"homepage": "https://git.gundel.dev/dg/claude-skills",
|
|
"repository": "https://git.gundel.dev/dg/claude-skills.git",
|
|
}
|
|
if variables:
|
|
plugin_json["variables"] = variables
|
|
(plugin / ".cursor-plugin" / "plugin.json").write_text(
|
|
json.dumps(plugin_json, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
)
|
|
print(f"OK cursor plugin {name} {ver}")
|
|
|
|
sys.exit(0 if ok else 1)
|
|
PY
|