feat: enable full AI automation and safe startup updates
This commit is contained in:
1 parent
2ddec53954
commit
7b1a0755e2
62 files changed
+3123
-240
No files matched your search
+36
-30
@@ -17,6 +17,7 @@ param(
|
||||
[int]$Port = 8000,
|
||||
[switch]$NoOpen = $false,
|
||||
[switch]$SetupOnly = $false,
|
||||
[switch]$SkipUpdate = $false,
|
||||
[ValidateSet('Official', 'China')][string]$Mirror
|
||||
)
|
||||
|
||||
@@ -32,6 +33,27 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$RootDir = Split-Path -Parent $ScriptDir
|
||||
Set-Location $RootDir
|
||||
|
||||
# Existing installations check source BEFORE importing bootstrap helpers.
|
||||
$existingPython = Join-Path $RootDir '.venv\Scripts\python.exe'
|
||||
if ((-not $SkipUpdate) -and (Test-Path -LiteralPath $existingPython)) {
|
||||
& $existingPython (Join-Path $ScriptDir 'update_installation.py') source --port $Port
|
||||
$updateExit = $LASTEXITCODE
|
||||
if ($updateExit -eq 20) { exit 1 }
|
||||
if ($updateExit -eq 10) {
|
||||
# Reparse the freshly updated launcher; do not continue running stale functions.
|
||||
$reloadArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $ScriptDir 'start.ps1'), '-Port', $Port, '-SkipUpdate')
|
||||
if ($NoOpen) { $reloadArgs += '-NoOpen' }
|
||||
if ($SetupOnly) { $reloadArgs += '-SetupOnly' }
|
||||
if ($Mirror) { $reloadArgs += @('-Mirror', $Mirror) }
|
||||
& powershell.exe @reloadArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if ($updateExit -ne 0) {
|
||||
Write-Host ' [ERROR] Source update check failed. Resolve the error above, or use -SkipUpdate for local startup.' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
. (Join-Path $ScriptDir 'download_config.ps1')
|
||||
try {
|
||||
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
|
||||
@@ -320,6 +342,11 @@ if (-not (Test-Path -LiteralPath (Join-Path $RootDir '.env'))) {
|
||||
Write-Host ' [OK] Created .env from .env.example. Configure your model in the web UI.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
Write-Host ' [CHECK] Refreshing already-configured MCP clients...' -ForegroundColor Cyan
|
||||
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') mcp
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ' [WARN] MCP refresh failed. Existing client settings were not replaced; check the errors above.' -ForegroundColor Yellow
|
||||
}
|
||||
if ($SetupOnly) {
|
||||
Write-Host ' [OK] Setup complete. Run start.bat to launch Artemis.' -ForegroundColor Green
|
||||
exit 0
|
||||
@@ -381,7 +408,10 @@ Start-LocalAdbServer
|
||||
$ShowcaseIndex = "$RootDir\apps\showcase_ui\dist\frontend\browser\index.html"
|
||||
$ShowcaseIndexAlt1 = "$RootDir\apps\showcase_ui\dist\browser\index.html"
|
||||
$ShowcaseIndexAlt2 = "$RootDir\apps\showcase_ui\dist\index.html"
|
||||
if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1)) -and (-not (Test-Path $ShowcaseIndexAlt2))) {
|
||||
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-check
|
||||
$frontendChanged = $LASTEXITCODE -eq 10
|
||||
if ($LASTEXITCODE -notin @(0, 10)) { Write-Host ' [ERROR] Could not check frontend sources.' -ForegroundColor Red; exit 1 }
|
||||
if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1)) -and (-not (Test-Path $ShowcaseIndexAlt2)))) {
|
||||
if (-not (Test-NodeCompatible)) {
|
||||
if (Test-CommandExists "node") {
|
||||
$curVer = (& node -v 2>$null)
|
||||
@@ -429,7 +459,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
if (Test-NodeCompatible) {
|
||||
$nodeVer = (& node -v 2>$null)
|
||||
Write-Host " [OK] Node.js $nodeVer is ready." -ForegroundColor Green
|
||||
Write-Host " [INFO] Showcase UI build not found. Compiling Angular Showcase UI..." -ForegroundColor Yellow
|
||||
Write-Host " [INFO] Showcase UI sources/dependencies changed or build missing. Compiling..." -ForegroundColor Yellow
|
||||
Push-Location "$RootDir\apps\showcase_ui"
|
||||
try {
|
||||
$npmExec = if (Test-CommandExists "npm.cmd") { "npm.cmd" } else { "npm" }
|
||||
@@ -443,11 +473,13 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
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) {
|
||||
} elseif ((Test-Path $ShowcaseIndexAlt1) -or (Test-Path $ShowcaseIndexAlt2)) {
|
||||
Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green
|
||||
} else {
|
||||
throw 'Frontend build did not produce index.html.'
|
||||
}
|
||||
& $ArtemisPython (Join-Path $ScriptDir 'update_installation.py') frontend-mark
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not save frontend build fingerprint.' }
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to build Showcase UI: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
@@ -460,33 +492,7 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
|
||||
}
|
||||
}
|
||||
|
||||
# 7. Optionally configure MCP for detected AI IDEs
|
||||
Write-Host ""
|
||||
Write-Host " [INFO] Would you like to configure ARTEMIS MCP & testing rules for your AI IDEs?" -ForegroundColor Cyan
|
||||
Write-Host " (Supported: Antigravity, Cursor, Claude Code/Desktop, Codex, OpenClaw, Windsurf)"
|
||||
$installMcp = "Y"
|
||||
if ([Console]::IsInputRedirected -eq $false) {
|
||||
$response = Read-Host " Install MCP configuration & rules now? [Y/n]"
|
||||
if ($response -ne "") {
|
||||
$installMcp = $response
|
||||
}
|
||||
} else {
|
||||
$installMcp = "N"
|
||||
}
|
||||
|
||||
if ($installMcp -match "^[Yy]") {
|
||||
Write-Host " Installing MCP server configuration & testing rules..." -ForegroundColor Cyan
|
||||
& $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
|
||||
} else {
|
||||
Write-Host " [WARN] MCP installation failed. Review the error above and retry with: uv run artemis mcp --install all" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host " [SKIP] You can install MCP anytime later with: uv run artemis mcp --install all" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
# MCP clients were refreshed after Python dependency sync above.
|
||||
|
||||
# 8. Launch unified Showcase UI & open browser
|
||||
# Ensure local ADB daemon is warm & listening
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
"""Check an existing checkout and refresh only already-configured local MCP clients."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import urllib.request
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def git(root: Path, *args: str, timeout: int = 15) -> str:
|
||||
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GCM_INTERACTIVE": "Never"}
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
timeout=timeout,
|
||||
)
|
||||
if result.returncode:
|
||||
# Remote diagnostics can contain URLs/userinfo. Keep them out of launcher logs.
|
||||
raise RuntimeError(
|
||||
f"git {args[0]} failed (exit {result.returncode}); check network, credentials or repository state in Git."
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def update_source(root: Path, port: int = 8000) -> int:
|
||||
"""Return 10 after a fast-forward (caller reloads launcher); never overwrite local work."""
|
||||
try:
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with opener.open(f"http://127.0.0.1:{port}/api/status", timeout=2) as response:
|
||||
status = json.load(response)
|
||||
if (
|
||||
status.get("active_tasks")
|
||||
or status.get("queue")
|
||||
or (status.get("status") == "running" and status.get("pid"))
|
||||
):
|
||||
print(
|
||||
"[WAIT] Tasks are active. Finish/stop them before updating dependencies or restarting."
|
||||
)
|
||||
return 20
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
if not shutil.which("git"):
|
||||
print(
|
||||
"[SKIP] Git is not installed. Continuing with local source; install Git for automatic source updates."
|
||||
)
|
||||
return 0
|
||||
if not (root / ".git").exists():
|
||||
print(
|
||||
"[SKIP] This source ZIP has no Git metadata. Use a Git clone for automatic source updates; local files are preserved."
|
||||
)
|
||||
return 0
|
||||
try:
|
||||
branch = git(root, "symbolic-ref", "--quiet", "--short", "HEAD")
|
||||
remote = git(root, "config", "--get", f"branch.{branch}.remote")
|
||||
merge_ref = git(root, "config", "--get", f"branch.{branch}.merge")
|
||||
if remote == "." or remote.startswith("-") or not merge_ref.startswith("refs/heads/"):
|
||||
raise RuntimeError(
|
||||
"No remote tracking branch; configure an upstream before enabling source updates."
|
||||
)
|
||||
url = git(root, "remote", "get-url", remote)
|
||||
parsed = urlsplit(url)
|
||||
if parsed.scheme in {"http", "https"} and (parsed.username or parsed.password):
|
||||
raise RuntimeError(
|
||||
"Use a clean Git remote URL and a credential manager, not embedded credentials."
|
||||
)
|
||||
print("[CHECK] Checking the current branch's configured upstream...")
|
||||
git(root, "fetch", "--no-tags", remote, merge_ref, timeout=45)
|
||||
target = git(root, "rev-parse", "FETCH_HEAD")
|
||||
current = git(root, "rev-parse", "HEAD")
|
||||
if current == target:
|
||||
print("[OK] Source is up to date.")
|
||||
return 0
|
||||
if git(root, "status", "--porcelain"):
|
||||
print(
|
||||
"[SKIP] Upstream differs, but local changes exist. Commit/back up your work before updating; continuing locally."
|
||||
)
|
||||
return 0
|
||||
ancestor = git(root, "merge-base", "HEAD", target)
|
||||
if ancestor != current:
|
||||
print(
|
||||
"[SKIP] Local branch is ahead or diverged. No automatic merge/rebase; continuing locally."
|
||||
)
|
||||
return 0
|
||||
git(root, "merge", "--ff-only", target, timeout=30)
|
||||
print(
|
||||
f"[UPDATED] Source {current[:12]} -> {target[:12]}. Reloading launcher and checking dependencies."
|
||||
)
|
||||
return 10
|
||||
except (RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||
print(
|
||||
f"[WARN] {exc if isinstance(exc, RuntimeError) else 'Git update timed out.'} Continuing with local source."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _atomic_write(path: Path, text: str) -> None:
|
||||
if path.read_text(encoding="utf-8") == text:
|
||||
return
|
||||
backup = path.with_name(path.name + ".artemis-update.bak")
|
||||
if not backup.exists():
|
||||
shutil.copy2(path, backup)
|
||||
fd, name = tempfile.mkstemp(prefix=".artemis-update-", dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
|
||||
stream.write(text)
|
||||
os.replace(name, path)
|
||||
finally:
|
||||
Path(name).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _belongs_to_project(config: dict, root: Path) -> bool:
|
||||
if config.get("url"):
|
||||
return False
|
||||
args = config.get("args", [])
|
||||
if args[:2] not in (["-m", "mcp_server"], ["-m", "mcp_server.server"]):
|
||||
# A hand-written wrapper/uv/SSE command may need custom arguments; leave it alone.
|
||||
return False
|
||||
candidates = [config.get("cwd"), config.get("env", {}).get("PYTHONPATH")]
|
||||
for value in candidates:
|
||||
if isinstance(value, str) and Path(value).resolve() == root.resolve():
|
||||
return True
|
||||
command = config.get("command")
|
||||
return (
|
||||
isinstance(command, str)
|
||||
and Path(command).resolve().parent == (root / ".venv" / "Scripts").resolve()
|
||||
)
|
||||
|
||||
|
||||
def _tool_updates(config: dict, names: tuple[str, ...]) -> dict:
|
||||
updates = {}
|
||||
if "enabled_tools" in config:
|
||||
updates["enabled_tools"] = list(dict.fromkeys([*config["enabled_tools"], *names]))
|
||||
if isinstance(config.get("tools"), dict):
|
||||
updates["tools"] = {**{name: {"eager": True} for name in names}, **config["tools"]}
|
||||
return updates
|
||||
|
||||
|
||||
def refresh_json(
|
||||
path: Path, keys: tuple[str, ...], root: Path, python: str, names: tuple[str, ...]
|
||||
) -> bool:
|
||||
if not path.is_file():
|
||||
return False
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
data = json.loads(raw)
|
||||
parent = data
|
||||
for key in keys:
|
||||
parent = parent.get(key, {})
|
||||
config = parent.get("artemis")
|
||||
if not isinstance(config, dict) or not _belongs_to_project(config, root):
|
||||
return False
|
||||
updated = {**config, "command": python, **_tool_updates(config, names)}
|
||||
if updated == config:
|
||||
return False
|
||||
parent["artemis"] = updated
|
||||
_atomic_write(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
||||
return True
|
||||
|
||||
|
||||
def refresh_codex(path: Path, root: Path, python: str, names: tuple[str, ...]) -> bool:
|
||||
if not path.is_file():
|
||||
return False
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
config = tomllib.loads(raw).get("mcp_servers", {}).get("artemis")
|
||||
if not isinstance(config, dict) or not _belongs_to_project(config, root):
|
||||
return False
|
||||
wanted = {"command": python, **_tool_updates(config, names)}
|
||||
wanted = {key: value for key, value in wanted.items() if config.get(key) != value}
|
||||
if not wanted:
|
||||
return False
|
||||
lines = raw.splitlines(keepends=True)
|
||||
inside = False
|
||||
found = set()
|
||||
output = []
|
||||
|
||||
def append_missing():
|
||||
for key, value in wanted.items():
|
||||
if key not in found:
|
||||
output.append(f"{key} = {json.dumps(value, ensure_ascii=False)}\n")
|
||||
found.add(key)
|
||||
|
||||
# Replace complete assignment spans, including multiline arrays, using TOML parsing.
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
line = lines[index]
|
||||
if line.lstrip().startswith("["):
|
||||
if inside:
|
||||
append_missing()
|
||||
header = re.sub(r"[\s\"']", "", line.split("#", 1)[0]).strip()
|
||||
inside = header == "[mcp_servers.artemis]"
|
||||
match = re.match(r"\s*(\w+)\s*=", line) if inside else None
|
||||
if match and match[1] in wanted:
|
||||
key = match[1]
|
||||
original = line
|
||||
while True:
|
||||
try:
|
||||
tomllib.loads(original)
|
||||
break
|
||||
except tomllib.TOMLDecodeError:
|
||||
index += 1
|
||||
if index >= len(lines):
|
||||
raise ValueError("Cannot safely parse existing MCP setting")
|
||||
original += lines[index]
|
||||
output.append(f"{key} = {json.dumps(wanted[key], ensure_ascii=False)}\n")
|
||||
found.add(key)
|
||||
else:
|
||||
output.append(line)
|
||||
index += 1
|
||||
if inside:
|
||||
append_missing()
|
||||
updated = "".join(output)
|
||||
parsed = tomllib.loads(updated)
|
||||
if any(parsed["mcp_servers"]["artemis"].get(k) != v for k, v in wanted.items()):
|
||||
raise ValueError("Could not locate the existing Artemis table safely")
|
||||
_atomic_write(path, updated)
|
||||
return updated != raw
|
||||
|
||||
|
||||
def refresh_mcp(root: Path) -> None:
|
||||
sys.path.insert(0, str(root))
|
||||
from artemis.interfaces.cli.commands.mcp import ARTEMIS_MCP_TOOLS, _get_vscode_user_dir
|
||||
|
||||
home = Path.home()
|
||||
vscode = _get_vscode_user_dir()
|
||||
appdata = Path(os.environ.get("APPDATA", home / "AppData/Roaming"))
|
||||
paths = [
|
||||
(home / ".gemini/config/mcp_config.json", ("mcpServers",)),
|
||||
(home / ".gemini/jetski/mcp_config.json", ("mcpServers",)),
|
||||
(home / ".gemini/antigravity/mcp_config.json", ("mcpServers",)),
|
||||
(home / ".cursor/mcp.json", ("mcpServers",)),
|
||||
(home / ".codeium/windsurf/mcp_config.json", ("mcpServers",)),
|
||||
(home / ".claude.json", ("mcpServers",)),
|
||||
(appdata / "Claude/claude_desktop_config.json", ("mcpServers",)),
|
||||
(vscode / "mcp.json", ("servers",)),
|
||||
(home / ".copilot/mcp-config.json", ("servers",)),
|
||||
(home / ".cline/data/settings/cline_mcp_settings.json", ("mcpServers",)),
|
||||
(home / ".cline/mcp.json", ("mcpServers",)),
|
||||
(
|
||||
vscode / "globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
|
||||
("mcpServers",),
|
||||
),
|
||||
(
|
||||
vscode / "globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json",
|
||||
("mcpServers",),
|
||||
),
|
||||
(
|
||||
vscode / "globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json",
|
||||
("mcpServers",),
|
||||
),
|
||||
(root / ".roo/mcp.json", ("mcpServers",)),
|
||||
(home / ".openclaw/openclaw.json", ("mcp", "servers")),
|
||||
]
|
||||
changed = 0
|
||||
codex = Path(os.environ.get("CODEX_HOME", home / ".codex")) / "config.toml"
|
||||
for path, keys in [*paths, (codex, ())]:
|
||||
try:
|
||||
updated = (
|
||||
refresh_json(path, keys, root, sys.executable, ARTEMIS_MCP_TOOLS)
|
||||
if keys
|
||||
else refresh_codex(path, root, sys.executable, ARTEMIS_MCP_TOOLS)
|
||||
)
|
||||
if updated:
|
||||
print(f"[UPDATED] Existing MCP configuration: {path}")
|
||||
changed += 1
|
||||
except (OSError, ValueError, TypeError, AttributeError):
|
||||
print(f"[WARN] Could not safely refresh {path}; original configuration preserved.")
|
||||
print(
|
||||
f"[OK] MCP source is current; refreshed {changed} existing client configuration(s). Reload the MCP connection in your AI client after a source update."
|
||||
)
|
||||
print("[TIP] For a new client run: uv run artemis mcp --install <client>")
|
||||
|
||||
|
||||
def frontend_fingerprint(root: Path) -> str:
|
||||
ui = root / "apps/showcase_ui"
|
||||
files = list((ui / "src").rglob("*")) + list((ui / "public").rglob("*"))
|
||||
files += [
|
||||
ui / name
|
||||
for name in (
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"angular.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.app.json",
|
||||
)
|
||||
]
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(p for p in files if p.is_file()):
|
||||
digest.update(path.relative_to(ui).as_posix().encode())
|
||||
digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=["source", "mcp", "frontend-check", "frontend-mark"])
|
||||
parser.add_argument("--root", type=Path, default=ROOT)
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
args = parser.parse_args()
|
||||
if args.action == "source":
|
||||
sys.exit(update_source(args.root, args.port))
|
||||
elif args.action == "mcp":
|
||||
refresh_mcp(args.root)
|
||||
else:
|
||||
marker = args.root / "apps/showcase_ui/node_modules/.artemis-source-hash"
|
||||
current = frontend_fingerprint(args.root)
|
||||
if args.action == "frontend-mark":
|
||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||
marker.write_text(current, encoding="ascii")
|
||||
elif not marker.is_file() or marker.read_text(encoding="ascii").strip() != current:
|
||||
sys.exit(10)
|
||||
Reference in new issue
Block a user