fix: fall back to ZIP download when git clone fails on Windows

Git for Windows can completely fail to write files during clone due to
antivirus software, Windows Defender Controlled Folder Access, or NTFS
filter drivers. Even with windows.appendAtomically=false, the checkout
phase fails with 'unable to create file: Invalid argument'.

New install strategy (3 attempts):
1. git clone with -c windows.appendAtomically=false (SSH then HTTPS)
2. If clone fails: download GitHub ZIP archive, extract with
   Expand-Archive (Windows native, no git file I/O), then git init
   the result for future updates
3. All git commands now use -c flag to inject the atomic write fix

Also passes -c flag on update path (fetch/checkout/pull) and makes
submodule init failure non-fatal with a warning.
This commit is contained in:
teknium1 2026-03-02 22:53:28 -08:00
parent 354af6ccee
commit 4766b3cdb9

View file

@ -416,9 +416,9 @@ function Install-Repository {
if (Test-Path "$InstallDir\.git") { if (Test-Path "$InstallDir\.git") {
Write-Info "Existing installation found, updating..." Write-Info "Existing installation found, updating..."
Push-Location $InstallDir Push-Location $InstallDir
git fetch origin git -c windows.appendAtomically=false fetch origin
git checkout $Branch git -c windows.appendAtomically=false checkout $Branch
git pull origin $Branch git -c windows.appendAtomically=false pull origin $Branch
Pop-Location Pop-Location
} else { } else {
Write-Err "Directory exists but is not a git repository: $InstallDir" Write-Err "Directory exists but is not a git repository: $InstallDir"
@ -426,73 +426,93 @@ function Install-Repository {
throw "Directory exists but is not a git repository: $InstallDir" throw "Directory exists but is not a git repository: $InstallDir"
} }
} else { } else {
$cloneSuccess = $false
# Fix Windows git "copy-fd: write returned: Invalid argument" error. # Fix Windows git "copy-fd: write returned: Invalid argument" error.
# Git for Windows can fail on atomic file operations (hook templates, # Git for Windows can fail on atomic file operations (hook templates,
# config lock files) due to antivirus, OneDrive, or NTFS filter drivers. # config lock files) due to antivirus, OneDrive, or NTFS filter drivers.
# Setting windows.appendAtomically=false via ENVIRONMENT bypasses the # The -c flag injects config before any file I/O occurs.
# issue entirely — git reads these before touching any files, unlike
# --global config which itself may fail to write.
Write-Info "Configuring git for Windows compatibility..." Write-Info "Configuring git for Windows compatibility..."
$env:GIT_CONFIG_COUNT = "1" $env:GIT_CONFIG_COUNT = "1"
$env:GIT_CONFIG_KEY_0 = "windows.appendAtomically" $env:GIT_CONFIG_KEY_0 = "windows.appendAtomically"
$env:GIT_CONFIG_VALUE_0 = "false" $env:GIT_CONFIG_VALUE_0 = "false"
# Also try global config (may fail but harmless)
git config --global windows.appendAtomically false 2>$null git config --global windows.appendAtomically false 2>$null
# Try SSH first (for private repo access), fall back to HTTPS. # Try SSH first, then HTTPS, with -c flag for atomic write fix
# GIT_SSH_COMMAND with BatchMode=yes prevents SSH from hanging
# when no key is configured (fails immediately instead of prompting).
#
# IMPORTANT: Do NOT use 2>&1 on git commands in PowerShell.
# With $ErrorActionPreference = "Stop", PowerShell wraps captured
# stderr lines in ErrorRecord objects, turning git's normal progress
# messages ("Cloning into ...") into terminating NativeCommandErrors.
# Let stderr flow to the console naturally (like OpenClaw does).
Write-Info "Trying SSH clone..." Write-Info "Trying SSH clone..."
$env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5" $env:GIT_SSH_COMMAND = "ssh -o BatchMode=yes -o ConnectTimeout=5"
try { try {
git clone --branch $Branch --recurse-submodules $RepoUrlSsh $InstallDir git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlSsh $InstallDir
$sshExitCode = $LASTEXITCODE if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
} catch { } catch { }
$sshExitCode = 1
}
$env:GIT_SSH_COMMAND = $null $env:GIT_SSH_COMMAND = $null
if ($sshExitCode -eq 0) { if (-not $cloneSuccess) {
Write-Success "Cloned via SSH"
} else {
# Clean up partial SSH clone before retrying
if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
Write-Info "SSH failed, trying HTTPS..." Write-Info "SSH failed, trying HTTPS..."
git clone --branch $Branch --recurse-submodules $RepoUrlHttps $InstallDir try {
git -c windows.appendAtomically=false clone --branch $Branch --recurse-submodules $RepoUrlHttps $InstallDir
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) { $cloneSuccess = $true }
Write-Success "Cloned via HTTPS" } catch { }
} else { }
# Last resort: skip hook templates entirely (they're optional sample files)
if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } # Fallback: download ZIP archive (bypasses git file I/O issues entirely)
Write-Warn "Standard clone failed, retrying without hook templates..." if (-not $cloneSuccess) {
git clone --branch $Branch --recurse-submodules --template="" $RepoUrlHttps $InstallDir if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue }
Write-Warn "Git clone failed — downloading ZIP archive instead..."
try {
$zipUrl = "https://github.com/NousResearch/hermes-agent/archive/refs/heads/$Branch.zip"
$zipPath = "$env:TEMP\hermes-agent-$Branch.zip"
$extractPath = "$env:TEMP\hermes-agent-extract"
if ($LASTEXITCODE -eq 0) { Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
Write-Success "Cloned via HTTPS (no templates)" if (Test-Path $extractPath) { Remove-Item -Recurse -Force $extractPath }
} else { Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force
Write-Err "Failed to clone repository"
throw "Failed to clone repository" # GitHub ZIPs extract to repo-branch/ subdirectory
$extractedDir = Get-ChildItem $extractPath -Directory | Select-Object -First 1
if ($extractedDir) {
New-Item -ItemType Directory -Force -Path (Split-Path $InstallDir) -ErrorAction SilentlyContinue | Out-Null
Move-Item $extractedDir.FullName $InstallDir -Force
Write-Success "Downloaded and extracted"
# Initialize git repo so updates work later
Push-Location $InstallDir
git -c windows.appendAtomically=false init 2>$null
git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
git remote add origin $RepoUrlHttps 2>$null
Pop-Location
Write-Success "Git repo initialized for future updates"
$cloneSuccess = $true
} }
# Cleanup temp files
Remove-Item -Force $zipPath -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $extractPath -ErrorAction SilentlyContinue
} catch {
Write-Err "ZIP download also failed: $_"
} }
} }
if (-not $cloneSuccess) {
throw "Failed to download repository (tried git clone SSH, HTTPS, and ZIP)"
}
} }
# Also set per-repo (in case global wasn't persisted) # Set per-repo config (harmless if it fails)
Push-Location $InstallDir Push-Location $InstallDir
git config windows.appendAtomically false git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
# Ensure submodules are initialized and updated # Ensure submodules are initialized and updated
Write-Info "Initializing submodules (mini-swe-agent, tinker-atropos)..." Write-Info "Initializing submodules (mini-swe-agent, tinker-atropos)..."
git submodule update --init --recursive git -c windows.appendAtomically=false submodule update --init --recursive 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Warn "Submodule init failed (terminal/RL tools may need manual setup)"
} else {
Write-Success "Submodules ready"
}
Pop-Location Pop-Location
Write-Success "Submodules ready"
Write-Success "Repository ready" Write-Success "Repository ready"
} }