evo-cli 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {evo_cli-0.7.0 → evo_cli-0.8.0}/PKG-INFO +1 -1
  2. evo_cli-0.8.0/evo_cli/VERSION +1 -0
  3. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/cli.py +11 -2
  4. evo_cli-0.8.0/evo_cli/commands/opencode.py +323 -0
  5. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/PKG-INFO +1 -1
  6. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/SOURCES.txt +2 -0
  7. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/test_cli.py +12 -1
  8. evo_cli-0.8.0/tests/test_opencode.py +117 -0
  9. evo_cli-0.7.0/evo_cli/VERSION +0 -1
  10. {evo_cli-0.7.0 → evo_cli-0.8.0}/Containerfile +0 -0
  11. {evo_cli-0.7.0 → evo_cli-0.8.0}/HISTORY.md +0 -0
  12. {evo_cli-0.7.0 → evo_cli-0.8.0}/LICENSE +0 -0
  13. {evo_cli-0.7.0 → evo_cli-0.8.0}/MANIFEST.in +0 -0
  14. {evo_cli-0.7.0 → evo_cli-0.8.0}/README.md +0 -0
  15. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/__init__.py +0 -0
  16. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/__main__.py +0 -0
  17. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/base.py +0 -0
  18. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/__init__.py +0 -0
  19. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/cloudflare.py +0 -0
  20. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/fix_claude.py +0 -0
  21. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/gdrive.py +0 -0
  22. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/localproxy.py +0 -0
  23. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/miniconda.py +0 -0
  24. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/netcheck.py +0 -0
  25. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/plantuml.py +0 -0
  26. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/site2s.py +0 -0
  27. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/ssh.py +0 -0
  28. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/sysmon.py +0 -0
  29. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/commands/wifi.py +0 -0
  30. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli/console.py +0 -0
  31. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  32. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/entry_points.txt +0 -0
  33. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/requires.txt +0 -0
  34. {evo_cli-0.7.0 → evo_cli-0.8.0}/evo_cli.egg-info/top_level.txt +0 -0
  35. {evo_cli-0.7.0 → evo_cli-0.8.0}/pyproject.toml +0 -0
  36. {evo_cli-0.7.0 → evo_cli-0.8.0}/setup.cfg +0 -0
  37. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/__init__.py +0 -0
  38. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/test_fix_claude.py +0 -0
  39. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/test_plantuml.py +0 -0
  40. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/test_sysmon.py +0 -0
  41. {evo_cli-0.7.0 → evo_cli-0.8.0}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.8.0
@@ -7,12 +7,18 @@ from evo_cli.commands.gdrive import gdrive
7
7
  from evo_cli.commands.localproxy import localproxy
8
8
  from evo_cli.commands.miniconda import miniconda
9
9
  from evo_cli.commands.netcheck import netcheck
10
+ from evo_cli.commands.opencode import setup_opencode
10
11
  from evo_cli.commands.plantuml import plantuml
11
12
  from evo_cli.commands.site2s import site2s
12
13
  from evo_cli.commands.ssh import setupssh
13
14
  from evo_cli.commands.sysmon import sysmon
14
15
  from evo_cli.commands.wifi import wifi
15
16
 
17
+
18
+ @click.group("setup", help="Set up development tools and environments.")
19
+ def setup_group():
20
+ """Commands for bootstrapping tools on a fresh machine."""
21
+
16
22
  click.rich_click.USE_MARKDOWN = True
17
23
  click.rich_click.SHOW_ARGUMENTS = True
18
24
  click.rich_click.STYLE_OPTIONS_TABLE_BOX = "SIMPLE"
