evo-cli 0.17.0__tar.gz → 0.19.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.17.0 → evo_cli-0.19.0}/PKG-INFO +63 -1
- {evo_cli-0.17.0 → evo_cli-0.19.0}/README.md +62 -0
- evo_cli-0.19.0/evo_cli/VERSION +1 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/cli.py +3 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/__init__.py +3 -1
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_paths.py +4 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_render.py +10 -0
- evo_cli-0.19.0/evo_cli/commands/harness/clone.py +123 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/pull.py +3 -7
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/views.py +4 -0
- evo_cli-0.19.0/evo_cli/commands/serp.py +402 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/registry.py +11 -0
- evo_cli-0.19.0/evo_cli/serp/__init__.py +7 -0
- evo_cli-0.19.0/evo_cli/serp/client.py +254 -0
- evo_cli-0.19.0/evo_cli/serp/creds.py +94 -0
- evo_cli-0.19.0/evo_cli/serp/errors.py +2 -0
- evo_cli-0.19.0/evo_cli/serp/install.py +239 -0
- evo_cli-0.19.0/evo_cli/serp/render.py +173 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/PKG-INFO +63 -1
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/SOURCES.txt +10 -0
- evo_cli-0.19.0/tests/test_harness_clone.py +270 -0
- evo_cli-0.19.0/tests/test_serp.py +455 -0
- evo_cli-0.17.0/evo_cli/VERSION +0 -1
- {evo_cli-0.17.0 → evo_cli-0.19.0}/Containerfile +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/HISTORY.md +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/LICENSE +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/MANIFEST.in +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/base.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-CsALFg16.css +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-DwEXL5pV.js +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/console.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/pyproject.toml +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/setup.cfg +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/__init__.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_cli.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_console.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_cred.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_download.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_gh.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_harness.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_opencode.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_tts.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_update.py +0 -0
- {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.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
|
|
@@ -159,6 +159,21 @@ else has the flat file open.
|
|
|
159
159
|
|
|
160
160
|
#### Harness Repositories
|
|
161
161
|
|
|
162
|
+
Clone every repository a harness manifest declares but this machine does not have yet:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
evo harness clone
|
|
166
|
+
evo harness clone --dry-run # show what lands where, change nothing
|
|
167
|
+
evo harness clone --repo backend --depth 1
|
|
168
|
+
evo harness clone --all # include repos marked present: false
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh machine
|
|
172
|
+
ends up with the layout the manifest, the contract seams and the exec-plans already assume. A repo
|
|
173
|
+
already on disk is left alone; if its `origin` is not the remote the manifest names, the summary
|
|
174
|
+
says so instead of touching it. When the manifest names a `branch`, the clone moves onto it, and a
|
|
175
|
+
branch nobody has pushed yet is reported rather than treated as a failure.
|
|
176
|
+
|
|
162
177
|
Fast-forward every available repository declared in a harness manifest:
|
|
163
178
|
|
|
164
179
|
```bash
|
|
@@ -235,6 +250,52 @@ to disk and the command warns.
|
|
|
235
250
|
The same engine backs the `evo-tts` MCP server in
|
|
236
251
|
[agent-skills](https://github.com/maycuatroi1/agent-skills), which gives an agent a `speak` tool.
|
|
237
252
|
|
|
253
|
+
#### Google Search (SerpApi)
|
|
254
|
+
|
|
255
|
+
Search Google from the terminal through [SerpApi](https://serpapi.com). One-time setup installs the
|
|
256
|
+
official `serpapi` CLI and stores the key in the omelet credential store:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
evo setup serp # install the CLI + prompt for the key (no echo)
|
|
260
|
+
evo setup serp --from-stdin < key.txt # non-interactive
|
|
261
|
+
evo setup serp --method none # key only; searches use the HTTPS API
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`--method auto` (the default) tries Homebrew first, then falls back to the GitHub release binary
|
|
265
|
+
dropped into `~/.evo/bin`. Homebrew 5 refuses to build from an untrusted third-party tap, so the
|
|
266
|
+
brew path needs `evo setup serp --trust-tap` (which runs `brew trust --formula
|
|
267
|
+
serpapi/tap/serpapi-cli`); without it the fallback fetches the same official binary.
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
evo serp search "evo cli python" # top 10 Google results
|
|
271
|
+
evo serp search coffee -l "Austin, Texas" -n 5 # localized
|
|
272
|
+
evo serp search openai -t news --hl vi --gl vn # news vertical, Vietnamese
|
|
273
|
+
evo serp search "site:github.com serpapi" --links # only URLs, pipe-friendly
|
|
274
|
+
evo serp search "rust async" -p 3 --json > out.json # 3 merged pages of raw JSON
|
|
275
|
+
evo serp search x -P tbs=qdr:d # any extra SerpApi parameter
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`-t` picks the vertical (`web`, `news`, `images`, `videos`, `shopping`, `scholar`). Google treats
|
|
279
|
+
`num` as a hint and often returns more, so the rendered output is capped at what `-n` asked for
|
|
280
|
+
while `--json` / `-o` keep the full payload.
|
|
281
|
+
|
|
282
|
+
Supporting commands:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
evo serp account # plan, searches used and left this month
|
|
286
|
+
evo serp locations Hanoi # canonical names to pass to --location
|
|
287
|
+
evo serp doctor # binary, key source (masked), remaining quota
|
|
288
|
+
evo serp raw search --jq '.organic_results[0]' engine=google q=coffee
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`search` runs through the `serpapi` binary when it is installed and falls back to the SerpApi HTTPS
|
|
292
|
+
endpoint otherwise, so a machine that only has the key still works (`--transport cli|http|auto`
|
|
293
|
+
forces the choice). The key is resolved from `SERPAPI_KEY`, then `evo cred get serpapi_api_key`,
|
|
294
|
+
then `~/.config/serpapi/config.toml`, and is passed to the binary through the environment - never on
|
|
295
|
+
the command line, where any process listing would show it. `evo setup serp --write-config` writes
|
|
296
|
+
`~/.config/serpapi/config.toml` (mode 600) if you also want the bare `serpapi` binary to
|
|
297
|
+
authenticate on its own.
|
|
298
|
+
|
|
238
299
|
## `evo harness` - read a repo cluster
|
|
239
300
|
|
|
240
301
|
A harness is a repo that describes a *cluster* of repos: `harness.yaml` lists them, `contracts.yaml`
|
|
@@ -253,6 +314,7 @@ evo harness plans # progress across every exec-plan
|
|
|
253
314
|
evo harness show <plan>
|
|
254
315
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
255
316
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
317
|
+
evo harness clone # clone every declared repo into its declared path
|
|
256
318
|
evo harness pull # fast-forward every repo
|
|
257
319
|
```
|
|
258
320
|
|
|
@@ -131,6 +131,21 @@ else has the flat file open.
|
|
|
131
131
|
|
|
132
132
|
#### Harness Repositories
|
|
133
133
|
|
|
134
|
+
Clone every repository a harness manifest declares but this machine does not have yet:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
evo harness clone
|
|
138
|
+
evo harness clone --dry-run # show what lands where, change nothing
|
|
139
|
+
evo harness clone --repo backend --depth 1
|
|
140
|
+
evo harness clone --all # include repos marked present: false
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh machine
|
|
144
|
+
ends up with the layout the manifest, the contract seams and the exec-plans already assume. A repo
|
|
145
|
+
already on disk is left alone; if its `origin` is not the remote the manifest names, the summary
|
|
146
|
+
says so instead of touching it. When the manifest names a `branch`, the clone moves onto it, and a
|
|
147
|
+
branch nobody has pushed yet is reported rather than treated as a failure.
|
|
148
|
+
|
|
134
149
|
Fast-forward every available repository declared in a harness manifest:
|
|
135
150
|
|
|
136
151
|
```bash
|
|
@@ -207,6 +222,52 @@ to disk and the command warns.
|
|
|
207
222
|
The same engine backs the `evo-tts` MCP server in
|
|
208
223
|
[agent-skills](https://github.com/maycuatroi1/agent-skills), which gives an agent a `speak` tool.
|
|
209
224
|
|
|
225
|
+
#### Google Search (SerpApi)
|
|
226
|
+
|
|
227
|
+
Search Google from the terminal through [SerpApi](https://serpapi.com). One-time setup installs the
|
|
228
|
+
official `serpapi` CLI and stores the key in the omelet credential store:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
evo setup serp # install the CLI + prompt for the key (no echo)
|
|
232
|
+
evo setup serp --from-stdin < key.txt # non-interactive
|
|
233
|
+
evo setup serp --method none # key only; searches use the HTTPS API
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`--method auto` (the default) tries Homebrew first, then falls back to the GitHub release binary
|
|
237
|
+
dropped into `~/.evo/bin`. Homebrew 5 refuses to build from an untrusted third-party tap, so the
|
|
238
|
+
brew path needs `evo setup serp --trust-tap` (which runs `brew trust --formula
|
|
239
|
+
serpapi/tap/serpapi-cli`); without it the fallback fetches the same official binary.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
evo serp search "evo cli python" # top 10 Google results
|
|
243
|
+
evo serp search coffee -l "Austin, Texas" -n 5 # localized
|
|
244
|
+
evo serp search openai -t news --hl vi --gl vn # news vertical, Vietnamese
|
|
245
|
+
evo serp search "site:github.com serpapi" --links # only URLs, pipe-friendly
|
|
246
|
+
evo serp search "rust async" -p 3 --json > out.json # 3 merged pages of raw JSON
|
|
247
|
+
evo serp search x -P tbs=qdr:d # any extra SerpApi parameter
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`-t` picks the vertical (`web`, `news`, `images`, `videos`, `shopping`, `scholar`). Google treats
|
|
251
|
+
`num` as a hint and often returns more, so the rendered output is capped at what `-n` asked for
|
|
252
|
+
while `--json` / `-o` keep the full payload.
|
|
253
|
+
|
|
254
|
+
Supporting commands:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
evo serp account # plan, searches used and left this month
|
|
258
|
+
evo serp locations Hanoi # canonical names to pass to --location
|
|
259
|
+
evo serp doctor # binary, key source (masked), remaining quota
|
|
260
|
+
evo serp raw search --jq '.organic_results[0]' engine=google q=coffee
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`search` runs through the `serpapi` binary when it is installed and falls back to the SerpApi HTTPS
|
|
264
|
+
endpoint otherwise, so a machine that only has the key still works (`--transport cli|http|auto`
|
|
265
|
+
forces the choice). The key is resolved from `SERPAPI_KEY`, then `evo cred get serpapi_api_key`,
|
|
266
|
+
then `~/.config/serpapi/config.toml`, and is passed to the binary through the environment - never on
|
|
267
|
+
the command line, where any process listing would show it. `evo setup serp --write-config` writes
|
|
268
|
+
`~/.config/serpapi/config.toml` (mode 600) if you also want the bare `serpapi` binary to
|
|
269
|
+
authenticate on its own.
|
|
270
|
+
|
|
210
271
|
## `evo harness` - read a repo cluster
|
|
211
272
|
|
|
212
273
|
A harness is a repo that describes a *cluster* of repos: `harness.yaml` lists them, `contracts.yaml`
|
|
@@ -225,6 +286,7 @@ evo harness plans # progress across every exec-plan
|
|
|
225
286
|
evo harness show <plan>
|
|
226
287
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
227
288
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
289
|
+
evo harness clone # clone every declared repo into its declared path
|
|
228
290
|
evo harness pull # fast-forward every repo
|
|
229
291
|
```
|
|
230
292
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.19.0
|
|
@@ -18,6 +18,7 @@ from evo_cli.commands.miniconda import miniconda
|
|
|
18
18
|
from evo_cli.commands.netcheck import netcheck
|
|
19
19
|
from evo_cli.commands.opencode import setup_opencode
|
|
20
20
|
from evo_cli.commands.plantuml import plantuml
|
|
21
|
+
from evo_cli.commands.serp import serp_group, setup_serp
|
|
21
22
|
from evo_cli.commands.site2s import site2s
|
|
22
23
|
from evo_cli.commands.ssh import setupssh
|
|
23
24
|
from evo_cli.commands.sysmon import sysmon
|
|
@@ -56,6 +57,7 @@ setup_group.add_command(agent_toy)
|
|
|
56
57
|
setup_group.add_command(setup_claude)
|
|
57
58
|
setup_group.add_command(setup_gh)
|
|
58
59
|
setup_group.add_command(setup_opencode)
|
|
60
|
+
setup_group.add_command(setup_serp)
|
|
59
61
|
setup_group.add_command(miniconda)
|
|
60
62
|
setup_group.add_command(setupssh)
|
|
61
63
|
|
|
@@ -73,6 +75,7 @@ cli.add_command(localproxy)
|
|
|
73
75
|
cli.add_command(mcp_group)
|
|
74
76
|
cli.add_command(netcheck)
|
|
75
77
|
cli.add_command(plantuml)
|
|
78
|
+
cli.add_command(serp_group)
|
|
76
79
|
cli.add_command(sysmon)
|
|
77
80
|
cli.add_command(tts_group)
|
|
78
81
|
cli.add_command(update)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import rich_click as click
|
|
2
2
|
|
|
3
3
|
from evo_cli.commands.harness.check import check
|
|
4
|
+
from evo_cli.commands.harness.clone import clone
|
|
4
5
|
from evo_cli.commands.harness.edit import debt, question, repo, step
|
|
5
6
|
from evo_cli.commands.harness.pull import pull
|
|
6
7
|
from evo_cli.commands.harness.serve import serve
|
|
@@ -21,9 +22,10 @@ def harness_group():
|
|
|
21
22
|
evo harness graph <plan> print a DAG as an adjacency list
|
|
22
23
|
evo harness check <plan> check what the plan claims against real git
|
|
23
24
|
evo harness step <plan> 3 done mark a step
|
|
25
|
+
evo harness clone clone every declared repo into its declared path
|
|
24
26
|
evo harness pull fast-forward every repo
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
for command in (serve, repos, seams, plans, show, graph, check, step, debt, question, repo, pull):
|
|
30
|
+
for command in (serve, repos, seams, plans, show, graph, check, step, debt, question, repo, clone, pull):
|
|
29
31
|
harness_group.add_command(command)
|
|
@@ -62,6 +62,10 @@ def load_repos(manifest_path):
|
|
|
62
62
|
{
|
|
63
63
|
"name": name,
|
|
64
64
|
"path": path,
|
|
65
|
+
# Where the manifest says the repo belongs, whether or not it is there yet.
|
|
66
|
+
# `path` falls back to the workspace when the declared path is missing, so a
|
|
67
|
+
# command that creates the checkout has to read this one instead.
|
|
68
|
+
"declared_path": declared or fallback.resolve(),
|
|
65
69
|
"present": bool(present) and path.is_dir(),
|
|
66
70
|
"declared_present": bool(present),
|
|
67
71
|
"role": str(entry.get("role") or ""),
|
|
@@ -85,6 +85,16 @@ def print_graph(title: str, graph: dict) -> None:
|
|
|
85
85
|
print_warnings(graph["warnings"])
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def print_summary(results: list) -> None:
|
|
89
|
+
"""One `name status` line per repo, coloured by the verb the status starts with."""
|
|
90
|
+
click.echo()
|
|
91
|
+
click.secho("Summary", bold=True)
|
|
92
|
+
width = max((len(name) for name, _ in results), default=0)
|
|
93
|
+
for name, status in results:
|
|
94
|
+
color = "red" if status.startswith("failed") else "yellow" if status.startswith("skipped") else "green"
|
|
95
|
+
click.secho(f" {name:<{width}} {status}", fg=color)
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def fail_if(condition: bool) -> None:
|
|
89
99
|
if condition:
|
|
90
100
|
raise click.exceptions.Exit(1)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import rich_click as click
|
|
5
|
+
|
|
6
|
+
from evo_cli.commands.harness._paths import find_manifest, git, harness_option, load_repos
|
|
7
|
+
from evo_cli.commands.harness._render import print_summary
|
|
8
|
+
|
|
9
|
+
SCHEME_RE = re.compile(r"^\w+://")
|
|
10
|
+
USER_RE = re.compile(r"^[^@/]+@")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_remote(url):
|
|
14
|
+
"""Reduce a remote URL to host + path so the ssh and https forms of one repo compare equal."""
|
|
15
|
+
text = str(url).strip().rstrip("/")
|
|
16
|
+
if text.endswith(".git"):
|
|
17
|
+
text = text[: -len(".git")]
|
|
18
|
+
text = USER_RE.sub("", SCHEME_RE.sub("", text))
|
|
19
|
+
return text.replace(":", "/", 1).lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detail_of(result, fallback):
|
|
23
|
+
lines = ((result.stderr or "") + (result.stdout or "")).strip().splitlines()
|
|
24
|
+
return lines[-1] if lines else fallback
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def already_cloned(entry, target):
|
|
28
|
+
"""A checkout is already there. Say so, and say if it came from a different remote."""
|
|
29
|
+
declared = entry["origin"]
|
|
30
|
+
actual = git(target, "remote", "get-url", "origin").stdout.strip()
|
|
31
|
+
if declared and actual and normalize_remote(declared) != normalize_remote(actual):
|
|
32
|
+
return f"skipped (already cloned, but origin is {actual})"
|
|
33
|
+
return "skipped (already cloned)"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def checkout_declared_branch(entry, target):
|
|
37
|
+
"""Move onto the branch the manifest names. A branch that is not on origin is not an error.
|
|
38
|
+
|
|
39
|
+
Cloning with `--branch` would fail the whole clone for a branch that has not been pushed yet,
|
|
40
|
+
which is a normal state for a plan in progress. Clone the default branch, then try to move.
|
|
41
|
+
"""
|
|
42
|
+
branch = entry["branch"]
|
|
43
|
+
if not branch or branch.upper() == "TBD":
|
|
44
|
+
return ""
|
|
45
|
+
current = git(target, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
|
|
46
|
+
if current == branch:
|
|
47
|
+
return ""
|
|
48
|
+
if git(target, "checkout", branch, timeout=120).returncode != 0:
|
|
49
|
+
return f" (branch {branch!r} is not on origin, left on {current or 'HEAD'})"
|
|
50
|
+
return f" (on {branch})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.command("clone", help="Clone the repositories a harness declares into their declared paths.")
|
|
54
|
+
@harness_option
|
|
55
|
+
@click.option("--repo", "repos", multiple=True, help="Clone only this repo name. Repeat for multiple repos.")
|
|
56
|
+
@click.option("--all", "include_absent", is_flag=True, help="Also clone repos the manifest marks present: false.")
|
|
57
|
+
@click.option("--depth", type=int, help="Shallow clone with this many commits of history.")
|
|
58
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be cloned, and where.")
|
|
59
|
+
def clone(harness_path, repos, include_absent, depth, dry_run):
|
|
60
|
+
"""Clone every repository the harness manifest declares but this machine does not have.
|
|
61
|
+
|
|
62
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh
|
|
63
|
+
machine ends up with the layout the manifest, the contract seams and the exec-plans already
|
|
64
|
+
assume. Repos already on disk are left alone.
|
|
65
|
+
"""
|
|
66
|
+
if shutil.which("git") is None:
|
|
67
|
+
raise click.ClickException("git is not installed or is not on PATH")
|
|
68
|
+
|
|
69
|
+
manifest_path = find_manifest(harness_path)
|
|
70
|
+
manifest, entries = load_repos(manifest_path)
|
|
71
|
+
available = {entry["name"] for entry in entries}
|
|
72
|
+
unknown = sorted(set(repos) - available)
|
|
73
|
+
if unknown:
|
|
74
|
+
raise click.ClickException(f"Unknown repo name(s): {', '.join(unknown)}")
|
|
75
|
+
selected = [entry for entry in entries if not repos or entry["name"] in repos]
|
|
76
|
+
|
|
77
|
+
click.echo(f"Harness: {manifest.get('name') or manifest_path.parent.name}")
|
|
78
|
+
click.echo(f"Manifest: {manifest_path}")
|
|
79
|
+
results = []
|
|
80
|
+
failures = 0
|
|
81
|
+
|
|
82
|
+
for entry in selected:
|
|
83
|
+
name = entry["name"]
|
|
84
|
+
target = entry["declared_path"]
|
|
85
|
+
|
|
86
|
+
# `present: false` means the manifest knows about the repo and deliberately does not want
|
|
87
|
+
# it here. Naming it with --repo, or asking for --all, overrides that.
|
|
88
|
+
if not entry["declared_present"] and not include_absent and not repos:
|
|
89
|
+
results.append((name, "skipped (present: false)"))
|
|
90
|
+
continue
|
|
91
|
+
if (target / ".git").is_dir():
|
|
92
|
+
results.append((name, already_cloned(entry, target)))
|
|
93
|
+
continue
|
|
94
|
+
if entry["path"] != target and (entry["path"] / ".git").is_dir():
|
|
95
|
+
results.append((name, f"skipped (already cloned at {entry['path']}, manifest says {target})"))
|
|
96
|
+
continue
|
|
97
|
+
if target.exists() and (not target.is_dir() or any(target.iterdir())):
|
|
98
|
+
results.append((name, f"failed (path is taken and is not an empty directory: {target})"))
|
|
99
|
+
failures += 1
|
|
100
|
+
continue
|
|
101
|
+
if not entry["origin"]:
|
|
102
|
+
results.append((name, "failed (no origin declared in the manifest)"))
|
|
103
|
+
failures += 1
|
|
104
|
+
continue
|
|
105
|
+
if dry_run:
|
|
106
|
+
results.append((name, f"would clone {entry['origin']} into {target}"))
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
args = ["clone"]
|
|
111
|
+
if depth:
|
|
112
|
+
args += ["--depth", str(depth)]
|
|
113
|
+
args += [entry["origin"], str(target)]
|
|
114
|
+
result = git(target.parent, *args, timeout=900)
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
results.append((name, f"failed ({detail_of(result, 'git clone failed')})"))
|
|
117
|
+
failures += 1
|
|
118
|
+
continue
|
|
119
|
+
results.append((name, f"cloned into {target}{checkout_declared_branch(entry, target)}"))
|
|
120
|
+
|
|
121
|
+
print_summary(results)
|
|
122
|
+
if failures:
|
|
123
|
+
raise click.exceptions.Exit(1)
|
|
@@ -3,6 +3,7 @@ import shutil
|
|
|
3
3
|
import rich_click as click
|
|
4
4
|
|
|
5
5
|
from evo_cli.commands.harness._paths import find_manifest, git, harness_option, load_repos
|
|
6
|
+
from evo_cli.commands.harness._render import print_summary
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@click.command("pull", help="Fast-forward all available repositories in a harness.")
|
|
@@ -34,7 +35,7 @@ def pull(harness_path, repos, prune, dry_run):
|
|
|
34
35
|
results.append((name, "skipped (present: false)"))
|
|
35
36
|
continue
|
|
36
37
|
if not path.is_dir():
|
|
37
|
-
results.append((name, f"failed (missing: {path})"))
|
|
38
|
+
results.append((name, f"failed (missing: {path} - run `evo harness clone`)"))
|
|
38
39
|
incomplete += 1
|
|
39
40
|
continue
|
|
40
41
|
|
|
@@ -64,11 +65,6 @@ def pull(harness_path, repos, prune, dry_run):
|
|
|
64
65
|
head = git(path, "log", "-1", "--oneline")
|
|
65
66
|
results.append((name, head.stdout.strip() or "updated"))
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
click.secho("Summary", bold=True)
|
|
69
|
-
width = max((len(name) for name, _ in results), default=0)
|
|
70
|
-
for name, status in results:
|
|
71
|
-
color = "red" if status.startswith("failed") else "yellow" if status.startswith("skipped") else "green"
|
|
72
|
-
click.secho(f" {name:<{width}} {status}", fg=color)
|
|
68
|
+
print_summary(results)
|
|
73
69
|
if incomplete:
|
|
74
70
|
raise click.exceptions.Exit(1)
|
|
@@ -71,6 +71,10 @@ def repos(harness_path, with_git):
|
|
|
71
71
|
grid.add_row(f"[bold]{entry['name']}[/]", entry["role"] or "-", branch, state, one_line(entry["note"], width))
|
|
72
72
|
console.print(grid)
|
|
73
73
|
|
|
74
|
+
missing = [entry["name"] for entry in info["repos"] if not entry["present"]]
|
|
75
|
+
if missing:
|
|
76
|
+
console.print(f"\n[red]{len(missing)} repo(s) missing[/] - run [accent]evo harness clone[/] to fetch them")
|
|
77
|
+
|
|
74
78
|
if info["proposals_pending"]:
|
|
75
79
|
console.print(f"\n[yellow]{len(info['proposals_pending'])} pending proposal(s)[/] in proposals/_pending")
|
|
76
80
|
|