witbitz-code 1.3.4__tar.gz → 1.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {witbitz_code-1.3.4/src/witbitz_code.egg-info → witbitz_code-1.3.5}/PKG-INFO +5 -3
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/README.md +4 -2
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/pyproject.toml +2 -2
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/cli.py +6 -0
- witbitz_code-1.3.5/src/witbitz_code/tools_catalog.json +219 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/tools_probe.py +8 -1
- witbitz_code-1.3.5/src/witbitz_code/toolsetup.py +179 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/winagent.py +5 -2
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/wizard.py +60 -3
- {witbitz_code-1.3.4 → witbitz_code-1.3.5/src/witbitz_code.egg-info}/PKG-INFO +5 -3
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code.egg-info/SOURCES.txt +3 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_setup.py +3 -1
- witbitz_code-1.3.5/tests/test_toolsetup.py +147 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_winagent.py +10 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/LICENSE +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/setup.cfg +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_asks.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_attachments.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_auto.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_cli.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_codetools.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_confidential.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_folders.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_outputs.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_plugins.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_policy.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_ports.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_registry.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.3.4 → witbitz_code-1.3.5}/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.3.
|
|
3
|
+
Version: 1.3.5
|
|
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
|
|
@@ -48,9 +48,11 @@ verifier — attestation, Sigstore and TLS pinning — for the confidential mode
|
|
|
48
48
|
witbitz-code setup
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`setup` walks
|
|
51
|
+
`setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
|
|
52
52
|
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), which gives the computer **its
|
|
53
|
-
own keys from your Spaces account**,
|
|
53
|
+
own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
|
|
54
|
+
Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
|
|
55
|
+
starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
|
|
54
56
|
stops it); next time, `witbitz-code serve` is enough.
|
|
55
57
|
|
|
56
58
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
@@ -23,9 +23,11 @@ verifier — attestation, Sigstore and TLS pinning — for the confidential mode
|
|
|
23
23
|
witbitz-code setup
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`setup` walks
|
|
26
|
+
`setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
|
|
27
27
|
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), which gives the computer **its
|
|
28
|
-
own keys from your Spaces account**,
|
|
28
|
+
own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
|
|
29
|
+
Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
|
|
30
|
+
starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
|
|
29
31
|
stops it); next time, `witbitz-code serve` is enough.
|
|
30
32
|
|
|
31
33
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.5"
|
|
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"]
|
|
46
|
+
witbitz_code = ["opencode_plugins/*", "confidential_models.json", "offered_models.json", "tools_catalog.json"]
|
|
47
47
|
|
|
48
48
|
[tool.pytest.ini_options]
|
|
49
49
|
testpaths = ["tests"]
|
|
@@ -298,11 +298,17 @@ async def setup(args: list[str]) -> int:
|
|
|
298
298
|
return {"state": st["why"]}
|
|
299
299
|
return {"state": "unknown", "why": str(st.get("why") or "no answer")}
|
|
300
300
|
|
|
301
|
+
def refresh_path() -> None: # Windows: a tool winget just installed is on the registry's PATH, not yet on ours
|
|
302
|
+
reg = toolsetup.registry_path()
|
|
303
|
+
if reg:
|
|
304
|
+
os.environ["PATH"] = toolsetup.merged_path(os.environ.get("PATH", ""), reg)
|
|
305
|
+
|
|
301
306
|
d = wizard.Deps(
|
|
302
307
|
say=_err, ask=lambda q: keys.read_line(q), secret=lambda q: keys.read_line(q, hidden=True),
|
|
303
308
|
port=port, port_explicit="--port" in args,
|
|
304
309
|
pair=lambda p: pair_cmd.main(["--port", str(p)]),
|
|
305
310
|
keys_state=keys_state,
|
|
311
|
+
run_tools=lambda plan: toolsetup.run_plan(plan, say=_err), refresh_path=refresh_path,
|
|
306
312
|
is_listening=is_listening, serve_here=lambda p: serve(["--port", str(p)]),
|
|
307
313
|
)
|
|
308
314
|
try:
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
{
|
|
4
|
+
"id": "git",
|
|
5
|
+
"label": "Git",
|
|
6
|
+
"bins": [
|
|
7
|
+
"git"
|
|
8
|
+
],
|
|
9
|
+
"pkg": {
|
|
10
|
+
"brew": "git",
|
|
11
|
+
"apt": "git",
|
|
12
|
+
"dnf": "git",
|
|
13
|
+
"pacman": "git",
|
|
14
|
+
"winget": "Git.Git"
|
|
15
|
+
},
|
|
16
|
+
"auto": [],
|
|
17
|
+
"note": ""
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "ripgrep",
|
|
21
|
+
"label": "ripgrep",
|
|
22
|
+
"bins": [
|
|
23
|
+
"rg"
|
|
24
|
+
],
|
|
25
|
+
"pkg": {
|
|
26
|
+
"brew": "ripgrep",
|
|
27
|
+
"apt": "ripgrep",
|
|
28
|
+
"dnf": "ripgrep",
|
|
29
|
+
"pacman": "ripgrep",
|
|
30
|
+
"winget": "BurntSushi.ripgrep.MSVC"
|
|
31
|
+
},
|
|
32
|
+
"auto": [],
|
|
33
|
+
"note": ""
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "jq",
|
|
37
|
+
"label": "jq",
|
|
38
|
+
"bins": [
|
|
39
|
+
"jq"
|
|
40
|
+
],
|
|
41
|
+
"pkg": {
|
|
42
|
+
"brew": "jq",
|
|
43
|
+
"apt": "jq",
|
|
44
|
+
"dnf": "jq",
|
|
45
|
+
"pacman": "jq",
|
|
46
|
+
"winget": "jqlang.jq"
|
|
47
|
+
},
|
|
48
|
+
"auto": [],
|
|
49
|
+
"note": ""
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "python",
|
|
53
|
+
"label": "Python 3",
|
|
54
|
+
"bins": [
|
|
55
|
+
"python3",
|
|
56
|
+
"python"
|
|
57
|
+
],
|
|
58
|
+
"pkg": {
|
|
59
|
+
"brew": "python",
|
|
60
|
+
"apt": "python3",
|
|
61
|
+
"dnf": "python3",
|
|
62
|
+
"pacman": "python",
|
|
63
|
+
"winget": "Python.Python.3.12"
|
|
64
|
+
},
|
|
65
|
+
"auto": [],
|
|
66
|
+
"note": ""
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "uv",
|
|
70
|
+
"label": "uv",
|
|
71
|
+
"bins": [
|
|
72
|
+
"uv"
|
|
73
|
+
],
|
|
74
|
+
"pkg": {
|
|
75
|
+
"brew": "uv",
|
|
76
|
+
"apt": "",
|
|
77
|
+
"dnf": "",
|
|
78
|
+
"pacman": "uv",
|
|
79
|
+
"winget": "astral-sh.uv"
|
|
80
|
+
},
|
|
81
|
+
"auto": [],
|
|
82
|
+
"note": "where there is no package: the official installer from astral.sh"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "ffmpeg",
|
|
86
|
+
"label": "ffmpeg",
|
|
87
|
+
"bins": [
|
|
88
|
+
"ffmpeg"
|
|
89
|
+
],
|
|
90
|
+
"pkg": {
|
|
91
|
+
"brew": "ffmpeg",
|
|
92
|
+
"apt": "ffmpeg",
|
|
93
|
+
"dnf": "ffmpeg",
|
|
94
|
+
"pacman": "ffmpeg",
|
|
95
|
+
"winget": "Gyan.FFmpeg"
|
|
96
|
+
},
|
|
97
|
+
"auto": [],
|
|
98
|
+
"note": ""
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "whisper",
|
|
102
|
+
"label": "Whisper",
|
|
103
|
+
"bins": [
|
|
104
|
+
"whisper-cli",
|
|
105
|
+
"whisper-cpp",
|
|
106
|
+
"whisper"
|
|
107
|
+
],
|
|
108
|
+
"pkg": {
|
|
109
|
+
"brew": "whisper-cpp",
|
|
110
|
+
"apt": "",
|
|
111
|
+
"dnf": "",
|
|
112
|
+
"pacman": "",
|
|
113
|
+
"winget": ""
|
|
114
|
+
},
|
|
115
|
+
"auto": [
|
|
116
|
+
"linux",
|
|
117
|
+
"win32"
|
|
118
|
+
],
|
|
119
|
+
"note": "where there is no package: `uv tool install openai-whisper`; either way it needs a model downloaded before first use — say how big it is before fetching it"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "poppler",
|
|
123
|
+
"label": "Poppler",
|
|
124
|
+
"bins": [
|
|
125
|
+
"pdftotext"
|
|
126
|
+
],
|
|
127
|
+
"pkg": {
|
|
128
|
+
"brew": "poppler",
|
|
129
|
+
"apt": "poppler-utils",
|
|
130
|
+
"dnf": "poppler-utils",
|
|
131
|
+
"pacman": "poppler",
|
|
132
|
+
"winget": ""
|
|
133
|
+
},
|
|
134
|
+
"auto": [],
|
|
135
|
+
"note": ""
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "qpdf",
|
|
139
|
+
"label": "qpdf",
|
|
140
|
+
"bins": [
|
|
141
|
+
"qpdf"
|
|
142
|
+
],
|
|
143
|
+
"pkg": {
|
|
144
|
+
"brew": "qpdf",
|
|
145
|
+
"apt": "qpdf",
|
|
146
|
+
"dnf": "qpdf",
|
|
147
|
+
"pacman": "qpdf",
|
|
148
|
+
"winget": "QPDF.QPDF"
|
|
149
|
+
},
|
|
150
|
+
"auto": [],
|
|
151
|
+
"note": ""
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "tesseract",
|
|
155
|
+
"label": "Tesseract",
|
|
156
|
+
"bins": [
|
|
157
|
+
"tesseract"
|
|
158
|
+
],
|
|
159
|
+
"pkg": {
|
|
160
|
+
"brew": "tesseract",
|
|
161
|
+
"apt": "tesseract-ocr",
|
|
162
|
+
"dnf": "tesseract",
|
|
163
|
+
"pacman": "tesseract",
|
|
164
|
+
"winget": "UB-Mannheim.TesseractOCR"
|
|
165
|
+
},
|
|
166
|
+
"auto": [],
|
|
167
|
+
"note": ""
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "pandoc",
|
|
171
|
+
"label": "Pandoc",
|
|
172
|
+
"bins": [
|
|
173
|
+
"pandoc"
|
|
174
|
+
],
|
|
175
|
+
"pkg": {
|
|
176
|
+
"brew": "pandoc",
|
|
177
|
+
"apt": "pandoc",
|
|
178
|
+
"dnf": "pandoc",
|
|
179
|
+
"pacman": "pandoc",
|
|
180
|
+
"winget": "JohnMacFarlane.Pandoc"
|
|
181
|
+
},
|
|
182
|
+
"auto": [],
|
|
183
|
+
"note": ""
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "imagemagick",
|
|
187
|
+
"label": "ImageMagick",
|
|
188
|
+
"bins": [
|
|
189
|
+
"magick",
|
|
190
|
+
"convert"
|
|
191
|
+
],
|
|
192
|
+
"pkg": {
|
|
193
|
+
"brew": "imagemagick",
|
|
194
|
+
"apt": "imagemagick",
|
|
195
|
+
"dnf": "ImageMagick",
|
|
196
|
+
"pacman": "imagemagick",
|
|
197
|
+
"winget": "ImageMagick.ImageMagick"
|
|
198
|
+
},
|
|
199
|
+
"auto": [],
|
|
200
|
+
"note": ""
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"adminLines": {
|
|
204
|
+
"apt": "sudo apt-get update && sudo apt-get install -y {pkgs}",
|
|
205
|
+
"dnf": "sudo dnf install -y {pkgs}",
|
|
206
|
+
"pacman": "sudo pacman -S --needed --noconfirm {pkgs}",
|
|
207
|
+
"zypper": "sudo zypper install -y {pkgs}",
|
|
208
|
+
"apk": "sudo apk add {pkgs}"
|
|
209
|
+
},
|
|
210
|
+
"windowsReleases": {
|
|
211
|
+
"poppler": {
|
|
212
|
+
"url": "https://github.com/oschwartz10612/poppler-windows/releases/download/v26.09.0-0/Release-26.09.0-0.zip",
|
|
213
|
+
"sha256": "7a6f256a0ddf7536182246a5733331bf4677cbcc34f4663774947ad34556c8d0",
|
|
214
|
+
"mb": 42,
|
|
215
|
+
"inner": "poppler-26.09.0/Library/bin/",
|
|
216
|
+
"dir": "poppler\\bin"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -27,6 +27,9 @@ PACKAGE_MANAGERS: dict[str, list[tuple[str, str]]] = {
|
|
|
27
27
|
}
|
|
28
28
|
_EXTRA_POSIX = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/snap/bin", "/home/linuxbrew/.linuxbrew/bin"]
|
|
29
29
|
_EXTRA_HOME = [".local/bin", ".cargo/bin", "bin"]
|
|
30
|
+
# Windows: under %LOCALAPPDATA%\Witbitz — what the Windows installer (code.ps1) fills and witbitz-code serve gives the agent:
|
|
31
|
+
# its Git (Git Bash, the agent's shell), its own Python, and what setup's Tools step puts there (Poppler). = code-tools-probe.mjs
|
|
32
|
+
_WIN_WITBITZ = ["bin", "git\\cmd", "git\\bin", "agent-python\\Scripts", "poppler\\bin"]
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
def _is_executable(path: str) -> bool:
|
|
@@ -42,7 +45,11 @@ def search_dirs(*, env: Mapping[str, str], platform: str, home: str) -> list[str
|
|
|
42
45
|
win = platform == "win32"
|
|
43
46
|
raw = env.get("PATH") or env.get("Path") or ""
|
|
44
47
|
from_path = [d for d in raw.split(";" if win else ":") if d]
|
|
45
|
-
|
|
48
|
+
if win:
|
|
49
|
+
local = (env.get("LOCALAPPDATA") or "").rstrip("\\/")
|
|
50
|
+
extra = [f"{local}\\Witbitz\\{d}" for d in _WIN_WITBITZ] if local else []
|
|
51
|
+
else:
|
|
52
|
+
extra = _EXTRA_POSIX + ([f"{home.rstrip('/')}/{d}" for d in _EXTRA_HOME] if home else [])
|
|
46
53
|
return list(dict.fromkeys(from_path + extra))
|
|
47
54
|
|
|
48
55
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""witbitz-code setup's Tools step — the Python twin of tools/code-tools-install.mjs and codeTools.js installPlan.
|
|
2
|
+
|
|
3
|
+
Mechanical, in the setup terminal (the owner, 2026-10-08: "Why all this setup (or most of it can't be mechanical)?"): a
|
|
4
|
+
package manager's own install with the person's terminal attached (sudo can ask for the password, winget's Windows
|
|
5
|
+
permission prompt appears in front of them), a pinned download checked against its publisher's SHA-256, and nothing for
|
|
6
|
+
what fetches itself on first use. No model, no approval cards; the agent on the Code card is the fallback.
|
|
7
|
+
The catalog data is tools_catalog.json, written by tools/code-py-models.mjs; tests/test_toolsetup.py holds the plans equal.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import ntpath
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import zipfile
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
_CATALOG = json.loads(Path(__file__).with_name("tools_catalog.json").read_text(encoding="utf-8"))
|
|
28
|
+
TOOLS: list[dict] = _CATALOG["tools"]
|
|
29
|
+
ADMIN_LINES: dict[str, str] = _CATALOG["adminLines"]
|
|
30
|
+
WINDOWS_RELEASES: dict[str, dict] = _CATALOG["windowsReleases"]
|
|
31
|
+
|
|
32
|
+
WINGET_FLAGS = ["--exact", "--source", "winget", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def label(tool_id: str) -> str:
|
|
36
|
+
return next((t["label"] for t in TOOLS if t["id"] == tool_id), tool_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def missing_tools(report: dict | None) -> list[dict]:
|
|
40
|
+
"""= codeTools.js missingTools: missing per the report, in catalog order, never one this OS gets by itself (`auto`)."""
|
|
41
|
+
if not report:
|
|
42
|
+
return []
|
|
43
|
+
os_id = (report.get("platform") or {}).get("os") or ""
|
|
44
|
+
return [t for t in TOOLS if report.get("tools", {}).get(t["id"]) is False and os_id not in t["auto"]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def install_plan(ids: list[str], platform: dict) -> list[dict]:
|
|
48
|
+
"""= codeTools.js installPlan → the steps setup runs, in order (see there)."""
|
|
49
|
+
os_id, pm = (platform or {}).get("os") or "", (platform or {}).get("pm") or ""
|
|
50
|
+
steps: list[dict] = []
|
|
51
|
+
batch: list[str] = []
|
|
52
|
+
batch_ids: list[str] = []
|
|
53
|
+
for t in TOOLS:
|
|
54
|
+
if t["id"] not in ids:
|
|
55
|
+
continue
|
|
56
|
+
if os_id in t["auto"]:
|
|
57
|
+
steps.append({"kind": "auto", "id": t["id"]})
|
|
58
|
+
continue
|
|
59
|
+
pkg = t["pkg"].get(pm, "") if pm else ""
|
|
60
|
+
if pkg and pm == "winget":
|
|
61
|
+
steps.append({"kind": "cmd", "ids": [t["id"]], "argv": ["winget", "install", "--id", pkg, *WINGET_FLAGS]})
|
|
62
|
+
elif pkg and (pm == "brew" or pm in ADMIN_LINES):
|
|
63
|
+
batch.append(pkg)
|
|
64
|
+
batch_ids.append(t["id"])
|
|
65
|
+
elif os_id == "win32" and t["id"] in WINDOWS_RELEASES:
|
|
66
|
+
steps.append({"kind": "release", "id": t["id"], **WINDOWS_RELEASES[t["id"]]})
|
|
67
|
+
else:
|
|
68
|
+
steps.append({"kind": "none", "id": t["id"], "note": t["note"]})
|
|
69
|
+
if batch:
|
|
70
|
+
steps.insert(0, {"kind": "cmd", "ids": batch_ids, "argv": ["brew", "install", *batch]} if pm == "brew"
|
|
71
|
+
else {"kind": "cmd", "ids": batch_ids, "admin": True, "argv": ["sh", "-c", ADMIN_LINES[pm].replace("{pkgs}", " ".join(batch))]})
|
|
72
|
+
return steps
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def witbitz_dir(env: dict | None = None) -> str:
|
|
76
|
+
env = os.environ if env is None else env
|
|
77
|
+
return ntpath.join(env.get("LOCALAPPDATA", ""), "Witbitz")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_inherit(argv: list[str]) -> int:
|
|
81
|
+
"""A command with the person's terminal attached → its exit code (-1: it could not start)."""
|
|
82
|
+
try:
|
|
83
|
+
return subprocess.run(argv, check=False).returncode # noqa: S603 — argv from installPlan, never a shell string of ours
|
|
84
|
+
except (OSError, ValueError):
|
|
85
|
+
return -1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def install_release(step: dict, *, client: httpx.Client | None = None, dest: str | None = None) -> str:
|
|
89
|
+
"""A pinned zip → its `inner` folder's files into <witbitz_dir>/<dir>. Raises ValueError with a sentence."""
|
|
90
|
+
own = client is None
|
|
91
|
+
client = client or httpx.Client(timeout=httpx.Timeout(300, connect=20), follow_redirects=True)
|
|
92
|
+
try:
|
|
93
|
+
r = client.get(step["url"])
|
|
94
|
+
except httpx.HTTPError as e:
|
|
95
|
+
raise ValueError(f"could not download it ({str(e)[:80]})") from e
|
|
96
|
+
finally:
|
|
97
|
+
if own:
|
|
98
|
+
client.close()
|
|
99
|
+
if r.status_code != 200:
|
|
100
|
+
raise ValueError(f"the download answered {r.status_code}")
|
|
101
|
+
got = hashlib.sha256(r.content).hexdigest()
|
|
102
|
+
if got != step["sha256"]:
|
|
103
|
+
raise ValueError(f"the download is not the pinned file (sha256 {got[:12]}…) — nothing was installed")
|
|
104
|
+
to = Path(dest or witbitz_dir(), *step["dir"].split("\\"))
|
|
105
|
+
inner = step["inner"]
|
|
106
|
+
with zipfile.ZipFile(io.BytesIO(r.content)) as z:
|
|
107
|
+
members = [m for m in z.infolist() if m.filename.startswith(inner) and not m.is_dir()]
|
|
108
|
+
if not members:
|
|
109
|
+
raise ValueError(f"the download does not hold {inner}")
|
|
110
|
+
to.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
for m in members:
|
|
112
|
+
rel = m.filename[len(inner):]
|
|
113
|
+
if not rel or ".." in Path(rel).parts:
|
|
114
|
+
continue
|
|
115
|
+
out = to / rel
|
|
116
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
with z.open(m) as src, open(out, "wb") as dst:
|
|
118
|
+
shutil.copyfileobj(src, dst)
|
|
119
|
+
return str(to)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run_plan(steps: list[dict], *, say: Callable[[str], Any] = print, run: Callable[[list[str]], int] = run_inherit,
|
|
123
|
+
release: Callable[[dict], str] = install_release) -> dict:
|
|
124
|
+
"""Carry out the plan → {done, failed, auto, none} (tool ids). Says each step as it goes. = code-tools-install.mjs runPlan"""
|
|
125
|
+
out: dict = {"done": [], "failed": [], "auto": [], "none": []}
|
|
126
|
+
for s in steps:
|
|
127
|
+
if s["kind"] == "cmd":
|
|
128
|
+
say(f" $ {s['argv'][2] if s['argv'][0] == 'sh' else ' '.join(s['argv'])}")
|
|
129
|
+
code = run(s["argv"])
|
|
130
|
+
(out["done"] if code == 0 else out["failed"]).extend(s["ids"])
|
|
131
|
+
if code != 0:
|
|
132
|
+
say(f" ✖ {', '.join(label(i) for i in s['ids'])}: that {'could not start' if code == -1 else f'ended with {code}'}")
|
|
133
|
+
elif s["kind"] == "release":
|
|
134
|
+
say(f" Downloading {label(s['id'])} ({s['mb']} MB) from its publisher, checked against its SHA-256 …")
|
|
135
|
+
try:
|
|
136
|
+
to = release(s)
|
|
137
|
+
out["done"].append(s["id"])
|
|
138
|
+
say(f" ✓ {label(s['id'])} → {to}")
|
|
139
|
+
except (ValueError, OSError) as e:
|
|
140
|
+
out["failed"].append(s["id"])
|
|
141
|
+
say(f" ✖ {label(s['id'])}: {e}")
|
|
142
|
+
elif s["kind"] == "auto":
|
|
143
|
+
out["auto"].append(s["id"])
|
|
144
|
+
say(f" ✓ {label(s['id'])}: fetched by itself the first time it is needed — nothing to install now")
|
|
145
|
+
else:
|
|
146
|
+
out["none"].append(s["id"])
|
|
147
|
+
note = f" ({s['note']})" if s.get("note") else ""
|
|
148
|
+
say(f" – {label(s['id'])}: nothing here installs it{note} — \"Install with the agent\" in Code can try")
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def registry_path(reg: Any = None) -> str | None:
|
|
153
|
+
"""Windows: PATH as the registry has it now (machine, then user) — a program winget just installed is on it, while this
|
|
154
|
+
process still has the PATH it started with. None elsewhere or when it cannot be read."""
|
|
155
|
+
if sys.platform != "win32" and reg is None:
|
|
156
|
+
return None
|
|
157
|
+
if reg is None:
|
|
158
|
+
import winreg as reg # noqa: PLC0415 — Windows only
|
|
159
|
+
parts = []
|
|
160
|
+
for root, key in ((reg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"), (reg.HKEY_CURRENT_USER, "Environment")):
|
|
161
|
+
try:
|
|
162
|
+
with reg.OpenKey(root, key) as k:
|
|
163
|
+
value, _kind = reg.QueryValueEx(k, "Path")
|
|
164
|
+
parts.append(reg.ExpandEnvironmentStrings(value) if hasattr(reg, "ExpandEnvironmentStrings") else value)
|
|
165
|
+
except OSError:
|
|
166
|
+
continue
|
|
167
|
+
return ";".join(p for p in parts if p) or None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def merged_path(current: str, from_registry: str | None) -> str:
|
|
171
|
+
"""This process's PATH with the registry's entries added (new installs), keeping what it had. = mergedPath"""
|
|
172
|
+
seen: set[str] = set()
|
|
173
|
+
parts: list[str] = []
|
|
174
|
+
for d in (from_registry or "").split(";") + (current or "").split(";"):
|
|
175
|
+
d = d.strip()
|
|
176
|
+
if d and d.lower() not in seen:
|
|
177
|
+
seen.add(d.lower())
|
|
178
|
+
parts.append(d)
|
|
179
|
+
return ";".join(parts)
|
|
@@ -81,8 +81,11 @@ def windows_agent(config_dir: Path, env: dict | None = None, platform: str | Non
|
|
|
81
81
|
if bash:
|
|
82
82
|
add["SHELL"] = bash
|
|
83
83
|
py = find_agent_python(env, exists)
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
# Poppler from setup's Tools step (toolsetup.py: winget has no package, so a pinned release into Witbitz\poppler\bin)
|
|
85
|
+
poppler = ntpath.join(_witbitz_dir(env), "poppler", "bin")
|
|
86
|
+
prepend = [d for d in (py, poppler if exists(ntpath.join(poppler, "pdftotext.exe")) else None) if d]
|
|
87
|
+
if prepend:
|
|
88
|
+
add["PATH_PREPEND"] = prepend
|
|
86
89
|
f = Path(config_dir) / INSTRUCTIONS_NAME
|
|
87
90
|
instr: str | None = None
|
|
88
91
|
try:
|
|
@@ -21,7 +21,7 @@ from dataclasses import dataclass, field
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
-
from . import codekeys, ports
|
|
24
|
+
from . import codekeys, ports, tools_probe, toolsetup
|
|
25
25
|
from . import pairings as pp
|
|
26
26
|
from .connector import _url_port
|
|
27
27
|
|
|
@@ -77,6 +77,10 @@ class Deps:
|
|
|
77
77
|
port_owners: Callable[[int], list] = ports.port_owners # [{pid, cmd}] — this user's processes on a port (ports.py)
|
|
78
78
|
stop_process: Callable[[int], bool] = lambda pid: __import__("witbitz_code.uninstall", fromlist=["stop_process"]).stop_process(pid)
|
|
79
79
|
wsl: bool = field(default_factory=ports.on_wsl)
|
|
80
|
+
# setup's Tools step (toolsetup.py): what is here, carrying out the plan, and PATH after installs (Windows: the registry)
|
|
81
|
+
probe_tools: Callable[[], dict] = lambda: tools_probe.probe_tools()
|
|
82
|
+
run_tools: Callable[[list], dict] = None # type: ignore[assignment]
|
|
83
|
+
refresh_path: Callable[[], None] = lambda: None
|
|
80
84
|
serve_here: Callable[[int], Awaitable[int]] = None # type: ignore[assignment]
|
|
81
85
|
result: dict = field(default_factory=lambda: {"opencode": False, "paired": False, "keys": False, "running": ""})
|
|
82
86
|
|
|
@@ -88,7 +92,7 @@ def _yes(answer: str) -> bool:
|
|
|
88
92
|
async def run_setup(d: Deps) -> dict:
|
|
89
93
|
port = d.port
|
|
90
94
|
result = d.result
|
|
91
|
-
d.say("witbitz-code setup —
|
|
95
|
+
d.say("witbitz-code setup — five steps; anything already done is skipped.\n")
|
|
92
96
|
|
|
93
97
|
# 1 — OpenCode
|
|
94
98
|
d.say("1. OpenCode, the coding agent")
|
|
@@ -178,7 +182,12 @@ async def run_setup(d: Deps) -> dict:
|
|
|
178
182
|
result["keys"] = True
|
|
179
183
|
|
|
180
184
|
# 4 — running (this package has no background service yet: it runs in this window)
|
|
181
|
-
|
|
185
|
+
# 4 — the tools the agent works with (the Code card's list), installed HERE, mechanically (toolsetup.py; the owner,
|
|
186
|
+
# 2026-10-08: "Why all this setup (or most of it can't be mechanical)?"). Asked once; nothing runs on a no.
|
|
187
|
+
d.say("\n4. Tools for audio, video, PDFs and scans")
|
|
188
|
+
result["tools"] = await _tools_step(d)
|
|
189
|
+
|
|
190
|
+
d.say("\n5. Keep it running")
|
|
182
191
|
# OpenCode's port AND the model proxy's (port + 100): an earlier witbitz-code still running holds both, and keeps answering
|
|
183
192
|
# the phone with the keys it started with (the owner, 2026-10-08: `pipx uninstall` left it running — "could not start
|
|
184
193
|
# the model proxy … address already in use", then "Invalid API key"). This person's own process: offer to stop it.
|
|
@@ -218,3 +227,51 @@ async def run_setup(d: Deps) -> dict:
|
|
|
218
227
|
result["port"] = port
|
|
219
228
|
result["served"] = await d.serve_here(port)
|
|
220
229
|
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _describe(step: dict) -> str:
|
|
233
|
+
if step["kind"] == "cmd":
|
|
234
|
+
line = step["argv"][2] if step["argv"][0] == "sh" else " ".join(step["argv"][:4])
|
|
235
|
+
return f" {line}" + (" (asks for your password)" if step.get("admin") else "")
|
|
236
|
+
if step["kind"] == "release":
|
|
237
|
+
return f" {toolsetup.label(step['id'])}: a {step['mb']} MB download from its publisher, checked against its SHA-256"
|
|
238
|
+
if step["kind"] == "auto":
|
|
239
|
+
return f" {toolsetup.label(step['id'])}: comes by itself the first time it is needed"
|
|
240
|
+
return f" {toolsetup.label(step['id'])}: nothing here installs it" + (f" ({step['note']})" if step.get("note") else "")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def _tools_step(d: Deps) -> str:
|
|
244
|
+
"""→ 'here' | 'installed' | 'partly' | 'skipped' | 'unavailable'."""
|
|
245
|
+
try:
|
|
246
|
+
report = d.probe_tools()
|
|
247
|
+
except Exception: # noqa: BLE001 — a probe that fails never stops setup
|
|
248
|
+
d.say(" (could not check which tools are here — skipped)")
|
|
249
|
+
return "skipped"
|
|
250
|
+
missing = toolsetup.missing_tools(report)
|
|
251
|
+
if not missing:
|
|
252
|
+
d.say(" ✓ Everything the agent uses for these is here")
|
|
253
|
+
return "here"
|
|
254
|
+
plan = toolsetup.install_plan([t["id"] for t in missing], report.get("platform") or {})
|
|
255
|
+
d.say(f" Missing here: {', '.join(t['label'] for t in missing)}.")
|
|
256
|
+
if not any(st["kind"] in ("cmd", "release") for st in plan):
|
|
257
|
+
d.say(" Nothing on this computer can install them (no package manager found) — \"Install with the agent\" in Code can try.")
|
|
258
|
+
return "unavailable"
|
|
259
|
+
d.say(" This would run:")
|
|
260
|
+
for st in plan:
|
|
261
|
+
d.say(_describe(st))
|
|
262
|
+
if not _yes(d.ask(" Install them now? [Y/n] ")):
|
|
263
|
+
d.say(" Skipped — run witbitz-code setup again to install them, or use the card in Code.")
|
|
264
|
+
return "skipped"
|
|
265
|
+
d.run_tools(plan)
|
|
266
|
+
d.refresh_path()
|
|
267
|
+
try:
|
|
268
|
+
after = {t["id"] for t in toolsetup.missing_tools(d.probe_tools())}
|
|
269
|
+
except Exception: # noqa: BLE001
|
|
270
|
+
after = {t["id"] for t in missing}
|
|
271
|
+
still = [t["label"] for t in missing if t["id"] in after]
|
|
272
|
+
if not still:
|
|
273
|
+
d.say(" ✓ Installed")
|
|
274
|
+
return "installed"
|
|
275
|
+
d.say(f" Still missing: {', '.join(still)} — \"Install with the agent\" in Code can try, or run setup again.")
|
|
276
|
+
return "partly"
|
|
277
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
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
|
|
@@ -48,9 +48,11 @@ verifier — attestation, Sigstore and TLS pinning — for the confidential mode
|
|
|
48
48
|
witbitz-code setup
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`setup` walks
|
|
51
|
+
`setup` walks five steps and skips whatever is already done: it installs OpenCode if it is missing (it asks first), pairs
|
|
52
52
|
this computer (scan the QR code in Spaces → Settings → Back up & recovery → Add a device), which gives the computer **its
|
|
53
|
-
own keys from your Spaces account**,
|
|
53
|
+
own keys from your Spaces account**, offers to install the tools the agent uses for audio, video, PDFs and scans (ffmpeg,
|
|
54
|
+
Poppler, Tesseract, … — with winget, Homebrew or one `sudo apt-get` line; it shows what it will run and asks once), then
|
|
55
|
+
starts `serve` in the window. Then open Code in Spaces. Leave it running (Ctrl-C
|
|
54
56
|
stops it); next time, `witbitz-code serve` is enough.
|
|
55
57
|
|
|
56
58
|
**No key to paste.** Pairing asks your Spaces account for this computer's own TrustedRouter and Tinfoil keys, capped by the
|
|
@@ -37,7 +37,9 @@ src/witbitz_code/receipt.py
|
|
|
37
37
|
src/witbitz_code/relay.py
|
|
38
38
|
src/witbitz_code/seen.py
|
|
39
39
|
src/witbitz_code/tinfoil_read.py
|
|
40
|
+
src/witbitz_code/tools_catalog.json
|
|
40
41
|
src/witbitz_code/tools_probe.py
|
|
42
|
+
src/witbitz_code/toolsetup.py
|
|
41
43
|
src/witbitz_code/uninstall.py
|
|
42
44
|
src/witbitz_code/winagent.py
|
|
43
45
|
src/witbitz_code/winconsole.py
|
|
@@ -72,6 +74,7 @@ tests/test_registry.py
|
|
|
72
74
|
tests/test_relay_codec.py
|
|
73
75
|
tests/test_setup.py
|
|
74
76
|
tests/test_tools_probe.py
|
|
77
|
+
tests/test_toolsetup.py
|
|
75
78
|
tests/test_uninstall.py
|
|
76
79
|
tests/test_winagent.py
|
|
77
80
|
tests/test_winconsole.py
|
|
@@ -120,7 +120,9 @@ class Fake:
|
|
|
120
120
|
code_keys=lambda p: {"caps": {"modelsUsd": 50, "toolsUsd": 5}} if p.get("computerId") in self._keyed else None,
|
|
121
121
|
serve_here=serve_here,
|
|
122
122
|
# never this machine's real processes: the real lookup finds whatever listens on 4096 HERE (an OpenCode does)
|
|
123
|
-
|
|
123
|
+
# and never this machine's tools: nothing is missing, nothing is installed (the Tools step has its own tests)
|
|
124
|
+
**{"is_listening": listening, "port_owners": lambda port: [], "stop_process": lambda pid: False, "wsl": False,
|
|
125
|
+
"probe_tools": lambda: {"tools": {}, "platform": {"os": "", "pm": ""}}, "run_tools": lambda plan: {}, "refresh_path": lambda: None, **over})
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
def test_a_new_computer_is_paired_gets_its_keys_from_the_account_and_is_served_without_a_key_prompt():
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""setup's Tools step (toolsetup.py) — the plan held to codeTools.js installPlan, the runner, the pinned download, PATH after
|
|
2
|
+
installs, and the wizard step. Nothing here installs anything on this computer: every command and download is a fake."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import hashlib
|
|
8
|
+
import io
|
|
9
|
+
import zipfile
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import pytest
|
|
13
|
+
from conftest import requires_node, run_node
|
|
14
|
+
|
|
15
|
+
from witbitz_code import toolsetup as ts, wizard
|
|
16
|
+
|
|
17
|
+
CASES = [
|
|
18
|
+
(["git", "ripgrep", "ffmpeg", "whisper", "poppler", "uv", "tesseract"], {"os": "win32", "pm": "winget"}),
|
|
19
|
+
(["git", "ffmpeg", "whisper", "poppler", "uv", "pandoc"], {"os": "linux", "pm": "apt"}),
|
|
20
|
+
(["ffmpeg", "whisper", "poppler", "qpdf"], {"os": "darwin", "pm": "brew"}),
|
|
21
|
+
(["ffmpeg", "poppler"], {"os": "win32", "pm": "scoop"}),
|
|
22
|
+
(["jq", "whisper"], {"os": "darwin", "pm": ""}),
|
|
23
|
+
(["git"], {"os": "linux", "pm": "zypper"}),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@requires_node
|
|
28
|
+
def test_the_plans_and_the_catalog_copy_are_the_javascript_ones():
|
|
29
|
+
js = run_node("import { installPlan, missingTools } from '@REPO@/spaces/public/codeTools.js'; import { toolsCatalog } from '@TOOLS@/code-py-models.mjs';"
|
|
30
|
+
"OUT({ plans: INPUT.cases.map(([ids, p]) => installPlan(ids, p)), catalog: toolsCatalog(),"
|
|
31
|
+
" missing: INPUT.reports.map((r) => missingTools(r).map((t) => t.id)) })".replace("@REPO@", (__import__("conftest").REPO).as_uri()),
|
|
32
|
+
{"cases": CASES, "reports": REPORTS})
|
|
33
|
+
assert js["catalog"] == {"tools": ts.TOOLS, "adminLines": ts.ADMIN_LINES, "windowsReleases": ts.WINDOWS_RELEASES}, "tools_catalog.json is stale — run node tools/code-py-models.mjs"
|
|
34
|
+
assert [ts.install_plan(ids, p) for ids, p in CASES] == js["plans"]
|
|
35
|
+
assert [[t["id"] for t in ts.missing_tools(r)] for r in REPORTS] == js["missing"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
REPORTS = [
|
|
39
|
+
{"tools": {"whisper": False, "ffmpeg": False, "git": True}, "platform": {"os": "win32", "pm": "winget"}},
|
|
40
|
+
{"tools": {"whisper": False, "ffmpeg": False}, "platform": {"os": "darwin", "pm": "brew"}},
|
|
41
|
+
{"tools": {"whisper": False}, "platform": {"os": "linux", "pm": "apt"}},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_whisper_is_never_missing_where_the_media_tools_fetch_it():
|
|
46
|
+
assert [t["id"] for t in ts.missing_tools(REPORTS[0])] == ["ffmpeg"]
|
|
47
|
+
assert [t["id"] for t in ts.missing_tools(REPORTS[1])] == ["ffmpeg", "whisper"], "on a Mac it comes from Homebrew"
|
|
48
|
+
assert ts.missing_tools(REPORTS[2]) == []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_the_runner_says_each_step_and_sorts_the_outcomes():
|
|
52
|
+
said, ran = [], []
|
|
53
|
+
plan = ts.install_plan(["git", "ffmpeg", "poppler", "uv", "whisper"], {"os": "win32", "pm": "winget"}) + [{"kind": "none", "id": "pandoc", "note": ""}]
|
|
54
|
+
def run(argv):
|
|
55
|
+
ran.append(argv)
|
|
56
|
+
return 0 if "Git.Git" in argv else 1
|
|
57
|
+
out = ts.run_plan(plan, say=said.append, run=run, release=lambda st: "C:\\W\\poppler\\bin")
|
|
58
|
+
assert out == {"done": ["git", "poppler"], "failed": ["uv", "ffmpeg"], "auto": ["whisper"], "none": ["pandoc"]}
|
|
59
|
+
assert all(a[:2] == ["winget", "install"] and "--accept-package-agreements" in a for a in ran)
|
|
60
|
+
assert any("ffmpeg: that ended with 1" in s for s in said) and any("Poppler (42 MB)" in s for s in said)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _zip(files: dict) -> bytes:
|
|
64
|
+
b = io.BytesIO()
|
|
65
|
+
with zipfile.ZipFile(b, "w") as z:
|
|
66
|
+
for name, data in files.items():
|
|
67
|
+
z.writestr(name, data)
|
|
68
|
+
return b.getvalue()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_the_pinned_download_lands_its_inner_folder_and_a_changed_file_is_refused(tmp_path):
|
|
72
|
+
body = _zip({"poppler-26.09.0/Library/bin/pdftotext.exe": b"MZ-text", "poppler-26.09.0/Library/bin/poppler.dll": b"MZ-dll",
|
|
73
|
+
"poppler-26.09.0/share/x.txt": b"no", "poppler-26.09.0/Library/bin/../../evil.txt": b"no"})
|
|
74
|
+
step = {"url": "https://example.test/p.zip", "sha256": hashlib.sha256(body).hexdigest(), "mb": 1, "inner": "poppler-26.09.0/Library/bin/", "dir": "poppler\\bin"}
|
|
75
|
+
with httpx.Client(transport=httpx.MockTransport(lambda req: httpx.Response(200, content=body))) as c:
|
|
76
|
+
to = ts.install_release(step, client=c, dest=str(tmp_path))
|
|
77
|
+
got = sorted(p.name for p in (tmp_path / "poppler" / "bin").iterdir())
|
|
78
|
+
assert got == ["pdftotext.exe", "poppler.dll"] and to.endswith("bin") and not (tmp_path / "evil.txt").exists()
|
|
79
|
+
bad = {**step, "dir": "other\\bin"}
|
|
80
|
+
with httpx.Client(transport=httpx.MockTransport(lambda req: httpx.Response(200, content=body + b"x"))) as c:
|
|
81
|
+
with pytest.raises(ValueError, match="not the pinned file"):
|
|
82
|
+
ts.install_release(bad, client=c, dest=str(tmp_path))
|
|
83
|
+
assert not (tmp_path / "other").exists(), "nothing written from a changed file"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_path_after_installs_comes_from_the_registry_and_keeps_this_process_own_entries():
|
|
87
|
+
class Reg:
|
|
88
|
+
HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER = "HKLM", "HKCU"
|
|
89
|
+
vals = {"HKLM": r"C:\Windows;%SystemRoot%\x", "HKCU": r"C:\Users\u\AppData\Local\Microsoft\WinGet\Links"}
|
|
90
|
+
class _K:
|
|
91
|
+
def __init__(self, root): self.root = root
|
|
92
|
+
def __enter__(self): return self
|
|
93
|
+
def __exit__(self, *a): return False
|
|
94
|
+
def OpenKey(self, root, key): return Reg._K(root) # noqa: N802
|
|
95
|
+
def QueryValueEx(self, k, name): return Reg.vals[k.root], 2 # noqa: N802
|
|
96
|
+
def ExpandEnvironmentStrings(self, v): return v.replace("%SystemRoot%", r"C:\Windows") # noqa: N802
|
|
97
|
+
reg = ts.registry_path(Reg())
|
|
98
|
+
assert reg == r"C:\Windows;C:\Windows\x;C:\Users\u\AppData\Local\Microsoft\WinGet\Links"
|
|
99
|
+
assert ts.merged_path(r"C:\W\bin;c:\windows", reg) == r"C:\Windows;C:\Windows\x;C:\Users\u\AppData\Local\Microsoft\WinGet\Links;C:\W\bin"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Tools:
|
|
103
|
+
"""setup's view of this computer's tools: `missing` until run_tools installs them (or `installs` says which)."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, missing: list[str], platform: dict, installs: list[str] | None = None) -> None:
|
|
106
|
+
self.missing, self.platform, self.installs, self.plans, self.refreshed = set(missing), platform, installs, [], 0
|
|
107
|
+
|
|
108
|
+
def probe(self) -> dict:
|
|
109
|
+
return {"tools": {t["id"]: t["id"] not in self.missing for t in ts.TOOLS}, "platform": self.platform}
|
|
110
|
+
|
|
111
|
+
def run(self, plan: list) -> dict:
|
|
112
|
+
self.plans.append(plan)
|
|
113
|
+
for st in plan:
|
|
114
|
+
for i in st.get("ids") or [st.get("id")]:
|
|
115
|
+
if self.installs is None or i in self.installs:
|
|
116
|
+
self.missing.discard(i)
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def step(tools: Tools, answers: list[str]) -> tuple[str, list[str]]:
|
|
121
|
+
said = []
|
|
122
|
+
d = wizard.Deps(say=said.append, ask=lambda q: answers.pop(0) if answers else "", secret=lambda q: "",
|
|
123
|
+
probe_tools=tools.probe, run_tools=tools.run, refresh_path=lambda: setattr(tools, "refreshed", tools.refreshed + 1))
|
|
124
|
+
return asyncio.run(wizard._tools_step(d)), said
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_setup_asks_once_installs_mechanically_and_checks_again():
|
|
128
|
+
t = Tools(["ffmpeg", "poppler", "tesseract", "whisper"], {"os": "win32", "pm": "winget"})
|
|
129
|
+
r, said = step(t, [""])
|
|
130
|
+
assert r == "installed" and t.refreshed == 1
|
|
131
|
+
assert [s["kind"] for s in t.plans[0]] == ["cmd", "release", "cmd"], "Whisper is not asked for on Windows: it comes by itself"
|
|
132
|
+
assert any("Missing here: ffmpeg, Poppler, Tesseract." in s for s in said) and any("42 MB download" in s for s in said)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_a_no_installs_nothing_and_what_could_not_be_installed_is_named():
|
|
136
|
+
t = Tools(["ffmpeg", "pandoc"], {"os": "linux", "pm": "apt"})
|
|
137
|
+
r, said = step(t, ["n"])
|
|
138
|
+
assert r == "skipped" and t.plans == []
|
|
139
|
+
t2 = Tools(["ffmpeg", "pandoc"], {"os": "linux", "pm": "apt"}, installs=["ffmpeg"])
|
|
140
|
+
r2, said2 = step(t2, [""])
|
|
141
|
+
assert r2 == "partly" and any("(asks for your password)" in s for s in said2) and any("Still missing: Pandoc" in s for s in said2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_no_package_manager_says_so_without_asking():
|
|
145
|
+
t = Tools(["jq"], {"os": "darwin", "pm": ""})
|
|
146
|
+
r, said = step(t, [])
|
|
147
|
+
assert r == "unavailable" and t.plans == [] and any("no package manager found" in s for s in said)
|
|
@@ -67,3 +67,13 @@ def test_apply_puts_python_ahead_of_ast_grep_and_adds_to_the_instructions():
|
|
|
67
67
|
# no ast-grep: PATH starts from the base
|
|
68
68
|
env, config = winagent.apply({}, {}, add, None, base_path=r"C:\Windows\system32")
|
|
69
69
|
assert env["PATH"] == r"C:\py\Scripts;C:\Windows\system32" and config == {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_setups_poppler_joins_the_agents_path_when_it_is_there(tmp_path):
|
|
73
|
+
# setup's Tools step puts Poppler in Witbitz\poppler\bin (winget has no package) — the agent's PATH must reach it
|
|
74
|
+
py = r"C:\Users\t\AppData\Local\Witbitz\agent-python\Scripts\python.exe"
|
|
75
|
+
pdf = r"C:\Users\t\AppData\Local\Witbitz\poppler\bin\pdftotext.exe"
|
|
76
|
+
add, _, _ = winagent.windows_agent(tmp_path, env=ENV, platform="win32", exists=exists_of(py, pdf))
|
|
77
|
+
assert add["PATH_PREPEND"] == [r"C:\Users\t\AppData\Local\Witbitz\agent-python\Scripts", r"C:\Users\t\AppData\Local\Witbitz\poppler\bin"]
|
|
78
|
+
add2, _, _ = winagent.windows_agent(tmp_path, env=ENV, platform="win32", exists=exists_of(py))
|
|
79
|
+
assert add2["PATH_PREPEND"] == [r"C:\Users\t\AppData\Local\Witbitz\agent-python\Scripts"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs
RENAMED
|
File without changes
|
{witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.3.4 → witbitz_code-1.3.5}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|