# Vendor-side tool. Builds a signed offline update package that customers can # apply from a USB drive or local folder without any internet access. # # The package layout is: # \ # update-manifest.json signed manifest (product, version, file hashes) # update-manifest.json.sig RSA SHA-256 signature # payload\ the actual replacement files # # Example: # .\Create-UpdatePackage.ps1 -Version "v16" -Files "bin\jackailocald.exe","webui\app-v15.js" -OutputDir "E:\update-v16" [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$Version, [Parameter(Mandatory=$true)][string[]]$Files, [Parameter(Mandatory=$true)][string]$OutputDir, [string]$SourceRoot = "", [string]$Channel = "stable", [string]$PrivateKeyXmlPath = "" ) $ErrorActionPreference = "Stop" $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path if (-not $SourceRoot) { $SourceRoot = (Resolve-Path (Join-Path $scriptRoot "..")).Path } $SourceRoot = (Resolve-Path $SourceRoot).Path New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $payloadDir = Join-Path $OutputDir "payload" New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null $entries = @() foreach ($relative in $Files) { $source = Join-Path $SourceRoot $relative if (!(Test-Path $source -PathType Leaf)) { throw "Update file not found in source root: $source" } $dest = Join-Path $payloadDir $relative New-Item -ItemType Directory -Force -Path (Split-Path $dest) | Out-Null Copy-Item $source $dest -Force $hash = (Get-FileHash -Algorithm SHA256 $dest).Hash.ToLowerInvariant() $entries += [pscustomobject]@{ path = ($relative -replace "\\", "/") sha256 = $hash } } $manifest = [pscustomobject]@{ product = "JackAILocal" version = $Version channel = $Channel created = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") files = $entries } $manifestPath = Join-Path $OutputDir "update-manifest.json" $manifest | ConvertTo-Json -Depth 5 | Set-Content -Encoding utf8 $manifestPath $signArgs = @{ ManifestPath = $manifestPath } if ($PrivateKeyXmlPath) { $signArgs.PrivateKeyXmlPath = $PrivateKeyXmlPath } & (Join-Path $scriptRoot "Sign-Manifest.ps1") @signArgs Write-Host "" Write-Host "Offline update package created: $OutputDir" -ForegroundColor Green Write-Host "Ship the whole folder (manifest + .sig + payload) to the customer." Write-Host "The customer applies it with: updates\JackAILocal-Update-Offline.ps1 -PackagePath -Apply"