meshcode 1.8.2__tar.gz → 1.8.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.
- {meshcode-1.8.2 → meshcode-1.8.4}/PKG-INFO +1 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/__init__.py +1 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/comms_v4.py +25 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/server.py +1 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/preferences.py +75 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/run_agent.py +7 -0
- meshcode-1.8.4/meshcode/self_update.py +345 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/pyproject.toml +1 -1
- {meshcode-1.8.2 → meshcode-1.8.4}/README.md +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/cli.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/invites.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/launcher.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/launcher_install.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/protocol_v2.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/secrets.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode/setup_clients.py +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-1.8.2 → meshcode-1.8.4}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "1.8.
|
|
2
|
+
__version__ = "1.8.4"
|
|
@@ -2024,13 +2024,37 @@ if __name__ == "__main__":
|
|
|
2024
2024
|
|
|
2025
2025
|
elif cmd == "prefs":
|
|
2026
2026
|
# meshcode prefs permission-mode [bypass|safe|ask] (no arg = show)
|
|
2027
|
+
# meshcode prefs auto-update [on|off|reset] (no arg = show)
|
|
2027
2028
|
# meshcode prefs reset
|
|
2028
2029
|
from meshcode.preferences import (
|
|
2029
2030
|
get_permission_mode, set_permission_mode, reset_permission_mode,
|
|
2031
|
+
get_auto_update, set_auto_update, reset_auto_update,
|
|
2030
2032
|
VALID_PERMISSION_MODES,
|
|
2031
2033
|
)
|
|
2032
2034
|
sub = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
2033
|
-
if sub == "
|
|
2035
|
+
if sub == "auto-update":
|
|
2036
|
+
if len(sys.argv) > 3:
|
|
2037
|
+
arg = sys.argv[3].lower()
|
|
2038
|
+
if arg in ("on", "yes", "y", "true", "1"):
|
|
2039
|
+
ok = set_auto_update(True)
|
|
2040
|
+
print("[meshcode] auto_update = ON" if ok else "[ERROR] failed to save")
|
|
2041
|
+
elif arg in ("off", "no", "n", "false", "0"):
|
|
2042
|
+
ok = set_auto_update(False)
|
|
2043
|
+
print("[meshcode] auto_update = OFF" if ok else "[ERROR] failed to save")
|
|
2044
|
+
elif arg == "reset":
|
|
2045
|
+
reset_auto_update()
|
|
2046
|
+
print("[meshcode] auto_update preference cleared")
|
|
2047
|
+
ok = True
|
|
2048
|
+
else:
|
|
2049
|
+
print("Usage: meshcode prefs auto-update [on|off|reset]")
|
|
2050
|
+
sys.exit(1)
|
|
2051
|
+
sys.exit(0 if ok else 1)
|
|
2052
|
+
else:
|
|
2053
|
+
cur = get_auto_update()
|
|
2054
|
+
label = "(unset — will prompt on next run)" if cur is None else ("ON" if cur else "OFF")
|
|
2055
|
+
print(f"auto_update: {label}")
|
|
2056
|
+
sys.exit(0)
|
|
2057
|
+
elif sub == "permission-mode":
|
|
2034
2058
|
if len(sys.argv) > 3:
|
|
2035
2059
|
mode = sys.argv[3].lower()
|
|
2036
2060
|
if mode not in VALID_PERMISSION_MODES:
|
|
@@ -342,7 +342,7 @@ YOUR DEFAULT BEHAVIOR LOOP (do this without being asked):
|
|
|
342
342
|
|
|
343
343
|
6. GLOBAL DONE: when ANY agent (typically commander) calls
|
|
344
344
|
meshcode_done(reason), every other agent's meshcode_wait returns
|
|
345
|
-
with {got_done: true, reason}. Treat this as "task complete, exit
|
|
345
|
+
with {{got_done: true, reason}}. Treat this as "task complete, exit
|
|
346
346
|
the loop". Do NOT call meshcode_wait again. Return cleanly to the
|
|
347
347
|
user with a summary of what the mesh accomplished. Stay quiet
|
|
348
348
|
until the user gives a new task.
|
|
@@ -30,6 +30,9 @@ PREFS_PATH = Path.home() / ".meshcode" / "preferences.json"
|
|
|
30
30
|
VALID_PERMISSION_MODES = {"bypass", "safe", "ask"}
|
|
31
31
|
DEFAULT_PERMISSION_MODE_FOR_NON_TTY = "bypass"
|
|
32
32
|
|
|
33
|
+
# auto_update: True | False | None (unset → prompt on first interactive run)
|
|
34
|
+
DEFAULT_AUTO_UPDATE_FOR_NON_TTY = False
|
|
35
|
+
|
|
33
36
|
|
|
34
37
|
def load_prefs() -> Dict[str, Any]:
|
|
35
38
|
"""Load the prefs file. Returns {} if missing or unparseable."""
|
|
@@ -169,3 +172,75 @@ def prompt_permission_mode_quick() -> str:
|
|
|
169
172
|
if ans in ("s", "safe"):
|
|
170
173
|
return "safe"
|
|
171
174
|
return "bypass"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ============================================================
|
|
178
|
+
# Auto-update preferences
|
|
179
|
+
# ============================================================
|
|
180
|
+
|
|
181
|
+
def get_auto_update() -> Optional[bool]:
|
|
182
|
+
"""Returns True / False / None (unset)."""
|
|
183
|
+
val = load_prefs().get("auto_update")
|
|
184
|
+
if isinstance(val, bool):
|
|
185
|
+
return val
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def set_auto_update(enabled: bool) -> bool:
|
|
190
|
+
prefs = load_prefs()
|
|
191
|
+
prefs["auto_update"] = bool(enabled)
|
|
192
|
+
prefs["auto_update_set_at"] = int(time.time())
|
|
193
|
+
return save_prefs(prefs)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def reset_auto_update() -> bool:
|
|
197
|
+
prefs = load_prefs()
|
|
198
|
+
prefs.pop("auto_update", None)
|
|
199
|
+
prefs.pop("auto_update_set_at", None)
|
|
200
|
+
return save_prefs(prefs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def prompt_auto_update() -> bool:
|
|
204
|
+
"""Interactive first-run prompt. Returns the chosen value and saves it.
|
|
205
|
+
|
|
206
|
+
Non-TTY: silently picks DEFAULT_AUTO_UPDATE_FOR_NON_TTY (False) and saves.
|
|
207
|
+
"""
|
|
208
|
+
if not sys.stdin.isatty():
|
|
209
|
+
set_auto_update(DEFAULT_AUTO_UPDATE_FOR_NON_TTY)
|
|
210
|
+
return DEFAULT_AUTO_UPDATE_FOR_NON_TTY
|
|
211
|
+
|
|
212
|
+
print("", file=sys.stderr)
|
|
213
|
+
print("[meshcode] AUTO-UPDATE — meshcode is iterating fast right now.", file=sys.stderr)
|
|
214
|
+
print("[meshcode]", file=sys.stderr)
|
|
215
|
+
print("[meshcode] When a new version is published to PyPI, meshcode can", file=sys.stderr)
|
|
216
|
+
print("[meshcode] pull it in the background (no interruption to your", file=sys.stderr)
|
|
217
|
+
print("[meshcode] current launch — the next launch picks it up).", file=sys.stderr)
|
|
218
|
+
print("[meshcode]", file=sys.stderr)
|
|
219
|
+
print("[meshcode] [Y] Yes (recommended) — silent background pip install -U", file=sys.stderr)
|
|
220
|
+
print("[meshcode] [n] No — I'll run pip install -U meshcode myself", file=sys.stderr)
|
|
221
|
+
print("[meshcode]", file=sys.stderr)
|
|
222
|
+
try:
|
|
223
|
+
ans = input("[meshcode] Pick [Y/n] (default Y): ").strip().lower()
|
|
224
|
+
except (EOFError, KeyboardInterrupt):
|
|
225
|
+
ans = ""
|
|
226
|
+
|
|
227
|
+
chosen = False if ans in ("n", "no") else True
|
|
228
|
+
set_auto_update(chosen)
|
|
229
|
+
label = "ON" if chosen else "OFF"
|
|
230
|
+
print(f"[meshcode] ✓ Auto-update {label}. Change later with: meshcode prefs auto-update <on|off>", file=sys.stderr)
|
|
231
|
+
print("", file=sys.stderr)
|
|
232
|
+
return chosen
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def resolve_auto_update() -> bool:
|
|
236
|
+
"""Returns True if auto-update should run for this launch.
|
|
237
|
+
|
|
238
|
+
Order:
|
|
239
|
+
1. saved preference
|
|
240
|
+
2. interactive prompt + save (TTY only)
|
|
241
|
+
3. fallback False for non-TTY
|
|
242
|
+
"""
|
|
243
|
+
saved = get_auto_update()
|
|
244
|
+
if saved is not None:
|
|
245
|
+
return saved
|
|
246
|
+
return prompt_auto_update()
|
|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
from typing import Optional, Tuple
|
|
27
27
|
|
|
28
28
|
from .preferences import resolve_permission_mode
|
|
29
|
+
from . import self_update
|
|
29
30
|
|
|
30
31
|
WORKSPACES_ROOT = Path.home() / "meshcode"
|
|
31
32
|
REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
|
|
@@ -101,6 +102,12 @@ def _detect_editor() -> Optional[str]:
|
|
|
101
102
|
|
|
102
103
|
def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None, permission_override: Optional[str] = None) -> int:
|
|
103
104
|
"""Launch the user's editor with ONLY the named agent's MCP server loaded."""
|
|
105
|
+
# Non-blocking self-update check (consumes prior result, may spawn bg pip)
|
|
106
|
+
try:
|
|
107
|
+
self_update.check_and_maybe_update()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
104
111
|
found = _find_agent_workspace(agent, project)
|
|
105
112
|
if not found:
|
|
106
113
|
return 2
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Background self-update for the meshcode CLI.
|
|
2
|
+
|
|
3
|
+
Design goals:
|
|
4
|
+
- ZERO added latency to `meshcode run`. The PyPI check + pip install
|
|
5
|
+
runs in a fully detached background subprocess. The current launch
|
|
6
|
+
is never blocked.
|
|
7
|
+
- Opt-in (first-run prompt, like permission_mode). Default ON for
|
|
8
|
+
interactive TTY users, default OFF for CI / non-TTY.
|
|
9
|
+
- Cache the PyPI check for 1 hour so we don't hit the network on
|
|
10
|
+
every launch.
|
|
11
|
+
- Two-launch model: launch N detects + downloads in the background;
|
|
12
|
+
launch N+1 reads the result file, prints "[meshcode] updated X → Y",
|
|
13
|
+
and the new code is already on disk.
|
|
14
|
+
- Safe in all the edge cases that bite Python CLI updaters:
|
|
15
|
+
* editable installs (dev mode) → skip
|
|
16
|
+
* pipx installs → use `pipx upgrade meshcode`
|
|
17
|
+
* MCP server subprocess → skip (we're inside an agent runtime)
|
|
18
|
+
* --no-update flag, MESHCODE_NO_UPDATE=1 env → skip
|
|
19
|
+
* offline / network error → silent skip
|
|
20
|
+
* concurrent runs → file lock so only one bg update at a time
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional, Tuple
|
|
31
|
+
|
|
32
|
+
PKG_NAME = "meshcode"
|
|
33
|
+
PYPI_URL = f"https://pypi.org/pypi/{PKG_NAME}/json"
|
|
34
|
+
CACHE_TTL_SEC = 3600 # 1 hour
|
|
35
|
+
NETWORK_TIMEOUT_SEC = 1.5
|
|
36
|
+
|
|
37
|
+
STATE_DIR = Path.home() / ".meshcode"
|
|
38
|
+
RESULT_PATH = STATE_DIR / ".update_result.json"
|
|
39
|
+
LOCK_PATH = STATE_DIR / ".update.lock"
|
|
40
|
+
LOG_PATH = STATE_DIR / "update.log"
|
|
41
|
+
LOCK_STALE_SEC = 600 # 10 min — if a lock is older, treat as crashed
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================
|
|
45
|
+
# Detection helpers — when NOT to auto-update
|
|
46
|
+
# ============================================================
|
|
47
|
+
|
|
48
|
+
def _current_version() -> str:
|
|
49
|
+
try:
|
|
50
|
+
from . import __version__
|
|
51
|
+
return __version__
|
|
52
|
+
except Exception:
|
|
53
|
+
return "0.0.0"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_editable_install() -> bool:
|
|
57
|
+
"""True if meshcode is installed via `pip install -e .` (dev mode)."""
|
|
58
|
+
try:
|
|
59
|
+
import meshcode
|
|
60
|
+
path = os.path.realpath(meshcode.__file__)
|
|
61
|
+
return "site-packages" not in path and "dist-packages" not in path
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_inside_mcp_serve() -> bool:
|
|
67
|
+
"""True if we're running inside the MCP subprocess of an agent client."""
|
|
68
|
+
if os.environ.get("MESHCODE_MCP_SERVE") == "1":
|
|
69
|
+
return True
|
|
70
|
+
argv0 = " ".join(sys.argv).lower()
|
|
71
|
+
return "meshcode_mcp" in argv0 or "meshcode.meshcode_mcp" in argv0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_pipx_install() -> bool:
|
|
75
|
+
"""True if installed via pipx (we should use `pipx upgrade` instead)."""
|
|
76
|
+
exe = os.path.realpath(sys.executable)
|
|
77
|
+
return "/pipx/" in exe or "\\pipx\\" in exe
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_disabled() -> bool:
|
|
81
|
+
"""User explicitly opted out for this run / globally."""
|
|
82
|
+
if os.environ.get("MESHCODE_NO_UPDATE") == "1":
|
|
83
|
+
return True
|
|
84
|
+
if "--no-update" in sys.argv:
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _version_tuple(v: str) -> Tuple[int, ...]:
|
|
90
|
+
parts = []
|
|
91
|
+
for p in v.split("."):
|
|
92
|
+
digits = "".join(c for c in p if c.isdigit())
|
|
93
|
+
parts.append(int(digits) if digits else 0)
|
|
94
|
+
return tuple(parts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_newer(remote: str, local: str) -> bool:
|
|
98
|
+
try:
|
|
99
|
+
return _version_tuple(remote) > _version_tuple(local)
|
|
100
|
+
except Exception:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================
|
|
105
|
+
# Cache + result file (consumed by next launch)
|
|
106
|
+
# ============================================================
|
|
107
|
+
|
|
108
|
+
def _read_prefs() -> dict:
|
|
109
|
+
try:
|
|
110
|
+
from .preferences import load_prefs
|
|
111
|
+
return load_prefs()
|
|
112
|
+
except Exception:
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _write_prefs_kv(**kv) -> None:
|
|
117
|
+
try:
|
|
118
|
+
from .preferences import load_prefs, save_prefs
|
|
119
|
+
prefs = load_prefs()
|
|
120
|
+
prefs.update(kv)
|
|
121
|
+
save_prefs(prefs)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cache_fresh() -> bool:
|
|
127
|
+
last = _read_prefs().get("last_update_check_at", 0)
|
|
128
|
+
return (time.time() - last) < CACHE_TTL_SEC
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _mark_checked(latest: Optional[str]) -> None:
|
|
132
|
+
_write_prefs_kv(
|
|
133
|
+
last_update_check_at=int(time.time()),
|
|
134
|
+
last_known_latest_version=latest or _current_version(),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def consume_pending_result() -> None:
|
|
139
|
+
"""Read + delete .update_result.json, print outcome to stderr.
|
|
140
|
+
|
|
141
|
+
Called at the START of each `meshcode run` to surface what the
|
|
142
|
+
previous launch's background updater did.
|
|
143
|
+
"""
|
|
144
|
+
if not RESULT_PATH.exists():
|
|
145
|
+
return
|
|
146
|
+
try:
|
|
147
|
+
data = json.loads(RESULT_PATH.read_text())
|
|
148
|
+
RESULT_PATH.unlink(missing_ok=True)
|
|
149
|
+
except Exception:
|
|
150
|
+
return
|
|
151
|
+
cur = _current_version()
|
|
152
|
+
new_v = data.get("version") or "?"
|
|
153
|
+
if data.get("ok"):
|
|
154
|
+
if _is_newer(new_v, cur):
|
|
155
|
+
print(f"[meshcode] updated {cur} → {new_v}. Restart your editor to load the new version.", file=sys.stderr)
|
|
156
|
+
else:
|
|
157
|
+
# update ran but pip didn't actually upgrade (already at latest)
|
|
158
|
+
pass
|
|
159
|
+
else:
|
|
160
|
+
err = data.get("error", "unknown error")
|
|
161
|
+
print(f"[meshcode] WARN: last auto-update failed: {err}", file=sys.stderr)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ============================================================
|
|
165
|
+
# Network — PyPI version probe
|
|
166
|
+
# ============================================================
|
|
167
|
+
|
|
168
|
+
def fetch_latest_version(timeout: float = NETWORK_TIMEOUT_SEC) -> Optional[str]:
|
|
169
|
+
try:
|
|
170
|
+
import urllib.request
|
|
171
|
+
req = urllib.request.Request(PYPI_URL, headers={"User-Agent": f"meshcode/{_current_version()}"})
|
|
172
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
173
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
174
|
+
return data.get("info", {}).get("version")
|
|
175
|
+
except Exception:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ============================================================
|
|
180
|
+
# File lock — prevent concurrent bg updates
|
|
181
|
+
# ============================================================
|
|
182
|
+
|
|
183
|
+
def _acquire_lock() -> bool:
|
|
184
|
+
try:
|
|
185
|
+
if LOCK_PATH.exists():
|
|
186
|
+
age = time.time() - LOCK_PATH.stat().st_mtime
|
|
187
|
+
if age < LOCK_STALE_SEC:
|
|
188
|
+
return False
|
|
189
|
+
LOCK_PATH.unlink(missing_ok=True)
|
|
190
|
+
LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
LOCK_PATH.write_text(str(os.getpid()))
|
|
192
|
+
return True
|
|
193
|
+
except Exception:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ============================================================
|
|
198
|
+
# Background update spawn
|
|
199
|
+
# ============================================================
|
|
200
|
+
|
|
201
|
+
_BG_RUNNER_PY = r'''
|
|
202
|
+
import json, os, subprocess, sys, time
|
|
203
|
+
from pathlib import Path
|
|
204
|
+
|
|
205
|
+
result = {"version": None, "ok": False, "error": None, "finished_at": None}
|
|
206
|
+
state_dir = Path.home() / ".meshcode"
|
|
207
|
+
result_path = state_dir / ".update_result.json"
|
|
208
|
+
lock_path = state_dir / ".update.lock"
|
|
209
|
+
log_path = state_dir / "update.log"
|
|
210
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
|
|
212
|
+
mode = sys.argv[1] if len(sys.argv) > 1 else "pip"
|
|
213
|
+
target_version = sys.argv[2] if len(sys.argv) > 2 else None
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
if mode == "pipx":
|
|
217
|
+
cmd = ["pipx", "upgrade", "meshcode"]
|
|
218
|
+
else:
|
|
219
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U", "--disable-pip-version-check", "--quiet", "meshcode"]
|
|
220
|
+
with open(log_path, "ab") as logf:
|
|
221
|
+
logf.write(f"\n=== {time.strftime('%Y-%m-%d %H:%M:%S')} bg update via {mode} ===\n".encode())
|
|
222
|
+
logf.flush()
|
|
223
|
+
proc = subprocess.run(cmd, stdout=logf, stderr=logf, timeout=180)
|
|
224
|
+
if proc.returncode == 0:
|
|
225
|
+
result["ok"] = True
|
|
226
|
+
result["version"] = target_version
|
|
227
|
+
else:
|
|
228
|
+
result["error"] = f"pip exit {proc.returncode}"
|
|
229
|
+
except subprocess.TimeoutExpired:
|
|
230
|
+
result["error"] = "pip install timed out after 180s"
|
|
231
|
+
except Exception as e:
|
|
232
|
+
result["error"] = str(e)
|
|
233
|
+
finally:
|
|
234
|
+
result["finished_at"] = int(time.time())
|
|
235
|
+
try:
|
|
236
|
+
result_path.write_text(json.dumps(result))
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
try:
|
|
240
|
+
lock_path.unlink()
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
'''
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _spawn_background_updater(target_version: str) -> bool:
|
|
247
|
+
"""Spawn a fully detached subprocess that runs the updater.
|
|
248
|
+
|
|
249
|
+
The parent (current `meshcode run`) returns immediately. The child
|
|
250
|
+
runs pip install in the background, writes the result to disk, and
|
|
251
|
+
exits. Next `meshcode run` consumes the result.
|
|
252
|
+
"""
|
|
253
|
+
if not _acquire_lock():
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
mode = "pipx" if is_pipx_install() else "pip"
|
|
257
|
+
|
|
258
|
+
# We pass the runner code via stdin so we don't need to ship a
|
|
259
|
+
# second .py file. The child reads it from sys.stdin and execs it.
|
|
260
|
+
runner = f"import sys; exec(sys.stdin.read())"
|
|
261
|
+
args = [sys.executable, "-c", runner, mode, target_version]
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
if sys.platform == "win32":
|
|
265
|
+
DETACHED_PROCESS = 0x00000008
|
|
266
|
+
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
267
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
268
|
+
flags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
|
269
|
+
proc = subprocess.Popen(
|
|
270
|
+
args,
|
|
271
|
+
stdin=subprocess.PIPE,
|
|
272
|
+
stdout=subprocess.DEVNULL,
|
|
273
|
+
stderr=subprocess.DEVNULL,
|
|
274
|
+
creationflags=flags,
|
|
275
|
+
close_fds=True,
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
proc = subprocess.Popen(
|
|
279
|
+
args,
|
|
280
|
+
stdin=subprocess.PIPE,
|
|
281
|
+
stdout=subprocess.DEVNULL,
|
|
282
|
+
stderr=subprocess.DEVNULL,
|
|
283
|
+
start_new_session=True,
|
|
284
|
+
close_fds=True,
|
|
285
|
+
)
|
|
286
|
+
if proc.stdin:
|
|
287
|
+
proc.stdin.write(_BG_RUNNER_PY.encode("utf-8"))
|
|
288
|
+
proc.stdin.close()
|
|
289
|
+
return True
|
|
290
|
+
except Exception:
|
|
291
|
+
try:
|
|
292
|
+
LOCK_PATH.unlink(missing_ok=True)
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ============================================================
|
|
299
|
+
# Public entrypoint — called from meshcode run
|
|
300
|
+
# ============================================================
|
|
301
|
+
|
|
302
|
+
def check_and_maybe_update(verbose: bool = False) -> None:
|
|
303
|
+
"""Non-blocking. Call at the start of `meshcode run`.
|
|
304
|
+
|
|
305
|
+
Order of checks (any failure → silent return, never raise):
|
|
306
|
+
1. consume any pending result from previous launch
|
|
307
|
+
2. opt-out gates: --no-update, MESHCODE_NO_UPDATE=1
|
|
308
|
+
3. context gates: editable install, MCP subprocess
|
|
309
|
+
4. user preference: resolve_auto_update() (may prompt first time)
|
|
310
|
+
5. cache TTL: skip if checked < 1h ago
|
|
311
|
+
6. PyPI fetch (1.5s timeout)
|
|
312
|
+
7. version compare; if newer → spawn background updater
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
consume_pending_result()
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
if update_disabled():
|
|
320
|
+
return
|
|
321
|
+
if is_inside_mcp_serve():
|
|
322
|
+
return
|
|
323
|
+
if is_editable_install():
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
from .preferences import resolve_auto_update
|
|
328
|
+
if not resolve_auto_update():
|
|
329
|
+
return
|
|
330
|
+
except Exception:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
if _cache_fresh():
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
latest = fetch_latest_version()
|
|
337
|
+
_mark_checked(latest)
|
|
338
|
+
if not latest:
|
|
339
|
+
return
|
|
340
|
+
if not _is_newer(latest, _current_version()):
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
started = _spawn_background_updater(latest)
|
|
344
|
+
if started and verbose:
|
|
345
|
+
print(f"[meshcode] downloading {latest} in background...", file=sys.stderr)
|
|
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
|