param([Parameter(Mandatory)][string]$Output, [ValidateSet('x64','x86','ARM64','AnyCPU')][string]$Platform = 'x64')
$ErrorActionPreference = 'Stop'
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null
function AgentFileHash([string]$Path) {
$agentStream = [IO.File]::OpenRead($Path); $agentHasher = [Security.Cryptography.SHA256]::Create()
try { return [BitConverter]::ToString($agentHasher.ComputeHash($agentStream)).Replace('-','').ToLowerInvariant() }
finally { $agentHasher.Dispose(); $agentStream.Dispose() }
}
$agentRepo = Split-Path $PSScriptRoot
$agentArchitecture = if ($Platform -eq 'AnyCPU') { 'x64' } else { $Platform.ToLowerInvariant() }
$agentPins = @{
x64 = @('24.19.0','57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73')
arm64 = @('24.19.0','8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f')
x86 = @('22.23.3','1e07ac00e61b41b7ecf242de8e38883d237d5ad3fdcad7c16f2dd3a88670e2b6')
}
$agentVersion, $agentHash = $agentPins[$agentArchitecture]
$agentCache = Join-Path $agentRepo '.artifacts/agent-runtime'
New-Item -ItemType Directory -Force -Path $agentCache | Out-Null
$agentLock = $null
for ($agentAttempt = 0; $agentAttempt -lt 120 -and !$agentLock; $agentAttempt++) {
try { $agentLock = [IO.File]::Open((Join-Path $agentCache '.restore.lock'), [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) }
catch [IO.IOException] { Start-Sleep -Milliseconds 500 }
}
if (!$agentLock) { throw 'Timed out waiting for the Agent runtime build cache.' }
try {
$agentArchiveName = "node-v$agentVersion-win-$agentArchitecture"
$agentArchive = Join-Path $agentCache ($agentArchiveName + '.zip')
if (!(Test-Path -LiteralPath $agentArchive)) {
$agentDownload = [Net.WebClient]::new()
try { $agentDownload.DownloadFile("https://nodejs.org/dist/v$agentVersion/$agentArchiveName.zip", $agentArchive) }
finally { $agentDownload.Dispose() }
}
if ((AgentFileHash $agentArchive) -ne $agentHash) { throw 'Bundled Node.js archive SHA256 mismatch.' }
$agentNodeRoot = Join-Path $agentCache $agentArchiveName
if (!(Test-Path -LiteralPath (Join-Path $agentNodeRoot 'node.exe')) -or !(Test-Path -LiteralPath (Join-Path $agentNodeRoot 'node_modules/npm/bin/npm-cli.js'))) {
$agentExtractionRoot = [IO.Path]::GetFullPath($agentCache).TrimEnd('\','/') + [IO.Path]::DirectorySeparatorChar
$agentZip = [IO.Compression.ZipFile]::OpenRead($agentArchive)
try {
foreach ($agentEntry in $agentZip.Entries) {
$agentTarget = [IO.Path]::GetFullPath((Join-Path $agentExtractionRoot $agentEntry.FullName))
if (!$agentTarget.StartsWith($agentExtractionRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Runtime archive extraction path escapes cache.' }
if ($agentEntry.FullName.EndsWith('/')) { [IO.Directory]::CreateDirectory($agentTarget) | Out-Null; continue }
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($agentTarget)) | Out-Null
[IO.Compression.ZipFileExtensions]::ExtractToFile($agentEntry, $agentTarget, $true)
}
} finally { $agentZip.Dispose() }
}
$agentSkillSource = Join-Path $agentRepo 'XFEWorkflow/XFEWorkflow.Plugins.AgentBrowser/Skill'
$agentPackageLock = Join-Path $agentSkillSource 'package-lock.json'
$agentDependencyHash = AgentFileHash $agentPackageLock
$agentDependencies = Join-Path $agentCache ('dependencies-' + $agentDependencyHash)
if (!(Test-Path -LiteralPath (Join-Path $agentDependencies '.complete'))) {
New-Item -ItemType Directory -Force -Path $agentDependencies | Out-Null
Copy-Item -LiteralPath (Join-Path $agentSkillSource 'package.json'), $agentPackageLock -Destination $agentDependencies -Force
Push-Location -LiteralPath $agentDependencies
try {
& (Join-Path $agentNodeRoot 'node.exe') (Join-Path $agentNodeRoot 'node_modules/npm/bin/npm-cli.js') ci --ignore-scripts --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw 'Agent browser locked dependency restore failed.' }
} finally { Pop-Location }
Set-Content -LiteralPath (Join-Path $agentDependencies '.complete') -Value $agentDependencyHash -Encoding ASCII
}
$agentSkillDestination = Join-Path $Output 'Skill'
New-Item -ItemType Directory -Force -Path $agentSkillDestination | Out-Null
Get-ChildItem -LiteralPath $agentSkillSource | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $agentSkillDestination -Recurse -Force }
Copy-Item -LiteralPath (Join-Path $agentDependencies 'node_modules') -Destination $agentSkillDestination -Recurse -Force
$agentRuntimeDestination = Join-Path $Output 'Runtime'
New-Item -ItemType Directory -Force -Path $agentRuntimeDestination | Out-Null
Copy-Item -LiteralPath (Join-Path $agentNodeRoot 'node.exe'), (Join-Path $agentNodeRoot 'LICENSE') -Destination $agentRuntimeDestination -Force
@{ nodeVersion=$agentVersion; architecture=$agentArchitecture; archiveSha256=$agentHash; source="https://nodejs.org/dist/v$agentVersion/$agentArchiveName.zip"; dependencyLockSha256=$agentDependencyHash } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $agentRuntimeDestination 'sources.json') -Encoding UTF8
} finally { $agentLock.Dispose() }
param([Parameter(Mandatory)][string]$Output, [ValidateSet('x64','x86','ARM64','AnyCPU')][string]$Platform = 'x64')
$ErrorActionPreference = 'Stop'
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null
function AgentFileHash([string]$Path) {
$agentStream = [IO.File]::OpenRead($Path); $agentHasher = [Security.Cryptography.SHA256]::Create()
try { return [BitConverter]::ToString($agentHasher.ComputeHash($agentStream)).Replace('-','').ToLowerInvariant() }
finally { $agentHasher.Dispose(); $agentStream.Dispose() }
}
$agentRepo = Split-Path $PSScriptRoot
$agentArchitecture = if ($Platform -eq 'AnyCPU') { 'x64' } else { $Platform.ToLowerInvariant() }
$agentPins = @{
x64 = @('24.19.0','57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73')
arm64 = @('24.19.0','8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f')
x86 = @('22.23.3','1e07ac00e61b41b7ecf242de8e38883d237d5ad3fdcad7c16f2dd3a88670e2b6')
}
$agentVersion, $agentHash = $agentPins[$agentArchitecture]
$agentCache = Join-Path $agentRepo '.artifacts/agent-runtime'
New-Item -ItemType Directory -Force -Path $agentCache | Out-Null
$agentLock = $null
for ($agentAttempt = 0; $agentAttempt -lt 120 -and !$agentLock; $agentAttempt++) {
try { $agentLock = [IO.File]::Open((Join-Path $agentCache '.restore.lock'), [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) }
catch [IO.IOException] { Start-Sleep -Milliseconds 500 }
}
if (!$agentLock) { throw 'Timed out waiting for the Agent runtime build cache.' }
try {
$agentArchiveName = "node-v$agentVersion-win-$agentArchitecture"
$agentArchive = Join-Path $agentCache ($agentArchiveName + '.zip')
if (!(Test-Path -LiteralPath $agentArchive)) {
$agentDownload = [Net.WebClient]::new()
try { $agentDownload.DownloadFile("https://nodejs.org/dist/v$agentVersion/$agentArchiveName.zip", $agentArchive) }
finally { $agentDownload.Dispose() }
}
if ((AgentFileHash $agentArchive) -ne $agentHash) { throw 'Bundled Node.js archive SHA256 mismatch.' }
$agentNodeRoot = Join-Path $agentCache $agentArchiveName
if (!(Test-Path -LiteralPath (Join-Path $agentNodeRoot 'node.exe')) -or !(Test-Path -LiteralPath (Join-Path $agentNodeRoot 'node_modules/npm/bin/npm-cli.js'))) {
$agentExtractionRoot = [IO.Path]::GetFullPath($agentCache).TrimEnd('\','/') + [IO.Path]::DirectorySeparatorChar
$agentZip = [IO.Compression.ZipFile]::OpenRead($agentArchive)
try {
foreach ($agentEntry in $agentZip.Entries) {
$agentTarget = [IO.Path]::GetFullPath((Join-Path $agentExtractionRoot $agentEntry.FullName))
if (!$agentTarget.StartsWith($agentExtractionRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Runtime archive extraction path escapes cache.' }
if ($agentEntry.FullName.EndsWith('/')) { [IO.Directory]::CreateDirectory($agentTarget) | Out-Null; continue }
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($agentTarget)) | Out-Null
[IO.Compression.ZipFileExtensions]::ExtractToFile($agentEntry, $agentTarget, $true)
}
} finally { $agentZip.Dispose() }
}
$agentSkillSource = Join-Path $agentRepo 'XFEWorkflow/XFEWorkflow.Plugins.AgentBrowser/Skill'
$agentPackageLock = Join-Path $agentSkillSource 'package-lock.json'
$agentDependencyHash = AgentFileHash $agentPackageLock
$agentDependencies = Join-Path $agentCache ('dependencies-' + $agentDependencyHash)
if (!(Test-Path -LiteralPath (Join-Path $agentDependencies '.complete'))) {
New-Item -ItemType Directory -Force -Path $agentDependencies | Out-Null
Copy-Item -LiteralPath (Join-Path $agentSkillSource 'package.json'), $agentPackageLock -Destination $agentDependencies -Force
Push-Location -LiteralPath $agentDependencies
try {
& (Join-Path $agentNodeRoot 'node.exe') (Join-Path $agentNodeRoot 'node_modules/npm/bin/npm-cli.js') ci --ignore-scripts --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw 'Agent browser locked dependency restore failed.' }
} finally { Pop-Location }
Set-Content -LiteralPath (Join-Path $agentDependencies '.complete') -Value $agentDependencyHash -Encoding ASCII
}
$agentSkillDestination = Join-Path $Output 'Skill'
New-Item -ItemType Directory -Force -Path $agentSkillDestination | Out-Null
Get-ChildItem -LiteralPath $agentSkillSource | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $agentSkillDestination -Recurse -Force }
Copy-Item -LiteralPath (Join-Path $agentDependencies 'node_modules') -Destination $agentSkillDestination -Recurse -Force
$agentRuntimeDestination = Join-Path $Output 'Runtime'
New-Item -ItemType Directory -Force -Path $agentRuntimeDestination | Out-Null
Copy-Item -LiteralPath (Join-Path $agentNodeRoot 'node.exe'), (Join-Path $agentNodeRoot 'LICENSE') -Destination $agentRuntimeDestination -Force
@{ nodeVersion=$agentVersion; architecture=$agentArchitecture; archiveSha256=$agentHash; source="https://nodejs.org/dist/v$agentVersion/$agentArchiveName.zip"; dependencyLockSha256=$agentDependencyHash } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $agentRuntimeDestination 'sources.json') -Encoding UTF8
} finally { $agentLock.Dispose() }