witbitz-code 1.6.7__tar.gz → 1.6.9__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.7/src/witbitz_code.egg-info → witbitz_code-1.6.9}/PKG-INFO +2 -2
  2. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/README.md +1 -1
  3. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/pyproject.toml +1 -1
  4. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/__init__.py +1 -1
  5. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/mkdir.py +13 -3
  6. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/uninstall.py +25 -6
  7. {witbitz_code-1.6.7 → witbitz_code-1.6.9/src/witbitz_code.egg-info}/PKG-INFO +2 -2
  8. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_dirs.py +11 -1
  9. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_uninstall.py +25 -0
  10. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/LICENSE +0 -0
  11. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/setup.cfg +0 -0
  12. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/__main__.py +0 -0
  13. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/_js.py +0 -0
  14. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/account.py +0 -0
  15. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/asks.py +0 -0
  16. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/attachments.py +0 -0
  17. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/auto.py +0 -0
  18. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/auto_runner.py +0 -0
  19. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/cli.py +0 -0
  20. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/codekeys.py +0 -0
  21. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/codetools.py +0 -0
  22. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/compress.py +0 -0
  23. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/computers.py +0 -0
  24. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/confidential.py +0 -0
  25. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/confidential_models.json +0 -0
  26. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/connector.py +0 -0
  27. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/devicelink.py +0 -0
  28. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/dirs.py +0 -0
  29. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/folders.py +0 -0
  30. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/gateway.py +0 -0
  31. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  32. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  33. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/keys.py +0 -0
  34. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/link.py +0 -0
  35. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/models.py +0 -0
  36. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/net.py +0 -0
  37. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/offered_models.json +0 -0
  38. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  39. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  40. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  41. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  42. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/outputs.py +0 -0
  43. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/pair.py +0 -0
  44. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/pairings.py +0 -0
  45. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/payers.py +0 -0
  46. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/plugins.py +0 -0
  47. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/policy.py +0 -0
  48. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/ports.py +0 -0
  49. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/qr.py +0 -0
  50. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/receipt.py +0 -0
  51. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/relay.py +0 -0
  52. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/seen.py +0 -0
  53. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/tinfoil_read.py +0 -0
  54. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/tools_catalog.json +0 -0
  55. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/tools_probe.py +0 -0
  56. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/toolsetup.py +0 -0
  57. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/ui.py +0 -0
  58. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/ui_static/app.css +0 -0
  59. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/ui_static/app.js +0 -0
  60. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/ui_static/index.html +0 -0
  61. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/uitools.py +0 -0
  62. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/winagent.py +0 -0
  63. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/winbg.py +0 -0
  64. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/winconsole.py +0 -0
  65. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/wizard.py +0 -0
  66. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code/wslstart.py +0 -0
  67. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  68. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  69. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  70. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code.egg-info/requires.txt +0 -0
  71. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/src/witbitz_code.egg-info/top_level.txt +0 -0
  72. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_asks.py +0 -0
  73. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_attachments.py +0 -0
  74. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_auto.py +0 -0
  75. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_cli.py +0 -0
  76. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_codekeys.py +0 -0
  77. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_codetools.py +0 -0
  78. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_confidential.py +0 -0
  79. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_connector_e2e.py +0 -0
  80. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_devicelink.py +0 -0
  81. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_files_and_compress.py +0 -0
  82. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_folders.py +0 -0
  83. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_outputs.py +0 -0
  84. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_pair_flows.py +0 -0
  85. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_plugins.py +0 -0
  86. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_policy.py +0 -0
  87. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_ports.py +0 -0
  88. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_ports_win.py +0 -0
  89. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_registry.py +0 -0
  90. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_relay_codec.py +0 -0
  91. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_setup.py +0 -0
  92. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_tools_probe.py +0 -0
  93. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_toolsetup.py +0 -0
  94. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_ui.py +0 -0
  95. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_uitools.py +0 -0
  96. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_winagent.py +0 -0
  97. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_winbg.py +0 -0
  98. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_winconsole.py +0 -0
  99. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_windows_files.py +0 -0
  100. {witbitz_code-1.6.7 → witbitz_code-1.6.9}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.7
3
+ Version: 1.6.9
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
@@ -50,7 +50,7 @@ witbitz-code setup
50
50
  ```
51
51
 
52
52
  `setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
53
- this computer (scan the QR code in Spaces → Settings → Privacy & security → Back up & recovery → Add a device — scan its code), which gives the computer **its
53
+ this computer (scan the QR code in Spaces → Settings → Privacy & security → Add a device), which gives the computer **its
54
54
  own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
