<#
.SYNOPSIS
Build per-component update ZIP packages and a remote version.json manifest.
.DESCRIPTION
This script packages each updatable component into a separate ZIP file and
generates a version.json manifest under output/v{Version}/ for hosting.
The manifest contains SHA256 checksums and file sizes for integrity verification.
Server-side directory structure (versioned folders for rollback support):
{OutputDir}/
setup/
v0.4.0/
MEC_Agent-Setup-v0.4.0.exe ← installer EXE (managed by build-release.ps1)
updates/
index.json ← version list (updated after each build)
v0.4.0/
manifest.json ← per-version manifest (ZIPs referenced by URL)
api_server-0.1.2.zip ← only changed components
mecui-0.1.5.zip
v0.3.1/
manifest.json
mec_agent-0.3.1.zip
...
models/ ← model weight files (managed separately)
index.json
gemma-4-E4B-it-Q4_K_M.gguf
All components (incl. python_embed, llama_cpp, downloads) use the app version
number for tracking. Actual binary versions are stored in the metadata field.
Model weight files (.gguf) are managed separately in models/ — not packaged here.
Automatically packaged (9 lightweight components):
mec_agent, api_server, whisper, mecui, skills, launcher, config, proxy, ein_wiki
Large binary components (packaged when their source directories exist):
python_embed — agent_packaging/python-embed/ (built by build-installer.ps1)
llama_cpp — agent_packaging/llama/ (llama-b????-bin-win-*.zip files)
whl — {repo}/whl/ (offline Python wheel cache)
downloads — agent_packaging/downloads/ (raw Python embed ZIP)
Model weight files (.gguf) are managed separately — use build-model-index.ps1.
.PARAMETER OutputDir
Root directory for all versioned packages. Default: installer/output
Each build creates a v{Version}/ subdirectory inside.
The index.json is written to this root directory.
.PARAMETER BaseUrl
Base URL where the packages will be hosted. Used to generate download URLs in the manifest.
Example: http://update-server/packages
ZIPs will be served at {BaseUrl}/v{Version}/{component}-{version}.zip
.PARAMETER Version
Version string for this release. Default: read from NanobotSetup.iss
.PARAMETER ReleaseNotes
Release notes text to include in the manifest and index.json.
.PARAMETER PythonEmbedMetaVersion
Actual Python interpreter version for metadata (e.g. "3.12.10").
Defaults to auto-detected from python.exe. Used only as metadata — the
update tracking version is always the app version.
.PARAMETER LlamaCppMetaVersion
Actual llama.cpp build tag for metadata (e.g. "b9240").
Defaults to auto-detected from ZIP filename. Used only as metadata.
.PARAMETER Components
Optional list of component names to package. When specified, only those components
are packaged and the versioned output directory is NOT cleaned (preserving existing ZIPs).
When omitted, all components are packaged.
Valid names: mec_agent, api_server, whisper, mecui, skills, launcher, config, proxy,
ein_wiki, python_embed, downloads, llama_cpp, whl
(model weight files are managed separately via build-model-index.ps1)
.EXAMPLE
# Package all components (standalone, without installer EXE)
.\build-update-packages.ps1 -BaseUrl "http://172.18.212.49/mec-updates" -ReleaseNotes "Bug fixes"
.EXAMPLE
# Package only mecui (fast, incremental — other ZIPs in v{Ver}/ are preserved)
.\build-update-packages.ps1 -BaseUrl "http://172.18.212.49/mec-updates" -Components mecui
.EXAMPLE
# Full release (EXE + update packages in same versioned folder) — preferred workflow
.\build-release.ps1 -BaseUrl "http://172.18.212.49/mec-updates" -ReleaseNotes "Bug fixes"
#>
param(
[string]$OutputDir = (Join-Path $PSScriptRoot "output"),
[string]$BaseUrl = "http://update-server/packages",
[string]$Version = "",
[string]$ReleaseNotes = "",
[string]$PythonEmbedMetaVersion = "", # actual Python version for metadata only
[string]$LlamaCppMetaVersion = "", # actual llama build tag for metadata only
[string[]]$Components = @() # empty = all; specify names to package only those
)
$ErrorActionPreference = "Stop"
$AgentPackagingRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
# Load .NET ZIP support (no 2 GB per-file limit, unlike Compress-Archive in PS 5.1)
Add-Type -AssemblyName System.IO.Compression.FileSystem
# Determine version from .iss if not specified
if (-not $Version) {
$issFile = Join-Path $PSScriptRoot "NanobotSetup.iss"
if (Test-Path $issFile) {
$issContent = Get-Content $issFile -Raw -Encoding UTF8
if ($issContent -match '#define\s+MyAppVersion\s+"([^"]+)"') {
$Version = $Matches[1]
}
}
if (-not $Version) { $Version = "0.0.0" }
}
# ── 載入各模組的獨立版號(來自 version.json)────────────────────────────────
# 若 version.json 存在,優先使用各模組的 components[name].version;
# 找不到的模組才 fall back 到應用程式版號 $Version。
$script:moduleVersions = @{}
$_vjPath = Join-Path $AgentPackagingRoot "update\version.json"
if (Test-Path $_vjPath) {
try {
$_vj = Get-Content $_vjPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($_prop in $_vj.components.PSObject.Properties) {
if ($_prop.Value.PSObject.Properties["version"]) {
$script:moduleVersions[$_prop.Name] = $_prop.Value.version
}
}
Write-Host "Loaded module versions from version.json ($($script:moduleVersions.Count) components)"
} catch {
Write-Warning "Cannot read version.json module versions: $_"
}
}
# Server layout separates setup (full installer) from updates (delta modules):
# {OutputDir}/setup/v{ver}/ ← EXE managed by build-release.ps1
# {OutputDir}/updates/v{ver}/ ← update ZIPs + manifest (this script)
# {OutputDir}/updates/index.json
$VersionSlug = "v$Version"
$UpdatesRoot = Join-Path $OutputDir "updates"
$VersionedOutputDir = Join-Path $UpdatesRoot $VersionSlug
Write-Host "Building update packages for v$Version"
Write-Host "Output: $VersionedOutputDir"
Write-Host "Base URL: $BaseUrl/updates/$VersionSlug"
if ($Components.Count -gt 0) {
Write-Host "Components filter: $($Components -join ', ')"
}
# ── Auto-detect changed components when -Components is not specified ────────────
# Compare version.json module versions against the most recent published manifest.
# Only package modules whose version has changed.
# Falls back to packaging ALL when no previous manifest is found (first release).
if ($Components.Count -eq 0) {
$prevManifestObj = $null
$idxPath = Join-Path $UpdatesRoot "index.json"
if (Test-Path $idxPath) {
try {
$idx = Get-Content $idxPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($entry in @($idx.versions)) {
if ("$($entry.version)" -eq "$Version") { continue } # skip current version
$prevSlug = "v$($entry.version)"
$prevMPath = Join-Path $UpdatesRoot "$prevSlug\manifest.json"
if (Test-Path $prevMPath) {
$candidate = Get-Content $prevMPath -Raw -Encoding UTF8 | ConvertFrom-Json
if (($candidate.components.PSObject.Properties | Measure-Object).Count -gt 0) {
$prevManifestObj = $candidate
Write-Host "Auto-detect: comparing against v$($entry.version) manifest"
break
}
}
}
} catch {
Write-Warning "Auto-detect: failed to read index.json — will package all components"
}
}
if ($null -eq $prevManifestObj) {
Write-Host "Auto-detect: no previous manifest found — packaging ALL components (first release)"
# $Components stays empty → package all
} else {
$autoList = [System.Collections.Generic.List[string]]::new()
# Modules whose version changed relative to previous manifest
foreach ($prop in $prevManifestObj.components.PSObject.Properties) {
$cName = $prop.Name
$prevVer = $prop.Value.version
$curVer = if ($script:moduleVersions.ContainsKey($cName)) { $script:moduleVersions[$cName] } else { $Version }
if ($curVer -ne $prevVer) { $autoList.Add($cName) }
}
# New modules not present in previous manifest
foreach ($cName in $script:moduleVersions.Keys) {
if (-not $prevManifestObj.components.PSObject.Properties[$cName] -and
-not $autoList.Contains($cName)) {
$autoList.Add($cName)
}
}
if ($autoList.Count -eq 0) {
Write-Host "Auto-detect: no version changes found — nothing to package" -ForegroundColor Yellow
Write-Host " (manifest will be rebuilt inheriting all components from previous version)"
# Keep $Components empty so the manifest is still regenerated (with inherited data)
} else {
$Components = $autoList.ToArray()
Write-Host "Auto-detect: changed components → $($Components -join ', ')" -ForegroundColor Cyan
}
}
}
# Clean the versioned subdirectory only when packaging all components.
# Partial builds (-Components) keep existing ZIPs so other components are preserved.
if ($Components.Count -eq 0) {
if (Test-Path $VersionedOutputDir) { Remove-Item $VersionedOutputDir -Recurse -Force }
}
New-Item -ItemType Directory -Path $VersionedOutputDir -Force | Out-Null
# Ensure updates root and root OutputDir exist (needed for index.json)
New-Item -ItemType Directory -Path $UpdatesRoot -Force | Out-Null
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
$manifest = @{
version = $Version
buildDate = (Get-Date -Format "yyyy-MM-dd")
minClientVersion = "0.3.0"
releaseNotes = $ReleaseNotes
components = @{}
}
# Returns $true if $name should be packaged given the -Components filter.
function Should-Package([string]$name) {
return ($Components.Count -eq 0 -or $Components -contains $name)
}
# Add-ComponentPackage: all components use the unified app $Version for update tracking.
# Optional $metadataFields hashtable is merged into the manifest entry (e.g. actualVersion).
function Add-ComponentPackage([string]$name, [string[]]$sourcePaths, [hashtable]$metadataFields = @{}) {
if (-not (Should-Package $name)) {
Write-Host " [$name] skipped (not in -Components filter)"
return
}
# Use the component-specific version from version.json; fall back to app version.
$componentVersion = if ($script:moduleVersions.ContainsKey($name)) { $script:moduleVersions[$name] } else { $Version }
$zipFile = Join-Path $VersionedOutputDir "$name-$componentVersion.zip"
$tempDir = Join-Path $env:TEMP "mec_update_pkg_$name"
if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$hasContent = $false
foreach ($src in $sourcePaths) {
if (-not (Test-Path $src)) {
Write-Warning "Source not found for $name`: $src — skipping"
continue
}
if ((Get-Item $src).PSIsContainer) {
Copy-Item -Path (Join-Path $src "*") -Destination $tempDir -Recurse -Force -ErrorAction SilentlyContinue
} else {
Copy-Item -Path $src -Destination $tempDir -Force
}
$hasContent = $true
}
if (-not $hasContent) {
Write-Warning "No content for component '$name' — skipping"
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
return
}
# Use ZipFile::CreateFromDirectory instead of Compress-Archive:
# Compress-Archive in PS 5.1 silently fails for files > 2 GB (e.g. large .gguf models).
if (Test-Path $zipFile) { Remove-Item $zipFile -Force }
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$tempDir, $zipFile,
[System.IO.Compression.CompressionLevel]::Optimal,
$false # includeBaseDirectory = false
)
Remove-Item $tempDir -Recurse -Force
if (-not (Test-Path $zipFile)) {
Write-Warning "ZIP not created for '$name' — skipping component"
return
}
$hash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash.ToLower()
$size = (Get-Item $zipFile).Length
# URL: {BaseUrl}/updates/{VersionSlug}/{name}-{ver}.zip
$url = "$BaseUrl/updates/$VersionSlug/$name-$componentVersion.zip"
$entry = @{
version = $componentVersion
sha256 = $hash
size = $size
url = $url
}
# Merge optional metadata fields (e.g. actualVersion for python_embed / llama_cpp)
foreach ($k in $metadataFields.Keys) { $entry[$k] = $metadataFields[$k] }
$manifest.components[$name] = $entry
Write-Host " [$name] v$componentVersion — $([math]::Round($size / 1MB, 2)) MB — $hash"
}
Write-Host ""
Write-Host "Packaging components..."
# 1. mec_agent.exe (source is nanobot.exe, rename to mec_agent.exe in package)
$nanobotExe = Join-Path $AgentPackagingRoot "nanobot_packaging\dist\nanobot.exe"
if (Test-Path $nanobotExe) {
$tmpMecAgent = Join-Path $env:TEMP "mec_agent_rename"
if (Test-Path $tmpMecAgent) { Remove-Item $tmpMecAgent -Recurse -Force }
New-Item -ItemType Directory -Path $tmpMecAgent -Force | Out-Null
Copy-Item $nanobotExe (Join-Path $tmpMecAgent "mec_agent.exe") -Force
Add-ComponentPackage "mec_agent" @((Join-Path $tmpMecAgent "mec_agent.exe"))
Remove-Item $tmpMecAgent -Recurse -Force
} else {
Write-Warning "nanobot.exe not found — skipping mec_agent component"
}
# 2. api_server — exe only; server.py / update_manager.py / integrity_monitor.py
# are compiled into api_server.exe and must NOT be distributed as visible source.
$apiServerExe = Join-Path $AgentPackagingRoot "api_server\api_server.exe"
Add-ComponentPackage "api_server" @($apiServerExe)
# 2b. whisper models (faster-whisper-tiny; model.bin included when present on build machine)
$whisperDir = Join-Path $AgentPackagingRoot "api_server\whisper"
Add-ComponentPackage "whisper" @($whisperDir)
# 3. mecui
$mecuiDir = Join-Path $AgentPackagingRoot "web\mecui"
if (-not (Test-Path $mecuiDir)) {
$mecuiDir = Join-Path $RepoRoot "nanobot\web\dist"
}
Add-ComponentPackage "mecui" @($mecuiDir)
# 4. skills
$skillsDir = Join-Path $AgentPackagingRoot ".github\skills"
Add-ComponentPackage "skills" @($skillsDir)
# 5. launcher scripts
$launcherFiles = @(
(Join-Path $AgentPackagingRoot "launcher\launch_mec_agent.ps1"),
(Join-Path $AgentPackagingRoot "launcher\launch_mec_agent.cmd"),
(Join-Path $AgentPackagingRoot "launcher\launch_mec_agent.vbs")
)
Add-ComponentPackage "launcher" $launcherFiles
# 6. config templates
$configFiles = @(
(Join-Path $AgentPackagingRoot "config\config.json"),
(Join-Path $AgentPackagingRoot "config\config_llama_cpp.json")
)
Add-ComponentPackage "config" $configFiles
# 7. proxy
$proxyDir = Join-Path $AgentPackagingRoot "proxy"
Add-ComponentPackage "proxy" @($proxyDir)
# 8. updater — mec_updater.ps1 is the primary updater (used by launcher for apply-staged).
$updaterFile = Join-Path $AgentPackagingRoot "update\mec_updater.ps1"
Add-ComponentPackage "updater" @($updaterFile)
# 9. ein_wiki
$einWikiDir = Join-Path $AgentPackagingRoot "bundled\ein-wiki"
if (Test-Path $einWikiDir) {
Add-ComponentPackage "ein_wiki" @($einWikiDir)
} else {
Write-Warning "bundled/ein-wiki/ not found — run build-installer.ps1 (Prepare-EinWiki) first to generate it"
}
# 10. python_embed
# Tracking version = app version (unified). Actual Python interpreter version
# stored in metadata.actualVersion for display purposes only.
$pythonEmbedDir = Join-Path $AgentPackagingRoot "python-embed"
if (Test-Path $pythonEmbedDir) {
$pyActualVer = $PythonEmbedMetaVersion
if (-not $pyActualVer) {
$pyExe = Join-Path $pythonEmbedDir "python.exe"
if (Test-Path $pyExe) {
try {
$pyActualVer = (& $pyExe -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')" 2>$null).Trim()
} catch { }
}
if (-not $pyActualVer) { $pyActualVer = "3.12.10" }
}
Add-ComponentPackage "python_embed" @($pythonEmbedDir) @{ actualVersion = $pyActualVer }
} else {
Write-Warning "python-embed/ not found — run build-installer.ps1 (without -SkipPythonEmbed) first to generate it"
}
# 11. downloads — raw Python embed ZIP (recovery fallback for launcher self-heal)
$downloadsDir = Join-Path $AgentPackagingRoot "downloads"
if (Test-Path $downloadsDir) {
$embedZips = @(Get-ChildItem -Path $downloadsDir -Filter "python-*-embed-amd64.zip" -ErrorAction SilentlyContinue)
if ($embedZips.Count -gt 0) {
# Detect actual Python version from ZIP filename for metadata
$dlActualVer = $PythonEmbedMetaVersion
if (-not $dlActualVer -and $embedZips[0].Name -match 'python-([\d.]+)-embed') {
$dlActualVer = $Matches[1]
}
if (-not $dlActualVer) { $dlActualVer = if ($pyActualVer) { $pyActualVer } else { "3.12.10" } }
# Pass only the python embed ZIP file(s) — exclude build-only artifacts
Add-ComponentPackage "downloads" @($embedZips.FullName) @{ actualVersion = $dlActualVer }
} else {
Write-Warning "downloads/ found but no python-*-embed-amd64.zip — skipping downloads component"
}
} else {
Write-Warning "downloads/ not found — place python-*-embed-amd64.zip in agent_packaging/downloads/ first"
}
# 12. llama_cpp
# Tracking version = app version (unified). Actual llama.cpp build stored in metadata.
$llamaDir = Join-Path $AgentPackagingRoot "llama"
if (Test-Path $llamaDir) {
$llamaActualVer = $LlamaCppMetaVersion
if (-not $llamaActualVer) {
$llamaZip = Get-ChildItem -Path $llamaDir -Filter "llama-b*-bin-win-*.zip" |
Sort-Object Name -Descending | Select-Object -First 1
if ($llamaZip -and $llamaZip.Name -match 'llama-(b\d+)-bin-win') {
$llamaActualVer = $Matches[1]
}
}
if (-not $llamaActualVer) { $llamaActualVer = "b0000" }
Add-ComponentPackage "llama_cpp" @($llamaDir) @{ actualVersion = $llamaActualVer }
} else {
Write-Warning "llama/ not found — place llama-b????-bin-win-*.zip files in agent_packaging/llama/ first"
}
# 13. whl
$whlDir = Join-Path $RepoRoot "whl"
if (Test-Path $whlDir) {
Add-ComponentPackage "whl" @($whlDir)
} else {
Write-Warning "whl/ not found at $whlDir"
}
# NOTE: model weight files (.gguf) are managed separately in models/ on the server.
# Use build-model-index.ps1 to update models/index.json after uploading a new .gguf.
# ── Partial build: inherit unpackaged components from the previous manifest ───
# The spec requires manifest.json to declare ALL components for the version,
# with unchanged components pointing to the URL of the last version that changed them.
if ($Components.Count -gt 0) {
$prevManifestObj = $null
$prevManifestLabel = ""
# Priority 1: existing manifest for THIS version (same-version patch / rebuild).
# The versioned output dir is NOT cleaned during partial builds, so the file survives.
$sameVersionManifestPath = Join-Path $VersionedOutputDir "manifest.json"
if (Test-Path $sameVersionManifestPath) {
try {
$candidate = Get-Content $sameVersionManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
# Only use it if it contains components BEYOND what we are currently packaging.
# If it only has the same components we're building now, it was itself a partial build
# and cannot serve as a reliable source for the other components.
$otherCount = @($candidate.components.PSObject.Properties |
Where-Object { $Components -notcontains $_.Name }).Count
if ($otherCount -gt 0) {
$prevManifestObj = $candidate
$prevManifestLabel = "existing v$Version"
}
} catch { }
}
# Priority 2: latest previous version from updates/index.json.
# Iterate in order (newest first) until we find one whose manifest exists locally.
if (-not $prevManifestObj) {
$indexFile = Join-Path $UpdatesRoot "index.json"
if (Test-Path $indexFile) {
try {
$idx = Get-Content $indexFile -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($prevEntry in @($idx.versions)) {
if ("$($prevEntry.version)" -eq "$Version") { continue }
$prevVersionSlug = "v$($prevEntry.version)"
$prevManifestPath = Join-Path $UpdatesRoot "$prevVersionSlug\manifest.json"
if (Test-Path $prevManifestPath) {
$candidate = Get-Content $prevManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
# Skip if this manifest is also a partial (no extra components to inherit)
$otherCount = @($candidate.components.PSObject.Properties |
Where-Object { $Components -notcontains $_.Name }).Count
if ($otherCount -gt 0) {
$prevManifestObj = $candidate
$prevManifestLabel = "v$($prevEntry.version)"
break
}
}
}
} catch { }
}
}
if ($prevManifestObj) {
Write-Host ""
Write-Host "Inheriting unpackaged components from $prevManifestLabel ..."
foreach ($prop in $prevManifestObj.components.PSObject.Properties) {
if (-not $manifest.components.ContainsKey($prop.Name)) {
$pe = $prop.Value
$entry = @{ version = $pe.version; sha256 = $pe.sha256; size = $pe.size; url = $pe.url }
# Preserve optional metadata fields (e.g. actualVersion)
if ($pe.PSObject.Properties['actualVersion']) { $entry['actualVersion'] = $pe.actualVersion }
$manifest.components[$prop.Name] = $entry
Write-Host " [$($prop.Name)] v$($pe.version) — inherited from $prevManifestLabel"
}
}
} else {
Write-Warning ""
Write-Warning "No previous manifest found to inherit from."
Write-Warning "Partial build manifest will only contain: $($Components -join ', ')"
Write-Warning "This does NOT comply with the manifest spec (all components must be declared)."
Write-Warning "Run a full build first, then re-run with -Components for subsequent patches."
}
}
# ── Write per-version manifest (BOM-free UTF-8) ────────────────────────────────
$manifestPath = Join-Path $VersionedOutputDir "manifest.json"
$manifestJson = $manifest | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($manifestPath, $manifestJson, (New-Object System.Text.UTF8Encoding $false))
# ── Update updates/index.json (version history list) ──────────────────────────
# index.json lives in updates/ alongside all versioned subdirectories.
$indexPath = Join-Path $UpdatesRoot "index.json"
$index = @{ latest = $Version; versions = @() }
if (Test-Path $indexPath) {
try {
$existing = Get-Content $indexPath -Raw -Encoding UTF8 | ConvertFrom-Json
$index.versions = @($existing.versions | Where-Object { $_.version -ne $Version })
} catch { }
}
# Prepend current version at the top
$newEntry = @{
version = $Version
buildDate = (Get-Date -Format "yyyy-MM-dd")
releaseNotes = $ReleaseNotes
manifestUrl = "$BaseUrl/updates/$VersionSlug/manifest.json"
}
$index.versions = @($newEntry) + @($index.versions)
$indexJson = $index | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($indexPath, $indexJson, (New-Object System.Text.UTF8Encoding $false))
# ── Copy CHANGELOG.md to versioned output directory ──────────────────────
# The update server hosts CHANGELOG.md alongside manifest.json so that
# update_manager.py can download it after applying component updates.
$_changelogSrc = Join-Path $AgentPackagingRoot "update\CHANGELOG.md"
if (Test-Path $_changelogSrc) {
Copy-Item $_changelogSrc (Join-Path $VersionedOutputDir "CHANGELOG.md") -Force
Write-Host "Copied CHANGELOG.md to output directory"
} else {
Write-Warning "update\CHANGELOG.md not found — clients will not be able to download it via auto-update"
}
Write-Host ""
Write-Host "Versioned manifest : $manifestPath"
Write-Host "Version index : $indexPath"
if ($Components.Count -gt 0) {
Write-Host "NOTE: Partial build — packaged: $($Components -join ', ')"
Write-Host " All components declared in manifest: $($manifest.components.Keys -join ', ')"
} else {
Write-Host "Upload to your update server:"
Write-Host " $VersionedOutputDir → $BaseUrl/updates/$VersionSlug/"
Write-Host " $indexPath → $BaseUrl/updates/index.json"
}
Write-Host ""
Write-Host "Done. $($manifest.components.Count) component(s) packaged for v$Version."
Comments...
No Comments Yet...