evo-cli 0.35.1__tar.gz → 0.36.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 (156) hide show
  1. {evo_cli-0.35.1 → evo_cli-0.36.0}/PKG-INFO +1 -1
  2. evo_cli-0.36.0/evo_cli/VERSION +1 -0
  3. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/cred.py +28 -0
  4. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_spoofdpi.py +3 -60
  5. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/openvpn.py +0 -16
  6. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/PKG-INFO +1 -1
  7. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_cred.py +49 -1
  8. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_openvpn.py +1 -11
  9. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_route.py +0 -30
  10. evo_cli-0.35.1/evo_cli/VERSION +0 -1
  11. {evo_cli-0.35.1 → evo_cli-0.36.0}/Containerfile +0 -0
  12. {evo_cli-0.35.1 → evo_cli-0.36.0}/HISTORY.md +0 -0
  13. {evo_cli-0.35.1 → evo_cli-0.36.0}/LICENSE +0 -0
  14. {evo_cli-0.35.1 → evo_cli-0.36.0}/MANIFEST.in +0 -0
  15. {evo_cli-0.35.1 → evo_cli-0.36.0}/README.md +0 -0
  16. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/__init__.py +0 -0
  17. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/__main__.py +0 -0
  18. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/base.py +0 -0
  19. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/cli.py +0 -0
  20. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/__init__.py +0 -0
  21. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/agent_toy.py +0 -0
  22. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/claude_code.py +0 -0
  23. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/cloudflare.py +0 -0
  24. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/download.py +0 -0
  25. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/fix_claude.py +0 -0
  26. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/gdrive.py +0 -0
  27. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/gh.py +0 -0
  28. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/__init__.py +0 -0
  29. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_dag.py +0 -0
  30. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_git.py +0 -0
  31. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_hub.py +0 -0
  32. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_mirror.py +0 -0
  33. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_model.py +0 -0
  34. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_mutate.py +0 -0
  35. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_paths.py +0 -0
  36. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_render.py +0 -0
  37. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/_server.py +0 -0
  38. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/check.py +0 -0
  39. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/clone.py +0 -0
  40. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/edit.py +0 -0
  41. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/pull.py +0 -0
  42. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/serve.py +0 -0
  43. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/show.py +0 -0
  44. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/views.py +0 -0
  45. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
  46. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
  47. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  48. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  49. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  50. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  51. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  52. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  53. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  54. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  55. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  56. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  57. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  58. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  59. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/harness/web/index.html +0 -0
  60. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/hwid.py +0 -0
  61. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/hwid_reset.py +0 -0
  62. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/image.py +0 -0
  63. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/localproxy.py +0 -0
  64. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/mcp.py +0 -0
  65. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/miniconda.py +0 -0
  66. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/netcheck.py +0 -0
  67. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/ngrok.py +0 -0
  68. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/opencode.py +0 -0
  69. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/openvpn.py +0 -0
  70. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/plantuml.py +0 -0
  71. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/__init__.py +0 -0
  72. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_dns.py +0 -0
  73. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_dpi.py +0 -0
  74. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_hosts.py +0 -0
  75. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_page.py +0 -0
  76. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/_probe.py +0 -0
  77. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/check.py +0 -0
  78. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/fix.py +0 -0
  79. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/probe.py +0 -0
  80. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/route/status.py +0 -0
  81. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/serp.py +0 -0
  82. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/site2s.py +0 -0
  83. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/ssh.py +0 -0
  84. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/storage.py +0 -0
  85. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/sysmon.py +0 -0
  86. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/tts.py +0 -0
  87. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/update.py +0 -0
  88. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/commands/wifi.py +0 -0
  89. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/console.py +0 -0
  90. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/__init__.py +0 -0
  91. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/doctor.py +0 -0
  92. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/google_oauth.py +0 -0
  93. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/migrate.py +0 -0
  94. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/oauth_flow.py +0 -0
  95. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/registry.py +0 -0
  96. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/store.py +0 -0
  97. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/credentials/sync.py +0 -0
  98. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/__init__.py +0 -0
  99. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/align.py +0 -0
  100. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/core.py +0 -0
  101. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/creds.py +0 -0
  102. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/errors.py +0 -0
  103. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/gemini.py +0 -0
  104. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/imaging/ncnn.py +0 -0
  105. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/mcp_registry.py +0 -0
  106. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/__init__.py +0 -0
  107. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/client.py +0 -0
  108. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/creds.py +0 -0
  109. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/errors.py +0 -0
  110. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/install.py +0 -0
  111. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/serp/render.py +0 -0
  112. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/__init__.py +0 -0
  113. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/chunking.py +0 -0
  114. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/core.py +0 -0
  115. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/creds.py +0 -0
  116. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/errors.py +0 -0
  117. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/gemini.py +0 -0
  118. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/http.py +0 -0
  119. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/openai.py +0 -0
  120. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/player.py +0 -0
  121. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli/tts/vbee.py +0 -0
  122. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/SOURCES.txt +0 -0
  123. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  124. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/entry_points.txt +0 -0
  125. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/requires.txt +0 -0
  126. {evo_cli-0.35.1 → evo_cli-0.36.0}/evo_cli.egg-info/top_level.txt +0 -0
  127. {evo_cli-0.35.1 → evo_cli-0.36.0}/pyproject.toml +0 -0
  128. {evo_cli-0.35.1 → evo_cli-0.36.0}/setup.cfg +0 -0
  129. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/__init__.py +0 -0
  130. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/fake_evo_agents.py +0 -0
  131. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/fixtures/hub/contract-0.2.0.json +0 -0
  132. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/fixtures/hub/mirror-demo.yaml +0 -0
  133. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_agent_toy.py +0 -0
  134. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_claude_code.py +0 -0
  135. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_cli.py +0 -0
  136. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_console.py +0 -0
  137. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_download.py +0 -0
  138. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_fix_claude.py +0 -0
  139. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_gdrive.py +0 -0
  140. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_gh.py +0 -0
  141. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_harness.py +0 -0
  142. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_harness_clone.py +0 -0
  143. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_harness_dag.py +0 -0
  144. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_harness_hub.py +0 -0
  145. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_harness_show.py +0 -0
  146. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_image.py +0 -0
  147. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_mcp.py +0 -0
  148. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_ngrok.py +0 -0
  149. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_opencode.py +0 -0
  150. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_plantuml.py +0 -0
  151. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_serp.py +0 -0
  152. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_storage.py +0 -0
  153. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_sysmon.py +0 -0
  154. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_tts.py +0 -0
  155. {evo_cli-0.35.1 → evo_cli-0.36.0}/tests/test_update.py +0 -0
  156. {evo_cli-0.35.1 → evo_cli-0.36.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.35.1
3
+ Version: 0.36.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
@@ -0,0 +1 @@
1
+ 0.36.0
@@ -81,6 +81,34 @@ def get(key_path, export_var):
81
81
  sys.stdout.write("\n")
82
82
 
83
83
 
84
+ def _key_paths(node, prefix=""):
85
+ for key, value in node.items():
86
+ path = f"{prefix}.{key}" if prefix else key
87
+ if isinstance(value, dict) and value:
88
+ yield from _key_paths(value, path)
89
+ else:
90
+ yield path
91
+
92
+
93
+ @cred_group.command("keys", help="List dotted key paths in the store, never their values.")
94
+ @click.argument("pattern", required=False)
95
+ @click.option("--json", "as_json", is_flag=True, help="Print the paths as a JSON list.")
96
+ def keys(pattern, as_json):
97
+ flat = _guard(read_flat)
98
+ flat.pop("_generated", None)
99
+ paths = list(_key_paths(flat))
100
+ if pattern:
101
+ paths = [path for path in paths if pattern.lower() in path.lower()]
102
+ if not paths:
103
+ raise click.ClickException(f"no key path contains '{pattern}'")
104
+
105
+ if as_json:
106
+ sys.stdout.write(json.dumps(paths) + "\n")
107
+ return
108
+ for path in paths:
109
+ sys.stdout.write(path + "\n")
110
+
111
+
84
112
  @cred_group.command("list", help="List every credential in the store with its health.")
85
113
  def list_credentials():
86
114
  _guard(require_folder)
@@ -3,10 +3,9 @@
3
3
  GoodbyeDPI has no macOS build. SpoofDPI is a local HTTP proxy that splits the
4
4
  TLS ClientHello, so the filter never reads the server name in one piece. It only
5
5
  does that for the hosts `evo route open` named: every other name passes through
6
- untouched and resolves with the system's DNS server. The named hosts resolve over
7
- DoH instead, because a name the filter blocks usually has its DNS answer poisoned
8
- too. SpoofDPI's resolver never reads /etc/resolver, so names a connected evo VPN
9
- resolves through split DNS skip the proxy altogether (`sync_vpn_bypass`).
6
+ untouched and resolves with the system's DNS server (SpoofDPI's own resolver,
7
+ which does not read /etc/resolver). The named hosts resolve over DoH instead,
8
+ because a name the filter blocks usually has its DNS answer poisoned too.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
@@ -47,10 +46,6 @@ dns = {{ mode = "https", cache = true }}
47
46
  https = {{ skip = false, split-mode = "{SPLIT_MODE}" }}
