Files
codebase-memory-mcp/scripts/setup-windows.ps1
Martin Vogel 6c00f96311 fix(setup): install through install.sh / install.ps1, one install path
scripts/setup.sh and scripts/setup-windows.ps1 carried their own copy of
the release download: resolve the latest tag, fetch the archive, unpack it
into the install directory. Two implementations of the same step drifted
apart: the setup scripts did not follow the installers as those gained
their checks, their architecture selection and their staging directory.

Both setup scripts now fetch the installer from the origin and branch they
are themselves served from and hand the download and install over to it,
so there is exactly one implementation and the setup scripts behave like
the installers: the same conditions stop them with a non-zero exit before
anything is installed, setup-windows.ps1 selects the archive through the
installer's architecture detection instead of always amd64 (CBM_ARCH pins
it) and stages in a freshly created directory, and the file is pure ASCII
again. CBM_DOWNLOAD_URL, the
installers' download-base override, also moves the installer fetch so both
scripts can be driven against a local fixture.

Everything the setup scripts do beyond installing the binary is unchanged:
the --from-source / -FromSource builds, the interactive Claude Code
configuration, the PATH hint, the git-hook activation and the uninstall
notes.

tests/test_setup_scripts_contract.sh runs setup.sh against a loopback
fixture release built from a stub binary, in file mode and in curl | bash
mode, covers the cases in which the installer refuses, and pins the static
contract for setup-windows.ps1. It is Step 0z of scripts/test.sh.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-02 06:50:04 +02:00

391 lines
14 KiB
PowerShell

