Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
This commit was merged in pull request #18.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
write_sha256() {
|
||||
file=$1
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$file" > "$file.sha256"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" > "$file.sha256"
|
||||
else
|
||||
echo "[WARN] Weder sha256sum noch shasum verfügbar; keine Prüfsummendatei erzeugt." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
target="${1:-./vendoo-docker-backup-$(date +%Y%m%d-%H%M%S)}"
|
||||
mkdir -p "$target"
|
||||
absolute_target="$(cd "$target" && pwd)"
|
||||
|
||||
docker compose config -q
|
||||
restart=0
|
||||
if docker compose ps --status running --services | grep -qx vendoo; then
|
||||
restart=1
|
||||
docker compose stop vendoo
|
||||
fi
|
||||
cleanup() {
|
||||
code=$?
|
||||
if [ "$restart" -eq 1 ]; then docker compose up -d vendoo >/dev/null; fi
|
||||
exit "$code"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
docker compose run --rm --no-deps --user 0:0 \
|
||||
-v "$absolute_target":/backup \
|
||||
--entrypoint sh vendoo -eu -c \
|
||||
'cd /app/data && tar czf /backup/vendoo-data.tar.gz db uploads backups operations logs config modules'
|
||||
write_sha256 "$target/vendoo-data.tar.gz"
|
||||
|
||||
trap - EXIT INT TERM
|
||||
if [ "$restart" -eq 1 ]; then docker compose up -d vendoo >/dev/null; fi
|
||||
echo "Backup erstellt: $target/vendoo-data.tar.gz"
|
||||
[ "$restart" -eq 0 ] || echo "Vendoo wurde wieder gestartet."
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
for dir in \
|
||||
"$(dirname "${VENDOO_DB_PATH:-/app/data/db/vendoo.db}")" \
|
||||
"${VENDOO_UPLOADS_DIR:-/app/data/uploads}" \
|
||||
"${VENDOO_BACKUP_DIR:-/app/data/backups}" \
|
||||
"${VENDOO_OPERATIONS_DIR:-/app/data/operations}" \
|
||||
"${VENDOO_LOG_DIR:-/app/data/logs}" \
|
||||
"$(dirname "${VENDOO_CONFIG_PATH:-/app/data/config/vendoo.env}")" \
|
||||
"${VENDOO_MODULES_DIR:-/app/data/modules}"; do
|
||||
mkdir -p "$dir"
|
||||
test -w "$dir" || { echo "[FATAL] Kein Schreibzugriff auf $dir (Container-UID 1000)." >&2; exit 70; }
|
||||
done
|
||||
|
||||
if [ "${NODE_ENV:-production}" = "production" ] && [ "${VENDOO_REQUIRE_SETUP_TOKEN:-true}" = "true" ]; then
|
||||
token="${VENDOO_SETUP_TOKEN:-}"
|
||||
if [ "${#token}" -lt 32 ]; then
|
||||
echo "[FATAL] VENDOO_SETUP_TOKEN muss im Produktionsbetrieb mindestens 32 Zeichen lang sein." >&2
|
||||
exit 78
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${VENDOO_TRUST_PROXY:-false}" != "false" ] && [ -z "${PUBLIC_BASE_URL:-}" ]; then
|
||||
echo "[WARN] VENDOO_TRUST_PROXY ist aktiv, aber PUBLIC_BASE_URL fehlt. HTTPS-/Cookie-Erkennung sorgfältig prüfen." >&2
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
verify_sha256() {
|
||||
checksum=$1
|
||||
directory=$2
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "$directory" && sha256sum -c "$(basename "$checksum")")
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "$directory" && shasum -a 256 -c "$(basename "$checksum")")
|
||||
else
|
||||
echo "[WARN] Weder sha256sum noch shasum verfügbar; Prüfsumme kann nicht geprüft werden." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
archive="${1:-}"
|
||||
confirm="${2:-}"
|
||||
[ -n "$archive" ] && [ -f "$archive" ] || {
|
||||
echo "Aufruf: scripts/docker-restore.sh /pfad/vendoo-data.tar.gz --confirm" >&2
|
||||
exit 64
|
||||
}
|
||||
[ "$confirm" = "--confirm" ] || {
|
||||
echo "[ABBRUCH] Restore ersetzt die aktuellen Docker-Daten. Erneut mit --confirm aufrufen." >&2
|
||||
exit 64
|
||||
}
|
||||
|
||||
archive_dir="$(cd "$(dirname "$archive")" && pwd)"
|
||||
archive_name="$(basename "$archive")"
|
||||
archive_abs="$archive_dir/$archive_name"
|
||||
checksum_file="$archive_abs.sha256"
|
||||
|
||||
docker compose config -q
|
||||
if [ -f "$checksum_file" ]; then
|
||||
echo "Prüfe SHA-256 des Restore-Archivs ..."
|
||||
verify_sha256 "$checksum_file" "$archive_dir"
|
||||
else
|
||||
echo "[WARN] Keine Prüfsummendatei $checksum_file gefunden; Archivintegrität kann nicht automatisch bestätigt werden." >&2
|
||||
fi
|
||||
|
||||
echo "Erstelle vor dem Restore ein Sicherheitsbackup ..."
|
||||
pre_restore="./vendoo-pre-restore-$(date +%Y%m%d-%H%M%S)"
|
||||
"$(dirname "$0")/docker-backup.sh" "$pre_restore"
|
||||
|
||||
echo "Stoppe Vendoo und stelle die persistenten Daten wieder her ..."
|
||||
docker compose stop vendoo
|
||||
restart_on_error() {
|
||||
code=$?
|
||||
if [ "$code" -ne 0 ]; then
|
||||
echo "[FEHLER] Restore fehlgeschlagen. Vendoo wird zur Diagnose erneut gestartet." >&2
|
||||
docker compose up -d vendoo >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
trap restart_on_error EXIT INT TERM
|
||||
|
||||
docker compose run --rm --no-deps --user 0:0 \
|
||||
-v "$archive_dir":/input:ro \
|
||||
--entrypoint sh vendoo -eu -c \
|
||||
'for dir in db uploads backups operations logs config modules; do
|
||||
mkdir -p "/app/data/$dir"
|
||||
find "/app/data/$dir" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||
done
|
||||
tar xzf "/input/$1" -C /app/data' sh "$archive_name"
|
||||
|
||||
trap - EXIT INT TERM
|
||||
docker compose up -d vendoo
|
||||
echo "Restore eingespielt. Sicherheitsbackup: $pre_restore/vendoo-data.tar.gz"
|
||||
echo "Jetzt /readyz, Login, Benutzer, Artikel, Bilder und Operations Center prüfen."
|
||||
@@ -0,0 +1,210 @@
|
||||
# Vendoo 1.41.2 – kontrollierter GitHub-Erstimport
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
$RepoUrl = 'https://github.com/Masterluke77/vendoo.git'
|
||||
$PullRequestUrl = 'https://github.com/Masterluke77/vendoo/pull/18'
|
||||
$Branch = 'release/1.41.2-git-ignore-boundary-hotfix'
|
||||
$WorkRoot = Join-Path $env:TEMP ('vendoo-github-deploy-' + [Guid]::NewGuid().ToString('N'))
|
||||
$CloneDir = Join-Path $WorkRoot 'repo'
|
||||
|
||||
function Info($Text) { Write-Host "[INFO] $Text" -ForegroundColor Cyan }
|
||||
function Ok($Text) { Write-Host "[OK] $Text" -ForegroundColor Green }
|
||||
function Warn($Text) { Write-Host "[WARN] $Text" -ForegroundColor Yellow }
|
||||
function Fail($Text) { Write-Host "[FEHLER] $Text" -ForegroundColor Red }
|
||||
|
||||
function Test-Command([string]$Name) {
|
||||
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Ensure-Git {
|
||||
if (Test-Command 'git.exe') { return }
|
||||
Warn 'Git für Windows wurde nicht gefunden.'
|
||||
if (Test-Command 'winget.exe') {
|
||||
$answer = Read-Host 'Git jetzt automatisch über winget installieren? (ja/nein)'
|
||||
if ($answer -eq 'ja') {
|
||||
& winget.exe install --id Git.Git --exact --source winget --accept-package-agreements --accept-source-agreements
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Git-Installation über winget ist fehlgeschlagen.' }
|
||||
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
|
||||
}
|
||||
}
|
||||
if (-not (Test-Command 'git.exe')) { throw 'Git für Windows wird benötigt. Starte den Assistenten nach der Git-Installation erneut.' }
|
||||
}
|
||||
|
||||
function Invoke-Git([string[]]$Arguments, [string]$WorkingDirectory = $CloneDir) {
|
||||
Push-Location $WorkingDirectory
|
||||
try {
|
||||
& git.exe @Arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "git $($Arguments -join ' ') fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
function Copy-ProjectClean {
|
||||
if (-not (Test-Command 'node.exe')) {
|
||||
throw 'Node.js ist für den sicheren GitHub-Import erforderlich. Bitte zuerst über setup.bat installieren oder aktualisieren.'
|
||||
}
|
||||
Info 'Bereite den bereinigten Git-Arbeitsordner mit dem plattformübergreifenden Staging-Builder vor ...'
|
||||
$builder = Join-Path $Root 'tools\prepare-git-staging.mjs'
|
||||
$result = Invoke-NodeProcess -ScriptPath $builder -Arguments @($Root, $CloneDir) -WorkingDirectory $Root -Prefix 'Staging'
|
||||
if ($result.ExitCode -ne 0) {
|
||||
$result.Lines | ForEach-Object { Write-Host $_ -ForegroundColor Red }
|
||||
throw 'Der sichere Git-Staging-Aufbau ist fehlgeschlagen.'
|
||||
}
|
||||
$result.Lines | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
|
||||
function Invoke-NodeProcess {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ScriptPath,
|
||||
[string[]]$Arguments = @(),
|
||||
[string]$WorkingDirectory = $CloneDir,
|
||||
[string]$Prefix = 'Node'
|
||||
)
|
||||
$id = [Guid]::NewGuid().ToString('N')
|
||||
$stdoutPath = Join-Path $WorkRoot ("$Prefix-$id-stdout.txt")
|
||||
$stderrPath = Join-Path $WorkRoot ("$Prefix-$id-stderr.txt")
|
||||
$nodePath = (Get-Command node.exe -ErrorAction Stop).Source
|
||||
$allArguments = @($ScriptPath) + $Arguments
|
||||
$argumentLine = ($allArguments | ForEach-Object { '"' + ([string]$_).Replace('"','\"') + '"' }) -join ' '
|
||||
$process = Start-Process -FilePath $nodePath `
|
||||
-ArgumentList $argumentLine `
|
||||
-WorkingDirectory $WorkingDirectory `
|
||||
-NoNewWindow `
|
||||
-Wait `
|
||||
-PassThru `
|
||||
-RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath
|
||||
|
||||
$lines = @()
|
||||
if (Test-Path -LiteralPath $stdoutPath) { $lines += @(Get-Content -LiteralPath $stdoutPath -Encoding UTF8 -ErrorAction SilentlyContinue) }
|
||||
if (Test-Path -LiteralPath $stderrPath) { $lines += @(Get-Content -LiteralPath $stderrPath -Encoding UTF8 -ErrorAction SilentlyContinue) }
|
||||
return [pscustomobject]@{ ExitCode = [int]$process.ExitCode; Lines = $lines }
|
||||
}
|
||||
|
||||
function Run-SafetyGate {
|
||||
$ReportDir = Join-Path $Root 'logs'
|
||||
$ReportPath = Join-Path $ReportDir 'github-deploy-safety-report.txt'
|
||||
if (-not (Test-Command 'node.exe')) {
|
||||
throw 'Node.js ist für den sicheren GitHub-Import erforderlich. Bitte zuerst über setup.bat installieren oder aktualisieren.'
|
||||
}
|
||||
|
||||
Info 'Führe das Vendoo Git-Sicherheitsgate über den vollständigen Staging-Arbeitsbaum aus ...'
|
||||
$result = Invoke-NodeProcess -ScriptPath 'tools/verify-git-safety.mjs' -WorkingDirectory $CloneDir -Prefix 'SafetyGate'
|
||||
foreach ($line in $result.Lines) {
|
||||
if ($line -match '^Git-Sicherheitsgate fehlgeschlagen|^- ') { Write-Host $line -ForegroundColor Red }
|
||||
elseif ($line -match '^\[WARN\]') { Write-Host $line -ForegroundColor Yellow }
|
||||
else { Write-Host $line }
|
||||
}
|
||||
|
||||
if ($result.ExitCode -ne 0) {
|
||||
New-Item -ItemType Directory -Path $ReportDir -Force | Out-Null
|
||||
@(
|
||||
'Vendoo Git-Sicherheitsgate – vollständiger Fehlerbericht',
|
||||
('Zeitpunkt: ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')),
|
||||
('Projekt: ' + $Root),
|
||||
('Staging: ' + $CloneDir),
|
||||
'',
|
||||
$result.Lines
|
||||
) | Set-Content -LiteralPath $ReportPath -Encoding UTF8
|
||||
Fail "Das Gate hat $($result.Lines.Count) Diagnosezeilen erzeugt. Bericht: $ReportPath"
|
||||
try { Start-Process notepad.exe -ArgumentList ('"' + $ReportPath + '"') | Out-Null } catch {}
|
||||
throw 'Git-Sicherheitsgate ist fehlgeschlagen. Die konkreten Treffer wurden oben angezeigt und im Bericht gespeichert.'
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $ReportPath -Force -ErrorAction SilentlyContinue
|
||||
$forbidden = Get-ChildItem -LiteralPath $CloneDir -Recurse -Force -File -ErrorAction SilentlyContinue | Where-Object {
|
||||
($_.Name -match '^\.env(?:\..+)?$' -and $_.Name -notin @('.env.example','.env.docker.example')) -or
|
||||
$_.Extension -in @('.db','.sqlite','.sqlite3','.log','.key','.pem','.pfx','.p12')
|
||||
}
|
||||
if ($forbidden) {
|
||||
throw ('Verbotene Dateien im Git-Staging: ' + (($forbidden | Select-Object -First 20 -ExpandProperty FullName) -join ', '))
|
||||
}
|
||||
Ok 'Git-Sicherheitsgate erfolgreich: keine lokalen Secrets oder Laufzeitdaten im Import.'
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ''
|
||||
Write-Host 'Vendoo 1.41.2 – GitHub Deploy Assistent' -ForegroundColor Green
|
||||
Write-Host 'Repository: Masterluke77/vendoo (privat)' -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
|
||||
Ensure-Git
|
||||
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
|
||||
|
||||
Info 'Klone die vorhandene GitHub-Baseline ...'
|
||||
& git.exe clone $RepoUrl $CloneDir
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Repository konnte nicht geklont werden. Die Git-Anmeldung öffnet sich normalerweise automatisch im Browser.' }
|
||||
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
& git.exe ls-remote --exit-code --heads origin $Branch 1>$null 2>$null
|
||||
$branchExists = ($LASTEXITCODE -eq 0)
|
||||
} finally { Pop-Location }
|
||||
if ($branchExists) {
|
||||
Invoke-Git @('fetch','origin',$Branch)
|
||||
Invoke-Git @('switch','--track','-c',$Branch,("origin/" + $Branch))
|
||||
} else {
|
||||
Invoke-Git @('switch','-c',$Branch)
|
||||
}
|
||||
|
||||
Copy-ProjectClean
|
||||
Run-SafetyGate
|
||||
|
||||
Info 'Prüfe produktive Source-Dateien und Git-Ignore-Grenzen im Staging ...'
|
||||
$sourceGate = Invoke-NodeProcess -ScriptPath 'tools/verify-source-boundaries-1.41.2.mjs' -WorkingDirectory $CloneDir -Prefix 'SourceGate'
|
||||
$sourceGate.Lines | ForEach-Object { Write-Host $_ }
|
||||
if ($sourceGate.ExitCode -ne 0) { throw 'Produktive Source-Root-Prüfung ist fehlgeschlagen.' }
|
||||
$ignoreGate = Invoke-NodeProcess -ScriptPath 'tools/verify-git-ignore-boundaries-1.41.2.mjs' -WorkingDirectory $CloneDir -Prefix 'IgnoreGate'
|
||||
$ignoreGate.Lines | ForEach-Object { Write-Host $_ }
|
||||
if ($ignoreGate.ExitCode -ne 0) { throw 'Git-Ignore-Grenzprüfung ist fehlgeschlagen.' }
|
||||
|
||||
Invoke-Git @('config','user.name','Markus Müller')
|
||||
Invoke-Git @('config','user.email','163446896+Masterluke77@users.noreply.github.com')
|
||||
Invoke-Git @('add','--all')
|
||||
|
||||
$requiredTracked = @(
|
||||
'app/modules/listings/routes.mjs',
|
||||
'app/modules/inventory/index.mjs',
|
||||
'app/modules/inventory/repository.mjs',
|
||||
'app/modules/inventory/routes.mjs',
|
||||
'app/modules/sessions/index.mjs'
|
||||
)
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
$ignoredModules = @(& git.exe ls-files --others --ignored --exclude-standard -- app/modules)
|
||||
if ($ignoredModules.Count -gt 0) { throw ('Produktive Moduldateien werden von .gitignore ausgeschlossen: ' + ($ignoredModules -join ', ')) }
|
||||
foreach ($rel in $requiredTracked) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $CloneDir $rel))) { throw "Pflichtdatei fehlt im Git-Staging: $rel" }
|
||||
& git.exe ls-files --error-unmatch -- $rel 1>$null 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Pflichtdatei wurde von Git nicht getrackt: $rel" }
|
||||
}
|
||||
} finally { Pop-Location }
|
||||
Ok 'Produktive Moduldateien sind vorhanden und werden von Git getrackt.'
|
||||
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
$status = & git.exe status --short
|
||||
if (-not $status) { Warn 'Es gibt keine Änderungen gegenüber GitHub.'; exit 0 }
|
||||
Write-Host ''
|
||||
Write-Host $status
|
||||
Write-Host ''
|
||||
} finally { Pop-Location }
|
||||
|
||||
$confirm = Read-Host 'Geprüften Vendoo-Stand als Release-Branch zu GitHub übertragen? Zum Fortfahren PUSH eingeben'
|
||||
if ($confirm -cne 'PUSH') { Warn 'Deployment abgebrochen. Auf GitHub wurde nichts verändert.'; exit 0 }
|
||||
|
||||
Invoke-Git @('commit','-m','fix: track native source modules with root-anchored runtime ignores')
|
||||
Invoke-Git @('push','--set-upstream','origin',$Branch)
|
||||
Ok "Branch $Branch wurde erfolgreich übertragen."
|
||||
|
||||
Info 'Öffne jetzt Draft Pull Request #18 im Browser ...'
|
||||
Start-Process $PullRequestUrl
|
||||
Write-Host ''
|
||||
Write-Host 'Nächster Schritt im Browser: Pull Request #18 und CI prüfen. Erst nach grüner CI nach main mergen.' -ForegroundColor Yellow
|
||||
} catch {
|
||||
Fail $_.Exception.Message
|
||||
exit 1
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $WorkRoot) {
|
||||
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
REPO_URL="https://github.com/Masterluke77/vendoo.git"
|
||||
BRANCH="release/1.41.2-git-ignore-boundary-hotfix"
|
||||
PR_URL="https://github.com/Masterluke77/vendoo/pull/18"
|
||||
WORK_DIR=$(mktemp -d)
|
||||
cleanup() { rm -rf "$WORK_DIR"; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo "Git fehlt." >&2; exit 2; }
|
||||
command -v node >/dev/null 2>&1 || { echo "Node.js wird für den sicheren GitHub-Import benötigt." >&2; exit 2; }
|
||||
|
||||
git clone "$REPO_URL" "$WORK_DIR/repo"
|
||||
cd "$WORK_DIR/repo"
|
||||
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
|
||||
git fetch origin "$BRANCH"
|
||||
git switch --track -c "$BRANCH" "origin/$BRANCH"
|
||||
else
|
||||
git switch -c "$BRANCH"
|
||||
fi
|
||||
|
||||
node "$ROOT_DIR/tools/prepare-git-staging.mjs" "$ROOT_DIR" "$WORK_DIR/repo"
|
||||
|
||||
GATE_OUTPUT="$WORK_DIR/git-safety-output.txt"
|
||||
if ! node tools/verify-git-safety.mjs >"$GATE_OUTPUT" 2>&1; then
|
||||
cat "$GATE_OUTPUT" >&2
|
||||
mkdir -p "$ROOT_DIR/logs"
|
||||
{
|
||||
echo "Vendoo Git-Sicherheitsgate – vollständiger Fehlerbericht"
|
||||
echo "Projekt: $ROOT_DIR"
|
||||
echo
|
||||
cat "$GATE_OUTPUT"
|
||||
} > "$ROOT_DIR/logs/github-deploy-safety-report.txt"
|
||||
echo "Git-Sicherheitsgate fehlgeschlagen. Details: $ROOT_DIR/logs/github-deploy-safety-report.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$GATE_OUTPUT"
|
||||
rm -f "$ROOT_DIR/logs/github-deploy-safety-report.txt"
|
||||
|
||||
git config user.name "Markus Müller"
|
||||
git config user.email "163446896+Masterluke77@users.noreply.github.com"
|
||||
git add --all
|
||||
node tools/verify-source-boundaries-1.41.2.mjs
|
||||
node tools/verify-git-ignore-boundaries-1.41.2.mjs
|
||||
ignored_modules="$(git ls-files --others --ignored --exclude-standard -- app/modules || true)"
|
||||
[ -z "$ignored_modules" ] || { echo "Produktive Moduldateien werden ignoriert: $ignored_modules" >&2; exit 1; }
|
||||
for required in app/modules/listings/routes.mjs app/modules/inventory/index.mjs app/modules/inventory/repository.mjs app/modules/inventory/routes.mjs app/modules/sessions/index.mjs; do
|
||||
test -f "$required" || { echo "Pflichtdatei fehlt im Git-Staging: $required" >&2; exit 1; }
|
||||
git ls-files --error-unmatch -- "$required" >/dev/null || { echo "Pflichtdatei wird nicht getrackt: $required" >&2; exit 1; }
|
||||
done
|
||||
git status --short
|
||||
printf 'Zum Pushen exakt PUSH eingeben: '
|
||||
read -r answer
|
||||
[ "$answer" = "PUSH" ] || { echo "Abgebrochen."; exit 0; }
|
||||
git commit -m "fix: track native source modules with root-anchored runtime ignores"
|
||||
git push --set-upstream origin "$BRANCH"
|
||||
echo "Branch übertragen. Draft Pull Request #15 öffnen: $PR_URL"
|
||||
@@ -0,0 +1,803 @@
|
||||
param(
|
||||
[ValidateSet('auto','nvidia','amd','intel','cpu')]
|
||||
[string]$Gpu = 'auto',
|
||||
[switch]$InstallModel,
|
||||
[switch]$EnableAutostart,
|
||||
[switch]$StartNow,
|
||||
[switch]$Force,
|
||||
[switch]$UpdateOnly,
|
||||
[switch]$NonInteractive
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$VendooDir = Split-Path -Parent $ScriptDir
|
||||
$LocalAiDir = Join-Path $VendooDir 'local-ai'
|
||||
$InstallRoot = Join-Path $LocalAiDir 'comfyui'
|
||||
$PortableRoot = Join-Path $InstallRoot 'ComfyUI_windows_portable'
|
||||
$ComfyDir = Join-Path $PortableRoot 'ComfyUI'
|
||||
$EmbeddedPython = Join-Path $PortableRoot 'python_embeded\python.exe'
|
||||
$ModelDir = Join-Path $ComfyDir 'models\checkpoints'
|
||||
$ModelPath = Join-Path $ModelDir 'flux1-schnell-fp8.safetensors'
|
||||
$WorkflowDir = Join-Path $LocalAiDir 'workflows'
|
||||
$WorkflowPath = Join-Path $WorkflowDir 'vendoo_flux_schnell_api.json'
|
||||
$LogDir = Join-Path $VendooDir 'logs'
|
||||
$LogFile = Join-Path $LogDir 'local-flux-install.log'
|
||||
$StateFile = Join-Path $LocalAiDir 'install-state.json'
|
||||
$EnvFile = Join-Path $VendooDir '.env'
|
||||
$DownloadDir = Join-Path $LocalAiDir 'downloads'
|
||||
$StartScript = Join-Path $LocalAiDir 'start-local-flux.ps1'
|
||||
$StopScript = Join-Path $LocalAiDir 'stop-local-flux.ps1'
|
||||
$TaskName = 'Vendoo Local FLUX'
|
||||
$ComfyPort = 8188
|
||||
$ComfyUrl = "http://127.0.0.1:$ComfyPort"
|
||||
$GitHubApi = 'https://api.github.com/repos/Comfy-Org/ComfyUI/releases/latest'
|
||||
$ModelUrl = 'https://huggingface.co/Comfy-Org/flux1-schnell/resolve/main/flux1-schnell-fp8.safetensors?download=true'
|
||||
|
||||
$InstallStartedAt = Get-Date
|
||||
$LastConsoleProgressAt = [DateTime]::MinValue
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LocalAiDir,$InstallRoot,$WorkflowDir,$LogDir,$DownloadDir | Out-Null
|
||||
|
||||
function Write-Utf8NoBom([string]$Path, [string]$Content) {
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
|
||||
}
|
||||
|
||||
|
||||
function Write-Utf8Bom([string]$Path, [string]$Content) {
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $utf8Bom)
|
||||
}
|
||||
|
||||
function Write-Log([string]$Message, [string]$Level = 'INFO') {
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
|
||||
Add-Content -Path $LogFile -Value $line -Encoding utf8
|
||||
if (-not $NonInteractive) {
|
||||
$color = 'Gray'
|
||||
if ($Level -eq 'OK') { $color = 'Green' }
|
||||
elseif ($Level -eq 'WARN') { $color = 'Yellow' }
|
||||
elseif ($Level -eq 'ERROR') { $color = 'Red' }
|
||||
Write-Host $line -ForegroundColor $color
|
||||
}
|
||||
}
|
||||
|
||||
function Save-State([string]$Status, [string]$Phase, [int]$Progress, [string]$Message, [hashtable]$Extra) {
|
||||
$previous = $null
|
||||
if (Test-Path -LiteralPath $StateFile) {
|
||||
try { $previous = Get-Content -LiteralPath $StateFile -Raw | ConvertFrom-Json } catch {}
|
||||
}
|
||||
$startedAt = if ($Phase -eq 'preflight' -and $Status -eq 'running') { $InstallStartedAt.ToString('o') } elseif ($previous -and $previous.started_at) { [string]$previous.started_at } else { $InstallStartedAt.ToString('o') }
|
||||
$state = [ordered]@{
|
||||
status = $Status
|
||||
phase = $Phase
|
||||
progress = [math]::Max(0, [math]::Min(100, $Progress))
|
||||
message = $Message
|
||||
started_at = $startedAt
|
||||
updated_at = (Get-Date).ToString('o')
|
||||
heartbeat_at = (Get-Date).ToString('o')
|
||||
process_id = $PID
|
||||
elapsed_seconds = [math]::Round(((Get-Date) - ([DateTime]::Parse($startedAt))).TotalSeconds)
|
||||
install_root = $InstallRoot
|
||||
comfy_url = $ComfyUrl
|
||||
model_path = $ModelPath
|
||||
workflow_path = $WorkflowPath
|
||||
log_file = $LogFile
|
||||
requested_install_model = [bool]$InstallModel
|
||||
requested_enable_autostart = [bool]$EnableAutostart
|
||||
requested_start_now = [bool]$StartNow
|
||||
}
|
||||
if ($Extra) {
|
||||
foreach ($key in $Extra.Keys) { $state[$key] = $Extra[$key] }
|
||||
}
|
||||
$tempState = "$StateFile.tmp"
|
||||
Write-Utf8NoBom -Path $tempState -Content ($state | ConvertTo-Json -Depth 6)
|
||||
Move-Item -LiteralPath $tempState -Destination $StateFile -Force
|
||||
}
|
||||
|
||||
function Set-EnvVar([string]$Key, [string]$Value) {
|
||||
$content = if (Test-Path $EnvFile) { Get-Content $EnvFile -Raw } else { '' }
|
||||
$line = "$Key=$Value"
|
||||
if ($content -match "(?m)^$([regex]::Escape($Key))=") {
|
||||
$content = [regex]::Replace($content, "(?m)^$([regex]::Escape($Key))=.*$", $line)
|
||||
} else {
|
||||
$content = $content.TrimEnd() + "`r`n" + $line + "`r`n"
|
||||
}
|
||||
Write-Utf8NoBom -Path $EnvFile -Content $content
|
||||
}
|
||||
|
||||
function Get-EnvValue([string]$Key) {
|
||||
if (-not (Test-Path $EnvFile)) { return '' }
|
||||
$content = Get-Content $EnvFile -Raw
|
||||
$match = [regex]::Match($content, "(?m)^$([regex]::Escape($Key))=(.*)$")
|
||||
if ($match.Success) { return $match.Groups[1].Value.Trim() }
|
||||
return ''
|
||||
}
|
||||
|
||||
function New-LocalToken {
|
||||
$bytes = New-Object byte[] 32
|
||||
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
|
||||
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+','-').Replace('/','_')
|
||||
}
|
||||
|
||||
function Test-Command([string]$Name) {
|
||||
return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Get-GpuType {
|
||||
if ($Gpu -ne 'auto') { return $Gpu }
|
||||
try {
|
||||
$names = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | ForEach-Object { $_.Name })
|
||||
$joined = ($names -join ' ').ToLowerInvariant()
|
||||
if ($joined -match 'nvidia|geforce|quadro|rtx|gtx') { return 'nvidia' }
|
||||
if ($joined -match 'amd|radeon') { return 'amd' }
|
||||
if ($joined -match 'intel') { return 'intel' }
|
||||
} catch {}
|
||||
return 'cpu'
|
||||
}
|
||||
|
||||
function Get-FreeSpaceGb([string]$Path) {
|
||||
try {
|
||||
$resolved = (Resolve-Path -LiteralPath $Path).Path
|
||||
$root = [System.IO.Path]::GetPathRoot($resolved)
|
||||
$drive = New-Object System.IO.DriveInfo -ArgumentList $root
|
||||
return [math]::Round($drive.AvailableFreeSpace / 1GB, 1)
|
||||
} catch {
|
||||
Write-Log "Freier Speicher konnte nicht ermittelt werden: $($_.Exception.Message)" 'WARN'
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
function Get-7ZipCandidates {
|
||||
$items = New-Object System.Collections.Generic.List[string]
|
||||
try {
|
||||
$command = Get-Command 7z.exe -ErrorAction SilentlyContinue
|
||||
if ($command -and $command.Source) { [void]$items.Add([string]$command.Source) }
|
||||
} catch {}
|
||||
|
||||
$known = New-Object System.Collections.Generic.List[string]
|
||||
if ($env:ProgramFiles) { [void]$known.Add((Join-Path $env:ProgramFiles '7-Zip\7z.exe')) }
|
||||
if (${env:ProgramFiles(x86)}) { [void]$known.Add((Join-Path ${env:ProgramFiles(x86)} '7-Zip\7z.exe')) }
|
||||
if ($env:LOCALAPPDATA) { [void]$known.Add((Join-Path $env:LOCALAPPDATA 'Programs\7-Zip\7z.exe')) }
|
||||
foreach ($path in $known) {
|
||||
if ($path -and (Test-Path -LiteralPath $path)) { [void]$items.Add([string]$path) }
|
||||
}
|
||||
return @($items | Select-Object -Unique)
|
||||
}
|
||||
|
||||
function Refresh-ProcessPath {
|
||||
$machine = [System.Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$user = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$env:Path = "$machine;$user"
|
||||
}
|
||||
|
||||
function Ensure-7Zip {
|
||||
Save-State 'running' 'dependency-7zip' 24 '7-Zip wird geprüft.' @{}
|
||||
$candidates = @(Get-7ZipCandidates)
|
||||
if ($candidates.Count -gt 0) {
|
||||
$path = [string]$candidates[0]
|
||||
Write-Log "7-Zip gefunden: $path" 'OK'
|
||||
return $path
|
||||
}
|
||||
|
||||
if (Test-Command 'winget.exe') {
|
||||
Write-Log '7-Zip fehlt. Installation über winget wird gestartet.' 'WARN'
|
||||
Save-State 'running' 'install-7zip' 25 '7-Zip wird automatisch installiert.' @{}
|
||||
& winget.exe install --id 7zip.7zip -e --silent --accept-source-agreements --accept-package-agreements | Out-Null
|
||||
$wingetExit = $LASTEXITCODE
|
||||
Refresh-ProcessPath
|
||||
Start-Sleep -Seconds 2
|
||||
$candidates = @(Get-7ZipCandidates)
|
||||
if ($candidates.Count -gt 0) {
|
||||
$path = [string]$candidates[0]
|
||||
Write-Log "7-Zip installiert und gefunden: $path" 'OK'
|
||||
return $path
|
||||
}
|
||||
Write-Log "winget beendete die 7-Zip-Installation mit Exit-Code $wingetExit, aber 7z.exe wurde nicht gefunden." 'WARN'
|
||||
}
|
||||
|
||||
throw '7-Zip ist erforderlich und konnte nicht automatisch installiert oder gefunden werden. Installiere 7-Zip und starte die FLUX-Installation danach erneut.'
|
||||
}
|
||||
|
||||
function Get-DirectoryMetrics([string]$Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
return @{ file_count = 0; bytes = [long]0; megabytes = 0 }
|
||||
}
|
||||
try {
|
||||
$files = @(Get-ChildItem -LiteralPath $Path -Recurse -File -Force -ErrorAction SilentlyContinue)
|
||||
$bytes = [long](($files | Measure-Object -Property Length -Sum).Sum)
|
||||
return @{
|
||||
file_count = $files.Count
|
||||
bytes = $bytes
|
||||
megabytes = [math]::Round($bytes / 1MB, 1)
|
||||
}
|
||||
} catch {
|
||||
return @{ file_count = 0; bytes = [long]0; megabytes = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-Stale7ZipForArchive([string]$ArchivePath, [string]$Destination) {
|
||||
try {
|
||||
$archiveEscaped = [regex]::Escape($ArchivePath)
|
||||
$destinationEscaped = [regex]::Escape($Destination)
|
||||
$items = @(Get-CimInstance Win32_Process -Filter "Name='7z.exe' OR Name='7zG.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -and ($_.CommandLine -match $archiveEscaped -or $_.CommandLine -match $destinationEscaped) })
|
||||
foreach ($item in $items) {
|
||||
Write-Log "Beende veralteten 7-Zip-Prozess PID $($item.ProcessId) für diese Installation." 'WARN'
|
||||
Stop-Process -Id $item.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Veraltete 7-Zip-Prozesse konnten nicht geprüft werden: $($_.Exception.Message)" 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-7ZipExtract([string]$SevenZipPath, [string]$ArchivePath, [string]$Destination) {
|
||||
if (-not (Test-Path -LiteralPath $SevenZipPath)) { throw "7-Zip wurde nicht gefunden: $SevenZipPath" }
|
||||
if (-not (Test-Path -LiteralPath $ArchivePath)) { throw "ComfyUI-Archiv wurde nicht gefunden: $ArchivePath" }
|
||||
|
||||
Stop-Stale7ZipForArchive -ArchivePath $ArchivePath -Destination $Destination
|
||||
$stdoutFile = Join-Path $LogDir 'local-flux-7zip-output.log'
|
||||
$stderrFile = Join-Path $LogDir 'local-flux-7zip-error.log'
|
||||
Remove-Item -LiteralPath $stdoutFile,$stderrFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$arguments = "x `"$ArchivePath`" -o`"$Destination`" -y -bsp1 -bso1 -bse1"
|
||||
Write-Log "Starte 7-Zip-Entpackung: `"$SevenZipPath`" x `"$ArchivePath`" -o`"$Destination`"" 'INFO'
|
||||
Save-State 'running' 'extract-comfyui' 33 '7-Zip wurde gestartet. ComfyUI wird entpackt.' @{ seven_zip = $SevenZipPath; archive = $ArchivePath; extract_root = $Destination; extract_files = 0; extract_mb = 0 }
|
||||
|
||||
$process = Start-Process -FilePath $SevenZipPath -ArgumentList $arguments -WorkingDirectory $DownloadDir -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile
|
||||
if (-not $process) { throw '7-Zip-Prozess konnte nicht gestartet werden.' }
|
||||
|
||||
$started = Get-Date
|
||||
$lastReportedFiles = -1
|
||||
$lastActivity = Get-Date
|
||||
$timeoutMinutes = 45
|
||||
|
||||
while (-not $process.HasExited) {
|
||||
Start-Sleep -Seconds 5
|
||||
$process.Refresh()
|
||||
$metrics = Get-DirectoryMetrics $Destination
|
||||
if ($metrics.file_count -ne $lastReportedFiles) {
|
||||
$lastReportedFiles = $metrics.file_count
|
||||
$lastActivity = Get-Date
|
||||
}
|
||||
$elapsed = [math]::Round(((Get-Date) - $started).TotalSeconds)
|
||||
$progress = 34 + [math]::Min(16, [math]::Floor($elapsed / 20))
|
||||
$message = "ComfyUI wird entpackt: $($metrics.file_count) Dateien · $($metrics.megabytes) MB · ${elapsed}s"
|
||||
Save-State 'running' 'extract-comfyui' $progress $message @{
|
||||
seven_zip = $SevenZipPath
|
||||
archive = $ArchivePath
|
||||
extract_root = $Destination
|
||||
extract_files = $metrics.file_count
|
||||
extract_bytes = [long]$metrics.bytes
|
||||
extract_mb = $metrics.megabytes
|
||||
extract_elapsed_seconds = $elapsed
|
||||
extract_process_id = $process.Id
|
||||
extract_stdout = $stdoutFile
|
||||
extract_stderr = $stderrFile
|
||||
}
|
||||
if (-not $NonInteractive -and ((Get-Date) - $script:LastConsoleProgressAt).TotalSeconds -ge 10) {
|
||||
Write-Host " [..] $message" -ForegroundColor Cyan
|
||||
$script:LastConsoleProgressAt = Get-Date
|
||||
}
|
||||
if (((Get-Date) - $started).TotalMinutes -gt $timeoutMinutes) {
|
||||
try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
||||
throw "7-Zip-Entpackung wurde nach $timeoutMinutes Minuten wegen Zeitüberschreitung abgebrochen."
|
||||
}
|
||||
if (((Get-Date) - $lastActivity).TotalMinutes -gt 3 -and $metrics.file_count -eq 0) {
|
||||
try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {}
|
||||
throw '7-Zip zeigt seit 3 Minuten keine Extraktionsaktivität. Prüfe Archiv, Virenscanner und freien Speicher.'
|
||||
}
|
||||
}
|
||||
|
||||
$process.WaitForExit()
|
||||
$process.Refresh()
|
||||
|
||||
$rawExitCode = $null
|
||||
try {
|
||||
$rawExitCode = $process.ExitCode
|
||||
} catch {
|
||||
Write-Log "7-Zip Exit-Code konnte über das Process-Objekt nicht direkt gelesen werden: $($_.Exception.Message)" 'WARN'
|
||||
}
|
||||
|
||||
$stdoutTail = if (Test-Path -LiteralPath $stdoutFile) { (Get-Content -LiteralPath $stdoutFile -Tail 20 -ErrorAction SilentlyContinue | Out-String).Trim() } else { '' }
|
||||
$stderrTail = if (Test-Path -LiteralPath $stderrFile) { (Get-Content -LiteralPath $stderrFile -Tail 20 -ErrorAction SilentlyContinue | Out-String).Trim() } else { '' }
|
||||
$finalMetrics = Get-DirectoryMetrics $Destination
|
||||
|
||||
$exitCode = $rawExitCode
|
||||
if ($null -eq $exitCode) {
|
||||
$hasSuccessMarker = $stdoutTail -match '(?im)^Everything is Ok\s*$'
|
||||
$hasPlausibleResult = ($finalMetrics.file_count -ge 20 -and $finalMetrics.bytes -ge 50MB)
|
||||
$hasErrorOutput = -not [string]::IsNullOrWhiteSpace($stderrTail)
|
||||
|
||||
if ($hasSuccessMarker -and $hasPlausibleResult -and -not $hasErrorOutput) {
|
||||
$exitCode = 0
|
||||
Write-Log "7-Zip Exit-Code war unter Windows PowerShell 5.1 leer. Erfolg wurde sicher über 'Everything is Ok', leeres Fehlerprotokoll und $($finalMetrics.file_count) Dateien / $($finalMetrics.megabytes) MB bestätigt." 'WARN'
|
||||
} else {
|
||||
$details = (($stderrTail, $stdoutTail | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ' | ')
|
||||
throw "7-Zip-Prozess wurde beendet, aber der Exit-Code konnte nicht ermittelt werden und die Erfolgsprüfung war nicht eindeutig. $details"
|
||||
}
|
||||
}
|
||||
|
||||
if ([int]$exitCode -ne 0) {
|
||||
$details = (($stderrTail, $stdoutTail | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ' | ')
|
||||
throw "ComfyUI-Archiv konnte nicht entpackt werden (7-Zip Exit-Code $exitCode). $details"
|
||||
}
|
||||
if ($finalMetrics.file_count -lt 20 -or $finalMetrics.bytes -lt 50MB) {
|
||||
throw "7-Zip meldete Erfolg, aber das Entpackungsergebnis ist unvollständig ($($finalMetrics.file_count) Dateien, $($finalMetrics.megabytes) MB)."
|
||||
}
|
||||
|
||||
Save-State 'running' 'extract-comfyui' 51 "ComfyUI wurde entpackt: $($finalMetrics.file_count) Dateien · $($finalMetrics.megabytes) MB." @{
|
||||
seven_zip = $SevenZipPath
|
||||
archive = $ArchivePath
|
||||
extract_root = $Destination
|
||||
extract_files = $finalMetrics.file_count
|
||||
extract_bytes = [long]$finalMetrics.bytes
|
||||
extract_mb = $finalMetrics.megabytes
|
||||
extract_exit_code = $exitCode
|
||||
}
|
||||
Write-Log "7-Zip-Entpackung abgeschlossen: $($finalMetrics.file_count) Dateien, $($finalMetrics.megabytes) MB." 'OK'
|
||||
}
|
||||
|
||||
function Get-LatestPortableAsset([string]$GpuType) {
|
||||
Write-Log 'Frage aktuelle ComfyUI-Portable-Version über GitHub API ab.'
|
||||
$release = Invoke-RestMethod -Uri $GitHubApi -Headers @{ 'User-Agent' = 'Vendoo-Local-Flux-Installer' }
|
||||
$assets = @($release.assets)
|
||||
if ($assets.Count -eq 0) { throw 'Die aktuelle ComfyUI-Version enthält keine Download-Assets.' }
|
||||
|
||||
$patterns = @()
|
||||
switch ($GpuType) {
|
||||
'nvidia' { $patterns = @('windows_portable.*nvidia.*\.7z$','windows_portable.*cu13.*\.7z$','windows_portable.*\.7z$') }
|
||||
'amd' { $patterns = @('windows_portable.*amd.*\.7z$','windows_portable.*\.7z$') }
|
||||
'intel' { $patterns = @('windows_portable.*intel.*\.7z$','windows_portable.*\.7z$') }
|
||||
default { $patterns = @('windows_portable.*\.7z$') }
|
||||
}
|
||||
|
||||
foreach ($pattern in $patterns) {
|
||||
$asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1
|
||||
if ($asset) { return $asset }
|
||||
}
|
||||
throw "Kein passendes ComfyUI-Windows-Portable-Paket für GPU-Typ '$GpuType' gefunden."
|
||||
}
|
||||
|
||||
function Format-DownloadDuration([double]$Seconds) {
|
||||
if ($Seconds -lt 0 -or [double]::IsInfinity($Seconds) -or [double]::IsNaN($Seconds)) { return 'unbekannt' }
|
||||
$span = [TimeSpan]::FromSeconds([math]::Round($Seconds))
|
||||
if ($span.TotalHours -ge 1) { return ('{0}h {1}m' -f [math]::Floor($span.TotalHours), $span.Minutes) }
|
||||
if ($span.TotalMinutes -ge 1) { return ('{0}m {1}s' -f [math]::Floor($span.TotalMinutes), $span.Seconds) }
|
||||
return ('{0}s' -f [math]::Max(0, $span.Seconds))
|
||||
}
|
||||
|
||||
function Remove-StaleVendooBitsJobs {
|
||||
try {
|
||||
Import-Module BitsTransfer -ErrorAction SilentlyContinue
|
||||
$jobs = @(Get-BitsTransfer -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like 'Vendoo *' })
|
||||
foreach ($job in $jobs) {
|
||||
try { Remove-BitsTransfer -BitsJob $job -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
if ($jobs.Count -gt 0) {
|
||||
Write-Log "$($jobs.Count) veraltete(r) Vendoo-BITS-Auftrag/Aufträge wurden entfernt." 'WARN'
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Invoke-ResumableHttpDownload(
|
||||
[string]$Url,
|
||||
[string]$PartialPath,
|
||||
[string]$Label,
|
||||
[string]$Phase,
|
||||
[int]$ProgressStart,
|
||||
[int]$ProgressEnd
|
||||
) {
|
||||
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
|
||||
|
||||
$maxAttempts = 5
|
||||
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
||||
$handler = $null
|
||||
$client = $null
|
||||
$request = $null
|
||||
$response = $null
|
||||
$networkStream = $null
|
||||
$fileStream = $null
|
||||
|
||||
try {
|
||||
$resumeFrom = 0L
|
||||
if (Test-Path -LiteralPath $PartialPath) {
|
||||
$resumeFrom = [long](Get-Item -LiteralPath $PartialPath).Length
|
||||
}
|
||||
|
||||
$handler = [System.Net.Http.HttpClientHandler]::new()
|
||||
$handler.AllowAutoRedirect = $true
|
||||
$handler.AutomaticDecompression = [System.Net.DecompressionMethods]::None
|
||||
$client = [System.Net.Http.HttpClient]::new($handler)
|
||||
$client.Timeout = [TimeSpan]::FromHours(24)
|
||||
|
||||
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $Url)
|
||||
[void]$request.Headers.TryAddWithoutValidation('User-Agent', 'Vendoo-Local-Flux-Installer/1.14.5')
|
||||
if ($resumeFrom -gt 0) {
|
||||
$request.Headers.Range = [System.Net.Http.Headers.RangeHeaderValue]::Parse("bytes=$resumeFrom-")
|
||||
Write-Log "$Label wird ab $([math]::Round($resumeFrom / 1MB, 1)) MB fortgesetzt." 'OK'
|
||||
}
|
||||
|
||||
$response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
|
||||
$statusCode = [int]$response.StatusCode
|
||||
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "HTTP-Fehler $statusCode ($($response.ReasonPhrase))."
|
||||
}
|
||||
|
||||
$append = ($resumeFrom -gt 0 -and $statusCode -eq 206)
|
||||
if ($resumeFrom -gt 0 -and -not $append) {
|
||||
Write-Log 'Der Downloadserver unterstützt an dieser Stelle keine Fortsetzung. Die Teildatei wird kontrolliert neu begonnen.' 'WARN'
|
||||
Remove-Item -LiteralPath $PartialPath -Force -ErrorAction SilentlyContinue
|
||||
$resumeFrom = 0L
|
||||
}
|
||||
|
||||
$remainingLength = 0L
|
||||
if ($null -ne $response.Content.Headers.ContentLength) {
|
||||
$remainingLength = [long]$response.Content.Headers.ContentLength
|
||||
}
|
||||
$totalBytes = if ($remainingLength -gt 0) { $resumeFrom + $remainingLength } else { 0L }
|
||||
|
||||
$networkStream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
|
||||
$mode = if ($append) { [System.IO.FileMode]::Append } else { [System.IO.FileMode]::Create }
|
||||
$fileStream = [System.IO.File]::Open($PartialPath, $mode, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read)
|
||||
|
||||
$buffer = New-Object byte[] (1MB)
|
||||
$downloaded = $resumeFrom
|
||||
$lastSampleBytes = $downloaded
|
||||
$lastSampleAt = Get-Date
|
||||
$lastStateAt = [DateTime]::MinValue
|
||||
$lastConsoleAt = [DateTime]::MinValue
|
||||
$smoothedBytesPerSecond = 0.0
|
||||
|
||||
while (($read = $networkStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$fileStream.Write($buffer, 0, $read)
|
||||
$downloaded += $read
|
||||
$now = Get-Date
|
||||
|
||||
if (($now - $lastSampleAt).TotalSeconds -ge 2) {
|
||||
$sampleSeconds = [math]::Max(0.1, ($now - $lastSampleAt).TotalSeconds)
|
||||
$sampleSpeed = ($downloaded - $lastSampleBytes) / $sampleSeconds
|
||||
if ($smoothedBytesPerSecond -le 0) { $smoothedBytesPerSecond = $sampleSpeed }
|
||||
else { $smoothedBytesPerSecond = ($smoothedBytesPerSecond * 0.65) + ($sampleSpeed * 0.35) }
|
||||
$lastSampleBytes = $downloaded
|
||||
$lastSampleAt = $now
|
||||
}
|
||||
|
||||
if (($now - $lastStateAt).TotalSeconds -ge 2) {
|
||||
$percent = 0
|
||||
if ($totalBytes -gt 0) {
|
||||
$percent = [math]::Min(100, [math]::Round(($downloaded / $totalBytes) * 100, 1))
|
||||
}
|
||||
$overall = $ProgressStart
|
||||
if ($totalBytes -gt 0) {
|
||||
$overall = $ProgressStart + [math]::Round((($ProgressEnd - $ProgressStart) * $percent) / 100)
|
||||
}
|
||||
$downloadedMb = [math]::Round($downloaded / 1MB, 1)
|
||||
$totalMb = if ($totalBytes -gt 0) { [math]::Round($totalBytes / 1MB, 1) } else { 0 }
|
||||
$speedMbps = [math]::Round($smoothedBytesPerSecond / 1MB, 1)
|
||||
$etaSeconds = -1
|
||||
if ($totalBytes -gt 0 -and $smoothedBytesPerSecond -gt 0) {
|
||||
$etaSeconds = [math]::Max(0, ($totalBytes - $downloaded) / $smoothedBytesPerSecond)
|
||||
}
|
||||
$etaText = Format-DownloadDuration $etaSeconds
|
||||
$message = if ($totalBytes -gt 0) {
|
||||
"${Label}: $percent% · $downloadedMb / $totalMb MB · $speedMbps MB/s · Rest ca. $etaText"
|
||||
} else {
|
||||
"${Label}: $downloadedMb MB · $speedMbps MB/s · Gesamtgröße wird ermittelt"
|
||||
}
|
||||
|
||||
Save-State 'running' $Phase $overall $message @{
|
||||
download_percent = $percent
|
||||
bytes_transferred = [long]$downloaded
|
||||
bytes_total = [long]$totalBytes
|
||||
download_speed_mbps = $speedMbps
|
||||
eta_seconds = [math]::Round($etaSeconds)
|
||||
download_transport = 'http-stream-resumable'
|
||||
download_attempt = $attempt
|
||||
partial_path = $PartialPath
|
||||
}
|
||||
|
||||
if (-not $NonInteractive) {
|
||||
$progressPercent = if ($totalBytes -gt 0) { [int][math]::Min(100, [math]::Round($percent)) } else { 0 }
|
||||
Write-Progress -Activity "$Label wird heruntergeladen" -Status $message -PercentComplete $progressPercent
|
||||
if (($now - $lastConsoleAt).TotalSeconds -ge 10) {
|
||||
Write-Host " [DOWNLOAD] $message" -ForegroundColor Cyan
|
||||
$lastConsoleAt = $now
|
||||
}
|
||||
}
|
||||
$lastStateAt = $now
|
||||
}
|
||||
}
|
||||
|
||||
$fileStream.Flush()
|
||||
if (-not $NonInteractive) {
|
||||
Write-Progress -Activity "$Label wird heruntergeladen" -Completed
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
$errorText = $_.Exception.Message
|
||||
if ($attempt -ge $maxAttempts) {
|
||||
throw "$Label konnte nach $maxAttempts Versuchen nicht vollständig geladen werden. Letzter Fehler: $errorText"
|
||||
}
|
||||
|
||||
$currentBytes = 0L
|
||||
if (Test-Path -LiteralPath $PartialPath) {
|
||||
try { $currentBytes = [long](Get-Item -LiteralPath $PartialPath).Length } catch {}
|
||||
}
|
||||
Write-Log "$Label-Download wurde unterbrochen: $errorText. Automatische Fortsetzung in 5 Sekunden (bereits $([math]::Round($currentBytes / 1MB, 1)) MB)." 'WARN'
|
||||
Save-State 'running' $Phase $ProgressStart "$Label wird nach einer Netzwerkunterbrechung automatisch fortgesetzt." @{
|
||||
bytes_transferred = $currentBytes
|
||||
download_transport = 'http-stream-resumable'
|
||||
download_attempt = $attempt
|
||||
retry_in_seconds = 5
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
} finally {
|
||||
if ($fileStream) { try { $fileStream.Dispose() } catch {} }
|
||||
if ($networkStream) { try { $networkStream.Dispose() } catch {} }
|
||||
if ($response) { try { $response.Dispose() } catch {} }
|
||||
if ($request) { try { $request.Dispose() } catch {} }
|
||||
if ($client) { try { $client.Dispose() } catch {} }
|
||||
if ($handler) { try { $handler.Dispose() } catch {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Download-File([string]$Url, [string]$Destination, [string]$Label, [long]$MinimumBytes = 1) {
|
||||
$partial = "$Destination.part"
|
||||
|
||||
if (Test-Path -LiteralPath $Destination) {
|
||||
$size = [long](Get-Item -LiteralPath $Destination).Length
|
||||
if ($size -ge $MinimumBytes) {
|
||||
Write-Log "$Label bereits vollständig vorhanden; Download wird übersprungen." 'OK'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log "$Label ist unvollständig. Bereits vorhandene Daten werden für die Fortsetzung übernommen." 'WARN'
|
||||
if (-not (Test-Path -LiteralPath $partial) -or (Get-Item -LiteralPath $partial).Length -lt $size) {
|
||||
Move-Item -LiteralPath $Destination -Destination $partial -Force
|
||||
} else {
|
||||
Remove-Item -LiteralPath $Destination -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Remove-StaleVendooBitsJobs
|
||||
|
||||
$existingBytes = 0L
|
||||
if (Test-Path -LiteralPath $partial) {
|
||||
$existingBytes = [long](Get-Item -LiteralPath $partial).Length
|
||||
}
|
||||
|
||||
$phase = if ($Label -like 'FLUX*') { 'download-model' } else { 'download-comfyui' }
|
||||
$progressStart = if ($phase -eq 'download-model') { 65 } else { 10 }
|
||||
$progressEnd = if ($phase -eq 'download-model') { 85 } else { 23 }
|
||||
|
||||
if ($existingBytes -gt 0) {
|
||||
Write-Log "$Label-Teildownload gefunden: $([math]::Round($existingBytes / 1MB, 1)) MB. Download wird fortgesetzt." 'OK'
|
||||
} else {
|
||||
Write-Log "$Label wird mit sichtbarem Fortschritt heruntergeladen."
|
||||
}
|
||||
|
||||
Save-State 'running' $phase $progressStart "$Label-Download wird vorbereitet." @{
|
||||
download_percent = 0
|
||||
bytes_transferred = $existingBytes
|
||||
download_transport = 'http-stream-resumable'
|
||||
partial_path = $partial
|
||||
}
|
||||
|
||||
Invoke-ResumableHttpDownload -Url $Url -PartialPath $partial -Label $Label -Phase $phase -ProgressStart $progressStart -ProgressEnd $progressEnd
|
||||
|
||||
if (-not (Test-Path -LiteralPath $partial)) { throw "$Label wurde nicht heruntergeladen." }
|
||||
$downloadedSize = [long](Get-Item -LiteralPath $partial).Length
|
||||
if ($downloadedSize -lt $MinimumBytes) {
|
||||
throw "$Label ist mit $downloadedSize Bytes unerwartet klein und wahrscheinlich unvollständig. Die Teildatei bleibt für eine Fortsetzung erhalten."
|
||||
}
|
||||
|
||||
Move-Item -LiteralPath $partial -Destination $Destination -Force
|
||||
Save-State 'running' $phase $progressEnd "$Label vollständig heruntergeladen." @{
|
||||
download_percent = 100
|
||||
bytes_transferred = $downloadedSize
|
||||
bytes_total = $downloadedSize
|
||||
download_transport = 'http-stream-resumable'
|
||||
}
|
||||
Write-Log "$Label heruntergeladen ($([math]::Round($downloadedSize / 1MB, 1)) MB)." 'OK'
|
||||
}
|
||||
|
||||
function Write-Workflow {
|
||||
$workflow = @'
|
||||
{
|
||||
"3": { "inputs": { "ckpt_name": "flux1-schnell-fp8.safetensors" }, "class_type": "CheckpointLoaderSimple" },
|
||||
"4": { "inputs": { "text": "__PROMPT__", "clip": ["3", 1] }, "class_type": "CLIPTextEncode" },
|
||||
"5": { "inputs": { "text": "__NEGATIVE_PROMPT__", "clip": ["3", 1] }, "class_type": "CLIPTextEncode" },
|
||||
"6": { "inputs": { "width": 1024, "height": 1024, "batch_size": 1 }, "class_type": "EmptyLatentImage" },
|
||||
"7": { "inputs": { "seed": "__SEED__", "steps": 4, "cfg": 1, "sampler_name": "euler", "scheduler": "simple", "denoise": 1, "model": ["3", 0], "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0] }, "class_type": "KSampler" },
|
||||
"8": { "inputs": { "samples": ["7", 0], "vae": ["3", 2] }, "class_type": "VAEDecode" },
|
||||
"9": { "inputs": { "filename_prefix": "Vendoo/flux_schnell", "images": ["8", 0] }, "class_type": "SaveImage" }
|
||||
}
|
||||
'@
|
||||
Write-Utf8NoBom -Path $WorkflowPath -Content $workflow
|
||||
Write-Log 'FLUX-Prompt-Workflow wurde erstellt.' 'OK'
|
||||
}
|
||||
|
||||
function Write-StartStopScripts {
|
||||
$runtimeStartTemplate = Join-Path $ScriptDir 'runtime-start-local-flux.ps1'
|
||||
$runtimeStopTemplate = Join-Path $ScriptDir 'runtime-stop-local-flux.ps1'
|
||||
if (-not (Test-Path -LiteralPath $runtimeStartTemplate)) {
|
||||
throw "FLUX-Runtime-Startvorlage fehlt: $runtimeStartTemplate"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $runtimeStopTemplate)) {
|
||||
throw "FLUX-Runtime-Stoppvorlage fehlt: $runtimeStopTemplate"
|
||||
}
|
||||
Copy-Item -LiteralPath $runtimeStartTemplate -Destination $StartScript -Force
|
||||
Copy-Item -LiteralPath $runtimeStopTemplate -Destination $StopScript -Force
|
||||
Write-Log 'Gehärtete FLUX-Runtime-Skripte wurden installiert.' 'OK'
|
||||
}
|
||||
|
||||
function Register-AutostartTask {
|
||||
if (-not $EnableAutostart) { return }
|
||||
$taskCommand = "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartScript`""
|
||||
& schtasks.exe /Create /SC ONLOGON /TN $TaskName /TR $taskCommand /F | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Autostart-Aufgabe konnte nicht erstellt werden.' }
|
||||
Write-Log 'Autostart-Aufgabe für lokalen FLUX-Server erstellt.' 'OK'
|
||||
}
|
||||
|
||||
function Test-ComfyReachable {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$ComfyUrl/system_stats" -TimeoutSec 5
|
||||
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
try {
|
||||
Save-State 'running' 'preflight' 2 'Vorprüfung läuft.' @{}
|
||||
$gpuType = Get-GpuType
|
||||
$freeGb = Get-FreeSpaceGb $LocalAiDir
|
||||
Write-Log "Erkannte GPU-Klasse: $gpuType"
|
||||
Write-Log "Freier Speicher: $freeGb GB"
|
||||
if ($InstallModel -and $freeGb -lt 28 -and -not $Force) {
|
||||
throw 'Für ComfyUI plus FLUX.1-schnell FP8 werden mindestens 28 GB freier Speicher empfohlen.'
|
||||
}
|
||||
|
||||
$alreadyInstalled = (Test-Path $EmbeddedPython) -and (Test-Path (Join-Path $ComfyDir 'main.py'))
|
||||
if ($alreadyInstalled -and -not $Force -and -not $UpdateOnly) {
|
||||
Write-Log 'ComfyUI ist bereits installiert. Neuinstallation wird übersprungen.' 'OK'
|
||||
} elseif ($UpdateOnly -and $alreadyInstalled) {
|
||||
Save-State 'running' 'update' 15 'ComfyUI wird aktualisiert.' @{ gpu = $gpuType }
|
||||
$updateScript = Join-Path $PortableRoot 'update\update_comfyui.bat'
|
||||
if (Test-Path $updateScript) {
|
||||
& cmd.exe /d /c "`"$updateScript`"" | Out-Null
|
||||
Write-Log 'ComfyUI-Update ausgeführt.' 'OK'
|
||||
} else {
|
||||
Write-Log 'Kein portables Update-Skript gefunden; aktuelle Installation bleibt unverändert.' 'WARN'
|
||||
}
|
||||
} else {
|
||||
Save-State 'running' 'download-comfyui' 10 'ComfyUI Portable wird geladen.' @{ gpu = $gpuType }
|
||||
$asset = Get-LatestPortableAsset $gpuType
|
||||
$archive = Join-Path $DownloadDir $asset.name
|
||||
Download-File $asset.browser_download_url $archive 'ComfyUI Portable' 100MB
|
||||
|
||||
Save-State 'running' 'extract-comfyui' 30 'ComfyUI Portable wird für die Entpackung vorbereitet.' @{ asset = $asset.name; gpu = $gpuType }
|
||||
$sevenZip = Ensure-7Zip
|
||||
$extractTemp = Join-Path $LocalAiDir 'extract-temp'
|
||||
$reuseExtractedPortable = $false
|
||||
|
||||
if (Test-Path -LiteralPath $extractTemp) {
|
||||
$resumePortable = Get-ChildItem -LiteralPath $extractTemp -Directory -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$_.Name -match '^ComfyUI_windows_portable' -and
|
||||
(Test-Path -LiteralPath (Join-Path $_.FullName 'ComfyUI\main.py')) -and
|
||||
(Test-Path -LiteralPath (Join-Path $_.FullName 'python_embeded\python.exe'))
|
||||
} |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($resumePortable) {
|
||||
$resumeMetrics = Get-DirectoryMetrics $extractTemp
|
||||
if ($resumeMetrics.file_count -ge 20 -and $resumeMetrics.bytes -ge 50MB) {
|
||||
$reuseExtractedPortable = $true
|
||||
Write-Log "Vollständiger temporärer ComfyUI-Entpackstand wird wiederverwendet: $($resumeMetrics.file_count) Dateien, $($resumeMetrics.megabytes) MB." 'OK'
|
||||
Save-State 'running' 'extract-comfyui' 51 "Vorhandener vollständiger Entpackstand wird fortgesetzt: $($resumeMetrics.file_count) Dateien · $($resumeMetrics.megabytes) MB." @{
|
||||
seven_zip = $sevenZip
|
||||
archive = $archive
|
||||
extract_root = $extractTemp
|
||||
extract_files = $resumeMetrics.file_count
|
||||
extract_bytes = [long]$resumeMetrics.bytes
|
||||
extract_mb = $resumeMetrics.megabytes
|
||||
extract_reused = $true
|
||||
extract_exit_code = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $reuseExtractedPortable) {
|
||||
Write-Log 'Vorhandenes temporäres Entpackungsverzeichnis ist nicht vollständig und wird bereinigt.' 'WARN'
|
||||
Remove-Item -LiteralPath $extractTemp -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $reuseExtractedPortable) {
|
||||
New-Item -ItemType Directory -Force -Path $extractTemp | Out-Null
|
||||
Invoke-7ZipExtract -SevenZipPath $sevenZip -ArchivePath $archive -Destination $extractTemp
|
||||
}
|
||||
|
||||
Save-State 'running' 'install-comfyui' 52 'Entpackte ComfyUI-Struktur wird geprüft.' @{ asset = $asset.name; gpu = $gpuType; extract_reused = $reuseExtractedPortable }
|
||||
$sourcePortable = Get-ChildItem -LiteralPath $extractTemp -Directory -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match '^ComfyUI_windows_portable' -and (Test-Path -LiteralPath (Join-Path $_.FullName 'ComfyUI\main.py')) } |
|
||||
Select-Object -First 1
|
||||
if (-not $sourcePortable) {
|
||||
$mainFile = Get-ChildItem -LiteralPath $extractTemp -Filter 'main.py' -File -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match '[\\/]ComfyUI[\\/]main\.py$' } |
|
||||
Select-Object -First 1
|
||||
if ($mainFile) {
|
||||
$comfyFolder = Split-Path -Parent $mainFile.FullName
|
||||
$sourcePortablePath = Split-Path -Parent $comfyFolder
|
||||
$sourcePortable = Get-Item -LiteralPath $sourcePortablePath
|
||||
}
|
||||
}
|
||||
if (-not $sourcePortable) { throw 'Entpacktes ComfyUI-Portable-Verzeichnis wurde nicht gefunden. Prüfe local-ai\extract-temp und das 7-Zip-Protokoll.' }
|
||||
if (Test-Path -LiteralPath $PortableRoot) { Remove-Item -LiteralPath $PortableRoot -Recurse -Force }
|
||||
Move-Item -LiteralPath $sourcePortable.FullName -Destination $PortableRoot
|
||||
if (-not (Test-Path -LiteralPath $EmbeddedPython) -or -not (Test-Path -LiteralPath (Join-Path $ComfyDir 'main.py'))) {
|
||||
throw 'ComfyUI wurde verschoben, aber Python oder main.py fehlen. Die Archivstruktur ist unerwartet.'
|
||||
}
|
||||
Remove-Item -LiteralPath $extractTemp -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Save-State 'running' 'install-comfyui' 54 'ComfyUI Portable ist vollständig installiert.' @{ asset = $asset.name; gpu = $gpuType; comfy_installed = $true }
|
||||
Write-Log 'ComfyUI Portable installiert.' 'OK'
|
||||
}
|
||||
|
||||
Save-State 'running' 'configure' 55 'Sichere localhost-Konfiguration wird erstellt.' @{ gpu = $gpuType }
|
||||
Write-Workflow
|
||||
Write-StartStopScripts
|
||||
$token = Get-EnvValue 'LOCAL_IMAGE_TOKEN'
|
||||
if ([string]::IsNullOrWhiteSpace($token)) { $token = New-LocalToken }
|
||||
Set-EnvVar 'LOCAL_IMAGE_ENABLED' 'true'
|
||||
Set-EnvVar 'LOCAL_IMAGE_URL' $ComfyUrl
|
||||
Set-EnvVar 'LOCAL_IMAGE_TOKEN' $token
|
||||
Set-EnvVar 'LOCAL_IMAGE_WORKFLOW_PATH' 'local-ai/workflows/vendoo_flux_schnell_api.json'
|
||||
Set-EnvVar 'LOCAL_IMAGE_INSTALL_PATH' 'local-ai/comfyui/ComfyUI_windows_portable'
|
||||
Set-EnvVar 'LOCAL_IMAGE_REQUIRE_TOKEN' 'true'
|
||||
|
||||
if ($InstallModel) {
|
||||
Save-State 'running' 'download-model' 65 'FLUX.1-schnell FP8 wird geladen.' @{ gpu = $gpuType }
|
||||
New-Item -ItemType Directory -Force -Path $ModelDir | Out-Null
|
||||
Download-File $ModelUrl $ModelPath 'FLUX.1-schnell FP8' 1GB
|
||||
if ((Get-Item $ModelPath).Length -lt 1GB) { throw 'Die heruntergeladene FLUX-Datei ist unerwartet klein und wahrscheinlich unvollständig.' }
|
||||
Write-Log 'FLUX.1-schnell FP8 installiert.' 'OK'
|
||||
} else {
|
||||
Write-Log 'Modell-Download wurde nicht ausgewählt.' 'WARN'
|
||||
}
|
||||
|
||||
Save-State 'running' 'autostart' 88 'Autostart wird eingerichtet.' @{ gpu = $gpuType }
|
||||
Register-AutostartTask
|
||||
|
||||
if ($StartNow) {
|
||||
Save-State 'running' 'start' 94 'Lokaler FLUX-Server wird gestartet.' @{ gpu = $gpuType }
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $StartScript
|
||||
Start-Sleep -Seconds 8
|
||||
}
|
||||
|
||||
$reachable = Test-ComfyReachable
|
||||
$modelInstalled = Test-Path $ModelPath
|
||||
$message = 'Lokales FLUX / ComfyUI ist installiert.'
|
||||
if (-not $modelInstalled) { $message += ' Das FLUX-Modell muss noch installiert werden.' }
|
||||
if ($StartNow -and -not $reachable) { $message += ' Der Server wurde gestartet, ist aber noch nicht erreichbar; prüfe das Runtime-Log.' }
|
||||
Save-State 'completed' 'done' 100 $message @{ gpu = $gpuType; comfy_installed = $true; model_installed = $modelInstalled; reachable = $reachable; autostart = [bool]$EnableAutostart }
|
||||
Write-Log $message 'OK'
|
||||
exit 0
|
||||
} catch {
|
||||
$message = $_.Exception.Message
|
||||
$position = $_.InvocationInfo.PositionMessage
|
||||
$failedProgress = 0
|
||||
try {
|
||||
if (Test-Path -LiteralPath $StateFile) {
|
||||
$previousState = Get-Content -LiteralPath $StateFile -Raw | ConvertFrom-Json
|
||||
$failedProgress = [int]$previousState.progress
|
||||
}
|
||||
} catch {}
|
||||
Write-Log $message 'ERROR'
|
||||
if ($position) { Write-Log $position 'ERROR' }
|
||||
Save-State 'failed' 'error' $failedProgress $message @{ error_position = $position; can_resume = $true; comfy_installed = ((Test-Path $EmbeddedPython) -and (Test-Path (Join-Path $ComfyDir 'main.py'))); model_installed = (Test-Path $ModelPath) }
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
param(
|
||||
[int]$StartupTimeoutSeconds = 180,
|
||||
[ValidateSet('auto','fast','balanced','lowvram')]
|
||||
[string]$Profile = 'auto'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
if ((Split-Path -Leaf $ScriptDir) -eq 'local-ai') {
|
||||
$VendooDir = Split-Path -Parent $ScriptDir
|
||||
} else {
|
||||
$VendooDir = Split-Path -Parent $ScriptDir
|
||||
}
|
||||
$LocalAiDir = Join-Path $VendooDir 'local-ai'
|
||||
$PortableRoot = Join-Path $LocalAiDir 'comfyui\ComfyUI_windows_portable'
|
||||
$Python = Join-Path $PortableRoot 'python_embeded\python.exe'
|
||||
$Main = Join-Path $PortableRoot 'ComfyUI\main.py'
|
||||
$LogDir = Join-Path $VendooDir 'logs'
|
||||
$StdoutLog = Join-Path $LogDir 'local-flux-runtime.log'
|
||||
$StderrLog = Join-Path $LogDir 'local-flux-runtime-error.log'
|
||||
$StateFile = Join-Path $LogDir 'local-flux-runtime-state.json'
|
||||
$PidFile = Join-Path $LogDir 'local-flux-runtime.pid'
|
||||
$ComfyUrl = 'http://127.0.0.1:8188'
|
||||
$ComfyPort = 8188
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$env:PYTHONUTF8 = '1'
|
||||
$env:PYTHONIOENCODING = 'utf-8'
|
||||
$env:NO_COLOR = '1'
|
||||
|
||||
function Write-Utf8NoBom([string]$Path, [string]$Content) {
|
||||
$encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
function Remove-Ansi([string]$Text) {
|
||||
if ([string]::IsNullOrEmpty($Text)) { return '' }
|
||||
$pattern = ([string][char]27) + '\\[[0-?]*[ -/]*[@-~]'
|
||||
return [regex]::Replace($Text, $pattern, '')
|
||||
}
|
||||
|
||||
function Read-Tail([string]$Path, [int]$Count = 16) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
||||
try {
|
||||
return @(Get-Content -LiteralPath $Path -Tail $Count -ErrorAction SilentlyContinue | ForEach-Object { Remove-Ansi ([string]$_) })
|
||||
} catch { return @() }
|
||||
}
|
||||
|
||||
function Save-RuntimeState([string]$Status, [string]$Message, [int]$ProcessId = 0, [int]$Elapsed = 0, [string]$ErrorMessage = '') {
|
||||
$state = [ordered]@{
|
||||
status = $Status
|
||||
message = $Message
|
||||
pid = if ($ProcessId -gt 0) { $ProcessId } else { $null }
|
||||
port = $ComfyPort
|
||||
url = $ComfyUrl
|
||||
elapsed_seconds = $Elapsed
|
||||
updated_at = (Get-Date).ToString('o')
|
||||
stdout_log = $StdoutLog
|
||||
stderr_log = $StderrLog
|
||||
error = $ErrorMessage
|
||||
stdout_tail = @(Read-Tail $StdoutLog 12)
|
||||
stderr_tail = @(Read-Tail $StderrLog 16)
|
||||
}
|
||||
Write-Utf8NoBom -Path $StateFile -Content ($state | ConvertTo-Json -Depth 5)
|
||||
if ($ProcessId -gt 0) { Write-Utf8NoBom -Path $PidFile -Content ([string]$ProcessId) }
|
||||
}
|
||||
|
||||
|
||||
function Test-ComfyFlag([string]$Flag) {
|
||||
try {
|
||||
$help = & $Python -s $Main --help 2>&1 | Out-String
|
||||
return $help -match [regex]::Escape($Flag)
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Get-NvidiaVramGb {
|
||||
try {
|
||||
$line = & nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>$null | Select-Object -First 1
|
||||
if ($line -match '^\s*(\d+)') { return [math]::Round(([double]$Matches[1] / 1024), 1) }
|
||||
} catch {}
|
||||
return 0
|
||||
}
|
||||
|
||||
function Get-PerformanceArguments([string]$RequestedProfile) {
|
||||
$effective = $RequestedProfile
|
||||
$vramGb = Get-NvidiaVramGb
|
||||
if ($effective -eq 'auto') {
|
||||
if ($vramGb -ge 12) { $effective = 'fast' }
|
||||
elseif ($vramGb -gt 0 -and $vramGb -lt 8) { $effective = 'lowvram' }
|
||||
else { $effective = 'balanced' }
|
||||
}
|
||||
$flags = New-Object System.Collections.Generic.List[string]
|
||||
if ($effective -eq 'fast') {
|
||||
$flags.Add('--highvram')
|
||||
$flags.Add('--cache-classic')
|
||||
if (Test-ComfyFlag '--fast') {
|
||||
$flags.Add('--fast'); $flags.Add('fp16_accumulation'); $flags.Add('cublas_ops')
|
||||
}
|
||||
} elseif ($effective -eq 'lowvram') {
|
||||
$flags.Add('--lowvram')
|
||||
$flags.Add('--cache-none')
|
||||
} else {
|
||||
$flags.Add('--cache-classic')
|
||||
}
|
||||
return [pscustomobject]@{ profile = $effective; requested = $RequestedProfile; vram_gb = $vramGb; flags = @($flags) }
|
||||
}
|
||||
|
||||
function Test-ComfyReady {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "$ComfyUrl/system_stats" -TimeoutSec 4
|
||||
return $response.StatusCode -ge 200 -and $response.StatusCode -lt 300
|
||||
} catch { return $false }
|
||||
}
|
||||
|
||||
function Get-ComfyProcess {
|
||||
try {
|
||||
$items = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -match 'ComfyUI[\\/]main\.py' -and
|
||||
$_.CommandLine -match "--port\s+$ComfyPort"
|
||||
} | Select-Object -First 1)
|
||||
if ($items.Count -gt 0) { return $items[0] }
|
||||
return $null
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $Python)) { throw "Embedded Python wurde nicht gefunden: $Python" }
|
||||
if (-not (Test-Path -LiteralPath $Main)) { throw "ComfyUI main.py wurde nicht gefunden: $Main" }
|
||||
|
||||
if (Test-ComfyReady) {
|
||||
$existingReady = Get-ComfyProcess
|
||||
$readyPid = if ($existingReady) { [int]$existingReady.ProcessId } else { 0 }
|
||||
Save-RuntimeState 'ready' 'FLUX / ComfyUI ist bereits aktiv und erreichbar.' $readyPid 0
|
||||
exit 0
|
||||
}
|
||||
|
||||
$existing = Get-ComfyProcess
|
||||
$comfyPid = 0
|
||||
$process = $null
|
||||
if ($existing) {
|
||||
$comfyPid = [int]$existing.ProcessId
|
||||
Save-RuntimeState 'starting' 'Ein vorhandener ComfyUI-Prozess initialisiert noch.' $comfyPid 0
|
||||
} else {
|
||||
Remove-Item -LiteralPath $StdoutLog,$StderrLog -Force -ErrorAction SilentlyContinue
|
||||
$performance = Get-PerformanceArguments $Profile
|
||||
$arguments = @(
|
||||
'-s', $Main,
|
||||
'--listen', '127.0.0.1',
|
||||
'--port', [string]$ComfyPort,
|
||||
'--disable-auto-launch',
|
||||
'--preview-method', 'none',
|
||||
'--windows-standalone-build',
|
||||
'--disable-all-custom-nodes',
|
||||
'--log-stdout',
|
||||
'--verbose', 'INFO'
|
||||
)
|
||||
foreach ($flag in $performance.flags) { $arguments += $flag }
|
||||
$process = Start-Process -FilePath $Python -ArgumentList $arguments -WorkingDirectory $PortableRoot -WindowStyle Hidden -RedirectStandardOutput $StdoutLog -RedirectStandardError $StderrLog -PassThru
|
||||
if (-not $process) { throw 'Der ComfyUI-Prozess konnte nicht gestartet werden.' }
|
||||
$comfyPid = [int]$process.Id
|
||||
Save-RuntimeState 'starting' ("FLUX / ComfyUI wird initialisiert · Profil: {0} · VRAM: {1} GB." -f $performance.profile, $performance.vram_gb) $comfyPid 0
|
||||
}
|
||||
|
||||
$startedAt = Get-Date
|
||||
while (((Get-Date) - $startedAt).TotalSeconds -lt $StartupTimeoutSeconds) {
|
||||
Start-Sleep -Seconds 2
|
||||
$elapsed = [int][math]::Round(((Get-Date) - $startedAt).TotalSeconds)
|
||||
|
||||
if (Test-ComfyReady) {
|
||||
Save-RuntimeState 'ready' "FLUX / ComfyUI ist nach ${elapsed}s erreichbar." $comfyPid $elapsed
|
||||
exit 0
|
||||
}
|
||||
|
||||
$alive = $false
|
||||
try { $alive = $null -ne (Get-Process -Id $comfyPid -ErrorAction SilentlyContinue) } catch {}
|
||||
if (-not $alive) {
|
||||
$stderrTail = (Read-Tail $StderrLog 24) -join "`n"
|
||||
$stdoutTail = (Read-Tail $StdoutLog 24) -join "`n"
|
||||
$detail = if ($stderrTail) { $stderrTail } elseif ($stdoutTail) { $stdoutTail } else { 'Der Prozess wurde ohne verwertbare Ausgabe beendet.' }
|
||||
Save-RuntimeState 'failed' 'ComfyUI wurde während des Starts beendet.' $comfyPid $elapsed $detail
|
||||
throw "ComfyUI wurde während des Starts beendet. $detail"
|
||||
}
|
||||
|
||||
$tail = @((Read-Tail $StdoutLog 3) + (Read-Tail $StderrLog 3)) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
$lastLine = if ($tail.Count -gt 0) { [string]$tail[-1] } else { 'ComfyUI initialisiert Python, Datenbank und Modelle.' }
|
||||
Save-RuntimeState 'starting' "Start läuft seit ${elapsed}s: $lastLine" $comfyPid $elapsed
|
||||
}
|
||||
|
||||
$timeoutDetail = @((Read-Tail $StderrLog 24) + (Read-Tail $StdoutLog 24)) -join "`n"
|
||||
Stop-Process -Id $comfyPid -Force -ErrorAction SilentlyContinue
|
||||
Save-RuntimeState 'failed' "ComfyUI war nach ${StartupTimeoutSeconds}s noch nicht erreichbar und wurde beendet." $comfyPid $StartupTimeoutSeconds $timeoutDetail
|
||||
throw "ComfyUI war nach ${StartupTimeoutSeconds}s noch nicht erreichbar und wurde beendet. Prüfe $StderrLog und $StdoutLog."
|
||||
} catch {
|
||||
$message = Remove-Ansi ([string]$_.Exception.Message)
|
||||
$currentPid = 0
|
||||
try { if (Test-Path -LiteralPath $PidFile) { $currentPid = [int](Get-Content -LiteralPath $PidFile -Raw).Trim() } } catch {}
|
||||
$elapsed = 0
|
||||
try {
|
||||
if (Test-Path -LiteralPath $StateFile) {
|
||||
$oldState = Get-Content -LiteralPath $StateFile -Raw | ConvertFrom-Json
|
||||
$elapsed = [int]$oldState.elapsed_seconds
|
||||
}
|
||||
} catch {}
|
||||
Save-RuntimeState 'failed' 'FLUX / ComfyUI konnte nicht gestartet werden.' $currentPid $elapsed $message
|
||||
Write-Error $message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$VendooDir = Split-Path -Parent $ScriptDir
|
||||
$LogDir = Join-Path $VendooDir 'logs'
|
||||
$StateFile = Join-Path $LogDir 'local-flux-runtime-state.json'
|
||||
$PidFile = Join-Path $LogDir 'local-flux-runtime.pid'
|
||||
|
||||
function Write-Utf8NoBom([string]$Path, [string]$Content) {
|
||||
$encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
$items = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.CommandLine -and $_.CommandLine -match 'ComfyUI[\\/]main\.py' -and $_.CommandLine -match '--port\s+8188'
|
||||
})
|
||||
foreach ($item in $items) { Stop-Process -Id $item.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$state = [ordered]@{
|
||||
status = 'stopped'
|
||||
message = 'FLUX / ComfyUI wurde beendet.'
|
||||
pid = $null
|
||||
port = 8188
|
||||
url = 'http://127.0.0.1:8188'
|
||||
elapsed_seconds = 0
|
||||
updated_at = (Get-Date).ToString('o')
|
||||
error = ''
|
||||
}
|
||||
Write-Utf8NoBom -Path $StateFile -Content ($state | ConvertTo-Json -Depth 4)
|
||||
exit 0
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
cd /d "%~dp0\.."
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0\..\local-ai\start-local-flux.ps1"
|
||||
pause
|
||||
@@ -0,0 +1,114 @@
|
||||
param(
|
||||
[switch]$NonInteractive,
|
||||
[switch]$RemoveDownloads
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$VendooDir = Split-Path -Parent $ScriptDir
|
||||
$LocalAiDir = Join-Path $VendooDir 'local-ai'
|
||||
$ComfyRoot = Join-Path $LocalAiDir 'comfyui'
|
||||
$ExtractTemp = Join-Path $LocalAiDir 'extract-temp'
|
||||
$Downloads = Join-Path $LocalAiDir 'downloads'
|
||||
$StateFile = Join-Path $LocalAiDir 'install-state.json'
|
||||
$StartScript = Join-Path $LocalAiDir 'start-local-flux.ps1'
|
||||
$StopScript = Join-Path $LocalAiDir 'stop-local-flux.ps1'
|
||||
$TaskName = 'Vendoo Local FLUX'
|
||||
$LogDir = Join-Path $VendooDir 'logs'
|
||||
$LogFile = Join-Path $LogDir 'local-flux-uninstall.log'
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $LocalAiDir | Out-Null
|
||||
|
||||
function Write-Log([string]$Message, [string]$Level = 'INFO') {
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
|
||||
Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
|
||||
function Write-Utf8NoBom([string]$Path, [string]$Content) {
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Log 'Deinstallation von lokalem FLUX wurde gestartet.'
|
||||
|
||||
if (Test-Path -LiteralPath $StopScript) {
|
||||
try {
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $StopScript | Out-Null
|
||||
Write-Log 'Vorhandenes Stoppskript wurde ausgeführt.' 'OK'
|
||||
} catch {
|
||||
Write-Log "Stoppskript meldete: $($_.Exception.Message)" 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$processes = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.CommandLine -match 'ComfyUI\\main.py' -or
|
||||
($_.CommandLine -match [regex]::Escape($VendooDir) -and $_.CommandLine -match 'python_embeded\\python.exe')
|
||||
})
|
||||
foreach ($process in $processes) {
|
||||
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($processes.Count -gt 0) { Write-Log "$($processes.Count) ComfyUI-Prozess(e) beendet." 'OK' }
|
||||
} catch {
|
||||
Write-Log "Prozessbereinigung meldete: $($_.Exception.Message)" 'WARN'
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
& schtasks.exe /Delete /TN $TaskName /F 2>$null | Out-Null
|
||||
Write-Log 'Autostart-Aufgabe wurde entfernt oder war nicht vorhanden.' 'OK'
|
||||
|
||||
if (Test-Path -LiteralPath $ComfyRoot) {
|
||||
Remove-Item -LiteralPath $ComfyRoot -Recurse -Force
|
||||
Write-Log 'ComfyUI und FLUX-Modell wurden entfernt.' 'OK'
|
||||
}
|
||||
if (Test-Path -LiteralPath $ExtractTemp) {
|
||||
Remove-Item -LiteralPath $ExtractTemp -Recurse -Force
|
||||
Write-Log 'Temporäre Entpackdaten wurden entfernt.' 'OK'
|
||||
}
|
||||
foreach ($path in @($StartScript, $StopScript)) {
|
||||
if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force }
|
||||
}
|
||||
if ($RemoveDownloads -and (Test-Path -LiteralPath $Downloads)) {
|
||||
Remove-Item -LiteralPath $Downloads -Recurse -Force
|
||||
Write-Log 'Installationsarchive und Teildownloads wurden entfernt.' 'OK'
|
||||
} else {
|
||||
Write-Log 'Installationsarchive bleiben für eine schnelle Neuinstallation erhalten.' 'INFO'
|
||||
}
|
||||
|
||||
$state = [ordered]@{
|
||||
status = 'not-started'
|
||||
phase = 'uninstalled'
|
||||
progress = 0
|
||||
message = 'Lokales FLUX wurde deinstalliert.'
|
||||
started_at = $null
|
||||
updated_at = (Get-Date).ToString('o')
|
||||
heartbeat_at = (Get-Date).ToString('o')
|
||||
process_id = $PID
|
||||
elapsed_seconds = 0
|
||||
can_resume = $false
|
||||
model_installed = $false
|
||||
comfy_installed = $false
|
||||
}
|
||||
Write-Utf8NoBom -Path $StateFile -Content ($state | ConvertTo-Json -Depth 4)
|
||||
Write-Log 'Deinstallation erfolgreich abgeschlossen.' 'OK'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Log $_.Exception.Message 'ERROR'
|
||||
try {
|
||||
$failed = [ordered]@{
|
||||
status = 'failed'
|
||||
phase = 'error'
|
||||
progress = 0
|
||||
message = "Deinstallation fehlgeschlagen: $($_.Exception.Message)"
|
||||
updated_at = (Get-Date).ToString('o')
|
||||
heartbeat_at = (Get-Date).ToString('o')
|
||||
process_id = $PID
|
||||
}
|
||||
Write-Utf8NoBom -Path $StateFile -Content ($failed | ConvertTo-Json -Depth 4)
|
||||
} catch {}
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user