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.
Files changed (119) hide show
  1. {evo_cli-0.17.0 → evo_cli-0.19.0}/PKG-INFO +63 -1
  2. {evo_cli-0.17.0 → evo_cli-0.19.0}/README.md +62 -0
  3. evo_cli-0.19.0/evo_cli/VERSION +1 -0
  4. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/cli.py +3 -0
  5. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/__init__.py +3 -1
  6. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_paths.py +4 -0
  7. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_render.py +10 -0
  8. evo_cli-0.19.0/evo_cli/commands/harness/clone.py +123 -0
  9. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/pull.py +3 -7
  10. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/views.py +4 -0
  11. evo_cli-0.19.0/evo_cli/commands/serp.py +402 -0
  12. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/registry.py +11 -0
  13. evo_cli-0.19.0/evo_cli/serp/__init__.py +7 -0
  14. evo_cli-0.19.0/evo_cli/serp/client.py +254 -0
  15. evo_cli-0.19.0/evo_cli/serp/creds.py +94 -0
  16. evo_cli-0.19.0/evo_cli/serp/errors.py +2 -0
  17. evo_cli-0.19.0/evo_cli/serp/install.py +239 -0
  18. evo_cli-0.19.0/evo_cli/serp/render.py +173 -0
  19. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/PKG-INFO +63 -1
  20. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/SOURCES.txt +10 -0
  21. evo_cli-0.19.0/tests/test_harness_clone.py +270 -0
  22. evo_cli-0.19.0/tests/test_serp.py +455 -0
  23. evo_cli-0.17.0/evo_cli/VERSION +0 -1
  24. {evo_cli-0.17.0 → evo_cli-0.19.0}/Containerfile +0 -0
  25. {evo_cli-0.17.0 → evo_cli-0.19.0}/HISTORY.md +0 -0
  26. {evo_cli-0.17.0 → evo_cli-0.19.0}/LICENSE +0 -0
  27. {evo_cli-0.17.0 → evo_cli-0.19.0}/MANIFEST.in +0 -0
  28. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/__init__.py +0 -0
  29. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/__main__.py +0 -0
  30. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/base.py +0 -0
  31. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/__init__.py +0 -0
  32. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/agent_toy.py +0 -0
  33. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/claude_code.py +0 -0
  34. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/cloudflare.py +0 -0
  35. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/cred.py +0 -0
  36. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/download.py +0 -0
  37. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/fix_claude.py +0 -0
  38. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/gdrive.py +0 -0
  39. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/gh.py +0 -0
  40. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_dag.py +0 -0
  41. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_git.py +0 -0
  42. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_model.py +0 -0
  43. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_mutate.py +0 -0
  44. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_server.py +0 -0
  45. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/check.py +0 -0
  46. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/edit.py +0 -0
  47. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/serve.py +0 -0
  48. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/show.py +0 -0
  49. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-CsALFg16.css +0 -0
  50. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-DwEXL5pV.js +0 -0
  51. {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
  52. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  53. {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
  54. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  55. {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
  56. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  57. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/index.html +0 -0
  64. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/hwid.py +0 -0
  65. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/hwid_reset.py +0 -0
  66. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/localproxy.py +0 -0
  67. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/mcp.py +0 -0
  68. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/miniconda.py +0 -0
  69. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/netcheck.py +0 -0
  70. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/opencode.py +0 -0
  71. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/plantuml.py +0 -0
  72. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/site2s.py +0 -0
  73. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/ssh.py +0 -0
  74. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/sysmon.py +0 -0
  75. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/tts.py +0 -0
  76. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/update.py +0 -0
  77. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/commands/wifi.py +0 -0
  78. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/console.py +0 -0
  79. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/__init__.py +0 -0
  80. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/doctor.py +0 -0
  81. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/google_oauth.py +0 -0
  82. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/migrate.py +0 -0
  83. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/oauth_flow.py +0 -0
  84. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/store.py +0 -0
  85. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/credentials/sync.py +0 -0
  86. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/mcp_registry.py +0 -0
  87. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/__init__.py +0 -0
  88. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/chunking.py +0 -0
  89. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/core.py +0 -0
  90. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/creds.py +0 -0
  91. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/errors.py +0 -0
  92. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/http.py +0 -0
  93. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/openai.py +0 -0
  94. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/player.py +0 -0
  95. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli/tts/vbee.py +0 -0
  96. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  97. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/entry_points.txt +0 -0
  98. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/requires.txt +0 -0
  99. {evo_cli-0.17.0 → evo_cli-0.19.0}/evo_cli.egg-info/top_level.txt +0 -0
  100. {evo_cli-0.17.0 → evo_cli-0.19.0}/pyproject.toml +0 -0
  101. {evo_cli-0.17.0 → evo_cli-0.19.0}/setup.cfg +0 -0
  102. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/__init__.py +0 -0
  103. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_agent_toy.py +0 -0
  104. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_claude_code.py +0 -0
  105. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_cli.py +0 -0
  106. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_console.py +0 -0
  107. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_cred.py +0 -0
  108. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_download.py +0 -0
  109. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_fix_claude.py +0 -0
  110. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_gh.py +0 -0
  111. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_harness.py +0 -0
  112. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_harness_dag.py +0 -0
  113. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_mcp.py +0 -0
  114. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_opencode.py +0 -0
  115. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_plantuml.py +0 -0
  116. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_sysmon.py +0 -0
  117. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_tts.py +0 -0
  118. {evo_cli-0.17.0 → evo_cli-0.19.0}/tests/test_update.py +0 -0
  119. {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.17.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
- click.echo()
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