# MakeMode — Windows setup (served at makemode.eu/setup.ps1) # # irm https://makemode.eu/setup.ps1 | iex # # Native Windows, no WSL: installs the official opencode Windows build into # %USERPROFILE%\.opencode\bin, signs you in from the browser (the same device # flow setup.sh uses), mints this machine's sk-gw key, and writes the same # config files — opencode's paths are identical on Windows (xdg-basedir keeps # ~\.config and ~\.local\share everywhere). A `makemode` command is installed # next to opencode. No admin rights, no restart. # # Flags travel in an environment variable, because `irm | iex` cannot carry # arguments: # $env:MAKEMODE_SETUP_ARGS = '--claude' # configure Claude Code instead # $env:MAKEMODE_SETUP_ARGS = '--both' # both clients, one key # $env:MAKEMODE_SETUP_ARGS = '--code XXXX' # pre-approved code from the account page # $env:MAKEMODE_SETUP_ARGS = 'sk-gw-...' # existing key inline # A key can also come from $env:MAKEMODE_KEY. # # The double-click route (makemode.eu/makemode-setup.cmd) runs exactly this. # Every request runs through the gateway in France; nothing leaves the EU. $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' function Say([string]$msg) { Write-Host $msg } if ($env:OS -ne 'Windows_NT') { Say 'This is the Windows path. On Mac or Linux, run:' Say ' curl -fsSL https://makemode.eu/setup.sh | sh' return } # PowerShell 5.1 defaults to old TLS; GitHub and Scaleway require 1.2. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 } catch {} $Gateway = 'https://api.makemode.eu' $Account = 'https://account.makemode.eu' $Site = 'https://makemode.eu' $BinDir = Join-Path $HOME '.opencode\bin' $CfgDir = Join-Path $HOME '.config\opencode' $DataDir = Join-Path $HOME '.local\share\opencode' $MmDir = Join-Path $HOME '.makemode' # ---- flags ----------------------------------------------------------------- $Client = 'opencode'; $ArgKey = ''; $ArgCode = '' $tokens = @() # @() everywhere: a single token would otherwise arrive as a bare string, and # indexing a string yields its first character, silently eating every flag. if ($env:MAKEMODE_SETUP_ARGS) { $tokens = @($env:MAKEMODE_SETUP_ARGS -split '\s+' | Where-Object { $_ }) } for ($i = 0; $i -lt $tokens.Count; $i++) { switch -regex ($tokens[$i]) { '^--claude$' { $Client = 'claude' } '^--opencode$' { $Client = 'opencode' } '^--both$' { $Client = 'both' } '^--code$' { if ($i + 1 -lt $tokens.Count) { $i++; $ArgCode = $tokens[$i] } } '^--code=(.+)$'{ $ArgCode = $Matches[1] } '^--key$' { if ($i + 1 -lt $tokens.Count) { $i++; $ArgKey = $tokens[$i] } } '^sk-gw-' { $ArgKey = $tokens[$i] } } } if (-not $ArgKey -and $env:MAKEMODE_KEY) { $ArgKey = $env:MAKEMODE_KEY } # ---- 1. opencode (skipped for --claude alone) ------------------------------ if ($Client -ne 'claude') { $oc = Get-Command opencode -ErrorAction SilentlyContinue if ($oc) { Say "opencode is already installed ($($oc.Source))." } else { $arch = 'x64' if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'arm64' } $zipUrl = "https://github.com/sst/opencode/releases/latest/download/opencode-windows-$arch.zip" $zip = Join-Path $env:TEMP 'opencode-windows.zip' Say "Downloading opencode for Windows ($arch)... (about a minute)" Invoke-WebRequest -Uri $zipUrl -OutFile $zip -UseBasicParsing New-Item -ItemType Directory -Force -Path $BinDir | Out-Null Expand-Archive -Path $zip -DestinationPath $BinDir -Force Remove-Item $zip -ErrorAction SilentlyContinue if (-not (Test-Path (Join-Path $BinDir 'opencode.exe'))) { # Some releases nest the exe in a folder inside the zip; surface it. $found = Get-ChildItem -Path $BinDir -Recurse -Filter 'opencode.exe' | Select-Object -First 1 if ($found) { Move-Item $found.FullName (Join-Path $BinDir 'opencode.exe') -Force } } if (-not (Test-Path (Join-Path $BinDir 'opencode.exe'))) { Say 'X the download unpacked without an opencode.exe inside - re-run this command,' Say " or install another way (npm i -g opencode-ai) and run it again." return } Say "Installed opencode to $BinDir." } # Put the bin dir on the user PATH (registry, for future windows) and on this # session's PATH, so `makemode` works right away. $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not $userPath) { $userPath = '' } if (($userPath -split ';') -notcontains $BinDir) { [Environment]::SetEnvironmentVariable('Path', ($userPath.TrimEnd(';') + ';' + $BinDir), 'User') } if (($env:Path -split ';') -notcontains $BinDir) { $env:Path = $env:Path + ';' + $BinDir } } # ---- 2. guard rails (permissions map) -------------------------------------- New-Item -ItemType Directory -Force -Path $MmDir | Out-Null $PermsJson = '{ "makemode_publish": "ask" }' try { $p = (Invoke-WebRequest -Uri "$Site/permissions.json" -UseBasicParsing).Content if ($p -and ($p | ConvertFrom-Json)) { $PermsJson = $p } } catch { Say ' (could not fetch the permissions map - using the safe default: publish asks first)' } Set-Content -Path (Join-Path $MmDir 'permissions.json') -Value $PermsJson -Encoding UTF8 # ---- 3. sign in / key ------------------------------------------------------ $Key = $ArgKey; $Token = ''; $Email = '' if (-not $Key) { $deviceCode = $ArgCode; $verifyUrl = '' if (-not $deviceCode) { try { $c = Invoke-RestMethod -Method Post -Uri "$Account/device/code" $deviceCode = $c.device_code; $verifyUrl = $c.verification_uri_complete } catch { } if ($deviceCode -and $verifyUrl) { Say '' Say 'Sign in to MakeMode - a browser tab should open; if not, open this link and confirm:' Say " $verifyUrl" Say '' try { Start-Process $verifyUrl } catch { } Write-Host 'Waiting for you to confirm in the browser' -NoNewline } } if ($deviceCode) { for ($t = 0; $t -lt 180; $t++) { try { $r = Invoke-RestMethod -Method Post -Uri "$Account/device/token" -ContentType 'application/json' -Body (@{ device_code = $deviceCode } | ConvertTo-Json -Compress) if ($r.access_token) { $Token = $r.access_token; $Email = "$($r.email)"; break } if ("$r" -match 'expired_token') { break } } catch { if ("$($_.ErrorDetails.Message)" -match 'expired_token') { break } } Write-Host '.' -NoNewline; Start-Sleep -Seconds 5 } if (-not $ArgCode) { Say '' } } if ($Token) { if ($Email) { Say "Signed in as $Email." } else { Say 'Signed in.' } $label = ($env:COMPUTERNAME -replace '[^A-Za-z0-9 ._-]', '') if (-not $label) { $label = 'machine' } if ($label.Length -gt 40) { $label = $label.Substring(0, 40) } try { $m = Invoke-RestMethod -Method Post -Uri "$Account/keys/mint" -Headers @{ Authorization = "Bearer $Token" } -ContentType 'application/json' -Body (@{ label = "terminal on $label" } | ConvertTo-Json -Compress) $Key = $m.key } catch { } if ($Key) { @{ access_token = $Token; email = $Email } | ConvertTo-Json -Compress | Set-Content -Path (Join-Path $MmDir 'session.json') -Encoding UTF8 Say "Made a MakeMode key for this machine (labelled ""terminal on $label"" on your account page - revoke it there any time)." } else { Say 'Signed in, but could not make a key (the account service may be an older version).' } } if (-not $Key) { Say 'Could not sign in from the browser. Create a key at https://account.makemode.eu instead.' $Key = Read-Host 'Paste your MakeMode API key (sk-gw-...)' } } if ($Key -notmatch '^sk-gw-[A-Za-z0-9_-]+$') { Say 'X that does not look like a MakeMode key (expected sk-gw-...). Create one at https://account.makemode.eu' return } # ---- 4. opencode config (mirrors setup.sh step 4) -------------------------- if ($Client -ne 'claude') { New-Item -ItemType Directory -Force -Path $CfgDir, $DataDir, (Join-Path $CfgDir 'themes') | Out-Null $ocJson = @" { "`$schema": "https://opencode.ai/config.json", "model": "makemode/glm-5.2", "theme": "makemode", "permission": $PermsJson, "provider": { "makemode": { "npm": "@ai-sdk/openai-compatible", "name": "MakeMode (EU, metered)", "options": { "baseURL": "$Gateway/v1" }, "models": { "mistral-small-3.2-24b-instruct-2506": { "name": "Mistral Small (EU, fast)", "limit": { "context": 128000, "output": 8192 } }, "devstral-2-123b-instruct-2512": { "name": "Devstral 2 (EU, coding)", "limit": { "context": 128000, "output": 8192 } }, "mistral-medium-3.5-128b": { "name": "Mistral Medium (EU, balanced)", "limit": { "context": 128000, "output": 8192 } }, "glm-5.2": { "name": "GLM 5.2 (EU)", "limit": { "context": 128000, "output": 8192 } }, "qwen3.5-397b-a17b": { "name": "Qwen 3.5 (EU, reasoning)", "limit": { "context": 256000, "output": 8192 } }, "kimi-k2.6": { "name": "Kimi K2.6 (EU, reasoning)", "limit": { "context": 256000, "output": 8192 } }, "kimi-k2.7-code": { "name": "Kimi K2.7 Code (EU, coding)", "limit": { "context": 256000, "output": 8192 } }, "deepseek-v4-pro": { "name": "DeepSeek V4 Pro (EU, agentic)", "limit": { "context": 256000, "output": 8192 } } } } } } "@ Set-Content -Path (Join-Path $CfgDir 'opencode.json') -Value $ocJson -Encoding UTF8 Set-Content -Path (Join-Path $DataDir 'auth.json') -Value "{ ""makemode"": { ""type"": ""api"", ""key"": ""$Key"" } }" -Encoding UTF8 # Brand theme + house rules - best effort, opencode runs fine without them. try { Invoke-WebRequest -Uri "$Site/makemode-theme.json" -OutFile (Join-Path $CfgDir 'themes\makemode.json') -UseBasicParsing } catch { } try { $rules = (Invoke-WebRequest -Uri "$Site/agent-rules.md" -UseBasicParsing).Content $agents = Join-Path $CfgDir 'AGENTS.md' $head = ' (installed by makemode.eu/setup.ps1 - re-running it refreshes this block; edit outside the markers)' $block = "$head`n$rules`n" if (Test-Path $agents) { $cur = Get-Content $agents -Raw $cur = $cur -replace '(?s).*?\r?\n?', '' Set-Content -Path $agents -Value ($cur.TrimEnd() + "`n`n" + $block) -Encoding UTF8 } else { Set-Content -Path $agents -Value $block -Encoding UTF8 } } catch { Say ' (could not fetch the house rules - the agent runs without them; re-run setup later)' } # The `makemode` command: a launcher next to opencode.exe. `makemode` opens # opencode on the gateway; `makemode publish` / `makemode open` hand off to # the same makemode-open.mjs CLI the Mac launcher uses (needs Node.js). $cmd = @' @echo off setlocal if /i "%~1"=="publish" goto handoff if /i "%~1"=="open" goto handoff "%~dp0opencode.exe" %* exit /b %errorlevel% :handoff where node >nul 2>nul if errorlevel 1 ( echo makemode %1 needs Node.js v20+ on Windows: https://nodejs.org - then run this again. exit /b 1 ) if not exist "%USERPROFILE%\.makemode" mkdir "%USERPROFILE%\.makemode" if not exist "%USERPROFILE%\.makemode\makemode-open.mjs" ( powershell -NoProfile -Command "Invoke-WebRequest -Uri 'https://makemode.eu/makemode-open.mjs' -OutFile '%USERPROFILE%\.makemode\makemode-open.mjs' -UseBasicParsing" ) node "%USERPROFILE%\.makemode\makemode-open.mjs" %* exit /b %errorlevel% '@ Set-Content -Path (Join-Path $BinDir 'makemode.cmd') -Value $cmd -Encoding ASCII } # ---- 5. Claude Code (for --claude / --both) -------------------------------- if ($Client -ne 'opencode') { $claudeDir = Join-Path $HOME '.claude' $settings = Join-Path $claudeDir 'settings.json' $envBlock = [ordered]@{ ANTHROPIC_BASE_URL = 'https://api.makemode.eu' ANTHROPIC_AUTH_TOKEN = $Key ANTHROPIC_MODEL = 'glm-5.2' ANTHROPIC_SMALL_FAST_MODEL = 'mistral-small-3.2-24b-instruct-2506' CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1' } if (Test-Path $settings) { Say '' Say "-> Claude Code: $settings already exists - leaving it alone." Say ' Merge this into it (top-level "env" object; create it if missing):' Say '' Say (' "env": ' + ($envBlock | ConvertTo-Json)) Say '' } else { New-Item -ItemType Directory -Force -Path $claudeDir | Out-Null @{ env = $envBlock } | ConvertTo-Json | Set-Content -Path $settings -Encoding UTF8 Say "-> Claude Code configured ($settings). Run: claude" } } # ---- done ------------------------------------------------------------------ Say '' Say 'Done. Everything runs through the gateway in France, billed to your MakeMode balance.' if ($Client -ne 'claude') { Say 'To build something, copy this line (it works in this window):' Say '' Say ' mkdir my-project; cd my-project; makemode' Say '' Say 'A new PowerShell window works too - the PATH change is saved for future windows.' }