aboutsummaryrefslogtreecommitdiff
path: root/windows/Documents/PowerShell/bootstrap.ps1
diff options
context:
space:
mode:
Diffstat (limited to 'windows/Documents/PowerShell/bootstrap.ps1')
-rw-r--r--windows/Documents/PowerShell/bootstrap.ps1669
1 files changed, 669 insertions, 0 deletions
diff --git a/windows/Documents/PowerShell/bootstrap.ps1 b/windows/Documents/PowerShell/bootstrap.ps1
new file mode 100644
index 0000000..d2f4369
--- /dev/null
+++ b/windows/Documents/PowerShell/bootstrap.ps1
@@ -0,0 +1,669 @@
+#!/usr/bin/env pwsh
+
+# Created By: srdusr
+# Created On: Windows PowerShell Bootstrap Script
+# Project: Dotfiles installation script for Windows
+
+# Dependencies: git, powershell
+
+param(
+ [string]$Profile = "essentials",
+ [switch]$Force = $false,
+ [switch]$Ask = $false,
+ [switch]$Help = $false
+)
+
+# Color definitions for pretty UI
+$Script:Colors = @{
+ Reset = "`e[0m"
+ Red = "`e[0;31m"
+ Green = "`e[0;32m"
+ Yellow = "`e[0;33m"
+ Blue = "`e[0;34m"
+ Cyan = "`e[0;36m"
+ White = "`e[0;37m"
+ Bold = "`e[1m"
+}
+
+# Prompt helper: Yes/No with default; in non-Ask mode, returns default immediately
+function Prompt-YesNo {
+ param(
+ [Parameter(Mandatory=$true)][string]$Question,
+ [ValidateSet('Y','N')][string]$Default = 'Y'
+ )
+ if (-not $Script:AskPreference) {
+ return $Default -eq 'Y'
+ }
+ $suffix = if ($Default -eq 'Y') { '[Y/n]' } else { '[y/N]' }
+ while ($true) {
+ Write-Host -NoNewline "$Question $suffix: " -ForegroundColor Yellow
+ $resp = Read-Host
+ if ([string]::IsNullOrWhiteSpace($resp)) { $resp = $Default }
+ switch ($resp.ToUpper()) {
+ 'Y' { return $true }
+ 'YES' { return $true }
+ 'N' { return $false }
+ 'NO' { return $false }
+ default { Write-Warning "Please answer Y/yes or N/no" }
+ }
+ }
+}
+
+# Configuration
+$Script:Config = @{
+ DotfilesUrl = 'https://github.com/srdusr/dotfiles.git'
+ DotfilesDir = "$HOME\.cfg"
+ LogFile = "$HOME\AppData\Local\dotfiles_install.log"
+ StateFile = "$HOME\AppData\Local\dotfiles_install_state"
+ BackupDir = "$HOME\.dotfiles-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
+ PackagesFile = "packages.yml"
+ OS = "windows"
+}
+
+# Logging functions
+function Write-Log {
+ param([string]$Message, [string]$Level = "INFO")
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
+ $logEntry = "[$timestamp] [$Level] $Message"
+ Add-Content -Path $Script:Config.LogFile -Value $logEntry -Force
+}
+
+function Write-ColorOutput {
+ param([string]$Message, [string]$Color = "White")
+ Write-Host $Message -ForegroundColor $Color
+ Write-Log $Message
+}
+
+function Write-Success { param([string]$Message) Write-ColorOutput "✓ $Message" "Green" }
+function Write-Info { param([string]$Message) Write-ColorOutput "ℹ $Message" "Cyan" }
+function Write-Warning { param([string]$Message) Write-ColorOutput "⚠ $Message" "Yellow" }
+function Write-Error { param([string]$Message) Write-ColorOutput "✗ $Message" "Red" }
+
+function Write-Header {
+ param([string]$Title)
+ Write-Host ""
+ Write-Host "=" * 60 -ForegroundColor Blue
+ Write-Host " $Title" -ForegroundColor Bold
+ Write-Host "=" * 60 -ForegroundColor Blue
+ Write-Host ""
+}
+
+# Utility functions
+function Test-CommandExists {
+ param([string]$Command)
+ return [bool](Get-Command $Command -ErrorAction SilentlyContinue)
+}
+
+function Test-IsAdmin {
+ $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
+ return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+function Invoke-AdminCommand {
+ param([string]$Command)
+ if (-not (Test-IsAdmin)) {
+ Write-Warning "Elevating privileges for: $Command"
+ Start-Process powershell.exe -ArgumentList "-NoProfile", "-Command", $Command -Verb RunAs -Wait
+ } else {
+ Invoke-Expression $Command
+ }
+}
+
+# Package management functions
+function Get-PackageManager {
+ if (Test-CommandExists "choco") { return "chocolatey" }
+ if (Test-CommandExists "winget") { return "winget" }
+ if (Test-CommandExists "scoop") { return "scoop" }
+ return $null
+}
+
+# Return $true if a package appears to be installed for the given manager
+function Test-PackageInstalled {
+ param(
+ [Parameter(Mandatory=$true)][string]$Manager,
+ [Parameter(Mandatory=$true)][string]$Name
+ )
+ switch ($Manager) {
+ "chocolatey" {
+ $out = choco list --local-only --exact $Name 2>$null
+ return ($out | Select-String -Pattern "^\s*$([regex]::Escape($Name))\s").Length -gt 0
+ }
+ "winget" {
+ # Winget list may return multiple rows; use --exact name match when possible
+ $out = winget list --name $Name 2>$null
+ return ($out | Select-String -SimpleMatch $Name).Length -gt 0
+ }
+ "scoop" {
+ # scoop list <name> returns 0 when installed
+ scoop list $Name *> $null
+ return $LASTEXITCODE -eq 0
+ }
+ default { return $false }
+ }
+}
+
+function Install-PackageManager {
+ Write-Header "Installing Package Manager"
+
+ if (-not (Test-CommandExists "choco")) {
+ Write-Info "Installing Chocolatey..."
+ Set-ExecutionPolicy Bypass -Scope Process -Force
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
+ Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
+
+ if (Test-CommandExists "choco") {
+ Write-Success "Chocolatey installed successfully"
+ } else {
+ Write-Error "Failed to install Chocolatey"
+ return $false
+ }
+ } else {
+ Write-Info "Chocolatey already installed"
+ }
+ return $true
+}
+
+function Install-Packages {
+ param([string]$PackagesFile, [string]$Profile)
+
+ if (-not (Test-Path $PackagesFile)) {
+ Write-Warning "Packages file not found: $PackagesFile"
+ return
+ }
+
+ Write-Header "Installing Packages"
+
+ # Install powershell-yaml if not available
+ if (-not (Get-Module powershell-yaml -ListAvailable)) {
+ Write-Info "Installing powershell-yaml module..."
+ $policy = Get-PSRepository -Name 'PSGallery' | Select-Object -ExpandProperty InstallationPolicy
+ Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
+ Install-Module powershell-yaml -Force
+ Set-PSRepository -Name 'PSGallery' -InstallationPolicy $policy
+ }
+
+ Import-Module powershell-yaml
+
+ # Helper: run custom_installs.<name>.windows if condition passes
+ function Invoke-CustomInstallsWindows {
+ param([Parameter(Mandatory=$true)]$Yaml)
+ if (-not $Yaml.custom_installs) { return }
+ foreach ($name in $Yaml.custom_installs.PSObject.Properties.Name) {
+ $entry = $Yaml.custom_installs.$name
+ if (-not $entry) { continue }
+ $winCmd = $entry.windows
+ if (-not $winCmd) { continue }
+ $shouldRun = $true
+ if ($entry.condition) {
+ $cond = [string]$entry.condition
+ if ($cond -match "!\s*command\s+-v\s+([A-Za-z0-9._-]+)") {
+ $shouldRun = -not (Test-CommandExists $Matches[1])
+ } elseif ($cond -match "command\s+-v\s+([A-Za-z0-9._-]+)") {
+ $shouldRun = (Test-CommandExists $Matches[1])
+ }
+ }
+ if (-not $shouldRun) { Write-Info "Skipping custom install: $name"; continue }
+ Write-Info "Running custom install: $name"
+ try { Invoke-Expression $winCmd; Write-Success "Custom install completed: $name" }
+ catch { Write-Warning "Custom install failed for '$name': $_" }
+ }
+ }
+
+ try {
+ $packages = Get-Content $PackagesFile | ConvertFrom-Yaml
+ $packageManager = Get-PackageManager
+
+ if (-not $packageManager) {
+ Write-Error "No package manager available"
+ return
+ }
+
+ # Get packages for current profile and OS
+ $profilePackages = @()
+ if ($packages.profiles.$Profile.windows) {
+ $profilePackages += $packages.profiles.$Profile.windows
+ }
+ if ($packages.profiles.$Profile.common) {
+ $profilePackages += $packages.profiles.$Profile.common
+ }
+
+ foreach ($package in $profilePackages) {
+ $packageName = if ($packages.packages.$package.windows) {
+ $packages.packages.$package.windows
+ } else {
+ $package
+ }
+
+ if (Test-PackageInstalled -Manager $packageManager -Name $packageName) {
+ Write-Info "Already installed: $packageName"
+ continue
+ }
+
+ Write-Info "Installing package: $packageName"
+
+ switch ($packageManager) {
+ "chocolatey" {
+ if (-not (choco list --local-only | Select-String -Pattern "^$packageName\s")) {
+ choco install $packageName -y
+ if ($LASTEXITCODE -eq 0) {
+ Write-Success "Installed: $packageName"
+ } else {
+ Write-Error "Failed to install: $packageName"
+ }
+ } else {
+ Write-Info "Already installed: $packageName"
+ }
+ }
+ "winget" {
+ winget install $packageName --accept-package-agreements --accept-source-agreements
+ }
+ "scoop" {
+ scoop install $packageName
+ }
+ }
+ }
+
+ # Also install top-level Windows packages list if present
+ if ($packages.windows) {
+ foreach ($pkg in $packages.windows) {
+ if ([string]::IsNullOrWhiteSpace($pkg)) { continue }
+ if (Test-PackageInstalled -Manager $packageManager -Name $pkg) { Write-Info "Already installed: $pkg"; continue }
+ Write-Info "Installing package: $pkg"
+ switch ($packageManager) {
+ "chocolatey" {
+ if (-not (choco list --local-only | Select-String -Pattern "^$([regex]::Escape($pkg))\s")) { choco install $pkg -y }
+ }
+ "winget" { winget install --id $pkg --silent --accept-package-agreements --accept-source-agreements }
+ "scoop" { scoop install $pkg }
+ }
+ }
+ }
+
+ # Execute Windows custom installs from packages.yml
+ Invoke-CustomInstallsWindows -Yaml $packages
+ } catch {
+ Write-Error "Error processing packages: $_"
+ }
+}
+
+# Dotfiles management functions
+function Install-Dotfiles {
+ Write-Header "Installing Dotfiles"
+
+ if (Test-Path $Script:Config.DotfilesDir) {
+ Write-Info "Updating existing dotfiles repository..."
+ & git --git-dir="$($Script:Config.DotfilesDir)" --work-tree="$($Script:Config.DotfilesDir)" pull origin main
+ } else {
+ Write-Info "Cloning dotfiles repository..."
+ git clone --bare $Script:Config.DotfilesUrl $Script:Config.DotfilesDir
+
+ if (-not (Test-Path $Script:Config.DotfilesDir)) {
+ Write-Error "Failed to clone dotfiles repository"
+ return $false
+ }
+ }
+
+ # Set up config alias for this session
+ function script:config {
+ git --git-dir="$($Script:Config.DotfilesDir)" --work-tree="$($Script:Config.DotfilesDir)" @args
+ }
+
+ # Configure repository
+ config config --local status.showUntrackedFiles no
+
+ # Checkout files to restore directory structure
+ Write-Info "Checking out dotfiles..."
+ try {
+ config checkout 2>$null
+ if ($LASTEXITCODE -ne 0) {
+ Write-Info "Forcing checkout to overwrite existing files..."
+ config checkout -f
+ }
+ Write-Success "Dotfiles checked out successfully"
+ } catch {
+ Write-Error "Failed to checkout dotfiles: $_"
+ return $false
+ }
+
+ return $true
+}
+
+function Deploy-Dotfiles {
+ Write-Header "Deploying Dotfiles"
+
+ if (-not (Test-Path $Script:Config.DotfilesDir)) {
+ Write-Error "Dotfiles directory not found. Run Install-Dotfiles first."
+ return $false
+ }
+
+ # Source the config command from profile if available
+ $profilePath = "$HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1"
+ if (Test-Path $profilePath) {
+ Write-Info "Loading config command from profile..."
+ . $profilePath
+ }
+
+ # Deploy using config command if available, otherwise manual deployment
+ if (Get-Command config -ErrorAction SilentlyContinue) {
+ Write-Info "Deploying dotfiles using config command..."
+ config deploy
+ } else {
+ Write-Warning "Config command not available, using manual deployment..."
+ # Manual deployment fallback would go here
+ }
+
+ Write-Success "Dotfiles deployment completed"
+ return $true
+}
+
+# Locate profile-specific packages.yml similar to Linux installer
+function Get-ProfilePackagesFile {
+ param([string]$Profile)
+ $candidates = @(
+ Join-Path $HOME ".cfg/profile/$Profile/packages.yml",
+ Join-Path $HOME "profile/$Profile/packages.yml",
+ Join-Path $HOME "dot_setup/profile/$Profile/packages.yml",
+ Join-Path $Script:Config.DotfilesDir "common/$($Script:Config.PackagesFile)"
+ )
+ foreach ($pf in $candidates) {
+ if (Test-Path $pf) { return $pf }
+ }
+ return $null
+}
+
+# System configuration functions
+function Set-WindowsConfiguration {
+ param(
+ [string]$PackagesFile
+ )
+
+ Write-Header "Configuring Windows Settings"
+
+ if (-not $PackagesFile -or -not (Test-Path $PackagesFile)) {
+ Write-Warning "Packages file not found, skipping Windows configuration"
+ return
+ }
+
+ try {
+ # Load YAML content
+ $yamlContent = Get-Content $PackagesFile -Raw | ConvertFrom-Yaml
+ $registrySettings = $yamlContent.system_tweaks.windows.registry
+
+ if (-not $registrySettings) {
+ Write-Warning "No Windows registry settings found in packages.yml"
+ return
+ }
+
+ Write-Info "Applying registry settings from packages.yml..."
+
+ foreach ($setting in $registrySettings) {
+ try {
+ $path = $setting.path
+ $name = $setting.name
+ $value = $setting.value
+ $type = $setting.type
+ $description = $setting.description
+
+ Write-Info "Setting: $description"
+
+ # Ensure the registry path exists
+ $pathParts = $path -split '\\'
+ $currentPath = $pathParts[0]
+ for ($i = 1; $i -lt $pathParts.Length; $i++) {
+ $currentPath = "$currentPath\$($pathParts[$i])"
+ if (-not (Test-Path $currentPath)) {
+ New-Item -Path $currentPath -Force | Out-Null
+ }
+ }
+
+ # Set the registry value
+ Set-ItemProperty -Path $path -Name $name -Value $value -Type $type -Force
+ Write-Success "Applied: $description"
+
+ } catch {
+ Write-Warning "Failed to apply setting '$($setting.description)': $_"
+ }
+ }
+
+ Write-Success "Windows configuration applied"
+
+ # Restart explorer to apply changes
+ Write-Info "Restarting Windows Explorer..."
+ Stop-Process -Name explorer -Force
+ Start-Process explorer.exe
+
+ } catch {
+ Write-Warning "Failed to process Windows configuration: $_"
+ }
+}
+
+function Enable-WindowsFeatures {
+ param(
+ [string]$PackagesFile
+ )
+
+ Write-Header "Enabling Windows Features"
+
+ if (-not $PackagesFile -or -not (Test-Path $PackagesFile)) {
+ Write-Warning "Packages file not found, skipping Windows features"
+ return
+ }
+
+ try {
+ # Load YAML content
+ $yamlContent = Get-Content $PackagesFile -Raw | ConvertFrom-Yaml
+ $features = $yamlContent.system_tweaks.windows.features
+
+ if (-not $features) {
+ Write-Warning "No Windows features found in packages.yml"
+ return
+ }
+
+ foreach ($feature in $features) {
+ $featureName = $feature.name
+ $description = $feature.description
+ $requiresAdmin = $feature.requires_admin
+
+ if ($requiresAdmin -and -not (Test-IsAdmin)) {
+ Write-Warning "Skipping '$description' - requires administrator privileges"
+ continue
+ }
+
+ try {
+ Write-Info "Enabling: $description"
+ dism.exe /online /enable-feature /featurename:$featureName /all /norestart
+ Write-Success "Enabled: $description"
+ } catch {
+ Write-Warning "Failed to enable '$description': $_"
+ }
+ }
+
+ Write-Success "Windows features processing complete (restart may be required)"
+
+ } catch {
+ Write-Warning "Failed to process Windows features: $_"
+ }
+}
+
+function Install-PowerShellProfile {
+ Write-Header "Setting up PowerShell Profile"
+
+ $documentsPath = [System.Environment]::GetFolderPath('MyDocuments')
+ $powerShellProfileDir = "$documentsPath\PowerShell"
+ $profilePath = "$powerShellProfileDir\Microsoft.PowerShell_profile.ps1"
+
+ Write-Info "PowerShell profile directory: $powerShellProfileDir"
+
+ if (-not (Test-Path $powerShellProfileDir)) {
+ New-Item -ItemType Directory -Path $powerShellProfileDir -Force | Out-Null
+ Write-Success "Created PowerShell profile directory"
+ }
+
+ # Copy profile from dotfiles if it exists
+ $dotfilesProfile = "$($Script:Config.DotfilesDir)\windows\Documents\PowerShell\Microsoft.PowerShell_profile.ps1"
+ if (Test-Path $dotfilesProfile) {
+ Copy-Item $dotfilesProfile $profilePath -Force
+ Write-Success "PowerShell profile installed from dotfiles"
+ } else {
+ Write-Warning "PowerShell profile not found in dotfiles"
+ }
+}
+
+# Main execution function
+function Start-Bootstrap {
+ param([string]$Profile, [switch]$Force, [switch]$Ask)
+
+ Write-Header "Windows Dotfiles Bootstrap"
+ Write-Info "Profile: $Profile"
+ Write-Info "Force mode: $Force"
+ Write-Info "Interactive mode: $Ask"
+
+ # Initialize logging
+ $logDir = Split-Path $Script:Config.LogFile
+ if (-not (Test-Path $logDir)) {
+ New-Item -ItemType Directory -Path $logDir -Force | Out-Null
+ }
+
+ Write-Log "Bootstrap started with profile: $Profile"
+
+ # Set Ask preference for all prompts
+ $Script:AskPreference = [bool]$Ask
+
+ # Check dependencies
+ Write-Header "Checking Dependencies"
+ $requiredCommands = @("git", "powershell")
+ $missingCommands = @()
+
+ foreach ($cmd in $requiredCommands) {
+ if (-not (Test-CommandExists $cmd)) {
+ $missingCommands += $cmd
+ Write-Error "Required command not found: $cmd"
+ } else {
+ Write-Success "Found: $cmd"
+ }
+ }
+
+ if ($missingCommands.Count -gt 0) {
+ Write-Error "Missing required dependencies. Please install: $($missingCommands -join ', ')"
+ return $false
+ }
+
+ # Install package manager (skippable)
+ if (Prompt-YesNo -Question "Install/check package manager?" -Default 'Y') {
+ if (-not (Install-PackageManager)) {
+ Write-Error "Failed to install package manager"
+ return $false
+ }
+ } else {
+ Write-Warning "Skipped package manager step by user choice"
+ }
+
+ # Install dotfiles
+ if (Prompt-YesNo -Question "Install or update dotfiles?" -Default 'Y') {
+ if (-not (Install-Dotfiles)) {
+ Write-Error "Failed to install dotfiles"
+ return $false
+ }
+ } else {
+ Write-Warning "Skipped dotfiles installation by user choice"
+ }
+
+ # Get packages file (profile-aware)
+ $packagesFile = Get-ProfilePackagesFile -Profile $Profile
+ if (-not $packagesFile) {
+ Write-Error "Failed to get packages file for profile '$Profile'"
+ return $false
+ }
+
+ # Install packages
+ if (Prompt-YesNo -Question "Install profile packages?" -Default 'Y') {
+ Install-Packages -PackagesFile $packagesFile -Profile $Profile
+ } else {
+ Write-Warning "Skipped package installation by user choice"
+ }
+
+ # Set up PowerShell profile
+ if (Prompt-YesNo -Question "Install PowerShell profile?" -Default 'Y') {
+ Install-PowerShellProfile
+ } else {
+ Write-Warning "Skipped PowerShell profile setup by user choice"
+ }
+
+ # Deploy dotfiles
+ if (Prompt-YesNo -Question "Deploy dotfiles to system locations?" -Default 'Y') {
+ if (-not (Deploy-Dotfiles)) {
+ Write-Error "Failed to deploy dotfiles"
+ return $false
+ }
+ } else {
+ Write-Warning "Skipped dotfiles deployment by user choice"
+ }
+
+ # Configure Windows
+ if (Prompt-YesNo -Question "Apply Windows configuration from packages.yml?" -Default 'N') {
+ Set-WindowsConfiguration -PackagesFile $packagesPath
+ } else {
+ Write-Warning "Skipped Windows configuration by user choice"
+ }
+
+ # Enable Windows features (if admin)
+ if (Prompt-YesNo -Question "Enable Windows optional features?" -Default 'N') {
+ Enable-WindowsFeatures -PackagesFile $packagesPath
+ } else {
+ Write-Warning "Skipped enabling Windows features by user choice"
+ }
+
+ Write-Header "Bootstrap Complete"
+ Write-Success "Windows dotfiles bootstrap completed successfully!"
+ Write-Info "Please restart your computer to apply all changes."
+ Write-Log "Bootstrap completed successfully"
+
+ return $true
+}
+
+# Help function
+function Show-Help {
+ Write-Host @"
+Windows Dotfiles Bootstrap Script
+
+USAGE:
+ .\bootstrap.ps1 [-Profile <profile>] [-Force] [-Ask] [-Help]
+
+PARAMETERS:
+ -Profile <string> Installation profile (default: essentials)
+ Available: essentials, minimal, dev, server, full, or a custom profile.
+ Custom profile files are resolved from:
+ - %USERPROFILE%\.cfg\profile\<name>\packages.yml
+ - %USERPROFILE%\profile\<name>\packages.yml
+ - %USERPROFILE%\dot_setup\profile\<name>\packages.yml
+ -Force Force installation without prompts
+ -Ask Interactive mode with step-by-step prompts
+ -Help Show this help message
+
+EXAMPLES:
+ .\bootstrap.ps1 # Install with essentials profile
+ .\bootstrap.ps1 -Profile dev # Install development profile
+ .\bootstrap.ps1 -Profile full -Force # Force install full profile
+ .\bootstrap.ps1 -Ask # Interactive installation
+
+"@ -ForegroundColor Cyan
+}
+
+# Script entry point
+if ($Help) {
+ Show-Help
+ exit 0
+}
+
+# Run bootstrap
+try {
+ $result = Start-Bootstrap -Profile $Profile -Force:$Force -Ask:$Ask
+ if (-not $result) {
+ exit 1
+ }
+} catch {
+ Write-Error "Bootstrap failed: $_"
+ Write-Log "Bootstrap failed: $_" "ERROR"
+ exit 1
+}