$ErrorActionPreference = "Stop" function Invoke-RemotePowerShell { param( [Parameter(Mandatory = $true)][string]$SshTarget, [Parameter(Mandatory = $true)][string]$Script ) $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Script)) ssh $SshTarget "powershell -NoProfile -EncodedCommand $encoded" } function New-AppArchive { $archive = Join-Path $env:TEMP "llm-model-chat-app.zip" Remove-Item -Force $archive -ErrorAction SilentlyContinue Get-ChildItem -Path "scripts", "tools", "plugins" -Recurse -Directory -Filter "__pycache__" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } $items = @( "config", "scripts", "tools", "registry", "plugins", "docs", "evals", "datasets", "requirements.txt", "requirements-training.txt", "README.md" ) Compress-Archive -Path $items -DestinationPath $archive -Force Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::Open($archive, [System.IO.Compression.ZipArchiveMode]::Update) try { @($zip.Entries | Where-Object { $_.FullName.Replace("\", "/") -match "/__pycache__/" }) | ForEach-Object { $_.Delete() } } finally { $zip.Dispose() } $preflightReport = "reports/model-chat/preflight.json" if (Test-Path -LiteralPath $preflightReport) { $zip = [System.IO.Compression.ZipFile]::Open($archive, [System.IO.Compression.ZipArchiveMode]::Update) try { [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( $zip, (Get-Item -LiteralPath $preflightReport).FullName, "reports/model-chat/preflight.json" ) | Out-Null } finally { $zip.Dispose() } } return $archive } function Test-AppArchive { param([Parameter(Mandatory = $true)][string]$Archive) Add-Type -AssemblyName System.IO.Compression.FileSystem $requiredEntries = @( "config/gpu_profiles.json", "scripts/model_chat_server.py", "scripts/transformers_plugin_server.py", "scripts/common.py", "tools/model-chat/index.html", "registry/index.json", "plugins/1c/rag/profiles.yaml", "requirements.txt", "requirements-training.txt", "README.md" ) if (Test-Path -LiteralPath "reports/model-chat/preflight.json") { $requiredEntries += "reports/model-chat/preflight.json" } $zip = [System.IO.Compression.ZipFile]::OpenRead($Archive) try { $entryNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($entry in $zip.Entries) { [void]$entryNames.Add($entry.FullName.Replace("\", "/")) } $missing = @($requiredEntries | Where-Object { -not $entryNames.Contains($_) }) if ($missing.Count -gt 0) { throw "Archive is missing required entries: $($missing -join ', ')" } $pycacheEntries = @($entryNames | Where-Object { $_ -match "/__pycache__/" }) if ($pycacheEntries.Count -gt 0) { throw "Archive contains __pycache__ entries: $($pycacheEntries[0])" } Write-Host "Archive validation passed: $Archive" } finally { $zip.Dispose() } } function Sync-AppDirectory { param( [Parameter(Mandatory = $true)][string]$SshTarget, [Parameter(Mandatory = $true)][string]$RemoteArchive, [Parameter(Mandatory = $true)][string]$RemoteAppDir, [string]$Archive ) if (-not $Archive) { $Archive = New-AppArchive Test-AppArchive -Archive $Archive } ssh $SshTarget "cmd /c if not exist C:\ProgramData\LLM mkdir C:\ProgramData\LLM" $remoteArchiveForScp = $RemoteArchive.Replace("\", "/") scp $Archive "${SshTarget}:$remoteArchiveForScp" $unpackScript = @" New-Item -ItemType Directory -Force '$RemoteAppDir' | Out-Null Remove-Item -Recurse -Force '$RemoteAppDir\*' -ErrorAction SilentlyContinue Expand-Archive -Path '$RemoteArchive' -DestinationPath '$RemoteAppDir' -Force New-Item -ItemType Directory -Force '$RemoteAppDir\reports','$RemoteAppDir\models\incoming' | Out-Null "@ Invoke-RemotePowerShell -SshTarget $SshTarget -Script $unpackScript }