Public/Install-WinslopFix.ps1

function Install-WinslopFix {
    <#
    .SYNOPSIS
        Installs WinslopFix as a persistent Scheduled Task running as SYSTEM.

    .DESCRIPTION
        Deploys the WinslopFix module to ProgramData, registers the Event Log
        source, and creates a Scheduled Task that runs at system startup under
        the SYSTEM account. No UAC prompts. No user interaction required.

        The installation is idempotent — safe to run multiple times. Existing
        installations are upgraded in place when -Force is specified.

        Optionally disables Copilot AI features (Recall, Click to Do) during
        installation via the -DisableAIFeatures switch.

    .PARAMETER InstallPath
        Destination directory for the deployed module files.
        Default: $env:ProgramData\WinslopFix

    .PARAMETER DisableAIFeatures
        Also disable all Copilot AI features (Recall, Click to Do, Windows AI)
        during installation. Equivalent to running:
        Disable-CopilotAIFeature -Feature All

    .PARAMETER Force
        Overwrite an existing installation without prompting.

    .EXAMPLE
        Install-WinslopFix

        Install with default settings. Creates a SYSTEM-level Scheduled Task.

    .EXAMPLE
        Install-WinslopFix -DisableAIFeatures

        Install the guard and also disable Recall, Click to Do, etc.

    .EXAMPLE
        Install-WinslopFix -Force -Verbose

        Reinstall/upgrade with verbose output showing each step.

    .LINK
        https://github.com/DailenG/WinslopFix
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [string]$InstallPath = "$env:ProgramData\WinslopFix",

        [Parameter()]
        [switch]$DisableAIFeatures,

        [Parameter()]
        [switch]$Force
    )

    process {
        Assert-Administrator

        $taskName = 'WinslopFix'
        $taskPath = '\WinslopFix\'

        # --- Pre-flight: Platform detection ---
        $platform = Test-CopilotPlatform
        if (-not $platform.IsCopilotPlatform) {
            Write-Warning (
                'This machine does not appear to be a Copilot+ PC. ' +
                "NPU: $($platform.HasNPU), AI Service: $($platform.HasAIFabricService), " +
                "Active Sessions: $($platform.WorkloadsSessionHostCount). " +
                'WinslopFix may not be necessary on this system.'
            )
        }

        # --- Check for existing installation ---
        $existingTask = Get-ScheduledTask -TaskName $taskName -TaskPath $taskPath -ErrorAction SilentlyContinue
        if ($existingTask -and -not $Force) {
            Write-Warning 'WinslopFix is already installed. Use -Force to reinstall/upgrade.'
            return
        }

        # --- Step 1: Create installation directory ---
        if ($PSCmdlet.ShouldProcess($InstallPath, 'Create installation directory')) {
            if (-not (Test-Path -LiteralPath $InstallPath)) {
                New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
                Write-Verbose "Created directory: $InstallPath"
            }
        }

        # --- Step 2: Copy module files ---
        if ($PSCmdlet.ShouldProcess($InstallPath, 'Deploy module files')) {
            # Determine source — the module's own root directory
            $moduleRoot = Split-Path $PSScriptRoot -Parent

            $filesToCopy = @(
                'WinslopFix.psd1'
                'WinslopFix.psm1'
            )
            $dirsToCopy = @('Public', 'Private', 'Config')

            foreach ($file in $filesToCopy) {
                $src = Join-Path $moduleRoot $file
                if (Test-Path -LiteralPath $src) {
                    Copy-Item -LiteralPath $src -Destination $InstallPath -Force
                    Write-Verbose "Copied: $file"
                }
                else {
                    Write-Warning "Module file not found: $src"
                }
            }

            foreach ($dir in $dirsToCopy) {
                $src = Join-Path $moduleRoot $dir
                if (Test-Path -LiteralPath $src) {
                    $dest = Join-Path $InstallPath $dir
                    Copy-Item -LiteralPath $src -Destination $dest -Recurse -Force
                    Write-Verbose "Copied directory: $dir"
                }
            }

            Write-Verbose "Module deployed to: $InstallPath"
        }

        # --- Step 3: Register Event Log source ---
        if ($PSCmdlet.ShouldProcess('WinslopFix', 'Register Event Log source')) {
            try {
                if (-not [System.Diagnostics.EventLog]::SourceExists('WinslopFix')) {
                    New-EventLog -LogName Application -Source 'WinslopFix' -ErrorAction Stop
                    Write-Verbose 'Event Log source registered: WinslopFix'
                }
                else {
                    Write-Verbose 'Event Log source already exists: WinslopFix'
                }
            }
            catch {
                Write-Warning "Failed to register Event Log source: $($_.Exception.Message)"
            }
        }

        # --- Step 4: Clean up legacy WinslopFix/SafeAutentic artifacts ---
        if ($PSCmdlet.ShouldProcess('Legacy artifacts', 'Remove WinslopFix/SafeAutentic remnants')) {
            # Remove old SafeAutentic scheduled tasks
            $legacyTasks = @(
                @{ TaskPath = '\SafeAutentic\'; TaskName = 'WorkloadManager_Init' }
                @{ TaskPath = '\SafeAutentic\'; TaskName = 'WorkloadManager_Run' }
            )
            foreach ($task in $legacyTasks) {
                $existing = Get-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -ErrorAction SilentlyContinue
                if ($existing) {
                    Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -Confirm:$false
                    Write-Verbose "Removed legacy task: $($task.TaskPath)$($task.TaskName)"
                }
            }

            # Remove old Run keys from original SafeAutentic installer
            $runKeyPaths = @(
                'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
                'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
            )
            foreach ($regPath in $runKeyPaths) {
                $existing = Get-ItemProperty -Path $regPath -Name 'WorkloadManager' -ErrorAction SilentlyContinue
                if ($existing) {
                    Remove-ItemProperty -Path $regPath -Name 'WorkloadManager' -ErrorAction SilentlyContinue
                    Write-Verbose "Removed legacy Run key: $regPath\WorkloadManager"
                }
            }

            # Remove old ProgramData directory
            $legacyPath = "$env:ProgramData\WorkloadManager"
            if (Test-Path -LiteralPath $legacyPath) {
                Remove-Item -LiteralPath $legacyPath -Recurse -Force -ErrorAction SilentlyContinue
                Write-Verbose "Removed legacy directory: $legacyPath"
            }

            # Remove old Event Log source
            try {
                if ([System.Diagnostics.EventLog]::SourceExists('WorkloadManager')) {
                    Remove-EventLog -Source 'WorkloadManager' -ErrorAction SilentlyContinue
                    Write-Verbose 'Removed legacy Event Log source: WorkloadManager'
                }
            } catch { }
        }

        # --- Step 5: Remove existing scheduled task if upgrading ---
        if ($existingTask) {
            if ($PSCmdlet.ShouldProcess($taskName, 'Remove existing Scheduled Task for upgrade')) {
                Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false
                Write-Verbose 'Removed existing Scheduled Task for upgrade.'
            }
        }

        # --- Step 6: Create Scheduled Task (SYSTEM, no UAC) ---
        if ($PSCmdlet.ShouldProcess($taskName, 'Register Scheduled Task as SYSTEM')) {
            $scriptPath = Join-Path $InstallPath 'WinslopFix.psm1'
            $command    = "Import-Module '$scriptPath' -Force; Start-WinslopFix"

            $action  = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument (
                "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$command`""
            )

            $trigger   = New-ScheduledTaskTrigger -AtStartup
            $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest

            $settings = New-ScheduledTaskSettingsSet `
                -AllowStartIfOnBatteries `
                -DontStopIfGoingOnBatteries `
                -StartWhenAvailable `
                -RestartCount 3 `
                -RestartInterval (New-TimeSpan -Minutes 1) `
                -ExecutionTimeLimit (New-TimeSpan -Days 365)

            Register-ScheduledTask `
                -TaskName $taskName `
                -TaskPath $taskPath `
                -Action $action `
                -Trigger $trigger `
                -Principal $principal `
                -Settings $settings `
                -Description 'WinslopFix: Manages WorkloadsSessionHost memory consumption on Copilot+ PCs.' `
                -Force | Out-Null

            Write-Verbose "Scheduled Task registered: $taskPath$taskName (SYSTEM, at startup)"
        }

        # --- Step 7: Optionally disable AI features ---
        if ($DisableAIFeatures) {
            Write-Verbose 'Disabling Copilot AI features as requested...'
            Disable-CopilotAIFeature -Feature All
        }

        # --- Step 8: Start the task immediately ---
        if ($PSCmdlet.ShouldProcess($taskName, 'Start Scheduled Task now')) {
            Start-ScheduledTask -TaskName $taskName -TaskPath $taskPath -ErrorAction SilentlyContinue
            Write-Verbose 'WinslopFix Scheduled Task started.'
        }

        # --- Output summary ---
        Write-WinslopFixLog -Message "WinslopFix installed to '$InstallPath'. Scheduled Task registered as SYSTEM." -EventId 1000

        [PSCustomObject]@{
            PSTypeName     = 'WinslopFix.InstallResult'
            InstallPath    = $InstallPath
            TaskName       = "$taskPath$taskName"
            TaskPrincipal  = 'SYSTEM'
            AIDisabled     = [bool]$DisableAIFeatures
            IsCopilotPC    = $platform.IsCopilotPlatform
            LegacyCleaned  = $true
            Timestamp      = (Get-Date).ToString('o')
        }
    }
}