[CmdletBinding()] param( [switch]$BuildOnly, [switch]$SyncOnly, [string]$WorkRoot ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $repositoryRoot = Split-Path -Parent $PSScriptRoot $sourceApp = Join-Path $repositoryRoot 'flutter_app' if (-not (Test-Path -LiteralPath (Join-Path $sourceApp 'pubspec.yaml') -PathType Leaf)) { throw "Flutter project was not found under '$sourceApp'." } $localAppData = [IO.Path]::GetFullPath($env:LOCALAPPDATA).TrimEnd('\') $tempDirectory = [IO.Path]::GetFullPath($env:TEMP).TrimEnd('\') if ([string]::IsNullOrWhiteSpace($WorkRoot)) { $WorkRoot = Join-Path $localAppData 'SovereignAI-Starter\windows-dev\flutter_app' } $workRootPath = [IO.Path]::GetFullPath($WorkRoot).TrimEnd('\') $allowedRoots = @($localAppData, $tempDirectory) $isAllowedRoot = $false foreach ($allowedRoot in $allowedRoots) { if ($workRootPath.StartsWith($allowedRoot + '\', [StringComparison]::OrdinalIgnoreCase)) { $isAllowedRoot = $true break } } if (-not $isAllowedRoot) { throw 'WorkRoot must be a child directory of LOCALAPPDATA or TEMP.' } if (Test-Path -LiteralPath $workRootPath) { $workRootItem = Get-Item -LiteralPath $workRootPath -Force if (($workRootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Refusing to use a reparse-point work directory: $workRootPath" } } else { New-Item -ItemType Directory -Path $workRootPath | Out-Null } $destinationAppPath = $workRootPath if (-not (Test-Path -LiteralPath $destinationAppPath)) { New-Item -ItemType Directory -Path $destinationAppPath | Out-Null } $destinationAppPath = (Resolve-Path -LiteralPath $destinationAppPath).Path $destinationItem = Get-Item -LiteralPath $destinationAppPath -Force if (($destinationItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "Refusing to mirror source into a reparse-point directory: $destinationAppPath" } if (-not $SyncOnly) { $runningApps = @(Get-Process -Name 'flutter_app' -ErrorAction SilentlyContinue) if ($BuildOnly) { $runningApps = @($runningApps | Where-Object { $_.Path -and $_.Path.StartsWith($workRootPath + '\', [StringComparison]::OrdinalIgnoreCase) }) } if ($runningApps.Count -gt 0) { $processIds = ($runningApps | ForEach-Object Id) -join ', ' throw "Close the running Mithqal AI Debug window(s) first (PID: $processIds), then run this script again." } } # Keep native build outputs and package caches in the local copy for fast, # incremental rebuilds. They are deliberately excluded from mirroring. $excludedDirectories = @( (Join-Path $sourceApp 'build'), (Join-Path $sourceApp '.dart_tool'), (Join-Path $sourceApp 'windows\flutter\ephemeral'), (Join-Path $sourceApp 'linux\flutter\ephemeral'), (Join-Path $sourceApp 'macos\Flutter\ephemeral') ) $excludedFiles = @( '.flutter-plugins', '.flutter-plugins-dependencies', '.packages', '.env', '.env.*' ) & robocopy $sourceApp $destinationAppPath /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP /XD $excludedDirectories /XF $excludedFiles if ($LASTEXITCODE -ge 8) { throw "Could not sync Flutter sources to the local build directory (robocopy $LASTEXITCODE)." } Write-Host "Local Windows build workspace: $destinationAppPath" if ($SyncOnly) { exit 0 } Push-Location $destinationAppPath try { & flutter pub get --offline if ($LASTEXITCODE -ne 0) { throw 'Flutter dependencies are not available in the local pub cache. Run flutter pub get once with network access, then retry.' } if ($BuildOnly) { & flutter build windows --debug --no-pub } else { & flutter run -d windows --debug --no-pub } if ($LASTEXITCODE -ne 0) { throw "Flutter exited with code $LASTEXITCODE." } } finally { Pop-Location }