48
47
  """
49
48
 
50
- # The evo openvpn DNS hook starts each /etc/resolver file it writes with this tag.
51
- RESOLVER = Path("/etc/resolver")
52
- VPN_TAG = "# evo openvpn "
53
-
54
49
  # networksetup names both proxies by these words: -getwebproxy, -setsecurewebproxystate, ...
55
50
  PROXY_KINDS = ("webproxy", "securewebproxy")
56
51
 
@@ -263,56 +258,6 @@ def clear_system_proxy():
263
258
  return cleared
264
259
 
265
260
 
266
- def vpn_domains():
267
- """Domains a connected evo VPN resolves with its own DNS: its tagged files under /etc/resolver."""
268
- domains = []
269
- for path in sorted(RESOLVER.glob("*")):
270
- try:
271
- with path.open(encoding="utf-8") as handle:
272
- tagged = handle.readline().startswith(VPN_TAG)
273
- except (OSError, UnicodeDecodeError):
274
- continue
275
- if tagged:
276
- domains.append(path.name)
277
- return domains
278
-
279
-
280
- def _bypass(service):
281
- lines = _networksetup("-getproxybypassdomains", service).stdout.splitlines()
282
- return [line.strip() for line in lines if line.strip() and not line.startswith("There aren't any")]
283
-
284
-
285
- def sync_vpn_bypass():
286
- """Let the names a connected evo VPN resolves skip the proxy, and take them out again once it is down.
287
-
288
- SpoofDPI would ask the system's DNS server for them and fail. Each service whose proxy is ours bypasses those
289
- domains; what this added is recorded per service, so only that comes out again and a user's entries stay.
290
- """
291
- record = home() / "vpn-bypass.json"
292
- try:
293
- added = json.loads(record.read_text())
294
- except (OSError, ValueError):
295
- added = {}
296
- wanted = [entry for domain in vpn_domains() for entry in (domain, f"*.{domain}")]
297
- proxied = proxied_services()
298
- now = {}
299
- for service in network_services():
300
- mine = added.get(service, [])
301
- if not mine and service not in proxied:
302
- continue
303
- current = _bypass(service)
304
- kept = [entry for entry in current if entry not in mine]
305
- new = [entry for entry in wanted if entry not in kept] if service in proxied else []
306
- if kept + new != current:
307
- _networksetup("-setproxybypassdomains", service, *(kept + new or ["Empty"]))
308
- if new:
309
- now[service] = new
310
- if now or added:
311
- home().mkdir(parents=True, exist_ok=True)
312
- record.write_text(json.dumps(now))
313
- return now
314
-
315
-
316
261
  def _extension_name(profile, ext_id):
317
262
  for path in sorted((profile / "Extensions" / ext_id).glob("*/manifest.json")):
318
263
  try:
@@ -378,7 +323,6 @@ def ensure(*hosts):
378
323
  if not _start(reload=_write_agent(exe)):
379
324
  return service_state()
380
325
  set_system_proxy()
381
- sync_vpn_bypass()
382
326
  return service_state()
383
327
 
384
328
 
@@ -395,7 +339,6 @@ def forget(host):
395
339
  def remove():
396
340
  """Clear the system proxy first, so nothing points at a proxy that is gone, then stop and drop the agent."""
397
341
  clear_system_proxy()
398
- sync_vpn_bypass()
399
342
  _launchctl("bootout", f"{_domain()}/{LABEL}")
400
343
  agent_path().unlink(missing_ok=True)
401
344
  hosts_path().unlink(missing_ok=True)
@@ -355,18 +355,6 @@ def split_dns(command, name, hooks=Path("/var/run"), resolver=Path("/etc/resolve
355
355
  return ["sh", "-c", wrapper, "sh", script, *hooked]
356
356
 
357
357
 
358
- def sync_proxy_bypass():
359
- """On macOS, let the names this VPN resolves skip evo route's proxy; never fatal to the connection."""
360
- if sys.platform != "darwin":
361
- return
362
- try:
363
- from evo_cli.commands.route import _spoofdpi
364
-
365
- _spoofdpi.sync_vpn_bypass()
366
- except Exception:
367
- pass
368
-
369
-
370
358
  def parse_totp(value):
