diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb75cda..1d0c827 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,10 +23,19 @@ env: jobs: test: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - name: Free disk space (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + sudo apt-get clean + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -38,11 +47,8 @@ jobs: - name: Cache cargo uses: Swatinem/rust-cache@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + run: cargo test lint: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ccfb63..e80526b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,9 @@ jobs: - target: x86_64-unknown-linux-musl os: ubuntu-latest name: sediment-x86_64-unknown-linux-musl + - target: x86_64-pc-windows-msvc + os: windows-latest + name: sediment-x86_64-pc-windows-msvc runs-on: ${{ matrix.os }} @@ -58,7 +61,8 @@ jobs: if: ${{ !contains(matrix.target, 'musl') }} run: cargo build --release --target ${{ matrix.target }} - - name: Package + - name: Package (Unix) + if: ${{ !contains(matrix.target, 'windows') }} run: | mkdir -p dist cp target/${{ matrix.target }}/release/sediment dist/ @@ -67,6 +71,16 @@ jobs: cd .. shasum -a 256 ${{ matrix.name }}.tar.gz > ${{ matrix.name }}.tar.gz.sha256 + - name: Package (Windows) + if: contains(matrix.target, 'windows') + shell: pwsh + run: | + mkdir dist + cp target/${{ matrix.target }}/release/sediment.exe dist/ + Compress-Archive -Path dist/sediment.exe -DestinationPath ${{ matrix.name }}.zip + $hash = (Get-FileHash ${{ matrix.name }}.zip -Algorithm SHA256).Hash.ToLower() + "$hash ${{ matrix.name }}.zip" | Out-File -Encoding ascii ${{ matrix.name }}.zip.sha256 + - name: Upload Release Assets uses: softprops/action-gh-release@v1 with: @@ -74,6 +88,8 @@ jobs: files: | ${{ matrix.name }}.tar.gz ${{ matrix.name }}.tar.gz.sha256 + ${{ matrix.name }}.zip + ${{ matrix.name }}.zip.sha256 checksums: needs: build diff --git a/README.md b/README.md index 0145d54..4616fa1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Combines vector search, a relationship graph, and access tracking into a unified - **Single binary, zero config** — no Docker, no Postgres, no Qdrant. Just `sediment`. - **50ms store, 103ms recall** — local embeddings and vector search at 1K items, no network round-trips. - **4-tool focused API** — `store`, `recall`, `list`, `forget`. That's it. -- **Works everywhere** — macOS (Intel + ARM), Linux x86_64. All data stays on your machine. +- **Works everywhere** — macOS (Intel + ARM), Linux x86_64, Windows x86_64. All data stays on your machine. ### Comparison @@ -36,9 +36,12 @@ cargo install sediment-mcp brew tap rendro/tap brew install sediment -# Via shell installer +# Via shell installer (macOS/Linux) curl -fsSL https://raw.githubusercontent.com/rendro/sediment/main/install.sh | sh +# Via PowerShell installer (Windows) +irm https://raw.githubusercontent.com/rendro/sediment/main/install.ps1 | iex + # From source cargo install --path . ``` @@ -195,9 +198,14 @@ Benchmarked against 5 alternatives with 1,000 memories and 200 queries. See [BEN ### Data Location +**macOS / Linux:** - Vector store: `~/.sediment/data/` - Graph + access tracking: `~/.sediment/access.db` +**Windows:** +- Vector store: `%LOCALAPPDATA%\sediment\data\` +- Graph + access tracking: `%LOCALAPPDATA%\sediment\access.db` + Everything runs locally. Your data never leaves your machine. ## Contributing diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..a896989 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,84 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Install sediment on Windows. +.DESCRIPTION + Downloads the latest sediment release from GitHub and installs it to + $env:LOCALAPPDATA\sediment\bin (or $env:SEDIMENT_INSTALL_DIR if set). +#> +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Repo = 'rendro/sediment' +$InstallDir = if ($env:SEDIMENT_INSTALL_DIR) { $env:SEDIMENT_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'sediment\bin' } + +# Detect architecture +$Arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture +if ($Arch -ne 'X64') { + Write-Error "Unsupported architecture: $Arch. Only x86_64 is supported." + exit 1 +} + +$Target = 'x86_64-pc-windows-msvc' + +# Get latest version +Write-Host 'Fetching latest release...' +$Release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" +$Version = $Release.tag_name -replace '^v', '' +if (-not $Version) { + Write-Error 'Failed to determine latest version' + exit 1 +} +Write-Host "Installing sediment v$Version ($Target)..." + +$ZipName = "sediment-$Target.zip" +$Url = "https://github.com/$Repo/releases/download/v$Version/$ZipName" + +# Download +$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "sediment-install-$([guid]::NewGuid())" +New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null + +try { + $ZipPath = Join-Path $TmpDir $ZipName + Invoke-WebRequest -Uri $Url -OutFile $ZipPath -UseBasicParsing + + # Verify checksum + $ChecksumsUrl = "https://github.com/$Repo/releases/download/v$Version/checksums.txt" + try { + $ChecksumsPath = Join-Path $TmpDir 'checksums.txt' + Invoke-WebRequest -Uri $ChecksumsUrl -OutFile $ChecksumsPath -UseBasicParsing + $Expected = (Get-Content $ChecksumsPath | Where-Object { $_ -match $ZipName } | ForEach-Object { ($_ -split '\s+')[0] }) + if ($Expected) { + $Actual = (Get-FileHash $ZipPath -Algorithm SHA256).Hash.ToLower() + if ($Actual -ne $Expected) { + Write-Error "Checksum verification failed!`n Expected: $Expected`n Actual: $Actual" + exit 1 + } + Write-Host 'Checksum verified.' + } + } catch { + Write-Warning 'No checksums.txt available, binary integrity could not be verified.' + } + + # Extract + $ExtractDir = Join-Path $TmpDir 'extract' + Expand-Archive -Path $ZipPath -DestinationPath $ExtractDir -Force + + # Install + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + Copy-Item -Path (Join-Path $ExtractDir 'sediment.exe') -Destination (Join-Path $InstallDir 'sediment.exe') -Force + + Write-Host "Installed sediment to $InstallDir\sediment.exe" + + # Check PATH + if (-not ($env:PATH -split ';' | Where-Object { $_ -eq $InstallDir })) { + Write-Host '' + Write-Host "Add $InstallDir to your PATH:" + Write-Host " [Environment]::SetEnvironmentVariable('PATH', `"$InstallDir;`$env:PATH`", 'User')" + } +} finally { + Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/src/lib.rs b/src/lib.rs index 72700b3..693acba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,17 +111,29 @@ impl std::fmt::Display for ListScope { /// Get the central database path. /// -/// Returns `~/.sediment/data` or the path specified in `SEDIMENT_DB` environment variable. +/// Returns `~/.sediment/data` on Unix, `%LOCALAPPDATA%\sediment\data` on Windows, +/// or the path specified in `SEDIMENT_DB` environment variable. /// Note: LanceDB uses a directory, not a single file. pub fn central_db_path() -> PathBuf { if let Ok(path) = std::env::var("SEDIMENT_DB") { return PathBuf::from(path); } - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".sediment") - .join("data") + #[cfg(unix)] + { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".sediment") + .join("data") + } + + #[cfg(windows)] + { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("sediment") + .join("data") + } } /// Project configuration stored in `.sediment/config` @@ -266,6 +278,11 @@ fn write_config_atomic( let tmp_path = sediment_dir.join(format!("config.tmp.{}", std::process::id())); std::fs::write(&tmp_path, &content)?; + // On Windows, rename fails if destination exists — remove it first. + // This is not atomic but acceptable for a config file. + #[cfg(windows)] + let _ = std::fs::remove_file(config_path); + if let Err(e) = std::fs::rename(&tmp_path, config_path) { let _ = std::fs::remove_file(&tmp_path); return Err(e);