pi-web-toolkit 0.3.2 → 0.3.3
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 +18 -1
- package/README.md +150 -118
- package/docs/adr/0001-firecrawl-keyless-cloud-fallback.md +1 -1
- package/docs/adr/0002-toolkit-config-for-installer-selections.md +3 -0
- package/docs/adr/0003-conservative-installer-prerequisites.md +3 -0
- package/docs/adr/0004-searxng-endpoint-discovery.md +3 -0
- package/docs/guide.md +19 -3
- package/docs/tools.md +16 -1
- package/extensions/utils/agent-browser.ts +4 -3
- package/extensions/utils/config.ts +170 -0
- package/extensions/utils/firecrawl.ts +27 -3
- package/extensions/utils/scrapling.ts +2 -1
- package/extensions/utils/web-search-core.ts +146 -0
- package/extensions/web_search.ts +37 -112
- package/install.sh +801 -0
- package/package.json +6 -3
package/install.sh
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
VERSION="0.3.3"
|
|
5
|
+
USER_AGENT="pi-web-toolkit-installer/${VERSION} (+https://github.com/Wade11s/pi-web-toolkit)"
|
|
6
|
+
PUBLIC_INSTANCES_URL="https://searx.space/data/instances.json"
|
|
7
|
+
LOCAL_SEARXNG_CONTAINER="pi-web-toolkit-searxng"
|
|
8
|
+
DEFAULT_SEARXNG_PORT="8080"
|
|
9
|
+
|
|
10
|
+
YES=0
|
|
11
|
+
DOCTOR=0
|
|
12
|
+
LOCAL_INSTALL=0
|
|
13
|
+
DEPS_ONLY=0
|
|
14
|
+
EXTENSION_ONLY=0
|
|
15
|
+
AGENT_BROWSER_WITH_DEPS=0
|
|
16
|
+
SEARXNG_URL_ARG=""
|
|
17
|
+
AUTO_SEARXNG=""
|
|
18
|
+
FIRECRAWL_CHOICE=""
|
|
19
|
+
FIRECRAWL_RUNNER="installed"
|
|
20
|
+
SEARXNG_PORT="$DEFAULT_SEARXNG_PORT"
|
|
21
|
+
|
|
22
|
+
CONFIG_FILE="${PI_WEB_TOOLKIT_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/pi-web-toolkit/config.json}"
|
|
23
|
+
CONFIG_DIR="$(dirname "$CONFIG_FILE")"
|
|
24
|
+
|
|
25
|
+
SELECTED_SEARXNG_URL=""
|
|
26
|
+
SCRAPLING_BIN=""
|
|
27
|
+
AGENT_BROWSER_BIN=""
|
|
28
|
+
FIRECRAWL_BIN=""
|
|
29
|
+
FIRECRAWL_FALLBACK=""
|
|
30
|
+
SEARXNG_SOURCE=""
|
|
31
|
+
FAIL_COUNT=0
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
cat <<'USAGE'
|
|
35
|
+
pi-web-toolkit installer
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
install.sh [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--yes, -y Accept safe non-interactive defaults
|
|
42
|
+
--doctor Verify readiness without changing anything
|
|
43
|
+
--searxng-url URL Use and verify an existing SearXNG endpoint
|
|
44
|
+
--auto-searxng public Explicitly auto-select a verified public endpoint
|
|
45
|
+
--auto-searxng local-docker Explicitly start/reuse isolated local Docker SearXNG
|
|
46
|
+
--searxng-port PORT Local Docker SearXNG port (default: 8080)
|
|
47
|
+
--with-firecrawl Install/enable optional Firecrawl keyless fallback CLI
|
|
48
|
+
--no-firecrawl Skip/disable optional Firecrawl fallback
|
|
49
|
+
--firecrawl-runner installed|npx|bunx
|
|
50
|
+
Explicit Firecrawl runner (default: installed)
|
|
51
|
+
--agent-browser-with-deps Use agent-browser install --with-deps (Linux)
|
|
52
|
+
--local Install the pi package from the current checkout
|
|
53
|
+
--deps-only Install dependencies/config only, skip pi install
|
|
54
|
+
--extension-only Install the pi package only, skip dependency install
|
|
55
|
+
--config PATH Toolkit config path (also exported as PI_WEB_TOOLKIT_CONFIG)
|
|
56
|
+
--help, -h Show this help
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
curl -fsSL https://raw.githubusercontent.com/Wade11s/pi-web-toolkit/main/install.sh | bash
|
|
60
|
+
./install.sh --yes --searxng-url https://searxng.example --no-firecrawl
|
|
61
|
+
./install.sh --doctor
|
|
62
|
+
USAGE
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log() { printf '%s\n' "$*"; }
|
|
66
|
+
warn() { printf 'WARN: %s\n' "$*" >&2; }
|
|
67
|
+
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
68
|
+
|
|
69
|
+
status() {
|
|
70
|
+
local state="$1"
|
|
71
|
+
local label="$2"
|
|
72
|
+
printf '%-8s %s\n' "$state" "$label"
|
|
73
|
+
if [ "$state" = "FAIL" ]; then
|
|
74
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
is_interactive() {
|
|
79
|
+
[ "$YES" -eq 0 ] && [ -r /dev/tty ] && [ -w /dev/tty ]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
read_tty() {
|
|
83
|
+
local __var="$1"
|
|
84
|
+
if ! IFS= read -r "$__var" < /dev/tty; then
|
|
85
|
+
printf -v "$__var" '%s' ""
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
confirm() {
|
|
90
|
+
local prompt="$1"
|
|
91
|
+
local default="${2:-n}"
|
|
92
|
+
local suffix="[y/N]"
|
|
93
|
+
if [ "$default" = "y" ]; then suffix="[Y/n]"; fi
|
|
94
|
+
if ! is_interactive; then
|
|
95
|
+
[ "$default" = "y" ]
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
local answer
|
|
99
|
+
printf '%s %s ' "$prompt" "$suffix"
|
|
100
|
+
read_tty answer
|
|
101
|
+
if [ -z "$answer" ]; then answer="$default"; fi
|
|
102
|
+
case "$answer" in
|
|
103
|
+
y|Y|yes|YES) return 0 ;;
|
|
104
|
+
*) return 1 ;;
|
|
105
|
+
esac
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
normalize_url() {
|
|
109
|
+
printf '%s' "$1" | sed 's#/*$##'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
command_path() {
|
|
113
|
+
command -v "$1" 2>/dev/null || true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resolve_executable_path() {
|
|
117
|
+
local command_name="$1"
|
|
118
|
+
local found
|
|
119
|
+
found="$(command_path "$command_name")"
|
|
120
|
+
if [ -n "$found" ]; then
|
|
121
|
+
printf '%s' "$found"
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
if [ -x "$HOME/.local/bin/$command_name" ]; then
|
|
125
|
+
printf '%s' "$HOME/.local/bin/$command_name"
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
parse_args() {
|
|
132
|
+
while [ "$#" -gt 0 ]; do
|
|
133
|
+
case "$1" in
|
|
134
|
+
--yes|-y) YES=1; shift ;;
|
|
135
|
+
--doctor) DOCTOR=1; shift ;;
|
|
136
|
+
--local) LOCAL_INSTALL=1; shift ;;
|
|
137
|
+
--deps-only) DEPS_ONLY=1; shift ;;
|
|
138
|
+
--extension-only) EXTENSION_ONLY=1; shift ;;
|
|
139
|
+
--agent-browser-with-deps) AGENT_BROWSER_WITH_DEPS=1; shift ;;
|
|
140
|
+
--searxng-url)
|
|
141
|
+
[ "$#" -ge 2 ] || die "--searxng-url requires a URL"
|
|
142
|
+
SEARXNG_URL_ARG="$2"; shift 2 ;;
|
|
143
|
+
--auto-searxng)
|
|
144
|
+
[ "$#" -ge 2 ] || die "--auto-searxng requires public or local-docker"
|
|
145
|
+
AUTO_SEARXNG="$2"
|
|
146
|
+
case "$AUTO_SEARXNG" in public|local-docker) ;; *) die "--auto-searxng must be public or local-docker" ;; esac
|
|
147
|
+
shift 2 ;;
|
|
148
|
+
--searxng-port)
|
|
149
|
+
[ "$#" -ge 2 ] || die "--searxng-port requires a port"
|
|
150
|
+
SEARXNG_PORT="$2"; shift 2 ;;
|
|
151
|
+
--with-firecrawl) FIRECRAWL_CHOICE="with"; shift ;;
|
|
152
|
+
--no-firecrawl) FIRECRAWL_CHOICE="no"; shift ;;
|
|
153
|
+
--firecrawl-runner)
|
|
154
|
+
[ "$#" -ge 2 ] || die "--firecrawl-runner requires installed, npx, or bunx"
|
|
155
|
+
FIRECRAWL_RUNNER="$2"
|
|
156
|
+
case "$FIRECRAWL_RUNNER" in installed|npx|bunx) ;; *) die "--firecrawl-runner must be installed, npx, or bunx" ;; esac
|
|
157
|
+
shift 2 ;;
|
|
158
|
+
--config)
|
|
159
|
+
[ "$#" -ge 2 ] || die "--config requires a path"
|
|
160
|
+
CONFIG_FILE="$2"
|
|
161
|
+
export PI_WEB_TOOLKIT_CONFIG="$CONFIG_FILE"
|
|
162
|
+
CONFIG_DIR="$(dirname "$CONFIG_FILE")"
|
|
163
|
+
shift 2 ;;
|
|
164
|
+
--help|-h) usage; exit 0 ;;
|
|
165
|
+
*) die "Unknown option: $1" ;;
|
|
166
|
+
esac
|
|
167
|
+
done
|
|
168
|
+
|
|
169
|
+
if [ "$DEPS_ONLY" -eq 1 ] && [ "$EXTENSION_ONLY" -eq 1 ]; then
|
|
170
|
+
die "--deps-only and --extension-only cannot be combined"
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
config_get() {
|
|
175
|
+
local dotted_path="$1"
|
|
176
|
+
[ -f "$CONFIG_FILE" ] || return 1
|
|
177
|
+
node - "$CONFIG_FILE" "$dotted_path" <<'NODE'
|
|
178
|
+
const fs = require('fs');
|
|
179
|
+
const file = process.argv[2];
|
|
180
|
+
const dotted = process.argv[3];
|
|
181
|
+
let cfg;
|
|
182
|
+
try {
|
|
183
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(`Invalid toolkit config at ${file}: ${err.message}`);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
let cur = cfg;
|
|
189
|
+
for (const part of dotted.split('.')) {
|
|
190
|
+
if (!cur || typeof cur !== 'object' || !(part in cur)) process.exit(1);
|
|
191
|
+
cur = cur[part];
|
|
192
|
+
}
|
|
193
|
+
if (cur === undefined || cur === null) process.exit(1);
|
|
194
|
+
if (typeof cur === 'object') console.log(JSON.stringify(cur));
|
|
195
|
+
else console.log(String(cur));
|
|
196
|
+
NODE
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
validate_config_if_present() {
|
|
200
|
+
[ -f "$CONFIG_FILE" ] || return 0
|
|
201
|
+
node - "$CONFIG_FILE" <<'NODE'
|
|
202
|
+
const fs = require('fs');
|
|
203
|
+
const file = process.argv[2];
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
206
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
207
|
+
throw new Error('expected a JSON object');
|
|
208
|
+
}
|
|
209
|
+
const optionalString = (value, key) => {
|
|
210
|
+
if (value !== undefined && typeof value !== 'string') throw new Error(`${key} must be a string`);
|
|
211
|
+
};
|
|
212
|
+
optionalString(parsed.searxngUrl, 'searxngUrl');
|
|
213
|
+
if (parsed.firecrawlFallback !== undefined && typeof parsed.firecrawlFallback !== 'boolean') {
|
|
214
|
+
throw new Error('firecrawlFallback must be a boolean');
|
|
215
|
+
}
|
|
216
|
+
if (parsed.firecrawlRunner !== undefined && !['installed', 'npx', 'bunx'].includes(parsed.firecrawlRunner)) {
|
|
217
|
+
throw new Error('firecrawlRunner must be one of: installed, npx, bunx');
|
|
218
|
+
}
|
|
219
|
+
if (parsed.commands !== undefined) {
|
|
220
|
+
if (!parsed.commands || typeof parsed.commands !== 'object' || Array.isArray(parsed.commands)) {
|
|
221
|
+
throw new Error('commands must be an object');
|
|
222
|
+
}
|
|
223
|
+
optionalString(parsed.commands.scrapling, 'commands.scrapling');
|
|
224
|
+
optionalString(parsed.commands.agentBrowser, 'commands.agentBrowser');
|
|
225
|
+
optionalString(parsed.commands.firecrawl, 'commands.firecrawl');
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`Invalid toolkit config at ${file}: ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
NODE
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
runtime_searxng_url() {
|
|
235
|
+
if [ -n "${SEARXNG_URL:-}" ]; then normalize_url "$SEARXNG_URL"; return 0; fi
|
|
236
|
+
local cfg_url=""
|
|
237
|
+
cfg_url="$(config_get searxngUrl 2>/dev/null || true)"
|
|
238
|
+
if [ -n "$cfg_url" ]; then normalize_url "$cfg_url"; return 0; fi
|
|
239
|
+
printf '%s' "http://localhost:8080"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
runtime_firecrawl_enabled() {
|
|
243
|
+
if [ -n "${PI_WEB_FIRECRAWL_FALLBACK+x}" ]; then
|
|
244
|
+
local v
|
|
245
|
+
v="$(printf '%s' "$PI_WEB_FIRECRAWL_FALLBACK" | tr '[:upper:]' '[:lower:]')"
|
|
246
|
+
case "$v" in 0|false|no|off) return 1 ;; *) return 0 ;; esac
|
|
247
|
+
fi
|
|
248
|
+
local cfg_value=""
|
|
249
|
+
cfg_value="$(config_get firecrawlFallback 2>/dev/null || true)"
|
|
250
|
+
case "$cfg_value" in false|0|no|off) return 1 ;; *) return 0 ;; esac
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
runtime_command() {
|
|
254
|
+
local key="$1"
|
|
255
|
+
local env_name="$2"
|
|
256
|
+
local default_name="$3"
|
|
257
|
+
local env_value="${!env_name:-}"
|
|
258
|
+
if [ -n "$env_value" ]; then printf '%s' "$env_value"; return 0; fi
|
|
259
|
+
local cfg_value=""
|
|
260
|
+
cfg_value="$(config_get "commands.$key" 2>/dev/null || true)"
|
|
261
|
+
if [ -n "$cfg_value" ]; then printf '%s' "$cfg_value"; return 0; fi
|
|
262
|
+
printf '%s' "$default_name"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
runtime_firecrawl_runner() {
|
|
266
|
+
local env_value="${PI_WEB_FIRECRAWL_RUNNER:-}"
|
|
267
|
+
if [ -n "$env_value" ]; then
|
|
268
|
+
case "$env_value" in installed|npx|bunx) printf '%s' "$env_value"; return 0 ;; *) printf '%s' "installed"; return 0 ;; esac
|
|
269
|
+
fi
|
|
270
|
+
local cfg_value=""
|
|
271
|
+
cfg_value="$(config_get firecrawlRunner 2>/dev/null || true)"
|
|
272
|
+
case "$cfg_value" in installed|npx|bunx) printf '%s' "$cfg_value" ;; *) printf '%s' "installed" ;; esac
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
command_is_available() {
|
|
276
|
+
local cmd="$1"
|
|
277
|
+
if [ -x "$cmd" ]; then return 0; fi
|
|
278
|
+
command -v "$cmd" >/dev/null 2>&1
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
verify_json_results() {
|
|
282
|
+
node -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { try { const j = JSON.parse(s); process.exit(Array.isArray(j.results) ? 0 : 1); } catch { process.exit(1); } });'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
verify_searxng_url() {
|
|
286
|
+
local base
|
|
287
|
+
base="$(normalize_url "$1")"
|
|
288
|
+
local body
|
|
289
|
+
if ! body="$(curl -fsSL -A "$USER_AGENT" --get "$base/search" --data-urlencode "q=pi-web-toolkit" --data "format=json" 2>/dev/null)"; then
|
|
290
|
+
return 1
|
|
291
|
+
fi
|
|
292
|
+
printf '%s' "$body" | verify_json_results
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
port_has_http_service() {
|
|
296
|
+
local base
|
|
297
|
+
base="$(normalize_url "$1")"
|
|
298
|
+
curl -fsSL -A "$USER_AGENT" --max-time 2 "$base/" >/dev/null 2>&1
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
check_node_required() {
|
|
302
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
303
|
+
status FAIL "Node.js 22+ missing. Install Node.js 22+ before running this installer."
|
|
304
|
+
return
|
|
305
|
+
fi
|
|
306
|
+
if node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1; then
|
|
307
|
+
status OK "Node.js $(node -v)"
|
|
308
|
+
else
|
|
309
|
+
status FAIL "Node.js $(node -v) is too old; install Node.js 22+."
|
|
310
|
+
fi
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
check_command_required() {
|
|
314
|
+
local cmd="$1"
|
|
315
|
+
local label="$2"
|
|
316
|
+
local hint="$3"
|
|
317
|
+
if command -v "$cmd" >/dev/null 2>&1; then
|
|
318
|
+
local version=""
|
|
319
|
+
case "$cmd" in
|
|
320
|
+
npm|pi|uv) version="$($cmd --version 2>/dev/null | head -n 1 || true)" ;;
|
|
321
|
+
esac
|
|
322
|
+
if [ -n "$version" ]; then status OK "$label $version"; else status OK "$label"; fi
|
|
323
|
+
else
|
|
324
|
+
status FAIL "$label missing. $hint"
|
|
325
|
+
fi
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
check_prerequisites() {
|
|
329
|
+
FAIL_COUNT=0
|
|
330
|
+
check_node_required
|
|
331
|
+
check_command_required npm npm "Install npm with Node.js 22+."
|
|
332
|
+
check_command_required pi pi "Install pi first: curl -fsSL https://pi.dev/install.sh | sh"
|
|
333
|
+
check_command_required curl curl "Install curl with your OS package manager."
|
|
334
|
+
check_command_required openssl OpenSSL "Install OpenSSL with your OS package manager."
|
|
335
|
+
check_command_required uv uv "Install uv from https://docs.astral.sh/uv/ before installing Scrapling."
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
ensure_prerequisites_or_exit() {
|
|
339
|
+
check_prerequisites
|
|
340
|
+
if [ "$FAIL_COUNT" -ne 0 ]; then
|
|
341
|
+
die "Missing required prerequisites; no system-level packages were installed."
|
|
342
|
+
fi
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
select_existing_searxng_url() {
|
|
346
|
+
local candidate="$1"
|
|
347
|
+
local source="$2"
|
|
348
|
+
candidate="$(normalize_url "$candidate")"
|
|
349
|
+
log "Verifying SearXNG endpoint ($source): $candidate"
|
|
350
|
+
if verify_searxng_url "$candidate"; then
|
|
351
|
+
SELECTED_SEARXNG_URL="$candidate"
|
|
352
|
+
SEARXNG_SOURCE="$source"
|
|
353
|
+
return 0
|
|
354
|
+
fi
|
|
355
|
+
return 1
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
discover_public_searxng() {
|
|
359
|
+
local raw candidates verified
|
|
360
|
+
raw="$(curl -fsSL -A "$USER_AGENT" "$PUBLIC_INSTANCES_URL")" || return 1
|
|
361
|
+
candidates="$(mktemp)"
|
|
362
|
+
verified="$(mktemp)"
|
|
363
|
+
printf '%s' "$raw" | node -e '
|
|
364
|
+
const fs = require("fs");
|
|
365
|
+
const data = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
366
|
+
const rows = [];
|
|
367
|
+
for (const [url, meta] of Object.entries(data.instances || {})) {
|
|
368
|
+
if (meta.main !== true) continue;
|
|
369
|
+
if (meta.network_type !== "normal") continue;
|
|
370
|
+
if (!meta.http || meta.http.status_code !== 200 || meta.http.error) continue;
|
|
371
|
+
if (meta.generator !== "searxng") continue;
|
|
372
|
+
const uptime = meta.uptime || {};
|
|
373
|
+
const timing = meta.timing || {};
|
|
374
|
+
const search = timing.search || {};
|
|
375
|
+
const all = search.all || {};
|
|
376
|
+
const month = Number(uptime.uptimeMonth || 0);
|
|
377
|
+
const year = Number(uptime.uptimeYear || 0);
|
|
378
|
+
const success = Number(search.success_percentage || 0);
|
|
379
|
+
const median = Number(all.median || 999);
|
|
380
|
+
if (month < 95 || success < 80) continue;
|
|
381
|
+
const analyticsPenalty = meta.analytics ? 0.5 : 0;
|
|
382
|
+
const score = (month / 100) * 2 + (year / 100) + (success / 100) * 2 - Math.min(median, 10) / 10 - analyticsPenalty;
|
|
383
|
+
rows.push({ score, url, month, year, success, median, analytics: Boolean(meta.analytics) });
|
|
384
|
+
}
|
|
385
|
+
rows.sort((a, b) => b.score - a.score);
|
|
386
|
+
for (const row of rows.slice(0, 20)) {
|
|
387
|
+
console.log([row.score.toFixed(3), row.url, row.month, row.year, row.success, row.median, row.analytics].join("\t"));
|
|
388
|
+
}
|
|
389
|
+
' > "$candidates"
|
|
390
|
+
|
|
391
|
+
local checked=0 found=0
|
|
392
|
+
while IFS=$'\t' read -r score url month year success median analytics; do
|
|
393
|
+
[ -n "$url" ] || continue
|
|
394
|
+
checked=$((checked + 1))
|
|
395
|
+
if verify_searxng_url "$url"; then
|
|
396
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$url" "$month" "$year" "$success" "$median" "$analytics" >> "$verified"
|
|
397
|
+
found=$((found + 1))
|
|
398
|
+
[ "$found" -ge 5 ] && break
|
|
399
|
+
fi
|
|
400
|
+
[ "$checked" -ge 20 ] && break
|
|
401
|
+
sleep 1
|
|
402
|
+
done < "$candidates"
|
|
403
|
+
|
|
404
|
+
if [ "$found" -eq 0 ]; then
|
|
405
|
+
rm -f "$candidates" "$verified"
|
|
406
|
+
return 1
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
if [ "$AUTO_SEARXNG" = "public" ] || ! is_interactive; then
|
|
410
|
+
IFS=$'\t' read -r url month year success median analytics < "$verified"
|
|
411
|
+
SELECTED_SEARXNG_URL="$(normalize_url "$url")"
|
|
412
|
+
SEARXNG_SOURCE="public SearXNG endpoint from searx.space"
|
|
413
|
+
log "Selected public SearXNG endpoint: $SELECTED_SEARXNG_URL (uptime month ${month}%, year ${year}%, median ${median}s)"
|
|
414
|
+
rm -f "$candidates" "$verified"
|
|
415
|
+
return 0
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
log "Found JSON-capable public SearXNG endpoints (queries leave your machine):"
|
|
419
|
+
local i=1
|
|
420
|
+
while IFS=$'\t' read -r url month year success median analytics; do
|
|
421
|
+
printf ' [%s] %s\n uptime: month %s%%, year %s%%; search success: %s%%; median: %ss; analytics: %s\n' "$i" "$url" "$month" "$year" "$success" "$median" "$analytics"
|
|
422
|
+
i=$((i + 1))
|
|
423
|
+
done < "$verified"
|
|
424
|
+
printf 'Choose endpoint [1-%s], or press Enter to use [1]: ' "$found"
|
|
425
|
+
local choice
|
|
426
|
+
read_tty choice
|
|
427
|
+
[ -n "$choice" ] || choice="1"
|
|
428
|
+
case "$choice" in ''|*[!0-9]*) rm -f "$candidates" "$verified"; return 1 ;; esac
|
|
429
|
+
if [ "$choice" -lt 1 ] || [ "$choice" -gt "$found" ]; then rm -f "$candidates" "$verified"; return 1; fi
|
|
430
|
+
url="$(sed -n "${choice}p" "$verified" | cut -f1)"
|
|
431
|
+
SELECTED_SEARXNG_URL="$(normalize_url "$url")"
|
|
432
|
+
SEARXNG_SOURCE="public SearXNG endpoint from searx.space"
|
|
433
|
+
rm -f "$candidates" "$verified"
|
|
434
|
+
return 0
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
ensure_local_docker_searxng() {
|
|
438
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
439
|
+
die "Docker is required for --auto-searxng local-docker. Install/start Docker and retry."
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
local base="http://127.0.0.1:${SEARXNG_PORT}"
|
|
443
|
+
if verify_searxng_url "$base"; then
|
|
444
|
+
SELECTED_SEARXNG_URL="$base"
|
|
445
|
+
SEARXNG_SOURCE="existing local SearXNG endpoint"
|
|
446
|
+
log "Using existing JSON-capable SearXNG at $base"
|
|
447
|
+
return 0
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
while port_has_http_service "$base"; do
|
|
451
|
+
if ! is_interactive; then
|
|
452
|
+
die "Port ${SEARXNG_PORT} is occupied by a non-SearXNG service. Pass --searxng-port with a free port."
|
|
453
|
+
fi
|
|
454
|
+
printf 'Port %s is occupied by a non-SearXNG service. Enter another port: ' "$SEARXNG_PORT"
|
|
455
|
+
read_tty SEARXNG_PORT
|
|
456
|
+
base="http://127.0.0.1:${SEARXNG_PORT}"
|
|
457
|
+
done
|
|
458
|
+
|
|
459
|
+
local searxng_config_dir="$CONFIG_DIR/searxng"
|
|
460
|
+
mkdir -p "$searxng_config_dir"
|
|
461
|
+
cat > "$searxng_config_dir/settings.yml" <<'YAML'
|
|
462
|
+
use_default_settings: true
|
|
463
|
+
|
|
464
|
+
search:
|
|
465
|
+
formats:
|
|
466
|
+
- html
|
|
467
|
+
- json
|
|
468
|
+
YAML
|
|
469
|
+
|
|
470
|
+
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$LOCAL_SEARXNG_CONTAINER" >/dev/null; then
|
|
471
|
+
log "Reusing Docker container: $LOCAL_SEARXNG_CONTAINER"
|
|
472
|
+
docker start "$LOCAL_SEARXNG_CONTAINER" >/dev/null
|
|
473
|
+
else
|
|
474
|
+
log "Starting isolated local SearXNG Docker container: $LOCAL_SEARXNG_CONTAINER"
|
|
475
|
+
docker run -d \
|
|
476
|
+
--name "$LOCAL_SEARXNG_CONTAINER" \
|
|
477
|
+
--restart unless-stopped \
|
|
478
|
+
-p "127.0.0.1:${SEARXNG_PORT}:8080" \
|
|
479
|
+
-e FORCE_OWNERSHIP=false \
|
|
480
|
+
-e "SEARXNG_SECRET=$(openssl rand -hex 32)" \
|
|
481
|
+
-v "$searxng_config_dir/settings.yml:/etc/searxng/settings.yml:ro" \
|
|
482
|
+
docker.io/searxng/searxng:latest >/dev/null
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
local attempt=1
|
|
486
|
+
while [ "$attempt" -le 15 ]; do
|
|
487
|
+
if verify_searxng_url "$base"; then
|
|
488
|
+
SELECTED_SEARXNG_URL="$base"
|
|
489
|
+
SEARXNG_SOURCE="installer-managed local Docker SearXNG"
|
|
490
|
+
return 0
|
|
491
|
+
fi
|
|
492
|
+
sleep 1
|
|
493
|
+
attempt=$((attempt + 1))
|
|
494
|
+
done
|
|
495
|
+
die "Local Docker SearXNG did not become ready at $base"
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
select_searxng() {
|
|
499
|
+
if [ -n "$SEARXNG_URL_ARG" ]; then
|
|
500
|
+
select_existing_searxng_url "$SEARXNG_URL_ARG" "--searxng-url" || die "SearXNG endpoint does not support JSON search: $SEARXNG_URL_ARG"
|
|
501
|
+
return
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
if [ -n "${SEARXNG_URL:-}" ]; then
|
|
505
|
+
if select_existing_searxng_url "$SEARXNG_URL" "SEARXNG_URL"; then return; fi
|
|
506
|
+
warn "SEARXNG_URL is set but did not verify: $SEARXNG_URL"
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
local cfg_url=""
|
|
510
|
+
cfg_url="$(config_get searxngUrl 2>/dev/null || true)"
|
|
511
|
+
if [ -n "$cfg_url" ]; then
|
|
512
|
+
if select_existing_searxng_url "$cfg_url" "toolkit config"; then return; fi
|
|
513
|
+
warn "Configured SearXNG endpoint did not verify: $cfg_url"
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
if select_existing_searxng_url "http://localhost:8080" "localhost default"; then return; fi
|
|
517
|
+
if select_existing_searxng_url "http://127.0.0.1:8080" "localhost default"; then return; fi
|
|
518
|
+
|
|
519
|
+
case "$AUTO_SEARXNG" in
|
|
520
|
+
public)
|
|
521
|
+
log "Discovering public SearXNG endpoints from searx.space..."
|
|
522
|
+
discover_public_searxng || die "No verified public SearXNG endpoint found. Provide --searxng-url or use --auto-searxng local-docker."
|
|
523
|
+
return ;;
|
|
524
|
+
local-docker)
|
|
525
|
+
ensure_local_docker_searxng
|
|
526
|
+
return ;;
|
|
527
|
+
esac
|
|
528
|
+
|
|
529
|
+
if is_interactive; then
|
|
530
|
+
log "No JSON-capable SearXNG endpoint was found."
|
|
531
|
+
log "Options:"
|
|
532
|
+
log " 1) Enter an existing SearXNG URL"
|
|
533
|
+
log " 2) Choose a verified public SearXNG endpoint"
|
|
534
|
+
log " 3) Start an isolated local Docker SearXNG instance"
|
|
535
|
+
log " 4) Abort"
|
|
536
|
+
printf 'Choose [1-4]: '
|
|
537
|
+
local choice custom
|
|
538
|
+
read_tty choice
|
|
539
|
+
case "$choice" in
|
|
540
|
+
1)
|
|
541
|
+
printf 'SearXNG URL: '
|
|
542
|
+
read_tty custom
|
|
543
|
+
select_existing_searxng_url "$custom" "custom input" || die "SearXNG endpoint does not support JSON search: $custom" ;;
|
|
544
|
+
2) discover_public_searxng || die "No verified public SearXNG endpoint found." ;;
|
|
545
|
+
3) ensure_local_docker_searxng ;;
|
|
546
|
+
*) die "SearXNG endpoint is required for web_search." ;;
|
|
547
|
+
esac
|
|
548
|
+
return
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
die "No JSON-capable SearXNG endpoint found. Pass --searxng-url, --auto-searxng public, or --auto-searxng local-docker."
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
ensure_scrapling() {
|
|
555
|
+
if ! SCRAPLING_BIN="$(resolve_executable_path scrapling 2>/dev/null || true)" || [ -z "$SCRAPLING_BIN" ]; then
|
|
556
|
+
log "Installing Scrapling with uv..."
|
|
557
|
+
uv tool install "scrapling[all]"
|
|
558
|
+
SCRAPLING_BIN="$(resolve_executable_path scrapling 2>/dev/null || true)"
|
|
559
|
+
else
|
|
560
|
+
log "Reusing Scrapling: $SCRAPLING_BIN"
|
|
561
|
+
fi
|
|
562
|
+
[ -n "$SCRAPLING_BIN" ] || die "Scrapling was installed but is not on PATH. Add ~/.local/bin to PATH or set SCRAPLING_BIN."
|
|
563
|
+
"$SCRAPLING_BIN" install >/dev/null
|
|
564
|
+
"$SCRAPLING_BIN" --help >/dev/null
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
ensure_agent_browser() {
|
|
568
|
+
if ! AGENT_BROWSER_BIN="$(resolve_executable_path agent-browser 2>/dev/null || true)" || [ -z "$AGENT_BROWSER_BIN" ]; then
|
|
569
|
+
log "Installing agent-browser with npm..."
|
|
570
|
+
npm install -g agent-browser
|
|
571
|
+
AGENT_BROWSER_BIN="$(resolve_executable_path agent-browser 2>/dev/null || true)"
|
|
572
|
+
else
|
|
573
|
+
log "Reusing agent-browser: $AGENT_BROWSER_BIN"
|
|
574
|
+
fi
|
|
575
|
+
[ -n "$AGENT_BROWSER_BIN" ] || die "agent-browser was installed but is not on PATH. Set AGENT_BROWSER_BIN or fix npm global PATH."
|
|
576
|
+
if [ "$AGENT_BROWSER_WITH_DEPS" -eq 1 ]; then
|
|
577
|
+
"$AGENT_BROWSER_BIN" install --with-deps >/dev/null
|
|
578
|
+
else
|
|
579
|
+
"$AGENT_BROWSER_BIN" install >/dev/null
|
|
580
|
+
fi
|
|
581
|
+
"$AGENT_BROWSER_BIN" doctor >/dev/null
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
ensure_firecrawl() {
|
|
585
|
+
if [ -z "$FIRECRAWL_CHOICE" ]; then
|
|
586
|
+
if is_interactive && confirm "Install optional Firecrawl fallback? Queries/URLs may leave your machine when fallback runs." "n"; then
|
|
587
|
+
FIRECRAWL_CHOICE="with"
|
|
588
|
+
if is_interactive; then
|
|
589
|
+
log "Firecrawl runner options: installed (global CLI), npx, bunx."
|
|
590
|
+
log "npx/bunx may run or download firecrawl-cli at fallback time."
|
|
591
|
+
printf 'Firecrawl runner [installed/npx/bunx] (default: installed): '
|
|
592
|
+
local runner_choice
|
|
593
|
+
read_tty runner_choice
|
|
594
|
+
if [ -n "$runner_choice" ]; then
|
|
595
|
+
case "$runner_choice" in installed|npx|bunx) FIRECRAWL_RUNNER="$runner_choice" ;; *) die "Unknown Firecrawl runner: $runner_choice" ;; esac
|
|
596
|
+
fi
|
|
597
|
+
fi
|
|
598
|
+
else
|
|
599
|
+
FIRECRAWL_CHOICE="no"
|
|
600
|
+
fi
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
if [ "$FIRECRAWL_CHOICE" = "no" ]; then
|
|
604
|
+
FIRECRAWL_FALLBACK="false"
|
|
605
|
+
log "Skipping optional Firecrawl fallback."
|
|
606
|
+
return
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
FIRECRAWL_FALLBACK="true"
|
|
610
|
+
case "$FIRECRAWL_RUNNER" in
|
|
611
|
+
installed)
|
|
612
|
+
if ! FIRECRAWL_BIN="$(resolve_executable_path firecrawl 2>/dev/null || true)" || [ -z "$FIRECRAWL_BIN" ]; then
|
|
613
|
+
log "Installing firecrawl-cli with npm..."
|
|
614
|
+
npm install -g firecrawl-cli
|
|
615
|
+
FIRECRAWL_BIN="$(resolve_executable_path firecrawl 2>/dev/null || true)"
|
|
616
|
+
else
|
|
617
|
+
log "Reusing firecrawl: $FIRECRAWL_BIN"
|
|
618
|
+
fi
|
|
619
|
+
[ -n "$FIRECRAWL_BIN" ] || die "firecrawl-cli was installed but the firecrawl command was not found."
|
|
620
|
+
"$FIRECRAWL_BIN" --help >/dev/null
|
|
621
|
+
;;
|
|
622
|
+
npx)
|
|
623
|
+
command_is_available npx || die "npx is required for --firecrawl-runner npx. Install npm/npx or use --firecrawl-runner installed."
|
|
624
|
+
npx --version >/dev/null
|
|
625
|
+
FIRECRAWL_BIN=""
|
|
626
|
+
log "Using Firecrawl runner: npx (opt-in; may run/download firecrawl-cli at fallback time)."
|
|
627
|
+
;;
|
|
628
|
+
bunx)
|
|
629
|
+
command_is_available bunx || die "bunx is required for --firecrawl-runner bunx. Install Bun or use --firecrawl-runner installed."
|
|
630
|
+
bunx --version >/dev/null
|
|
631
|
+
FIRECRAWL_BIN=""
|
|
632
|
+
log "Using Firecrawl runner: bunx (opt-in; may run/download firecrawl-cli at fallback time)."
|
|
633
|
+
;;
|
|
634
|
+
esac
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
write_toolkit_config() {
|
|
638
|
+
node - "$CONFIG_FILE" "$SELECTED_SEARXNG_URL" "$SCRAPLING_BIN" "$AGENT_BROWSER_BIN" "$FIRECRAWL_BIN" "$FIRECRAWL_FALLBACK" "$FIRECRAWL_RUNNER" <<'NODE'
|
|
639
|
+
const fs = require('fs');
|
|
640
|
+
const path = require('path');
|
|
641
|
+
const [file, searxngUrl, scrapling, agentBrowser, firecrawl, fallback, runner] = process.argv.slice(2);
|
|
642
|
+
let cfg = {};
|
|
643
|
+
if (fs.existsSync(file)) {
|
|
644
|
+
try {
|
|
645
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
646
|
+
} catch (err) {
|
|
647
|
+
console.error(`Invalid toolkit config at ${file}: ${err.message}`);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) cfg = {};
|
|
652
|
+
if (searxngUrl) cfg.searxngUrl = searxngUrl.replace(/\/+$/, '');
|
|
653
|
+
cfg.commands = cfg.commands && typeof cfg.commands === 'object' && !Array.isArray(cfg.commands) ? cfg.commands : {};
|
|
654
|
+
if (scrapling) cfg.commands.scrapling = scrapling;
|
|
655
|
+
if (agentBrowser) cfg.commands.agentBrowser = agentBrowser;
|
|
656
|
+
if (firecrawl) cfg.commands.firecrawl = firecrawl;
|
|
657
|
+
if (fallback === 'false') {
|
|
658
|
+
cfg.firecrawlFallback = false;
|
|
659
|
+
delete cfg.firecrawlRunner;
|
|
660
|
+
delete cfg.commands.firecrawl;
|
|
661
|
+
} else if (fallback === 'true') {
|
|
662
|
+
cfg.firecrawlFallback = true;
|
|
663
|
+
cfg.firecrawlRunner = runner || 'installed';
|
|
664
|
+
if (cfg.firecrawlRunner !== 'installed') delete cfg.commands.firecrawl;
|
|
665
|
+
}
|
|
666
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
667
|
+
fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
668
|
+
NODE
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
install_pi_package() {
|
|
672
|
+
if [ "$DEPS_ONLY" -eq 1 ]; then
|
|
673
|
+
log "Skipping pi package install (--deps-only)."
|
|
674
|
+
return
|
|
675
|
+
fi
|
|
676
|
+
if [ "$LOCAL_INSTALL" -eq 1 ]; then
|
|
677
|
+
log "Installing local pi package..."
|
|
678
|
+
pi install ./
|
|
679
|
+
else
|
|
680
|
+
log "Installing pi package from npm..."
|
|
681
|
+
pi install npm:pi-web-toolkit
|
|
682
|
+
fi
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
run_final_verification() {
|
|
686
|
+
log ""
|
|
687
|
+
log "Final verification:"
|
|
688
|
+
FAIL_COUNT=0
|
|
689
|
+
check_node_required
|
|
690
|
+
check_command_required npm npm "Install npm with Node.js 22+."
|
|
691
|
+
check_command_required pi pi "Install pi first."
|
|
692
|
+
if verify_searxng_url "$SELECTED_SEARXNG_URL"; then status OK "SearXNG $SELECTED_SEARXNG_URL"; else status FAIL "SearXNG $SELECTED_SEARXNG_URL"; fi
|
|
693
|
+
if [ -n "$SCRAPLING_BIN" ] && "$SCRAPLING_BIN" --help >/dev/null 2>&1; then status OK "scrapling $SCRAPLING_BIN"; else status FAIL "scrapling"; fi
|
|
694
|
+
if [ -n "$AGENT_BROWSER_BIN" ] && "$AGENT_BROWSER_BIN" doctor >/dev/null 2>&1; then status OK "agent-browser $AGENT_BROWSER_BIN"; else status FAIL "agent-browser"; fi
|
|
695
|
+
if [ "$FIRECRAWL_FALLBACK" = "true" ]; then
|
|
696
|
+
case "$FIRECRAWL_RUNNER" in
|
|
697
|
+
installed)
|
|
698
|
+
if [ -n "$FIRECRAWL_BIN" ] && "$FIRECRAWL_BIN" --help >/dev/null 2>&1; then status OK "Firecrawl runner installed ($FIRECRAWL_BIN)"; else status FAIL "Firecrawl enabled but installed CLI not ready"; fi ;;
|
|
699
|
+
npx)
|
|
700
|
+
if command_is_available npx; then status OK "Firecrawl runner npx"; else status FAIL "Firecrawl runner npx missing"; fi ;;
|
|
701
|
+
bunx)
|
|
702
|
+
if command_is_available bunx; then status OK "Firecrawl runner bunx"; else status FAIL "Firecrawl runner bunx missing"; fi ;;
|
|
703
|
+
esac
|
|
704
|
+
else
|
|
705
|
+
status SKIP "Firecrawl optional fallback disabled"
|
|
706
|
+
fi
|
|
707
|
+
[ "$FAIL_COUNT" -eq 0 ] || die "Final verification failed."
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
run_doctor() {
|
|
711
|
+
log "pi-web-toolkit doctor"
|
|
712
|
+
FAIL_COUNT=0
|
|
713
|
+
check_node_required
|
|
714
|
+
check_command_required npm npm "Install npm with Node.js 22+."
|
|
715
|
+
check_command_required pi pi "Install pi first."
|
|
716
|
+
check_command_required curl curl "Install curl with your OS package manager."
|
|
717
|
+
check_command_required openssl OpenSSL "Install OpenSSL with your OS package manager."
|
|
718
|
+
check_command_required uv uv "Install uv before installing Scrapling."
|
|
719
|
+
|
|
720
|
+
if validate_config_if_present >/dev/null 2>&1; then
|
|
721
|
+
if [ -f "$CONFIG_FILE" ]; then status OK "toolkit config $CONFIG_FILE"; else status SKIP "toolkit config not found; defaults/env will be used"; fi
|
|
722
|
+
else
|
|
723
|
+
status FAIL "toolkit config invalid: $CONFIG_FILE"
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
local searxng
|
|
727
|
+
searxng="$(runtime_searxng_url)"
|
|
728
|
+
if verify_searxng_url "$searxng"; then status OK "SearXNG $searxng"; else status FAIL "SearXNG $searxng did not return JSON results"; fi
|
|
729
|
+
|
|
730
|
+
local scrapling agent_browser firecrawl firecrawl_runner
|
|
731
|
+
scrapling="$(runtime_command scrapling SCRAPLING_BIN scrapling)"
|
|
732
|
+
if command_is_available "$scrapling" && "$scrapling" --help >/dev/null 2>&1; then status OK "scrapling $scrapling"; else status FAIL "scrapling missing or not working"; fi
|
|
733
|
+
|
|
734
|
+
agent_browser="$(runtime_command agentBrowser AGENT_BROWSER_BIN agent-browser)"
|
|
735
|
+
if command_is_available "$agent_browser" && "$agent_browser" doctor >/dev/null 2>&1; then status OK "agent-browser $agent_browser"; else status FAIL "agent-browser missing or doctor failed"; fi
|
|
736
|
+
|
|
737
|
+
if runtime_firecrawl_enabled; then
|
|
738
|
+
firecrawl_runner="$(runtime_firecrawl_runner)"
|
|
739
|
+
case "$firecrawl_runner" in
|
|
740
|
+
installed)
|
|
741
|
+
firecrawl="$(runtime_command firecrawl FIRECRAWL_BIN firecrawl)"
|
|
742
|
+
if command_is_available "$firecrawl" && "$firecrawl" --help >/dev/null 2>&1; then status OK "Firecrawl runner installed ($firecrawl)"; else status SKIP "Firecrawl runner installed but firecrawl CLI is not installed"; fi ;;
|
|
743
|
+
npx)
|
|
744
|
+
if command_is_available npx; then status OK "Firecrawl runner npx"; else status SKIP "Firecrawl runner npx missing; install npm/npx or choose installed/bunx"; fi ;;
|
|
745
|
+
bunx)
|
|
746
|
+
if command_is_available bunx; then status OK "Firecrawl runner bunx"; else status SKIP "Firecrawl runner bunx missing; install Bun or choose installed/npx"; fi ;;
|
|
747
|
+
esac
|
|
748
|
+
else
|
|
749
|
+
status SKIP "Firecrawl optional fallback disabled"
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
if pi list 2>/dev/null | grep -q 'pi-web-toolkit'; then
|
|
753
|
+
status OK "pi-web-toolkit package installed"
|
|
754
|
+
else
|
|
755
|
+
status WARN "pi-web-toolkit package not listed by pi"
|
|
756
|
+
fi
|
|
757
|
+
|
|
758
|
+
[ "$FAIL_COUNT" -eq 0 ] || exit 1
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
run_install() {
|
|
762
|
+
validate_config_if_present || die "Fix or remove invalid toolkit config before installing: $CONFIG_FILE"
|
|
763
|
+
|
|
764
|
+
if [ "$EXTENSION_ONLY" -eq 0 ]; then
|
|
765
|
+
ensure_prerequisites_or_exit
|
|
766
|
+
select_searxng
|
|
767
|
+
ensure_scrapling
|
|
768
|
+
ensure_agent_browser
|
|
769
|
+
ensure_firecrawl
|
|
770
|
+
write_toolkit_config
|
|
771
|
+
else
|
|
772
|
+
log "Skipping dependency setup (--extension-only)."
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
install_pi_package
|
|
776
|
+
|
|
777
|
+
if [ "$EXTENSION_ONLY" -eq 0 ]; then
|
|
778
|
+
run_final_verification
|
|
779
|
+
fi
|
|
780
|
+
|
|
781
|
+
log ""
|
|
782
|
+
log "pi-web-toolkit installation summary"
|
|
783
|
+
log " Toolkit config: $CONFIG_FILE"
|
|
784
|
+
if [ -n "$SELECTED_SEARXNG_URL" ]; then log " SearXNG endpoint: $SELECTED_SEARXNG_URL ($SEARXNG_SOURCE)"; fi
|
|
785
|
+
if [ -n "$SCRAPLING_BIN" ]; then log " Scrapling: $SCRAPLING_BIN"; fi
|
|
786
|
+
if [ -n "$AGENT_BROWSER_BIN" ]; then log " agent-browser: $AGENT_BROWSER_BIN"; fi
|
|
787
|
+
if [ "$FIRECRAWL_FALLBACK" = "true" ]; then
|
|
788
|
+
if [ "$FIRECRAWL_RUNNER" = "installed" ]; then log " Firecrawl fallback: enabled (installed: $FIRECRAWL_BIN)"; else log " Firecrawl fallback: enabled ($FIRECRAWL_RUNNER)"; fi
|
|
789
|
+
else
|
|
790
|
+
log " Firecrawl fallback: disabled/skipped"
|
|
791
|
+
fi
|
|
792
|
+
log ""
|
|
793
|
+
log "Next step: Restart pi. If pi-web-toolkit was already loaded and only config changed, /reload may also work."
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
parse_args "$@"
|
|
797
|
+
if [ "$DOCTOR" -eq 1 ]; then
|
|
798
|
+
run_doctor
|
|
799
|
+
else
|
|
800
|
+
run_install
|
|
801
|
+
fi
|