diff --git a/ScriptMonkey_v2.ps1 b/ScriptMonkey_v2.ps1
new file mode 100644
index 0000000..f3da843
--- /dev/null
+++ b/ScriptMonkey_v2.ps1
@@ -0,0 +1,1806 @@
+#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
+#endregion changes to be done
+
+<#
+.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
+ # This function is used as a fallback if the SVSMSP module is not installed
+ 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 )
+ # ─────────────────────────────────────────────────────────────────────────
+
+ 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' }
+ )
+
+ #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
+ # Starts the HTTP listener loop
+ function Start-Server {
+ # make it accessible to Dispatch-Request
+ $Global:Listener = [System.Net.HttpListener]::new()
+ $Global:Listener.Prefixes.Add("http://localhost:$Port/")
+ $Global:Listener.Start()
+ Write-LogHybrid "Listening on http://localhost:$Port/" Info Server
+
+
+ try {
+ while ($Global:Listener.IsListening) {
+ $ctx = $Global:Listener.GetContext()
+ try {
+ Dispatch-Request $ctx
+ } catch {
+ Write-LogHybrid "Dispatch error: $_" "Error" "Server" -LogToEvent
+ }
+ }
+ } finally {
+ # once the loop exits, clean up
+ $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."
+ }
+}
+ #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' {
+ Write-LogHybrid "Starting ScriptMonkey UI on http://localhost:$Port/" Info Startup
+ Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port"
+ Start-Server # blocks until you click Exit
+ 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
+ }
+}
+
+