55
55
  Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
56
56
  starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
@@ -24,7 +24,7 @@ witbitz-code setup
24
24
  ```
25
25
 
26
26
  `setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
27
- this computer (scan the QR code in Spaces → Settings → Privacy & security → Back up & recovery → Add a device — scan its code), which gives the computer **its
27
+ this computer (scan the QR code in Spaces → Settings → Privacy & security → Add a device), which gives the computer **its
28
28
  own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
29
29
  Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
30
30
  starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.6.7"
7
+ version = "1.6.9"
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"
@@ -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.7"
7
+ __version__ = "1.6.9"
@@ -12,9 +12,10 @@ import ntpath
12
12
  import os
13
13
  import posixpath
14
14
  import re
15
+ import unicodedata
15
16
 
16
17
  MKDIR_ROUTE = "/witbitz/mkdir"
17
- _CLEAN = re.compile(r"^[A-Za-z0-9_ ()-][A-Za-z0-9._ ()-]*$")
18
+ _PUNCT = set("._ ()-")
18
19
  _WIN_ABS = re.compile(r"^[A-Za-z]:[\\/]")
19
20
 
20
21
 
@@ -22,12 +23,21 @@ def _error(st: int, message: str) -> tuple[int, str]:
22
23
  return st, json.dumps({"error": message}, ensure_ascii=False, separators=(",", ":"))
23
24
 
24
25
 
26
+ def _ok_char(ch: str) -> bool:
27
+ # letters, digits and combining marks of ANY script (Hebrew, Arabic, …) plus . _ space ( ) - — ⇄ code-mkdir.mjs. A control
28
+ # or an invisible direction mark (category C*) is not a letter, so it stays out.
29
+ return ch in _PUNCT or unicodedata.category(ch)[0] in ("L", "N", "M")
30
+
31
+
25
32
  def _clean(n: object) -> bool:
26
- s = str(n or "") if isinstance(n, str) else ""
27
- return bool(s) and len(s) <= 120 and bool(_CLEAN.match(s)) and ".." not in s and not re.search(r"[/\\\0]", s) and not re.search(r"[^ -~]", s)
33
+ s = unicodedata.normalize("NFC", n) if isinstance(n, str) else ""
34
+ return (bool(s) and len(s) <= 120 and s[0] != "." and all(_ok_char(c) for c in s)
35
+ and ".." not in s and not re.search(r"[/\\\0]", s))
28
36
 
29
37
 
30
38
  def mkdir_in(*, root: object, rel: object, name: object) -> tuple[int, str]:
39
+ if isinstance(name, str):
40
+ name = unicodedata.normalize("NFC", name) # one spelling of a name, whatever keyboard typed it
31
41
  if not _clean(name):
32
42
  return _error(400, "a folder name: no /, . or .., and not hidden")
33
43
  win = isinstance(root, str) and bool(_WIN_ABS.match(root))
@@ -302,11 +302,26 @@ def remove_opencode_program(info: dict) -> dict:
302
302
  return {"ok": False, "why": f"{info['path']} is still there", "said": said} if os.path.exists(info["path"]) else {"ok": True, "said": said}
303
303
 
304
304
 
305
+ WINDOWS_REINSTALL = "irm https://app.witbitz.chat/code.ps1 | iex"
306
+
307
+
308
+ def windows_installer_root() -> str:
309
+ """%LOCALAPPDATA%\\Witbitz — where the Windows installer (code.ps1) puts this program, OpenCode and the uninstaller."""
310
+ base = os.environ.get("LOCALAPPDATA") or ""
311
+ return (base.rstrip("\\/") + "\\Witbitz") if base else ""
312
+
313
+
305
314
  def pipx_install() -> dict:
306
- """This package as pipx installed it → {"kind": "pipx", "venv"} (removable with `pipx uninstall`), else pip / other."""
315
+ """This package as pipx installed it → {"kind": "pipx", "venv"} (removable with `pipx uninstall`); as the Windows
316
+ installer (code.ps1) installed it → {"kind": "windows-installer"} (its own uninstaller removes the program, OpenCode
317
+ and the folder right after this — recognised by the flag it sets, or by where the program lives, so an uninstaller
318
+ written by an older install is covered too); else pip / other."""
307
319
  prefix = Path(sys.prefix)
308
320
  if (prefix / "pipx_metadata.json").exists(): # pipx writes it into every venv it makes, wherever PIPX_HOME is
309
321
  return {"kind": "pipx", "venv": str(prefix)}