371
359
  value = value.strip()
372
360
  settings = {"secret": value, "algorithm": "SHA1", "digits": 6, "period": 30}
@@ -823,8 +811,6 @@ def worker(name, timeout):
823
811
  state["since"] = time.time()
824
812
  state.pop("auth_url", None)
825
813
  state.pop("auth_expires", None)
826
- # The DNS hook ran before CONNECTED (route-up); its domains now exist.
827
- sync_proxy_bypass()
828
814
  # Once up, let OpenVPN retry for as long as the network is gone.
829
815
  deadline = float("inf")
830
816
  elif fields[1] != "CONNECTED":
@@ -876,8 +862,6 @@ def worker(name, timeout):
876
862
  "error": "OpenVPN did not exit; inspect processes manually.",
877
863
  }
878
864
  publish()
879
- # OpenVPN has exited and its DNS hook has removed the domains; the proxy takes them back.
880
- sync_proxy_bypass()
881
865
  selector.close()
882
866
  control.close()
883
867
  listener.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.35.1
3
+ Version: 0.36.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
@@ -41,7 +41,7 @@ def _write(store, rel, entry):
41
41
 
42
42
  def test_cred_command_is_registered():
43
43
  assert "cred" in cli.commands
44
- for name in ("get", "list", "doctor", "compile", "add", "auth", "refresh", "migrate", "sync", "path"):
44
+ for name in ("get", "keys", "list", "doctor", "compile", "add", "auth", "refresh", "migrate", "sync", "path"):
45
45
  assert name in cli.commands["cred"].commands
