From 3fab891383d967d5a69a44a81a59c34a66357e31 Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Wed, 16 Sep 2026 14:49:49 +0200 Subject: [PATCH] feat(doctor): add --summary/-Summary output mode Co-Authored-By: Claude Haiku 4.5 --- scripts/doctor.ps1 | 22 ++++++++++++++++------ scripts/doctor.sh | 21 ++++++++++++++++++--- scripts/test-platform-packages.ps1 | 9 +++++++-- scripts/test-platform-packages.sh | 13 ++++++++++++- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/scripts/doctor.ps1 b/scripts/doctor.ps1 index 88427a7..2606aee 100644 --- a/scripts/doctor.ps1 +++ b/scripts/doctor.ps1 @@ -1,11 +1,16 @@ [CmdletBinding()] -param() +param([switch]$Summary) $ErrorActionPreference = 'Stop' $root = Split-Path $PSScriptRoot -Parent . (Join-Path $PSScriptRoot 'lib/manifest.ps1') $homePath = [Environment]::GetFolderPath('UserProfile') -$fail = $false -function Result($state, $message) { Write-Output ("$state $message"); if ($state -eq 'FAIL') { $script:fail = $true } } +$script:fail = $false +$script:checkCount = 0 +function Result($state, $message) { + $script:checkCount++ + if ($state -eq 'FAIL') { $script:fail = $true } + if (-not $Summary -or $script:fail -or $state -ne 'PASS') { Write-Output ("$state $message") } +} foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } } if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude status line' } else { Result 'WARN' 'jq unavailable; Claude status line is disabled' } } @@ -42,7 +47,7 @@ if (Test-Path -LiteralPath $settingsPath) { } $configTomlPath = Join-Path $homePath '.codex/config.toml' if (Test-Path -LiteralPath $configTomlPath) { - $parseResult = & python -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' $configTomlPath 2>&1 + try { $parseResult = & python -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' $configTomlPath 2>&1 } catch { $parseResult = $_ } if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" } if (Get-Command codex -ErrorAction SilentlyContinue) { $previousCodexHome = $env:CODEX_HOME @@ -77,6 +82,11 @@ foreach ($client in @('codex','claude')) { if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" } } -if ($fail) { exit 1 } -Write-Output 'PASS doctor' +if ($Summary) { + if ($script:fail) { Write-Output "Doctor: FAIL | $script:checkCount checks"; exit 1 } + Write-Output "Doctor: PASS | $script:checkCount checks" +} else { + if ($script:fail) { exit 1 } + Write-Output 'PASS doctor' +} exit 0 diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 326fea9..d4cb84f 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -1,14 +1,24 @@ #!/usr/bin/env bash set -euo pipefail +summary=false +while [ $# -gt 0 ]; do + case "$1" in + --summary) summary=true; shift ;; + *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; + esac +done + root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) . "$root/scripts/lib/manifest.sh" home_path=${HOME:?HOME is required} failed=false +check_count=0 result() { - printf '%s %s\n' "$1" "$2" + check_count=$((check_count + 1)) [ "$1" != FAIL ] || failed=true + if [ "$summary" = false ] || [ "$failed" = true ] || [ "$1" != PASS ]; then printf '%s %s\n' "$1" "$2"; fi } for tool in codex claude; do @@ -85,5 +95,10 @@ for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do [ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path" done -"$failed" && exit 1 -printf 'PASS doctor\n' +if [ "$summary" = true ]; then + if [ "$failed" = true ]; then printf 'Doctor: FAIL | %s checks\n' "$check_count"; exit 1; fi + printf 'Doctor: PASS | %s checks\n' "$check_count" +else + [ "$failed" = false ] || exit 1 + printf 'PASS doctor\n' +fi diff --git a/scripts/test-platform-packages.ps1 b/scripts/test-platform-packages.ps1 index 31bd18c..543ec5e 100644 --- a/scripts/test-platform-packages.ps1 +++ b/scripts/test-platform-packages.ps1 @@ -5,8 +5,10 @@ $bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on Windows and Linux.' } $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' $branch = & git -C $root branch --show-current -$statusOutput = $statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1') -if ($statusOutput -ne "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700") { throw 'PowerShell status line output is incorrect.' } +$statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n" +$plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '') +$expectedStatusOutput = "Test Model $([char]0x00B7) high`nTestRepo on $branch $([char]0x00B7) 9% ctx (16.7k tok)" +if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status line output is incorrect.' } $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count $sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count $ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count @@ -48,6 +50,9 @@ foreach ($platformSelection in @('1','2')) { if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' } } } +$doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary +if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' } +if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" } Write-Output 'PASS four platform packages and six PowerShell selections' exit 0 diff --git a/scripts/test-platform-packages.sh b/scripts/test-platform-packages.sh index d701be8..c510bc3 100755 --- a/scripts/test-platform-packages.sh +++ b/scripts/test-platform-packages.sh @@ -6,7 +6,9 @@ grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps [ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ] branch=$(git -C "$root" branch --show-current) status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh") -[ "$status_output" = "Model: Test Model | Effort: high | Repo: TestRepo | Branch: $branch | Max Context: 200000 | Used Context: 9% | Used Tokens: 16700" ] +plain_status_output=$(printf '%s' "$status_output" | sed -E $'s/\x1b\\[[0-9;]*m//g') +expected_status_output=$'Test Model \xc2\xb7 high\nTestRepo on '"$branch"$' \xc2\xb7 9% ctx (16.7k tok)' +[ "$plain_status_output" = "$expected_status_output" ] source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l) source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l) rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1)) @@ -54,5 +56,14 @@ for platform_selection in 1 2; do [[ "$output" == *"-$platform"* ]] done done +doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary) +if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then + printf 'Doctor summary mode must not print individual PASS lines.\n' >&2 + exit 1 +fi +if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'; then + printf "Doctor summary mode must print one 'Doctor: PASS | N checks' line: %s\n" "$doctor_summary_output" >&2 + exit 1 +fi printf 'PASS four platform packages and six Bash selections\n'