health.psm1
|
# Health module for Delta-Code # Single authoritative implementation of installation / runtime verification. # Consumed by Install-DeltaCode.ps1 now and by the `delta-code health` command later. $script:ModuleNames = @('delta-code', 'health', 'satellite', 'workspace', 'workflow', 'entry') $script:SatelliteNames = @('git', 'build') $script:DataDirectories = @('data', 'data\workspaces', 'data\workflows', 'data\satellites') # Registry file -> expected top-level property $script:RegistryContracts = [ordered]@{ 'data\workspaces.json' = 'Workspaces' 'data\workflows.json' = 'Workflows' 'data\workflow-history.json' = 'History' 'data\automation-profiles.json' = 'Profiles' } function New-DeltaCodeHealthCheck { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [ValidateSet('Pass', 'Warn', 'Fail')] [string]$Status, [string]$Detail = '' ) [pscustomobject]@{ Name = $Name Status = $Status Detail = $Detail } } function Test-DeltaCodeModuleLayout { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath ) $results = @() $modulesPath = Join-Path $RootPath 'modules' if (-not (Test-Path -LiteralPath $modulesPath)) { $results += New-DeltaCodeHealthCheck -Name 'modules' -Status 'Fail' -Detail "Directory not found: $modulesPath" return $results } $results += New-DeltaCodeHealthCheck -Name 'modules' -Status 'Pass' -Detail $modulesPath foreach ($name in $script:ModuleNames) { $manifest = Join-Path $modulesPath "$name.psd1" $rootModule = Join-Path $modulesPath "$name.psm1" if (-not (Test-Path -LiteralPath $manifest)) { $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail "Missing $name.psd1" continue } if (-not (Test-Path -LiteralPath $rootModule)) { $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail "Missing $name.psm1" continue } try { $data = Import-PowerShellDataFile -Path $manifest -ErrorAction Stop if (-not $data.RootModule) { $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail 'Manifest has no RootModule' continue } if (-not $data.ModuleVersion) { $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail 'Manifest has no ModuleVersion' continue } $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Pass' -Detail "v$($data.ModuleVersion)" } catch { $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail $_.Exception.Message } } return $results } function Test-DeltaCodeSatelliteLayout { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath ) $results = @() $satellitesPath = Join-Path $RootPath 'satellites' if (-not (Test-Path -LiteralPath $satellitesPath)) { $results += New-DeltaCodeHealthCheck -Name 'satellites' -Status 'Warn' -Detail "Directory not found: $satellitesPath" return $results } $results += New-DeltaCodeHealthCheck -Name 'satellites' -Status 'Pass' -Detail $satellitesPath foreach ($name in $script:SatelliteNames) { $satPath = Join-Path $satellitesPath $name if (-not (Test-Path -LiteralPath $satPath)) { $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Not installed: $satPath" continue } $manifest = Join-Path $satPath "$name.psd1" $rootModule = Join-Path $satPath "$name.psm1" if (-not (Test-Path -LiteralPath $manifest)) { $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Missing $name.psd1" continue } if (-not (Test-Path -LiteralPath $rootModule)) { $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Missing $name.psm1" continue } $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Pass' -Detail $satPath } return $results } function Test-DeltaCodeDataLayout { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath ) $results = @() foreach ($dir in $script:DataDirectories) { $full = Join-Path $RootPath $dir if (-not (Test-Path -LiteralPath $full)) { $results += New-DeltaCodeHealthCheck -Name "data:$dir" -Status 'Warn' -Detail "Directory not found: $full" } else { $results += New-DeltaCodeHealthCheck -Name "data:$dir" -Status 'Pass' -Detail $full } } return $results } function Test-DeltaCodeRegistryData { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath ) $results = @() foreach ($relative in $script:RegistryContracts.Keys) { $expectedProperty = $script:RegistryContracts[$relative] $full = Join-Path $RootPath $relative if (-not (Test-Path -LiteralPath $full)) { # Registry files are recreated automatically on first use, so a missing # file is a first-run condition rather than a broken installation. $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Warn' -Detail 'Not initialised yet (created on first use)' continue } try { $raw = Get-Content -LiteralPath $full -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail 'File is empty' continue } $parsed = $raw | ConvertFrom-Json -ErrorAction Stop if ($null -eq $parsed.PSObject.Properties[$expectedProperty]) { $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail "Missing '$expectedProperty' property" continue } $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Pass' -Detail "Property '$expectedProperty' present" } catch { $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail $_.Exception.Message } } return $results } function Test-DeltaCodeModuleLoad { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath ) $results = @() $modulesPath = Join-Path $RootPath 'modules' # Never re-import the module that is currently executing: `Import-Module -Force` # rebuilds its script scope and invalidates the function table of the call # stack that is still running, producing "term not recognized" errors. $runningModuleName = $ExecutionContext.SessionState.Module.Name foreach ($name in $script:ModuleNames) { $manifest = Join-Path $modulesPath "$name.psd1" if (-not (Test-Path -LiteralPath $manifest)) { $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Fail' -Detail 'Manifest missing' continue } if ($name -eq $runningModuleName) { $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Pass' -Detail 'Verified in place (module is running)' continue } try { Import-Module $manifest -Force -ErrorAction Stop -DisableNameChecking $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Pass' -Detail 'Imported' } catch { $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Fail' -Detail $_.Exception.Message } } return $results } function Test-DeltaCodeCli { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$RootPath, [int]$TimeoutSeconds = 60 ) $launcher = Join-Path $RootPath 'delta-code.ps1' if (-not (Test-Path -LiteralPath $launcher)) { return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail "Launcher not found: $launcher") } $pwsh = (Get-Process -Id $PID).Path if (-not $pwsh) { $pwsh = 'pwsh' } try { $proc = Start-Process -FilePath $pwsh ` -ArgumentList @('-NoProfile', '-NonInteractive', '-File', $launcher, '-h') ` -Wait -PassThru -NoNewWindow ` -RedirectStandardOutput ([System.IO.Path]::GetTempFileName()) ` -RedirectStandardError ([System.IO.Path]::GetTempFileName()) if ($proc.ExitCode -eq 0) { return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Pass' -Detail 'Launcher responded to -h') } return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail "Launcher exited with code $($proc.ExitCode)") } catch { return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail $_.Exception.Message) } } function Test-DeltaCodeInstallation { <# .SYNOPSIS Verifies a Delta-Code installation or working tree. .DESCRIPTION Single authoritative verification implementation. Used by Install-DeltaCode.ps1 for post-install validation and (later) by the `delta-code health` command. .PARAMETER RootPath Root of the installation or repository to verify. .PARAMETER SkipModuleLoad Skip importing each module. .PARAMETER SkipCli Skip the launcher smoke test. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$RootPath = (Split-Path -Parent $PSScriptRoot), [switch]$SkipModuleLoad, [switch]$SkipCli ) $resolvedRoot = $RootPath if (Test-Path -LiteralPath $RootPath) { $resolvedRoot = (Get-Item -LiteralPath $RootPath).FullName } $checks = @() $checks += Test-DeltaCodeModuleLayout -RootPath $resolvedRoot $checks += Test-DeltaCodeSatelliteLayout -RootPath $resolvedRoot $checks += Test-DeltaCodeDataLayout -RootPath $resolvedRoot $checks += Test-DeltaCodeRegistryData -RootPath $resolvedRoot if (-not $SkipModuleLoad) { $checks += Test-DeltaCodeModuleLoad -RootPath $resolvedRoot } if (-not $SkipCli) { $checks += Test-DeltaCodeCli -RootPath $resolvedRoot } $errors = @($checks | Where-Object { $_.Status -eq 'Fail' } | ForEach-Object { "$($_.Name): $($_.Detail)" }) $warnings = @($checks | Where-Object { $_.Status -eq 'Warn' } | ForEach-Object { "$($_.Name): $($_.Detail)" }) [pscustomobject]@{ RootPath = $resolvedRoot Success = ($errors.Count -eq 0) Errors = $errors Warnings = $warnings Checks = $checks } } function Format-DeltaCodeHealthReport { <# .SYNOPSIS Renders a health result object as console output. .PARAMETER Result Result object returned by Test-DeltaCodeInstallation. .PARAMETER PassThru Return the result object (allows piping after rendering). #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [psobject]$Result, [switch]$PassThru ) process { Write-Host "Delta-Code health check: $($Result.RootPath)" Write-Host "" foreach ($check in $Result.Checks) { $symbol = switch ($check.Status) { 'Pass' { '[ OK ]' } 'Warn' { '[WARN]' } 'Fail' { '[FAIL]' } } $color = switch ($check.Status) { 'Pass' { 'Green' } 'Warn' { 'Yellow' } 'Fail' { 'Red' } } Write-Host ("{0} {1,-28} {2}" -f $symbol, $check.Name, $check.Detail) -ForegroundColor $color } Write-Host "" if ($Result.Success) { $summary = "Health check PASSED ($($Result.Checks.Count) checks, $($Result.Warnings.Count) warnings)" Write-Host $summary -ForegroundColor Green } else { $summary = "Health check FAILED ($($Result.Errors.Count) errors, $($Result.Warnings.Count) warnings)" Write-Host $summary -ForegroundColor Red } if ($PassThru) { return $Result } } } Export-ModuleMember -Function Test-DeltaCodeInstallation, Format-DeltaCodeHealthReport, New-DeltaCodeHealthCheck |