evo-cli 0.8.0__tar.gz → 0.8.1__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 (42) hide show
  1. {evo_cli-0.8.0 → evo_cli-0.8.1}/PKG-INFO +1 -1
  2. evo_cli-0.8.1/evo_cli/VERSION +1 -0
  3. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/opencode.py +19 -3
  4. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/console.py +23 -9
  5. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/PKG-INFO +1 -1
  6. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/SOURCES.txt +1 -0
  7. evo_cli-0.8.1/tests/test_console.py +32 -0
  8. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_opencode.py +20 -0
  9. evo_cli-0.8.0/evo_cli/VERSION +0 -1
  10. {evo_cli-0.8.0 → evo_cli-0.8.1}/Containerfile +0 -0
  11. {evo_cli-0.8.0 → evo_cli-0.8.1}/HISTORY.md +0 -0
  12. {evo_cli-0.8.0 → evo_cli-0.8.1}/LICENSE +0 -0
  13. {evo_cli-0.8.0 → evo_cli-0.8.1}/MANIFEST.in +0 -0
  14. {evo_cli-0.8.0 → evo_cli-0.8.1}/README.md +0 -0
  15. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/__init__.py +0 -0
  16. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/__main__.py +0 -0
  17. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/base.py +0 -0
  18. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/cli.py +0 -0
  19. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/__init__.py +0 -0
  20. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/cloudflare.py +0 -0
  21. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/fix_claude.py +0 -0
  22. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/gdrive.py +0 -0
  23. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/localproxy.py +0 -0
  24. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/miniconda.py +0 -0
  25. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/netcheck.py +0 -0
  26. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/plantuml.py +0 -0
  27. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/site2s.py +0 -0
  28. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/ssh.py +0 -0
  29. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/sysmon.py +0 -0
  30. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli/commands/wifi.py +0 -0
  31. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/dependency_links.txt +0 -0
  32. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/entry_points.txt +0 -0
  33. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/requires.txt +0 -0
  34. {evo_cli-0.8.0 → evo_cli-0.8.1}/evo_cli.egg-info/top_level.txt +0 -0
  35. {evo_cli-0.8.0 → evo_cli-0.8.1}/pyproject.toml +0 -0
  36. {evo_cli-0.8.0 → evo_cli-0.8.1}/setup.cfg +0 -0
  37. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/__init__.py +0 -0
  38. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_cli.py +0 -0
  39. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_fix_claude.py +0 -0
  40. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_plantuml.py +0 -0
  41. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_sysmon.py +0 -0
  42. {evo_cli-0.8.0 → evo_cli-0.8.1}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.8.0
3
+ Version: 0.8.1
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.1
@@ -112,15 +112,29 @@ def ensure_node_installed():
112
112
 
113
113
 
114
114
  def install_mcp_servers():
115
- """Install MCP server npm packages globally via npx."""
115
+ """Pre-fetch MCP server npm packages into the npx cache.
116
+
117
+ MCP servers speak JSON-RPC over stdio, so most do not implement a real
118
+ ``--version`` flag - they simply start up and block reading stdin. Run with
119
+ stdin detached (DEVNULL) so a server that ignores ``--version`` sees EOF and
120
+ exits right away instead of hanging on the terminal, and cap each call with a
121
+ timeout as a safety net. ``check=False`` keeps a non-zero exit from aborting
122
+ the whole setup; the verify step confirms the servers actually work.
123
+ """
116
124
  step("Installing MCP servers")
117
125
  servers = [
118
126
  "@mcp-server/google-search-mcp@latest",
119
127
  "@playwright/mcp@latest",
120
128
  ]
121
129
  for server in servers:
122
- info(f"Installing {server}")
123
- run_command(["npx", "-y", server, "--version"], status=f"Fetching {server}")
130
+ info(f"Fetching {server}")
131
+ run_command(
132
+ ["npx", "-y", server, "--version"],
133
+ status=f"Fetching {server}",
134
+ stdin=subprocess.DEVNULL,
135
+ timeout=180,
136
+ check=False,
137
+ )
124
138
  success("MCP server packages ready")
125
139
 
126
140
 
@@ -131,6 +145,8 @@ def install_playwright_browsers():
131
145
  run_command(
132
146
  ["npx", "playwright", "install", "chromium"],
133
147
  status="Downloading Chromium for Playwright",
148
+ stdin=subprocess.DEVNULL,
149
+ timeout=600,
134
150
  )
135
151
  success("Playwright Chromium installed")
136
152
  except Exception as exc:
@@ -52,17 +52,31 @@ def step(title):
52
52
  console.rule(f"[step]{title}[/step]", style="step", align="left")
53
53
 
54
54
 
55
- def run_command(cmd, capture=False, check=True, input_text=None, status=None):
55
+ def run_command(cmd, capture=False, check=True, input_text=None, status=None, timeout=None, stdin=None):
56
56
  cmd = [str(part) for part in cmd]
57
57
  console.print(f"[cmd]$ {' '.join(cmd)}[/cmd]")
