evo-cli 0.5.1__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.5.1 → evo_cli-0.8.0}/PKG-INFO +1 -1
  2. evo_cli-0.8.0/evo_cli/VERSION +1 -0
  3. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/cli.py +15 -2
  4. evo_cli-0.8.0/evo_cli/commands/opencode.py +323 -0
  5. evo_cli-0.8.0/evo_cli/commands/sysmon.py +545 -0
  6. evo_cli-0.8.0/evo_cli/commands/wifi.py +837 -0
  7. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/PKG-INFO +1 -1
  8. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/SOURCES.txt +7 -1
  9. {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_cli.py +12 -1
  10. evo_cli-0.8.0/tests/test_opencode.py +117 -0
  11. evo_cli-0.8.0/tests/test_sysmon.py +91 -0
  12. evo_cli-0.8.0/tests/test_wifi.py +154 -0
  13. evo_cli-0.5.1/evo_cli/VERSION +0 -1
  14. {evo_cli-0.5.1 → evo_cli-0.8.0}/Containerfile +0 -0
  15. {evo_cli-0.5.1 → evo_cli-0.8.0}/HISTORY.md +0 -0
  16. {evo_cli-0.5.1 → evo_cli-0.8.0}/LICENSE +0 -0
  17. {evo_cli-0.5.1 → evo_cli-0.8.0}/MANIFEST.in +0 -0
  18. {evo_cli-0.5.1 → evo_cli-0.8.0}/README.md +0 -0
  19. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/__init__.py +0 -0
  20. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/__main__.py +0 -0
  21. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/base.py +0 -0
  22. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/__init__.py +0 -0
  23. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/cloudflare.py +0 -0
  24. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/fix_claude.py +0 -0
  25. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/gdrive.py +0 -0
  26. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/localproxy.py +0 -0
  27. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/miniconda.py +0 -0
  28. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/netcheck.py +0 -0
  29. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/plantuml.py +0 -0
  30. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/site2s.py +0 -0
  31. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/ssh.py +0 -0
  32. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/console.py +0 -0
  33. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  34. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/entry_points.txt +0 -0
  35. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/requires.txt +0 -0
  36. {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/top_level.txt +0 -0
  37. {evo_cli-0.5.1 → evo_cli-0.8.0}/pyproject.toml +0 -0
  38. {evo_cli-0.5.1 → evo_cli-0.8.0}/setup.cfg +0 -0
  39. {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/__init__.py +0 -0
  40. {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_fix_claude.py +0 -0
  41. {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_plantuml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.5.1
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,9 +7,17 @@ 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
14
+ from evo_cli.commands.sysmon import sysmon
15
+ from evo_cli.commands.wifi import wifi
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."""
13
21
 
14
22
  click.rich_click.USE_MARKDOWN = True
15
23
  click.rich_click.SHOW_ARGUMENTS = True
@@ -32,8 +40,11 @@ def cli():
32
40
  """
33
41
 
34
42
 
35
- cli.add_command(setupssh)
36
- 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)
37
48
  cli.add_command(cfssh)
38
49
  cli.add_command(f_claude)
39
50
  cli.add_command(gdrive)
@@ -41,6 +52,8 @@ cli.add_command(site2s)
41
52
  cli.add_command(localproxy)
42
53
  cli.add_command(netcheck)
43
54
  cli.add_command(plantuml)
55
+ cli.add_command(sysmon)
56
+ cli.add_command(wifi)
44
57
 
45
58
 
46
59
  def main():
@@ -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)