<# .SYNOPSIS Script Automation Monkey (SAMY) main entry point. .DESCRIPTION This file is now the orchestration layer only. It handles: - Execution policy bypass for restricted environments - Global config (branch, repo base, URLs) - Loading subsystem scripts (logging, SVSMSP, Datto, printers, UI, HTTP, etc.) - Exposing Invoke-ScriptAutomationMonkey with parameter sets for: - UI - Toolkit-only install - Toolkit cleanup - Headless Datto site fetch - Headless Datto install - Headless offboarding - The iwr | iex glue at the bottom so remote calls still work. All heavy logic lives in the Samy.*.ps1 subsystem files that are dot-sourced or loaded from your Git repo, now under a "module" subfolder. #> #region 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://samy.svstools.com' -UseBasicParsing | iex }" } exit } #endregion Safely bypass Restricted Execution Policy #region Global defaults and config [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' # Default HTTP listening port for UI $Script:SamyPort = 8082 # SAMY asset config (change branch or base once and it updates everything) $Script:SamyBranch = 'beta' # or 'main' $Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch' # Top level assets $Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg" $Script:SamyBgLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SAMY.png" $Script:SamyFaviconUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_Favicon.ico" $Script:SamyCssUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.css?raw=1" $Script:SamyJsUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.js?raw=1" # Datto webhook URL (used by Datto subsystem) $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' # In-memory log cache if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { $Global:LogCache = [System.Collections.ArrayList]::new() } #endregion Global defaults and config #region Module loader function Import-SamyModule { <# .SYNOPSIS Loads a SAMY subsystem script from local disk or from the Git repo. .DESCRIPTION Local: - Prefer .\module\ - Fallback to .\ Remote (iwr | iex): - Try .../module/?raw=1 first - If that 404s, fallback to .../?raw=1 #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Name ) # 1) Local dev mode: script saved to disk if ($PSCommandPath) { $moduleRoot = Join-Path -Path $PSScriptRoot -ChildPath 'module' $localModulePath = Join-Path -Path $moduleRoot -ChildPath $Name $localRootPath = Join-Path -Path $PSScriptRoot -ChildPath $Name if (Test-Path $localModulePath) { . $localModulePath return } if (Test-Path $localRootPath) { . $localRootPath return } } # 2) Remote mode (iwr | iex): pull module from repo $base = "$Script:SamyRepoBase/$Script:SamyBranch" $primaryUrl = "$base/module/$Name?raw=1" $fallbackUrl = "$base/$Name?raw=1" # Helper to download and load a URL function Invoke-LoadUrl { param( [Parameter(Mandatory)][string]$Url, [Parameter(Mandatory)][string]$ModuleName ) $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop $content = $resp.Content if (-not $content) { Write-Host ("[Error] Module {0} from {1} returned empty content." -f $ModuleName, $Url) -ForegroundColor Red throw "Empty module content." } Invoke-Expression $content } try { # Try /module/?raw=1 first Invoke-LoadUrl -Url $primaryUrl -ModuleName $Name } catch [System.Net.WebException] { $response = $_.Exception.Response $statusCode = $null if ($response -and $response.StatusCode) { $statusCode = [int]$response.StatusCode } if ($statusCode -eq 404) { # Fallback to root-level file Write-Host ("[Info] Module {0} not found at {1} (404). Trying fallback {2}." -f $Name, $primaryUrl, $fallbackUrl) -ForegroundColor Yellow try { Invoke-LoadUrl -Url $fallbackUrl -ModuleName $Name } catch { Write-Host ("[Error] Failed to load module {0} from fallback {1}: {2}" -f $Name, $fallbackUrl, $_.Exception.Message) -ForegroundColor Red throw } } else { Write-Host ("[Error] Failed to load module {0} from {1}: {2}" -f $Name, $primaryUrl, $_.Exception.Message) -ForegroundColor Red throw } } catch { if (-not ($_ -is [System.Net.WebException])) { Write-Host ("[Error] Failed to load module {0} from {1}: {2}" -f $Name, $primaryUrl, $_.Exception.Message) -ForegroundColor Red } throw } } # Load subsystems in a predictable order Import-SamyModule -Name 'Samy.Logging.ps1' # Write-LogHelper, Write-LogHybrid Import-SamyModule -Name 'Samy.SVSBootstrap.ps1' # Install-SVSMSP, cleanup, NuGet bootstrap Import-SamyModule -Name 'Samy.UI.ps1' # $Global:SamyTasks, UI HTML, Get-UIHtml, etc. Import-SamyModule -Name 'Samy.Datto.ps1' # Install-DattoRMM, Datto HTTP handlers Import-SamyModule -Name 'Samy.Printers.ps1' # Printer config, drivers, HTTP handlers Import-SamyModule -Name 'Samy.Apps.ps1' # Winget app handlers (Chrome, Acrobat, etc.) Import-SamyModule -Name 'Samy.Offboard.ps1' # Offboarding handlers and full offboard flow Import-SamyModule -Name 'Samy.Onboarding.ps1' # Onboarding handlers, RenameComputer, etc. Import-SamyModule -Name 'Samy.Http.ps1' # Send-Text/JSON, Dispatch-Request, Start-SamyHttpServer #endregion Module loader #region Simple helpers that are local to this main file function Test-ComputerName { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name ) if ([string]::IsNullOrWhiteSpace($Name)) { return $false } if ($Name.Length -gt 15) { return $false } if ($Name -notmatch '^[A-Za-z0-9-]+$') { return $false } return $true } #endregion Simple helpers #region Main entry point: Invoke-ScriptAutomationMonkey function Invoke-ScriptAutomationMonkey { [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, # Headless offboarding [Parameter(Mandatory, ParameterSetName = 'Offboard')] [switch]$Offboard, # Datto headless mode shared params [Parameter(Mandatory, ParameterSetName = 'DattoFetch')] [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [switch]$UseWebhook, [Parameter(Mandatory, ParameterSetName = 'DattoFetch')] [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [string]$WebhookPassword, [string]$WebhookUrl = $Global:DattoWebhookUrl, # DattoFetch only [Parameter(ParameterSetName = 'DattoFetch')] [switch]$FetchSites, [Parameter(ParameterSetName = 'DattoFetch')] [switch]$SaveSitesList, [Parameter(ParameterSetName = 'DattoFetch')] [ValidatePattern('\.csv$|\.json$')] [string]$OutputFile = 'datto_sites.csv', # DattoInstall only [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 ) switch ($PSCmdlet.ParameterSetName) { 'Toolkit' { Write-LogHybrid "Toolkit-only mode requested." Info Startup -LogToEvent Install-SVSMSP -InstallToolkit return } 'Cleanup' { Write-LogHybrid "Toolkit cleanup requested." Info Startup -LogToEvent Install-SVSMSP -Cleanup return } 'DattoFetch' { Write-LogHybrid "DattoFetch mode: fetching site list." Info DattoAuth -LogToEvent $sites = Install-DattoRMM ` -UseWebhook ` -WebhookPassword $WebhookPassword ` -WebhookUrl $WebhookUrl ` -FetchSites ` -SaveSitesList:$SaveSitesList ` -OutputFile $OutputFile $count = if ($sites) { $sites.Count } else { 0 } Write-LogHybrid "DattoFetch completed with $count sites." Success DattoAuth -LogToEvent return } 'DattoInstall' { Write-LogHybrid "DattoInstall mode: headless RMM deploy to '$SiteName'." Info DattoAuth -LogToEvent if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless Install-DattoRMM")) { Install-DattoRMM ` -UseWebhook ` -WebhookPassword $WebhookPassword ` -WebhookUrl $WebhookUrl ` -SiteUID $SiteUID ` -SiteName $SiteName ` -PushSiteVars:$PushSiteVars ` -InstallRMM:$InstallRMM ` -SaveCopy:$SaveCopy } Write-LogHybrid "DattoInstall completed for '$SiteName'." Success DattoAuth -LogToEvent return } 'Offboard' { Write-LogHybrid "Headless offboarding requested." Info OffBoard -LogToEvent Invoke-SamyFullOffboard return } 'UI' { # Default UI mode: launch browser and start HTTP listener $port = $Script:SamyPort $url = "http://localhost:$port/" Write-LogHybrid "Starting ScriptAutomationMonkey UI on $url" Info Startup -LogToEvent # Resolve Edge path explicitly $edgeCandidates = @( "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" ) $edgePath = $edgeCandidates | Where-Object { $_ -and (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) or default browser in a background job Start-Job -Name 'OpenScriptAutomationMonkeyUI' -ScriptBlock { param([string]$u, [string]$edgeExe) Start-Sleep -Milliseconds 400 try { if ($edgeExe -and (Test-Path $edgeExe)) { Start-Process -FilePath $edgeExe -ArgumentList @('--new-window', "--app=$u") } else { Start-Process -FilePath $u } } catch { } } -ArgumentList $url, $edgePath | Out-Null # Start HTTP listener loop (implemented in Samy.Http.ps1) Start-SamyHttpServer -Port $port return } } } #endregion Main entry point: Invoke-ScriptAutomationMonkey #region Auto invoke for direct execution and iwr | iex if ($MyInvocation.InvocationName -eq '.') { # Dot-sourced, just expose functions } elseif ($PSCommandPath) { # Script was saved and run directly Invoke-ScriptAutomationMonkey @PSBoundParameters } else { # iwr | iex fallback with simple -Param value parsing if ($args.Count -gt 0) { $namedArgs = @{} for ($i = 0; $i -lt $args.Count; $i++) { $current = $args[$i] if ($current -is [string] -and $current.StartsWith('-')) { $key = $current.TrimStart('-') $next = $null if ($i + 1 -lt $args.Count) { $next = $args[$i + 1] } if ($next -and ($next -notlike '-*')) { $namedArgs[$key] = $next $i++ } else { $namedArgs[$key] = $true } } } Invoke-ScriptAutomationMonkey @namedArgs } else { Invoke-ScriptAutomationMonkey } } #endregion Auto invoke for direct execution and iwr | iex