#Requires -Version 5.1 [CmdletBinding()] param( [Parameter(Position = 0)] [ValidateSet('start', 'update')] [string]$Action = 'start', [ValidateNotNullOrEmpty()] [string]$ListenAddress = '0.0.0.0', [ValidateRange(1, 65535)] [int]$Port = 9000, [ValidateNotNullOrEmpty()] [string]$DataDir = 'C:\ProgramData\WebTools\data', [ValidateNotNullOrEmpty()] [string]$Remote = 'origin', [ValidateNotNullOrEmpty()] [string]$Branch = 'main', [switch]$NoStart ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $projectRoot = $PSScriptRoot $serverEntry = Join-Path $projectRoot 'server\index.mjs' $distIndex = Join-Path $projectRoot 'dist\index.html' function Resolve-Application { param( [Parameter(Mandatory = $true)] [string]$Name ) $command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -eq $command) { throw "Required command '$Name' was not found in PATH." } return $command.Source } function Invoke-NativeCommand { param( [Parameter(Mandatory = $true)] [string]$Executable, [Parameter(Mandatory = $true)] [string[]]$Arguments ) & $Executable @Arguments $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { throw "Command '$Executable $($Arguments -join ' ')' failed with exit code $exitCode." } } function Assert-ProductionBuild { if (-not (Test-Path -LiteralPath $serverEntry -PathType Leaf)) { throw "Server entry was not found: $serverEntry" } if (-not (Test-Path -LiteralPath $distIndex -PathType Leaf)) { throw "Production build was not found: $distIndex. Run '.\web-tools.ps1 update' first." } } function Update-WebTools { $git = Resolve-Application 'git.exe' $npm = Resolve-Application 'npm.cmd' Push-Location $projectRoot try { $insideWorkTree = @(& $git rev-parse --is-inside-work-tree) if ($LASTEXITCODE -ne 0 -or ($insideWorkTree -join '').Trim() -ne 'true') { throw "Project directory is not a Git work tree: $projectRoot" } $statusLines = @(& $git status --porcelain --untracked-files=normal) if ($LASTEXITCODE -ne 0) { throw "Unable to inspect the Git work tree." } if ($statusLines.Count -gt 0) { throw "The Git work tree contains local changes. Update was stopped to avoid overwriting them:`n$($statusLines -join "`n")" } $currentBranchOutput = @(& $git branch --show-current) if ($LASTEXITCODE -ne 0) { throw "Unable to determine the current Git branch." } $currentBranch = ($currentBranchOutput -join '').Trim() if ($currentBranch -ne $Branch) { throw "Current branch is '$currentBranch'; expected '$Branch'." } Write-Host "Updating $Remote/$Branch..." Invoke-NativeCommand $git @('pull', '--ff-only', $Remote, $Branch) $previousNodeEnv = [Environment]::GetEnvironmentVariable('NODE_ENV', 'Process') try { [Environment]::SetEnvironmentVariable('NODE_ENV', $null, 'Process') Write-Host 'Installing locked dependencies (npm ci replaces node_modules automatically)...' Invoke-NativeCommand $npm @('ci', '--include=dev') Write-Host 'Building the production application...' Invoke-NativeCommand $npm @('run', 'build') } finally { [Environment]::SetEnvironmentVariable('NODE_ENV', $previousNodeEnv, 'Process') } Assert-ProductionBuild Write-Host 'Update and production build completed successfully.' } finally { Pop-Location } } function Start-WebTools { Assert-ProductionBuild $node = Resolve-Application 'node.exe' $dataDirectory = (New-Item -ItemType Directory -Force -Path $DataDir).FullName $previousEnvironment = @{ NODE_ENV = [Environment]::GetEnvironmentVariable('NODE_ENV', 'Process') HOST = [Environment]::GetEnvironmentVariable('HOST', 'Process') PORT = [Environment]::GetEnvironmentVariable('PORT', 'Process') DATA_DIR = [Environment]::GetEnvironmentVariable('DATA_DIR', 'Process') } try { [Environment]::SetEnvironmentVariable('NODE_ENV', 'production', 'Process') [Environment]::SetEnvironmentVariable('HOST', $ListenAddress, 'Process') [Environment]::SetEnvironmentVariable('PORT', $Port.ToString(), 'Process') [Environment]::SetEnvironmentVariable('DATA_DIR', $dataDirectory, 'Process') Write-Host "Starting Web Tools on http://${ListenAddress}:$Port" Write-Host "Navigation config: $(Join-Path $dataDirectory 'navigation.json')" Write-Host 'Press Ctrl+C to stop.' Push-Location $projectRoot try { & $node $serverEntry $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { throw "Web Tools exited with code $exitCode." } } finally { Pop-Location } } finally { foreach ($name in $previousEnvironment.Keys) { [Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process') } } } if ($Action -eq 'start' -and $NoStart) { throw "-NoStart can only be used with the 'update' action." } if ($Action -eq 'update') { Update-WebTools if ($NoStart) { return } } Start-WebTools