322
+ root = windows_installer_root()
323
+ if os.environ.get("WITBITZ_INSTALLER") == "windows" or (root and str(sys.prefix).lower().startswith(root.lower() + "\\")):
324
+ return {"kind": "windows-installer"}
310
325
  return {"kind": "pip", "hint": f"{Path(sys.executable).name} -m pip uninstall witbitz-code"}
311
326
 
312
327
 
@@ -364,9 +379,12 @@ async def run_uninstall(d: Deps) -> dict:
364
379
  d.say(f" • delete {d.code_dir} (the pairing secrets, files you attached in Code, the Auto-mode log)")
365
380
  if owned:
366
381
  d.say(f" • remove what witbitz-code added to OpenCode ({len(owned)} item{'s' if len(owned) > 1 else ''}: the project-notes and progress plugins, the media and code tools)")
382
+ winst = pkg["kind"] == "windows-installer"
367
383
  if pkg["kind"] == "pipx":
368
384
  d.say(" • uninstall the witbitz-code program (pipx uninstall witbitz-code)")
369
- d.say("It keeps OpenCode and your projects — and, unless you say so next, your keys, notes and sessions.\n")
385
+ if winst:
386
+ d.say(" • then the Windows uninstaller removes the program, OpenCode and the Witbitz folder")
387
+ d.say(("It keeps your projects" if winst else "It keeps OpenCode and your projects") + " — and, unless you say so next, your keys, notes and sessions.\n")
370
388
  if not d.yes and not d.ask("Continue? [y/N] ").strip().lower().startswith("y"):
371
389
  d.say("Nothing changed.")
372
390
  return {"done": False}
@@ -469,21 +487,22 @@ async def run_uninstall(d: Deps) -> dict:
469
487
  pkg_gone = bool(r.get("ok"))
470
488
  d.say("✓ Uninstalled the witbitz-code program" if pkg_gone else f"✖ Could not uninstall the program ({r.get('why')}) — run: pipx uninstall witbitz-code")
471
489
  d.say("")
472
- if pairs:
490
+ winst = pkg["kind"] == "windows-installer"
491
+ if pairs and not winst: # the Windows uninstaller stops whatever still runs from its folder
473
492
  d.say("If witbitz-code serve is still running in a terminal window, close that window (Ctrl-C).")
474
493
  if left:
475
494
  d.say(f"Your phone may still list {', '.join(chr(34) + str(p.get('name')) + chr(34) for p in left)}: open Code → Settings → Remove there"
476
495
  " — that also cancels its coding keys.")
477
- if pkg["kind"] != "pipx":
496
+ if pkg["kind"] not in ("pipx", "windows-installer"):
478
497
  d.say(f"To remove the program itself: {pkg.get('hint')}")
479
498
  if oc_gone:
480
499
  tail = "" if remove_sessions else f"\nOpenCode's own data stays: {sessions['dir']} (sessions, saved logins) and its settings in ~/.config/opencode — delete those folders to remove everything."
481
500
  d.say(f"witbitz-code and OpenCode are removed. Open a new terminal window so it forgets the old PATH.{tail}")
482
- elif oc["kind"] != "none":
501
+ elif oc["kind"] != "none" and not winst:
483
502
  d.say(f"witbitz-code is removed. OpenCode is still installed — to remove it too: {oc.get('hint')}")
484
503
  else:
485
504
  d.say("witbitz-code is removed.")
486
- d.say("To start again: pipx install witbitz-code, then witbitz-code setup.")
505
+ d.say(f"To install again: {WINDOWS_REINSTALL}" if winst else "To start again: pipx install witbitz-code, then witbitz-code setup.")
487
506
  return {"done": True, "left": len(left), "package": pkg_gone}
488
507
 
489
508
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.7
3
+ Version: 1.6.9
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
@@ -50,7 +50,7 @@ witbitz-code setup
50
50
  ```
51
51
 
52
52
  `setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
53
- this computer (scan the QR code in Spaces → Settings → Privacy & security → Back up & recovery → Add a device — scan its code), which gives the computer **its
53
+ this computer (scan the QR code in Spaces → Settings → Privacy & security → Add a device), which gives the computer **its
54
54
  own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
55
55
  Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
56
56
  starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
@@ -52,9 +52,19 @@ def test_the_javascript_lists_the_same():
52
52
 
53
53
 
54
54
  def test_new_folder_names_are_one_clean_leaf_on_both_kinds_of_root(tmp_path):
