Initial commit: claude-skills with wordcraft-mcp and gridcraft-mcp 0.1.0.
This commit is contained in:
commit
cf01e22a1e
50 files changed
+2726
No files matched your search
@@ -0,0 +1,97 @@
|
||||
# Checks that the ArtCraft-Launcher CLI is installed and refreshes upstream.json + LICENSES.
|
||||
# Does NOT copy or ship the EXE into the extension package.
|
||||
# Usage: scripts\fetch-cli.ps1 -Name <skill-name> [-CliPath <path>]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[string]$CliPath
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
$dir = Join-Path $SkillsDir $Name
|
||||
$ext = Join-Path $dir 'extension'
|
||||
if (-not (Test-Path (Join-Path $ext 'manifest.json'))) {
|
||||
Write-Host "skills/$Name/extension/manifest.json fehlt."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$defaults = @{
|
||||
'wordcraft-mcp' = @{
|
||||
ExeName = 'wordcraft-cli.exe'
|
||||
Default = 'C:\Program Files\WordCraft\wordcraft-cli.exe'
|
||||
Repo = 'https://github.com/storytold/wordcraft'
|
||||
RepoSlug = 'storytold/wordcraft'
|
||||
}
|
||||
'gridcraft-mcp' = @{
|
||||
ExeName = 'gridcraft-cli.exe'
|
||||
Default = 'C:\Program Files\GridCraft\gridcraft-cli.exe'
|
||||
Repo = 'https://github.com/storytold/gridcraft'
|
||||
RepoSlug = 'storytold/gridcraft'
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $defaults.ContainsKey($Name)) {
|
||||
Write-Host "Kein CLI-Mapping fuer Skill '$Name'."
|
||||
exit 1
|
||||
}
|
||||
$meta = $defaults[$Name]
|
||||
$src = if ($CliPath) { $CliPath } else { $meta.Default }
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Write-Host "CLI fehlt: $src"
|
||||
Write-Host "Bitte WordCraft/GridCraft ueber den ArtCraft-Launcher installieren, oder -CliPath angeben."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure no EXE is left in the extension package from older builds.
|
||||
$serverDir = Join-Path $ext 'server'
|
||||
New-Item -ItemType Directory -Force -Path $serverDir | Out-Null
|
||||
Get-ChildItem -LiteralPath $serverDir -Filter '*.exe' -ErrorAction SilentlyContinue | Remove-Item -Force
|
||||
$readme = Join-Path $serverDir 'README.txt'
|
||||
if (-not (Test-Path $readme)) {
|
||||
Write-Text $readme @"
|
||||
Diese Erweiterung enthaelt keine CLI.
|
||||
Installiere die App ueber den ArtCraft-Launcher.
|
||||
Erwarteter Pfad: $src
|
||||
"@
|
||||
}
|
||||
|
||||
$verOut = & $src --version 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0 -and $Name -eq 'gridcraft-mcp') {
|
||||
$verOut = & $src version 2>&1 | Out-String
|
||||
}
|
||||
$ver = ''
|
||||
if ($verOut -match '(\d+\.\d+\.\d+)') { $ver = $Matches[1] }
|
||||
if (-not $ver) {
|
||||
Write-Host "Version der CLI nicht lesbar: $verOut"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$hash = (Get-FileHash -LiteralPath $src -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$upstream = @{
|
||||
source = 'ArtCraft-Launcher'
|
||||
path = $src
|
||||
exe = $meta.ExeName
|
||||
version = $ver
|
||||
sha256 = $hash
|
||||
repository = $meta.Repo
|
||||
tag = "v$ver"
|
||||
bundled = $false
|
||||
} | ConvertTo-Json -Compress
|
||||
Write-Text (Join-Path $ext 'upstream.json') ($upstream + "`n")
|
||||
|
||||
$licDir = Join-Path $ext 'LICENSES'
|
||||
New-Item -ItemType Directory -Force -Path $licDir | Out-Null
|
||||
$tag = "v$ver"
|
||||
foreach ($f in @('LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE')) {
|
||||
$url = "https://raw.githubusercontent.com/$($meta.RepoSlug)/$tag/$f"
|
||||
$out = Join-Path $licDir $f
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing
|
||||
}
|
||||
catch {
|
||||
Write-Host "Lizenzdatei fehlgeschlagen: $url"
|
||||
Write-Host $_
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "OK $Name $ver (nicht gebundelt) $src"
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks that the ArtCraft-Launcher CLI is installed and refreshes upstream.json + LICENSES.
|
||||
# Does NOT copy or ship the EXE into the extension package.
|
||||
# Usage: scripts/fetch-cli.sh <skill-name> [cli-path]
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
name="${1:-}"
|
||||
cli_path="${2:-}"
|
||||
if [[ -z "$name" ]]; then
|
||||
echo "Aufruf: scripts/fetch-cli.sh <skill-name> [cli-path]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ext="$SKILLS_DIR/$name/extension"
|
||||
if [[ ! -f "$ext/manifest.json" ]]; then
|
||||
echo "skills/$name/extension/manifest.json fehlt." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$name" in
|
||||
wordcraft-mcp)
|
||||
exe_name=wordcraft-cli.exe
|
||||
default='C:/Program Files/WordCraft/wordcraft-cli.exe'
|
||||
repo='https://github.com/storytold/wordcraft'
|
||||
repo_slug='storytold/wordcraft'
|
||||
;;
|
||||
gridcraft-mcp)
|
||||
exe_name=gridcraft-cli.exe
|
||||
default='C:/Program Files/GridCraft/gridcraft-cli.exe'
|
||||
repo='https://github.com/storytold/gridcraft'
|
||||
repo_slug='storytold/gridcraft'
|
||||
;;
|
||||
*)
|
||||
echo "Kein CLI-Mapping fuer Skill '$name'." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
src="${cli_path:-$default}"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "CLI fehlt: $src" >&2
|
||||
echo "Bitte WordCraft/GridCraft ueber den ArtCraft-Launcher installieren, oder Pfad als Argument angeben." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ext/server"
|
||||
rm -f "$ext/server"/*.exe
|
||||
if [[ ! -f "$ext/server/README.txt" ]]; then
|
||||
printf '%s\n' \
|
||||
'Diese Erweiterung enthaelt keine CLI.' \
|
||||
'Installiere die App ueber den ArtCraft-Launcher.' \
|
||||
"Erwarteter Pfad: $src" > "$ext/server/README.txt"
|
||||
fi
|
||||
|
||||
ver_out="$("$src" --version 2>&1 || true)"
|
||||
if [[ ! "$ver_out" =~ [0-9]+\.[0-9]+\.[0-9]+ ]] && [[ "$name" == gridcraft-mcp ]]; then
|
||||
ver_out="$("$src" version 2>&1 || true)"
|
||||
fi
|
||||
if [[ ! "$ver_out" =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then
|
||||
echo "Version der CLI nicht lesbar: $ver_out" >&2
|
||||
exit 1
|
||||
fi
|
||||
ver="${BASH_REMATCH[1]}"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
hash="$(sha256sum "$src" | awk '{print $1}')"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
hash="$(shasum -a 256 "$src" | awk '{print $1}')"
|
||||
else
|
||||
hash="$(python3 -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$src")"
|
||||
fi
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
print(json.dumps({
|
||||
'source': 'ArtCraft-Launcher',
|
||||
'path': sys.argv[1],
|
||||
'exe': sys.argv[2],
|
||||
'version': sys.argv[3],
|
||||
'sha256': sys.argv[4],
|
||||
'repository': sys.argv[5],
|
||||
'tag': 'v' + sys.argv[3],
|
||||
'bundled': False,
|
||||
}, separators=(',', ':')))
|
||||
" "$src" "$exe_name" "$ver" "$hash" "$repo" > "$ext/upstream.json"
|
||||
printf '\n' >> "$ext/upstream.json" || true
|
||||
|
||||
mkdir -p "$ext/LICENSES"
|
||||
tag="v$ver"
|
||||
for f in LICENSE-MIT LICENSE-APACHE NOTICE; do
|
||||
url="https://raw.githubusercontent.com/$repo_slug/$tag/$f"
|
||||
if ! curl -fsSL "$url" -o "$ext/LICENSES/$f"; then
|
||||
echo "Lizenzdatei fehlgeschlagen: $url" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "OK $name $ver (nicht gebundelt) $src"
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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', 'README.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-index.ps1')
|
||||
|
||||
Write-Host "Angelegt: skills/$Name"
|
||||
Write-Host "Weiter: SKILL.md und README.md ausfuellen, dann scripts\validate.ps1 -Name $Name"
|
||||
Executable
+36
@@ -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/README.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-index.sh"
|
||||
|
||||
echo "Angelegt: skills/$name"
|
||||
echo "Weiter: SKILL.md und README.md ausfüllen, dann scripts/validate.sh $name"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Builds ZIP for every skill and .mcpb when skills\<name>\extension\ exists.
|
||||
# Usage: scripts\package-all.ps1
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
& (Join-Path $PSScriptRoot 'package.ps1')
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
foreach ($n in (Get-SkillNames)) {
|
||||
$ext = Join-Path (Join-Path $SkillsDir $n) 'extension\manifest.json'
|
||||
if (Test-Path $ext) {
|
||||
& (Join-Path $PSScriptRoot 'package-mcpb.ps1') -Name $n
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds ZIP for every skill and .mcpb when skills/<name>/extension/ exists.
|
||||
# Usage: scripts/package-all.sh
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
bash "$(dirname "$0")/package.sh"
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
|
||||
bash "$(dirname "$0")/package-mcpb.sh" "$name"
|
||||
fi
|
||||
done < <(skill_names)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Builds dist\<name>-v<version>.mcpb from skills\<name>\extension\
|
||||
# Steps: fetch-cli, mcpb validate, mcpb pack. Unsigned (no cert).
|
||||
# Usage: scripts\package-mcpb.ps1 -Name <skill-name> [-CliPath <path>]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[string]$CliPath
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
$dir = Join-Path $SkillsDir $Name
|
||||
$ext = Join-Path $dir 'extension'
|
||||
if (-not (Test-Path (Join-Path $ext 'manifest.json'))) {
|
||||
Write-Host "skills/$Name/extension/ fehlt."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ver = Get-SkillVersion $dir
|
||||
if (-not $ver) {
|
||||
Write-Host "CHANGELOG von $Name hat keine Version."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$fetchArgs = @{ Name = $Name }
|
||||
if ($CliPath) { $fetchArgs.CliPath = $CliPath }
|
||||
& (Join-Path $PSScriptRoot 'fetch-cli.ps1') @fetchArgs
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
$manPath = Join-Path $ext 'manifest.json'
|
||||
$man = Read-Text $manPath | ConvertFrom-Json
|
||||
if ($man.version -cne $ver) {
|
||||
Write-Host "manifest.json version '$($man.version)' != CHANGELOG '$ver'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
|
||||
$out = Join-Path $DistDir "$Name-v$ver.mcpb"
|
||||
if (Test-Path $out) { Remove-Item $out -Force }
|
||||
|
||||
Push-Location $ext
|
||||
try {
|
||||
npx --yes @anthropic-ai/mcpb validate .
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
npx --yes @anthropic-ai/mcpb pack . $out
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
}
|
||||
finally { Pop-Location }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "dist/$Name-v$ver.mcpb"
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zip = [IO.Compression.ZipFile]::OpenRead($out)
|
||||
try {
|
||||
foreach ($e in $zip.Entries) {
|
||||
Write-Host ('{0,12} {1}' -f $e.Length, $e.FullName)
|
||||
}
|
||||
}
|
||||
finally { $zip.Dispose() }
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds dist/<name>-v<version>.mcpb from skills/<name>/extension/
|
||||
# Steps: fetch-cli, mcpb validate, mcpb pack. Unsigned (no cert).
|
||||
# Usage: scripts/package-mcpb.sh <skill-name> [cli-path]
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
name="${1:-}"
|
||||
cli_path="${2:-}"
|
||||
if [[ -z "$name" ]]; then
|
||||
echo "Aufruf: scripts/package-mcpb.sh <skill-name> [cli-path]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dir="$SKILLS_DIR/$name"
|
||||
ext="$dir/extension"
|
||||
[[ -f "$ext/manifest.json" ]] || { echo "skills/$name/extension/ fehlt." >&2; exit 1; }
|
||||
|
||||
ver="$(skill_version "$dir")"
|
||||
[[ -n "$ver" ]] || { echo "CHANGELOG von $name hat keine Version." >&2; exit 1; }
|
||||
|
||||
if [[ -n "$cli_path" ]]; then
|
||||
bash "$(dirname "$0")/fetch-cli.sh" "$name" "$cli_path"
|
||||
else
|
||||
bash "$(dirname "$0")/fetch-cli.sh" "$name"
|
||||
fi
|
||||
|
||||
man_ver="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1],encoding="utf-8"))["version"])' "$ext/manifest.json")"
|
||||
if [[ "$man_ver" != "$ver" ]]; then
|
||||
echo "manifest.json version '$man_ver' != CHANGELOG '$ver'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DIST_DIR"
|
||||
out="$DIST_DIR/$name-v$ver.mcpb"
|
||||
rm -f "$out"
|
||||
|
||||
(
|
||||
cd "$ext"
|
||||
npx --yes @anthropic-ai/mcpb validate .
|
||||
npx --yes @anthropic-ai/mcpb pack . "$out"
|
||||
)
|
||||
|
||||
echo
|
||||
echo "dist/$name-v$ver.mcpb"
|
||||
unzip -l "$out"
|
||||
@@ -0,0 +1,52 @@
|
||||
# Prueft und verpackt alle Skills (oder nur den angegebenen) als dist\<name>-v<version>.zip.
|
||||
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene.
|
||||
# README.md und CHANGELOG.md werden 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', 'README.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-v$version.zip"
|
||||
if (Test-Path $zipPath) { Remove-Item $zipPath }
|
||||
|
||||
$extPrefix = (Join-Path $src 'extension') + [IO.Path]::DirectorySeparatorChar
|
||||
$files = @(Get-ChildItem -Path $src -Recurse -File -Force |
|
||||
Where-Object {
|
||||
$exclude -notcontains $_.Name -and
|
||||
(-not $_.FullName.StartsWith($extPrefix, [StringComparison]::OrdinalIgnoreCase))
|
||||
} |
|
||||
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-v$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() }
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prüft und verpackt alle Skills (oder nur den angegebenen) als dist/<name>-v<version>.zip.
|
||||
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene.
|
||||
# README.md und CHANGELOG.md werden 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-v$version.zip"
|
||||
rm -f "$zip_path"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
mkdir "$tmp/$name"
|
||||
(cd "$SKILLS_DIR/$name" && tar cf - --exclude=CHANGELOG.md --exclude=README.md --exclude=.DS_Store --exclude=Thumbs.db --exclude=extension .) \
|
||||
| (cd "$tmp/$name" && tar xf -)
|
||||
(cd "$tmp" && zip -qrX "$zip_path" "$name")
|
||||
rm -rf "$tmp"
|
||||
|
||||
echo
|
||||
echo "dist/$name-v$version.zip"
|
||||
unzip -l "$zip_path"
|
||||
done
|
||||
@@ -0,0 +1,141 @@
|
||||
# Builds the skill ZIP (and .mcpb if extension/) and creates a remote release with those assets.
|
||||
# Auth: same as git push (credential helper), or env RELEASE_TOKEN if set.
|
||||
# The tag <name>/v<version> must exist locally and on the remote. Does not push tags.
|
||||
# Usage: scripts\release.ps1 -Name <skill-name>
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
$RepoApi = 'https://git.gundel.dev/api/v1/repos/dg/claude-skills'
|
||||
$RepoHost = 'git.gundel.dev'
|
||||
|
||||
$AuthHeader = $null
|
||||
if ($env:RELEASE_TOKEN) {
|
||||
$AuthHeader = "token $($env:RELEASE_TOKEN)"
|
||||
}
|
||||
else {
|
||||
$credRaw = "protocol=https`nhost=$RepoHost`n`n" | git credential fill 2>$null
|
||||
$user = $null; $pass = $null
|
||||
foreach ($line in $credRaw) {
|
||||
if ($line -match '^username=(.*)$') { $user = $Matches[1] }
|
||||
if ($line -match '^password=(.*)$') { $pass = $Matches[1] }
|
||||
}
|
||||
if ($user -and $pass) {
|
||||
$pair = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${user}:${pass}"))
|
||||
$AuthHeader = "Basic $pair"
|
||||
}
|
||||
}
|
||||
if (-not $AuthHeader) {
|
||||
Write-Host 'Keine Auth fuer das Release (git credential oder RELEASE_TOKEN).'
|
||||
exit 1
|
||||
}
|
||||
|
||||
$dir = Join-Path $SkillsDir $Name
|
||||
if (-not (Test-Path $dir -PathType Container)) {
|
||||
Write-Host "Ordner skills/$Name existiert nicht."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ver = Get-SkillVersion $dir
|
||||
if (-not $ver) {
|
||||
Write-Host "CHANGELOG von $Name hat keine Version '## x.y.z (YYYY-MM-DD)'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tag = "$Name/v$ver"
|
||||
git -C $Root rev-parse --verify --quiet "refs/tags/$tag" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Tag $tag fehlt lokal. Setzen, nicht verschieben: git tag $tag"
|
||||
exit 1
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'package.ps1') -Name $Name
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
$zipName = "$Name-v$ver.zip"
|
||||
$zipPath = Join-Path $DistDir $zipName
|
||||
if (-not (Test-Path $zipPath)) {
|
||||
Write-Host "ZIP fehlt: dist/$zipName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$on = $false
|
||||
$acc = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($l in ((Read-Text (Join-Path $dir 'CHANGELOG.md')) -split "`n")) {
|
||||
if (-not $on) {
|
||||
if ($l.StartsWith("## $ver ")) { $on = $true }
|
||||
continue
|
||||
}
|
||||
if ($l.StartsWith('## ')) { break }
|
||||
$acc.Add($l)
|
||||
}
|
||||
$notes = (($acc -join "`n").Trim())
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
function Invoke-ReleaseApi([string]$Method, [string]$Url, [byte[]]$Body, [string]$ContentType) {
|
||||
$req = [Net.HttpWebRequest]::Create($Url)
|
||||
$req.Method = $Method
|
||||
$req.Headers.Add('Authorization', $AuthHeader)
|
||||
$req.Accept = 'application/json'
|
||||
if ($null -ne $Body) {
|
||||
$req.ContentType = $ContentType
|
||||
$req.ContentLength = $Body.LongLength
|
||||
$stream = $req.GetRequestStream()
|
||||
$stream.Write($Body, 0, $Body.Length)
|
||||
$stream.Close()
|
||||
}
|
||||
try {
|
||||
$resp = $req.GetResponse()
|
||||
}
|
||||
catch [System.Net.WebException] {
|
||||
$errResp = $_.Exception.Response
|
||||
$detail = ''
|
||||
if ($errResp) {
|
||||
$reader = New-Object IO.StreamReader($errResp.GetResponseStream())
|
||||
$detail = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
}
|
||||
throw "API-Fehler $Url : $detail"
|
||||
}
|
||||
$reader = New-Object IO.StreamReader($resp.GetResponseStream(), $Utf8NoBom)
|
||||
$text = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
$resp.Close()
|
||||
return $text
|
||||
}
|
||||
|
||||
$payload = @{
|
||||
tag_name = $tag
|
||||
name = "$Name $ver"
|
||||
body = $notes
|
||||
} | ConvertTo-Json -Compress
|
||||
try {
|
||||
$created = Invoke-ReleaseApi 'POST' "$RepoApi/releases" $Utf8NoBom.GetBytes($payload) 'application/json; charset=utf-8' | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
Write-Host $_
|
||||
Write-Host "Tag auf dem Server? git push origin $tag"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$assetUrl = "$RepoApi/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($zipName))"
|
||||
$asset = Invoke-ReleaseApi 'POST' $assetUrl ([IO.File]::ReadAllBytes($zipPath)) 'application/octet-stream' | ConvertFrom-Json
|
||||
|
||||
$mcpbName = "$Name-v$ver.mcpb"
|
||||
$mcpbPath = Join-Path $DistDir $mcpbName
|
||||
$mcpbAsset = $null
|
||||
if (Test-Path (Join-Path (Join-Path $dir 'extension') 'manifest.json')) {
|
||||
if (-not (Test-Path $mcpbPath)) {
|
||||
& (Join-Path $PSScriptRoot 'package-mcpb.ps1') -Name $Name
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
}
|
||||
if (Test-Path $mcpbPath) {
|
||||
$mcpbUrl = "$RepoApi/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($mcpbName))"
|
||||
$mcpbAsset = Invoke-ReleaseApi 'POST' $mcpbUrl ([IO.File]::ReadAllBytes($mcpbPath)) 'application/octet-stream' | ConvertFrom-Json
|
||||
}
|
||||
}
|
||||
|
||||
if ($created.html_url) { Write-Host $created.html_url }
|
||||
if ($asset.browser_download_url) { Write-Host $asset.browser_download_url }
|
||||
if ($mcpbAsset -and $mcpbAsset.browser_download_url) { Write-Host $mcpbAsset.browser_download_url }
|
||||
Write-Host "Release $tag angelegt."
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the skill ZIP (and .mcpb if extension/) and creates a remote release with those assets.
|
||||
# Auth: git credential helper, or env RELEASE_TOKEN if set.
|
||||
# The tag <name>/v<version> must exist locally and on the remote. Does not push tags.
|
||||
# Usage: scripts/release.sh <skill-name>
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
name="${1:-}"
|
||||
if [[ -z "$name" ]]; then
|
||||
echo "Aufruf: scripts/release.sh <skill-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v python3 >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
|
||||
echo "release.sh braucht curl und python3. Unter Windows: scripts\\release.ps1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_HOST=git.gundel.dev
|
||||
api="https://$REPO_HOST/api/v1/repos/dg/claude-skills"
|
||||
|
||||
auth_header=""
|
||||
if [[ -n "${RELEASE_TOKEN:-}" ]]; then
|
||||
auth_header="Authorization: token $RELEASE_TOKEN"
|
||||
else
|
||||
cred="$(printf 'protocol=https\nhost=%s\n\n' "$REPO_HOST" | git credential fill 2>/dev/null || true)"
|
||||
user="$(printf '%s\n' "$cred" | sed -n 's/^username=//p' | head -n1)"
|
||||
pass="$(printf '%s\n' "$cred" | sed -n 's/^password=//p' | head -n1)"
|
||||
if [[ -n "$user" && -n "$pass" ]]; then
|
||||
basic="$(printf '%s:%s' "$user" "$pass" | base64 | tr -d '\n')"
|
||||
auth_header="Authorization: Basic $basic"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$auth_header" ]]; then
|
||||
echo "Keine Auth fuer das Release (git credential oder RELEASE_TOKEN)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dir="$SKILLS_DIR/$name"
|
||||
version="$(skill_version "$dir")"
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "CHANGELOG von $name hat keine Version '## x.y.z (YYYY-MM-DD)'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag="$name/v$version"
|
||||
if ! git -C "$ROOT" rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; then
|
||||
echo "Tag $tag fehlt lokal. Setzen, nicht verschieben: git tag $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$(dirname "$0")/package.sh" "$name"
|
||||
|
||||
zip_name="$name-v$version.zip"
|
||||
zip_path="$DIST_DIR/$zip_name"
|
||||
[[ -f "$zip_path" ]] || { echo "ZIP fehlt: dist/$zip_name" >&2; exit 1; }
|
||||
|
||||
notes="$(mktemp)"
|
||||
payload="$(mktemp)"
|
||||
trap 'rm -f "$notes" "$payload"' EXIT
|
||||
awk -v v="$version" '
|
||||
index($0, "## " v " ") == 1 { on = 1; next }
|
||||
/^## / && on { exit }
|
||||
on { print }' "$dir/CHANGELOG.md" > "$notes"
|
||||
|
||||
TAG="$tag" TITLE="$name $version" NOTES="$notes" python3 -c '
|
||||
import json, os
|
||||
print(json.dumps({
|
||||
"tag_name": os.environ["TAG"],
|
||||
"name": os.environ["TITLE"],
|
||||
"body": open(os.environ["NOTES"], encoding="utf-8").read().strip(),
|
||||
}))' > "$payload"
|
||||
|
||||
resp="$(mktemp)"
|
||||
code="$(curl -sS -o "$resp" -w '%{http_code}' -X POST "$api/releases" \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @"$payload")"
|
||||
if [[ "$code" != "201" ]]; then
|
||||
echo "API $code" >&2
|
||||
cat "$resp" >&2
|
||||
echo "Tag auf dem Server? git push origin $tag" >&2
|
||||
rm -f "$resp"
|
||||
exit 1
|
||||
fi
|
||||
created="$(cat "$resp")"
|
||||
rm -f "$resp"
|
||||
rel_id="$(CREATED="$created" python3 -c 'import json,os; print(json.loads(os.environ["CREATED"])["id"])')"
|
||||
html="$(CREATED="$created" python3 -c 'import json,os; print(json.loads(os.environ["CREATED"]).get("html_url",""))')"
|
||||
|
||||
upload_asset() {
|
||||
local file_path="$1" file_name="$2"
|
||||
local asset_resp code asset download
|
||||
asset_resp="$(mktemp)"
|
||||
code="$(curl -sS -o "$asset_resp" -w '%{http_code}' -X POST "$api/releases/$rel_id/assets?name=$file_name" \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$file_path")"
|
||||
if [[ "$code" != "201" ]]; then
|
||||
echo "API $code" >&2
|
||||
cat "$asset_resp" >&2
|
||||
rm -f "$asset_resp"
|
||||
exit 1
|
||||
fi
|
||||
asset="$(cat "$asset_resp")"
|
||||
rm -f "$asset_resp"
|
||||
download="$(ASSET="$asset" python3 -c 'import json,os; print(json.loads(os.environ["ASSET"]).get("browser_download_url",""))')"
|
||||
[[ -n "$download" ]] && echo "$download"
|
||||
}
|
||||
|
||||
upload_asset "$zip_path" "$zip_name"
|
||||
|
||||
mcpb_name="$name-v$version.mcpb"
|
||||
mcpb_path="$DIST_DIR/$mcpb_name"
|
||||
if [[ -f "$dir/extension/manifest.json" ]]; then
|
||||
if [[ ! -f "$mcpb_path" ]]; then
|
||||
bash "$(dirname "$0")/package-mcpb.sh" "$name"
|
||||
fi
|
||||
if [[ -f "$mcpb_path" ]]; then
|
||||
upload_asset "$mcpb_path" "$mcpb_name"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n "$html" ]] && echo "$html"
|
||||
echo "Release $tag angelegt."
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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-index.ps1 [-Check]
|
||||
param([switch]$Check)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
$RepoWeb = 'https://git.gundel.dev/dg/claude-skills'
|
||||
$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 | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |', '|---|---|---|---|---|---|')
|
||||
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('|', '\|')
|
||||
$ver = Get-SkillVersion $dir
|
||||
$tagEnc = [Uri]::EscapeDataString("$n/v$ver")
|
||||
$zipLink = "[{0}-v{1}.zip]({2}/releases/download/{3}/{0}-v{1}.zip)" -f $n, $ver, $RepoWeb, $tagEnc
|
||||
$mcpb = ''
|
||||
if (Test-Path (Join-Path $dir 'extension\manifest.json')) {
|
||||
$mcpb = "[{0}-v{1}.mcpb]({2}/releases/download/{3}/{0}-v{1}.mcpb)" -f $n, $ver, $RepoWeb, $tagEnc
|
||||
}
|
||||
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} |' -f $n, $desc, $ver, $zipLink, $mcpb)
|
||||
}
|
||||
|
||||
$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-index.ps1 ausfuehren und committen.'
|
||||
exit 1
|
||||
}
|
||||
Write-Host 'README.md ist aktuell.'
|
||||
}
|
||||
else {
|
||||
Write-Text $readme $new
|
||||
Write-Host 'README.md aktualisiert.'
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/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-index.sh [--check]
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
REPO_WEB="https://git.gundel.dev/dg/claude-skills"
|
||||
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 | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
desc=""
|
||||
ver="$(skill_version "$SKILLS_DIR/$name")"
|
||||
if fm="$(frontmatter "$SKILLS_DIR/$name/SKILL.md" 2>/dev/null)"; then
|
||||
desc="$(fm_value "$fm" description || true)"
|
||||
fi
|
||||
desc="${desc//|/\\|}"
|
||||
tag_enc="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name/v$ver")"
|
||||
zip_link="[${name}-v${ver}.zip](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.zip)"
|
||||
mcpb=""
|
||||
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
|
||||
mcpb="[${name}-v${ver}.mcpb](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.mcpb)"
|
||||
fi
|
||||
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s |\n' \
|
||||
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb"
|
||||
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-index.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
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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 'README.md'))) {
|
||||
$errs += 'README.md fehlt'
|
||||
}
|
||||
|
||||
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
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/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/README.md" ]]; then
|
||||
errs+=("README.md fehlt")
|
||||
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"
|
||||
Reference in new issue
Block a user