diff --git a/archdev/references/bootstrap.md b/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/archdev/references/bootstrap.md +++ b/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/archdev/scripts/bootstrap.ps1 b/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/archdev/scripts/bootstrap.ps1 +++ b/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/archdev/scripts/bootstrap.sh b/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/archdev/scripts/bootstrap.sh +++ b/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/antigravity/skills/archdev/references/bootstrap.md b/plugins/antigravity/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/antigravity/skills/archdev/references/bootstrap.md +++ b/plugins/antigravity/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/antigravity/skills/archdev/scripts/bootstrap.ps1 b/plugins/antigravity/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/antigravity/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/antigravity/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/antigravity/skills/archdev/scripts/bootstrap.sh b/plugins/antigravity/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/antigravity/skills/archdev/scripts/bootstrap.sh +++ b/plugins/antigravity/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.sh b/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/antigravity/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/antigravity/skills/tasks/scripts/bootstrap.ps1 b/plugins/antigravity/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/antigravity/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/antigravity/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/antigravity/skills/tasks/scripts/bootstrap.sh b/plugins/antigravity/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/antigravity/skills/tasks/scripts/bootstrap.sh +++ b/plugins/antigravity/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/claude/skills/archdev/references/bootstrap.md b/plugins/claude/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/claude/skills/archdev/references/bootstrap.md +++ b/plugins/claude/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/claude/skills/archdev/scripts/bootstrap.ps1 b/plugins/claude/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/claude/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/claude/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/claude/skills/archdev/scripts/bootstrap.sh b/plugins/claude/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/claude/skills/archdev/scripts/bootstrap.sh +++ b/plugins/claude/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/claude/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/claude/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/claude/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/claude/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/claude/skills/tasks-execute/scripts/bootstrap.sh b/plugins/claude/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/claude/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/claude/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/claude/skills/tasks/scripts/bootstrap.ps1 b/plugins/claude/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/claude/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/claude/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/claude/skills/tasks/scripts/bootstrap.sh b/plugins/claude/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/claude/skills/tasks/scripts/bootstrap.sh +++ b/plugins/claude/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/codex/skills/archdev/references/bootstrap.md b/plugins/codex/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/codex/skills/archdev/references/bootstrap.md +++ b/plugins/codex/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/codex/skills/archdev/scripts/bootstrap.ps1 b/plugins/codex/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/codex/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/codex/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/codex/skills/archdev/scripts/bootstrap.sh b/plugins/codex/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/codex/skills/archdev/scripts/bootstrap.sh +++ b/plugins/codex/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/codex/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/codex/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/codex/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/codex/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/codex/skills/tasks-execute/scripts/bootstrap.sh b/plugins/codex/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/codex/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/codex/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/codex/skills/tasks/scripts/bootstrap.ps1 b/plugins/codex/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/codex/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/codex/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/codex/skills/tasks/scripts/bootstrap.sh b/plugins/codex/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/codex/skills/tasks/scripts/bootstrap.sh +++ b/plugins/codex/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/copilot/skills/archdev/references/bootstrap.md b/plugins/copilot/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/copilot/skills/archdev/references/bootstrap.md +++ b/plugins/copilot/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/copilot/skills/archdev/scripts/bootstrap.ps1 b/plugins/copilot/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/copilot/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/copilot/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/copilot/skills/archdev/scripts/bootstrap.sh b/plugins/copilot/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/copilot/skills/archdev/scripts/bootstrap.sh +++ b/plugins/copilot/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/copilot/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/copilot/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/copilot/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/copilot/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/copilot/skills/tasks-execute/scripts/bootstrap.sh b/plugins/copilot/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/copilot/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/copilot/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/copilot/skills/tasks/scripts/bootstrap.ps1 b/plugins/copilot/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/copilot/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/copilot/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/copilot/skills/tasks/scripts/bootstrap.sh b/plugins/copilot/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/copilot/skills/tasks/scripts/bootstrap.sh +++ b/plugins/copilot/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/cursor/skills/archdev/references/bootstrap.md b/plugins/cursor/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/cursor/skills/archdev/references/bootstrap.md +++ b/plugins/cursor/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/cursor/skills/archdev/scripts/bootstrap.ps1 b/plugins/cursor/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/cursor/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/cursor/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/cursor/skills/archdev/scripts/bootstrap.sh b/plugins/cursor/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/cursor/skills/archdev/scripts/bootstrap.sh +++ b/plugins/cursor/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/cursor/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/cursor/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/cursor/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/cursor/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/cursor/skills/tasks-execute/scripts/bootstrap.sh b/plugins/cursor/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/cursor/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/cursor/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/cursor/skills/tasks/scripts/bootstrap.ps1 b/plugins/cursor/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/cursor/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/cursor/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/cursor/skills/tasks/scripts/bootstrap.sh b/plugins/cursor/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/cursor/skills/tasks/scripts/bootstrap.sh +++ b/plugins/cursor/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/grok/skills/archdev/references/bootstrap.md b/plugins/grok/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/grok/skills/archdev/references/bootstrap.md +++ b/plugins/grok/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/grok/skills/archdev/scripts/bootstrap.ps1 b/plugins/grok/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/grok/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/grok/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/grok/skills/archdev/scripts/bootstrap.sh b/plugins/grok/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/grok/skills/archdev/scripts/bootstrap.sh +++ b/plugins/grok/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/grok/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/grok/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/grok/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/grok/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/grok/skills/tasks-execute/scripts/bootstrap.sh b/plugins/grok/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/grok/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/grok/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/grok/skills/tasks/scripts/bootstrap.ps1 b/plugins/grok/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/grok/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/grok/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/grok/skills/tasks/scripts/bootstrap.sh b/plugins/grok/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/grok/skills/tasks/scripts/bootstrap.sh +++ b/plugins/grok/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/opencode/skills/archdev/references/bootstrap.md b/plugins/opencode/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/opencode/skills/archdev/references/bootstrap.md +++ b/plugins/opencode/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/opencode/skills/archdev/scripts/bootstrap.ps1 b/plugins/opencode/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/opencode/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/opencode/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/opencode/skills/archdev/scripts/bootstrap.sh b/plugins/opencode/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/opencode/skills/archdev/scripts/bootstrap.sh +++ b/plugins/opencode/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/opencode/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/opencode/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/opencode/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/opencode/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/opencode/skills/tasks-execute/scripts/bootstrap.sh b/plugins/opencode/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/opencode/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/opencode/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/opencode/skills/tasks/scripts/bootstrap.ps1 b/plugins/opencode/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/opencode/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/opencode/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/opencode/skills/tasks/scripts/bootstrap.sh b/plugins/opencode/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/opencode/skills/tasks/scripts/bootstrap.sh +++ b/plugins/opencode/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/pi/skills/archdev/references/bootstrap.md b/plugins/pi/skills/archdev/references/bootstrap.md index f067d0a..5ce8d80 100644 --- a/plugins/pi/skills/archdev/references/bootstrap.md +++ b/plugins/pi/skills/archdev/references/bootstrap.md @@ -19,7 +19,11 @@ scope. Do not edit agent instruction files or commit changes without permission. The Bash and PowerShell bootstrap scripts resolve or install the executable and check its capabilities. They do not install or refresh hooks, install skills, authenticate, or create repository configuration. Do not run them to -bypass the guide's consent steps. +bypass the guide's consent steps. When the executable is Homebrew-managed +(linked from a Homebrew Cellar), they update it with +`brew upgrade archastro/tools/archdev` (or `brew reinstall` for a current but +broken copy) and never install a second copy; without a working `brew` they +stop with an error naming that binary. ## Version and login diff --git a/plugins/pi/skills/archdev/scripts/bootstrap.ps1 b/plugins/pi/skills/archdev/scripts/bootstrap.ps1 index a950b39..cc380b6 100644 --- a/plugins/pi/skills/archdev/scripts/bootstrap.ps1 +++ b/plugins/pi/skills/archdev/scripts/bootstrap.ps1 @@ -30,9 +30,6 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } - $minVersion = [Version]"0.49.6" function Test-Version([string]$Binary) { @@ -60,17 +57,125 @@ function Test-Skill([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($helpText -join "`n") -match "--local")) } -if (-not (Test-Skill $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } } +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Skill $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Skill $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Skill $archdev)) { throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" } +if (-not (Test-Skill $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback" +} # Resolving the executable must not choose configuration scope. Install and # repair hooks only through the approved branch in https://archdev.ai/install.md. diff --git a/plugins/pi/skills/archdev/scripts/bootstrap.sh b/plugins/pi/skills/archdev/scripts/bootstrap.sh index e3a195c..297ba3d 100755 --- a/plugins/pi/skills/archdev/scripts/bootstrap.sh +++ b/plugins/pi/skills/archdev/scripts/bootstrap.sh @@ -59,14 +59,6 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" @@ -85,19 +77,122 @@ supports_skill() { "$1" repo hook setup --help 2>/dev/null | grep -F -- "--local" >/dev/null } -if ! supports_skill "$executable"; then - printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_skill "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_skill "$executable"; then + printf 'Updating ArchDev: this skill requires 0.49.6+ and repository hook setup with --local.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_skill "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks required commands or --local hook setup (need 0.49.6+); stopping without a global fallback.\n' >&2 exit 1 } diff --git a/plugins/pi/skills/tasks-execute/scripts/bootstrap.ps1 b/plugins/pi/skills/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/plugins/pi/skills/tasks-execute/scripts/bootstrap.ps1 +++ b/plugins/pi/skills/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/pi/skills/tasks-execute/scripts/bootstrap.sh b/plugins/pi/skills/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/plugins/pi/skills/tasks-execute/scripts/bootstrap.sh +++ b/plugins/pi/skills/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/plugins/pi/skills/tasks/scripts/bootstrap.ps1 b/plugins/pi/skills/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/plugins/pi/skills/tasks/scripts/bootstrap.ps1 +++ b/plugins/pi/skills/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/plugins/pi/skills/tasks/scripts/bootstrap.sh b/plugins/pi/skills/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/plugins/pi/skills/tasks/scripts/bootstrap.sh +++ b/plugins/pi/skills/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/scripts/fake-brew b/scripts/fake-brew new file mode 100755 index 0000000..6b7531d --- /dev/null +++ b/scripts/fake-brew @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Stand-in Homebrew for the skill bootstrap tests. It appends its argv to +# $FAKE_BREW_LOG, answers `--prefix` with $FAKE_BREW_PREFIX, and for +# `upgrade` or `reinstall archastro/tools/archdev` replaces the keg with +# Cellar/archdev/0.60.0 (a scripts/fake-archdev wrapper at $ARCHDEV_FAKE_BIN) +# and relinks /bin/archdev and /opt/archdev, as Homebrew does. +# `upgrade` leaves an existing 0.60.0 keg alone, even a broken one. +# +# FAKE_BREW_EXIT=N fail upgrade/reinstall with status N, keg unchanged + +set -euo pipefail + +printf '%s\n' "$*" >>"$FAKE_BREW_LOG" +case "${1:-}" in + --prefix) printf '%s\n' "$FAKE_BREW_PREFIX" ;; + upgrade | reinstall) + [[ "${2:-}" == archastro/tools/archdev ]] || exit 1 + if [[ "${FAKE_BREW_EXIT:-0}" != 0 ]]; then + printf 'Error: fake brew %s failed\n' "$1" >&2 + exit "$FAKE_BREW_EXIT" + fi + # Like Homebrew, upgrading the latest version changes nothing. + if [[ "$1" == upgrade && -d "$FAKE_BREW_PREFIX/Cellar/archdev/0.60.0" ]]; then + printf 'Warning: archastro/tools/archdev 0.60.0 already installed\n' >&2 + exit 0 + fi + # Real brew reports progress on stdout; the bootstrap must keep it off its own. + printf '==> %s archastro/tools/archdev 0.60.0\n' "$1" + rm -rf "$FAKE_BREW_PREFIX/Cellar/archdev" + keg="$FAKE_BREW_PREFIX/Cellar/archdev/0.60.0/bin" + mkdir -p "$keg" + printf '#!/bin/sh\nARCHDEV_FAKE_VERSION=0.60.0 exec "%s" "$@"\n' "$ARCHDEV_FAKE_BIN" >"$keg/archdev" + chmod +x "$keg/archdev" + ln -sfn ../Cellar/archdev/0.60.0/bin/archdev "$FAKE_BREW_PREFIX/bin/archdev" + mkdir -p "$FAKE_BREW_PREFIX/opt" + ln -sfn ../Cellar/archdev/0.60.0 "$FAKE_BREW_PREFIX/opt/archdev" + ;; + *) exit 1 ;; +esac diff --git a/scripts/test-skill-bootstrap.ps1 b/scripts/test-skill-bootstrap.ps1 index 383fd02..32be075 100644 --- a/scripts/test-skill-bootstrap.ps1 +++ b/scripts/test-skill-bootstrap.ps1 @@ -112,7 +112,179 @@ exit 0 } } +# Homebrew layouts (macOS and Linux only): /Cellar/archdev//bin/archdev +# linked from /bin/archdev, and scripts/fake-brew recording its calls. +# Bootstrap must repair that copy with brew and never install a second one. +function New-FakeArchDev([string]$Path, [string]$Version) { + Set-Content -LiteralPath $Path -Value "#!/bin/sh`nARCHDEV_FAKE_VERSION=$Version exec `"$(Join-Path $repo 'scripts/fake-archdev')`" `"`$@`"" + & chmod +x $Path +} + +function Invoke-BrewCase { + param( + [string]$Name, + [string]$Skill, + # An old keg is 0.49.0; every other keg is the formula's latest, 0.60.0, + # so a `brew upgrade` of it changes nothing, as with real Homebrew. + [ValidateSet("old", "stub", "crash", "current", "missing", "none")][string]$Keg, + [ValidateSet("yes", "no", "fail")][string]$Brew, + [ValidateSet("none", "old", "current")][string]$Copy, + # upgrade / reinstall: brew repairs the keg; current: used as is; + # copy: the installer copy is used as is; + # error: stops naming Homebrew; installer: reaches the pinned installer. + [string]$Expect, + [string]$ErrorText = "", + # opt: PATH reaches the keg through Homebrew's opt link. + # other-brew: another prefix's brew comes first on PATH. + [ValidateSet("", "opt", "other-brew")][string]$Variant = "" + ) + $label = "brew $Skill-$Name" + $caseDir = Join-Path $work "brew-$Skill-$Name" + $homeDir = Join-Path $caseDir "home" + $prefix = Join-Path $caseDir "homebrew" + $prefixBin = Join-Path $prefix "bin" + $brewLog = Join-Path $caseDir "brew.log" + $setupLog = Join-Path $caseDir "setup.log" + New-Item -ItemType Directory -Path $homeDir, $prefixBin | Out-Null + Set-Content -LiteralPath $brewLog -Value $null -NoNewline + Set-Content -LiteralPath $setupLog -Value $null -NoNewline + $kegVersion = if ($Keg -eq "old") { "0.49.0" } else { "0.60.0" } + if ($Keg -ne "none") { + $kegBin = Join-Path $prefix "Cellar/archdev/$kegVersion/bin" + New-Item -ItemType Directory -Path $kegBin, (Join-Path $prefix "opt") | Out-Null + $kegArchDev = Join-Path $kegBin "archdev" + switch ($Keg) { + "old" { New-FakeArchDev $kegArchDev "0.49.5" } + "current" { New-FakeArchDev $kegArchDev "0.49.6" } + # The stub that replaced a real keg: a high version, no commands. + "stub" { Set-Content -LiteralPath $kegArchDev -Value "#!/bin/sh`necho `"archdev 3.0.0`""; & chmod +x $kegArchDev } + # A binary that crashes before it can report a version. + "crash" { Set-Content -LiteralPath $kegArchDev -Value "#!/bin/sh`nexit 139"; & chmod +x $kegArchDev } + "missing" { Remove-Item -LiteralPath (Join-Path $prefix "Cellar/archdev") -Recurse -Force } + } + & ln -s "../Cellar/archdev/$kegVersion/bin/archdev" (Join-Path $prefixBin "archdev") + & ln -s "../Cellar/archdev/$kegVersion" (Join-Path $prefix "opt/archdev") + } + if ($Brew -ne "no") { + Copy-Item (Join-Path $repo "scripts/fake-brew") (Join-Path $prefixBin "brew") + & chmod +x (Join-Path $prefixBin "brew") + } + $casePath = "$prefixBin`:/usr/bin:/bin" + $linked = Join-Path $prefixBin "archdev" + $otherBrewLog = Join-Path $caseDir "other-brew.log" + if ($Variant -eq "opt") { + $casePath = "$(Join-Path $prefix 'opt/archdev/bin')`:$casePath" + $linked = Join-Path $prefix "opt/archdev/bin/archdev" + } elseif ($Variant -eq "other-brew") { + $otherBin = Join-Path $caseDir "other-homebrew/bin" + New-Item -ItemType Directory -Path $otherBin | Out-Null + Set-Content -LiteralPath (Join-Path $otherBin "brew") -Value "#!/bin/sh`necho `"`$*`" >>'$otherBrewLog'`necho '$(Join-Path $caseDir "other-homebrew")'" + & chmod +x (Join-Path $otherBin "brew") + $casePath = "$otherBin`:$casePath" + } + if ($Copy -ne "none") { + # A previous installer copy that comes first on PATH. + $copyBin = Join-Path $homeDir ".local/bin" + New-Item -ItemType Directory -Path $copyBin | Out-Null + New-FakeArchDev (Join-Path $copyBin "archdev") $(if ($Copy -eq "old") { "0.49.5" } else { "0.49.6" }) + $casePath = "$copyBin`:$casePath" + } + $homeBefore = (Get-ChildItem -LiteralPath $homeDir -Recurse -File | ForEach-Object { "$($_.FullName) $((Get-FileHash -LiteralPath $_.FullName).Hash)" }) -join "`n" + + foreach ($variable in $caseVariables) { Remove-Item "Env:$variable" -ErrorAction SilentlyContinue } + $env:HOME = $homeDir + $env:PATH = $casePath + $env:ARCHDEV_FAKE_LOG = $setupLog + $env:ARCHDEV_FAKE_BIN = Join-Path $repo "scripts/fake-archdev" + $env:FAKE_BREW_LOG = $brewLog + $env:FAKE_BREW_PREFIX = $prefix + $env:FAKE_BREW_EXIT = if ($Brew -eq "fail") { "1" } else { "0" } + $env:ARCHDEV_INSTALL_DIR = Join-Path $caseDir "install" + $runner = Join-Path $caseDir "runner.ps1" + $bootstrap = (Join-Path $repo "$Skill/scripts/bootstrap.ps1").Replace("'", "''") + Set-Content -LiteralPath $runner -Value @" +`$ErrorActionPreference = 'Stop' +function global:Invoke-WebRequest { throw 'Blocked installer download in bootstrap fixture' } +try { & '$bootstrap' } catch { + [Console]::Error.WriteLine(`$_.ToString()) + exit 1 +} +exit 0 +"@ + try { + $out = & (Get-Process -Id $PID).Path -NoProfile -ExecutionPolicy Bypass -File $runner 2>(Join-Path $caseDir "stderr") + $exit = $LASTEXITCODE + } finally { + $env:PATH = $savedPath + $env:HOME = $savedHome + foreach ($name in @("ARCHDEV_FAKE_BIN", "FAKE_BREW_LOG", "FAKE_BREW_PREFIX", "FAKE_BREW_EXIT")) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue } + } + $errorText = Get-Content (Join-Path $caseDir "stderr") -Raw + if (-not $errorText) { $errorText = "" } + $brewCalls = @(Get-Content -LiteralPath $brewLog) + $homeAfter = (Get-ChildItem -LiteralPath $homeDir -Recurse -File | ForEach-Object { "$($_.FullName) $((Get-FileHash -LiteralPath $_.FullName).Hash)" }) -join "`n" + $problem = $null + switch ($Expect) { + { $_ -in "upgrade", "reinstall" } { + if ($exit -ne 0) { $problem = "exited $exit" } + elseif (@($out).Count -ne 1 -or @($out)[0] -ne $linked) { $problem = "printed '$out', not the Homebrew archdev" } + elseif ($brewCalls -notcontains "$Expect archastro/tools/archdev") { $problem = "brew was not asked to ${Expect}: $($brewCalls -join '; ')" } + elseif (-not $errorText.Contains("brew $Expect archastro/tools/archdev")) { $problem = "the brew command was not shown" } + elseif ((& $linked --version) -ne "0.60.0") { $problem = "the Homebrew keg was not updated" } + elseif ((Test-Path -LiteralPath $otherBrewLog) -and (Get-Item $otherBrewLog).Length -ne 0) { $problem = "ran another prefix's brew" } + } + { $_ -in "current", "copy" } { + $want = if ($_ -eq "copy") { Join-Path $homeDir ".local/bin/archdev" } else { $linked } + if ($exit -ne 0) { $problem = "exited $exit" } + elseif (@($out).Count -ne 1 -or @($out)[0] -ne $want) { $problem = "printed '$out', not $want" } + elseif ($brewCalls | Where-Object { $_ -match "^(upgrade|reinstall|install)" }) { $problem = "changed Homebrew: $($brewCalls -join '; ')" } + elseif ($Expect -eq "current" -and $Copy -ne "none" -and -not $errorText.Contains("comes first on PATH")) { $problem = "did not say which copy it skipped" } + } + "error" { + if ($exit -eq 0 -or @($out).Count -ne 0) { $problem = "continued without a working Homebrew copy" } + elseif (-not $errorText.Contains($ErrorText)) { $problem = "error does not contain '$ErrorText'" } + } + "installer" { + if ($exit -eq 0 -or -not $errorText.Contains("Blocked installer download")) { $problem = "did not use the pinned installer" } + elseif ($brewCalls | Where-Object { $_ -match "^(upgrade|reinstall|install)" }) { $problem = "changed Homebrew: $($brewCalls -join '; ')" } + } + } + if (-not $problem -and $Expect -ne "installer") { + if ($errorText.Contains("Blocked installer download")) { $problem = "downloaded the installer for a second copy" } + elseif ($homeAfter -ne $homeBefore) { $problem = "changed files under HOME" } + } + if (-not $problem -and (Get-Item $setupLog).Length -ne 0) { $problem = "wrote hooks" } + if ($problem) { + Write-Host "FAIL ${label}: $problem" + Write-Host $errorText + $script:failures++ + } else { Write-Host "ok $label" } +} + try { + if (-not $onWindows) { + foreach ($skill in @("archdev", "tasks", "tasks-execute")) { + Invoke-BrewCase "outdated" $skill -Keg old -Brew yes -Copy none -Expect upgrade + # The incident: a stub reporting 3.0.0 passed the version check and + # failed the capability probes, so `brew upgrade` would be a no-op. + Invoke-BrewCase "broken" $skill -Keg stub -Brew yes -Copy none -Expect reinstall + Invoke-BrewCase "crash" $skill -Keg crash -Brew yes -Copy none -Expect reinstall + Invoke-BrewCase "opt" $skill -Keg old -Brew yes -Copy none -Expect upgrade -Variant opt + Invoke-BrewCase "other-brew" $skill -Keg old -Brew yes -Copy none -Expect upgrade -Variant other-brew + Invoke-BrewCase "missing" $skill -Keg missing -Brew yes -Copy none -Expect reinstall + Invoke-BrewCase "behind-copy" $skill -Keg old -Brew yes -Copy old -Expect upgrade + Invoke-BrewCase "current-behind-copy" $skill -Keg current -Brew yes -Copy current -Expect current + Invoke-BrewCase "current" $skill -Keg current -Brew yes -Copy none -Expect current + Invoke-BrewCase "no-brew" $skill -Keg old -Brew no -Copy none -Expect error -ErrorText "is managed by Homebrew, but $(Join-Path $work "brew-$skill-no-brew/homebrew/bin/brew") is missing" + Invoke-BrewCase "brew-fails" $skill -Keg old -Brew fail -Copy none -Expect error -ErrorText "brew upgrade archastro/tools/archdev failed" + # Without a Homebrew keg, installer copies and fresh installs keep the + # pinned installer, and brew is never asked to install anything. + Invoke-BrewCase "installer-copy" $skill -Keg none -Brew yes -Copy old -Expect installer + Invoke-BrewCase "working-installer-copy" $skill -Keg none -Brew yes -Copy current -Expect copy + Invoke-BrewCase "nothing-installed-brew-yes" $skill -Keg none -Brew yes -Copy none -Expect installer + Invoke-BrewCase "nothing-installed-brew-no" $skill -Keg none -Brew no -Copy none -Expect installer + } + } foreach ($skill in @("archdev", "tasks", "tasks-execute")) { Invoke-Case "$skill-claude" "" @{ CLAUDECODE = "1" } $skill Invoke-Case "$skill-codex" "" @{ CODEX_THREAD_ID = "019a-thread" } $skill diff --git a/scripts/test-skill-bootstrap.sh b/scripts/test-skill-bootstrap.sh index a4df495..2ed8e49 100755 --- a/scripts/test-skill-bootstrap.sh +++ b/scripts/test-skill-bootstrap.sh @@ -76,5 +76,227 @@ for skill in archdev tasks tasks-execute; do done done +# Homebrew owns a copy linked from its Cellar. Bootstrap repairs that copy with +# brew and never installs a second one, which would shadow or be shadowed by it. +# Each case builds /Cellar/archdev//bin/archdev with a +# /bin/archdev link, as `brew install archastro/tools/archdev` leaves it. +fake_archdev_at() { + printf '#!/bin/sh\nARCHDEV_FAKE_VERSION=%s exec "%s" "$@"\n' "$2" "$repo/scripts/fake-archdev" >"$1" + chmod +x "$1" +} + +# brew_case +# [] +# An old keg is 0.49.0; every other keg is the formula's latest, 0.60.0, so a +# `brew upgrade` of it changes nothing, as with real Homebrew. +brew_case() { + local name="$1" skill="$2" keg="$3" brew="$4" copy="$5" variant="${6:-}" keg_version=0.60.0 keg_dir path + case_dir="$work/brew-$skill-$name" + prefix="$case_dir/homebrew" + mkdir -p "$case_dir/home" "$case_dir/bin" "$prefix/bin" + printf '#!/bin/sh\nprintf "Blocked installer download\\n" >&2\nexit 1\n' >"$case_dir/bin/curl" + chmod +x "$case_dir/bin/curl" + : >"$case_dir/setup.log" + : >"$case_dir/brew.log" + expected_path="$prefix/bin/archdev" + if [[ "$keg" != none ]]; then + [[ "$keg" == old ]] && keg_version=0.49.0 + keg_dir="$prefix/Cellar/archdev/$keg_version/bin" + mkdir -p "$keg_dir" "$prefix/opt" + case "$keg" in + old) fake_archdev_at "$keg_dir/archdev" 0.49.5 ;; + current) fake_archdev_at "$keg_dir/archdev" 0.49.6 ;; + # The stub that replaced a real keg: a high version, no commands. + stub) printf '#!/bin/sh\necho "archdev 3.0.0"\n' >"$keg_dir/archdev" && chmod +x "$keg_dir/archdev" ;; + # A binary that crashes before it can report a version. + crash) printf '#!/bin/sh\nexit 139\n' >"$keg_dir/archdev" && chmod +x "$keg_dir/archdev" ;; + missing) rm -rf "$prefix/Cellar/archdev" ;; + esac + ln -s "../Cellar/archdev/$keg_version/bin/archdev" "$prefix/bin/archdev" + ln -s "../Cellar/archdev/$keg_version" "$prefix/opt/archdev" + fi + [[ "$brew" == no ]] || cp "$repo/scripts/fake-brew" "$prefix/bin/brew" + path="$prefix/bin:$case_dir/bin:/usr/bin:/bin" + case "$variant" in + # PATH reaches the keg through Homebrew's opt link, which survives upgrades. + opt) + path="$prefix/opt/archdev/bin:$path" + expected_path="$prefix/opt/archdev/bin/archdev" + ;; + # Another Homebrew prefix's brew comes first on PATH; it must not be used. + other-brew) + mkdir -p "$case_dir/other-homebrew/bin" + printf '#!/bin/sh\necho "$*" >>"%s"\necho %s\n' "$case_dir/other-brew.log" "$case_dir/other-homebrew" >"$case_dir/other-homebrew/bin/brew" + chmod +x "$case_dir/other-homebrew/bin/brew" + : >"$case_dir/other-brew.log" + path="$case_dir/other-homebrew/bin:$path" + ;; + esac + if [[ "$copy" != none ]]; then + # A previous installer copy that comes first on PATH. + mkdir -p "$case_dir/home/.local/bin" + fake_archdev_at "$case_dir/home/.local/bin/archdev" "$([[ "$copy" == old ]] && echo 0.49.5 || echo 0.49.6)" + path="$case_dir/home/.local/bin:$path" + fi + home_files >"$case_dir/home.before" + set +e + env -i HOME="$case_dir/home" TMPDIR="$case_dir" PATH="$path" \ + ARCHDEV_FAKE_LOG="$case_dir/setup.log" ARCHDEV_FAKE_BIN="$repo/scripts/fake-archdev" \ + FAKE_BREW_LOG="$case_dir/brew.log" FAKE_BREW_PREFIX="$prefix" \ + FAKE_BREW_EXIT="$([[ "$brew" == fail ]] && echo 1 || echo 0)" \ + bash "$repo/$skill/scripts/bootstrap.sh" >"$case_dir/out" 2>"$case_dir/stderr" + case_exit=$? + set -e +} + +home_files() { + find "$case_dir/home" -type f -exec cksum {} + | sort +} + +brew_fail() { + printf 'FAIL brew %s: %s\n' "$case_dir" "$1" >&2 + sed 's/^/ stderr: /' "$case_dir/stderr" >&2 + failures=$((failures + 1)) +} + +# Brew repaired the keg in place: the path on stdout is the Homebrew link, +# now reporting the upgraded version, and no other copy was downloaded. +expect_brew_repaired() { + local action="$1" + if ((case_exit != 0)); then + brew_fail "exited $case_exit" + elif [[ "$(cat "$case_dir/out")" != "$expected_path" ]]; then + brew_fail "printed '$(cat "$case_dir/out")', not $expected_path" + elif ! grep -Fx "$action archastro/tools/archdev" "$case_dir/brew.log" >/dev/null; then + brew_fail "brew was not asked to $action: $(tr '\n' ';' <"$case_dir/brew.log")" + elif ! grep -F "brew $action archastro/tools/archdev" "$case_dir/stderr" >/dev/null; then + brew_fail "the brew command was not shown" + elif [[ "$("$expected_path" --version)" != 0.60.0 ]]; then + brew_fail "the Homebrew keg was not updated" + else + return 0 + fi + return 1 +} + +expect_no_second_copy() { + if grep -F 'Blocked installer download' "$case_dir/stderr" >/dev/null; then + brew_fail "downloaded the installer for a second copy" + elif [[ "$(home_files)" != "$(cat "$case_dir/home.before")" ]]; then + brew_fail "changed files under HOME" + elif [[ -s "$case_dir/setup.log" ]]; then + brew_fail "wrote hooks" + else + return 0 + fi + return 1 +} + +expect_brew_untouched() { + if grep -E '^(upgrade|reinstall|install)' "$case_dir/brew.log" >/dev/null; then + brew_fail "changed Homebrew: $(tr '\n' ';' <"$case_dir/brew.log")" + return 1 + fi +} + +for skill in archdev tasks tasks-execute; do + brew_case outdated "$skill" old yes none + expect_brew_repaired upgrade && expect_no_second_copy && + printf 'ok %s: outdated Homebrew copy upgraded with brew, no second copy\n' "$skill" + + # The incident: a stub reporting 3.0.0 passed the version check and failed + # the capability probes, so `brew upgrade` would have been a no-op. + brew_case broken "$skill" stub yes none + expect_brew_repaired reinstall && expect_no_second_copy && + printf 'ok %s: broken current Homebrew copy reinstalled with brew, no second copy\n' "$skill" + + brew_case crash "$skill" crash yes none + expect_brew_repaired reinstall && expect_no_second_copy && + printf 'ok %s: crashing current Homebrew copy reinstalled with brew, no second copy\n' "$skill" + + brew_case opt "$skill" old yes none opt + expect_brew_repaired upgrade && expect_no_second_copy && + printf 'ok %s: Homebrew copy on PATH through its opt link upgraded, opt path kept\n' "$skill" + + brew_case other-brew "$skill" old yes none other-brew + if expect_brew_repaired upgrade && expect_no_second_copy; then + if [[ -s "$case_dir/other-brew.log" ]]; then + brew_fail "ran another prefix's brew: $(tr '\n' ';' <"$case_dir/other-brew.log")" + else + printf 'ok %s: the keg'"'"'s own brew upgrades it, not the brew first on PATH\n' "$skill" + fi + fi + + brew_case missing "$skill" missing yes none + expect_brew_repaired reinstall && expect_no_second_copy && + printf 'ok %s: dangling Homebrew link reinstalled with brew, no second copy\n' "$skill" + + brew_case behind-copy "$skill" old yes old + expect_brew_repaired upgrade && expect_no_second_copy && + printf 'ok %s: Homebrew copy behind an installer copy on PATH is preferred and upgraded\n' "$skill" + + brew_case current-behind-copy "$skill" current yes current + if ((case_exit != 0)) || [[ "$(cat "$case_dir/out")" != "$prefix/bin/archdev" ]]; then + brew_fail "did not prefer the current Homebrew copy" + elif ! grep -F 'comes first on PATH' "$case_dir/stderr" >/dev/null; then + brew_fail "did not say which copy it skipped" + elif expect_brew_untouched && expect_no_second_copy; then + printf 'ok %s: current Homebrew copy behind an installer copy is used as is\n' "$skill" + fi + + brew_case current "$skill" current yes none + if ((case_exit != 0)) || [[ "$(cat "$case_dir/out")" != "$prefix/bin/archdev" ]]; then + brew_fail "did not use the current Homebrew copy" + elif [[ -s "$case_dir/brew.log" ]]; then + brew_fail "ran brew for a current copy: $(tr '\n' ';' <"$case_dir/brew.log")" + elif expect_no_second_copy; then + printf 'ok %s: current Homebrew copy used without running brew\n' "$skill" + fi + + brew_case no-brew "$skill" old no none + if ((case_exit == 0)) || [[ -s "$case_dir/out" ]]; then + brew_fail "accepted an outdated Homebrew copy without brew" + elif ! grep -F "$prefix/bin/archdev is managed by Homebrew, but $prefix/bin/brew is missing" "$case_dir/stderr" >/dev/null; then + brew_fail "error does not name the Homebrew-managed binary" + elif expect_no_second_copy; then + printf 'ok %s: outdated Homebrew copy without brew stops with an error, no second copy\n' "$skill" + fi + + brew_case brew-fails "$skill" old fail none + if ((case_exit == 0)) || [[ -s "$case_dir/out" ]]; then + brew_fail "continued after brew failed" + elif ! grep -F "brew upgrade archastro/tools/archdev failed. ArchDev at $prefix/bin/archdev is managed by Homebrew" "$case_dir/stderr" >/dev/null; then + brew_fail "error does not name the failed brew command and binary" + elif expect_no_second_copy; then + printf 'ok %s: failed brew upgrade stops with an error, no second copy\n' "$skill" + fi + + # Without a Homebrew keg, an installer copy keeps today's path: the pinned + # installer (blocked here) replaces it, and brew is never asked to install. + brew_case installer-copy "$skill" none yes old + if ((case_exit == 0)) || ! grep -F 'Blocked installer download' "$case_dir/stderr" >/dev/null; then + brew_fail "did not update the outdated installer copy with the pinned installer" + elif expect_brew_untouched; then + printf 'ok %s: outdated installer copy still uses the pinned installer\n' "$skill" + fi + + # A working installer copy with brew but no keg is used as is. + brew_case working-installer-copy "$skill" none yes current + if ((case_exit != 0)) || [[ "$(cat "$case_dir/out")" != "$case_dir/home/.local/bin/archdev" ]]; then + brew_fail "did not use the working installer copy" + elif expect_brew_untouched && expect_no_second_copy; then + printf 'ok %s: working installer copy used as is when brew has no keg\n' "$skill" + fi + + for brew in yes no; do + brew_case "nothing-installed-brew-$brew" "$skill" none "$brew" none + if ((case_exit == 0)) || ! grep -F 'Blocked installer download' "$case_dir/stderr" >/dev/null; then + brew_fail "a fresh install did not use the pinned installer" + elif expect_brew_untouched; then + printf 'ok %s: no archdev (brew on PATH: %s) still uses the pinned installer\n' "$skill" "$brew" + fi + done +done + ((failures == 0)) || exit 1 printf 'All bootstrap cases passed\n' diff --git a/tasks-execute/scripts/bootstrap.ps1 b/tasks-execute/scripts/bootstrap.ps1 index 8840aa5..f2a8a53 100644 --- a/tasks-execute/scripts/bootstrap.ps1 +++ b/tasks-execute/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } $heartbeatHelp = & $Binary tasks heartbeat --help 2>$null @@ -47,17 +52,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/tasks-execute/scripts/bootstrap.sh b/tasks-execute/scripts/bootstrap.sh index cac5be7..30378df 100755 --- a/tasks-execute/scripts/bootstrap.sh +++ b/tasks-execute/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,20 +59,16 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # Executing a task needs a lease heartbeat the agent can run in the background. "$1" tasks heartbeat --help 2>/dev/null | grep -F 'Usage: archdev tasks heartbeat ' >/dev/null && @@ -79,19 +76,122 @@ supports_tasks() { "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: task execution requires 0.49.6+, tasks heartbeat, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks tasks heartbeat, Tasks web review commands, or consent-safe repository hook support.\n' >&2 exit 1 } diff --git a/tasks/scripts/bootstrap.ps1 b/tasks/scripts/bootstrap.ps1 index ac285bf..eeab898 100644 --- a/tasks/scripts/bootstrap.ps1 +++ b/tasks/scripts/bootstrap.ps1 @@ -30,14 +30,19 @@ function Install-ArchDev { return (Resolve-ArchDevPath (Join-Path $installDir "archdev.exe")) } -$existing = Get-Command archdev -ErrorAction SilentlyContinue -$archdev = if ($existing) { Resolve-ArchDevPath $existing.Source } else { Install-ArchDev } +$minVersion = [Version]"0.49.6" -function Test-Tasks([string]$Binary) { +function Test-Version([string]$Binary) { $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" try { - if ([Version]$raw -lt [Version]"0.49.6") { return $false } - } catch { return $false } + return ([Version]$raw -ge $minVersion) + } catch { + return $false + } +} + +function Test-Tasks([string]$Binary) { + if (-not (Test-Version $Binary)) { return $false } $helpText = & $Binary tasks review update --help 2>$null if ($LASTEXITCODE -ne 0 -or (($helpText -join "`n") -notmatch "(?m)^Usage: archdev tasks review update ")) { return $false } # This capability marks the release with consent-safe hook self-heal. @@ -45,17 +50,125 @@ function Test-Tasks([string]$Binary) { return ($LASTEXITCODE -eq 0 -and (($hookHelp -join "`n") -match "--local")) } -if (-not (Test-Tasks $archdev)) { - [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") - $archdev = Install-ArchDev +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +# Homebrew exists only on macOS and Linux; Windows keeps the installer path. +$brewFormula = "archastro/tools/archdev" + +# Resolves every symlink in the path, including directory links such as +# Homebrew's opt link. A dangling keg link still yields its Cellar path. +function Get-RealPath([string]$Path) { + $pending = [Collections.Generic.List[string]]::new() + $pending.AddRange([string[]]@([IO.Path]::GetFullPath($Path).Split("/") | Where-Object { $_ })) + $real = "" + $hops = 0 + while ($pending.Count -gt 0) { + $part = $pending[0] + $pending.RemoveAt(0) + if ($part -eq ".") { continue } + if ($part -eq "..") { + $real = $real.Substring(0, [Math]::Max(0, $real.LastIndexOf("/"))) + continue + } + $next = "$real/$part" + $item = Get-Item -LiteralPath $next -Force -ErrorAction SilentlyContinue + if ($item -and $item.LinkTarget -and $hops -lt 32) { + $hops++ + if ($item.LinkTarget.StartsWith("/")) { $real = "" } + $pending.InsertRange(0, [string[]]@($item.LinkTarget.Split("/") | Where-Object { $_ })) + continue + } + $real = $next + } + return $real +} + +# Test-Path accepts a dangling link; a removed keg leaves one behind. +function Test-LinkedFile([string]$Path) { + return (Test-Path -LiteralPath (Get-RealPath $Path) -PathType Leaf) +} + +function Test-HomebrewArchDev([string]$Path) { + return ((Get-RealPath $Path) -like "*/Cellar/archdev/*") +} + +function Get-Brew { + return (Get-Command brew -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1) } +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +function Find-HomebrewArchDev($Candidate) { + if ($env:OS -eq "Windows_NT") { return $null } + if ($Candidate -and (Test-HomebrewArchDev $Candidate)) { return $Candidate } + $brew = Get-Brew + if (-not $brew) { return $null } + $prefix = @(& $brew.Source --prefix 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -ne 0 -or -not $prefix) { return $null } + $linked = Join-Path $prefix "bin/archdev" + if ((Get-Item -LiteralPath $linked -Force -ErrorAction SilentlyContinue) -and (Test-HomebrewArchDev $linked)) { + return $linked + } + return $null +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +function Update-HomebrewArchDev([string]$Binary) { + # The brew that owns this keg; another prefix's brew could install a second copy. + $real = Get-RealPath $Binary + $brew = $real.Substring(0, $real.IndexOf("/Cellar/archdev/")) + "/bin/brew" + if (-not (Test-Path -LiteralPath $brew -PathType Leaf)) { + throw "ArchDev at $Binary is managed by Homebrew, but $brew is missing. Run: brew upgrade $brewFormula. Not installing a second copy of ArchDev." + } + $version = $null + if (Test-LinkedFile $Binary) { + $raw = (& $Binary --version 2>$null | Select-Object -First 1) -replace "[^0-9.]", "" + try { $version = [Version]$raw } catch { $version = $null } + } + # Only a readable, too-old version needs an upgrade; anything else is broken. + $action = if ($version -and $version -lt $minVersion) { "upgrade" } else { "reinstall" } + [Console]::Error.WriteLine("Updating Homebrew-managed ArchDev at ${Binary}: brew $action $brewFormula") + # Callers read stdout as the archdev path, so brew's progress goes to stderr. + & $brew $action $brewFormula | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + throw "brew $action $brewFormula failed. ArchDev at $Binary is managed by Homebrew; not installing a second copy." + } +} + +$existing = Get-Command archdev -ErrorAction SilentlyContinue +$candidate = if ($existing) { Resolve-ArchDevPath $existing.Source } else { $null } +$homebrewArchDev = Find-HomebrewArchDev $candidate +if ($homebrewArchDev) { + $archdev = $homebrewArchDev + if ($candidate -and $candidate -ne $archdev) { + [Console]::Error.WriteLine("Using Homebrew-managed ArchDev at $archdev; $candidate comes first on PATH.") + } + if (-not (Test-LinkedFile $archdev) -or -not (Test-Tasks $archdev)) { + Update-HomebrewArchDev $archdev + } +} else { + $archdev = if ($candidate) { $candidate } else { Install-ArchDev } + if (-not (Test-Tasks $archdev)) { + [Console]::Error.WriteLine("Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.") + $archdev = Install-ArchDev + } +} + +if ($homebrewArchDev -and -not (Test-LinkedFile $archdev)) { + throw "Homebrew did not leave an executable at $archdev; not installing a second copy." +} if (-not (Test-Path -LiteralPath $archdev -PathType Leaf)) { throw "ArchDev installer did not create an executable at $archdev" } & $archdev --version *> $null if ($LASTEXITCODE -ne 0) { throw "ArchDev version verification failed" } -if (-not (Test-Tasks $archdev)) { throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" } +if (-not (Test-Tasks $archdev)) { + if ($homebrewArchDev) { throw "Homebrew-managed ArchDev at $archdev still fails the capability check after brew; not installing a second copy." } + throw "Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support on 0.49.6+" +} # Tasks executable resolution does not authorize changing hook configuration. Write-Output $archdev diff --git a/tasks/scripts/bootstrap.sh b/tasks/scripts/bootstrap.sh index 32f4343..fddc381 100755 --- a/tasks/scripts/bootstrap.sh +++ b/tasks/scripts/bootstrap.sh @@ -6,6 +6,7 @@ installer_revision="a530d21dd04eaed5742b8ad7daf9140a3ee2ef0f" installer_url="https://raw.githubusercontent.com/ArchAstro/archdev/${installer_revision}/install.sh" installer_sha256="f838ca858f7db526cdf2a2697001ae1d404edf3df93617e525efa9d171be7e64" install_dir="${ARCHDEV_INSTALL_DIR:-$HOME/.local/bin}" +min_version="0.49.6" absolute_path() { local candidate="$1" @@ -58,38 +59,137 @@ install_archdev() ( fi ) -candidate="$(command -v archdev 2>/dev/null || true)" -if [[ -n "$candidate" ]]; then - executable="$(absolute_path "$candidate")" -else - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" -fi - -supports_tasks() { +version_ok() { local raw version raw="$("$1" --version 2>/dev/null | head -n 1)" version="$(printf '%s' "$raw" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" [[ -n "$version" ]] || return 1 - [[ "$(printf '%s\n%s\n' 0.49.6 "$version" | sort -V | head -n 1)" == 0.49.6 ]] && + [[ "$(printf '%s\n%s\n' "$min_version" "$version" | sort -V | head -n 1)" == "$min_version" ]] +} + +supports_tasks() { + version_ok "$1" && "$1" tasks review update --help 2>/dev/null | grep -F 'Usage: archdev tasks review update ' >/dev/null && # This capability marks the release with consent-safe hook self-heal. "$1" repo hook setup --help 2>/dev/null | grep -F -- '--local' >/dev/null } -if ! supports_tasks "$executable"; then - printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 - install_archdev || exit 1 - executable="$(absolute_path "$install_dir/archdev")" +# A Homebrew install owns its copy. Installing another one elsewhere leaves two +# binaries that are upgraded separately, and whichever comes first on PATH wins. +brew_formula="archastro/tools/archdev" + +# Follows symlinks one hop at a time; macOS before 12.3 has no readlink -f. +real_path() { + local path="$1" link hops=0 directory + while [[ -L "$path" && "$hops" -lt 32 ]]; do + link="$(readlink "$path")" || break + case "$link" in + /*) path="$link" ;; + *) path="$(dirname "$path")/$link" ;; + esac + hops=$((hops + 1)) + done + # A dangling keg link still names its Cellar path. + directory="$(cd -P "$(dirname "$path")" 2>/dev/null && pwd)" || { + printf '%s\n' "$path" + return 0 + } + printf '%s/%s\n' "$directory" "$(basename "$path")" +} + +is_homebrew_archdev() { + case "$(real_path "$1")" in + */Cellar/archdev/*) return 0 ;; + esac + return 1 +} + +# The Homebrew-managed archdev on PATH, else /bin/archdev when +# Homebrew links it there, even if another copy comes first on PATH. +find_homebrew_archdev() { + local candidate="$1" prefix + if [[ -n "$candidate" ]] && is_homebrew_archdev "$candidate"; then + # Keep the link path: an upgrade removes the versioned keg behind it. + printf '%s/%s\n' "$(cd "$(dirname "$candidate")" && pwd -L)" "$(basename "$candidate")" + return 0 + fi + command -v brew >/dev/null 2>&1 || return 1 + prefix="$(brew --prefix 2>/dev/null)" || return 1 + [[ -n "$prefix" ]] || return 1 + if [[ -e "$prefix/bin/archdev" || -L "$prefix/bin/archdev" ]] && + is_homebrew_archdev "$prefix/bin/archdev"; then + printf '%s/bin/archdev\n' "$prefix" + return 0 + fi + return 1 +} + +# Repairs a Homebrew-managed archdev with Homebrew rather than a second copy: +# upgrade when it reports a too-old version; reinstall when the binary is +# missing, crashes, or a current version fails the capability probes, where +# upgrade would be a no-op. +update_homebrew_archdev() { + local binary="$1" action=reinstall brew version="" + # The brew that owns this keg; another prefix's brew could install a second copy. + brew="$(real_path "$binary")" + brew="${brew%%/Cellar/archdev/*}/bin/brew" + if [[ ! -x "$brew" ]]; then + printf 'ArchDev at %s is managed by Homebrew, but %s is missing. Run: brew upgrade %s\n' "$binary" "$brew" "$brew_formula" >&2 + printf 'Not installing a second copy of ArchDev.\n' >&2 + return 1 + fi + if [[ -x "$binary" ]]; then + version="$("$binary" --version 2>/dev/null | head -n 1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + fi + # Only a readable, too-old version needs an upgrade; anything else is broken. + if [[ -n "$version" ]] && ! version_ok "$binary"; then + action=upgrade + fi + printf 'Updating Homebrew-managed ArchDev at %s: brew %s %s\n' "$binary" "$action" "$brew_formula" >&2 + if ! "$brew" "$action" "$brew_formula" >&2; then + printf 'brew %s %s failed. ArchDev at %s is managed by Homebrew; not installing a second copy.\n' "$action" "$brew_formula" "$binary" >&2 + return 1 + fi +} + +candidate="$(command -v archdev 2>/dev/null || true)" +homebrew_archdev="$(find_homebrew_archdev "$candidate" || true)" +if [[ -n "$homebrew_archdev" ]]; then + executable="$homebrew_archdev" + if [[ -n "$candidate" && "$(absolute_path "$candidate")" != "$executable" ]]; then + printf 'Using Homebrew-managed ArchDev at %s; %s comes first on PATH.\n' "$executable" "$(absolute_path "$candidate")" >&2 + fi + if ! supports_tasks "$executable"; then + update_homebrew_archdev "$executable" || exit 1 + fi +else + if [[ -n "$candidate" ]]; then + executable="$(absolute_path "$candidate")" + else + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi + if ! supports_tasks "$executable"; then + printf 'Updating ArchDev: Tasks requires 0.49.6+, web review commands, and consent-safe repository hook support.\n' >&2 + install_archdev || exit 1 + executable="$(absolute_path "$install_dir/archdev")" + fi fi [[ -x "$executable" ]] || { - printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew did not leave an executable at %s; not installing a second copy.\n' "$executable" >&2 + else + printf 'ArchDev installer did not create an executable at %s\n' "$executable" >&2 + fi exit 1 } "$executable" --version >&2 supports_tasks "$executable" || { + if [[ -n "$homebrew_archdev" ]]; then + printf 'Homebrew-managed ArchDev at %s still fails the capability check after brew; not installing a second copy.\n' "$executable" >&2 + fi printf 'Installed ArchDev lacks Tasks web review commands or consent-safe repository hook support.\n' >&2 exit 1 }