<# .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. #> #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 1. If running from a saved script (PSCommandPath) and the file exists next to it, dot-sources that local file (dev mode). 2. Otherwise, downloads the module from $Script:SamyRepoBase / $Script:SamyBranch and Invoke-Expression on its content (remote iwr|iex mode). .PARAMETER Name File name of the module, for example "Samy.Logging.ps1". #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Name ) # 1) Local dev mode: script saved to disk, use PSScriptRoot if ($PSCommandPath) { $localPath = Join-Path -Path $PSScriptRoot -ChildPath $Name if (Test-Path $localPath) { . $localPath return } } # 2) Remote mode (iwr | iex): pull module from repo $url = "$Script:SamyRepoBase/$Script:SamyBranch/$Name?raw=1" try { $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop $content = $resp.Content if (-not $content) { Write-Host "[Error] Module $Name from $url returned empty content." -ForegroundColor Red throw "Empty module content." } Invoke-Expression $content } catch { Write-Host "[Error] Failed to load module $Name from $url: $($_.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) # Expected exported function: # Start-SamyHttpServer -Port 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