58
- if status:
59
- with console.status(f"[info]{status}[/info]", spinner="dots"):
60
- result = subprocess.run(cmd, capture_output=True, text=True, input=input_text)
61
- output = (result.stdout or "").strip()
62
- if output:
63
- console.print(output)
64
- else:
65
- result = subprocess.run(cmd, capture_output=capture, text=True, input=input_text)
58
+
59
+ run_kwargs = {"text": True, "input": input_text, "timeout": timeout}
60
+ # subprocess.run rejects passing both `input` and `stdin`. Only attach an
61
+ # explicit stdin (e.g. DEVNULL) when we are not feeding input, so callers can
62
+ # detach a child from the terminal and avoid it blocking on an inherited TTY.
63
+ if input_text is None and stdin is not None:
64
+ run_kwargs["stdin"] = stdin
65
+
66
+ try:
67
+ if status:
68
+ with console.status(f"[info]{status}[/info]", spinner="dots"):
69
+ result = subprocess.run(cmd, capture_output=True, **run_kwargs)
70
+ output = (result.stdout or "").strip()
71
+ if output:
72
+ console.print(output)
73
+ else:
74
+ result = subprocess.run(cmd, capture_output=capture, **run_kwargs)
75
+ except subprocess.TimeoutExpired as exc:
76
+ console.print(f"[error]command timed out after {timeout}s: {' '.join(cmd)}[/error]")
77
+ if check:
78
+ raise CommandError(f"command timed out after {timeout}s: {' '.join(cmd)}") from exc
79
+ return exc
66
80
 
67
81
  if result.returncode != 0:
68
82
  message = (result.stderr or "").strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.8.0
3
+ Version: 0.8.1
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
@@ -31,6 +31,7 @@ evo_cli/commands/sysmon.py
31
31
  evo_cli/commands/wifi.py
32
32
  tests/__init__.py
33
33
  tests/test_cli.py
34
+ tests/test_console.py
34
35
  tests/test_fix_claude.py
35
36
  tests/test_opencode.py
36
37
  tests/test_plantuml.py
@@ -0,0 +1,32 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from evo_cli.console import CommandError, run_command
7
+
8
+
9
+ def test_run_command_raises_on_timeout():
10
+ # A command that outlives the timeout must not hang; it should raise.
11
+ with pytest.raises(CommandError, match="timed out"):
12
+ run_command([sys.executable, "-c", "import time; time.sleep(5)"], timeout=1)
13
+
14
+
15
+ def test_run_command_timeout_without_check_returns_exception():
16
+ result = run_command(
17
+ [sys.executable, "-c", "import time; time.sleep(5)"],
18
+ timeout=1,
19
+ check=False,
20
+ )
21
+ assert isinstance(result, subprocess.TimeoutExpired)
22
+
23
+
24
+ def test_run_command_detaches_stdin_when_requested():
25
+ # With stdin=DEVNULL a child that reads stdin sees EOF immediately instead
26
+ # of blocking on an inherited terminal.
27
+ result = run_command(
28
+ [sys.executable, "-c", "import sys; sys.exit(0 if sys.stdin.read() == '' else 1)"],
29
+ stdin=subprocess.DEVNULL,
30
+ timeout=10,
31
+ )
32
+ assert result.returncode == 0
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import subprocess
2
3
 
3
4
  import pytest
4
5
  from click.testing import CliRunner
@@ -8,6 +9,7 @@ from evo_cli.commands.opencode import (
8
9
  DEFAULT_MCP_SERVERS,
9
10
  configure_opencode_global,
10
11
  configure_opencode_project,
12
+ install_mcp_servers,
11
13
  load_jsonc,
12
14
  merge_mcp_config,
13
15
  save_jsonc,
@@ -73,6 +75,24 @@ def test_configure_opencode_project(tmp_path):
73
75
  assert set(data["mcp"].keys()) == {"google-search", "playwright"}
74
76
 
75
77
 
78
+ def test_install_mcp_servers_detaches_stdin_and_sets_timeout(monkeypatch):
79
+ """Regression: MCP servers read stdin, so the fetch must not block on a TTY."""
80
+ calls = []
81
+
82
+ def fake_run_command(cmd, **kwargs):
83
+ calls.append((cmd, kwargs))
84
+
85
+ monkeypatch.setattr("evo_cli.commands.opencode.run_command", fake_run_command)
86
+ install_mcp_servers()
87
+
88
+ assert calls, "expected at least one MCP server fetch"
89
+ for cmd, kwargs in calls:
90
+ assert kwargs.get("stdin") is subprocess.DEVNULL
91
+ assert kwargs.get("timeout")
92
+ # A server that ignores --version must not abort the whole setup.
93
+ assert kwargs.get("check") is False
94
+
95
+
76
96
  def test_setup_opencode_writes_global_and_project(runner, tmp_path, monkeypatch):
77
97
  monkeypatch.setattr(
78
98
  "evo_cli.commands.opencode.get_global_config_path",
@@ -1 +0,0 @@
1
- 0.8.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
File without changes