46
46
  assert set(cli.commands["cred"].commands["sync"].commands) == {"push", "pull"}
47
47
 
@@ -113,6 +113,54 @@ def test_get_command_missing_key_fails(store):
113
113
  assert "key not found: nope.deeper" in result.output
114
114
 
115
115
 
116
+ def _write_secrets(store):
117
+ _write(store, "ai/openai.json", {"id": "openai", "flat": {"openai_api_key": "sk-live-123"}})
118
+ _write(
119
+ store,
120
+ "net/mikrotik.json",
121
+ {"id": "mikrotik", "flat": {"ialab_network": {"username": "admin-user", "password": "pw-456"}}},
122
+ )
123
+ compile_flat()
124
+
125
+
126
+ def test_keys_lists_every_path_and_no_value(store):
127
+ _write_secrets(store)
128
+
129
+ result = CliRunner().invoke(cli, ["cred", "keys"])
130
+
131
+ assert result.exit_code == 0
132
+ assert result.output.split() == ["openai_api_key", "ialab_network.username", "ialab_network.password"]
133
+ for value in ("sk-live-123", "admin-user", "pw-456"):
134
+ assert value not in result.output
135
+
136
+
137
+ def test_keys_filters_case_insensitively(store):
138
+ _write_secrets(store)
139
+
140
+ result = CliRunner().invoke(cli, ["cred", "keys", "NETWORK"])
141
+
142
+ assert result.exit_code == 0
143
+ assert result.output == "ialab_network.username\nialab_network.password\n"
144
+
145
+
146
+ def test_keys_json_form(store):
147
+ _write_secrets(store)
148
+
149
+ result = CliRunner().invoke(cli, ["cred", "keys", "openai", "--json"])
150
+
151
+ assert result.exit_code == 0
152
+ assert json.loads(result.output) == ["openai_api_key"]
153
+
154
+
155
+ def test_keys_no_match_fails(store):
156
+ _write_secrets(store)
157
+
158
+ result = CliRunner().invoke(cli, ["cred", "keys", "nothing-like-this"])
159
+
160
+ assert result.exit_code != 0
161
+ assert "no key path contains 'nothing-like-this'" in result.output
162
+
163
+
116
164
  def test_set_value_routes_known_key_to_its_spec_file(store):
117
165
  path, existed = set_value("openai_api_key", "sk-new")
118
166
 
@@ -18,14 +18,6 @@ CONFIG = "client\ndev tun\nremote vpn.example 1194 udp\nauth-user-pass\n<ca>\nte
18
18
  SECRET = base64.b32encode(b"12345678901234567890").decode()
