build-pe-script-launcher/resources/Driver-Manager.ps1
Claude b06374a562 Initial commit: Build PE Script Launcher
Production-grade Windows PE builder with PowerShell 7 integration.

Features:
- PowerShell 7 integrated into WinPE environment
- Automatic script launcher for external media
- Configurable startup and fallback scripts
- Custom console colors
- Maximized console window on boot
- PowerShell module integration
- Windows ADK auto-installation
- Defender optimization for faster builds

Project Structure:
- Build-PE.ps1: Main build orchestrator
- config.json: Configuration file
- lib/: Build modules (Dependencies, WinPEBuilder, PowerShell7, ModuleManager, DefenderOptimization)
- resources/: Runtime scripts (Invoke-ScriptLauncher, Invoke-ScriptMenu, Driver-Manager)
- CLAUDE.md: Complete project documentation
- README.md: Quick start guide

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 16:39:43 -05:00

2166 lines
78 KiB
PowerShell

#Requires -RunAsAdministrator
#Requires -Version 5.1
<#
.SYNOPSIS
Windows Driver Import/Export Manager with Terminal GUI
.DESCRIPTION
Interactive terminal-based GUI for managing Windows driver backups and restoration.
Combines Export-Drivers and Import-Drivers functionality into a unified interface.
Can be run in three modes:
1. GUI Mode (default) - Interactive terminal interface
2. Export Mode (-Export) - Headless driver export
3. Import Mode (-Import) - Headless driver import
.PARAMETER Mode
Operation mode: GUI (default), Export, or Import
.PARAMETER TargetDrive
Target drive letter for export (e.g., "D:") or "Auto" to use first available removable drive
.PARAMETER SourceDrive
Source drive letter for import (e.g., "D:") or "Auto" to search all drives
.PARAMETER Directory
Directory name for driver storage (default: ".drivers")
.PARAMETER Force
Skip confirmation prompts in headless mode
.PARAMETER Quiet
Suppress non-essential output in headless mode
.EXAMPLE
Driver-Manager.ps1
Launches interactive GUI mode
.EXAMPLE
Driver-Manager.ps1 -Mode Export -TargetDrive "E:" -Force
Export drivers to E:\.drivers\<MODEL> without prompts
.EXAMPLE
Driver-Manager.ps1 -Mode Export -TargetDrive Auto
Export drivers to first removable drive automatically
.EXAMPLE
Driver-Manager.ps1 -Mode Import -SourceDrive Auto -Force
Import drivers from auto-detected source without prompts
.EXAMPLE
Driver-Manager.ps1 -Mode Import -SourceDrive "D:" -Quiet
Import drivers from D: with minimal output
.NOTES
Author: Seton (based on original Export/Import scripts)
Requires: Microsoft.PowerShell.ConsoleGuiTools module (GUI mode only)
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet('GUI', 'Export', 'Import')]
[string]$Mode = 'GUI',
[Parameter()]
[string]$TargetDrive,
[Parameter()]
[string]$SourceDrive,
[Parameter()]
[string]$Directory = '.drivers',
[Parameter()]
[switch]$Force,
[Parameter()]
[switch]$Quiet
)
#region PowerShell 7 Check and Auto-Relaunch
# Only enforce PowerShell 7 for GUI mode (headless modes work fine on PS 5.1+)
if ($Mode -eq 'GUI' -and $PSVersionTable.PSVersion.Major -lt 7) {
Write-Host "PowerShell 7+ is required for optimal compatibility." -ForegroundColor Yellow
Write-Host "Current version: PowerShell $($PSVersionTable.PSVersion)" -ForegroundColor Gray
Write-Host ""
# Try to find PowerShell 7
$pwshPath = $null
$pwshLocations = @(
(Get-Command pwsh -ErrorAction SilentlyContinue).Source,
"C:\Program Files\PowerShell\7\pwsh.exe",
"$env:ProgramFiles\PowerShell\7\pwsh.exe",
"${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe",
"$env:LOCALAPPDATA\Microsoft\PowerShell\7\pwsh.exe"
)
foreach ($location in $pwshLocations) {
if ($location -and (Test-Path $location)) {
$pwshPath = $location
Write-Host "Found PowerShell 7 at: $pwshPath" -ForegroundColor Green
break
}
}
if ($pwshPath) {
# Relaunch in PowerShell 7
Write-Host "Relaunching in PowerShell 7..." -ForegroundColor Cyan
Write-Host ""
if ($PSCommandPath) {
# Running from a file - can relaunch directly
$arguments = @('-NoProfile', '-File', $PSCommandPath)
# Pass through all parameters
if ($Mode) { $arguments += @('-Mode', $Mode) }
if ($TargetDrive) { $arguments += @('-TargetDrive', $TargetDrive) }
if ($SourceDrive) { $arguments += @('-SourceDrive', $SourceDrive) }
if ($Directory) { $arguments += @('-Directory', $Directory) }
if ($Force) { $arguments += '-Force' }
if ($Quiet) { $arguments += '-Quiet' }
Start-Process -FilePath $pwshPath -ArgumentList $arguments -NoNewWindow -Wait
exit
}
else {
# Running via irm | iex - can't easily relaunch
Write-Host "Please rerun this script using PowerShell 7:" -ForegroundColor Yellow
Write-Host " pwsh -Command `"irm YOUR_URL | iex`"" -ForegroundColor White
Write-Host ""
Write-Host "Or install as a file and run it." -ForegroundColor Gray
exit 1
}
}
else {
# PowerShell 7 not found - offer to install
Write-Host "PowerShell 7 not found. Installing automatically in 3 seconds..." -ForegroundColor Yellow
Write-Host "(Press Ctrl+C to cancel and install manually)" -ForegroundColor Gray
Write-Host ""
# Countdown
for ($i = 3; $i -gt 0; $i--) {
Write-Host " Installing in $i..." -ForegroundColor Cyan -NoNewline
Start-Sleep -Seconds 1
Write-Host "`r" -NoNewline
}
Write-Host ""
try {
# Try winget first (fastest)
Write-Host "Attempting installation via winget..." -ForegroundColor Yellow
$wingetResult = & winget install Microsoft.PowerShell --silent --accept-source-agreements --accept-package-agreements 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "PowerShell 7 installed successfully via winget!" -ForegroundColor Green
Write-Host ""
Write-Host "Please restart this script to use PowerShell 7." -ForegroundColor Yellow
Write-Host "The script will auto-relaunch in PowerShell 7 next time." -ForegroundColor Gray
exit 0
}
else {
throw "winget installation failed"
}
}
catch {
# Fallback to MSI installer
Write-Host "winget not available, using MSI installer..." -ForegroundColor Yellow
try {
iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet"
Write-Host "PowerShell 7 installed successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "Please restart this script to use PowerShell 7." -ForegroundColor Yellow
exit 0
}
catch {
Write-Host "Automatic installation failed: $_" -ForegroundColor Red
Write-Host "" -ForegroundColor Red
Write-Host "Please install PowerShell 7 manually:" -ForegroundColor Yellow
Write-Host " winget install Microsoft.PowerShell" -ForegroundColor White
Write-Host " or download from: https://aka.ms/powershell-release?tag=stable" -ForegroundColor White
exit 1
}
}
}
}
#endregion
#region Debug Logging
$script:DebugLogPath = Join-Path $env:TEMP "Driver-Manager-Debug.log"
function Write-DebugLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$logMessage = "[$timestamp] $Message"
# Always write to log file
Add-Content -Path $script:DebugLogPath -Value $logMessage -Force
# Also write to verbose stream
Write-Verbose $logMessage
}
# Initialize log
Write-DebugLog "=== Driver Manager Started ==="
Write-DebugLog "Mode: $Mode"
Write-DebugLog "Debug log: $script:DebugLogPath"
#endregion
#region Windows PE and Offline Windows Detection
# Script-level variables for offline Windows support
$script:IsWindowsPE = $false
$script:OfflineWindowsPath = $null
$script:OfflineWindowsDrive = $null
$script:PEDriveLetter = $null
function Test-WindowsPE {
<#
.SYNOPSIS
Detect if running in Windows PE environment
#>
try {
# Primary check: MiniNT registry key exists in WinPE
if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT") {
Write-DebugLog "Windows PE detected: MiniNT registry key exists"
return $true
}
# Secondary check: PE typically runs from X: drive
if (Test-Path "X:\Windows\System32" -ErrorAction SilentlyContinue) {
Write-DebugLog "Windows PE detected: X:\Windows\System32 exists"
return $true
}
Write-DebugLog "Not running in Windows PE"
return $false
}
catch {
Write-DebugLog "Error detecting Windows PE: $_"
return $false
}
}
function Get-OfflineWindowsInstallations {
<#
.SYNOPSIS
Find offline Windows installations on available drives
.DESCRIPTION
Searches all available drives for Windows\System32 directories
and returns information about discovered installations.
Excludes the Windows PE drive (typically X:)
#>
$installations = @()
Write-DebugLog "Searching for offline Windows installations..."
# Get all fixed drives
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object {
$_.Root -match '^[A-Z]:\\$' -and
(Test-Path $_.Root -ErrorAction SilentlyContinue)
}
foreach ($drive in $drives) {
$driveLetter = $drive.Root
# Skip X: drive (Windows PE boot drive)
if ($driveLetter -eq "X:\") {
Write-DebugLog "Skipping PE drive: $driveLetter"
continue
}
$windowsPath = Join-Path $driveLetter "Windows"
$system32Path = Join-Path $windowsPath "System32"
Write-DebugLog "Checking drive $driveLetter for Windows installation"
# Check if this drive has a Windows installation
# Also verify it's not the PE environment by checking for specific PE files
if ((Test-Path $system32Path -ErrorAction SilentlyContinue)) {
# Additional check: PE usually has winpeshl.exe in System32
$winpeshlPath = Join-Path $system32Path "winpeshl.exe"
if (Test-Path $winpeshlPath -ErrorAction SilentlyContinue) {
Write-DebugLog "Skipping PE image at $windowsPath (winpeshl.exe detected)"
continue
}
Write-DebugLog "Found Windows installation at $windowsPath"
# Try to get more info about the installation
$installInfo = @{
Drive = $driveLetter
WindowsPath = $windowsPath
System32Path = $system32Path
DisplayName = $driveLetter.TrimEnd('\')
}
# Try to read version info from registry hive (if accessible)
$regPath = Join-Path $windowsPath "System32\config\SOFTWARE"
if (Test-Path $regPath -ErrorAction SilentlyContinue) {
try {
# Try to get Windows version info
$installInfo.DisplayName = "$($driveLetter.TrimEnd('\')) - Windows Installation"
Write-DebugLog "Windows installation verified at $windowsPath"
}
catch {
Write-DebugLog "Could not read version info for $windowsPath"
}
}
$installations += [PSCustomObject]$installInfo
}
}
Write-DebugLog "Found $($installations.Count) Windows installation(s)"
return $installations
}
function Get-OfflineWindowsModel {
<#
.SYNOPSIS
Get computer model from offline Windows registry or BIOS
.PARAMETER WindowsPath
Path to offline Windows installation (e.g., "D:\Windows")
#>
param(
[Parameter(Mandatory = $true)]
[string]$WindowsPath
)
# First, try getting model from the current system's BIOS
# In PE, the hardware is the same, so this should work
Write-DebugLog "Attempting to get model from system BIOS (physical hardware)"
try {
# Try WMI first - this reads from actual hardware
$biosInfo = Get-CimInstance -ClassName Win32_ComputerSystemProduct -ErrorAction SilentlyContinue
if ($biosInfo -and $biosInfo.Name) {
$model = $biosInfo.Name
Write-DebugLog "Got model from BIOS: $model"
return $model
}
}
catch {
Write-DebugLog "Could not get model from BIOS via WMI: $_"
}
# Try wmic if CIM fails
try {
$wmicResult = & wmic csproduct get name 2>&1 | Select-Object -Skip 1 | Where-Object { $_.Trim() -ne "" }
if ($wmicResult -and $wmicResult.Trim()) {
$model = $wmicResult.Trim()
Write-DebugLog "Got model from wmic: $model"
return $model
}
}
catch {
Write-DebugLog "Could not get model from wmic: $_"
}
# Try reading from offline Windows registry
Write-DebugLog "Attempting to get model from offline Windows registry: $WindowsPath"
try {
# Path to SYSTEM registry hive
$systemHivePath = Join-Path $WindowsPath "System32\config\SYSTEM"
if (-not (Test-Path $systemHivePath)) {
Write-DebugLog "SYSTEM hive not found at: $systemHivePath"
return "Offline Windows"
}
Write-DebugLog "SYSTEM hive found at: $systemHivePath"
# Mount the registry hive temporarily
$mountPoint = "HKLM\TempOfflineSystem"
Write-DebugLog "Mounting registry hive to $mountPoint"
# Load the hive using reg.exe - don't use extra quotes
$loadCmd = "reg.exe load $mountPoint `"$systemHivePath`""
Write-DebugLog "Running command: $loadCmd"
$result = cmd.exe /c $loadCmd 2>&1
Write-DebugLog "Reg load result: $result"
Write-DebugLog "Reg load exit code: $LASTEXITCODE"
if ($LASTEXITCODE -ne 0) {
Write-DebugLog "Failed to load registry hive, trying without quotes"
# Try without quotes if path has no spaces
if ($systemHivePath -notmatch '\s') {
$result = & reg.exe load $mountPoint $systemHivePath 2>&1
Write-DebugLog "Second attempt result: $result"
Write-DebugLog "Second attempt exit code: $LASTEXITCODE"
}
if ($LASTEXITCODE -ne 0) {
Write-DebugLog "Failed to load registry hive after retry"
return "Offline Windows"
}
}
try {
# Wait a moment for registry to be available
Start-Sleep -Milliseconds 200
# First, find which control set is current
$selectPath = "HKLM:\TempOfflineSystem\Select"
$currentControlSet = "ControlSet001"
if (Test-Path $selectPath) {
$selectInfo = Get-ItemProperty -Path $selectPath -ErrorAction SilentlyContinue
if ($selectInfo.Current) {
$currentControlSet = "ControlSet" + ([string]$selectInfo.Current).PadLeft(3, '0')
Write-DebugLog "Current control set: $currentControlSet"
}
}
# Try multiple locations for system information
$pathsToTry = @(
"HKLM:\TempOfflineSystem\$currentControlSet\Control\SystemInformation",
"HKLM:\TempOfflineSystem\ControlSet001\Control\SystemInformation",
"HKLM:\TempOfflineSystem\$currentControlSet\Hardware Abstraction Layer\ComputerName"
)
foreach ($keyPath in $pathsToTry) {
Write-DebugLog "Trying path: $keyPath"
if (Test-Path $keyPath) {
Write-DebugLog "Path exists!"
$systemInfo = Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue
if ($systemInfo) {
Write-DebugLog "Got properties: $($systemInfo.PSObject.Properties.Name -join ', ')"
if ($systemInfo.SystemManufacturer -and $systemInfo.SystemProductName) {
$model = "$($systemInfo.SystemManufacturer) $($systemInfo.SystemProductName)"
Write-DebugLog "Found model: $model"
return $model
}
elseif ($systemInfo.SystemProductName) {
Write-DebugLog "Found model: $($systemInfo.SystemProductName)"
return $systemInfo.SystemProductName
}
}
}
else {
Write-DebugLog "Path does not exist"
}
}
Write-DebugLog "No model information found in any location"
return "Offline Windows"
}
finally {
# Always unload the hive
Write-DebugLog "Unloading registry hive"
$unloadResult = & reg.exe unload $mountPoint 2>&1
Write-DebugLog "Unload result: $unloadResult"
# Sometimes unload needs a moment
Start-Sleep -Milliseconds 500
}
}
catch {
Write-DebugLog "Error reading offline model: $_"
Write-DebugLog "Error details: $($_.Exception.Message)"
Write-DebugLog "Stack trace: $($_.ScriptStackTrace)"
# Try to unload if we loaded it
try { & reg.exe unload "HKLM\TempOfflineSystem" 2>&1 | Out-Null } catch { }
return "Offline Windows"
}
}
function Show-WindowsInstallationSelector {
<#
.SYNOPSIS
Show dialog to select offline Windows installation
.PARAMETER Installations
Array of discovered Windows installations
#>
param(
[Parameter(Mandatory = $true)]
[array]$Installations
)
Write-DebugLog "Showing Windows installation selector dialog"
# If only one installation, auto-select it
if ($Installations.Count -eq 1) {
Write-DebugLog "Only one installation found, auto-selecting"
return $Installations[0]
}
# Create selection dialog
$dialog = [Terminal.Gui.Dialog]::new()
$dialog.Title = "Select Windows Installation"
$dialog.Width = 70
$dialog.Height = 18
# Instructions label
$lblInstructions = [Terminal.Gui.Label]::new()
$lblInstructions.X = 1
$lblInstructions.Y = 1
$lblInstructions.Width = [Terminal.Gui.Dim]::Fill() - 2
$lblInstructions.Height = 2
$lblInstructions.Text = "Multiple Windows installations detected. Select one:"
$dialog.Add($lblInstructions)
# Create list view for installations
$listView = [Terminal.Gui.ListView]::new()
$listView.X = 1
$listView.Y = [Terminal.Gui.Pos]::Bottom($lblInstructions)
$listView.Width = [Terminal.Gui.Dim]::Fill() - 2
$listView.Height = [Terminal.Gui.Dim]::Fill() - 4
# Build display items
$displayItems = $Installations | ForEach-Object {
"$($_.DisplayName) ($($_.WindowsPath))"
}
$listView.SetSource($displayItems)
$dialog.Add($listView)
# Store installations in script scope for button handler
$script:TempInstallations = $Installations
$script:SelectedInstallation = $null
# OK button
$btnOK = [Terminal.Gui.Button]::new()
$btnOK.Text = "OK"
$btnOK.X = [Terminal.Gui.Pos]::Center() - 10
$btnOK.Y = [Terminal.Gui.Pos]::Bottom($listView) + 1
$btnOK.Width = 10
$btnOK.Height = 1
$btnOK.IsDefault = $true
$btnOK.add_Clicked({
$selectedIndex = $listView.SelectedItem
if ($selectedIndex -ge 0 -and $selectedIndex -lt $script:TempInstallations.Count) {
$script:SelectedInstallation = $script:TempInstallations[$selectedIndex]
Write-DebugLog "Selected installation: $($script:SelectedInstallation.WindowsPath)"
[Terminal.Gui.Application]::RequestStop()
}
})
$dialog.AddButton($btnOK)
# Cancel button
$btnCancel = [Terminal.Gui.Button]::new()
$btnCancel.Text = "Cancel"
$btnCancel.X = [Terminal.Gui.Pos]::Center() + 2
$btnCancel.Y = [Terminal.Gui.Pos]::Bottom($listView) + 1
$btnCancel.Width = 10
$btnCancel.Height = 1
$btnCancel.add_Clicked({
$script:SelectedInstallation = $null
[Terminal.Gui.Application]::RequestStop()
})
$dialog.AddButton($btnCancel)
# Show dialog
[Terminal.Gui.Application]::Run($dialog)
# Clean up temp variable
$result = $script:SelectedInstallation
$script:TempInstallations = $null
$script:SelectedInstallation = $null
return $result
}
#endregion
#region Terminal.Gui Initialization
function Initialize-TerminalGui {
<#
.SYNOPSIS
Initialize Terminal.Gui framework
#>
try {
$dllPath = $null
# Try to find Terminal.Gui.dll in multiple locations
$searchPaths = @()
# Only add script directory paths if PSScriptRoot is available (not when run via irm | iex)
if ($PSScriptRoot) {
$searchPaths += (Join-Path $PSScriptRoot "Terminal.Gui.dll")
$searchPaths += (Join-Path $PSScriptRoot "lib\Terminal.Gui.dll")
}
# Module path (for installed module)
$modulePath = Get-Module -ListAvailable -Name Microsoft.PowerShell.ConsoleGuiTools |
Select-Object -First 1 -ExpandProperty ModuleBase
if ($modulePath) {
$searchPaths += (Join-Path $modulePath "Terminal.Gui.dll")
}
# Find first valid DLL path
foreach ($path in $searchPaths) {
if ($path -and (Test-Path $path)) {
$dllPath = $path
Write-Verbose "Found Terminal.Gui.dll at: $dllPath"
break
}
}
# If DLL not found, try to import or install module
if (-not $dllPath) {
Write-Host "Microsoft.PowerShell.ConsoleGuiTools module is required for GUI mode." -ForegroundColor Yellow
Write-Host ""
try {
Import-Module Microsoft.PowerShell.ConsoleGuiTools -ErrorAction Stop
$module = (Get-Module Microsoft.PowerShell.ConsoleGuiTools).ModuleBase
$dllPath = Join-Path $module "Terminal.Gui.dll"
Write-Host "Module loaded successfully!" -ForegroundColor Green
}
catch {
# Module not installed - offer to install it
Write-Host "Module not found. Installing automatically in 3 seconds..." -ForegroundColor Yellow
Write-Host "(You can still use headless modes: -Mode Export or -Mode Import)" -ForegroundColor Gray
Write-Host ""
# Countdown
for ($i = 3; $i -gt 0; $i--) {
Write-Host " Installing in $i..." -ForegroundColor Cyan -NoNewline
Start-Sleep -Seconds 1
Write-Host "`r" -NoNewline
}
Write-Host ""
try {
Write-Host "Installing Microsoft.PowerShell.ConsoleGuiTools module..." -ForegroundColor Yellow
Install-Module Microsoft.PowerShell.ConsoleGuiTools -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
Write-Host "Module installed successfully!" -ForegroundColor Green
Write-Host ""
# Import the newly installed module
Import-Module Microsoft.PowerShell.ConsoleGuiTools -ErrorAction Stop
$module = (Get-Module Microsoft.PowerShell.ConsoleGuiTools).ModuleBase
$dllPath = Join-Path $module "Terminal.Gui.dll"
}
catch {
Write-Host "Failed to install module: $_" -ForegroundColor Red
Write-Host "" -ForegroundColor Red
Write-Host "You can install it manually with:" -ForegroundColor Yellow
Write-Host " Install-Module Microsoft.PowerShell.ConsoleGuiTools -Scope CurrentUser" -ForegroundColor White
Write-Host "" -ForegroundColor Yellow
Write-Host "Or use headless modes without the module:" -ForegroundColor Cyan
Write-Host " .\Driver-Manager.ps1 -Mode Export -TargetDrive Auto -Force" -ForegroundColor White
Write-Host " .\Driver-Manager.ps1 -Mode Import -Force" -ForegroundColor White
exit 1
}
}
}
# Verify DLL exists
if (-not (Test-Path $dllPath)) {
throw "Terminal.Gui.dll not found at: $dllPath"
}
# Load the DLL
Add-Type -Path $dllPath
# Initialize application
[Terminal.Gui.Application]::Init()
return $true
}
catch {
Write-Host "Failed to initialize Terminal.Gui: $_" -ForegroundColor Red
Write-Host "" -ForegroundColor Red
Write-Host "You can still use headless modes:" -ForegroundColor Cyan
Write-Host " .\Driver-Manager.ps1 -Mode Export -TargetDrive Auto -Force" -ForegroundColor White
Write-Host " .\Driver-Manager.ps1 -Mode Import -Force" -ForegroundColor White
return $false
}
}
#endregion
#region Driver Export Functions (from Export-Drivers.ps1)
function Select-ExportTargetDisk {
<#
.SYNOPSIS
Select a target disk for driver export from available volumes.
.DESCRIPTION
This function lists all available volumes and prompts the user to select a drive letter,
then returns the selected drive letter with a colon appended if not already present.
Prioritizes external/removable drives for faster scanning.
In PE mode, filters out the PE drive and the offline Windows installation drive.
.PARAMETER IncludeInternal
Include internal fixed drives in the list. Default is to show only removable/external drives first.
.EXAMPLE
$ExternalDrive = Select-ExportTargetDisk
#>
[CmdletBinding()]
[OutputType([string])]
param (
[switch]$IncludeInternal
)
$Volumes = (Get-Volume | Select-Object -Property DriveLetter, FileSystemLabel, FileSystemType, Size, SizeRemaining, DriveType)
# Filter out volumes without drive letters
$Volumes = $Volumes | Where-Object { $_.DriveLetter }
# Build list of drives to exclude
$excludeDrives = @()
# In PE mode, exclude PE drive (typically X:) and offline Windows drive
if ($script:IsWindowsPE) {
# Exclude PE drive (typically X:)
if (Test-Path "X:\Windows\System32") {
$excludeDrives += 'X'
Write-DebugLog "Excluding PE drive: X:"
}
# Exclude the drive containing offline Windows installation
if ($script:OfflineWindowsDrive) {
$offlineDriveLetter = $script:OfflineWindowsDrive.TrimEnd(':', '\')
$excludeDrives += $offlineDriveLetter
Write-DebugLog "Excluding offline Windows drive: $offlineDriveLetter"
}
}
# Filter out excluded drives
if ($excludeDrives.Count -gt 0) {
$Volumes = $Volumes | Where-Object { $_.DriveLetter -notin $excludeDrives }
}
# If not including internal, filter to only Removable and external drives
# In PE mode, be more permissive since USB drives may appear as Fixed
if (-not $IncludeInternal) {
if ($script:IsWindowsPE) {
# In PE: Show all remaining drives (already filtered out X: and Windows drive)
# Just exclude common system drives
$Volumes = $Volumes | Where-Object { $_.DriveLetter -notin @('C') }
}
else {
# Normal Windows: Filter to removable/network only
$Volumes = $Volumes | Where-Object { $_.DriveType -in @('Removable', 'Network') -or ($_.DriveType -eq 'Fixed' -and $_.DriveLetter -notin @('C', 'D')) }
}
}
return $Volumes
}
function Initialize-ExportPath {
<#
.SYNOPSIS
Validate that the selected export path exists and create it if it does not.
.DESCRIPTION
This function validates the export path for driver export, ensuring that it exists or creating it if necessary.
If the directory already exists, it prompts the user for confirmation to overwrite it.
.PARAMETER ExternalDrive
The drive letter of the external drive where drivers will be exported. Provided by Select-ExportTargetDisk.
.PARAMETER Directory
The directory where drivers will be exported. In my case, this is ".drivers".
.PARAMETER Model
The model name of the machine, used to find and/or create a subdirectory to use for the driver export.
.EXAMPLE
$DriverPath = Initialize-ExportPath -ExternalDrive "D:" -Directory ".drivers" -Model "OptiPlex 9020"
#>
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory = $true)]
[string]$ExternalDrive,
[string]$Directory,
[string]$Model,
[switch]$AutoConfirm
)
$AssembledPath = "$($ExternalDrive)\$($Directory)\$($Model)"
# Try to resolve the exact path first
if (Test-Path $AssembledPath) {
$ResolvedPath = (Resolve-Path $AssembledPath).Path
} else {
# Try to find a near match containing the model name
$PathMatches = Get-ChildItem -Path "$($ExternalDrive)\$($Directory)" -Directory -Filter "*$($Model)*" -ErrorAction SilentlyContinue
# use the first match if found
if ($PathMatches) {
$ResolvedPath = $PathMatches[0].FullName
} else { # otherwise use the exact model name
$ResolvedPath = $AssembledPath
}
}
# if directory already exists, prompt for confirmation to overwrite
if (Test-Path $ResolvedPath 2> $null) {
if (-not $AutoConfirm) {
# Return special value to indicate confirmation needed
return @{
Path = $ResolvedPath
NeedsConfirmation = $true
}
}
# Auto-confirmed, remove existing
try {
Remove-Item $ResolvedPath -Recurse -Force -ErrorAction Stop | Out-Null
}
catch {
throw "An error occurred while removing the existing directory: $($_.Exception.Message)"
}
}
try {
# attempt to create the directory if it does not exist or if we've removed it
if (-not (Test-Path -Path $ResolvedPath)) {
New-Item -ItemType Directory -Path $ResolvedPath -Force -ErrorAction Stop | Out-Null
}
}
catch {
throw "An error occurred while creating the path: $($_.Exception.Message)"
}
return @{
Path = $ResolvedPath
NeedsConfirmation = $false
}
}
function Invoke-DriverExport {
<#
.SYNOPSIS
Export drivers from the online or offline Windows image
.PARAMETER DriverPath
Destination path for exported drivers
.PARAMETER ProgressCallback
Scriptblock to call for progress updates
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$DriverPath,
[scriptblock]$ProgressCallback
)
try {
if ($ProgressCallback) {
& $ProgressCallback -Status "Exporting drivers..." -Percent 0
}
# Export drivers - check if we're in offline mode or online mode
if ($script:IsWindowsPE -and $script:OfflineWindowsPath) {
# Offline mode: Export from offline Windows installation
Write-DebugLog "Exporting drivers from offline Windows: $($script:OfflineWindowsPath)"
Write-DebugLog "Export destination: $DriverPath"
# Verify the offline Windows path exists
if (-not (Test-Path $script:OfflineWindowsPath)) {
throw "Offline Windows path does not exist: $($script:OfflineWindowsPath)"
}
# IMPORTANT: Export-WindowsDriver -Path expects the DRIVE ROOT, not the Windows folder
# Example: If Windows is at D:\Windows, pass "D:\" to -Path
$driveLetter = $script:OfflineWindowsDrive
if (-not $driveLetter) {
# Extract drive from Windows path (e.g., "D:\Windows" -> "D:\")
$driveLetter = (Split-Path $script:OfflineWindowsPath -Qualifier) + "\"
}
Write-DebugLog "Using drive root for DISM: $driveLetter"
# Verify the drive root exists
if (-not (Test-Path $driveLetter)) {
throw "Drive root does not exist: $driveLetter"
}
# Use DISM to export drivers from offline image
Write-DebugLog "Running: Export-WindowsDriver -Path '$driveLetter' -Destination '$DriverPath'"
$result = Export-WindowsDriver -Path $driveLetter -Destination $DriverPath -ErrorAction Stop -Verbose 4>&1
Write-DebugLog "Export result: $result"
}
else {
# Online mode: Export from running system
Write-DebugLog "Exporting drivers from online system"
Export-WindowsDriver -Online -Destination $DriverPath -ErrorAction Stop
}
if ($ProgressCallback) {
& $ProgressCallback -Status "Export complete!" -Percent 100
}
return $true
}
catch {
$errorMsg = "Driver export failed: $($_.Exception.Message)"
Write-DebugLog "ERROR: $errorMsg"
Write-DebugLog "Full error: $_"
throw $errorMsg
}
}
#endregion
#region Driver Import Functions (from Import-Drivers.ps1)
function Get-DriverFiles {
<#
.SYNOPSIS
Search for driver files for a machine's model
in a .drivers directory at the root of mounted drives.
.INPUTS
$Model:
the machine's Win32_ComputerSystem.Model identifier, e.g. '21G2002CUS'
or 'Dell Pro 16 PC16250' This is the directory that the function
will search for in the .drivers directory.
.OUTPUTS
$DriverFiles:
.inf children of located system model driver directory, if found
$false, if not found.
.DESCRIPTION
Searches for .drivers\MachineModel (e.g. .drivers\21G2002CUS) directory at the
root of all system drives.
Returns objects pertaining to the driver files found in the model's directory,
and the discovered .drivers directory itself.
#>
param (
[Parameter(Mandatory = $true)]
[string]$Model
)
# Search for a .drivers directory at the root of all filesystem drives
$DriverDirectoryRoot = Get-PSDrive -PSProvider FileSystem |
Where-Object { Test-Path "$($_.Root).drivers" } |
Select-Object -ExpandProperty Root -First 1
if ($DriverDirectoryRoot) {
$DriverDirectoryRoot = "$DriverDirectoryRoot.drivers"
$Directories = Get-ChildItem -Path $DriverDirectoryRoot -Directory
# Try exact match (case-insensitive)
$DriverDirectory = $Directories | Where-Object { $_.Name -ieq $Model } | Select-Object -First 1
# If not found, try wildcard match (case-insensitive)
if (-not $DriverDirectory) {
$DriverDirectory = $Directories | Where-Object { $_.Name -ilike "*$($Model)*" } | Select-Object -First 1
}
if ($DriverDirectory) {
# Get all .inf files in the driver directory and its subdirectories
$DriverFiles = Get-ChildItem -Path $DriverDirectory.FullName -Recurse -Include "*.inf"
if ($DriverFiles) {
return @{
Files = $DriverFiles
Directory = $DriverDirectory
Success = $true
}
} else {
return @{
Files = $null
Directory = $DriverDirectory
Success = $false
Error = "No driver files (.inf) found in $($DriverDirectory.FullName)"
}
}
} else {
return @{
Files = $null
Directory = $null
Success = $false
Error = "No directory for device model '$($Model)' found at $($DriverDirectoryRoot)"
}
}
} else {
return @{
Files = $null
Directory = $null
Success = $false
Error = "No '.drivers' directory found at the root of drives on this system"
}
}
}
function Install-Drivers {
<#
.SYNOPSIS
Install an array of .infs with pnputil.
.INPUTS
Object[] $DriverFiles:
Output from Get-DriverFiles - an array of System.IO.FileSystemInfo objects
pointing to driver .inf files to be installed.
.OUTPUTS
Hashtable with SuccessCount and FailureCount
#>
param (
[Parameter(Mandatory = $true)]
[Object[]]$DriverFiles,
[scriptblock]$ProgressCallback
)
$TotalDrivers = $DriverFiles.Count
$CurrentDriver, $SuccessCount, $FailureCount = 0, 0, 0
foreach ($DriverFile in $DriverFiles) {
$CurrentDriver++
$Percent = [math]::Round(($CurrentDriver / $TotalDrivers) * 100)
if ($ProgressCallback) {
& $ProgressCallback -Status "Installing driver $CurrentDriver of $TotalDrivers" -Percent $Percent -CurrentFile $DriverFile.Name
}
try {
# cannot use Add-WindowsDriver here as it does not support online installation
$Result = & pnputil.exe /add-driver "$($DriverFile.FullName)" /install 2>&1
if ($Result -match "Failed") {
$FailureCount++
} else {
$SuccessCount++
}
} catch {
$FailureCount++
}
}
return @{
SuccessCount = $SuccessCount
FailureCount = $FailureCount
TotalDrivers = $TotalDrivers
}
}
#endregion
#region GUI Application
function Show-DriverManagerGui {
<#
.SYNOPSIS
Main Terminal.Gui application window
#>
Write-DebugLog "Show-DriverManagerGui started"
# Get system information
# If in offline mode, get model from offline Windows; otherwise from current system
if ($script:IsWindowsPE -and $script:OfflineWindowsPath) {
# For offline mode, try to get the actual model from the offline Windows registry
Write-DebugLog "Offline mode: Working with $($script:OfflineWindowsPath)"
$Model = Get-OfflineWindowsModel -WindowsPath $script:OfflineWindowsPath
$Manufacturer = "Offline"
if ($Model -ne "Offline Windows") {
Write-DebugLog "Retrieved offline model: $Model"
}
else {
Write-DebugLog "Could not retrieve model from offline Windows, using default"
}
}
else {
# Wrap in try-catch for PE environment where CIM queries may fail
try {
$Model = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Model
$Manufacturer = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Manufacturer
Write-DebugLog "System: $Manufacturer $Model"
}
catch {
Write-DebugLog "Failed to get CIM instance (PE environment?): $_"
$Model = "Unknown"
$Manufacturer = "Unknown"
}
}
# Create main window
$MainWindow = [Terminal.Gui.Window]::new()
$MainWindow.Title = "Windows Driver Manager"
$MainWindow.X = 0
$MainWindow.Y = 1
$MainWindow.Width = [Terminal.Gui.Dim]::Fill()
$MainWindow.Height = [Terminal.Gui.Dim]::Fill()
# Create menu bar - check if Top is available first
if ([Terminal.Gui.Application]::Top) {
$MenuBar = [Terminal.Gui.MenuBar]::new()
$MenuBar.Menus = @(
[Terminal.Gui.MenuBarItem]::new("_File", @(
[Terminal.Gui.MenuItem]::new("_Quit", "", { [Terminal.Gui.Application]::RequestStop() })
)),
[Terminal.Gui.MenuBarItem]::new("_Help", @(
[Terminal.Gui.MenuItem]::new("_About", "", {
$aboutText = "Windows Driver Manager v1.0`n`n"
if ($script:IsWindowsPE) {
$aboutText += "Mode: Windows PE (Offline)`n"
$aboutText += "Target: $($script:OfflineWindowsPath)`n`n"
}
else {
$aboutText += "System: $Manufacturer $Model`n`n"
}
$aboutText += "Backup and restore Windows drivers`n" +
"using an intuitive terminal interface.`n`n" +
"Created by Seton"
[Terminal.Gui.MessageBox]::Query(50, 12, "About", $aboutText, "OK")
})
))
)
[Terminal.Gui.Application]::Top.Add($MenuBar)
}
else {
Write-DebugLog "Warning: Terminal.Gui.Application.Top is null, skipping menu bar"
}
# Logo/Title with border
$lblTitle = [Terminal.Gui.Label]::new()
$lblTitle.X = 0
$lblTitle.Y = 1
$lblTitle.Width = [Terminal.Gui.Dim]::Fill()
$lblTitle.Height = 5
$lblTitle.TextAlignment = [Terminal.Gui.TextAlignment]::Centered
if ($script:IsWindowsPE) {
$lblTitle.Text = @"
Windows Driver Manager v1.0 - PE Mode
Import Disabled - Export Only
"@
} else {
$lblTitle.Text = @"
Windows Driver Manager v1.0
"@
}
$MainWindow.Add($lblTitle)
# System info
$lblSysInfo = [Terminal.Gui.Label]::new()
$lblSysInfo.X = 0
$lblSysInfo.Y = [Terminal.Gui.Pos]::Bottom($lblTitle)
$lblSysInfo.Width = [Terminal.Gui.Dim]::Fill()
$lblSysInfo.Height = 2
$lblSysInfo.TextAlignment = [Terminal.Gui.TextAlignment]::Centered
if ($script:IsWindowsPE -and $script:OfflineWindowsPath) {
$lblSysInfo.Text = "Model: $Model`nOffline Windows: $($script:OfflineWindowsPath)"
}
else {
$lblSysInfo.Text = "System: $Manufacturer $Model"
}
$MainWindow.Add($lblSysInfo)
# Set current Y position for buttons
$currentY = [Terminal.Gui.Pos]::Bottom($lblSysInfo)
# Buttons - fixed widths for consistent centering
$buttonWidth = 20
$buttonGap = 2
$totalButtonWidth = ($buttonWidth * 2) + $buttonGap
# Calculate starting X position (assuming 80 char wide terminal)
$terminalWidth = 80
$buttonsStartX = [int][Math]::Floor(($terminalWidth - $totalButtonWidth) / 2)
# Export button
$btnExport = [Terminal.Gui.Button]::new()
$btnExport.Text = "Export Drivers"
$btnExport.X = $buttonsStartX
$btnExport.Y = $currentY + 2
$btnExport.Width = $buttonWidth
$btnExport.Height = 1
$btnExport.add_Clicked({
Show-ExportDialog -Model $Model
})
$MainWindow.Add($btnExport)
# Import button
$btnImport = [Terminal.Gui.Button]::new()
$btnImport.Text = "Import Drivers"
$btnImport.X = $buttonsStartX + $buttonWidth + $buttonGap
$btnImport.Y = $currentY + 2
$btnImport.Width = $buttonWidth
$btnImport.Height = 1
# Disable import button in offline/PE mode (just gray out, keep same text)
if ($script:IsWindowsPE) {
$btnImport.Enabled = $false
Write-DebugLog "Import button disabled in PE mode"
}
else {
$btnImport.add_Clicked({
Show-ImportDialog -Model $Model -Manufacturer $Manufacturer
})
}
$MainWindow.Add($btnImport)
# Instructions
$lblInstructions = [Terminal.Gui.Label]::new()
$lblInstructions.X = 0
$lblInstructions.Y = [Terminal.Gui.Pos]::Bottom($btnExport) + 3
$lblInstructions.Width = [Terminal.Gui.Dim]::Fill()
$lblInstructions.Height = 5
$lblInstructions.TextAlignment = [Terminal.Gui.TextAlignment]::Centered
if ($script:IsWindowsPE) {
$lblInstructions.Text = @"
Export: Backup all drivers from offline Windows to external drive
Import: Not available in PE mode (offline Windows)
Press Ctrl+Q to quit
"@
}
else {
$lblInstructions.Text = @"
Export: Backup all drivers from this system to external drive
Import: Restore drivers from external drive to this system
Press Ctrl+Q to quit
"@
}
$MainWindow.Add($lblInstructions)
# Add main window to application
[Terminal.Gui.Application]::Top.Add($MainWindow)
# Run the application
[Terminal.Gui.Application]::Run()
}
function Show-ExportDialog {
param(
[string]$Model,
[switch]$ShowInternal
)
# Get available volumes (external only by default)
$Volumes = Select-ExportTargetDisk -IncludeInternal:$ShowInternal
# Check if we have volumes
if (-not $Volumes -or $Volumes.Count -eq 0) {
if (-not $ShowInternal) {
# No external drives found, offer to show internal
$result = [Terminal.Gui.MessageBox]::Query("No External Drives Found",
"No external/removable drives found.`nWould you like to view internal drives?",
@("Yes", "No"))
if ($result -eq 0) {
Show-ExportDialog -Model $Model -ShowInternal
return
}
} else {
[Terminal.Gui.MessageBox]::ErrorQuery("No Volumes Found", "No available volumes found for export.", "OK")
}
return
}
# Create dialog
$Dialog = [Terminal.Gui.Dialog]::new()
$Dialog.Title = "Export Drivers" + $(if ($ShowInternal) { " - All Drives" } else { " - External Drives" })
$Dialog.Width = 80
$Dialog.Height = 22
# Instructions
$lblInstr = [Terminal.Gui.Label]::new()
$lblInstr.X = 1
$lblInstr.Y = 1
$lblInstr.Width = [Terminal.Gui.Dim]::Fill(2)
$lblInstr.Height = 2
$lblInstr.Text = "Select destination drive for driver export:`n[*] = Existing .drivers folder found"
$Dialog.Add($lblInstr)
# ListView for volumes
$lvVolumes = [Terminal.Gui.ListView]::new()
$lvVolumes.X = 1
$lvVolumes.Y = [Terminal.Gui.Pos]::Bottom($lblInstr) + 1
$lvVolumes.Width = [Terminal.Gui.Dim]::Fill(2)
$lvVolumes.Height = 8
# Format volume list with indicator for existing .drivers folder
$volumeList = $Volumes | ForEach-Object {
$label = if ($_.FileSystemLabel) { $_.FileSystemLabel } else { "(No Label)" }
$sizeMB = [math]::Round($_.Size / 1MB, 0)
$hasDrivers = Test-Path "$($_.DriveLetter):\.drivers"
$indicator = if ($hasDrivers) { "[*] " } else { " " }
"$indicator$($_.DriveLetter): - $label - $($_.FileSystemType) - $sizeMB MB"
}
$lvVolumes.SetSource($volumeList)
$Dialog.Add($lvVolumes)
# Store volumes in script scope for button closure
$script:ExportVolumes = $Volumes
$script:ExportModel = $Model
# OK button
$btnOK = [Terminal.Gui.Button]::new()
$btnOK.Text = "Export"
$btnOK.IsDefault = $true
$btnOK.add_Clicked({
$selectedIndex = $lvVolumes.SelectedItem
if ($selectedIndex -ge 0 -and $selectedIndex -lt $script:ExportVolumes.Count) {
$selectedVolume = $script:ExportVolumes[$selectedIndex]
$driveLetter = "$($selectedVolume.DriveLetter):"
[Terminal.Gui.Application]::RequestStop()
Start-DriverExportProcess -DriveLetter $driveLetter -Model $script:ExportModel
}
})
$Dialog.AddButton($btnOK)
# Show All Drives button (if currently showing external only)
if (-not $ShowInternal) {
$btnShowAll = [Terminal.Gui.Button]::new()
$btnShowAll.Text = "Show All Drives"
$btnShowAll.add_Clicked({
[Terminal.Gui.Application]::RequestStop()
Show-ExportDialog -Model $script:ExportModel -ShowInternal
})
$Dialog.AddButton($btnShowAll)
}
# Cancel button
$btnCancel = [Terminal.Gui.Button]::new()
$btnCancel.Text = "Cancel"
$btnCancel.add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
$Dialog.AddButton($btnCancel)
[Terminal.Gui.Application]::Run($Dialog)
}
function Start-DriverExportProcess {
param(
[string]$DriveLetter,
[string]$Model
)
Write-DebugLog "Start-DriverExportProcess called: Drive=$DriveLetter, Model=$Model"
# Initialize export path
Write-DebugLog "Calling Initialize-ExportPath..."
$pathResult = Initialize-ExportPath -ExternalDrive $DriveLetter -Directory ".drivers" -Model $Model
Write-DebugLog "Initialize-ExportPath returned: Path=$($pathResult.Path), NeedsConfirmation=$($pathResult.NeedsConfirmation)"
if ($pathResult.NeedsConfirmation) {
Write-DebugLog "Showing overwrite confirmation dialog"
$overwrite = [Terminal.Gui.MessageBox]::Query("Confirm Overwrite",
"Directory '$($pathResult.Path)' already exists.`nDo you want to overwrite it?",
@("Yes", "No"))
if ($overwrite -eq 0) {
Write-DebugLog "User confirmed overwrite"
# User confirmed, retry with auto-confirm
$pathResult = Initialize-ExportPath -ExternalDrive $DriveLetter -Directory ".drivers" -Model $Model -AutoConfirm
} else {
Write-DebugLog "User cancelled overwrite, aborting export"
return
}
}
$exportPath = $pathResult.Path
Write-DebugLog "Final export path: $exportPath"
# Show progress dialog
Write-DebugLog "Creating progress dialog..."
$progressDialog = [Terminal.Gui.Dialog]::new()
$progressDialog.Title = "Exporting Drivers"
$progressDialog.Width = 70
$progressDialog.Height = 12
$lblStatus = [Terminal.Gui.Label]::new()
$lblStatus.X = 1
$lblStatus.Y = 1
$lblStatus.Width = [Terminal.Gui.Dim]::Fill(2)
$lblStatus.Height = 2
$lblStatus.Text = "Preparing to export drivers..."
$progressDialog.Add($lblStatus)
$progressBar = [Terminal.Gui.ProgressBar]::new()
$progressBar.X = 1
$progressBar.Y = [Terminal.Gui.Pos]::Bottom($lblStatus) + 1
$progressBar.Width = [Terminal.Gui.Dim]::Fill(2)
$progressBar.Height = 1
$progressBar.Fraction = [float]0.0
$progressDialog.Add($progressBar)
# Close button (disabled initially)
$btnClose = [Terminal.Gui.Button]::new()
$btnClose.Text = "Close"
$btnClose.Enabled = $false
$btnClose.add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
$progressDialog.AddButton($btnClose)
# Determine if we're in PE mode with offline Windows
$isOfflineMode = ($script:IsWindowsPE -and $script:OfflineWindowsPath)
$offlineDrive = if ($isOfflineMode) {
if ($script:OfflineWindowsDrive) {
$script:OfflineWindowsDrive
} else {
(Split-Path $script:OfflineWindowsPath -Qualifier) + "\"
}
} else {
$null
}
Write-DebugLog "Export mode: $(if ($isOfflineMode) { 'Offline (PE)' } else { 'Online' })"
if ($isOfflineMode) {
Write-DebugLog "Offline Windows drive: $offlineDrive"
}
# Don't enumerate beforehand - do it in the background job to avoid UI freeze
Write-DebugLog "Starting ThreadJob for export to: $exportPath (enumeration will happen in background)"
# Create progress tracking file for total count
$countFile = Join-Path $env:TEMP "driver-export-count-$(Get-Random).json"
# Start export in background
$exportJob = Start-ThreadJob -ScriptBlock {
param($Path, $LogPath, $IsOfflineMode, $OfflineDrive, $CountFile)
# Create inner logging function
function Write-JobLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
Add-Content -Path $LogPath -Value "[$timestamp] [JOB] $Message" -Force
}
Write-JobLog "ThreadJob started, exporting to: $Path"
Write-JobLog "Mode: $(if ($IsOfflineMode) { "Offline from $OfflineDrive" } else { 'Online' })"
try {
# Create destination directory if it doesn't exist
if (-not (Test-Path $Path)) {
New-Item -ItemType Directory -Path $Path -Force | Out-Null
Write-JobLog "Created directory: $Path"
}
# Enumerate drivers to get total count (this is slow but necessary)
Write-JobLog "Enumerating drivers..."
if ($IsOfflineMode) {
$driverList = Get-WindowsDriver -Path $OfflineDrive -ErrorAction Stop
}
else {
$driverList = Get-WindowsDriver -Online -ErrorAction Stop
}
$totalDrivers = @($driverList).Count
Write-JobLog "Found $totalDrivers drivers to enumerate and export"
# Write total count to file so UI can switch to determinate progress
@{ Total = $totalDrivers } | ConvertTo-Json | Set-Content -Path $CountFile -Force
# Export all drivers using DISM (automatically creates organized folders per driver)
Write-JobLog "Starting bulk export with Export-WindowsDriver..."
if ($IsOfflineMode) {
Write-JobLog "Running: Export-WindowsDriver -Path '$OfflineDrive' -Destination '$Path'"
$exportResult = Export-WindowsDriver -Path $OfflineDrive -Destination $Path -ErrorAction Stop
}
else {
Write-JobLog "Running: Export-WindowsDriver -Online -Destination '$Path'"
$exportResult = Export-WindowsDriver -Online -Destination $Path -ErrorAction Stop
}
Write-JobLog "Export-WindowsDriver completed"
# Count exported drivers by counting .inf files in subdirectories
$exportedDrivers = Get-ChildItem -Path $Path -Filter "*.inf" -Recurse -ErrorAction SilentlyContinue
$driverCount = @($exportedDrivers).Count
Write-JobLog "Found $driverCount exported driver .inf files"
# Clean up count file
Remove-Item -Path $CountFile -Force -ErrorAction SilentlyContinue
if ($driverCount -gt 0) {
Write-JobLog "Export completed successfully: $driverCount driver(s)"
return @{
Success = $true
Error = $null
SuccessCount = $driverCount
FailureCount = 0
}
} else {
Write-JobLog "Export completed but no drivers were found"
return @{
Success = $false
Error = "No drivers were exported"
SuccessCount = 0
FailureCount = 0
}
}
}
catch {
Write-JobLog "Export failed: $($_.Exception.Message)"
Write-JobLog "Full error: $_"
Remove-Item -Path $CountFile -Force -ErrorAction SilentlyContinue
return @{
Success = $false
Error = $_.Exception.Message
SuccessCount = 0
FailureCount = 0
}
}
} -ArgumentList $exportPath, $script:DebugLogPath, $isOfflineMode, $offlineDrive, $countFile
Write-DebugLog "ThreadJob started, ID: $($exportJob.Id), State: $($exportJob.State)"
# Store job and UI elements in script scope for timeout access
$script:CurrentExportJob = $exportJob
$script:CurrentProgressBar = $progressBar
$script:CurrentStatusLabel = $lblStatus
$script:CurrentCloseButton = $btnClose
$script:CurrentExportPath = $exportPath
$script:CurrentCountFile = $countFile
$script:CurrentExportTotalDrivers = 0 # Will be updated once count file is written
# Create a timeout to poll the job (Terminal.Gui's built-in timeout mechanism)
Write-DebugLog "Creating timeout for job monitoring..."
$pollAction = {
if ($script:CurrentExportJob.State -ne 'Running') {
Write-DebugLog "Job completed with state: $($script:CurrentExportJob.State)"
$result = Receive-Job -Job $script:CurrentExportJob -Wait
Write-DebugLog "Job result: Success=$($result.Success), Error=$($result.Error), Exported=$($result.SuccessCount), Failed=$($result.FailureCount)"
if ($result.Success) {
$script:CurrentProgressBar.Fraction = [float]1.0
$script:CurrentStatusLabel.Text = "Export completed successfully! Exported $($result.SuccessCount) driver(s)."
} else {
$script:CurrentStatusLabel.Text = "Export failed: $($result.Error)"
}
$script:CurrentCloseButton.Enabled = $true
# Return false to stop polling
return $false
} else {
# Job still running - check if we have the total count yet
try {
# Try to read total count from file (written by background job after enumeration)
if (($script:CurrentExportTotalDrivers -eq 0) -and (Test-Path $script:CurrentCountFile)) {
$countData = Get-Content -Path $script:CurrentCountFile -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
if ($countData -and $countData.Total) {
$script:CurrentExportTotalDrivers = $countData.Total
Write-DebugLog "Got total driver count from background job: $($script:CurrentExportTotalDrivers)"
}
}
# Count how many driver folders have been created
$exportedFolders = Get-ChildItem -Path $script:CurrentExportPath -Directory -ErrorAction SilentlyContinue
$currentCount = @($exportedFolders).Count
if ($script:CurrentExportTotalDrivers -gt 0) {
# We know the total - show determinate progress
$fraction = [Math]::Min(1.0, [float]$currentCount / [float]$script:CurrentExportTotalDrivers)
$script:CurrentProgressBar.Fraction = [float]$fraction
$percent = [Math]::Round($fraction * 100, 0)
$script:CurrentStatusLabel.Text = "Exporting drivers... ($currentCount / $($script:CurrentExportTotalDrivers)) - $percent%"
} else {
# Still analyzing - show indeterminate progress
$script:CurrentProgressBar.Fraction = [float]0.5
if ($currentCount -gt 0) {
$script:CurrentStatusLabel.Text = "Exporting drivers... ($currentCount so far)"
} else {
$script:CurrentStatusLabel.Text = "Analyzing installed drivers... Please wait..."
}
}
}
catch {
# If we can't check the folder, show generic message
$script:CurrentProgressBar.Fraction = [float]0.5
$script:CurrentStatusLabel.Text = "Exporting drivers... Please wait..."
}
# Return true to continue polling
return $true
}
}
# Add timeout that fires every 500ms
$timeout = [Terminal.Gui.Application]::MainLoop.AddTimeout([TimeSpan]::FromMilliseconds(500), $pollAction)
Write-DebugLog "Timeout added"
# Show dialog
Write-DebugLog "Calling Application.Run() for progress dialog..."
[Terminal.Gui.Application]::Run($progressDialog)
Write-DebugLog "Application.Run() returned"
Write-DebugLog "Start-DriverExportProcess completed"
}
function Show-ImportDialog {
param(
[string]$Model,
[string]$Manufacturer
)
# Search for drivers
$driverResult = Get-DriverFiles -Model $Model
if (-not $driverResult.Success) {
[Terminal.Gui.MessageBox]::ErrorQuery("Driver Search Failed", $driverResult.Error, "OK")
return
}
# Show confirmation dialog
$driverCount = $driverResult.Files.Count
$driverPath = $driverResult.Directory.FullName
$confirm = [Terminal.Gui.MessageBox]::Query("Confirm Import",
"Found $driverCount driver(s) in:`n$driverPath`n`nProceed with installation?",
@("Yes", "No"))
if ($confirm -ne 0) {
return
}
# Start import process
Start-DriverImportProcess -DriverFiles $driverResult.Files -DriverDirectory $driverResult.Directory
}
function Start-DriverImportProcess {
param(
[Object[]]$DriverFiles,
[Object]$DriverDirectory
)
# Show progress dialog
$progressDialog = [Terminal.Gui.Dialog]::new()
$progressDialog.Title = "Installing Drivers"
$progressDialog.Width = 70
$progressDialog.Height = 14
$lblStatus = [Terminal.Gui.Label]::new()
$lblStatus.X = 1
$lblStatus.Y = 1
$lblStatus.Width = [Terminal.Gui.Dim]::Fill(2)
$lblStatus.Height = 3
$lblStatus.Text = "Installing drivers..."
$progressDialog.Add($lblStatus)
$progressBar = [Terminal.Gui.ProgressBar]::new()
$progressBar.X = 1
$progressBar.Y = [Terminal.Gui.Pos]::Bottom($lblStatus) + 1
$progressBar.Width = [Terminal.Gui.Dim]::Fill(2)
$progressBar.Height = 1
$progressBar.Fraction = [float]0.0
$progressDialog.Add($progressBar)
$lblDetails = [Terminal.Gui.Label]::new()
$lblDetails.X = 1
$lblDetails.Y = [Terminal.Gui.Pos]::Bottom($progressBar) + 1
$lblDetails.Width = [Terminal.Gui.Dim]::Fill(2)
$lblDetails.Height = 2
$lblDetails.Text = ""
$progressDialog.Add($lblDetails)
# Close button
$btnClose = [Terminal.Gui.Button]::new()
$btnClose.Text = "Close"
$btnClose.Enabled = $false
$btnClose.add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
$progressDialog.AddButton($btnClose)
# Create temp file for progress tracking
$progressFile = Join-Path $env:TEMP "driver-import-progress-$(Get-Random).json"
# Start installation in background
$importJob = Start-ThreadJob -ScriptBlock {
param($Files, $ProgressFile)
$TotalDrivers = $Files.Count
$CurrentDriver, $SuccessCount, $FailureCount = 0, 0, 0
foreach ($DriverFile in $Files) {
$CurrentDriver++
# Write current progress to file
$progressData = @{
Current = $CurrentDriver
Total = $TotalDrivers
Success = $SuccessCount
Failed = $FailureCount
CurrentFile = $DriverFile.Name
}
$progressData | ConvertTo-Json | Set-Content -Path $ProgressFile -Force
try {
$Result = & pnputil.exe /add-driver "$($DriverFile.FullName)" /install 2>&1
if ($Result -match "Failed") {
$FailureCount++
} else {
$SuccessCount++
}
} catch {
$FailureCount++
}
}
# Clean up progress file
Remove-Item -Path $ProgressFile -Force -ErrorAction SilentlyContinue
return @{
SuccessCount = $SuccessCount
FailureCount = $FailureCount
TotalDrivers = $TotalDrivers
}
} -ArgumentList @(,$DriverFiles), $progressFile
# Store job and UI elements in script scope for timeout access
$script:CurrentImportJob = $importJob
$script:CurrentImportProgressBar = $progressBar
$script:CurrentImportStatusLabel = $lblStatus
$script:CurrentImportDetailsLabel = $lblDetails
$script:CurrentImportCloseButton = $btnClose
$script:CurrentImportTotalDrivers = $DriverFiles.Count
$script:CurrentImportProgressFile = $progressFile
# Create a timeout to poll the job (Terminal.Gui's built-in timeout mechanism)
$pollAction = {
if ($script:CurrentImportJob.State -ne 'Running') {
$result = Receive-Job -Job $script:CurrentImportJob -Wait
$script:CurrentImportProgressBar.Fraction = [float]1.0
$script:CurrentImportStatusLabel.Text = "Installation complete!"
$script:CurrentImportDetailsLabel.Text = "Success: $($result.SuccessCount) | Failed: $($result.FailureCount) | Total: $($result.TotalDrivers)"
$script:CurrentImportCloseButton.Enabled = $true
# Clean up progress file if it still exists
if (Test-Path $script:CurrentImportProgressFile) {
Remove-Item -Path $script:CurrentImportProgressFile -Force -ErrorAction SilentlyContinue
}
# Return false to stop polling
return $false
} else {
# Read progress from file
if (Test-Path $script:CurrentImportProgressFile) {
try {
$progress = Get-Content -Path $script:CurrentImportProgressFile -Raw | ConvertFrom-Json
$fraction = [float]$progress.Current / [float]$progress.Total
$script:CurrentImportProgressBar.Fraction = $fraction
$script:CurrentImportStatusLabel.Text = "Installing drivers... ($($progress.Current) / $($progress.Total))"
$script:CurrentImportDetailsLabel.Text = "Success: $($progress.Success) | Failed: $($progress.Failed)`nCurrent: $($progress.CurrentFile)"
}
catch {
# If we can't read progress, fall back to indeterminate
$script:CurrentImportProgressBar.Fraction = [float]0.5
$script:CurrentImportStatusLabel.Text = "Installing drivers..."
$script:CurrentImportDetailsLabel.Text = "Processing $($script:CurrentImportTotalDrivers) driver(s)..."
}
} else {
$script:CurrentImportProgressBar.Fraction = [float]0.5
$script:CurrentImportStatusLabel.Text = "Installing drivers..."
$script:CurrentImportDetailsLabel.Text = "Processing $($script:CurrentImportTotalDrivers) driver(s)..."
}
# Return true to continue polling
return $true
}
}
# Add timeout that fires every 500ms
$timeout = [Terminal.Gui.Application]::MainLoop.AddTimeout([TimeSpan]::FromMilliseconds(500), $pollAction)
# Show dialog
[Terminal.Gui.Application]::Run($progressDialog)
}
#endregion
#region Headless Mode Functions
function Invoke-HeadlessExport {
<#
.SYNOPSIS
Perform headless driver export
#>
param(
[string]$TargetDrive,
[string]$Directory,
[switch]$Force,
[switch]$Quiet
)
# Get model - handle PE environment where CIM may fail
if ($script:IsWindowsPE -and $script:OfflineWindowsPath) {
$Model = Get-OfflineWindowsModel -WindowsPath $script:OfflineWindowsPath
if ($Model -eq "Offline Windows") {
Write-Host "Warning: Could not detect model from offline Windows" -ForegroundColor Yellow
}
}
else {
try {
$Model = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Model
}
catch {
Write-Host "Warning: Failed to get system model, using 'Unknown'" -ForegroundColor Yellow
$Model = "Unknown"
}
}
if (-not $Quiet) {
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Driver Export - Headless Mode" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if ($script:IsWindowsPE) {
Write-Host "Mode: Windows PE (Offline)" -ForegroundColor Yellow
Write-Host "Target: $($script:OfflineWindowsPath)" -ForegroundColor White
}
else {
Write-Host "System Model: $Model" -ForegroundColor White
}
Write-Host ""
}
# Determine target drive
if ($TargetDrive -eq "Auto") {
if (-not $Quiet) { Write-Host "Auto-detecting removable drive..." -ForegroundColor Yellow }
$removableDrive = Get-Volume | Where-Object {
$_.DriveType -eq 'Removable' -and $_.DriveLetter
} | Select-Object -First 1
if (-not $removableDrive) {
Write-Host "ERROR: No removable drive found" -ForegroundColor Red
return 1
}
$TargetDrive = "$($removableDrive.DriveLetter):"
if (-not $Quiet) { Write-Host "Found: $TargetDrive" -ForegroundColor Green }
}
# Ensure drive letter has colon
if ($TargetDrive -notlike "*:") {
$TargetDrive += ":"
}
# Validate target drive exists
if (-not (Test-Path $TargetDrive)) {
Write-Host "ERROR: Target drive '$TargetDrive' not found" -ForegroundColor Red
return 1
}
if (-not $Quiet) {
Write-Host "Target Drive: $TargetDrive" -ForegroundColor White
Write-Host "Export Path: $TargetDrive\$Directory\$Model" -ForegroundColor White
Write-Host ""
}
# Initialize export path
try {
$pathResult = Initialize-ExportPath -ExternalDrive $TargetDrive -Directory $Directory -Model $Model
if ($pathResult.NeedsConfirmation) {
if ($Force) {
if (-not $Quiet) { Write-Host "Overwriting existing directory (forced)..." -ForegroundColor Yellow }
$pathResult = Initialize-ExportPath -ExternalDrive $TargetDrive -Directory $Directory -Model $Model -AutoConfirm
} else {
Write-Host "Directory already exists: $($pathResult.Path)" -ForegroundColor Yellow
$confirm = Read-Host "Overwrite? (Y/N)"
if ($confirm -eq 'Y' -or $confirm -eq 'y') {
$pathResult = Initialize-ExportPath -ExternalDrive $TargetDrive -Directory $Directory -Model $Model -AutoConfirm
} else {
Write-Host "Export cancelled by user" -ForegroundColor Yellow
return 0
}
}
}
$exportPath = $pathResult.Path
if (-not $Quiet) {
Write-Host "Exporting drivers from Windows image..." -ForegroundColor Yellow
Write-Host "This may take several minutes..." -ForegroundColor Gray
}
# Create transcript
$transcriptPath = "$env:TEMP\driver-export-$Model-$(Get-Date -UFormat %s).log"
Start-Transcript -Path $transcriptPath | Out-Null
# Perform export - check if we're in offline mode or online mode
if ($script:IsWindowsPE -and $script:OfflineWindowsPath) {
# Offline mode: Export from offline Windows installation
# IMPORTANT: Export-WindowsDriver -Path expects the DRIVE ROOT, not the Windows folder
$driveLetter = $script:OfflineWindowsDrive
if (-not $driveLetter) {
# Extract drive from Windows path (e.g., "D:\Windows" -> "D:\")
$driveLetter = (Split-Path $script:OfflineWindowsPath -Qualifier) + "\"
}
if (-not $Quiet) {
Write-Host "Exporting from offline Windows at: $driveLetter" -ForegroundColor Cyan
}
Export-WindowsDriver -Path $driveLetter -Destination $exportPath -ErrorAction Stop
}
else {
# Online mode: Export from running system
Export-WindowsDriver -Online -Destination $exportPath -ErrorAction Stop
}
Stop-Transcript | Out-Null
# Move transcript to export directory
Move-Item -Path $transcriptPath -Destination $exportPath -Force
if (-not $Quiet) {
Write-Host ""
Write-Host "Export completed successfully!" -ForegroundColor Green
Write-Host "Drivers exported to: $exportPath" -ForegroundColor Green
Write-Host "Log file: $exportPath\$(Split-Path $transcriptPath -Leaf)" -ForegroundColor Gray
}
return 0
} catch {
Write-Host "ERROR: Export failed - $($_.Exception.Message)" -ForegroundColor Red
return 1
}
}
function Invoke-HeadlessImport {
<#
.SYNOPSIS
Perform headless driver import
#>
param(
[string]$SourceDrive,
[string]$Directory,
[switch]$Force,
[switch]$Quiet
)
# Get system information - handle PE environment where CIM may fail
try {
$Model = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Model
$Manufacturer = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Manufacturer
$SerialNumber = (Get-CimInstance -Class Win32_BIOS -ErrorAction Stop).SerialNumber
}
catch {
Write-Host "Warning: Failed to get system information, using defaults" -ForegroundColor Yellow
$Model = "Unknown"
$Manufacturer = "Unknown"
$SerialNumber = "Unknown"
}
if (-not $Quiet) {
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Driver Import - Headless Mode" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "System: $Manufacturer $Model" -ForegroundColor White
Write-Host "Serial: $SerialNumber" -ForegroundColor White
Write-Host ""
}
# Search for drivers
if (-not $Quiet) { Write-Host "Searching for driver directory..." -ForegroundColor Yellow }
$driverResult = Get-DriverFiles -Model $Model
if (-not $driverResult.Success) {
Write-Host "ERROR: $($driverResult.Error)" -ForegroundColor Red
return 1
}
$driverCount = $driverResult.Files.Count
$driverPath = $driverResult.Directory.FullName
if (-not $Quiet) {
Write-Host "Found: $driverPath" -ForegroundColor Green
Write-Host "Driver count: $driverCount" -ForegroundColor White
Write-Host ""
}
# Confirm if not forced
if (-not $Force) {
Write-Host "Ready to install $driverCount driver(s)" -ForegroundColor Yellow
$confirm = Read-Host "Proceed? (Y/N)"
if ($confirm -ne 'Y' -and $confirm -ne 'y') {
Write-Host "Import cancelled by user" -ForegroundColor Yellow
return 0
}
}
# Create transcript
$transcriptPath = "$env:TEMP\driver-import-$Manufacturer-$Model-$SerialNumber-$(Get-Date -UFormat %s).log"
Start-Transcript -Path $transcriptPath | Out-Null
if (-not $Quiet) {
Write-Host "Installing drivers..." -ForegroundColor Yellow
Write-Host "This may take several minutes..." -ForegroundColor Gray
Write-Host ""
}
# Install drivers
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$progressCallback = if (-not $Quiet) {
{
param($Status, $Percent, $CurrentFile)
Write-Host "[$Percent%] $Status" -ForegroundColor Cyan
if ($CurrentFile) {
Write-Host " Current: $CurrentFile" -ForegroundColor Gray
}
}
} else {
$null
}
$result = Install-Drivers -DriverFiles $driverResult.Files -ProgressCallback $progressCallback
$stopwatch.Stop()
Stop-Transcript | Out-Null
# Move transcript to driver directory
Move-Item -Path $transcriptPath -Destination $driverPath -Force
if (-not $Quiet) {
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Import Complete" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Duration: $($stopwatch.Elapsed.TotalSeconds) seconds" -ForegroundColor White
Write-Host "Total drivers: $($result.TotalDrivers)" -ForegroundColor White
Write-Host "Successful: $($result.SuccessCount)" -ForegroundColor Green
Write-Host "Failed: $($result.FailureCount)" -ForegroundColor $(if ($result.FailureCount -gt 0) { 'Red' } else { 'Gray' })
Write-Host "Log file: $driverPath\$(Split-Path $transcriptPath -Leaf)" -ForegroundColor Gray
Write-Host ""
}
if ($result.SuccessCount -gt 0) {
return 0
} else {
return 1
}
}
#endregion
#region Main Entry Point
# Show debug log location
Write-Host "Debug log: $script:DebugLogPath" -ForegroundColor Gray
Write-Host ""
# Detect Windows PE environment
$script:IsWindowsPE = Test-WindowsPE
if ($script:IsWindowsPE) {
Write-Host "Windows PE environment detected" -ForegroundColor Yellow
Write-DebugLog "Windows PE environment detected"
# Get offline Windows installations
$offlineInstallations = Get-OfflineWindowsInstallations
if ($offlineInstallations.Count -eq 0) {
Write-Host "ERROR: No offline Windows installations found" -ForegroundColor Red
Write-Host "Please ensure a Windows installation is available on a connected drive" -ForegroundColor Yellow
Write-DebugLog "No offline Windows installations found"
exit 1
}
Write-Host "Found $($offlineInstallations.Count) offline Windows installation(s)" -ForegroundColor Green
Write-DebugLog "Found $($offlineInstallations.Count) offline Windows installation(s)"
# For GUI mode, show selection dialog
if ($Mode -eq 'GUI') {
# Initialize Terminal.Gui first for the selection dialog
if (-not (Initialize-TerminalGui)) {
exit 1
}
# Show selection dialog
$selectedInstallation = Show-WindowsInstallationSelector -Installations $offlineInstallations
if (-not $selectedInstallation) {
Write-Host "No Windows installation selected. Exiting." -ForegroundColor Yellow
[Terminal.Gui.Application]::Shutdown()
exit 0
}
# Store selected installation info
$script:OfflineWindowsPath = $selectedInstallation.WindowsPath
$script:OfflineWindowsDrive = $selectedInstallation.Drive
Write-Host "Selected: $($selectedInstallation.WindowsPath)" -ForegroundColor Green
Write-DebugLog "Selected offline Windows: $($script:OfflineWindowsPath)"
}
else {
# For headless modes, auto-select the first (or only) installation
$selectedInstallation = $offlineInstallations[0]
$script:OfflineWindowsPath = $selectedInstallation.WindowsPath
$script:OfflineWindowsDrive = $selectedInstallation.Drive
Write-Host "Auto-selected: $($selectedInstallation.WindowsPath)" -ForegroundColor Green
Write-DebugLog "Auto-selected offline Windows: $($script:OfflineWindowsPath)"
}
# Store PE drive letter
if (Test-Path "X:\Windows\System32") {
$script:PEDriveLetter = "X:"
Write-DebugLog "PE drive: X:"
}
Write-Host ""
}
# Determine operation mode
if ($Mode -eq 'Export') {
# Headless export mode
if (-not $TargetDrive) {
Write-Host "ERROR: -TargetDrive is required for Export mode" -ForegroundColor Red
Write-Host "Use -TargetDrive 'Auto' for automatic detection or specify drive letter (e.g., 'D:')" -ForegroundColor Yellow
exit 1
}
$exitCode = Invoke-HeadlessExport -TargetDrive $TargetDrive -Directory $Directory -Force:$Force -Quiet:$Quiet
exit $exitCode
}
elseif ($Mode -eq 'Import') {
# Headless import mode
$exitCode = Invoke-HeadlessImport -SourceDrive $SourceDrive -Directory $Directory -Force:$Force -Quiet:$Quiet
exit $exitCode
}
else {
# GUI mode (default)
# Initialize Terminal.Gui if not already initialized (PE mode initializes it earlier)
if (-not $script:IsWindowsPE) {
if (-not (Initialize-TerminalGui)) {
exit 1
}
}
try {
# Show main GUI
Show-DriverManagerGui
}
finally {
# Shutdown Terminal.Gui
[Terminal.Gui.Application]::Shutdown()
}
}
#endregion