witbitz-code 1.6.0__tar.gz → 1.6.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.
Files changed (100) hide show
  1. {witbitz_code-1.6.0/src/witbitz_code.egg-info → witbitz_code-1.6.2}/PKG-INFO +1 -1
  2. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/pyproject.toml +2 -2
  3. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/__init__.py +1 -1
  4. witbitz_code-1.6.2/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  5. witbitz_code-1.6.2/src/witbitz_code/icons/witbitz-code.ico +0 -0
  6. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/uninstall.py +72 -5
  7. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/wslstart.py +83 -12
  8. {witbitz_code-1.6.0 → witbitz_code-1.6.2/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  9. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code.egg-info/SOURCES.txt +2 -0
  10. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_uninstall.py +37 -1
  11. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_wslstart.py +39 -4
  12. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/LICENSE +0 -0
  13. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/README.md +0 -0
  14. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/setup.cfg +0 -0
  15. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/__main__.py +0 -0
  16. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/_js.py +0 -0
  17. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/account.py +0 -0
  18. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/asks.py +0 -0
  19. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/attachments.py +0 -0
  20. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/auto.py +0 -0
  21. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/auto_runner.py +0 -0
  22. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/cli.py +0 -0
  23. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/codekeys.py +0 -0
  24. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/codetools.py +0 -0
  25. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/compress.py +0 -0
  26. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/computers.py +0 -0
  27. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/confidential.py +0 -0
  28. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/confidential_models.json +0 -0
  29. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/connector.py +0 -0
  30. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/devicelink.py +0 -0
  31. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/dirs.py +0 -0
  32. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/folders.py +0 -0
  33. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/gateway.py +0 -0
  34. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/keys.py +0 -0
  35. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/link.py +0 -0
  36. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/mkdir.py +0 -0
  37. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/models.py +0 -0
  38. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/net.py +0 -0
  39. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/offered_models.json +0 -0
  40. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  41. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  42. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  43. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  44. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/outputs.py +0 -0
  45. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/pair.py +0 -0
  46. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/pairings.py +0 -0
  47. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/payers.py +0 -0
  48. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/plugins.py +0 -0
  49. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/policy.py +0 -0
  50. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/ports.py +0 -0
  51. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/qr.py +0 -0
  52. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/receipt.py +0 -0
  53. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/relay.py +0 -0
  54. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/seen.py +0 -0
  55. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/tinfoil_read.py +0 -0
  56. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/tools_catalog.json +0 -0
  57. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/tools_probe.py +0 -0
  58. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/toolsetup.py +0 -0
  59. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/ui.py +0 -0
  60. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/ui_static/app.css +0 -0
  61. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/ui_static/app.js +0 -0
  62. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/ui_static/index.html +0 -0
  63. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/uitools.py +0 -0
  64. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/winagent.py +0 -0
  65. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/winbg.py +0 -0
  66. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/winconsole.py +0 -0
  67. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code/wizard.py +0 -0
  68. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  69. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  70. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code.egg-info/requires.txt +0 -0
  71. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/src/witbitz_code.egg-info/top_level.txt +0 -0
  72. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_asks.py +0 -0
  73. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_attachments.py +0 -0
  74. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_auto.py +0 -0
  75. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_cli.py +0 -0
  76. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_codekeys.py +0 -0
  77. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_codetools.py +0 -0
  78. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_confidential.py +0 -0
  79. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_connector_e2e.py +0 -0
  80. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_devicelink.py +0 -0
  81. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_dirs.py +0 -0
  82. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_files_and_compress.py +0 -0
  83. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_folders.py +0 -0
  84. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_outputs.py +0 -0
  85. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_pair_flows.py +0 -0
  86. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_plugins.py +0 -0
  87. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_policy.py +0 -0
  88. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_ports.py +0 -0
  89. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_ports_win.py +0 -0
  90. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_registry.py +0 -0
  91. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_relay_codec.py +0 -0
  92. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_setup.py +0 -0
  93. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_tools_probe.py +0 -0
  94. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_toolsetup.py +0 -0
  95. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_ui.py +0 -0
  96. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_uitools.py +0 -0
  97. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_winagent.py +0 -0
  98. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_winbg.py +0 -0
  99. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_winconsole.py +0 -0
  100. {witbitz_code-1.6.0 → witbitz_code-1.6.2}/tests/test_windows_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.6.0"
7
+ version = "1.6.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"
@@ -43,7 +43,7 @@ where = ["src"]
43
43
 
44
44
  # Our OpenCode plugins, installed by `serve` (plugins.py) — copies of tools/opencode-plugins, kept current by embed.mjs
45
45
  [tool.setuptools.package-data]
46
- witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json", "tools_catalog.json", "ui_static/*"]
46
+ witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json", "tools_catalog.json", "ui_static/*", "icons/*"]
47
47
 
48
48
  [tool.pytest.ini_options]
49
49
  testpaths = ["tests"]
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.6.0"
7
+ __version__ = "1.6.2"
@@ -23,7 +23,7 @@ from dataclasses import dataclass, field
23
23
  from pathlib import Path
24
24
  from typing import Any
25
25
 
26
- from . import keys, plugins
26
+ from . import keys, plugins, ports
27
27
  from . import pairings as pp
28
28
  from .wizard import find_opencode
29
29
 
@@ -138,6 +138,8 @@ def opencode_holders(data_dir: str) -> list[dict]:
138
138
  cmd = ""
139
139
  hits.append({"pid": int(p), "cmd": cmd})
140
140
  return hits
141
+ if sys.platform == "win32": # no /proc, no lsof: every opencode.exe of this user's (the owner, 2026-10-09: the background
142
+ return win_opencode_processes() # copy's OpenCode held opencode.db and the delete died "WinError 32")
141
143
  files = [os.path.join(data_dir, f) for f in ("opencode.db", "opencode.db-wal", "opencode.db-shm") if os.path.exists(os.path.join(data_dir, f))]
142
144
  if not files or not shutil.which("lsof"):
143
145
  return hits
@@ -150,6 +152,55 @@ def opencode_holders(data_dir: str) -> list[dict]:
150
152
  return hits
151
153
 
152
154
 
155
+ def win_opencode_processes(*, run: Callable[[list[str]], str] | None = None, user: str | None = None) -> list[dict]:
156
+ """Windows: this user's opencode.exe processes (tasklist, one CSV row each)."""
157
+ run = run or ports._run_text
158
+ user = os.environ.get("USERNAME", "") if user is None else user
159
+ hits = []
160
+ for line in run(["tasklist", "/V", "/FO", "CSV", "/NH", "/FI", "IMAGENAME eq opencode.exe"]).splitlines():
161
+ row = ports.parse_tasklist_row(line)
162
+ if row and (not user or row["user"].rsplit("\\", 1)[-1].lower() == user.lower()):
163
+ hits.append({"pid": row["pid"], "cmd": row["image"]})
164
+ return hits
165
+
166
+
167
+ def running_witbitz(*, lock: Path | None = None, pairings: Callable[[], list] | None = None,
168
+ owners: Callable[[int], list] | None = None, alive: Callable[[int], bool] | None = None) -> list[dict]:
169
+ """What runs witbitz-code here now: the page's manager (its ui.json names it — in the background it has no window to
170
+ close) and whatever holds each paired OpenCode port or its model proxy's (port + 100): a serve, its OpenCode."""
171
+ lock = lock or pp.pairings_path().parent / "ui.json"
172
+ pairings = pairings or (lambda: pp.load_pairings(log=lambda _m: None))
173
+ owners = owners or ports.port_owners
174
+ alive = alive or _alive
175
+ found: list[dict] = []
176
+ try:
177
+ pid = json.loads(lock.read_text(encoding="utf-8")).get("pid")
178
+ if isinstance(pid, int) and pid != os.getpid() and alive(pid):
179
+ found.append({"pid": pid, "cmd": "the Witbitz Code page"})
180
+ except (OSError, ValueError, AttributeError):
181
+ pass
182
+ for p in pairings():
183
+ tail = str(p.get("opencodeUrl") or "").rsplit(":", 1)[-1].rstrip("/")
184
+ port = int(tail) if tail.isdigit() else 0
185
+ for q in ([port, port + 100] if port else []):
186
+ for o in owners(q):
187
+ if o["pid"] != os.getpid() and all(o["pid"] != x["pid"] for x in found):
188
+ found.append({"pid": int(o["pid"]), "cmd": str(o.get("cmd") or "")[:70]})
189
+ return found
190
+
191
+
192
+ def _alive(pid: int) -> bool:
193
+ if sys.platform == "win32":
194
+ return _win_alive(pid)
195
+ try:
196
+ os.kill(pid, 0)
197
+ return True
198
+ except ProcessLookupError:
199
+ return False
200
+ except PermissionError:
201
+ return False # another user's: not ours
202
+
203
+
153
204
  def _win_alive(pid: int) -> bool:
154
205
  """Windows: is `pid` still running? (OpenProcess + GetExitCodeProcess; access denied = another user's, still running.)"""
155
206
  import ctypes # noqa: PLC0415 — Windows only
@@ -287,6 +338,7 @@ class Deps:
287
338
  sessions: Callable[[], dict] = None # type: ignore[assignment]
288
339
  delete_sessions: Callable[[], None] = None # type: ignore[assignment]
289
340
  opencode_processes: Callable[[], list] = lambda: opencode_holders(str(keys.auth_path().parent))
341
+ running: Callable[[], list] = running_witbitz
290
342
  stop_process: Callable[[int], bool] = stop_process
291
343
  opencode: Callable[[], dict] = opencode_here
292
344
  remove_opencode_program: Callable[[dict], dict] = remove_opencode_program
@@ -332,6 +384,12 @@ async def run_uninstall(d: Deps) -> dict:
332
384
  how = {"installer": "from OpenCode's installer, in ~/.opencode", "npm": "with npm", "brew": "with Homebrew"}.get(oc["kind"])
333
385
  remove_oc = ask(d.remove_opencode, f"Also remove OpenCode itself (installed {how})? [y/N] ") if oc["kind"] not in ("none", "other") else False
334
386
 
387
+ # The running witbitz-code first — the page's manager in the background, a serve, their OpenCode: it holds the files
388
+ # about to go (Windows refuses to delete an open file) and would keep answering the phone. Continue already said yes.
389
+ for p in d.running():
390
+ d.say(f"✓ Stopped the running witbitz-code: {p['cmd'] or 'a program'} (process {p['pid']})" if d.stop_process(p["pid"])
391
+ else f"✖ Could not stop {p['cmd'] or 'a program'} (process {p['pid']}) — close it, then run uninstall again")
392
+
335
393
  # Sessions: an OpenCode that has its database open would write it back — never delete under it.
336
394
  while remove_sessions:
337
395
  procs = d.opencode_processes()
@@ -376,8 +434,11 @@ async def run_uninstall(d: Deps) -> dict:
376
434
  d.remove_path(notes["root"])
377
435
  d.say(f"✓ Deleted {notes['root']}")
378
436
  if remove_sessions:
379
- d.delete_sessions()
380
- d.say(f"✓ Deleted the OpenCode sessions in {sessions['dir']} (provider logins kept)")
437
+ stuck = d.delete_sessions() or []
438
+ if stuck:
439
+ d.say(f"✖ Could not delete {', '.join(stuck[:4])} in {sessions['dir']} — another program still has it open; close OpenCode and delete it yourself")
440
+ else:
441
+ d.say(f"✓ Deleted the OpenCode sessions in {sessions['dir']} (provider logins kept)")
381
442
  oc_gone = False
382
443
  if remove_oc:
383
444
  try:
@@ -454,12 +515,18 @@ def sessions_info() -> dict:
454
515
  return {"dir": str(d), "exists": exists}
455
516
 
456
517
 
457
- def delete_sessions() -> None:
518
+ def delete_sessions() -> list[str]:
519
+ """→ the names that could not be deleted (Windows: a file another program still has open), never a crash."""
458
520
  d = keys.auth_path().parent
521
+ left = []
459
522
  for f in d.iterdir():
460
523
  if f.name == "auth.json":
461
524
  continue
462
- shutil.rmtree(f, ignore_errors=True) if f.is_dir() and not f.is_symlink() else f.unlink(missing_ok=True)
525
+ try:
526
+ shutil.rmtree(f, ignore_errors=True) if f.is_dir() and not f.is_symlink() else f.unlink(missing_ok=True)
527
+ except OSError:
528
+ left.append(f.name)
529
+ return left
463
530
 
464
531
 
465
532
  def saved_keys() -> list[str]:
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import base64
20
20
  import getpass
21
21
  import os
22
+ import re
22
23
  import shutil
23
24
  import subprocess
24
25
  import sys
@@ -50,6 +51,31 @@ def wsl_args(distro: str, user: str, cmd: list[str], verb: list[str] | None = No
50
51
  return _join(parts)
51
52
 
52
53
 
54
+ _SAFE_DIR = re.compile(r"[A-Za-z0-9_./+:@-]+")
55
+
56
+
57
+ def carried_path(path: str | None = None) -> str:
58
+ """This terminal's PATH for a start that runs the command directly — wsl.exe's `--` reads no ~/.bashrc, so a hidden copy
59
+ had only the system folders: no OpenCode (nvm's, ~/.opencode/bin), no witbitz-code (the owner, 2026-10-09: "OpenCode is
60
+ not installed" and "Start when I log in — not available"). Only plain folder names cross the Windows command line."""
61
+ parts = (os.environ.get("PATH", "") if path is None else path).split(":")
62
+ return ":".join(dict.fromkeys(p for p in parts if p and _SAFE_DIR.fullmatch(p) and ":" not in p))
63
+
64
+
65
+ def wsl_command(exe: str, path: str | None = None) -> list[str]:
66
+ """[env, PATH=…, exe] — the command a hidden start runs, with this terminal's PATH."""
67
+ return ["env", "PATH=" + carried_path(path), exe]
68
+
69
+
70
+ def this_program() -> str:
71
+ """witbitz-code on PATH, else the program running now (a hidden copy started before 1.6.2 had no ~/.local/bin)."""
72
+ exe = shutil.which("witbitz-code")
73
+ if exe:
74
+ return exe
75
+ argv0 = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else ""
76
+ return argv0 if argv0 and os.path.basename(argv0) == "witbitz-code" and os.path.exists(argv0) else ""
77
+
78
+
53
79
  def headless_args(distro: str, user: str, cmd: list[str], verb: list[str]) -> str:
54
80
  """conhost.exe's arguments: no window, then wsl.exe with wsl_args."""
55
81
  return "--headless wsl.exe " + wsl_args(distro, user, cmd, verb)
@@ -59,11 +85,52 @@ def _q(s: str) -> str: # a PowerShell single-quoted string
59
85
  return "'" + s.replace("'", "''") + "'"
60
86
 
61
87
 
62
- def enable_script(name: str, args: str, folder: str = STARTUP, target: str = WSL_EXE) -> str:
63
- """`target` is a PowerShell expression (wsl.exe), or a path made one with _q (native Windows: the launcher)."""
88
+ def enable_script(name: str, args: str, folder: str = STARTUP, target: str = WSL_EXE, icon: str = "") -> str:
89
+ """`target` is a PowerShell expression (wsl.exe), or a path made one with _q (native Windows: the launcher). `icon`: a
90
+ PowerShell expression for an .ico (else the target's own icon — a console window's, for conhost)."""
64
91
  return (f"$p = Join-Path ({folder}) {_q(name)}; $s = (New-Object -ComObject WScript.Shell).CreateShortcut($p); "
65
92
  f"$s.TargetPath = {target}; $s.Arguments = {_q(args)}; $s.WorkingDirectory = $env:USERPROFILE; $s.WindowStyle = 7; "
66
- "$s.Description = 'Witbitz Code'; $s.Save(); 'ok'")
93
+ + (f"if (Test-Path ({icon})) {{ $s.IconLocation = ({icon}) + ',0' }}; " if icon else "")
94
+ + "$s.Description = 'Witbitz Code'; $s.Save(); 'ok'")
95
+
96
+
97
+ # The shortcuts' icons (icons/ in this package; the owner, 2026-10-09: the WSL entry "has a terminal icon"). WSL's is
98
+ # copied to a Windows folder of its own — not under %LOCALAPPDATA%\Witbitz, which the Windows copy's uninstall deletes.
99
+ ICON_DIR = "(Join-Path $env:LOCALAPPDATA 'Witbitz-WSL')"
100
+ WSL_ICON = f"(Join-Path {ICON_DIR} 'witbitz-code-wsl.ico')"
101
+
102
+
103
+ def icon_file(name: str) -> str:
104
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "icons", name)
105
+
106
+
107
+ def linux_path(windows: str, run: Callable = subprocess.run) -> str:
108
+ """wslpath -u — '' when it cannot say."""
109
+ try:
110
+ r = run(["wslpath", "-u", windows], capture_output=True, text=True, timeout=10)
111
+ return (r.stdout or "").strip() if r.returncode == 0 else ""
112
+ except (OSError, subprocess.SubprocessError):
113
+ return ""
114
+
115
+
116
+ def copy_icon(*, ps: Callable[[str], tuple[int, str]] | None = None, to_linux: Callable[[str], str] = linux_path,
117
+ src: str | None = None) -> bool:
118
+ """Put WSL's icon in its Windows folder, written from THIS side through /mnt/c (Windows could not read the
119
+ \\\\wsl.localhost path of the package on the owner's laptop: "does not exist"). → whether it is there now."""
120
+ src = src or icon_file("witbitz-code-wsl.ico")
121
+ code, out = (ps or run_ps)("$env:LOCALAPPDATA")
122
+ local = out.splitlines()[-1].strip() if code == 0 and out else ""
123
+ if not re.fullmatch(r"[A-Za-z]:\\[^\x00-\x1f]+", local): # only a real Windows folder ("C:\Users\…\Local")
124
+ return False
125
+ dest_dir = to_linux(local)
126
+ if not dest_dir or not os.path.isabs(dest_dir) or not os.path.exists(src):
127
+ return False
128
+ try:
129
+ os.makedirs(os.path.join(dest_dir, "Witbitz-WSL"), exist_ok=True)
130
+ shutil.copyfile(src, os.path.join(dest_dir, "Witbitz-WSL", "witbitz-code-wsl.ico"))
131
+ return True
132
+ except OSError:
133
+ return False
67
134
 
68
135
 
69
136
  def state_script(name: str, folder: str = STARTUP) -> str:
@@ -101,11 +168,12 @@ def startup_here() -> dict | None:
101
168
  """What the WSL shortcut would be — None off WSL, with no Windows PowerShell to write it, or no witbitz-code on PATH."""
102
169
  if not ports.on_wsl() or not (os.path.exists(POWERSHELL) or shutil.which("powershell.exe")):
103
170
  return None
104
- exe = shutil.which("witbitz-code")
171
+ exe = this_program()
105
172
  if not exe:
106
173
  return None
107
174
  distro = os.environ.get("WSL_DISTRO_NAME", "")
108
- return {"name": shortcut_name(distro), "args": headless_args(distro, getpass.getuser(), [exe], UI_QUIET), "target": CONHOST}
175
+ return {"name": shortcut_name(distro), "args": headless_args(distro, getpass.getuser(), wsl_command(exe), UI_QUIET), "target": CONHOST,
176
+ "icon": WSL_ICON}
109
177
 
110
178
 
111
179
  def menu_name(distro: str) -> str:
@@ -119,7 +187,7 @@ def launch_script(args: str) -> str:
119
187
 
120
188
 
121
189
  def launch_background(*, ps: Callable[[str], tuple[int, str]] = run_ps, which: Callable[[str], str | None] = shutil.which,
122
- on_wsl: Callable[[], bool] = ports.on_wsl) -> tuple[bool, str]:
190
+ on_wsl: Callable[[], bool] = ports.on_wsl, copy: Callable[..., bool] | None = None) -> tuple[bool, str]:
123
191
  """WSL: start the page's manager with no window, held by a hidden wsl.exe on the Windows side, and let it open the
124
192
  page in the Windows browser — setup's hand-off (this terminal may then close). → (ok, why)."""
125
193
  if not on_wsl():
@@ -127,14 +195,16 @@ def launch_background(*, ps: Callable[[str], tuple[int, str]] = run_ps, which: C
127
195
  exe = which("witbitz-code")
128
196
  if not exe:
129
197
  return False, "witbitz-code is not on PATH"
198
+ copy = copy or copy_icon
130
199
  distro = os.environ.get("WSL_DISTRO_NAME", "")
131
- args = headless_args(distro, getpass.getuser(), [exe], ["ui", "--hidden"])
200
+ args = headless_args(distro, getpass.getuser(), wsl_command(exe), ["ui", "--hidden"])
132
201
  # The Start-menu entry first (it opens this page again, starting it if it is not running), then the start itself.
133
- menu = enable_script(menu_name(distro), args, folder=PROGRAMS, target=CONHOST)
202
+ icon = WSL_ICON if copy(ps=ps) else ""
203
+ menu = enable_script(menu_name(distro), args, folder=PROGRAMS, target=CONHOST, icon=icon)
134
204
  # A login start written by 1.4.0–1.5.x opens a minimized window: an existing one is rewritten to start hidden too.
135
- quiet = headless_args(distro, getpass.getuser(), [exe], UI_QUIET)
205
+ quiet = headless_args(distro, getpass.getuser(), wsl_command(exe), UI_QUIET)
136
206
  login = (f"if (Test-Path (Join-Path ({STARTUP}) {_q(shortcut_name(distro))})) {{ "
137
- f"{enable_script(shortcut_name(distro), quiet, target=CONHOST)} | Out-Null }}")
207
+ f"{enable_script(shortcut_name(distro), quiet, target=CONHOST, icon=icon)} | Out-Null }}")
138
208
  code, out = ps(f"try {{ {menu} | Out-Null; {login} }} catch {{ }}; {launch_script(args)}")
139
209
  ok = code == 0 and bool(out) and out.splitlines()[-1].strip() == "ok"
140
210
  return ok, "" if ok else ("Windows PowerShell could not be started" if code == -1 else f"Windows PowerShell answered {code}")
@@ -148,7 +218,8 @@ def windows_startup_spec(env: dict | None = None, which: Callable[[str], str | N
148
218
  env = os.environ if env is None else env
149
219
  pyw = os.path.join(os.path.dirname(executable or sys.executable), "pythonw.exe")
150
220
  if exists(pyw):
151
- return {"name": "Witbitz Code.lnk", "args": _join(["-m", "witbitz_code", *WIN_QUIET]), "target": _q(pyw)}
221
+ return {"name": "Witbitz Code.lnk", "args": _join(["-m", "witbitz_code", *WIN_QUIET]), "target": _q(pyw),
222
+ "icon": _q(icon_file("witbitz-code.ico"))}
152
223
  launcher = os.path.join(env.get("LOCALAPPDATA", ""), "Witbitz", "Witbitz Code.cmd")
153
224
  if env.get("LOCALAPPDATA") and exists(launcher):
154
225
  return {"name": "Witbitz Code.lnk", "args": "--no-browser", "target": _q(launcher)}
@@ -171,7 +242,7 @@ def startup_state(spec: dict, *, ps: Callable[[str], tuple[int, str]] = run_ps)
171
242
 
172
243
 
173
244
  def startup_enable(spec: dict, *, ps: Callable[[str], tuple[int, str]] = run_ps) -> tuple[bool, str]:
174
- code, out = ps(enable_script(spec["name"], spec["args"], target=spec.get("target", WSL_EXE)))
245
+ code, out = ps(enable_script(spec["name"], spec["args"], target=spec.get("target", WSL_EXE), icon=spec.get("icon", "")))
175
246
  ok = code == 0 and out.splitlines()[-1].strip() == "ok" if out else False
176
247
  return ok, "" if ok else (f"Windows PowerShell answered {code}" if code != -1 else "Windows PowerShell could not be started")
177
248
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -56,6 +56,8 @@ src/witbitz_code.egg-info/dependency_links.txt
56
56
  src/witbitz_code.egg-info/entry_points.txt
57
57
  src/witbitz_code.egg-info/requires.txt
58
58
  src/witbitz_code.egg-info/top_level.txt
59
+ src/witbitz_code/icons/witbitz-code-wsl.ico
60
+ src/witbitz_code/icons/witbitz-code.ico
59
61
  src/witbitz_code/opencode_plugins/notes-init.md
60
62
  src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
61
63
  src/witbitz_code/opencode_plugins/witbitz-notes.js
@@ -72,7 +72,7 @@ class Fake:
72
72
  remove_opencode_program=lambda info: self.done.append(("opencode",)) or {"ok": True, "said": []},
73
73
  package=lambda: {"kind": self._pkg, "hint": "python3 -m pip uninstall witbitz-code"},
74
74
  remove_package=lambda info: self.done.append(("package",)) or {"ok": True},
75
- **{"login_start": lambda: None, "menu_remove": lambda: False, **over}) # never this machine's Windows Startup folder or Start menu
75
+ **{"login_start": lambda: None, "menu_remove": lambda: False, "running": lambda: [], **over}) # never this machine's Windows Startup folder or Start menu
76
76
 
77
77
 
78
78
  PAIR = {"name": "desk", "account": "a@x.com", "idx": {"room": "r"}, "computerId": "c"}
@@ -174,3 +174,39 @@ def test_on_wsl_the_windows_startup_shortcut_setup_made_goes_too():
174
174
  assert gone == ["Witbitz Code (WSL UbuntuD).lnk", "menu"]
175
175
  assert any('Removed "Witbitz Code (WSL UbuntuD)" from the Start menu' in line for line in f.said)
176
176
  assert any('Removed the Windows Startup shortcut "Witbitz Code (WSL UbuntuD)"' in line for line in f.said)
177
+
178
+
179
+ def test_the_running_witbitz_code_is_stopped_before_anything_is_deleted():
180
+ """The owner, 2026-10-09 (Windows): the background copy's OpenCode held opencode.db and the delete died WinError 32."""
181
+ f = Fake(pairs=[PAIR], sessions=True, answers=["y", "y"])
182
+ run(un.run_uninstall(f.deps(running=lambda: [{"pid": 8420, "cmd": "the Witbitz Code page"}, {"pid": 19228, "cmd": "opencode.exe"}])))
183
+ stops = [i for i, x in enumerate(f.done) if x[0] == "stop"]
184
+ assert [f.done[i] for i in stops] == [("stop", 8420), ("stop", 19228)]
185
+ assert stops[-1] < f.done.index(("sessions",)) and stops[-1] < f.done.index(("rm", "/h/.witbitz/code"))
186
+ assert any("Stopped the running witbitz-code: the Witbitz Code page (process 8420)" in line for line in f.said)
187
+
188
+
189
+ def test_a_session_file_still_open_is_said_not_a_crash():
190
+ f = Fake(sessions=True, answers=["y", "y"])
191
+ d = f.deps()
192
+ d.delete_sessions = lambda: ["opencode.db"]
193
+ r = run(un.run_uninstall(d))
194
+ assert r.get("done") is not False
195
+ assert any("Could not delete opencode.db" in line for line in f.said)
196
+
197
+
198
+ def test_what_runs_witbitz_code_is_the_page_s_manager_and_the_paired_ports_owners(tmp_path):
199
+ lock = tmp_path / "ui.json"
200
+ lock.write_text('{"port": 4290, "token": "t", "pid": 8420}')
201
+ owners = {4097: [{"pid": 19228, "cmd": "opencode.exe"}], 4197: [{"pid": 8420, "cmd": "python.exe"}]}
202
+ got = un.running_witbitz(lock=lock, pairings=lambda: [{"opencodeUrl": "http://127.0.0.1:4097"}],
203
+ owners=lambda q: owners.get(q, []), alive=lambda pid: pid == 8420)
204
+ assert got == [{"pid": 8420, "cmd": "the Witbitz Code page"}, {"pid": 19228, "cmd": "opencode.exe"}]
205
+ lock.write_text('{"pid": 8420}')
206
+ assert un.running_witbitz(lock=lock, pairings=lambda: [], owners=lambda q: [], alive=lambda pid: False) == []
207
+
208
+
209
+ def test_windows_finds_opencode_by_name():
210
+ out = '"opencode.exe","19228","Console","1","95,000 K","Running","ALONASUS\\shter","0:00:03","N/A"\n' \
211
+ '"opencode.exe","555","Console","1","1 K","Running","OTHER\\bob","0:00:01","N/A"\n'
212
+ assert un.win_opencode_processes(run=lambda argv: out, user="shter") == [{"pid": 19228, "cmd": "opencode.exe"}]
@@ -47,7 +47,8 @@ def test_the_windows_login_start_runs_pythonw_in_the_background_when_it_is_there
47
47
  exe = r"C:\Users\a\AppData\Local\Witbitz\tools\witbitz-code\Scripts\python.exe"
48
48
  pyw = __import__("os").path.join(__import__("os").path.dirname(exe), "pythonw.exe")
49
49
  spec = ws.windows_startup_spec(env={"LOCALAPPDATA": r"C:\Users\a\AppData\Local"}, exists=lambda p: p == pyw, executable=exe)
50
- assert spec == {"name": "Witbitz Code.lnk", "args": "-m witbitz_code ui --no-browser", "target": "'" + pyw + "'"}
50
+ assert {k: spec[k] for k in ("name", "args", "target")} == {"name": "Witbitz Code.lnk", "args": "-m witbitz_code ui --no-browser", "target": "'" + pyw + "'"}
51
+ assert spec["icon"].endswith("witbitz-code.ico'"), "the robot, not the launcher's icon"
51
52
  launcher_only = ws.windows_startup_spec(env={"LOCALAPPDATA": "L"}, exists=lambda p: p.endswith("Witbitz Code.cmd"), executable=exe)
52
53
  assert launcher_only["args"] == "--no-browser" and launcher_only["target"].endswith("Witbitz Code.cmd'")
53
54
 
@@ -56,12 +57,46 @@ def test_wsl_runs_headless_held_by_a_hidden_wsl_exe():
56
57
  from witbitz_code import wslstart as ws
57
58
  seen = []
58
59
  ok, why = ws.launch_background(ps=lambda script: seen.append(script) or (0, "ok"), which=lambda n: "/home/a/.local/bin/witbitz-code",
59
- on_wsl=lambda: True)
60
+ on_wsl=lambda: True, copy=lambda ps: False)
60
61
  assert ok and why == ""
61
62
  assert "GetFolderPath('Programs')" in seen[0] and "'Witbitz Code (WSL " in seen[0], "its own Start-menu entry"
62
63
  assert seen[0].index("Programs") < seen[0].index("Start-Process"), "the entry is written before the start"
63
64
  assert "if (Test-Path (Join-Path ([Environment]::GetFolderPath('Startup'))" in seen[0], "an older login start is rewritten hidden"
64
- assert "conhost.exe" in seen[0] and "'--headless wsl.exe " in seen[0] and "-- /home/a/.local/bin/witbitz-code ui --hidden'" in seen[0]
65
+ assert "conhost.exe" in seen[0] and "'--headless wsl.exe " in seen[0] and " /home/a/.local/bin/witbitz-code ui --hidden'" in seen[0] and "-- env PATH=" in seen[0]
65
66
  assert ws.launch_background(ps=lambda s: (0, "ok"), on_wsl=lambda: False) == (False, "not in WSL")
66
- assert ws.launch_background(ps=lambda s: (1, ""), which=lambda n: "/w", on_wsl=lambda: True) == (False, "Windows PowerShell answered 1")
67
+ assert ws.launch_background(ps=lambda s: (1, ""), which=lambda n: "/w", on_wsl=lambda: True, copy=lambda ps: False) == (False, "Windows PowerShell answered 1")
67
68
  assert ws.headless_args("UbuntuD", "u", ["/w"], ws.UI_QUIET) == "--headless wsl.exe -d UbuntuD -u u --cd ~ -- /w ui --no-browser --hidden"
69
+
70
+
71
+ def test_a_hidden_start_carries_this_terminal_s_path():
72
+ """wsl.exe -- reads no ~/.bashrc: the hidden copy found no OpenCode (nvm's) and no witbitz-code (the owner, 2026-10-09)."""
73
+ from witbitz_code import wslstart as ws
74
+ path = "/home/u/.nvm/versions/node/v24.3.0/bin:/home/u/.local/bin:/usr/bin:/mnt/c/Program Files/Git/cmd:/mnt/c/WINDOWS/system32:/usr/bin::/x;rm"
75
+ assert ws.carried_path(path) == "/home/u/.nvm/versions/node/v24.3.0/bin:/home/u/.local/bin:/usr/bin:/mnt/c/WINDOWS/system32"
76
+ assert ws.wsl_command("/home/u/.local/bin/witbitz-code", path)[:2] == ["env", "PATH=" + ws.carried_path(path)]
77
+
78
+
79
+ def test_the_shortcuts_carry_witbitz_icons_and_wsl_copies_its_own_to_windows(monkeypatch):
80
+ """The owner, 2026-10-09: the WSL entry "has a terminal icon" (conhost's)."""
81
+ import os
82
+ from witbitz_code import wslstart as ws
83
+ assert os.path.getsize(ws.icon_file("witbitz-code.ico")) > 1000 and os.path.getsize(ws.icon_file("witbitz-code-wsl.ico")) > 1000
84
+ s = ws.enable_script("x.lnk", "-a", icon=ws.WSL_ICON)
85
+ assert "if (Test-Path ((Join-Path (Join-Path $env:LOCALAPPDATA 'Witbitz-WSL') 'witbitz-code-wsl.ico')))" in s and "IconLocation" in s
86
+ assert "IconLocation" not in ws.enable_script("x.lnk", "-a"), "no icon asked → the script is as before (Node parity)"
87
+ seen = []
88
+ ws.launch_background(ps=lambda script: seen.append(script) or (0, "ok"), which=lambda n: "/w", on_wsl=lambda: True, copy=lambda ps: True)
89
+ assert "IconLocation" in seen[0] and "Witbitz-WSL" in seen[0]
90
+ seen.clear()
91
+ ws.launch_background(ps=lambda script: seen.append(script) or (0, "ok"), which=lambda n: "/w", on_wsl=lambda: True, copy=lambda ps: False)
92
+ assert "IconLocation" not in seen[0], "no icon copied → the shortcut keeps its own icon"
93
+
94
+
95
+ def test_the_icon_is_written_into_windows_local_app_data_from_this_side(tmp_path):
96
+ from witbitz_code import wslstart as ws
97
+ win = tmp_path / "Local"
98
+ win.mkdir()
99
+ ok = ws.copy_icon(ps=lambda s: (0, "C:\\Users\\a\\AppData\\Local"), to_linux=lambda p: str(win) if p.endswith("Local") else "")
100
+ assert ok and (win / "Witbitz-WSL" / "witbitz-code-wsl.ico").read_bytes() == open(ws.icon_file("witbitz-code-wsl.ico"), "rb").read()
101
+ assert ws.copy_icon(ps=lambda s: (-1, ""), to_linux=lambda p: str(win)) is False
102
+ assert ws.copy_icon(ps=lambda s: (0, "ok"), to_linux=lambda p: "ok") is False, "not a Windows folder: never written"
File without changes
File without changes
File without changes