witbitz-code 1.9.2__tar.gz → 1.9.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.
Files changed (117) hide show
  1. {witbitz_code-1.9.2/src/witbitz_code.egg-info → witbitz_code-1.9.4}/PKG-INFO +1 -1
  2. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/pyproject.toml +1 -1
  3. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/confidential.py +46 -1
  5. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/ui.py +2 -0
  6. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/ui_static/app.js +24 -7
  7. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/ui_static/index.html +1 -0
  8. {witbitz_code-1.9.2 → witbitz_code-1.9.4/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  9. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_codekeys.py +2 -1
  10. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_confidential.py +32 -0
  11. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_pair_flows.py +11 -11
  12. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_setup.py +4 -4
  13. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_ui.py +64 -9
  14. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_uninstall.py +2 -2
  15. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_wslstart.py +2 -2
  16. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/LICENSE +0 -0
  17. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/README.md +0 -0
  18. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/setup.cfg +0 -0
  19. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/__main__.py +0 -0
  20. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/_js.py +0 -0
  21. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/account.py +0 -0
  22. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/asks.py +0 -0
  23. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/attachments.py +0 -0
  24. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/auto.py +0 -0
  25. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/auto_runner.py +0 -0
  26. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/browse.py +0 -0
  27. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/browsetree.py +0 -0
  28. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/cli.py +0 -0
  29. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/codekeys.py +0 -0
  30. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/codetools.py +0 -0
  31. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/compress.py +0 -0
  32. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/computers.py +0 -0
  33. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/confidential_models.json +0 -0
  34. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/connector.py +0 -0
  35. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/devicelink.py +0 -0
  36. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/dirs.py +0 -0
  37. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/folders.py +0 -0
  38. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/gateway.py +0 -0
  39. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  40. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  41. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/images.py +0 -0
  42. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/keys.py +0 -0
  43. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/link.py +0 -0
  44. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/mkdir.py +0 -0
  45. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/models.py +0 -0
  46. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/net.py +0 -0
  47. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/offered_models.json +0 -0
  48. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  49. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
  50. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
  51. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +0 -0
  52. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +0 -0
  53. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
  54. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  55. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  56. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  57. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/outputs.py +0 -0
  58. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/pair.py +0 -0
  59. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/pairings.py +0 -0
  60. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/payers.py +0 -0
  61. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/plugins.py +0 -0
  62. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/policy.py +0 -0
  63. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/ports.py +0 -0
  64. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/qr.py +0 -0
  65. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/receipt.py +0 -0
  66. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/relay.py +0 -0
  67. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/remind.py +0 -0
  68. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/seccheck.py +0 -0
  69. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/seen.py +0 -0
  70. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/selfupdate.py +0 -0
  71. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/tinfoil_read.py +0 -0
  72. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/tools_catalog.json +0 -0
  73. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/tools_probe.py +0 -0
  74. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/toolsetup.py +0 -0
  75. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/ui_static/app.css +0 -0
  76. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/uitools.py +0 -0
  77. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/uninstall.py +0 -0
  78. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/winagent.py +0 -0
  79. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/winbg.py +0 -0
  80. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/winconsole.py +0 -0
  81. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/wizard.py +0 -0
  82. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code/wslstart.py +0 -0
  83. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  84. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  85. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  86. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code.egg-info/requires.txt +0 -0
  87. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/src/witbitz_code.egg-info/top_level.txt +0 -0
  88. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_asks.py +0 -0
  89. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_attachments.py +0 -0
  90. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_auto.py +0 -0
  91. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_browse.py +0 -0
  92. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_browsetree.py +0 -0
  93. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_cli.py +0 -0
  94. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_codetools.py +0 -0
  95. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_connector_e2e.py +0 -0
  96. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_devicelink.py +0 -0
  97. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_dirs.py +0 -0
  98. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_files_and_compress.py +0 -0
  99. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_folders.py +0 -0
  100. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_images.py +0 -0
  101. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_outputs.py +0 -0
  102. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_plugins.py +0 -0
  103. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_policy.py +0 -0
  104. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_ports.py +0 -0
  105. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_ports_win.py +0 -0
  106. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_registry.py +0 -0
  107. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_relay_codec.py +0 -0
  108. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_remind.py +0 -0
  109. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_seccheck.py +0 -0
  110. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_selfupdate.py +0 -0
  111. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_tools_probe.py +0 -0
  112. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_toolsetup.py +0 -0
  113. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_uitools.py +0 -0
  114. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_winagent.py +0 -0
  115. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_winbg.py +0 -0
  116. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/tests/test_winconsole.py +0 -0
  117. {witbitz_code-1.9.2 → witbitz_code-1.9.4}/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.9.2
3
+ Version: 1.9.4
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.9.2"
7
+ version = "1.9.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"
@@ -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.9.2"
7
+ __version__ = "1.9.4"
@@ -261,6 +261,28 @@ def without_words(frame: str) -> str:
261
261
  return f"data: {dumps({**ev, 'choices': [{**ch, 'delta': rest}, *ev['choices'][1:]]})}\n\n"
262
262
 
263
263
 
264
+ # The confidential default (⇄ tools/code-confidential.mjs CONFIDENTIAL_DEFAULT): what answers when a pick has no verified-enclave
265
+ # route right now. The note says so once per session — a Code task makes many calls.
266
+ CONFIDENTIAL_DEFAULT = "deepseek/deepseek-v4.1-flash"
267
+ _NOTED: dict = {}
268
+
269
+
270
+ def fallback_note(frm: str, by: str, session_id: str = "") -> str:
271
+ if not frm:
272
+ return ""
273
+ k = session_id + "\0" + frm
274
+ if k in _NOTED:
275
+ return ""
276
+ _NOTED[k] = True
277
+ if len(_NOTED) > 500:
278
+ _NOTED.pop(next(iter(_NOTED)))
279
+ return f"\n\n_Answered by {by} — {frm} isn't available confidentially right now._"
280
+
281
+
282
+ def note_frame(note: str, model: str) -> str:
283
+ return "data: " + dumps({"id": "witbitz-note", "object": "chat.completion.chunk", "model": model, "choices": [{"index": 0, "delta": {"content": note}}]}) + "\n\n"
284
+
285
+
264
286
  def completion_of(events: list, model: str) -> dict:
265
287
  """The streamed events as one chat.completion — for a caller that asked without `stream`."""
266
288
  content = reasoning = ""
@@ -652,7 +674,10 @@ class ConfidentialProxy:
652
674
  except Exception: # noqa: BLE001 — progress must never fail a call
653
675
  pass
654
676
 
677
+ fell = {"from": ""} # the pick's label when TrustedRouter had no verified-enclave route for it and the default answered
678
+
655
679
  async def once(words_shown: bool = False) -> None:
680
+ nonlocal body, conf, label
656
681
  ends = self._proof_ends.get(body["model"])
657
682
  hold = attempt["n"] > 1 or (ends is not None and self._now() / 1000 >= ends - HOLD_BEFORE_LAPSE_SEC)
658
683
  n = self._nonce()
@@ -670,6 +695,16 @@ class ConfidentialProxy:
670
695
  used = allowance_used(up.status_code, text)
671
696
  if used:
672
697
  raise used
698
+ # No verified-enclave route for the pick (2026-10-20, ⇄ tools/code-confidential.mjs): refused BEFORE any upstream
699
+ # saw the request — the same request goes again as the confidential default, attested and checked, once.
700
+ if (up.status_code == 400 and "no route candidates" in text.lower() and not fell["from"] and body.get("model") != CONFIDENTIAL_DEFAULT
701
+ and CONFIDENTIAL_DEFAULT in self.models and not res.headers_sent):
702
+ fell["from"] = conf["label"]
703
+ self.log(f"code-confidential: {body['model']} has no verified-enclave route now → {CONFIDENTIAL_DEFAULT} answers")
704
+ body = {**body, "model": CONFIDENTIAL_DEFAULT}
705
+ conf = self.models[CONFIDENTIAL_DEFAULT]
706
+ label = f"{conf['label']} (confidential)"
707
+ return await once(words_shown)
673
708
  if res.headers_sent:
674
709
  raise Refusal(502, f"{label}: TrustedRouter answered {up.status_code} when asked again ({text[:160]})", "upstream_error")
675
710
  await res.head(up.status_code, {"content-type": up.headers.get("content-type", "application/json")})
@@ -710,15 +745,25 @@ class ConfidentialProxy:
710
745
  exp = ((v.get("claims") or {}).get("upstream") or {}).get("verification_expires_at")
711
746
  if isinstance(exp, (int, float)):
712
747
  self._proof_ends[body["model"]] = exp
748
+ note = fallback_note(fell["from"], conf["label"], session_id)
713
749
  if wants_stream:
750
+ noted = not note
714
751
  for f in answer.held():
752
+ if not noted and re.search(r'"finish_reason":"|^data: \[DONE\]', f, re.M):
753
+ await res.write(note_frame(note, body["model"])) # the note goes in before the answer ends
754
+ noted = True
715
755
  out = without_words(f) if words_shown else f
716
756
  if out:
717
757
  await res.write(out)
758
+ if not noted:
759
+ await res.write(note_frame(note, body["model"]))
718
760
  await res.end()
719
761
  return
720
762
  await res.head(200, {"content-type": "application/json"})
721
- await res.end(dumps(completion_of(answer.events(), body["model"])))
763
+ done = completion_of(answer.events(), body["model"])
764
+ if note and done.get("choices") and done["choices"][0].get("message") is not None:
765
+ done["choices"][0]["message"]["content"] = (done["choices"][0]["message"].get("content") or "") + note
766
+ await res.end(dumps(done))
722
767
 
723
768
  try:
724
769
  if not conf.get("vision"):
@@ -382,6 +382,8 @@ class Manager:
382
382
  await self.start_serving()
383
383
  return {"ok": False, "why": next((s for s in reversed(said) if "✖" in s or "pass --account" in s), "could not unpair").replace("witbitz-code: ", "", 1)}
384
384
  self.pair_state = {"state": "idle", "n": self.pair_state.get("n", 0)}
385
+ if self.pairings(): # one account of several unpaired: the others stay reachable (the owner, 2026-10-22: several accounts)
386
+ await self.start_serving()
385
387
  return {"ok": True}
386
388
 
387
389
  async def set_login(self, on: bool) -> dict:
@@ -60,6 +60,13 @@ function render(s) {
60
60
  const who = document.createElement('div'); who.className = 'who'; who.textContent = `${p.name} → ${p.account || 'your account'}`
61
61
  const keys = document.createElement('div'); keys.className = 'keys'; keys.textContent = p.keys || ''
62
62
  li.append(who, keys)
63
+ // SEVERAL ACCOUNTS (the owner, 2026-10-22: "can we make it so it can handle multiple accounts?"): each one leaves on its own
64
+ if (s.paired.length > 1 && p.account) {
65
+ const out = document.createElement('button'); out.type = 'button'; out.className = 'ghost danger small'; out.textContent = 'Unpair ' + p.account
66
+ out.disabled = !!s.busy
67
+ out.onclick = () => act('/api/unpair', { account: p.account }, `Unpair ${p.account}? Code in that account stops reaching this computer; the other accounts keep it.`)
68
+ li.append(out)
69
+ }
63
70
  list.append(li)
64
71
  }
65
72
 
@@ -70,8 +77,10 @@ function render(s) {
70
77
  $('start').textContent = run === 'elsewhere' ? 'Run it here' : 'Start'
71
78
  show($('stop'), paired && run !== 'stopped')
72
79
  show($('pairAgain'), paired && !waiting)
80
+ show($('pairAdd'), paired && !waiting)
73
81
  show($('unpair'), paired && !waiting)
74
- for (const id of ['pair', 'start', 'stop', 'pairAgain', 'unpair', 'pairCancel', 'pairRetry']) $(id).disabled = busy
82
+ $('unpair').textContent = s.paired.length > 1 ? 'Unpair all' : 'Unpair'
83
+ for (const id of ['pair', 'start', 'stop', 'pairAgain', 'pairAdd', 'unpair', 'pairCancel', 'pairRetry']) $(id).disabled = busy
75
84
 
76
85
  show($('pairCard'), waiting || s.pair.state === 'failed')
77
86
  if (waiting) {
@@ -121,7 +130,7 @@ async function refresh(fresh = false) {
121
130
 
122
131
  async function act(path, body, ask) {
123
132
  if (ask && !window.confirm(ask)) return false
124
- for (const id of ['pair', 'start', 'stop', 'pairAgain', 'unpair']) $(id).disabled = true
133
+ for (const id of ['pair', 'start', 'stop', 'pairAgain', 'pairAdd', 'unpair']) $(id).disabled = true
125
134
  let ok = false
126
135
  try {
127
136
  const r = await api(path, body)
@@ -257,7 +266,7 @@ async function quit() {
257
266
  quitting = true
258
267
  try { await api('/api/quit', {}) } catch { /* it is going away */ }
259
268
  note('Witbitz Code stopped. Start menu → Witbitz Code starts it again.', 'bad')
260
- for (const id of ['pair', 'start', 'stop', 'pairAgain', 'unpair', 'quit']) $(id).disabled = true
269
+ for (const id of ['pair', 'start', 'stop', 'pairAgain', 'pairAdd', 'unpair', 'quit']) $(id).disabled = true
261
270
  }
262
271
 
263
272
  document.addEventListener('DOMContentLoaded', () => {
@@ -268,17 +277,25 @@ document.addEventListener('DOMContentLoaded', () => {
268
277
  $('pair').onclick = () => act('/api/pair', {})
269
278
  $('start').onclick = () => act('/api/serve', { on: true })
270
279
  $('stop').onclick = () => act('/api/serve', { on: false }, 'Stop? Code in Spaces will not reach this computer until you start it again.')
271
- $('unpair').onclick = () => {
272
- const account = accountOf()
273
- if (account === null) return
274
- act('/api/unpair', { account }, 'Unpair this computer? It leaves your Spaces account, its coding keys go back, and Code on every device drops it.')
280
+ $('unpair').onclick = async () => {
281
+ const accounts = ((state && state.paired) || []).map((p) => p.account).filter(Boolean)
282
+ if (accounts.length > 1) { // every account, one at a time (the command unpairs one account per call)
283
+ if (!window.confirm(`Unpair all ${accounts.length} accounts? Code in each of them stops reaching this computer, and its coding keys go back.`)) return
284
+ for (const account of accounts) if (!(await act('/api/unpair', { account }))) return
285
+ return
286
+ }
287
+ act('/api/unpair', { account: accounts[0] || '' }, 'Unpair this computer? It leaves your Spaces account, its coding keys go back, and Code on every device drops it.')
275
288
  }
289
+
276
290
  $('pairAgain').onclick = async () => {
277
291
  const account = accountOf()
278
292
  if (account === null) return
279
293
  if (!window.confirm('Pair again? This computer is unpaired first, then shows a new code to scan.')) return
280
294
  if (await act('/api/unpair', { account })) act('/api/pair', {})
281
295
  }
296
+ // ANOTHER ACCOUNT ON THIS COMPUTER: a new code beside the pairings there are (pairings.json holds one per account, on the
297
+ // same OpenCode); nothing is unpaired. Code here pauses while the code is up and comes back when it is scanned or cancelled.
298
+ $('pairAdd').onclick = () => act('/api/pair', {}, 'Add another account? A new code appears — scan it from the other account: Code in Spaces → Computers → Add a computer. Code on this computer pauses until it is scanned or cancelled.')
282
299
  $('pairCancel').onclick = () => act('/api/pair/cancel', {})
283
300
  $('pairRetry').onclick = () => act('/api/pair', {})
284
301
  $('login').onchange = (e) => act('/api/login', { on: e.target.checked })
@@ -25,6 +25,7 @@
25
25
  <button id="start" type="button" hidden>Start</button>
26
26
  <button id="stop" type="button" class="ghost" hidden>Stop</button>
27
27
  <button id="pairAgain" type="button" class="ghost" hidden>Pair again</button>
28
+ <button id="pairAdd" type="button" class="ghost" hidden>Add another account</button>
28
29
  <button id="unpair" type="button" class="ghost danger" hidden>Unpair</button>
29
30
  </div>
30
31
  <p id="note" class="note" hidden></p>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.9.2
3
+ Version: 1.9.4
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
@@ -10,7 +10,7 @@ import json
10
10
 
11
11
  import httpx
12
12
  import pytest
13
- from conftest import requires_node, run_node
13
+ from conftest import requires_server_keys, requires_node, run_node
14
14
 
15
15
  from witbitz_code import codekeys, confidential as cf, models as mdl
16
16
  from witbitz_code.payers import SessionPayers
@@ -47,6 +47,7 @@ def test_a_secret_the_server_seals_to_this_computer_opens_here_and_nowhere_else(
47
47
 
48
48
 
49
49
  @requires_node
50
+ @requires_server_keys
50
51
  def test_this_computers_signature_is_the_one_the_server_accepts_and_only_for_its_op():
51
52
  ident = codekeys.new_identity()
52
53
  entry = {**ident, "keyId": f"{U}.{K}"}
@@ -410,3 +410,35 @@ def test_the_months_allowance_used_is_a_402_that_says_so_never_a_429_opencode_wo
410
410
  with_proxy(t, net=LimitNet(meta, stream.decode("utf-8")), models=models)
411
411
  assert cf.allowance_used(429, '{"error":{"message":"rate limited"}}') is None, "a real rate limit stays a rate limit"
412
412
  assert cf.allowance_used(400, json.dumps(SPEND_LIMIT)) is None
413
+
414
+
415
+ class NoRouteThenReplay(FakeNet):
416
+ """TrustedRouter refuses any model but the default ("No route candidates" — before any upstream sees it); the default
417
+ answers with the captured stream."""
418
+
419
+ def handler(self, req: httpx.Request):
420
+ if "googleapis.com/service_accounts" not in str(req.url) and json.loads(req.content.decode("utf-8")).get("model") != cf.CONFIDENTIAL_DEFAULT:
421
+ self.calls.append({"url": str(req.url), "headers": dict(req.headers), "raw": list(req.headers.raw), "body": req.content.decode("utf-8")})
422
+ return httpx.Response(400, json={"error": {"message": "No route candidates match the requested provider filters"}})
423
+ return super().handler(req)
424
+
425
+
426
+ @needs_fixture
427
+ def test_a_pick_with_no_verified_enclave_route_is_answered_by_the_default_with_one_note_per_session():
428
+ meta, stream, request = fixture()
429
+ net = NoRouteThenReplay(meta, stream.decode("utf-8"))
430
+ logs = []
431
+ glm = {**SAY_OK, "model": "z-ai/glm-5.3"}
432
+
433
+ async def t(post, net_, _p):
434
+ r = await post(glm, {"x-session-id": "ses_fb"})
435
+ text = r.text
436
+ assert [json.loads(c["body"])["model"] for c in net.calls] == ["z-ai/glm-5.3", "deepseek/deepseek-v4.1-flash"], "the pick, then the default — once"
437
+ assert net.calls[1]["body"].encode() == request, "the same request, only the model changed — the receipt signs it"
438
+ assert '"content":"OK"' in text
439
+ assert "Answered by DeepSeek V4.1 Flash — GLM 5.3 isn't available confidentially right now." in text
440
+ assert text.index("Answered by") < text.index("[DONE]")
441
+ again = (await post(glm, {"x-session-id": "ses_fb"})).text
442
+ assert "Answered by" not in again and '"content":"OK"' in again, "said once per session"
443
+ assert any("has no verified-enclave route now" in line for line in logs)
444
+ with_proxy(t, net=net, log=logs.append)
@@ -277,18 +277,18 @@ def test_status_output_is_json_free_of_secrets_even_for_js_written_files():
277
277
 
278
278
 
279
279
  def test_rename_relabels_the_computer_in_the_account_and_here_with_no_scan(monkeypatch):
280
- # WSL's pairing and Windows' were two "AlonASUS" in Code (the owner, 2026-10-09); setup renames WSL's "… (WSL)"
281
- api = MockApi(docs={"computers": with_computer(None, "cmp_wsl_1", {"name": "AlonASUS", "relay": RELAY, "secret": SECRET}, 500)})
282
- entry = {"account": "a@x.com", "idx": IDX, "computerId": "cmp_wsl_1", "secret": SECRET, "name": "AlonASUS", "relay": RELAY,
280
+ # WSL's pairing and Windows' were two "my-laptop" in Code (the owner, 2026-10-09); setup renames WSL's "… (WSL)"
281
+ api = MockApi(docs={"computers": with_computer(None, "cmp_wsl_1", {"name": "my-laptop", "relay": RELAY, "secret": SECRET}, 500)})
282
+ entry = {"account": "a@example.com", "idx": IDX, "computerId": "cmp_wsl_1", "secret": SECRET, "name": "my-laptop", "relay": RELAY,
283
283
  "opencodeUrl": "http://127.0.0.1:4096"}
284
284
  pp.write_pairings({"v": 1, "pairings": [entry]})
285
- assert run(pair.rename(entry, "AlonASUS (WSL)", call=api.call, out=lambda m: None, err=lambda m: None)) is True
285
+ assert run(pair.rename(entry, "my-laptop (WSL)", call=api.call, out=lambda m: None, err=lambda m: None)) is True
286
286
  reg = norm_registry(api.plain("computers"))
287
- assert reg["computers"]["cmp_wsl_1"]["name"] == "AlonASUS (WSL)" and reg["computers"]["cmp_wsl_1"]["secret"] == SECRET
288
- assert [p["name"] for p in pp.read_pairings()["pairings"]] == ["AlonASUS (WSL)"]
287
+ assert reg["computers"]["cmp_wsl_1"]["name"] == "my-laptop (WSL)" and reg["computers"]["cmp_wsl_1"]["secret"] == SECRET
288
+ assert [p["name"] for p in pp.read_pairings()["pairings"]] == ["my-laptop (WSL)"]
289
289
  refused = MockApi(read_status=500)
290
- assert run(pair.rename({**entry, "name": "AlonASUS (WSL)"}, "x", call=refused.call, out=lambda m: None, err=lambda m: None)) is False
291
- assert [p["name"] for p in pp.read_pairings()["pairings"]] == ["AlonASUS (WSL)"], "nothing renamed here when the account refused"
290
+ assert run(pair.rename({**entry, "name": "my-laptop (WSL)"}, "x", call=refused.call, out=lambda m: None, err=lambda m: None)) is False
291
+ assert [p["name"] for p in pp.read_pairings()["pairings"]] == ["my-laptop (WSL)"], "nothing renamed here when the account refused"
292
292
 
293
293
 
294
294
  def test_a_new_pairing_on_wsl_is_named_for_wsl(monkeypatch):
@@ -296,8 +296,8 @@ def test_a_new_pairing_on_wsl_is_named_for_wsl(monkeypatch):
296
296
 
297
297
  from witbitz_code import ports
298
298
 
299
- monkeypatch.setattr(socket, "gethostname", lambda: "AlonASUS")
299
+ monkeypatch.setattr(socket, "gethostname", lambda: "my-laptop")
300
300
  monkeypatch.setattr(ports, "on_wsl", lambda: True)
301
- assert pp.hostname() == "AlonASUS (WSL)"
301
+ assert pp.hostname() == "my-laptop (WSL)"
302
302
  monkeypatch.setattr(ports, "on_wsl", lambda: False)
303
- assert pp.hostname() == "AlonASUS"
303
+ assert pp.hostname() == "my-laptop"
@@ -108,7 +108,7 @@ class Fake:
108
108
  async def pair(port):
109
109
  self.paired.append(port)
110
110
  if not any(p.get("computerId") == "c" for p in self._pairings):
111
- self._pairings.append({"name": "desk", "account": "a@x.com", "opencodeUrl": f"http://127.0.0.1:{port}", "idx": {"room": "r"}, "computerId": "c"})
111
+ self._pairings.append({"name": "desk", "account": "a@example.com", "opencodeUrl": f"http://127.0.0.1:{port}", "idx": {"room": "r"}, "computerId": "c"})
112
112
  if self._gives:
113
113
  self._keyed.add("c")
114
114
  async def listening(port):
@@ -213,14 +213,14 @@ def test_s_stops_setup_and_leaves_the_running_one_alone():
213
213
  def test_a_computer_removed_from_the_account_is_paired_again_and_shows_the_qr():
214
214
  # the owner, 2026-10-08: reinstalled after the computer was removed and "it does not present the QR code" — its pairing
215
215
  # and keys were still on disk, so setup said "✓ Paired" over keys the account had already taken back
216
- f = Fake(pairings=[{"name": "desk", "account": "a@x.com", "opencodeUrl": "http://127.0.0.1:4096", "idx": {"room": "r"}, "computerId": "c"}], keyed={"c"})
216
+ f = Fake(pairings=[{"name": "desk", "account": "a@example.com", "opencodeUrl": "http://127.0.0.1:4096", "idx": {"room": "r"}, "computerId": "c"}], keyed={"c"})
217
217
  asked = []
218
218
  async def keys_state(p):
219
219
  asked.append(p["computerId"])
220
220
  return "live" if f.paired else "revoked"
221
221
  r = run(wizard.run_setup(f.deps(keys_state=keys_state, forget_keys=lambda p: f._keyed.discard(p["computerId"]))))
222
222
  assert f.paired == [4096] and r["paired"] and r["keys"] and f.served == [4096]
223
- assert any('"desk" → a@x.com was removed from your Witbitz account' in line for line in f.said)
223
+ assert any('"desk" → a@example.com was removed from your Witbitz account' in line for line in f.said)
224
224
  assert any("scan the code below" in line for line in f.said)
225
225
  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))])
226
226
 
@@ -248,7 +248,7 @@ def test_windows_and_wsl_each_run_their_own_the_windows_one_moves_off_the_port_w
248
248
 
249
249
 
250
250
  def test_on_wsl_a_pairing_named_like_the_windows_computer_is_renamed_once():
251
- # WSL calls itself by the Windows computer's name: Windows' and WSL's pairings were two "AlonASUS" in Code
251
+ # WSL calls itself by the Windows computer's name: Windows' and WSL's pairings were two "my-laptop" in Code
252
252
  f = Fake(pairings=[DESK], keyed={"c"})
253
253
  renamed = []
254
254
 
@@ -11,7 +11,7 @@ import httpx
11
11
 
12
12
  from witbitz_code import ui
13
13
 
14
- DESK = {"name": "AlonASUS", "account": "a@x.com", "opencodeUrl": "http://127.0.0.1:4097", "idx": {"room": "r"}, "computerId": "c"}
14
+ DESK = {"name": "my-laptop", "account": "a@example.com", "opencodeUrl": "http://127.0.0.1:4097", "idx": {"room": "r"}, "computerId": "c"}
15
15
 
16
16
 
17
17
  def run(coro):
@@ -39,11 +39,11 @@ class World:
39
39
  self.pair_calls.append(argv)
40
40
  if "--unpair" in argv:
41
41
  self.pairings = []
42
- out("witbitz-code: ✓ removed \"AlonASUS\" from a@x.com — every device drops it on its next sync")
42
+ out("witbitz-code: ✓ removed \"my-laptop\" from a@example.com — every device drops it on its next sync")
43
43
  return 0
44
44
  await self.scan.wait()
45
45
  self.pairings = [{**DESK, "opencodeUrl": f"http://127.0.0.1:{argv[argv.index('--port') + 1]}"}]
46
- out("witbitz-code: ✓ paired \"AlonASUS\" with a@x.com (pairings.json)")
46
+ out("witbitz-code: ✓ paired \"my-laptop\" with a@example.com (pairings.json)")
47
47
  return 0
48
48
 
49
49
  def manager(self, listening=()):
@@ -58,7 +58,7 @@ class World:
58
58
  keys_line=keys_line, login_spec=spec, login_state=lambda s: "on" if self.login["on"] else "off",
59
59
  login_enable=lambda s: (self.login.__setitem__("on", True), (True, ""))[1],
60
60
  login_disable=lambda s: self.login.__setitem__("on", False) or True,
61
- label={"label": "AlonASUS", "kind": "Windows"}, log=deque(maxlen=60))
61
+ label={"label": "my-laptop", "kind": "Windows"}, log=deque(maxlen=60))
62
62
 
63
63
 
64
64
  async def with_server(m):
@@ -113,8 +113,8 @@ def test_status_start_and_stop_serving_on_the_paired_port():
113
113
  try:
114
114
  async with httpx.AsyncClient() as c:
115
115
  st = (await c.get(f"{base}/api/status", headers=authed(srv))).json()
116
- assert st["computer"]["label"] == "AlonASUS" and st["serving"]["state"] == "stopped"
117
- assert st["paired"] == [{"name": "AlonASUS", "account": "a@x.com", "port": 4097, "keys": "coding keys: live · models this month $1.2 of $50 · tools up to $5"}]
116
+ assert st["computer"]["label"] == "my-laptop" and st["serving"]["state"] == "stopped"
117
+ assert st["paired"] == [{"name": "my-laptop", "account": "a@example.com", "port": 4097, "keys": "coding keys: live · models this month $1.2 of $50 · tools up to $5"}]
118
118
  assert (await c.post(f"{base}/api/serve", headers=authed(srv), json={"on": True})).json() == {"ok": True}
119
119
  assert await until(lambda: _state(c, base, srv, "online"))
120
120
  assert w.serves == [["--port", "4097"]], "the port this computer is paired for"
@@ -187,11 +187,11 @@ def test_a_cancelled_pairing_goes_back_to_serving_and_the_login_switch_writes_th
187
187
 
188
188
 
189
189
  def test_the_page_names_which_computer_it_controls(monkeypatch):
190
- monkeypatch.setattr(ui.socket, "gethostname", lambda: "AlonASUS")
190
+ monkeypatch.setattr(ui.socket, "gethostname", lambda: "my-laptop")
191
191
  monkeypatch.setattr(ui.ports, "on_wsl", lambda: True)
192
192
  monkeypatch.setattr(ui.sys, "platform", "linux")
193
193
  monkeypatch.setenv("WSL_DISTRO_NAME", "UbuntuD")
194
- assert ui.computer_label() == {"label": "UbuntuD (WSL) on AlonASUS", "kind": "WSL UbuntuD"}
194
+ assert ui.computer_label() == {"label": "UbuntuD (WSL) on my-laptop", "kind": "WSL UbuntuD"}
195
195
 
196
196
 
197
197
  def _other_window_manager(w, held):
@@ -208,7 +208,7 @@ def _other_window_manager(w, held):
208
208
 
209
209
  m = ui.Manager(serve=w.serve, is_listening=is_listening, pair=w.pair, load_pairings=lambda: list(w.pairings),
210
210
  port_owners=lambda q: [{"pid": 19824, "cmd": "python.exe"}] if q in held else [],
211
- stop_pid=stop_pid, login_spec=lambda: None, label={"label": "AlonASUS", "kind": "Windows"}, log=deque(maxlen=60))
211
+ stop_pid=stop_pid, login_spec=lambda: None, label={"label": "my-laptop", "kind": "Windows"}, log=deque(maxlen=60))
212
212
  return m, stopped
213
213
 
214
214
 
@@ -334,3 +334,58 @@ def test_opening_the_page_asks_for_a_release_again_a_status_poll_does_not():
334
334
  await m.shutdown()
335
335
  await srv.close()
336
336
  run(t())
337
+
338
+
339
+ class TwoAccounts(World):
340
+ """A second account pairs beside the first (pairings.json holds one entry per account), and unpairs alone."""
341
+
342
+ async def pair(self, argv, *, link_fn=None, out=print, err=print):
343
+ self.pair_calls.append(argv)
344
+ if "--unpair" in argv:
345
+ who = argv[argv.index("--account") + 1] if "--account" in argv else ""
346
+ self.pairings = [p for p in self.pairings if who and p["account"] != who]
347
+ out(f"witbitz-code: ✓ removed \"my-laptop\" from {who or 'every account'}")
348
+ return 0
349
+ await self.scan.wait()
350
+ port = argv[argv.index("--port") + 1]
351
+ self.pairings = self.pairings + [{**DESK, "account": "b@example.com", "idx": {"room": "r2"}, "opencodeUrl": f"http://127.0.0.1:{port}"}]
352
+ out("witbitz-code: ✓ paired \"my-laptop\" with b@example.com (pairings.json)")
353
+ return 0
354
+
355
+
356
+ def test_another_account_pairs_beside_the_first_and_one_unpairs_while_the_other_keeps_serving():
357
+ # The owner, 2026-10-22: "can we make it so it can handle multiple accounts?" — the page's Add another account
358
+ async def go():
359
+ w = TwoAccounts([DESK])
360
+ m = w.manager(listening={4096, 4196})
361
+ srv, base = await with_server(m)
362
+ try:
363
+ await m.start_serving()
364
+ async with httpx.AsyncClient() as c:
365
+ assert (await c.post(f"{base}/api/pair", headers=authed(srv), json={})).json() == {"ok": True}
366
+ assert w.pair_calls[-1] == ["--port", "4097"], "the same OpenCode as the first account's — nothing unpaired"
367
+ w.scan.set()
368
+ assert await until(lambda: _state(c, base, srv, "online"))
369
+ st = (await c.get(f"{base}/api/status", headers=authed(srv))).json()
370
+ assert sorted(p["account"] for p in st["paired"]) == ["a@example.com", "b@example.com"]
371
+ # one account leaves; the other stays paired AND reachable
372
+ assert (await c.post(f"{base}/api/unpair", headers=authed(srv), json={"account": "b@example.com"})).json() == {"ok": True}
373
+ assert w.pair_calls[-1] == ["--unpair", "--account", "b@example.com"]
374
+ assert [p["account"] for p in w.pairings] == ["a@example.com"]
375
+ assert await until(lambda: _state(c, base, srv, "online")), "serving resumes for the account that stayed"
376
+ finally:
377
+ await m.shutdown()
378
+ await srv.close()
379
+
380
+ run(go())
381
+
382
+
383
+ def test_the_page_offers_another_account_and_an_unpair_per_account():
384
+ from pathlib import Path
385
+ root = Path(ui.__file__).parent / "ui_static"
386
+ html, js = (root / "index.html").read_text(), (root / "app.js").read_text()
387
+ assert 'id="pairAdd"' in html and "Add another account" in html
388
+ assert "show($('pairAdd'), paired && !waiting)" in js
389
+ assert "$('pairAdd').onclick = () => act('/api/pair', {}" in js, "a new code beside the pairings there are — no unpair first"
390
+ assert "if (s.paired.length > 1 && p.account)" in js and "act('/api/unpair', { account: p.account }" in js
391
+ assert "for (const account of accounts) if (!(await act('/api/unpair', { account }))) return" in js, "Unpair all = each account in turn"
@@ -75,7 +75,7 @@ class Fake:
75
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
- PAIR = {"name": "desk", "account": "a@x.com", "idx": {"room": "r"}, "computerId": "c"}
78
+ PAIR = {"name": "desk", "account": "a@example.com", "idx": {"room": "r"}, "computerId": "c"}
79
79
 
80
80
 
81
81
  def test_saying_no_changes_nothing():
@@ -207,7 +207,7 @@ def test_what_runs_witbitz_code_is_the_page_s_manager_and_the_paired_ports_owner
207
207
 
208
208
 
209
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' \
210
+ out = '"opencode.exe","19228","Console","1","95,000 K","Running","MY-LAPTOP\\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
213
 
@@ -12,7 +12,7 @@ from witbitz_code import wslstart
12
12
 
13
13
  def test_the_shortcut_runs_this_distro_this_user_from_home_then_serve():
14
14
  assert wslstart.shortcut_name("UbuntuD") == "Witbitz Code (WSL UbuntuD).lnk" and wslstart.shortcut_name("") == "Witbitz Code (WSL).lnk"
15
- assert wslstart.wsl_args("UbuntuD", "ubuntu", ["/home/ubuntu/.local/bin/witbitz-code"]) == "-d UbuntuD -u ubuntu --cd ~ -- /home/ubuntu/.local/bin/witbitz-code serve"
15
+ assert wslstart.wsl_args("UbuntuD", "ubuntu", ["/home/dana/.local/bin/witbitz-code"]) == "-d UbuntuD -u ubuntu --cd ~ -- /home/dana/.local/bin/witbitz-code serve"
16
16
  assert wslstart.wsl_args("", "a b", ["/x y/witbitz-code"]) == '-u "a b" --cd ~ -- "/x y/witbitz-code" serve'
17
17
  s = wslstart.enable_script("it's.lnk", "-d X -- /w serve")
18
18
  assert "'it''s.lnk'" in s and "$s.WindowStyle = 7" in s and "System32\\wsl.exe" in s and "[Environment]::GetFolderPath('Startup')" in s
@@ -32,7 +32,7 @@ def test_state_enable_and_disable_read_what_powershell_says():
32
32
 
33
33
  @requires_node
34
34
  def test_the_javascript_writes_the_same_shortcut():
35
- cases = [["UbuntuD", "ubuntu", ["/home/ubuntu/.local/bin/witbitz-code"]], ["", "a b", ["/usr/bin/node", "/x y/witbitz-code.mjs"]]]
35
+ cases = [["UbuntuD", "ubuntu", ["/home/dana/.local/bin/witbitz-code"]], ["", "a b", ["/usr/bin/node", "/x y/witbitz-code.mjs"]]]
36
36
  js = run_node("""
37
37
  import { shortcutName, wslArgs, enableScript, stateScript, disableScript } from '@TOOLS@/code-wsl-start.mjs'
38
38
  OUT(INPUT.cases.map(([d, u, e]) => [shortcutName(d), wslArgs(d, u, e), enableScript(shortcutName(d), wslArgs(d, u, e)), stateScript(shortcutName(d)), disableScript(shortcutName(d))]))
File without changes
File without changes
File without changes