Public/Invoke-TechCare.ps1

function Invoke-TechCare {
    <#
    .SYNOPSIS
        Orquesta una pasada de mantenimiento completa sobre el equipo local.
    .DESCRIPTION
        Ejecuta los modulos del preset indicado en un orden fijo: diagnosticar,
        reparar, actualizar, limpiar, rendimiento, seguridad e informe. Un modulo
        que falla queda registrado en rojo y la pasada continua, porque dejar el
        equipo a medias es peor que terminar con un apartado incompleto.

        VISTA PREVIA POR DEFECTO. Sin -Apply cada modulo se ejecuta bajo -WhatIf,
        asi que el equipo no se modifica: lo que obtienes es el diagnostico, la
        lista de lo que se haria y el informe. Con -Apply se exige sesion elevada,
        se crea un punto de restauracion y se avisa si el equipo esta gestionado.

        Nunca reinicia, nunca borra aplicaciones integradas y nunca desactiva
        Defender ni Windows Update.
    .PARAMETER MaintenanceProfile
        Preset a ejecutar: Basic, Standard o Full. Por defecto Standard. Acepta
        los alias -Preset y -Profile.

        No se llama -Profile a secas porque $Profile es una variable automatica de
        PowerShell y declararla como parametro es un defecto conocido.
    .PARAMETER Modules
        Modulos concretos, en cualquier orden: TechCare los reordena al orden
        canonico. Excluyente con -MaintenanceProfile por conjunto de parametros.
    .PARAMETER Apply
        Aplica los cambios. Sin este modificador nada se modifica.
    .PARAMETER Force
        No pregunta en equipos gestionados ni cuando falla el punto de
        restauracion. Necesario para una ejecucion desatendida.
    .PARAMETER ReportPath
        Carpeta donde se escriben el informe y el log.
    .PARAMETER TechnicianName
        Nombre del tecnico para la cabecera del informe.
    .PARAMETER CompanyName
        Nombre de la empresa para la cabecera del informe.
    .PARAMETER ContactInfo
        Contacto para la cabecera del informe.
    .PARAMETER PowerPlan
        Plan de energia a activar durante el modulo Performance.
    .PARAMETER SkipRestorePoint
        No crea punto de restauracion antes de aplicar. Usalo solo si el equipo
        tiene otra copia de seguridad.
    .EXAMPLE
        Invoke-TechCare

        Vista previa con el preset Standard: no modifica nada y deja informe.
    .EXAMPLE
        Invoke-TechCare -MaintenanceProfile Standard -Apply

        Pasada real. Exige consola elevada y crea punto de restauracion.
    .EXAMPLE
        Invoke-TechCare -Modules Repair,Cleanup -Apply -Force

        Solo reparar y limpiar, sin preguntas, para una intervencion desatendida.
    .EXAMPLE
        Invoke-TechCare -MaintenanceProfile Full -Apply -ReportPath C:\Reportes -TechnicianName 'Eduard Arbona'

        Todo, con informe de marca en una carpeta concreta.
    .EXAMPLE
        Invoke-TechCare -MaintenanceProfile Full -WhatIf

        No escribe ni el informe: -WhatIf se propaga a todos los modulos.
    .OUTPUTS
        PSCustomObject[]
    .LINK
        https://github.com/earbona23/TechCare
    #>

    [CmdletBinding(DefaultParameterSetName = 'Preset', SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(ParameterSetName = 'Preset', Position = 0)]
        [Alias('Preset', 'Profile')]
        [ValidateSet('Basic', 'Standard', 'Full')]
        [string]$MaintenanceProfile,

        [Parameter(ParameterSetName = 'Modules', Mandatory)]
        [ValidateSet('Diagnose', 'Repair', 'Update', 'Cleanup', 'Performance', 'Security', 'Report')]
        [string[]]$Modules,

        [switch]$Apply,
        [switch]$Force,

        [ValidateNotNullOrEmpty()]
        [string]$ReportPath,

        [AllowEmptyString()]
        [string]$TechnicianName = '',

        [AllowEmptyString()]
        [string]$CompanyName = '',

        [AllowEmptyString()]
        [string]$ContactInfo = '',

        [ValidateSet('Balanced', 'HighPerformance', 'PowerSaver')]
        [string]$PowerPlan,

        [switch]$SkipRestorePoint
    )

    if (-not (Test-TechCarePlatform)) { throw $script:PlatformMessage }

    # El estado se reinicia aqui para que dos pasadas en la misma consola no
    # acumulen los resultados de la primera en el informe de la segunda.
    Initialize-TechCareState

    $preset = $MaintenanceProfile
    if ($PSCmdlet.ParameterSetName -eq 'Modules') {
        $selected = @($Modules)
        $preset = 'Custom'
    } else {
        if (-not $preset) { $preset = 'Standard' }
        $selected = @(Get-TechCareProfile -MaintenanceProfile $preset)
    }

    $order = @('Diagnose', 'Repair', 'Update', 'Cleanup', 'Performance', 'Security', 'Report')
    $selected = @($selected | Sort-Object -Property { $order.IndexOf($_) })

    $script:State.Preset = $preset
    $script:State.Apply  = [bool]$Apply

    $prereq = Test-TechCarePrerequisite
    $script:State.Admin     = $prereq.IsAdmin
    $script:State.Managed   = $prereq.IsManaged
    $script:State.ManagedBy = $prereq.ManagedReasons

    if ($Apply -and -not $prereq.IsAdmin) {
        throw ("Se requieren privilegios de administrador para aplicar cambios. Relanza con:`n " + $prereq.RelaunchCommand)
    }

    if (-not $prereq.IsSupportedWindows) {
        Write-TechCareLog -Level Warning -Message ('Version de Windows no reconocida como 10 u 11 (build {0}). Los modulos pueden comportarse distinto.' -f $prereq.OSBuild)
    }

    # Carpeta de la pasada: el log se escribe linea a linea desde el principio,
    # de modo que una sesion interrumpida a mitad de DISM deje rastro en disco.
    $runFolder = $ReportPath
    if (-not $runFolder) {
        $runFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('TechCare\' + $script:State.RunId)
    }
    $script:State.ReportPath = $runFolder
    if (-not $WhatIfPreference) {
        try {
            if (-not (Test-Path -LiteralPath $runFolder)) { [void](New-Item -ItemType Directory -Path $runFolder -Force) }
            $script:State.LogPath = Join-Path -Path $runFolder -ChildPath 'TechCare-Log.txt'
        } catch {
            Write-TechCareLog -Level Warning -Message 'No se pudo preparar la carpeta de la pasada; el log solo quedara en memoria.' -ErrorText $_.Exception.Message
        }
    }

    # Preview es la decision central del modulo: si es cierto, TODOS los modulos
    # que escriben se invocan con -WhatIf y el equipo no se toca. Sin esto, una
    # pasada sin -Apply ejecutaria DISM y borraria los temporales de verdad.
    $preview = -not $Apply

    if ($Apply) {
        $actionText = 'Ejecutar TechCare y APLICAR cambios - preset {0}' -f $preset
        if (-not $PSCmdlet.ShouldProcess($prereq.OSCaption + ' / ' + [System.Environment]::MachineName, $actionText)) {
            $preview = $true
        }
    }

    if ($preview) {
        Write-TechCareLog -Message 'Modo VISTA PREVIA: no se modificara nada. Anade -Apply para aplicar.' -Level Warning
    } else {
        Write-TechCareLog -Message ('Modo APLICAR: preset {0}, ejecucion {1}.' -f $preset, $script:State.RunId) -Level Info
    }

    if ($prereq.IsManaged -and -not $preview) {
        Write-TechCareLog -Level Warning -Message 'Equipo gestionado centralmente:'
        foreach ($reason in $prereq.ManagedReasons) { Write-TechCareLog -Level Warning -Message (' - ' + $reason) }
        Write-TechCareLog -Level Warning -Message 'Los cambios pueden chocar con politicas corporativas y volver a aplicarse solos.'
        if (-not $Force) {
            # Test-TechCareConsent, no ShouldContinue a secas: en una sesion sin
            # entrada interactiva ShouldContinue lanza, y la excepcion mataria la
            # pasada con un error que no dice que falta -Force.
            if (-not (Test-TechCareConsent -Question 'Continuar en un equipo gestionado?' -Caller $PSCmdlet)) {
                Write-TechCareLog -Message 'Pasada cancelada por el tecnico.' -Level Warning
                return [pscustomobject[]]$script:State.Results.ToArray()
            }
        }
    }

    if (-not $preview -and -not $SkipRestorePoint) {
        $restore = New-TechCareRestorePoint -Description ('TechCare ' + $script:State.RunId) -Confirm:$false
        if ($restore.Created) {
            Write-TechCareLog -Level Success -Message ('Punto de restauracion creado via {0}.' -f $restore.Method)
        } else {
            Write-TechCareLog -Level Warning -Message 'No se pudo crear el punto de restauracion.' -ErrorText $restore.Message
            if (-not $Force) {
                if (-not (Test-TechCareConsent -Question 'Continuar sin punto de restauracion?' -Caller $PSCmdlet)) {
                    Write-TechCareLog -Message 'Pasada cancelada por el tecnico.' -Level Warning
                    return [pscustomobject[]]$script:State.Results.ToArray()
                }
            }
        }
    } elseif (-not $preview -and $SkipRestorePoint) {
        Write-TechCareLog -Level Warning -Message 'Punto de restauracion omitido por -SkipRestorePoint.'
    }

    $total = $selected.Count
    $index = 0
    foreach ($moduleName in $selected) {
        $index++
        $percent = [math]::Round((($index - 1) / $total) * 100)
        Write-Progress -Activity 'TechCare' -Status ('Modulo {0} de {1}: {2}' -f $index, $total, $moduleName) -PercentComplete $percent
        Write-TechCareLog -Message ('=== Modulo {0} ===' -f $moduleName) -Level Detail
        try {
            switch ($moduleName) {
                'Diagnose' {
                    [void](Get-TechCareDiagnostics)
                }
                'Repair' {
                    [void](Invoke-TechCareRepair -Confirm:$false -WhatIf:$preview)
                }
                'Update' {
                    [void](Invoke-TechCareUpdate -Confirm:$false -WhatIf:$preview)
                }
                'Cleanup' {
                    [void](Invoke-TechCareCleanup -Confirm:$false -WhatIf:$preview)
                }
                'Performance' {
                    # -PowerPlan se anade solo si el tecnico lo indico: pasarlo
                    # vacio viola el ValidateSet y tumbaria el modulo entero.
                    $perfArgs = @{ Confirm = $false; WhatIf = $preview }
                    if ($PowerPlan) { $perfArgs['PowerPlan'] = $PowerPlan }
                    [void](Get-TechCarePerformance @perfArgs)
                }
                'Security' {
                    [void](Get-TechCareSecurity -Apply:$Apply -Confirm:$false -WhatIf:$preview)
                }
                'Report' {
                    # El informe se escribe tambien en vista previa: es el
                    # entregable de la revision. Solo -WhatIf explicito lo frena.
                    $rep = New-TechCareReport -ReportPath $runFolder -TechnicianName $TechnicianName -CompanyName $CompanyName -ContactInfo $ContactInfo -Confirm:$false
                    if ($rep.Written) {
                        Write-TechCareLog -Level Success -Message ('Informe: ' + $rep.HtmlPath)
                    } else {
                        Write-TechCareLog -Level Warning -Message '-WhatIf activo: no se escribio el informe.'
                    }
                }
            }
        } catch {
            Write-TechCareLog -Level Error -Message ('Fallo en el modulo {0}.' -f $moduleName) -ErrorText $_.Exception.Message
            [void](Add-TechCareResult -Module $moduleName -Status 'Red' -Findings @('Modulo interrumpido: ' + $_.Exception.Message))
        }
    }

    Write-Progress -Activity 'TechCare' -Status 'Finalizado' -Completed
    return [pscustomobject[]]$script:State.Results.ToArray()
}