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>
This commit is contained in:
Claude 2026-01-12 16:39:43 -05:00
commit b06374a562
14 changed files with 4822 additions and 0 deletions

31
.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
# Build directories
work/
output/
# Build artifacts
*.wim
*.iso
*.log
# PowerShell 7 downloads
PowerShell-*.zip
# Git credentials (sensitive)
gitcreds.txt
# PowerShell modules (too large, use Update-Modules.ps1 to download)
modules/
# Windows temp files
Thumbs.db
Desktop.ini
# Editor files
.vscode/
.vs/
*.swp
*~
# Claude temp files
.claude/
tmpclaude-*

516
Build-PE.ps1 Normal file
View file

@ -0,0 +1,516 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Builds a customized Windows PE image with PowerShell 7 and script launcher.
.DESCRIPTION
Automates the complete process of creating a WinPE image that includes:
- PowerShell 7 integration
- Custom PowerShell modules
- Automatic script launcher on boot
- Detection of external media with .scripts folder
.PARAMETER SkipADKCheck
Skip Windows ADK installation check (not recommended).
.PARAMETER CleanBuild
Removes existing work directory for a clean build.
.PARAMETER DefenderOptimization
Optimize Windows Defender for faster build performance:
- None: No changes to Defender (default, safest)
- DisableRealTime: Temporarily disable real-time protection (fastest, 50-70% improvement, restored after build)
- AddExclusions: Add build directories to exclusions (balanced, 20-30% improvement, cleaned up after)
.EXAMPLE
.\Build-PE.ps1
Performs a standard build with all checks
.EXAMPLE
.\Build-PE.ps1 -CleanBuild
Removes previous build artifacts and starts fresh
.EXAMPLE
.\Build-PE.ps1 -DefenderOptimization DisableRealTime
Builds with real-time protection temporarily disabled for maximum speed
.EXAMPLE
.\Build-PE.ps1 -DefenderOptimization AddExclusions
Adds build directories to Defender exclusions during build
.NOTES
Requires Windows 10/11 with administrator privileges
First run will download and install Windows ADK (~4GB, 20-30 min)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[switch]$SkipADKCheck,
[Parameter(Mandatory = $false)]
[switch]$CleanBuild,
[Parameter(Mandatory = $false)]
[ValidateSet('None', 'DisableRealTime', 'AddExclusions')]
[string]$DefenderOptimization = 'None'
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$Script:StartTime = Get-Date
$Script:LogPath = Join-Path $PSScriptRoot "output\build.log"
#region Logging
function Write-Log {
param(
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
if (-not (Test-Path (Split-Path $Script:LogPath))) {
New-Item -ItemType Directory -Path (Split-Path $Script:LogPath) -Force | Out-Null
}
Add-Content -Path $Script:LogPath -Value $logMessage
}
function Write-Status {
param(
[string]$Message,
[string]$Type = "Info"
)
$color = switch ($Type) {
"Success" { "Green" }
"Warning" { "Yellow" }
"Error" { "Red" }
"Info" { "Cyan" }
"Progress" { "Magenta" }
default { "White" }
}
$prefix = switch ($Type) {
"Success" { "[✓]" }
"Warning" { "[!]" }
"Error" { "[✗]" }
"Info" { "[i]" }
"Progress" { "[*]" }
default { "[-]" }
}
Write-Host "$prefix " -ForegroundColor $color -NoNewline
Write-Host $Message
Write-Log -Message $Message -Level $Type.ToUpper()
}
function Write-Header {
param([string]$Title)
$line = "=" * 80
Write-Host ""
Write-Host $line -ForegroundColor Cyan
Write-Host " $Title" -ForegroundColor Yellow
Write-Host $line -ForegroundColor Cyan
Write-Host ""
}
#endregion
#region Initialization
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Cyan
Write-Host " WinPE Script Launcher Builder" -ForegroundColor Yellow
Write-Host " Production Build System" -ForegroundColor White
Write-Host "================================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Log "Build started" "INFO"
Write-Status "Build log: $Script:LogPath" "Info"
Write-Host ""
if (Test-Path $Script:LogPath) {
Clear-Content $Script:LogPath
}
#endregion
#region Import Modules
Write-Header "Loading Build Modules"
$libModules = @(
"Dependencies.psm1",
"WinPEBuilder.psm1",
"PowerShell7.psm1",
"ModuleManager.psm1",
"DefenderOptimization.psm1"
)
foreach ($module in $libModules) {
$modulePath = Join-Path $PSScriptRoot "lib\$module"
if (-not (Test-Path $modulePath)) {
Write-Status "Module not found: $module" "Error"
throw "Required module missing: $modulePath"
}
try {
Import-Module $modulePath -Force -ErrorAction Stop
Write-Status "Loaded: $module" "Success"
}
catch {
Write-Status "Failed to load: $module" "Error"
throw "Module import failed: $_"
}
}
Write-Host ""
#endregion
#region Load Configuration
Write-Header "Loading Configuration"
$configPath = Join-Path $PSScriptRoot "config.json"
if (-not (Test-Path $configPath)) {
Write-Status "Configuration file not found: $configPath" "Error"
throw "config.json is required"
}
try {
$config = Get-Content $configPath -Raw | ConvertFrom-Json
Write-Status "Configuration loaded successfully" "Success"
Write-Status "PowerShell 7 Version: $($config.powershell7Version)" "Info"
Write-Status "Startup Script: $(if ($config.startupScript) {$config.startupScript} else {'Invoke-ScriptLauncher.ps1'})" "Info"
Write-Status "Target Script (External): $($config.targetScript)" "Info"
Write-Status "Fallback Script (Offline): $(if ($config.fallbackScript) {$config.fallbackScript} else {'Invoke-ScriptMenu.ps1'})" "Info"
Write-Status "Scripts Folder: $($config.scriptsFolder)" "Info"
Write-Status "Modules: $($config.powershellModules.Count) configured" "Info"
}
catch {
Write-Status "Failed to parse configuration" "Error"
throw "Configuration error: $_"
}
Write-Host ""
#endregion
#region Dependency Checks
Write-Header "Validating Dependencies"
Write-Status "Checking administrator privileges..." "Progress"
if (-not (Test-AdminPrivileges)) {
Write-Status "Administrator privileges required" "Error"
throw "Please run this script as Administrator"
}
Write-Status "Running with administrator privileges" "Success"
if (-not $SkipADKCheck) {
Write-Status "Checking Windows ADK installation..." "Progress"
if (-not (Test-WindowsADK)) {
Write-Status "Windows ADK not found" "Warning"
Write-Host ""
Write-Host "The Windows Assessment and Deployment Kit (ADK) is required." -ForegroundColor Yellow
Write-Host "This will download and install:" -ForegroundColor White
Write-Host " - Windows ADK (~150MB download)" -ForegroundColor Cyan
Write-Host " - WinPE Add-on (~5GB download)" -ForegroundColor Cyan
Write-Host " - Installation time: ~20-30 minutes" -ForegroundColor Cyan
Write-Host ""
$response = Read-Host "Install Windows ADK now? (Y/N)"
if ($response -ne 'Y' -and $response -ne 'y') {
Write-Status "Build cancelled by user" "Warning"
exit 0
}
Install-WindowsADK
Write-Status "Windows ADK installed successfully" "Success"
}
else {
Write-Status "Windows ADK is installed" "Success"
}
}
Write-Status "Checking PowerShell 7 archive..." "Progress"
$ps7FileName = "PowerShell-$($config.powershell7Version)-win-x64.zip"
$ps7Path = Join-Path $PSScriptRoot $ps7FileName
if (-not (Test-PowerShell7Archive -Config $config)) {
Write-Status "PowerShell 7 archive not found" "Warning"
Write-Status "Downloading PowerShell $($config.powershell7Version)..." "Progress"
Get-PowerShell7Archive -Config $config
Write-Status "PowerShell 7 downloaded" "Success"
}
else {
Write-Status "PowerShell 7 archive found" "Success"
}
Write-Host ""
#endregion
#region Clean Build Option
if ($CleanBuild) {
Write-Header "Clean Build"
$workDir = Join-Path $PSScriptRoot "work"
if (Test-Path $workDir) {
Write-Status "Removing existing work directory..." "Progress"
try {
Remove-Item $workDir -Recurse -Force
Write-Status "Work directory cleaned" "Success"
}
catch {
Write-Status "Failed to clean work directory" "Warning"
Write-Status "Error: $($_.Exception.Message)" "Warning"
}
}
Write-Host ""
}
#endregion
#region Build Process
# Windows Defender Optimization
$defenderState = $null
$defenderExclusionsAdded = $false
if ($DefenderOptimization -ne 'None') {
Write-Header "Windows Defender Optimization"
if (-not (Test-DefenderAvailable)) {
Write-Status "Windows Defender not available on this system" "Warning"
Write-Status "Continuing without Defender optimization" "Info"
Write-Host ""
}
else {
try {
if ($DefenderOptimization -eq 'DisableRealTime') {
Write-Status "Mode: Temporarily disable real-time protection" "Info"
Write-Status "Disabling Windows Defender real-time protection..." "Progress"
$defenderState = Disable-DefenderRealTimeProtection
if ($null -ne $defenderState) {
Write-Status "Real-time protection disabled for build duration" "Success"
Write-Status "Will be restored automatically when build completes" "Info"
Write-Status "Expected performance improvement: 50-70%" "Info"
}
}
elseif ($DefenderOptimization -eq 'AddExclusions') {
Write-Status "Mode: Add build directories to exclusions" "Info"
Write-Status "Adding build directories to Defender exclusions..." "Progress"
Add-DefenderExclusions -PSScriptRoot $PSScriptRoot
$defenderExclusionsAdded = $true
Write-Status "Exclusions added (will be removed after build)" "Success"
Write-Status "Expected performance improvement: 20-30%" "Info"
}
}
catch {
Write-Status "Defender optimization failed: $($_.Exception.Message)" "Warning"
Write-Status "Continuing without Defender optimization" "Info"
}
Write-Host ""
}
}
try {
Write-Header "Building WinPE Image"
Write-Status "Step 1/10: Initializing WinPE workspace" "Progress"
Initialize-WinPEWorkspace
Write-Status "Workspace initialized" "Success"
Write-Host ""
Write-Status "Step 2/10: Mounting WinPE image" "Progress"
Mount-WinPEImage
$mountDir = Get-MountDirectory
Write-Status "Image mounted at: $mountDir" "Success"
Write-Host ""
Write-Status "Step 3/10: Adding optional components" "Progress"
$components = @(
"WinPE-WMI",
"WinPE-NetFX",
"WinPE-Scripting",
"WinPE-PowerShell",
"WinPE-StorageWMI",
"WinPE-DismCmdlets"
)
foreach ($component in $components) {
Add-WinPEOptionalComponent -Name $component
}
Write-Status "Optional components installed" "Success"
Write-Host ""
Write-Status "Step 4/10: Setting scratch space" "Progress"
Set-WinPEScratchSpace -SizeMB 512
Write-Host ""
Write-Status "Step 5/10: Installing PowerShell 7" "Progress"
Install-PowerShell7ToWinPE -ZipPath $ps7Path -MountDir $mountDir
Write-Host ""
Write-Status "Step 6/10: Copying PowerShell modules" "Progress"
if ($config.powershellModules -and $config.powershellModules.Count -gt 0) {
foreach ($moduleName in $config.powershellModules) {
$modulePath = Get-RepositoryModulePath -ModuleName $moduleName
Install-ModuleToWinPE -ModuleName $moduleName -SourcePath $modulePath -MountDir $mountDir
}
Write-Status "All modules copied successfully" "Success"
}
else {
Write-Status "No modules configured - skipping" "Info"
}
Write-Host ""
Write-Status "Step 7/10: Copying launcher scripts and configuration" "Progress"
# Copy startup script
$startupScriptName = if ($config.startupScript) { $config.startupScript } else { "Invoke-ScriptLauncher.ps1" }
$startupSource = Join-Path $PSScriptRoot "resources\$startupScriptName"
$startupDest = Join-Path $mountDir "Windows\System32\$startupScriptName"
if (Test-Path $startupSource) {
Copy-Item -Path $startupSource -Destination $startupDest -Force
Write-Status "Startup script copied: $startupScriptName" "Success"
}
else {
Write-Status "Startup script not found: $startupScriptName" "Error"
throw "Startup script missing: $startupSource"
}
# Copy fallback script
$fallbackScriptName = if ($config.fallbackScript) { $config.fallbackScript } else { "Invoke-ScriptMenu.ps1" }
$fallbackSource = Join-Path $PSScriptRoot "resources\$fallbackScriptName"
$fallbackDest = Join-Path $mountDir "Windows\System32\$fallbackScriptName"
if (Test-Path $fallbackSource) {
Copy-Item -Path $fallbackSource -Destination $fallbackDest -Force
Write-Status "Fallback script copied: $fallbackScriptName" "Success"
}
else {
Write-Status "Fallback script not found: $fallbackScriptName" "Error"
throw "Fallback script missing: $fallbackSource"
}
# Copy configuration file
$configDest = Join-Path $mountDir "Windows\System32\winpe-config.json"
Copy-Item -Path $configPath -Destination $configDest -Force
Write-Status "Configuration copied to image" "Success"
Write-Host ""
Write-Status "Step 8/10: Configuring startup script" "Progress"
Set-PowerShell7Environment -MountDir $mountDir -LauncherScriptPath $startupScriptName
Write-Host ""
Write-Status "Step 9/10: Unmounting and committing changes" "Progress"
Dismount-WinPEImage
Write-Host ""
Write-Status "Step 10/10: Creating bootable ISO" "Progress"
New-BootableMedia -OutputPath (Join-Path $PSScriptRoot "output")
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Green
Write-Status "BUILD COMPLETED SUCCESSFULLY!" "Success"
Write-Host "================================================================================" -ForegroundColor Green
Write-Host ""
$outputISO = Join-Path $PSScriptRoot "output\WinPE.iso"
$isoSize = (Get-Item $outputISO).Length / 1MB
Write-Status "Output: $outputISO" "Success"
Write-Status "Size: $([math]::Round($isoSize, 2)) MB" "Info"
$elapsed = (Get-Date) - $Script:StartTime
Write-Status "Build time: $($elapsed.ToString('mm\:ss'))" "Info"
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " 1. Test ISO in virtual machine (Hyper-V, VirtualBox, etc.)" -ForegroundColor Cyan
Write-Host " 2. Create .scripts folder on external media" -ForegroundColor Cyan
Write-Host " 3. Add your scripts to .scripts folder" -ForegroundColor Cyan
Write-Host " 4. Boot the WinPE image" -ForegroundColor Cyan
Write-Host ""
Write-Log "Build completed successfully" "INFO"
}
catch {
Write-Host ""
Write-Host "================================================================================" -ForegroundColor Red
Write-Status "BUILD FAILED!" "Error"
Write-Host "================================================================================" -ForegroundColor Red
Write-Host ""
Write-Status "Error: $($_.Exception.Message)" "Error"
Write-Host ""
Write-Status "Check log file for details: $Script:LogPath" "Info"
Write-Host ""
Write-Log "Build failed: $($_.Exception.Message)" "ERROR"
try {
Write-Status "Attempting to unmount image..." "Warning"
Dismount-WinPEImage
}
catch {
Write-Status "Could not unmount image cleanly" "Warning"
Write-Status "Run 'dism /Cleanup-Mountpoints' to clean up" "Info"
}
exit 1
}
finally {
# Restore Windows Defender settings
if ($DefenderOptimization -ne 'None') {
Write-Host ""
Write-Header "Restoring Windows Defender Settings"
try {
if ($DefenderOptimization -eq 'DisableRealTime' -and $null -ne $defenderState) {
Write-Status "Restoring Windows Defender real-time protection..." "Progress"
Restore-DefenderRealTimeProtection -OriginalState $defenderState
Write-Status "Real-time protection restored" "Success"
}
elseif ($DefenderOptimization -eq 'AddExclusions' -and $defenderExclusionsAdded) {
Write-Status "Removing temporary Defender exclusions..." "Progress"
Remove-DefenderExclusions -PSScriptRoot $PSScriptRoot
Write-Status "Exclusions removed" "Success"
}
}
catch {
Write-Status "Failed to restore Defender settings: $($_.Exception.Message)" "Warning"
Write-Status "You may need to manually restore Defender settings" "Warning"
}
Write-Host ""
}
}
#endregion

416
CLAUDE.md Normal file
View file

@ -0,0 +1,416 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Build PE Script Launcher - A production-grade Windows PE builder that creates bootable images with PowerShell 7, custom modules, and automatic script launching from external media. The PE image remains static; rebuilds are only needed for PowerShell 7 or module updates.
## Quick Start
```powershell
# Clone and build
.\Build-PE.ps1
# Update PowerShell modules (optional)
.\Update-Modules.ps1
# Test the ISO in a VM
# Boot WinPE.iso with external media containing .scripts folder
```
## Architecture
### Core Components
**Build-PE.ps1** - Main orchestrator
- Validates dependencies (auto-installs Windows ADK if missing)
- Downloads PowerShell 7 if needed
- Mounts WinPE image via DISM
- Integrates PowerShell 7 and modules
- Injects launcher scripts
- Creates bootable ISO
**config.json** - Configuration
- PowerShell modules list
- Target script name
- Scripts folder name
- PowerShell 7 version and download URL
**lib/** - Build modules
- `Dependencies.psm1` - ADK validation and auto-installation
- `WinPEBuilder.psm1` - DISM operations (mount, unmount, add packages)
- `PowerShell7.psm1` - PS7 extraction and integration
- `ModuleManager.psm1` - Module copying to WinPE image
**resources/** - Runtime scripts
- `Invoke-ScriptLauncher.ps1` - Runs on PE boot, scans for external media
- `Invoke-ScriptMenu.ps1` - Fallback menu for local .scripts folder
**modules/** - PowerShell modules
- Pre-downloaded modules from PSGallery
- Committed to repository for offline builds
- Updated via `Update-Modules.ps1`
### Boot Flow
```
WinPE Boot
startnet.cmd (modified)
Console window maximizes automatically
Invoke-ScriptLauncher.ps1 (PowerShell 7)
Scans all drives for .scripts folder
├─ Found on external media?
│ ↓
│ Execute configured target script
│ (e.g., user's custom Invoke-ScriptMenu.ps1)
└─ Not found?
Check for local X:\.scripts
├─ Found? → Launch Invoke-ScriptMenu.ps1
└─ Not found? → Show instructions, drop to PowerShell 7 prompt
```
**Console Features:**
- **PowerShell 7 by default** - All scripts and the command prompt use PS7
- **Maximized window** - Console automatically maximizes on boot for full-screen experience
- **Elevated privileges** - Everything runs as SYSTEM (no UAC in WinPE)
- **Classic UI** - WinPE uses legacy console rendering (no modern W10/W11 decorations available)
### Build Process Workflow
1. **Dependency Check** - Validates admin rights, ADK installation, PS7 availability
2. **Workspace Init** - Creates work directory using ADK's copype.cmd
3. **Mount Image** - Mounts boot.wim for modification
4. **Add Components** - Installs WinPE optional components (WMI, NetFX, PowerShell, etc.)
5. **Install PS7** - Extracts PowerShell 7 to `Program Files\PowerShell\7`
6. **Copy Modules** - Copies modules from repository to image
7. **Inject Scripts** - Copies launcher and menu scripts to System32
8. **Configure Startup** - Modifies startnet.cmd to run launcher
9. **Unmount** - Commits changes and unmounts image
10. **Create ISO** - Generates bootable ISO using MakeWinPEMedia
## Configuration
### config.json Structure
```json
{
"powershellModules": ["ModuleName1", "ModuleName2"],
"targetScript": "Invoke-ScriptMenu.ps1",
"scriptsFolder": ".scripts",
"startupScript": "Invoke-ScriptLauncher.ps1",
"fallbackScript": "Invoke-ScriptMenu.ps1",
"consoleColors": {
"backgroundColor": "Black",
"foregroundColor": "Gray"
},
"powershell7Version": "7.4.13",
"powershell7ZipUrl": "https://github.com/PowerShell/PowerShell/releases/..."
}
```
**Configuration Options:**
**powershellModules** - Array of module names to include in WinPE (from PSGallery)
**targetScript** - Script name to execute from external media .scripts folder
**scriptsFolder** - Name of folder to search for on external media (default: ".scripts")
**startupScript** - Script that runs first on boot (default: "Invoke-ScriptLauncher.ps1")
- Must exist in resources/ folder
- This is the entry point for your WinPE environment
**fallbackScript** - Script embedded in image for offline/local use (default: "Invoke-ScriptMenu.ps1")
- Must exist in resources/ folder
- Runs when no external media is found but X:\.scripts exists
**consoleColors** - Customize PowerShell console appearance
- backgroundColor: Console background color (Black, DarkBlue, DarkGreen, etc.)
- foregroundColor: Console text color (Gray, White, Cyan, etc.)
**powershell7Version** - Version of PowerShell 7 to integrate
**powershell7ZipUrl** - Direct download URL for PS7 zip archive
## Commands
### Build WinPE Image
```powershell
.\Build-PE.ps1
```
First run downloads Windows ADK (~4GB, 20-30 min), subsequent builds are faster (~10 min).
Options:
- `-CleanBuild` - Remove existing work directory
- `-SkipADKCheck` - Skip ADK validation (not recommended)
- `-DefenderOptimization <Mode>` - Optimize Windows Defender for faster builds (see Performance Optimization below)
### Build with Performance Optimization
Windows Defender real-time scanning significantly impacts DISM performance. Use `-DefenderOptimization` for faster builds:
```powershell
# Fastest: Temporarily disable real-time protection (50-70% improvement)
.\Build-PE.ps1 -DefenderOptimization DisableRealTime
# Balanced: Add build directories to exclusions (20-30% improvement)
.\Build-PE.ps1 -DefenderOptimization AddExclusions
# Default: No Defender changes (safest)
.\Build-PE.ps1 -DefenderOptimization None
```
**Defender optimization modes:**
- `None` (default) - No changes to Defender, safest option
- `DisableRealTime` - Temporarily disable real-time protection for build duration, restored automatically (fastest)
- `AddExclusions` - Add build directories and ADK paths to Defender exclusions, removed after build (balanced)
**Note:** Defender settings are automatically restored when the build completes, even if it fails.
### Update Modules
```powershell
# Update all modules in config.json
.\Update-Modules.ps1
# Update specific module
.\Update-Modules.ps1 -ModuleName "PSWindowsUpdate"
# Force re-download
.\Update-Modules.ps1 -ModuleName "PSWindowsUpdate" -Force
```
## Directory Structure
```
Build PE Script Launcher/
├── Build-PE.ps1 # Main build script
├── Update-Modules.ps1 # Module updater utility
├── config.json # Build configuration
├── lib/ # Build-time modules
│ ├── Dependencies.psm1
│ ├── WinPEBuilder.psm1
│ ├── PowerShell7.psm1
│ ├── ModuleManager.psm1
│ └── DefenderOptimization.psm1
├── resources/ # Runtime scripts
│ ├── Invoke-ScriptLauncher.ps1
│ └── Invoke-ScriptMenu.ps1
├── modules/ # PowerShell modules (committed)
│ └── <ModuleName>/
├── work/ # Build workspace (gitignored)
│ ├── mount/ # Mounted WinPE image
│ └── media/ # WinPE media files
└── output/ # Build outputs (gitignored)
├── WinPE.iso
└── build.log
```
## Key Technical Details
### Windows ADK Integration
- Auto-installs via silent parameters: `/quiet /norestart /features OptionId.DeploymentTools`
- Installs both ADK core and WinPE add-on
- Uses `copype.cmd` to create initial WinPE workspace
- Uses `MakeWinPEMedia.cmd` to create bootable ISO
### PowerShell 7 Integration
- Based on community method (not officially supported by Microsoft)
- Extracts PS7 portable zip to `X:\Program Files\PowerShell\7`
- Modifies startnet.cmd to add PS7 to PATH
- Uses PowerShell 7 LTS (v7.4.13) for 3-year support
### DISM Operations
All image modifications use DISM commands:
- Mount: `dism /Mount-Image /ImageFile:boot.wim /Index:1 /MountDir:mount`
- Add package: `dism /Add-Package /Image:mount /PackagePath:package.cab`
- Unmount: `dism /Unmount-Image /MountDir:mount /Commit`
### Module Management
- Modules stored in `modules/` using `Save-Module` structure
- Copied to `X:\Program Files\WindowsPowerShell\Modules\` in image
- Dependencies automatically resolved by `Update-Modules.ps1`
## Troubleshooting
### Build fails with "Image is already mounted"
```powershell
dism /Cleanup-Mountpoints
```
### ADK installation fails
- Check internet connection
- Ensure sufficient disk space (~10GB)
- Run as Administrator
### Module not found during build
```powershell
# Download missing module
.\Update-Modules.ps1 -ModuleName "ModuleName"
# Or verify config.json module names match PSGallery exactly
```
### PowerShell 7 not in PATH when PE boots
- Check startnet.cmd modification in mounted image
- Verify PS7 extracted to correct path
- Review build.log for errors
## Modifying the Project
### Adding a New Module
1. Add module name to `config.json` powershellModules array
2. Run `.\Update-Modules.ps1 -ModuleName "NewModule"`
3. Rebuild: `.\Build-PE.ps1`
### Changing Scripts
**External Media Script (runs from USB/external drive):**
1. Modify `targetScript` in `config.json`
2. Ensure your script exists in external media's .scripts folder
3. Rebuild: `.\Build-PE.ps1`
**Startup Script (first script that runs on boot):**
1. Create your custom script in `resources/` folder
2. Update `startupScript` in `config.json` with your script name
3. Rebuild: `.\Build-PE.ps1`
4. Script must handle external media detection or call other scripts
**Fallback Script (embedded for offline use):**
1. Create your custom script in `resources/` folder
2. Update `fallbackScript` in `config.json` with your script name
3. Rebuild: `.\Build-PE.ps1`
4. Script will be available at X:\Windows\System32\ when no external media found
### Customizing Console Colors
1. Edit `consoleColors` in `config.json`
2. Choose from: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
3. Rebuild: `.\Build-PE.ps1`
Example - Dark blue theme:
```json
"consoleColors": {
"backgroundColor": "DarkBlue",
"foregroundColor": "White"
}
```
### Upgrading PowerShell 7
1. Update `powershell7Version` and `powershell7ZipUrl` in config.json
2. Delete old PowerShell zip file
3. Rebuild: `.\Build-PE.ps1`
### Customizing Launcher Behavior
Edit `resources/Invoke-ScriptLauncher.ps1`:
- Change drive scanning logic
- Modify error messages
- Add additional checks
- Customize banner display
## Use Cases
### Driver Extraction from Offline Windows
User boots WinPE → Launcher finds .scripts on USB → Runs driver export script
### System Recovery Scripts
Multiple recovery scripts on external media → Menu lets user choose → Script runs with PS7 and modules available
### Automated Deployment
Pre-configured script on known media → Launcher auto-executes → Unattended deployment
## External Media Setup
### For End Users
1. Format USB drive
2. Create `.scripts` folder in root
3. Add target script (e.g., `Invoke-ScriptMenu.ps1`)
4. Add additional scripts
5. Boot WinPE image
### Example .scripts Structure
```
USB:\
└── .scripts\
├── Invoke-ScriptMenu.ps1 # Entry point
├── Export-Drivers.ps1
├── Backup-System.ps1
└── utilities\
└── helpers.psm1
```
## Performance Notes
### Build Times (without optimization)
- First build: 30-45 minutes (ADK download + install)
- Subsequent builds: 5-10 minutes
- ISO size: ~500MB
- Module impact: ~10-50MB per module typically
### Windows Defender Optimization
Windows Defender real-time scanning significantly impacts DISM and ADK performance. Use `-DefenderOptimization` for dramatic speed improvements:
**Performance Impact by Mode:**
| Mode | First Build | Subsequent | Improvement | Risk Level |
|------|-------------|------------|-------------|------------|
| None (default) | 30-45 min | 5-10 min | Baseline | Safest |
| AddExclusions | 24-32 min | 4-7 min | 20-30% faster | Low |
| DisableRealTime | 15-20 min | 2-4 min | 50-70% faster | Low* |
*DisableRealTime temporarily disables protection but automatically restores it when build completes (even on failure).
**Which mode to use:**
- **Development:** Use `DisableRealTime` for fastest builds
- **CI/CD pipelines:** Use `AddExclusions` for good performance with minimal risk
- **Production/Shared systems:** Use `None` (default) for safety
- **Enterprise:** Use `AddExclusions` with approval from security team
**How it works:**
- `DisableRealTime` - Temporarily disables Windows Defender real-time protection during build, restores original state when done
- `AddExclusions` - Adds work/, downloads/, output/, and ADK paths to Defender exclusions during build, removes them when done
- `None` - No changes to Defender (default behavior)
All Defender changes are automatically reverted when the build completes, even if the build fails.
## Security Considerations
- Scripts run with SYSTEM privileges in WinPE
- No execution policy restrictions
- Only use trusted scripts
- Validate all external media scripts before use
- Consider signing scripts for enterprise deployment
- **Defender Optimization:** When using `-DefenderOptimization`, Defender settings are automatically restored after build. Even if build fails, the finally block ensures restoration. Safe for development use.
## Known Limitations
- PowerShell 7 in WinPE is unofficial (works but unsupported)
- Some cmdlets (Get-PhysicalDisk) may not work under PS7 in WinPE
- UAC not present in WinPE (all scripts run elevated)
- Network drivers may need manual addition for some hardware
- **UI Limitations:**
- Console uses legacy Windows console rendering (no modern W10/W11 window decorations)
- WinPE lacks DWM (Desktop Window Manager) and modern shell components
- True borderless fullscreen not available; window maximization is the best alternative
- No Aero, Fluent Design, or modern theming support

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# Build PE Script Launcher
A production-grade Windows PE builder that creates bootable images with PowerShell 7, custom modules, and automatic script launching from external media.
## Quick Start
```powershell
# Build the WinPE ISO
.\Build-PE.ps1
# Update PowerShell modules (optional)
.\Update-Modules.ps1
```
## Features
- **PowerShell 7** - Full PS7 integration in WinPE environment
- **Maximized Console** - Auto-maximizes on boot for full-screen experience
- **Custom Modules** - Include any PSGallery modules
- **External Media Detection** - Automatically scans for scripts on USB drives
- **Offline Fallback** - Built-in script menu when no external media found
- **Configurable** - All settings in config.json
## Documentation
See **[CLAUDE.md](CLAUDE.md)** for complete documentation including:
- Architecture overview
- Configuration options
- Build process
- Customization guide
- Troubleshooting
## Requirements
- Windows 10/11
- Administrator privileges
- ~10GB disk space (for Windows ADK)
## License
MIT License - See LICENSE file for details

201
Update-Modules.ps1 Normal file
View file

@ -0,0 +1,201 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Updates PowerShell modules in the repository for WinPE integration.
.DESCRIPTION
Downloads or updates PowerShell modules from PSGallery to the modules/ directory.
These modules are then committed to the repository and included in WinPE builds.
.PARAMETER ModuleName
Name of a specific module to update. If not specified, updates all modules in config.json.
.PARAMETER Force
Forces re-download even if module already exists.
.EXAMPLE
.\Update-Modules.ps1
Updates all modules listed in config.json
.EXAMPLE
.\Update-Modules.ps1 -ModuleName "PSWindowsUpdate" -Force
Forces update of specific module
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$ModuleName,
[Parameter(Mandatory = $false)]
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$ConfigPath = Join-Path $PSScriptRoot "config.json"
$ModulesRoot = Join-Path $PSScriptRoot "modules"
function Write-Status {
param(
[string]$Message,
[string]$Type = "Info"
)
$color = switch ($Type) {
"Success" { "Green" }
"Warning" { "Yellow" }
"Error" { "Red" }
"Info" { "Cyan" }
default { "White" }
}
$prefix = switch ($Type) {
"Success" { "[+]" }
"Warning" { "[!]" }
"Error" { "[X]" }
"Info" { "[i]" }
default { "[-]" }
}
Write-Host "$prefix " -ForegroundColor $color -NoNewline
Write-Host $Message
}
function Get-ConfiguredModules {
if (-not (Test-Path $ConfigPath)) {
throw "Configuration file not found: $ConfigPath"
}
try {
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
return $config.powershellModules
}
catch {
throw "Failed to read configuration: $_"
}
}
function Initialize-ModulesDirectory {
if (-not (Test-Path $ModulesRoot)) {
Write-Status "Creating modules directory: $ModulesRoot" "Info"
New-Item -ItemType Directory -Path $ModulesRoot | Out-Null
}
}
function Update-PSGalleryModule {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
Write-Host ""
Write-Host "=" * 70 -ForegroundColor Cyan
Write-Status "Processing module: $Name" "Info"
Write-Host "=" * 70 -ForegroundColor Cyan
$modulePath = Join-Path $ModulesRoot $Name
if ((Test-Path $modulePath) -and -not $Force) {
Write-Status "Module already exists: $modulePath" "Warning"
Write-Status "Use -Force to re-download" "Info"
return
}
if (Test-Path $modulePath) {
Write-Status "Removing existing module..." "Warning"
Remove-Item $modulePath -Recurse -Force
}
Write-Status "Searching PSGallery for module: $Name" "Info"
try {
$moduleInfo = Find-Module -Name $Name -Repository PSGallery -ErrorAction Stop
Write-Status "Found: $Name v$($moduleInfo.Version)" "Success"
Write-Status "Description: $($moduleInfo.Description)" "Info"
if ($moduleInfo.Dependencies -and $moduleInfo.Dependencies.Count -gt 0) {
Write-Status "Dependencies detected:" "Warning"
foreach ($dep in $moduleInfo.Dependencies) {
Write-Host " - $($dep.Name)" -ForegroundColor Yellow
}
Write-Status "Dependencies will be downloaded automatically" "Info"
}
Write-Status "Downloading module to repository..." "Info"
Save-Module -Name $Name -Path $ModulesRoot -Repository PSGallery -Force
if (Test-Path $modulePath) {
$savedVersion = (Get-ChildItem $modulePath -Directory | Select-Object -First 1).Name
Write-Status "Successfully saved: $Name v$savedVersion" "Success"
$moduleSizeMB = (Get-ChildItem $modulePath -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Status "Module size: $([math]::Round($moduleSizeMB, 2)) MB" "Info"
}
else {
throw "Module save completed but directory not found"
}
}
catch {
Write-Status "Failed to update module: $Name" "Error"
Write-Status "Error: $($_.Exception.Message)" "Error"
throw
}
}
try {
Write-Host ""
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " PowerShell Module Repository Updater" -ForegroundColor Yellow
Write-Host "================================================" -ForegroundColor Cyan
Write-Host ""
Initialize-ModulesDirectory
if ($ModuleName) {
Write-Status "Single module update requested: $ModuleName" "Info"
$modulesToUpdate = @($ModuleName)
}
else {
Write-Status "Reading configuration: $ConfigPath" "Info"
$modulesToUpdate = Get-ConfiguredModules
if ($null -eq $modulesToUpdate -or $modulesToUpdate.Count -eq 0) {
Write-Status "No modules configured in config.json" "Warning"
Write-Status "Add modules to 'powershellModules' array in config.json" "Info"
exit 0
}
Write-Status "Found $($modulesToUpdate.Count) module(s) in configuration" "Success"
}
Write-Host ""
foreach ($module in $modulesToUpdate) {
Update-PSGalleryModule -Name $module
}
Write-Host ""
Write-Host "================================================" -ForegroundColor Green
Write-Status "Module update completed successfully" "Success"
Write-Host "================================================" -ForegroundColor Green
Write-Host ""
Write-Status "Modules saved to: $ModulesRoot" "Info"
Write-Status "These modules will be included in the next WinPE build" "Info"
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host " 1. Review downloaded modules in: $ModulesRoot" -ForegroundColor Cyan
Write-Host " 2. Commit changes to git if desired" -ForegroundColor Cyan
Write-Host " 3. Run Build-PE.ps1 to create updated WinPE image" -ForegroundColor Cyan
Write-Host ""
}
catch {
Write-Host ""
Write-Status "FATAL ERROR: $($_.Exception.Message)" "Error"
Write-Host ""
exit 1
}

15
config.json Normal file
View file

@ -0,0 +1,15 @@
{
"powershellModules": [
"Microsoft.PowerShell.ConsoleGuiTools"
],
"targetScript": "Driver-Manager.ps1",
"scriptsFolder": ".scripts",
"startupScript": "Invoke-ScriptLauncher.ps1",
"fallbackScript": "Invoke-ScriptMenu.ps1",
"consoleColors": {
"backgroundColor": "Black",
"foregroundColor": "Gray"
},
"powershell7Version": "7.4.13",
"powershell7ZipUrl": "https://github.com/PowerShell/PowerShell/releases/download/v7.4.13/PowerShell-7.4.13-win-x64.zip"
}

View file

@ -0,0 +1,201 @@
function Get-DefenderRealTimeProtectionStatus {
<#
.SYNOPSIS
Gets the current Windows Defender real-time protection status.
#>
[CmdletBinding()]
param()
try {
$preference = Get-MpPreference -ErrorAction Stop
return -not $preference.DisableRealtimeMonitoring
}
catch {
Write-Verbose "Could not get Defender status: $_"
return $null
}
}
function Disable-DefenderRealTimeProtection {
<#
.SYNOPSIS
Temporarily disables Windows Defender real-time protection and returns original state.
.OUTPUTS
Returns the original state (True if enabled, False if already disabled, $null if failed)
#>
[CmdletBinding()]
param()
try {
$originalState = Get-DefenderRealTimeProtectionStatus
if ($null -eq $originalState) {
Write-Warning "Cannot determine Defender status. Skipping optimization."
return $null
}
if (-not $originalState) {
Write-Verbose "Real-time protection already disabled"
return $false
}
Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction Stop
Write-Verbose "Real-time protection disabled successfully"
return $true
}
catch {
Write-Warning "Failed to disable Defender real-time protection: $_"
Write-Warning "Continuing without Defender optimization"
return $null
}
}
function Restore-DefenderRealTimeProtection {
<#
.SYNOPSIS
Restores Windows Defender real-time protection to its original state.
.PARAMETER OriginalState
The state to restore (from Disable-DefenderRealTimeProtection)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[AllowNull()]
$OriginalState
)
if ($null -eq $OriginalState) {
Write-Verbose "No original state to restore"
return
}
if ($OriginalState -eq $false) {
Write-Verbose "Real-time protection was already disabled, leaving as-is"
return
}
try {
Set-MpPreference -DisableRealtimeMonitoring $false -ErrorAction Stop
Write-Verbose "Real-time protection restored successfully"
}
catch {
Write-Warning "Failed to restore Defender real-time protection: $_"
Write-Warning "You may need to manually re-enable it in Windows Security"
}
}
$script:AddedExclusions = @()
function Add-DefenderExclusions {
<#
.SYNOPSIS
Adds build directories and ADK paths to Windows Defender exclusions.
.PARAMETER PSScriptRoot
The root directory of the build script
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PSScriptRoot
)
$script:AddedExclusions = @()
$exclusionPaths = @(
(Join-Path $PSScriptRoot "work"),
(Join-Path $PSScriptRoot "downloads"),
(Join-Path $PSScriptRoot "output"),
"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\DISM",
"C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment"
)
foreach ($path in $exclusionPaths) {
try {
$existingExclusions = (Get-MpPreference).ExclusionPath
if ($existingExclusions -contains $path) {
Write-Verbose "Exclusion already exists: $path"
continue
}
Add-MpPreference -ExclusionPath $path -ErrorAction Stop
$script:AddedExclusions += $path
Write-Verbose "Added exclusion: $path"
}
catch {
Write-Warning "Failed to add Defender exclusion for $path : $_"
}
}
if ($script:AddedExclusions.Count -gt 0) {
Write-Verbose "Added $($script:AddedExclusions.Count) Defender exclusions"
}
else {
Write-Warning "No Defender exclusions were added"
}
}
function Remove-DefenderExclusions {
<#
.SYNOPSIS
Removes Defender exclusions that were added by Add-DefenderExclusions.
.PARAMETER PSScriptRoot
The root directory of the build script
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PSScriptRoot
)
if ($script:AddedExclusions.Count -eq 0) {
Write-Verbose "No exclusions to remove"
return
}
$removedCount = 0
foreach ($path in $script:AddedExclusions) {
try {
Remove-MpPreference -ExclusionPath $path -ErrorAction Stop
Write-Verbose "Removed exclusion: $path"
$removedCount++
}
catch {
Write-Warning "Failed to remove Defender exclusion for $path : $_"
}
}
Write-Verbose "Removed $removedCount of $($script:AddedExclusions.Count) Defender exclusions"
$script:AddedExclusions = @()
}
function Test-DefenderAvailable {
<#
.SYNOPSIS
Tests if Windows Defender cmdlets are available on this system.
#>
[CmdletBinding()]
param()
try {
$null = Get-MpPreference -ErrorAction Stop
return $true
}
catch {
return $false
}
}
Export-ModuleMember -Function @(
'Get-DefenderRealTimeProtectionStatus',
'Disable-DefenderRealTimeProtection',
'Restore-DefenderRealTimeProtection',
'Add-DefenderExclusions',
'Remove-DefenderExclusions',
'Test-DefenderAvailable'
)

196
lib/Dependencies.psm1 Normal file
View file

@ -0,0 +1,196 @@
function Test-AdminPrivileges {
<#
.SYNOPSIS
Verifies script is running with administrator privileges.
#>
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-WindowsADK {
<#
.SYNOPSIS
Checks if Windows ADK and WinPE add-on are installed.
#>
try {
$adkPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit"
$deploymentToolsPath = Join-Path $adkPath "Deployment Tools"
$winPEPath = Join-Path $adkPath "Windows Preinstallation Environment"
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots"
$registryExists = Test-Path $registryPath
if (-not $registryExists) {
Write-Verbose "ADK registry key not found"
return $false
}
if (-not (Test-Path $deploymentToolsPath)) {
Write-Verbose "Deployment Tools not found at: $deploymentToolsPath"
return $false
}
if (-not (Test-Path $winPEPath)) {
Write-Verbose "WinPE add-on not found at: $winPEPath"
return $false
}
$dismPath = Join-Path $deploymentToolsPath "DISM\dism.exe"
$copypePath = Join-Path $winPEPath "copype.cmd"
if (-not (Test-Path $dismPath)) {
Write-Verbose "DISM executable not found"
return $false
}
if (-not (Test-Path $copypePath)) {
Write-Verbose "copype.cmd not found"
return $false
}
Write-Verbose "Windows ADK and WinPE add-on are installed"
return $true
}
catch {
Write-Verbose "Error checking ADK: $_"
return $false
}
}
function Install-WindowsADK {
<#
.SYNOPSIS
Downloads and installs Windows ADK and WinPE add-on silently.
#>
[CmdletBinding()]
param()
Write-Host "`n=== Windows ADK Installation ===" -ForegroundColor Cyan
Write-Host "This will download and install:"
Write-Host " - Windows ADK (~150MB download)"
Write-Host " - WinPE Add-on (~5GB download)"
Write-Host " - Total time: ~20-30 minutes depending on connection`n"
$downloadsDir = ".\downloads"
if (-not (Test-Path $downloadsDir)) {
New-Item -ItemType Directory -Path $downloadsDir | Out-Null
}
$adkInstallerPath = Join-Path $downloadsDir "adksetup.exe"
$winpeInstallerPath = Join-Path $downloadsDir "adkwinpesetup.exe"
$adkUrl = "https://go.microsoft.com/fwlink/?linkid=2271337"
$winpeUrl = "https://go.microsoft.com/fwlink/?linkid=2271338"
try {
Write-Host "[1/4] Downloading Windows ADK installer..." -ForegroundColor Yellow
if (-not (Test-Path $adkInstallerPath)) {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $adkUrl -OutFile $adkInstallerPath -UseBasicParsing
$ProgressPreference = 'Continue'
Write-Host " ✓ ADK installer downloaded" -ForegroundColor Green
} else {
Write-Host " ✓ ADK installer already exists" -ForegroundColor Green
}
Write-Host "[2/4] Installing Windows ADK (this may take 5-10 minutes)..." -ForegroundColor Yellow
$adkProcess = Start-Process -FilePath $adkInstallerPath `
-ArgumentList "/quiet", "/norestart", "/features", "OptionId.DeploymentTools" `
-Wait -PassThru -NoNewWindow
if ($adkProcess.ExitCode -ne 0) {
throw "ADK installation failed with exit code: $($adkProcess.ExitCode)"
}
Write-Host " ✓ Windows ADK installed successfully" -ForegroundColor Green
Write-Host "[3/4] Downloading WinPE add-on installer (large download)..." -ForegroundColor Yellow
if (-not (Test-Path $winpeInstallerPath)) {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $winpeUrl -OutFile $winpeInstallerPath -UseBasicParsing
$ProgressPreference = 'Continue'
Write-Host " ✓ WinPE installer downloaded" -ForegroundColor Green
} else {
Write-Host " ✓ WinPE installer already exists" -ForegroundColor Green
}
Write-Host "[4/4] Installing WinPE add-on (this may take 10-15 minutes)..." -ForegroundColor Yellow
$winpeProcess = Start-Process -FilePath $winpeInstallerPath `
-ArgumentList "/quiet", "/norestart", "/features", "OptionId.WindowsPreinstallationEnvironment" `
-Wait -PassThru -NoNewWindow
if ($winpeProcess.ExitCode -ne 0) {
throw "WinPE add-on installation failed with exit code: $($winpeProcess.ExitCode)"
}
Write-Host " ✓ WinPE add-on installed successfully" -ForegroundColor Green
Write-Host "`n✓ Windows ADK and WinPE add-on installation complete!`n" -ForegroundColor Green
}
catch {
Write-Error "Failed to install Windows ADK: $_"
throw
}
}
function Test-PowerShell7Archive {
<#
.SYNOPSIS
Verifies PowerShell 7 zip archive exists locally.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$Config
)
$ps7FileName = "PowerShell-$($Config.powershell7Version)-win-x64.zip"
$ps7Path = Join-Path (Get-Location) $ps7FileName
if (Test-Path $ps7Path) {
Write-Verbose "PowerShell 7 archive found: $ps7Path"
return $true
}
Write-Verbose "PowerShell 7 archive not found: $ps7Path"
return $false
}
function Get-PowerShell7Archive {
<#
.SYNOPSIS
Downloads PowerShell 7 zip archive from GitHub.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$Config
)
$ps7FileName = "PowerShell-$($Config.powershell7Version)-win-x64.zip"
$ps7Path = Join-Path (Get-Location) $ps7FileName
Write-Host "Downloading PowerShell $($Config.powershell7Version)..." -ForegroundColor Yellow
try {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $Config.powershell7ZipUrl -OutFile $ps7Path -UseBasicParsing
$ProgressPreference = 'Continue'
if (Test-Path $ps7Path) {
Write-Host " ✓ PowerShell 7 downloaded successfully" -ForegroundColor Green
} else {
throw "Download completed but file not found"
}
}
catch {
Write-Error "Failed to download PowerShell 7: $_"
throw
}
}
Export-ModuleMember -Function @(
'Test-AdminPrivileges',
'Test-WindowsADK',
'Install-WindowsADK',
'Test-PowerShell7Archive',
'Get-PowerShell7Archive'
)

129
lib/ModuleManager.psm1 Normal file
View file

@ -0,0 +1,129 @@
function Install-ModuleToWinPE {
<#
.SYNOPSIS
Copies a PowerShell module from the repository to the WinPE image.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ModuleName,
[Parameter(Mandatory = $true)]
[string]$SourcePath,
[Parameter(Mandatory = $true)]
[string]$MountDir
)
Write-Host " Installing module: $ModuleName" -ForegroundColor Cyan
if (-not (Test-Path $SourcePath)) {
throw "Module source path not found: $SourcePath"
}
$winpeModulesPath = Join-Path $MountDir "Program Files\WindowsPowerShell\Modules"
if (-not (Test-Path $winpeModulesPath)) {
New-Item -ItemType Directory -Path $winpeModulesPath -Force | Out-Null
}
$destModulePath = Join-Path $winpeModulesPath $ModuleName
if (Test-Path $destModulePath) {
Write-Host " Removing existing module installation..." -ForegroundColor Yellow
Remove-Item $destModulePath -Recurse -Force
}
try {
Copy-Item -Path $SourcePath -Destination $destModulePath -Recurse -Force
Write-Host "$ModuleName installed" -ForegroundColor Green
}
catch {
throw "Failed to copy module $ModuleName : $_"
}
$manifestPath = Get-ChildItem -Path $destModulePath -Filter "*.psd1" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($manifestPath) {
Write-Verbose "Module manifest found: $($manifestPath.FullName)"
} else {
Write-Warning "No module manifest (.psd1) found for $ModuleName"
}
}
function Test-ModuleInRepository {
<#
.SYNOPSIS
Checks if a module exists in the repository's modules directory.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ModuleName,
[Parameter(Mandatory = $false)]
[string]$ModulesRoot = ".\modules"
)
$modulePath = Join-Path $ModulesRoot $ModuleName
if (-not (Test-Path $modulePath)) {
return $false
}
$hasContent = (Get-ChildItem -Path $modulePath -Recurse -File -ErrorAction SilentlyContinue).Count -gt 0
return $hasContent
}
function Get-RepositoryModulePath {
<#
.SYNOPSIS
Returns the full path to a module in the repository.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ModuleName,
[Parameter(Mandatory = $false)]
[string]$ModulesRoot = ".\modules"
)
$modulePath = Join-Path $ModulesRoot $ModuleName
if (-not (Test-Path $modulePath)) {
throw "Module not found in repository: $ModuleName (expected at: $modulePath)"
}
return $modulePath
}
function Get-InstalledModulesInImage {
<#
.SYNOPSIS
Lists all PowerShell modules installed in the WinPE image.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$MountDir
)
$winpeModulesPath = Join-Path $MountDir "Program Files\WindowsPowerShell\Modules"
if (-not (Test-Path $winpeModulesPath)) {
return @()
}
$modules = Get-ChildItem -Path $winpeModulesPath -Directory -ErrorAction SilentlyContinue
return $modules | Select-Object -ExpandProperty Name
}
Export-ModuleMember -Function @(
'Install-ModuleToWinPE',
'Test-ModuleInRepository',
'Get-RepositoryModulePath',
'Get-InstalledModulesInImage'
)

142
lib/PowerShell7.psm1 Normal file
View file

@ -0,0 +1,142 @@
function Install-PowerShell7ToWinPE {
<#
.SYNOPSIS
Extracts and installs PowerShell 7 to the mounted WinPE image.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ZipPath,
[Parameter(Mandatory = $true)]
[string]$MountDir
)
Write-Host "Installing PowerShell 7 to WinPE..." -ForegroundColor Yellow
if (-not (Test-Path $ZipPath)) {
throw "PowerShell 7 zip not found: $ZipPath"
}
$ps7DestPath = Join-Path $MountDir "Program Files\PowerShell\7"
if (Test-Path $ps7DestPath) {
Write-Host " Removing existing PowerShell 7 installation..." -ForegroundColor Cyan
Remove-Item $ps7DestPath -Recurse -Force
}
New-Item -ItemType Directory -Path $ps7DestPath -Force | Out-Null
Write-Host " Extracting PowerShell 7..." -ForegroundColor Cyan
try {
Expand-Archive -Path $ZipPath -DestinationPath $ps7DestPath -Force
Write-Host " ✓ PowerShell 7 extracted to: $ps7DestPath" -ForegroundColor Green
}
catch {
throw "Failed to extract PowerShell 7: $_"
}
$pwshExePath = Join-Path $ps7DestPath "pwsh.exe"
if (-not (Test-Path $pwshExePath)) {
throw "pwsh.exe not found after extraction. Archive may be corrupt."
}
Write-Host " ✓ PowerShell 7 installed successfully" -ForegroundColor Green
}
function Set-PowerShell7Environment {
<#
.SYNOPSIS
Configures startnet.cmd to add PowerShell 7 to PATH and set up the environment.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$MountDir,
[Parameter(Mandatory = $true)]
[string]$LauncherScriptPath
)
Write-Host "Configuring WinPE startup script..." -ForegroundColor Yellow
$startnetPath = Join-Path $MountDir "Windows\System32\startnet.cmd"
if (-not (Test-Path $startnetPath)) {
throw "startnet.cmd not found at: $startnetPath"
}
$startnetContent = @"
@echo off
echo.
echo =====================================
echo WinPE Script Launcher Environment
echo =====================================
echo.
wpeinit
REM Add PowerShell 7 to PATH
set PATH=%PATH%;X:\Program Files\PowerShell\7
echo Initializing script launcher...
echo.
REM Launch the script launcher with PowerShell 7
pwsh.exe -ExecutionPolicy Bypass -File X:\Windows\System32\$LauncherScriptPath
REM If launcher exits, drop to PowerShell 7 prompt
pwsh.exe -NoExit -ExecutionPolicy Bypass -Command "& { `$host.UI.RawUI.WindowTitle = 'WinPE - PowerShell 7'; try { `$w = Add-Type -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern IntPtr GetConsoleWindow();' -Name Console -Namespace Win32 -PassThru; `$h = `$w::GetConsoleWindow(); `$SW_MAXIMIZE = 3; Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class User32 { [DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); }'; [User32]::ShowWindow(`$h, `$SW_MAXIMIZE) | Out-Null } catch { } }"
"@
Set-Content -Path $startnetPath -Value $startnetContent -Encoding ASCII
Write-Host " ✓ Startup script configured" -ForegroundColor Green
Write-Host " - PowerShell 7 added to PATH" -ForegroundColor Cyan
Write-Host " - Launcher script: $LauncherScriptPath" -ForegroundColor Cyan
}
function Test-PowerShell7Installation {
<#
.SYNOPSIS
Validates that PowerShell 7 was installed correctly in the WinPE image.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$MountDir
)
$ps7Path = Join-Path $MountDir "Program Files\PowerShell\7"
$pwshExe = Join-Path $ps7Path "pwsh.exe"
if (-not (Test-Path $ps7Path)) {
return $false
}
if (-not (Test-Path $pwshExe)) {
return $false
}
$requiredFiles = @(
"pwsh.dll",
"System.Management.Automation.dll",
"Microsoft.PowerShell.ConsoleHost.dll"
)
foreach ($file in $requiredFiles) {
$filePath = Join-Path $ps7Path $file
if (-not (Test-Path $filePath)) {
Write-Warning "Missing required file: $file"
return $false
}
}
return $true
}
Export-ModuleMember -Function @(
'Install-PowerShell7ToWinPE',
'Set-PowerShell7Environment',
'Test-PowerShell7Installation'
)

277
lib/WinPEBuilder.psm1 Normal file
View file

@ -0,0 +1,277 @@
$script:WorkDir = ".\work"
$script:MountDir = ".\work\mount"
$script:MediaDir = ".\work\media"
$script:BootWimPath = ".\work\media\sources\boot.wim"
function Initialize-WinPEWorkspace {
<#
.SYNOPSIS
Creates WinPE workspace using copype.cmd from Windows ADK.
#>
[CmdletBinding()]
param()
if (Test-Path $script:WorkDir) {
Write-Host "Cleaning existing workspace..." -ForegroundColor Yellow
Remove-Item $script:WorkDir -Recurse -Force
}
Write-Host "Initializing WinPE workspace..." -ForegroundColor Yellow
$adkPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit"
$copypePath = Join-Path $adkPath "Windows Preinstallation Environment\copype.cmd"
$setEnvPath = Join-Path $adkPath "Deployment Tools\DandISetEnv.bat"
if (-not (Test-Path $copypePath)) {
throw "copype.cmd not found. Is Windows ADK installed?"
}
if (-not (Test-Path $setEnvPath)) {
throw "DandISetEnv.bat not found. ADK installation may be incomplete."
}
$currentLocation = Get-Location
$workPath = Join-Path $currentLocation "work"
# Create a temporary batch file to run both commands in sequence
$tempBatchFile = Join-Path $env:TEMP "winpe-init-$(Get-Random).cmd"
try {
# Write batch file that calls DandISetEnv, then copype
$batchContent = @"
@echo off
call "$setEnvPath"
call "$copypePath" amd64 "$workPath"
exit /b %ERRORLEVEL%
"@
Set-Content -Path $tempBatchFile -Value $batchContent -Encoding ASCII
# Execute the batch file
$process = Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c", $tempBatchFile `
-Wait -PassThru -NoNewWindow
$exitCode = $process.ExitCode
}
finally {
# Clean up temp file
if (Test-Path $tempBatchFile) {
Remove-Item $tempBatchFile -Force -ErrorAction SilentlyContinue
}
}
if ($exitCode -ne 0) {
throw "Failed to initialize WinPE workspace. Exit code: $exitCode"
}
if (-not (Test-Path $script:BootWimPath)) {
throw "boot.wim not found after workspace initialization"
}
Write-Host " ✓ WinPE workspace initialized" -ForegroundColor Green
}
function Mount-WinPEImage {
<#
.SYNOPSIS
Mounts the WinPE boot.wim image for modification.
#>
[CmdletBinding()]
param()
if (-not (Test-Path $script:MountDir)) {
New-Item -ItemType Directory -Path $script:MountDir | Out-Null
}
Write-Host "Mounting WinPE image..." -ForegroundColor Yellow
# Use DISM directly
& dism.exe /Mount-Image /ImageFile:$script:BootWimPath /Index:1 /MountDir:$script:MountDir | Out-Host
# Verify mount succeeded by checking for Windows directory in mount point
$windowsDir = Join-Path $script:MountDir "Windows"
if (-not (Test-Path $windowsDir)) {
throw "Failed to mount WinPE image. Mount verification failed - Windows directory not found."
}
Write-Host " ✓ WinPE image mounted at: $script:MountDir" -ForegroundColor Green
}
function Add-WinPEOptionalComponent {
<#
.SYNOPSIS
Adds an optional component (and its language pack) to the mounted WinPE image.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
$adkPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit"
$ocPath = Join-Path $adkPath "Windows Preinstallation Environment\amd64\WinPE_OCs"
$componentCab = Join-Path $ocPath "$Name.cab"
$languageCab = Join-Path $ocPath "en-us\$Name`_en-us.cab"
if (-not (Test-Path $componentCab)) {
throw "Optional component not found: $componentCab"
}
Write-Host " Adding $Name..." -ForegroundColor Cyan
& dism.exe /Add-Package /Image:$script:MountDir /PackagePath:$componentCab
if ($LASTEXITCODE -ne 0) {
throw "Failed to add component $Name. Exit code: $LASTEXITCODE"
}
if (Test-Path $languageCab) {
Write-Host " Adding $Name language pack..." -ForegroundColor Cyan
& dism.exe /Add-Package /Image:$script:MountDir /PackagePath:$languageCab
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to add language pack for $Name (non-critical)"
}
}
Write-Host "$Name installed" -ForegroundColor Green
}
function Set-WinPEScratchSpace {
<#
.SYNOPSIS
Sets the scratch space size for better WinPE performance.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[int]$SizeMB = 512
)
Write-Host "Setting scratch space to ${SizeMB}MB..." -ForegroundColor Yellow
& dism.exe /Set-ScratchSpace:$SizeMB /Image:$script:MountDir
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to set scratch space (non-critical)"
} else {
Write-Host " ✓ Scratch space configured" -ForegroundColor Green
}
}
function Dismount-WinPEImage {
<#
.SYNOPSIS
Unmounts and commits changes to the WinPE image.
#>
[CmdletBinding()]
param()
Write-Host "Unmounting WinPE image and committing changes..." -ForegroundColor Yellow
& dism.exe /Unmount-Image /MountDir:$script:MountDir /Commit
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to unmount image. Attempting cleanup..."
& dism.exe /Cleanup-Mountpoints
throw "Failed to unmount WinPE image. Exit code: $LASTEXITCODE"
}
Write-Host " ✓ WinPE image unmounted successfully" -ForegroundColor Green
}
function New-BootableMedia {
<#
.SYNOPSIS
Creates bootable ISO file from WinPE workspace.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OutputPath = ".\output"
)
if (-not (Test-Path $OutputPath)) {
New-Item -ItemType Directory -Path $OutputPath | Out-Null
}
$isoPath = Join-Path $OutputPath "WinPE.iso"
if (Test-Path $isoPath) {
Write-Host "Removing existing ISO..." -ForegroundColor Yellow
Remove-Item $isoPath -Force
}
Write-Host "Creating bootable ISO..." -ForegroundColor Yellow
$adkPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit"
$makeWinPEMediaPath = Join-Path $adkPath "Windows Preinstallation Environment\MakeWinPEMedia.cmd"
$setEnvPath = Join-Path $adkPath "Deployment Tools\DandISetEnv.bat"
if (-not (Test-Path $makeWinPEMediaPath)) {
throw "MakeWinPEMedia.cmd not found"
}
if (-not (Test-Path $setEnvPath)) {
throw "DandISetEnv.bat not found. ADK installation may be incomplete."
}
$currentLocation = Get-Location
$workPath = Join-Path $currentLocation "work"
# Create a temporary batch file - need to set up ADK environment first
$tempBatchFile = Join-Path $env:TEMP "winpe-makeiso-$(Get-Random).cmd"
try {
# Write batch file that calls DandISetEnv, then MakeWinPEMedia
$batchContent = @"
@echo off
call "$setEnvPath"
call "$makeWinPEMediaPath" /ISO "$workPath" "$isoPath"
exit /b %ERRORLEVEL%
"@
Set-Content -Path $tempBatchFile -Value $batchContent -Encoding ASCII
# Execute the batch file
$process = Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c", $tempBatchFile `
-Wait -PassThru -NoNewWindow
$exitCode = $process.ExitCode
}
finally {
# Clean up temp file
if (Test-Path $tempBatchFile) {
Remove-Item $tempBatchFile -Force -ErrorAction SilentlyContinue
}
}
if ($exitCode -ne 0) {
throw "Failed to create ISO. Exit code: $exitCode"
}
if (-not (Test-Path $isoPath)) {
throw "ISO creation reported success but file not found"
}
$isoSize = (Get-Item $isoPath).Length / 1MB
Write-Host " ✓ Bootable ISO created: $isoPath ($([math]::Round($isoSize, 2)) MB)" -ForegroundColor Green
}
function Get-MountDirectory {
<#
.SYNOPSIS
Returns the current mount directory path.
#>
return $script:MountDir
}
Export-ModuleMember -Function @(
'Initialize-WinPEWorkspace',
'Mount-WinPEImage',
'Add-WinPEOptionalComponent',
'Set-WinPEScratchSpace',
'Dismount-WinPEImage',
'New-BootableMedia',
'Get-MountDirectory'
)

2166
resources/Driver-Manager.ps1 Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,239 @@
<#
.SYNOPSIS
WinPE Script Launcher - Automatically detects and runs scripts from external media.
.DESCRIPTION
This script runs automatically when WinPE boots. It scans all available drives
for a .scripts folder on the root, then executes the configured target script.
#>
$ErrorActionPreference = "Continue"
# Load configuration from WinPE image
$ConfigPath = "X:\Windows\System32\winpe-config.json"
$Config = $null
if (Test-Path $ConfigPath) {
try {
$Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$ScriptsFolder = $Config.scriptsFolder
$TargetScript = $Config.targetScript
$FallbackScript = $Config.fallbackScript
}
catch {
Write-Warning "Failed to load config, using defaults"
$ScriptsFolder = ".scripts"
$TargetScript = "Invoke-ScriptMenu.ps1"
$FallbackScript = "Invoke-ScriptMenu.ps1"
}
}
else {
# Fallback to defaults if config not found
$ScriptsFolder = ".scripts"
$TargetScript = "Invoke-ScriptMenu.ps1"
$FallbackScript = "Invoke-ScriptMenu.ps1"
}
# Apply console customizations
try {
$host.UI.RawUI.WindowTitle = 'WinPE Script Launcher - PowerShell 7'
# Set console colors if configured
if ($Config -and $Config.consoleColors) {
if ($Config.consoleColors.backgroundColor) {
$host.UI.RawUI.BackgroundColor = $Config.consoleColors.backgroundColor
}
if ($Config.consoleColors.foregroundColor) {
$host.UI.RawUI.ForegroundColor = $Config.consoleColors.foregroundColor
}
Clear-Host # Apply color changes
}
# Get console window handle and maximize it
Add-Type -MemberDefinition @'
[DllImport("kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
'@ -Name Console -Namespace Win32 -PassThru -ErrorAction SilentlyContinue | Out-Null
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public class User32 {
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
'@ -ErrorAction SilentlyContinue
$consoleWindow = [Win32.Console]::GetConsoleWindow()
$SW_MAXIMIZE = 3
[User32]::ShowWindow($consoleWindow, $SW_MAXIMIZE) | Out-Null
}
catch {
# Silently continue if window manipulation fails
}
function Write-Banner {
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " WinPE Script Launcher v1.0" -ForegroundColor White
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host ""
}
function Find-ScriptsFolder {
param(
[string]$FolderName
)
Write-Host "Scanning drives for [$FolderName] folder..." -ForegroundColor Yellow
Write-Host ""
Start-Sleep -Seconds 2
try {
$drives = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue |
Where-Object { $_.Used -gt 0 -and $_.Root -ne "X:\" }
if (-not $drives) {
Write-Warning "No drives found with content"
return $null
}
foreach ($drive in $drives) {
$testPath = Join-Path $drive.Root $FolderName
Write-Host " Checking $($drive.Root)..." -ForegroundColor Cyan
if (Test-Path $testPath) {
Write-Host " [FOUND] $testPath" -ForegroundColor Green
Write-Host ""
return $testPath
}
}
Write-Host ""
Write-Warning "No [$FolderName] folder found on any drive"
return $null
}
catch {
Write-Error "Error scanning drives: $_"
return $null
}
}
function Invoke-TargetScript {
param(
[string]$ScriptsPath,
[string]$ScriptName
)
$scriptPath = Join-Path $ScriptsPath $ScriptName
if (-not (Test-Path $scriptPath)) {
Write-Error "Target script not found: $scriptPath"
Write-Host ""
Write-Host "Expected script: $ScriptName" -ForegroundColor Yellow
Write-Host "Scripts folder: $ScriptsPath" -ForegroundColor Yellow
Write-Host ""
Write-Host "Available files in scripts folder:" -ForegroundColor Cyan
try {
Get-ChildItem -Path $ScriptsPath -File | ForEach-Object {
Write-Host " - $($_.Name)" -ForegroundColor White
}
}
catch {
Write-Warning "Could not list files in scripts folder"
}
return $false
}
Write-Host "Executing: $scriptPath" -ForegroundColor Green
Write-Host ""
Write-Host "-------------------------------------------" -ForegroundColor DarkGray
Write-Host ""
try {
Push-Location $ScriptsPath
& $scriptPath
Pop-Location
Write-Host ""
Write-Host "-------------------------------------------" -ForegroundColor DarkGray
Write-Host "Script execution completed" -ForegroundColor Green
return $true
}
catch {
Pop-Location
Write-Error "Script execution failed: $_"
return $false
}
}
Write-Banner
$scriptsPath = Find-ScriptsFolder -FolderName $ScriptsFolder
if ($scriptsPath) {
Write-Host "Scripts folder located at: $scriptsPath" -ForegroundColor Green
Write-Host ""
$success = Invoke-TargetScript -ScriptsPath $scriptsPath -ScriptName $TargetScript
if (-not $success) {
Write-Host ""
Write-Host "Press any key to continue to command prompt..." -ForegroundColor Yellow
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
}
else {
Write-Host ""
Write-Host "=============================================" -ForegroundColor Yellow
Write-Host " No external scripts found" -ForegroundColor Yellow
Write-Host "=============================================" -ForegroundColor Yellow
Write-Host ""
$localScriptsPath = "X:\$ScriptsFolder"
if (Test-Path $localScriptsPath) {
Write-Host "Found local scripts folder on WinPE image" -ForegroundColor Green
Write-Host "Launching built-in fallback script..." -ForegroundColor Cyan
Write-Host ""
Start-Sleep -Seconds 1
$fallbackScriptPath = "X:\Windows\System32\$FallbackScript"
if (Test-Path $fallbackScriptPath) {
try {
& $fallbackScriptPath -ScriptsPath $localScriptsPath
}
catch {
Write-Error "Failed to launch fallback script: $_"
Write-Host ""
Write-Host "Press any key to continue to command prompt..." -ForegroundColor Yellow
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
}
else {
Write-Warning "Fallback script not found in WinPE image: $FallbackScript"
Write-Host ""
Write-Host "Press any key to continue to command prompt..." -ForegroundColor Yellow
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
}
else {
Write-Host "To use this WinPE environment:" -ForegroundColor White
Write-Host " 1. Insert USB drive or external media" -ForegroundColor Cyan
Write-Host " 2. Create folder: [$ScriptsFolder] in root" -ForegroundColor Cyan
Write-Host " 3. Add script: [$TargetScript]" -ForegroundColor Cyan
Write-Host " 4. Reboot or run this launcher again" -ForegroundColor Cyan
Write-Host ""
Write-Host "OR" -ForegroundColor Yellow
Write-Host ""
Write-Host " Create folder: X:\$ScriptsFolder on this WinPE image" -ForegroundColor Cyan
Write-Host " Add your scripts and run this launcher again" -ForegroundColor Cyan
Write-Host ""
Write-Host "Press any key to continue to command prompt..." -ForegroundColor Yellow
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
}

View file

@ -0,0 +1,252 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Built-in Script Menu for WinPE Script Launcher
.DESCRIPTION
Discovers and executes scripts from a .scripts folder. This is a simplified
version designed for WinPE environments without UAC elevation requirements.
#>
[CmdletBinding()]
param(
[string]$ScriptsPath = ".scripts"
)
$Script:SupportedExtensions = @('.ps1', '.bat', '.cmd')
$Script:ExitCode = 0
function Write-Header {
param([string]$Title)
$width = 70
Write-Host ""
Write-Host ("=" * $width) -ForegroundColor Cyan
Write-Host " $Title" -ForegroundColor Yellow
Write-Host ("=" * $width) -ForegroundColor Cyan
Write-Host ""
}
function Write-InfoBox {
param(
[string]$Message,
[string]$Type = 'Info'
)
$color = switch ($Type) {
'Success' { 'Green' }
'Warning' { 'Yellow' }
'Error' { 'Red' }
default { 'Cyan' }
}
$icon = switch ($Type) {
'Success' { '[+]' }
'Warning' { '[!]' }
'Error' { '[X]' }
default { '[i]' }
}
Write-Host "$icon " -ForegroundColor $color -NoNewline
Write-Host $Message
}
function Write-Separator {
Write-Host ("-" * 70) -ForegroundColor DarkGray
}
function Get-ScriptFiles {
param([string]$Path)
if (-not (Test-Path $Path)) {
return $null
}
$scripts = Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue |
Where-Object { $Script:SupportedExtensions -contains $_.Extension } |
Sort-Object Name
return $scripts
}
function Show-ScriptMenu {
param([array]$Scripts)
Write-Host " Available Scripts:" -ForegroundColor Green
Write-Host ""
for ($i = 0; $i -lt $Scripts.Count; $i++) {
$num = $i + 1
$script = $Scripts[$i]
$ext = $script.Extension
$extColor = switch ($ext) {
'.ps1' { 'Magenta' }
'.bat' { 'Yellow' }
'.cmd' { 'Cyan' }
default { 'White' }
}
Write-Host " " -NoNewline
Write-Host ("[{0,2}]" -f $num) -ForegroundColor White -NoNewline
Write-Host " " -NoNewline
Write-Host $script.Name -ForegroundColor $extColor
}
Write-Host ""
Write-Separator
Write-Host ""
}
function Get-UserSelection {
param([int]$MaxNumber)
Write-Host " Selection Options:" -ForegroundColor Yellow
Write-Host " - Enter number (e.g., '1')" -ForegroundColor Gray
Write-Host " - Enter 'all' to run all scripts sequentially" -ForegroundColor Gray
Write-Host " - Enter 'q' or 'quit' to exit" -ForegroundColor Gray
Write-Host ""
while ($true) {
Write-Host " Select script to run: " -ForegroundColor Cyan -NoNewline
$input = Read-Host
if ([string]::IsNullOrWhiteSpace($input)) {
Write-InfoBox "Please enter a selection" "Warning"
continue
}
$input = $input.Trim().ToLower()
if ($input -eq 'q' -or $input -eq 'quit') {
return $null
}
if ($input -eq 'all') {
return 1..$MaxNumber
}
if ($input -match '^\d+$') {
$num = [int]$input
if ($num -lt 1 -or $num -gt $MaxNumber) {
Write-InfoBox "Invalid number. Please enter 1-$MaxNumber" "Error"
continue
}
return @($num)
}
Write-InfoBox "Invalid input. Please try again" "Error"
}
}
function Invoke-Script {
param(
[string]$ScriptPath,
[string]$ScriptName
)
Write-Host ""
Write-InfoBox "Executing: $ScriptName" "Info"
Write-Host ""
$extension = [System.IO.Path]::GetExtension($ScriptPath)
try {
if ($extension -eq '.ps1') {
& $ScriptPath
$exitCode = $LASTEXITCODE
}
else {
$process = Start-Process -FilePath $ScriptPath -Wait -PassThru -NoNewWindow
$exitCode = $process.ExitCode
}
Write-Host ""
if ($exitCode -eq 0 -or $null -eq $exitCode) {
Write-InfoBox "Completed: $ScriptName" "Success"
}
else {
Write-InfoBox "Completed with exit code $exitCode: $ScriptName" "Warning"
$Script:ExitCode = $exitCode
}
}
catch {
Write-InfoBox "Failed to execute: $ScriptName" "Error"
Write-InfoBox "Error: $($_.Exception.Message)" "Error"
$Script:ExitCode = 1
}
}
function Start-ScriptMenu {
try {
Clear-Host
Write-Header "WinPE SCRIPT MENU"
if (-not (Test-Path $ScriptsPath)) {
Write-InfoBox "Scripts folder not found: $ScriptsPath" "Error"
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit 1
}
Write-InfoBox "Scripts Folder: $ScriptsPath" "Info"
$scripts = Get-ScriptFiles -Path $ScriptsPath
if ($null -eq $scripts -or $scripts.Count -eq 0) {
Write-Host ""
Write-InfoBox "No scripts found" "Warning"
Write-InfoBox "Supported: $($Script:SupportedExtensions -join ', ')" "Info"
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit 1
}
Write-InfoBox "Found $($scripts.Count) script(s)" "Success"
Write-Host ""
Write-Separator
Write-Host ""
Show-ScriptMenu -Scripts $scripts
$selections = Get-UserSelection -MaxNumber $scripts.Count
if ($null -eq $selections) {
Write-Host ""
Write-InfoBox "Exiting..." "Info"
Write-Host ""
exit 0
}
Write-Host ""
Write-Header "Executing Selected Scripts"
foreach ($selection in $selections) {
$script = $scripts[$selection - 1]
Invoke-Script -ScriptPath $script.FullName -ScriptName $script.Name
}
Write-Host ""
Write-Separator
Write-InfoBox "All scripts completed" "Success"
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit $Script:ExitCode
}
catch {
Write-Host ""
Write-InfoBox "FATAL ERROR: $($_.Exception.Message)" "Error"
Write-Host ""
Write-Host "Press any key to exit..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit 1
}
}
Start-ScriptMenu