gemini-computer-use 0.2.3 → 0.4.1
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/README.md +935 -148
- package/bin/cli.js +112 -17
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/agent.cpython-313.pyc +0 -0
- package/core/__pycache__/code_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/mcp_tools.cpython-313.pyc +0 -0
- package/core/__pycache__/schemas.cpython-313.pyc +0 -0
- package/core/__pycache__/server.cpython-313.pyc +0 -0
- package/core/__pycache__/vitals.cpython-313.pyc +0 -0
- package/core/__pycache__/web_share.cpython-313.pyc +0 -0
- package/core/agent.py +688 -27
- package/core/agent_harness.py +9 -2
- package/core/code_cache.py +15 -3
- package/core/device.py +2400 -0
- package/core/domain.py +303 -0
- package/core/mcp_tools.py +1949 -110
- package/core/policies.py +35 -5
- package/core/server.py +78 -32
- package/core/termux.py +916 -0
- package/core/updater.py +1713 -0
- package/core/version.py +31 -0
- package/core/vitals.py +66 -2
- package/core/web_share.py +814 -0
- package/gateway.py +1469 -98
- package/install-gui.cmd +40 -0
- package/install-gui.ps1 +1882 -0
- package/install-gui.strings.json +211 -0
- package/install.ps1 +960 -92
- package/install.sh +1214 -81
- package/ops/doctor.ps1 +735 -0
- package/ops/mesh-domain.sh +86 -0
- package/ops/nginx/README.md +77 -0
- package/ops/nginx/antigravity-mesh-mcp.conf +172 -0
- package/ops/nginx/mesh-domain.vhost.template +58 -0
- package/ops/nginx/render-domain.sh +123 -0
- package/ops/update.ps1 +373 -0
- package/ops/windows/README.md +298 -0
- package/ops/windows/agent-watchdog.ps1 +575 -0
- package/ops/windows/run-hidden.vbs +64 -0
- package/package.json +6 -2
- package/skills/orchestrator.md +12 -0
package/install.sh
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Antigravity Mesh - Universal Turnkey Installer (v0.1.2)
|
|
4
4
|
# Bilingual: English (Default) & Russian, Device Detection, SSH & Autostart
|
|
5
|
+
# Targets: Linux, macOS, Windows (Git Bash) and Android/Termux (phone/tablet)
|
|
5
6
|
# ==============================================================================
|
|
6
7
|
set -e
|
|
7
8
|
|
|
@@ -10,7 +11,7 @@ MODE=""
|
|
|
10
11
|
TLS="none"
|
|
11
12
|
PORT="8096"
|
|
12
13
|
DOMAIN=""
|
|
13
|
-
GATEWAY="
|
|
14
|
+
GATEWAY=""
|
|
14
15
|
USERNAME=""
|
|
15
16
|
TOKEN=""
|
|
16
17
|
DRY_RUN=false
|
|
@@ -23,18 +24,267 @@ EXPLICIT_LANG=""
|
|
|
23
24
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
24
25
|
[ -z "$SCRIPT_DIR" ] && SCRIPT_DIR="."
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
# ------------------------------------------------------------------------------
|
|
28
|
+
# PUBLIC DOMAIN - resolved from the one source of truth
|
|
29
|
+
#
|
|
30
|
+
# The domain is declared exactly once, in core/domain.py. Everything below asks
|
|
31
|
+
# that same chain, in the same order, and this is the ONLY place in this script
|
|
32
|
+
# that picks a hostname:
|
|
33
|
+
#
|
|
34
|
+
# 1. an explicit --domain= / --gateway= on the command line;
|
|
35
|
+
# 2. MESH_PUBLIC_URL in the environment;
|
|
36
|
+
# 3. AGY_PUBLIC_BASE_URL in the environment (legacy alias, same meaning);
|
|
37
|
+
# 4. the domain file - MESH_DOMAIN_FILE when set, otherwise
|
|
38
|
+
# /etc/antigravity-mesh/domain.env, or on Windows/Git Bash
|
|
39
|
+
# %USERPROFILE%\.config\antigravity-mesh\domain.env;
|
|
40
|
+
# 5. $PUBLISHED_DOMAIN - the domain this copy was published with. The repository
|
|
41
|
+
# carries the placeholder __MESH_DOMAIN__ here and deploy_gateway.sh rewrites
|
|
42
|
+
# that line while publishing, so a served copy already knows its domain;
|
|
43
|
+
# 6. core/domain.py's DEFAULT_PUBLIC_BASE_URL, but only when that module sits
|
|
44
|
+
# next to this script (a repository checkout, or a node that already
|
|
45
|
+
# bootstrapped). A clone has no published value and refusing to install from
|
|
46
|
+
# a clone would be wrong.
|
|
47
|
+
#
|
|
48
|
+
# If none of them yields a real hostname the installer stops: registering a node
|
|
49
|
+
# against a domain that does not resolve produces a node that can never connect
|
|
50
|
+
# and an autostart entry that retries forever.
|
|
51
|
+
# ------------------------------------------------------------------------------
|
|
52
|
+
DOMAIN_PLACEHOLDER="__MESH_DOMAIN__"
|
|
53
|
+
# deploy_gateway.sh rewrites THIS line only (it is anchored on PUBLISHED_DOMAIN=),
|
|
54
|
+
# so the placeholder token above survives and still proves whether this copy was
|
|
55
|
+
# published. In the repository the two are equal, which means "not published".
|
|
56
|
+
PUBLISHED_DOMAIN="__MESH_DOMAIN__"
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------------------
|
|
59
|
+
# TERMUX / ANDROID
|
|
60
|
+
#
|
|
61
|
+
# Termux is NOT FHS-compliant: there is no /etc, no /bin and no /usr at the
|
|
62
|
+
# usual locations, there is no root and no sudo, and its packages are built
|
|
63
|
+
# against Android's bionic libc. A phone is therefore neither "just Linux" (the
|
|
64
|
+
# Debian package names and the host-level domain file do not exist) nor
|
|
65
|
+
# something the Linux branches can serve unchanged. Everything Termux-specific
|
|
66
|
+
# in this script keys off the single flag below:
|
|
67
|
+
#
|
|
68
|
+
# IS_TERMUX - the one question every branch asks;
|
|
69
|
+
# TERMUX_PREFIX - $PREFIX, the Termux root (.../com.termux/files/usr);
|
|
70
|
+
# TERMUX_SUPERVISED - set by configure_termux_node when runit supervises.
|
|
71
|
+
#
|
|
72
|
+
# Detection never trusts one signal: Termux exports TERMUX_VERSION, its $PREFIX
|
|
73
|
+
# points into com.termux, and its application data directory has a fixed path.
|
|
74
|
+
# A Linux host that happens to export an unrelated $PREFIX is not a phone.
|
|
75
|
+
# ------------------------------------------------------------------------------
|
|
76
|
+
IS_TERMUX=false
|
|
77
|
+
if [ -n "${TERMUX_VERSION:-}" ]; then
|
|
78
|
+
IS_TERMUX=true
|
|
37
79
|
fi
|
|
80
|
+
if [ "$IS_TERMUX" = false ] && [ -n "${PREFIX:-}" ]; then
|
|
81
|
+
case "$PREFIX" in
|
|
82
|
+
*com.termux*) IS_TERMUX=true ;;
|
|
83
|
+
esac
|
|
84
|
+
fi
|
|
85
|
+
if [ "$IS_TERMUX" = false ] && [ -d /data/data/com.termux/files/usr/bin ]; then
|
|
86
|
+
IS_TERMUX=true
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
TERMUX_PREFIX=""
|
|
90
|
+
TERMUX_SUPERVISED=false
|
|
91
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
92
|
+
TERMUX_PREFIX="${PREFIX:-}"
|
|
93
|
+
if [ -z "$TERMUX_PREFIX" ] || [ ! -d "$TERMUX_PREFIX/bin" ]; then
|
|
94
|
+
TERMUX_PREFIX="/data/data/com.termux/files/usr"
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
case "$(uname -s 2>/dev/null)" in
|
|
99
|
+
MINGW*|MSYS*|CYGWIN*) DEFAULT_DOMAIN_DIR="${USERPROFILE:-$HOME}/.config/antigravity-mesh" ;;
|
|
100
|
+
*) DEFAULT_DOMAIN_DIR="/etc/antigravity-mesh" ;;
|
|
101
|
+
esac
|
|
102
|
+
# A phone has no /etc and no root, so the per-user directory is the only place
|
|
103
|
+
# that can hold the domain file there. core/domain.py answers the same question
|
|
104
|
+
# in default_domain_file(), so the installer and the node never disagree about
|
|
105
|
+
# where the domain lives.
|
|
106
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
107
|
+
DEFAULT_DOMAIN_DIR="$HOME/.config/antigravity-mesh"
|
|
108
|
+
fi
|
|
109
|
+
DEFAULT_DOMAIN_FILE="${MESH_DOMAIN_FILE:-$DEFAULT_DOMAIN_DIR/domain.env}"
|
|
110
|
+
|
|
111
|
+
# The command that stores the domain, spelled for the platform it is printed on:
|
|
112
|
+
# there is no sudo in Termux and no writable /etc, so the hint must not send a
|
|
113
|
+
# phone user to a command that cannot exist there.
|
|
114
|
+
domain_file_write_cmd() {
|
|
115
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
116
|
+
printf "mkdir -p %s && echo 'MESH_PUBLIC_URL=%s' > %s" \
|
|
117
|
+
"$DEFAULT_DOMAIN_DIR" "$1" "$DEFAULT_DOMAIN_FILE"
|
|
118
|
+
else
|
|
119
|
+
printf "echo 'MESH_PUBLIC_URL=%s' | sudo tee %s" "$1" "$DEFAULT_DOMAIN_FILE"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# https://mesh.example.com/path/ -> mesh.example.com (the authority only)
|
|
124
|
+
normalise_host() {
|
|
125
|
+
printf '%s' "$1" | sed -e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' -e 's|/.*$||' \
|
|
126
|
+
-e 's|^[[:space:]]*||' -e 's|[[:space:]]*$||'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# A KEY=VALUE file without its leading UTF-8 BOM (Windows PowerShell writes one).
|
|
130
|
+
strip_bom() {
|
|
131
|
+
if [ "$(head -c 3 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')" = "efbbbf" ]; then
|
|
132
|
+
tail -c +4 "$1"
|
|
133
|
+
else
|
|
134
|
+
cat "$1"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# MESH_PUBLIC_URL outranks the legacy alias, whatever their order in the file.
|
|
139
|
+
domain_file_value() {
|
|
140
|
+
local file="$1" value=""
|
|
141
|
+
if [ ! -f "$file" ]; then
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
value="$(strip_bom "$file" | sed -n 's/^[[:space:]]*MESH_PUBLIC_URL[[:space:]]*=[[:space:]]*//p' | head -n 1)"
|
|
145
|
+
if [ -z "$value" ]; then
|
|
146
|
+
value="$(strip_bom "$file" | sed -n 's/^[[:space:]]*AGY_PUBLIC_BASE_URL[[:space:]]*=[[:space:]]*//p' | head -n 1)"
|
|
147
|
+
fi
|
|
148
|
+
printf '%s' "$value" | tr -d '"' | tr -d "'" | tr -d '\r' | sed -e 's|^[[:space:]]*||' -e 's|[[:space:]]*$||'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# The project default, taken from core/domain.py when that file is available here.
|
|
152
|
+
module_default_domain() {
|
|
153
|
+
local module="$SCRIPT_DIR/core/domain.py" value=""
|
|
154
|
+
if [ ! -f "$module" ]; then
|
|
155
|
+
return 0
|
|
156
|
+
fi
|
|
157
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
158
|
+
value="$(cd "$SCRIPT_DIR" 2>/dev/null && python3 -c 'from core import domain; print(domain.DEFAULT_PUBLIC_BASE_URL)' 2>/dev/null)" || true
|
|
159
|
+
fi
|
|
160
|
+
if [ -z "$value" ]; then
|
|
161
|
+
value="$(sed -n 's/^DEFAULT_PUBLIC_BASE_URL[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$module" | head -n 1)"
|
|
162
|
+
fi
|
|
163
|
+
printf '%s' "$value"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
resolve_domain() {
|
|
167
|
+
local explicit_value="${1:-}" candidate=""
|
|
168
|
+
if [ -n "$explicit_value" ]; then
|
|
169
|
+
printf '%s' "$(normalise_host "$explicit_value")"
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
for candidate in "${MESH_PUBLIC_URL:-}" "${AGY_PUBLIC_BASE_URL:-}"; do
|
|
173
|
+
if [ -n "$candidate" ]; then
|
|
174
|
+
printf '%s' "$(normalise_host "$candidate")"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
candidate="$(domain_file_value "$DEFAULT_DOMAIN_FILE")"
|
|
179
|
+
if [ -n "$candidate" ]; then
|
|
180
|
+
printf '%s' "$(normalise_host "$candidate")"
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
if [ -n "$PUBLISHED_DOMAIN" ] && [ "$PUBLISHED_DOMAIN" != "$DOMAIN_PLACEHOLDER" ]; then
|
|
184
|
+
printf '%s' "$(normalise_host "$PUBLISHED_DOMAIN")"
|
|
185
|
+
return 0
|
|
186
|
+
fi
|
|
187
|
+
candidate="$(module_default_domain)"
|
|
188
|
+
if [ -n "$candidate" ]; then
|
|
189
|
+
printf '%s' "$(normalise_host "$candidate")"
|
|
190
|
+
return 0
|
|
191
|
+
fi
|
|
192
|
+
printf '%s' "$DOMAIN_PLACEHOLDER"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------------------
|
|
196
|
+
# PERSIST THE DOMAIN (what the node reads to build its public links)
|
|
197
|
+
#
|
|
198
|
+
# core/domain.py resolves the public domain from MESH_PUBLIC_URL in the
|
|
199
|
+
# environment, then from the domain file, then from its built-in default - and it
|
|
200
|
+
# never reads the legacy MESH_GATEWAY that agent.env carries. A node installed
|
|
201
|
+
# with only agent.env therefore dials the right gateway (gateway_host() does
|
|
202
|
+
# honour MESH_GATEWAY) while minting share links on the built-in default: a real
|
|
203
|
+
# Termux node published links on the default relay while its tunnel went to the
|
|
204
|
+
# gateway it was actually registered on.
|
|
205
|
+
#
|
|
206
|
+
# This script has already resolved the domain, so it writes it down here - the
|
|
207
|
+
# same thing deploy_gateway.sh does for /etc/antigravity-mesh/domain.env on the
|
|
208
|
+
# gateway host. The place that decides the domain is then also the place that
|
|
209
|
+
# records it, and the node's links, its tunnel and the gateway agree by
|
|
210
|
+
# construction.
|
|
211
|
+
#
|
|
212
|
+
# Never fatal: an unwritable /etc (no sudo, a locked-down box) still leaves a
|
|
213
|
+
# working node - the legacy MESH_GATEWAY keeps the tunnel where it was - so a
|
|
214
|
+
# failure only prints the command the operator can run by hand.
|
|
215
|
+
# ------------------------------------------------------------------------------
|
|
216
|
+
write_domain_file() {
|
|
217
|
+
local domain existing content module_default parent
|
|
218
|
+
domain="$(normalise_host "${1:-}")"
|
|
219
|
+
if [ -z "$domain" ] || [ "$domain" = "$DOMAIN_PLACEHOLDER" ]; then
|
|
220
|
+
return 0
|
|
221
|
+
fi
|
|
222
|
+
# Never record the project's built-in default. It is a fallback, not a decision:
|
|
223
|
+
# writing it would turn "nothing is configured" into a pinned domain file, and
|
|
224
|
+
# once that file exists core/domain.py stops consulting the legacy MESH_GATEWAY
|
|
225
|
+
# at all - the operator's own agent.env knob would silently go inert. Nothing is
|
|
226
|
+
# lost by skipping it: with no file the resolver's last fallback IS that value,
|
|
227
|
+
# so the node's links and its tunnel still name the same host.
|
|
228
|
+
module_default="$(normalise_host "$(module_default_domain)")"
|
|
229
|
+
if [ -n "$module_default" ] && [ "$domain" = "$module_default" ]; then
|
|
230
|
+
return 0
|
|
231
|
+
fi
|
|
232
|
+
existing="$(normalise_host "$(domain_file_value "$DEFAULT_DOMAIN_FILE")")"
|
|
233
|
+
if [ "$existing" = "$domain" ]; then
|
|
234
|
+
return 0
|
|
235
|
+
fi
|
|
236
|
+
content="MESH_PUBLIC_URL=https://${domain}"
|
|
237
|
+
# The parent of the file that is actually used, not of the default path: with
|
|
238
|
+
# MESH_DOMAIN_FILE pointing into a directory that does not exist yet, the old
|
|
239
|
+
# code created a directory nobody would read and then failed to write.
|
|
240
|
+
parent="$(dirname "$DEFAULT_DOMAIN_FILE")"
|
|
241
|
+
|
|
242
|
+
if mkdir -p "$parent" 2>/dev/null \
|
|
243
|
+
&& printf '%s\n' "$content" > "$DEFAULT_DOMAIN_FILE" 2>/dev/null; then
|
|
244
|
+
:
|
|
245
|
+
elif run_privileged mkdir -p "$parent" >/dev/null 2>&1 \
|
|
246
|
+
&& printf '%s\n' "$content" | run_privileged tee "$DEFAULT_DOMAIN_FILE" >/dev/null 2>&1; then
|
|
247
|
+
:
|
|
248
|
+
else
|
|
249
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
250
|
+
echo -e "${YELLOW}[!] Не удалось записать домен в ${DEFAULT_DOMAIN_FILE}.${RESET}"
|
|
251
|
+
echo -e " Ссылки узла будут строиться на встроенном домене по умолчанию."
|
|
252
|
+
echo -e " Запишите вручную: ${CYAN}$(domain_file_write_cmd "https://${domain}")${RESET}"
|
|
253
|
+
else
|
|
254
|
+
echo -e "${YELLOW}[!] Could not write the domain to ${DEFAULT_DOMAIN_FILE}.${RESET}"
|
|
255
|
+
echo -e " The node's links would use the built-in default domain."
|
|
256
|
+
echo -e " Write it by hand: ${CYAN}$(domain_file_write_cmd "https://${domain}")${RESET}"
|
|
257
|
+
fi
|
|
258
|
+
return 0
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
262
|
+
if [ -n "$existing" ]; then
|
|
263
|
+
echo -e "${GREEN}[OK] Домен узла: ${content} (${DEFAULT_DOMAIN_FILE}, было https://${existing})${RESET}"
|
|
264
|
+
else
|
|
265
|
+
echo -e "${GREEN}[OK] Домен узла: ${content} (${DEFAULT_DOMAIN_FILE})${RESET}"
|
|
266
|
+
fi
|
|
267
|
+
else
|
|
268
|
+
if [ -n "$existing" ]; then
|
|
269
|
+
echo -e "${GREEN}[OK] Node domain: ${content} (${DEFAULT_DOMAIN_FILE}, was https://${existing})${RESET}"
|
|
270
|
+
else
|
|
271
|
+
echo -e "${GREEN}[OK] Node domain: ${content} (${DEFAULT_DOMAIN_FILE})${RESET}"
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
return 0
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Downloading the node code needs the domain, and so does every cloud-gateway
|
|
278
|
+
# mode. A standalone install that already has the code next to it needs neither.
|
|
279
|
+
domain_required() {
|
|
280
|
+
if [ ! -f "$SCRIPT_DIR/core/agent.py" ]; then
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
if [ "$MODE" != "standalone" ]; then
|
|
284
|
+
return 0
|
|
285
|
+
fi
|
|
286
|
+
return 1
|
|
287
|
+
}
|
|
38
288
|
|
|
39
289
|
CONFIG_DIR="$HOME/.config/antigravity-mesh"
|
|
40
290
|
CONFIG_FILE="$CONFIG_DIR/agent.env"
|
|
@@ -48,48 +298,395 @@ BLUE="\033[0;34m"
|
|
|
48
298
|
MAGENTA="\033[0;35m"
|
|
49
299
|
RESET="\033[0m"
|
|
50
300
|
|
|
51
|
-
|
|
52
|
-
|
|
301
|
+
# ------------------------------------------------------------------------------
|
|
302
|
+
# PYTHON AND ITS DEPENDENCIES
|
|
303
|
+
#
|
|
304
|
+
# websockets is the node's ONLY external requirement: core/mcp_tools.py and
|
|
305
|
+
# core/server.py are standard library only. Everything below exists because a
|
|
306
|
+
# fresh or minimal system can fail to provide it for a different reason each time:
|
|
307
|
+
#
|
|
308
|
+
# * no Python at all -> install it from the system package manager;
|
|
309
|
+
# * Python but no pip -> ensurepip, or uv, which needs no pip;
|
|
310
|
+
# * pip refuses to write -> PEP 668 "externally managed", or no permission
|
|
311
|
+
# on site-packages: --user, then a venv;
|
|
312
|
+
# * no usable package manager -> uv, which fetches its own CPython;
|
|
313
|
+
# * nothing works at all -> STOP, like the Windows installer does.
|
|
314
|
+
#
|
|
315
|
+
# The last point is the important one. This script used to continue with
|
|
316
|
+
# PYTHON_BIN=/usr/bin/python3 whether or not that file existed, wrote a systemd
|
|
317
|
+
# unit or a launchd plist naming it, and reported success. The node then never
|
|
318
|
+
# started and the only evidence was one log line - the exact silent failure the
|
|
319
|
+
# Windows installer already refuses to produce.
|
|
320
|
+
#
|
|
321
|
+
# PYTHON_BIN is the single interpreter everything afterwards must use, including
|
|
322
|
+
# the autostart unit. It is NOT necessarily "python3" from PATH: the venv and uv
|
|
323
|
+
# fallbacks both live outside PATH on purpose.
|
|
324
|
+
# ------------------------------------------------------------------------------
|
|
325
|
+
PYTHON_BIN=""
|
|
326
|
+
UV_BIN=""
|
|
327
|
+
|
|
328
|
+
# Is this a real, runnable Python 3? A name that answers to "command -v" is not
|
|
329
|
+
# evidence: a broken shim, a half-removed distro package and a stale symlink all
|
|
330
|
+
# pass that test and then fail every call.
|
|
331
|
+
python_works() {
|
|
332
|
+
[ -n "$1" ] || return 1
|
|
333
|
+
command -v "$1" >/dev/null 2>&1 || [ -x "$1" ] || return 1
|
|
334
|
+
"$1" -c 'import sys; raise SystemExit(0 if sys.version_info[0] == 3 else 1)' >/dev/null 2>&1
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
python_has_websockets() {
|
|
338
|
+
[ -n "$1" ] || return 1
|
|
339
|
+
"$1" -c 'import websockets' >/dev/null 2>&1
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
python_has_pip() {
|
|
343
|
+
[ -n "$1" ] || return 1
|
|
344
|
+
"$1" -m pip --version >/dev/null 2>&1
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Every call site after ensure_dependencies uses "$PYTHON_BIN"; this is what finds
|
|
348
|
+
# it. The uv-managed interpreter is searched too, because on a machine where uv is
|
|
349
|
+
# what provided Python there is no python3 on PATH at all.
|
|
350
|
+
resolve_python() {
|
|
351
|
+
local cand uv_root
|
|
352
|
+
for cand in python3 python; do
|
|
353
|
+
if python_works "$cand"; then
|
|
354
|
+
PYTHON_BIN="$(command -v "$cand")"
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
done
|
|
358
|
+
uv_root="${UV_PYTHON_INSTALL_DIR:-$HOME/.local/share/uv/python}"
|
|
359
|
+
if [ -d "$uv_root" ]; then
|
|
360
|
+
for cand in "$uv_root"/cpython-3*/bin/python3; do
|
|
361
|
+
if [ -x "$cand" ] && python_works "$cand"; then
|
|
362
|
+
PYTHON_BIN="$cand"
|
|
363
|
+
return 0
|
|
364
|
+
fi
|
|
365
|
+
done
|
|
366
|
+
fi
|
|
367
|
+
return 1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# sudo when not already root, and nothing at all when neither is available. The old
|
|
371
|
+
# code spelled this out per branch and forgot it for apk, so `apk add` silently did
|
|
372
|
+
# nothing on a non-root Alpine - the one platform where Python is never preinstalled.
|
|
373
|
+
run_privileged() {
|
|
374
|
+
if [ "$(id -u)" -eq 0 ]; then
|
|
375
|
+
"$@"
|
|
376
|
+
elif command -v sudo >/dev/null 2>&1; then
|
|
377
|
+
sudo "$@"
|
|
378
|
+
else
|
|
379
|
+
return 1
|
|
380
|
+
fi
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# One system-package install per platform, in a subshell with errexit off: a package
|
|
384
|
+
# manager that fails (no network, no sudo, a locked database, a dead mirror) must
|
|
385
|
+
# fall through to the next source instead of aborting the installer. Whether Python
|
|
386
|
+
# actually arrived is decided by resolve_python afterwards, never by these exit codes.
|
|
387
|
+
install_python_packages() {
|
|
388
|
+
(
|
|
389
|
+
set +e
|
|
390
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
391
|
+
# Termux comes first: it HAS apt-get, but the Debian package names below
|
|
392
|
+
# (python3-pip, python3-venv) do not exist there, `pkg` is the wrapper
|
|
393
|
+
# that refreshes the index itself, and run_privileged cannot help -
|
|
394
|
+
# a phone is never root and has no sudo. pip and the venv wheels are
|
|
395
|
+
# separate packages, and python3 is a virtual name provided by python.
|
|
396
|
+
pkg install -y python python-pip python-ensurepip-wheels curl ca-certificates
|
|
397
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
398
|
+
run_privileged apt-get update -qq
|
|
399
|
+
run_privileged apt-get install -y -qq python3 python3-pip python3-venv curl ca-certificates
|
|
400
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
401
|
+
run_privileged dnf install -y -q python3 python3-pip curl ca-certificates
|
|
402
|
+
elif command -v yum >/dev/null 2>&1; then
|
|
403
|
+
run_privileged yum install -y -q python3 python3-pip curl ca-certificates
|
|
404
|
+
elif command -v zypper >/dev/null 2>&1; then
|
|
405
|
+
run_privileged zypper --non-interactive install python3 python3-pip curl ca-certificates
|
|
406
|
+
elif command -v pacman >/dev/null 2>&1; then
|
|
407
|
+
run_privileged pacman -Sy --noconfirm python python-pip curl ca-certificates
|
|
408
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
409
|
+
run_privileged apk add --no-cache python3 py3-pip curl ca-certificates
|
|
410
|
+
elif command -v xbps-install >/dev/null 2>&1; then
|
|
411
|
+
run_privileged xbps-install -Sy python3 python3-pip curl ca-certificates
|
|
412
|
+
elif [ "$(uname -s 2>/dev/null)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
|
|
413
|
+
brew install python3
|
|
414
|
+
else
|
|
415
|
+
return 1
|
|
416
|
+
fi
|
|
53
417
|
return 0
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# curl is the script's transport, not a Python dependency: the bootstrap download,
|
|
422
|
+
# node registration and the standalone health check all go through it. A machine can
|
|
423
|
+
# have a perfectly good Python 3 and no curl at all (a minimal WSL image, a slim
|
|
424
|
+
# container, a phone where only python was installed), and install_python_packages()
|
|
425
|
+
# never runs in that case - it is called only when Python itself is missing. So the
|
|
426
|
+
# tool is asked for separately, BEFORE the step that needs it.
|
|
427
|
+
install_helper_packages() {
|
|
428
|
+
(
|
|
429
|
+
set +e
|
|
430
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
431
|
+
pkg install -y curl ca-certificates
|
|
432
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
433
|
+
run_privileged apt-get update -qq
|
|
434
|
+
run_privileged apt-get install -y -qq curl ca-certificates
|
|
435
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
436
|
+
run_privileged dnf install -y -q curl ca-certificates
|
|
437
|
+
elif command -v yum >/dev/null 2>&1; then
|
|
438
|
+
run_privileged yum install -y -q curl ca-certificates
|
|
439
|
+
elif command -v zypper >/dev/null 2>&1; then
|
|
440
|
+
run_privileged zypper --non-interactive install curl ca-certificates
|
|
441
|
+
elif command -v pacman >/dev/null 2>&1; then
|
|
442
|
+
run_privileged pacman -Sy --noconfirm curl ca-certificates
|
|
443
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
444
|
+
run_privileged apk add --no-cache curl ca-certificates
|
|
445
|
+
elif command -v xbps-install >/dev/null 2>&1; then
|
|
446
|
+
run_privileged xbps-install -Sy curl ca-certificates
|
|
447
|
+
elif [ "$(uname -s 2>/dev/null)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
|
|
448
|
+
brew install curl
|
|
449
|
+
else
|
|
450
|
+
return 1
|
|
451
|
+
fi
|
|
452
|
+
return 0
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# True when curl is usable. Installs it if it is not, and fails closed when it cannot
|
|
457
|
+
# be obtained: without it the node can never be registered, and the alternative - the
|
|
458
|
+
# raw "curl: command not found" at the registration line - names neither the cause nor
|
|
459
|
+
# the fix. Called before the bootstrap download, before registration and before the
|
|
460
|
+
# standalone health check.
|
|
461
|
+
ensure_curl() {
|
|
462
|
+
command -v curl >/dev/null 2>&1 && return 0
|
|
463
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
464
|
+
echo -e "\033[1;33m[!] curl не найден. Устанавливаю пакеты...\033[0m"
|
|
465
|
+
else
|
|
466
|
+
echo -e "\033[1;33m[!] curl not found. Installing it...\033[0m"
|
|
467
|
+
fi
|
|
468
|
+
install_helper_packages || true
|
|
469
|
+
command -v curl >/dev/null 2>&1 && return 0
|
|
470
|
+
|
|
471
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
472
|
+
echo -e "\033[1;31m[!] Без curl установка невозможна.\033[0m" >&2
|
|
473
|
+
echo -e " Через него скачивается код узла, регистрируется узел и проверяется локальный порт." >&2
|
|
474
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
475
|
+
echo -e " Установите вручную и повторите: ${CYAN}pkg install curl${RESET}" >&2
|
|
476
|
+
else
|
|
477
|
+
echo -e " Установите вручную и повторите: ${CYAN}sudo apt install curl${RESET} # или ваш менеджер пакетов" >&2
|
|
478
|
+
fi
|
|
479
|
+
echo -e " Остановка: без curl ничего не создано." >&2
|
|
480
|
+
else
|
|
481
|
+
echo -e "\033[1;31m[!] curl is required and could not be installed.\033[0m" >&2
|
|
482
|
+
echo -e " It downloads the node code, registers the node and checks the local port." >&2
|
|
483
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
484
|
+
echo -e " Install it and re-run: ${CYAN}pkg install curl${RESET}" >&2
|
|
485
|
+
else
|
|
486
|
+
echo -e " Install it and re-run: ${CYAN}sudo apt install curl${RESET} # or your package manager" >&2
|
|
487
|
+
fi
|
|
488
|
+
echo -e " Stopping: nothing was created without it." >&2
|
|
489
|
+
fi
|
|
490
|
+
return 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# uv is the last resort: one static binary that fetches its own CPython and installs
|
|
494
|
+
# packages without pip. The release archive is downloaded and unpacked here rather
|
|
495
|
+
# than piping astral.sh's install script into sh - a script that arrives over the
|
|
496
|
+
# network and is executed unread is a worse default than an archive this unpacks.
|
|
497
|
+
ensure_uv() {
|
|
498
|
+
if command -v uv >/dev/null 2>&1; then
|
|
499
|
+
UV_BIN="$(command -v uv)"
|
|
500
|
+
return 0
|
|
501
|
+
fi
|
|
502
|
+
if [ -x "$CONFIG_DIR/bin/uv" ]; then
|
|
503
|
+
UV_BIN="$CONFIG_DIR/bin/uv"
|
|
504
|
+
return 0
|
|
505
|
+
fi
|
|
506
|
+
# UV_TARGET is empty on an architecture uv publishes no build for, and asking
|
|
507
|
+
# for the wrong one produces an "Exec format error" that names no cause.
|
|
508
|
+
[ -n "$UV_TARGET" ] || return 1
|
|
509
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
510
|
+
local tmp
|
|
511
|
+
tmp="$(mktemp -d 2>/dev/null)" || return 1
|
|
512
|
+
if curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/uv-${UV_TARGET}.tar.gz" \
|
|
513
|
+
-o "$tmp/uv.tar.gz" 2>/dev/null; then
|
|
514
|
+
if tar -xzf "$tmp/uv.tar.gz" -C "$tmp" 2>/dev/null && [ -f "$tmp/uv-${UV_TARGET}/uv" ]; then
|
|
515
|
+
mkdir -p "$CONFIG_DIR/bin"
|
|
516
|
+
cp "$tmp/uv-${UV_TARGET}/uv" "$CONFIG_DIR/bin/uv" 2>/dev/null && chmod +x "$CONFIG_DIR/bin/uv"
|
|
517
|
+
fi
|
|
518
|
+
fi
|
|
519
|
+
rm -rf "$tmp"
|
|
520
|
+
if [ -x "$CONFIG_DIR/bin/uv" ]; then
|
|
521
|
+
UV_BIN="$CONFIG_DIR/bin/uv"
|
|
522
|
+
return 0
|
|
523
|
+
fi
|
|
524
|
+
return 1
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
uv_pip_install() {
|
|
528
|
+
[ -n "$UV_BIN" ] || return 1
|
|
529
|
+
"$UV_BIN" pip install --python "$1" websockets >/dev/null 2>&1
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# The two packages that make a Termux node behave like a supervised Linux node
|
|
533
|
+
# rather than a bare process: termux-services (runit: Restart=always) and
|
|
534
|
+
# termux-api (every termux-* command the device branch uses; the Termux:API app
|
|
535
|
+
# must be installed as well for these to answer). Both are optional - the install
|
|
536
|
+
# degrades instead of failing - so this never reports an error.
|
|
537
|
+
ensure_termux_extras() {
|
|
538
|
+
[ "$IS_TERMUX" = true ] || return 0
|
|
539
|
+
command -v pkg >/dev/null 2>&1 || return 0
|
|
540
|
+
local missing=""
|
|
541
|
+
command -v sv >/dev/null 2>&1 || missing="termux-services"
|
|
542
|
+
# termux-wake-lock is NOT proof that termux-api is installed: on a real phone
|
|
543
|
+
# (Android 16, OPPO PHY110) that one script existed while termux-battery-status,
|
|
544
|
+
# termux-wifi-connectioninfo, termux-sensor and every other command were
|
|
545
|
+
# missing. Checking the wake lock therefore declared the package present and
|
|
546
|
+
# left the whole device branch dead with nothing to explain it. A command that
|
|
547
|
+
# only termux-api ships is the honest probe.
|
|
548
|
+
command -v termux-battery-status >/dev/null 2>&1 || missing="$missing termux-api"
|
|
549
|
+
# Termux ships pip separately from python, and its interpreter is built
|
|
550
|
+
# --without-ensurepip: without these two packages neither `pip install` nor the
|
|
551
|
+
# venv fallback below can work, and the failure would look like a broken node
|
|
552
|
+
# rather than a missing package.
|
|
553
|
+
if ! python_has_pip "$PYTHON_BIN"; then
|
|
554
|
+
missing="$missing python-pip python-ensurepip-wheels"
|
|
555
|
+
fi
|
|
556
|
+
[ -n "$missing" ] || return 0
|
|
557
|
+
# Unquoted on purpose: this is a package list, and pkg install takes them all.
|
|
558
|
+
# shellcheck disable=SC2086
|
|
559
|
+
pkg install -y $missing >/dev/null 2>&1 || true
|
|
560
|
+
return 0
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# True when a usable Python 3 is on the machine. Installs one if it is not.
|
|
564
|
+
ensure_python() {
|
|
565
|
+
resolve_python && return 0
|
|
566
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
567
|
+
echo -e "\033[1;33m[!] Python 3 не найден. Автоматическая установка системных пакетов...\033[0m"
|
|
568
|
+
else
|
|
569
|
+
echo -e "\033[1;33m[!] Python 3 not found. Installing system packages...\033[0m"
|
|
54
570
|
fi
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
elif [ "$(uname -s)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then
|
|
67
|
-
brew install python3
|
|
571
|
+
install_python_packages || true
|
|
572
|
+
resolve_python && return 0
|
|
573
|
+
|
|
574
|
+
# No usable package manager: uv can still bring an interpreter of its own.
|
|
575
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
576
|
+
echo -e "\033[1;33m[!] Пакетный менеджер не дал Python. Пробую uv...\033[0m"
|
|
577
|
+
else
|
|
578
|
+
echo -e "\033[1;33m[!] The package manager did not provide Python. Trying uv...\033[0m"
|
|
579
|
+
fi
|
|
580
|
+
if ensure_uv && "$UV_BIN" python install 3.12 >/dev/null 2>&1; then
|
|
581
|
+
resolve_python && return 0
|
|
68
582
|
fi
|
|
583
|
+
return 1
|
|
69
584
|
}
|
|
70
585
|
|
|
71
586
|
ensure_dependencies() {
|
|
72
|
-
ensure_python
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
[ "$LANG_CHOICE" = "ru" ]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
587
|
+
if ! ensure_python; then
|
|
588
|
+
# Fail closed. An autostart unit naming an interpreter that does not exist
|
|
589
|
+
# is a node that never connects, and nothing in the output would have said so.
|
|
590
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
591
|
+
echo -e "\033[1;31m[!] Рабочий Python 3 получить не удалось.\033[0m" >&2
|
|
592
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
593
|
+
echo -e " Проверено: ${CYAN}pkg${RESET} (пакетный менеджер Termux; uv не используется," >&2
|
|
594
|
+
echo -e " его сборки линкуются с glibc и на Android/bionic не запускаются)." >&2
|
|
595
|
+
echo -e " Установите Python 3 вручную и повторите:" >&2
|
|
596
|
+
echo -e " ${CYAN}pkg install python python-pip${RESET}" >&2
|
|
597
|
+
else
|
|
598
|
+
echo -e " Проверены: пакетный менеджер (apt/dnf/yum/zypper/pacman/apk/xbps/brew) и uv." >&2
|
|
599
|
+
echo -e " Установите Python 3 вручную и повторите:" >&2
|
|
600
|
+
echo -e " ${CYAN}sudo apt install python3 python3-pip python3-venv${RESET} # или ваш менеджер" >&2
|
|
85
601
|
fi
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
602
|
+
echo -e " Остановка: без интерпретатора автозапуск узла работать не будет, ничего не создано." >&2
|
|
603
|
+
else
|
|
604
|
+
echo -e "\033[1;31m[!] Could not obtain a working Python 3.\033[0m" >&2
|
|
605
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
606
|
+
echo -e " Tried: ${CYAN}pkg${RESET}, the Termux package manager (uv is skipped there: its" >&2
|
|
607
|
+
echo -e " builds link against glibc and cannot run on Android's bionic libc)." >&2
|
|
608
|
+
echo -e " Install Python 3 by hand and re-run:" >&2
|
|
609
|
+
echo -e " ${CYAN}pkg install python python-pip${RESET}" >&2
|
|
610
|
+
else
|
|
611
|
+
echo -e " Tried: the system package manager (apt/dnf/yum/zypper/pacman/apk/xbps/brew) and uv." >&2
|
|
612
|
+
echo -e " Install Python 3 by hand and re-run:" >&2
|
|
613
|
+
echo -e " ${CYAN}sudo apt install python3 python3-pip python3-venv${RESET} # or your package manager" >&2
|
|
89
614
|
fi
|
|
90
|
-
|
|
615
|
+
echo -e " Stopping: an autostart entry without an interpreter can never work, nothing was created." >&2
|
|
616
|
+
fi
|
|
617
|
+
exit 1
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
ensure_termux_extras
|
|
621
|
+
|
|
622
|
+
if python_has_websockets "$PYTHON_BIN"; then
|
|
623
|
+
return 0
|
|
624
|
+
fi
|
|
625
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
626
|
+
echo "[1/4] Автономная установка зависимостей (websockets)..."
|
|
627
|
+
else
|
|
628
|
+
echo "[1/4] Installing dependencies (websockets)..."
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
# 1. pip, in both spellings. The --break-system-packages retry is the escape
|
|
632
|
+
# hatch a PEP 668 distribution documents; without it a Debian 12 or Fedora 38
|
|
633
|
+
# refuses the install and the message never mentions the flag.
|
|
634
|
+
"$PYTHON_BIN" -m pip install --quiet websockets 2>/dev/null || true
|
|
635
|
+
python_has_websockets "$PYTHON_BIN" && return 0
|
|
636
|
+
"$PYTHON_BIN" -m pip install --quiet --break-system-packages websockets 2>/dev/null || true
|
|
637
|
+
python_has_websockets "$PYTHON_BIN" && return 0
|
|
638
|
+
|
|
639
|
+
# 2. --user: site-packages is read-only, the home directory is not.
|
|
640
|
+
"$PYTHON_BIN" -m pip install --quiet --user websockets 2>/dev/null || true
|
|
641
|
+
python_has_websockets "$PYTHON_BIN" && return 0
|
|
642
|
+
|
|
643
|
+
# 3. ensurepip: a distribution can ship python3 without a pip at all.
|
|
644
|
+
"$PYTHON_BIN" -m ensurepip --default-pip >/dev/null 2>&1 || true
|
|
645
|
+
"$PYTHON_BIN" -m pip install --quiet websockets 2>/dev/null || true
|
|
646
|
+
python_has_websockets "$PYTHON_BIN" && return 0
|
|
647
|
+
|
|
648
|
+
# 4. uv: installs into a managed environment without the interpreter's own pip.
|
|
649
|
+
if ensure_uv; then
|
|
650
|
+
uv_pip_install "$PYTHON_BIN" || true
|
|
651
|
+
python_has_websockets "$PYTHON_BIN" && return 0
|
|
652
|
+
fi
|
|
653
|
+
|
|
654
|
+
# 5. venv: needs no write access to the system Python, only the interpreter.
|
|
655
|
+
local venv_dir="$CONFIG_DIR/venv"
|
|
656
|
+
if [ ! -x "$venv_dir/bin/python3" ]; then
|
|
657
|
+
"$PYTHON_BIN" -m venv "$venv_dir" >/dev/null 2>&1 || true
|
|
658
|
+
fi
|
|
659
|
+
if [ -x "$venv_dir/bin/python3" ]; then
|
|
660
|
+
"$venv_dir/bin/python3" -m pip install --quiet --upgrade pip >/dev/null 2>&1 || true
|
|
661
|
+
"$venv_dir/bin/python3" -m pip install --quiet websockets >/dev/null 2>&1 || true
|
|
662
|
+
if python_has_websockets "$venv_dir/bin/python3"; then
|
|
663
|
+
PYTHON_BIN="$venv_dir/bin/python3"
|
|
664
|
+
return 0
|
|
665
|
+
fi
|
|
666
|
+
if uv_pip_install "$venv_dir/bin/python3" && python_has_websockets "$venv_dir/bin/python3"; then
|
|
667
|
+
PYTHON_BIN="$venv_dir/bin/python3"
|
|
668
|
+
return 0
|
|
669
|
+
fi
|
|
91
670
|
fi
|
|
92
|
-
|
|
671
|
+
|
|
672
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
673
|
+
echo -e "\033[1;31m[!] Не удалось установить websockets автоматически.\033[0m" >&2
|
|
674
|
+
echo -e " Проверены: pip, pip --break-system-packages, pip --user, ensurepip, uv и venv." >&2
|
|
675
|
+
echo -e " Выполните вручную: ${CYAN}$PYTHON_BIN -m pip install websockets${RESET}" >&2
|
|
676
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
677
|
+
echo -e " Termux: ${CYAN}pkg install python-pip python-ensurepip-wheels${RESET}, затем повторите." >&2
|
|
678
|
+
fi
|
|
679
|
+
echo -e " Остановка: узел без websockets не сможет подключиться к шлюзу." >&2
|
|
680
|
+
else
|
|
681
|
+
echo -e "\033[1;31m[!] Could not install websockets automatically.\033[0m" >&2
|
|
682
|
+
echo -e " Tried: pip, pip --break-system-packages, pip --user, ensurepip, uv and a venv." >&2
|
|
683
|
+
echo -e " Run manually: ${CYAN}$PYTHON_BIN -m pip install websockets${RESET}" >&2
|
|
684
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
685
|
+
echo -e " Termux: ${CYAN}pkg install python-pip python-ensurepip-wheels${RESET}, then re-run." >&2
|
|
686
|
+
fi
|
|
687
|
+
echo -e " Stopping: a node without websockets can never reach the gateway." >&2
|
|
688
|
+
fi
|
|
689
|
+
exit 1
|
|
93
690
|
}
|
|
94
691
|
|
|
95
692
|
|
|
@@ -118,7 +715,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
118
715
|
shift
|
|
119
716
|
;;
|
|
120
717
|
--gateway=*)
|
|
121
|
-
|
|
718
|
+
# Historical alias of --domain=: both are the explicit override.
|
|
719
|
+
DOMAIN="${1#*=}"
|
|
122
720
|
shift
|
|
123
721
|
;;
|
|
124
722
|
--port=*)
|
|
@@ -155,6 +753,15 @@ while [[ $# -gt 0 ]]; do
|
|
|
155
753
|
echo ""
|
|
156
754
|
echo "All nodes use ONE shared domain; the node is selected by ?user=<node-name>:"
|
|
157
755
|
echo " MCP URL: https://<shared-domain>/sse?user=<node-name>&token=<token>"
|
|
756
|
+
echo ""
|
|
757
|
+
echo "The shared domain is resolved from the one place that declares it, in this order:"
|
|
758
|
+
echo " --domain= / --gateway=, then MESH_PUBLIC_URL, then AGY_PUBLIC_BASE_URL,"
|
|
759
|
+
echo " then ${DEFAULT_DOMAIN_FILE}, then the value this copy was published with,"
|
|
760
|
+
echo " then core/domain.py's default when the repository is present."
|
|
761
|
+
echo ""
|
|
762
|
+
echo "Android/Termux is detected automatically: packages come from pkg, the node is"
|
|
763
|
+
echo "named after the device model, and autostart is a runit service plus the"
|
|
764
|
+
echo "Termux:Boot app instead of systemd (see docs/TERMUX.md)."
|
|
158
765
|
exit 0
|
|
159
766
|
;;
|
|
160
767
|
*)
|
|
@@ -164,6 +771,145 @@ while [[ $# -gt 0 ]]; do
|
|
|
164
771
|
esac
|
|
165
772
|
done
|
|
166
773
|
|
|
774
|
+
# ------------------------------------------------------------------------------
|
|
775
|
+
# Resolve the shared domain, then fetch the node code from it.
|
|
776
|
+
#
|
|
777
|
+
# Order matters: the CLI arguments are parsed first (so --domain= counts), the
|
|
778
|
+
# domain is resolved second, and only then is the bootstrap downloaded - from
|
|
779
|
+
# the resolved domain, never from a name baked into this file.
|
|
780
|
+
# ------------------------------------------------------------------------------
|
|
781
|
+
GATEWAY="$(resolve_domain "$DOMAIN")"
|
|
782
|
+
|
|
783
|
+
if [ "$GATEWAY" = "$DOMAIN_PLACEHOLDER" ]; then
|
|
784
|
+
if domain_required; then
|
|
785
|
+
echo ""
|
|
786
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
787
|
+
echo -e "${RED}${BOLD}[ОШИБКА] Домен шлюза не задан.${RESET}"
|
|
788
|
+
echo -e " В этой копии установщика остался плейсхолдер ${BOLD}${DOMAIN_PLACEHOLDER}${RESET}: она не была опубликована"
|
|
789
|
+
echo -e " скриптом deploy_gateway.sh, а домена нет ни в окружении, ни в файле ${BOLD}${DEFAULT_DOMAIN_FILE}${RESET}."
|
|
790
|
+
echo -e " Укажите домен одним из способов:"
|
|
791
|
+
echo -e " ${CYAN}./install.sh --domain=<общий-домен>${RESET}"
|
|
792
|
+
echo -e " ${CYAN}MESH_PUBLIC_URL=https://<общий-домен> ./install.sh${RESET}"
|
|
793
|
+
echo -e " ${CYAN}$(domain_file_write_cmd 'https://<общий-домен>')${RESET}"
|
|
794
|
+
echo -e " ${YELLOW}Установка остановлена: регистрация на несуществующем домене не выполняется, ничего не создано.${RESET}"
|
|
795
|
+
else
|
|
796
|
+
echo -e "${RED}${BOLD}[ERROR] No gateway domain configured.${RESET}"
|
|
797
|
+
echo -e " This copy of the installer still carries the ${BOLD}${DOMAIN_PLACEHOLDER}${RESET} placeholder: it was not"
|
|
798
|
+
echo -e " published by deploy_gateway.sh, and no domain was found in the environment or in ${BOLD}${DEFAULT_DOMAIN_FILE}${RESET}."
|
|
799
|
+
echo -e " Pass the domain in one of these ways:"
|
|
800
|
+
echo -e " ${CYAN}./install.sh --domain=<shared-domain>${RESET}"
|
|
801
|
+
echo -e " ${CYAN}MESH_PUBLIC_URL=https://<shared-domain> ./install.sh${RESET}"
|
|
802
|
+
echo -e " ${CYAN}$(domain_file_write_cmd 'https://<shared-domain>')${RESET}"
|
|
803
|
+
echo -e " ${YELLOW}Stopping: a node is never registered against a domain that does not exist, nothing was created.${RESET}"
|
|
804
|
+
fi
|
|
805
|
+
echo ""
|
|
806
|
+
exit 1
|
|
807
|
+
fi
|
|
808
|
+
# Standalone install with the node code already next to this script: no domain
|
|
809
|
+
# is needed, so this is a warning rather than a stop.
|
|
810
|
+
GATEWAY=""
|
|
811
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
812
|
+
echo -e "${YELLOW}[!] Домен не задан - автономному режиму он не нужен, продолжаю.${RESET}"
|
|
813
|
+
else
|
|
814
|
+
echo -e "${YELLOW}[!] No domain configured - not needed for standalone mode, continuing.${RESET}"
|
|
815
|
+
fi
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
# ------------------------------------------------------------------------------
|
|
819
|
+
# GATEWAY REACHABILITY (the one preflight that cannot be deferred)
|
|
820
|
+
#
|
|
821
|
+
# A node downloads its code from the gateway, registers there, and then dials the
|
|
822
|
+
# same name for its tunnel. A name this device cannot resolve therefore dooms
|
|
823
|
+
# every later step - and the failure used to surface much later, as "Failed to
|
|
824
|
+
# obtain authentication token", which blames the gateway's answer instead of the
|
|
825
|
+
# missing DNS record. Phones are where this bites: a gateway on a private network
|
|
826
|
+
# (Tailscale, a VPN, a DNS override on the operator's laptop) resolves on that
|
|
827
|
+
# laptop and nowhere else, and mobile data resolves nothing private.
|
|
828
|
+
#
|
|
829
|
+
# Only curl's exit code 6 ("could not resolve host") stops the install: it is the
|
|
830
|
+
# one answer that cannot become true later in this run. A timeout, a 404 or a 5xx
|
|
831
|
+
# is not fatal - a gateway without /health still serves the tunnel, and refusing
|
|
832
|
+
# to install for that would be wrong.
|
|
833
|
+
# ------------------------------------------------------------------------------
|
|
834
|
+
require_gateway_dns() {
|
|
835
|
+
[ -n "$GATEWAY" ] || return 0
|
|
836
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
837
|
+
local rc=0
|
|
838
|
+
curl -fsS -m 8 -o /dev/null "https://${GATEWAY}/health" >/dev/null 2>&1 || rc=$?
|
|
839
|
+
[ "$rc" -eq 6 ] || return 0
|
|
840
|
+
|
|
841
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
842
|
+
echo ""
|
|
843
|
+
echo -e "${RED}${BOLD}[ОШИБКА] Имя шлюза не разрешается на этом устройстве: ${GATEWAY}${RESET}"
|
|
844
|
+
echo -e " DNS не знает такого имени, поэтому ни скачать код узла, ни зарегистрировать его не выйдет."
|
|
845
|
+
echo -e " Обычно так бывает, когда шлюз живёт в приватной сети (Tailscale, VPN, локальный DNS):"
|
|
846
|
+
echo -e " на рабочем ноутбуке имя резолвится, а на телефоне в мобильной сети - нет."
|
|
847
|
+
echo -e " Что делать:"
|
|
848
|
+
echo -e " • подключить это устройство к той же сети (на Android - приложение Tailscale);"
|
|
849
|
+
echo -e " • или указать домен, который резолвится публично: ${CYAN}--domain=<домен>${RESET};"
|
|
850
|
+
echo -e " • проверить вручную: ${CYAN}curl -v https://${GATEWAY}/health${RESET}"
|
|
851
|
+
echo -e " ${YELLOW}Остановка: без DNS узел не подключится к шлюзу, ничего не создано.${RESET}"
|
|
852
|
+
echo ""
|
|
853
|
+
else
|
|
854
|
+
echo ""
|
|
855
|
+
echo -e "${RED}${BOLD}[ERROR] This device cannot resolve the gateway name: ${GATEWAY}${RESET}"
|
|
856
|
+
echo -e " DNS does not know the name, so neither the node code nor a registration can arrive."
|
|
857
|
+
echo -e " This is the usual state of a gateway on a private network (Tailscale, a VPN, a local"
|
|
858
|
+
echo -e " DNS override): it resolves on the operator's laptop and not on a phone on mobile data."
|
|
859
|
+
echo -e " What to do:"
|
|
860
|
+
echo -e " • put this device on the same network (on Android: the Tailscale app);"
|
|
861
|
+
echo -e " • or name a domain that resolves publicly: ${CYAN}--domain=<domain>${RESET};"
|
|
862
|
+
echo -e " • check by hand: ${CYAN}curl -v https://${GATEWAY}/health${RESET}"
|
|
863
|
+
echo -e " ${YELLOW}Stopping: without DNS the node can never reach the gateway, nothing was created.${RESET}"
|
|
864
|
+
echo ""
|
|
865
|
+
fi
|
|
866
|
+
return 1
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
# Bootstrap when piped from curl or run outside the project: the node code is
|
|
870
|
+
# fetched from the SAME domain that was just resolved. core/domain.py travels with
|
|
871
|
+
# it, because the node asks that module for the domain as well.
|
|
872
|
+
if [ ! -f "$SCRIPT_DIR/core/agent.py" ]; then
|
|
873
|
+
# The node code is fetched with curl. Piping this script through curl means curl
|
|
874
|
+
# exists; arriving through wget, a copy-paste or an embedded runner does not.
|
|
875
|
+
ensure_curl || exit 1
|
|
876
|
+
require_gateway_dns || exit 1
|
|
877
|
+
BOOTSTRAP_DIR="$HOME/.gemini-computer-use"
|
|
878
|
+
mkdir -p "$BOOTSTRAP_DIR/core" "$BOOTSTRAP_DIR/skills"
|
|
879
|
+
for f in core/agent.py core/server.py core/mcp_tools.py core/web_share.py core/domain.py core/vitals.py core/updater.py core/version.py core/__init__.py skills/orchestrator.md; do
|
|
880
|
+
bootstrap_tmp="$BOOTSTRAP_DIR/${f}.part"
|
|
881
|
+
if curl -fsSL "https://${GATEWAY}/${f}" -o "$bootstrap_tmp" 2>/dev/null \
|
|
882
|
+
|| curl -fsSL "https://raw.githubusercontent.com/LevRa7/Computer-use-for-Gemini-App-Web/main/${f}" -o "$bootstrap_tmp" 2>/dev/null; then
|
|
883
|
+
mv "$bootstrap_tmp" "$BOOTSTRAP_DIR/${f}"
|
|
884
|
+
else
|
|
885
|
+
rm -f "$bootstrap_tmp"
|
|
886
|
+
fi
|
|
887
|
+
done
|
|
888
|
+
unset bootstrap_tmp
|
|
889
|
+
# A bootstrap that downloaded nothing (an unreachable gateway and no GitHub)
|
|
890
|
+
# leaves a directory that names modules which do not exist: every step after it
|
|
891
|
+
# would write a launcher for code that is not there, and the node would fail
|
|
892
|
+
# with a Python import error at boot.
|
|
893
|
+
if [ ! -f "$BOOTSTRAP_DIR/core/agent.py" ]; then
|
|
894
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
895
|
+
echo -e "${RED}${BOLD}[ОШИБКА] Не удалось скачать код узла.${RESET}" >&2
|
|
896
|
+
echo -e " Пробовали: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} и ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
|
|
897
|
+
echo -e " Проверьте сеть и DNS либо скопируйте репозиторий на устройство и запустите из него:" >&2
|
|
898
|
+
echo -e " ${CYAN}git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git && cd Computer-use-for-Gemini-App-Web && ./install.sh --quick${RESET}" >&2
|
|
899
|
+
echo -e " ${YELLOW}Остановка: без кода узла запускать нечего, ничего не создано.${RESET}" >&2
|
|
900
|
+
else
|
|
901
|
+
echo -e "${RED}${BOLD}[ERROR] Could not download the node code.${RESET}" >&2
|
|
902
|
+
echo -e " Tried: ${CYAN}https://${GATEWAY}/core/agent.py${RESET} and ${CYAN}github.com/LevRa7/Computer-use-for-Gemini-App-Web${RESET}." >&2
|
|
903
|
+
echo -e " Check the network and DNS, or put the repository on this device and install from it:" >&2
|
|
904
|
+
echo -e " ${CYAN}git clone https://github.com/LevRa7/Computer-use-for-Gemini-App-Web.git && cd Computer-use-for-Gemini-App-Web && ./install.sh --quick${RESET}" >&2
|
|
905
|
+
echo -e " ${YELLOW}Stopping: there is no node code to run, nothing was created.${RESET}" >&2
|
|
906
|
+
fi
|
|
907
|
+
echo ""
|
|
908
|
+
exit 1
|
|
909
|
+
fi
|
|
910
|
+
SCRIPT_DIR="$BOOTSTRAP_DIR"
|
|
911
|
+
fi
|
|
912
|
+
|
|
167
913
|
# ------------------------------------------------------------------------------
|
|
168
914
|
# SHARED-DOMAIN CONTRACT (canonical step)
|
|
169
915
|
# Every node is reached through ONE public domain ($GATEWAY); the node is
|
|
@@ -198,11 +944,71 @@ if [ -z "$LANG_CHOICE" ]; then
|
|
|
198
944
|
fi
|
|
199
945
|
|
|
200
946
|
# Device detection
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
947
|
+
# ------------------------------------------------------------------------------
|
|
948
|
+
# ARCHITECTURE
|
|
949
|
+
#
|
|
950
|
+
# The system package managers resolve their own architecture, so on Linux this
|
|
951
|
+
# matters for exactly two things: the uv archive, which is published per CPU and
|
|
952
|
+
# answers the wrong one with an unhelpful "Exec format error", and the
|
|
953
|
+
# registration payload, so a gateway operator can see which build a node runs.
|
|
954
|
+
# macOS reports the same machine names as Linux but needs the -apple-darwin assets.
|
|
955
|
+
# ------------------------------------------------------------------------------
|
|
956
|
+
detect_arch() {
|
|
957
|
+
DETECTED_ARCH="$(uname -m 2>/dev/null || echo unknown)"
|
|
958
|
+
case "$DETECTED_ARCH" in
|
|
959
|
+
x86_64|amd64) ARCH_LABEL="x86_64 (64-bit)"; UV_TARGET="x86_64-unknown-linux-gnu" ;;
|
|
960
|
+
aarch64|arm64) ARCH_LABEL="aarch64 (64-bit ARM)"; UV_TARGET="aarch64-unknown-linux-gnu" ;;
|
|
961
|
+
armv7l|armv7|armv6l) ARCH_LABEL="armv7 (32-bit ARM)"; UV_TARGET="armv7-unknown-linux-gnueabihf" ;;
|
|
962
|
+
i386|i486|i586|i686) ARCH_LABEL="x86 (32-bit)"; UV_TARGET="i686-unknown-linux-gnu" ;;
|
|
963
|
+
*) ARCH_LABEL="$DETECTED_ARCH"; UV_TARGET="" ;;
|
|
964
|
+
esac
|
|
965
|
+
if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then
|
|
966
|
+
case "$DETECTED_ARCH" in
|
|
967
|
+
x86_64) UV_TARGET="x86_64-apple-darwin" ;;
|
|
968
|
+
arm64) UV_TARGET="aarch64-apple-darwin" ;;
|
|
969
|
+
esac
|
|
970
|
+
fi
|
|
971
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
972
|
+
# uv's archives are glibc/musl builds; neither runs against Android's
|
|
973
|
+
# bionic libc, and asking for one yields an "Exec format error" that names
|
|
974
|
+
# no cause. On a phone the package manager is the only source of Python.
|
|
975
|
+
UV_TARGET=""
|
|
976
|
+
fi
|
|
977
|
+
}
|
|
204
978
|
|
|
205
|
-
|
|
979
|
+
# Android system properties. getprop lives in /system/bin on every Android build
|
|
980
|
+
# and is readable without root; a missing getprop (or an empty property) simply
|
|
981
|
+
# yields an empty value, never a failed install.
|
|
982
|
+
android_prop() {
|
|
983
|
+
command -v getprop >/dev/null 2>&1 || return 0
|
|
984
|
+
getprop "$1" 2>/dev/null | tr -d '\r' | head -n 1
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
detect_device() {
|
|
988
|
+
# `|| true` on purpose: a minimal container (and Termux, which has no
|
|
989
|
+
# hostname(1) of its own) answers neither spelling, and an assignment whose
|
|
990
|
+
# substitution fails would abort the whole installer under `set -e`.
|
|
991
|
+
DETECTED_HOSTNAME=$(hostname -s 2>/dev/null || hostname 2>/dev/null || true)
|
|
992
|
+
detect_arch
|
|
993
|
+
|
|
994
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
995
|
+
ANDROID_MODEL="$(android_prop ro.product.model)"
|
|
996
|
+
[ -n "$ANDROID_MODEL" ] || ANDROID_MODEL="$(android_prop ro.product.device)"
|
|
997
|
+
ANDROID_RELEASE="$(android_prop ro.build.version.release)"
|
|
998
|
+
[ -n "$ANDROID_RELEASE" ] || ANDROID_RELEASE="$(android_prop ro.build.version.sdk)"
|
|
999
|
+
DETECTED_OS="Android ${ANDROID_RELEASE:-unknown} (Termux)"
|
|
1000
|
+
# The phone itself names the node. Termux has no hostname binary, and
|
|
1001
|
+
# Android's gethostname(2) answers "localhost" - a name every phone would
|
|
1002
|
+
# claim, and the gateway keeps exactly one tunnel per name, so they would
|
|
1003
|
+
# knock each other offline in a loop. The device model is stable, and it is
|
|
1004
|
+
# sanitised below exactly like a hostname.
|
|
1005
|
+
DETECTED_HOSTNAME="${ANDROID_MODEL:-android-node}"
|
|
1006
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1007
|
+
DETECTED_TYPE="Смартфон/планшет Android (Termux)"
|
|
1008
|
+
else
|
|
1009
|
+
DETECTED_TYPE="Android phone/tablet (Termux)"
|
|
1010
|
+
fi
|
|
1011
|
+
elif [ "$(uname -s)" = "Darwin" ]; then
|
|
206
1012
|
DETECTED_OS="macOS $(sw_vers -productVersion 2>/dev/null || '')"
|
|
207
1013
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
208
1014
|
DETECTED_TYPE="Apple Mac"
|
|
@@ -219,26 +1025,31 @@ detect_device() {
|
|
|
219
1025
|
DETECTED_OS=$(uname -s)
|
|
220
1026
|
fi
|
|
221
1027
|
|
|
222
|
-
if [ "$(uname -s)" != "Darwin" ]; then
|
|
1028
|
+
if [ "$(uname -s)" != "Darwin" ] && [ "$IS_TERMUX" != true ]; then
|
|
1029
|
+
# The virtualised environments are asked FIRST, before the battery. WSL2
|
|
1030
|
+
# forwards the host's battery into /sys/class/power_supply, so the battery
|
|
1031
|
+
# test matched inside WSL and reported the user's laptop instead of the
|
|
1032
|
+
# subsystem they were actually installing into - the node registered as
|
|
1033
|
+
# "Laptop" while running under Windows.
|
|
223
1034
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
224
1035
|
DETECTED_TYPE="Десктоп / Сервер"
|
|
225
|
-
if
|
|
226
|
-
DETECTED_TYPE="Ноутбук (Laptop)"
|
|
227
|
-
elif grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
1036
|
+
if grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
228
1037
|
DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
|
|
229
1038
|
elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
|
|
230
1039
|
DETECTED_TYPE="Контейнер (Docker/LXC)"
|
|
1040
|
+
elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
|
|
1041
|
+
DETECTED_TYPE="Ноутбук (Laptop)"
|
|
231
1042
|
elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
|
|
232
1043
|
DETECTED_TYPE="Облачный сервер / VPS ($(systemd-detect-virt))"
|
|
233
1044
|
fi
|
|
234
1045
|
else
|
|
235
1046
|
DETECTED_TYPE="Desktop / Server"
|
|
236
|
-
if
|
|
237
|
-
DETECTED_TYPE="Laptop"
|
|
238
|
-
elif grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
1047
|
+
if grep -q -i "microsoft" /proc/version 2>/dev/null; then
|
|
239
1048
|
DETECTED_TYPE="WSL (Windows Subsystem for Linux)"
|
|
240
1049
|
elif [ -f /.dockerenv ] || grep -q "docker\|containerd" /proc/1/cgroup 2>/dev/null; then
|
|
241
1050
|
DETECTED_TYPE="Container (Docker/LXC)"
|
|
1051
|
+
elif [ -d /sys/class/power_supply ] && ls /sys/class/power_supply/BAT* 1>/dev/null 2>&1; then
|
|
1052
|
+
DETECTED_TYPE="Laptop"
|
|
242
1053
|
elif command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt -q; then
|
|
243
1054
|
DETECTED_TYPE="Cloud VPS ($(systemd-detect-virt))"
|
|
244
1055
|
fi
|
|
@@ -255,7 +1066,7 @@ print_device_info() {
|
|
|
255
1066
|
echo -e "${CYAN}║${RESET} • Имя хоста : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
|
|
256
1067
|
echo -e "${CYAN}║${RESET} • Тип : ${YELLOW}${DETECTED_TYPE}${RESET}"
|
|
257
1068
|
echo -e "${CYAN}║${RESET} • ОС : ${DETECTED_OS}"
|
|
258
|
-
echo -e "${CYAN}║${RESET} • Архитектура : ${
|
|
1069
|
+
echo -e "${CYAN}║${RESET} • Архитектура : ${ARCH_LABEL}"
|
|
259
1070
|
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
|
|
260
1071
|
else
|
|
261
1072
|
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${RESET}"
|
|
@@ -263,7 +1074,7 @@ print_device_info() {
|
|
|
263
1074
|
echo -e "${CYAN}║${RESET} • Hostname : ${GREEN}${DETECTED_HOSTNAME}${RESET}"
|
|
264
1075
|
echo -e "${CYAN}║${RESET} • Type : ${YELLOW}${DETECTED_TYPE}${RESET}"
|
|
265
1076
|
echo -e "${CYAN}║${RESET} • OS : ${DETECTED_OS}"
|
|
266
|
-
echo -e "${CYAN}║${RESET} • Architecture: ${
|
|
1077
|
+
echo -e "${CYAN}║${RESET} • Architecture: ${ARCH_LABEL}"
|
|
267
1078
|
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${RESET}"
|
|
268
1079
|
fi
|
|
269
1080
|
}
|
|
@@ -307,21 +1118,253 @@ print_mcp_box() {
|
|
|
307
1118
|
echo -e "${GREEN}${border}${RESET}"
|
|
308
1119
|
}
|
|
309
1120
|
|
|
1121
|
+
# ------------------------------------------------------------------------------
|
|
1122
|
+
# TERMUX (ANDROID) AUTOSTART
|
|
1123
|
+
#
|
|
1124
|
+
# An unprivileged Android app gets no service manager, so there is no systemd
|
|
1125
|
+
# unit, no launchd agent and no init script to install. Two Termux mechanisms
|
|
1126
|
+
# replace all three, and configure_termux_node sets up both:
|
|
1127
|
+
#
|
|
1128
|
+
# 1. runit, from the termux-services package: $PREFIX/var/service/<name>/run is
|
|
1129
|
+
# supervised inside the running Termux session, so a node that dies is
|
|
1130
|
+
# restarted - the systemd Restart=always equivalent;
|
|
1131
|
+
# 2. ~/.termux/boot/<name>.sh, executed by the Termux:Boot app (F-Droid) after
|
|
1132
|
+
# the device boots: it takes the wake lock and starts the supervisor.
|
|
1133
|
+
#
|
|
1134
|
+
# Without termux-services the node still runs and still autostarts at boot, but
|
|
1135
|
+
# nothing restarts it when Android kills the process. The installer says that
|
|
1136
|
+
# plainly instead of implying supervision that is not there.
|
|
1137
|
+
#
|
|
1138
|
+
# configure_termux_node <service name> <python module> <extra args> <env file>
|
|
1139
|
+
# ------------------------------------------------------------------------------
|
|
1140
|
+
configure_termux_node() {
|
|
1141
|
+
local name="$1" module="$2" extra="$3" env_file="$4"
|
|
1142
|
+
local run_script="$CONFIG_DIR/${name}.sh"
|
|
1143
|
+
local boot_script="$HOME/.termux/boot/${name}.sh"
|
|
1144
|
+
local svdir="$TERMUX_PREFIX/var/service"
|
|
1145
|
+
local svlogdir="$TERMUX_PREFIX/var/log"
|
|
1146
|
+
local log_file="$CONFIG_DIR/${name}.log"
|
|
1147
|
+
local env_block=""
|
|
1148
|
+
local module_args="$module"
|
|
1149
|
+
local alive=false
|
|
1150
|
+
local waited=0
|
|
1151
|
+
|
|
1152
|
+
[ -n "$extra" ] && module_args="$module $extra"
|
|
1153
|
+
|
|
1154
|
+
TERMUX_SUPERVISED=false
|
|
1155
|
+
mkdir -p "$CONFIG_DIR" "$HOME/.termux/boot"
|
|
1156
|
+
|
|
1157
|
+
if [ -n "$env_file" ]; then
|
|
1158
|
+
env_block="if [ -f \"$env_file\" ]; then
|
|
1159
|
+
set -a
|
|
1160
|
+
. \"$env_file\"
|
|
1161
|
+
set +a
|
|
1162
|
+
fi"
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
# The launcher is the one thing every mechanism below starts. Absolute paths
|
|
1166
|
+
# only: runit and the boot script run it with a minimal environment in which
|
|
1167
|
+
# $PATH may not even contain $PREFIX/bin.
|
|
1168
|
+
cat << RUNNER_EOF > "$run_script"
|
|
1169
|
+
#!$TERMUX_PREFIX/bin/sh
|
|
1170
|
+
# Antigravity Mesh - $name launcher (written by install.sh; do not edit).
|
|
1171
|
+
# Run this file to start the node by hand; docs/TERMUX.md documents the automatic
|
|
1172
|
+
# paths (runit service, Termux:Boot) and where the logs are.
|
|
1173
|
+
export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
|
|
1174
|
+
$env_block
|
|
1175
|
+
cd "$SCRIPT_DIR" || exit 1
|
|
1176
|
+
exec "$PYTHON_BIN" -m $module_args
|
|
1177
|
+
RUNNER_EOF
|
|
1178
|
+
chmod 700 "$run_script"
|
|
1179
|
+
|
|
1180
|
+
# 1. runit supervision, when termux-services is installed. install.sh asks the
|
|
1181
|
+
# package manager for it first; a phone that refused (no network, an old
|
|
1182
|
+
# mirror) simply keeps the unattended launcher below.
|
|
1183
|
+
if command -v sv >/dev/null 2>&1 && command -v runsvdir >/dev/null 2>&1; then
|
|
1184
|
+
mkdir -p "$svdir/$name/log"
|
|
1185
|
+
cat << SV_EOF > "$svdir/$name/run"
|
|
1186
|
+
#!$TERMUX_PREFIX/bin/sh
|
|
1187
|
+
exec "$run_script"
|
|
1188
|
+
SV_EOF
|
|
1189
|
+
chmod 755 "$svdir/$name/run"
|
|
1190
|
+
# runit refuses to supervise without log/run, and svlogger is a script, not
|
|
1191
|
+
# a program: the documented setup is a symlink to it. A $PREFIX that refuses
|
|
1192
|
+
# symlinks (a FUSE-backed home, an unusual mount) gets an equivalent script
|
|
1193
|
+
# instead, so the node is never left unsupervised for want of a link.
|
|
1194
|
+
if ! ln -sf "$TERMUX_PREFIX/share/termux-services/svlogger" "$svdir/$name/log/run" 2>/dev/null; then
|
|
1195
|
+
printf '#!%s/bin/sh\nexec "%s/share/termux-services/svlogger"\n' \
|
|
1196
|
+
"$TERMUX_PREFIX" "$TERMUX_PREFIX" > "$svdir/$name/log/run"
|
|
1197
|
+
chmod 755 "$svdir/$name/log/run"
|
|
1198
|
+
fi
|
|
1199
|
+
export SVDIR="$svdir"
|
|
1200
|
+
export LOGDIR="$svlogdir"
|
|
1201
|
+
# The supervisor is normally started by the login shell (profile.d), which
|
|
1202
|
+
# has not necessarily run when this installer is piped straight into bash.
|
|
1203
|
+
# Starting it here is what makes the node run *now*; the pgrep guard keeps a
|
|
1204
|
+
# second runsvdir off an SVDIR that already has one.
|
|
1205
|
+
if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
|
|
1206
|
+
command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1 || true
|
|
1207
|
+
fi
|
|
1208
|
+
sv up "$name" >/dev/null 2>&1 || true
|
|
1209
|
+
sv-enable "$name" >/dev/null 2>&1 || true
|
|
1210
|
+
TERMUX_SUPERVISED=true
|
|
1211
|
+
fi
|
|
1212
|
+
|
|
1213
|
+
# 2. The boot entry, written even when runit supervises: after a reboot the
|
|
1214
|
+
# supervisor itself has to be started by something, and on Android that
|
|
1215
|
+
# something is the Termux:Boot app.
|
|
1216
|
+
cat << BOOT_EOF > "$boot_script"
|
|
1217
|
+
#!$TERMUX_PREFIX/bin/sh
|
|
1218
|
+
# Antigravity Mesh - starts the $name node after the device boots (Termux:Boot).
|
|
1219
|
+
#
|
|
1220
|
+
# Requirements, both of them outside this file (docs/TERMUX.md):
|
|
1221
|
+
# * the Termux:Boot app (F-Droid), opened once so Android lets it run;
|
|
1222
|
+
# * battery optimisation OFF for Termux and for Termux:Boot.
|
|
1223
|
+
export PATH="$TERMUX_PREFIX/bin:/system/bin:\$PATH"
|
|
1224
|
+
export HOME="$HOME"
|
|
1225
|
+
export SVDIR="$svdir"
|
|
1226
|
+
export LOGDIR="$svlogdir"
|
|
1227
|
+
|
|
1228
|
+
# A partial wake lock keeps Android from freezing the node while the screen is
|
|
1229
|
+
# off. It needs the termux-api package plus the Termux:API app; without either,
|
|
1230
|
+
# this line is a no-op rather than a failure.
|
|
1231
|
+
command -v termux-wake-lock >/dev/null 2>&1 && termux-wake-lock >/dev/null 2>&1
|
|
1232
|
+
|
|
1233
|
+
if command -v sv >/dev/null 2>&1 && [ -d "$svdir/$name" ]; then
|
|
1234
|
+
if ! pgrep -f "runsvdir" >/dev/null 2>&1; then
|
|
1235
|
+
command -v service-daemon >/dev/null 2>&1 && service-daemon start >/dev/null 2>&1
|
|
1236
|
+
fi
|
|
1237
|
+
sv up "$name" >/dev/null 2>&1
|
|
1238
|
+
else
|
|
1239
|
+
# No supervisor on this phone: start the launcher directly and keep its output.
|
|
1240
|
+
"$run_script" >> "$CONFIG_DIR/$name.boot.log" 2>&1 &
|
|
1241
|
+
fi
|
|
1242
|
+
BOOT_EOF
|
|
1243
|
+
chmod 700 "$boot_script"
|
|
1244
|
+
|
|
1245
|
+
if [ "$TERMUX_SUPERVISED" != true ]; then
|
|
1246
|
+
# The systemd branch's fallback, spelled for a phone: nothing supervises
|
|
1247
|
+
# the process, so it is started detached and its log is the only evidence.
|
|
1248
|
+
pkill -f "$module" 2>/dev/null || true
|
|
1249
|
+
nohup "$run_script" >> "$log_file" 2>&1 &
|
|
1250
|
+
fi
|
|
1251
|
+
|
|
1252
|
+
# Termux has no `systemctl status` to consult, so the installer waits for the
|
|
1253
|
+
# process it just started (runit may take a moment) and reports what it saw.
|
|
1254
|
+
while [ "$waited" -lt 10 ]; do
|
|
1255
|
+
if pgrep -f "$module" >/dev/null 2>&1; then
|
|
1256
|
+
alive=true
|
|
1257
|
+
break
|
|
1258
|
+
fi
|
|
1259
|
+
sleep 1
|
|
1260
|
+
waited=$((waited + 1))
|
|
1261
|
+
done
|
|
1262
|
+
|
|
1263
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1264
|
+
if [ "$TERMUX_SUPERVISED" = true ]; then
|
|
1265
|
+
echo -e "${GREEN}[OK] runit (termux-services) следит за узлом и перезапустит его сам.${RESET}"
|
|
1266
|
+
echo -e " Статус: ${CYAN}sv status $name${RESET} Логи: ${CYAN}$svlogdir/sv/$name/current${RESET}"
|
|
1267
|
+
else
|
|
1268
|
+
echo -e "${YELLOW}[!] Узел запущен без супервизора: termux-services не установлен.${RESET}"
|
|
1269
|
+
echo -e " Автоперезапуск после сбоя: ${CYAN}pkg install termux-services${RESET}, затем повторите установку."
|
|
1270
|
+
echo -e " Логи: ${CYAN}$log_file${RESET}"
|
|
1271
|
+
fi
|
|
1272
|
+
echo -e " Автозапуск после перезагрузки: ${CYAN}$boot_script${RESET} (нужно приложение Termux:Boot)."
|
|
1273
|
+
if [ "$alive" != true ]; then
|
|
1274
|
+
echo -e "${YELLOW}[!] Процесс узла пока не виден — смотрите лог: ${CYAN}$log_file${RESET}"
|
|
1275
|
+
fi
|
|
1276
|
+
else
|
|
1277
|
+
if [ "$TERMUX_SUPERVISED" = true ]; then
|
|
1278
|
+
echo -e "${GREEN}[OK] runit (termux-services) supervises the node and restarts it.${RESET}"
|
|
1279
|
+
echo -e " Status: ${CYAN}sv status $name${RESET} Logs: ${CYAN}$svlogdir/sv/$name/current${RESET}"
|
|
1280
|
+
else
|
|
1281
|
+
echo -e "${YELLOW}[!] The node runs unsupervised: termux-services is not installed.${RESET}"
|
|
1282
|
+
echo -e " Restart after a crash: ${CYAN}pkg install termux-services${RESET}, then re-run the installer."
|
|
1283
|
+
echo -e " Logs: ${CYAN}$log_file${RESET}"
|
|
1284
|
+
fi
|
|
1285
|
+
echo -e " Autostart after a reboot: ${CYAN}$boot_script${RESET} (the Termux:Boot app is required)."
|
|
1286
|
+
if [ "$alive" != true ]; then
|
|
1287
|
+
echo -e "${YELLOW}[!] The node process is not visible yet - check: ${CYAN}$log_file${RESET}"
|
|
1288
|
+
fi
|
|
1289
|
+
fi
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
# The Android-side switches no installer can flip for the user: whether the node
|
|
1293
|
+
# comes back after a reboot and whether Android keeps it alive with the screen
|
|
1294
|
+
# off. Printed after the MCP URL, in the language of the run.
|
|
1295
|
+
print_termux_hints() {
|
|
1296
|
+
local name="$1"
|
|
1297
|
+
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
1298
|
+
echo -e " ${BOLD}📱 Termux (Android): что проверить на телефоне${RESET} (служба: ${CYAN}$name${RESET})"
|
|
1299
|
+
echo -e " 1. Автозапуск после перезагрузки делает приложение ${CYAN}Termux:Boot${RESET} (F-Droid)."
|
|
1300
|
+
echo -e " Установите и ${BOLD}запустите его один раз${RESET}, иначе скрипт в ~/.termux/boot не выполнится."
|
|
1301
|
+
echo -e " 2. Отключите оптимизацию батареи для ${CYAN}Termux${RESET} и Termux:Boot: иначе Android"
|
|
1302
|
+
echo -e " выгружает фоновый процесс вместе с туннелем."
|
|
1303
|
+
echo -e " 3. Скопировать ссылку выше: долгое нажатие в терминале, либо приложение Termux:API и"
|
|
1304
|
+
echo -e " ${CYAN}termux-clipboard-set '<ссылка>'${RESET}."
|
|
1305
|
+
echo -e " 4. Выдайте приложению ${CYAN}Termux:API${RESET} разрешения для инструментов устройства"
|
|
1306
|
+
echo -e " (Настройки Android → Приложения → Termux:API → Разрешения): камера, микрофон,"
|
|
1307
|
+
echo -e " местоположение, SMS, журнал вызовов, контакты. Без них ${CYAN}device_capture${RESET} и"
|
|
1308
|
+
echo -e " ${CYAN}device_messages${RESET} ответят, какого именно разрешения не хватает."
|
|
1309
|
+
echo -e " 5. Ветка устройства включена по умолчанию (${CYAN}MESH_DEVICE=auto${RESET}). SMS, контакты и"
|
|
1310
|
+
echo -e " журнал вызовов открывает ${CYAN}MESH_DEVICE_PIM=1${RESET}; ${CYAN}MESH_DEVICE_QUICK=1${RESET} оставляет"
|
|
1311
|
+
echo -e " system_info мгновенным (без сети, камер, микрофонов и датчиков)."
|
|
1312
|
+
echo -e " 6. Полная инструкция: ${CYAN}docs/TERMUX.md${RESET} (установка, автозапуск, логи, удаление)."
|
|
1313
|
+
echo ""
|
|
1314
|
+
else
|
|
1315
|
+
echo -e " ${BOLD}📱 Termux (Android): what to check on the phone${RESET} (service: ${CYAN}$name${RESET})"
|
|
1316
|
+
echo -e " 1. Autostart after a reboot is done by the ${CYAN}Termux:Boot${RESET} app (F-Droid)."
|
|
1317
|
+
echo -e " Install it and ${BOLD}open it once${RESET}, or the script in ~/.termux/boot never runs."
|
|
1318
|
+
echo -e " 2. Turn battery optimisation OFF for ${CYAN}Termux${RESET} and Termux:Boot: otherwise Android"
|
|
1319
|
+
echo -e " unloads the background process together with the tunnel."
|
|
1320
|
+
echo -e " 3. Copy the URL above: long-press the terminal, or install the Termux:API app and use"
|
|
1321
|
+
echo -e " ${CYAN}termux-clipboard-set '<url>'${RESET}."
|
|
1322
|
+
echo -e " 4. Grant the ${CYAN}Termux:API${RESET} app the permissions the device tools need"
|
|
1323
|
+
echo -e " (Android Settings → Apps → Termux:API → Permissions): camera, microphone, location,"
|
|
1324
|
+
echo -e " SMS, call log, contacts. Without them ${CYAN}device_capture${RESET} / ${CYAN}device_messages${RESET}"
|
|
1325
|
+
echo -e " answer with the exact permission that is missing."
|
|
1326
|
+
echo -e " 5. The device branch is on by default (${CYAN}MESH_DEVICE=auto${RESET}). ${CYAN}MESH_DEVICE_PIM=1${RESET}"
|
|
1327
|
+
echo -e " enables SMS, contacts and the call log; ${CYAN}MESH_DEVICE_QUICK=1${RESET} keeps system_info"
|
|
1328
|
+
echo -e " instant (no network, cameras, microphones or sensors)."
|
|
1329
|
+
echo -e " 6. Full guide: ${CYAN}docs/TERMUX.md${RESET} (install, autostart, logs, uninstall)."
|
|
1330
|
+
echo ""
|
|
1331
|
+
fi
|
|
1332
|
+
}
|
|
1333
|
+
|
|
310
1334
|
# Dry-run
|
|
311
1335
|
if [ "$DRY_RUN" = true ]; then
|
|
312
1336
|
echo "[DRY-RUN] Simulating Antigravity Mesh installation..."
|
|
313
1337
|
echo "[DRY-RUN] Language: $LANG_CHOICE"
|
|
314
1338
|
echo "[DRY-RUN] Detected Host: $DETECTED_HOSTNAME ($DETECTED_TYPE)"
|
|
1339
|
+
echo "[DRY-RUN] Architecture: ${ARCH_LABEL:-$DETECTED_ARCH}"
|
|
315
1340
|
echo "[DRY-RUN] Selected Mode: ${MODE:-standalone}"
|
|
316
1341
|
echo "[DRY-RUN] Port: $PORT"
|
|
317
1342
|
echo "[DRY-RUN] TLS: $TLS"
|
|
318
|
-
if [ -n "$
|
|
1343
|
+
if [ -n "$GATEWAY" ]; then echo "[DRY-RUN] Domain: $GATEWAY"; fi
|
|
319
1344
|
if [ -n "$USERNAME" ]; then
|
|
320
1345
|
echo "[DRY-RUN] User: $USERNAME"
|
|
321
|
-
|
|
1346
|
+
if [ -n "$GATEWAY" ]; then
|
|
1347
|
+
echo "[DRY-RUN] Canonical MCP URL: $(node_sse_url "$USERNAME" "${TOKEN:-<token>}")"
|
|
1348
|
+
fi
|
|
322
1349
|
fi
|
|
323
1350
|
if [ -n "$TOKEN" ]; then echo "[DRY-RUN] Token: set (standalone: stored in ${CONFIG_DIR}/standalone.env, chmod 600)"; fi
|
|
324
|
-
|
|
1351
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1352
|
+
echo "[DRY-RUN] Platform: Termux (Android)"
|
|
1353
|
+
echo "[DRY-RUN] Termux prefix: $TERMUX_PREFIX"
|
|
1354
|
+
echo "[DRY-RUN] Package manager: pkg (sudo, systemd and uv do not exist on Android)"
|
|
1355
|
+
echo "[DRY-RUN] Autostart: runit service <name> + ~/.termux/boot/<name>.sh (Termux:Boot)"
|
|
1356
|
+
echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
|
|
1357
|
+
else
|
|
1358
|
+
echo "[DRY-RUN] Domain file: $DEFAULT_DOMAIN_FILE"
|
|
1359
|
+
echo "[DRY-RUN] Systemd service and dependencies check: OK"
|
|
1360
|
+
fi
|
|
1361
|
+
if [ -z "$GATEWAY" ]; then
|
|
1362
|
+
echo "[DRY-RUN] Domain file value: not written (no domain configured)"
|
|
1363
|
+
elif [ "$GATEWAY" = "$(normalise_host "$(module_default_domain)")" ]; then
|
|
1364
|
+
echo "[DRY-RUN] Domain file value: not written (the built-in default is a fallback, not a configuration)"
|
|
1365
|
+
else
|
|
1366
|
+
echo "[DRY-RUN] Domain file value: MESH_PUBLIC_URL=https://$GATEWAY"
|
|
1367
|
+
fi
|
|
325
1368
|
exit 0
|
|
326
1369
|
fi
|
|
327
1370
|
|
|
@@ -348,7 +1391,15 @@ if [ -n "$SSH_TARGET" ]; then
|
|
|
348
1391
|
if [ -d "$SCRIPT_DIR/skills" ]; then
|
|
349
1392
|
$SCP_BIN -P "$SSH_PORT" -r "$SCRIPT_DIR/skills"/* "$SSH_TARGET:~/antigravity-mesh/skills/" 2>/dev/null || true
|
|
350
1393
|
fi
|
|
351
|
-
|
|
1394
|
+
# The domain was resolved on THIS machine, so it is handed to the target: the
|
|
1395
|
+
# remote copy otherwise re-resolves from its own environment, and a repository
|
|
1396
|
+
# copy whose $PUBLISHED_DOMAIN is still the placeholder falls back to the
|
|
1397
|
+
# built-in default - which the target would now also record as its domain.
|
|
1398
|
+
REMOTE_DOMAIN_ARG=""
|
|
1399
|
+
if [ -n "$GATEWAY" ] && [ "$GATEWAY" != "$DOMAIN_PLACEHOLDER" ]; then
|
|
1400
|
+
REMOTE_DOMAIN_ARG=" --domain=$GATEWAY"
|
|
1401
|
+
fi
|
|
1402
|
+
$SSH_BIN -p "$SSH_PORT" "$SSH_TARGET" "cd ~/antigravity-mesh && bash install.sh --quick --lang=$LANG_CHOICE$REMOTE_DOMAIN_ARG"
|
|
352
1403
|
exit 0
|
|
353
1404
|
fi
|
|
354
1405
|
|
|
@@ -439,7 +1490,12 @@ if [ "$MODE" = "standalone" ]; then
|
|
|
439
1490
|
echo "[1/3] Checking Python environment..."
|
|
440
1491
|
fi
|
|
441
1492
|
ensure_dependencies
|
|
1493
|
+
# The endpoint check at the end of this branch runs through curl.
|
|
1494
|
+
ensure_curl || exit 1
|
|
442
1495
|
mkdir -p "$CONFIG_DIR"
|
|
1496
|
+
# A standalone node answers share_file too, so the domain it resolved is written
|
|
1497
|
+
# down here as well - before the server is started, not after.
|
|
1498
|
+
write_domain_file "$GATEWAY"
|
|
443
1499
|
|
|
444
1500
|
# Honour --token in standalone mode without embedding the secret in the
|
|
445
1501
|
# (world-readable) unit/plist: keep it in a 0600 env file for systemd
|
|
@@ -473,7 +1529,11 @@ if [ "$MODE" = "standalone" ]; then
|
|
|
473
1529
|
fi
|
|
474
1530
|
|
|
475
1531
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
476
|
-
|
|
1532
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1533
|
+
echo "[2/3] Настройка автозапуска на телефоне (runit + Termux:Boot)..."
|
|
1534
|
+
else
|
|
1535
|
+
echo "[2/3] Настройка systemd автозапуска на ПК..."
|
|
1536
|
+
fi
|
|
477
1537
|
else
|
|
478
1538
|
echo "[2/3] Configuring autostart background service..."
|
|
479
1539
|
fi
|
|
@@ -491,7 +1551,7 @@ if [ "$MODE" = "standalone" ]; then
|
|
|
491
1551
|
<string>com.antigravity.mesh.standalone</string>
|
|
492
1552
|
<key>ProgramArguments</key>
|
|
493
1553
|
<array>
|
|
494
|
-
<string>$
|
|
1554
|
+
<string>$PYTHON_BIN</string>
|
|
495
1555
|
<string>-m</string>
|
|
496
1556
|
<string>core.server</string>
|
|
497
1557
|
<string>--host=127.0.0.1</string>
|
|
@@ -512,6 +1572,10 @@ EOF
|
|
|
512
1572
|
fi
|
|
513
1573
|
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
514
1574
|
launchctl load -w "$PLIST_FILE" 2>/dev/null || true
|
|
1575
|
+
elif [ "$IS_TERMUX" = true ]; then
|
|
1576
|
+
# Android: neither systemd nor launchd exists - runit inside Termux
|
|
1577
|
+
# supervises the server, and Termux:Boot brings it back after a reboot.
|
|
1578
|
+
configure_termux_node "agy-standalone" "core.server" "--host 127.0.0.1 --port=$PORT" "$STANDALONE_ENV_FILE"
|
|
515
1579
|
else
|
|
516
1580
|
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
|
|
517
1581
|
mkdir -p "$USER_SYSTEMD_DIR"
|
|
@@ -562,12 +1626,26 @@ EOF
|
|
|
562
1626
|
if [ "$HEALTH_OK" != true ]; then
|
|
563
1627
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
564
1628
|
echo -e "\033[1;31m[ОШИБКА] Standalone MCP-сервер не отвечает (HTTP ${HTTP_CODE:-нет ответа}): $HEALTH_URL\033[0m" >&2
|
|
565
|
-
|
|
566
|
-
|
|
1629
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1630
|
+
echo -e " Статус: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
|
|
1631
|
+
echo -e " Логи: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
|
|
1632
|
+
echo -e " Локальный сервер на телефоне доступен только внутри Termux (127.0.0.1):" >&2
|
|
1633
|
+
echo -e " для Gemini с телефона используйте режим туннеля (--mode=tunnel)." >&2
|
|
1634
|
+
else
|
|
1635
|
+
echo -e " Проверьте статус: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
|
|
1636
|
+
echo -e " Логи: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
|
|
1637
|
+
fi
|
|
567
1638
|
else
|
|
568
1639
|
echo -e "\033[1;31m[ERROR] Standalone MCP server is not responding (HTTP ${HTTP_CODE:-no response}): $HEALTH_URL\033[0m" >&2
|
|
569
|
-
|
|
570
|
-
|
|
1640
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1641
|
+
echo -e " Status: ${CYAN}sv status agy-standalone${RESET} (runit, termux-services)" >&2
|
|
1642
|
+
echo -e " Logs: ${CYAN}$TERMUX_PREFIX/var/log/sv/agy-standalone/current${RESET}" >&2
|
|
1643
|
+
echo -e " A local server on a phone is reachable inside Termux only (127.0.0.1):" >&2
|
|
1644
|
+
echo -e " use --mode=tunnel to drive this phone from Gemini." >&2
|
|
1645
|
+
else
|
|
1646
|
+
echo -e " Check status: ${CYAN}systemctl --user status agy-standalone.service${RESET}" >&2
|
|
1647
|
+
echo -e " Logs: ${CYAN}journalctl --user -u agy-standalone.service -n 50 --no-pager${RESET}" >&2
|
|
1648
|
+
fi
|
|
571
1649
|
fi
|
|
572
1650
|
exit 1
|
|
573
1651
|
fi
|
|
@@ -578,7 +1656,13 @@ EOF
|
|
|
578
1656
|
COPIED=false
|
|
579
1657
|
B64_URL=$(printf "%s" "$MCP_LOCAL_URL" | base64 | tr -d '\r\n')
|
|
580
1658
|
printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
|
|
581
|
-
if command -v
|
|
1659
|
+
if command -v termux-clipboard-set >/dev/null 2>&1; then
|
|
1660
|
+
# Termux on a phone: termux-api plus the Termux:API app put the URL on
|
|
1661
|
+
# Android's clipboard, where the Gemini app can paste it. Without the app
|
|
1662
|
+
# the command exists but fails, so COPIED stays false and the hint below
|
|
1663
|
+
# tells the user to copy by hand.
|
|
1664
|
+
printf "%s" "$MCP_LOCAL_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
|
|
1665
|
+
elif command -v wl-copy >/dev/null 2>&1; then
|
|
582
1666
|
printf "%s" "$MCP_LOCAL_URL" | wl-copy 2>/dev/null && COPIED=true
|
|
583
1667
|
elif command -v xclip >/dev/null 2>&1; then
|
|
584
1668
|
printf "%s" "$MCP_LOCAL_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
|
|
@@ -623,6 +1707,9 @@ EOF
|
|
|
623
1707
|
echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
|
|
624
1708
|
echo ""
|
|
625
1709
|
fi
|
|
1710
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1711
|
+
print_termux_hints "agy-standalone"
|
|
1712
|
+
fi
|
|
626
1713
|
exit 0
|
|
627
1714
|
fi
|
|
628
1715
|
|
|
@@ -651,6 +1738,14 @@ else
|
|
|
651
1738
|
fi
|
|
652
1739
|
|
|
653
1740
|
ensure_dependencies
|
|
1741
|
+
# Registration - the step right after this - is a curl POST. A machine with a usable
|
|
1742
|
+
# Python and no curl (a minimal WSL image, a slim container) used to die there with
|
|
1743
|
+
# "curl: command not found" and no hint about what to install.
|
|
1744
|
+
ensure_curl || exit 1
|
|
1745
|
+
# Registration is also the first step that truly needs the gateway: a name that does
|
|
1746
|
+
# not resolve here would be reported a moment later as "no authentication token",
|
|
1747
|
+
# which blames the gateway's answer instead of this device's DNS.
|
|
1748
|
+
require_gateway_dns || exit 1
|
|
654
1749
|
|
|
655
1750
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
656
1751
|
echo "[2/4] Регистрация узла '${USERNAME}' на общем домене (${GATEWAY})..."
|
|
@@ -676,8 +1771,10 @@ else
|
|
|
676
1771
|
fi
|
|
677
1772
|
|
|
678
1773
|
DETECTED_MAC=""
|
|
679
|
-
|
|
680
|
-
|
|
1774
|
+
# "$PYTHON_BIN", never a bare python3: after the venv or uv fallback the working
|
|
1775
|
+
# interpreter is not on PATH, and the bare name would silently produce no MAC.
|
|
1776
|
+
if python_works "$PYTHON_BIN"; then
|
|
1777
|
+
DETECTED_MAC=$("$PYTHON_BIN" -c "import uuid; print(':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]))" 2>/dev/null || true)
|
|
681
1778
|
fi
|
|
682
1779
|
if [ -z "$DETECTED_MAC" ] || [ "$DETECTED_MAC" = "00:00:00:00:00:00" ]; then
|
|
683
1780
|
for iface in /sys/class/net/*; do
|
|
@@ -691,7 +1788,19 @@ if [ -z "$DETECTED_MAC" ] || [ "$DETECTED_MAC" = "00:00:00:00:00:00" ]; then
|
|
|
691
1788
|
done
|
|
692
1789
|
fi
|
|
693
1790
|
|
|
694
|
-
|
|
1791
|
+
# One field out of the registration answer. The JSON arrives on stdin rather than
|
|
1792
|
+
# in argv (a token or an error message can contain quotes), and "$PYTHON_BIN" is
|
|
1793
|
+
# used because after the venv or uv fallback there is no python3 on PATH - a bare
|
|
1794
|
+
# name would return an empty token, which the check below would then blame on the
|
|
1795
|
+
# gateway.
|
|
1796
|
+
json_field() {
|
|
1797
|
+
# tr strips the CR that a Windows python.exe appends under Git Bash/MSYS.
|
|
1798
|
+
# install.sh explicitly supports MINGW/MSYS/CYGWIN (see DEFAULT_DOMAIN_DIR), and
|
|
1799
|
+
# a trailing CR inside the token would be pasted straight into the wss:// URL.
|
|
1800
|
+
printf '%s' "$1" | "$PYTHON_BIN" -c 'import json, sys; print(json.load(sys.stdin).get(sys.argv[1], ""))' "$2" 2>/dev/null | tr -d '\r\n' || true
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
REG_BODY="{\"username\": \"${USERNAME}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\""
|
|
695
1804
|
if [ -n "$REQ_TOKEN" ]; then
|
|
696
1805
|
REG_BODY="${REG_BODY}, \"token\": \"${REQ_TOKEN}\""
|
|
697
1806
|
fi
|
|
@@ -701,16 +1810,16 @@ REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
|
|
|
701
1810
|
-H "Content-Type: application/json" \
|
|
702
1811
|
-d "$REG_BODY")
|
|
703
1812
|
|
|
704
|
-
ASSIGNED_USER=$(
|
|
705
|
-
ASSIGNED_TOKEN=$(
|
|
1813
|
+
ASSIGNED_USER=$(json_field "$REG_RESP" username)
|
|
1814
|
+
ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
|
|
706
1815
|
|
|
707
1816
|
if [ -z "$ASSIGNED_TOKEN" ]; then
|
|
708
1817
|
FALLBACK_USER="${USERNAME}-$(date +%s | tail -c 4)"
|
|
709
1818
|
REG_RESP=$(curl -s -X POST "https://${GATEWAY}/api/register" \
|
|
710
1819
|
-H "Content-Type: application/json" \
|
|
711
|
-
-d "{\"username\": \"${FALLBACK_USER}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\"}")
|
|
712
|
-
ASSIGNED_USER=$(
|
|
713
|
-
ASSIGNED_TOKEN=$(
|
|
1820
|
+
-d "{\"username\": \"${FALLBACK_USER}\", \"auto_suffix\": true, \"mac_address\": \"${DETECTED_MAC}\", \"os\": \"${DETECTED_OS}\", \"arch\": \"${DETECTED_ARCH}\"}")
|
|
1821
|
+
ASSIGNED_USER=$(json_field "$REG_RESP" username)
|
|
1822
|
+
ASSIGNED_TOKEN=$(json_field "$REG_RESP" token)
|
|
714
1823
|
fi
|
|
715
1824
|
|
|
716
1825
|
if [ -z "$ASSIGNED_TOKEN" ]; then
|
|
@@ -739,8 +1848,17 @@ MESH_TOKEN=${ASSIGNED_TOKEN}
|
|
|
739
1848
|
EOF
|
|
740
1849
|
chmod 600 "$CONFIG_FILE"
|
|
741
1850
|
|
|
1851
|
+
# agent.env carries only the legacy MESH_GATEWAY, which core/domain.py honours for
|
|
1852
|
+
# the tunnel but never for the links a node publishes. Writing the domain down is
|
|
1853
|
+
# what keeps share links on the gateway this node is registered on.
|
|
1854
|
+
write_domain_file "$GATEWAY"
|
|
1855
|
+
|
|
742
1856
|
if [ "$LANG_CHOICE" = "ru" ]; then
|
|
743
|
-
|
|
1857
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1858
|
+
echo "[4/4] Настройка и запуск автозапуска на телефоне (runit + Termux:Boot)..."
|
|
1859
|
+
else
|
|
1860
|
+
echo "[4/4] Настройка и запуск службы автозапуска на ПК..."
|
|
1861
|
+
fi
|
|
744
1862
|
else
|
|
745
1863
|
echo "[4/4] Configuring and starting background autostart service..."
|
|
746
1864
|
fi
|
|
@@ -758,7 +1876,7 @@ if [ "$(uname -s)" = "Darwin" ]; then
|
|
|
758
1876
|
<string>com.antigravity.mesh.agent</string>
|
|
759
1877
|
<key>ProgramArguments</key>
|
|
760
1878
|
<array>
|
|
761
|
-
<string>$
|
|
1879
|
+
<string>$PYTHON_BIN</string>
|
|
762
1880
|
<string>-m</string>
|
|
763
1881
|
<string>core.agent</string>
|
|
764
1882
|
</array>
|
|
@@ -782,6 +1900,10 @@ if [ "$(uname -s)" = "Darwin" ]; then
|
|
|
782
1900
|
EOF
|
|
783
1901
|
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
784
1902
|
launchctl load -w "$PLIST_FILE" 2>/dev/null || true
|
|
1903
|
+
elif [ "$IS_TERMUX" = true ]; then
|
|
1904
|
+
# Android: the tunnel node is supervised by runit inside Termux, and the
|
|
1905
|
+
# Termux:Boot entry brings it back after a reboot - see configure_termux_node.
|
|
1906
|
+
configure_termux_node "agy-agent" "core.agent" "" "$CONFIG_FILE"
|
|
785
1907
|
else
|
|
786
1908
|
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
|
|
787
1909
|
mkdir -p "$USER_SYSTEMD_DIR"
|
|
@@ -818,7 +1940,12 @@ MCP_URL="$(node_sse_url "$ASSIGNED_USER" "$ASSIGNED_TOKEN")"
|
|
|
818
1940
|
COPIED=false
|
|
819
1941
|
B64_URL=$(printf "%s" "$MCP_URL" | base64 | tr -d '\r\n')
|
|
820
1942
|
printf "\033]52;c;%s\a" "$B64_URL" 2>/dev/null || true
|
|
821
|
-
if command -v
|
|
1943
|
+
if command -v termux-clipboard-set >/dev/null 2>&1; then
|
|
1944
|
+
# Termux on a phone: termux-api plus the Termux:API app put the URL on
|
|
1945
|
+
# Android's clipboard, where the Gemini app can paste it. Without the app the
|
|
1946
|
+
# command exists but fails, so COPIED stays false and the hint says so.
|
|
1947
|
+
printf "%s" "$MCP_URL" | termux-clipboard-set 2>/dev/null && COPIED=true
|
|
1948
|
+
elif command -v wl-copy >/dev/null 2>&1; then
|
|
822
1949
|
printf "%s" "$MCP_URL" | wl-copy 2>/dev/null && COPIED=true
|
|
823
1950
|
elif command -v xclip >/dev/null 2>&1; then
|
|
824
1951
|
printf "%s" "$MCP_URL" | xclip -selection clipboard 2>/dev/null && COPIED=true
|
|
@@ -863,3 +1990,9 @@ else
|
|
|
863
1990
|
echo -e " 3. Paste the URL into the ${BOLD}'Server URL'${RESET} field (${BOLD}Ctrl+V${RESET}) and click ${BOLD}Connect${RESET}."
|
|
864
1991
|
echo ""
|
|
865
1992
|
fi
|
|
1993
|
+
|
|
1994
|
+
# The phone-specific steps (Termux:Boot, battery optimisation, clipboard) are the
|
|
1995
|
+
# same in both modes; on a Linux/macOS/Windows node there is nothing to add.
|
|
1996
|
+
if [ "$IS_TERMUX" = true ]; then
|
|
1997
|
+
print_termux_hints "agy-agent"
|
|
1998
|
+
fi
|