diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000..3264b00 --- /dev/null +++ b/install.cmd @@ -0,0 +1,256 @@ +@echo off +setlocal enabledelayedexpansion + +REM Claude Code Windows CMD Bootstrap Script +REM Installs Claude Code for environments where PowerShell is not available + +REM Parse command line argument +set "TARGET=%~1" +if "!TARGET!"=="" set "TARGET=latest" + +REM Validate target parameter +if /i "!TARGET!"=="stable" goto :target_valid +if /i "!TARGET!"=="latest" goto :target_valid +echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul +if !ERRORLEVEL! equ 0 goto :target_valid + +echo Usage: %0 [stable^|latest^|VERSION] >&2 +echo Example: %0 1.0.58 >&2 +exit /b 1 + +:target_valid + +REM Check for 64-bit Windows +if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid + +echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2 +exit /b 1 + +:arch_valid + +REM Set constants +set "DOWNLOAD_BASE_URL=https://downloads.claude.ai/claude-code-releases" +set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads" +REM Use native ARM64 binary on ARM64 Windows, x64 otherwise +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "PLATFORM=win32-arm64" +) else ( + set "PLATFORM=win32-x64" +) + +REM Create download directory +if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!" + +REM Check for curl availability +curl --version >nul 2>&1 +if !ERRORLEVEL! neq 0 ( + echo curl is required but not available. Please install curl or use PowerShell installer. >&2 + exit /b 1 +) + +REM Always download latest version (which has the most up-to-date installer) +call :download_file "!DOWNLOAD_BASE_URL!/latest" "!DOWNLOAD_DIR!\latest" +if !ERRORLEVEL! neq 0 ( + echo Failed to get latest version >&2 + exit /b 1 +) + +REM Reject non-version content (e.g. an HTML error page) before it reaches the manifest URL +findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" "!DOWNLOAD_DIR!\latest" >nul +if !ERRORLEVEL! neq 0 ( + del "!DOWNLOAD_DIR!\latest" + echo Failed to get a valid version from downloads.claude.ai - got unexpected content. >&2 + echo This can happen if the download service is unreachable or not available in your region - see https://www.anthropic.com/supported-countries >&2 + exit /b 1 +) + +REM Read version from file +set /p VERSION=<"!DOWNLOAD_DIR!\latest" +del "!DOWNLOAD_DIR!\latest" + +REM Download manifest +call :download_file "!DOWNLOAD_BASE_URL!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json" +if !ERRORLEVEL! neq 0 ( + echo Failed to get manifest >&2 + exit /b 1 +) + +REM Extract checksum from manifest +call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!" +if !ERRORLEVEL! neq 0 ( + echo Platform !PLATFORM! not found in manifest >&2 + del "!DOWNLOAD_DIR!\manifest.json" 2>nul + exit /b 1 +) +del "!DOWNLOAD_DIR!\manifest.json" + +REM Download binary +set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe" +call :download_file "!DOWNLOAD_BASE_URL!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!" +if !ERRORLEVEL! neq 0 ( + echo Failed to download binary >&2 + if exist "!BINARY_PATH!" del "!BINARY_PATH!" + exit /b 1 +) + +REM Verify checksum +call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!" +if !ERRORLEVEL! neq 0 ( + echo Checksum verification failed >&2 + del "!BINARY_PATH!" + exit /b 1 +) + +REM Run claude install to set up launcher and shell integration +echo Setting up Claude Code... +"!BINARY_PATH!" install "!TARGET!" +set "INSTALL_RESULT=!ERRORLEVEL!" + +REM Clean up downloaded file +REM Wait a moment for any file handles to be released +timeout /t 1 /nobreak >nul 2>&1 +del /f "!BINARY_PATH!" >nul 2>&1 +if exist "!BINARY_PATH!" ( + echo Warning: Could not remove temporary file: !BINARY_PATH! +) + +if !INSTALL_RESULT! neq 0 ( + echo Installation failed >&2 + exit /b 1 +) + +echo. +echo Installation complete^^! +echo. +exit /b 0 + +REM ============================================================================ +REM SUBROUTINES +REM ============================================================================ + +:download_file +REM Downloads a file using curl +REM Args: %1=URL, %2=OutputPath +set "URL=%~1" +set "OUTPUT=%~2" + +REM Capture stderr so a certificate-revocation failure can be detected below +curl -fsSL "!URL!" -o "!OUTPUT!" 2>"!OUTPUT!.stderr" +set "CURL_RESULT=!ERRORLEVEL!" +if !CURL_RESULT! equ 0 goto :download_file_done + +REM Some networks (commonly corporate ones) block the endpoints Windows uses +REM to check whether a TLS certificate has been revoked. curl's schannel +REM backend then fails with CRYPT_E_NO_REVOCATION_CHECK (0x80092012) or +REM CRYPT_E_REVOCATION_OFFLINE (0x80092013). Retry once with best-effort +REM revocation checking - the soft-fail behavior browsers use: an unreachable +REM revocation server is tolerated, but a certificate that is known to be +REM revoked is still rejected. +findstr /i /c:"0x80092012" /c:"0x80092013" "!OUTPUT!.stderr" >nul +if !ERRORLEVEL! neq 0 ( + type "!OUTPUT!.stderr" >&2 + goto :download_file_done +) + +echo This network appears to block certificate revocation checks. Retrying the download with best-effort revocation checking... >&2 +curl -fsSL --ssl-revoke-best-effort "!URL!" -o "!OUTPUT!" +set "CURL_RESULT=!ERRORLEVEL!" +REM If the retry also failed, re-emit the original error so the actionable +REM schannel diagnostic is not lost +if !CURL_RESULT! neq 0 type "!OUTPUT!.stderr" >&2 + +:download_file_done +del "!OUTPUT!.stderr" >nul 2>&1 +exit /b !CURL_RESULT! + +:parse_manifest +REM Parse JSON manifest to extract checksum for platform +REM Args: %1=ManifestPath, %2=Platform +set "MANIFEST_PATH=%~1" +set "PLATFORM_NAME=%~2" +set "EXPECTED_CHECKSUM=" + +REM Use findstr to find platform section, then look for checksum +set "FOUND_PLATFORM=" +set "IN_PLATFORM_SECTION=" + +REM Read the manifest line by line +for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do ( + set "LINE=%%i" + + REM Check if this line contains our platform + echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul + if !ERRORLEVEL! equ 0 ( + set "IN_PLATFORM_SECTION=1" + ) + + REM If we're in the platform section, look for checksum + if defined IN_PLATFORM_SECTION ( + echo !LINE! | findstr /c:"\"checksum\":" >nul + if !ERRORLEVEL! equ 0 ( + REM Extract checksum value + for /f "tokens=2 delims=:" %%j in ("!LINE!") do ( + set "CHECKSUM_PART=%%j" + REM Remove quotes, whitespace, and comma + set "CHECKSUM_PART=!CHECKSUM_PART: =!" + set "CHECKSUM_PART=!CHECKSUM_PART:"=!" + set "CHECKSUM_PART=!CHECKSUM_PART:,=!" + + REM Check if it looks like a SHA256 (64 hex chars) + if not "!CHECKSUM_PART!"=="" ( + call :check_length "!CHECKSUM_PART!" 64 + if !ERRORLEVEL! equ 0 ( + set "EXPECTED_CHECKSUM=!CHECKSUM_PART!" + exit /b 0 + ) + ) + ) + ) + + REM Check if we've left the platform section (closing brace) + echo !LINE! | findstr /c:"}" >nul + if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION=" + ) +) + +if "!EXPECTED_CHECKSUM!"=="" exit /b 1 +exit /b 0 + +:check_length +REM Check if string length equals expected length +REM Args: %1=String, %2=ExpectedLength +set "STR=%~1" +set "EXPECTED_LEN=%~2" +set "LEN=0" +:count_loop +if "!STR:~%LEN%,1!"=="" goto :count_done +set /a LEN+=1 +goto :count_loop +:count_done +if %LEN%==%EXPECTED_LEN% exit /b 0 +exit /b 1 + +:verify_checksum +REM Verify file checksum using certutil +REM Args: %1=FilePath, %2=ExpectedChecksum +set "FILE_PATH=%~1" +set "EXPECTED=%~2" + +for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do ( + set "ACTUAL=%%i" + set "ACTUAL=!ACTUAL: =!" + if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done + if "!ACTUAL!" neq "" ( + if /i "!ACTUAL!"=="!EXPECTED!" ( + exit /b 0 + ) else ( + exit /b 1 + ) + ) +) + +:verify_done +exit /b 1