<# .SYNOPSIS Bootstrap a Python virtualenv and install the project on Windows (PowerShell). .DESCRIPTION Creates a Python virtual environment (default: .venv), upgrades pip, installs the project (either editable or normal), and optionally creates Desktop and Start Menu shortcuts. .EXAMPLE # Create .venv and install in editable mode, create Desktop shortcut .\scripts\bootstrap.ps1 -Editable -CreateDesktopShortcut .EXAMPLE # Use a specific python executable and force overwrite existing venv .\scripts\bootstrap.ps1 -Python "C:\\Python39\\python.exe" -Force # Note: you may need to run PowerShell with ExecutionPolicy Bypass: # powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 -Editable #> param( [switch]$Editable, [switch]$CreateDesktopShortcut, [switch]$CreateStartMenuShortcut, [string]$VenvPath = ".venv", [string]$Python = "", [switch]$Force, [switch]$NoInstall, [switch]$Quiet ) function Write-Log { param([string]$msg,[string]$lvl="INFO") if (-not $Quiet) { if ($lvl -eq "ERROR") { Write-Host "[$lvl] $msg" -ForegroundColor Red } else { Write-Host "[$lvl] $msg" } } } function Find-Python { param([string]$preferred) $candidates = @() if ($preferred -and $preferred.Trim()) { $candidates += $preferred } $candidates += @("python","python3","py") foreach ($c in $candidates) { try { if ($c -eq "py") { $out = & py -3 -c "import sys, json; print(sys.executable)" 2>$null if ($out) { return $out.Trim() } } else { $out = & $c -c "import sys, json; print(sys.executable)" 2>$null if ($out) { return $out.Trim() } } } catch {} } return $null } # Resolve OS detection in a broad-compatible way try { $IsWindowsPlatform = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) } catch { $IsWindowsPlatform = $env:OS -match 'Windows' } # operate from repo root (parent of scripts dir) $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path Set-Location $repoRoot $pythonExe = Find-Python -preferred $Python if (-not $pythonExe) { Write-Log "No Python interpreter found. Specify -Python or install Python." "ERROR"; exit 2 } Write-Log "Using Python: $pythonExe" # Full venv path try { $venvFull = (Resolve-Path -LiteralPath $VenvPath -ErrorAction SilentlyContinue).Path } catch { $venvFull = $null } if (-not $venvFull) { $venvFull = (Join-Path $repoRoot $VenvPath) } # Handle existing venv $venvExists = Test-Path $venvFull if ($venvExists) { if ($Force) { Write-Log "Removing existing venv at $venvFull" Remove-Item -Recurse -Force $venvFull $venvExists = $false } else { # Quick health check: does the existing venv have a python executable? $venvPy1 = Join-Path $venvFull "Scripts\python.exe" $venvPy2 = Join-Path $venvFull "bin/python" $venvHasPython = $false try { if (Test-Path $venvPy1 -PathType Leaf -ErrorAction SilentlyContinue) { $venvHasPython = $true } elseif (Test-Path $venvPy2 -PathType Leaf -ErrorAction SilentlyContinue) { $venvHasPython = $true } } catch {} if (-not $venvHasPython) { if ($Quiet) { Write-Log "Existing venv appears incomplete or broken and quiet mode prevents prompting. Use -Force to recreate." "ERROR" exit 4 } $ans = Read-Host "$venvFull exists but appears invalid (no python executable). Overwrite to recreate? (y/N)" if ($ans -eq 'y' -or $ans -eq 'Y') { Write-Log "Removing broken venv at $venvFull" Remove-Item -Recurse -Force $venvFull $venvExists = $false } else { Write-Log "Aborted due to broken venv." "ERROR"; exit 4 } } else { if ($Quiet) { Write-Log "Using existing venv at $venvFull (quiet mode)" "INFO" } else { $ans = Read-Host "$venvFull already exists. Overwrite? (y/N) (default: use existing venv)" if ($ans -eq 'y' -or $ans -eq 'Y') { Write-Log "Removing existing venv at $venvFull" Remove-Item -Recurse -Force $venvFull $venvExists = $false } else { Write-Log "Continuing using existing venv at $venvFull" "INFO" } } } } } if (-not (Test-Path $venvFull)) { Write-Log "Creating venv at $venvFull" try { & $pythonExe -m venv $venvFull } catch { Write-Log "Failed to create venv: $_" "ERROR"; exit 3 } } else { Write-Log "Using existing venv at $venvFull" "INFO" } # Determine venv python executable $venvPython = Join-Path $venvFull "Scripts\python.exe" if (-not (Test-Path $venvPython)) { $venvPython = Join-Path $venvFull "bin/python" } if (-not (Test-Path $venvPython)) { Write-Log "Created venv but could not find python inside it." "ERROR"; exit 4 } Write-Log "Using venv python: $venvPython" if (-not $NoInstall) { Write-Log "Upgrading pip, setuptools, wheel" try { & $venvPython -m pip install -U pip setuptools wheel } catch { Write-Log "pip upgrade failed: $_" "ERROR"; exit 5 } if ($Editable) { $editable_label = "(editable)" } else { $editable_label = "" } Write-Log ("Installing project {0}" -f $editable_label) try { if ($Editable) { & $venvPython -m pip install -e . } else { & $venvPython -m pip install . } } catch { Write-Log "pip install failed: $_" "ERROR"; exit 6 } } else { Write-Log "Skipping install (--NoInstall set)" } # Install Deno (official installer) - installed automatically try { $denoCmd = Get-Command 'deno' -ErrorAction SilentlyContinue } catch { $denoCmd = $null } if ($denoCmd) { Write-Log "Deno is already installed: $($denoCmd.Path)" } else { Write-Log "Installing Deno via official installer (https://deno.land)" try { try { irm https://deno.land/install.ps1 | iex } catch { iwr https://deno.land/install.ps1 -UseBasicParsing | iex } # Ensure common install locations are on PATH for this session $denoCandidatePaths = @( Join-Path $env:USERPROFILE ".deno\bin", Join-Path $env:LOCALAPPDATA "deno\bin" ) foreach ($p in $denoCandidatePaths) { if (Test-Path $p) { if ($env:PATH -notmatch [regex]::Escape($p)) { $env:PATH = $env:PATH + ";" + $p } } } $v = & deno --version 2>$null if ($v) { Write-Log "Deno installed: $v" } else { Write-Log "Deno installer completed but 'deno' not found on PATH; you may need to restart your shell or add the Deno bin folder to PATH." "ERROR" } } catch { Write-Log "Deno install failed: $_" "ERROR" } } # Shortcuts (Windows only) if ($IsWindowsPlatform) { if ($CreateDesktopShortcut -or $CreateStartMenuShortcut) { $wsh = New-Object -ComObject WScript.Shell $mmExe = Join-Path $venvFull "Scripts\mm.exe" $target = $null $args = "" if (Test-Path $mmExe) { $target = $mmExe } else { $target = $venvPython $args = "-m medeia_macina.cli_entry" } if ($CreateDesktopShortcut) { $desk = [Environment]::GetFolderPath('Desktop') $link = Join-Path $desk "Medeia-Macina.lnk" Write-Log "Creating Desktop shortcut: $link" $sc = $wsh.CreateShortcut($link) $sc.TargetPath = $target $sc.Arguments = $args $sc.WorkingDirectory = $repoRoot $sc.IconLocation = "$target,0" $sc.Save() } if ($CreateStartMenuShortcut) { $start = Join-Path ([Environment]::GetFolderPath('ApplicationData')) 'Microsoft\Windows\Start Menu\Programs' $dir = Join-Path $start "Medeia-Macina" New-Item -ItemType Directory -Path $dir -Force | Out-Null $link2 = Join-Path $dir "Medeia-Macina.lnk" Write-Log "Creating Start Menu shortcut: $link2" $sc2 = $wsh.CreateShortcut($link2) $sc2.TargetPath = $target $sc2.Arguments = $args $sc2.WorkingDirectory = $repoRoot $sc2.IconLocation = "$target,0" $sc2.Save() } } } # Install global 'mm' launcher into the user's bin directory so it can be invoked from any shell. try { $globalBin = Join-Path $env:USERPROFILE 'bin' New-Item -ItemType Directory -Path $globalBin -Force | Out-Null $mmCmd = Join-Path $globalBin 'mm.cmd' $mmPs1 = Join-Path $globalBin 'mm.ps1' $repo = $repoRoot $cmdText = @" @echo off set "REPO=__REPO__" if exist "%REPO%\.venv\Scripts\mm.exe" ( "%REPO%\.venv\Scripts\mm.exe" %* exit /b %ERRORLEVEL% ) if exist "%REPO%\.venv\Scripts\python.exe" ( "%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %* exit /b %ERRORLEVEL% ) python -m medeia_macina.cli_entry %* "@ # Inject actual repo path safely (escape double-quotes if any) $cmdText = $cmdText.Replace('__REPO__', $repo.Replace('"', '""')) if (Test-Path $mmCmd) { $bak = "$mmCmd.bak$(Get-Date -UFormat %s)" Move-Item -Path $mmCmd -Destination $bak -Force } Set-Content -LiteralPath $mmCmd -Value $cmdText -Encoding UTF8 # PowerShell shim: use single-quoted here-string so literal PowerShell variables # (like $args) are not expanded by this script when writing the file. $ps1Text = @' Param([Parameter(ValueFromRemainingArguments=$true)] $args) $repo = "__REPO__" $venv = Join-Path $repo '.venv' $exe = Join-Path $venv 'Scripts\mm.exe' if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE } $py = Join-Path $venv 'Scripts\python.exe' if (Test-Path $py) { & $py -m medeia_entry @args; exit $LASTEXITCODE } # fallback python -m medeia_entry @args '@ # Inject the actual repo path safely (escape embedded double-quotes if any) $ps1Text = $ps1Text.Replace('__REPO__', $repo.Replace('"', '""')) # Ensure the PowerShell shim falls back to the correct module when the venv isn't present $ps1Text = $ps1Text.Replace(' -m medeia_entry ', ' -m medeia_macina.cli_entry ') $ps1Text = $ps1Text.Replace('python -m medeia_entry', 'python -m medeia_macina.cli_entry') if (Test-Path $mmPs1) { $bak = "$mmPs1.bak$(Get-Date -UFormat %s)" Move-Item -Path $mmPs1 -Destination $bak -Force } Set-Content -LiteralPath $mmPs1 -Value $ps1Text -Encoding UTF8 # Ensure user's bin is on PATH (User env var) try { $cur = [Environment]::GetEnvironmentVariable('PATH', 'User') if ($cur -notlike "*$globalBin*") { if ($cur) { $new = ($globalBin + ';' + $cur) } else { $new = $globalBin } [Environment]::SetEnvironmentVariable('PATH', $new, 'User') # Update current session PATH for immediate use $env:PATH = $globalBin + ';' + $env:PATH Write-Log "Added $globalBin to User PATH. Restart your shell to pick this up." "INFO" } else { Write-Log "$globalBin is already on the User PATH" "INFO" } } catch { Write-Log "Failed to update user PATH: $_" "ERROR" } } catch { Write-Log "Failed to install global launcher: $_" "ERROR" } Write-Log "Bootstrap complete." "INFO" Write-Host "" Write-Host "To activate the venv:" if ($IsWindowsPlatform) { Write-Host " PS> .\$VenvPath\Scripts\Activate.ps1" Write-Host " CMD> .\$VenvPath\Scripts\activate.bat" } else { Write-Host " $ source ./$VenvPath/bin/activate" } Write-Host "" Write-Host "To run the app:" Write-Host " $ .\$VenvPath\Scripts\mm.exe (Windows) or" Write-Host " $ ./$VenvPath/bin/mm (Linux) or" Write-Host " $ $venvPython -m medeia_macina.cli_entry"