# KidCAD update (V1.2.3) for Windows. Finds your KidCAD folder and the newest kidcad*.zip by itself. # Your family's things (the data folder), the AI/figure environments and your own printer profiles are kept. # Do NOT run this while a USB print is running: KidCAD restarts at the end and that stops a USB print. # powershell -ExecutionPolicy Bypass -File $HOME\Downloads\update_kidcad.ps1 $ErrorActionPreference = 'Continue' function Say($t) { Write-Host "`n==> $t" -ForegroundColor Magenta } function Warn($t) { Write-Host "[!] $t" -ForegroundColor Yellow } function Die($t) { Write-Host "[x] $t" -ForegroundColor Red; Read-Host 'Press Enter to close'; exit 1 } # ---- 1. the newest KidCAD zip for Windows (Downloads, Desktop, home) $places = @((Join-Path $HOME 'Downloads'), [Environment]::GetFolderPath('Desktop'), $HOME) | Where-Object { Test-Path $_ } $ZIP = Get-ChildItem -Path $places -Filter 'kidcad*.zip' -File -Depth 1 -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '_(linux|mac|source)\.zip$' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $ZIP) { Die 'No kidcad*.zip found in Downloads, Desktop or your home folder.' } Say ("Found this download: {0} ({1:MMM dd HH:mm}, {2:N1} MB)" -f $ZIP.FullName, $ZIP.LastWriteTime, ($ZIP.Length / 1MB)) $ok = Read-Host 'Install this one? Only say yes if you downloaded it yourself from kidcad.xyz or GitHub. (y/N)' if ($ok -notmatch '^[Yy]') { Die 'Nothing changed.' } # ---- 2. the installed KidCAD folder (the one with app\server.py and its .venv) $roots = @(Get-ChildItem -Path $HOME -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin @('AppData', 'OneDrive') -and $_.Name -notlike 'kidcad_update_tmp*' } | ForEach-Object { $_.FullName }) $DIRS = @(Get-ChildItem -Path $roots -Filter server.py -File -Recurse -Depth 3 -ErrorAction SilentlyContinue | Where-Object { $_.Directory.Name -eq 'app' -and $_.FullName -notmatch '\\(AppData|\.venv|kidcad_update_tmp)' } | ForEach-Object { $_.Directory.Parent.FullName } | Where-Object { Test-Path (Join-Path $_ '.venv\Scripts\python.exe') } | Select-Object -Unique) if ($DIRS.Count -eq 0) { Die "Couldn't find an installed KidCAD folder (one with app\server.py and .venv)." } if ($DIRS.Count -gt 1) { Write-Host 'More than one KidCAD folder found:' for ($i = 0; $i -lt $DIRS.Count; $i++) { Write-Host (" {0}) {1}" -f ($i + 1), $DIRS[$i]) } $n = Read-Host 'Which one should be updated? (number)' if (-not ($n -match '^\d+$') -or [int]$n -lt 1 -or [int]$n -gt $DIRS.Count) { Die 'No folder picked. Nothing changed.' } $K = $DIRS[[int]$n - 1] } else { $K = $DIRS[0] } Say "Updating KidCAD in: $K" # ---- 3. never during a USB print $busy = Read-Host 'Is the printer printing over USB right now? (y/N)' if ($busy -match '^[Yy]') { Die 'Run this again after the print is done. Nothing changed.' } # ---- 4. unpack and check the download $TMP = Join-Path $HOME ("kidcad_update_tmp_" + $PID) Remove-Item -Recurse -Force $TMP -ErrorAction SilentlyContinue try { Expand-Archive -Path $ZIP.FullName -DestinationPath $TMP -Force } catch { Die 'The zip could not be opened. Download it again.' } $srv = Get-ChildItem -Path $TMP -Filter server.py -File -Recurse -Depth 3 | Where-Object { $_.Directory.Name -eq 'app' } | Select-Object -First 1 if (-not $srv) { Remove-Item -Recurse -Force $TMP; Die "That zip doesn't look like KidCAD." } $SRC = $srv.Directory.Parent.FullName if (-not (Test-Path (Join-Path $SRC 'run.ps1'))) { Remove-Item -Recurse -Force $TMP; Die "That zip is not the Windows version of KidCAD (kidcad_..._windows.zip)." } Get-ChildItem -Path $SRC -Recurse -File | Unblock-File $newV = Get-Content (Join-Path $SRC 'VERSION') -ErrorAction SilentlyContinue $oldV = Get-Content (Join-Path $K 'VERSION') -ErrorAction SilentlyContinue Write-Host "New version: $newV Installed: $oldV" # ---- 5. safety copy of the program and the family's data (only the newest copy is kept) $BK = Join-Path $HOME 'kidcad_backups' New-Item -ItemType Directory -Force $BK | Out-Null $STAMP = Get-Date -Format 'yyyyMMdd_HHmmss' $drive = (Get-Item $HOME).PSDrive $freeGB = [math]::Floor($drive.Free / 1GB) if ($freeGB -lt 10) { Warn "Less than 10 GB free on the disk ($freeGB GB): no safety copy this time. Your last safety copy in $BK is kept." } else { Say "Safety copy -> $BK" $good = $true $skip = '\\(data|logs|\.venv|engines\\\.venv-figures|engines\\Hunyuan3D-2|engines\\\.venv-triposg|engines\\TripoSG|__pycache__)(\\|$)' $files = Get-ChildItem -Path $K -Recurse -File | Where-Object { $_.FullName.Substring($K.Length) -notmatch $skip } $stage = Join-Path $env:TEMP "kidcad_prog_$STAMP" foreach ($f in $files) { $to = Join-Path $stage $f.FullName.Substring($K.Length); New-Item -ItemType Directory -Force (Split-Path $to) | Out-Null; Copy-Item -LiteralPath $f.FullName -Destination $to } try { Compress-Archive -Path (Join-Path $stage '*') -DestinationPath (Join-Path $BK "program_$STAMP.zip") -Force; Write-Host 'program saved' } catch { $good = $false } Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue $data = Join-Path $K 'data' if (Test-Path $data) { $stage = Join-Path $env:TEMP "kidcad_data_$STAMP" Get-ChildItem -Path $data -Recurse -File | Where-Object { $_.FullName -notmatch '\\data\\tmp\\' -and $_.Extension -ne '.gcode' } | ForEach-Object { $to = Join-Path $stage $_.FullName.Substring($K.Length); New-Item -ItemType Directory -Force (Split-Path $to) | Out-Null; Copy-Item -LiteralPath $_.FullName -Destination $to } try { Compress-Archive -Path (Join-Path $stage '*') -DestinationPath (Join-Path $BK "data_$STAMP.zip") -Force; Write-Host 'family data saved' } catch { $good = $false } Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue } if ($good) { Get-ChildItem $BK -File | Where-Object { $_.Name -match '^(program|data)_\d+_\d+\.zip$' -and $_.Name -notmatch $STAMP } | Remove-Item -Force Write-Host 'older safety copies removed (the newest one is kept)' } else { Warn 'The safety copy had a problem, so the older safety copies were kept.' } } # ---- 6. stop KidCAD (Windows locks the files a running program uses), then put the new program in place Say 'Stopping KidCAD for the update' Get-CimInstance Win32_Process -Filter "Name='python.exe' OR Name='pythonw.exe'" | Where-Object { $_.CommandLine -match 'app\.server|figurine_worker\.py' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep -Seconds 3 Say 'Copying the new KidCAD files' $xd = @('data', 'logs', '.venv', 'engines\.venv-figures', 'engines\Hunyuan3D-2', 'engines\.venv-triposg', 'engines\TripoSG', 'app\printers') | ForEach-Object { Join-Path $SRC $_ } robocopy $SRC $K /E /NFL /NDL /NJH /NJS /NP /XD @xd __pycache__ | Out-Null New-Item -ItemType Directory -Force (Join-Path $K 'app\printers') | Out-Null Get-ChildItem (Join-Path $SRC 'app\printers') -Filter *.json | ForEach-Object { $to = Join-Path $K "app\printers\$($_.Name)"; if (-not (Test-Path $to)) { Copy-Item $_.FullName $to } } # new printers added, yours untouched Remove-Item -Recurse -Force $TMP -ErrorAction SilentlyContinue # ---- 7. Python packages Say 'Python packages' $VPY = Join-Path $K '.venv\Scripts\python.exe' & $VPY -m pip install -q --upgrade -r (Join-Path $K 'requirements.txt') $FPY = Join-Path $K 'engines\.venv-figures\Scripts\python.exe' if (Test-Path $FPY) { Say 'Figure maker packages (keeps the versions that work)' & $FPY -m pip install -q "transformers<5" "huggingface-hub<1.0" diffusers } # ---- 8. check-up and restart Say 'Check-up' $env:PYTHONUTF8 = '1' Push-Location $K; & $VPY -m app.doctor; Pop-Location Say 'Restarting KidCAD' Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$K\run.ps1`" -Restart" -WorkingDirectory $K Start-Sleep -Seconds 8 $port = if ($env:KIDCAD_PORT) { $env:KIDCAD_PORT } else { '7860' } try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 "http://127.0.0.1:$port/api/auth" | Out-Null Say "Done! KidCAD $newV is running: http://localhost:$port" } catch { Warn "KidCAD didn't answer yet. Give it a few seconds, or start it from the desktop icon." Write-Host "If it still won't start, the old program is in $BK\program_$STAMP.zip" } Read-Host 'Press Enter to close'