From c78c201e9467dd7c437dd44ec71feea28b6d933a Mon Sep 17 00:00:00 2001 From: somew1nd Date: Tue, 18 Aug 2026 22:33:50 -0700 Subject: [PATCH] Fix setup UI and improve CLI workflows --- README.md | 44 +++++-- README_CN.md | 44 +++++-- .../src/app/pages/home/home.component.scss | 45 +++++++ artemis/interfaces/cli/commands/init.py | 12 +- artemis/interfaces/cli/commands/mcp.py | 113 ++++++++++++++++-- scripts/start.ps1 | 39 +++--- start.sh | 12 +- tests/unit/test_cli.py | 47 +++++++- 8 files changed, 302 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index c26406a..a0d256f 100644 --- a/README.md +++ b/README.md @@ -82,26 +82,37 @@ Experience seamless collaboration between **Antigravity** and **ARTEMIS** via na Ensure an Android device (with **USB Debugging** enabled) or emulator is connected. The one-click startup script will automatically: - 🛠️ **Install System Toolchains**: Detect and auto-install ADB, scrcpy, FFmpeg, and Python (`uv`) dependencies. -- 🔌 **Mount Global MCP Server & AI Agent Rules**: Prompt to automatically install global MCP configurations and the **Artemis Mobile Testing Mindset (`rules.md`)** into your AI IDEs (**Antigravity**, **Cursor**, **Claude Code**, **Windsurf**, **VS Code**, **Cline/Roo**, **OpenClaw**). +- 🔌 **Mount Global MCP Server & AI Agent Rules**: Prompt to automatically install global MCP configurations and the **Artemis Mobile Testing Mindset (`rules.md`)** into your AI IDEs (**Antigravity**, **Cursor**, **Claude Code**, **Codex**, **Windsurf**, **VS Code**, **Cline/Roo**, **OpenClaw**). + +### 🍎 macOS and 🐧 Linux ```bash # 1. Clone repo & navigate to directory git clone https://github.com/google/artemis.git && cd artemis # 2. One-click launch -# 🍎 macOS & 🐧 Linux ./start.sh - -# 🪟 Windows -start.bat ``` +### 🪟 Windows PowerShell + +```powershell +# 1. Clone repo & navigate to directory +git clone https://github.com/google/artemis.git +cd artemis + +# 2. One-click launch +.\start.bat +``` + +> PowerShell does not search the current directory for executable scripts by default, so use `.\start.bat` without a trailing `\`. In Command Prompt (CMD), use `start.bat` instead. + > 💡 **Tip**: Opens `http://localhost:8000` in your default browser with a device connection wizard, live screen mirroring, prompt sandbox, and execution replays. You can also run directly from CLI: `uv run artemis run "Open Settings, find Battery and tell me current level" --profile flash`.
-🔌 MCP Setup for Antigravity / Claude Code / Windsurf (Click to expand) +🔌 MCP Setup for Codex / Antigravity / Claude Code / Windsurf (Click to expand)
@@ -109,13 +120,13 @@ ARTEMIS includes a native **Model Context Protocol (MCP)** server. Connect your ### 1. One-Click Auto Install (Recommended) -Running `./start.sh` or `start.bat` will prompt you to configure global MCP and testing rules for detected IDEs (or you can install/update anytime later manually using the commands below): +Running `./start.sh` (macOS/Linux) or `.\start.bat` (Windows PowerShell) will prompt you to configure global MCP and testing rules for detected IDEs (or you can install/update anytime later manually using the commands below): ```bash # Auto-install MCP server & global rules for Antigravity / Jetski: uv run artemis mcp --install antigravity -# Or install for all detected AI IDEs (Antigravity, Cursor, Claude Desktop/Code, OpenClaw): +# Or install for all supported AI IDEs (including Codex): uv run artemis mcp --install all ``` @@ -124,7 +135,19 @@ uv run artemis mcp --install all ### 2. Manual Configuration (Optional) -If you prefer to configure manually, run `uv run artemis mcp --generate-config antigravity` (or `all`) to output the exact JSON snippet, or paste the following into your IDE configuration (replace `/path/to/artemis` with your actual repo path and point `command` to your `.venv` Python executable): +If you prefer to configure manually, run `uv run artemis mcp --generate-config ` (for example, `codex` or `antigravity`) to output the appropriate TOML or JSON snippet. Replace `/path/to/artemis` with your actual repo path and point `command` to your `.venv` Python executable: + +* **Codex** (`~/.codex/config.toml`): +```toml +[mcp_servers.artemis] +command = "/path/to/artemis/.venv/bin/python" +args = ["-m", "mcp_server"] +cwd = "/path/to/artemis" + +[mcp_servers.artemis.env] +PYTHONUNBUFFERED = "1" +PYTHONPATH = "/path/to/artemis" +``` * **Antigravity** (`~/.gemini/jetski/mcp_config.json`): ```json @@ -169,12 +192,13 @@ You can mount or copy [`mcp_server/rules.md`](./mcp_server/rules.md) into your A * **Antigravity**: Add the contents of `rules.md` to your Workspace Rules, Global Rules settings, or agent instructions. * **Claude Code**: Copy or include the contents of `rules.md` in your project's `CLAUDE.md` file. * **Cursor**: Copy the contents into `.cursorrules` or create a rule file at `.cursor/rules/artemis.mdc`. +* **Codex**: Add the contents to `~/.codex/AGENTS.md` (or the active `AGENTS.override.md`). * **Windsurf / OpenClaw**: Add the rules to your workspace rules or global system prompts. > 💡 For more details on the testing mindset and MCP architecture, see the [MCP Server README](./mcp_server/README.md). ### 4. Prompt Your Phone in the IDE Chat -In Antigravity or Claude Code, simply prompt: +In Codex, Antigravity, or Claude Code, simply prompt: > 💬 *"Build the latest changes into an APK, install it on the connected device, open the login screen with a test account, verify if there are any unexpected popups after login, and return screenshots of the final page."*
diff --git a/README_CN.md b/README_CN.md index 0ee438e..f7a61be 100644 --- a/README_CN.md +++ b/README_CN.md @@ -84,26 +84,37 @@ 确保电脑已连接 Android 实体机(已开启 **USB 调试**)或 Android 模拟器。一键启动脚本将会自动完成以下配置: - 🛠️ **安装系统环境依赖**:自动检测并安装 ADB、scrcpy、FFmpeg 与 Python(`uv`)运行时及项目依赖。 -- 🔌 **全局挂载 MCP 服务与测试准则 (Rules)**:主动引导并自动将全局 MCP 服务与 **Artemis 移动端测试思维准则 (`rules.md`)** 挂载至你使用的 AI IDE(支持 **Antigravity**、**Cursor**、**Claude Code**、**Windsurf**、**VS Code**、**Cline/Roo**、**OpenClaw**)。 +- 🔌 **全局挂载 MCP 服务与测试准则 (Rules)**:主动引导并自动将全局 MCP 服务与 **Artemis 移动端测试思维准则 (`rules.md`)** 挂载至你使用的 AI IDE(支持 **Antigravity**、**Cursor**、**Claude Code**、**Codex**、**Windsurf**、**VS Code**、**Cline/Roo**、**OpenClaw**)。 + +### 🍎 macOS 与 🐧 Linux ```bash # 1. 克隆代码仓库并进入目录 git clone https://github.com/google/artemis.git && cd artemis # 2. 一键启动 -# 🍎 macOS & 🐧 Linux ./start.sh - -# 🪟 Windows -start.bat ``` +### 🪟 Windows PowerShell + +```powershell +# 1. 克隆代码仓库并进入目录 +git clone https://github.com/google/artemis.git +cd artemis + +# 2. 一键启动 +.\start.bat +``` + +> PowerShell 默认不会从当前目录查找可执行脚本,因此必须使用 `.\start.bat`,且命令末尾不要添加 `\`。如果使用传统命令提示符(CMD),则运行 `start.bat`。 + > 💡 **提示**:启动后将自动在默认浏览器中打开 Web 控制台(`http://localhost:8000`),提供设备连接向导、实时投屏、任务演练与状态回放面板。你也可以通过命令行直接运行:`uv run artemis run "打开系统设置,找到电池选项并告诉我当前电量" --profile flash`。
-🔌 接入 Antigravity / Claude Code / Windsurf (MCP)(点击展开) +🔌 接入 Codex / Antigravity / Claude Code / Windsurf (MCP)(点击展开)
@@ -111,13 +122,13 @@ ARTEMIS 内置原生 **Model Context Protocol (MCP)** 服务。只需将以下 ### 1. 一键自动安装到 IDE(推荐) -运行 `./start.sh` 或 `start.bat` 启动脚本时会主动询问是否自动挂载全局 MCP 与测试行为准则(支持跳过并在之后随时手动执行以下命令挂载): +运行 `./start.sh`(macOS/Linux)或 `.\start.bat`(Windows PowerShell)启动脚本时,会主动询问是否自动挂载全局 MCP 与测试行为准则(支持跳过并在之后随时手动执行以下命令挂载): ```bash # 一键安装全局 MCP 服务与 Rules 到 Antigravity / Jetski: uv run artemis mcp --install antigravity -# 或一键安装到所有检测到的 AI IDE(Antigravity、Cursor、Claude Desktop/Code、OpenClaw): +# 或一键安装到所有支持的 AI IDE(包括 Codex): uv run artemis mcp --install all ``` @@ -126,7 +137,19 @@ uv run artemis mcp --install all ### 2. 手动配置(可选) -如果你习惯手动复制配置,可运行 `uv run artemis mcp --generate-config antigravity`(或 `all`)获取自动计算好路径的 JSON 代码段,也可以参考以下模板将 `command` 填写为项目下 `.venv` 虚拟环境中的 Python 绝对路径,并将 `/path/to/artemis` 替换为项目实际路径: +如果你习惯手动复制配置,可运行 `uv run artemis mcp --generate-config `(例如 `codex` 或 `antigravity`)获取对应的 TOML 或 JSON 配置。请将 `command` 填写为项目下 `.venv` 虚拟环境中的 Python 绝对路径,并将 `/path/to/artemis` 替换为项目实际路径: + +* **Codex** (`~/.codex/config.toml`): +```toml +[mcp_servers.artemis] +command = "/path/to/artemis/.venv/bin/python" +args = ["-m", "mcp_server"] +cwd = "/path/to/artemis" + +[mcp_servers.artemis.env] +PYTHONUNBUFFERED = "1" +PYTHONPATH = "/path/to/artemis" +``` * **Antigravity** (`~/.gemini/jetski/mcp_config.json`): ```json @@ -171,12 +194,13 @@ uv run artemis mcp --install all * **Antigravity**:将 `rules.md` 内容添加至工作区规则(Workspace Rules)或全局规则设置中。 * **Claude Code**:将 `rules.md` 内容复制或引入至项目根目录的 `CLAUDE.md` 文件中。 * **Cursor**:将内容复制到 `.cursorrules` 文件或在 `.cursor/rules/artemis.mdc` 中创建新规则。 +* **Codex**:将内容添加至 `~/.codex/AGENTS.md`(或当前生效的 `AGENTS.override.md`)。 * **Windsurf / OpenClaw**:将内容添加到工作区规则或全局 System Prompt 中。 > 💡 更多规范设计细节与 MCP 架构说明,请参阅 [MCP Server 文档](./mcp_server/README.md)。 ### 4. 在 IDE 中体验真机协同 -在 Antigravity / Claude Code 对话框中直接输入: +在 Codex / Antigravity / Claude Code 对话框中直接输入: > 💬 *"请帮我把刚刚修改的代码编译成 APK 并安装到手机上,打开登录页面输入测试账号,验证登录后是否有异常弹窗,并把最终页面截图回传。"*
diff --git a/apps/showcase_ui/src/app/pages/home/home.component.scss b/apps/showcase_ui/src/app/pages/home/home.component.scss index 424a4df..69ff4fb 100644 --- a/apps/showcase_ui/src/app/pages/home/home.component.scss +++ b/apps/showcase_ui/src/app/pages/home/home.component.scss @@ -54,6 +54,51 @@ $text-secondary: #475569; $text-muted: #64748b; $text-light: #94a3b8; +// Shared OS switcher. It is rendered in both the dependency installer and +// the empty-AVD guide, so the styling cannot be scoped to either card. +.os-selector-pills { + display: inline-flex; + align-items: center; + padding: 2px; + gap: 2px; + background: rgba(255, 255, 255, 0.65); + border: 1px solid rgba(226, 232, 240, 0.85); + border-radius: 8px; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.8); + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); + + .os-pill { + padding: 3px 9px; + border: 0; + border-radius: 6px; + background: transparent; + color: $text-secondary; + font-family: inherit; + font-size: 11px; + font-weight: 600; + line-height: 1.35; + cursor: pointer; + transition: color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; + + &:hover { + color: $text-main; + background: rgba(255, 255, 255, 0.72); + } + + &.active { + color: #ffffff; + background: $primary; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12); + } + + &:focus-visible { + outline: 2px solid rgba($primary, 0.35); + outline-offset: 1px; + } + } +} + // ============================================================================= // FROSTED GLASS (GLASSMORPHISM) MIXINS // ============================================================================= diff --git a/artemis/interfaces/cli/commands/init.py b/artemis/interfaces/cli/commands/init.py index c73b59d..c8fa063 100644 --- a/artemis/interfaces/cli/commands/init.py +++ b/artemis/interfaces/cli/commands/init.py @@ -141,19 +141,21 @@ def init_command() -> None: console.print(" [3] Claude Desktop / Claude Code") console.print(" [4] Windsurf") console.print(" [5] VS Code / Cline / Roo Code") - console.print(" [6] All supported IDEs") - console.print(" [7] Skip for now") + console.print(" [6] Codex") + console.print(" [7] All supported IDEs") + console.print(" [8] Skip for now") mcp_choice = Prompt.ask( - "Select IDE for MCP auto-install", choices=["1", "2", "3", "4", "5", "6", "7"], default="1" + "Select IDE for MCP auto-install", choices=["1", "2", "3", "4", "5", "6", "7", "8"], default="1" ) - if mcp_choice != "7": + if mcp_choice != "8": target_map = { "1": "antigravity", "2": "cursor", "3": "claude", "4": "windsurf", "5": "vscode", - "6": "all", + "6": "codex", + "7": "all", } target = target_map.get(mcp_choice, "antigravity") try: diff --git a/artemis/interfaces/cli/commands/mcp.py b/artemis/interfaces/cli/commands/mcp.py index dfe1981..3375b18 100644 --- a/artemis/interfaces/cli/commands/mcp.py +++ b/artemis/interfaces/cli/commands/mcp.py @@ -17,7 +17,9 @@ import json import os from pathlib import Path +import re import sys +import tomllib from typing import Annotated from mcp_server.base import mcp as agent_mcp @@ -72,6 +74,8 @@ def _get_config_snippet(client: str, python_exe: str, project_root: str) -> dict } elif client == "openclaw": return {"plugins": {"artemis_mcp": {"enabled": True, "type": "mcp", "server": config_body}}} + elif client == "codex": + return {"mcp_servers": {"artemis": config_body}} else: # cursor, windsurf, claude, vscode, cline, roo, generic return {"mcpServers": {"artemis": config_body}} @@ -83,7 +87,6 @@ def _parse_json_lenient(text: str) -> dict: return data if isinstance(data, dict) else {} except Exception: pass - import re # Remove // single-line comments cleaned = re.sub(r"//.*", "", text) # Remove /* ... */ multi-line comments @@ -125,6 +128,76 @@ def _merge_json_file(file_path: Path, server_name: str, server_config: dict, key return False +def _codex_toml_block(server_config: dict) -> str: + """Renders a managed Codex MCP server block using TOML-compatible JSON strings.""" + command = json.dumps(str(server_config["command"]), ensure_ascii=False) + args = json.dumps(server_config.get("args", []), ensure_ascii=False) + cwd = json.dumps(str(server_config["cwd"]), ensure_ascii=False) + env = server_config.get("env", {}) + lines = [ + "# BEGIN ARTEMIS MCP CONFIG", + "[mcp_servers.artemis]", + f"command = {command}", + f"args = {args}", + f"cwd = {cwd}", + ] + if env: + lines.extend(["", "[mcp_servers.artemis.env]"]) + for key, value in env.items(): + lines.append(f"{key} = {json.dumps(str(value), ensure_ascii=False)}") + lines.append("# END ARTEMIS MCP CONFIG") + return "\n".join(lines) + "\n" + + +def _merge_codex_toml(file_path: Path, server_config: dict) -> bool: + """Adds or updates Artemis in Codex config.toml while preserving unrelated settings.""" + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + existing = file_path.read_text(encoding="utf-8") if file_path.exists() else "" + managed_block = _codex_toml_block(server_config) + begin_marker = "# BEGIN ARTEMIS MCP CONFIG" + end_marker = "# END ARTEMIS MCP CONFIG" + + if begin_marker in existing and end_marker in existing: + managed_pattern = re.compile( + rf"(?ms)^{re.escape(begin_marker)}\n.*?^{re.escape(end_marker)}\n?" + ) + updated = managed_pattern.sub(managed_block, existing, count=1) + else: + # Remove an older, unmanaged Artemis table before adding the managed block. + kept_lines: list[str] = [] + skipping_artemis = False + header_pattern = re.compile(r"^\s*\[{1,2}\s*(.+?)\s*\]{1,2}\s*(?:#.*)?$") + for line in existing.splitlines(keepends=True): + header_match = header_pattern.match(line.rstrip("\r\n")) + if header_match: + normalized = re.sub(r"[\s\"']", "", header_match.group(1)).lower() + skipping_artemis = normalized == "mcp_servers.artemis" or normalized.startswith( + "mcp_servers.artemis." + ) + if not skipping_artemis: + kept_lines.append(line) + preserved = "".join(kept_lines).rstrip() + updated = f"{preserved}\n\n{managed_block}" if preserved else managed_block + + # Never replace a user's config with invalid TOML. + tomllib.loads(updated) + if updated != existing: + file_path.write_text(updated, encoding="utf-8") + return True + except Exception as e: + if file_path.exists(): + try: + backup_path = file_path.with_suffix(file_path.suffix + ".bak") + backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8") + logger.warning(f"Could not update {file_path}; backup created at {backup_path}: {e}") + except Exception: + pass + else: + logger.warning(f"Could not create Codex MCP config file {file_path}: {e}") + return False + + def _inject_rules_block(file_path: Path, content: str) -> bool: """Injects or updates a managed ARTEMIS block in a global rules file without overwriting user content.""" try: @@ -219,7 +292,7 @@ def install_rules(client: str, project_root: str) -> list[str]: return [] targets = ( - ["antigravity", "cursor", "claude", "windsurf", "vscode", "cline", "roo", "openclaw"] + ["antigravity", "cursor", "claude", "windsurf", "vscode", "cline", "roo", "openclaw", "codex"] if client == "all" else [client] ) @@ -281,6 +354,16 @@ def install_rules(client: str, project_root: str) -> list[str]: installed_paths.append(str(openclaw_md)) if _write_rule_file(global_rule, raw_rules): installed_paths.append(str(global_rule)) + elif target == "codex": + codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex")) + override_file = codex_home / "AGENTS.override.md" + agents_file = ( + override_file + if override_file.exists() and override_file.read_text(encoding="utf-8").strip() + else codex_home / "AGENTS.md" + ) + if _inject_rules_block(agents_file, raw_rules): + installed_paths.append(str(agents_file)) return installed_paths @@ -289,7 +372,7 @@ def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[ """Auto-installs/merges ARTEMIS MCP configuration and testing rules into IDE config files across any OS.""" installed_paths: list[str] = [] targets = ( - ["antigravity", "cursor", "claude", "windsurf", "vscode", "cline", "roo", "openclaw"] + ["antigravity", "cursor", "claude", "windsurf", "vscode", "cline", "roo", "openclaw", "codex"] if client == "all" else [client] ) @@ -350,6 +433,12 @@ def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[ openclaw_path = Path.home() / ".openclaw" / "openclaw.json" if _merge_json_file(openclaw_path, "artemis_mcp", plugin_cfg, key_name="plugins"): installed_paths.append(str(openclaw_path)) + elif target == "codex": + server_cfg = snippet["mcp_servers"]["artemis"] + codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex")) + codex_path = codex_home / "config.toml" + if _merge_codex_toml(codex_path, server_cfg): + installed_paths.append(str(codex_path)) rules_paths = install_rules(client, project_root) installed_paths.extend(rules_paths) @@ -397,7 +486,7 @@ def mcp_command( typer.Option( "--install", "-i", - help="Auto-install and merge ARTEMIS MCP configuration and testing rules into 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', or 'all'.", + help="Auto-install and merge ARTEMIS MCP configuration and testing rules into 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.", ), ] = None, generate_config: Annotated[ @@ -405,7 +494,7 @@ def mcp_command( typer.Option( "--generate-config", "-g", - help="Output ready-to-use MCP configuration JSON for 'antigravity', 'cursor', 'claude', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', or 'all'.", + help="Output ready-to-use MCP configuration for 'antigravity', 'cursor', 'claude', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.", ), ] = None, ) -> None: @@ -428,10 +517,11 @@ def mcp_command( "roo", "roo_code", "openclaw", + "codex", "all", ): console.print( - f"[bold red]Unsupported install target: '{install_config}'. Use 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', or 'all'.[/bold red]" + f"[bold red]Unsupported install target: '{install_config}'. Use 'antigravity', 'claude', 'cursor', 'windsurf', 'vscode', 'cline', 'roo', 'openclaw', 'codex', or 'all'.[/bold red]" ) raise typer.Exit(1) installed_paths = install_mcp_config(client, python_exe, project_root) @@ -469,13 +559,22 @@ def mcp_command( "openclaw (openclaw.json)": _get_config_snippet( "openclaw", python_exe, project_root ), + "codex (~/.codex/config.toml)": _get_config_snippet( + "codex", python_exe, project_root + ), } json_str = json.dumps(all_configs, indent=2) + syntax_language = "json" + elif client == "codex": + server_cfg = _get_config_snippet("codex", python_exe, project_root)["mcp_servers"]["artemis"] + json_str = _codex_toml_block(server_cfg) + syntax_language = "toml" else: snippet = _get_config_snippet(client, python_exe, project_root) json_str = json.dumps(snippet, indent=2) + syntax_language = "json" - syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False) + syntax = Syntax(json_str, syntax_language, theme="monokai", line_numbers=False) console.print(f"[bold cyan]MCP Configuration for {client.upper()}:[/bold cyan]") console.print(syntax) raise typer.Exit(0) diff --git a/scripts/start.ps1 b/scripts/start.ps1 index 2041d44..39054fc 100644 --- a/scripts/start.ps1 +++ b/scripts/start.ps1 @@ -26,7 +26,7 @@ $RootDir = Split-Path -Parent $ScriptDir Set-Location $RootDir Write-Host "======================================================" -ForegroundColor Cyan -Write-Host " ✨ Artemis Autonomous Mobile Agent UI " -ForegroundColor Cyan +Write-Host " Artemis Autonomous Mobile Agent UI " -ForegroundColor Cyan Write-Host "======================================================" -ForegroundColor Cyan Write-Host "" @@ -59,7 +59,7 @@ Update-EnvironmentPath # 2. Check or install uv if (-not (Test-CommandExists "uv")) { - Write-Host "⚡ uv not found. Installing Astral uv..." -ForegroundColor Yellow + Write-Host "[INFO] uv not found. Installing Astral uv..." -ForegroundColor Yellow powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" Update-EnvironmentPath } @@ -72,7 +72,7 @@ if (-not (Test-CommandExists "scrcpy")) { $missingCore += "scrcpy" } if ($missingCore.Count -gt 0) { if (Test-CommandExists "winget") { - Write-Host "⚡ Auto-installing missing components ($($missingCore -join ', '))..." -ForegroundColor DarkYellow + Write-Host "[INFO] Auto-installing missing components ($($missingCore -join ', '))..." -ForegroundColor DarkYellow if ($missingCore -contains "adb") { winget install --id Google.PlatformTools -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null } @@ -90,14 +90,14 @@ if ($missingCore.Count -gt 0) { if (-not (Test-Path ".env")) { if (Test-Path ".env.example") { Copy-Item ".env.example" ".env" - Write-Host " ✔ Initialized .env configuration file." -ForegroundColor Green + Write-Host " [OK] Initialized .env configuration file." -ForegroundColor Green } else { New-Item -ItemType File -Path ".env" | Out-Null } } # 5. Synchronize dependencies -Write-Host " 📦 Synchronizing Python runtime and project dependencies..." -ForegroundColor Cyan +Write-Host " [INFO] Synchronizing Python runtime and project dependencies..." -ForegroundColor Cyan uv sync --quiet # 6. Check and build Showcase UI if not already compiled @@ -107,33 +107,33 @@ $ShowcaseIndexAlt2 = "$RootDir\apps\showcase_ui\dist\index.html" if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1)) -and (-not (Test-Path $ShowcaseIndexAlt2))) { if (-not (Test-CommandExists "npm")) { if (Test-CommandExists "winget") { - Write-Host " ⚡ Node.js/npm not found. Installing Node.js LTS via WinGet..." -ForegroundColor Yellow + Write-Host " [INFO] Node.js/npm not found. Installing Node.js LTS via WinGet..." -ForegroundColor Yellow winget install --id OpenJS.NodeJS.LTS -e --accept-source-agreements --accept-package-agreements --silent 2>$null | Out-Null Update-EnvironmentPath } } if (Test-CommandExists "npm") { - Write-Host " 🎨 Showcase UI build not found. Compiling Angular Showcase UI..." -ForegroundColor Yellow + Write-Host " [INFO] Showcase UI build not found. Compiling Angular Showcase UI..." -ForegroundColor Yellow Push-Location "$RootDir\apps\showcase_ui" try { npm install --silent npm run build - Write-Host " ✔ Showcase UI compiled successfully." -ForegroundColor Green + Write-Host " [OK] Showcase UI compiled successfully." -ForegroundColor Green } catch { - Write-Host " ⚠ Failed to build Showcase UI: $_" -ForegroundColor DarkYellow + Write-Host " [WARN] Failed to build Showcase UI: $_" -ForegroundColor DarkYellow } finally { Pop-Location } } else { - Write-Host " ⚠ Node.js/npm not found. Please install Node.js (>= 18) to compile Showcase UI." -ForegroundColor DarkYellow + Write-Host " [WARN] Node.js/npm not found. Please install Node.js (>= 18) to compile Showcase UI." -ForegroundColor DarkYellow } } # 7. Optionally configure MCP for detected AI IDEs Write-Host "" -Write-Host " 🔌 Would you like to configure ARTEMIS MCP & testing rules for your AI IDEs?" -ForegroundColor Cyan -Write-Host " (Supported: Antigravity, Cursor, Claude Code/Desktop, OpenClaw, Windsurf)" +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]" @@ -145,16 +145,21 @@ if ([Console]::IsInputRedirected -eq $false) { } if ($installMcp -match "^[Yy]") { - Write-Host " ✔ Installing MCP server configuration & testing rules..." -ForegroundColor Green - uv run artemis mcp --install all 2>$null | Out-Null - Write-Host " 💡 Tip: You can update or re-install anytime with: uv run artemis mcp --install all" -ForegroundColor Cyan + Write-Host " Installing MCP server configuration & testing rules..." -ForegroundColor Cyan + uv run 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 " ⏭️ Skipped. You can install MCP anytime later with: uv run artemis mcp --install all" -ForegroundColor Yellow + Write-Host " [SKIP] You can install MCP anytime later with: uv run artemis mcp --install all" -ForegroundColor Yellow } Write-Host "" # 8. Launch unified Showcase UI & open browser -Write-Host " 🚀 Launching Artemis Showcase UI & Admin Console..." -ForegroundColor Green +Write-Host " [INFO] Launching Artemis Showcase UI & Admin Console..." -ForegroundColor Green if ($NoOpen) { uv run artemis ui --port $Port --no-open } else { diff --git a/start.sh b/start.sh index 18396c2..f115bcc 100755 --- a/start.sh +++ b/start.sh @@ -156,7 +156,7 @@ fi # 7. Optionally configure MCP for detected AI IDEs echo "" echo -e " ${CYAN}🔌 Would you like to configure ARTEMIS MCP & testing rules for your AI IDEs?${NC}" -echo -e " (Supported: Antigravity, Cursor, Claude Code/Desktop, OpenClaw, Windsurf)" +echo -e " (Supported: Antigravity, Cursor, Claude Code/Desktop, Codex, OpenClaw, Windsurf)" if [ -t 0 ]; then read -r -p " Install MCP configuration & rules now? [Y/n]: " INSTALL_MCP INSTALL_MCP=${INSTALL_MCP:-Y} @@ -165,9 +165,13 @@ else fi if [[ "${INSTALL_MCP}" =~ ^[Yy]$ ]]; then - echo -e " ${GREEN}✔ Installing MCP server configuration & testing rules...${NC}" - uv run artemis mcp --install all >/dev/null 2>&1 || true - echo -e " ${CYAN}💡 Tip: You can update or re-install anytime with: ${BOLD}uv run artemis mcp --install all${NC}" + echo -e " ${CYAN}Installing MCP server configuration & testing rules...${NC}" + if uv run artemis mcp --install all; then + echo -e " ${GREEN}✔ MCP configuration and rules installed successfully.${NC}" + echo -e " ${CYAN}💡 Tip: You can update or re-install anytime with: ${BOLD}uv run artemis mcp --install all${NC}" + else + echo -e " ${YELLOW}⚠ MCP installation failed. Review the error above and retry with: ${BOLD}uv run artemis mcp --install all${NC}" + fi else echo -e " ${YELLOW}⏭️ Skipped. You can install MCP anytime later with: ${BOLD}uv run artemis mcp --install all${NC}" fi diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index e13ba12..756b444 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -90,7 +90,7 @@ def test_cli_mcp_generate_config_antigravity(): def test_cli_mcp_generate_config_all(): - """Verify 'artemis mcp --generate-config all' includes antigravity, cursor, windsurf, claude, vscode, cline, and roo.""" + """Verify 'artemis mcp --generate-config all' includes every supported client.""" result = runner.invoke(app, ["mcp", "--generate-config", "all"]) assert result.exit_code == 0 assert "antigravity" in result.output @@ -100,6 +100,15 @@ def test_cli_mcp_generate_config_all(): assert "vscode" in result.output assert "cline" in result.output assert "roo" in result.output + assert "codex" in result.output + + +def test_cli_mcp_generate_config_codex(): + """Verify Codex generation produces a ready-to-use TOML server block.""" + result = runner.invoke(app, ["mcp", "--generate-config", "codex"]) + assert result.exit_code == 0 + assert "[mcp_servers.artemis]" in result.output + assert "[mcp_servers.artemis.env]" in result.output def test_cli_mcp_install_antigravity(tmp_path, monkeypatch): @@ -137,6 +146,7 @@ def test_cli_mcp_install_all(tmp_path, monkeypatch): assert (tmp_path / ".codeium" / "windsurf" / "mcp_config.json").exists() assert (tmp_path / ".openclaw" / "openclaw.json").exists() assert (tmp_path / ".claude.json").exists() + assert (tmp_path / ".codex" / "config.toml").exists() # Verify global rule files installed across IDEs assert (tmp_path / ".gemini" / "GEMINI.md").exists() @@ -154,6 +164,41 @@ def test_cli_mcp_install_all(tmp_path, monkeypatch): assert (tmp_path / ".roo" / "rules" / "artemis.md").exists() assert (tmp_path / ".openclaw" / "OPENCLAW.md").exists() assert (tmp_path / ".openclaw" / "rules" / "artemis.md").exists() + assert (tmp_path / ".codex" / "AGENTS.md").exists() + + +def test_cli_mcp_install_codex_preserves_config_and_is_idempotent(tmp_path, monkeypatch): + """Verify Codex TOML merging preserves other servers and updates only Artemis.""" + monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) + monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path)) + codex_dir = tmp_path / ".codex" + codex_dir.mkdir(parents=True) + config_file = codex_dir / "config.toml" + config_file.write_text( + 'model = "test-model"\n\n' + '[mcp_servers.existing]\ncommand = "echo"\n\n' + '[mcp_servers.artemis]\ncommand = "old-command"\n', + encoding="utf-8", + ) + + result = runner.invoke(app, ["mcp", "--install", "codex"]) + assert result.exit_code == 0 + result2 = runner.invoke(app, ["mcp", "--install", "codex"]) + assert result2.exit_code == 0 + + import tomllib + config_text = config_file.read_text(encoding="utf-8") + data = tomllib.loads(config_text) + assert data["model"] == "test-model" + assert data["mcp_servers"]["existing"]["command"] == "echo" + assert data["mcp_servers"]["artemis"]["args"] == ["-m", "mcp_server"] + assert data["mcp_servers"]["artemis"]["env"]["PYTHONPATH"] == str(tmp_path) + assert config_text.count("# BEGIN ARTEMIS MCP CONFIG") == 1 + + agents_file = codex_dir / "AGENTS.md" + agents_text = agents_file.read_text(encoding="utf-8") + assert "Mobile Testing Mindset" in agents_text + assert agents_text.count("") == 1 def test_cli_mcp_install_jsonc_and_backup(tmp_path, monkeypatch):