@allansantos-dev/smart-tool 0.9.1 → 0.9.2
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.
- package/CHANGELOG.md +15 -0
- package/install_runtime.py +37 -5
- package/package.json +1 -1
- package/setup_ui.py +60 -35
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +48 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2 - beta
|
|
4
|
+
|
|
5
|
+
- Installer reported success after a broken Camoufox download: camoufox 0.5.6 prints the error of `fetch` and exits 0.
|
|
6
|
+
The installer now catches that error and fails the step (retried 3 times), every request of the download has a
|
|
7
|
+
60 s read timeout (a stalled download used to wait until the 3-minute idle limit), and the final check requires the
|
|
8
|
+
Camoufox and Chromium executables on disk, so an incomplete browser can never end in "Smart Tool ready".
|
|
9
|
+
- Setup screen: text, model and adapter fields were unstyled boxes (inputs without a type), selects and model fields
|
|
10
|
+
now look like comboboxes; the hook change opens inside the client's card in a scrollable block with Confirm and
|
|
11
|
+
Cancel instead of overflowing the page; "Save behavior" works without registering the MCP first; web search keys
|
|
12
|
+
are set one provider at a time from a provider selector instead of a stack of key fields.
|
|
13
|
+
- Product page: the display font was a family that Google Fonts does not serve (titles fell back to Arial Narrow);
|
|
14
|
+
it now uses Big Shoulders Display. The page shows the current version (a test keeps it in sync with version.py),
|
|
15
|
+
links to the release notes and says what the installer does on failures and older installations. A failed Pages
|
|
16
|
+
deploy is retried once, the run fails unless the live page shows the version, and every release republishes it.
|
|
17
|
+
|
|
3
18
|
## 0.9.1 - beta
|
|
4
19
|
|
|
5
20
|
- Installer on a regular Windows code page (cp1252): the steps' output was garbled or crashed the step; every child
|
package/install_runtime.py
CHANGED
|
@@ -133,7 +133,7 @@ def _run(args, *, cwd=None, label="Web dependency", idle_s=600, stream=False, at
|
|
|
133
133
|
except RuntimeError as exc:
|
|
134
134
|
if attempt == attempts:
|
|
135
135
|
raise
|
|
136
|
-
print(f"{
|
|
136
|
+
print(f"{exc}; trying again.", flush=True)
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def _run_once(args, cwd, env, flags, label, idle_s, stream):
|
|
@@ -179,8 +179,10 @@ def _run_once(args, cwd, env, flags, label, idle_s, stream):
|
|
|
179
179
|
time.sleep(0.5)
|
|
180
180
|
reader.join(timeout=10)
|
|
181
181
|
if process.returncode:
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if not stream:
|
|
183
|
+
for line in list(tail)[-15:]:
|
|
184
|
+
print(f"{label}: {line}", flush=True)
|
|
185
|
+
raise RuntimeError(f"{label} failed (exit {process.returncode}): {tail[-1] if tail else 'no output'}")
|
|
184
186
|
|
|
185
187
|
|
|
186
188
|
def _check():
|
|
@@ -193,6 +195,8 @@ def _check():
|
|
|
193
195
|
"angular_parser":bool(node and ANGULAR_PARSER.is_file()),
|
|
194
196
|
"browser_python": BROWSER_PYTHON.is_file(),
|
|
195
197
|
"browser_packages": False,
|
|
198
|
+
"camoufox_browser": False,
|
|
199
|
+
"chromium_browser": False,
|
|
196
200
|
}
|
|
197
201
|
if status["browser_python"]:
|
|
198
202
|
try:
|
|
@@ -203,9 +207,36 @@ def _check():
|
|
|
203
207
|
status["browser_packages"] = result.returncode == 0
|
|
204
208
|
except subprocess.TimeoutExpired:
|
|
205
209
|
status["browser_packages"] = False
|
|
210
|
+
if status["browser_packages"]:
|
|
211
|
+
status.update(_browser_executables())
|
|
206
212
|
return status
|
|
207
213
|
|
|
208
214
|
|
|
215
|
+
_BROWSERS_PROBE = """
|
|
216
|
+
import json, os
|
|
217
|
+
from camoufox.pkgman import LAUNCH_FILE, OS_NAME, camoufox_path
|
|
218
|
+
from playwright.sync_api import sync_playwright
|
|
219
|
+
found = {}
|
|
220
|
+
try:
|
|
221
|
+
found["camoufox_browser"] = os.path.isfile(os.path.join(camoufox_path(download_if_missing=False), LAUNCH_FILE[OS_NAME]))
|
|
222
|
+
except Exception:
|
|
223
|
+
found["camoufox_browser"] = False
|
|
224
|
+
with sync_playwright() as playwright:
|
|
225
|
+
found["chromium_browser"] = os.path.isfile(playwright.chromium.executable_path)
|
|
226
|
+
print(json.dumps(found))
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _browser_executables():
|
|
231
|
+
"""Both browsers must exist on disk: a download tool reporting success is not enough."""
|
|
232
|
+
try:
|
|
233
|
+
result = subprocess.run([str(BROWSER_PYTHON), "-c", _BROWSERS_PROBE], capture_output=True, text=True,
|
|
234
|
+
encoding="utf-8", errors="replace", check=False, timeout=60)
|
|
235
|
+
return json.loads(result.stdout.strip().splitlines()[-1])
|
|
236
|
+
except (subprocess.TimeoutExpired, ValueError, IndexError):
|
|
237
|
+
return {"camoufox_browser": False, "chromium_browser": False}
|
|
238
|
+
|
|
239
|
+
|
|
209
240
|
def _link_existing_browser(existing, link):
|
|
210
241
|
try:
|
|
211
242
|
os.symlink(existing, link, target_is_directory=True)
|
|
@@ -268,9 +299,10 @@ def main():
|
|
|
268
299
|
help="Reuse an existing venv without duplicating files (useful when disk is low)")
|
|
269
300
|
args = parser.parse_args()
|
|
270
301
|
if args.check:
|
|
271
|
-
|
|
302
|
+
status = _check()
|
|
303
|
+
for name, ready in status.items():
|
|
272
304
|
print(f"{name}: {'ok' if ready else 'missing'}")
|
|
273
|
-
return 0 if all(
|
|
305
|
+
return 0 if all(status.values()) else 1
|
|
274
306
|
try:
|
|
275
307
|
install(args.reuse_browser_runtime)
|
|
276
308
|
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allansantos-dev/smart-tool",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Allan Santos",
|
package/setup_ui.py
CHANGED
|
@@ -397,9 +397,18 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
397
397
|
.pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
|
|
398
398
|
label { display: block; font-size: .8rem; font-weight: 600; margin-bottom: 4px; }
|
|
399
399
|
.hint { display: block; font-size: .76rem; color: var(--muted); margin-top: 3px; }
|
|
400
|
-
select, input[type=text], input[type=url], input[type=number], input[type=password] {
|
|
400
|
+
select, input:not([type]), input[type=text], input[type=url], input[type=number], input[type=password] {
|
|
401
401
|
width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
|
|
402
|
-
background: var(--bg); color: var(--ink); font: .84rem var(--mono); min-width: 0; max-width: 100%; }
|
|
402
|
+
background-color: var(--bg); color: var(--ink); font: .84rem var(--mono); min-width: 0; max-width: 100%; }
|
|
403
|
+
select, input[list] { appearance: none; padding-right: 34px; cursor: pointer;
|
|
404
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
|
|
405
|
+
background-position: calc(100% - 17px) 52%, calc(100% - 12px) 52%; background-size: 5px 5px; background-repeat: no-repeat; }
|
|
406
|
+
input[list] { cursor: text; }
|
|
407
|
+
input[list]::-webkit-calendar-picker-indicator { opacity: 0; cursor: pointer; }
|
|
408
|
+
select:hover, input[list]:hover { border-color: var(--muted); }
|
|
409
|
+
.code-preview { display: block; margin: 10px 0 0; padding: 12px 14px; max-height: 320px; overflow: auto; white-space: pre;
|
|
410
|
+
font: .78rem/1.5 var(--mono); color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; }
|
|
411
|
+
.plan { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
|
|
403
412
|
input[type=password] { font-family: var(--body); }
|
|
404
413
|
input::placeholder { color: var(--idle); }
|
|
405
414
|
select:focus-visible, input:focus-visible, button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
|
|
@@ -556,10 +565,8 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
556
565
|
</select>
|
|
557
566
|
<span class="hint" id="hook-mode-hint">Applies to every client on the next tool call; nothing is reinstalled.</span></div>
|
|
558
567
|
<div class="actions"><button type="button" id="btn-save-hook-mode">Save behavior</button></div>
|
|
559
|
-
<div id="agents-list"></div>
|
|
560
|
-
<pre class="hint" id="agents-preview" hidden></pre>
|
|
561
|
-
<div class="actions"><button type="button" id="btn-agents-confirm" hidden>Confirm and write</button></div>
|
|
562
568
|
<div class="msg" id="agents-msg" role="status"></div>
|
|
569
|
+
<div id="agents-list"></div>
|
|
563
570
|
</section>
|
|
564
571
|
|
|
565
572
|
<section id="providers-section">
|
|
@@ -570,7 +577,10 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
570
577
|
<span class="hint" id="contact-hint">Wikimedia asks tools to identify a contact in the User-Agent. It stays on this machine and is sent only to the search APIs.</span></div>
|
|
571
578
|
<div class="actions"><button type="button" id="btn-save-contact">Save contact</button></div>
|
|
572
579
|
<div class="msg" id="contact-msg" role="status"></div>
|
|
573
|
-
<div
|
|
580
|
+
<div class="field" style="margin-top:18px"><label for="provider-select">Provider</label>
|
|
581
|
+
<select id="provider-select"></select>
|
|
582
|
+
<span class="hint">Pick a provider to see its free tier and set or remove your key for it.</span></div>
|
|
583
|
+
<div id="provider-panel"></div>
|
|
574
584
|
</section>
|
|
575
585
|
</div>
|
|
576
586
|
</div>
|
|
@@ -643,8 +653,10 @@ function renderGatewayFields() {
|
|
|
643
653
|
for (const field of (adapter || { fields: [] }).fields) {
|
|
644
654
|
const wrap = document.createElement("div");
|
|
645
655
|
wrap.className = "field";
|
|
646
|
-
wrap.innerHTML = `<label></label><input autocomplete="off" spellcheck="false"><span class="hint"></span>`;
|
|
656
|
+
wrap.innerHTML = `<label></label><input type="text" autocomplete="off" spellcheck="false"><span class="hint"></span>`;
|
|
647
657
|
const input = wrap.querySelector("input");
|
|
658
|
+
input.id = "gateway-field-" + field.name;
|
|
659
|
+
wrap.querySelector("label").htmlFor = input.id;
|
|
648
660
|
wrap.querySelector("label").textContent = field.label + (field.required ? "" : " (optional)");
|
|
649
661
|
input.dataset.field = field.name;
|
|
650
662
|
input.dataset.secret = field.secret ? "1" : "";
|
|
@@ -831,7 +843,6 @@ $("btn-save-local-models").addEventListener("click", async () => {
|
|
|
831
843
|
}
|
|
832
844
|
});
|
|
833
845
|
|
|
834
|
-
let agentsPlan = null;
|
|
835
846
|
async function loadAgents() {
|
|
836
847
|
try {
|
|
837
848
|
const resp = await fetch("/setup/integration");
|
|
@@ -847,7 +858,9 @@ async function loadAgents() {
|
|
|
847
858
|
const mcp = item.mcp_url ? "MCP registered: " + item.mcp_url : "smart-tool MCP not registered in this client";
|
|
848
859
|
const hook = item.hook_error ? "Hook unreadable: " + item.hook_error : item.hook ? "Hook installed and up to date" : "Hook missing or outdated";
|
|
849
860
|
row.innerHTML = `<div class="provider-head"><strong></strong><span class="state"></span></div><p class="hint mcp"></p><p class="hint mono cmd"></p><p class="hint file"></p>
|
|
850
|
-
<div class="actions"><button type="button" class="register">Register MCP</button><button type="button" class="hook">Install or update hook</button></div
|
|
861
|
+
<div class="actions"><button type="button" class="register">Register MCP</button><button type="button" class="hook">Install or update hook</button></div>
|
|
862
|
+
<div class="plan" hidden><span class="hint plan-file"></span><pre class="code-preview"></pre>
|
|
863
|
+
<div class="actions"><button type="button" class="confirm">Confirm and write</button><button type="button" class="quiet cancel">Cancel</button></div></div>`;
|
|
851
864
|
row.querySelector("strong").textContent = item.label;
|
|
852
865
|
row.querySelector(".state").textContent = hook;
|
|
853
866
|
row.querySelector(".mcp").textContent = mcp;
|
|
@@ -862,34 +875,37 @@ async function loadAgents() {
|
|
|
862
875
|
const result = await post("/setup/integration", { client: item.client, action: "register_mcp" });
|
|
863
876
|
setMsg("agents-msg", "ok", "MCP registered with: " + result.command + ". Reconnect open sessions (/mcp).");
|
|
864
877
|
await loadAgents();
|
|
865
|
-
$("btn-save-hook-mode").addEventListener("click", async () => {
|
|
866
|
-
try {
|
|
867
|
-
await post("/setup/integration", { action: "hook_mode", mode: $("sel-hook-mode").value });
|
|
868
|
-
setMsg("agents-msg", "ok", "Hook behavior saved; it applies on the next tool call.");
|
|
869
|
-
await loadAgents();
|
|
870
|
-
} catch (error) {
|
|
871
|
-
setMsg("agents-msg", "error", error.message);
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
878
|
} catch (error) {
|
|
875
879
|
setMsg("agents-msg", "error", error.message);
|
|
876
880
|
register.disabled = false;
|
|
877
881
|
}
|
|
878
882
|
});
|
|
879
883
|
const button = row.querySelector(".hook");
|
|
884
|
+
const plan = row.querySelector(".plan");
|
|
880
885
|
button.hidden = Boolean(item.hook);
|
|
881
886
|
button.addEventListener("click", async () => {
|
|
882
887
|
setMsg("agents-msg", "", "Preparing the change…");
|
|
883
888
|
try {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
+
const preview = await post("/setup/integration", { client: item.client, action: "preview" });
|
|
890
|
+
row.querySelector(".plan-file").textContent = "Entry added to " + preview.file + " (the file is backed up first):";
|
|
891
|
+
row.querySelector(".code-preview").textContent = JSON.stringify(preview.change, null, 2);
|
|
892
|
+
plan.hidden = false;
|
|
893
|
+
button.hidden = true;
|
|
894
|
+
setMsg("agents-msg", "", "");
|
|
895
|
+
row.querySelector(".confirm").onclick = async () => {
|
|
896
|
+
try {
|
|
897
|
+
const result = await post("/setup/integration", { client: item.client, action: "install", token: preview.token });
|
|
898
|
+
setMsg("agents-msg", "ok", item.label + ": hook written. Backup: " + (result.backup || "new file") + (result.after_install ? ". " + result.after_install : ""));
|
|
899
|
+
await loadAgents();
|
|
900
|
+
} catch (error) {
|
|
901
|
+
setMsg("agents-msg", "error", error.message);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
889
904
|
} catch (error) {
|
|
890
905
|
setMsg("agents-msg", "error", error.message);
|
|
891
906
|
}
|
|
892
907
|
});
|
|
908
|
+
row.querySelector(".cancel").addEventListener("click", () => { plan.hidden = true; button.hidden = false; });
|
|
893
909
|
list.appendChild(row);
|
|
894
910
|
}
|
|
895
911
|
} catch (error) {
|
|
@@ -897,14 +913,10 @@ $("btn-save-hook-mode").addEventListener("click", async () => {
|
|
|
897
913
|
}
|
|
898
914
|
}
|
|
899
915
|
loadAgents();
|
|
900
|
-
$("btn-
|
|
901
|
-
if (!agentsPlan) return;
|
|
916
|
+
$("btn-save-hook-mode").addEventListener("click", async () => {
|
|
902
917
|
try {
|
|
903
|
-
|
|
904
|
-
setMsg("agents-msg", "ok", "Hook
|
|
905
|
-
agentsPlan = null;
|
|
906
|
-
$("agents-preview").hidden = true;
|
|
907
|
-
$("btn-agents-confirm").hidden = true;
|
|
918
|
+
await post("/setup/integration", { action: "hook_mode", mode: $("sel-hook-mode").value });
|
|
919
|
+
setMsg("agents-msg", "ok", "Hook behavior saved; it applies on the next tool call.");
|
|
908
920
|
await loadAgents();
|
|
909
921
|
} catch (error) {
|
|
910
922
|
setMsg("agents-msg", "error", error.message);
|
|
@@ -946,8 +958,7 @@ function providerCard(name, info) {
|
|
|
946
958
|
try {
|
|
947
959
|
const data = await post("/setup/providers", { provider: name, action, ...(action === "save" ? { api_key: input.value.trim() } : {}) });
|
|
948
960
|
input.value = "";
|
|
949
|
-
await loadProviders();
|
|
950
|
-
setMsg(name + "-msg", "ok", action === "save" ? `Key accepted by the provider (${data.validated_results} result(s) in the test) and protected on this Windows machine.` : "Key removed; free tier active.");
|
|
961
|
+
await loadProviders(action === "save" ? `Key accepted by the provider (${data.validated_results} result(s) in the test) and protected on this Windows machine.` : "Key removed; free tier active.");
|
|
951
962
|
} catch (error) {
|
|
952
963
|
setMsg(msg.id, "error", error.message);
|
|
953
964
|
button.disabled = false;
|
|
@@ -966,13 +977,27 @@ $("btn-save-contact").addEventListener("click", async () => {
|
|
|
966
977
|
}
|
|
967
978
|
});
|
|
968
979
|
|
|
969
|
-
|
|
970
|
-
|
|
980
|
+
let providers = {};
|
|
981
|
+
function showProvider(message) {
|
|
982
|
+
const name = $("provider-select").value;
|
|
983
|
+
$("provider-panel").replaceChildren(...(providers[name] ? [providerCard(name, providers[name])] : []));
|
|
984
|
+
if (message) setMsg(name + "-msg", "ok", message);
|
|
985
|
+
}
|
|
986
|
+
$("provider-select").addEventListener("change", () => showProvider());
|
|
987
|
+
|
|
988
|
+
async function loadProviders(message) {
|
|
989
|
+
const list = $("provider-panel");
|
|
971
990
|
try {
|
|
972
991
|
const resp = await fetch("/setup/providers");
|
|
973
992
|
const data = await resp.json();
|
|
974
993
|
if (!resp.ok || data.error) throw new Error(data.error);
|
|
975
|
-
|
|
994
|
+
providers = data.providers;
|
|
995
|
+
const select = $("provider-select");
|
|
996
|
+
const chosen = select.value;
|
|
997
|
+
select.replaceChildren(...Object.entries(providers).map(([name, info]) =>
|
|
998
|
+
new Option(info.label + " · " + ((providerLabels[info.status] || [null, info.status])[1]), name)));
|
|
999
|
+
if (chosen && providers[chosen]) select.value = chosen;
|
|
1000
|
+
showProvider(message);
|
|
976
1001
|
if (document.activeElement !== $("contact-input")) $("contact-input").value = data.contact;
|
|
977
1002
|
$("contact-hint").textContent = "Sent as: " + data.user_agent;
|
|
978
1003
|
} catch (error) {
|
package/version.py
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
"""`camoufox fetch` for the installer
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""`camoufox fetch` for the installer. camoufox 0.5.6 prints a failed download and still exits 0, so the error is
|
|
2
|
+
caught here and turned into a failing exit, and the browser executable must exist afterwards. Its downloads have no
|
|
3
|
+
timeout (requests passes None, which also overrides socket.setdefaulttimeout), so every request gets one here: a
|
|
4
|
+
stalled download fails after 60 s instead of hanging. Progress is printed as plain lines (the rich bar stays
|
|
5
|
+
invisible in a pipe)."""
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
4
8
|
|
|
9
|
+
import camoufox.__main__ as camoufox_cli
|
|
10
|
+
import requests
|
|
5
11
|
from camoufox import pkgman
|
|
6
|
-
from camoufox.__main__ import cli
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
TIMEOUT_S = (30, 60)
|
|
9
14
|
REPORT_EVERY_MB = 50
|
|
10
15
|
_webdl = pkgman.webdl
|
|
16
|
+
_update = camoufox_cli.CamoufoxUpdate.update
|
|
17
|
+
_get = requests.get
|
|
18
|
+
failures = []
|
|
11
19
|
|
|
12
20
|
|
|
13
21
|
def _reporting_webdl(url, desc=None, buffer=None, bar=True, progress_callback=None):
|
|
@@ -22,7 +30,40 @@ def _reporting_webdl(url, desc=None, buffer=None, bar=True, progress_callback=No
|
|
|
22
30
|
return _webdl(url, desc=desc, buffer=buffer, bar=False, progress_callback=progress_callback or report)
|
|
23
31
|
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
def _get_with_timeout(*args, **kwargs):
|
|
34
|
+
kwargs.setdefault("timeout", TIMEOUT_S)
|
|
35
|
+
return _get(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _recording_update(self, *args, **kwargs):
|
|
39
|
+
try:
|
|
40
|
+
return _update(self, *args, **kwargs)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
failures.append(exc)
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def installed_executable():
|
|
47
|
+
"""The browser executable, without the download that launch_path() starts when it is missing."""
|
|
48
|
+
executable = Path(pkgman.camoufox_path(download_if_missing=False)) / pkgman.LAUNCH_FILE[pkgman.OS_NAME]
|
|
49
|
+
if not executable.is_file():
|
|
50
|
+
raise FileNotFoundError(executable)
|
|
51
|
+
return executable
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
requests.get = _get_with_timeout
|
|
56
|
+
pkgman.webdl = _reporting_webdl
|
|
57
|
+
camoufox_cli.CamoufoxUpdate.update = _recording_update
|
|
58
|
+
camoufox_cli.cli.main(["fetch"], standalone_mode=False)
|
|
59
|
+
if failures:
|
|
60
|
+
sys.exit(f"Camoufox download failed: {failures[-1]}")
|
|
61
|
+
try:
|
|
62
|
+
executable = installed_executable()
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
sys.exit(f"Camoufox is not installed after the download: {exc}")
|
|
65
|
+
print(f"Camoufox {pkgman.installed_verstr()} ready: {executable}", flush=True)
|
|
66
|
+
|
|
26
67
|
|
|
27
68
|
if __name__ == "__main__":
|
|
28
|
-
|
|
69
|
+
main()
|