evo-cli 0.11.3__tar.gz → 0.11.4__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.11.3 → evo_cli-0.11.4}/PKG-INFO +23 -1
- {evo_cli-0.11.3 → evo_cli-0.11.4}/README.md +21 -0
- evo_cli-0.11.4/evo_cli/VERSION +1 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/cli.py +2 -0
- evo_cli-0.11.4/evo_cli/commands/harness.py +213 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/PKG-INFO +23 -1
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/SOURCES.txt +2 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/requires.txt +1 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/pyproject.toml +1 -0
- evo_cli-0.11.4/tests/test_harness.py +136 -0
- evo_cli-0.11.3/evo_cli/VERSION +0 -1
- {evo_cli-0.11.3 → evo_cli-0.11.4}/Containerfile +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/HISTORY.md +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/LICENSE +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/MANIFEST.in +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/__init__.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/__main__.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/base.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/console.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/setup.cfg +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/__init__.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_claude_code.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_cli.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_console.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_download.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_gh.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_mcp.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_opencode.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_plantuml.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_sysmon.py +0 -0
- {evo_cli-0.11.3 → evo_cli-0.11.4}/tests/test_wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.4
|
|
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
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: cryptography>=41.0
|
|
18
18
|
Requires-Dist: paramiko>=2.7.0
|
|
19
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
20
|
Requires-Dist: rich>=13.0
|
|
20
21
|
Requires-Dist: rich-click>=1.8
|
|
21
22
|
Provides-Extra: test
|
|
@@ -112,3 +113,24 @@ Options:
|
|
|
112
113
|
- `-y, --yes` - Skip the confirmation prompt
|
|
113
114
|
- `-f, --force` - Apply the fix even if the version is not in the affected range
|
|
114
115
|
- `--unpin` - Undo the fix: re-enable the auto-updater and install the latest build
|
|
116
|
+
|
|
117
|
+
#### Harness Repositories
|
|
118
|
+
|
|
119
|
+
Fast-forward every available repository declared in a harness manifest:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
evo harness pull
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Run the command from a harness directory or any registered member repository. Use `--harness PATH`
|
|
126
|
+
when a repository belongs to multiple harnesses or the harness is not registered.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
evo harness pull --harness ~/github/my-project-harness
|
|
130
|
+
evo harness pull --repo backend --repo frontend
|
|
131
|
+
evo harness pull --dry-run
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The command reads `harness.yaml` and its optional `harness.local.yaml` overlay. Repositories marked
|
|
135
|
+
`present: false` are skipped. Repositories with uncommitted changes are not modified, and every pull
|
|
136
|
+
uses `git pull --ff-only` so the command never creates merge commits.
|
|
@@ -85,3 +85,24 @@ Options:
|
|
|
85
85
|
- `-y, --yes` - Skip the confirmation prompt
|
|
86
86
|
- `-f, --force` - Apply the fix even if the version is not in the affected range
|
|
87
87
|
- `--unpin` - Undo the fix: re-enable the auto-updater and install the latest build
|
|
88
|
+
|
|
89
|
+
#### Harness Repositories
|
|
90
|
+
|
|
91
|
+
Fast-forward every available repository declared in a harness manifest:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
evo harness pull
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run the command from a harness directory or any registered member repository. Use `--harness PATH`
|
|
98
|
+
when a repository belongs to multiple harnesses or the harness is not registered.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
evo harness pull --harness ~/github/my-project-harness
|
|
102
|
+
evo harness pull --repo backend --repo frontend
|
|
103
|
+
evo harness pull --dry-run
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The command reads `harness.yaml` and its optional `harness.local.yaml` overlay. Repositories marked
|
|
107
|
+
`present: false` are skipped. Repositories with uncommitted changes are not modified, and every pull
|
|
108
|
+
uses `git pull --ff-only` so the command never creates merge commits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.11.4
|
|
@@ -7,6 +7,7 @@ from evo_cli.commands.download import download
|
|
|
7
7
|
from evo_cli.commands.fix_claude import f_claude
|
|
8
8
|
from evo_cli.commands.gdrive import gdrive
|
|
9
9
|
from evo_cli.commands.gh import setup_gh
|
|
10
|
+
from evo_cli.commands.harness import harness_group
|
|
10
11
|
from evo_cli.commands.hwid import hwid
|
|
11
12
|
from evo_cli.commands.hwid_reset import hwid_reset
|
|
12
13
|
from evo_cli.commands.localproxy import localproxy
|
|
@@ -58,6 +59,7 @@ cli.add_command(cfssh)
|
|
|
58
59
|
cli.add_command(download)
|
|
59
60
|
cli.add_command(f_claude)
|
|
60
61
|
cli.add_command(gdrive)
|
|
62
|
+
cli.add_command(harness_group)
|
|
61
63
|
cli.add_command(hwid)
|
|
62
64
|
cli.add_command(hwid_reset)
|
|
63
65
|
cli.add_command(site2s)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import rich_click as click
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_yaml(path, required=True):
|
|
12
|
+
if not path.is_file():
|
|
13
|
+
if required:
|
|
14
|
+
raise click.ClickException(f"Harness manifest not found: {path}")
|
|
15
|
+
return {}
|
|
16
|
+
try:
|
|
17
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
18
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
19
|
+
raise click.ClickException(f"Cannot read {path}: {exc}") from exc
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
raise click.ClickException(f"Expected a YAML mapping in {path}")
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_path(value, base):
|
|
26
|
+
path = Path(os.path.expandvars(str(value))).expanduser()
|
|
27
|
+
return (path if path.is_absolute() else base / path).resolve()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_repos(manifest_path):
|
|
31
|
+
manifest = _read_yaml(manifest_path)
|
|
32
|
+
local = _read_yaml(manifest_path.with_name("harness.local.yaml"), required=False)
|
|
33
|
+
root = manifest_path.parent.resolve()
|
|
34
|
+
workspace_value = (
|
|
35
|
+
os.environ.get("EVO_HARNESS_WORKSPACE") or local.get("workspace") or manifest.get("workspace") or root.parent
|
|
36
|
+
)
|
|
37
|
+
workspace = _resolve_path(workspace_value, root)
|
|
38
|
+
entries = manifest.get("repos", [])
|
|
39
|
+
if not isinstance(entries, list):
|
|
40
|
+
raise click.ClickException(f"Expected 'repos' to be a list in {manifest_path}")
|
|
41
|
+
present_overrides = local.get("present", {}) or {}
|
|
42
|
+
if not isinstance(present_overrides, dict):
|
|
43
|
+
raise click.ClickException(
|
|
44
|
+
f"Expected 'present' to be a mapping in {manifest_path.with_name('harness.local.yaml')}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
repos = []
|
|
48
|
+
names = set()
|
|
49
|
+
for index, entry in enumerate(entries):
|
|
50
|
+
if not isinstance(entry, dict):
|
|
51
|
+
raise click.ClickException(f"Expected repos[{index}] to be a mapping in {manifest_path}")
|
|
52
|
+
name = str(entry.get("name", "")).strip()
|
|
53
|
+
if not name:
|
|
54
|
+
raise click.ClickException(f"Expected repos[{index}].name in {manifest_path}")
|
|
55
|
+
if name in names:
|
|
56
|
+
raise click.ClickException(f"Duplicate repo name '{name}' in {manifest_path}")
|
|
57
|
+
names.add(name)
|
|
58
|
+
fallback = workspace / name
|
|
59
|
+
declared = _resolve_path(entry["path"], workspace) if entry.get("path") else None
|
|
60
|
+
path = declared if declared and declared.exists() else fallback.resolve()
|
|
61
|
+
present = present_overrides.get(name, entry.get("present", True))
|
|
62
|
+
repos.append({"name": name, "path": path, "present": bool(present)})
|
|
63
|
+
return manifest, repos
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _contains(parent, child):
|
|
67
|
+
try:
|
|
68
|
+
child.relative_to(parent)
|
|
69
|
+
return True
|
|
70
|
+
except ValueError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _as_manifest(value):
|
|
75
|
+
path = _resolve_path(value, Path.cwd())
|
|
76
|
+
return path / "harness.yaml" if path.is_dir() else path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_manifest(value=None):
|
|
80
|
+
configured = value or os.environ.get("EVO_HARNESS")
|
|
81
|
+
if configured:
|
|
82
|
+
manifest_path = _as_manifest(configured)
|
|
83
|
+
if not manifest_path.is_file():
|
|
84
|
+
raise click.ClickException(f"Harness manifest not found: {manifest_path}")
|
|
85
|
+
return manifest_path
|
|
86
|
+
|
|
87
|
+
cwd = Path.cwd().resolve()
|
|
88
|
+
for base in (cwd, *cwd.parents):
|
|
89
|
+
candidate = base / "harness.yaml"
|
|
90
|
+
if candidate.is_file():
|
|
91
|
+
return candidate
|
|
92
|
+
|
|
93
|
+
registry_path = _resolve_path(
|
|
94
|
+
os.environ.get("EVO_HARNESS_REGISTRY", Path.home() / ".claude" / "harness" / "registry.json"),
|
|
95
|
+
Path.cwd(),
|
|
96
|
+
)
|
|
97
|
+
matches = []
|
|
98
|
+
if registry_path.is_file():
|
|
99
|
+
try:
|
|
100
|
+
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
101
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
102
|
+
raise click.ClickException(f"Cannot read {registry_path}: {exc}") from exc
|
|
103
|
+
for cluster in registry.get("clusters", []):
|
|
104
|
+
if not isinstance(cluster, dict) or not cluster.get("root"):
|
|
105
|
+
continue
|
|
106
|
+
candidate = _as_manifest(cluster["root"])
|
|
107
|
+
if not candidate.is_file():
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
_, repos = _load_repos(candidate)
|
|
111
|
+
except click.ClickException:
|
|
112
|
+
continue
|
|
113
|
+
root = candidate.parent.resolve()
|
|
114
|
+
if _contains(root, cwd) or any(_contains(repo["path"], cwd) for repo in repos):
|
|
115
|
+
matches.append(candidate)
|
|
116
|
+
|
|
117
|
+
matches = list(dict.fromkeys(matches))
|
|
118
|
+
if len(matches) == 1:
|
|
119
|
+
return matches[0]
|
|
120
|
+
if len(matches) > 1:
|
|
121
|
+
choices = ", ".join(str(path.parent) for path in matches)
|
|
122
|
+
raise click.ClickException(f"Current directory belongs to multiple harnesses: {choices}. Use --harness PATH.")
|
|
123
|
+
raise click.ClickException("Cannot find harness.yaml. Run inside a harness repo or use --harness PATH.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _git(path, *args):
|
|
127
|
+
return subprocess.run(
|
|
128
|
+
["git", "-C", str(path), *args],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
errors="replace",
|
|
133
|
+
check=False,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@click.group("harness", help="Manage repositories declared by a harness manifest.")
|
|
138
|
+
def harness_group():
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@harness_group.command("pull", help="Fast-forward all available repositories in a harness.")
|
|
143
|
+
@click.option(
|
|
144
|
+
"--harness",
|
|
145
|
+
"harness_path",
|
|
146
|
+
type=click.Path(path_type=Path),
|
|
147
|
+
help="Harness directory or harness.yaml path. Defaults to auto-discovery.",
|
|
148
|
+
)
|
|
149
|
+
@click.option("--repo", "repos", multiple=True, help="Pull only this repo name. Repeat for multiple repos.")
|
|
150
|
+
@click.option("--prune/--no-prune", default=True, show_default=True, help="Prune deleted remote refs while pulling.")
|
|
151
|
+
@click.option("--dry-run", is_flag=True, help="Show repositories that would be pulled.")
|
|
152
|
+
def pull(harness_path, repos, prune, dry_run):
|
|
153
|
+
if shutil.which("git") is None:
|
|
154
|
+
raise click.ClickException("git is not installed or is not on PATH")
|
|
155
|
+
|
|
156
|
+
manifest_path = _find_manifest(harness_path)
|
|
157
|
+
manifest, entries = _load_repos(manifest_path)
|
|
158
|
+
available = {entry["name"] for entry in entries}
|
|
159
|
+
unknown = sorted(set(repos) - available)
|
|
160
|
+
if unknown:
|
|
161
|
+
raise click.ClickException(f"Unknown repo name(s): {', '.join(unknown)}")
|
|
162
|
+
selected = [entry for entry in entries if not repos or entry["name"] in repos]
|
|
163
|
+
|
|
164
|
+
click.echo(f"Harness: {manifest.get('name') or manifest_path.parent.name}")
|
|
165
|
+
click.echo(f"Manifest: {manifest_path}")
|
|
166
|
+
results = []
|
|
167
|
+
incomplete = 0
|
|
168
|
+
|
|
169
|
+
for entry in selected:
|
|
170
|
+
name = entry["name"]
|
|
171
|
+
path = entry["path"]
|
|
172
|
+
if not entry["present"]:
|
|
173
|
+
results.append((name, "skipped (present: false)"))
|
|
174
|
+
continue
|
|
175
|
+
if not path.is_dir():
|
|
176
|
+
results.append((name, f"failed (missing: {path})"))
|
|
177
|
+
incomplete += 1
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
status = _git(path, "status", "--porcelain")
|
|
181
|
+
if status.returncode != 0:
|
|
182
|
+
detail = (status.stderr or status.stdout).strip().splitlines()
|
|
183
|
+
results.append((name, f"failed ({detail[-1] if detail else 'not a git repository'})"))
|
|
184
|
+
incomplete += 1
|
|
185
|
+
continue
|
|
186
|
+
if status.stdout.strip():
|
|
187
|
+
results.append((name, "skipped (uncommitted changes)"))
|
|
188
|
+
incomplete += 1
|
|
189
|
+
continue
|
|
190
|
+
if dry_run:
|
|
191
|
+
results.append((name, "would pull"))
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
args = ["pull", "--ff-only"]
|
|
195
|
+
if prune:
|
|
196
|
+
args.append("--prune")
|
|
197
|
+
update = _git(path, *args)
|
|
198
|
+
if update.returncode != 0:
|
|
199
|
+
detail = (update.stderr or update.stdout).strip().splitlines()
|
|
200
|
+
results.append((name, f"failed ({detail[-1] if detail else 'git pull failed'})"))
|
|
201
|
+
incomplete += 1
|
|
202
|
+
continue
|
|
203
|
+
head = _git(path, "log", "-1", "--oneline")
|
|
204
|
+
results.append((name, head.stdout.strip() or "updated"))
|
|
205
|
+
|
|
206
|
+
click.echo()
|
|
207
|
+
click.secho("Summary", bold=True)
|
|
208
|
+
width = max((len(name) for name, _ in results), default=0)
|
|
209
|
+
for name, status in results:
|
|
210
|
+
color = "red" if status.startswith("failed") else "yellow" if status.startswith("skipped") else "green"
|
|
211
|
+
click.secho(f" {name:<{width}} {status}", fg=color)
|
|
212
|
+
if incomplete:
|
|
213
|
+
raise click.exceptions.Exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.4
|
|
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
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: cryptography>=41.0
|
|
18
18
|
Requires-Dist: paramiko>=2.7.0
|
|
19
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
20
|
Requires-Dist: rich>=13.0
|
|
20
21
|
Requires-Dist: rich-click>=1.8
|
|
21
22
|
Provides-Extra: test
|
|
@@ -112,3 +113,24 @@ Options:
|
|
|
112
113
|
- `-y, --yes` - Skip the confirmation prompt
|
|
113
114
|
- `-f, --force` - Apply the fix even if the version is not in the affected range
|
|
114
115
|
- `--unpin` - Undo the fix: re-enable the auto-updater and install the latest build
|
|
116
|
+
|
|
117
|
+
#### Harness Repositories
|
|
118
|
+
|
|
119
|
+
Fast-forward every available repository declared in a harness manifest:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
evo harness pull
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Run the command from a harness directory or any registered member repository. Use `--harness PATH`
|
|
126
|
+
when a repository belongs to multiple harnesses or the harness is not registered.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
evo harness pull --harness ~/github/my-project-harness
|
|
130
|
+
evo harness pull --repo backend --repo frontend
|
|
131
|
+
evo harness pull --dry-run
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The command reads `harness.yaml` and its optional `harness.local.yaml` overlay. Repositories marked
|
|
135
|
+
`present: false` are skipped. Repositories with uncommitted changes are not modified, and every pull
|
|
136
|
+
uses `git pull --ff-only` so the command never creates merge commits.
|
|
@@ -24,6 +24,7 @@ evo_cli/commands/download.py
|
|
|
24
24
|
evo_cli/commands/fix_claude.py
|
|
25
25
|
evo_cli/commands/gdrive.py
|
|
26
26
|
evo_cli/commands/gh.py
|
|
27
|
+
evo_cli/commands/harness.py
|
|
27
28
|
evo_cli/commands/hwid.py
|
|
28
29
|
evo_cli/commands/hwid_reset.py
|
|
29
30
|
evo_cli/commands/localproxy.py
|
|
@@ -43,6 +44,7 @@ tests/test_console.py
|
|
|
43
44
|
tests/test_download.py
|
|
44
45
|
tests/test_fix_claude.py
|
|
45
46
|
tests/test_gh.py
|
|
47
|
+
tests/test_harness.py
|
|
46
48
|
tests/test_mcp.py
|
|
47
49
|
tests/test_opencode.py
|
|
48
50
|
tests/test_plantuml.py
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from evo_cli.cli import cli
|
|
7
|
+
from evo_cli.commands import harness as harness_command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _result(returncode=0, stdout="", stderr=""):
|
|
11
|
+
return subprocess.CompletedProcess([], returncode, stdout, stderr)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _write_harness(root, workspace):
|
|
15
|
+
root.mkdir()
|
|
16
|
+
(root / "harness.yaml").write_text(
|
|
17
|
+
f"name: test-cluster\nworkspace: {workspace.as_posix()}\nrepos:\n"
|
|
18
|
+
"- name: alpha\n present: true\n"
|
|
19
|
+
"- name: beta\n present: true\n",
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_harness_command_is_registered():
|
|
25
|
+
assert "harness" in cli.commands
|
|
26
|
+
assert "pull" in cli.commands["harness"].commands
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pull_dry_run_honors_local_present_override(tmp_path, monkeypatch):
|
|
30
|
+
workspace = tmp_path / "repos"
|
|
31
|
+
workspace.mkdir()
|
|
32
|
+
(workspace / "alpha").mkdir()
|
|
33
|
+
(workspace / "beta").mkdir()
|
|
34
|
+
root = tmp_path / "cluster"
|
|
35
|
+
_write_harness(root, workspace)
|
|
36
|
+
(root / "harness.local.yaml").write_text("present:\n beta: false\n", encoding="utf-8")
|
|
37
|
+
calls = []
|
|
38
|
+
|
|
39
|
+
def fake_git(path, *args):
|
|
40
|
+
calls.append((path, args))
|
|
41
|
+
return _result()
|
|
42
|
+
|
|
43
|
+
monkeypatch.setattr(harness_command, "_git", fake_git)
|
|
44
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--harness", str(root), "--dry-run"])
|
|
45
|
+
|
|
46
|
+
assert result.exit_code == 0
|
|
47
|
+
assert "alpha would pull" in result.output
|
|
48
|
+
assert "beta skipped (present: false)" in result.output
|
|
49
|
+
assert calls == [(workspace / "alpha", ("status", "--porcelain"))]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_pull_refuses_dirty_repo(tmp_path, monkeypatch):
|
|
53
|
+
workspace = tmp_path / "repos"
|
|
54
|
+
workspace.mkdir()
|
|
55
|
+
(workspace / "alpha").mkdir()
|
|
56
|
+
root = tmp_path / "cluster"
|
|
57
|
+
_write_harness(root, workspace)
|
|
58
|
+
(root / "harness.local.yaml").write_text("present:\n beta: false\n", encoding="utf-8")
|
|
59
|
+
monkeypatch.setattr(harness_command, "_git", lambda path, *args: _result(stdout=" M local.txt\n"))
|
|
60
|
+
|
|
61
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--harness", str(root)])
|
|
62
|
+
|
|
63
|
+
assert result.exit_code == 1
|
|
64
|
+
assert "alpha skipped (uncommitted changes)" in result.output
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_pull_uses_fast_forward_and_prune(tmp_path, monkeypatch):
|
|
68
|
+
workspace = tmp_path / "repos"
|
|
69
|
+
workspace.mkdir()
|
|
70
|
+
(workspace / "alpha").mkdir()
|
|
71
|
+
root = tmp_path / "cluster"
|
|
72
|
+
_write_harness(root, workspace)
|
|
73
|
+
calls = []
|
|
74
|
+
|
|
75
|
+
def fake_git(path, *args):
|
|
76
|
+
calls.append((path, args))
|
|
77
|
+
if args[0] == "log":
|
|
78
|
+
return _result(stdout="abc123 Latest change\n")
|
|
79
|
+
return _result()
|
|
80
|
+
|
|
81
|
+
monkeypatch.setattr(harness_command, "_git", fake_git)
|
|
82
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--harness", str(root), "--repo", "alpha"])
|
|
83
|
+
|
|
84
|
+
assert result.exit_code == 0
|
|
85
|
+
assert "abc123 Latest change" in result.output
|
|
86
|
+
assert calls == [
|
|
87
|
+
(workspace / "alpha", ("status", "--porcelain")),
|
|
88
|
+
(workspace / "alpha", ("pull", "--ff-only", "--prune")),
|
|
89
|
+
(workspace / "alpha", ("log", "-1", "--oneline")),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_pull_discovers_manifest_from_environment(tmp_path, monkeypatch):
|
|
94
|
+
workspace = tmp_path / "repos"
|
|
95
|
+
workspace.mkdir()
|
|
96
|
+
(workspace / "alpha").mkdir()
|
|
97
|
+
root = tmp_path / "cluster"
|
|
98
|
+
_write_harness(root, workspace)
|
|
99
|
+
monkeypatch.setenv("EVO_HARNESS", str(root))
|
|
100
|
+
monkeypatch.setattr(harness_command, "_git", lambda path, *args: _result())
|
|
101
|
+
|
|
102
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--repo", "alpha", "--dry-run"])
|
|
103
|
+
|
|
104
|
+
assert result.exit_code == 0
|
|
105
|
+
assert "Harness: test-cluster" in result.output
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_pull_discovers_registered_member_repo(tmp_path, monkeypatch):
|
|
109
|
+
workspace = tmp_path / "repos"
|
|
110
|
+
workspace.mkdir()
|
|
111
|
+
member = workspace / "alpha"
|
|
112
|
+
member.mkdir()
|
|
113
|
+
root = tmp_path / "cluster"
|
|
114
|
+
_write_harness(root, workspace)
|
|
115
|
+
registry = tmp_path / "registry.json"
|
|
116
|
+
registry.write_text(json.dumps({"clusters": [{"root": str(root)}]}), encoding="utf-8")
|
|
117
|
+
monkeypatch.chdir(member)
|
|
118
|
+
monkeypatch.setenv("EVO_HARNESS_REGISTRY", str(registry))
|
|
119
|
+
monkeypatch.setattr(harness_command, "_git", lambda path, *args: _result())
|
|
120
|
+
|
|
121
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--repo", "alpha", "--dry-run"])
|
|
122
|
+
|
|
123
|
+
assert result.exit_code == 0
|
|
124
|
+
assert f"Manifest: {root / 'harness.yaml'}" in result.output
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_pull_rejects_unknown_repo(tmp_path):
|
|
128
|
+
workspace = tmp_path / "repos"
|
|
129
|
+
workspace.mkdir()
|
|
130
|
+
root = tmp_path / "cluster"
|
|
131
|
+
_write_harness(root, workspace)
|
|
132
|
+
|
|
133
|
+
result = CliRunner().invoke(cli, ["harness", "pull", "--harness", str(root), "--repo", "missing"])
|
|
134
|
+
|
|
135
|
+
assert result.exit_code == 1
|
|
136
|
+
assert "Unknown repo name(s): missing" in result.output
|
evo_cli-0.11.3/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.11.3
|
|
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
|
|
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
|
|
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
|