55
- for bad in ["..", ".x", "a/b", "a\\b", "\\x", "", "a\x00b", "é"]:
55
+ for bad in ["..", ".x", "a/b", "a\\b", "\\x", "", "a\x00b", "a\u202eb"]: # "é" is a letter now (any script, 2026-10-11)
56
56
  assert mkdir.mkdir_in(root=str(tmp_path), rel=".", name=bad)[0] == 400, bad
57
57
  assert mkdir.mkdir_in(root="relative", rel=".", name="x")[0] == 400
58
58
  assert mkdir.mkdir_in(root="C:\\Users\\nobody-here", rel=".", name="x")[0] == 404, "a Windows root is a root (not on this computer)"
59
59
  assert mkdir.mkdir_in(root=str(tmp_path), rel="..", name="x")[0] == 403, "never above the root"
60
60
  assert mkdir.mkdir_in(root=str(tmp_path), rel=".", name="ok one")[0] == 200 and (tmp_path / "ok one").is_dir()
61
+
62
+
63
+ def test_mkdir_any_script_name_is_made(tmp_path):
64
+ # the owner, 2026-10-11: "making a directory in hebrew does not work" (it was ASCII only) — ⇄ tools/code-mkdir.test.mjs
65
+ for name in ["ערך 3", "עבודה (2)", "Ünïcode", "مشروع", "项目-1"]:
66
+ assert mkdir.mkdir_in(root=str(tmp_path), rel=".", name=name)[0] == 200, name
67
+ for bad in ["x\u202eexe", "a\u200fb", "a\u2066b"]:
68
+ assert mkdir.mkdir_in(root=str(tmp_path), rel=".", name=bad)[0] == 400, repr(bad)
69
+ assert mkdir.mkdir_in(root=str(tmp_path), rel=".", name="U\u0308ber")[0] == 200
70
+ assert "\u00dcber" in [p.name for p in tmp_path.iterdir()], "stored composed (NFC)"
@@ -210,3 +210,28 @@ def test_windows_finds_opencode_by_name():
210
210
  out = '"opencode.exe","19228","Console","1","95,000 K","Running","ALONASUS\\shter","0:00:03","N/A"\n' \
211
211
  '"opencode.exe","555","Console","1","1 K","Running","OTHER\\bob","0:00:01","N/A"\n'
212
212
  assert un.win_opencode_processes(run=lambda argv: out, user="shter") == [{"pid": 19228, "cmd": "opencode.exe"}]
213
+
214
+
215
+ # THE WINDOWS INSTALLER (code.ps1) — the owner, 2026-10-11, Settings → Apps → Witbitz Code → Uninstall printed pip/pipx advice:
216
+ # "To remove the program itself: python.exe -m pip uninstall witbitz-code", "OpenCode is still installed … remove it the
217
+ # way it was installed", "To start again: pipx install witbitz-code". That uninstaller removes the program, OpenCode (in
218
+ # %LOCALAPPDATA%\Witbitz\bin) and the folder right after this, and stops anything still running from it.
219
+ def test_under_the_windows_installer_the_summary_says_what_that_uninstaller_does_not_pip_or_pipx_advice():
220
+ f = Fake(answers=["y"], pairs=[PAIR], oc="installer", pkg="windows-installer")
221
+ r = run(un.run_uninstall(f.deps(yes=True)))
222
+ said = "\n".join(f.said)
223
+ assert r["done"]
224
+ for wrong in ("pip uninstall", "pipx", "OpenCode is still installed", "still running in a terminal window"):
225
+ assert wrong not in said, f"{wrong!r} does not apply to the Windows installer:\n{said}"
226
+ assert "irm https://app.witbitz.chat/code.ps1 | iex" in said, said
227
+ assert ("package",) not in f.done, "the Windows uninstaller removes the program itself"
228
+
229
+
230
+ def test_the_windows_installer_is_recognised_by_where_the_program_lives_even_from_an_older_uninstaller(monkeypatch):
231
+ monkeypatch.delenv("WITBITZ_INSTALLER", raising=False)
232
+ monkeypatch.setenv("LOCALAPPDATA", r"C:\Users\shter\AppData\Local")
233
+ monkeypatch.setattr(un.sys, "prefix", r"C:\Users\shter\AppData\Local\Witbitz\venv")
234
+ assert un.pipx_install()["kind"] == "windows-installer"
235
+ monkeypatch.setattr(un.sys, "prefix", "/home/a/.local/pipx/venvs/witbitz-code")
236
+ monkeypatch.setenv("WITBITZ_INSTALLER", "windows")
237
+ assert un.pipx_install()["kind"] == "windows-installer", "or by the flag the new uninstaller sets"
File without changes
File without changes