KillerScripts.psm1
|
#Requires -Version 5.1 <# KillerScripts Always-latest launcher for the killer-scripts repo. Every tool is pulled fresh from GitHub the moment you run it, so you always execute the current version. A local cache under %LOCALAPPDATA%\KillerScripts is kept only as an offline fallback. Repo: https://github.com/SteveTheKiller/killer-scripts #> $script:RawBase = 'https://raw.githubusercontent.com/SteveTheKiller/killer-scripts/main/' $script:CacheDir = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'KillerScripts' # Custom short aliases for scripts whose lowercase name would collide with a # built-in (PRINT.ps1 -> KillerPrint, since 'print' is print.exe). $script:AliasOverrides = @{ 'PRINT.ps1' = 'KillerPrint' } function Set-KsTls { # GitHub requires TLS 1.2. Windows PowerShell 5.1 on older boxes can default lower. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } } function Get-KsCacheDir { if (-not (Test-Path -LiteralPath $script:CacheDir)) { New-Item -ItemType Directory -Path $script:CacheDir -Force | Out-Null } $script:CacheDir } function Get-KsWeb { param([Parameter(Mandatory = $true)][string]$Url) Set-KsTls (Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop).Content } function Get-KsManifest { # Returns parsed descriptions.json. Live from the repo, cached on success, # read from cache only when the repo is unreachable. [CmdletBinding()] param() $cacheFile = Join-Path (Get-KsCacheDir) 'descriptions.json' try { $json = Get-KsWeb -Url ($script:RawBase + 'descriptions.json') [System.IO.File]::WriteAllText($cacheFile, $json) } catch { if (Test-Path -LiteralPath $cacheFile) { Write-Verbose 'KillerScripts: repo unreachable, using cached manifest.' $json = [System.IO.File]::ReadAllText($cacheFile) } else { throw "KillerScripts: cannot reach the repo and no cached manifest exists. $($_.Exception.Message)" } } $json | ConvertFrom-Json } function Get-KillerScript { # List the available killer-scripts with their titles and descriptions. [CmdletBinding()] param() $m = Get-KsManifest $out = foreach ($p in $m.PSObject.Properties) { if ($p.Name -notlike '*.ps1') { continue } [pscustomobject]@{ Command = 'Invoke-' + ($p.Name -replace '\.ps1$', '') Script = $p.Name Name = $p.Value.name Description = $p.Value.description } } $out | Sort-Object Script } function Invoke-KillerScript { # Fetch the latest version of a killer-script from the repo and run it. # Falls back to the cached copy only if the repo is unreachable. [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string]$Name, [switch]$Offline, [Parameter(ValueFromRemainingArguments = $true)] [object[]]$ScriptArgs ) $file = if ($Name -like '*.ps1') { $Name } else { "$Name.ps1" } $cachePath = Join-Path (Get-KsCacheDir) $file if (-not $Offline) { try { $body = Get-KsWeb -Url ($script:RawBase + $file) [System.IO.File]::WriteAllText($cachePath, $body) } catch { if (Test-Path -LiteralPath $cachePath) { Write-Warning "KillerScripts: could not fetch $file, running the cached copy." } else { throw "KillerScripts: could not fetch $file and there is no cached copy. $($_.Exception.Message)" } } } elseif (-not (Test-Path -LiteralPath $cachePath)) { throw "KillerScripts: no cached copy of $file. Run it once online first." } # Run with the call operator so the script executes in its own scope and # exit / top-level flow behave exactly as they do when run standalone. & $cachePath @ScriptArgs } function Update-KillerScripts { # Refresh the cached manifest and pre-download every script for offline use. [CmdletBinding()] param() $m = Get-KsManifest $count = 0 foreach ($p in $m.PSObject.Properties) { if ($p.Name -notlike '*.ps1') { continue } try { $body = Get-KsWeb -Url ($script:RawBase + $p.Name) [System.IO.File]::WriteAllText((Join-Path (Get-KsCacheDir) $p.Name), $body) $count++ } catch { Write-Warning "KillerScripts: failed to cache $($p.Name): $($_.Exception.Message)" } } Write-Host "KillerScripts: cached $count scripts for offline use." } # Tab completion of -Name, sourced from the manifest that shipped with the module. Register-ArgumentCompleter -CommandName Invoke-KillerScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete) $shipped = Join-Path $PSScriptRoot 'descriptions.json' if (-not (Test-Path -LiteralPath $shipped)) { return } $m = [System.IO.File]::ReadAllText($shipped) | ConvertFrom-Json foreach ($p in $m.PSObject.Properties) { if ($p.Name -notlike '*.ps1') { continue } $base = $p.Name -replace '\.ps1$', '' if ($base -like "$wordToComplete*") { [System.Management.Automation.CompletionResult]::new($base, $base, 'ParameterValue', $p.Value.name) } } } # Build a friendly wrapper (Invoke-<NAME>) and short alias (<name>) per script from # the manifest shipped with this version. Content is still pulled live at run time. # New scripts get wrappers after the next publish (Build-KillerScripts.ps1). $script:KsFunctions = @('Get-KillerScript', 'Invoke-KillerScript', 'Update-KillerScripts') $script:KsAliases = @() $shippedManifest = Join-Path $PSScriptRoot 'descriptions.json' if (Test-Path -LiteralPath $shippedManifest) { $m = [System.IO.File]::ReadAllText($shippedManifest) | ConvertFrom-Json foreach ($p in $m.PSObject.Properties) { if ($p.Name -notlike '*.ps1') { continue } $base = $p.Name -replace '\.ps1$', '' $fn = "Invoke-$base" Set-Item -Path "function:\$fn" -Value ([ScriptBlock]::Create( "param([Parameter(ValueFromRemainingArguments=`$true)]`$ScriptArgs) Invoke-KillerScript -Name '$($p.Name)' @ScriptArgs" )) -Force $script:KsFunctions += $fn # Use a custom alias where defined, otherwise the lowercase name. Only # claim it when nothing already owns that name, so built-ins like # print.exe and any future collision stay untouched. $short = if ($script:AliasOverrides.ContainsKey($p.Name)) { $script:AliasOverrides[$p.Name] } else { $base.ToLower() } if (-not (Get-Command -Name $short -ErrorAction SilentlyContinue)) { Set-Alias -Name $short -Value $fn -Scope Script -Force $script:KsAliases += $short } } } Export-ModuleMember -Function $script:KsFunctions -Alias $script:KsAliases |