Compare commits

...
7 Commits
7 changed files with 640 additions and 9 deletions

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
*.bak *.bak
*.exe *.exe
scripts/*
!scripts/update-manifests.py
.vscode/* .vscode/*
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
/tests/*
!/tests/test_update_manifests.py
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"version": "20261020T080626Z", "version": "20261109T091718Z",
"description": "MyElexis CLI", "description": "MyElexis CLI",
"url": "https://download.medelexis.ch/myelexis-cli/windows/myelexis-cli-1.0.0-SNAPSHOT-runner.exe#/myelexis-cli.exe", "url": "https://download.medelexis.ch/myelexis-cli/windows/myelexis-cli-1.0.0-SNAPSHOT-runner.exe#/myelexis-cli.exe",
"hash": "a397bae3e6c340f40b4b0a35ea22bafd3c30a02990e0b26bde7957a803902078", "hash": "b266ad9f0b01bebd0c15134096e011933aac6080af2b26a68b8467dbcc6e60b2",
"bin": [ "bin": [
[ [
"myelexis-cli.exe", "myelexis-cli.exe",
+12
View File
@@ -0,0 +1,12 @@
# Scoop Medelexis AG - Intern
Damit die PowerShell-Scripte ausgeführt werden können, muss im Terminal-Fenster zuerst `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` eingegeben und ausgeführt werden.
**Scripts:**
* Install -> installiert Scoop und fügt die Buckets extras und medelexis hinzu
* update-bucket -> Migriert bestehende Buckets und Pakete auf medelexis *(kann mehrfach ausgeführt werden)
(ohne -Apply wird nur ein Probelauf durchgeführt. Wenn das Update angewendet werden soll, muss "update-bucket.ps1 -Apply" verwendet werden.)*
* setup-auto-update -> Richtet eine tägliche Scoop-Update-Aufgabe ein, repariert oder entfernt sie.
Andere Scripts sind System-Scripts und nicht für manuelle Ausführung gedacht.
+49
View File
@@ -0,0 +1,49 @@
#requires -Version 5.1
<#
.SYNOPSIS
Aktualisiert Scoop und alle benutzerspezifischen Pakete; elexis* bleibt gesperrt.
.DESCRIPTION
Führt ausschliesslich den Update-Lauf aus. Die Aufgabe wird von update-bucket.ps1
eingerichtet und startet dieses Skript verborgen. Der letzte Lauf steht in
auto-update.log neben diesem Skript. Globale Pakete werden nicht aktualisiert.
#>
[CmdletBinding()]
param(
[string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
)
$ErrorActionPreference = 'Stop'
$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot)
$env:SCOOP = $ScoopRoot
$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1'
if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) {
throw "Scoop wurde nicht gefunden: $scoopCommand"
}
$transcriptStarted = $false
$exitCode = 0
try {
Start-Transcript -LiteralPath (Join-Path $PSScriptRoot 'auto-update.log') -Force | Out-Null
$transcriptStarted = $true
# Ein absoluter Scoop-Pfad und die Shims im PATH funktionieren auch ohne Profil.
$env:PATH = (Join-Path $ScoopRoot 'shims') + [IO.Path]::PathSeparator + $env:PATH
$installed = @(& $scoopCommand list)
if ($LASTEXITCODE -ne 0) { throw 'Installierte Scoop-Pakete konnten nicht ermittelt werden.' }
foreach ($package in $installed) {
if ($package.Name -notlike 'elexis*' -or $package.Info -match 'Global install|Install failed') { continue }
& $scoopCommand hold $package.Name
if ($LASTEXITCODE -ne 0) {
throw "Updatesperre für '$($package.Name)' fehlgeschlagen. Kein Update ausgeführt."
}
}
& $scoopCommand update
if ($LASTEXITCODE -ne 0) { throw 'scoop update fehlgeschlagen.' }
& $scoopCommand update '*'
if ($LASTEXITCODE -ne 0) { throw 'scoop update * fehlgeschlagen.' }
} catch {
Write-Error $_ -ErrorAction Continue
$exitCode = 1
} finally {
if ($transcriptStarted) { Stop-Transcript | Out-Null }
}
exit $exitCode
+5 -5
View File
@@ -1,4 +1,4 @@
# 1. SCOOP CHECK & INSTALL # 1. SCOOP CHECK & INSTALL
$bucketList = scoop bucket list 2>$null | Out-String $bucketList = scoop bucket list 2>$null | Out-String
if ($bucketList) { if ($bucketList) {
@@ -29,12 +29,12 @@ else {
} }
# 3. BUCKET CHECKS # 3. BUCKET CHECKS
if ($bucketList -match "elexis") { if ($bucketList -match '(?m)^\s*medelexis\s') {
Write-Host "elexis Bucket ist bereits vorhanden." -ForegroundColor White Write-Host "Medelexis-Bucket ist bereits vorhanden." -ForegroundColor White
} }
else { else {
scoop bucket add elexis https://gitea.medelexis.ch/public/scoop scoop bucket add medelexis https://gitea.medelexis.ch/public/scoop
Write-Host "elexis Bucket hinzugefügt." -ForegroundColor Green Write-Host "Medelexis-Bucket hinzugefügt." -ForegroundColor Green
} }
if ($bucketList -match "extras") { if ($bucketList -match "extras") {
+163
View File
@@ -0,0 +1,163 @@
#requires -Version 5.1
<#
.SYNOPSIS
Fragt nach und richtet optional die täglichen Scoop-Auto-Updates ein.
.DESCRIPTION
Eigenständig ausführbar, ohne Bucket-Migration. Bei y wird auto-update.ps1
heruntergeladen und eine Aufgabe für täglich 03:00 Uhr erstellt.
Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach,
sobald die Benutzersitzung und eine Netzwerkverbindung verfügbar sind.
Bestehende Aufgaben können nach Rückfrage repariert oder samt Updateskript
entfernt werden. Werden beide Fragen verneint, bleibt alles unverändert.
.EXAMPLE
.\scripts\setup-auto-update.ps1
#>
[CmdletBinding()]
param(
[string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
)
$ErrorActionPreference = 'Stop'
$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot)
$repository = 'https://gitea.medelexis.ch/public/scoop'
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$taskName = 'Medelexis AG - Scoop AutoUpdate'
$legacyTaskName = "Medelexis-Scoop-AutoUpdate-$userSid"
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
$existingTasks = @(
foreach ($name in @($taskName, $legacyTaskName)) {
Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue
}
)
$autoUpdateRemoved = $false
if ($existingTasks.Count -gt 0) {
do {
$autoUpdateAnswer = (Read-Host 'Bestehende Scoop-Auto-Update-Aufgabe reparieren (ersetzen)? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n'))
if ($autoUpdateAnswer -eq 'n') {
do {
$removeAnswer = (Read-Host 'Scoop-Auto-Update entfernen? [y/n]').Trim().ToLowerInvariant()
} while ($removeAnswer -notin @('y', 'n'))
if ($removeAnswer -eq 'y') {
foreach ($existingTask in $existingTasks) {
if ($existingTask.State -eq 'Running') {
Stop-ScheduledTask -TaskName $existingTask.TaskName
}
Unregister-ScheduledTask -TaskName $existingTask.TaskName -Confirm:$false
}
# Nur die zugehörigen Dateien entfernen, niemals den Ordner rekursiv.
foreach ($fileName in @('auto-update.ps1', 'auto-update-hidden.exe', 'auto-update.log')) {
$filePath = Join-Path $autoUpdateDirectory $fileName
if (Test-Path -LiteralPath $filePath -PathType Leaf) {
Remove-Item -LiteralPath $filePath -Force
}
}
$autoUpdateRemoved = $true
Write-Host 'Scoop-Auto-Update entfernt (Aufgabe und zugehörige Dateien).'
}
}
} else {
do {
$autoUpdateAnswer = (Read-Host 'Scoop-Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n'))
}
if ($autoUpdateAnswer -eq 'y') {
$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1'
if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) {
throw "Scoop wurde nicht gefunden: $scoopCommand"
}
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null
$autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1'
$downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid())
try {
$autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1"
Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing
$parseTokens = $null
$parseErrors = $null
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
throw 'Das heruntergeladene Auto-Update-Skript ist kein gültiges Updateskript.'
}
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
# Bei einer Reparatur zuerst eine laufende Aufgabe anhalten,
# damit ihr Starter ersetzt werden kann.
foreach ($existingTask in $existingTasks) {
if ($existingTask.State -eq 'Running') {
Stop-ScheduledTask -TaskName $existingTask.TaskName
}
}
# Ein GUI-Programm startet PowerShell ohne Konsole, auch wenn Windows
# Terminal als Standardterminal eingerichtet ist. Es wartet auf das Ende
# des Updates und reicht den Exitcode weiter (Fehlerstatus / IgnoreNew).
$launcherPath = Join-Path $autoUpdateDirectory 'auto-update-hidden.exe'
$launcherSource = @'
using System;
using System.Diagnostics;
public static class ScoopAutoUpdateLauncher {
[STAThread]
public static int Main(string[] args) {
if (args.Length != 2) return 2;
try {
var start = new ProcessStartInfo();
start.FileName = System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"),
@"System32\WindowsPowerShell\v1.0\powershell.exe");
start.Arguments = "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" +
args[0] + "\" -ScoopRoot \"" + args[1] + "\"";
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WindowStyle = ProcessWindowStyle.Hidden;
using (var child = Process.Start(start)) {
child.WaitForExit();
return child.ExitCode;
}
} catch { return 1; }
}
}
'@
$compiledLauncher = Join-Path $autoUpdateDirectory ('auto-update-{0}.exe' -f [guid]::NewGuid())
$launcherSourcePath = [IO.Path]::ChangeExtension($compiledLauncher, '.cs')
# Der .NET-Framework-Compiler erzeugt eine Windows-Anwendung auch unter
# PowerShell 7, wo Add-Type -OutputType WindowsApplication nicht existiert.
$compiler = @(
(Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'),
(Join-Path $env:SystemRoot 'Microsoft.NET\Framework\v4.0.30319\csc.exe')
) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
if (-not $compiler) { throw 'Der Windows-.NET-Framework-Compiler wurde nicht gefunden.' }
try {
[IO.File]::WriteAllText($launcherSourcePath, $launcherSource, [Text.Encoding]::UTF8)
$compilerOutput = & $compiler /nologo /target:winexe "/out:$compiledLauncher" $launcherSourcePath 2>&1
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $compiledLauncher -PathType Leaf)) {
throw "Der fensterlose Starter konnte nicht erstellt werden: $($compilerOutput | Out-String)"
}
Move-Item -LiteralPath $compiledLauncher -Destination $launcherPath -Force
} finally {
foreach ($temporaryFile in @($compiledLauncher, $launcherSourcePath)) {
if (Test-Path -LiteralPath $temporaryFile) { Remove-Item -LiteralPath $temporaryFile }
}
}
$action = New-ScheduledTaskAction -Execute $launcherPath -Argument ('"{0}" "{1}"' -f $autoUpdateScript, $ScoopRoot.TrimEnd('\'))
$trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Hours 4)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force `
-Description 'Tägliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)' | Out-Null
# Die frühere Aufgabe dieses Benutzers entfernen, damit nur ein Update läuft.
if (Get-ScheduledTask -TaskName $legacyTaskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $legacyTaskName -Confirm:$false
}
Write-Host "Scoop-Auto-Update eingeschaltet: $taskName"
} finally {
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
}
} elseif (-not $autoUpdateRemoved) {
Write-Host 'Keine Änderung an Scoop-Auto-Update vorgenommen.'
}
+407
View File
@@ -0,0 +1,407 @@
#requires -Version 5.1
<#
.SYNOPSIS
Stellt bestehende Scoop-Installationen von elexis/streichtech auf medelexis um.
.DESCRIPTION
Ohne -Apply nur ein Probelauf. Keine Sicherungen 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.
ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert.
Mit -Apply wird abschliessend gefragt, ob tägliche Auto-Updates um 03:00 Uhr
eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen.
.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 ($app.Name -in @('ee-iso', 'ee_iso')) { continue }
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
}
}
}
$installCli = (Test-Path -LiteralPath (Join-Path $apps 'ee-cli')) -or
-not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))
$obsoleteApps = @(@('ee-iso', 'ee_iso', 'ee-cli') | Where-Object {
Test-Path -LiteralPath (Join-Path $apps $_)
})
if ($installCli -and (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))) {
$obsoleteApps += 'myelexis-cli'
}
foreach ($obsolete in $obsoleteApps) {
Write-Host "Veraltetes Paket deinstallieren: $obsolete"
}
if ($installCli) { Write-Host 'MyElexis CLI wird aus medelexis neu installiert.' }
$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) {
if ($app.Name -in $obsoleteApps) { continue }
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 "Probelauf: $(@($obsoleteApps).Count) alte Pakete würden deinstalliert."
Write-Host 'Mit -Apply wird optional ein Auto-Update um 03:00 Uhr mit Nachholen eingerichtet (y/n).'
Write-Host 'Mit -Apply anwenden. Es werden keine Sicherungen erstellt.'
return
}
$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 'ScoopRoot muss der aktiven Scoop-Installation entsprechen.'
}
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))
}
if ($installCli -and -not (Test-Path -LiteralPath (Join-Path $target 'bucket\myelexis-cli.json')) -and
-not (Test-Path -LiteralPath (Join-Path $target 'myelexis-cli.json'))) {
throw 'Manifest myelexis-cli fehlt im Ziel-Bucket. Keine Deinstallation.'
}
foreach ($obsolete in $obsoleteApps) {
$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 Deinstallationen muss ScoopRoot der aktiven Scoop-Installation entsprechen.'
}
& scoop uninstall $obsolete
if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath (Join-Path $apps $obsolete))) {
throw "'$obsolete' konnte nicht deinstalliert werden."
}
}
if ($installCli) {
Write-Host 'MyElexis CLI aus medelexis neu installieren ...'
& scoop install 'medelexis/myelexis-cli'
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli\current\myelexis-cli.exe'))) {
throw 'MyElexis CLI konnte nicht vollständig installiert werden.'
}
}
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'
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$taskName = 'Medelexis AG - Scoop AutoUpdate'
$legacyTaskName = "Medelexis-Scoop-AutoUpdate-$userSid"
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
$existingTasks = @(
foreach ($name in @($taskName, $legacyTaskName)) {
Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue
}
)
$autoUpdateRemoved = $false
if ($existingTasks.Count -gt 0) {
do {
$autoUpdateAnswer = (Read-Host 'Bestehende Scoop-Auto-Update-Aufgabe reparieren (ersetzen)? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n'))
if ($autoUpdateAnswer -eq 'n') {
do {
$removeAnswer = (Read-Host 'Scoop-Auto-Update entfernen? [y/n]').Trim().ToLowerInvariant()
} while ($removeAnswer -notin @('y', 'n'))
if ($removeAnswer -eq 'y') {
foreach ($existingTask in $existingTasks) {
if ($existingTask.State -eq 'Running') {
Stop-ScheduledTask -TaskName $existingTask.TaskName
}
Unregister-ScheduledTask -TaskName $existingTask.TaskName -Confirm:$false
}
# Nur die zugehörigen Dateien entfernen, niemals den Ordner rekursiv.
foreach ($fileName in @('auto-update.ps1', 'auto-update-hidden.exe', 'auto-update.log')) {
$filePath = Join-Path $autoUpdateDirectory $fileName
if (Test-Path -LiteralPath $filePath -PathType Leaf) {
Remove-Item -LiteralPath $filePath -Force
}
}
$autoUpdateRemoved = $true
Write-Host 'Scoop-Auto-Update entfernt (Aufgabe und zugehörige Dateien).'
}
}
} else {
do {
$autoUpdateAnswer = (Read-Host 'Scoop-Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n'))
}
if ($autoUpdateAnswer -eq 'y') {
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null
$autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1'
$downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid())
try {
$autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1"
Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing
$parseTokens = $null
$parseErrors = $null
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
throw 'Das heruntergeladene Auto-Update-Skript ist kein gültiges Updateskript.'
}
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
# Bei einer Reparatur zuerst eine laufende Aufgabe anhalten,
# damit ihr Starter ersetzt werden kann.
foreach ($existingTask in $existingTasks) {
if ($existingTask.State -eq 'Running') {
Stop-ScheduledTask -TaskName $existingTask.TaskName
}
}
# Ein GUI-Programm startet PowerShell ohne Konsole, auch wenn Windows
# Terminal als Standardterminal eingerichtet ist. Es wartet auf das Ende
# des Updates und reicht den Exitcode weiter (Fehlerstatus / IgnoreNew).
$launcherPath = Join-Path $autoUpdateDirectory 'auto-update-hidden.exe'
$launcherSource = @'
using System;
using System.Diagnostics;
public static class ScoopAutoUpdateLauncher {
[STAThread]
public static int Main(string[] args) {
if (args.Length != 2) return 2;
try {
var start = new ProcessStartInfo();
start.FileName = System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"),
@"System32\WindowsPowerShell\v1.0\powershell.exe");
start.Arguments = "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" +
args[0] + "\" -ScoopRoot \"" + args[1] + "\"";
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.WindowStyle = ProcessWindowStyle.Hidden;
using (var child = Process.Start(start)) {
child.WaitForExit();
return child.ExitCode;
}
} catch { return 1; }
}
}
'@
$compiledLauncher = Join-Path $autoUpdateDirectory ('auto-update-{0}.exe' -f [guid]::NewGuid())
$launcherSourcePath = [IO.Path]::ChangeExtension($compiledLauncher, '.cs')
# Der .NET-Framework-Compiler erzeugt eine Windows-Anwendung auch unter
# PowerShell 7, wo Add-Type -OutputType WindowsApplication nicht existiert.
$compiler = @(
(Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'),
(Join-Path $env:SystemRoot 'Microsoft.NET\Framework\v4.0.30319\csc.exe')
) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
if (-not $compiler) { throw 'Der Windows-.NET-Framework-Compiler wurde nicht gefunden.' }
try {
[IO.File]::WriteAllText($launcherSourcePath, $launcherSource, [Text.Encoding]::UTF8)
$compilerOutput = & $compiler /nologo /target:winexe "/out:$compiledLauncher" $launcherSourcePath 2>&1
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $compiledLauncher -PathType Leaf)) {
throw "Der fensterlose Starter konnte nicht erstellt werden: $($compilerOutput | Out-String)"
}
Move-Item -LiteralPath $compiledLauncher -Destination $launcherPath -Force
} finally {
foreach ($temporaryFile in @($compiledLauncher, $launcherSourcePath)) {
if (Test-Path -LiteralPath $temporaryFile) { Remove-Item -LiteralPath $temporaryFile }
}
}
$action = New-ScheduledTaskAction -Execute $launcherPath -Argument ('"{0}" "{1}"' -f $autoUpdateScript, $ScoopRoot.TrimEnd('\'))
$trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Hours 4)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force `
-Description 'Tägliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)' | Out-Null
# Die frühere Aufgabe dieses Benutzers entfernen, damit nur ein Update läuft.
if (Get-ScheduledTask -TaskName $legacyTaskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $legacyTaskName -Confirm:$false
}
Write-Host "Scoop-Auto-Update eingeschaltet: $taskName"
} finally {
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
}
} elseif (-not $autoUpdateRemoved) {
Write-Host 'Keine Änderung an Scoop-Auto-Update vorgenommen.'
}