|
| 1 | +[CmdletBinding()] |
| 2 | +param( |
| 3 | + [string]$RepositoryRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)), |
| 4 | + [string]$ManifestPath = (Join-Path $PSScriptRoot 'Sync-AgenticCopilotScaffold.psd1'), |
| 5 | + [string]$OutputRoot |
| 6 | +) |
| 7 | + |
| 8 | +Set-StrictMode -Version Latest |
| 9 | +$ErrorActionPreference = 'Stop' |
| 10 | +$ProgressPreference = 'SilentlyContinue' |
| 11 | + |
| 12 | +function ConvertTo-AgenticNormalizedRelativePath { |
| 13 | + param([Parameter(Mandatory)][string]$Path) |
| 14 | + |
| 15 | + $normalizedPath = $Path.Replace('\', '/') |
| 16 | + if ( $normalizedPath.StartsWith('./', [System.StringComparison]::Ordinal)) { |
| 17 | + return $normalizedPath.Substring(2) |
| 18 | + } |
| 19 | + |
| 20 | + return $normalizedPath.TrimStart('/') |
| 21 | +} |
| 22 | + |
| 23 | +function Get-AgenticRelativePath { |
| 24 | + param( |
| 25 | + [Parameter(Mandatory)][string]$RootPath, |
| 26 | + [Parameter(Mandatory)][string]$Path |
| 27 | + ) |
| 28 | + |
| 29 | + $relativePath = [System.IO.Path]::GetRelativePath( |
| 30 | + [System.IO.Path]::GetFullPath($RootPath), |
| 31 | + [System.IO.Path]::GetFullPath($Path) |
| 32 | + ) |
| 33 | + |
| 34 | + return ConvertTo-AgenticNormalizedRelativePath -Path $relativePath |
| 35 | +} |
| 36 | + |
| 37 | +function Resolve-AgenticRepositoryPath { |
| 38 | + param( |
| 39 | + [Parameter(Mandatory)][string]$RootPath, |
| 40 | + [Parameter(Mandatory)][string]$RelativePath |
| 41 | + ) |
| 42 | + |
| 43 | + return [System.IO.Path]::GetFullPath((Join-Path $RootPath $RelativePath)) |
| 44 | +} |
| 45 | + |
| 46 | +function Test-AgenticPathExcluded { |
| 47 | + param( |
| 48 | + [Parameter(Mandatory)][string]$RelativePath, |
| 49 | + [Parameter(Mandatory)][string[]]$ExcludedPathList |
| 50 | + ) |
| 51 | + |
| 52 | + $normalizedRelativePath = ConvertTo-AgenticNormalizedRelativePath -Path $RelativePath |
| 53 | + foreach ($excludedPath in $ExcludedPathList) { |
| 54 | + $normalizedExcludedPath = ConvertTo-AgenticNormalizedRelativePath -Path $excludedPath |
| 55 | + if ( $normalizedExcludedPath.EndsWith('/', [System.StringComparison]::Ordinal)) { |
| 56 | + if ( $normalizedRelativePath.StartsWith($normalizedExcludedPath, [System.StringComparison]::Ordinal)) { |
| 57 | + return $true |
| 58 | + } |
| 59 | + } |
| 60 | + elseif ($normalizedRelativePath -eq $normalizedExcludedPath) { |
| 61 | + return $true |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + return $false |
| 66 | +} |
| 67 | + |
| 68 | +function Get-AgenticMirrorSourceFile { |
| 69 | + param( |
| 70 | + [Parameter(Mandatory)][string]$RootPath, |
| 71 | + [Parameter(Mandatory)][string[]]$SourcePathList, |
| 72 | + [Parameter(Mandatory)][string[]]$ExcludedPathList |
| 73 | + ) |
| 74 | + |
| 75 | + $sourceFileList = foreach ($sourcePath in $SourcePathList) { |
| 76 | + $resolvedSourcePath = Resolve-AgenticRepositoryPath -RootPath $RootPath -RelativePath $sourcePath |
| 77 | + if (-not (Test-Path -LiteralPath $resolvedSourcePath)) { |
| 78 | + throw "Agentic scaffold source path does not exist: $sourcePath" |
| 79 | + } |
| 80 | + |
| 81 | + if (Test-Path -LiteralPath $resolvedSourcePath -PathType Leaf) { |
| 82 | + Get-Item -LiteralPath $resolvedSourcePath |
| 83 | + } |
| 84 | + else { |
| 85 | + Get-ChildItem -LiteralPath $resolvedSourcePath -File -Recurse -Force |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + foreach ($sourceFile in $sourceFileList) { |
| 90 | + $relativePath = Get-AgenticRelativePath -RootPath $RootPath -Path $sourceFile.FullName |
| 91 | + if (-not (Test-AgenticPathExcluded -RelativePath $relativePath -ExcludedPathList $ExcludedPathList)) { |
| 92 | + [pscustomobject]@{ |
| 93 | + SourcePath = $sourceFile.FullName |
| 94 | + RelativePath = $relativePath |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +function ConvertTo-AgenticScaffoldContent { |
| 101 | + param( |
| 102 | + [Parameter(Mandatory)][string]$Content, |
| 103 | + [Parameter(Mandatory)][object[]]$ReplacementList |
| 104 | + ) |
| 105 | + |
| 106 | + $updatedContent = $Content |
| 107 | + foreach ($replacement in $ReplacementList) { |
| 108 | + $updatedContent = $updatedContent.Replace([string]$replacement.Old, [string]$replacement.New) |
| 109 | + } |
| 110 | + |
| 111 | + while ( $updatedContent.Contains("`n`n`n")) { |
| 112 | + $updatedContent = $updatedContent.Replace("`n`n`n", "`n`n") |
| 113 | + } |
| 114 | + |
| 115 | + return $updatedContent |
| 116 | +} |
| 117 | + |
| 118 | +function Copy-AgenticScaffoldOwnedPath { |
| 119 | + param( |
| 120 | + [Parameter(Mandatory)][string]$SourceRoot, |
| 121 | + [Parameter(Mandatory)][string]$TargetRoot, |
| 122 | + [Parameter(Mandatory)][string]$RelativePath |
| 123 | + ) |
| 124 | + |
| 125 | + $sourcePath = Resolve-AgenticRepositoryPath -RootPath $SourceRoot -RelativePath $RelativePath |
| 126 | + if (-not (Test-Path -LiteralPath $sourcePath)) { |
| 127 | + throw "Agentic scaffold-owned path does not exist: $RelativePath" |
| 128 | + } |
| 129 | + |
| 130 | + $targetPath = Join-Path $TargetRoot ($RelativePath -replace '/', [System.IO.Path]::DirectorySeparatorChar) |
| 131 | + $targetDirectory = Split-Path -Parent $targetPath |
| 132 | + if (-not (Test-Path -LiteralPath $targetDirectory)) { |
| 133 | + New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null |
| 134 | + } |
| 135 | + |
| 136 | + Copy-Item -LiteralPath $sourcePath -Destination $targetPath -Recurse -Force |
| 137 | +} |
| 138 | + |
| 139 | +function Write-AgenticMirroredFile { |
| 140 | + param( |
| 141 | + [Parameter(Mandatory)][object]$SourceFile, |
| 142 | + [Parameter(Mandatory)][string]$TargetRoot, |
| 143 | + [Parameter(Mandatory)][object[]]$ReplacementList |
| 144 | + ) |
| 145 | + |
| 146 | + $targetPath = Join-Path $TargetRoot ($SourceFile.RelativePath -replace '/', [System.IO.Path]::DirectorySeparatorChar) |
| 147 | + $targetDirectory = Split-Path -Parent $targetPath |
| 148 | + if (-not (Test-Path -LiteralPath $targetDirectory)) { |
| 149 | + New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null |
| 150 | + } |
| 151 | + |
| 152 | + $sourceContent = Get-Content -LiteralPath $SourceFile.SourcePath -Raw |
| 153 | + $targetContent = ConvertTo-AgenticScaffoldContent -Content $sourceContent -ReplacementList $ReplacementList |
| 154 | + Set-Content -LiteralPath $targetPath -Value $targetContent -Encoding utf8 -NoNewline |
| 155 | +} |
| 156 | + |
| 157 | +function Assert-AgenticOutputRootSafe { |
| 158 | + param( |
| 159 | + [Parameter(Mandatory)][string]$RootPath, |
| 160 | + [Parameter(Mandatory)][string]$TargetPath |
| 161 | + ) |
| 162 | + |
| 163 | + $resolvedRootPath = [System.IO.Path]::GetFullPath($RootPath).TrimEnd([System.IO.Path]::DirectorySeparatorChar) |
| 164 | + $resolvedTargetPath = [System.IO.Path]::GetFullPath($TargetPath).TrimEnd([System.IO.Path]::DirectorySeparatorChar) |
| 165 | + if ($resolvedTargetPath -eq $resolvedRootPath) { |
| 166 | + throw 'Agentic scaffold output root cannot be the repository root.' |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +function Invoke-AgenticCopilotScaffoldSync { |
| 171 | + param( |
| 172 | + [Parameter(Mandatory)][string]$RootPath, |
| 173 | + [Parameter(Mandatory)][string]$ConfigPath, |
| 174 | + [Parameter(Mandatory)][string]$TargetRoot |
| 175 | + ) |
| 176 | + |
| 177 | + $manifest = Import-PowerShellDataFile -LiteralPath $ConfigPath |
| 178 | + $sourceRoot = Resolve-AgenticRepositoryPath -RootPath $RootPath -RelativePath $manifest.OutputPath |
| 179 | + $sourceFileList = @(Get-AgenticMirrorSourceFile -RootPath $RootPath -SourcePathList $manifest.SourcePaths -ExcludedPathList $manifest.ExcludedPaths | Sort-Object RelativePath) |
| 180 | + $stagingParent = Join-Path ([System.IO.Path]::GetTempPath()) "NovaAgenticCopilotScaffold-$([guid]::NewGuid() )" |
| 181 | + $stagingRoot = Join-Path $stagingParent 'agentic-copilot' |
| 182 | + |
| 183 | + try { |
| 184 | + New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null |
| 185 | + foreach ($ownedPath in $manifest.ScaffoldOwnedPaths) { |
| 186 | + Copy-AgenticScaffoldOwnedPath -SourceRoot $sourceRoot -TargetRoot $stagingRoot -RelativePath $ownedPath |
| 187 | + } |
| 188 | + |
| 189 | + foreach ($sourceFile in $sourceFileList) { |
| 190 | + Write-AgenticMirroredFile -SourceFile $sourceFile -TargetRoot $stagingRoot -ReplacementList $manifest.TextReplacements |
| 191 | + } |
| 192 | + |
| 193 | + if (Test-Path -LiteralPath $TargetRoot) { |
| 194 | + Remove-Item -LiteralPath $TargetRoot -Recurse -Force |
| 195 | + } |
| 196 | + |
| 197 | + $targetParent = Split-Path -Parent $TargetRoot |
| 198 | + if (-not (Test-Path -LiteralPath $targetParent)) { |
| 199 | + New-Item -ItemType Directory -Path $targetParent -Force | Out-Null |
| 200 | + } |
| 201 | + |
| 202 | + Move-Item -LiteralPath $stagingRoot -Destination $TargetRoot |
| 203 | + |
| 204 | + return [pscustomobject]@{ |
| 205 | + OutputRoot = $TargetRoot |
| 206 | + MirroredFileCount = $sourceFileList.Count |
| 207 | + ScaffoldOwnedPathCount = @($manifest.ScaffoldOwnedPaths).Count |
| 208 | + } |
| 209 | + } |
| 210 | + finally { |
| 211 | + if (Test-Path -LiteralPath $stagingParent) { |
| 212 | + Remove-Item -LiteralPath $stagingParent -Recurse -Force |
| 213 | + } |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +$resolvedRepositoryRoot = [System.IO.Path]::GetFullPath($RepositoryRoot) |
| 218 | +$resolvedManifestPath = [System.IO.Path]::GetFullPath($ManifestPath) |
| 219 | +$manifestForOutput = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath |
| 220 | +if ( [string]::IsNullOrWhiteSpace($OutputRoot)) { |
| 221 | + $OutputRoot = Resolve-AgenticRepositoryPath -RootPath $resolvedRepositoryRoot -RelativePath $manifestForOutput.OutputPath |
| 222 | +} |
| 223 | + |
| 224 | +$resolvedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot) |
| 225 | +Assert-AgenticOutputRootSafe -RootPath $resolvedRepositoryRoot -TargetPath $resolvedOutputRoot |
| 226 | +Invoke-AgenticCopilotScaffoldSync -RootPath $resolvedRepositoryRoot -ConfigPath $resolvedManifestPath -TargetRoot $resolvedOutputRoot |
0 commit comments