diff --git a/.gitignore b/.gitignore
index 6ce825b..cad0729 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,16 @@ dmypy.json
# IDEs & Editors
.vscode/
.idea/
+.cursor/
+.gemini/
+.claude/
+.windsurf/
+.cline/
+.roo/
+.openclaw/
+.clinerules
+.windsurfrules
+.roorules
*.swp
*.swo
*~
@@ -80,6 +90,7 @@ scratch/
data_engine.db
data_engine.db-journal
.ipc_port
+.artemis_ipc_port
.ls_address
.jetski_ls_address
diff --git a/README.md b/README.md
index 7eec50e..c26406a 100644
--- a/README.md
+++ b/README.md
@@ -80,17 +80,19 @@ Experience seamless collaboration between **Antigravity** and **ARTEMIS** via na
## ⚡ Quick Start
-Ensure an Android device (with **USB Debugging** enabled) or emulator is connected.
+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**).
```bash
# 1. Clone repo & navigate to directory
git clone https://github.com/google/artemis.git && cd artemis
-# 2. One-click launch (automatically installs ADB, scrcpy, FFmpeg, uv runtime, and opens console)
+# 2. One-click launch
# 🍎 macOS & 🐧 Linux
./start.sh
-# 🪟 Windows (CMD / PowerShell)
+# 🪟 Windows
start.bat
```
@@ -107,10 +109,10 @@ 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 MCP for detected IDEs (or you can install/update anytime later manually using the commands below):
+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):
```bash
-# Auto-install for Antigravity / Jetski:
+# 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):
diff --git a/README_CN.md b/README_CN.md
index d5337ce..0ee438e 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -82,17 +82,19 @@
## ⚡ 快速上手
-确保电脑已连接 Android 实体机(已开启 **USB 调试**)或 Android 模拟器。
+确保电脑已连接 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**)。
```bash
# 1. 克隆代码仓库并进入目录
git clone https://github.com/google/artemis.git && cd artemis
-# 2. 一键启动(自动检测并安装 ADB、scrcpy、FFmpeg、uv 运行时等全部依赖,并拉起控制台)
+# 2. 一键启动
# 🍎 macOS & 🐧 Linux
./start.sh
-# 🪟 Windows (CMD / PowerShell)
+# 🪟 Windows
start.bat
```
@@ -109,10 +111,10 @@ ARTEMIS 内置原生 **Model Context Protocol (MCP)** 服务。只需将以下
### 1. 一键自动安装到 IDE(推荐)
-运行 `./start.sh` 或 `start.bat` 启动脚本时会主动询问是否需要配置当前检测到的 AI IDE(支持跳过并在之后随时手动执行以下命令挂载):
+运行 `./start.sh` 或 `start.bat` 启动脚本时会主动询问是否自动挂载全局 MCP 与测试行为准则(支持跳过并在之后随时手动执行以下命令挂载):
```bash
-# 一键安装到 Antigravity / Jetski:
+# 一键安装全局 MCP 服务与 Rules 到 Antigravity / Jetski:
uv run artemis mcp --install antigravity
# 或一键安装到所有检测到的 AI IDE(Antigravity、Cursor、Claude Desktop/Code、OpenClaw):
diff --git a/artemis/interfaces/cli/commands/mcp.py b/artemis/interfaces/cli/commands/mcp.py
index 56d3b66..dfe1981 100644
--- a/artemis/interfaces/cli/commands/mcp.py
+++ b/artemis/interfaces/cli/commands/mcp.py
@@ -76,18 +76,43 @@ def _get_config_snippet(client: str, python_exe: str, project_root: str) -> dict
return {"mcpServers": {"artemis": config_body}}
+def _parse_json_lenient(text: str) -> dict:
+ """Parses JSON or JSONC (JSON with comments / trailing commas) safely."""
+ try:
+ data = json.loads(text)
+ 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
+ cleaned = re.sub(r"/\*.*?\*/", "", cleaned, flags=re.DOTALL)
+ # Remove trailing commas before } or ]
+ cleaned = re.sub(r",\s*([}\]])", r"\1", cleaned)
+ try:
+ data = json.loads(cleaned)
+ return data if isinstance(data, dict) else {}
+ except Exception:
+ return {}
+
+
def _merge_json_file(file_path: Path, server_name: str, server_config: dict, key_name: str = "mcpServers") -> bool:
"""Merges server configuration into a target JSON file without overwriting existing servers."""
try:
file_path.parent.mkdir(parents=True, exist_ok=True)
data: dict = {}
if file_path.exists() and file_path.stat().st_size > 0:
- try:
- data = json.loads(file_path.read_text(encoding="utf-8"))
- if not isinstance(data, dict):
- data = {}
- except Exception:
- data = {}
+ raw_text = file_path.read_text(encoding="utf-8")
+ data = _parse_json_lenient(raw_text)
+ if not data and raw_text.strip():
+ # Backup unparseable existing file to prevent accidental loss
+ backup_path = file_path.with_suffix(file_path.suffix + ".bak")
+ try:
+ backup_path.write_text(raw_text, encoding="utf-8")
+ logger.warning(f"Existing JSON in {file_path} could not be parsed; backup created at {backup_path}")
+ except Exception:
+ pass
if key_name not in data or not isinstance(data.get(key_name), dict):
data[key_name] = {}
@@ -100,8 +125,168 @@ def _merge_json_file(file_path: Path, server_name: str, server_config: dict, key
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:
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+ begin_marker = ""
+ end_marker = ""
+ new_block = f"{begin_marker}\n{content.strip()}\n{end_marker}\n"
+
+ existing = ""
+ if file_path.exists():
+ try:
+ existing = file_path.read_text(encoding="utf-8")
+ except Exception:
+ existing = ""
+
+ if begin_marker in existing and end_marker in existing:
+ prefix = existing.split(begin_marker)[0]
+ suffix = existing.split(end_marker)[1]
+ updated = f"{prefix.rstrip()}\n\n{new_block}\n{suffix.lstrip()}".strip() + "\n"
+ else:
+ updated = f"{existing.rstrip()}\n\n{new_block}".strip() + "\n"
+
+ if existing == updated:
+ return True
+
+ file_path.write_text(updated, encoding="utf-8")
+ return True
+ except Exception as e:
+ logger.warning(f"Could not inject rules block into {file_path}: {e}")
+ return False
+
+
+def _write_rule_file(file_path: Path, content: str) -> bool:
+ """Writes or overwrites a standalone rule file."""
+ try:
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+ new_text = content.strip() + "\n"
+ if file_path.exists():
+ try:
+ if file_path.read_text(encoding="utf-8") == new_text:
+ return True
+ except Exception:
+ pass
+ file_path.write_text(new_text, encoding="utf-8")
+ return True
+ except Exception as e:
+ logger.warning(f"Could not write rule file {file_path}: {e}")
+ return False
+
+
+def _write_cursor_mdc(file_path: Path, content: str) -> bool:
+ """Writes a Cursor .mdc rule file with required YAML frontmatter."""
+ try:
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+ mdc_header = (
+ "---\n"
+ "description: Artemis Mobile Testing Mindset & Rules\n"
+ "globs: **/*\n"
+ "alwaysApply: true\n"
+ "---\n\n"
+ )
+ new_text = mdc_header + content.strip() + "\n"
+ if file_path.exists():
+ try:
+ if file_path.read_text(encoding="utf-8") == new_text:
+ return True
+ except Exception:
+ pass
+ file_path.write_text(new_text, encoding="utf-8")
+ return True
+ except Exception as e:
+ logger.warning(f"Could not write Cursor rule file {file_path}: {e}")
+ return False
+
+
+def install_rules(client: str, project_root: str) -> list[str]:
+ """Auto-installs ARTEMIS testing rules globally into user IDE rule directories across any OS."""
+ installed_paths: list[str] = []
+ rules_src = Path(project_root) / "mcp_server" / "rules.md"
+ if not rules_src.exists():
+ alt_src = Path(__file__).resolve().parents[4] / "mcp_server" / "rules.md"
+ if alt_src.exists():
+ rules_src = alt_src
+ else:
+ logger.warning(f"Could not locate rules.md at {rules_src}")
+ return []
+
+ try:
+ raw_rules = rules_src.read_text(encoding="utf-8").strip()
+ except Exception as e:
+ logger.warning(f"Could not read rules file {rules_src}: {e}")
+ return []
+
+ targets = (
+ ["antigravity", "cursor", "claude", "windsurf", "vscode", "cline", "roo", "openclaw"]
+ if client == "all"
+ else [client]
+ )
+
+ for target in targets:
+ if target in ("antigravity", "jetski"):
+ gemini_md = Path.home() / ".gemini" / "GEMINI.md"
+ global_rule = Path.home() / ".gemini" / "rules" / "artemis.md"
+ if _inject_rules_block(gemini_md, raw_rules):
+ installed_paths.append(str(gemini_md))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ elif target == "cursor":
+ cursor_rules_file = Path.home() / ".cursorrules"
+ global_mdc = Path.home() / ".cursor" / "rules" / "artemis.mdc"
+ if _inject_rules_block(cursor_rules_file, raw_rules):
+ installed_paths.append(str(cursor_rules_file))
+ if _write_cursor_mdc(global_mdc, raw_rules):
+ installed_paths.append(str(global_mdc))
+ elif target in ("claude", "claude_code", "claude_desktop"):
+ claude_md = Path.home() / ".claude" / "CLAUDE.md"
+ global_rule = Path.home() / ".claude" / "rules" / "artemis.md"
+ if _inject_rules_block(claude_md, raw_rules):
+ installed_paths.append(str(claude_md))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ elif target == "windsurf":
+ global_rule = Path.home() / ".codeium" / "windsurf" / "rules" / "artemis.md"
+ global_mem = Path.home() / ".codeium" / "windsurf" / "memories" / "global_rules.md"
+ if _inject_rules_block(global_mem, raw_rules):
+ installed_paths.append(str(global_mem))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ elif target == "vscode":
+ global_rule = Path.home() / ".vscode" / "rules" / "artemis.md"
+ user_rule = _get_vscode_user_dir() / "rules" / "artemis.md"
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ if _write_rule_file(user_rule, raw_rules):
+ installed_paths.append(str(user_rule))
+ elif target == "cline":
+ clinerules = Path.home() / ".clinerules"
+ global_rule = Path.home() / ".cline" / "rules" / "artemis.md"
+ if _inject_rules_block(clinerules, raw_rules):
+ installed_paths.append(str(clinerules))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ elif target in ("roo", "roo_code"):
+ roorules = Path.home() / ".roorules"
+ global_rule = Path.home() / ".roo" / "rules" / "artemis.md"
+ if _inject_rules_block(roorules, raw_rules):
+ installed_paths.append(str(roorules))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+ elif target == "openclaw":
+ openclaw_md = Path.home() / ".openclaw" / "OPENCLAW.md"
+ global_rule = Path.home() / ".openclaw" / "rules" / "artemis.md"
+ if _inject_rules_block(openclaw_md, raw_rules):
+ installed_paths.append(str(openclaw_md))
+ if _write_rule_file(global_rule, raw_rules):
+ installed_paths.append(str(global_rule))
+
+ return installed_paths
+
+
def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[str]:
- """Auto-installs/merges ARTEMIS MCP configuration into IDE config files across any OS."""
+ """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"]
@@ -166,7 +351,14 @@ def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[
if _merge_json_file(openclaw_path, "artemis_mcp", plugin_cfg, key_name="plugins"):
installed_paths.append(str(openclaw_path))
- return installed_paths
+ rules_paths = install_rules(client, project_root)
+ installed_paths.extend(rules_paths)
+
+ unique_paths: list[str] = []
+ for p in installed_paths:
+ if p not in unique_paths:
+ unique_paths.append(p)
+ return unique_paths
def mcp_command(
@@ -205,7 +397,7 @@ def mcp_command(
typer.Option(
"--install",
"-i",
- help="Auto-install and merge ARTEMIS MCP configuration 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', or 'all'.",
),
] = None,
generate_config: Annotated[
@@ -243,7 +435,7 @@ def mcp_command(
)
raise typer.Exit(1)
installed_paths = install_mcp_config(client, python_exe, project_root)
- console.print("[bold green]✔ Successfully installed ARTEMIS MCP server configuration to:[/bold green]")
+ console.print("[bold green]✔ Successfully installed ARTEMIS MCP server configuration & rules to:[/bold green]")
for path in installed_paths:
console.print(f" • [cyan]{path}[/cyan]")
console.print("\n[dim]Please restart or reload your IDE window to activate the Artemis MCP tools.[/dim]")
diff --git a/mcp_server/README.md b/mcp_server/README.md
index ed884f5..ae1309e 100644
--- a/mcp_server/README.md
+++ b/mcp_server/README.md
@@ -41,11 +41,17 @@ The included [`rules.md`](./rules.md) file contains the **Mobile Testing Mindset
3. **Latency & Timing Compensation**: Clarifies the difference between AI exploratory latency (e.g., model decision intervals) and the deterministic timing requirements of final test code.
4. **"Dynamic-First, Coordinate-Fallback" Locator Pattern**: Teaches the AI to prioritize dynamic UI locators (Resource IDs, OCR text, semantics) for layout resilience, while implementing absolute coordinate fallbacks for maximum execution reliability.
-### How to Apply `rules.md` in Your IDE
-* **Antigravity**: Add or copy the contents of `rules.md` into 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 (or reference it directly in your instructions).
-* **Cursor**: Copy the contents of `rules.md` into your `.cursorrules` or create a new rule file in `.cursor/rules/artemis.mdc`.
-* **Windsurf / OpenClaw**: Add the rules to your global/workspace rules or agent system prompts so the assistant always follows verified mobile testing principles.
+### How Global Rules & MCP are Installed
+When you run `uv run artemis mcp --install all` (or target a specific IDE like `--install cursor`, `--install claude`, etc.), the installer **automatically synchronizes both global MCP server configuration and global testing rules** into the corresponding user home directories (without creating project-level rule files in your workspace):
+* **Antigravity / Jetski**: Automatically installs global rules to `~/.gemini/rules/artemis.md`.
+* **Cursor**: Automatically installs global YAML-frontmatter rule file at `~/.cursor/rules/artemis.mdc`.
+* **Claude Code / Desktop**: Automatically installs global rules to `~/.claude/rules/artemis.md`.
+* **Windsurf**: Automatically installs global rules to `~/.codeium/windsurf/rules/artemis.md`.
+* **VS Code**: Automatically installs global rules to `~/.vscode/rules/artemis.md`.
+* **Cline / Roo Code**: Automatically installs global rules to `~/.cline/rules/artemis.md` and `~/.roo/rules/artemis.md`.
+* **OpenClaw**: Automatically installs global rules to `~/.openclaw/rules/artemis.md`.
+
+*(Note: If you prefer manual configuration, you can also copy the contents of [`rules.md`](./rules.md) into your IDE's system prompt or global rules settings.)*
## 🛠️ MCP Tools Overview
diff --git a/scripts/start.ps1 b/scripts/start.ps1
index fced28c..2041d44 100644
--- a/scripts/start.ps1
+++ b/scripts/start.ps1
@@ -132,11 +132,11 @@ if ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Path $ShowcaseIndexAlt1))
# 7. Optionally configure MCP for detected AI IDEs
Write-Host ""
-Write-Host " 🔌 Would you like to configure ARTEMIS MCP for your AI IDEs?" -ForegroundColor Cyan
+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)"
$installMcp = "Y"
if ([Console]::IsInputRedirected -eq $false) {
- $response = Read-Host " Install MCP configuration now? [Y/n]"
+ $response = Read-Host " Install MCP configuration & rules now? [Y/n]"
if ($response -ne "") {
$installMcp = $response
}
@@ -145,7 +145,7 @@ if ([Console]::IsInputRedirected -eq $false) {
}
if ($installMcp -match "^[Yy]") {
- Write-Host " ✔ Installing MCP server configuration..." -ForegroundColor Green
+ 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
} else {
diff --git a/start.sh b/start.sh
index e775656..18396c2 100755
--- a/start.sh
+++ b/start.sh
@@ -155,17 +155,17 @@ fi
# 7. Optionally configure MCP for detected AI IDEs
echo ""
-echo -e " ${CYAN}🔌 Would you like to configure ARTEMIS MCP for your AI IDEs?${NC}"
+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)"
if [ -t 0 ]; then
- read -r -p " Install MCP configuration now? [Y/n]: " INSTALL_MCP
+ read -r -p " Install MCP configuration & rules now? [Y/n]: " INSTALL_MCP
INSTALL_MCP=${INSTALL_MCP:-Y}
else
INSTALL_MCP="n"
fi
if [[ "${INSTALL_MCP}" =~ ^[Yy]$ ]]; then
- echo -e " ${GREEN}✔ Installing MCP server configuration...${NC}"
+ 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}"
else
diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py
index 16f4445..e13ba12 100644
--- a/tests/unit/test_cli.py
+++ b/tests/unit/test_cli.py
@@ -103,11 +103,12 @@ def test_cli_mcp_generate_config_all():
def test_cli_mcp_install_antigravity(tmp_path, monkeypatch):
- """Verify 'artemis mcp --install antigravity' writes configuration into target file."""
+ """Verify 'artemis mcp --install antigravity' writes configuration and global rules into target files."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
+ monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
result = runner.invoke(app, ["mcp", "--install", "antigravity"])
assert result.exit_code == 0
- assert "Successfully installed ARTEMIS MCP server configuration" in result.output
+ assert "Successfully installed ARTEMIS MCP server configuration & rules" in result.output
jetski_file = tmp_path / ".gemini" / "jetski" / "mcp_config.json"
assert jetski_file.exists()
import json
@@ -116,16 +117,72 @@ def test_cli_mcp_install_antigravity(tmp_path, monkeypatch):
assert "mobile_run_task" in data["mcpServers"]["artemis"]["tools"]
assert "PYTHONPATH" in data["mcpServers"]["artemis"]["env"]
+ # Verify global rule file installed
+ gemini_md = tmp_path / ".gemini" / "GEMINI.md"
+ assert gemini_md.exists()
+ assert "Mobile Testing Mindset" in gemini_md.read_text(encoding="utf-8")
+ rule_file = tmp_path / ".gemini" / "rules" / "artemis.md"
+ assert rule_file.exists()
+ assert "Mobile Testing Mindset" in rule_file.read_text(encoding="utf-8")
+
def test_cli_mcp_install_all(tmp_path, monkeypatch):
- """Verify 'artemis mcp --install all' installs to all supported IDE locations."""
+ """Verify 'artemis mcp --install all' installs configs and global rules to all supported IDE locations."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
+ monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
result = runner.invoke(app, ["mcp", "--install", "all"])
assert result.exit_code == 0
- assert "Successfully installed ARTEMIS MCP server configuration" in result.output
+ assert "Successfully installed ARTEMIS MCP server configuration & rules" in result.output
assert (tmp_path / ".cursor" / "mcp.json").exists()
assert (tmp_path / ".codeium" / "windsurf" / "mcp_config.json").exists()
assert (tmp_path / ".openclaw" / "openclaw.json").exists()
assert (tmp_path / ".claude.json").exists()
+ # Verify global rule files installed across IDEs
+ assert (tmp_path / ".gemini" / "GEMINI.md").exists()
+ assert (tmp_path / ".gemini" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".cursorrules").exists()
+ assert (tmp_path / ".cursor" / "rules" / "artemis.mdc").exists()
+ assert (tmp_path / ".claude" / "CLAUDE.md").exists()
+ assert (tmp_path / ".claude" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".codeium" / "windsurf" / "memories" / "global_rules.md").exists()
+ assert (tmp_path / ".codeium" / "windsurf" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".vscode" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".clinerules").exists()
+ assert (tmp_path / ".cline" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".roorules").exists()
+ assert (tmp_path / ".roo" / "rules" / "artemis.md").exists()
+ assert (tmp_path / ".openclaw" / "OPENCLAW.md").exists()
+ assert (tmp_path / ".openclaw" / "rules" / "artemis.md").exists()
+
+
+def test_cli_mcp_install_jsonc_and_backup(tmp_path, monkeypatch):
+ """Verify merging configuration preserves valid JSONC and creates .bak for unparseable JSON."""
+ monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
+ monkeypatch.setattr("mcp_server.utils.env_utils.get_project_root", lambda: str(tmp_path))
+
+ cursor_dir = tmp_path / ".cursor"
+ cursor_dir.mkdir(parents=True, exist_ok=True)
+ mcp_json = cursor_dir / "mcp.json"
+
+ # 1. Valid JSONC with comments and trailing comma
+ mcp_json.write_text(
+ '{\n // My existing server\n "mcpServers": {\n "test": {"command": "echo"},\n }\n}',
+ encoding="utf-8",
+ )
+ result = runner.invoke(app, ["mcp", "--install", "cursor"])
+ assert result.exit_code == 0
+ import json
+ data = json.loads(mcp_json.read_text(encoding="utf-8"))
+ assert "test" in data["mcpServers"]
+ assert "artemis" in data["mcpServers"]
+
+ # 2. Corrupt / unparseable file triggers backup
+ mcp_json.write_text("INVALID JSON DATA {{{{", encoding="utf-8")
+ result2 = runner.invoke(app, ["mcp", "--install", "cursor"])
+ assert result2.exit_code == 0
+ backup_file = cursor_dir / "mcp.json.bak"
+ assert backup_file.exists()
+ assert "INVALID JSON DATA" in backup_file.read_text(encoding="utf-8")
+