param( [Parameter(Mandatory = $false)] [string]$OutputDir = "dist/wireframe-system", [Parameter(Mandatory = $false)] [string]$ManifestPath = "wireframe-system.manifest.json" ) $ErrorActionPreference = "Stop" function Get-RepoRoot { return (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) } function Normalize-RelativePath { param([string]$Path) $value = $Path -replace "\\", "/" $value = $value.Trim() while ($value.StartsWith("./")) { $value = $value.Substring(2) } $value = $value.TrimStart("/") return $value.TrimEnd("/") } function Test-PathMatchesPrefix { param( [string]$RelativePath, [string]$Prefix ) $pathValue = Normalize-RelativePath -Path $RelativePath $prefixValue = Normalize-RelativePath -Path $Prefix if ([string]::IsNullOrWhiteSpace($prefixValue)) { return $false } return $pathValue -eq $prefixValue -or $pathValue.StartsWith("$prefixValue/") } function Test-IsExcluded { param( [string]$RelativePath, [string[]]$ExcludedPaths ) foreach ($excluded in $ExcludedPaths) { if (Test-PathMatchesPrefix -RelativePath $RelativePath -Prefix $excluded) { return $true } } return $false } function Get-RelativePath { param( [string]$Root, [string]$Path ) $rootPath = (Resolve-Path -LiteralPath $Root).Path if (-not $rootPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $rootPath += [System.IO.Path]::DirectorySeparatorChar } $pathValue = (Resolve-Path -LiteralPath $Path).Path $rootUri = New-Object System.Uri($rootPath) $pathUri = New-Object System.Uri($pathValue) $relativeUri = $rootUri.MakeRelativeUri($pathUri) return [System.Uri]::UnescapeDataString($relativeUri.ToString()).Replace("/", "\") } function New-Directory { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Force -Path $Path | Out-Null } } $root = Get-RepoRoot $manifestFullPath = Join-Path $root $ManifestPath if (-not (Test-Path -LiteralPath $manifestFullPath)) { throw "System manifest not found: $manifestFullPath" } $manifest = Get-Content -LiteralPath $manifestFullPath -Raw -Encoding UTF8 | ConvertFrom-Json $packageName = [string]$manifest.package_name $version = [string]$manifest.version if ([string]::IsNullOrWhiteSpace($packageName) -or [string]::IsNullOrWhiteSpace($version)) { throw "System manifest must include package_name and version." } $excludedPaths = @($manifest.excluded_paths | ForEach-Object { [string]$_ }) $outputRoot = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path $root $OutputDir } New-Directory -Path $outputRoot $timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd-HHmmss-fff") $packageRoot = Join-Path $outputRoot "$packageName-$version-$timestamp" New-Directory -Path $packageRoot foreach ($relativePath in @($manifest.system_paths)) { $relative = Normalize-RelativePath -Path ([string]$relativePath) if (Test-IsExcluded -RelativePath $relative -ExcludedPaths $excludedPaths) { throw "System path is excluded and cannot be packaged: $relative" } $source = Join-Path $root $relative if (-not (Test-Path -LiteralPath $source)) { throw "System path listed in manifest does not exist: $relative" } $destination = Join-Path $packageRoot $relative $destinationParent = Split-Path -Parent $destination if (-not [string]::IsNullOrWhiteSpace($destinationParent)) { New-Directory -Path $destinationParent } Copy-Item -LiteralPath $source -Destination $destination -Recurse -Force } $files = New-Object System.Collections.Generic.List[object] Get-ChildItem -LiteralPath $packageRoot -Recurse -File -Force | Sort-Object FullName | ForEach-Object { $relative = (Get-RelativePath -Root $packageRoot -Path $_.FullName) -replace "\\", "/" $hash = Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 $files.Add([ordered]@{ path = $relative sha256 = $hash.Hash.ToLowerInvariant() length = $_.Length }) } $packageManifest = [ordered]@{ package_name = $packageName version = $version exported_at = (Get-Date).ToUniversalTime().ToString("o") source_manifest = $ManifestPath default_artifact_dir = [string]$manifest.default_artifact_dir system_paths = @($manifest.system_paths) preserve_paths = @($manifest.preserve_paths) excluded_paths = @($manifest.excluded_paths) files = $files } $packageManifestPath = Join-Path $packageRoot "package-manifest.json" $packageManifest | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $packageManifestPath -Encoding UTF8 Write-Output "Exported $($files.Count) system file(s)." Write-Output $packageRoot