From e5be7395fe2612c025c2c5d114b65ff168363bfc Mon Sep 17 00:00:00 2001 From: Andrei Fedotov Date: Mon, 27 Jul 2026 18:03:03 +0300 Subject: [PATCH 1/3] chore: add build for Windows support --- .github/workflows/release.yml | 79 ++++++++++++++++++++++++++++++ .github/workflows/test-build.yml | 80 ++++++++++++++++++++++++++++++ .gitignore | 2 + README.md | 47 ++++++++++++++++-- install.ps1 | 83 ++++++++++++++++++++++++++++++++ 5 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 install.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e995cb..4cafc11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -247,3 +247,82 @@ jobs: tag_name: ${{ github.ref_name }} name: Release ${{ github.ref_name }} body_path: .github/release-notes/${{ github.ref_name }}.md + + build-windows: + name: Build for Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install pnpm + uses: pnpm/action-setup@v6 + + - name: Get pnpm store directory + shell: pwsh + run: | + "STORE_PATH=$(pnpm store path --silent)" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build frontend + working-directory: ./anycode + run: pnpm build + + - name: Verify frontend build + working-directory: ./anycode-backend + shell: pwsh + run: | + if (-not (Test-Path "dist")) { + Write-Error "Error: dist directory is missing" + } + $files = Get-ChildItem -Path dist -Recurse -File + if ($files.Count -eq 0) { + Write-Error "Error: dist directory is empty" + } + Write-Host "Frontend build verified. Total files: $($files.Count)" + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.96.0 + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + anycode-backend -> target + + - name: Build Windows binary + working-directory: ./anycode-backend + run: cargo build --release + + - name: Package as zip + working-directory: ./anycode-backend + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path dist-release | Out-Null + Copy-Item target/release/anycode.exe dist-release/anycode.exe + Compress-Archive -Path dist-release/anycode.exe -DestinationPath anycode-windows-x86_64.zip -Force + + - name: Upload to release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: anycode-backend/anycode-windows-x86_64.zip + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body_path: .github/release-notes/${{ github.ref_name }}.md diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 1a466fa..cef942f 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -103,3 +103,83 @@ jobs: working-directory: ./anycode-backend run: | ./target/x86_64-unknown-linux-musl/release/anycode --help + + test-build-windows: + name: Test Build Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install pnpm + uses: pnpm/action-setup@v6 + + - name: Get pnpm store directory + shell: pwsh + run: | + "STORE_PATH=$(pnpm store path --silent)" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build frontend + working-directory: ./anycode + run: pnpm build + + - name: Verify frontend build + working-directory: ./anycode-backend + shell: pwsh + run: | + if (-not (Test-Path "dist")) { + Write-Error "Error: dist directory is missing" + } + $files = Get-ChildItem -Path dist -Recurse -File + if ($files.Count -eq 0) { + Write-Error "Error: dist directory is empty" + } + Write-Host "Frontend build verified. Total files: $($files.Count)" + + - name: Run core library tests + working-directory: ./anycode-base + run: pnpm test -- --run + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.96.0 + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + anycode-backend -> target + + - name: Run Rust backend tests + working-directory: ./anycode-backend + run: cargo test + + - name: Build Windows binary + working-directory: ./anycode-backend + run: cargo build --release + + - name: Check binary output + working-directory: ./anycode-backend + shell: pwsh + run: | + Get-Item target/release/anycode.exe | Format-List FullName,Length,LastWriteTime + + - name: Test binary help + working-directory: ./anycode-backend + run: .\target\release\anycode.exe --help diff --git a/.gitignore b/.gitignore index 6d7f911..071b6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ anycode-react/dist/Component.js awesome.md promo.md promo_ru.md +.cargo-home/ +.cargo-home-sparse/ diff --git a/README.md b/README.md index fdbd086..c262ddd 100644 --- a/README.md +++ b/README.md @@ -33,28 +33,65 @@ The project consists of several packages: ## Installation ### From releases -One-shot installer: +#### macOS and Linux one-shot installer: ```bash curl -fsSL https://raw.githubusercontent.com/anycode-ade/anycode/main/install.sh | sh ``` +#### Windows PowerShell installer (x64): +```powershell +irm https://raw.githubusercontent.com/anycode-ade/anycode/main/install.ps1 | iex +``` + +Windows installer details: +- Installs `anycode.exe` into `%USERPROFILE%\AppData\Local\anycode\bin` by default +- Adds that directory to the user `PATH` if needed +- Supports overrides via `ANYCODE_VERSION`, `ANYCODE_INSTALL_DIR`, and `ANYCODE_REPO` +- Current release installer targets Windows x64; Windows ARM64 can still build from source + +If you want a specific release: +```powershell +$env:ANYCODE_VERSION="v0.0.24" +irm https://raw.githubusercontent.com/anycode-ade/anycode/main/install.ps1 | iex +``` + ## Development -1. **Start frontend:** +Prerequisites: +- Node.js 24+ +- `pnpm` +- Rust 1.96.0+ + +1. **Install workspace dependencies:** ```bash pnpm install + ``` + +2. **Start frontend:** + ```bash cd anycode - pnpm build pnpm dev ``` -2. **Start rust backend:** +3. **Build frontend assets for the backend:** + ```bash + cd anycode + pnpm build + ``` + +4. **Start Rust backend:** ```bash cd anycode-backend cargo run --release ``` -3. **Open your browser** and navigate to the frontend URL +5. **Open your browser** and navigate to the frontend URL + +### Windows notes + +- Run the commands in PowerShell, Windows Terminal, or another shell with `cargo`, `node`, and `pnpm` on `PATH`. +- The backend already uses `cmd.exe` as the integrated terminal shell on Windows. +- Release-based installation on Windows depends on the published `anycode-windows-x86_64.zip` asset. ## Contributing diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..adbc67f --- /dev/null +++ b/install.ps1 @@ -0,0 +1,83 @@ +param( + [string]$Version = $env:ANYCODE_VERSION, + [string]$InstallDir = $env:ANYCODE_INSTALL_DIR, + [string]$Repo = $(if ($env:ANYCODE_REPO) { $env:ANYCODE_REPO } else { "anycode-ade/anycode" }) +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($Version)) { + $Version = "latest" +} + +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + $InstallDir = Join-Path $HOME "AppData\Local\anycode\bin" +} + +$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +switch ($arch) { + "X64" { $asset = "anycode-windows-x86_64.zip" } + "Arm64" { throw "Windows ARM64 releases are not published yet. Build from source instead." } + default { + throw "Unsupported Windows architecture: $arch" + } +} + +if ($Version -eq "latest") { + $downloadUrl = "https://github.com/$Repo/releases/latest/download/$asset" +} else { + $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$asset" +} + +$tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("anycode-install-" + [System.Guid]::NewGuid().ToString("N")) +$archivePath = Join-Path $tmpRoot $asset +$extractDir = Join-Path $tmpRoot "extract" + +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +New-Item -ItemType Directory -Force -Path $tmpRoot | Out-Null +New-Item -ItemType Directory -Force -Path $extractDir | Out-Null + +try { + Write-Host "Downloading $asset from $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath + + Expand-Archive -Path $archivePath -DestinationPath $extractDir -Force + + $sourceExe = Join-Path $extractDir "anycode.exe" + if (-not (Test-Path $sourceExe)) { + throw "Archive did not contain expected binary: anycode.exe" + } + + $installExe = Join-Path $InstallDir "anycode.exe" + Copy-Item -Path $sourceExe -Destination $installExe -Force + + Write-Host "Installed anycode to $installExe" + + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $pathEntries = @() + if (-not [string]::IsNullOrWhiteSpace($userPath)) { + $pathEntries = $userPath.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) + } + + $pathContainsInstallDir = $pathEntries | Where-Object { $_.TrimEnd('\') -ieq $InstallDir.TrimEnd('\') } + if (-not $pathContainsInstallDir) { + $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { + $InstallDir + } else { + "$userPath;$InstallDir" + } + + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + $env:Path = "$env:Path;$InstallDir" + + Write-Host "Added $InstallDir to the user PATH." + Write-Host "Open a new terminal to use 'anycode'." + } else { + Write-Host "$InstallDir is already in PATH." + } +} finally { + if (Test-Path $tmpRoot) { + Remove-Item -LiteralPath $tmpRoot -Recurse -Force + } +} From 57c9504d12961d0e14e045a61b9808c480660454 Mon Sep 17 00:00:00 2001 From: Andrei Fedotov Date: Mon, 27 Jul 2026 19:07:14 +0300 Subject: [PATCH 2/3] chore: support installation for local build --- .gitignore | 5 +---- install.ps1 | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 071b6b9..50d4fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,4 @@ promo.md promo_ru.md .cargo-home/ .cargo-home-sparse/ - - - - +.pnpm-store/v11/index.db diff --git a/install.ps1 b/install.ps1 index adbc67f..1b7c168 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,7 +1,8 @@ param( [string]$Version = $env:ANYCODE_VERSION, [string]$InstallDir = $env:ANYCODE_INSTALL_DIR, - [string]$Repo = $(if ($env:ANYCODE_REPO) { $env:ANYCODE_REPO } else { "anycode-ade/anycode" }) + [string]$Repo = $(if ($env:ANYCODE_REPO) { $env:ANYCODE_REPO } else { "anycode-ade/anycode" }), + [string]$ArchivePath = $env:ANYCODE_ARCHIVE_PATH ) Set-StrictMode -Version Latest @@ -15,7 +16,29 @@ if ([string]::IsNullOrWhiteSpace($InstallDir)) { $InstallDir = Join-Path $HOME "AppData\Local\anycode\bin" } -$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +function Get-WindowsArchitecture { + $runtimeType = [System.Type]::GetType("System.Runtime.InteropServices.RuntimeInformation") + if ($null -ne $runtimeType) { + $osArchProperty = $runtimeType.GetProperty("OSArchitecture") + if ($null -ne $osArchProperty) { + return $osArchProperty.GetValue($null).ToString() + } + } + + $processArch = $env:PROCESSOR_ARCHITEW6432 + if ([string]::IsNullOrWhiteSpace($processArch)) { + $processArch = $env:PROCESSOR_ARCHITECTURE + } + + switch ($processArch.ToUpperInvariant()) { + "AMD64" { return "X64" } + "X86" { return "X86" } + "ARM64" { return "Arm64" } + default { return $processArch } + } +} + +$arch = Get-WindowsArchitecture switch ($arch) { "X64" { $asset = "anycode-windows-x86_64.zip" } "Arm64" { throw "Windows ARM64 releases are not published yet. Build from source instead." } @@ -24,14 +47,21 @@ switch ($arch) { } } -if ($Version -eq "latest") { - $downloadUrl = "https://github.com/$Repo/releases/latest/download/$asset" +if ([string]::IsNullOrWhiteSpace($ArchivePath)) { + if ($Version -eq "latest") { + $downloadUrl = "https://github.com/$Repo/releases/latest/download/$asset" + } else { + $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$asset" + } } else { - $downloadUrl = "https://github.com/$Repo/releases/download/$Version/$asset" + $ArchivePath = [System.IO.Path]::GetFullPath($ArchivePath) + if (-not (Test-Path $ArchivePath)) { + throw "Archive not found: $ArchivePath" + } } $tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("anycode-install-" + [System.Guid]::NewGuid().ToString("N")) -$archivePath = Join-Path $tmpRoot $asset +$tempArchivePath = Join-Path $tmpRoot $asset $extractDir = Join-Path $tmpRoot "extract" New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null @@ -39,10 +69,15 @@ New-Item -ItemType Directory -Force -Path $tmpRoot | Out-Null New-Item -ItemType Directory -Force -Path $extractDir | Out-Null try { - Write-Host "Downloading $asset from $downloadUrl" - Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath + if ([string]::IsNullOrWhiteSpace($ArchivePath)) { + Write-Host "Downloading $asset from $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -OutFile $tempArchivePath + } else { + Write-Host "Using local archive $ArchivePath" + Copy-Item -Path $ArchivePath -Destination $tempArchivePath -Force + } - Expand-Archive -Path $archivePath -DestinationPath $extractDir -Force + Expand-Archive -Path $tempArchivePath -DestinationPath $extractDir -Force $sourceExe = Join-Path $extractDir "anycode.exe" if (-not (Test-Path $sourceExe)) { From 42edcf507404ceb1d7b125610853e58b25bd68a2 Mon Sep 17 00:00:00 2001 From: Andrei Fedotov Date: Tue, 28 Jul 2026 18:50:19 +0300 Subject: [PATCH 3/3] Skip flaky terminal tests on Windows CI --- anycode-backend/src/terminal.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/anycode-backend/src/terminal.rs b/anycode-backend/src/terminal.rs index 61f9673..6cd16eb 100644 --- a/anycode-backend/src/terminal.rs +++ b/anycode-backend/src/terminal.rs @@ -199,6 +199,10 @@ mod tests { use tokio::time::{Duration, timeout}; #[tokio::test] + #[cfg_attr( + target_os = "windows", + ignore = "portable-pty shell echo is flaky on Windows CI runners" + )] async fn test_terminal_echo() -> Result<()> { let (tx, mut rx) = mpsc::channel::(10); @@ -244,6 +248,10 @@ mod tests { } #[tokio::test] + #[cfg_attr( + target_os = "windows", + ignore = "portable-pty unicode echo is flaky on Windows CI runners" + )] async fn test_terminal_unicode() -> Result<()> { let (tx, mut rx) = mpsc::channel::(10);