witbitz-code 1.7.2__tar.gz → 1.7.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.
- {witbitz_code-1.7.2/src/witbitz_code.egg-info → witbitz_code-1.7.4}/PKG-INFO +1 -1
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/pyproject.toml +1 -1
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/confidential.py +5 -1
- witbitz_code-1.7.4/src/witbitz_code/selfupdate.py +120 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/ui.py +38 -6
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/ui_static/app.js +28 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/ui_static/index.html +7 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code.egg-info/SOURCES.txt +2 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_confidential.py +15 -1
- witbitz_code-1.7.4/tests/test_selfupdate.py +94 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_ui.py +31 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/LICENSE +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/README.md +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/setup.cfg +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_asks.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_attachments.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_auto.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_cli.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_codetools.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_dirs.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_folders.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_images.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_outputs.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_plugins.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_policy.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_ports.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_registry.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_remind.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_setup.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_uitools.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_winagent.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_winbg.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.7.2 → witbitz_code-1.7.4}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.7.
|
|
7
|
+
version = "1.7.4"
|
|
8
8
|
description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -627,7 +627,11 @@ class ConfidentialProxy:
|
|
|
627
627
|
hold = attempt["n"] > 1 or (ends is not None and self._now() / 1000 >= ends - HOLD_BEFORE_LAPSE_SEC)
|
|
628
628
|
n = self._nonce()
|
|
629
629
|
raw = dumps(stamp_floor(body)).encode("utf-8")
|
|
630
|
-
|
|
630
|
+
# the forwarded headers WITHOUT their own content-type / x-inference-receipt, whatever their case: OpenCode sends
|
|
631
|
+
# "Content-Type", and two of them made TrustedRouter refuse every confidential call (2026-10-17: a small one 400
|
|
632
|
+
# "could not read request"; a large one closed mid-upload — an empty httpx ReadError, "could not reach TrustedRouter ()")
|
|
633
|
+
fwd = {k: v for k, v in _forward_headers(req.headers).items() if k.lower() not in ("content-type", "x-inference-receipt")}
|
|
634
|
+
headers = {**fwd, "content-type": "application/json", "x-inference-receipt": n}
|
|
631
635
|
tell("waiting", bytes=len(raw))
|
|
632
636
|
async with self.client.stream("POST", target, headers=headers, content=raw) as up:
|
|
633
637
|
if not up.is_success:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Update witbitz-code from its page (the owner, 2026-10-17: a fix on PyPI reached no computer that already had it).
|
|
2
|
+
|
|
3
|
+
The version to run is the one the Windows installer pins (`$WitbitzCode` in https://app.witbitz.chat/code.ps1): that
|
|
4
|
+
installer is served from the OFFICIAL release, so a computer moves when Witbitz releases — never to an upload that has
|
|
5
|
+
not been released, and the same version for a Windows, WSL, Mac or Linux install.
|
|
6
|
+
|
|
7
|
+
How it updates follows how it was installed (uninstall.pipx_install):
|
|
8
|
+
· the Windows installer → run that installer again in a window you can see: it stops this copy, installs the pinned
|
|
9
|
+
version and opens the page again (code.ps1 does all of it already);
|
|
10
|
+
· pipx → `pipx install --force witbitz-code==<v>`, then this manager starts itself again (ui.run re-executes);
|
|
11
|
+
· anything else → the command to run, shown on the page.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Awaitable, Callable
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from . import __version__, uninstall
|
|
26
|
+
|
|
27
|
+
INSTALLER_URL = "https://app.witbitz.chat/code.ps1"
|
|
28
|
+
CHECK_EVERY_S = 6 * 3600
|
|
29
|
+
_PIN = re.compile(r"""^\s*\$WitbitzCode\s*=\s*'(\d{1,4}\.\d{1,4}\.\d{1,4})'\s*$""", re.M)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_pin(script: str) -> str | None:
|
|
33
|
+
"""The version code.ps1 installs — None when the script does not say it the one way it does."""
|
|
34
|
+
m = _PIN.search(script or "")
|
|
35
|
+
return m.group(1) if m else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def newer(a: str, b: str) -> bool:
|
|
39
|
+
"""Is version `a` newer than `b`? (x.y.z only; anything else is never newer.)"""
|
|
40
|
+
try:
|
|
41
|
+
return tuple(int(x) for x in a.split(".")) > tuple(int(x) for x in b.split("."))
|
|
42
|
+
except (ValueError, AttributeError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def how(info: dict, version: str) -> dict:
|
|
47
|
+
"""What the Update button does here → {"kind": "installer"|"pipx"|"manual", "command"?}."""
|
|
48
|
+
kind = info.get("kind")
|
|
49
|
+
if kind == "windows-installer":
|
|
50
|
+
return {"kind": "installer"}
|
|
51
|
+
if kind == "pipx" and shutil.which("pipx"):
|
|
52
|
+
return {"kind": "pipx"}
|
|
53
|
+
py = os.path.basename(sys.executable) or "python3"
|
|
54
|
+
return {"kind": "manual", "command": f"{py} -m pip install --upgrade witbitz-code=={version}"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Updates:
|
|
58
|
+
"""The page's update row. `fetch_text(url)` → the installer's text; `run` and `launch` are injected for tests."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, *, fetch_text: Callable[[str], Awaitable[str]], install_info: Callable[[], dict] = uninstall.pipx_install,
|
|
61
|
+
run: Callable[[list], Any] | None = None, launch: Callable[[list], Any] | None = None,
|
|
62
|
+
current: str = __version__, now: Callable[[], float] = time.monotonic) -> None:
|
|
63
|
+
self._fetch, self._info, self._now = fetch_text, install_info, now
|
|
64
|
+
self._run = run or (lambda argv: subprocess.run(argv, capture_output=True, text=True, check=False))
|
|
65
|
+
self._launch = launch or _launch_visible
|
|
66
|
+
self.current = current
|
|
67
|
+
self.latest: str | None = None
|
|
68
|
+
self.checked_at = -1e18
|
|
69
|
+
self.why = ""
|
|
70
|
+
self.restart = False # set after a pipx update: ui.run starts this manager again on its way out
|
|
71
|
+
|
|
72
|
+
async def check(self, force: bool = False) -> None:
|
|
73
|
+
if not force and self._now() - self.checked_at < CHECK_EVERY_S:
|
|
74
|
+
return
|
|
75
|
+
self.checked_at = self._now()
|
|
76
|
+
try:
|
|
77
|
+
v = parse_pin(await self._fetch(INSTALLER_URL))
|
|
78
|
+
if v:
|
|
79
|
+
self.latest, self.why = v, ""
|
|
80
|
+
else:
|
|
81
|
+
self.why = "could not read the released version"
|
|
82
|
+
except Exception as e: # noqa: BLE001 — offline is not a failure of the page
|
|
83
|
+
self.why = f"could not check for updates ({type(e).__name__})"
|
|
84
|
+
|
|
85
|
+
def status(self) -> dict:
|
|
86
|
+
avail = bool(self.latest and newer(self.latest, self.current))
|
|
87
|
+
out: dict = {"current": self.current, "latest": self.latest, "available": avail}
|
|
88
|
+
if avail:
|
|
89
|
+
out.update(how(self._info(), self.latest or ""))
|
|
90
|
+
if self.why:
|
|
91
|
+
out["why"] = self.why
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
async def update(self) -> dict:
|
|
95
|
+
await self.check(force=True)
|
|
96
|
+
st = self.status()
|
|
97
|
+
if not st["available"]:
|
|
98
|
+
return {"ok": False, "why": self.why or f"witbitz-code {self.current} is the released version"}
|
|
99
|
+
v = st["latest"]
|
|
100
|
+
if st["kind"] == "installer":
|
|
101
|
+
self._launch(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", f"irm {INSTALLER_URL} | iex"])
|
|
102
|
+
return {"ok": True, "kind": "installer", "message": f"The installer is updating witbitz-code to {v} in its own window; this page opens again when it is done."}
|
|
103
|
+
if st["kind"] == "pipx":
|
|
104
|
+
r = self._run(["pipx", "install", "--force", f"witbitz-code=={v}"])
|
|
105
|
+
if getattr(r, "returncode", 1) != 0:
|
|
106
|
+
return {"ok": False, "why": ("pipx: " + ((getattr(r, "stderr", "") or getattr(r, "stdout", "") or "").strip().splitlines() or ["failed"])[-1])[:200]}
|
|
107
|
+
self.restart = True
|
|
108
|
+
return {"ok": True, "kind": "pipx", "message": f"Updated to {v} — witbitz-code starts again in a moment."}
|
|
109
|
+
return {"ok": False, "kind": "manual", "why": "Run this, then start witbitz-code again: " + st["command"]}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _launch_visible(argv: list) -> None:
|
|
113
|
+
"""Windows: a console window of its own, so the person sees the installer work (the manager itself has no window)."""
|
|
114
|
+
flags = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
|
|
115
|
+
subprocess.Popen(argv, creationflags=flags, cwd=os.path.expanduser("~")) # noqa: S603 — fixed argv, our own installer
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def restart_argv() -> list:
|
|
119
|
+
"""The same manager again, on the freshly installed code: `python -m witbitz_code <the same arguments>`."""
|
|
120
|
+
return [sys.executable, "-m", "witbitz_code", *sys.argv[1:]]
|
|
@@ -35,7 +35,7 @@ from importlib import resources
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
from typing import Any, Awaitable, Callable
|
|
37
37
|
|
|
38
|
-
from . import __version__, devicelink, link, net, ports, qr, seccheck, uitools, uninstall, winbg, wizard, wslstart
|
|
38
|
+
from . import __version__, devicelink, link, net, ports, qr, seccheck, selfupdate, uitools, uninstall, winbg, wizard, wslstart
|
|
39
39
|
from . import pair as pair_cmd
|
|
40
40
|
from . import pairings as pp
|
|
41
41
|
|
|
@@ -128,11 +128,12 @@ class Manager:
|
|
|
128
128
|
login_disable: Callable[[dict], bool] = wslstart.startup_disable,
|
|
129
129
|
port_owners: Callable[[int], list] | None = None, stop_pid: Callable[[int], bool] | None = None,
|
|
130
130
|
tools: uitools.Tools | None = None, label: dict | None = None, log: deque | None = None,
|
|
131
|
-
security: seccheck.SecurityCheck | None = None) -> None:
|
|
131
|
+
security: seccheck.SecurityCheck | None = None, updates: selfupdate.Updates | None = None) -> None:
|
|
132
132
|
self._serve, self._is_listening, self._pair, self._link = serve, is_listening, pair, link_node
|
|
133
133
|
self._load, self._keys_line = load_pairings, keys_line
|
|
134
134
|
self.tools = tools or uitools.Tools()
|
|
135
135
|
self.security = security or seccheck.SecurityCheck()
|
|
136
|
+
self.updates = updates or selfupdate.Updates(fetch_text=_fetch_text)
|
|
136
137
|
self._owners = port_owners or ports.port_owners
|
|
137
138
|
self._stop_pid = stop_pid or uninstall.stop_process
|
|
138
139
|
self._elsewhere_cache: tuple[float, dict | None] | None = None
|
|
@@ -214,7 +215,8 @@ class Manager:
|
|
|
214
215
|
el = await self.elsewhere()
|
|
215
216
|
if el:
|
|
216
217
|
serving = {"state": "elsewhere", "port": el["port"], "owners": el["owners"]}
|
|
217
|
-
|
|
218
|
+
asyncio.ensure_future(self.updates.check()) # at most every few hours (selfupdate.CHECK_EVERY_S); never waited for
|
|
219
|
+
return {"version": __version__, "update": self.updates.status(), "computer": self.label, "paired": paired, "serving": serving, "pair": pair,
|
|
218
220
|
"login": self.login(), "busy": self.busy, "background": self.background,
|
|
219
221
|
"tools": await asyncio.to_thread(self.tools.status), "security": self.security.peek(), "log": list(self.log)[-14:]}
|
|
220
222
|
|
|
@@ -398,6 +400,14 @@ class Manager:
|
|
|
398
400
|
asyncio.get_running_loop().call_later(0.3, self.quit_event.set) # after this answer reaches the page
|
|
399
401
|
return {"ok": True}
|
|
400
402
|
|
|
403
|
+
async def update(self) -> dict:
|
|
404
|
+
"""The page's Update: the released version (selfupdate.py). After a pipx update this manager stops and ui.run
|
|
405
|
+
starts it again on the new code; the Windows installer stops and reopens it itself."""
|
|
406
|
+
out = await self.updates.update()
|
|
407
|
+
if out.get("ok") and self.updates.restart:
|
|
408
|
+
await self.quit()
|
|
409
|
+
return out
|
|
410
|
+
|
|
401
411
|
async def shutdown(self) -> None:
|
|
402
412
|
if self.pair_task and not self.pair_task.done():
|
|
403
413
|
self.pair_task.cancel()
|
|
@@ -536,6 +546,7 @@ class Server:
|
|
|
536
546
|
"/api/unpair": lambda: self.m.unpair(str(data.get("account") or "")),
|
|
537
547
|
"/api/login": lambda: self.m.set_login(bool(data.get("on"))),
|
|
538
548
|
"/api/quit": self.m.quit,
|
|
549
|
+
"/api/update": self.m.update,
|
|
539
550
|
"/api/tools/install": self.m.tools.install,
|
|
540
551
|
"/api/security/fix": lambda: self.m.security.fix(str(data.get("id") or "")),
|
|
541
552
|
}
|
|
@@ -556,6 +567,22 @@ class Server:
|
|
|
556
567
|
|
|
557
568
|
|
|
558
569
|
# ── the command ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
570
|
+
async def _fetch_text(url: str) -> str:
|
|
571
|
+
async with net.new_client() as c:
|
|
572
|
+
r = await c.get(url, headers={"cache-control": "no-store"}, timeout=20)
|
|
573
|
+
r.raise_for_status()
|
|
574
|
+
return r.text
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
RESUME_ENV = "WITBITZ_UI_RESUME" # "<port>:<token>" — a restart after an update keeps the page's address and its sign-in
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _resume() -> tuple[int, str] | None:
|
|
581
|
+
v = os.environ.pop(RESUME_ENV, "")
|
|
582
|
+
port, _, token = v.partition(":")
|
|
583
|
+
return (int(port), token) if port.isdigit() and len(token) >= 16 else None
|
|
584
|
+
|
|
585
|
+
|
|
559
586
|
def _read_lock() -> dict:
|
|
560
587
|
try:
|
|
561
588
|
d = json.loads(lock_path().read_text(encoding="utf-8"))
|
|
@@ -599,16 +626,17 @@ async def run(args: list[str], *, serve: Callable[..., Awaitable[int]], is_liste
|
|
|
599
626
|
m.background = background
|
|
600
627
|
stop = asyncio.Event()
|
|
601
628
|
m.quit_event = stop
|
|
602
|
-
|
|
629
|
+
resume = _resume()
|
|
630
|
+
srv = Server(m, token=resume[1] if resume else None)
|
|
603
631
|
try:
|
|
604
|
-
await srv.start()
|
|
632
|
+
await srv.start(*([resume[0]] if resume else []))
|
|
605
633
|
pp.write_private(lock_path(), json.dumps({"port": srv.port, "token": srv.token, "pid": os.getpid()}) + "\n")
|
|
606
634
|
print(f"witbitz-code: {m.label['label']} — open {srv.url}", file=sys.stderr, flush=True)
|
|
607
635
|
print("witbitz-code: running in the background — Quit on the page stops it" if background else
|
|
608
636
|
"witbitz-code: keep this window open (closing it stops witbitz-code). Ctrl-C stops it.", file=sys.stderr, flush=True)
|
|
609
637
|
if m.pairings():
|
|
610
638
|
await m.start_serving()
|
|
611
|
-
if not no_browser and not open_browser(srv.url):
|
|
639
|
+
if not resume and not no_browser and not open_browser(srv.url):
|
|
612
640
|
print("witbitz-code: open the address above in your browser", file=sys.stderr, flush=True)
|
|
613
641
|
loop = asyncio.get_running_loop()
|
|
614
642
|
import signal # noqa: PLC0415
|
|
@@ -628,3 +656,7 @@ async def run(args: list[str], *, serve: Callable[..., Awaitable[int]], is_liste
|
|
|
628
656
|
except OSError:
|
|
629
657
|
pass
|
|
630
658
|
sys.stderr = old_err
|
|
659
|
+
if m.updates.restart: # updated by pipx: the same manager again, on the new code, at the same page address
|
|
660
|
+
os.environ[RESUME_ENV] = f"{srv.port}:{srv.token}"
|
|
661
|
+
print("witbitz-code: updated — starting again", file=sys.stderr, flush=True)
|
|
662
|
+
os.execv(sys.executable, selfupdate.restart_argv())
|
|
@@ -96,6 +96,7 @@ function render(s) {
|
|
|
96
96
|
// Windows, no window (winbg.py): nothing to keep open — Quit is how it stops
|
|
97
97
|
$('footerText').textContent = s.background ? 'runs in the background — Start menu → Witbitz Code opens this page' : 'keep its window open — closing it stops witbitz-code'
|
|
98
98
|
show($('quit'), !!s.background)
|
|
99
|
+
renderUpdate(s.update)
|
|
99
100
|
renderTools(s.tools, paired)
|
|
100
101
|
renderSecurity(s.security)
|
|
101
102
|
|
|
@@ -139,6 +140,32 @@ function accountOf() {
|
|
|
139
140
|
return pick || null
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// A released version newer than this one (selfupdate.py): the version the installer pins. Update runs that installer
|
|
144
|
+
// again on Windows, pipx elsewhere; anything else shows the command.
|
|
145
|
+
let updating = '', updatingFrom = ''
|
|
146
|
+
function renderUpdate(u) {
|
|
147
|
+
const card = $('updateCard')
|
|
148
|
+
if (updating && u && u.current !== updatingFrom) { updating = ''; note(`Updated to ${u.current} ✓`, 'ok') }
|
|
149
|
+
if (updating) { show(card, true); show($('update'), false); $('updateText').textContent = updating; return }
|
|
150
|
+
if (!u || !u.available) return show(card, false)
|
|
151
|
+
show(card, true)
|
|
152
|
+
$('updateTitle').textContent = `witbitz-code ${u.latest} is ready`
|
|
153
|
+
$('updateText').textContent = `This computer runs ${u.current}.` + (u.kind === 'manual' ? ' Run this in a terminal, then start witbitz-code again:' : ' Update installs it and starts witbitz-code again — Code in Spaces reconnects by itself.')
|
|
154
|
+
show($('updateCmd'), u.kind === 'manual')
|
|
155
|
+
$('updateCmd').textContent = u.command || ''
|
|
156
|
+
show($('update'), u.kind !== 'manual')
|
|
157
|
+
$('update').disabled = !!state.busy
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function update() {
|
|
161
|
+
$('update').disabled = true
|
|
162
|
+
try {
|
|
163
|
+
const r = await api('/api/update', {})
|
|
164
|
+
if (r && r.ok) { updatingFrom = state.version; updating = r.message || 'Updating…'; note(updating, 'ok') } else note((r && r.why) || 'The update did not run.', 'bad')
|
|
165
|
+
} catch (e) { note(e.message, 'bad') }
|
|
166
|
+
await refresh()
|
|
167
|
+
}
|
|
168
|
+
|
|
142
169
|
// setup's Tools step, on the page (uitools.py): what is missing, what Install runs, its progress.
|
|
143
170
|
function renderTools(t, paired) {
|
|
144
171
|
if (!t) return show($('toolsCard'), false)
|
|
@@ -213,6 +240,7 @@ async function quit() {
|
|
|
213
240
|
|
|
214
241
|
document.addEventListener('DOMContentLoaded', () => {
|
|
215
242
|
$('quit').onclick = quit
|
|
243
|
+
$('update').onclick = update
|
|
216
244
|
$('toolsInstall').onclick = () => act('/api/tools/install', {}, 'Install them now? Windows may ask once to allow an installer.')
|
|
217
245
|
$('pair').onclick = () => act('/api/pair', {})
|
|
218
246
|
$('start').onclick = () => act('/api/serve', { on: true })
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
<p id="note" class="note" hidden></p>
|
|
31
31
|
</section>
|
|
32
32
|
|
|
33
|
+
<section class="card" id="updateCard" hidden>
|
|
34
|
+
<h2 id="updateTitle">An update is ready</h2>
|
|
35
|
+
<p id="updateText" class="muted"></p>
|
|
36
|
+
<pre id="updateCmd" class="cmd" hidden></pre>
|
|
37
|
+
<div class="actions"><button id="update" type="button" hidden>Update</button></div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
33
40
|
<section class="card" id="pairCard" hidden>
|
|
34
41
|
<h2>Pair with your Spaces account</h2>
|
|
35
42
|
<ol class="steps">
|
|
@@ -41,6 +41,7 @@ src/witbitz_code/relay.py
|
|
|
41
41
|
src/witbitz_code/remind.py
|
|
42
42
|
src/witbitz_code/seccheck.py
|
|
43
43
|
src/witbitz_code/seen.py
|
|
44
|
+
src/witbitz_code/selfupdate.py
|
|
44
45
|
src/witbitz_code/tinfoil_read.py
|
|
45
46
|
src/witbitz_code/tools_catalog.json
|
|
46
47
|
src/witbitz_code/tools_probe.py
|
|
@@ -91,6 +92,7 @@ tests/test_registry.py
|
|
|
91
92
|
tests/test_relay_codec.py
|
|
92
93
|
tests/test_remind.py
|
|
93
94
|
tests/test_seccheck.py
|
|
95
|
+
tests/test_selfupdate.py
|
|
94
96
|
tests/test_setup.py
|
|
95
97
|
tests/test_tools_probe.py
|
|
96
98
|
tests/test_toolsetup.py
|
|
@@ -110,7 +110,7 @@ class FakeNet:
|
|
|
110
110
|
def handler(self, req: httpx.Request):
|
|
111
111
|
if "googleapis.com/service_accounts" in str(req.url):
|
|
112
112
|
return httpx.Response(200, json=self.meta["googleJwks"])
|
|
113
|
-
self.calls.append({"url": str(req.url), "headers": dict(req.headers), "body": req.content.decode("utf-8")})
|
|
113
|
+
self.calls.append({"url": str(req.url), "headers": dict(req.headers), "raw": list(req.headers.raw), "body": req.content.decode("utf-8")})
|
|
114
114
|
if self.status != 200:
|
|
115
115
|
return httpx.Response(self.status, json={"error": {"message": "upstream said no"}})
|
|
116
116
|
data = self.stream.encode("utf-8")
|
|
@@ -176,6 +176,20 @@ def test_a_confidential_answer_streams_through_and_the_receipt_verifies_on_the_r
|
|
|
176
176
|
with_proxy(t)
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
@needs_fixture
|
|
180
|
+
def test_one_content_type_goes_upstream_whatever_case_opencode_sends_it():
|
|
181
|
+
# 2026-10-17: OpenCode sends "Content-Type"; the proxy forwarded it and added its own "content-type", and TrustedRouter
|
|
182
|
+
# refused every confidential call with two (a small one 400 "could not read request", a large one closed mid-upload —
|
|
183
|
+
# an empty httpx ReadError the owner saw as "could not reach TrustedRouter ()").
|
|
184
|
+
async def t(post, net, _p):
|
|
185
|
+
r = await post(SAY_OK, {"Content-Type": "application/json", "X-Inference-Receipt": "from-the-caller"})
|
|
186
|
+
assert r.status_code == 200
|
|
187
|
+
names = [k.decode().lower() for k, _v in net.calls[0]["raw"]]
|
|
188
|
+
assert names.count("content-type") == 1, names
|
|
189
|
+
assert names.count("x-inference-receipt") == 1 and net.calls[0]["headers"]["x-inference-receipt"] != "from-the-caller"
|
|
190
|
+
with_proxy(t)
|
|
191
|
+
|
|
192
|
+
|
|
179
193
|
@needs_fixture
|
|
180
194
|
def test_a_receipt_that_does_not_match_ends_the_step_with_an_error_and_no_tool_runs():
|
|
181
195
|
meta, stream, _ = fixture()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Update from the page (selfupdate.py): the version the RELEASED installer pins, and the way this copy was installed."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
from witbitz_code import selfupdate as su
|
|
8
|
+
|
|
9
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(coro):
|
|
13
|
+
return asyncio.run(coro)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def script(v):
|
|
17
|
+
return f"$ErrorActionPreference = 'Stop'\n $WitbitzCode = '{v}'\n $Git = '2.47.1'\n"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def updates(pin="1.7.4", kind="pipx", current="1.7.3", rc=0, err=""):
|
|
21
|
+
seen = {"run": [], "launch": [], "fetch": 0}
|
|
22
|
+
|
|
23
|
+
async def fetch(url):
|
|
24
|
+
seen["fetch"] += 1
|
|
25
|
+
assert url == su.INSTALLER_URL
|
|
26
|
+
if isinstance(pin, Exception):
|
|
27
|
+
raise pin
|
|
28
|
+
return script(pin)
|
|
29
|
+
u = su.Updates(fetch_text=fetch, install_info=lambda: {"kind": kind}, current=current,
|
|
30
|
+
run=lambda argv: seen["run"].append(argv) or SimpleNamespace(returncode=rc, stderr=err, stdout=""),
|
|
31
|
+
launch=lambda argv: seen["launch"].append(argv))
|
|
32
|
+
return u, seen
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_the_pin_is_read_from_the_real_installer_as_it_is_written():
|
|
36
|
+
text = (REPO / "spaces" / "public" / "code.ps1").read_text(encoding="utf-8")
|
|
37
|
+
v = su.parse_pin(text)
|
|
38
|
+
assert v and v.count(".") == 2, "code.ps1 must keep its one $WitbitzCode = 'x.y.z' line — the page reads it"
|
|
39
|
+
assert su.parse_pin("$WitbitzCode = \"1.2.3\"") is None and su.parse_pin("no pin here") is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_newer_compares_numbers_not_text():
|
|
43
|
+
assert su.newer("1.7.10", "1.7.9") and su.newer("2.0.0", "1.9.9")
|
|
44
|
+
assert not su.newer("1.7.3", "1.7.3") and not su.newer("1.7.2", "1.7.3") and not su.newer("x", "1.0.0")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_nothing_offered_when_this_is_the_released_version_or_newer():
|
|
48
|
+
for pin in ("1.7.3", "1.7.2"):
|
|
49
|
+
u, _ = updates(pin=pin)
|
|
50
|
+
run(u.check())
|
|
51
|
+
assert u.status()["available"] is False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_offline_is_a_reading_not_a_failure_and_it_asks_at_most_every_few_hours():
|
|
55
|
+
u, seen = updates(pin=OSError("down"))
|
|
56
|
+
run(u.check()); run(u.check())
|
|
57
|
+
assert seen["fetch"] == 1 and u.status() == {"current": "1.7.3", "latest": None, "available": False, "why": "could not check for updates (OSError)"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_pipx_installs_exactly_the_released_version_then_asks_for_a_restart(monkeypatch):
|
|
61
|
+
monkeypatch.setattr(su.shutil, "which", lambda n: "/usr/bin/pipx")
|
|
62
|
+
u, seen = updates()
|
|
63
|
+
run(u.check())
|
|
64
|
+
assert u.status() == {"current": "1.7.3", "latest": "1.7.4", "available": True, "kind": "pipx"}
|
|
65
|
+
r = run(u.update())
|
|
66
|
+
assert r["ok"] and seen["run"] == [["pipx", "install", "--force", "witbitz-code==1.7.4"]] and u.restart
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_a_failed_pipx_install_says_why_and_does_not_restart(monkeypatch):
|
|
70
|
+
monkeypatch.setattr(su.shutil, "which", lambda n: "/usr/bin/pipx")
|
|
71
|
+
u, _ = updates(rc=1, err="ERROR: No matching distribution found for witbitz-code==1.7.4")
|
|
72
|
+
r = run(u.update())
|
|
73
|
+
assert not r["ok"] and "No matching distribution" in r["why"] and not u.restart
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_the_windows_install_runs_the_released_installer_again_in_a_window_of_its_own():
|
|
77
|
+
u, seen = updates(kind="windows-installer")
|
|
78
|
+
r = run(u.update())
|
|
79
|
+
assert r["ok"] and r["kind"] == "installer" and not u.restart and not seen["run"]
|
|
80
|
+
assert seen["launch"] == [["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", f"irm {su.INSTALLER_URL} | iex"]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_any_other_install_is_shown_the_command_never_run():
|
|
84
|
+
u, seen = updates(kind="pip")
|
|
85
|
+
run(u.check())
|
|
86
|
+
st = u.status()
|
|
87
|
+
assert st["kind"] == "manual" and st["command"].endswith("-m pip install --upgrade witbitz-code==1.7.4")
|
|
88
|
+
r = run(u.update())
|
|
89
|
+
assert not r["ok"] and st["command"] in r["why"] and not seen["run"] and not seen["launch"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_the_restart_runs_the_same_manager_with_the_same_arguments(monkeypatch):
|
|
93
|
+
monkeypatch.setattr(su.sys, "argv", ["/x/bin/witbitz-code", "ui", "--hidden"])
|
|
94
|
+
assert su.restart_argv() == [su.sys.executable, "-m", "witbitz_code", "ui", "--hidden"]
|
|
@@ -276,3 +276,34 @@ def test_in_wsl_the_page_opener_is_waited_for(monkeypatch):
|
|
|
276
276
|
assert ui.open_browser("http://127.0.0.1:4291/?t=x") is True
|
|
277
277
|
argv, kw = calls[0]
|
|
278
278
|
assert argv == ["wslview", "http://127.0.0.1:4291/?t=x"] and kw["timeout"] == 15
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_the_page_offers_the_released_update_and_a_pipx_update_ends_this_manager_for_its_restart(monkeypatch):
|
|
282
|
+
from witbitz_code import selfupdate as su
|
|
283
|
+
monkeypatch.setattr(su.shutil, "which", lambda n: "/usr/bin/pipx")
|
|
284
|
+
ran = []
|
|
285
|
+
|
|
286
|
+
async def fetch(_u):
|
|
287
|
+
return "$WitbitzCode = '9.9.9'\n"
|
|
288
|
+
w = World(pairings=[DESK])
|
|
289
|
+
m = w.manager()
|
|
290
|
+
m.updates = su.Updates(fetch_text=fetch, install_info=lambda: {"kind": "pipx"}, current="1.0.0",
|
|
291
|
+
run=lambda argv: ran.append(argv) or SimpleNamespace(returncode=0, stderr="", stdout=""))
|
|
292
|
+
|
|
293
|
+
async def t():
|
|
294
|
+
import httpx
|
|
295
|
+
m.quit_event = asyncio.Event()
|
|
296
|
+
srv, base = await with_server(m)
|
|
297
|
+
try:
|
|
298
|
+
async with httpx.AsyncClient() as c:
|
|
299
|
+
await m.updates.check()
|
|
300
|
+
s = (await c.get(base + "/api/status", headers=authed(srv))).json()
|
|
301
|
+
assert s["update"] == {"current": "1.0.0", "latest": "9.9.9", "available": True, "kind": "pipx"}
|
|
302
|
+
r = (await c.post(base + "/api/update", json={}, headers=authed(srv, **{"content-type": "application/json"}))).json()
|
|
303
|
+
assert r["ok"] and ran == [["pipx", "install", "--force", "witbitz-code==9.9.9"]]
|
|
304
|
+
await asyncio.wait_for(m.quit_event.wait(), 2)
|
|
305
|
+
assert m.updates.restart
|
|
306
|
+
finally:
|
|
307
|
+
await m.shutdown()
|
|
308
|
+
await srv.close()
|
|
309
|
+
run(t())
|
|
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
|
{witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.7.2 → witbitz_code-1.7.4}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|