11# codebase-memory-mcp setup script (Windows)
2- # Default: download pre-built native Windows binary
2+ # Default: install the pre-built native Windows binary through install.ps1
33# -FromSource: build from source inside WSL (requires Go + gcc in WSL)
44
55param (
@@ -123,7 +123,7 @@ if ($Help) {
123123 Write-Host " "
124124 Write-Host " Usage: .\setup-windows.ps1 [-FromSource] [-Help]"
125125 Write-Host " "
126- Write-Host " Default: Download pre-built Windows binary"
126+ Write-Host " Default: Download the pre-built Windows binary through install.ps1 "
127127 Write-Host " -FromSource: Build from source inside WSL (requires Go 1.23+ and gcc in WSL)"
128128 Write-Host " "
129129 exit 0
@@ -225,7 +225,7 @@ if ($FromSource) {
225225 exit 1
226226 }
227227
228- # Configure — WSL binary needs wsl.exe wrapper
228+ # Configure -- WSL binary needs wsl.exe wrapper
229229 $mcpConfig = [ordered ]@ {
230230 type = " stdio"
231231 command = " wsl.exe"
@@ -243,41 +243,105 @@ if ($FromSource) {
243243 Write-Host " wsl.exe -- rm -rf ~/.cache/codebase-memory-mcp/"
244244
245245} else {
246- # --- Download pre-built native Windows binary ---
247- Write-Host " Fetching latest release..." - ForegroundColor White
248-
249- $releaseUrl = " https://api.github.com/repos/$Repo /releases/latest"
250- $release = Invoke-RestMethod - Uri $releaseUrl - Headers @ { " User-Agent" = " codebase-memory-mcp-setup" }
251- $tag = $release.tag_name
246+ # --- Download + install through install.ps1 ---
247+ #
248+ # install.ps1 is the one implementation of "fetch a release and install
249+ # it": it downloads checksums.txt next to the archive, verifies the
250+ # archive's SHA-256 against it, validates the zip layout and only then
251+ # runs the binary's own `install`. This script used to carry a second
252+ # copy of that download, which did not keep up with the installer. It
253+ # now fetches install.ps1 from the same origin and branch it is itself
254+ # served from and hands over to it, so there is exactly one install path.
255+ #
256+ # CBM_DOWNLOAD_URL (the installers' download-base override, for local
257+ # testing) also moves the installer fetch: install.ps1 is then taken from
258+ # "$env:CBM_DOWNLOAD_URL/install.ps1".
259+ $installerUrl = " https://raw.githubusercontent.com/$Repo /main/install.ps1"
260+ if ($env: CBM_DOWNLOAD_URL ) {
261+ $installerUrl = $env: CBM_DOWNLOAD_URL.TrimEnd (' /' ) + " /install.ps1"
262+ }
252263
253- if (-not $tag ) {
254- Write-Fail " Could not determine latest release."
255- Write-Host " Check: https://github.com/$Repo /releases"
264+ # Same transport rule as install.ps1: HTTPS, or plain HTTP for a loopback
265+ # authority only (the local test fixture). No redirects are followed: both
266+ # origins serve the installer directly.
267+ try { $installerUri = [Uri ]$installerUrl } catch { $installerUri = $null }
268+ $loopbackHttp = (
269+ $installerUri -and $installerUri.IsAbsoluteUri -and
270+ $installerUri.Scheme -eq " http" -and $installerUri.IsLoopback -and
271+ [string ]::IsNullOrEmpty($installerUri.UserInfo )
272+ )
273+ if (-not $installerUri -or -not $installerUri.IsAbsoluteUri -or
274+ ($installerUri.Scheme -ne " https" -and -not $loopbackHttp ) -or
275+ -not [string ]::IsNullOrEmpty($installerUri.UserInfo )) {
276+ Write-Fail " Refusing non-HTTPS installer URL: $installerUrl "
256277 exit 1
257278 }
258- Write-Ok " Latest release: $tag "
259279
260- $asset = " codebase-memory-mcp-windows-amd64.zip"
261- $downloadUrl = " https://github.com/$Repo /releases/download/$tag /$asset "
262-
263- Write-Host " Downloading $asset ..." - ForegroundColor White
280+ # TLS 1.2+ for the fetch (older Windows PowerShell defaults to TLS 1.0,
281+ # which GitHub rejects). TLS 1.3 only where schannel can negotiate it; see
282+ # the note in install.ps1.
283+ $protocols = [Net.SecurityProtocolType ]::Tls12
284+ if ([Environment ]::OSVersion.Version.Build -ge 20348 -and
285+ ([enum ]::GetNames([Net.SecurityProtocolType ]) -contains ' Tls13' )) {
286+ $protocols = $protocols -bor [Net.SecurityProtocolType ]::Tls13
287+ }
288+ [Net.ServicePointManager ]::SecurityProtocol = $protocols
264289
265- # Create install directory
266- if (-not (Test-Path $InstallDir )) {
267- New-Item - ItemType Directory - Path $InstallDir - Force | Out-Null
290+ # A fresh staging directory of its own with an owner-only DACL, the
291+ # way install.ps1 stages its own download. The ACL is best effort: a
292+ # filesystem that cannot carry one must not fail the install.
293+ $stageDir = Join-Path ([System.IO.Path ]::GetTempPath()) " cbm-setup-$ ( Get-Random ) "
294+ New-Item - ItemType Directory - Path $stageDir - Force | Out-Null
295+ try {
296+ $stageAcl = New-Object System.Security.AccessControl.DirectorySecurity
297+ $stageAcl.SetAccessRuleProtection ($true , $false )
298+ $stageOwner = ([System.Security.Principal.WindowsIdentity ]::GetCurrent()).User
299+ $stageAcl.SetOwner ($stageOwner )
300+ $stageAcl.AddAccessRule ((New-Object System.Security.AccessControl.FileSystemAccessRule(
301+ $stageOwner , ' FullControl' , ' ContainerInherit,ObjectInherit' , ' None' , ' Allow' )))
302+ Set-Acl - Path $stageDir - AclObject $stageAcl - ErrorAction Stop
303+ } catch {
304+ Write-Host " note: could not harden the staging directory ACL: $ ( $_.Exception.Message ) "
268305 }
269306
270- $tmpZip = Join-Path $env: TEMP $asset
271- Invoke-WebRequest - Uri $downloadUrl - OutFile $tmpZip - UseBasicParsing
307+ $installer = Join-Path $stageDir " install.ps1"
308+ $failure = $null
309+ try {
310+ Write-Host " Fetching install.ps1..." - ForegroundColor White
311+ Invoke-WebRequest - Uri $installerUrl - OutFile $installer - UseBasicParsing - MaximumRedirection 0
312+ if (-not (Test-Path - LiteralPath $installer - PathType Leaf) -or
313+ (Get-Item - LiteralPath $installer ).Length -eq 0 ) {
314+ throw " fetched an empty install.ps1 from $installerUrl "
315+ }
316+ Write-Ok " install.ps1 fetched"
272317
273- # Extract
274- Expand-Archive - Path $tmpZip - DestinationPath $InstallDir - Force
275- Remove-Item $tmpZip - Force
318+ Write-Host " "
319+ Write-Host " Installing through install.ps1..." - ForegroundColor White
320+ # The installer configures nothing (--skip-config): agent configuration
321+ # stays this script's interactive step below. It runs in a child host
322+ # of the same PowerShell so its exit code is unambiguous and its global
323+ # settings do not leak into this session. The pre-seeded exit code
324+ # keeps a child that fails to start from reading as success.
325+ $hostExe = (Get-Process - Id $PID ).Path
326+ $global :LASTEXITCODE = 1
327+ & $hostExe - NoProfile - ExecutionPolicy Bypass - File $installer " --dir=$InstallDir " -- skip-config
328+ if ($LASTEXITCODE -ne 0 ) {
329+ throw " install.ps1 exited with $LASTEXITCODE "
330+ }
331+ } catch {
332+ $failure = " $_ "
333+ } finally {
334+ Remove-Item - Recurse - Force $stageDir - ErrorAction SilentlyContinue
335+ }
336+ if ($failure ) {
337+ Write-Fail " Installation through install.ps1 failed: $failure "
338+ exit 1
339+ }
276340
277341 $binaryPath = Join-Path $InstallDir " $BinaryName .exe"
278342
279343 if (-not (Test-Path $binaryPath )) {
280- Write-Fail " Binary not found at $binaryPath after extraction "
344+ Write-Fail " Binary not found at $binaryPath after installation "
281345 exit 1
282346 }
283347 Write-Ok " Installed to $binaryPath "
0 commit comments