19
19
 
20
20
 
21
- @pytest.fixture(autouse=True)
22
- def no_proxy_sync(monkeypatch):
23
- """The worker would otherwise change this machine's proxy bypass list."""
24
- synced = []
25
- monkeypatch.setattr(vpn, "sync_proxy_bypass", lambda: synced.append(True))
26
- return synced
27
-
28
-
29
21
  @pytest.fixture
30
22
  def store(tmp_path, monkeypatch):
31
23
  if sys.platform == "win32":
@@ -651,7 +643,7 @@ with socket.socket(socket.AF_UNIX) as s:
651
643
 
652
644
 
653
645
  @pytest.mark.parametrize("mode", ["ok", "fail", "reconnect", "webauth"])
654
- def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatch, mode, no_proxy_sync):
646
+ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatch, mode):
655
647
  fail, webauth = mode == "fail", mode == "webauth"
656
648
  config = CERT_ONLY if webauth else CONFIG
657
649
  vpn.save_profile(
@@ -698,8 +690,6 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
698
690
  thread.join(5)
699
691
  assert not thread.is_alive()
700
692
  assert opened == (["https://vpn.example/auth/1"] if webauth else [])
701
- # The proxy bypass follows the DNS hook: on each CONNECTED, and once more after OpenVPN exits.
702
- assert len(no_proxy_sync) == {"fail": 1, "reconnect": 3}.get(mode, 2)
703
693
  assert vpn.request("m1")["state"] == "STOPPED"
704
694
  base = vpn.session_path("m1")
705
695
  state = json.loads(Path(str(base) + ".json").read_text())
@@ -254,36 +254,6 @@ def _fake_networksetup(proxies, calls):
254
254
  return run
255
255
 
256
256
 
257
- def test_vpn_split_dns_domains_skip_the_proxy_and_only_ours_come_out(tmp_path, monkeypatch):
258
- monkeypatch.setenv("EVO_ROUTE_HOME", str(tmp_path / "route"))
259
- resolver = tmp_path / "resolver"
260
- resolver.mkdir()
261
- monkeypatch.setattr(_spoofdpi, "RESOLVER", resolver)
262
- (resolver / "corp.example").write_text("# evo openvpn m1\nnameserver 10.0.0.1\n")
263
- (resolver / "mine.example").write_text("nameserver 192.0.2.1\n")
264
- ours = (True, _spoofdpi.LISTEN_HOST, str(_spoofdpi.LISTEN_PORT))
265
- proxies = {("Wi-Fi", "webproxy"): ours, ("Wi-Fi", "securewebproxy"): ours}
266
- bypass = {"Wi-Fi": ["*.local", "*.corp.example"], "USB LAN": ["*.local"]}
267
- fake = _fake_networksetup(proxies, [])
268
-
269
- def run(*args):
270
- if args[0] == "-getproxybypassdomains":
271
- out = "\n".join(bypass[args[1]]) or f"There aren't any bypass domains set on {args[1]}."
272
- return subprocess.CompletedProcess(args, 0, out + "\n", "")
273
- if args[0] == "-setproxybypassdomains":
274
- bypass[args[1]] = [] if args[2:] == ("Empty",) else list(args[2:])
275
- return subprocess.CompletedProcess(args, 0, "", "")
276
- return fake(*args)
277
-
278
- monkeypatch.setattr(_spoofdpi, "_networksetup", run)
279
- # Only the tagged domain, only on the proxied service; the user's own *.corp.example is not added twice.
280
- assert _spoofdpi.sync_vpn_bypass() == {"Wi-Fi": ["corp.example"]}
281
- assert bypass == {"Wi-Fi": ["*.local", "*.corp.example", "corp.example"], "USB LAN": ["*.local"]}
282
- (resolver / "corp.example").unlink() # the VPN is down
283
- assert _spoofdpi.sync_vpn_bypass() == {}
284
- assert bypass == {"Wi-Fi": ["*.local", "*.corp.example"], "USB LAN": ["*.local"]}
285
-
286
-
287
257
  def test_spoofdpi_host_list_round_trips_with_subdomains(tmp_path, monkeypatch):
288
258
  monkeypatch.setenv("EVO_ROUTE_HOME", str(tmp_path))
289
259
  _spoofdpi._write_hosts(["medium.com", "x.com", "medium.com"])
@@ -1 +0,0 @@
1
- 0.35.1
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