@@ -34,8 +40,11 @@ def cli():
34
40
  """
35
41
 
36
42
 
37
- cli.add_command(setupssh)
38
- cli.add_command(miniconda)
43
+ setup_group.add_command(setup_opencode)
44
+ setup_group.add_command(miniconda)
45
+ setup_group.add_command(setupssh)
46
+
47
+ cli.add_command(setup_group)
39
48
  cli.add_command(cfssh)
40
49
  cli.add_command(f_claude)
41
50
  cli.add_command(gdrive)
@@ -0,0 +1,323 @@
1
+ """Install and configure OpenCode with common MCP servers."""
2
+
3
+ import json
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ import rich_click as click
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+
13
+ from evo_cli.console import console, error, info, run_command, step, success, warning
14
+
15
+ EPILOG = Text.from_markup(
16
+ "[bold]Examples[/bold]\n\n"
17
+ " [cyan]evo setup opencode[/cyan]\n"
18
+ " [cyan]evo setup opencode --global-only[/cyan]\n"
19
+ " [cyan]evo setup opencode --project .[/cyan]"
20
+ )
21
+
22
+ DEFAULT_MCP_SERVERS = {
23
+ "google-search": {
24
+ "type": "local",
25
+ "command": ["npx", "-y", "@mcp-server/google-search-mcp@latest"],
26
+ "enabled": True,
27
+ },
28
+ "playwright": {
29
+ "type": "local",
30
+ "command": ["npx", "-y", "@playwright/mcp@latest", "--headless"],
31
+ "enabled": True,
32
+ },
33
+ }
34
+
35
+
36
+ def is_windows():
37
+ return platform.system() == "Windows"
38
+
39
+
40
+ def get_global_config_dir():
41
+ home = Path.home()
42
+ if is_windows():
43
+ return home / "AppData" / "Roaming" / "opencode"
44
+ return home / ".config" / "opencode"
45
+
46
+
47
+ def get_global_config_path():
48
+ return get_global_config_dir() / "opencode.jsonc"
49
+
50
+
51
+ def ensure_node_installed():
52
+ """Ensure Node.js and npm are available; return their paths."""
53
+ node_cmd = shutil.which("node")
54
+ npm_cmd = shutil.which("npm")
55
+ npx_cmd = shutil.which("npx")
56
+
57
+ if node_cmd and npm_cmd and npx_cmd:
58
+ result = subprocess.run(["node", "--version"], capture_output=True, text=True, check=True)
59
+ info(f"Node.js found: {result.stdout.strip()}")
60
+ return node_cmd, npm_cmd, npx_cmd
61
+
62
+ info("Node.js/npm not found. Installing via package manager...")
63
+ system = platform.system()
64
+ if system == "Linux":
65
+ try:
66
+ if shutil.which("apt-get"):
67
+ run_command(
68
+ ["sudo", "apt-get", "update"],
69
+ status="Updating apt package lists",
70
+ )
71
+ run_command(
72
+ ["sudo", "apt-get", "install", "-y", "nodejs", "npm"],
73
+ status="Installing nodejs and npm",
74
+ )
75
+ elif shutil.which("dnf"):
76
+ run_command(
77
+ ["sudo", "dnf", "install", "-y", "nodejs", "npm"],
78
+ status="Installing nodejs and npm",
79
+ )
80
+ elif shutil.which("pacman"):
81
+ run_command(
82
+ ["sudo", "pacman", "-Sy", "--noconfirm", "nodejs", "npm"],
83
+ status="Installing nodejs and npm",
84
+ )
85
+ elif shutil.which("apk"):
86
+ run_command(
87
+ ["sudo", "apk", "add", "nodejs", "npm"],
88
+ status="Installing nodejs and npm",
89
+ )
90
+ else:
91
+ raise RuntimeError("No supported package manager found (apt-get, dnf, pacman, apk)")
92
+ except Exception as exc:
93
+ error(f"Could not install Node.js automatically: {exc}")
94
+ info("Please install Node.js 18+ manually from https://nodejs.org/")
95
+ raise
96
+ elif system == "Darwin":
97
+ if shutil.which("brew"):
98
+ run_command(["brew", "install", "node"], status="Installing Node.js via Homebrew")
99
+ else:
100
+ raise RuntimeError("Homebrew not found. Please install Node.js 18+ manually.")
101
+ elif system == "Windows":
102
+ raise RuntimeError("Please install Node.js 18+ manually from https://nodejs.org/")
103
+ else:
104
+ raise RuntimeError(f"Unsupported platform: {system}")
105
+
106
+ node_cmd = shutil.which("node")
107
+ npm_cmd = shutil.which("npm")
108
+ npx_cmd = shutil.which("npx")
109
+ if not (node_cmd and npm_cmd and npx_cmd):
110
+ raise RuntimeError("Node.js installation succeeded but binaries are not on PATH")
111
+ return node_cmd, npm_cmd, npx_cmd
112
+
113
+
114
+ def install_mcp_servers():
115
+ """Install MCP server npm packages globally via npx."""
116
+ step("Installing MCP servers")
117
+ servers = [
118
+ "@mcp-server/google-search-mcp@latest",
119
+ "@playwright/mcp@latest",
120
+ ]
121
+ for server in servers:
122
+ info(f"Installing {server}")
123
+ run_command(["npx", "-y", server, "--version"], status=f"Fetching {server}")
124
+ success("MCP server packages ready")
125
+
126
+
127
+ def install_playwright_browsers():
128
+ """Install Playwright Chromium browser binaries."""
129
+ step("Installing Playwright browsers")
130
+ try:
131
+ run_command(
132
+ ["npx", "playwright", "install", "chromium"],
133
+ status="Downloading Chromium for Playwright",
134
+ )
135
+ success("Playwright Chromium installed")
136
+ except Exception as exc:
137
+ warning(f"Playwright browser install failed: {exc}")
138
+ info("You can retry later with: npx playwright install chromium")
139
+
140
+
141
+ def load_jsonc(path):
142
+ """Load a JSONC file, stripping // comments.
143
+
144
+ Be careful not to truncate URLs that contain '://' inside a JSON string.
145
+ """
146
+ path = Path(path)
147
+ if not path.exists():
148
+ return {}
149
+ text = path.read_text(encoding="utf-8")
150
+ cleaned_lines = []
151
+ for line in text.splitlines():
152
+ new_line = []
153
+ in_string = False
154
+ escape = False
155
+ for idx, ch in enumerate(line):
156
+ if escape:
157
+ new_line.append(ch)
158
+ escape = False
159
+ continue
160
+ if ch == "\\":
161
+ new_line.append(ch)
162
+ escape = True
163
+ continue
164
+ if ch == '"':
165
+ in_string = not in_string
166
+ new_line.append(ch)
167
+ continue
168
+ if not in_string and ch == "/" and idx + 1 < len(line) and line[idx + 1] == "/":
169
+ break
170
+ new_line.append(ch)
171
+ stripped = "".join(new_line).strip()
172
+ if stripped.startswith("//"):
173
+ continue
174
+ cleaned_lines.append("".join(new_line))
175
+ cleaned = "\n".join(cleaned_lines)
176
+ if not cleaned.strip():
177
+ return {}
178
+ try:
179
+ return json.loads(cleaned)
180
+ except json.JSONDecodeError as exc:
181
+ raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
182
+
183
+
184
+ def save_jsonc(path, data, header=None):
185
+ """Save data as pretty-printed JSONC."""
186
+ path.parent.mkdir(parents=True, exist_ok=True)
187
+ content = json.dumps(data, indent=2, ensure_ascii=False)
188
+ if header:
189
+ content = header + "\n" + content
190
+ path.write_text(content + "\n", encoding="utf-8")
191
+
192
+
193
+ def merge_mcp_config(existing, new_servers):
194
+ """Merge new MCP servers into existing config, preserving user values."""
195
+ result = dict(existing)
196
+ mcp = result.setdefault("mcp", {})
197
+ for name, config in new_servers.items():
198
+ if name in mcp:
199
+ info(f"MCP server [accent]{name}[/accent] already configured; skipping")
200
+ continue
201
+ mcp[name] = dict(config)
202
+ success(f"Added MCP server [accent]{name}[/accent]")
203
+ return result
204
+
205
+
206
+ def configure_opencode_global():
207
+ """Write/update the global OpenCode config with MCP servers."""
208
+ step("Configuring global OpenCode")
209
+ config_path = get_global_config_path()
210
+ config_dir = config_path.parent
211
+ config_dir.mkdir(parents=True, exist_ok=True)
212
+
213
+ existing = load_jsonc(config_path) if config_path.exists() else {}
214
+ merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
215
+
216
+ header = "// OpenCode global configuration generated by evo setup opencode"
217
+ save_jsonc(config_path, merged, header=header)
218
+ success(f"Global config written to [accent]{config_path}[/accent]")
219
+ return config_path
220
+
221
+
222
+ def configure_opencode_project(project_path):
223
+ """Write/update a project-level OpenCode config with MCP servers."""
224
+ step("Configuring project OpenCode")
225
+ project_dir = Path(project_path).resolve()
226
+ config_path = project_dir / "opencode.json"
227
+
228
+ existing = load_jsonc(config_path) if config_path.exists() else {}
229
+ merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
230
+
231
+ header = "// OpenCode project configuration generated by evo setup opencode"
232
+ save_jsonc(config_path, merged, header=header)
233
+ success(f"Project config written to [accent]{config_path}[/accent]")
234
+ return config_path
235
+
236
+
237
+ def verify_mcp_servers():
238
+ """Run a basic JSON-RPC initialize check against installed MCP servers."""
239
+ step("Verifying MCP servers")
240
+ init_message = (
241
+ '{"jsonrpc":"2.0","id":1,"method":"initialize",'
242
+ '"params":{"protocolVersion":"2024-11-05","capabilities":{},'
243
+ '"clientInfo":{"name":"evo-cli","version":"1.0"}}}'
244
+ )
245
+ checks = [
246
+ ("playwright", ["npx", "-y", "@playwright/mcp@latest", "--headless"]),
247
+ ("google-search", ["npx", "-y", "@mcp-server/google-search-mcp@latest"]),
248
+ ]
249
+ for name, cmd in checks:
250
+ try:
251
+ result = subprocess.run(
252
+ cmd,
253
+ input=init_message,
254
+ capture_output=True,
255
+ text=True,
256
+ timeout=30,
257
+ )
258
+ if result.returncode == 0 and '"jsonrpc":"2.0"' in result.stdout:
259
+ success(f"MCP server [accent]{name}[/accent] responded to initialize")
260
+ else:
261
+ warning(f"MCP server [accent]{name}[/accent] did not respond as expected")
262
+ except Exception as exc:
263
+ warning(f"Could not verify MCP server [accent]{name}[/accent]: {exc}")
264
+
265
+
266
+ def run_setup_opencode(global_only, project):
267
+ step("evo setup opencode")
268
+
269
+ try:
270
+ ensure_node_installed()
271
+ except Exception as exc:
272
+ error(str(exc))
273
+ return
274
+
275
+ install_mcp_servers()
276
+ install_playwright_browsers()
277
+
278
+ global_path = configure_opencode_global()
279
+ project_path = None
280
+ if not global_only:
281
+ target = project or Path.cwd()
282
+ project_path = configure_opencode_project(target)
283
+
284
+ verify_mcp_servers()
285
+
286
+ console.print()
287
+ paths_text = f"Global: [accent]{global_path}[/accent]"
288
+ if project_path:
289
+ paths_text += f"\nProject: [accent]{project_path}[/accent]"
290
+
291
+ console.print(
292
+ Panel(
293
+ f"OpenCode configured with google-search and playwright MCP servers.\n\n{paths_text}\n\n"
294
+ "Restart OpenCode to load the new MCP tools.",
295
+ title="setup opencode complete",
296
+ border_style="success",
297
+ expand=False,
298
+ )
299
+ )
300
+
301
+
302
+ @click.command("opencode", epilog=EPILOG)
303
+ @click.option(
304
+ "--global-only",
305
+ is_flag=True,
306
+ help="Only update the global OpenCode config; skip project-level config.",
307
+ )
308
+ @click.option(
309
+ "--project",
310
+ type=click.Path(file_okay=False, dir_okay=True, writable=True),
311
+ help="Project directory to write opencode.json into. Defaults to the current directory.",
312
+ )
313
+ def setup_opencode(global_only, project):
314
+ """Install Node.js (if needed), OpenCode MCP servers, and write config files.
315
+
316
+ This command bootstraps a fresh machine with the same OpenCode + MCP setup
317
+ used across devices. It installs the google-search and playwright MCP servers,
318
+ downloads the Playwright Chromium browser, and writes both global
319
+ (~/.config/opencode/opencode.jsonc) and project-level (opencode.json) configs.
320
+
321
+ No credentials are bundled; only public MCP server references are added.
322
+ """
323
+ run_setup_opencode(global_only, project)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -23,6 +23,7 @@ evo_cli/commands/gdrive.py
23
23
  evo_cli/commands/localproxy.py
24
24
  evo_cli/commands/miniconda.py
25
25
  evo_cli/commands/netcheck.py
26
+ evo_cli/commands/opencode.py
26
27
  evo_cli/commands/plantuml.py
27
28
  evo_cli/commands/site2s.py
28
29
  evo_cli/commands/ssh.py
@@ -31,6 +32,7 @@ evo_cli/commands/wifi.py
31
32
  tests/__init__.py
32
33
  tests/test_cli.py
33
34
  tests/test_fix_claude.py
35
+ tests/test_opencode.py
34
36
  tests/test_plantuml.py
35
37
  tests/test_sysmon.py
36
38
  tests/test_wifi.py
@@ -4,10 +4,16 @@ from evo_cli.cli import cli
4
4
 
5
5
 
6
6
  def test_all_commands_are_registered():
7
- for name in ("setupssh", "miniconda", "cfssh", "f-claude"):
7
+ for name in ("setup", "cfssh", "f-claude"):
8
8
  assert name in cli.commands
9
9
 
10
10
 
11
+ def test_setup_subgroup_commands_are_registered():
12
+ setup_group = cli.commands["setup"]
13
+ for name in ("opencode", "miniconda", "setupssh"):
14
+ assert name in setup_group.commands
15
+
16
+
11
17
  def test_group_help_runs():
12
18
  result = CliRunner().invoke(cli, ["--help"])
13
19
  assert result.exit_code == 0
@@ -16,3 +22,8 @@ def test_group_help_runs():
16
22
  def test_fix_claude_help_runs():
17
23
  result = CliRunner().invoke(cli, ["f-claude", "--help"])
18
24
  assert result.exit_code == 0
25
+
26
+
27
+ def test_setup_opencode_help_runs():
28
+ result = CliRunner().invoke(cli, ["setup", "opencode", "--help"])
29
+ assert result.exit_code == 0
@@ -0,0 +1,117 @@
1
+ import json
2
+
3
+ import pytest
4
+ from click.testing import CliRunner
5
+
6
+ from evo_cli.cli import cli
7
+ from evo_cli.commands.opencode import (
8
+ DEFAULT_MCP_SERVERS,
9
+ configure_opencode_global,
10
+ configure_opencode_project,
11
+ load_jsonc,
12
+ merge_mcp_config,
13
+ save_jsonc,
14
+ )
15
+
16
+
17
+ @pytest.fixture
18
+ def runner():
19
+ return CliRunner()
20
+
21
+
22
+ def test_opencode_command_is_registered():
23
+ assert "setup" in cli.commands
24
+ assert "opencode" in cli.commands["setup"].commands
25
+
26
+
27
+ def test_opencode_help_runs(runner):
28
+ result = runner.invoke(cli, ["setup", "opencode", "--help"])
29
+ assert result.exit_code == 0
30
+ assert "Install Node.js" in result.output or "OpenCode" in result.output
31
+
32
+
33
+ def test_load_jsonc_ignores_comments(tmp_path):
34
+ path = tmp_path / "config.jsonc"
35
+ path.write_text("// header\n{\"a\": 1}\n// footer")
36
+ assert load_jsonc(path) == {"a": 1}
37
+
38
+
39
+ def test_load_jsonc_missing_file(tmp_path):
40
+ assert load_jsonc(tmp_path / "nope.jsonc") == {}
41
+
42
+
43
+ def test_save_and_load_jsonc(tmp_path):
44
+ path = tmp_path / "config.jsonc"
45
+ save_jsonc(path, {"foo": "bar"}, header="// test")
46
+ data = load_jsonc(path)
47
+ assert data == {"foo": "bar"}
48
+ assert path.read_text().startswith("// test")
49
+
50
+
51
+ def test_merge_mcp_config_preserves_existing():
52
+ existing = {"mcp": {"playwright": {"enabled": False}}}
53
+ merged = merge_mcp_config(existing, DEFAULT_MCP_SERVERS)
54
+ assert merged["mcp"]["playwright"]["enabled"] is False
55
+ assert merged["mcp"]["google-search"]["enabled"] is True
56
+
57
+
58
+ def test_configure_opencode_global(tmp_path, monkeypatch):
59
+ monkeypatch.setattr(
60
+ "evo_cli.commands.opencode.get_global_config_path",
61
+ lambda: tmp_path / "opencode" / "opencode.jsonc",
62
+ )
63
+ path = configure_opencode_global()
64
+ data = load_jsonc(path)
65
+ assert "mcp" in data
66
+ assert set(data["mcp"].keys()) == {"google-search", "playwright"}
67
+
68
+
69
+ def test_configure_opencode_project(tmp_path):
70
+ path = configure_opencode_project(tmp_path)
71
+ data = load_jsonc(path)
72
+ assert "mcp" in data
73
+ assert set(data["mcp"].keys()) == {"google-search", "playwright"}
74
+
75
+
76
+ def test_setup_opencode_writes_global_and_project(runner, tmp_path, monkeypatch):
77
+ monkeypatch.setattr(
78
+ "evo_cli.commands.opencode.get_global_config_path",
79
+ lambda: tmp_path / "global.jsonc",
80
+ )
81
+ monkeypatch.setattr(
82
+ "evo_cli.commands.opencode.ensure_node_installed",
83
+ lambda: ("node", "npm", "npx"),
84
+ )
85
+ monkeypatch.setattr("evo_cli.commands.opencode.install_mcp_servers", lambda: None)
86
+ monkeypatch.setattr(
87
+ "evo_cli.commands.opencode.install_playwright_browsers", lambda: None
88
+ )
89
+ monkeypatch.setattr("evo_cli.commands.opencode.verify_mcp_servers", lambda: None)
90
+
91
+ result = runner.invoke(cli, ["setup", "opencode", "--project", str(tmp_path)])
92
+ assert result.exit_code == 0
93
+ assert (tmp_path / "opencode.json").exists()
94
+ project_data = json.loads((tmp_path / "opencode.json").read_text().split("\n", 1)[1])
95
+ assert "mcp" in project_data
96
+
97
+
98
+ def test_setup_opencode_global_only(runner, tmp_path, monkeypatch):
99
+ monkeypatch.setattr(
100
+ "evo_cli.commands.opencode.get_global_config_path",
101
+ lambda: tmp_path / "global.jsonc",
102
+ )
103
+ monkeypatch.setattr(
104
+ "evo_cli.commands.opencode.ensure_node_installed",
105
+ lambda: ("node", "npm", "npx"),
106
+ )
107
+ monkeypatch.setattr("evo_cli.commands.opencode.install_mcp_servers", lambda: None)
108
+ monkeypatch.setattr(
109
+ "evo_cli.commands.opencode.install_playwright_browsers", lambda: None
110
+ )
111
+ monkeypatch.setattr("evo_cli.commands.opencode.verify_mcp_servers", lambda: None)
112
+
113
+ project = tmp_path / "project"
114
+ project.mkdir()
115
+ result = runner.invoke(cli, ["setup", "opencode", "--global-only", "--project", str(project)])
116
+ assert result.exit_code == 0
117
+ assert not (project / "opencode.json").exists()
@@ -1 +0,0 @@
1
- 0.7.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes