evo-cli 0.10.2__tar.gz → 0.11.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.10.2 → evo_cli-0.11.0}/PKG-INFO +1 -1
- evo_cli-0.11.0/evo_cli/VERSION +1 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/cli.py +5 -0
- evo_cli-0.11.0/evo_cli/commands/claude_code.py +309 -0
- evo_cli-0.11.0/evo_cli/commands/gh.py +246 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/opencode.py +62 -6
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/mcp_registry.py +5 -5
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/PKG-INFO +1 -1
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/SOURCES.txt +4 -0
- evo_cli-0.11.0/tests/test_claude_code.py +228 -0
- evo_cli-0.11.0/tests/test_gh.py +146 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_opencode.py +26 -3
- evo_cli-0.10.2/evo_cli/VERSION +0 -1
- {evo_cli-0.10.2 → evo_cli-0.11.0}/Containerfile +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/HISTORY.md +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/LICENSE +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/MANIFEST.in +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/README.md +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/base.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli/console.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/pyproject.toml +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/setup.cfg +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/__init__.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_cli.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_console.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.10.2 → evo_cli-0.11.0}/tests/test_wifi.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.11.0
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import rich_click as click
|
|
2
2
|
|
|
3
3
|
from evo_cli import __version__
|
|
4
|
+
from evo_cli.commands.claude_code import setup_claude
|
|
4
5
|
from evo_cli.commands.cloudflare import cfssh
|
|
5
6
|
from evo_cli.commands.fix_claude import f_claude
|
|
6
7
|
from evo_cli.commands.gdrive import gdrive
|
|
8
|
+
from evo_cli.commands.gh import setup_gh
|
|
7
9
|
from evo_cli.commands.localproxy import localproxy
|
|
8
10
|
from evo_cli.commands.mcp import mcp_group
|
|
9
11
|
from evo_cli.commands.miniconda import miniconda
|
|
@@ -20,6 +22,7 @@ from evo_cli.commands.wifi import wifi
|
|
|
20
22
|
def setup_group():
|
|
21
23
|
"""Commands for bootstrapping tools on a fresh machine."""
|
|
22
24
|
|
|
25
|
+
|
|
23
26
|
click.rich_click.USE_MARKDOWN = True
|
|
24
27
|
click.rich_click.SHOW_ARGUMENTS = True
|
|
25
28
|
click.rich_click.STYLE_OPTIONS_TABLE_BOX = "SIMPLE"
|
|
@@ -41,6 +44,8 @@ def cli():
|
|
|
41
44
|
"""
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
setup_group.add_command(setup_claude)
|
|
48
|
+
setup_group.add_command(setup_gh)
|
|
44
49
|
setup_group.add_command(setup_opencode)
|
|
45
50
|
setup_group.add_command(miniconda)
|
|
46
51
|
setup_group.add_command(setupssh)
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Install Claude Code and register its default MCP servers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
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.commands.fix_claude import is_affected, probe_version, version_str
|
|
14
|
+
from evo_cli.commands.gh import check_gh_auth, install_gh
|
|
15
|
+
from evo_cli.commands.mcp import add_to_claude
|
|
16
|
+
from evo_cli.console import CommandError, console, error, info, run_command, step, success, warning
|
|
17
|
+
from evo_cli.mcp_registry import MCP_REGISTRY
|
|
18
|
+
|
|
19
|
+
EPILOG = Text.from_markup(
|
|
20
|
+
"[bold]Examples[/bold]\n\n"
|
|
21
|
+
" [cyan]evo setup claude[/cyan] install Claude Code + gh + the default MCP servers\n"
|
|
22
|
+
" [cyan]evo setup claude --skip-install[/cyan] only register MCP servers\n"
|
|
23
|
+
" [cyan]evo setup claude --no-mcp --no-gh[/cyan] only install the CLI\n"
|
|
24
|
+
" [cyan]evo setup claude --mcp playwright --mcp github[/cyan]\n"
|
|
25
|
+
" [cyan]evo setup claude --method npm[/cyan] install via npm instead of the native installer\n"
|
|
26
|
+
" [cyan]evo setup claude --install-version 2.1.153[/cyan]\n"
|
|
27
|
+
" [cyan]evo setup claude --reinstall[/cyan] reinstall even if already present"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
INSTALL_SH = "https://claude.ai/install.sh"
|
|
31
|
+
INSTALL_PS1 = "https://claude.ai/install.ps1"
|
|
32
|
+
NPM_PACKAGE = "@anthropic-ai/claude-code"
|
|
33
|
+
|
|
34
|
+
# Claude Code already ships web search, file editing and shell tools, so the
|
|
35
|
+
# defaults only cover what it cannot do on its own: drive a real browser
|
|
36
|
+
# (playwright) and pull current library docs (context7). Add any other server
|
|
37
|
+
# from the library later with `evo mcp add <name>`.
|
|
38
|
+
DEFAULT_MCP_SERVERS = ("playwright", "context7")
|
|
39
|
+
|
|
40
|
+
# The native installer drops the binary in one of these; neither is on PATH until
|
|
41
|
+
# a new shell is started.
|
|
42
|
+
LOCAL_BIN_DIRS = (
|
|
43
|
+
Path.home() / ".local" / "bin",
|
|
44
|
+
Path.home() / ".claude" / "local",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_windows():
|
|
49
|
+
return platform.system() == "Windows"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ensure_claude_on_path():
|
|
53
|
+
"""Return the `claude` binary, making a fresh install visible to this process.
|
|
54
|
+
|
|
55
|
+
The native installer writes to ~/.local/bin, which the shell that launched
|
|
56
|
+
evo has not picked up yet, so `claude` stays invisible until the user opens a
|
|
57
|
+
new terminal. Prepend the install dir to PATH so the MCP and doctor steps can
|
|
58
|
+
still run in this same invocation.
|
|
59
|
+
"""
|
|
60
|
+
found = shutil.which("claude")
|
|
61
|
+
if found:
|
|
62
|
+
return found
|
|
63
|
+
|
|
64
|
+
binary = "claude.exe" if is_windows() else "claude"
|
|
65
|
+
for directory in LOCAL_BIN_DIRS:
|
|
66
|
+
candidate = directory / binary
|
|
67
|
+
if candidate.exists():
|
|
68
|
+
os.environ["PATH"] = f"{directory}{os.pathsep}{os.environ.get('PATH', '')}"
|
|
69
|
+
return str(candidate)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def native_install_command(version=None):
|
|
74
|
+
"""Build the official installer command for this platform."""
|
|
75
|
+
if is_windows():
|
|
76
|
+
return ["powershell", "-NoProfile", "-Command", f"irm {INSTALL_PS1} | iex"]
|
|
77
|
+
script = f"curl -fsSL {INSTALL_SH} | bash"
|
|
78
|
+
if version:
|
|
79
|
+
script += f" -s {version}"
|
|
80
|
+
return ["bash", "-c", script]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def npm_install_command(version=None):
|
|
84
|
+
package = f"{NPM_PACKAGE}@{version}" if version else NPM_PACKAGE
|
|
85
|
+
return ["npm", "install", "-g", package]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def native_install_available(version=None):
|
|
89
|
+
"""Whether the native installer can run here.
|
|
90
|
+
|
|
91
|
+
install.ps1 takes no version argument, so a pinned version on Windows has to
|
|
92
|
+
go through npm instead.
|
|
93
|
+
"""
|
|
94
|
+
if is_windows():
|
|
95
|
+
return bool(shutil.which("powershell")) and not version
|
|
96
|
+
return bool(shutil.which("curl") and shutil.which("bash"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def install_attempts(method, version):
|
|
100
|
+
"""Ordered (label, command) install attempts for the requested method."""
|
|
101
|
+
attempts = []
|
|
102
|
+
if method in ("auto", "native") and native_install_available(version):
|
|
103
|
+
attempts.append(("native installer", native_install_command(version)))
|
|
104
|
+
if method in ("auto", "npm") and shutil.which("npm"):
|
|
105
|
+
attempts.append(("npm", npm_install_command(version)))
|
|
106
|
+
return attempts
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def install_claude(method="auto", version=None, reinstall=False):
|
|
110
|
+
"""Install the Claude Code CLI. Returns True when `claude` ends up runnable."""
|
|
111
|
+
step("Installing Claude Code")
|
|
112
|
+
|
|
113
|
+
if ensure_claude_on_path() and not reinstall:
|
|
114
|
+
info(f"Claude Code already installed ({version_str(probe_version())}); skipping install")
|
|
115
|
+
info("Pass --reinstall to install it again, or run `claude update` to update.")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
attempts = install_attempts(method, version)
|
|
119
|
+
if not attempts:
|
|
120
|
+
error(f"No usable install method for --method {method} on {platform.system()}.")
|
|
121
|
+
if is_windows():
|
|
122
|
+
info(f"Install manually: [accent]irm {INSTALL_PS1} | iex[/accent] (PowerShell)")
|
|
123
|
+
else:
|
|
124
|
+
info(f"Install manually: [accent]curl -fsSL {INSTALL_SH} | bash[/accent]")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
for label, command in attempts:
|
|
128
|
+
info(f"Installing via {label}")
|
|
129
|
+
try:
|
|
130
|
+
run_command(command, status=f"Installing Claude Code via {label}", timeout=600)
|
|
131
|
+
except CommandError as exc:
|
|
132
|
+
warning(f"Install via {label} failed: {exc}")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if ensure_claude_on_path():
|
|
136
|
+
success(f"Claude Code installed ({version_str(probe_version())})")
|
|
137
|
+
return True
|
|
138
|
+
warning(f"Install via {label} finished but `claude` is not on PATH")
|
|
139
|
+
|
|
140
|
+
error("Could not install Claude Code.")
|
|
141
|
+
info("Open a new shell and retry, or add the install directory (~/.local/bin) to PATH.")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def configure_mcp_servers(names, scope):
|
|
146
|
+
"""Register the named library MCP servers with Claude Code."""
|
|
147
|
+
added = []
|
|
148
|
+
for name in names:
|
|
149
|
+
if add_to_claude(name, MCP_REGISTRY[name], scope):
|
|
150
|
+
added.append(name)
|
|
151
|
+
return added
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_doctor():
|
|
155
|
+
"""Run Claude Code's own diagnostic. Never fatal: it is a report, not a gate."""
|
|
156
|
+
step("Verifying installation")
|
|
157
|
+
result = run_command(
|
|
158
|
+
["claude", "doctor"],
|
|
159
|
+
status="Running claude doctor",
|
|
160
|
+
stdin=subprocess.DEVNULL,
|
|
161
|
+
timeout=120,
|
|
162
|
+
check=False,
|
|
163
|
+
)
|
|
164
|
+
if getattr(result, "returncode", None) == 0:
|
|
165
|
+
success("`claude doctor` reported no problems")
|
|
166
|
+
else:
|
|
167
|
+
warning("`claude doctor` reported problems; see its output above")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def setup_github_cli():
|
|
171
|
+
"""Install gh and report its auth state. Returns one of: ready, unauthenticated, missing.
|
|
172
|
+
|
|
173
|
+
Claude Code shells out to `gh` for PRs, issues and the GitHub API, so a fresh
|
|
174
|
+
machine wants it too. A failure here is not fatal: Claude Code still works,
|
|
175
|
+
it just cannot touch GitHub until gh is installed and signed in.
|
|
176
|
+
"""
|
|
177
|
+
if not install_gh():
|
|
178
|
+
return "missing"
|
|
179
|
+
return "ready" if check_gh_auth() else "unauthenticated"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def print_summary(installed, added, scope, gh_state=None):
|
|
183
|
+
console.print()
|
|
184
|
+
if not installed:
|
|
185
|
+
console.print(
|
|
186
|
+
Panel(
|
|
187
|
+
"Claude Code is not installed, so nothing was configured.\n\n"
|
|
188
|
+
f"Install it with [accent]curl -fsSL {INSTALL_SH} | bash[/accent] "
|
|
189
|
+
"(or [accent]evo setup claude --method npm[/accent]), then re-run this command.",
|
|
190
|
+
title="setup claude incomplete",
|
|
191
|
+
border_style="warning",
|
|
192
|
+
expand=False,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
version = probe_version()
|
|
198
|
+
lines = [f"Claude Code [accent]{version_str(version)}[/accent] is ready."]
|
|
199
|
+
if added:
|
|
200
|
+
lines.append(f"MCP servers registered ({scope} scope): [accent]{', '.join(added)}[/accent]")
|
|
201
|
+
lines.append("Run [accent]/mcp[/accent] inside Claude Code to authenticate any server that needs OAuth.")
|
|
202
|
+
lines.append("")
|
|
203
|
+
lines.append("Run [accent]claude[/accent] to start; the first run opens a browser to sign in.")
|
|
204
|
+
lines.append(
|
|
205
|
+
"Add more MCP servers anytime with [accent]evo mcp add <name>[/accent] ([accent]evo mcp list[/accent])."
|
|
206
|
+
)
|
|
207
|
+
if gh_state == "unauthenticated":
|
|
208
|
+
lines.append("Sign in to GitHub with [accent]gh auth login[/accent] so Claude Code can open PRs.")
|
|
209
|
+
elif gh_state == "missing":
|
|
210
|
+
lines.append("[warning]GitHub CLI is missing; retry with [accent]evo setup gh[/accent].[/warning]")
|
|
211
|
+
if is_affected(version):
|
|
212
|
+
lines.append("")
|
|
213
|
+
lines.append(
|
|
214
|
+
"[warning]This build has the tool-result bug; run [accent]evo f-claude[/accent] to fix it.[/warning]"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
console.print(
|
|
218
|
+
Panel(
|
|
219
|
+
Text.from_markup("\n".join(lines)),
|
|
220
|
+
title="setup claude complete",
|
|
221
|
+
border_style="success",
|
|
222
|
+
expand=False,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def run_setup_claude(skip_install, reinstall, method, install_version, mcp_names, no_mcp, scope, no_gh):
|
|
228
|
+
step("evo setup claude")
|
|
229
|
+
|
|
230
|
+
if skip_install:
|
|
231
|
+
info("Skipping install as requested")
|
|
232
|
+
installed = bool(ensure_claude_on_path())
|
|
233
|
+
if not installed:
|
|
234
|
+
error("`claude` was not found on PATH; nothing to configure.")
|
|
235
|
+
else:
|
|
236
|
+
installed = install_claude(method, install_version, reinstall)
|
|
237
|
+
|
|
238
|
+
gh_state = None
|
|
239
|
+
if no_gh:
|
|
240
|
+
info("Skipping the GitHub CLI as requested")
|
|
241
|
+
else:
|
|
242
|
+
gh_state = setup_github_cli()
|
|
243
|
+
|
|
244
|
+
added = []
|
|
245
|
+
if not installed:
|
|
246
|
+
pass
|
|
247
|
+
elif no_mcp:
|
|
248
|
+
info("Skipping MCP registration as requested")
|
|
249
|
+
else:
|
|
250
|
+
added = configure_mcp_servers(mcp_names or DEFAULT_MCP_SERVERS, scope)
|
|
251
|
+
|
|
252
|
+
if installed:
|
|
253
|
+
run_doctor()
|
|
254
|
+
|
|
255
|
+
print_summary(installed, added, scope, gh_state)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@click.command("claude", epilog=EPILOG)
|
|
259
|
+
@click.option("--skip-install", is_flag=True, help="Skip installing the CLI; only register MCP servers.")
|
|
260
|
+
@click.option("--reinstall", is_flag=True, help="Install even if Claude Code is already present.")
|
|
261
|
+
@click.option(
|
|
262
|
+
"--method",
|
|
263
|
+
type=click.Choice(["auto", "native", "npm"]),
|
|
264
|
+
default="auto",
|
|
265
|
+
show_default=True,
|
|
266
|
+
help="Install method. `auto` tries the native installer, then falls back to npm.",
|
|
267
|
+
)
|
|
268
|
+
@click.option(
|
|
269
|
+
"--install-version",
|
|
270
|
+
metavar="VERSION",
|
|
271
|
+
help="Install a specific version instead of the latest, e.g. 2.1.153.",
|
|
272
|
+
)
|
|
273
|
+
@click.option(
|
|
274
|
+
"--mcp",
|
|
275
|
+
"mcp_names",
|
|
276
|
+
multiple=True,
|
|
277
|
+
metavar="NAME",
|
|
278
|
+
help=f"MCP server from the library to register (repeatable). Default: {', '.join(DEFAULT_MCP_SERVERS)}.",
|
|
279
|
+
)
|
|
280
|
+
@click.option("--no-mcp", is_flag=True, help="Skip MCP server registration.")
|
|
281
|
+
@click.option("--no-gh", is_flag=True, help="Skip installing the GitHub CLI (gh).")
|
|
282
|
+
@click.option(
|
|
283
|
+
"--scope",
|
|
284
|
+
type=click.Choice(["local", "user", "project"]),
|
|
285
|
+
default="user",
|
|
286
|
+
show_default=True,
|
|
287
|
+
help="Claude Code config scope for the MCP servers.",
|
|
288
|
+
)
|
|
289
|
+
def setup_claude(skip_install, reinstall, method, install_version, mcp_names, no_mcp, no_gh, scope):
|
|
290
|
+
"""Install Claude Code, the GitHub CLI, and the default MCP servers.
|
|
291
|
+
|
|
292
|
+
Installs the CLI with Anthropic's native installer (falling back to npm),
|
|
293
|
+
installs the GitHub CLI that Claude Code shells out to for PRs and issues,
|
|
294
|
+
registers the MCP servers Claude Code lacks natively - playwright for browser
|
|
295
|
+
automation and context7 for up-to-date library docs - and runs `claude doctor`
|
|
296
|
+
to verify the result. Everything is idempotent: existing installs are left
|
|
297
|
+
alone unless --reinstall is passed, and servers already registered are skipped.
|
|
298
|
+
|
|
299
|
+
Pick your own servers with --mcp (repeatable, from `evo mcp list`), or skip
|
|
300
|
+
them with --no-mcp; skip the GitHub CLI with --no-gh (`evo setup gh` installs
|
|
301
|
+
it on its own). No credentials are written: `gh auth login` and OAuth MCP
|
|
302
|
+
servers (/mcp inside Claude Code) stay in your hands.
|
|
303
|
+
"""
|
|
304
|
+
unknown = [name for name in mcp_names if name not in MCP_REGISTRY]
|
|
305
|
+
if unknown:
|
|
306
|
+
error(f"Unknown MCP server(s): {', '.join(unknown)}. Run `evo mcp list` to see the library.")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
run_setup_claude(skip_install, reinstall, method, install_version, mcp_names, no_mcp, scope, no_gh)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Install the GitHub CLI (gh) and report whether it is authenticated."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import rich_click as click
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from evo_cli.console import CommandError, console, error, info, resolve_executable, run_command, step, success, warning
|
|
13
|
+
|
|
14
|
+
EPILOG = Text.from_markup(
|
|
15
|
+
"[bold]Examples[/bold]\n\n"
|
|
16
|
+
" [cyan]evo setup gh[/cyan] install gh, then check its auth status\n"
|
|
17
|
+
" [cyan]evo setup gh --reinstall[/cyan] install again even if gh is present\n"
|
|
18
|
+
" [cyan]evo setup gh --skip-auth-check[/cyan] install only; do not run `gh auth status`"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
RELEASES_URL = "https://github.com/cli/cli/releases/latest"
|
|
22
|
+
|
|
23
|
+
# gh is not in the default Debian/Ubuntu repositories, so the official
|
|
24
|
+
# instructions add GitHub's keyring and apt source first. Kept close to upstream:
|
|
25
|
+
# https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
|
26
|
+
APT_SCRIPT = (
|
|
27
|
+
"set -e\n"
|
|
28
|
+
"(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y))\n"
|
|
29
|
+
"sudo mkdir -p -m 755 /etc/apt/keyrings\n"
|
|
30
|
+
"out=$(mktemp)\n"
|
|
31
|
+
"wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg\n"
|
|
32
|
+
"cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null\n"
|
|
33
|
+
"sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg\n"
|
|
34
|
+
"sudo mkdir -p -m 755 /etc/apt/sources.list.d\n"
|
|
35
|
+
'echo "deb [arch=$(dpkg --print-architecture) '
|
|
36
|
+
"signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] "
|
|
37
|
+
'https://cli.github.com/packages stable main" '
|
|
38
|
+
"| sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null\n"
|
|
39
|
+
"sudo apt update\n"
|
|
40
|
+
"sudo apt install gh -y\n"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Fedora ships gh in its own repositories; only fall back to GitHub's rpm repo
|
|
44
|
+
# when the distro does not carry the package.
|
|
45
|
+
DNF_SCRIPT = (
|
|
46
|
+
"sudo dnf install -y gh || {\n"
|
|
47
|
+
" sudo dnf install -y dnf5-plugins\n"
|
|
48
|
+
" sudo dnf config-manager addrepo --from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo\n"
|
|
49
|
+
" sudo dnf install -y gh\n"
|
|
50
|
+
"}\n"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
ZYPPER_SCRIPT = (
|
|
54
|
+
"set -e\n"
|
|
55
|
+
"sudo zypper addrepo https://cli.github.com/packages/rpm/gh-cli.repo\n"
|
|
56
|
+
"sudo zypper ref\n"
|
|
57
|
+
"sudo zypper install -y gh\n"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def gh_version():
|
|
62
|
+
"""Return the installed gh version string, or None if gh is not on PATH."""
|
|
63
|
+
if not shutil.which("gh"):
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
resolve_executable(["gh", "--version"]),
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
errors="replace",
|
|
72
|
+
timeout=30,
|
|
73
|
+
)
|
|
74
|
+
except (OSError, subprocess.SubprocessError):
|
|
75
|
+
return "unknown"
|
|
76
|
+
match = re.search(r"\d+\.\d+\.\d+", result.stdout or "")
|
|
77
|
+
return match.group(0) if match else "unknown"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def linux_install_command():
|
|
81
|
+
if shutil.which("apt-get"):
|
|
82
|
+
return ["bash", "-c", APT_SCRIPT]
|
|
83
|
+
if shutil.which("dnf"):
|
|
84
|
+
return ["bash", "-c", DNF_SCRIPT]
|
|
85
|
+
if shutil.which("pacman"):
|
|
86
|
+
return ["sudo", "pacman", "-S", "--noconfirm", "github-cli"]
|
|
87
|
+
if shutil.which("apk"):
|
|
88
|
+
return ["sudo", "apk", "add", "github-cli"]
|
|
89
|
+
if shutil.which("zypper"):
|
|
90
|
+
return ["bash", "-c", ZYPPER_SCRIPT]
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def windows_install_command():
|
|
95
|
+
if shutil.which("winget"):
|
|
96
|
+
# The accept flags keep winget from stopping on an interactive agreement prompt.
|
|
97
|
+
return [
|
|
98
|
+
"winget",
|
|
99
|
+
"install",
|
|
100
|
+
"--id",
|
|
101
|
+
"GitHub.cli",
|
|
102
|
+
"--source",
|
|
103
|
+
"winget",
|
|
104
|
+
"--accept-package-agreements",
|
|
105
|
+
"--accept-source-agreements",
|
|
106
|
+
]
|
|
107
|
+
if shutil.which("scoop"):
|
|
108
|
+
return ["scoop", "install", "gh"]
|
|
109
|
+
if shutil.which("choco"):
|
|
110
|
+
return ["choco", "install", "gh", "-y"]
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def install_command():
|
|
115
|
+
"""The gh install command for this platform, or None if none is usable."""
|
|
116
|
+
system = platform.system()
|
|
117
|
+
if system == "Darwin":
|
|
118
|
+
return ["brew", "install", "gh"] if shutil.which("brew") else None
|
|
119
|
+
if system == "Linux":
|
|
120
|
+
return linux_install_command()
|
|
121
|
+
if system == "Windows":
|
|
122
|
+
return windows_install_command()
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def missing_package_manager_hint():
|
|
127
|
+
system = platform.system()
|
|
128
|
+
if system == "Darwin":
|
|
129
|
+
info("Homebrew not found. Install it from https://brew.sh, then re-run.")
|
|
130
|
+
elif system == "Linux":
|
|
131
|
+
info("No supported package manager found (apt-get, dnf, pacman, apk, zypper).")
|
|
132
|
+
elif system == "Windows":
|
|
133
|
+
info("No supported package manager found (winget, scoop, choco).")
|
|
134
|
+
else:
|
|
135
|
+
info(f"Unsupported platform: {system}")
|
|
136
|
+
info(f"You can also grab a binary from [accent]{RELEASES_URL}[/accent]")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def install_gh(reinstall=False):
|
|
140
|
+
"""Install the GitHub CLI. Returns True when `gh` ends up runnable."""
|
|
141
|
+
step("Installing GitHub CLI")
|
|
142
|
+
|
|
143
|
+
version = gh_version()
|
|
144
|
+
if version and not reinstall:
|
|
145
|
+
info(f"gh already installed ({version}); skipping install")
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
command = install_command()
|
|
149
|
+
if command is None:
|
|
150
|
+
error("Could not install gh automatically.")
|
|
151
|
+
missing_package_manager_hint()
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
run_command(command, status="Installing gh", stdin=subprocess.DEVNULL, timeout=900)
|
|
156
|
+
except CommandError as exc:
|
|
157
|
+
error(f"Installing gh failed: {exc}")
|
|
158
|
+
info(f"Install it manually from [accent]{RELEASES_URL}[/accent]")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
version = gh_version()
|
|
162
|
+
if not version:
|
|
163
|
+
warning("gh installed but it is not on PATH yet")
|
|
164
|
+
info("Open a new terminal, then run `gh --version` to confirm.")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
success(f"gh installed ({version})")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_gh_auth():
|
|
172
|
+
"""Report whether gh holds GitHub credentials.
|
|
173
|
+
|
|
174
|
+
`gh auth status` is read-only and exits non-zero when logged out, so it is
|
|
175
|
+
safe here. `gh auth login` is not: it needs a TTY and a browser, so leave it
|
|
176
|
+
to the user rather than blocking the run on it.
|
|
177
|
+
"""
|
|
178
|
+
step("Checking GitHub authentication")
|
|
179
|
+
result = run_command(
|
|
180
|
+
["gh", "auth", "status"],
|
|
181
|
+
status="Running gh auth status",
|
|
182
|
+
stdin=subprocess.DEVNULL,
|
|
183
|
+
timeout=60,
|
|
184
|
+
check=False,
|
|
185
|
+
)
|
|
186
|
+
if getattr(result, "returncode", None) == 0:
|
|
187
|
+
success("gh is authenticated")
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
warning("gh is not authenticated")
|
|
191
|
+
info("Run [accent]gh auth login[/accent] in your terminal; it opens a browser to sign in.")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def print_summary(installed, authenticated, auth_checked):
|
|
196
|
+
console.print()
|
|
197
|
+
if not installed:
|
|
198
|
+
console.print(
|
|
199
|
+
Panel(
|
|
200
|
+
f"gh was not installed. Install it manually from [accent]{RELEASES_URL}[/accent], then re-run.",
|
|
201
|
+
title="setup gh incomplete",
|
|
202
|
+
border_style="warning",
|
|
203
|
+
expand=False,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
lines = [f"GitHub CLI [accent]{gh_version()}[/accent] is ready."]
|
|
209
|
+
if not auth_checked:
|
|
210
|
+
lines.append("Auth check skipped; run [accent]gh auth status[/accent] to see if you are signed in.")
|
|
211
|
+
elif authenticated:
|
|
212
|
+
lines.append("You are signed in; `gh pr`, `gh issue` and Claude Code's GitHub workflows will work.")
|
|
213
|
+
else:
|
|
214
|
+
lines.append("Sign in with [accent]gh auth login[/accent] to finish setting it up.")
|
|
215
|
+
|
|
216
|
+
console.print(
|
|
217
|
+
Panel(
|
|
218
|
+
"\n".join(lines),
|
|
219
|
+
title="setup gh complete",
|
|
220
|
+
border_style="success",
|
|
221
|
+
expand=False,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@click.command("gh", epilog=EPILOG)
|
|
227
|
+
@click.option("--reinstall", is_flag=True, help="Install even if gh is already present.")
|
|
228
|
+
@click.option("--skip-auth-check", is_flag=True, help="Skip the `gh auth status` check afterwards.")
|
|
229
|
+
def setup_gh(reinstall, skip_auth_check):
|
|
230
|
+
"""Install the GitHub CLI (gh) and check whether it is signed in.
|
|
231
|
+
|
|
232
|
+
Uses the package manager for your platform - Homebrew on macOS, apt/dnf/
|
|
233
|
+
pacman/apk/zypper on Linux, winget/scoop/choco on Windows - following
|
|
234
|
+
GitHub's official instructions (on Debian/Ubuntu that means adding GitHub's
|
|
235
|
+
keyring and apt source, since gh is not in the default repos).
|
|
236
|
+
|
|
237
|
+
An existing install is left alone unless --reinstall is passed. Signing in is
|
|
238
|
+
left to you: `gh auth login` needs an interactive terminal and a browser, so
|
|
239
|
+
this command only reports whether credentials are already present.
|
|
240
|
+
"""
|
|
241
|
+
step("evo setup gh")
|
|
242
|
+
installed = install_gh(reinstall)
|
|
243
|
+
authenticated = False
|
|
244
|
+
if installed and not skip_auth_check:
|
|
245
|
+
authenticated = check_gh_auth()
|
|
246
|
+
print_summary(installed, authenticated, auth_checked=not skip_auth_check)
|