refactor(shell): use powershell and bash package names

This commit is contained in:
fraujulian committed 2026-09-16 22:43:55 +02:00
1 parent 9a2a606410
commit 6a0c4abb65
11 files changed
+108 -87

No files matched your search

+8 -6
View File
@@ -1,4 +1,4 @@
# AI instructions # AI instructions
This file contains maintenance instructions for this repository only. It is not This file contains maintenance instructions for this repository only. It is not
part of the generated Codex or Claude Code configuration and must not be read part of the generated Codex or Claude Code configuration and must not be read
@@ -20,12 +20,14 @@ Keep the architecture as:
## Portability ## Portability
- Design scripts, hooks, generated configuration, and installation behavior for both Windows and Linux. - Design scripts, hooks, generated configuration, and installation behavior for both PowerShell and Bash.
- Use cross-platform tools and paths where practical. - Name configuration choices and generated packages after their shell, not the operating system.
- When platform-specific behavior is necessary, provide an equivalent implementation or a safe no-op for the other supported platform. - Use portable tools and paths where practical.
- When shell-specific behavior is necessary, provide an equivalent implementation or a safe no-op for the other supported shell.
- Provide equivalent native Bash (`.sh`) and PowerShell (`.ps1`) entry points for every script and hook. - Provide equivalent native Bash (`.sh`) and PowerShell (`.ps1`) entry points for every script and hook.
- Support Windows PowerShell 5.1 and newer on Windows, and PowerShell 7 (`pwsh`) on Linux. Keep shared PowerShell syntax compatible with 5.1; Windows must not require PowerShell 7. - Keep shared PowerShell syntax compatible with 5.1 and support PowerShell 7 (`pwsh`) as well.
- Validate relevant changes on both supported platforms when practical. - Preserve operating-system API names and upstream schema keys when their exact spelling is required.
- Validate relevant changes with both supported shells when practical.
## Language ## Language
+1 -1
View File
@@ -17,5 +17,5 @@ hooks = true
[[hooks.Stop.hooks]] [[hooks.Stop.hooks]]
type = "command" type = "command"
command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"' command = '__HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__HOOK_SCRIPT__"'
command_windows = '__WINDOWS_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__WINDOWS_HOOK_SCRIPT__"' command_windows = '__POWERSHELL_HOOK_COMMAND__ "__AI_CONFIG_ROOT__/hooks/__POWERSHELL_HOOK_SCRIPT__"'
timeout = 8 timeout = 8
+9 -9
View File
@@ -18,26 +18,26 @@ function Result($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" } } 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' } } if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude Bash status line' } else { Result 'WARN' 'jq unavailable; Claude Bash status line is disabled' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count $sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
$ruleSkillManifestPath = Join-Path $root 'adapters/claude/rule-skills.tsv' $ruleSkillManifestPath = Join-Path $root 'adapters/claude/rule-skills.tsv'
$ruleSkillCount = if (Test-Path -LiteralPath $ruleSkillManifestPath) { @(Import-Csv -LiteralPath $ruleSkillManifestPath -Delimiter ([char]9)).Count } else { 0 } $ruleSkillCount = if (Test-Path -LiteralPath $ruleSkillManifestPath) { @(Import-Csv -LiteralPath $ruleSkillManifestPath -Delimiter ([char]9)).Count } else { 0 }
foreach ($platform in @('windows','linux')) { foreach ($shell in @('powershell','bash')) {
if (Test-Path (Join-Path $root "generated/codex-$platform/AGENTS.md")) { Result 'PASS' "generated output present ($platform)" } else { Result 'FAIL' "generated output missing ($platform); run build" } if (Test-Path (Join-Path $root "generated/codex-$shell/AGENTS.md")) { Result 'PASS' "generated output present ($shell)" } else { Result 'FAIL' "generated output missing ($shell); run build" }
if ((Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/codex-$platform/hooks/scripts/validate-command-safety.sh")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/claude-$platform/hooks/scripts/validate-command-safety.sh"))) { Result 'PASS' "generated hooks present ($platform)" } else { Result 'FAIL' "generated hooks missing ($platform); run build" } if ((Test-Path (Join-Path $root "generated/codex-$shell/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/codex-$shell/hooks/scripts/validate-command-safety.sh")) -and (Test-Path (Join-Path $root "generated/claude-$shell/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/claude-$shell/hooks/scripts/validate-command-safety.sh"))) { Result 'PASS' "generated hooks present ($shell)" } else { Result 'FAIL' "generated hooks missing ($shell); run build" }
foreach ($client in @('codex','claude')) { foreach ($client in @('codex','claude')) {
$package = Join-Path $root "generated/$client-$platform" $package = Join-Path $root "generated/$client-$shell"
if (-not (Test-Path $package)) { continue } if (-not (Test-Path $package)) { continue }
$agentCount = @(Get-ChildItem (Join-Path $package 'agents') -File -ErrorAction SilentlyContinue).Count $agentCount = @(Get-ChildItem (Join-Path $package 'agents') -File -ErrorAction SilentlyContinue).Count
if ($agentCount -eq $sourceAgentCount) { Result 'PASS' "$client-$platform agent count matches source ($agentCount)" } else { Result 'FAIL' "$client-$platform agent count $agentCount does not match source ($sourceAgentCount)" } if ($agentCount -eq $sourceAgentCount) { Result 'PASS' "$client-$shell agent count matches source ($agentCount)" } else { Result 'FAIL' "$client-$shell agent count $agentCount does not match source ($sourceAgentCount)" }
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount } $expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
$skillCount = @(Get-ChildItem (Join-Path $package 'skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count $skillCount = @(Get-ChildItem (Join-Path $package 'skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
if ($skillCount -eq $expectedSkillCount) { Result 'PASS' "$client-$platform skill count matches source ($skillCount)" } else { Result 'FAIL' "$client-$platform skill count $skillCount does not match source ($expectedSkillCount)" } if ($skillCount -eq $expectedSkillCount) { Result 'PASS' "$client-$shell skill count matches source ($skillCount)" } else { Result 'FAIL' "$client-$shell skill count $skillCount does not match source ($expectedSkillCount)" }
$placeholderHits = @(Get-ChildItem $package -File -Recurse | Where-Object { ((Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__', '') -cmatch '__[A-Z0-9_]+__' }) $placeholderHits = @(Get-ChildItem $package -File -Recurse | Where-Object { ((Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__', '') -cmatch '__[A-Z0-9_]+__' })
if ($placeholderHits.Count -eq 0) { Result 'PASS' "$client-$platform has no unresolved placeholders" } else { Result 'FAIL' "$client-$platform has unresolved placeholders: $($placeholderHits.FullName -join ', ')" } if ($placeholderHits.Count -eq 0) { Result 'PASS' "$client-$shell has no unresolved placeholders" } else { Result 'FAIL' "$client-$shell has unresolved placeholders: $($placeholderHits.FullName -join ', ')" }
} }
} }
+9 -9
View File
@@ -29,7 +29,7 @@ result() {
for tool in codex claude; do for tool in codex claude; do
if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi
done done
command -v jq >/dev/null && result PASS 'jq available for Claude status line' || result WARN 'jq unavailable; Claude status line is disabled' command -v jq >/dev/null && result PASS 'jq available for Claude Bash status line' || result WARN 'jq unavailable; Claude Bash status line is disabled'
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
source_skill_count=$(find "$root/shared/skills" -name SKILL.md 2>/dev/null | wc -l) source_skill_count=$(find "$root/shared/skills" -name SKILL.md 2>/dev/null | wc -l)
@@ -37,20 +37,20 @@ rule_skill_manifest="$root/adapters/claude/rule-skills.tsv"
rule_skill_count=0 rule_skill_count=0
[ -f "$rule_skill_manifest" ] && rule_skill_count=$(($(wc -l < "$rule_skill_manifest") - 1)) [ -f "$rule_skill_manifest" ] && rule_skill_count=$(($(wc -l < "$rule_skill_manifest") - 1))
for platform in windows linux; do for shell in powershell bash; do
[ -f "$root/generated/codex-$platform/AGENTS.md" ] && result PASS "generated output present ($platform)" || result FAIL "generated output missing ($platform); run build" [ -f "$root/generated/codex-$shell/AGENTS.md" ] && result PASS "generated output present ($shell)" || result FAIL "generated output missing ($shell); run build"
[ -f "$root/generated/codex-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/codex-$platform/hooks/scripts/validate-command-safety.sh" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/claude-$platform/hooks/scripts/validate-command-safety.sh" ] && result PASS "generated hooks present ($platform)" || result FAIL "generated hooks missing ($platform); run build" [ -f "$root/generated/codex-$shell/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/codex-$shell/hooks/scripts/validate-command-safety.sh" ] && [ -f "$root/generated/claude-$shell/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/claude-$shell/hooks/scripts/validate-command-safety.sh" ] && result PASS "generated hooks present ($shell)" || result FAIL "generated hooks missing ($shell); run build"
for client in codex claude; do for client in codex claude; do
package="$root/generated/$client-$platform" package="$root/generated/$client-$shell"
[ -d "$package" ] || continue [ -d "$package" ] || continue
agent_count=$(find "$package/agents" -type f 2>/dev/null | wc -l) agent_count=$(find "$package/agents" -type f 2>/dev/null | wc -l)
if [ "$agent_count" -eq "$source_agent_count" ]; then result PASS "$client-$platform agent count matches source ($agent_count)"; else result FAIL "$client-$platform agent count $agent_count does not match source ($source_agent_count)"; fi if [ "$agent_count" -eq "$source_agent_count" ]; then result PASS "$client-$shell agent count matches source ($agent_count)"; else result FAIL "$client-$shell agent count $agent_count does not match source ($source_agent_count)"; fi
expected_skill_count=$source_skill_count expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count)) [ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
skill_count=$(find "$package/skills" -name SKILL.md 2>/dev/null | wc -l) skill_count=$(find "$package/skills" -name SKILL.md 2>/dev/null | wc -l)
if [ "$skill_count" -eq "$expected_skill_count" ]; then result PASS "$client-$platform skill count matches source ($skill_count)"; else result FAIL "$client-$platform skill count $skill_count does not match source ($expected_skill_count)"; fi if [ "$skill_count" -eq "$expected_skill_count" ]; then result PASS "$client-$shell skill count matches source ($skill_count)"; else result FAIL "$client-$shell skill count $skill_count does not match source ($expected_skill_count)"; fi
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$package" 2>/dev/null | sort -u | grep -v '^__AI_CONFIG_ROOT__$' || true) leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$package" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__)$' || true)
if [ -n "$leftover_tokens" ]; then result FAIL "$client-$platform has unresolved placeholders"; else result PASS "$client-$platform has no unresolved placeholders"; fi if [ -n "$leftover_tokens" ]; then result FAIL "$client-$shell has unresolved placeholders"; else result PASS "$client-$shell has no unresolved placeholders"; fi
done done
done done
+8 -8
View File
@@ -1,7 +1,7 @@
function Read-InstallPlatform { function Read-InstallShell {
param([string]$Platform) param([string]$Shell)
if ($Platform) { return $Platform } if ($Shell) { return $Shell }
return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('Windows','Linux') return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('PowerShell','Bash')
} }
function Read-InstallClient { function Read-InstallClient {
param([string]$Client) param([string]$Client)
@@ -13,14 +13,14 @@ function Read-InteractiveSingle {
param([string]$Title,[string[]]$Names,[string[]]$Values) param([string]$Title,[string[]]$Names,[string[]]$Values)
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') {
Write-Host $Title; for($i=0;$i -lt $Names.Count;$i++){ Write-Host ('{0}) {1}' -f ($i+1),$Names[$i]) } Write-Host $Title; for($i=0;$i -lt $Names.Count;$i++){ Write-Host ('{0}) {1}' -f ($i+1),$Names[$i]) }
do { $answer=Read-Host 'Selection' } while($answer -notmatch ('^[1-{0}]$' -f $Names.Count)); return $Values[[int]$answer-1] do { $answer=Read-Host 'Selection'; if ($null -eq $answer -or ([Console]::IsInputRedirected -and $answer -eq '')) { throw 'Selection input ended before a choice was provided.' } } while($answer -notmatch ('^[1-{0}]$' -f $Names.Count)); return $Values[[int]$answer-1]
} }
$index=0 $index=0
while($true) { while($true) {
Clear-InteractiveScreen; Write-Host $Title Clear-InteractiveScreen; Write-Host $Title
for($i=0;$i -lt $Names.Count;$i++){ $prefix=if($i -eq $index){'>'}else{' '}; if($i -eq $index -and $env:NO_COLOR -ne '1'){Write-Host ('{0} {1}' -f $prefix,$Names[$i]) -ForegroundColor Cyan}else{Write-Host ('{0} {1}' -f $prefix,$Names[$i])} } for($i=0;$i -lt $Names.Count;$i++){ $prefix=if($i -eq $index){'>'}else{' '}; if($i -eq $index -and $env:NO_COLOR -ne '1'){Write-Host ('{0} {1}' -f $prefix,$Names[$i]) -ForegroundColor Cyan}else{Write-Host ('{0} {1}' -f $prefix,$Names[$i])} }
Write-Host 'Up/Down Navigate Enter Confirm'; $key=[Console]::ReadKey($true) Write-Host 'Up/Down Navigate Enter Confirm'; $key=[Console]::ReadKey($true)
if($key.Key -eq 'Up'){ $index=($index+$Names.Count-1)%$Names.Count } elseif($key.Key -eq 'Down'){ $index=($index+1)%$Names.Count } elseif($key.Key -eq 'Enter'){ Clear-InteractiveScreen; return $Values[$index] } if($key.Key -eq 'UpArrow'){ $index=($index+$Names.Count-1)%$Names.Count } elseif($key.Key -eq 'DownArrow'){ $index=($index+1)%$Names.Count } elseif($key.Key -eq 'Enter'){ Clear-InteractiveScreen; return $Values[$index] }
} }
} }
function Get-InstallDestinations { function Get-InstallDestinations {
@@ -28,8 +28,8 @@ function Get-InstallDestinations {
$destinations=@(); if($Client -in @('Codex','Both')){$destinations+=Join-Path $HomePath '.codex'}; if($Client -in @('Claude','Both')){$destinations+=Join-Path $HomePath '.claude'}; return $destinations $destinations=@(); if($Client -in @('Codex','Both')){$destinations+=Join-Path $HomePath '.codex'}; if($Client -in @('Claude','Both')){$destinations+=Join-Path $HomePath '.claude'}; return $destinations
} }
function Get-InstallTargets { function Get-InstallTargets {
param([Parameter(Mandatory=$true)][string]$Generated,[Parameter(Mandatory=$true)][string]$HomePath,[Parameter(Mandatory=$true)][string]$Platform,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client) param([Parameter(Mandatory=$true)][string]$Generated,[Parameter(Mandatory=$true)][string]$HomePath,[Parameter(Mandatory=$true)][string]$Shell,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
$targets=@(); if($Client -in @('Codex','Both')){$targets+=@{Source=(Join-Path $Generated "codex-$Platform");Destination=(Join-Path $HomePath '.codex')};$targets+=@{Source=(Join-Path $Generated "codex-$Platform/skills");Destination=(Join-Path $HomePath '.agents/skills')}}; if($Client -in @('Claude','Both')){$targets+=@{Source=(Join-Path $Generated "claude-$Platform");Destination=(Join-Path $HomePath '.claude')};}; return $targets $targets=@(); if($Client -in @('Codex','Both')){$targets+=@{Source=(Join-Path $Generated "codex-$($Shell.ToLowerInvariant())");Destination=(Join-Path $HomePath '.codex')};$targets+=@{Source=(Join-Path $Generated "codex-$($Shell.ToLowerInvariant())/skills");Destination=(Join-Path $HomePath '.agents/skills')}}; if($Client -in @('Claude','Both')){$targets+=@{Source=(Join-Path $Generated "claude-$($Shell.ToLowerInvariant())");Destination=(Join-Path $HomePath '.claude')};}; return $targets
} }
function Test-AnyManifestPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{Test-Path(Get-ManagedManifestPath $_)}).Count) } function Test-AnyManifestPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{Test-Path(Get-ManagedManifestPath $_)}).Count) }
function Test-AllManifestsPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{-not(Test-Path(Get-ManagedManifestPath $_))}).Count -eq 0) } function Test-AllManifestsPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{-not(Test-Path(Get-ManagedManifestPath $_))}).Count -eq 0) }
+33 -15
View File
@@ -1,15 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
read_install_platform() { read_install_shell() {
local platform=$1 local shell=$1
if [ -n "$platform" ]; then if [ -n "$shell" ]; then
case "$platform" in case "$shell" in
windows|Windows) printf 'windows\n'; return 0 ;; powershell|PowerShell) printf 'powershell\n'; return 0 ;;
linux|Linux) printf 'linux\n'; return 0 ;; bash|Bash) printf 'bash\n'; return 0 ;;
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;; *) printf 'Unknown --shell value: %s (expected powershell or bash)\n' "$shell" >&2; return 1 ;;
esac esac
fi fi
read_single_selection 'Select shell' 'PowerShell' 'windows' 'Bash' 'linux' read_single_selection 'Select shell' 'PowerShell' 'powershell' 'Bash' 'bash'
} }
read_install_client() { read_install_client() {
@@ -25,12 +25,30 @@ read_install_client() {
read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both' read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both'
} }
clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H'; } clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H' >&2; }
read_single_selection() { read_single_selection() {
local title=$1; shift; local labels=() values=() label value index=0 key local title=$1; shift; local labels=() values=() label value index=0 key
while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi
local selected=0; while :; do clear_interactive; printf '%s\n' "$title"; for ((index=0;index<${#labels[@]};index++)); do [ "$index" -eq "$selected" ] && printf '> %s\n' "${labels[$index]}" || printf ' %s\n' "${labels[$index]}"; done; printf '↑/↓ Navigate Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') selected=$(( (selected+${#labels[@]}-1)%${#labels[@]} ));; '[B') selected=$(( (selected+1)%${#labels[@]} ));; esac;; '') clear_interactive; printf '%s\n' "${values[$selected]}"; return;; esac; done local selected=0
while :; do
clear_interactive
printf '%s\n' "$title" >&2
for ((index=0;index<${#labels[@]};index++)); do
if [ "$index" -eq "$selected" ]; then printf '> %s\n' "${labels[$index]}" >&2; else printf ' %s\n' "${labels[$index]}" >&2; fi
done
printf 'Up/Down Navigate Enter Confirm\n' >&2
IFS= read -rsn1 key || return 1
case "$key" in
$'\x1b')
read -rsn2 key || return 1
case "$key" in
'[A') selected=$(( (selected+${#labels[@]}-1)%${#labels[@]} ));;
'[B') selected=$(( (selected+1)%${#labels[@]} ));;
esac;;
'') clear_interactive; printf '%s\n' "${values[$selected]}"; return;;
esac
done
} }
# get_install_destinations <home> <client> -- prints one destination per line # get_install_destinations <home> <client> -- prints one destination per line
@@ -40,16 +58,16 @@ get_install_destinations() {
case "$client" in claude|both) printf '%s/.claude\n' "$home_path" ;; esac case "$client" in claude|both) printf '%s/.claude\n' "$home_path" ;; esac
} }
# get_install_targets <generated> <home> <platform> <client> -- prints "source|dest" per line # get_install_targets <generated> <home> <shell> <client> -- prints "source|dest" per line
get_install_targets() { get_install_targets() {
local generated=$1 home_path=$2 platform=$3 client=$4 local generated=$1 home_path=$2 shell=$3 client=$4
case "$client" in case "$client" in
codex|both) codex|both)
printf '%s/codex-%s|%s/.codex\n' "$generated" "$platform" "$home_path" printf '%s/codex-%s|%s/.codex\n' "$generated" "$shell" "$home_path"
printf '%s/codex-%s/skills|%s/.agents/skills\n' "$generated" "$platform" "$home_path" printf '%s/codex-%s/skills|%s/.agents/skills\n' "$generated" "$shell" "$home_path"
;; ;;
esac esac
case "$client" in claude|both) printf '%s/claude-%s|%s/.claude\n' "$generated" "$platform" "$home_path" ;; esac case "$client" in claude|both) printf '%s/claude-%s|%s/.claude\n' "$generated" "$shell" "$home_path" ;; esac
} }
# any_manifest_present <destination>... -- true (0) if at least one has a manifest # any_manifest_present <destination>... -- true (0) if at least one has a manifest
+5 -5
View File
@@ -3,21 +3,21 @@ param([switch]$Summary)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
$generated = Join-Path $root 'generated' $generated = Join-Path $root 'generated'
if (-not (Test-Path (Join-Path $generated 'claude-linux/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' } if (-not (Test-Path (Join-Path $generated 'claude-bash/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' }
function Measure-Tree([string]$path) { function Measure-Tree([string]$path) {
if (-not (Test-Path $path)) { return 0 } if (-not (Test-Path $path)) { return 0 }
return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum) return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum)
} }
function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) } function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) }
$claude = (Get-Item (Join-Path $generated 'claude-linux/CLAUDE.md')).Length $claude = (Get-Item (Join-Path $generated 'claude-bash/CLAUDE.md')).Length
$codex = (Get-Item (Join-Path $generated 'codex-linux/AGENTS.md')).Length $codex = (Get-Item (Join-Path $generated 'codex-bash/AGENTS.md')).Length
$skills = Measure-Tree (Join-Path $generated 'claude-linux/skills') $skills = Measure-Tree (Join-Path $generated 'claude-bash/skills')
$triggers = (Get-Item (Join-Path $root 'adapters/claude/rule-skills.tsv')).Length $triggers = (Get-Item (Join-Path $root 'adapters/claude/rule-skills.tsv')).Length
$lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1 $lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1
$lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 } $lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 }
$baseline = Join-Path $root 'adapters/prompt-budget-baseline.json' $baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null } $old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill metadata';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)}) $rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill files (lazy)';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
$fail = $false $fail = $false
if ($old) { foreach ($row in $rows | Select-Object -First 2) { $previous = [double]$old.($row.Name); if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true } } } if ($old) { foreach ($row in $rows | Select-Object -First 2) { $previous = [double]$old.($row.Name); if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true } } }
if ($Summary) { Write-Output 'Prompt Budget'; $rows | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } if ($Summary) { Write-Output 'Prompt Budget'; $rows | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } }
+4 -3
View File
@@ -4,11 +4,12 @@ summary=false
for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
generated="$root/generated" generated="$root/generated"
[ -f "$generated/claude-linux/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; } [ -f "$generated/claude-bash/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; }
bytes() { wc -c < "$1" | tr -d ' '; } bytes() { wc -c < "$1" | tr -d ' '; }
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; } tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; } tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
claude=$(bytes "$generated/claude-linux/CLAUDE.md"); codex=$(bytes "$generated/codex-linux/AGENTS.md"); skills=$(tree_bytes "$generated/claude-linux/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false claude=$(bytes "$generated/claude-bash/CLAUDE.md"); codex=$(bytes "$generated/codex-bash/AGENTS.md"); skills=$(tree_bytes "$generated/claude-bash/skills"); triggers=$(bytes "$root/adapters/claude/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then while IFS=$'\t' read -r name old; do current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex"); awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }' && fail=true; done < "$root/adapters/prompt-budget-baseline.tsv"; fi if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then while IFS=$'\t' read -r name old; do current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex"); awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }' && fail=true; done < "$root/adapters/prompt-budget-baseline.tsv"; fi
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes %6s tokens\nCodex permanent context %8s bytes %6s tokens\nClaude skill metadata %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$(tokens "$claude")" "$codex" "$(tokens "$codex")" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes %6s tokens\nCodex permanent context %8s bytes %6s tokens\nClaude skill files (lazy) %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$(tokens "$claude")" "$codex" "$(tokens "$codex")" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; } $fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
exit 0
@@ -4,7 +4,7 @@ $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent $root = Split-Path $PSScriptRoot -Parent
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw $powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw $bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
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.' } 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 PowerShell and Bash.' }
$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}}' $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 $branch = & git -C $root branch --show-current
$buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary $buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary
@@ -17,33 +17,33 @@ if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status lin
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count $sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).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 $ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/claude/rule-skills.tsv') -Delimiter ([char]9)).Count
foreach ($platform in @('windows','linux')) { foreach ($shell in @('powershell','bash')) {
foreach ($client in @('codex','claude')) { foreach ($client in @('codex','claude')) {
$package = Join-Path $root "generated/$client-$platform" $package = Join-Path $root "generated/$client-$shell"
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' } $file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw $content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json } if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" } if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount } $expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" } if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
$expected = if ($platform -eq 'windows') { 'powershell .*flashbang.ps1' } else { 'bash .*flashbang.sh' } $expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
if ($content -notmatch $expected -or $content -match '__HOOK_|__WINDOWS_HOOK_') { throw "Incorrect platform command in $package" } if ($content -notmatch $expected -or $content -match '__HOOK_|__POWERSHELL_HOOK_') { throw "Incorrect shell command in $package" }
if ($platform -eq 'windows' -and $content -match 'WindowStyle\s+Hidden') { throw "Windows hook hides the terminal in $package" } if ($shell -eq 'powershell' -and $content -match 'WindowStyle\s+Hidden') { throw "PowerShell hook hides the terminal in $package" }
if ($client -eq 'codex') { if ($client -eq 'codex') {
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value }) $events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" } if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" } if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" } if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
} elseif (@($settings.hooks.PSObject.Properties.Name).Count -ne 1 -or $settings.hooks.PSObject.Properties.Name -ne 'Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) { } elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
throw "Claude finish hook is incorrect in $package" throw "Claude finish hook is incorrect in $package"
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') { } elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
throw "Claude auto permission mode is missing in $package" throw "Claude auto permission mode is missing in $package"
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($platform -eq 'windows') { 'ps1' } else { 'sh' })") { } elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") {
throw "Claude status line is incorrect in $package" throw "Claude status line is incorrect in $package"
} }
$docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' } $docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' }
$docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw $docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw
if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$platform must embed the priority rule exactly once" } if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$shell must embed the priority rule exactly once" }
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" } if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' } if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
foreach ($splitRule in @('angular','wpf','ui-ux')) { foreach ($splitRule in @('angular','wpf','ui-ux')) {
@@ -52,27 +52,27 @@ foreach ($platform in @('windows','linux')) {
} }
} }
} }
foreach ($platformSelection in @('1','2')) { foreach ($shellSelection in @('1','2')) {
foreach ($clientSelection in @('1','2','3')) { foreach ($clientSelection in @('1','2','3')) {
$script:answers = [System.Collections.Generic.Queue[string]]::new() $script:answers = [System.Collections.Generic.Queue[string]]::new()
$script:answers.Enqueue($platformSelection) $script:answers.Enqueue($shellSelection)
$script:answers.Enqueue($clientSelection) $script:answers.Enqueue($clientSelection)
function Read-Host { param($Prompt) $answers.Dequeue() } function Read-Host { param($Prompt) $answers.Dequeue() }
$output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null $output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' } if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
$platform = if ($platformSelection -eq '1') { 'windows' } else { 'linux' } $shell = if ($shellSelection -eq '1') { 'powershell' } else { 'bash' }
if (($output -join "`n") -notmatch "-$platform") { throw 'Incorrect selected package.' } if (($output -join "`n") -notmatch "-$shell") { throw 'Incorrect selected package.' }
} }
} }
$doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary $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 '^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 '; ')" } 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 '; ')" }
foreach ($entryPoint in @('install','update')) { foreach ($entryPoint in @('install','update')) {
$preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Platform Windows) $preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Shell PowerShell)
if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." } if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." }
if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." } if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." }
} }
$doctorSummaryOutput | Where-Object { $_ -match '^WARN ' } $doctorSummaryOutput | Where-Object { $_ -match '^WARN ' }
if ($Summary) { Write-Output 'Tests: PASS | platform packages' } else { Write-Output 'PASS four platform packages and six PowerShell selections' } if ($Summary) { Write-Output 'Tests: PASS | shell packages' } else { Write-Output 'PASS four shell packages and six PowerShell selections' }
exit 0 exit 0
+15 -15
View File
@@ -25,9 +25,9 @@ expected_status_output=$'Test Model \xc2\xb7 Effort high \xc2\xb7 TestRepo @ '"$
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l) 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) source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l)
rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1)) rule_skill_count=$(($(wc -l < "$root/adapters/claude/rule-skills.tsv") - 1))
for platform in windows linux; do for shell in powershell bash; do
for client in codex claude; do for client in codex claude; do
package="$root/generated/$client-$platform" package="$root/generated/$client-$shell"
file=config.toml file=config.toml
[ "$client" != claude ] || file=settings.json [ "$client" != claude ] || file=settings.json
test -f "$package/$file" test -f "$package/$file"
@@ -35,8 +35,8 @@ for platform in windows linux; do
expected_skill_count=$source_skill_count expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count)) [ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count" test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
if [ "$platform" = windows ]; then if [ "$shell" = powershell ]; then
grep -q 'powershell .*flashbang.ps1' "$package/$file" grep -q '__POWERSHELL_COMMAND__ .*flashbang.ps1' "$package/$file"
! grep -Eq 'WindowStyle[[:space:]]+Hidden' "$package/$file" ! grep -Eq 'WindowStyle[[:space:]]+Hidden' "$package/$file"
else else
grep -q 'bash .*flashbang.sh' "$package/$file" grep -q 'bash .*flashbang.sh' "$package/$file"
@@ -48,21 +48,21 @@ for platform in windows linux; do
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file" grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
grep -Fq 'status_line = ["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file" grep -Fq 'status_line = ["model", "reasoning", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
else else
[ "$(awk '/^ "hooks": \{/{inside=1; next} inside && /^ \}/{inside=0} inside && /^ "[^"]+": \[$/{gsub(/^ "|": \[$/, ""); print}' "$package/$file")" = Stop ] python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert set(settings["hooks"]) == {"PreCompact", "SessionStart", "Stop"}' "$package/$file"
! grep -q 'flashbang-if-input' "$package/$file" ! grep -q 'flashbang-if-input' "$package/$file"
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file" ! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file" grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
statusline_extension=sh statusline_extension=sh
[ "$platform" != windows ] || statusline_extension=ps1 [ "$shell" != powershell ] || statusline_extension=ps1
test -f "$package/statusline/statusline.$statusline_extension" test -f "$package/statusline/statusline.$statusline_extension"
grep -q "statusline.$statusline_extension" "$package/$file" grep -q "statusline.$statusline_extension" "$package/$file"
fi fi
if grep -Eq '__HOOK_|__WINDOWS_HOOK_' "$package/$file"; then exit 1; fi if grep -Eq '__HOOK_|__POWERSHELL_HOOK_' "$package/$file"; then exit 1; fi
doc_file=AGENTS.md doc_file=AGENTS.md
[ "$client" != claude ] || doc_file=CLAUDE.md [ "$client" != claude ] || doc_file=CLAUDE.md
doc_content=$(cat "$package/$doc_file") doc_content=$(cat "$package/$doc_file")
occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l) occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l)
[ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$platform" >&2; exit 1; } [ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$shell" >&2; exit 1; }
if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then
printf '%s must not still instruct loading general.md by path\n' "$package" >&2 printf '%s must not still instruct loading general.md by path\n' "$package" >&2
exit 1 exit 1
@@ -84,14 +84,14 @@ for platform in windows linux; do
done done
done done
done done
for platform_selection in 1 2; do for shell_selection in 1 2; do
for client_selection in 1 2 3; do for client_selection in 1 2 3; do
output=$(printf '%s\n%s\n' "$platform_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; } output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; }
printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true
[[ "$output" == *'PASS install dry-run'* ]] [[ "$output" == *'PASS install dry-run'* ]]
platform=windows shell=powershell
[ "$platform_selection" != 2 ] || platform=linux [ "$shell_selection" != 2 ] || shell=bash
[[ "$output" == *"-$platform"* ]] [[ "$output" == *"-$shell"* ]]
done done
done done
doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary) doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary)
@@ -104,7 +104,7 @@ if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+
exit 1 exit 1
fi fi
for entry_point in install update; do for entry_point in install update; do
preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --platform linux) preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --shell bash)
if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then
printf '%s summary leaked per-item output.\n' "$entry_point" >&2 printf '%s summary leaked per-item output.\n' "$entry_point" >&2
exit 1 exit 1
@@ -112,4 +112,4 @@ for entry_point in install update; do
printf '%s\n' "$preview" | grep -Eiq "^$entry_point: PASS" printf '%s\n' "$preview" | grep -Eiq "^$entry_point: PASS"
done done
printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true
if [ "$summary" = true ]; then printf 'Tests: PASS | platform packages\n'; else printf 'PASS four platform packages and six Bash selections\n'; fi if [ "$summary" = true ]; then printf 'Tests: PASS | shell packages\n'; else printf 'PASS four shell packages and six Bash selections\n'; fi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Linux equivalent of flashbang.ps1. The visual overlay is implemented with # Bash equivalent of flashbang.ps1. The visual overlay is implemented with
# Python's standard tkinter module because Bash itself cannot create windows. # Python's standard tkinter module because Bash itself cannot create windows.
# No third-party Python package is required. # No third-party Python package is required.