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.
- {evo_cli-0.5.1 → evo_cli-0.8.0}/PKG-INFO +1 -1
- evo_cli-0.8.0/evo_cli/VERSION +1 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/cli.py +15 -2
- evo_cli-0.8.0/evo_cli/commands/opencode.py +323 -0
- evo_cli-0.8.0/evo_cli/commands/sysmon.py +545 -0
- evo_cli-0.8.0/evo_cli/commands/wifi.py +837 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/SOURCES.txt +7 -1
- {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_cli.py +12 -1
- evo_cli-0.8.0/tests/test_opencode.py +117 -0
- evo_cli-0.8.0/tests/test_sysmon.py +91 -0
- evo_cli-0.8.0/tests/test_wifi.py +154 -0
- evo_cli-0.5.1/evo_cli/VERSION +0 -1
- {evo_cli-0.5.1 → evo_cli-0.8.0}/Containerfile +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/HISTORY.md +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/LICENSE +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/MANIFEST.in +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/README.md +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/base.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli/console.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/pyproject.toml +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/setup.cfg +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/__init__.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.5.1 → evo_cli-0.8.0}/tests/test_plantuml.py +0 -0
|
@@ -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
|
-
|
|
36
|
-
|
|
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)
|