lib/core.ps1
|
# WinMole Core Utilities # Speed-optimized helpers used across all WinMole commands Set-StrictMode -Version Latest # ── Version ────────────────────────────────────────────────────────────────── $script:WINMOLE_VERSION = '0.1.1' # ── PwshSpectreConsole ──────────────────────────────────────────────────────── if (-not (Get-Module PwshSpectreConsole)) { Import-Module PwshSpectreConsole -ErrorAction Stop } function script:Esc { param([string]$Text) if (-not $Text) { return '' } return [Spectre.Console.Markup]::Escape($Text) } # ── Output helpers ──────────────────────────────────────────────────────────── function Write-Success { param([string]$Message, [string]$Right = '') if ($Right) { $pad = [Math]::Max(0, 56 - $Message.Length) Write-SpectreHost " [green]✓[/] $(Esc $Message)$(' ' * $pad)[gold1]$(Esc $Right)[/]" } else { Write-SpectreHost " [green]✓[/] $(Esc $Message)" } } function Write-Info { param([string]$Message) Write-SpectreHost " [teal]→[/] $(Esc $Message)" } function Write-Warn { param([string]$Message) Write-SpectreHost " [yellow]⚠[/] $(Esc $Message)" } function Write-Err { param([string]$Message) Write-SpectreHost " [red]✗[/] $(Esc $Message)" } function Write-Header { param([string]$Title, [string]$Sub = '') Write-Host "" $line = " [bold deepskyblue1]$(Esc $Title)[/]" if ($Sub) { $line += " [grey]$(Esc $Sub)[/]" } Write-SpectreHost $line Write-Host "" } function Write-Divider { Write-SpectreHost "[grey]$('=' * 68)[/]" } function Write-Summary { param([string]$Label, [string]$Value) Write-Divider Write-SpectreHost " [bold]$(Esc $Label)[/] [green]$(Esc $Value)[/]" Write-Divider Write-Host "" } # ── Size formatting ─────────────────────────────────────────────────────────── function Format-Bytes { param([long]$Bytes) if ($Bytes -ge 1TB) { return '{0:F1}TB' -f ($Bytes / 1TB) } if ($Bytes -ge 1GB) { return '{0:F1}GB' -f ($Bytes / 1GB) } if ($Bytes -ge 1MB) { return '{0:F1}MB' -f ($Bytes / 1MB) } if ($Bytes -ge 1KB) { return '{0:F1}KB' -f ($Bytes / 1KB) } return "${Bytes}B" } # ── Fast directory-size calculation using .NET enumeration ──────────────────── function Get-DirectorySize { param([string]$Path) $total = [long]0 try { $opts = [System.IO.EnumerationOptions]::new() $opts.IgnoreInaccessible = $true $opts.RecurseSubdirectories = $true foreach ($f in [System.IO.Directory]::EnumerateFiles($Path, '*', $opts)) { try { $total += (New-Object System.IO.FileInfo($f)).Length } catch {} } } catch {} return $total } # ── Fast parallel directory sizes using runspaces ───────────────────────────── function Get-DirectorySizes { param([string[]]$Paths) $results = @{} if ($Paths.Count -eq 0) { return $results } $pool = [runspacefactory]::CreateRunspacePool( 1, [Math]::Min($Paths.Count, [Environment]::ProcessorCount * 2)) $pool.Open() $sizeBlock = { param($p) $total = [long]0 try { $opts = [System.IO.EnumerationOptions]::new() $opts.IgnoreInaccessible = $true $opts.RecurseSubdirectories = $true foreach ($f in [System.IO.Directory]::EnumerateFiles($p, '*', $opts)) { try { $total += [System.IO.FileInfo]::new($f).Length } catch {} } } catch {} [pscustomobject]@{ Path = $p; Size = $total } } $handles = foreach ($path in $Paths) { $ps = [System.Management.Automation.PowerShell]::Create() $ps.RunspacePool = $pool [void]$ps.AddScript($sizeBlock).AddArgument($path) [pscustomobject]@{ PS = $ps; Handle = $ps.BeginInvoke() } } foreach ($h in $handles) { try { $r = $h.PS.EndInvoke($h.Handle) if ($r) { $results[$r.Path] = $r.Size } } catch {} $h.PS.Dispose() } $pool.Close() $pool.Dispose() return $results } # ── Safe file removal ───────────────────────────────────────────────────────── function Remove-ItemSafe { param([string]$Path, [switch]$Recurse, [switch]$DryRun) if (-not (Test-Path $Path)) { return 0L } $size = [long]0 try { if ($Recurse) { $size = Get-DirectorySize -Path $Path } else { $size = (New-Object System.IO.FileInfo($Path)).Length } if (-not $DryRun) { Remove-Item -LiteralPath $Path -Recurse:$Recurse -Force -ErrorAction Stop } } catch { # Deletion failed — don't count as freed return 0L } return $size } # ── Progress bar ────────────────────────────────────────────────────────────── function Get-ProgressBar { param([double]$Pct, [int]$Width = 20, [string]$Color = '') $filled = [int]([Math]::Round($Pct / 100.0 * $Width)) $filled = [Math]::Max(0, [Math]::Min($filled, $Width)) $empty = $Width - $filled $bar = '█' * $filled + '░' * $empty if ($Color) { return "[$Color]$bar[/]" } return $bar } # ── Mini spark bar (5-block) ────────────────────────────────────────────────── function Get-SparkBar { param([double]$Pct) $blocks = @('▯','▮▯▯▯▯','▮▮▯▯▯','▮▮▮▯▯','▮▮▮▮▯','▮▮▮▮▮') $idx = [int]([Math]::Round($Pct / 100.0 * ($blocks.Count - 1))) $idx = [Math]::Max(0, [Math]::Min($idx, $blocks.Count - 1)) return $blocks[$idx] } # ── Confirmation prompt ─────────────────────────────────────────────────────── function Confirm-Action { param([string]$Message = 'Proceed?') Write-Host "" try { return Read-SpectreConfirm -Prompt $Message -DefaultAnswer n } catch { return $false } } # ── Whitelist support ───────────────────────────────────────────────────────── $_home = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } $script:WINMOLE_CONFIG = Join-Path $_home '.config\winmole' $script:WHITELIST_FILE = Join-Path $script:WINMOLE_CONFIG 'whitelist.txt' function Get-Whitelist { if (-not (Test-Path $script:WHITELIST_FILE)) { return @() } return Get-Content $script:WHITELIST_FILE -ErrorAction SilentlyContinue | Where-Object { $_ -and -not $_.StartsWith('#') } } function Test-Whitelisted { param([string]$Path, [string[]]$Whitelist) foreach ($w in $Whitelist) { if ($Path -like $w) { return $true } } return $false } # ── Admin check ─────────────────────────────────────────────────────────────── function Test-IsAdmin { try { if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( [System.Runtime.InteropServices.OSPlatform]::Windows)) { return $false } $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = [System.Security.Principal.WindowsPrincipal]$id return $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } # ── System info cache ───────────────────────────────────────────────────────── $script:_sysInfoCache = $null function Get-SysInfoSummary { if ($script:_sysInfoCache) { return $script:_sysInfoCache } try { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue $proc = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1 $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue | Select-Object -First 1 $totalRam = [Math]::Round($cs.TotalPhysicalMemory / 1GB, 0) $usedRam = [Math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / 1GB, 1) $diskUsed = [Math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 0) $diskTot = [Math]::Round($disk.Size / 1GB, 0) $uptime = Format-Uptime -Seconds ([long](New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).TotalSeconds) $script:_sysInfoCache = [pscustomobject]@{ MachineName = $env:COMPUTERNAME CPU = $proc.Name -replace '\s{2,}',' ' RamTotalGB = $totalRam RamUsedGB = $usedRam DiskUsedGB = $diskUsed DiskTotalGB = $diskTot OSCaption = $os.Caption Uptime = $uptime } } catch { $script:_sysInfoCache = [pscustomobject]@{ MachineName = $env:COMPUTERNAME CPU = 'Unknown'; RamTotalGB = 0; RamUsedGB = 0 DiskUsedGB = 0; DiskTotalGB = 0 OSCaption = 'Windows'; Uptime = '?' } } return $script:_sysInfoCache } function Format-Uptime { param([long]$Seconds) $d = [int]($Seconds / 86400) $h = [int](($Seconds % 86400) / 3600) $m = [int](($Seconds % 3600) / 60) if ($d -gt 0) { return "${d}d ${h}h" } if ($h -gt 0) { return "${h}h ${m}m" } return "${m}m" } # ── Log setup ───────────────────────────────────────────────────────────────── $_homeLog = if ($env:USERPROFILE) { $env:USERPROFILE } else { $env:HOME } $script:LOG_DIR = Join-Path $_homeLog '.cache\winmole\logs' $script:LOG_FILE = Join-Path $script:LOG_DIR 'operations.log' function Write-OpLog { param([string]$Message) if ($env:WINMOLE_NO_OPLOG -eq '1') { return } try { if (-not (Test-Path $script:LOG_DIR)) { [void][System.IO.Directory]::CreateDirectory($script:LOG_DIR) } $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' "$ts $Message" | Add-Content -LiteralPath $script:LOG_FILE -ErrorAction SilentlyContinue } catch {} } |