#region changes to be done
# We could change line 298 and 379 the have it log in "SVS Scripting" instead of "Application" if we can find a way to force create the log
# winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
# winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
#endregion changes to be done
## Last changes made should fix the issues we had wen running thi in Windows 11 25H2
<#
.SYNOPSIS
ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface,
and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment.
.DESCRIPTION
Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used
interactively or via HTTP endpoints, and includes built-in validation and error trapping.
Key features:
- Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook.
- OAuth management - automatically acquires and refreshes bearer tokens over TLS.
- Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json.
- Site list saving - writes fetched site list to the user's Desktop as CSV or JSON.
- Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment.
- Agent download & install - downloads the Datto RMM agent installer and launches it.
- Installer archiving - saves a copy of the downloaded installer to C:\Temp.
- HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch
to log errors and return proper HTTP 500 responses on failure.
- Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs.
Throughout, secrets are never written to logs or console, and all operations produce
clear success/failure messages via Write-LogHybrid.
.PARAMETER UseWebhook
Switch that forces credential retrieval from the webhook at WebhookUrl using WebhookPassword.
When omitted, you must supply ApiUrl, ApiKey, and ApiSecretKey directly.
.PARAMETER WebhookPassword
Password to authenticate to the credentials-fetch webhook. Mandatory when -UseWebhook is set.
.PARAMETER WebhookUrl
URL of the credentials webhook endpoint. Defaults to $Global:DattoWebhookUrl.
.PARAMETER ApiUrl
Direct Datto RMM API base URL (used if not fetching from webhook).
.PARAMETER ApiKey
Direct Datto RMM API key (used if not fetching from webhook).
.PARAMETER ApiSecretKey
Direct Datto RMM secret (used if not fetching from webhook).
.PARAMETER FetchSites
Switch to fetch the list of RMM sites and skip all install or variable-push actions.
.PARAMETER SaveSitesList
Switch to save the fetched site list to the desktop as a file named by OutputFile.
Must be used together with -FetchSites.
.PARAMETER OutputFile
Name of the file to write the site list to (must end in “.csv” or “.json”).
Defaults to 'datto_sites.csv'.
.PARAMETER PushSiteVars
Switch to fetch site-specific variables and write them under HKLM:\Software\SVS\Deployment.
.PARAMETER InstallRMM
Switch to download and launch the Datto RMM agent installer for the specified site.
.PARAMETER SaveCopy
Switch to save a copy of the downloaded Datto RMM installer into C:\Temp.
.PARAMETER SiteUID
The unique identifier of the Datto RMM site. Mandatory when performing install or variable-push.
.PARAMETER SiteName
The friendly name of the Datto RMM site (used in logging). Mandatory when performing install or variable-push.
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
-UseWebhook
-WebhookPassword 'pwd'
-SiteUID 'site-123'
-SiteName 'Acme Corp'
-PushSiteVars
-InstallRMM
# Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry.
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
-ApiUrl 'https://api.example.com' `
-ApiKey 'YourApiKey' `
-ApiSecretKey 'YourSecretKey' `
-SiteUID 'site-123' `
-SiteName 'Acme Corp' `
-PushSiteVars `
-InstallRMM
# Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry.
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
-UseWebhook `
-WebhookPassword 'pwd' `
-FetchSites `
-SaveSitesList `
-OutputFile 'sites.json'
# Fetches the full site list via webhook and saves it as JSON to your Desktop.
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
-ApiUrl 'https://api.example.com' `
-ApiKey 'YourApiKey' `
-ApiSecretKey 'YourSecretKey' `
-SiteUID 'site-123' `
-SiteName 'Acme Corp' `
-SaveCopy
# Downloads the RMM installer for “Acme Corp” and saves a copy under C:\Temp without running it.
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
-ApiUrl 'https://api.example.com' `
-ApiKey 'YourApiKey' `
-ApiSecretKey 'YourSecretKey' `
-SiteUID 'site-123' `
-SiteName 'Acme Corp' `
-InstallRMM `
-WhatIf
# Shows what would happen when installing the RMM agent, without making any changes.
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup
#>
#region Safely bypass Restricted Execution Policy
# ─── Safely bypass Restricted Execution Policy ───
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
(Get-ExecutionPolicy) -eq 'Restricted') {
Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow
if ($PSCommandPath) {
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`""
} else {
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }"
}
exit
}
# ─── TLS and silent install defaults ───
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference = 'None'
#endregion Safely bypass Restricted Execution Policy
function Invoke-ScriptMonkey {
# ─────────────────────────────────────────────────────────────────────────
# PARAMETERS + GLOBAL VARIABLES
# ─────────────────────────────────────────────────────────────────────────
[CmdletBinding(
DefaultParameterSetName='UI',
SupportsShouldProcess=$true,
ConfirmImpact= 'Medium'
)]
param(
# ─────────────────────────────────────────────────────────
# Toolkit-only mode
[Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
# ─────────────────────────────────────────────────────────
# remove Toolkit
[Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup,
# ─────────────────────────────────────────────────────────
# Datto headless mode
# ─── DattoFetch & DattoInstall share the webhook creds ─────────────
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
[switch]$UseWebhook,
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
[string]$WebhookPassword,
[string]$WebhookUrl = $Global:DattoWebhookUrl,
# ─── only DattoFetch uses these ────────────────────────────────────
[Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites,
[Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList,
[Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv',
# ─── only DattoInstall uses these ─────────────────────────────────
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID,
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName,
[Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars,
[Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM,
[Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy
)
#region global variables
# Listening port for HTTP UI
$Port = 8082
# Configurable endpoints
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
# Initialize a global in-memory log cache
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
#endregion global variables
#region SVS Module
function Install-SVSMSP {
param (
[switch] $Cleanup,
[switch] $InstallToolkit,
[Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }),
[Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }),
[Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP",
[Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo",
[Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/"
)
function Perform-Cleanup {
Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule"
# Attempt to uninstall all versions of SVSMSP
try {
Uninstall-Module -Name SVSMSP -AllVersions -Force -ErrorAction Stop
Write-LogHybrid "SVSMSP module uninstalled from system." "Success" "SVSModule" -LogToEvent
}
catch {
# If no module was found, just warn and continue
if ($_.Exception.Message -match 'No match was found') {
Write-LogHybrid "No existing SVSMSP module found to uninstall." "Warning" "SVSModule" -LogToEvent
}
else {
Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
# Remove the custom repository if registered
if (Get-PSRepository -Name SVS_Repo -ErrorAction SilentlyContinue) {
try {
Unregister-PSRepository -Name SVS_Repo -ErrorAction Stop
Write-LogHybrid "SVS_Repo repository unregistered." "Success" "SVSModule" -LogToEvent
}
catch {
Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
# Finally, remove it from the current session if loaded
if (Get-Module -Name SVSMSP) {
try {
Remove-Module SVSMSP -Force -ErrorAction Stop
Write-LogHybrid "SVSMSP module removed from current session." "Success" "SVSModule" -LogToEvent
}
catch {
Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
}
function Perform-ToolkitInstallation {
Perform-Cleanup
Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" -LogToEvent
if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
}
Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" -LogToEvent
Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force
Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent
}
Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent
if ($Cleanup) {
Perform-Cleanup; return
}
if ($InstallToolkit) {
Perform-ToolkitInstallation; return
}
# default if no switch passed:
Perform-ToolkitInstallation
}
#endregion SVS Module
#region Write-Log
# This function is used as a fallback if the SVSMSP module is not installed
# Should change this "[string]$EventLog = "Application", => [string]$EventLog = "SVS Scripting", "
function Write-LogHelper {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Message,
[ValidateSet("Info","Warning","Error","Success","General")]
[string]$Level = "Info",
[string]$TaskCategory = "GeneralTask",
[switch]$LogToEvent,
[string]$EventSource = "Script Automation Monkey",
[string]$EventLog = "Application",
[int] $CustomEventID,
[string]$LogFile,
[switch]$PassThru
)
# ─── IDs & Colors ────────────────────────────────────────────────
$idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }
$colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" }
$EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] }
$color = $colMap[$Level]
$fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
# ─── Console Output ─────────────────────────────────────────────
Write-Host $fmt -ForegroundColor $color
# ─── In-Memory Cache ─────────────────────────────────────────────
# ─── In-Memory Cache ─────────────────────────────────────────────
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
$Global:LogCache.Add([pscustomobject]@{
Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
Level = $Level
Message = $fmt
}) | Out-Null
# ─── File Logging ────────────────────────────────────────────────
if ($PSBoundParameters.LogFile) {
try {
"$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" |
Out-File -FilePath $LogFile -Append -Encoding UTF8
}
catch {
Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow
}
}
# ─── Event Log ──────────────────────────────────────────────────
if ($LogToEvent) {
try {
# 1) Ensure your custom source/log exist
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
}
} catch {
Write-Host "[Warning] Could not create event log '$EventLog' or source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow
return
}
# 2) Map level to entry type
$entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' }
# 3) Write to the Windows event log
try {
Write-EventLog `
-LogName $EventLog `
-Source $EventSource `
-EntryType $entryType `
-EventID $EventID `
-Message $fmt
}
catch {
Write-Host "[Warning] EventLog failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
if ($PassThru) { return $Global:LogCache[-1] }
}
# ─────────────────────────────────────────────────────────────────────────
# WRITE-LOG HYBRID (single definition, chooses at runtime if we use the
# Write-Log from the module or the built-in Write-LogHelper funtions )
# Should chanfge this "[string]$EventLog = "Application"," => "[string]$EventLog = "SVS Scripting","
# ─────────────────────────────────────────────────────────────────────────
function Write-LogHybrid {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$Message,
[ValidateSet("Info","Warning","Error","Success","General")]
[string]$Level = "Info",
[string]$TaskCategory = "GeneralTask",
[switch]$LogToEvent,
[string]$EventSource = "Script Automation Monkey",
[string]$EventLog = "Application",
[ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
[string]$ForegroundColorOverride
)
$formatted = "[$Level] [$TaskCategory] $Message"
if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) {
# 1) print to console with the override color
Write-Host $formatted -ForegroundColor $ForegroundColorOverride
# 2) then forward the call (sans the override) to Write-Log or Write-LogHelper
$invokeParams = @{
Message = $Message
Level = $Level
TaskCategory = $TaskCategory
LogToEvent = $LogToEvent
EventSource = $EventSource
EventLog = $EventLog
}
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log @invokeParams
}
else {
Write-LogHelper @invokeParams
}
}
else {
# No override: let Write-Log / Write-LogHelper handle everything (including console color)
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log `
-Message $Message `
-Level $Level `
-TaskCategory $TaskCategory `
-LogToEvent:$LogToEvent `
-EventSource $EventSource `
-EventLog $EventLog
}
else {
Write-LogHelper `
-Message $Message `
-Level $Level `
-TaskCategory $TaskCategory `
-LogToEvent:$LogToEvent `
-EventSource $EventSource `
-EventLog $EventLog
}
}
}
#endregion Write-Log
#region building the Menus
# Define every task once here:
# Id → checkbox HTML `id`
# Name → URL path (`/Name`)
# Label → user-visible text
# HandlerFn → the PowerShell function to invoke
# Page → which tab/page it appears on
$Global:Tasks = @(
# On-Boarding, left column
@{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' },
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' },
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' },
@{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' },
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' },
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' },
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left';
SubOptions= @(
@{ Value='inputVar'; Label='Copy Site Variables' },
@{ Value='rmm'; Label='Install RMM Agent' },
@{ Value='exe'; Label='Download Executable' }
)
},
# On-Boarding, right column (optional bits)
@{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' },
@{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' },
# Off-Boarding
@{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' },
@{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' },
# Tweaks
@{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' },
# SVS Apps
@{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }
@{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' }
@{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' }
)
#endregion building the Menus
#region Build-Checkboxes
function Build-Checkboxes {
param($Page, $Column)
(
$Global:Tasks |
Where-Object Page -EQ $Page |
Where-Object Column -EQ $Column |
ForEach-Object {
$taskId = $_.Id
$tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
" title='$($_.Tooltip)'"
} else { '' }
$html = ""
if ($_.SubOptions) {
# join inside the code block is fine
$subHtml = (
$_.SubOptions |
ForEach-Object {
""
}
) -join "`n"
$html += @"
$subHtml
"@
}
$html
}
) -join "`n"
}
#endregion Build-Checkboxes
#region Get-ModuleVersionHtml
### Get SVSMSP module version to display in the UI
function Get-ModuleVersionHtml {
$mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1
if ($mod) {
return "Module Version: $($mod.Version)
"
}
return "SVSMSP_Module not found
"
}
#endregion Get-ModuleVersionHtml
#region Strat-Server
function Get-NextFreePort {
param([int]$Start = $Port)
for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
$l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
try { $l.Start(); $l.Stop(); return $p } catch {}
}
throw "No free TCP port available."
}
# Starts the HTTP listener loop
function Start-Server {
$Global:Listener = [System.Net.HttpListener]::new()
$primaryPrefix = "http://localhost:$Port/"
$wildcardPrefix = "http://+:$Port/"
try {
$Global:Listener.Prefixes.Add($primaryPrefix)
$Global:Listener.Start()
Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
}
catch [System.Net.HttpListenerException] {
if ($_.Exception.ErrorCode -eq 5) {
Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL…" Warning Server -LogToEvent
try {
$user = "$env:USERDOMAIN\$env:USERNAME"
if (-not $user.Trim()) { $user = $env:USERNAME }
Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
$Global:Listener = [System.Net.HttpListener]::new()
$Global:Listener.Prefixes.Add($wildcardPrefix)
$Global:Listener.Start()
Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
} catch {
Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
return
}
}
elseif ($_.Exception.NativeErrorCode -in 32,183) {
$old = $Port
$Port = Get-NextFreePort -Start ($Port + 1)
$Global:Listener = [System.Net.HttpListener]::new()
$primaryPrefix = "http://localhost:$Port/"
$Global:Listener.Prefixes.Add($primaryPrefix)
$Global:Listener.Start()
Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
}
else {
Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
return
}
}
try {
while ($Global:Listener.IsListening) {
$ctx = $Global:Listener.GetContext()
try {
Dispatch-Request $ctx
} catch {
Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
}
}
}
finally {
$Global:Listener.Close()
Write-LogHybrid "Listener closed." Info Server -LogToEvent
}
}
#endregion Strat-Server
#region UIHtml
function Get-UIHtml {
param([string]$Page = 'onboard')
# no spaces before $style
$style = @'
'@
# no spaces before $script
$script = @'
'@
# no spaces before $htmlTemplate
$htmlTemplate = @"
Script Monkey
$style

{{moduleVersion}}
Script Automation Monkey (Yeah!)
On-Boarding
This new deployment method ensures everything is successfully deployed with greater ease!
Off-Boarding
{{offboardCheckboxes}}
Tweaks
{{tweaksCheckboxes}}
SVS APPs
{{appsCheckboxes}}
$script
"@
#
# 4) Build the checkbox HTML and tasks JS from $Global:Tasks
#
# On-boarding now has two columns:
$onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
$onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
# Off-boarding, Tweaks, SVSApps stay one-column:
$offboard = Build-Checkboxes -Page 'offboard' -Column ''
$tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
$apps = Build-Checkboxes -Page 'SVSApps' -Column ''
# Tasks JS array (fixed)
$tasksJsAll = (
$Global:Tasks | ForEach-Object {
" { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
}
) -join ",`n"
#
# 5) Inject into template
#
$html = $htmlTemplate
$html = $html.Replace('{{moduleVersion}}', (Get-ModuleVersionHtml))
$html = $html.Replace('{{onboardLeftColumn}}', $onboardLeft)
$html = $html.Replace('{{onboardRightColumn}}', $onboardRight)
$html = $html.Replace('{{offboardCheckboxes}}', $offboard)
$html = $html.Replace('{{tweaksCheckboxes}}', $tweaks)
$html = $html.Replace('{{appsCheckboxes}}', $apps)
$html = $html.Replace('{{tasksJsAll}}', $tasksJsAll)
$html = $html.Replace('{{defaultPage}}', $Page)
return $html
}
#endregion UIHtml
#region Handler Stubs
function Respond-Text {
param($Context, $Text)
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
$Context.Response.ContentType = 'text/plain'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
$Context.Response.OutputStream.Close()
}
function Respond-HTML {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][object] $Context,
[Parameter(Mandatory = $true)][string] $Html
)
$bytes = [Text.Encoding]::UTF8.GetBytes($Html)
$Context.Response.ContentType = 'text/html'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.OutputStream.Close()
}
function Respond-JSON {
param($Context, $Object)
$json = $Object | ConvertTo-Json -Depth 5
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.ContentType = 'application/json'
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
$Context.Response.OutputStream.Close()
}
function Handle-FetchSites {
param($Context)
try {
# 1) Read the incoming JSON payload (contains only the webhook password)
$raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$pw = (ConvertFrom-Json $raw).password
# ★ Store it globally for the next call ★
$Global:WebhookPassword = $pw
# 2) Delegate to your unified function
$sites = Install-DattoRMM `
-UseWebhook `
-WebhookPassword $pw `
-FetchSites `
-SaveSitesList:$SaveSitesList `
-OutputFile $OutputFile
# 3) Return JSON array of sites
Respond-JSON $Context $sites
}
catch {
# Log the exception and return HTTP 500
Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error fetching sites."
}
}
# On-boarding handlers
function Handle-SetSVSPowerPlan {
param($Context)
# 1) call into your module
Set-SVSPowerPlan
# 2) log & write back a simple text response
Write-LogHybrid "PowerPlan set" "Success" "OnBoard"
Respond-Text $Context "PowerPlan applied"
}
function Handle-InstallSVSMSP {
param($Context)
Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard"
try {
Install-SVSMSP -InstallToolkit
Respond-Text $Context "SVSMSP Module installed/updated."
} catch {
Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard"
Respond-Text $Context "ERROR: $_"
}
}
function Handle-InstallCyberQP {
param($Context)
# 1) call into your module
Install-CyberQP
# 2) log & write back a simple text response
Write-LogHybrid "CyberQP installed" "Success" "OnBoard"
Respond-Text $Context "CyberQP installed"
}
function Handle-InstallThreatLocker {
param($Context)
# 1) call into your module
Install-ThreatLocker
# 2) log & write back a simple text response
Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard"
Respond-Text $Context "ThreatLocker installed"
}
function Handle-InstallRocketCyber {
param($Context)
# 1) call into your module
Install-RocketCyber
# 2) log & write back a simple text response
Write-LogHybrid "RocketCyber installed" "Success" "OnBoard"
Respond-Text $Context "RocketCyber installed"
}
function Handle-InstallSVSHelpDesk {
param($Context)
# 1) call into your module
Install-SVSHelpDesk
# 2) log & write back a simple text response
Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard"
Respond-Text $Context "SVS HelpDesk installed"
}
function Handle-InstallDattoRMM {
param($Context)
try {
if ($Context.Request.HttpMethod -ne 'POST') {
$Context.Response.StatusCode = 405
Respond-Text $Context 'Use POST'
return
}
# 1) Read and parse the JSON body
$body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$data = ConvertFrom-Json $body
# 2) Delegate to your unified function for the install
Install-DattoRMM `
-UseWebhook `
-WebhookPassword $Global:WebhookPassword `
-SiteUID $data.UID `
-SiteName $data.Name `
-PushSiteVars:($data.checkedValues -contains 'inputVar') `
-InstallRMM: ($data.checkedValues -contains 'rmm') `
-SaveCopy: ($data.checkedValues -contains 'exe')
# 3) Acknowledge to the client
Respond-Text $Context "Triggered DattoRMM for $($data.Name)"
}
catch {
# Log the exception and return HTTP 500
Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error during DattoRMM install."
}
}
function Handle-InstallChrome { param($Context)
try {
winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
Write-LogHybrid "Installed Google Chrome via winget" Success SVSApps -LogToEvent
Respond-Text $Context "Chrome installed"
} catch {
Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
Respond-Text $Context "ERROR: $($_.Exception.Message)"
}
}
function Handle-InstallAcrobat { param($Context)
try {
winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" Success SVSApps -LogToEvent
Respond-Text $Context "Acrobat Reader installed"
} catch {
Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
Respond-Text $Context "ERROR: $($_.Exception.Message)"
}
}
#endregion Handler Stubs
#region Install-DattoRMM
<#
.SYNOPSIS
Installs/configures the Datto RMM agent, fetches site lists, and optionally saves the site list to disk.
.DESCRIPTION
Centralizes Datto RMM operations in one function:
- Fetch API credentials from a webhook (-UseWebhook)
- Acquire OAuth token
- Fetch site list (-FetchSites)
- Save site list to Desktop as JSON or CSV (-FetchSites + -SaveSitesList)
- Write site variables to registry (-PushSiteVars)
- Download & launch the RMM agent installer (-InstallRMM)
- Save a copy of the installer (-SaveCopy)
.PARAMETER UseWebhook
Fetches ApiUrl, ApiKey, and ApiSecretKey from the webhook when used with WebhookPassword.
.PARAMETER WebhookPassword
Password for authenticating to the credentials webhook.
.PARAMETER WebhookUrl
URL of the credentials webhook. Defaults to $Global:DattoWebhookUrl.
.PARAMETER ApiUrl
Direct Datto API endpoint URL (if not using webhook).
.PARAMETER ApiKey
Direct Datto API key (if not using webhook).
.PARAMETER ApiSecretKey
Direct Datto API secret (if not using webhook).
.PARAMETER FetchSites
Fetches the list of sites and skips all install steps.
.PARAMETER SaveSitesList
Saves the fetched site list to Desktop using OutputFile. Must be used with -FetchSites.
.PARAMETER OutputFile
Filename for saving the site list (.json or .csv). Defaults to 'datto_sites.csv'.
.PARAMETER PushSiteVars
Writes fetched site variables into HKLM:\Software\SVS\Deployment.
.PARAMETER InstallRMM
Downloads and runs the Datto RMM agent installer.
.PARAMETER SaveCopy
Saves a copy of the downloaded agent installer to C:\Temp.
.PARAMETER SiteUID
Unique identifier of the Datto site (required for install and registry push).
.PARAMETER SiteName
Friendly name of the Datto site (used for logging).
.EXAMPLE
# Fetch and save site list via webhook
Install-DattoRMM -UseWebhook -WebhookPassword 'Tndmeeisdwge!' -FetchSites -SaveSitesList -OutputFile 'sites.csv'
.EXAMPLE
# Headless install with site variables
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM
.EXAMPLE
# Download and save installer to C:\Temp without installing
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -SaveCopy
#>
function Install-DattoRMM {
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
param (
[switch]$UseWebhook,
[string]$WebhookPassword,
[string]$WebhookUrl = $Global:DattoWebhookUrl,
[string]$ApiUrl,
[string]$ApiKey,
[string]$ApiSecretKey,
[switch]$FetchSites,
[switch]$SaveSitesList,
[string]$OutputFile = 'datto_sites.csv',
[switch]$PushSiteVars,
[switch]$InstallRMM,
[switch]$SaveCopy,
[string]$SiteUID,
[string]$SiteName
)
# Validate mutually-dependent switches
if ($SaveSitesList -and -not $FetchSites) {
Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent; return
}
# 1) Optionally fetch credentials from webhook
if ($UseWebhook) {
if (-not $WebhookPassword) {
Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent; return
}
try {
$resp = Invoke-RestMethod -Uri $WebhookUrl `
-Headers @{ SVSMSPKit = $WebhookPassword } `
-Method GET
$ApiUrl = $resp.ApiUrl
$ApiKey = $resp.ApiKey
$ApiSecretKey = $resp.ApiSecretKey
Write-LogHybrid "Webhook credentials fetched." Success DattoRMM -LogToEvent
} catch {
Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return
}
}
# 2) Validate API parameters
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent; return
}
# 3) Acquire OAuth token
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
try {
$publicCred = New-Object System.Management.Automation.PSCredential(
'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
)
$tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
-Credential $publicCred `
-Method Post `
-ContentType 'application/x-www-form-urlencoded' `
-Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
$token = $tokenResp.access_token
Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent
} catch {
Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return
}
$headers = @{ Authorization = "Bearer $token" }
# 4) Fetch site list only
if ($FetchSites) {
try {
$sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
$siteList = $sitesResp.sites | ForEach-Object {
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
}
Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM -LogToEvent
if ($SaveSitesList) {
$desktop = [Environment]::GetFolderPath('Desktop')
$path = Join-Path $desktop $OutputFile
$ext = [IO.Path]::GetExtension($OutputFile).ToLower()
if ($ext -eq '.json') {
$siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8
} else {
$siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
}
Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM -LogToEvent
}
return $siteList
} catch {
Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return @()
}
}
# 5) Push site variables to registry
if ($PushSiteVars) {
try {
$varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent
} catch {
Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
$regPath = "HKLM:\Software\SVS\Deployment"
foreach ($v in $varsResp.variables) {
try {
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent
} catch {
Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
}
}
# 6) Download & install RMM agent
if ($InstallRMM) {
if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
try {
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
$tmp = "$env:TEMP\AgentInstall.exe"
Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent
Start-Process -FilePath $tmp -NoNewWindow
Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent
} catch {
Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
}
}
# 7) Save a copy of installer to C:\Temp
if ($SaveCopy) {
try {
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
$path = "C:\Temp\AgentInstall.exe"
if (-not (Test-Path 'C:\Temp')) { New-Item -Path 'C:\Temp' -ItemType Directory | Out-Null }
Invoke-WebRequest -Uri $dlUrl -OutFile $path -UseBasicParsing
Write-LogHybrid "Saved installer copy to $path." Info DattoRMM -LogToEvent
} catch {
Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
}
# 8) Warn if no action was taken
if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM -LogToEvent
}
}
#endregion Install-DattoRMM
#region Dispatch-Request
# Sends the HTML for a given page or invokes a task handler
function Dispatch-Request {
param($Context)
# figure out the path
$path = $Context.Request.Url.AbsolutePath.TrimStart('/')
# ---- Shutdown handler ----
if ($path -eq 'quit') {
Write-LogHybrid "Shutdown requested" "Info" "Server" -LogToEvent
Respond-Text $Context "Server shutting down."
# stop the listener loop
$Global:Listener.Stop()
return
}
# ---- Fetch Sites endpoint ----
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
Handle-FetchSites $Context
return
}
# ---- Serve UI pages ----
if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) {
$page = if ($path -eq '') { 'onboard' } else { $path }
$html = Get-UIHtml -Page $page
Respond-HTML $Context $html
return
}
# ---- Task invocation ----
$task = $Global:Tasks | Where-Object Name -EQ $path
if ($task) {
& $task.HandlerFn $Context
return
}
# ---- 404 ----
$Context.Response.StatusCode = 404
Respond-Text $Context '404 - Not Found'
}
#endregion Dispatch-Request
#region EntryPoint: Define Invoke-ScriptMonkey
# ─────────────────────────────────────────────────────────────────────────
# 3) MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI)
# ─────────────────────────────────────────────────────────────────────────
switch ($PSCmdlet.ParameterSetName) {
'Toolkit' {
Write-LogHybrid "Toolkit-only mode" Info Startup -LogToEvent
Install-SVSMSP -InstallToolkit
return
}
'Cleanup' {
Write-LogHybrid "Running Toolkit cleanup mode" Info Startup -LogToEvent
Install-SVSMSP -Cleanup
return
}
# ───────────────────────────────────────────────────────────
# 2) If user only wants the site list, do that and exit
# ───────────────────────────────────────────────────────────
'DattoFetch' {
Write-LogHybrid "Fetching site list only…" Info DattoAuth -LogToEvent
$sites = Install-DattoRMM `
-UseWebhook `
-WebhookPassword $WebhookPassword `
-FetchSites `
-SaveSitesList:$SaveSitesList `
-OutputFile $OutputFile
Write-LogHybrid "Done." Success DattoAuth -LogToEvent
return
}
# ────────────────────────────────────────────
# 3) Invoke the existing Install-DattoRMM cmdlet
# ────────────────────────────────────────────
'DattoInstall' {
Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth -LogToEvent
if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) {
Install-DattoRMM `
-UseWebhook `
-WebhookPassword $WebhookPassword `
-SiteUID $SiteUID `
-SiteName $SiteName `
-PushSiteVars:$PushSiteVars `
-InstallRMM:$InstallRMM `
-SaveCopy:$SaveCopy
}
return
}
<#
'UI' {
$url = "http://localhost:$Port/"
Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup
# Open the UI in a separate PowerShell job so Start-Server can block safely.
try {
Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock {
param($u)
Start-Sleep -Milliseconds 300
try {
if (Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue) {
Start-Process -FilePath 'msedge.exe' -ArgumentList "--app=$u"
} else {
Start-Process -FilePath $u
}
} catch { }
} -ArgumentList $url | Out-Null
} catch {
Write-LogHybrid "Failed to schedule browser launch: $($_.Exception.Message)" Warning Startup -LogToEvent
}
# Now start the blocking listener loop
Start-Server
return
}
#>
'UI' {
$url = "http://localhost:$Port/"
Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup
# Resolve Edge path explicitly (works in both 32/64-bit installs)
$edgeCandidates = @(
"${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
"$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe
)
$edgePath = ($edgeCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1)
if (-not $edgePath) {
$cmd = Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue
if ($cmd) { $edgePath = $cmd.Path }
}
# Launch Edge (app mode) in a background job so Start-Server can block
Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock {
param($u, $edge)
Start-Sleep -Milliseconds 400
try {
if ($edge -and (Test-Path $edge)) {
Start-Process -FilePath $edge -ArgumentList @('--new-window',"--app=$u")
} else {
Start-Process -FilePath $u # fallback to default browser
}
} catch { }
} -ArgumentList $url, $edgePath | Out-Null
# Now start the blocking listener loop
Start-Server
return
}
#endregion EntryPoint: Define Invoke-ScriptMonkey
#region — guarantee NuGet provider is present without prompting
# ─── Silent defaults ───
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference = 'None'
# ─── Pre-create folder if running as SYSTEM (avoids NuGet install bug) ───
$provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
if (-not (Test-Path $provPath)) {
try {
New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
}
}
# ─── Ensure PowerShellGet is available ───
if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
try {
Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
}
# ─── Ensure PackageManagement is up-to-date ───
$pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
if ($pkgMgmtVersion -lt [Version]"1.3.1") {
try {
Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
}
}
# ─── Import modules silently ───
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
# ─── Trust PSGallery if not already ───
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
try {
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
}
}
# ─── Ensure NuGet is installed silently ───
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
if (-not $nuget) {
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent
} catch {
Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
} else {
Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent
}
# ─── Final import check ───
try {
Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
} catch {
Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
}
#endregion guarantee NuGet provider is present without prompting
#region HTTP Listener & Routing
# Handle shutdown command
if ($path -eq 'quit') {
Write-LogHybrid "Shutdown requested" "Info" "Server"
Respond-Text $Context "Server shutting down."
# This will break out of the while loop in Start-Server
$Global:Listener.Stop()
return
}
#endregion
}
if ($MyInvocation.InvocationName -eq '.') {
# dot-sourced, don't invoke
} elseif ($PSCommandPath) {
# script was saved and run directly
Invoke-ScriptMonkey @PSBoundParameters
} else {
# iwr | iex fallback
if ($args.Count -gt 0) {
# Convert -Param value -Switch into a hashtable for splatting
$namedArgs = @{}
for ($i = 0; $i -lt $args.Count; $i++) {
if ($args[$i] -is [string] -and $args[$i].StartsWith('-')) {
$key = $args[$i].TrimStart('-')
$next = $args[$i + 1]
if ($next -and ($next -notlike '-*')) {
$namedArgs[$key] = $next
$i++ # Skip next one, it's the value
} else {
$namedArgs[$key] = $true
}
}
}
Invoke-ScriptMonkey @namedArgs
} else {
Invoke-ScriptMonkey
}
}