@@ -462,6 +462,8 @@ class UnattendedRequest(BaseModel):
462462 username : str = "user"
463463 password : str = "password"
464464 timezone : str = "Europe/Vienna"
465+ dev_tools : list [str ] | None = None # dev tool keys (python, git, node, etc.)
466+ use_host_ollama : bool = False # set OLLAMA_HOST to host gateway
465467
466468
467469# ── Template Storage ─────────────────────────────────────────────────────
@@ -1084,8 +1086,29 @@ def _generate_ubuntu_autoinstall(hostname: str, username: str, password: str, ti
10841086"""
10851087
10861088
1087- def _generate_autounattend (hostname : str , username : str , password : str , timezone : str ) -> str :
1088- """Generate Windows autounattend.xml content."""
1089+ def _generate_autounattend (
1090+ hostname : str , username : str , password : str , timezone : str ,
1091+ dev_tools : list [str ] | None = None , use_host_ollama : bool = False ,
1092+ ) -> str :
1093+ """Generate Windows autounattend.xml content.
1094+
1095+ If dev_tools is provided, injects a FirstLogonCommand that installs
1096+ the selected dev tooling via winget + pip + npm.
1097+ """
1098+ dev_script = _generate_win_dev_setup_ps1 (username , dev_tools , use_host_ollama )
1099+ first_logon = ""
1100+ if dev_script :
1101+ escaped = dev_script .replace ("&" , "&" ).replace ("<" , "<" ).replace (">" , ">" ).replace ('"' , """ )
1102+ first_logon = f"""
1103+ <FirstLogonCommands>
1104+ <SynchronousCommand wcm:action="add">
1105+ <CommandLine>powershell -ExecutionPolicy Bypass -Command "{ escaped } "</CommandLine>
1106+ <Description>Dev Environment Setup</Description>
1107+ <Order>1</Order>
1108+ <RequiresUserInput>false</RequiresUserInput>
1109+ </SynchronousCommand>
1110+ </FirstLogonCommands>"""
1111+
10891112 return f"""<?xml version="1.0" encoding="utf-8"?>
10901113<unattend xmlns="urn:schemas-microsoft-com:unattend">
10911114 <settings pass="windowsPE">
@@ -1138,12 +1161,86 @@ def _generate_autounattend(hostname: str, username: str, password: str, timezone
11381161 <Model>Automated VM</Model>
11391162 </OEMInformation>
11401163 <TimeZone>{ timezone } </TimeZone>
1164+ { first_logon }
11411165 </component>
11421166 </settings>
11431167</unattend>
11441168"""
11451169
11461170
1171+ def _generate_win_dev_setup_ps1 (
1172+ username : str = "user" ,
1173+ tools : list [str ] | None = None ,
1174+ use_host_ollama : bool = False ,
1175+ ) -> str :
1176+ """Generate a PowerShell script to install dev tools in a Windows VM.
1177+
1178+ Runs winget installs silently, then pip/npm setup. Uses the same tool IDs
1179+ as the Sandbox dev setup.
1180+ """
1181+ if not tools :
1182+ return ""
1183+
1184+ tool_map = {
1185+ "python" : "Python.Python.3.12" ,
1186+ "git" : "Git.Git" ,
1187+ "node" : "OpenJS.NodeJS.LTS" ,
1188+ "just" : "Casey.Just" ,
1189+ "vscode" : "Microsoft.VisualStudioCode" ,
1190+ "notepad++" : "Notepad++.Notepad++" ,
1191+ "uv" : "astral-sh.uv" ,
1192+ "windsurf" : "Codeium.Windsurf" ,
1193+ "cursor" : "Anysphere.Cursor" ,
1194+ "antigravity" : "Google.Antigravity" ,
1195+ }
1196+
1197+ winget_ids = [
1198+ tool_map [t ] for t in tools if t in tool_map
1199+ ]
1200+ has_python = "python" in tools
1201+ has_claude = any (t in ("claude_desktop" , "claudesktop" , "claudedesktop" ) for t in tools )
1202+
1203+ lines = [
1204+ "# Dev Environment Setup (generated by virtualization-mcp)" ,
1205+ "$logFile = \" $env:USERPROFILE\\ Desktop\\ dev-setup.log\" " ,
1206+ "Start-Transcript -Path $logFile -Append" ,
1207+ "Write-Host '--- Dev Environment Setup ---' -ForegroundColor Cyan" ,
1208+ ]
1209+
1210+ for wid in winget_ids :
1211+ lines .append (
1212+ f'Write-Host "Installing { wid } ..." -ForegroundColor Yellow; '
1213+ f'& winget install -e --id { wid } --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null'
1214+ )
1215+
1216+ if has_python :
1217+ lines .append (
1218+ 'Write-Host "Upgrading pip..." -ForegroundColor Yellow; '
1219+ 'python -m pip install --upgrade pip 2>&1 | Out-Null'
1220+ )
1221+
1222+ if has_claude :
1223+ lines .append (
1224+ 'Write-Host "Downloading Claude Desktop..." -ForegroundColor Yellow; '
1225+ '$msix = \" $env:TEMP\\ Claude.msix\" ; '
1226+ 'Invoke-WebRequest -Uri "https://claude.ai/api/desktop/win32/x64/msix/latest/redirect" -OutFile $msix -UseBasicParsing; '
1227+ 'Add-AppxPackage -Path $msix -ErrorAction SilentlyContinue'
1228+ )
1229+
1230+ if use_host_ollama :
1231+ lines .append (
1232+ '$gw = (Get-NetRoute -DestinationPrefix "0.0.0.0/0" -ErrorAction SilentlyContinue | Select-Object -First 1).NextHop; '
1233+ 'if ($gw) { '
1234+ '$env:OLLAMA_HOST = \" http://$gw`:11434\" ; '
1235+ '[Environment]::SetEnvironmentVariable(\" OLLAMA_HOST\" , $env:OLLAMA_HOST, \" User\" ) '
1236+ '}'
1237+ )
1238+
1239+ lines .append ("Write-Host 'Dev setup complete!' -ForegroundColor Green" )
1240+ lines .append ("Stop-Transcript" )
1241+ return "; " .join (lines )
1242+
1243+
11471244UNATTENDED_DIR = os .path .join (_repo_root , "assets" , "unattended" )
11481245
11491246
@@ -1174,15 +1271,21 @@ async def setup_unattended_install(name: str, request: UnattendedRequest):
11741271 "message" : "Cloud-init ISO generated. Attach it as the secondary drive after the main ISO." ,
11751272 }
11761273 elif os_type == "windows" :
1177- content = _generate_autounattend (request .hostname , request .username , request .password , request .timezone )
1274+ content = _generate_autounattend (
1275+ request .hostname , request .username , request .password , request .timezone ,
1276+ dev_tools = request .dev_tools , use_host_ollama = request .use_host_ollama ,
1277+ )
11781278 dest = os .path .join (UNATTENDED_DIR , f"{ name } -autounattend.xml" )
11791279 with open (dest , "w" ) as f :
11801280 f .write (content )
1281+ msg = "autounattend.xml generated."
1282+ if request .dev_tools :
1283+ msg += f" Dev tools ({ len (request .dev_tools )} selected) will install on first login."
11811284 return {
11821285 "success" : True ,
11831286 "os_type" : "windows" ,
11841287 "file" : dest ,
1185- "message" : "autounattend.xml generated. Attach it via VBoxManage storageattach." ,
1288+ "message" : msg ,
11861289 }
11871290 else :
11881291 raise HTTPException (status_code = 400 , detail = f"Unsupported OS type: { os_type } " )
0 commit comments