Initial commit: Skill-Repo mit wordcraft-mcp 0.1.0
Release / release (push) Canceled after 0s

This commit is contained in:
Dominic Gundel committed 2026-10-19 14:59:33 +02:00
commit 41816fbbae
21 files changed
+1003

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Gemeinsame Funktionen fuer die Skill-Skripte. Wird per Dot-Sourcing eingebunden.
# Laeuft unter Windows PowerShell 5.1 und PowerShell 7.
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$SkillsDir = Join-Path $Root 'skills'
$DistDir = Join-Path $Root 'dist'
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
# Datei als UTF-8 lesen, Zeilenenden auf LF vereinheitlichen.
function Read-Text([string]$Path) {
return ([IO.File]::ReadAllText($Path, [Text.Encoding]::UTF8) -replace "`r", '')
}
# Datei als UTF-8 ohne BOM mit LF schreiben (Set-Content wuerde in 5.1 ein BOM schreiben).
function Write-Text([string]$Path, [string]$Text) {
[IO.File]::WriteAllText($Path, $Text, $Utf8NoBom)
}
function Test-Bom([string]$Path) {
$b = [IO.File]::ReadAllBytes($Path)
return ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF)
}
# Alle Skill-Namen (Ordner unter skills/), ordinal sortiert wie "LC_ALL=C sort".
function Get-SkillNames {
if (-not (Test-Path $SkillsDir)) { return @() }
$names = [string[]]@(Get-ChildItem -Path $SkillsDir -Directory | ForEach-Object { $_.Name })
[Array]::Sort($names, [StringComparer]::Ordinal)
return $names
}
# Gibt pro Verstoss gegen die Namensregeln einen Text zurueck.
function Get-NameErrors([string]$Name) {
$e = @()
if ($Name -cnotmatch '^[a-z0-9]+(-[a-z0-9]+)*$') { $e += "Name '$Name' ist kein kebab-case (nur a-z, 0-9 und einzelne Bindestriche)" }
if ($Name.Length -gt 64) { $e += "Name '$Name' ist laenger als 64 Zeichen" }
if ($Name -match 'claude|anthropic') { $e += "Name '$Name' darf nicht 'claude' oder 'anthropic' enthalten" }
return $e
}
# Frontmatter-Zeilen (ohne die ---) einer SKILL.md oder $null, wenn keins da ist.
function Get-Frontmatter([string]$Path) {
$lines = (Read-Text $Path) -split "`n"
if ($lines.Count -lt 2 -or $lines[0] -ne '---') { return $null }
for ($i = 1; $i -lt $lines.Count; $i++) {
if ($lines[$i] -eq '---') {
if ($i -eq 1) { return , @() }
return , @($lines[1..($i - 1)])
}
}
return $null
}
# Rohwert eines Frontmatter-Schluessels (getrimmt, Anfuehrungszeichen noch enthalten) oder $null.
function Get-FmRaw($Lines, [string]$Key) {
foreach ($l in $Lines) {
if ($l -match "^$Key\s*:(.*)$") { return $Matches[1].Trim() }
}
return $null
}
# Wert eines Frontmatter-Schluessels ohne umschliessende Anfuehrungszeichen oder $null.
function Get-FmValue($Lines, [string]$Key) {
$v = Get-FmRaw $Lines $Key
if ($null -eq $v) { return $null }
if ($v -match '^"(.*)"$' -or $v -match "^'(.*)'$") { $v = $Matches[1] }
return $v
}
# Erste Ueberschrift "## ..." aus CHANGELOG.md oder $null.
function Get-ChangelogHeading([string]$Dir) {
$p = Join-Path $Dir 'CHANGELOG.md'
if (-not (Test-Path $p)) { return $null }
foreach ($l in ((Read-Text $p) -split "`n")) {
if ($l.StartsWith('## ')) { return $l }
}
return $null
}
# Aktuelle Version eines Skills (oberster CHANGELOG-Eintrag) oder ''.
function Get-SkillVersion([string]$Dir) {
$h = Get-ChangelogHeading $Dir
if ($h -and $h -match '^## (\d+\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)\s*$') { return $Matches[1] }
return ''
}
Executable
+68
View File
@@ -0,0 +1,68 @@
# Gemeinsame Funktionen für die Skill-Skripte. Wird per "source" eingebunden.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILLS_DIR="$ROOT/skills"
DIST_DIR="$ROOT/dist"
NAME_RE='^[a-z0-9]+(-[a-z0-9]+)*$'
# Alle Skill-Namen (Ordner unter skills/), sortiert.
skill_names() {
local d
for d in "$SKILLS_DIR"/*/; do
[[ -d "$d" ]] && basename "$d"
done | LC_ALL=C sort
}
# Gibt pro Verstoß gegen die Namensregeln eine Zeile aus.
name_errors() {
local n="$1"
[[ "$n" =~ $NAME_RE ]] || echo "Name '$n' ist kein kebab-case (nur a-z, 0-9 und einzelne Bindestriche)"
(( ${#n} <= 64 )) || echo "Name '$n' ist länger als 64 Zeichen"
case "$n" in *claude*|*anthropic*) echo "Name '$n' darf nicht 'claude' oder 'anthropic' enthalten" ;; esac
return 0
}
# Frontmatter-Zeilen (ohne die ---) einer SKILL.md; Exit-Code != 0, wenn keins da ist.
frontmatter() {
tr -d '\r' < "$1" | awk '
NR == 1 { if ($0 != "---") exit 2; next }
$0 == "---" { found = 1; exit }
{ print }
END { if (!found) exit 3 }'
}
# Rohwert eines Frontmatter-Schlüssels (getrimmt, Anführungszeichen noch enthalten).
fm_raw() {
local line v
line="$(grep -m1 -E "^$2[[:space:]]*:" <<< "$1" || true)"
[[ -z "$line" ]] && return 1
v="${line#*:}"
v="${v#"${v%%[![:space:]]*}"}"
v="${v%"${v##*[![:space:]]}"}"
printf '%s' "$v"
}
# Wert eines Frontmatter-Schlüssels ohne umschließende Anführungszeichen.
fm_value() {
local v
v="$(fm_raw "$1" "$2")" || return 1
if [[ "$v" =~ ^\"(.*)\"$ ]] || [[ "$v" =~ ^\'(.*)\'$ ]]; then
v="${BASH_REMATCH[1]}"
fi
printf '%s' "$v"
}
# Erste Überschrift "## ..." aus CHANGELOG.md.
changelog_heading() {
[[ -f "$1/CHANGELOG.md" ]] || return 1
tr -d '\r' < "$1/CHANGELOG.md" | grep -m1 '^## ' || true
}
# Aktuelle Version eines Skills (oberster CHANGELOG-Eintrag) oder leer.
skill_version() {
local h
h="$(changelog_heading "$1")" || return 0
if [[ "$h" =~ ^##\ ([0-9]+\.[0-9]+\.[0-9]+)\ \([0-9]{4}-[0-9]{2}-[0-9]{2}\)[[:space:]]*$ ]]; then
printf '%s' "${BASH_REMATCH[1]}"
fi
}
+30
View File
@@ -0,0 +1,30 @@
# Legt einen neuen Skill aus templates\skill an.
# Aufruf: scripts\new-skill.ps1 -Name <skill-name>
param([Parameter(Mandatory = $true)][string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
$errs = @(Get-NameErrors $Name)
if ($errs.Count -gt 0) {
foreach ($e in $errs) { Write-Host $e }
exit 1
}
$dir = Join-Path $SkillsDir $Name
if (Test-Path $dir) {
Write-Host "skills/$Name existiert bereits."
exit 1
}
New-Item -ItemType Directory -Force -Path $SkillsDir | Out-Null
Copy-Item -Path (Join-Path $Root 'templates/skill') -Destination $dir -Recurse
$today = Get-Date -Format 'yyyy-MM-dd'
foreach ($f in @('SKILL.md', 'CHANGELOG.md')) {
$p = Join-Path $dir $f
$t = (Read-Text $p).Replace('SKILL_NAME', $Name).Replace('DATUM', $today)
Write-Text $p $t
}
& (Join-Path $PSScriptRoot 'update-readme.ps1')
Write-Host "Angelegt: skills/$Name"
Write-Host "Weiter: SKILL.md ausfuellen, dann scripts\validate.ps1 -Name $Name"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Legt einen neuen Skill aus templates/skill an.
# Aufruf: scripts/new-skill.sh <skill-name>
set -euo pipefail
source "$(dirname "$0")/lib.sh"
name="${1:-}"
if [[ -z "$name" ]]; then
echo "Aufruf: scripts/new-skill.sh <skill-name>" >&2
exit 1
fi
errs="$(name_errors "$name")"
if [[ -n "$errs" ]]; then
echo "$errs" >&2
exit 1
fi
dir="$SKILLS_DIR/$name"
if [[ -e "$dir" ]]; then
echo "skills/$name existiert bereits." >&2
exit 1
fi
mkdir -p "$SKILLS_DIR"
cp -R "$ROOT/templates/skill" "$dir"
today="$(date +%Y-%m-%d)"
for f in "$dir/SKILL.md" "$dir/CHANGELOG.md"; do
sed -e "s/SKILL_NAME/$name/g" -e "s/DATUM/$today/g" "$f" > "$f.tmp"
mv "$f.tmp" "$f"
done
bash "$(dirname "$0")/update-readme.sh"
echo "Angelegt: skills/$name"
echo "Weiter: SKILL.md ausfüllen, dann scripts/validate.sh $name"
+47
View File
@@ -0,0 +1,47 @@
# Prueft und verpackt alle Skills (oder nur den angegebenen) als dist\<name>-<version>.zip.
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene, CHANGELOG.md wird nicht mitverpackt.
# Pfade im ZIP immer mit "/" (Compress-Archive in Windows PowerShell 5.1 schreibt "\",
# das bricht den Upload). Deshalb System.IO.Compression direkt.
# Aufruf: scripts\package.ps1 [-Name <skill-name>]
param([string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
& (Join-Path $PSScriptRoot 'validate.ps1') -Name $Name
if ($LASTEXITCODE -ne 0) { exit 1 }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
$exclude = @('CHANGELOG.md', '.DS_Store', 'Thumbs.db')
foreach ($n in $names) {
$src = (Get-Item (Join-Path $SkillsDir $n)).FullName
$version = Get-SkillVersion $src
$zipPath = Join-Path $DistDir "$n-$version.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath }
$files = @(Get-ChildItem -Path $src -Recurse -File -Force |
Where-Object { $exclude -notcontains $_.Name } |
ForEach-Object { $_.FullName })
[Array]::Sort($files, [StringComparer]::Ordinal)
$zip = [IO.Compression.ZipFile]::Open($zipPath, [IO.Compression.ZipArchiveMode]::Create)
try {
[void]$zip.CreateEntry("$n/")
foreach ($f in $files) {
$rel = $f.Substring($src.Length).TrimStart('\', '/') -replace '\\', '/'
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f, "$n/$rel")
}
}
finally { $zip.Dispose() }
Write-Host ''
Write-Host "dist/$n-$version.zip"
$read = [IO.Compression.ZipFile]::OpenRead($zipPath)
try {
foreach ($e in $read.Entries) { Write-Host ('{0,9} {1}' -f $e.Length, $e.FullName) }
}
finally { $read.Dispose() }
}
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Prüft und verpackt alle Skills (oder nur den angegebenen) als dist/<name>-<version>.zip.
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene, CHANGELOG.md wird nicht mitverpackt.
# Aufruf: scripts/package.sh [<skill-name>]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
bash "$(dirname "$0")/validate.sh" "${1:-}"
if [[ $# -gt 0 && -n "$1" ]]; then
names=("$1")
else
mapfile -t names < <(skill_names)
fi
mkdir -p "$DIST_DIR"
for name in "${names[@]}"; do
version="$(skill_version "$SKILLS_DIR/$name")"
zip_path="$DIST_DIR/$name-$version.zip"
rm -f "$zip_path"
tmp="$(mktemp -d)"
mkdir "$tmp/$name"
(cd "$SKILLS_DIR/$name" && tar cf - --exclude=CHANGELOG.md --exclude=.DS_Store --exclude=Thumbs.db .) \
| (cd "$tmp/$name" && tar xf -)
(cd "$tmp" && zip -qrX "$zip_path" "$name")
rm -rf "$tmp"
echo
echo "dist/$name-$version.zip"
unzip -l "$zip_path"
done
+49
View File
@@ -0,0 +1,49 @@
# Erzeugt die Skill-Tabelle in README.md zwischen <!-- skills:start --> und <!-- skills:end -->
# neu. Mit -Check wird nur geprueft, ob die Tabelle aktuell ist (Exit-Code 1, wenn nicht).
# Aufruf: scripts\update-readme.ps1 [-Check]
param([switch]$Check)
. (Join-Path $PSScriptRoot 'lib.ps1')
$readme = Join-Path $Root 'README.md'
$current = Read-Text $readme
$lines = $current -split "`n"
$start = [Array]::IndexOf($lines, '<!-- skills:start -->')
$end = [Array]::IndexOf($lines, '<!-- skills:end -->')
if ($start -lt 0 -or $end -lt $start) {
Write-Error 'README.md enthaelt die Marker <!-- skills:start --> / <!-- skills:end --> nicht.'
exit 1
}
$table = @('| Skill | Beschreibung | Version |', '|---|---|---|')
foreach ($n in (Get-SkillNames)) {
$dir = Join-Path $SkillsDir $n
$desc = ''
$skill = Join-Path $dir 'SKILL.md'
if (Test-Path $skill) {
$fm = Get-Frontmatter $skill
if ($null -ne $fm) {
$d = Get-FmValue $fm 'description'
if ($d) { $desc = $d }
}
}
$desc = $desc.Replace('|', '\|')
$table += ('| [{0}](skills/{0}/) | {1} | {2} |' -f $n, $desc, (Get-SkillVersion $dir))
}
$newLines = @()
if ($start -ge 0) { $newLines += $lines[0..$start] }
$newLines += $table
$newLines += $lines[$end..($lines.Count - 1)]
$new = $newLines -join "`n"
if ($Check) {
if ($new -cne $current) {
Write-Host 'README.md ist nicht aktuell - scripts\update-readme.ps1 ausfuehren und committen.'
exit 1
}
Write-Host 'README.md ist aktuell.'
}
else {
Write-Text $readme $new
Write-Host 'README.md aktualisiert.'
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Erzeugt die Skill-Tabelle in README.md zwischen <!-- skills:start --> und <!-- skills:end -->
# neu. Mit --check wird nur geprüft, ob die Tabelle aktuell ist (Exit-Code 1, wenn nicht).
# Aufruf: scripts/update-readme.sh [--check]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
readme="$ROOT/README.md"
check=0
[[ "${1:-}" == "--check" ]] && check=1
if ! grep -q '<!-- skills:start -->' "$readme" || ! grep -q '<!-- skills:end -->' "$readme"; then
echo "README.md enthält die Marker <!-- skills:start --> / <!-- skills:end --> nicht." >&2
exit 1
fi
table="$(mktemp)"
trap 'rm -f "$table"' EXIT
{
echo "| Skill | Beschreibung | Version |"
echo "|---|---|---|"
while IFS= read -r name; do
[[ -z "$name" ]] && continue
desc=""
if fm="$(frontmatter "$SKILLS_DIR/$name/SKILL.md" 2>/dev/null)"; then
desc="$(fm_value "$fm" description || true)"
fi
desc="${desc//|/\\|}"
printf '| [%s](skills/%s/) | %s | %s |\n' "$name" "$name" "$desc" "$(skill_version "$SKILLS_DIR/$name")"
done < <(skill_names)
} > "$table"
new="$(tr -d '\r' < "$readme" | awk -v t="$table" '
/<!-- skills:start -->/ { print; while ((getline l < t) > 0) print l; skip = 1; next }
/<!-- skills:end -->/ { skip = 0 }
!skip { print }')"
if [[ $check -eq 1 ]]; then
if [[ "$new" != "$(tr -d '\r' < "$readme")" ]]; then
echo "README.md ist nicht aktuell – scripts/update-readme.sh ausführen und committen." >&2
exit 1
fi
echo "README.md ist aktuell."
else
printf '%s\n' "$new" > "$readme"
echo "README.md aktualisiert."
fi
+77
View File
@@ -0,0 +1,77 @@
# Prueft alle Skills (oder nur den angegebenen) auf die Regeln aus CONTRIBUTING.md.
# Aufruf: scripts\validate.ps1 [-Name <skill-name>]
param([string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
function Test-Skill([string]$N) {
$dir = Join-Path $SkillsDir $N
if (-not (Test-Path $dir -PathType Container)) { return @("Ordner skills/$N existiert nicht") }
$errs = @(Get-NameErrors $N)
$skill = Join-Path $dir 'SKILL.md'
if (-not (Test-Path $skill)) {
$errs += 'SKILL.md fehlt'
}
elseif (Test-Bom $skill) {
$errs += 'SKILL.md beginnt mit einem BOM - als UTF-8 ohne BOM speichern'
}
else {
$fm = Get-Frontmatter $skill
if ($null -eq $fm) {
$errs += "SKILL.md hat kein Frontmatter (erste Zeile '---', danach schliessendes '---')"
}
else {
$fmName = Get-FmValue $fm 'name'
if (-not $fmName) { $errs += "Frontmatter: 'name' fehlt" }
elseif ($fmName -cne $N) { $errs += "Frontmatter: name '$fmName' passt nicht zum Ordnernamen '$N'" }
$desc = Get-FmValue $fm 'description'
if (-not $desc) {
$errs += "Frontmatter: 'description' fehlt oder ist leer"
}
else {
if ($desc.Length -gt 1024) { $errs += "Frontmatter: description ist laenger als 1024 Zeichen ($($desc.Length))" }
if ($desc -match '[<>]') { $errs += 'Frontmatter: description darf keine spitzen Klammern < > enthalten' }
$raw = Get-FmRaw $fm 'description'
if (-not ($raw.StartsWith('"') -or $raw.StartsWith("'")) -and $raw.Contains(': ')) {
$errs += "Frontmatter: description enthaelt ': ' - in Anfuehrungszeichen setzen, sonst ist das YAML ungueltig"
}
}
}
}
if (-not (Test-Path (Join-Path $dir 'CHANGELOG.md'))) {
$errs += 'CHANGELOG.md fehlt'
}
else {
$h = Get-ChangelogHeading $dir
if (-not $h) { $errs += "CHANGELOG.md hat keinen Eintrag '## x.y.z (YYYY-MM-DD)'" }
elseif (-not (Get-SkillVersion $dir)) { $errs += "CHANGELOG.md: oberster Eintrag '$h' hat nicht das Format '## x.y.z (YYYY-MM-DD)'" }
}
return $errs
}
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
if ($names.Count -eq 0) {
Write-Host 'Keine Skills unter skills/ gefunden.'
exit 0
}
$failed = $false
foreach ($n in $names) {
$errs = @(Test-Skill $n)
if ($errs.Count -eq 0) {
Write-Host ("OK {0} ({1})" -f $n, (Get-SkillVersion (Join-Path $SkillsDir $n)))
}
else {
Write-Host "FEHLER $n"
foreach ($e in $errs) { Write-Host " - $e" }
$failed = $true
}
}
if ($failed) { exit 1 }
exit 0
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Prüft alle Skills (oder nur den angegebenen) auf die Regeln aus CONTRIBUTING.md.
# Aufruf: scripts/validate.sh [<skill-name>]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
validate_skill() {
local name="$1" dir="$SKILLS_DIR/$1" fm fm_name desc raw heading
local -a errs=()
if [[ ! -d "$dir" ]]; then
echo " - Ordner skills/$name existiert nicht"
return
fi
mapfile -t errs < <(name_errors "$name")
if [[ ! -f "$dir/SKILL.md" ]]; then
errs+=("SKILL.md fehlt")
else
if [[ "$(head -c3 "$dir/SKILL.md" | od -An -tx1 | tr -d ' \n')" == "efbbbf" ]]; then
errs+=("SKILL.md beginnt mit einem BOM – als UTF-8 ohne BOM speichern")
elif ! fm="$(frontmatter "$dir/SKILL.md")"; then
errs+=("SKILL.md hat kein Frontmatter (erste Zeile '---', danach schließendes '---')")
else
if ! fm_name="$(fm_value "$fm" name)" || [[ -z "$fm_name" ]]; then
errs+=("Frontmatter: 'name' fehlt")
elif [[ "$fm_name" != "$name" ]]; then
errs+=("Frontmatter: name '$fm_name' passt nicht zum Ordnernamen '$name'")
fi
if ! desc="$(fm_value "$fm" description)" || [[ -z "$desc" ]]; then
errs+=("Frontmatter: 'description' fehlt oder ist leer")
else
(( ${#desc} <= 1024 )) || errs+=("Frontmatter: description ist länger als 1024 Zeichen (${#desc})")
[[ "$desc" == *"<"* || "$desc" == *">"* ]] && errs+=("Frontmatter: description darf keine spitzen Klammern < > enthalten")
raw="$(fm_raw "$fm" description)"
if [[ "$raw" != \"* && "$raw" != \'* && "$raw" == *": "* ]]; then
errs+=("Frontmatter: description enthält ': ' – in Anführungszeichen setzen, sonst ist das YAML ungültig")
fi
fi
fi
fi
if [[ ! -f "$dir/CHANGELOG.md" ]]; then
errs+=("CHANGELOG.md fehlt")
else
heading="$(changelog_heading "$dir")"
if [[ -z "$heading" ]]; then
errs+=("CHANGELOG.md hat keinen Eintrag '## x.y.z (YYYY-MM-DD)'")
elif [[ -z "$(skill_version "$dir")" ]]; then
errs+=("CHANGELOG.md: oberster Eintrag '$heading' hat nicht das Format '## x.y.z (YYYY-MM-DD)'")
fi
fi
local e
for e in "${errs[@]+"${errs[@]}"}"; do echo " - $e"; done
}
if [[ $# -gt 0 && -n "$1" ]]; then
names=("$1")
else
mapfile -t names < <(skill_names)
fi
if [[ ${#names[@]} -eq 0 ]]; then
echo "Keine Skills unter skills/ gefunden."
exit 0
fi
failed=0
for name in "${names[@]}"; do
out="$(validate_skill "$name")"
if [[ -z "$out" ]]; then
echo "OK $name ($(skill_version "$SKILLS_DIR/$name"))"
else
echo "FEHLER $name"
echo "$out"
failed=1
fi
done
exit "$failed"