witbitz-code 1.3.0__tar.gz → 1.3.2__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.3.0/src/witbitz_code.egg-info → witbitz_code-1.3.2}/PKG-INFO +1 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/pyproject.toml +1 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/account.py +5 -2
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/cli.py +21 -2
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/pair.py +1 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/uninstall.py +1 -1
- witbitz_code-1.3.2/src/witbitz_code/winconsole.py +60 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/wizard.py +12 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code.egg-info/SOURCES.txt +3 -1
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_pair_flows.py +8 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_setup.py +23 -0
- witbitz_code-1.3.2/tests/test_winconsole.py +58 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/LICENSE +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/README.md +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/setup.cfg +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_asks.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_attachments.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_auto.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_cli.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_codetools.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_confidential.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_folders.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_outputs.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_plugins.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_policy.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_registry.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.3.0 → witbitz_code-1.3.2}/tests/test_uninstall.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.3.
|
|
7
|
+
version = "1.3.2"
|
|
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"
|
|
@@ -73,13 +73,16 @@ async def publish_computer(*, call: ApiCall, idx: dict, computer_id: str, name:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
async def unpublish_computer(*, call: ApiCall, idx: dict, computer_id: str, now: float | None = None) -> dict:
|
|
76
|
-
"""Tombstone this computer in the account's registry (its secret leaves the account).
|
|
76
|
+
"""Tombstone this computer in the account's registry (its secret leaves the account). Written even when this doc does not
|
|
77
|
+
list it: an app that seals the list keeps it in 'xcomputers', which this tool cannot read (it holds no account key), and
|
|
78
|
+
an older app emptied this doc — so "not listed" here says nothing about the list the apps show (tools/opencode-pair.mjs
|
|
79
|
+
unpublishComputer). Only a tombstone already here is left alone."""
|
|
77
80
|
now = _now_ms() if now is None else now
|
|
78
81
|
rd = await read_doc(call, idx, COMPUTERS_DOC)
|
|
79
82
|
if not rd["ok"]:
|
|
80
83
|
return rd
|
|
81
84
|
cur = norm_registry(rd["state"])["computers"].get(computer_id)
|
|
82
|
-
if
|
|
85
|
+
if cur and cur.get("removed"):
|
|
83
86
|
return {"ok": True, "noop": True}
|
|
84
87
|
nxt = without_computer(rd["state"], computer_id, now)
|
|
85
88
|
wr = await call(_write_body(idx, COMPUTERS_DOC, wrap_index_state({**nxt, "at": now}), "computer unpaired"))
|
|
@@ -25,7 +25,7 @@ import httpx
|
|
|
25
25
|
from . import __version__
|
|
26
26
|
from . import pair as pair_cmd
|
|
27
27
|
from . import pairings as pp
|
|
28
|
-
from . import codekeys, codetools, confidential, keys, models, plugins, policy, uninstall as uninstall_mod, wizard
|
|
28
|
+
from . import codekeys, codetools, confidential, keys, models, plugins, policy, uninstall as uninstall_mod, winconsole, wizard
|
|
29
29
|
from ._js import to_number, truthy
|
|
30
30
|
from .connector import pairings_for_port, start_connector
|
|
31
31
|
from .payers import SessionPayers
|
|
@@ -263,13 +263,31 @@ async def _check_keys_every(live_keys: codekeys.LiveKeys, http: httpx.AsyncClien
|
|
|
263
263
|
async def setup(args: list[str]) -> int:
|
|
264
264
|
port_n = to_number(_flag(args, "--port", "4096"))
|
|
265
265
|
port = int(port_n) if truthy(port_n) and port_n == port_n and float(port_n).is_integer() else 4096
|
|
266
|
+
from . import net # noqa: PLC0415
|
|
267
|
+
|
|
268
|
+
client = net.new_client()
|
|
269
|
+
|
|
270
|
+
async def keys_state(p: dict) -> str:
|
|
271
|
+
entry = codekeys.for_computer(p.get("computerId") or "")
|
|
272
|
+
if not entry:
|
|
273
|
+
return "none"
|
|
274
|
+
try:
|
|
275
|
+
st = await codekeys.status(client, entry)
|
|
276
|
+
except Exception: # noqa: BLE001 — offline: unknown, never a removal
|
|
277
|
+
return "unknown"
|
|
278
|
+
return "live" if st.get("live") is True else st.get("why") if st.get("why") in ("revoked", "paused") else "unknown"
|
|
279
|
+
|
|
266
280
|
d = wizard.Deps(
|
|
267
281
|
say=_err, ask=lambda q: keys.read_line(q), secret=lambda q: keys.read_line(q, hidden=True),
|
|
268
282
|
port=port, port_explicit="--port" in args,
|
|
269
283
|
pair=lambda p: pair_cmd.main(["--port", str(p)]),
|
|
284
|
+
keys_state=keys_state,
|
|
270
285
|
is_listening=is_listening, serve_here=lambda p: serve(["--port", str(p)]),
|
|
271
286
|
)
|
|
272
|
-
|
|
287
|
+
try:
|
|
288
|
+
r = await wizard.run_setup(d)
|
|
289
|
+
finally:
|
|
290
|
+
await client.aclose()
|
|
273
291
|
return r.get("served", 0) if not r.get("stopped") else 1
|
|
274
292
|
|
|
275
293
|
|
|
@@ -352,6 +370,7 @@ async def _main(argv: list[str]) -> int:
|
|
|
352
370
|
|
|
353
371
|
|
|
354
372
|
def main(argv: list[str] | None = None) -> Any:
|
|
373
|
+
winconsole.prepare() # Windows: UTF-8 when not a console, ANSI colours for the QR when it is (winconsole.py)
|
|
355
374
|
try:
|
|
356
375
|
code = asyncio.run(_main(sys.argv[1:] if argv is None else argv))
|
|
357
376
|
except KeyboardInterrupt:
|
|
@@ -205,7 +205,7 @@ async def _run(args: Args, call: Any, link_fn: LinkFn | None, out: Callable, err
|
|
|
205
205
|
ok = False
|
|
206
206
|
err(f"witbitz-code: ✖ {p.get('account')}: {r['why']} — kept the local pairing so you can retry")
|
|
207
207
|
continue
|
|
208
|
-
note = " (it was
|
|
208
|
+
note = " (it was already removed)" if r.get("noop") else ""
|
|
209
209
|
out(f"witbitz-code: ✓ removed \"{p.get('name')}\" from {p.get('account')}{note} — every device drops it on its next sync")
|
|
210
210
|
pp.write_pairings(pp.remove_pairings(pp.read_pairings(), p.get("account") or "")["doc"])
|
|
211
211
|
out(f"witbitz-code: restart the connector ({START_HINT}) so it stops answering on the old channel.")
|
|
@@ -323,7 +323,7 @@ async def run_uninstall(d: Deps) -> dict:
|
|
|
323
323
|
except Exception as e: # noqa: BLE001 — one account failing must not stop the rest
|
|
324
324
|
r = {"ok": False, "why": str(e)}
|
|
325
325
|
if r.get("ok"):
|
|
326
|
-
d.say(f"✓ Removed \"{p.get('name')}\" from {p.get('account') or 'your account'}{' (it was
|
|
326
|
+
d.say(f"✓ Removed \"{p.get('name')}\" from {p.get('account') or 'your account'}{' (it was already removed)' if r.get('noop') else ''}")
|
|
327
327
|
else:
|
|
328
328
|
d.say(f"✖ Could not remove \"{p.get('name')}\" from {p.get('account') or 'your account'} ({r.get('why')})")
|
|
329
329
|
left.append(p)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Windows output, set up once before anything is printed (tests/test_winconsole.py).
|
|
2
|
+
|
|
3
|
+
- Not a console (piped, redirected, captured by another program — an installer, a service log): Windows Python writes the
|
|
4
|
+
locale's code page, cp1252, and the first arrow or QR block crashed witbitz-code ("'charmap' codec can't encode
|
|
5
|
+
character '\\u2192'", measured on Windows 11 with 1.3.0). Such a stream is switched to UTF-8.
|
|
6
|
+
- A console: the QR is drawn with ANSI colours (qr.render_ansi), which the console shows only once its virtual terminal
|
|
7
|
+
mode is on — Windows Terminal has it, an older console window prints the codes as text. It is turned on here.
|
|
8
|
+
|
|
9
|
+
Off Windows, and wherever the console cannot say what it is, nothing changes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
18
|
+
STD_HANDLES = (-11, -12) # stdout, stderr
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _kernel32() -> Any:
|
|
22
|
+
import ctypes # noqa: PLC0415 — Windows only
|
|
23
|
+
|
|
24
|
+
return ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def prepare(*, platform: str = sys.platform, streams: tuple = (), kernel32: Any = "auto") -> None:
|
|
28
|
+
if platform != "win32":
|
|
29
|
+
return
|
|
30
|
+
streams = streams or (sys.stdout, sys.stderr)
|
|
31
|
+
console = []
|
|
32
|
+
for i, s in enumerate(streams):
|
|
33
|
+
try:
|
|
34
|
+
tty = bool(s.isatty())
|
|
35
|
+
except (AttributeError, ValueError):
|
|
36
|
+
tty = False
|
|
37
|
+
if tty:
|
|
38
|
+
console.append(STD_HANDLES[i] if i < len(STD_HANDLES) else None)
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
s.reconfigure(encoding="utf-8", errors="replace")
|
|
42
|
+
except (AttributeError, ValueError):
|
|
43
|
+
pass
|
|
44
|
+
if not any(h is not None for h in console):
|
|
45
|
+
return
|
|
46
|
+
try:
|
|
47
|
+
import ctypes # noqa: PLC0415
|
|
48
|
+
|
|
49
|
+
k = _kernel32() if kernel32 == "auto" else kernel32
|
|
50
|
+
if k is None:
|
|
51
|
+
return
|
|
52
|
+
for n in console:
|
|
53
|
+
if n is None:
|
|
54
|
+
continue
|
|
55
|
+
handle = k.GetStdHandle(n)
|
|
56
|
+
mode = ctypes.c_ulong()
|
|
57
|
+
if k.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
58
|
+
k.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
59
|
+
except Exception: # noqa: BLE001 — an old console keeps the raw code line below the QR
|
|
60
|
+
pass
|
|
@@ -69,6 +69,10 @@ class Deps:
|
|
|
69
69
|
pair: Callable[[int], Awaitable[Any]] = None # type: ignore[assignment]
|
|
70
70
|
move_pairing: Callable[[dict, int], None] = move_pairing
|
|
71
71
|
code_keys: Callable[[dict], dict | None] = lambda p: codekeys.for_computer(p.get("computerId") or "")
|
|
72
|
+
# What the ACCOUNT says about a pairing's keys: "live" | "revoked" (the computer was removed — Remove in the app, an
|
|
73
|
+
# unpair) | "paused" | "unknown" (offline) | "none" (no keys here). The file on disk outlives a removal.
|
|
74
|
+
keys_state: Callable[[dict], Awaitable[str]] | None = None
|
|
75
|
+
forget_keys: Callable[[dict], Any] = lambda p: codekeys.forget(p.get("computerId") or "")
|
|
72
76
|
is_listening: Callable[[int], Awaitable[bool]] = None # type: ignore[assignment]
|
|
73
77
|
serve_here: Callable[[int], Awaitable[int]] = None # type: ignore[assignment]
|
|
74
78
|
result: dict = field(default_factory=lambda: {"opencode": False, "paired": False, "keys": False, "running": ""})
|
|
@@ -120,6 +124,14 @@ async def run_setup(d: Deps) -> dict:
|
|
|
120
124
|
d.move_pairing(p, port)
|
|
121
125
|
everything = d.all_pairings()
|
|
122
126
|
mine = on(everything, port)
|
|
127
|
+
# A pairing the account has let go of (the owner, 2026-10-08: reinstalled after a removal and "it does not present the
|
|
128
|
+
# QR code"): its keys come back revoked, so it is not a pairing any more — say so and scan again. Offline is not that.
|
|
129
|
+
gone = [p for p in mine if d.keys_state and d.code_keys(p) and await d.keys_state(p) == "revoked"]
|
|
130
|
+
for p in gone:
|
|
131
|
+
d.forget_keys(p)
|
|
132
|
+
if mine and len(gone) == len(mine):
|
|
133
|
+
d.say(f" {named(gone)} was removed from your Witbitz account, so this computer pairs again.")
|
|
134
|
+
mine = []
|
|
123
135
|
if not mine:
|
|
124
136
|
d.say(" On your phone, open Spaces → Settings → Back up & recovery → Add a device, and scan the code below.\n")
|
|
125
137
|
await d.pair(port)
|
|
@@ -38,6 +38,7 @@ src/witbitz_code/seen.py
|
|
|
38
38
|
src/witbitz_code/tinfoil_read.py
|
|
39
39
|
src/witbitz_code/tools_probe.py
|
|
40
40
|
src/witbitz_code/uninstall.py
|
|
41
|
+
src/witbitz_code/winconsole.py
|
|
41
42
|
src/witbitz_code/wizard.py
|
|
42
43
|
src/witbitz_code.egg-info/PKG-INFO
|
|
43
44
|
src/witbitz_code.egg-info/SOURCES.txt
|
|
@@ -68,4 +69,5 @@ tests/test_registry.py
|
|
|
68
69
|
tests/test_relay_codec.py
|
|
69
70
|
tests/test_setup.py
|
|
70
71
|
tests/test_tools_probe.py
|
|
71
|
-
tests/test_uninstall.py
|
|
72
|
+
tests/test_uninstall.py
|
|
73
|
+
tests/test_winconsole.py
|
|
@@ -70,6 +70,14 @@ def test_unpublish_tombstones_and_a_second_unpair_writes_nothing():
|
|
|
70
70
|
assert run(acct.unpublish_computer(call=MockApi(read_status=500).call, idx=IDX, computer_id="x"))["ok"] is False
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def test_unpublish_writes_the_tombstone_into_a_doc_an_app_emptied():
|
|
74
|
+
# the list lives sealed ('xcomputers'), where this tool cannot read — "not listed" here must still remove it
|
|
75
|
+
api = MockApi(docs={"computers": {"v": 2, "moved": "sealed"}})
|
|
76
|
+
r = run(acct.unpublish_computer(call=api.call, idx=IDX, computer_id="cmp_desk_01", now=1000))
|
|
77
|
+
assert r["ok"] and not r.get("noop"), r
|
|
78
|
+
assert norm_registry(api.plain("computers"))["computers"]["cmp_desk_01"] == {"removed": True, "mod": 1000}
|
|
79
|
+
|
|
80
|
+
|
|
73
81
|
def test_clear_legacy_direct_keeps_contacts_and_the_model():
|
|
74
82
|
api = MockApi(docs={"index2": {"v": 2, "contacts": [{"kind": "person", "email": "dana@example.com"}],
|
|
75
83
|
"code": {"base": "https://old.example", "pass": "p", "model": "m", "agent": "build", "mod": 50}}})
|
|
@@ -167,3 +167,26 @@ def test_an_opencode_started_elsewhere_is_named():
|
|
|
167
167
|
run(wizard.run_setup(f.deps()))
|
|
168
168
|
assert any("started some other way" in line for line in f.said)
|
|
169
169
|
assert any("close that OpenCode first" in line for line in f.said)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_a_computer_removed_from_the_account_is_paired_again_and_shows_the_qr():
|
|
173
|
+
# the owner, 2026-10-08: reinstalled after the computer was removed and "it does not present the QR code" — its pairing
|
|
174
|
+
# and keys were still on disk, so setup said "✓ Paired" over keys the account had already taken back
|
|
175
|
+
f = Fake(pairings=[{"name": "desk", "account": "a@x.com", "opencodeUrl": "http://127.0.0.1:4096", "idx": {"room": "r"}, "computerId": "c"}], keyed={"c"})
|
|
176
|
+
asked = []
|
|
177
|
+
async def keys_state(p):
|
|
178
|
+
asked.append(p["computerId"])
|
|
179
|
+
return "live" if f.paired else "revoked"
|
|
180
|
+
r = run(wizard.run_setup(f.deps(keys_state=keys_state, forget_keys=lambda p: f._keyed.discard(p["computerId"]))))
|
|
181
|
+
assert f.paired == [4096] and r["paired"] and r["keys"] and f.served == [4096]
|
|
182
|
+
assert any('"desk" → a@x.com was removed from your Witbitz account' in line for line in f.said)
|
|
183
|
+
assert any("scan the code below" in line for line in f.said)
|
|
184
|
+
assert not any(line.startswith(" ✓ Paired") for line in f.said[:f.said.index(next(x for x in f.said if "was removed" in x))])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_keys_that_cannot_be_checked_are_not_taken_for_a_removal():
|
|
188
|
+
f = Fake(pairings=[{"name": "desk", "opencodeUrl": "http://127.0.0.1:4096", "idx": {"room": "r"}, "computerId": "c"}], keyed={"c"})
|
|
189
|
+
async def keys_state(p):
|
|
190
|
+
return "unknown"
|
|
191
|
+
r = run(wizard.run_setup(f.deps(keys_state=keys_state, forget_keys=lambda p: f._keyed.discard(p["computerId"]))))
|
|
192
|
+
assert f.paired == [] and r["keys"] and f.served == [4096]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Windows output (the owner's Windows laptop, 2026-10-08; measured by the drawer/mention session on Windows 11 with 1.3.0):
|
|
2
|
+
piped, redirected or captured, Windows Python writes cp1252 and `witbitz-code --help` died "'charmap' codec can't encode
|
|
3
|
+
character '\\u2192'"; in a console window the QR is drawn with ANSI colours, which a console shows only once its virtual
|
|
4
|
+
terminal mode is on."""
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
from witbitz_code import winconsole
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cp1252_stream(tty: bool) -> io.TextIOWrapper:
|
|
12
|
+
s = io.TextIOWrapper(io.BytesIO(), encoding="cp1252", errors="strict", write_through=True)
|
|
13
|
+
s.isatty = lambda: tty # type: ignore[method-assign]
|
|
14
|
+
return s
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_a_redirected_stream_on_windows_writes_utf8_instead_of_crashing():
|
|
18
|
+
out = cp1252_stream(tty=False)
|
|
19
|
+
try:
|
|
20
|
+
out.write("→")
|
|
21
|
+
raise AssertionError("cp1252 should not encode an arrow")
|
|
22
|
+
except UnicodeEncodeError:
|
|
23
|
+
pass
|
|
24
|
+
winconsole.prepare(platform="win32", streams=(out,), kernel32=None)
|
|
25
|
+
out.write("pair → ▀")
|
|
26
|
+
out.flush()
|
|
27
|
+
assert out.buffer.getvalue().endswith("pair → ▀".encode("utf-8"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FakeKernel32:
|
|
31
|
+
def __init__(self, mode=0x0003, ok=True):
|
|
32
|
+
self.mode, self.ok, self.set = mode, ok, []
|
|
33
|
+
|
|
34
|
+
def GetStdHandle(self, n): # noqa: N802 — the Win32 name
|
|
35
|
+
return {-11: 7, -12: 8}[n]
|
|
36
|
+
|
|
37
|
+
def GetConsoleMode(self, handle, ref): # noqa: N802
|
|
38
|
+
ref._obj.value = self.mode
|
|
39
|
+
return 1 if self.ok else 0
|
|
40
|
+
|
|
41
|
+
def SetConsoleMode(self, handle, mode): # noqa: N802
|
|
42
|
+
self.set.append((handle, mode))
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_a_console_on_windows_gets_virtual_terminal_mode_so_the_qr_draws():
|
|
47
|
+
k = FakeKernel32()
|
|
48
|
+
winconsole.prepare(platform="win32", streams=(cp1252_stream(tty=True), cp1252_stream(tty=True)), kernel32=k)
|
|
49
|
+
assert k.set == [(7, 0x0003 | 0x0004), (8, 0x0003 | 0x0004)]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_nothing_changes_off_windows_or_when_the_console_cannot_say():
|
|
53
|
+
s = cp1252_stream(tty=False)
|
|
54
|
+
winconsole.prepare(platform="linux", streams=(s,), kernel32=FakeKernel32())
|
|
55
|
+
assert s.encoding == "cp1252"
|
|
56
|
+
k = FakeKernel32(ok=False)
|
|
57
|
+
winconsole.prepare(platform="win32", streams=(cp1252_stream(tty=True),), kernel32=k)
|
|
58
|
+
assert k.set == []
|
|
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.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.3.0 → witbitz_code-1.3.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.3.0 → witbitz_code-1.3.2}/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
|