<# .SYNOPSIS Nerdy Neighbor / Nerd Industries — script launcher menu ("easy button"). .DESCRIPTION Run in an ELEVATED (admin) PowerShell console: irm toolkit.nerdyneighbor.net | iex Shows a numbered menu of every shop script. Pick a number and it runs the matching `irm | iex` for you — no typing long URLs. .NOTES - Most targets #Requires -RunAsAdministrator, so run from an admin console. - This launcher itself needs no elevation; it just delegates. - The menu is the ONLY source of truth for what's deployed — edit this table to add/remove entries, push to GitHub, and it's live (GitHub Contents API proxy). #> # Force TLS 1.2 (matches every individual script) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # --------------------------------------------------------------- # The menu. Number = what the user types. Url = the irm target. # Sub = a nested [ordered] table, shown as a submenu (e.g. RustDesk). # Log = the script's own log in C:\ProgramData\NerdyNeighbor (gives OK/FAILED). # NoLog = never record this entry in the local run history (Massgrave). # Keep this table in sync with live deployments. # --------------------------------------------------------------- $Scripts = [ordered]@{ '1' = @{ Name = 'OpenSSH — install (LAN-locked, our key)'; Url = 'https://openssh.nerdyneighbor.net' } '2' = @{ Name = 'OpenSSH — uninstall (clean removal)'; Url = 'https://openssh-uninstall.nerdyneighbor.net' } '3' = @{ Name = 'Malwarebytes — install'; Url = 'https://mbam.nerdyneighbor.net' } '4' = @{ Name = 'Malwarebytes — uninstall'; Url = 'https://mbam-uninstall.nerdyneighbor.net' } '5' = @{ Name = 'Windows System Repair (live progress)'; Url = 'https://repair.nerdyneighbor.net' } '6' = @{ Name = 'Edge — lock search to Google/DuckDuckGo'; Url = 'https://edge.nerdyneighbor.net'; Log = 'edge-search.log' } '7' = @{ Name = 'MTR — packet-loss monitor'; Url = 'https://mtr.nerdyneighbor.net/mtr.ps1' } '8' = @{ Name = 'RustDesk >'; Sub = [ordered]@{ '1' = @{ Name = 'RustDesk — customer install'; Url = 'https://rustdesk.nerdyneighbor.net' } '2' = @{ Name = 'RustDesk — shop/tech install (perm password)'; Url = 'https://rustdesk-shop.nerdyneighbor.net' } '3' = @{ Name = 'RustDesk — uninstall'; Url = 'https://rustdesk-uninstall.nerdyneighbor.net' } '4' = @{ Name = 'RustDesk — convert shop to customer'; Url = 'https://rustdesk-convert.nerdyneighbor.net' } } } '9' = @{ Name = 'Remote Access Audit — check for remote-access software'; Url = 'https://audit.nerdyneighbor.net' } '10' = @{ Name = 'Convert Windows to Pro'; Url = 'https://winpro.nerdyneighbor.net' } '11' = @{ Name = 'Install Ublock Lite on all browsers'; Url = 'https://ublock.nerdyneighbor.net'; Log = 'ublock.log' } '12' = @{ Name = 'Chris Titus WinUtil'; Url = 'https://christitus.com/win' } '13' = @{ Name = 'Massgrave'; Url = 'https://get.activated.win'; NoLog = $true } '14' = @{ Name = 'Slow Disk Optimizer (Only Run on eMMC/HDD)'; Url = 'https://slowdisk.nerdyneighbor.net'; Log = 'slowdisk.log' } '15' = @{ Name = 'Remove third-party antivirus (McAfee/Norton/etc)'; Url = 'https://avremove.nerdyneighbor.net'; Log = 'avremove.log' } '16' = @{ Name = 'Force Windows Update'; Url = 'https://winupdate.nerdyneighbor.net' } '17' = @{ Name = 'Adobe Reader - silent install (no McAfee)'; Url = 'https://reader.nerdyneighbor.net'; Log = 'reader.log' } '18' = @{ Name = 'Clear PowerShell history'; Cmd = 'Clear-History; Remove-Item (Get-PSReadLineOption).HistorySavePath -Force -ErrorAction SilentlyContinue' } } # --------------------------------------------------------------- # Run history (local only) - so a tech can see "I already ran this here". # The toolkit appends one JSON line when it launches a script and one when # that script finishes. Scripts that write their own NerdyNeighbor log (Log =) # also get OK/FAILED from it, including runs started outside the toolkit. # Entries marked NoLog (Massgrave) are never written anywhere. # --------------------------------------------------------------- $NNDir = Join-Path $env:ProgramData 'NerdyNeighbor' $HistFile = Join-Path $NNDir 'toolkit-history.jsonl' function Get-ItemKey($Item) { if ($Item.Url) { $Item.Url } else { $Item.Name } } function Add-History($Item, [string]$Event, [string]$Id) { if ($Item.NoLog) { return } try { if (-not (Test-Path $NNDir)) { New-Item -ItemType Directory -Path $NNDir -Force | Out-Null } $o = [ordered]@{ Id = $Id; Key = (Get-ItemKey $Item); Name = $Item.Name; Event = $Event When = (Get-Date).ToString('o'); By = "$env:USERDOMAIN\$env:USERNAME" } Add-Content -Path $HistFile -Value (ConvertTo-Json -InputObject $o -Compress) -Encoding UTF8 } catch { } } $script:RunCache = $null; $script:RunCacheAt = [datetime]::MinValue function Get-RunHistory { if ($script:RunCache -and ((Get-Date) - $script:RunCacheAt).TotalSeconds -lt 3) { return $script:RunCache } $map = @{}; $ends = @{} if (Test-Path $HistFile) { foreach ($l in @(Get-Content -Path $HistFile -ErrorAction SilentlyContinue)) { try { $e = $l | ConvertFrom-Json } catch { continue } if ($e.Event -eq 'start') { $map[$e.Key] = @{ Id = $e.Id; When = [datetime]$e.When; Status = 'started' } } else { $ends[$e.Id] = $e.Event } } foreach ($k in @($map.Keys)) { if ($ends.ContainsKey($map[$k].Id)) { $map[$k].Status = $ends[$map[$k].Id] } } } $script:RunCache = $map; $script:RunCacheAt = Get-Date $map } # Returns @{ When; Text; Color } for an item that has run on this PC, else $null. function Get-RunMarker($Item) { if ($Item.NoLog) { return $null } if ($Item.Sub) { $last = $null foreach ($c in $Item.Sub.Values) { $m = Get-RunMarker $c; if ($m -and (-not $last -or $m.When -gt $last.When)) { $last = $m } } return $last } $h = (Get-RunHistory)[(Get-ItemKey $Item)] $when = $null; $res = $null if ($h) { $when = $h.When; $res = $h.Status } if ($Item.Log) { $lf = Join-Path $NNDir $Item.Log $fi = Get-Item -Path $lf -ErrorAction SilentlyContinue if ($fi -and (-not $when -or $fi.LastWriteTime -ge $when.AddSeconds(-5))) { $tail = @(Get-Content -Path $lf -Tail 300 -ErrorAction SilentlyContinue) $i = -1 for ($j = $tail.Count - 1; $j -ge 0; $j--) { if ($tail[$j] -match 'Run started') { $i = $j; break } } $seg = if ($i -ge 0) { $tail[$i..($tail.Count - 1)] } else { @($tail | Select-Object -Last 5) } if ($i -ge 0 -and $tail[$i] -match '^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)') { $when = [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss', $null) } elseif (-not $when) { $when = $fi.LastWriteTime } if (@($seg -match '\[ERROR\]').Count) { $res = 'FAILED' } elseif (@($seg -match 'Done').Count) { $res = 'OK' } elseif (-not $res) { $res = 'ran' } } } if (-not $when) { return $null } $label = switch ($res) { 'finished' { 'done' } 'error' { 'ERROR' } 'started' { 'not finished' } default { $res } } $color = switch ($label) { 'OK' { 'DarkGreen' } 'done' { 'DarkGreen' } 'FAILED' { 'Red' } 'ERROR' { 'Red' } default { 'DarkYellow' } } $suffix = if ($label -and $label -ne 'ran') { " - $label" } else { '' } @{ When = $when; Text = ('[ran {0}{1}]' -f $when.ToString('MM/dd/yy h:mmtt'), $suffix); Color = $color } } function Show-Menu($Items, [string]$Title, [bool]$IsSub) { Write-Host '' Write-Host ' ============================================' -ForegroundColor Cyan Write-Host (' {0}' -f $Title) -ForegroundColor Cyan Write-Host ' ============================================' -ForegroundColor Cyan Write-Host '' foreach ($k in $Items.Keys) { $m = Get-RunMarker $Items[$k] if ($m) { Write-Host (' {0,2}. {1} ' -f $k, $Items[$k].Name) -NoNewline Write-Host $m.Text -ForegroundColor $m.Color } else { Write-Host (' {0,2}. {1}' -f $k, $Items[$k].Name) } } Write-Host '' if ($IsSub) { Write-Host ' b. Back' -ForegroundColor DarkGray } Write-Host ' q. Quit' -ForegroundColor DarkGray Write-Host '' } # Runs a script in its OWN window (Windows Terminal if present, else a plain # PowerShell window) so the menu stays open. The child inherits this window's # elevation and any $env:NN_* options. Returns $true if a window was opened. function Start-ItemWindow($Item) { $id = [guid]::NewGuid().ToString('N').Substring(0, 8) $url = $Item.Url -replace "'", "''" $name = ($Item.Name -replace "'", "''") $hist = if ($Item.NoLog) { '' } else { $HistFile -replace "'", "''" } $key = (Get-ItemKey $Item) -replace "'", "''" $title = if ($Item.NoLog) { 'Nerd Toolkit' } else { "Nerd Toolkit - $name" } # `. { }` keeps a script's top-level `return` from skipping the finish mark. $wrapper = @" `$Host.UI.RawUI.WindowTitle = '$title' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function NNTK-Mark([string]`$ev) { if (-not '$hist') { return } try { `$o = [ordered]@{ Id = '$id'; Key = '$key'; Name = '$name'; Event = `$ev; When = (Get-Date).ToString('o'); By = "`$env:USERDOMAIN\`$env:USERNAME" } Add-Content -Path '$hist' -Value (ConvertTo-Json -InputObject `$o -Compress) -Encoding UTF8 } catch { } } Write-Host '' Write-Host ' >>> irm $url | iex' -ForegroundColor Green Write-Host '' try { . { irm '$url' | iex }; NNTK-Mark 'finished' } catch { Write-Host (' ' + `$_.Exception.Message) -ForegroundColor Red; NNTK-Mark 'error' } Write-Host '' Write-Host ' Finished - you can close this window.' -ForegroundColor DarkGray "@ $enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($wrapper)) $psArgs = "-NoExit -NoProfile -ExecutionPolicy Bypass -EncodedCommand $enc" Add-History $Item 'start' $id try { $wt = Get-Command wt.exe -ErrorAction SilentlyContinue if ($wt) { Start-Process -FilePath $wt.Source -ArgumentList "-w new powershell.exe $psArgs" -ErrorAction Stop } else { Start-Process -FilePath 'powershell.exe' -ArgumentList $psArgs -ErrorAction Stop } $script:RunCache = $null return $true } catch { return $false } } # Returns $true when the menu should keep running (script went to a new window). function Invoke-Item($Item) { if ($Item.Cmd) { Write-Host '' Write-Host (' >>> {0}' -f $Item.Cmd) -ForegroundColor Green Invoke-Expression $Item.Cmd | Out-Host $id = [guid]::NewGuid().ToString('N').Substring(0, 8) Add-History $Item 'start' $id; Add-History $Item 'finished' $id $script:RunCache = $null return $true } if (Start-ItemWindow $Item) { return $true } # Couldn't open a window (odd host/policy) - run it right here like before. Write-Host '' Write-Host (' >>> {0}' -f $Item.Name) -ForegroundColor Green Write-Host (' >>> irm {0} | iex' -f $Item.Url) -ForegroundColor Green Write-Host '' irm $Item.Url | iex | Out-Host return $false } # --- key-driven menu (real console) ----------------------------- # Keys act immediately: a number that opens a submenu (8 = RustDesk) expands # it inline right away, no Enter. Picking a script highlights it and Enter # runs it, so one stray key can't launch an install/uninstall. # "1" waits for a possible second digit (10-16); Enter takes plain 1. function Write-MenuLine([string]$Text, [string]$Color = 'Gray', [string]$Suffix = '', [string]$SuffixColor = 'DarkGray') { $w = [Math]::Max(20, [Console]::WindowWidth - 1) if (-not $Suffix) { if ($Text.Length -gt $w) { $Text = $Text.Substring(0, $w) } Write-Host $Text.PadRight($w) -ForegroundColor $Color return } $maxText = [Math]::Max(0, $w - $Suffix.Length - 1) if ($Text.Length -gt $maxText) { $Text = $Text.Substring(0, $maxText) } $col = [Math]::Min([Math]::Max($Text.Length + 2, 66), $w - $Suffix.Length) Write-Host $Text.PadRight($col) -NoNewline -ForegroundColor $Color Write-Host $Suffix.PadRight($w - $col) -ForegroundColor $SuffixColor } function Show-KeyMenu($S) { # If the menu won't fit between the saved top row and the bottom of the # buffer, drawing it scrolls the buffer and makes $S.Top stale (which then # throws on the next SetCursorPosition). Reset to the top of a cleared # screen instead. $need is an upper bound on the lines the menu draws. $bh = [Console]::BufferHeight $need = $Scripts.Count + 9 + $(if ($S.Open) { $Scripts[$S.Open].Sub.Count } else { 0 }) if ($S.Top -lt 0 -or $S.Top + $need -ge $bh) { Clear-Host; $S.Top = 0; $S.Lines = 0 } [Console]::SetCursorPosition(0, [Math]::Min([Math]::Max($S.Top, 0), $bh - 1)) $n = 0 $bar = ' ============================================' foreach ($l in @($bar, ' NERD TOOLKIT - pick a script to run', $bar, '')) { Write-MenuLine $l 'Cyan'; $n++ } foreach ($k in $Scripts.Keys) { $it = $Scripts[$k] $open = ($S.Open -eq $k) $name = $it.Name if ($it.Sub) { $name = ($name -replace '\s*>$', '') + $(if ($open) { ' v' } else { ' >' }) } $color = if ($open) { 'Cyan' } elseif ($S.Open) { 'DarkGray' } elseif ($S.Buf -eq $k) { 'Green' } else { 'Gray' } $m = Get-RunMarker $it if ($m) { Write-MenuLine (' {0,2}. {1}' -f $k, $name) $color $m.Text $(if ($S.Open -and -not $open) { 'DarkGray' } else { $m.Color }) } else { Write-MenuLine (' {0,2}. {1}' -f $k, $name) $color } $n++ if ($open) { foreach ($sk in $it.Sub.Keys) { $c = if ($S.Buf -eq $sk) { 'Green' } else { 'White' } $sm = Get-RunMarker $it.Sub[$sk] if ($sm) { Write-MenuLine (' {0}. {1}' -f $sk, $it.Sub[$sk].Name) $c $sm.Text $sm.Color } else { Write-MenuLine (' {0}. {1}' -f $sk, $it.Sub[$sk].Name) $c } $n++ } } } Write-MenuLine ''; $n++ $items = if ($S.Open) { $Scripts[$S.Open].Sub } else { $Scripts } if ($S.Buf -and $items.Contains($S.Buf) -and -not $items[$S.Buf].Sub) { $again = if (Get-RunMarker $items[$S.Buf]) { ' (already ran here)' } else { '' } Write-MenuLine (' > {0} - Enter = run, Esc = cancel{1}' -f $items[$S.Buf].Name, $again) 'Green' } elseif ($S.Buf) { Write-MenuLine (' > {0}_' -f $S.Buf) 'Green' } elseif ($S.Msg) { Write-MenuLine (' {0}' -f $S.Msg) $S.MsgColor } else { Write-MenuLine '' } $n++ $hint = if ($S.Open) { ' Press a number | b / Esc = back | q = quit' } else { ' Press a number | q = quit' } Write-MenuLine $hint 'DarkGray'; $n++ for ($i = $n; $i -lt $S.Lines; $i++) { Write-MenuLine '' } $S.Lines = $n } function Start-KeyMenu { Write-Host '' $S = @{ Top = [Console]::CursorTop; Lines = 0; Open = $null; Buf = ''; Msg = ''; MsgColor = 'Yellow' } while ($true) { try { Show-KeyMenu $S } catch { Clear-Host; $S.Top = 0; $S.Lines = 0; Show-KeyMenu $S } $key = [Console]::ReadKey($true) $S.Msg = ''; $S.MsgColor = 'Yellow' $items = if ($S.Open) { $Scripts[$S.Open].Sub } else { $Scripts } $ch = "$($key.KeyChar)" if ($key.Key -eq 'Enter') { if ($S.Buf -and $items.Contains($S.Buf) -and -not $items[$S.Buf].Sub) { $item = $items[$S.Buf] [Console]::SetCursorPosition(0, [Math]::Min($S.Top + $S.Lines, [Console]::BufferHeight - 1)) if (-not (Invoke-Item $item)) { return } if ($item.Cmd) { # inline command printed below the menu - start a fresh menu under it Write-Host ''; $S.Top = [Console]::CursorTop; $S.Lines = 0 } $S.Buf = '' $S.Msg = if ($item.Cmd) { "Done: $($item.Name)" } else { "Opened in a new window: $($item.Name)" } $S.MsgColor = 'Green' continue } if (-not $S.Buf -and -not $S.Open) { break } continue } if ($key.Key -eq 'Escape' -or $key.Key -eq 'Backspace' -or ($ch -match '^[bB]$' -and $S.Open)) { if ($S.Buf -and $key.Key -eq 'Backspace') { $S.Buf = $S.Buf.Substring(0, $S.Buf.Length - 1) } elseif ($S.Buf) { $S.Buf = '' } elseif ($S.Open) { $S.Open = $null } elseif ($key.Key -eq 'Escape') { break } continue } if ($ch -match '^[qQ]$') { break } if ($ch -notmatch '^\d$') { continue } # Inside an open submenu: its own numbers first; the parent's number # closes it; any other main-menu number jumps back out to that item. if ($S.Open) { if ($items.Contains($ch)) { $S.Buf = $ch; continue } if ($ch -eq $S.Open) { $S.Open = $null; $S.Buf = ''; continue } $S.Open = $null; $S.Buf = '' } $try = $S.Buf + $ch if (-not @($Scripts.Keys | Where-Object { $_.StartsWith($try) }).Count) { $try = $ch } $prefixed = @($Scripts.Keys | Where-Object { $_.StartsWith($try) }) if (-not $prefixed.Count) { $S.Buf = ''; $S.Msg = ('"{0}" is not a valid option.' -f $try); continue } if ($Scripts.Contains($try) -and $Scripts[$try].Sub -and $prefixed.Count -eq 1) { $S.Open = $try; $S.Buf = '' # submenu: expand instantly continue } $S.Buf = $try } [Console]::SetCursorPosition(0, [Math]::Min($S.Top + $S.Lines, [Console]::BufferHeight - 1)) Write-Host ' Exiting.' -ForegroundColor DarkGray } # --- line-based menu (ISE, redirected hosts) --------------------- # Entries with a Sub table open a submenu ('b' goes back to the main menu). function Start-LineMenu { $items = $Scripts; $title = 'NERD TOOLKIT — pick a script to run'; $isSub = $false while ($true) { Show-Menu $items $title $isSub $prompt = if ($isSub) { ' Enter a number (b = back, q = quit)' } else { ' Enter a number (or q)' } $pick = (Read-Host $prompt).Trim() if ($pick -match '^[qQ]$' -or ([string]::IsNullOrWhiteSpace($pick) -and -not $isSub)) { Write-Host ' Exiting.' -ForegroundColor DarkGray break } if ($isSub -and ($pick -match '^[bB]$' -or [string]::IsNullOrWhiteSpace($pick))) { $items = $Scripts; $title = 'NERD TOOLKIT — pick a script to run'; $isSub = $false continue } if ($items.Contains($pick)) { $item = $items[$pick] if ($item.Sub) { $items = $item.Sub; $title = ('NERD TOOLKIT — {0}' -f ($item.Name -replace '\s*>$', '')); $isSub = $true continue } if (-not (Invoke-Item $item)) { break } Write-Host (' Opened in a new window: {0}' -f $item.Name) -ForegroundColor Green continue } Write-Host (' "{0}" is not a valid option.' -f $pick) -ForegroundColor Yellow } } # --- interactive? ----------------------------------------------- if (-not [Console]::IsInputRedirected) { $keyMode = $Host.Name -eq 'ConsoleHost' if ($keyMode) { try { [void][Console]::CursorTop; [void][Console]::WindowWidth } catch { $keyMode = $false } } if ($keyMode) { Start-KeyMenu } else { Start-LineMenu } } else { Show-Menu $Scripts 'NERD TOOLKIT — pick a script to run' $false Write-Host ' Non-interactive input detected — run this interactively to pick a script.' -ForegroundColor Yellow }