# 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 = [System.Text.UTF8Encoding]::new($false) # Datei als UTF-8 lesen, Zeilenenden auf LF vereinheitlichen. function Read-Text([string]$Path) { return ([System.IO.File]::ReadAllText($Path, [System.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) { [System.IO.File]::WriteAllText($Path, $Text, $Utf8NoBom) } # Prozess starten und stdout+stderr als UTF-8 ohne BOM lesen (kein Out-String / Default-Encoding). # $Arguments ist die Argumentzeile (PS 5.1 hat kein ProcessStartInfo.ArgumentList). function Get-ProcessOutputUtf8([string]$FileName, [string]$Arguments) { $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $FileName $psi.Arguments = $Arguments $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.CreateNoWindow = $true $psi.StandardOutputEncoding = $Utf8NoBom $psi.StandardErrorEncoding = $Utf8NoBom $p = [System.Diagnostics.Process]::Start($psi) $stdout = $p.StandardOutput.ReadToEnd() $stderr = $p.StandardError.ReadToEnd() $p.WaitForExit() return @{ ExitCode = $p.ExitCode Output = ($stdout + $stderr) } } 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 '' }