function Apply-UserEnvironmentConfig {
param(
[string]$WorkHome
)
if ([string]::IsNullOrEmpty($WorkHome)) {
return
}
$UserConfig = Join-Path $WorkHome "user_config.ps1"
if (-not (Test-Path $UserConfig)) {
return
}
try {
$UserConfigContent = Get-Content -Path $UserConfig -Raw -Encoding UTF8
} catch {
Write-Log "WARN" "Failed to read user_config.ps1: $($_.Exception.Message)"
return
}
$HTTP_PROXY = ""
$HTTPS_PROXY = ""
$ENABLE_SESSION_ENV_PROXY = "true"
if ($UserConfigContent -match '(?m)^\s*\$HTTP_PROXY\s*=\s*["''](.*?)["'']\s*$') { $HTTP_PROXY = $Matches[1] }
if ($UserConfigContent -match '(?m)^\s*\$HTTPS_PROXY\s*=\s*["''](.*?)["'']\s*$') { $HTTPS_PROXY = $Matches[1] }
if ($UserConfigContent -match '(?m)^\s*\$ENABLE_SESSION_ENV_PROXY\s*=\s*["''](.*?)["'']\s*$') { $ENABLE_SESSION_ENV_PROXY = $Matches[1] }
$EnableSessionEnvProxy = $true
if (-not [string]::IsNullOrWhiteSpace($ENABLE_SESSION_ENV_PROXY) -and "$ENABLE_SESSION_ENV_PROXY".Trim() -match '^(?i:false|0|no)$') {
$EnableSessionEnvProxy = $false
}
if ($EnableSessionEnvProxy) {
if (-not [string]::IsNullOrEmpty($HTTP_PROXY)) {
$env:HTTP_PROXY = $HTTP_PROXY
$env:http_proxy = $HTTP_PROXY
Write-Log "INFO" "HTTP proxy applied for this session: $HTTP_PROXY"
}
if (-not [string]::IsNullOrEmpty($HTTPS_PROXY)) {
$env:HTTPS_PROXY = $HTTPS_PROXY
$env:https_proxy = $HTTPS_PROXY
Write-Log "INFO" "HTTPS proxy applied for this session: $HTTPS_PROXY"
}
} else {
Write-Log "INFO" "Skip applying session proxy environment variables (ENABLE_SESSION_ENV_PROXY=$ENABLE_SESSION_ENV_PROXY)"
}
}
$SCRIPT:MaxLogMessageLength = 4000
function Write-Log {
.SYNOPSIS
Writes a log message with timestamp and color coding.
.DESCRIPTION
Outputs a formatted log message with timestamp, level, and color-coded output.
Also writes to log file if $LOG_FILE is defined.
Messages longer than $MaxLogMessageLength are truncated to avoid console errors.
.PARAMETER Level
The log level: INFO, SUCCESS, WARN, ERROR
.PARAMETER Message
The message to log
.EXAMPLE
Write-Log "INFO" "Starting installation..."
Write-Log "ERROR" "Installation failed"
#>
param(
[string]$Level,
[string]$Message
)
$Message = if ($null -eq $Message) { "" } else { [string]$Message }
if ($Message.Length -gt $SCRIPT:MaxLogMessageLength) {
$Message = $Message.Substring(0, $SCRIPT:MaxLogMessageLength) + " ... [truncated, original length $($Message.Length) chars]"
}
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogMsg = "[$Timestamp] [$Level] $Message"
$Color = switch ($Level) {
"ERROR" { "Red" }
"SUCCESS" { "Green" }
"WARN" { "Yellow" }
default { "White" }
}
Write-Host $LogMsg -ForegroundColor $Color
if ($null -ne $global:LOG_FILE) {
try {
$LogMsg | Out-File -FilePath $global:LOG_FILE -Append -Encoding UTF8 -ErrorAction SilentlyContinue
} catch {
}
}
}
function Get-UvDefaultIndexArgsFromUserConfig {
.SYNOPSIS
Reads $UV_INDEX / $UV_TRUSTED_HOST from user_config.ps1 under WorkHome and returns uv CLI args.
Uses --index to prioritize the configured mirror over project-level [[tool.uv.index]],
and --allow-insecure-host for trusted hosts.
#>
param(
[string]$WorkHome
)
if ([string]::IsNullOrWhiteSpace($WorkHome)) {
return @()
}
$UserCfgForUv = Join-Path $WorkHome "user_config.ps1"
if (-not (Test-Path $UserCfgForUv)) {
return @()
}
try {
. $UserCfgForUv
$UvArgs = @()
if (-not [string]::IsNullOrWhiteSpace($UV_INDEX)) {
$UvIdx = $UV_INDEX.Trim()
Write-Log "INFO" "uv uses --index from user_config.ps1: $UvIdx"
$UvArgs += @('--index', $UvIdx)
}
if (-not [string]::IsNullOrWhiteSpace($UV_TRUSTED_HOST)) {
$TrustedHosts = $UV_TRUSTED_HOST -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
foreach ($TrustedHost in $TrustedHosts) {
$UvArgs += @('--allow-insecure-host', $TrustedHost)
}
if ($TrustedHosts.Count -gt 0) {
Write-Log "INFO" "uv uses --allow-insecure-host from user_config.ps1: $($TrustedHosts -join ',')"
}
}
if ($UvArgs.Count -gt 0) {
return $UvArgs
}
} catch {
Write-Log "WARN" "Failed to read user_config.ps1 for uv args: $($_.Exception.Message)"
}
return @()
}
function Get-DbHostPortFromUserConfig {
.SYNOPSIS
Reads $DB_HOST / $DB_PORT from user_config.ps1 (string assignment lines).
#>
param(
[string]$WorkHome,
[string]$DefaultHost = "127.0.0.1",
[int]$DefaultPort = 3306
)
$result = @{
Host = $DefaultHost
Port = $DefaultPort
}
if ([string]::IsNullOrWhiteSpace($WorkHome)) {
return $result
}
$UserConfigPath = Join-Path $WorkHome "user_config.ps1"
if (-not (Test-Path $UserConfigPath)) {
return $result
}
try {
$UserConfigContent = Get-Content -Path $UserConfigPath -Raw -Encoding UTF8
if ($UserConfigContent -match '(?m)^\s*\$DB_HOST\s*=\s*["''](.*?)["'']\s*$') {
$configuredHost = $Matches[1].Trim()
if (-not [string]::IsNullOrWhiteSpace($configuredHost)) {
$result.Host = $configuredHost
}
}
if ($UserConfigContent -match '(?m)^\s*\$DB_PORT\s*=\s*["''](.*?)["'']\s*$') {
$configuredPortRaw = $Matches[1].Trim()
[int]$configuredPort = 0
if ([int]::TryParse($configuredPortRaw, [ref]$configuredPort) -and $configuredPort -ge 1 -and $configuredPort -le 65535) {
$result.Port = $configuredPort
}
}
} catch {
}
return $result
}
function Remove-DirectoryRobust {
.SYNOPSIS
Robustly removes a directory, handling long paths and special characters.
.DESCRIPTION
Attempts to delete a directory using multiple methods:
1. Long path prefix (\\?\) for paths exceeding 260 characters
2. Standard Remove-Item with -LiteralPath
3. Robocopy mirror method as fallback
.PARAMETER Path
The path to the directory to remove (can be relative or absolute)
.EXAMPLE
Remove-DirectoryRobust -Path "C:\Long Path\With (Special) Characters"
#>
param([string]$Path)
$FullPath = if (Test-Path $Path) {
(Get-Item $Path -Force).FullName
} else {
if ([System.IO.Path]::IsPathRooted($Path)) {
$Path
} else {
(Join-Path (Get-Location) $Path)
}
}
if ($FullPath.Length -gt 260) {
$LongPath = "\\?\$FullPath"
try {
Remove-Item -LiteralPath $LongPath -Recurse -Force -ErrorAction Stop
return $true
} catch {
}
}
try {
Remove-Item -LiteralPath $FullPath -Recurse -Force -ErrorAction Stop
return $true
} catch {
}
try {
$TempDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
$RobocopyArgs = @(
"`"$TempDir`"",
"`"$FullPath`"",
"/MIR",
"/NFL",
"/NDL",
"/NJH",
"/NJS",
"/NP",
"/NS",
"/NC",
"/BYTES"
)
$NullOutput = Join-Path $env:TEMP "robocopy_null_$([System.Guid]::NewGuid().ToString()).txt"
$RobocopyProcess = Start-Process -FilePath "robocopy.exe" -ArgumentList $RobocopyArgs -Wait -NoNewWindow -PassThru -RedirectStandardOutput $NullOutput -RedirectStandardError $NullOutput
Remove-Item -Path $NullOutput -Force -ErrorAction SilentlyContinue
Remove-Item -Path $TempDir -Force -ErrorAction SilentlyContinue
if ($RobocopyProcess.ExitCode -le 1) {
if (Test-Path $FullPath) {
Remove-Item -LiteralPath $FullPath -Force -ErrorAction SilentlyContinue
}
return $true
}
} catch {
}
return $false
}
function Test-Command {
.SYNOPSIS
Tests if a command is available in the system PATH.
.DESCRIPTION
Checks if a command exists and is executable. Exits the script if not found.
.PARAMETER Command
The command name to test (e.g., "git", "node", "python")
.EXAMPLE
Test-Command "git"
#>
param([string]$Command)
$null = Get-Command $Command -ErrorAction SilentlyContinue
if (-not $?) {
Write-Log "ERROR" "Dependency command '$Command' not found, please install first"
exit 1
}
}
function Test-File {
.SYNOPSIS
Tests if a file exists, optionally creating it if it doesn't.
.DESCRIPTION
Checks if a file exists. If not found and CreateIfNotExist is true, creates an empty file.
Otherwise, exits the script with an error.
.PARAMETER File
The file path to test
.PARAMETER CreateIfNotExist
If true, creates an empty file if it doesn't exist. Default is false.
.EXAMPLE
Test-File "C:\path\to\file.txt"
Test-File "C:\path\to\file.txt" -CreateIfNotExist $true
#>
param(
[string]$File,
[bool]$CreateIfNotExist = $false
)
if (-not (Test-Path $File)) {
if ($CreateIfNotExist) {
Write-Log "WARN" "File $File not found, trying to create empty file"
$null = New-Item -ItemType File -Path $File -Force
} else {
Write-Log "ERROR" "File $File not found, cannot continue"
exit 1
}
}
}
function Test-Directory {
.SYNOPSIS
Tests if a directory exists.
.DESCRIPTION
Checks if a directory exists. Exits the script with an error if not found.
.PARAMETER Dir
The directory path to test
.EXAMPLE
Test-Directory "C:\path\to\directory"
#>
param([string]$Dir)
if (-not (Test-Path $Dir)) {
Write-Log "ERROR" "Directory $Dir not found, cannot continue"
exit 1
}
}
function Unblock-AllScripts {
.SYNOPSIS
Unblocks all PowerShell scripts in a directory to avoid security warnings.
.DESCRIPTION
Removes the "blocked" attribute from all .ps1 files in the specified directory and subdirectories.
.PARAMETER WorkHome
The working directory to search for PowerShell scripts. Defaults to current script directory.
.EXAMPLE
Unblock-AllScripts -WorkHome "C:\path\to\scripts"
#>
param(
[string]$WorkHome = $null
)
if ([string]::IsNullOrEmpty($WorkHome)) {
$WorkHome = Split-Path -Parent $MyInvocation.PSCommandPath
}
Write-Log "INFO" "Unblocking all PowerShell scripts in current directory..."
$ScriptFiles = Get-ChildItem -Path $WorkHome -Recurse -Filter "*.ps1" -ErrorAction SilentlyContinue
$ScriptFiles | Unblock-File -ErrorAction SilentlyContinue | Out-Null
$UnblockedCount = $ScriptFiles.Count
if ($UnblockedCount -gt 0) {
Write-Log "INFO" "Unblocked $UnblockedCount PowerShell script(s)"
}
}
function Save-Progress {
param(
[string]$Step
)
try {
$Step | Out-File -FilePath $PROGRESS_FILE -Encoding utf8 -Force
Write-Log "INFO" "Progress saved: $Step"
} catch {
Write-Log "WARN" "Failed to save progress: $_"
}
}
function Read-Progress {
if (Test-Path $PROGRESS_FILE) {
try {
$Progress = Get-Content $PROGRESS_FILE -ErrorAction SilentlyContinue | Where-Object { $_ -match "^\S+" }
if ($Progress) {
return $Progress.Trim()
}
} catch {
Write-Log "WARN" "Failed to read progress: $_"
}
}
return ""
}
function Clear-Progress {
if (Test-Path $PROGRESS_FILE) {
try {
Remove-Item $PROGRESS_FILE -Force -ErrorAction SilentlyContinue
Write-Log "INFO" "Progress file cleared"
} catch {
Write-Log "WARN" "Failed to clear progress file: $_"
}
}
}
function Test-SkipStep {
param(
[string]$CurrentStep,
[string]$LastProgress
)
if ([string]::IsNullOrEmpty($LastProgress)) {
return $false
}
$CurrentIndex = -1
$LastIndex = -1
for ($i = 0; $i -lt $INSTALL_STEPS.Count; $i++) {
if ($INSTALL_STEPS[$i] -eq $CurrentStep) {
$CurrentIndex = $i
}
if ($INSTALL_STEPS[$i] -eq $LastProgress) {
$LastIndex = $i
}
}
if ($CurrentIndex -eq -1 -or $LastIndex -eq -1) {
return $false
}
if ($LastIndex -ge $CurrentIndex) {
return $true
} else {
return $false
}
}