# Sets a Windows computer up to build Metal America tools with Claude. # # irm https://labs.metal-america.com/install.ps1 | iex # # It installs what is missing and skips what is there: Git, Node, the Google # Cloud CLI, Claude Code and the developer kit. Everything goes into your own # user folder. It signs you in to Google with your Metal America account, # which opens your browser once. # # This file is public and holds no secret. The kit itself is private: it is # downloaded at the end, and only for somebody signed in as staff. # # Safe to run again. $env:MA_SETUP_SKIP_SIGN_IN = '1' stops before the sign-in, for tests. $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # A progress bar makes a download many times slower. $NodeMajor = 24 # The profile folder as the environment names it, which is what Node and the others use too. $Mine = $env:USERPROFILE $Root = Join-Path $Mine '.metal-america' $KitBucket = 'https://storage.googleapis.com/ma-developer-kit' $Arm = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' function Say($line) { Write-Host " $line" } function Step($line) { Write-Host "`n $line" } function Stop-Setup { Write-Host '' foreach ($line in $args) { Write-Host " $line" } Write-Host '' throw 'The setup stopped. Read the lines above.' } function Have($name) { [bool](Get-Command $name -ErrorAction SilentlyContinue) } # Runs a program that may write to its error stream without that ending the # script: in Windows PowerShell, with errors set to stop, any such line does # (measured 2026-10-12, on the Google CLI's installer). Returns what it printed. function Invoke-Calmly([scriptblock]$block) { $before = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & $block 2>$null } finally { $ErrorActionPreference = $before } } # Every new terminal should find what is installed here. function Add-UserPath($dir) { $mine = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not $mine) { $mine = '' } if (($mine -split ';') -notcontains $dir) { [Environment]::SetEnvironmentVariable('Path', "$dir;$mine", 'User') } if (($env:Path -split ';') -notcontains $dir) { $env:Path = "$dir;$env:Path" } } function Expand-Zip($zip, $into) { New-Item -ItemType Directory -Force -Path $into | Out-Null # `tar` is part of Windows 10 and later, and far quicker than Expand-Archive. tar -xf $zip -C $into if ($LASTEXITCODE -ne 0) { Expand-Archive -Path $zip -DestinationPath $into -Force } } New-Item -ItemType Directory -Force -Path $Root | Out-Null # --- Git ----------------------------------------------------------------------- Step '1 of 5 Git' if (-not (Have git)) { if (-not (Have winget)) { Stop-Setup 'This computer has no Git, and no winget to install it with.' 'Install Git from https://git-scm.com/download/win, then run this line again.' } Say 'Installing Git.' winget install --id Git.Git -e --silent --scope user --accept-source-agreements --accept-package-agreements | Out-Null foreach ($dir in "$env:LOCALAPPDATA\Programs\Git\cmd", "$env:ProgramFiles\Git\cmd") { if (Test-Path $dir) { Add-UserPath $dir } } if (-not (Have git)) { Stop-Setup 'Git could not be installed automatically.' 'Install it from https://git-scm.com/download/win, then run this line again.' } } Say 'Git is here.' # --- Node ------------------------------------------------------------------------ Step '2 of 5 Node' function Test-Node { (Have node) -and ([int](node -p 'process.versions.node.split(\".\")[0]') -ge $NodeMajor) } if (-not (Have node) -and (Test-Path "$Root\node\node.exe")) { Add-UserPath "$Root\node" } if (Test-Node) { Say "Node $(node --version) is here." } else { $dist = "https://nodejs.org/dist/latest-v$NodeMajor.x" $sums = (Invoke-WebRequest -UseBasicParsing "$dist/SHASUMS256.txt").Content -split "`n" $want = if ($Arm) { 'win-arm64.zip' } else { 'win-x64.zip' } $line = $sums | Where-Object { $_ -like "*$want" } | Select-Object -First 1 if (-not $line) { Stop-Setup "Could not find Node $NodeMajor for this computer." 'Tell engineering.' } $sum, $file = ($line.Trim() -split '\s+') Say "Installing $($file -replace '\.zip$', '') into your user folder." $zip = Join-Path $Root $file Invoke-WebRequest -UseBasicParsing "$dist/$file" -OutFile $zip if ((Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() -ne $sum.ToLower()) { Stop-Setup 'The Node download does not match what Node published. Nothing was installed.' 'Run this line again.' } $node = Join-Path $Root 'node' Remove-Item -Recurse -Force $node, "$node-new" -ErrorAction SilentlyContinue Expand-Zip $zip "$node-new" Move-Item (Get-ChildItem "$node-new" -Directory | Select-Object -First 1).FullName $node Remove-Item -Recurse -Force "$node-new", $zip Add-UserPath $node if (-not (Test-Node)) { Stop-Setup 'Node was installed but does not run.' 'Tell engineering.' } Say "Node $(node --version) is installed." } # pnpm, which installs a tool's building blocks, comes with Node this way. if (-not (Have pnpm)) { Invoke-Calmly { corepack enable } | Out-Null } # --- Google Cloud CLI -------------------------------------------------------------- Step '3 of 5 Google Cloud CLI' $sdk = Join-Path $Mine 'google-cloud-sdk' if (-not (Have gcloud) -and (Test-Path "$sdk\bin\gcloud.cmd")) { Add-UserPath "$sdk\bin" } if (Have gcloud) { Say 'The Google Cloud CLI is here.' } else { Say 'Installing the Google Cloud CLI into your user folder. This takes a few minutes.' # The "bundled-python" archive carries the Python it runs on; the ARM one has none to carry. $name = if ($Arm) { 'google-cloud-cli-windows-arm.zip' } else { 'google-cloud-cli-windows-x86_64-bundled-python.zip' } $zip = Join-Path $Root $name Invoke-WebRequest -UseBasicParsing "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/$name" -OutFile $zip Expand-Zip $zip $Mine Remove-Item -Force $zip Invoke-Calmly { & "$sdk\install.bat" --quiet --usage-reporting=false --path-update=false } | Out-Null Add-UserPath "$sdk\bin" if (-not (Have gcloud)) { Stop-Setup 'The Google Cloud CLI was installed but does not run.' 'Tell engineering.' } Say 'The Google Cloud CLI is installed.' } # --- Claude Code ------------------------------------------------------------------- Step '4 of 5 Claude Code' $local = Join-Path $Mine '.local\bin' if (-not (Have claude) -and (Test-Path "$local\claude.exe")) { Add-UserPath $local } if (Have claude) { Say 'Claude Code is here.' } else { Say 'Installing Claude Code.' Invoke-RestMethod https://claude.ai/install.ps1 | Invoke-Expression | Out-Null Add-UserPath $local if (-not (Have claude)) { Stop-Setup 'Claude Code was installed but does not run.' 'Tell engineering.' } Say 'Claude Code is installed.' } if ($env:MA_SETUP_SKIP_SIGN_IN -eq '1') { Step 'Stopping before the sign-in, as asked.' return } # --- Google sign-in, then the kit ------------------------------------------------------ Step '5 of 5 Your Metal America sign-in, and the developer kit' function Get-Token { $token = Invoke-Calmly { gcloud auth print-access-token } if ($LASTEXITCODE -eq 0 -and $token) { "$token".Trim() } else { $null } } if (-not (Get-Token)) { Say 'Your browser is about to open. Choose your Metal America account and click Allow.' # Left to print: if the browser does not open, the address to visit is what it prints. gcloud auth login --quiet if (-not (Get-Token)) { Stop-Setup 'The Google sign-in was not finished.' 'Run this line again, and choose your Metal America account in the browser.' } } Say "Signed in to Google as $(Invoke-Calmly { gcloud config get-value account })." $setup = Join-Path $Root 'setup.cjs' try { Invoke-WebRequest -UseBasicParsing "$KitBucket/setup.cjs" -Headers @{ Authorization = "Bearer $(Get-Token)" } -OutFile $setup } catch { Stop-Setup 'Google would not let this account read the developer kit.' 'Sign in with your Metal America account, not a personal one: gcloud auth login' 'Then run this line again.' } node $setup $installed = $LASTEXITCODE Remove-Item -Force $setup if ($installed -ne 0) { Stop-Setup 'The developer kit was not installed. The lines above say why.' } Write-Host "`n All set. Close this window and open a new PowerShell, then:`n" Write-Host ' mkdir ~\coding\my-tool; cd ~\coding\my-tool; claude' Write-Host "`n The first time, Claude opens your browser to sign in. Then type /new-project`n"