mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
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>
391 lines
14 KiB
PowerShell
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"
|
|
}
|