diff --git a/.gitignore b/.gitignore index b4c4bb9..c3c8f3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ *.bak *.exe -scripts/* -!scripts/update-manifests.py .vscode/* __pycache__/ *.py[cod] diff --git a/installer/scoop.PS1 b/scripts/install.ps1 similarity index 100% rename from installer/scoop.PS1 rename to scripts/install.ps1 diff --git a/scripts/update-bucket.ps1 b/scripts/update-bucket.ps1 new file mode 100644 index 0000000..f283b6e --- /dev/null +++ b/scripts/update-bucket.ps1 @@ -0,0 +1,224 @@ +#requires -Version 5.1 +<# +.SYNOPSIS +Stellt bestehende Scoop-Installationen von elexis/streichtech auf medelexis um. +.DESCRIPTION +Ohne -Apply nur ein Probelauf. Keine Sicherungen, Neuinstallationen oder Git-Pushes. +Bearbeitet alle Versionsverzeichnisse der benutzerspezifischen Scoop-Installation. +Ein passender bestehender Bucket wird umbenannt. Danach werden elexis und streichtech entfernt. +Andere Buckets wie main und extras bleiben bestehen. +Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt. +.EXAMPLE +.\scripts\update-bucket.ps1 +.EXAMPLE +.\scripts\update-bucket.ps1 -Apply +#> +[CmdletBinding()] +param( + [switch] $Apply, + [string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) +) + +$ErrorActionPreference = 'Stop' +$repository = 'https://gitea.medelexis.ch/public/scoop' +$targetName = 'medelexis' +$target = Join-Path $ScoopRoot "buckets\$targetName" +$apps = Join-Path $ScoopRoot 'apps' +if (-not (Test-Path -LiteralPath $apps -PathType Container)) { + throw "Kein Scoop-Apps-Verzeichnis gefunden: $apps" +} + +function Test-MedelexisRepository([string] $Path) { + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $false } + $remote = & git -C $Path remote get-url origin 2>$null + if ($LASTEXITCODE -ne 0) { return $false } + try { $uri = [Uri] ([string] $remote).Trim() } catch { return $false } + return ($uri.Scheme -eq 'https' -and $uri.Host -eq 'gitea.medelexis.ch' -and + $uri.AbsolutePath.TrimEnd('/').Replace('.git', '') -eq '/public/scoop') +} + +function Get-PackageRenames([string] $BucketPath) { + if (-not $BucketPath -or -not (Test-Path -LiteralPath $BucketPath)) { return } + foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { + if (-not $app.Name.Contains('_')) { continue } + $newName = $app.Name.Replace('_', '-') + $newManifest = Join-Path $BucketPath "bucket\$newName.json" + if (-not (Test-Path -LiteralPath $newManifest)) { + $newManifest = Join-Path $BucketPath "$newName.json" + } + if (-not (Test-Path -LiteralPath $newManifest)) { continue } + $current = Join-Path $app.FullName 'current' + $installFile = Join-Path $current 'install.json' + if (-not (Test-Path -LiteralPath $installFile)) { continue } + $install = [IO.File]::ReadAllText($installFile) | ConvertFrom-Json + if ($install.bucket -notin @('elexis', 'streichtech', 'medelexis')) { continue } + $destination = Join-Path $apps $newName + if (Test-Path -LiteralPath $destination) { + throw "'$($app.Name)' und '$newName' sind beide installiert. Keine automatische Zusammenführung." + } + $link = Get-Item -LiteralPath $current -Force + if ($link.LinkType -ne 'Junction') { + throw "Für '$($app.Name)' wird eine Scoop-current-Junction erwartet. Keine automatische Umbenennung." + } + $versionPath = [IO.Path]::GetFullPath([string] @($link.Target)[0]) + if ((Split-Path $versionPath -Parent) -ne $app.FullName) { + throw "Die current-Verknüpfung von '$($app.Name)' zeigt ausserhalb des Paketordners." + } + $persistSource = Join-Path $ScoopRoot "persist\$($app.Name)" + $persistTarget = Join-Path $ScoopRoot "persist\$newName" + if ((Test-Path -LiteralPath $persistSource) -and (Test-Path -LiteralPath $persistTarget)) { + throw "Beide Persistenzordner für '$($app.Name)' und '$newName' existieren. Keine automatische Zusammenführung." + } + Write-Host "Paket umbenennen: $($app.Name) -> $newName" + [pscustomobject]@{ + OldName = $app.Name; NewName = $newName; Source = $app.FullName + Destination = $destination; Version = (Split-Path $versionPath -Leaf) + PersistSource = $persistSource; PersistTarget = $persistTarget + } + } +} + +$renameSource = $null +if (Test-Path -LiteralPath $target) { + if (-not (Test-MedelexisRepository $target)) { + throw 'Der vorhandene Bucket medelexis zeigt auf ein anderes Repository. Keine Änderung.' + } + Write-Host 'Bucket medelexis ist bereits korrekt eingerichtet.' +} else { + foreach ($oldName in @('elexis', 'streichtech')) { + $candidate = Join-Path $ScoopRoot "buckets\$oldName" + if (Test-MedelexisRepository $candidate) { + $renameSource = $candidate + Write-Host "Bucket umbenennen: $oldName -> $targetName" + break + } + } + if (-not $renameSource) { Write-Host "Bucket hinzufügen: $targetName -> $repository" } +} + +$changes = @( + foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { + foreach ($version in Get-ChildItem -LiteralPath $app.FullName -Directory) { + # current und andere Verknüpfungen nicht nochmals bearbeiten. + if ($version.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } + $file = Join-Path $version.FullName 'install.json' + if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue } + $original = [IO.File]::ReadAllText($file) + $data = $original | ConvertFrom-Json + if ($data.bucket -notin @('elexis', 'streichtech')) { continue } + $pattern = '(?i)("bucket"\s*:\s*)"(?:elexis|streichtech)"' + $updated = [regex]::Replace($original, $pattern, '${1}"medelexis"') + if (($updated | ConvertFrom-Json).bucket -ne $targetName) { + throw "Bucket-Zuordnung konnte nicht angepasst werden: $file" + } + Write-Host "$($app.Name) / $($version.Name): $($data.bucket) -> $targetName" + [pscustomobject]@{ File = $file; Original = $original; Updated = $updated; App = $app.Name } + } + } +) +$manifestRoot = $(if (Test-Path -LiteralPath $target) { $target } else { $renameSource }) +$renames = @(Get-PackageRenames $manifestRoot) + +if (-not $Apply) { + foreach ($oldName in @('elexis', 'streichtech')) { + if (Test-Path -LiteralPath (Join-Path $ScoopRoot "buckets\$oldName")) { + Write-Host "Alter Bucket wird nach der Umstellung entfernt oder umbenannt: $oldName" + } + } + Write-Host "Probelauf: $($changes.Count) install.json-Dateien würden geändert." + Write-Host "Probelauf: $($renames.Count) Paketnamen würden umgestellt." + Write-Host 'Mit -Apply anwenden. Es werden keine Sicherungen erstellt.' + return +} + +if (-not (Test-Path -LiteralPath $target)) { + if ($renameSource) { + $bucketRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'buckets')).Path + $sourcePath = (Resolve-Path -LiteralPath $renameSource).Path + $targetPath = [IO.Path]::GetFullPath($target) + if ((Split-Path $sourcePath -Parent) -ne $bucketRoot -or + (Split-Path $targetPath -Parent) -ne $bucketRoot) { + throw 'Bucket-Pfade liegen ausserhalb des erwarteten Verzeichnisses. Keine Umbenennung.' + } + [IO.Directory]::Move($sourcePath, $targetPath) + } else { + # scoop verwendet dieselbe Root-Variable für benutzerdefinierte Installationen. + if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne + [IO.Path]::GetFullPath($(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })).TrimEnd('\')) { + throw 'ScoopRoot weicht von der aktiven Scoop-Installation ab; Bucket bitte dort zuerst einrichten.' + } + & scoop bucket add $targetName $repository + if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $target)) { + throw 'Bucket konnte nicht hinzugefügt werden. Installationsmetadaten bleiben unverändert.' + } + } +} + +# Falls der Ziel-Bucket erst hinzugefügt wurde, stehen seine Manifestnamen jetzt bereit. +if (-not $manifestRoot) { $renames = @(Get-PackageRenames $target) } + +# Vor dem ersten Schreiben prüfen, ob während des Probelaufs Dateien geändert wurden. +foreach ($change in $changes) { + if ([IO.File]::ReadAllText($change.File) -cne $change.Original) { + throw 'Installationsmetadaten wurden zwischenzeitlich geändert. Skript bitte erneut starten.' + } +} +foreach ($change in $changes) { + [IO.File]::WriteAllText($change.File, $change.Updated, [Text.UTF8Encoding]::new($false)) +} +foreach ($rename in $renames) { + $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) + if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { + throw 'Für Paketumbenennungen muss ScoopRoot der aktiven Scoop-Installation entsprechen.' + } + $appsRoot = (Resolve-Path -LiteralPath $apps).Path + if ((Split-Path $rename.Source -Parent) -ne $appsRoot -or + (Split-Path $rename.Destination -Parent) -ne $appsRoot) { + throw 'Paketpfade liegen ausserhalb des Apps-Verzeichnisses.' + } + if (Test-Path -LiteralPath $rename.PersistSource) { + $persistRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'persist')).Path + if ((Split-Path $rename.PersistSource -Parent) -ne $persistRoot -or + (Split-Path $rename.PersistTarget -Parent) -ne $persistRoot) { + throw 'Persistenzpfade liegen ausserhalb des Persist-Verzeichnisses.' + } + [IO.Directory]::Move($rename.PersistSource, $rename.PersistTarget) + } + $oldCurrent = Join-Path $rename.Source 'current' + & attrib -R /L $oldCurrent + if ($LASTEXITCODE -ne 0) { throw 'current-Verknüpfung konnte nicht freigegeben werden.' } + # Nur die Junction entfernen, ohne rekursiv ihren Inhalt zu löschen. + [IO.Directory]::Delete($oldCurrent) + [IO.Directory]::Move($rename.Source, $rename.Destination) + $newCurrent = Join-Path $rename.Destination 'current' + $newVersion = Join-Path $rename.Destination $rename.Version + New-Item -ItemType Junction -Path $newCurrent -Target $newVersion | Out-Null + Write-Host "Startverknüpfungen für $($rename.NewName) erneuern ..." + & scoop reset "$($rename.NewName)@$($rename.Version)" + if ($LASTEXITCODE -ne 0) { throw "Scoop reset für '$($rename.NewName)' fehlgeschlagen." } +} +foreach ($oldName in @('elexis', 'streichtech')) { + $oldPath = Join-Path $ScoopRoot "buckets\$oldName" + if (-not (Test-Path -LiteralPath $oldPath)) { continue } + $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) + if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { + throw 'Zum Entfernen alter Buckets muss ScoopRoot der aktiven Scoop-Installation entsprechen.' + } + Write-Host "Alten Bucket entfernen: $oldName" + & scoop bucket rm $oldName + if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath $oldPath)) { + throw "Alter Bucket '$oldName' konnte nicht entfernt werden. Die Umstellung kann erneut ausgeführt werden." + } +} +foreach ($name in @((Get-ChildItem -LiteralPath $apps -Directory).Name)) { + $installFile = Join-Path $apps "$name\current\install.json" + if (-not (Test-Path -LiteralPath $installFile)) { continue } + if (([IO.File]::ReadAllText($installFile) | ConvertFrom-Json).bucket -ne $targetName) { continue } + if (-not (Test-Path -LiteralPath (Join-Path $target "bucket\$name.json")) -and + -not (Test-Path -LiteralPath (Join-Path $target "$name.json"))) { + Write-Warning "Für '$name' fehlt weiterhin ein gleichnamiges Manifest." + } +} +Write-Host "Fertig: $($changes.Count) install.json-Dateien auf medelexis umgestellt." +Write-Host "Paketnamen umgestellt: $($renames.Count)." +Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local'