feat: add Chinese UI, Responses models, and mirrored Windows setup
CI / Python 3.12 / ubuntu-latest (push) Canceled after 0s
CI / Python 3.12 / windows-latest (push) Canceled after 0s
CI / Frontend tests and production build (push) Canceled after 0s
CI / Build and smoke-test Python packages (push) Canceled after 0s

Localize the web console and persist custom model and capability settings.
Use OpenAI Responses exclusively and move runtime limits into environment configuration.
Bootstrap uv and the virtual environment on Windows, support optional China mirrors,
and package source distributions without environments, build output, or credentials.

Update regression tests and repository quality checks for the new behavior.
This commit is contained in:
chenmingxuan committed 2026-09-22 19:16:17 +08:00
1 parent 371aa6df56
commit 2ddec53954
183 files changed
+3427 -11857

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
# Python bootstrap shared by the Windows launcher and its isolated checks.
# Keep this file ASCII for Windows PowerShell 5.1.
function Initialize-ArtemisPython {
param([Parameter(Mandatory=$true)][string]$ProjectRoot)
$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot -ErrorAction Stop).Path
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'pyproject.toml'))) {
throw 'pyproject.toml is missing. Extract the complete source archive first.'
}
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot 'uv.lock'))) {
throw 'uv.lock is missing. Extract the complete source archive first.'
}
if (-not (Test-CommandExists 'uv')) {
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
Write-Host ' [INSTALL] Installing uv from the selected Python mirror...' -ForegroundColor Cyan
Install-ArtemisUvFromIndex -IndexUrl $ArtemisDownloads.python_index
} else {
Write-Host ' [INSTALL] uv was not found. Installing from the configured installer...' -ForegroundColor Cyan
$installer = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N') + '.ps1')
try {
$installerUrl = if ($ArtemisDownloads) { $ArtemisDownloads.uv_installer_url } else { 'https://astral.sh/uv/install.ps1' }
if (-not (Invoke-DownloadFile -Uri $installerUrl -OutFile $installer)) {
throw 'Could not download uv. Check your network/proxy and retry.'
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer | Out-Host
if ($LASTEXITCODE -ne 0) { throw "uv installation failed (exit $LASTEXITCODE)." }
} finally {
Remove-Item -LiteralPath $installer -Force -ErrorAction SilentlyContinue
}
}
Update-EnvironmentPath
} else {
Write-Host ' [SKIP] uv is already installed.' -ForegroundColor Green
}
if (-not (Test-CommandExists 'uv')) { throw 'uv is still unavailable. Install it from https://astral.sh/uv/ and retry.' }
& uv --version | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'The installed uv could not run. Repair its installation and retry.' }
$venvDir = Join-Path $ProjectRoot '.venv'
$pythonExe = Join-Path $venvDir 'Scripts\python.exe'
$previousEnvironment = $env:UV_PROJECT_ENVIRONMENT
$env:UV_PROJECT_ENVIRONMENT = $venvDir
Push-Location -LiteralPath $ProjectRoot
try {
if (-not (Test-Path -LiteralPath $venvDir)) {
Write-Host ' [INSTALL] Creating .venv with Python 3.12 (downloaded automatically if needed)...' -ForegroundColor Cyan
& uv venv --python 3.12 $venvDir | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Could not create .venv. Check the download errors above and retry.' }
} else {
Write-Host ' [SKIP] Reusing the existing project .venv.' -ForegroundColor Green
}
if (-not (Test-Path -LiteralPath $pythonExe)) {
throw 'The existing .venv is incomplete. Rename it as a backup, then rerun start.bat.'
}
& $pythonExe -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) and sys.prefix != sys.base_prefix else 1)' | Out-Host
if ($LASTEXITCODE -ne 0) {
throw 'The existing .venv is unusable or older than Python 3.12. Rename it as a backup, then rerun start.bat.'
}
# A cheap check skips installation when the lock and installed packages agree.
# --inexact preserves any optional development tools already installed.
$lockOption = if ($ArtemisDownloads -and $ArtemisDownloads.python_index) { '--frozen' } else { '--locked' }
& uv sync --check $lockOption --no-dev --inexact --quiet 2>$null | Out-Host
if ($LASTEXITCODE -eq 0) {
Write-Host ' [SKIP] Python dependencies are already ready.' -ForegroundColor Green
} else {
Write-Host ' [INSTALL] Synchronizing dependencies from uv.lock...' -ForegroundColor Cyan
if ($ArtemisDownloads -and $ArtemisDownloads.python_index) {
Install-ArtemisLockedDependencies -PythonExe $pythonExe -IndexUrl $ArtemisDownloads.python_index
} else {
& uv sync --locked --no-dev --inexact | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Fix the error above and rerun start.bat.' }
}
}
& $pythonExe -c 'import artemis, uvicorn, fastapi, langchain_openai' | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Python dependency verification failed. The UI will not be started.' }
Write-Host ' [OK] Python environment is ready.' -ForegroundColor Green
return $pythonExe
} finally {
Pop-Location
$env:UV_PROJECT_ENVIRONMENT = $previousEnvironment
}
}
+103
View File
@@ -0,0 +1,103 @@
# Download settings are process-local; never edit global pip/npm/uv settings.
# Keep this file ASCII for Windows PowerShell 5.1.
function Initialize-ArtemisDownloads {
param([string]$ProjectRoot, [string]$Mode)
$path = Join-Path $ProjectRoot 'config\downloads.json'
$config = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if (-not $Mode) { $Mode = $config.mode }
if ($Mode -notin @('Official', 'China')) { throw 'Download mode must be Official or China.' }
$timeout = 0
if (-not [int]::TryParse([string]$config.timeout_seconds, [ref]$timeout) -or $timeout -lt 10 -or $timeout -gt 3600) {
throw 'config/downloads.json: timeout_seconds must be between 10 and 3600.'
}
$values = @{
python_index = ''; python_download_mirror = ''; npm_registry = ''
node_mirror = 'https://nodejs.org/dist'
adb_url = 'https://dl.google.com/android/repository/platform-tools-latest-windows.zip'
scrcpy_url = 'https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-win64-v4.1.zip'
uv_installer_url = 'https://astral.sh/uv/install.ps1'
}
foreach ($name in @($values.Keys)) {
if ($Mode -eq 'China' -and $config.china.$name) { $values[$name] = [string]$config.china.$name }
if ($config.overrides.$name) { $values[$name] = [string]$config.overrides.$name }
if ($values[$name]) {
$uri = $null
if (-not [Uri]::TryCreate($values[$name], [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne 'https') {
throw "config/downloads.json: $name must be an HTTPS URL."
}
}
}
# Explicit caller environment settings take priority over the selected preset.
if ($env:UV_DEFAULT_INDEX) { $values.python_index = $env:UV_DEFAULT_INDEX }
elseif ($env:UV_INDEX_URL) { $values.python_index = $env:UV_INDEX_URL }
if ($env:UV_PYTHON_INSTALL_MIRROR) { $values.python_download_mirror = $env:UV_PYTHON_INSTALL_MIRROR }
if ($env:npm_config_registry) { $values.npm_registry = $env:npm_config_registry }
if ($values.python_download_mirror) { $env:UV_PYTHON_INSTALL_MIRROR = $values.python_download_mirror }
if ($values.npm_registry) { $env:npm_config_registry = $values.npm_registry }
$env:UV_HTTP_TIMEOUT = [string]$timeout
$env:npm_config_fetch_timeout = [string]($timeout * 1000)
$values.mode = $Mode
$values.timeout_seconds = $timeout
$script:ArtemisDownloads = [pscustomobject]$values
Write-Host " [INFO] Download mode: $Mode (config/downloads.json)." -ForegroundColor Cyan
}
function Install-ArtemisUvFromIndex {
param([string]$IndexUrl, [string]$InstallDir = (Join-Path $env:USERPROFILE '.local\bin'))
# A uv wheel contains the standalone executable; no preinstalled Python is needed.
$work = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work -ErrorAction Stop | Out-Null
try {
$index = $IndexUrl.TrimEnd('/') + '/uv/'
$page = Join-Path $work 'index.html'
if (-not (Invoke-DownloadFile -Uri $index -OutFile $page)) { throw 'Could not download the uv package index.' }
$platform = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'win_arm64' } else { 'win_amd64' }
$pattern = 'href=["''](?<link>[^"'']*uv-(?<version>\d+\.\d+\.\d+)-py3-none-' + $platform + '\.whl#sha256=(?<hash>[a-fA-F0-9]{64}))["'']'
$candidates = [regex]::Matches((Get-Content -LiteralPath $page -Raw), $pattern)
$candidate = $candidates | Sort-Object { [version]$_.Groups['version'].Value } -Descending | Select-Object -First 1
if (-not $candidate) { throw "No stable uv wheel for $platform was found on the selected index." }
$url = [Uri]::new([Uri]$index, [System.Net.WebUtility]::HtmlDecode($candidate.Groups['link'].Value))
if ($url.Scheme -ne 'https') { throw 'The uv download must use HTTPS.' }
$zip = Join-Path $work 'uv.zip'
if (-not (Invoke-DownloadFile -Uri $url.AbsoluteUri.Split('#')[0] -OutFile $zip)) { throw 'Could not download uv from the selected index.' }
$hasher = [Security.Cryptography.SHA256]::Create()
$stream = [IO.File]::OpenRead($zip)
try { $digest = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '') }
finally { $stream.Dispose(); $hasher.Dispose() }
if ($digest -ne $candidate.Groups['hash'].Value) {
throw 'The downloaded uv package failed SHA256 verification.'
}
$extract = Join-Path $work 'wheel'
Add-Type -AssemblyName System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::ExtractToDirectory($zip, $extract)
$executable = Get-ChildItem -LiteralPath $extract -Filter uv.exe -Recurse -File | Select-Object -First 1
if (-not $executable) { throw 'The uv package did not contain uv.exe.' }
New-Item -ItemType Directory -Path $InstallDir -Force -ErrorAction Stop | Out-Null
Copy-Item -LiteralPath $executable.FullName -Destination (Join-Path $InstallDir 'uv.exe') -Force -ErrorAction Stop
$uvx = Join-Path $executable.DirectoryName 'uvx.exe'
if (Test-Path -LiteralPath $uvx) { Copy-Item -LiteralPath $uvx -Destination $InstallDir -Force -ErrorAction Stop }
$env:PATH = "$InstallDir;$env:PATH"
} finally {
$resolved = (Resolve-Path -LiteralPath $work).Path
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
if ($resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and (Split-Path $resolved -Leaf) -like 'artemis-uv-*') {
Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
function Install-ArtemisLockedDependencies {
param([string]$PythonExe, [string]$IndexUrl)
# uv sync uses the original artifact URLs in uv.lock. Export exact versions and
# hashes, then let uv pip fetch those same artifacts through the chosen index.
$requirements = Join-Path ([IO.Path]::GetTempPath()) ('artemis-requirements-' + [guid]::NewGuid().ToString('N') + '.txt')
try {
& uv export --frozen --no-dev --format requirements-txt --output-file $requirements 1>$null
if ($LASTEXITCODE -ne 0) { throw 'Could not export the locked dependencies.' }
& uv pip install --python $PythonExe --index-url $IndexUrl --requirements $requirements | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Check mirror availability or select another source in config/downloads.json.' }
} finally {
Remove-Item -LiteralPath $requirements -Force -ErrorAction SilentlyContinue
}
}
+154
View File
@@ -0,0 +1,154 @@
"""Create a portable source ZIP without environments, builds, or local credentials."""
import argparse
from datetime import datetime
import hashlib
import os
from pathlib import Path
import zipfile
EXCLUDED_DIRS = {
".git",
".venv",
"venv",
"env",
"node_modules",
"__pycache__",
"__pypackages__",
".angular",
".gradle",
".pytest_cache",
".ruff_cache",
".mypy_cache",
".cache",
".tox",
".nox",
".hypothesis",
".eggs",
".idea",
".vscode",
".vs",
".codex",
".claude",
".gemini",
".cursor",
".ipynb_checkpoints",
"build",
"dist",
"out",
"coverage",
"htmlcov",
"artifacts",
"traces",
"scratch",
"outputs",
"trace_output",
}
EXCLUDED_SUFFIXES = {
".pyc",
".pyo",
".pyd",
".so",
".dll",
".exe",
".o",
".obj",
".a",
".lib",
".class",
".jar",
".apk",
".aab",
".wasm",
".map",
".tsbuildinfo",
".egg",
".whl",
".zip",
".7z",
".tar",
".gz",
".log",
".db",
".sqlite",
".sqlite3",
".pem",
".key",
".keystore",
".p12",
".pfx",
".swp",
".swo",
}
def excluded(path: Path) -> bool:
parts = tuple(part.lower() for part in path.parts)
name = parts[-1]
if any(part in EXCLUDED_DIRS or part.endswith(".egg-info") for part in parts):
return True
# This tracked directory also contains generated Angular bundles.
if parts[:3] == ("artemis", "resources", "showcase_ui"):
return True
if (name == ".env" or name.startswith(".env.")) and name != ".env.example":
return True
if name.startswith((".artemis_", ".jetski_", ".coverage")):
return True
if name in {".ds_store", "thumbs.db", ".ipc_port", ".ls_address", ".npmrc", ".pypirc"}:
return True
return path.suffix.lower() in EXCLUDED_SUFFIXES or name.endswith((".db-wal", ".db-shm", "~"))
def source_files(root: Path):
for directory, dirs, files in os.walk(root, followlinks=False):
current = Path(directory)
dirs[:] = sorted(
name
for name in dirs
if not (current / name).is_symlink()
and not excluded((current / name).relative_to(root))
)
for name in sorted(files):
path = current / name
if not path.is_symlink() and not excluded(path.relative_to(root)):
yield path
def package_source(root: Path, output: Path) -> tuple[int, str]:
root = root.resolve()
output = output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_suffix(".zip.tmp")
files = [p for p in source_files(root) if p.resolve() not in {output, temporary}]
try:
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
for path in files:
archive.write(path, Path("artemis") / path.relative_to(root))
with zipfile.ZipFile(temporary) as archive:
bad_file = archive.testzip()
if bad_file:
raise RuntimeError(f"ZIP integrity check failed: {bad_file}")
if any(excluded(Path(name).relative_to("artemis")) for name in archive.namelist()):
raise RuntimeError("An excluded file was found in the source archive")
temporary.replace(output)
finally:
temporary.unlink(missing_ok=True)
with output.open("rb") as stream:
digest = hashlib.file_digest(stream, "sha256").hexdigest()
return len(files), digest
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path)
args = parser.parse_args()
output = (
args.output
or args.root / "artifacts" / f"artemis-source-{datetime.now():%Y%m%d-%H%M%S}.zip"
)
count, digest = package_source(args.root, output)
print(f"Archive: {output.resolve()}")
print(f"Files: {count}; size: {output.stat().st_size / (1024 * 1024):.2f} MiB")
print(f"SHA256: {digest}")
+57 -51
View File
@@ -15,7 +15,9 @@
[CmdletBinding()]
param(
[int]$Port = 8000,
[switch]$NoOpen = $false
[switch]$NoOpen = $false,
[switch]$SetupOnly = $false,
[ValidateSet('Official', 'China')][string]$Mirror
)
$ErrorActionPreference = "Continue"
@@ -30,6 +32,14 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$RootDir = Split-Path -Parent $ScriptDir
Set-Location $RootDir
. (Join-Path $ScriptDir 'download_config.ps1')
try {
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
} catch {
Write-Host " [ERROR] Invalid download settings: $_" -ForegroundColor Red
exit 1
}
Write-Host "======================================================" -ForegroundColor Cyan
Write-Host " Artemis Autonomous Mobile Agent UI " -ForegroundColor Cyan
Write-Host "======================================================" -ForegroundColor Cyan
@@ -105,7 +115,7 @@ function Invoke-DownloadFile {
param(
[Parameter(Mandatory=$true)][string]$Uri,
[Parameter(Mandatory=$true)][string]$OutFile,
[int]$TimeoutSec = 45
[int]$TimeoutSec = $ArtemisDownloads.timeout_seconds
)
if (Test-Path $OutFile) {
Remove-Item -Path $OutFile -Force -ErrorAction SilentlyContinue
@@ -152,7 +162,7 @@ function Install-PortablePlatformTools {
Write-Host " [INFO] Installing Android platform-tools (adb) in user space..." -ForegroundColor Cyan
try {
$zipPath = "$env:TEMP\platform-tools-windows.zip"
if (Invoke-DownloadFile -Uri "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -OutFile $zipPath -TimeoutSec 60) {
if (Invoke-DownloadFile -Uri $ArtemisDownloads.adb_url -OutFile $zipPath) {
New-Item -ItemType Directory -Path "$env:LOCALAPPDATA\Programs" -Force | Out-Null
Expand-Archive -Path $zipPath -DestinationPath "$env:LOCALAPPDATA\Programs" -Force
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
@@ -186,9 +196,12 @@ function Install-PortableScrcpy {
}
Write-Host " [INFO] Installing portable scrcpy in user space..." -ForegroundColor Cyan
if ($ArtemisDownloads.mode -eq 'China' -and $ArtemisDownloads.scrcpy_url.StartsWith('https://github.com/')) {
Write-Host ' [INFO] scrcpy uses GitHub. If unreachable, set overrides.scrcpy_url in config/downloads.json.' -ForegroundColor Yellow
}
try {
$zipPath = "$env:TEMP\scrcpy-win64.zip"
if (Invoke-DownloadFile -Uri "https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-win64-v4.1.zip" -OutFile $zipPath -TimeoutSec 60) {
if (Invoke-DownloadFile -Uri $ArtemisDownloads.scrcpy_url -OutFile $zipPath) {
$extractDir = "$env:TEMP\scrcpy_extract"
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue }
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
@@ -262,7 +275,8 @@ function Install-PortableNode {
if ($env:PROCESSOR_ARCHITECTURE -match "ARM64") { "arm64" } else { "x64" }
} else { "x64" }
$zipPath = "$env:TEMP\node-$nodeVer-win-$arch.zip"
if (Invoke-DownloadFile -Uri "https://nodejs.org/dist/$nodeVer/node-$nodeVer-win-$arch.zip" -OutFile $zipPath -TimeoutSec 60) {
$nodeBase = $ArtemisDownloads.node_mirror.TrimEnd('/')
if (Invoke-DownloadFile -Uri "$nodeBase/$nodeVer/node-$nodeVer-win-$arch.zip" -OutFile $zipPath) {
$extractDir = "$env:TEMP\node_extract"
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue }
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
@@ -291,33 +305,39 @@ function Install-PortableNode {
# 1. Refresh PATH with standard toolchain paths
Update-EnvironmentPath
# 2. Check or install uv
if (-not (Test-CommandExists "uv")) {
Write-Host " [INFO] uv not found. Installing Astral uv..." -ForegroundColor Yellow
try {
Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1")
} catch {
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
}
Update-EnvironmentPath
# 2. Prepare the project Python environment before optional device/UI setup.
. (Join-Path $ScriptDir 'bootstrap_python.ps1')
try {
$ArtemisPython = Initialize-ArtemisPython -ProjectRoot $RootDir
} catch {
Write-Host " [ERROR] $_" -ForegroundColor Red
exit 1
}
if (-not (Test-CommandExists "uv")) {
Write-Host " [ERROR] Astral 'uv' package manager could not be installed or found in PATH." -ForegroundColor Red
Write-Host " Please install uv manually from https://astral.sh/ or run:" -ForegroundColor Yellow
Write-Host " powershell -c ""irm https://astral.sh/uv/install.ps1 | iex""" -ForegroundColor Cyan
exit 1
if (-not (Test-Path -LiteralPath (Join-Path $RootDir '.env'))) {
if (Test-Path -LiteralPath (Join-Path $RootDir '.env.example')) {
Copy-Item -LiteralPath (Join-Path $RootDir '.env.example') -Destination (Join-Path $RootDir '.env') -ErrorAction Stop
Write-Host ' [OK] Created .env from .env.example. Configure your model in the web UI.' -ForegroundColor Green
}
}
if ($SetupOnly) {
Write-Host ' [OK] Setup complete. Run start.bat to launch Artemis.' -ForegroundColor Green
exit 0
}
# 3. Check and try installing missing system toolchains (ADB, FFmpeg, scrcpy)
$missingCore = @()
if (-not (Test-CommandExists "adb")) { $missingCore += "adb" }
if (-not (Test-CommandExists "ffmpeg")) { $missingCore += "ffmpeg" }
# imageio-ffmpeg ships a working executable in its wheel, including mirror installs.
if (-not (Test-CommandExists "ffmpeg")) {
& $ArtemisPython -c 'import imageio_ffmpeg; imageio_ffmpeg.get_ffmpeg_exe()' 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { $missingCore += "ffmpeg" }
}
if (-not (Test-CommandExists "scrcpy")) { $missingCore += "scrcpy" }
if ($missingCore.Count -gt 0) {
$useWinGet = $false
if (Test-CommandExists "winget") {
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
if ([Console]::IsInputRedirected -eq $false) {
Write-Host " [INFO] Missing components detected: $($missingCore -join ', ')." -ForegroundColor Cyan
$ans = Read-Host " Install system packages via WinGet? [Y/n]"
@@ -348,27 +368,15 @@ if ($missingCore.Count -gt 0) {
Install-PortablePlatformTools
}
if (-not (Test-CommandExists "scrcpy")) {
Install-PortableScrcpy
if (-not (Install-PortableScrcpy)) {
Write-Host ' [WARN] scrcpy download failed. Set overrides.scrcpy_url in config/downloads.json to a reachable release ZIP, then retry.' -ForegroundColor Yellow
}
}
}
# Ensure local ADB daemon is warm & listening so probes do not hit 'Connection refused'
Start-LocalAdbServer
# 4. Check or initialize .env
if (-not (Test-Path ".env")) {
if (Test-Path ".env.example") {
Copy-Item ".env.example" ".env"
Write-Host " [OK] Initialized .env configuration file." -ForegroundColor Green
} else {
New-Item -ItemType File -Path ".env" | Out-Null
}
}
# 5. Synchronize dependencies
Write-Host " [INFO] Synchronizing Python runtime and project dependencies..." -ForegroundColor Cyan
uv sync --quiet
# 6. Check and build Showcase UI if not already compiled
$ShowcaseIndex = "$RootDir\apps\showcase_ui\dist\frontend\browser\index.html"
$ShowcaseIndexAlt1 = "$RootDir\apps\showcase_ui\dist\browser\index.html"
@@ -383,7 +391,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
}
# 1. Try nvm if available on Windows
if (Test-CommandExists "nvm") {
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "nvm")) {
Write-Host " [INFO] Installing Node.js 22 LTS via nvm..." -ForegroundColor Cyan
& nvm install 22.23.2 | Out-Null
& nvm use 22.23.2 | Out-Null
@@ -393,7 +401,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
# 2. Try WinGet
if (-not (Test-NodeCompatible)) {
$useWinGetNode = $false
if (Test-CommandExists "winget") {
if ($ArtemisDownloads.mode -ne 'China' -and (Test-CommandExists "winget")) {
if ([Console]::IsInputRedirected -eq $false) {
Write-Host " [INFO] Node.js >= v22.22.0 required for Showcase UI." -ForegroundColor Cyan
$ans = Read-Host " Install/Upgrade Node.js via WinGet (may require Administrator)? [Y/n]"
@@ -426,31 +434,29 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
try {
$npmExec = if (Test-CommandExists "npm.cmd") { "npm.cmd" } else { "npm" }
Write-Host " [INFO] Installing frontend npm dependencies..." -ForegroundColor Cyan
& $npmExec install --no-audit --no-fund --loglevel=error
& $npmExec ci --no-audit --no-fund --loglevel=error
if ($LASTEXITCODE -ne 0) {
Write-Host " [WARN] npm install returned exit code $LASTEXITCODE. Trying silent install..." -ForegroundColor DarkYellow
& $npmExec install --silent
}
$cliNodeVersion = "$RootDir\apps\showcase_ui\node_modules\@angular\cli\src\utilities\node-version.js"
if (Test-Path $cliNodeVersion) {
(Get-Content $cliNodeVersion) -replace '22\.22\.3', '22.22.0' | Set-Content $cliNodeVersion
throw "npm ci failed (exit $LASTEXITCODE). Check the network and retry."
}
Write-Host " [INFO] Building Angular frontend application..." -ForegroundColor Cyan
& $npmExec run build
if ($LASTEXITCODE -ne 0) { throw "Frontend build failed (exit $LASTEXITCODE)." }
if (Test-Path $ShowcaseIndex) {
Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green
} elseif (Test-Path $ShowcaseIndexAlt1) {
Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green
} else {
Write-Host " [WARN] Showcase UI build finished but index.html was not found in expected dist directory." -ForegroundColor DarkYellow
throw 'Frontend build did not produce index.html.'
}
} catch {
Write-Host " [WARN] Failed to build Showcase UI: $_" -ForegroundColor DarkYellow
Write-Host " [ERROR] Failed to build Showcase UI: $_" -ForegroundColor Red
exit 1
} finally {
Pop-Location
}
} else {
Write-Host " [WARN] Node.js >= v22.22.0 not found. Showcase UI will show fallback notice on launch." -ForegroundColor DarkYellow
Write-Host " [ERROR] A compatible Node.js/npm could not be installed. Install Node.js LTS and retry." -ForegroundColor Red
exit 1
}
}
@@ -470,7 +476,7 @@ if ([Console]::IsInputRedirected -eq $false) {
if ($installMcp -match "^[Yy]") {
Write-Host " Installing MCP server configuration & testing rules..." -ForegroundColor Cyan
uv run python -m artemis mcp --install all
& $ArtemisPython -m artemis mcp --install all
if ($LASTEXITCODE -eq 0) {
Write-Host " [OK] MCP configuration and rules installed successfully." -ForegroundColor Green
Write-Host " [TIP] You can update or re-install anytime with: uv run artemis mcp --install all" -ForegroundColor Cyan
@@ -490,9 +496,9 @@ Start-LocalAdbServer
# long-running server never locks .venv\Scripts\artemis.exe against `uv sync` reinstalls.
Write-Host " [INFO] Launching Artemis Showcase UI & Admin Console..." -ForegroundColor Green
if ($NoOpen) {
uv run python -m artemis ui --port $Port --no-open
& $ArtemisPython -m artemis ui --port $Port --no-open
} else {
uv run python -m artemis ui --port $Port --open
& $ArtemisPython -m artemis ui --port $Port --open
}
if ($LASTEXITCODE -ne 0) {