src/PowerShell/Health.ps1
|
# cdp PowerShell domain: Health.ps1 # Loaded by src/cdp.psm1; do not dot-source peer files. function New-CdpHealthCheck { param( [string]$Name, [bool]$Passed, [string]$Message, [ValidateSet('Info', 'Warning', 'Error')] [string]$Level = 'Info' ) [PSCustomObject]@{ Name = $Name Passed = $Passed Level = $Level Message = $Message } } function Write-CdpHealthCheck { param( [Parameter(Mandatory = $true)] [PSCustomObject]$Check ) if ($Check.Passed) { Write-Host "[OK] " -ForegroundColor Green -NoNewline } elseif ($Check.Level -eq 'Warning') { Write-Host "[WARN] " -ForegroundColor Yellow -NoNewline } else { Write-Host "[FAIL] " -ForegroundColor Red -NoNewline } Write-Host "$($Check.Name): " -NoNewline Write-Host $Check.Message -ForegroundColor Gray } function Get-CdpDependencyHealthChecks { $fzfCommand = Resolve-CdpFzfCommand if ($fzfCommand) { return @(New-CdpHealthCheck -Name "fzf" -Passed $true -Message "found at $fzfCommand") } return @(New-CdpHealthCheck -Name "fzf" -Passed $false -Level Error -Message "not found in PATH") } function Get-CdpUpgradeCommand { "Update-Module -Name cdp -Scope CurrentUser -Force" } function Get-CdpUpdateHealthChecks { param( [Parameter(Mandatory = $false)] [version]$CurrentVersion, [Parameter(Mandatory = $false)] [version]$LatestVersion ) if ($env:CDP_SKIP_UPDATE_CHECK -in @('1', 'true', 'TRUE', 'yes', 'YES')) { return @(New-CdpHealthCheck -Name "updates" -Passed $true -Message "skipped by CDP_SKIP_UPDATE_CHECK") } if (-not $CurrentVersion) { $CurrentVersion = Get-CdpCurrentModuleVersion } if (-not $CurrentVersion) { return @(New-CdpHealthCheck -Name "updates" -Passed $true ` -Message "current module version unknown; install with: Install-Module -Name cdp -Scope CurrentUser -Force -AllowClobber") } if (-not $LatestVersion) { if (-not (Get-Command Find-Module -ErrorAction SilentlyContinue)) { return @(New-CdpHealthCheck -Name "updates" -Passed $true ` -Message "PowerShellGet not available; upgrade with: $(Get-CdpUpgradeCommand)") } try { $galleryModule = Find-Module -Name cdp -Repository PSGallery -ErrorAction Stop $LatestVersion = [version]$galleryModule.Version } catch { return @(New-CdpHealthCheck -Name "updates" -Passed $true ` -Message "could not check PowerShell Gallery: $($_.Exception.Message)") } } if ($LatestVersion -gt $CurrentVersion) { return @(New-CdpHealthCheck -Name "updates" -Passed $false -Level Warning ` -Message "new version available: $CurrentVersion -> $LatestVersion; upgrade with: $(Get-CdpUpgradeCommand)") } if ($CurrentVersion -gt $LatestVersion) { return @(New-CdpHealthCheck -Name "updates" -Passed $true ` -Message "current version $CurrentVersion is newer than PowerShell Gallery $LatestVersion") } return @(New-CdpHealthCheck -Name "updates" -Passed $true ` -Message "current version $CurrentVersion is up to date") } function Get-CdpAboutInfo { param( [Parameter(Mandatory = $false)] [string]$ConfigPath ) $module = Get-CdpCurrentModule $versionText = Get-CdpVersionText $modulePath = if ($module) { $module.Path } else { $null } if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $configResult = Resolve-CdpHealthConfigPath -ConfigPath $ConfigPath $ConfigPath = $configResult.Path } $projectCount = 0 $enabledProjectCount = 0 if (-not [string]::IsNullOrWhiteSpace($ConfigPath) -and (Test-Path -LiteralPath $ConfigPath)) { try { $configData = Get-CdpProjectConfig -ConfigPath $ConfigPath $projectCount = @($configData.Projects).Count $enabledProjectCount = @($configData.EnabledProjects).Count } catch { $projectCount = 0 $enabledProjectCount = 0 } } [PSCustomObject]@{ Name = "cdp" Version = $versionText ModulePath = $modulePath ConfigPath = $ConfigPath ProjectCount = $projectCount EnabledProjectCount = $enabledProjectCount UpgradeCommand = Get-CdpUpgradeCommand } } function Show-CdpAbout { <# .SYNOPSIS Show cdp version and runtime information. .DESCRIPTION Displays a compact cdp brand header, module version, active config path, project counts, and the recommended upgrade command. .PARAMETER ConfigPath Optional custom path to projects.json file. .PARAMETER PassThru Returns the about information object in addition to console output. .EXAMPLE cdp about # Shows cdp version and runtime information #> [CmdletBinding()] param( [Parameter(Mandatory = $false, Position = 0)] [string]$ConfigPath, [Parameter(Mandatory = $false)] [switch]$PassThru ) $about = Get-CdpAboutInfo -ConfigPath $ConfigPath Write-CdpBrandHeader Write-Host "Module: " -NoNewline -ForegroundColor Gray Write-Host $about.ModulePath -ForegroundColor Cyan Write-Host "Config: " -NoNewline -ForegroundColor Gray Write-Host $about.ConfigPath -ForegroundColor Cyan Write-Host "Projects: " -NoNewline -ForegroundColor Gray Write-Host "$($about.EnabledProjectCount) enabled / $($about.ProjectCount) total" -ForegroundColor Green Write-Host "Upgrade: " -NoNewline -ForegroundColor Gray Write-Host $about.UpgradeCommand -ForegroundColor Cyan if ($PassThru) { return $about } } function Resolve-CdpHealthConfigPath { param( [string]$ConfigPath ) $checks = @() $configSource = "argument" if ([string]::IsNullOrWhiteSpace($ConfigPath)) { if (-not [string]::IsNullOrWhiteSpace($env:CDP_CONFIG)) { $ConfigPath = $env:CDP_CONFIG $configSource = "CDP_CONFIG" } else { $storedChoice = Get-StoredConfigChoice if ($storedChoice -and (Test-Path -LiteralPath $storedChoice)) { $ConfigPath = $storedChoice $configSource = "saved choice" } else { $availableConfigs = @(Get-AllAvailableConfigs) if ($availableConfigs.Count -gt 0) { $ConfigPath = $availableConfigs[0].Path $configSource = $availableConfigs[0].Source if ($availableConfigs.Count -gt 1) { $checks += New-CdpHealthCheck -Name "config selection" -Passed $false ` -Level Warning -Message "multiple configs found; run cdp-config to choose one" } } else { $ConfigPath = Join-Path (Get-CdpUserHome) ".cdp\projects.json" $configSource = "default custom config" } } } } [PSCustomObject]@{ Path = $ConfigPath Source = $configSource Checks = $checks } } function Get-CdpConfigParseResult { param( [string]$ConfigPath, [string]$ConfigSource ) $checks = @() $projects = @() $parsed = $false if (-not (Test-Path -LiteralPath $ConfigPath)) { $checks += New-CdpHealthCheck -Name "config file" -Passed $false ` -Level Error -Message "not found at $ConfigPath" return [PSCustomObject]@{ Projects = $projects; Checks = $checks; Parsed = $parsed } } $checks += New-CdpHealthCheck -Name "config file" -Passed $true -Message "$ConfigSource -> $ConfigPath" try { $jsonContent = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 $parsedProjects = ConvertFrom-Json -InputObject $jsonContent if ($null -ne $parsedProjects) { $projects = @($parsedProjects) } $checks += New-CdpHealthCheck -Name "JSON" -Passed $true -Message "parsed successfully" $parsed = $true } catch { $backupCount = @(Get-CdpValidJsonBackups -LiteralPath $ConfigPath).Count $message = if ($backupCount -gt 0) { "$($_.Exception.Message); $backupCount valid cdp backup(s) available" } else { $_.Exception.Message } $checks += New-CdpHealthCheck -Name "JSON" -Passed $false -Level Error -Message $message } [PSCustomObject]@{ Projects = $projects; Checks = $checks; Parsed = $parsed } } function Get-CdpProjectHealthChecks { param( [object[]]$Projects ) $invalidProjects = @($Projects | Where-Object { [string]::IsNullOrWhiteSpace($_.name) -or [string]::IsNullOrWhiteSpace($_.rootPath) -or -not ($_.enabled -is [bool]) }) $enabledProjects = @($Projects | Where-Object { $_.enabled -eq $true }) $duplicateNames = @($Projects | Group-Object -Property name | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Name) -and $_.Count -gt 1 }) $invalidPathProfiles = @() $missingPaths = @() foreach ($project in $enabledProjects) { try { $resolution = Resolve-CdpProjectPath -Project $project if ($resolution.ErrorCode) { $invalidPathProfiles += $project; continue } if (-not (Test-Path -LiteralPath $resolution.ResolvedPath)) { $missingPaths += $project } } catch { $invalidPathProfiles += $project } } @( New-CdpHealthCheck -Name "project schema" -Passed ($invalidProjects.Count -eq 0) ` -Level Error -Message "$($invalidProjects.Count) invalid project entries" New-CdpHealthCheck -Name "path profiles" -Passed ($invalidPathProfiles.Count -eq 0) ` -Level Error -Message "$($invalidPathProfiles.Count) invalid current path profiles" New-CdpHealthCheck -Name "enabled projects" -Passed ($enabledProjects.Count -gt 0) ` -Level Warning -Message "$($enabledProjects.Count) enabled of $($Projects.Count) total" New-CdpHealthCheck -Name "duplicate names" -Passed ($duplicateNames.Count -eq 0) ` -Level Warning -Message "$($duplicateNames.Count) duplicate project names" New-CdpHealthCheck -Name "project paths" -Passed ($missingPaths.Count -eq 0) ` -Level Warning -Message "$($missingPaths.Count) enabled project paths missing" ) } function Write-CdpHealthSummary { param( [object[]]$Checks ) Write-Host "`ncdp doctor" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Gray foreach ($check in $Checks) { Write-CdpHealthCheck -Check $check } Write-Host "" $errorCount = @($Checks | Where-Object { -not $_.Passed -and $_.Level -eq 'Error' }).Count $warningCount = @($Checks | Where-Object { -not $_.Passed -and $_.Level -eq 'Warning' }).Count if ($errorCount -eq 0 -and $warningCount -eq 0) { Write-Host "All checks passed." -ForegroundColor Green } else { Write-Host "Summary: $errorCount error(s), $warningCount warning(s)." -ForegroundColor Yellow } } function Test-ProjectHealth { <# .SYNOPSIS Diagnose the cdp runtime environment and project configuration. .DESCRIPTION Checks fzf availability, cdp update status, active configuration discovery, JSON shape, duplicate project names, enabled project count, and missing paths. .PARAMETER ConfigPath Optional custom path to projects.json file. .PARAMETER PassThru Returns a structured health summary object in addition to console output. .PARAMETER SkipUpdateCheck Skips the PowerShell Gallery version check. .PARAMETER Fix Repairs the project configuration instead of only reporting issues. .EXAMPLE Test-ProjectHealth # Runs diagnostics for the active cdp configuration .EXAMPLE cdp doctor # Runs diagnostics through the short cdp command #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $false, Position = 0)] [string]$ConfigPath, [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false)] [switch]$SkipUpdateCheck, [Parameter(Mandatory = $false)] [switch]$Fix ) if ($Fix) { $parameters = @{ ConfigPath = $ConfigPath; PassThru = $PassThru } if ($WhatIfPreference) { $parameters.WhatIf = $true } if ($PSBoundParameters.ContainsKey('Confirm')) { $parameters.Confirm = $PSBoundParameters['Confirm'] } Repair-ProjectConfig @parameters return } $configResult = Resolve-CdpHealthConfigPath -ConfigPath $ConfigPath $parseResult = Get-CdpConfigParseResult -ConfigPath $configResult.Path -ConfigSource $configResult.Source $projects = @($parseResult.Projects) $checks = @(Get-CdpDependencyHealthChecks) + @($configResult.Checks) + @($parseResult.Checks) if (-not $SkipUpdateCheck) { $checks += Get-CdpUpdateHealthChecks } if ($parseResult.Parsed) { $checks += Get-CdpProjectHealthChecks -Projects $projects } $errorCount = @($checks | Where-Object { -not $_.Passed -and $_.Level -eq 'Error' }).Count $warningCount = @($checks | Where-Object { -not $_.Passed -and $_.Level -eq 'Warning' }).Count Write-CdpBrandHeader Write-CdpHealthSummary -Checks $checks if ($PassThru) { [PSCustomObject]@{ ConfigPath = $configResult.Path Checks = $checks ErrorCount = $errorCount WarningCount = $warningCount ProjectCount = $projects.Count EnabledProjectCount = @($projects | Where-Object { $_.enabled -eq $true }).Count MissingPathCount = @($projects | Where-Object { if ($_.enabled -ne $true) { return $false } try { $resolution = Resolve-CdpProjectPath -Project $_ return -not $resolution.ErrorCode -and -not (Test-Path -LiteralPath $resolution.ResolvedPath) } catch { return $false } }).Count } } } |