#!/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", "pdfcraft-mcp": "pdfcraft", "figcraft-mcp": "figcraft", "vectorcraft-mcp": "vectorcraft", } 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