# codebase-memory-mcp setup script (Windows)
# Default: install the pre-built native Windows binary through install.ps1
# -FromSource: build from source inside WSL (requires Go + gcc in WSL)
param(
[switch]$FromSource,
[switch]$Help
)
$ErrorActionPreference = "Stop"
$Repo = "DeusData/codebase-memory-mcp"
$BinaryName = "codebase-memory-mcp"
$InstallDir = Join-Path $env:LOCALAPPDATA "codebase-memory-mcp"
# --- Helpers ---
function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green }
function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red }
function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow }
function Read-SettingsJson($Path) {
# PS5.1-compatible: ConvertFrom-Json returns PSCustomObject, not Hashtable.
# We convert to ordered hashtable manually.
if (-not (Test-Path $Path)) {
return @{}
}
$raw = Get-Content $Path -Raw
if (-not $raw -or $raw.Trim() -eq "") {
return @{}
}
$obj = $raw | ConvertFrom-Json
$ht = [ordered]@{}
foreach ($prop in $obj.PSObject.Properties) {
if ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
$inner = [ordered]@{}
foreach ($p in $prop.Value.PSObject.Properties) {
$inner[$p.Name] = $p.Value
}
$ht[$prop.Name] = $inner
} else {
$ht[$prop.Name] = $prop.Value
}
}
return $ht
}
function Write-SettingsJson($Path, $Settings) {
# Back up existing file before writing
if (Test-Path $Path) {
Copy-Item $Path "$Path.bak" -Force
}
$Settings | ConvertTo-Json -Depth 10 | Set-Content $Path -Encoding UTF8
}
function Configure-ClaudeCode($McpConfig) {
Write-Host ""
$answer = Read-Host "Configure Claude Code to use codebase-memory-mcp? [y/N]"
if ($answer -match '^[Yy]$') {
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
$settingsDir = Split-Path $settingsPath -Parent
if (-not (Test-Path $settingsDir)) {
New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null
}
$settings = Read-SettingsJson $settingsPath
if (-not $settings.Contains("mcpServers")) {
$settings["mcpServers"] = [ordered]@{}
}
$settings["mcpServers"]["codebase-memory-mcp"] = $McpConfig
Write-SettingsJson $settingsPath $settings
Write-Ok "Updated $settingsPath"
} else {
Write-Host ""
Write-Host " Add this to your .mcp.json or %USERPROFILE%\.claude\settings.json:" -ForegroundColor White
Write-Host ""
$snippet = @{ mcpServers = @{ "codebase-memory-mcp" = $McpConfig } }
$snippet | ConvertTo-Json -Depth 10 | Write-Host
}
}
function Test-WSL {
try {
$null = wsl.exe --status 2>&1
if ($LASTEXITCODE -ne 0) { return $false }
return $true
} catch {
return $false
}
}
function Get-WSLDistro {
$output = wsl.exe -l -v 2>&1 | Out-String
$lines = $output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -Skip 1
foreach ($line in $lines) {
$clean = $line -replace '\x00', '' -replace '^\s+', ''
if ($clean -match '^\*?\s*(\S+)\s+') {
$name = $Matches[1]
if ($name -ne "NAME" -and $name -ne "") {
return $name
}
}
}
return $null
}
function Invoke-WSL {
param([string]$Command)
$result = wsl.exe -- bash -c $Command 2>&1
if ($LASTEXITCODE -ne 0) {
throw "WSL command failed: $Command`n$result"
}
return $result
}
# --- Main ---
if ($Help) {
Write-Host ""
Write-Host "Usage: .\setup-windows.ps1 [-FromSource] [-Help]"
Write-Host ""
Write-Host " Default: Download the pre-built Windows binary through install.ps1"
Write-Host " -FromSource: Build from source inside WSL (requires Go 1.23+ and gcc in WSL)"
Write-Host ""
exit 0
}
Write-Host ""
Write-Host "codebase-memory-mcp installer (Windows)" -ForegroundColor White
Write-Host ""
if ($FromSource) {
# --- Build from source via WSL ---
Write-Host "Checking WSL2 (required for building from source)..." -ForegroundColor White
if (-not (Test-WSL)) {
Write-Fail "WSL2 is not available. Required for building from source (CGO needs gcc)."
Write-Host ""
Write-Host " Install WSL2:" -ForegroundColor Yellow
Write-Host " wsl --install -d Ubuntu"
Write-Host " Then restart your computer and run this script again."
Write-Host ""
Write-Host " Alternatively, download a pre-built binary (no WSL needed):" -ForegroundColor Yellow
Write-Host " .\setup-windows.ps1"
exit 1
}
Write-Ok "WSL2 is available"
$distro = Get-WSLDistro
if (-not $distro) {
Write-Fail "No WSL Linux distribution found."
Write-Host ""
Write-Host " Install Ubuntu:" -ForegroundColor Yellow
Write-Host " wsl --install -d Ubuntu"
exit 1
}
Write-Ok "WSL distro: $distro"
$wslUser = (Invoke-WSL "whoami").Trim()
Write-Ok "WSL user: $wslUser"
Write-Host ""
Write-Host "Checking prerequisites inside WSL..." -ForegroundColor White
# Check for gcc
try {
Invoke-WSL "command -v gcc" | Out-Null
Write-Ok "gcc found"
} catch {
Write-Warn "gcc not found. Installing build-essential..."
Invoke-WSL "sudo apt-get update && sudo apt-get install -y build-essential"
}
# Check for Go
try {
$goVersion = Invoke-WSL "go version"
Write-Ok "Go: $goVersion"
} catch {
Write-Fail "Go not found in WSL."
Write-Host ""
Write-Host " Install Go 1.23+ inside WSL:" -ForegroundColor Yellow
Write-Host " See https://go.dev/dl/ for Linux amd64 tarball"
exit 1
}
# Check for git
try {
Invoke-WSL "command -v git" | Out-Null
Write-Ok "git found"
} catch {
Write-Fail "git not found in WSL. Install with: sudo apt install git"
exit 1
}
# Clone or update
Write-Host ""
$sourceDir = "/home/$wslUser/.local/share/codebase-memory-mcp"
try {
Invoke-WSL "test -d $sourceDir/.git" | Out-Null
Write-Host "Updating source..." -ForegroundColor White
Invoke-WSL "git -C $sourceDir pull --ff-only"
} catch {
Write-Host "Cloning repository..." -ForegroundColor White
Invoke-WSL "mkdir -p /home/$wslUser/.local/share && git clone https://github.com/$Repo.git $sourceDir"
}
Write-Ok "Source at $sourceDir"
# Build
Write-Host ""
Write-Host "Building binary (this may take a minute)..." -ForegroundColor White
$wslBinaryPath = "/home/$wslUser/.local/bin/$BinaryName"
Invoke-WSL "mkdir -p /home/$wslUser/.local/bin && cd $sourceDir && scripts/build.sh && cp build/c/codebase-memory-mcp $wslBinaryPath"
Write-Ok "Built to $wslBinaryPath (inside WSL)"
# Verify
try {
Invoke-WSL "test -x $wslBinaryPath" | Out-Null
Write-Ok "Binary is executable"
} catch {
Write-Fail "Binary at $wslBinaryPath is not executable"
exit 1
}
# Configure -- WSL binary needs wsl.exe wrapper
$mcpConfig = [ordered]@{
type = "stdio"
command = "wsl.exe"
args = @("-d", $distro, "--", $wslBinaryPath)
}
Configure-ClaudeCode $mcpConfig
Write-Host ""
Write-Ok "Done! Restart Claude Code and verify with /mcp"
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor White
Write-Host " wsl.exe -- rm $wslBinaryPath"
Write-Host " wsl.exe -- rm -rf $sourceDir"
Write-Host " wsl.exe -- rm -rf ~/.cache/codebase-memory-mcp/"
} else {
# --- Download + install through install.ps1 ---
#
# install.ps1 is the one implementation of "fetch a release and install
# it": it downloads checksums.txt next to the archive, verifies the
# archive's SHA-256 against it, validates the zip layout and only then
# runs the binary's own `install`. This script used to carry a second
# copy of that download, which did not keep up with the installer. It
# now fetches install.ps1 from the same origin and branch it is itself
# served from and hands over to it, so there is exactly one install path.
#
# CBM_DOWNLOAD_URL (the installers' download-base override, for local
# testing) also moves the installer fetch: install.ps1 is then taken from
# "$env:CBM_DOWNLOAD_URL/install.ps1".
$installerUrl = "https://raw.githubusercontent.com/$Repo/main/install.ps1"
if ($env:CBM_DOWNLOAD_URL) {
$installerUrl = $env:CBM_DOWNLOAD_URL.TrimEnd('/') + "/install.ps1"
}
# Same transport rule as install.ps1: HTTPS, or plain HTTP for a loopback
# authority only (the local test fixture). No redirects are followed: both
# origins serve the installer directly.
try { $installerUri = [Uri]$installerUrl } catch { $installerUri = $null }
$loopbackHttp = (
$installerUri -and $installerUri.IsAbsoluteUri -and
$installerUri.Scheme -eq "http" -and $installerUri.IsLoopback -and
[string]::IsNullOrEmpty($installerUri.UserInfo)
)
if (-not $installerUri -or -not $installerUri.IsAbsoluteUri -or
($installerUri.Scheme -ne "https" -and -not $loopbackHttp) -or
-not [string]::IsNullOrEmpty($installerUri.UserInfo)) {
Write-Fail "Refusing non-HTTPS installer URL: $installerUrl"
exit 1
}
# TLS 1.2+ for the fetch (older Windows PowerShell defaults to TLS 1.0,
# which GitHub rejects). TLS 1.3 only where schannel can negotiate it; see
# the note in install.ps1.
$protocols = [Net.SecurityProtocolType]::Tls12
if ([Environment]::OSVersion.Version.Build -ge 20348 -and
([enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13')) {
$protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13
}
[Net.ServicePointManager]::SecurityProtocol = $protocols
# A fresh staging directory of its own with an owner-only DACL, the
# way install.ps1 stages its own download. The ACL is best effort: a
# filesystem that cannot carry one must not fail the install.
$stageDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-setup-$(Get-Random)"
New-Item -ItemType Directory -Path $stageDir -Force | Out-Null
try {
$stageAcl = New-Object System.Security.AccessControl.DirectorySecurity
$stageAcl.SetAccessRuleProtection($true, $false)
$stageOwner = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User
$stageAcl.SetOwner($stageOwner)
$stageAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
$stageOwner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
Set-Acl -Path $stageDir -AclObject $stageAcl -ErrorAction Stop
} catch {
Write-Host " note: could not harden the staging directory ACL: $($_.Exception.Message)"
}
$installer = Join-Path $stageDir "install.ps1"
$failure = $null
try {
Write-Host "Fetching install.ps1..." -ForegroundColor White
Invoke-WebRequest -Uri $installerUrl -OutFile $installer -UseBasicParsing -MaximumRedirection 0
if (-not (Test-Path -LiteralPath $installer -PathType Leaf) -or
(Get-Item -LiteralPath $installer).Length -eq 0) {
throw "fetched an empty install.ps1 from $installerUrl"
}
Write-Ok "install.ps1 fetched"
Write-Host ""
Write-Host "Installing through install.ps1..." -ForegroundColor White
# The installer configures nothing (--skip-config): agent configuration
# stays this script's interactive step below. It runs in a child host
# of the same PowerShell so its exit code is unambiguous and its global
# settings do not leak into this session. The pre-seeded exit code
# keeps a child that fails to start from reading as success.
$hostExe = (Get-Process -Id $PID).Path
$global:LASTEXITCODE = 1
& $hostExe -NoProfile -ExecutionPolicy Bypass -File $installer "--dir=$InstallDir" --skip-config
if ($LASTEXITCODE -ne 0) {
throw "install.ps1 exited with $LASTEXITCODE"
}
} catch {
$failure = "$_"
} finally {
Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue
}
if ($failure) {
Write-Fail "Installation through install.ps1 failed: $failure"
exit 1
}
$binaryPath = Join-Path $InstallDir "$BinaryName.exe"
if (-not (Test-Path $binaryPath)) {
Write-Fail "Binary not found at $binaryPath after installation"
exit 1
}
Write-Ok "Installed to $binaryPath"
# Verify binary runs
try {
$verOut = & $binaryPath --version 2>&1
Write-Ok "Version: $verOut"
} catch {
Write-Warn "Could not verify binary version (may still work)"
}
# SmartScreen note
Write-Host ""
Write-Warn "Windows SmartScreen may show a warning when the binary runs for the first time."
Write-Host " This is normal for unsigned open-source binaries." -ForegroundColor Yellow
Write-Host " Click 'More info' then 'Run anyway' to proceed." -ForegroundColor Yellow
Write-Host " Verify checksums at: https://github.com/$Repo/releases" -ForegroundColor Yellow
# Check if install dir is on PATH
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$InstallDir*") {
Write-Host ""
Write-Warn "$InstallDir is not on your PATH."
$addPath = Read-Host " Add it to your user PATH? [y/N]"
if ($addPath -match '^[Yy]$') {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$InstallDir", "User")
Write-Ok "Added to user PATH (restart your terminal to take effect)"
}
}
# Configure Claude Code
$mcpConfig = [ordered]@{
type = "stdio"
command = $binaryPath
}
Configure-ClaudeCode $mcpConfig
Write-Host ""
Write-Ok "Done! Restart Claude Code and verify with /mcp"
Write-Host ""
Write-Host " To uninstall:" -ForegroundColor White
Write-Host " Remove-Item -Recurse -Force '$InstallDir'"
Write-Host " Remove-Item -Recurse -Force `"$env:LOCALAPPDATA\codebase-memory-mcp`" # graph database"
}